diff --git a/Cargo.lock b/Cargo.lock index f78c7084b..147e19ccf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,8 +77,8 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" dependencies = [ - "quote 1.0.23", - "syn 1.0.107", + "quote", + "syn", ] [[package]] @@ -211,9 +211,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13" dependencies = [ "actix-router", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -330,9 +330,9 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -341,9 +341,9 @@ version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -448,15 +448,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - [[package]] name = "bit-vec" version = "0.6.3" @@ -566,9 +557,9 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fe233b960f12f8007e3db2d136e3cb1c291bfd7396e384ee76025fc1a3932b4" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -761,9 +752,9 @@ checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -774,9 +765,9 @@ checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ "heck", "proc-macro-error", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -803,9 +794,9 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df715824eb382e34b7afb7463b0247bf41538aeba731fba05241ecdb5dc3747" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1056,10 +1047,10 @@ checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.49", - "quote 1.0.23", + "proc-macro2", + "quote", "strsim", - "syn 1.0.107", + "syn", ] [[package]] @@ -1069,8 +1060,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" dependencies = [ "darling_core", - "quote 1.0.23", - "syn 1.0.107", + "quote", + "syn", ] [[package]] @@ -1089,9 +1080,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" dependencies = [ "darling", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1101,7 +1092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" dependencies = [ "derive_builder_core", - "syn 1.0.107", + "syn", ] [[package]] @@ -1111,17 +1102,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case 0.4.0", - "proc-macro2 1.0.49", - "quote 1.0.23", + "proc-macro2", + "quote", "rustc_version", - "syn 1.0.107", + "syn", ] [[package]] name = "deserr" -version = "0.1.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86290491a2b5c21a1a5083da8dae831006761258fabd5617309c3eebc5f89468" +checksum = "28380303ca15ec07e1d5b079baf19cf849b09edad5cab219c1c51b2bd07523de" dependencies = [ "deserr-internal", "serde-cs", @@ -1130,14 +1121,14 @@ dependencies = [ [[package]] name = "deserr-internal" -version = "0.1.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7131de1c27581bc376a22166c9f570be91b76cb096be2f6aecf224c27bf7c49a" +checksum = "860928cd8af78d223a3d70dd581f21d7c3de8aa2eecd938e0c0a399ded7c1451" dependencies = [ "convert_case 0.5.0", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1315,9 +1306,9 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1380,9 +1371,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35c9bb4a2c13ffb3a93a39902aaf4e7190a1706a4779b6db0449aee433d26c4a" dependencies = [ "darling", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", "uuid 0.8.2", ] @@ -1511,9 +1502,9 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1585,9 +1576,9 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ce01e8bbb3e7e0758dcf907fe799f5998a54368963f766ae94b84624ba60c8" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1642,9 +1633,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2393,9 +2384,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a9062912d7952c5588cc474795e0b9ee008e7e6781127945b85413d4b99d81" dependencies = [ "log", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2415,9 +2406,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f08150cf2bab1fc47c2196f4f41173a27fcd0f684165e5458c0046b53a472e2f" dependencies = [ "once_cell", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2455,6 +2446,7 @@ dependencies = [ "assert-json-diff", "async-stream", "async-trait", + "atty", "brotli", "bstr 1.1.0", "byte-unit", @@ -2503,7 +2495,6 @@ dependencies = [ "rustls-pemfile", "segment", "serde", - "serde-cs", "serde_json", "serde_urlencoded", "sha-1", @@ -2515,6 +2506,7 @@ dependencies = [ "tar", "temp-env", "tempfile", + "termcolor", "thiserror", "time", "tokio", @@ -2564,10 +2556,9 @@ dependencies = [ "meili-snap", "memmap2", "milli", - "proptest", - "proptest-derive", "roaring", "serde", + "serde-cs", "serde_json", "tar", "tempfile", @@ -2991,9 +2982,9 @@ checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c" dependencies = [ "pest", "pest_meta", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3119,9 +3110,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", "version_check", ] @@ -3131,20 +3122,11 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", + "proc-macro2", + "quote", "version_check", ] -[[package]] -name = "proc-macro2" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -dependencies = [ - "unicode-xid 0.1.0", -] - [[package]] name = "proc-macro2" version = "1.0.49" @@ -3184,71 +3166,19 @@ dependencies = [ "thiserror", ] -[[package]] -name = "proptest" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" -dependencies = [ - "bit-set", - "bitflags", - "byteorder", - "lazy_static", - "num-traits", - "quick-error 2.0.1", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", -] - -[[package]] -name = "proptest-derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90b46295382dc76166cb7cf2bb4a97952464e4b7ed5a43e6cd34e1fec3349ddc" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", -] - [[package]] name = "protobuf" version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - [[package]] name = "quote" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ - "proc-macro2 1.0.49", + "proc-macro2", ] [[package]] @@ -3281,15 +3211,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_xorshift" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" -dependencies = [ - "rand_core", -] - [[package]] name = "rayon" version = "1.6.1" @@ -3504,18 +3425,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" -[[package]] -name = "rusty-fork" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", -] - [[package]] name = "ryu" version = "1.0.12" @@ -3591,9 +3500,9 @@ version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3788,25 +3697,14 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" -[[package]] -name = "syn" -version = "0.15.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid 0.1.0", -] - [[package]] name = "syn" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", + "proc-macro2", + "quote", "unicode-ident", ] @@ -3825,10 +3723,10 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", - "unicode-xid 0.2.4", + "proc-macro2", + "quote", + "syn", + "unicode-xid", ] [[package]] @@ -3910,9 +3808,9 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3993,9 +3891,9 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -4130,12 +4028,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" - [[package]] name = "unicode-xid" version = "0.2.4" @@ -4218,15 +4110,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" version = "2.3.2" @@ -4284,9 +4167,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", "wasm-bindgen-shared", ] @@ -4308,7 +4191,7 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ - "quote 1.0.23", + "quote", "wasm-bindgen-macro-support", ] @@ -4318,9 +4201,9 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4517,8 +4400,8 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d498dbd1fd7beb83c86709ae1c33ca50942889473473d287d56ce4770a18edfb" dependencies = [ - "proc-macro2 1.0.49", - "syn 1.0.107", + "proc-macro2", + "syn", "synstructure", ] diff --git a/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-3.snap b/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-3.snap index f7e1736b1..8edf789d0 100644 --- a/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-3.snap +++ b/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-3.snap @@ -10,6 +10,7 @@ expression: products.settings().unwrap() "*" ], "filterableAttributes": [], + "sortableAttributes": [], "rankingRules": [ "typo", "words", diff --git a/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-6.snap b/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-6.snap index 8c36fe96c..80c26874b 100644 --- a/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-6.snap +++ b/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-6.snap @@ -13,13 +13,17 @@ expression: movies.settings().unwrap() "genres", "id" ], + "sortableAttributes": [ + "genres", + "id" + ], "rankingRules": [ "typo", "words", "proximity", "attribute", "exactness", - "asc(release_date)" + "release_date:asc" ], "stopWords": [], "synonyms": {}, diff --git a/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-9.snap b/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-9.snap index 1adf85e6a..89da27c25 100644 --- a/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-9.snap +++ b/dump/src/reader/compat/snapshots/dump__reader__compat__v1_to_v2__test__compat_v1_v2-9.snap @@ -10,6 +10,7 @@ expression: spells.settings().unwrap() "*" ], "filterableAttributes": [], + "sortableAttributes": [], "rankingRules": [ "typo", "words", diff --git a/dump/src/reader/compat/v1_to_v2.rs b/dump/src/reader/compat/v1_to_v2.rs index 741d18fa8..789e8e0b1 100644 --- a/dump/src/reader/compat/v1_to_v2.rs +++ b/dump/src/reader/compat/v1_to_v2.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeSet; use std::str::FromStr; use super::v2_to_v3::CompatV2ToV3; @@ -102,14 +101,15 @@ impl CompatIndexV1ToV2 { impl From for v2::Settings { fn from(source: v1::settings::Settings) -> Self { - let displayed_attributes = source - .displayed_attributes - .map(|opt| opt.map(|displayed_attributes| displayed_attributes.into_iter().collect())); - let attributes_for_faceting = source.attributes_for_faceting.map(|opt| { - opt.map(|attributes_for_faceting| attributes_for_faceting.into_iter().collect()) - }); - let ranking_rules = source.ranking_rules.map(|opt| { - opt.map(|ranking_rules| { + Self { + displayed_attributes: option_to_setting(source.displayed_attributes) + .map(|displayed| displayed.into_iter().collect()), + searchable_attributes: option_to_setting(source.searchable_attributes), + filterable_attributes: option_to_setting(source.attributes_for_faceting.clone()) + .map(|filterable| filterable.into_iter().collect()), + sortable_attributes: option_to_setting(source.attributes_for_faceting) + .map(|sortable| sortable.into_iter().collect()), + ranking_rules: option_to_setting(source.ranking_rules).map(|ranking_rules| { ranking_rules .into_iter() .filter_map(|ranking_rule| { @@ -119,26 +119,33 @@ impl From for v2::Settings { ranking_rule.into(); criterion.as_ref().map(ToString::to_string) } - Err(()) => Some(ranking_rule), + Err(()) => { + log::warn!( + "Could not import the following ranking rule: `{}`.", + ranking_rule + ); + None + } } }) .collect() - }) - }); - - Self { - displayed_attributes, - searchable_attributes: source.searchable_attributes, - filterable_attributes: attributes_for_faceting, - ranking_rules, - stop_words: source.stop_words, - synonyms: source.synonyms, - distinct_attribute: source.distinct_attribute, + }), + stop_words: option_to_setting(source.stop_words), + synonyms: option_to_setting(source.synonyms), + distinct_attribute: option_to_setting(source.distinct_attribute), _kind: std::marker::PhantomData, } } } +fn option_to_setting(opt: Option>) -> v2::Setting { + match opt { + Some(Some(t)) => v2::Setting::Set(t), + None => v2::Setting::NotSet, + Some(None) => v2::Setting::Reset, + } +} + impl From for Option { fn from(source: v1::update::UpdateStatus) -> Self { use v1::update::UpdateStatus as UpdateStatusV1; @@ -251,38 +258,27 @@ impl From for Option { impl From for v2::Settings { fn from(source: v1::settings::SettingsUpdate) -> Self { - let displayed_attributes: Option>> = - source.displayed_attributes.into(); - - let attributes_for_faceting: Option>> = - source.attributes_for_faceting.into(); - - let ranking_rules: Option>> = - source.ranking_rules.into(); + let ranking_rules = v2::Setting::from(source.ranking_rules); // go from the concrete types of v1 (RankingRule) to the concrete type of v2 (Criterion), // and then back to string as this is what the settings manipulate - let ranking_rules = ranking_rules.map(|opt| { - opt.map(|ranking_rules| { - ranking_rules - .into_iter() - // filter out the WordsPosition ranking rule that exists in v1 but not v2 - .filter_map(|ranking_rule| { - Option::::from(ranking_rule) - }) - .map(|criterion| criterion.to_string()) - .collect() - }) + let ranking_rules = ranking_rules.map(|ranking_rules| { + ranking_rules + .into_iter() + // filter out the WordsPosition ranking rule that exists in v1 but not v2 + .filter_map(Option::::from) + .map(|criterion| criterion.to_string()) + .collect() }); Self { - displayed_attributes: displayed_attributes.map(|opt| { - opt.map(|displayed_attributes| displayed_attributes.into_iter().collect()) - }), + displayed_attributes: v2::Setting::from(source.displayed_attributes) + .map(|displayed_attributes| displayed_attributes.into_iter().collect()), searchable_attributes: source.searchable_attributes.into(), - filterable_attributes: attributes_for_faceting.map(|opt| { - opt.map(|attributes_for_faceting| attributes_for_faceting.into_iter().collect()) - }), + filterable_attributes: v2::Setting::from(source.attributes_for_faceting.clone()) + .map(|attributes_for_faceting| attributes_for_faceting.into_iter().collect()), + sortable_attributes: v2::Setting::from(source.attributes_for_faceting) + .map(|attributes_for_faceting| attributes_for_faceting.into_iter().collect()), ranking_rules, stop_words: source.stop_words.into(), synonyms: source.synonyms.into(), @@ -314,12 +310,12 @@ impl From for Option { } } -impl From> for Option> { +impl From> for v2::Setting { fn from(source: v1::settings::UpdateState) -> Self { match source { - v1::settings::UpdateState::Update(new_value) => Some(Some(new_value)), - v1::settings::UpdateState::Clear => Some(None), - v1::settings::UpdateState::Nothing => None, + v1::settings::UpdateState::Update(new_value) => v2::Setting::Set(new_value), + v1::settings::UpdateState::Clear => v2::Setting::Reset, + v1::settings::UpdateState::Nothing => v2::Setting::NotSet, } } } @@ -352,7 +348,7 @@ pub(crate) mod test { // tasks let tasks = dump.tasks().collect::>>().unwrap(); let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip(); - meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"ad6245d98d1a8e30535f3339a9a8d223"); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"2298010973ee98cf4670787314176a3a"); assert_eq!(update_files.len(), 9); assert!(update_files[..].iter().all(|u| u.is_none())); // no update file in dumps v1 diff --git a/dump/src/reader/compat/v2_to_v3.rs b/dump/src/reader/compat/v2_to_v3.rs index 8574e04b4..14fc0ee4d 100644 --- a/dump/src/reader/compat/v2_to_v3.rs +++ b/dump/src/reader/compat/v2_to_v3.rs @@ -361,28 +361,29 @@ impl From for v3::Code { } } -fn option_to_setting(opt: Option>) -> v3::Setting { - match opt { - Some(Some(t)) => v3::Setting::Set(t), - None => v3::Setting::NotSet, - Some(None) => v3::Setting::Reset, +impl From> for v3::Setting { + fn from(setting: v2::Setting) -> Self { + match setting { + v2::settings::Setting::Set(a) => v3::settings::Setting::Set(a), + v2::settings::Setting::Reset => v3::settings::Setting::Reset, + v2::settings::Setting::NotSet => v3::settings::Setting::NotSet, + } } } impl From> for v3::Settings { fn from(settings: v2::Settings) -> Self { v3::Settings { - displayed_attributes: option_to_setting(settings.displayed_attributes), - searchable_attributes: option_to_setting(settings.searchable_attributes), - filterable_attributes: option_to_setting(settings.filterable_attributes) - .map(|f| f.into_iter().collect()), - sortable_attributes: v3::Setting::NotSet, - ranking_rules: option_to_setting(settings.ranking_rules).map(|criteria| { + displayed_attributes: settings.displayed_attributes.into(), + searchable_attributes: settings.searchable_attributes.into(), + filterable_attributes: settings.filterable_attributes.into(), + sortable_attributes: settings.sortable_attributes.into(), + ranking_rules: v3::Setting::from(settings.ranking_rules).map(|criteria| { criteria.into_iter().map(|criterion| patch_ranking_rules(&criterion)).collect() }), - stop_words: option_to_setting(settings.stop_words), - synonyms: option_to_setting(settings.synonyms), - distinct_attribute: option_to_setting(settings.distinct_attribute), + stop_words: settings.stop_words.into(), + synonyms: settings.synonyms.into(), + distinct_attribute: settings.distinct_attribute.into(), _kind: std::marker::PhantomData, } } @@ -394,6 +395,7 @@ fn patch_ranking_rules(ranking_rule: &str) -> String { Ok(v2::settings::Criterion::Typo) => String::from("typo"), Ok(v2::settings::Criterion::Proximity) => String::from("proximity"), Ok(v2::settings::Criterion::Attribute) => String::from("attribute"), + Ok(v2::settings::Criterion::Sort) => String::from("sort"), Ok(v2::settings::Criterion::Exactness) => String::from("exactness"), Ok(v2::settings::Criterion::Asc(name)) => format!("{name}:asc"), Ok(v2::settings::Criterion::Desc(name)) => format!("{name}:desc"), diff --git a/dump/src/reader/compat/v5_to_v6.rs b/dump/src/reader/compat/v5_to_v6.rs index 237381414..6d1e698b3 100644 --- a/dump/src/reader/compat/v5_to_v6.rs +++ b/dump/src/reader/compat/v5_to_v6.rs @@ -260,7 +260,7 @@ impl From for v6::ResponseError { "index_already_exists" => v6::Code::IndexAlreadyExists, "index_not_found" => v6::Code::IndexNotFound, "invalid_index_uid" => v6::Code::InvalidIndexUid, - "invalid_min_word_length_for_typo" => v6::Code::InvalidMinWordLengthForTypo, + "invalid_min_word_length_for_typo" => v6::Code::InvalidSettingsTypoTolerance, "invalid_state" => v6::Code::InvalidState, "primary_key_inference_failed" => v6::Code::IndexPrimaryKeyNoCandidateFound, "index_primary_key_already_exists" => v6::Code::IndexPrimaryKeyAlreadyExists, @@ -439,7 +439,7 @@ pub(crate) mod test { // tasks let tasks = dump.tasks().unwrap().collect::>>().unwrap(); let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip(); - meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"10c673c97f053830aa659876d7aa0b53"); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"41f91d3a94911b2735ec41b07540df5c"); assert_eq!(update_files.len(), 22); assert!(update_files[0].is_none()); // the dump creation assert!(update_files[1].is_some()); // the enqueued document addition diff --git a/dump/src/reader/mod.rs b/dump/src/reader/mod.rs index 259c9ce53..a5a66591b 100644 --- a/dump/src/reader/mod.rs +++ b/dump/src/reader/mod.rs @@ -201,7 +201,7 @@ pub(crate) mod test { // tasks let tasks = dump.tasks().unwrap().collect::>>().unwrap(); let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip(); - meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"10c673c97f053830aa659876d7aa0b53"); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"41f91d3a94911b2735ec41b07540df5c"); assert_eq!(update_files.len(), 22); assert!(update_files[0].is_none()); // the dump creation assert!(update_files[1].is_some()); // the enqueued document addition @@ -279,7 +279,7 @@ pub(crate) mod test { // tasks let tasks = dump.tasks().unwrap().collect::>>().unwrap(); let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip(); - meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"12eca43d5d1e1f334200eb4df653b0c9"); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"c2445ddd1785528b80f2ba534d3bd00c"); assert_eq!(update_files.len(), 10); assert!(update_files[0].is_some()); // the enqueued document addition assert!(update_files[1..].iter().all(|u| u.is_none())); // everything already processed @@ -356,7 +356,7 @@ pub(crate) mod test { // tasks let tasks = dump.tasks().unwrap().collect::>>().unwrap(); let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip(); - meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"2f51c6345fabccf47b18c82bad618ffe"); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"cd12efd308fe3ed226356a727ab42ed3"); assert_eq!(update_files.len(), 10); assert!(update_files[0].is_some()); // the enqueued document addition assert!(update_files[1..].iter().all(|u| u.is_none())); // everything already processed @@ -449,7 +449,7 @@ pub(crate) mod test { // tasks let tasks = dump.tasks().unwrap().collect::>>().unwrap(); let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip(); - meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"b27292d0bb86d4b4dd1b375a46b33890"); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"bc616290adfe7d09a624cf6065ca9069"); assert_eq!(update_files.len(), 9); assert!(update_files[0].is_some()); // the enqueued document addition assert!(update_files[1..].iter().all(|u| u.is_none())); // everything already processed @@ -530,6 +530,82 @@ pub(crate) mod test { meili_snap::snapshot_hash!(format!("{:#?}", documents), @"235016433dd04262c7f2da01d1e808ce"); } + #[test] + fn import_dump_v2_from_meilisearch_v0_22_0_issue_3435() { + let dump = File::open("tests/assets/v2-v0.22.0.dump").unwrap(); + let mut dump = DumpReader::open(dump).unwrap(); + + // top level infos + insta::assert_display_snapshot!(dump.date().unwrap(), @"2023-01-30 16:26:09.247261 +00:00:00"); + assert_eq!(dump.instance_uid().unwrap(), None); + + // tasks + let tasks = dump.tasks().unwrap().collect::>>().unwrap(); + let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip(); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"2db37756d8af1fb7623436b76e8956a6"); + assert_eq!(update_files.len(), 8); + assert!(update_files[0..].iter().all(|u| u.is_none())); // everything already processed + + // keys + let keys = dump.keys().unwrap().collect::>>().unwrap(); + meili_snap::snapshot_hash!(meili_snap::json_string!(keys), @"d751713988987e9331980363e24189ce"); + + // indexes + let mut indexes = dump.indexes().unwrap().collect::>>().unwrap(); + // the index are not ordered in any way by default + indexes.sort_by_key(|index| index.metadata().uid.to_string()); + + let mut products = indexes.pop().unwrap(); + let mut movies = indexes.pop().unwrap(); + let mut spells = indexes.pop().unwrap(); + assert!(indexes.is_empty()); + + // products + insta::assert_json_snapshot!(products.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###" + { + "uid": "products", + "primaryKey": "sku", + "createdAt": "[now]", + "updatedAt": "[now]" + } + "###); + + insta::assert_json_snapshot!(products.settings().unwrap()); + let documents = products.documents().unwrap().collect::>>().unwrap(); + assert_eq!(documents.len(), 10); + meili_snap::snapshot_hash!(format!("{:#?}", documents), @"548284a84de510f71e88e6cdea495cf5"); + + // movies + insta::assert_json_snapshot!(movies.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###" + { + "uid": "movies", + "primaryKey": "id", + "createdAt": "[now]", + "updatedAt": "[now]" + } + "###); + + insta::assert_json_snapshot!(movies.settings().unwrap()); + let documents = movies.documents().unwrap().collect::>>().unwrap(); + assert_eq!(documents.len(), 10); + meili_snap::snapshot_hash!(format!("{:#?}", documents), @"0227598af846e574139ee0b80e03a720"); + + // spells + insta::assert_json_snapshot!(spells.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###" + { + "uid": "dnd_spells", + "primaryKey": "index", + "createdAt": "[now]", + "updatedAt": "[now]" + } + "###); + + insta::assert_json_snapshot!(spells.settings().unwrap()); + let documents = spells.documents().unwrap().collect::>>().unwrap(); + assert_eq!(documents.len(), 10); + meili_snap::snapshot_hash!(format!("{:#?}", documents), @"235016433dd04262c7f2da01d1e808ce"); + } + #[test] fn import_dump_v1() { let dump = File::open("tests/assets/v1.dump").unwrap(); @@ -542,7 +618,7 @@ pub(crate) mod test { // tasks let tasks = dump.tasks().unwrap().collect::>>().unwrap(); let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip(); - meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"9725ccfceea3f8d5846c44006c9e1e7b"); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"8df6eab075a44b3c1af6b726f9fd9a43"); assert_eq!(update_files.len(), 9); assert!(update_files[..].iter().all(|u| u.is_none())); // no update file in dump v1 diff --git a/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-11.snap b/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-11.snap index 997d303e7..92fc61d72 100644 --- a/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-11.snap +++ b/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-11.snap @@ -10,6 +10,7 @@ expression: spells.settings().unwrap() "*" ], "filterableAttributes": [], + "sortableAttributes": [], "rankingRules": [ "typo", "words", diff --git a/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-5.snap b/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-5.snap index 282cd6ba7..b0b54c136 100644 --- a/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-5.snap +++ b/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-5.snap @@ -10,6 +10,7 @@ expression: products.settings().unwrap() "*" ], "filterableAttributes": [], + "sortableAttributes": [], "rankingRules": [ "typo", "words", diff --git a/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-8.snap b/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-8.snap index d20fdc77e..5c12a0438 100644 --- a/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-8.snap +++ b/dump/src/reader/snapshots/dump__reader__test__import_dump_v1-8.snap @@ -13,6 +13,10 @@ expression: movies.settings().unwrap() "genres", "id" ], + "sortableAttributes": [ + "genres", + "id" + ], "rankingRules": [ "typo", "words", diff --git a/dump/src/reader/snapshots/dump__reader__test__import_dump_v2_from_meilisearch_v0_22_0_issue_3435-11.snap b/dump/src/reader/snapshots/dump__reader__test__import_dump_v2_from_meilisearch_v0_22_0_issue_3435-11.snap new file mode 100644 index 000000000..d8a5bafbe --- /dev/null +++ b/dump/src/reader/snapshots/dump__reader__test__import_dump_v2_from_meilisearch_v0_22_0_issue_3435-11.snap @@ -0,0 +1,25 @@ +--- +source: dump/src/reader/mod.rs +expression: spells.settings().unwrap() +--- +{ + "displayedAttributes": [ + "*" + ], + "searchableAttributes": [ + "*" + ], + "filterableAttributes": [], + "sortableAttributes": [], + "rankingRules": [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness" + ], + "stopWords": [], + "synonyms": {}, + "distinctAttribute": null +} diff --git a/dump/src/reader/snapshots/dump__reader__test__import_dump_v2_from_meilisearch_v0_22_0_issue_3435-5.snap b/dump/src/reader/snapshots/dump__reader__test__import_dump_v2_from_meilisearch_v0_22_0_issue_3435-5.snap new file mode 100644 index 000000000..abf97a8ab --- /dev/null +++ b/dump/src/reader/snapshots/dump__reader__test__import_dump_v2_from_meilisearch_v0_22_0_issue_3435-5.snap @@ -0,0 +1,39 @@ +--- +source: dump/src/reader/mod.rs +expression: products.settings().unwrap() +--- +{ + "displayedAttributes": [ + "*" + ], + "searchableAttributes": [ + "*" + ], + "filterableAttributes": [], + "sortableAttributes": [], + "rankingRules": [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness" + ], + "stopWords": [], + "synonyms": { + "android": [ + "phone", + "smartphone" + ], + "iphone": [ + "phone", + "smartphone" + ], + "phone": [ + "android", + "iphone", + "smartphone" + ] + }, + "distinctAttribute": null +} diff --git a/dump/src/reader/snapshots/dump__reader__test__import_dump_v2_from_meilisearch_v0_22_0_issue_3435-8.snap b/dump/src/reader/snapshots/dump__reader__test__import_dump_v2_from_meilisearch_v0_22_0_issue_3435-8.snap new file mode 100644 index 000000000..f02a3685e --- /dev/null +++ b/dump/src/reader/snapshots/dump__reader__test__import_dump_v2_from_meilisearch_v0_22_0_issue_3435-8.snap @@ -0,0 +1,30 @@ +--- +source: dump/src/reader/mod.rs +expression: movies.settings().unwrap() +--- +{ + "displayedAttributes": [ + "*" + ], + "searchableAttributes": [ + "*" + ], + "filterableAttributes": [ + "genres", + "id" + ], + "sortableAttributes": [ + "release_date" + ], + "rankingRules": [ + "words", + "typo", + "proximity", + "attribute", + "exactness", + "release_date:asc" + ], + "stopWords": [], + "synonyms": {}, + "distinctAttribute": null +} diff --git a/dump/src/reader/v2/mod.rs b/dump/src/reader/v2/mod.rs index befebbdb3..4016e6341 100644 --- a/dump/src/reader/v2/mod.rs +++ b/dump/src/reader/v2/mod.rs @@ -41,6 +41,7 @@ use super::Document; use crate::{IndexMetadata, Result, Version}; pub type Settings = settings::Settings; +pub type Setting = settings::Setting; pub type Checked = settings::Checked; pub type Unchecked = settings::Unchecked; @@ -306,4 +307,81 @@ pub(crate) mod test { assert_eq!(documents.len(), 10); meili_snap::snapshot_hash!(format!("{:#?}", documents), @"235016433dd04262c7f2da01d1e808ce"); } + + #[test] + fn read_dump_v2_from_meilisearch_v0_22_0_issue_3435() { + let dump = File::open("tests/assets/v2-v0.22.0.dump").unwrap(); + let dir = TempDir::new().unwrap(); + let mut dump = BufReader::new(dump); + let gz = GzDecoder::new(&mut dump); + let mut archive = tar::Archive::new(gz); + archive.unpack(dir.path()).unwrap(); + + let mut dump = V2Reader::open(dir).unwrap(); + + // top level infos + insta::assert_display_snapshot!(dump.date().unwrap(), @"2023-01-30 16:26:09.247261 +00:00:00"); + + // tasks + let tasks = dump.tasks().collect::>>().unwrap(); + let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip(); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"aca8ba13046272664eb3ea2da3031633"); + assert_eq!(update_files.len(), 8); + assert!(update_files[0..].iter().all(|u| u.is_none())); // everything has already been processed + + // indexes + let mut indexes = dump.indexes().unwrap().collect::>>().unwrap(); + // the index are not ordered in any way by default + indexes.sort_by_key(|index| index.metadata().uid.to_string()); + + let mut products = indexes.pop().unwrap(); + let mut movies = indexes.pop().unwrap(); + let mut spells = indexes.pop().unwrap(); + assert!(indexes.is_empty()); + + // products + insta::assert_json_snapshot!(products.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###" + { + "uid": "products", + "primaryKey": "sku", + "createdAt": "[now]", + "updatedAt": "[now]" + } + "###); + + insta::assert_json_snapshot!(products.settings().unwrap()); + let documents = products.documents().unwrap().collect::>>().unwrap(); + assert_eq!(documents.len(), 10); + meili_snap::snapshot_hash!(format!("{:#?}", documents), @"548284a84de510f71e88e6cdea495cf5"); + + // movies + insta::assert_json_snapshot!(movies.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###" + { + "uid": "movies", + "primaryKey": "id", + "createdAt": "[now]", + "updatedAt": "[now]" + } + "###); + + insta::assert_json_snapshot!(movies.settings().unwrap()); + let documents = movies.documents().unwrap().collect::>>().unwrap(); + assert_eq!(documents.len(), 10); + meili_snap::snapshot_hash!(format!("{:#?}", documents), @"0227598af846e574139ee0b80e03a720"); + + // spells + insta::assert_json_snapshot!(spells.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###" + { + "uid": "dnd_spells", + "primaryKey": "index", + "createdAt": "[now]", + "updatedAt": "[now]" + } + "###); + + insta::assert_json_snapshot!(spells.settings().unwrap()); + let documents = spells.documents().unwrap().collect::>>().unwrap(); + assert_eq!(documents.len(), 10); + meili_snap::snapshot_hash!(format!("{:#?}", documents), @"235016433dd04262c7f2da01d1e808ce"); + } } diff --git a/dump/src/reader/v2/settings.rs b/dump/src/reader/v2/settings.rs index 1a7935b56..c7ecb7f9b 100644 --- a/dump/src/reader/v2/settings.rs +++ b/dump/src/reader/v2/settings.rs @@ -1,35 +1,33 @@ use std::collections::{BTreeMap, BTreeSet}; -use std::fmt::Display; +use std::fmt; use std::marker::PhantomData; use std::str::FromStr; -use once_cell::sync::Lazy; -use regex::Regex; use serde::{Deserialize, Deserializer}; #[cfg(test)] fn serialize_with_wildcard( - field: &Option>>, + field: &Setting>, s: S, ) -> std::result::Result where S: serde::Serializer, { - let wildcard = vec!["*".to_string()]; - s.serialize_some(&field.as_ref().map(|o| o.as_ref().unwrap_or(&wildcard))) -} + use serde::Serialize; -fn deserialize_some<'de, T, D>(deserializer: D) -> std::result::Result, D::Error> -where - T: Deserialize<'de>, - D: Deserializer<'de>, -{ - Deserialize::deserialize(deserializer).map(Some) + let wildcard = vec!["*".to_string()]; + match field { + Setting::Set(value) => Some(value), + Setting::Reset => Some(&wildcard), + Setting::NotSet => None, + } + .serialize(s) } #[derive(Clone, Default, Debug)] #[cfg_attr(test, derive(serde::Serialize))] pub struct Checked; + #[derive(Clone, Default, Debug, Deserialize)] #[cfg_attr(test, derive(serde::Serialize))] pub struct Unchecked; @@ -42,75 +40,54 @@ pub struct Unchecked; pub struct Settings { #[serde( default, - deserialize_with = "deserialize_some", serialize_with = "serialize_with_wildcard", - skip_serializing_if = "Option::is_none" + skip_serializing_if = "Setting::is_not_set" )] - pub displayed_attributes: Option>>, + pub displayed_attributes: Setting>, #[serde( default, - deserialize_with = "deserialize_some", serialize_with = "serialize_with_wildcard", - skip_serializing_if = "Option::is_none" + skip_serializing_if = "Setting::is_not_set" )] - pub searchable_attributes: Option>>, + pub searchable_attributes: Setting>, - #[serde( - default, - deserialize_with = "deserialize_some", - skip_serializing_if = "Option::is_none" - )] - pub filterable_attributes: Option>>, - - #[serde( - default, - deserialize_with = "deserialize_some", - skip_serializing_if = "Option::is_none" - )] - pub ranking_rules: Option>>, - #[serde( - default, - deserialize_with = "deserialize_some", - skip_serializing_if = "Option::is_none" - )] - pub stop_words: Option>>, - #[serde( - default, - deserialize_with = "deserialize_some", - skip_serializing_if = "Option::is_none" - )] - pub synonyms: Option>>>, - #[serde( - default, - deserialize_with = "deserialize_some", - skip_serializing_if = "Option::is_none" - )] - pub distinct_attribute: Option>, + #[serde(default, skip_serializing_if = "Setting::is_not_set")] + pub filterable_attributes: Setting>, + #[serde(default, skip_serializing_if = "Setting::is_not_set")] + pub sortable_attributes: Setting>, + #[serde(default, skip_serializing_if = "Setting::is_not_set")] + pub ranking_rules: Setting>, + #[serde(default, skip_serializing_if = "Setting::is_not_set")] + pub stop_words: Setting>, + #[serde(default, skip_serializing_if = "Setting::is_not_set")] + pub synonyms: Setting>>, + #[serde(default, skip_serializing_if = "Setting::is_not_set")] + pub distinct_attribute: Setting, #[serde(skip)] pub _kind: PhantomData, } impl Settings { - pub fn check(mut self) -> Settings { - let displayed_attributes = match self.displayed_attributes.take() { - Some(Some(fields)) => { + pub fn check(self) -> Settings { + let displayed_attributes = match self.displayed_attributes { + Setting::Set(fields) => { if fields.iter().any(|f| f == "*") { - Some(None) + Setting::Reset } else { - Some(Some(fields)) + Setting::Set(fields) } } otherwise => otherwise, }; - let searchable_attributes = match self.searchable_attributes.take() { - Some(Some(fields)) => { + let searchable_attributes = match self.searchable_attributes { + Setting::Set(fields) => { if fields.iter().any(|f| f == "*") { - Some(None) + Setting::Reset } else { - Some(Some(fields)) + Setting::Set(fields) } } otherwise => otherwise, @@ -120,6 +97,7 @@ impl Settings { displayed_attributes, searchable_attributes, filterable_attributes: self.filterable_attributes, + sortable_attributes: self.sortable_attributes, ranking_rules: self.ranking_rules, stop_words: self.stop_words, synonyms: self.synonyms, @@ -129,10 +107,61 @@ impl Settings { } } -static ASC_DESC_REGEX: Lazy = - Lazy::new(|| Regex::new(r#"(asc|desc)\(([\w_-]+)\)"#).unwrap()); +#[derive(Debug, Clone, PartialEq)] +pub enum Setting { + Set(T), + Reset, + NotSet, +} -#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +impl Default for Setting { + fn default() -> Self { + Self::NotSet + } +} + +impl Setting { + pub const fn is_not_set(&self) -> bool { + matches!(self, Self::NotSet) + } + + pub fn map(self, f: fn(T) -> A) -> Setting { + match self { + Setting::Set(a) => Setting::Set(f(a)), + Setting::Reset => Setting::Reset, + Setting::NotSet => Setting::NotSet, + } + } +} + +#[cfg(test)] +impl serde::Serialize for Setting { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + match self { + Self::Set(value) => Some(value), + // Usually not_set isn't serialized by setting skip_serializing_if field attribute + Self::NotSet | Self::Reset => None, + } + .serialize(serializer) + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for Setting { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + Deserialize::deserialize(deserializer).map(|x| match x { + Some(x) => Self::Set(x), + None => Self::Reset, // Reset is forced by sending null value + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Criterion { /// Sorted by decreasing number of matched query terms. /// Query words at the front of an attribute is considered better than if it was at the back. @@ -142,8 +171,11 @@ pub enum Criterion { /// Sorted by increasing distance between matched query terms. Proximity, /// Documents with quey words contained in more important - /// attributes are considred better. + /// attributes are considered better. Attribute, + /// Dynamically sort at query time the documents. None, one or multiple Asc/Desc sortable + /// attributes can be used in place of this criterion at query time. + Sort, /// Sorted by the similarity of the matched words with the query words. Exactness, /// Sorted by the increasing value of the field specified. @@ -152,40 +184,86 @@ pub enum Criterion { Desc(String), } +impl Criterion { + /// Returns the field name parameter of this criterion. + pub fn field_name(&self) -> Option<&str> { + match self { + Criterion::Asc(name) | Criterion::Desc(name) => Some(name), + _otherwise => None, + } + } +} + impl FromStr for Criterion { + // since we're not going to show the custom error message we can override the + // error type. type Err = (); - fn from_str(txt: &str) -> Result { - match txt { + fn from_str(text: &str) -> Result { + match text { "words" => Ok(Criterion::Words), "typo" => Ok(Criterion::Typo), "proximity" => Ok(Criterion::Proximity), "attribute" => Ok(Criterion::Attribute), + "sort" => Ok(Criterion::Sort), "exactness" => Ok(Criterion::Exactness), - text => { - let caps = ASC_DESC_REGEX.captures(text).ok_or(())?; - let order = caps.get(1).unwrap().as_str(); - let field_name = caps.get(2).unwrap().as_str(); - match order { - "asc" => Ok(Criterion::Asc(field_name.to_string())), - "desc" => Ok(Criterion::Desc(field_name.to_string())), - _text => Err(()), - } - } + text => match AscDesc::from_str(text) { + Ok(AscDesc::Asc(field)) => Ok(Criterion::Asc(field)), + Ok(AscDesc::Desc(field)) => Ok(Criterion::Desc(field)), + Err(_) => Err(()), + }, } } } -impl Display for Criterion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Criterion::Words => write!(f, "words"), - Criterion::Typo => write!(f, "typo"), - Criterion::Proximity => write!(f, "proximity"), - Criterion::Attribute => write!(f, "attribute"), - Criterion::Exactness => write!(f, "exactness"), - Criterion::Asc(field_name) => write!(f, "asc({})", field_name), - Criterion::Desc(field_name) => write!(f, "desc({})", field_name), +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub enum AscDesc { + Asc(String), + Desc(String), +} + +impl FromStr for AscDesc { + type Err = (); + + // since we don't know if this comes from the old or new syntax we need to check + // for both syntax. + // WARN: this code doesn't come from the original meilisearch v0.22.0 but was + // written specifically to be able to import the dump of meilisearch v0.21.0 AND + // meilisearch v0.22.0. + fn from_str(text: &str) -> Result { + if let Some((field_name, asc_desc)) = text.rsplit_once(':') { + match asc_desc { + "asc" => Ok(AscDesc::Asc(field_name.to_string())), + "desc" => Ok(AscDesc::Desc(field_name.to_string())), + _ => Err(()), + } + } else if text.starts_with("asc(") && text.ends_with(')') { + Ok(AscDesc::Asc( + text.strip_prefix("asc(").unwrap().strip_suffix(')').unwrap().to_string(), + )) + } else if text.starts_with("desc(") && text.ends_with(')') { + Ok(AscDesc::Desc( + text.strip_prefix("desc(").unwrap().strip_suffix(')').unwrap().to_string(), + )) + } else { + Err(()) + } + } +} + +impl fmt::Display for Criterion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Criterion::*; + + match self { + Words => f.write_str("words"), + Typo => f.write_str("typo"), + Proximity => f.write_str("proximity"), + Attribute => f.write_str("attribute"), + Sort => f.write_str("sort"), + Exactness => f.write_str("exactness"), + Asc(attr) => write!(f, "{}:asc", attr), + Desc(attr) => write!(f, "{}:desc", attr), } } } diff --git a/dump/src/reader/v2/snapshots/dump__reader__v2__test__read_dump_v2_from_meilisearch_v0_22_0_issue_3435-10.snap b/dump/src/reader/v2/snapshots/dump__reader__v2__test__read_dump_v2_from_meilisearch_v0_22_0_issue_3435-10.snap new file mode 100644 index 000000000..fbff0a0e6 --- /dev/null +++ b/dump/src/reader/v2/snapshots/dump__reader__v2__test__read_dump_v2_from_meilisearch_v0_22_0_issue_3435-10.snap @@ -0,0 +1,25 @@ +--- +source: dump/src/reader/v2/mod.rs +expression: spells.settings().unwrap() +--- +{ + "displayedAttributes": [ + "*" + ], + "searchableAttributes": [ + "*" + ], + "filterableAttributes": [], + "sortableAttributes": [], + "rankingRules": [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness" + ], + "stopWords": [], + "synonyms": {}, + "distinctAttribute": null +} diff --git a/dump/src/reader/v2/snapshots/dump__reader__v2__test__read_dump_v2_from_meilisearch_v0_22_0_issue_3435-4.snap b/dump/src/reader/v2/snapshots/dump__reader__v2__test__read_dump_v2_from_meilisearch_v0_22_0_issue_3435-4.snap new file mode 100644 index 000000000..809ced4f6 --- /dev/null +++ b/dump/src/reader/v2/snapshots/dump__reader__v2__test__read_dump_v2_from_meilisearch_v0_22_0_issue_3435-4.snap @@ -0,0 +1,39 @@ +--- +source: dump/src/reader/v2/mod.rs +expression: products.settings().unwrap() +--- +{ + "displayedAttributes": [ + "*" + ], + "searchableAttributes": [ + "*" + ], + "filterableAttributes": [], + "sortableAttributes": [], + "rankingRules": [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness" + ], + "stopWords": [], + "synonyms": { + "android": [ + "phone", + "smartphone" + ], + "iphone": [ + "phone", + "smartphone" + ], + "phone": [ + "android", + "iphone", + "smartphone" + ] + }, + "distinctAttribute": null +} diff --git a/dump/src/reader/v2/snapshots/dump__reader__v2__test__read_dump_v2_from_meilisearch_v0_22_0_issue_3435-7.snap b/dump/src/reader/v2/snapshots/dump__reader__v2__test__read_dump_v2_from_meilisearch_v0_22_0_issue_3435-7.snap new file mode 100644 index 000000000..016ada2a3 --- /dev/null +++ b/dump/src/reader/v2/snapshots/dump__reader__v2__test__read_dump_v2_from_meilisearch_v0_22_0_issue_3435-7.snap @@ -0,0 +1,30 @@ +--- +source: dump/src/reader/v2/mod.rs +expression: movies.settings().unwrap() +--- +{ + "displayedAttributes": [ + "*" + ], + "searchableAttributes": [ + "*" + ], + "filterableAttributes": [ + "genres", + "id" + ], + "sortableAttributes": [ + "release_date" + ], + "rankingRules": [ + "words", + "typo", + "proximity", + "attribute", + "exactness", + "release_date:asc" + ], + "stopWords": [], + "synonyms": {}, + "distinctAttribute": null +} diff --git a/dump/src/reader/v4/errors.rs b/dump/src/reader/v4/errors.rs index 5a9a8d5df..afa640de4 100644 --- a/dump/src/reader/v4/errors.rs +++ b/dump/src/reader/v4/errors.rs @@ -5,10 +5,8 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] pub struct ResponseError { #[serde(skip)] - #[cfg_attr(feature = "test-traits", proptest(strategy = "strategy::status_code_strategy()"))] pub code: StatusCode, pub message: String, #[serde(rename = "code")] diff --git a/dump/src/reader/v5/errors.rs b/dump/src/reader/v5/errors.rs index c918c301c..f4067d4c6 100644 --- a/dump/src/reader/v5/errors.rs +++ b/dump/src/reader/v5/errors.rs @@ -5,7 +5,6 @@ use serde::Deserialize; #[derive(Debug, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] #[cfg_attr(test, derive(serde::Serialize))] pub struct ResponseError { #[serde(skip)] diff --git a/dump/tests/assets/v2-v0.22.0.dump b/dump/tests/assets/v2-v0.22.0.dump new file mode 100644 index 000000000..8932284ea Binary files /dev/null and b/dump/tests/assets/v2-v0.22.0.dump differ diff --git a/file-store/src/lib.rs b/file-store/src/lib.rs index e05694c92..4b7e52e5d 100644 --- a/file-store/src/lib.rs +++ b/file-store/src/lib.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeSet; use std::fs::File as StdFile; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; @@ -11,10 +10,14 @@ const UPDATE_FILES_PATH: &str = "updates/updates_files"; #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("Could not parse file name as utf-8")] + CouldNotParseFileNameAsUtf8, #[error(transparent)] IoError(#[from] std::io::Error), #[error(transparent)] PersistError(#[from] tempfile::PersistError), + #[error(transparent)] + UuidError(#[from] uuid::Error), } pub type Result = std::result::Result; @@ -33,13 +36,11 @@ impl DerefMut for File { } } -#[cfg_attr(test, faux::create)] #[derive(Clone, Debug)] pub struct FileStore { path: PathBuf, } -#[cfg(not(test))] impl FileStore { pub fn new(path: impl AsRef) -> Result { let path = path.as_ref().to_path_buf(); @@ -48,7 +49,6 @@ impl FileStore { } } -#[cfg_attr(test, faux::methods)] impl FileStore { /// Creates a new temporary update file. /// A call to `persist` is needed to persist the file in the database. @@ -94,7 +94,17 @@ impl FileStore { Ok(()) } - pub fn get_size(&self, uuid: Uuid) -> Result { + /// Compute the size of all the updates contained in the file store. + pub fn compute_total_size(&self) -> Result { + let mut total = 0; + for uuid in self.all_uuids()? { + total += self.compute_size(uuid?).unwrap_or_default(); + } + Ok(total) + } + + /// Compute the size of one update + pub fn compute_size(&self, uuid: Uuid) -> Result { Ok(self.get_update(uuid)?.metadata()?.len()) } @@ -105,17 +115,12 @@ impl FileStore { } /// List the Uuids of the files in the FileStore - /// - /// This function is meant to be used by tests only. - #[doc(hidden)] - pub fn __all_uuids(&self) -> BTreeSet { - let mut uuids = BTreeSet::new(); - for entry in self.path.read_dir().unwrap() { - let entry = entry.unwrap(); - let uuid = Uuid::from_str(entry.file_name().to_str().unwrap()).unwrap(); - uuids.insert(uuid); - } - uuids + pub fn all_uuids(&self) -> Result>> { + Ok(self.path.read_dir()?.map(|entry| { + Ok(Uuid::from_str( + entry?.file_name().to_str().ok_or(Error::CouldNotParseFileNameAsUtf8)?, + )?) + })) } } diff --git a/index-scheduler/src/autobatcher.rs b/index-scheduler/src/autobatcher.rs index d1ed691c6..e1e48ab90 100644 --- a/index-scheduler/src/autobatcher.rs +++ b/index-scheduler/src/autobatcher.rs @@ -19,10 +19,16 @@ use crate::KindWithContent; /// /// Only the non-prioritised tasks that can be grouped in a batch have a corresponding [`AutobatchKind`] enum AutobatchKind { - DocumentImport { method: IndexDocumentsMethod, allow_index_creation: bool }, + DocumentImport { + method: IndexDocumentsMethod, + allow_index_creation: bool, + primary_key: Option, + }, DocumentDeletion, DocumentClear, - Settings { allow_index_creation: bool }, + Settings { + allow_index_creation: bool, + }, IndexCreation, IndexDeletion, IndexUpdate, @@ -38,14 +44,24 @@ impl AutobatchKind { _ => None, } } + + fn primary_key(&self) -> Option> { + match self { + AutobatchKind::DocumentImport { primary_key, .. } => Some(primary_key.as_deref()), + _ => None, + } + } } impl From for AutobatchKind { fn from(kind: KindWithContent) -> Self { match kind { - KindWithContent::DocumentAdditionOrUpdate { method, allow_index_creation, .. } => { - AutobatchKind::DocumentImport { method, allow_index_creation } - } + KindWithContent::DocumentAdditionOrUpdate { + method, + allow_index_creation, + primary_key, + .. + } => AutobatchKind::DocumentImport { method, allow_index_creation, primary_key }, KindWithContent::DocumentDeletion { .. } => AutobatchKind::DocumentDeletion, KindWithContent::DocumentClear { .. } => AutobatchKind::DocumentClear, KindWithContent::SettingsUpdate { allow_index_creation, is_deletion, .. } => { @@ -75,6 +91,7 @@ pub enum BatchKind { DocumentImport { method: IndexDocumentsMethod, allow_index_creation: bool, + primary_key: Option, import_ids: Vec, }, DocumentDeletion { @@ -89,6 +106,7 @@ pub enum BatchKind { settings_ids: Vec, method: IndexDocumentsMethod, allow_index_creation: bool, + primary_key: Option, import_ids: Vec, }, Settings { @@ -120,6 +138,16 @@ impl BatchKind { _ => None, } } + + fn primary_key(&self) -> Option> { + match self { + BatchKind::DocumentImport { primary_key, .. } + | BatchKind::SettingsAndDocumentImport { primary_key, .. } => { + Some(primary_key.as_deref()) + } + _ => None, + } + } } impl BatchKind { @@ -131,6 +159,7 @@ impl BatchKind { pub fn new( task_id: TaskId, kind: KindWithContent, + primary_key: Option<&str>, ) -> (ControlFlow, bool) { use AutobatchKind as K; @@ -140,10 +169,25 @@ impl BatchKind { K::IndexUpdate => (Break(BatchKind::IndexUpdate { id: task_id }), false), K::IndexSwap => (Break(BatchKind::IndexSwap { id: task_id }), false), K::DocumentClear => (Continue(BatchKind::DocumentClear { ids: vec![task_id] }), false), - K::DocumentImport { method, allow_index_creation } => ( - Continue(BatchKind::DocumentImport { + K::DocumentImport { method, allow_index_creation, primary_key: pk } + if primary_key.is_none() || pk.is_none() || primary_key == pk.as_deref() => + { + ( + Continue(BatchKind::DocumentImport { + method, + allow_index_creation, + primary_key: pk, + import_ids: vec![task_id], + }), + allow_index_creation, + ) + } + // if the primary key set in the task was different than ours we should stop and make this batch fail asap. + K::DocumentImport { method, allow_index_creation, primary_key } => ( + Break(BatchKind::DocumentImport { method, allow_index_creation, + primary_key, import_ids: vec![task_id], }), allow_index_creation, @@ -163,7 +207,7 @@ impl BatchKind { /// To ease the writting of the code. `true` can be returned when you don't need to create an index /// but false can't be returned if you needs to create an index. #[rustfmt::skip] - fn accumulate(self, id: TaskId, kind: AutobatchKind, index_already_exists: bool) -> ControlFlow { + fn accumulate(self, id: TaskId, kind: AutobatchKind, index_already_exists: bool, primary_key: Option<&str>) -> ControlFlow { use AutobatchKind as K; match (self, kind) { @@ -173,11 +217,39 @@ impl BatchKind { (this, kind) if !index_already_exists && this.allow_index_creation() == Some(false) && kind.allow_index_creation() == Some(true) => { Break(this) }, + // NOTE: We need to negate the whole condition since we're checking if we need to break instead of continue. + // I wrote it this way because it's easier to understand than the other way around. + (this, kind) if !( + // 1. If both task don't interact with primary key -> we can continue + (this.primary_key().is_none() && kind.primary_key().is_none()) || + // 2. Else -> + ( + // 2.1 If we already have a primary-key -> + ( + primary_key.is_some() && + // 2.1.1 If the task we're trying to accumulate have a pk it must be equal to our primary key + // 2.1.2 If the task don't have a primary-key -> we can continue + kind.primary_key().map_or(true, |pk| pk == primary_key) + ) || + // 2.2 If we don't have a primary-key -> + ( + // 2.2.1 If both the batch and the task have a primary key they should be equal + // 2.2.2 If the batch is set to Some(None), the task should be too + // 2.2.3 If the batch is set to None -> we can continue + this.primary_key().zip(kind.primary_key()).map_or(true, |(this, kind)| this == kind) + ) + ) + + ) // closing the negation + + => { + Break(this) + }, // The index deletion can batch with everything but must stop after ( BatchKind::DocumentClear { mut ids } | BatchKind::DocumentDeletion { deletion_ids: mut ids } - | BatchKind::DocumentImport { method: _, allow_index_creation: _, import_ids: mut ids } + | BatchKind::DocumentImport { method: _, allow_index_creation: _, primary_key: _, import_ids: mut ids } | BatchKind::Settings { allow_index_creation: _, settings_ids: mut ids }, K::IndexDeletion, ) => { @@ -186,7 +258,7 @@ impl BatchKind { } ( BatchKind::ClearAndSettings { settings_ids: mut ids, allow_index_creation: _, mut other } - | BatchKind::SettingsAndDocumentImport { import_ids: mut ids, method: _, allow_index_creation: _, settings_ids: mut other }, + | BatchKind::SettingsAndDocumentImport { import_ids: mut ids, method: _, allow_index_creation: _, primary_key: _, settings_ids: mut other }, K::IndexDeletion, ) => { ids.push(id); @@ -206,7 +278,7 @@ impl BatchKind { K::DocumentImport { .. } | K::Settings { .. }, ) => Break(this), ( - BatchKind::DocumentImport { method: _, allow_index_creation: _, import_ids: mut ids }, + BatchKind::DocumentImport { method: _, allow_index_creation: _, primary_key: _, import_ids: mut ids }, K::DocumentClear, ) => { ids.push(id); @@ -215,24 +287,27 @@ impl BatchKind { // we can autobatch the same kind of document additions / updates ( - BatchKind::DocumentImport { method: ReplaceDocuments, allow_index_creation, mut import_ids }, - K::DocumentImport { method: ReplaceDocuments, .. }, + BatchKind::DocumentImport { method: ReplaceDocuments, allow_index_creation, primary_key: _, mut import_ids }, + K::DocumentImport { method: ReplaceDocuments, primary_key: pk, .. }, ) => { import_ids.push(id); Continue(BatchKind::DocumentImport { method: ReplaceDocuments, allow_index_creation, import_ids, + primary_key: pk, }) } ( - BatchKind::DocumentImport { method: UpdateDocuments, allow_index_creation, mut import_ids }, - K::DocumentImport { method: UpdateDocuments, .. }, + BatchKind::DocumentImport { method: UpdateDocuments, allow_index_creation, primary_key: _, mut import_ids }, + K::DocumentImport { method: UpdateDocuments, primary_key: pk, .. }, ) => { + import_ids.push(id); Continue(BatchKind::DocumentImport { method: UpdateDocuments, allow_index_creation, + primary_key: pk, import_ids, }) } @@ -245,12 +320,13 @@ impl BatchKind { ) => Break(this), ( - BatchKind::DocumentImport { method, allow_index_creation, import_ids }, + BatchKind::DocumentImport { method, allow_index_creation, primary_key, import_ids }, K::Settings { .. }, ) => Continue(BatchKind::SettingsAndDocumentImport { settings_ids: vec![id], method, allow_index_creation, + primary_key, import_ids, }), @@ -327,7 +403,7 @@ impl BatchKind { }) } ( - BatchKind::SettingsAndDocumentImport { settings_ids, method: _, import_ids: mut other, allow_index_creation }, + BatchKind::SettingsAndDocumentImport { settings_ids, method: _, import_ids: mut other, allow_index_creation, primary_key: _ }, K::DocumentClear, ) => { other.push(id); @@ -339,26 +415,28 @@ impl BatchKind { } ( - BatchKind::SettingsAndDocumentImport { settings_ids, method: ReplaceDocuments, mut import_ids, allow_index_creation }, - K::DocumentImport { method: ReplaceDocuments, .. }, + BatchKind::SettingsAndDocumentImport { settings_ids, method: ReplaceDocuments, mut import_ids, allow_index_creation, primary_key: _}, + K::DocumentImport { method: ReplaceDocuments, primary_key: pk2, .. }, ) => { import_ids.push(id); Continue(BatchKind::SettingsAndDocumentImport { settings_ids, method: ReplaceDocuments, allow_index_creation, + primary_key: pk2, import_ids, }) } ( - BatchKind::SettingsAndDocumentImport { settings_ids, method: UpdateDocuments, allow_index_creation, mut import_ids }, - K::DocumentImport { method: UpdateDocuments, .. }, + BatchKind::SettingsAndDocumentImport { settings_ids, method: UpdateDocuments, allow_index_creation, primary_key: _, mut import_ids }, + K::DocumentImport { method: UpdateDocuments, primary_key: pk2, .. }, ) => { import_ids.push(id); Continue(BatchKind::SettingsAndDocumentImport { settings_ids, method: UpdateDocuments, allow_index_creation, + primary_key: pk2, import_ids, }) } @@ -369,7 +447,7 @@ impl BatchKind { K::DocumentDeletion | K::DocumentImport { .. }, ) => Break(this), ( - BatchKind::SettingsAndDocumentImport { mut settings_ids, method, allow_index_creation, import_ids }, + BatchKind::SettingsAndDocumentImport { mut settings_ids, method, allow_index_creation,primary_key, import_ids }, K::Settings { .. }, ) => { settings_ids.push(id); @@ -377,6 +455,7 @@ impl BatchKind { settings_ids, method, allow_index_creation, + primary_key, import_ids, }) } @@ -406,6 +485,7 @@ impl BatchKind { pub fn autobatch( enqueued: Vec<(TaskId, KindWithContent)>, index_already_exists: bool, + primary_key: Option<&str>, ) -> Option<(BatchKind, bool)> { let mut enqueued = enqueued.into_iter(); let (id, kind) = enqueued.next()?; @@ -413,7 +493,7 @@ pub fn autobatch( // index_exist will keep track of if the index should exist at this point after the tasks we batched. let mut index_exist = index_already_exists; - let (mut acc, must_create_index) = match BatchKind::new(id, kind) { + let (mut acc, must_create_index) = match BatchKind::new(id, kind, primary_key) { (Continue(acc), create) => (acc, create), (Break(acc), create) => return Some((acc, create)), }; @@ -422,7 +502,7 @@ pub fn autobatch( index_exist |= must_create_index; for (id, kind) in enqueued { - acc = match acc.accumulate(id, kind.into(), index_exist) { + acc = match acc.accumulate(id, kind.into(), index_exist, primary_key) { Continue(acc) => acc, Break(acc) => return Some((acc, must_create_index)), }; @@ -441,18 +521,24 @@ mod tests { fn autobatch_from( index_already_exists: bool, + primary_key: Option<&str>, input: impl IntoIterator, ) -> Option<(BatchKind, bool)> { autobatch( input.into_iter().enumerate().map(|(id, kind)| (id as TaskId, kind)).collect(), index_already_exists, + primary_key, ) } - fn doc_imp(method: IndexDocumentsMethod, allow_index_creation: bool) -> KindWithContent { + fn doc_imp( + method: IndexDocumentsMethod, + allow_index_creation: bool, + primary_key: Option<&str>, + ) -> KindWithContent { KindWithContent::DocumentAdditionOrUpdate { index_uid: String::from("doggo"), - primary_key: None, + primary_key: primary_key.map(|pk| pk.to_string()), method, content_file: Uuid::new_v4(), documents_count: 0, @@ -502,226 +588,268 @@ mod tests { fn autobatch_simple_operation_together() { // we can autobatch one or multiple `ReplaceDocuments` together. // if the index exists. - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, false)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), doc_imp( ReplaceDocuments, true ), doc_imp(ReplaceDocuments, true )]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0, 1, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, false), doc_imp( ReplaceDocuments, false ), doc_imp(ReplaceDocuments, false )]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0, 1, 2] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, false, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp( ReplaceDocuments, true , None), doc_imp(ReplaceDocuments, true , None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, false, None), doc_imp( ReplaceDocuments, false , None), doc_imp(ReplaceDocuments, false , None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0, 1, 2] }, false))"); // if it doesn't exists. - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, true)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, false)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, true), doc_imp( ReplaceDocuments, true ), doc_imp(ReplaceDocuments, true )]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0, 1, 2] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, false), doc_imp( ReplaceDocuments, true ), doc_imp(ReplaceDocuments, true )]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, false, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, true, None), doc_imp( ReplaceDocuments, true , None), doc_imp(ReplaceDocuments, true , None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, false, None), doc_imp( ReplaceDocuments, true , None), doc_imp(ReplaceDocuments, true , None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); // we can autobatch one or multiple `UpdateDocuments` together. // if the index exists. - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), doc_imp(UpdateDocuments, true), doc_imp(UpdateDocuments, true)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, import_ids: [0, 1, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, false)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, import_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, false), doc_imp(UpdateDocuments, false), doc_imp(UpdateDocuments, false)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, import_ids: [0, 1, 2] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), doc_imp(UpdateDocuments, true, None), doc_imp(UpdateDocuments, true, None)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, false, None)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, false, None), doc_imp(UpdateDocuments, false, None), doc_imp(UpdateDocuments, false, None)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, primary_key: None, import_ids: [0, 1, 2] }, false))"); // if it doesn't exists. - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, true)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, true), doc_imp(UpdateDocuments, true), doc_imp(UpdateDocuments, true)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, import_ids: [0, 1, 2] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, false)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, import_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, false), doc_imp(UpdateDocuments, false), doc_imp(UpdateDocuments, false)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, import_ids: [0, 1, 2] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, true, None)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, true, None), doc_imp(UpdateDocuments, true, None), doc_imp(UpdateDocuments, true, None)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, false, None)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, false, None), doc_imp(UpdateDocuments, false, None), doc_imp(UpdateDocuments, false, None)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, primary_key: None, import_ids: [0, 1, 2] }, false))"); // we can autobatch one or multiple DocumentDeletion together - debug_snapshot!(autobatch_from(true, [doc_del()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_del(), doc_del(), doc_del()]), @"Some((DocumentDeletion { deletion_ids: [0, 1, 2] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_del()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_del(), doc_del(), doc_del()]), @"Some((DocumentDeletion { deletion_ids: [0, 1, 2] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_del()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_del(), doc_del(), doc_del()]), @"Some((DocumentDeletion { deletion_ids: [0, 1, 2] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_del()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_del(), doc_del(), doc_del()]), @"Some((DocumentDeletion { deletion_ids: [0, 1, 2] }, false))"); // we can autobatch one or multiple Settings together - debug_snapshot!(autobatch_from(true, [settings(true)]), @"Some((Settings { allow_index_creation: true, settings_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [settings(true), settings(true), settings(true)]), @"Some((Settings { allow_index_creation: true, settings_ids: [0, 1, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [settings(false)]), @"Some((Settings { allow_index_creation: false, settings_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [settings(false), settings(false), settings(false)]), @"Some((Settings { allow_index_creation: false, settings_ids: [0, 1, 2] }, false))"); + debug_snapshot!(autobatch_from(true, None, [settings(true)]), @"Some((Settings { allow_index_creation: true, settings_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [settings(true), settings(true), settings(true)]), @"Some((Settings { allow_index_creation: true, settings_ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [settings(false)]), @"Some((Settings { allow_index_creation: false, settings_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [settings(false), settings(false), settings(false)]), @"Some((Settings { allow_index_creation: false, settings_ids: [0, 1, 2] }, false))"); - debug_snapshot!(autobatch_from(false, [settings(true)]), @"Some((Settings { allow_index_creation: true, settings_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(false, [settings(true), settings(true), settings(true)]), @"Some((Settings { allow_index_creation: true, settings_ids: [0, 1, 2] }, true))"); - debug_snapshot!(autobatch_from(false, [settings(false)]), @"Some((Settings { allow_index_creation: false, settings_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [settings(false), settings(false), settings(false)]), @"Some((Settings { allow_index_creation: false, settings_ids: [0, 1, 2] }, false))"); + debug_snapshot!(autobatch_from(false,None, [settings(true)]), @"Some((Settings { allow_index_creation: true, settings_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(false,None, [settings(true), settings(true), settings(true)]), @"Some((Settings { allow_index_creation: true, settings_ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(false,None, [settings(false)]), @"Some((Settings { allow_index_creation: false, settings_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [settings(false), settings(false), settings(false)]), @"Some((Settings { allow_index_creation: false, settings_ids: [0, 1, 2] }, false))"); } #[test] fn simple_document_operation_dont_autobatch_with_other() { // addition, updates and deletion can't batch together - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), doc_imp(UpdateDocuments, true)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), doc_del()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), doc_imp(ReplaceDocuments, true)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), doc_del()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_del(), doc_imp(ReplaceDocuments, true)]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_del(), doc_imp(UpdateDocuments, true)]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp(UpdateDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_del()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), doc_del()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_del(), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_del(), doc_imp(UpdateDocuments, true, None)]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), idx_create()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), idx_create()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_del(), idx_create()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), idx_create()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), idx_create()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_del(), idx_create()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), idx_update()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), idx_update()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_del(), idx_update()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), idx_update()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), idx_update()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_del(), idx_update()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), idx_swap()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), idx_swap()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_del(), idx_swap()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), idx_swap()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), idx_swap()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_del(), idx_swap()]), @"Some((DocumentDeletion { deletion_ids: [0] }, false))"); } #[test] fn document_addition_batch_with_settings() { // simple case - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); // multiple settings and doc addition - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), doc_imp(ReplaceDocuments, true), settings(true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [2, 3], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0, 1] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), doc_imp(ReplaceDocuments, true), settings(true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [2, 3], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, None), settings(true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [2, 3], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, None), settings(true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [2, 3], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1] }, true))"); // addition and setting unordered - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true), doc_imp(ReplaceDocuments, true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1, 3], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true), doc_imp(UpdateDocuments, true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1, 3], method: UpdateDocuments, allow_index_creation: true, import_ids: [0, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true), doc_imp(ReplaceDocuments, true, None), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1, 3], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true), doc_imp(UpdateDocuments, true, None), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1, 3], method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 2] }, true))"); // We ensure this kind of batch doesn't batch with forbidden operations - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true), doc_imp(UpdateDocuments, true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true), doc_imp(ReplaceDocuments, true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true), doc_del()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true), doc_del()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true), idx_create()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true), idx_create()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true), idx_update()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true), idx_update()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true), idx_swap()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true), idx_swap()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true), doc_imp(UpdateDocuments, true, None)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true), doc_imp(ReplaceDocuments, true, None)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true), doc_del()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true), doc_del()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true), idx_create()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true), idx_create()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true), idx_update()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true), idx_update()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true), idx_swap()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true), idx_swap()]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: UpdateDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); } #[test] fn clear_and_additions() { // these two doesn't need to batch - debug_snapshot!(autobatch_from(true, [doc_clr(), doc_imp(ReplaceDocuments, true)]), @"Some((DocumentClear { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_clr(), doc_imp(UpdateDocuments, true)]), @"Some((DocumentClear { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_clr(), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentClear { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_clr(), doc_imp(UpdateDocuments, true, None)]), @"Some((DocumentClear { ids: [0] }, false))"); // Basic use case - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), doc_imp(ReplaceDocuments, true), doc_clr()]), @"Some((DocumentClear { ids: [0, 1, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), doc_imp(UpdateDocuments, true), doc_clr()]), @"Some((DocumentClear { ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, None), doc_clr()]), @"Some((DocumentClear { ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), doc_imp(UpdateDocuments, true, None), doc_clr()]), @"Some((DocumentClear { ids: [0, 1, 2] }, true))"); // This batch kind doesn't mix with other document addition - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), doc_imp(ReplaceDocuments, true), doc_clr(), doc_imp(ReplaceDocuments, true)]), @"Some((DocumentClear { ids: [0, 1, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), doc_imp(UpdateDocuments, true), doc_clr(), doc_imp(UpdateDocuments, true)]), @"Some((DocumentClear { ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, None), doc_clr(), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentClear { ids: [0, 1, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), doc_imp(UpdateDocuments, true, None), doc_clr(), doc_imp(UpdateDocuments, true, None)]), @"Some((DocumentClear { ids: [0, 1, 2] }, true))"); // But you can batch multiple clear together - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), doc_imp(ReplaceDocuments, true), doc_clr(), doc_clr(), doc_clr()]), @"Some((DocumentClear { ids: [0, 1, 2, 3, 4] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), doc_imp(UpdateDocuments, true), doc_clr(), doc_clr(), doc_clr()]), @"Some((DocumentClear { ids: [0, 1, 2, 3, 4] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, None), doc_clr(), doc_clr(), doc_clr()]), @"Some((DocumentClear { ids: [0, 1, 2, 3, 4] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), doc_imp(UpdateDocuments, true, None), doc_clr(), doc_clr(), doc_clr()]), @"Some((DocumentClear { ids: [0, 1, 2, 3, 4] }, true))"); } #[test] fn clear_and_additions_and_settings() { // A clear don't need to autobatch the settings that happens AFTER there is no documents - debug_snapshot!(autobatch_from(true, [doc_clr(), settings(true)]), @"Some((DocumentClear { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_clr(), settings(true)]), @"Some((DocumentClear { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [settings(true), doc_clr(), settings(true)]), @"Some((ClearAndSettings { other: [1], allow_index_creation: true, settings_ids: [0, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true), doc_clr()]), @"Some((ClearAndSettings { other: [0, 2], allow_index_creation: true, settings_ids: [1] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true), doc_clr()]), @"Some((ClearAndSettings { other: [0, 2], allow_index_creation: true, settings_ids: [1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [settings(true), doc_clr(), settings(true)]), @"Some((ClearAndSettings { other: [1], allow_index_creation: true, settings_ids: [0, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true), doc_clr()]), @"Some((ClearAndSettings { other: [0, 2], allow_index_creation: true, settings_ids: [1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true), doc_clr()]), @"Some((ClearAndSettings { other: [0, 2], allow_index_creation: true, settings_ids: [1] }, true))"); } #[test] fn anything_and_index_deletion() { // The `IndexDeletion` doesn't batch with anything that happens AFTER. - debug_snapshot!(autobatch_from(true, [idx_del(), doc_imp(ReplaceDocuments, true)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [idx_del(), doc_imp(UpdateDocuments, true)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [idx_del(), doc_imp(ReplaceDocuments, false)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [idx_del(), doc_imp(UpdateDocuments, false)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [idx_del(), doc_del()]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [idx_del(), doc_clr()]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [idx_del(), settings(true)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(true, [idx_del(), settings(false)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [idx_del(), doc_imp(ReplaceDocuments, true, None)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [idx_del(), doc_imp(UpdateDocuments, true, None)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [idx_del(), doc_imp(ReplaceDocuments, false, None)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [idx_del(), doc_imp(UpdateDocuments, false, None)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [idx_del(), doc_del()]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [idx_del(), doc_clr()]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [idx_del(), settings(true)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [idx_del(), settings(false)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [idx_del(), doc_imp(ReplaceDocuments, true)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [idx_del(), doc_imp(UpdateDocuments, true)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [idx_del(), doc_imp(ReplaceDocuments, false)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [idx_del(), doc_imp(UpdateDocuments, false)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [idx_del(), doc_del()]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [idx_del(), doc_clr()]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [idx_del(), settings(true)]), @"Some((IndexDeletion { ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [idx_del(), settings(false)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [idx_del(), doc_imp(ReplaceDocuments, true, None)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [idx_del(), doc_imp(UpdateDocuments, true, None)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [idx_del(), doc_imp(ReplaceDocuments, false, None)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [idx_del(), doc_imp(UpdateDocuments, false, None)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [idx_del(), doc_del()]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [idx_del(), doc_clr()]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [idx_del(), settings(true)]), @"Some((IndexDeletion { ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [idx_del(), settings(false)]), @"Some((IndexDeletion { ids: [0] }, false))"); // The index deletion can accept almost any type of `BatchKind` and transform it to an `IndexDeletion`. // First, the basic cases - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, false), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, false), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_del(), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); - debug_snapshot!(autobatch_from(true, [settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, false, None), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, false, None), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_del(), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, true), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, true), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, false), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, false), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_del(), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(false, [settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); - debug_snapshot!(autobatch_from(false, [settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, true, None), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, true, None), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, false, None), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, false, None), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_del(), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(false,None, [settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(false,None, [settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 1] }, false))"); // Then the mixed cases. // The index already exists, whatever is the right of the tasks it shouldn't change the result. - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments,false), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, false), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments,false), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, false), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments,false), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, false), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments,false), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, false), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments,true), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments,true), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(UpdateDocuments, true), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments,false, None), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, false, None), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments,false, None), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, false, None), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments,false, None), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, false, None), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments,false, None), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, false, None), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments,true, None), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments,true, None), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(UpdateDocuments, true, None), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); // When the index doesn't exists yet it's more complicated. // Either the first task we encounter create it, in which case we can create a big batch with everything. - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, true), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, true), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, true), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, true), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, true, None), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, true, None), settings(true), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, true, None), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, true, None), settings(true), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); // The right of the tasks following isn't really important. - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments,true), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, true), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments,true), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, true), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments,true, None), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, true, None), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments,true, None), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, true, None), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, true))"); // Or, the second case; the first task doesn't create the index and thus we wants to batch it with only tasks that can't create an index. // that can be a second task that don't have the right to create an index. Or anything that can't create an index like an index deletion, document deletion, document clear, etc. // All theses tasks are going to throw an error `Index doesn't exist` once the batch is processed. - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments,false), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, false), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments,false), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, false), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments,false, None), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, false, None), settings(false), idx_del()]), @"Some((IndexDeletion { ids: [0, 2, 1] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments,false, None), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, false, None), settings(false), doc_clr(), idx_del()]), @"Some((IndexDeletion { ids: [1, 3, 0, 2] }, false))"); // The third and final case is when the first task doesn't create an index but is directly followed by a task creating an index. In this case we can't batch whith what // follows because we first need to process the erronous batch. - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments,false), settings(true), idx_del()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, false), settings(true), idx_del()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, import_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments,false), settings(true), doc_clr(), idx_del()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(UpdateDocuments, false), settings(true), doc_clr(), idx_del()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments,false, None), settings(true), idx_del()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, false, None), settings(true), idx_del()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments,false, None), settings(true), doc_clr(), idx_del()]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(UpdateDocuments, false, None), settings(true), doc_clr(), idx_del()]), @"Some((DocumentImport { method: UpdateDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); } #[test] fn allowed_and_disallowed_index_creation() { // `DocumentImport` can't be mixed with those disallowed to do so except if the index already exists. - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, false), doc_imp(ReplaceDocuments, true)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), doc_imp(ReplaceDocuments, true)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0, 1] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, false), doc_imp(ReplaceDocuments, false)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(true, [doc_imp(ReplaceDocuments, false), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: false, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, false, None), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, false, None), doc_imp(ReplaceDocuments, false, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, false, None), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, false), doc_imp(ReplaceDocuments, true)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, true), doc_imp(ReplaceDocuments, true)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, import_ids: [0, 1] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, false), doc_imp(ReplaceDocuments, false)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0, 1] }, false))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, true), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, import_ids: [0] }, true))"); - debug_snapshot!(autobatch_from(false, [doc_imp(ReplaceDocuments, false), settings(true)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, false, None), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, false, None), doc_imp(ReplaceDocuments, false, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0, 1] }, false))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, true, None), settings(true)]), @"Some((SettingsAndDocumentImport { settings_ids: [1], method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(false,None, [doc_imp(ReplaceDocuments, false, None), settings(true)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: false, primary_key: None, import_ids: [0] }, false))"); + } + + #[test] + fn autobatch_primary_key() { + // ==> If I have a pk + // With a single update + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("id"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("other"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("other"), import_ids: [0] }, true))"###); + + // With a multiple updates + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, Some("id"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0, 1] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, None)]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0, 1] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, Some("other"))]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, Some("id"))]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, None)]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, Some("id"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0, 1] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, None)]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0, 1] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, Some("other"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, None)]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, Some("id"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0] }, true))"###); + + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, None)]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("other"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, Some("id"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("other"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, None)]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("other"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, Some("other"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("other"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, None)]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("other"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, Some("id"), [doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, Some("other")), doc_imp(ReplaceDocuments, true, Some("id"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("other"), import_ids: [0] }, true))"###); + + // ==> If I don't have a pk + // With a single update + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, Some("id"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0] }, true))"###); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, Some("other"))]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("other"), import_ids: [0] }, true))"###); + + // With a multiple updates + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, None)]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0, 1] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, None), doc_imp(ReplaceDocuments, true, Some("id"))]), @"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: None, import_ids: [0] }, true))"); + debug_snapshot!(autobatch_from(true, None, [doc_imp(ReplaceDocuments, true, Some("id")), doc_imp(ReplaceDocuments, true, None)]), @r###"Some((DocumentImport { method: ReplaceDocuments, allow_index_creation: true, primary_key: Some("id"), import_ids: [0] }, true))"###); } } diff --git a/index-scheduler/src/batch.rs b/index-scheduler/src/batch.rs index e7e52c7c3..8a479a12b 100644 --- a/index-scheduler/src/batch.rs +++ b/index-scheduler/src/batch.rs @@ -217,7 +217,8 @@ impl IndexScheduler { let mut documents_counts = Vec::new(); let mut content_files = Vec::new(); - for task in &tasks { + + for task in tasks.iter() { match task.kind { KindWithContent::DocumentAdditionOrUpdate { content_file, @@ -325,6 +326,7 @@ impl IndexScheduler { settings_ids, method, allow_index_creation, + primary_key, import_ids, } => { let settings = self.create_next_batch_index( @@ -337,7 +339,12 @@ impl IndexScheduler { let document_import = self.create_next_batch_index( rtxn, index_uid.clone(), - BatchKind::DocumentImport { method, allow_index_creation, import_ids }, + BatchKind::DocumentImport { + method, + allow_index_creation, + primary_key, + import_ids, + }, must_create_index, )?; @@ -467,6 +474,12 @@ impl IndexScheduler { }; let index_already_exists = self.index_mapper.exists(rtxn, index_name)?; + let mut primary_key = None; + if index_already_exists { + let index = self.index_mapper.index(rtxn, index_name)?; + let rtxn = index.read_txn()?; + primary_key = index.primary_key(&rtxn)?.map(|pk| pk.to_string()); + } let index_tasks = self.index_tasks(rtxn, index_name)? & enqueued; @@ -484,7 +497,7 @@ impl IndexScheduler { .collect::>>()?; if let Some((batchkind, create_index)) = - autobatcher::autobatch(enqueued, index_already_exists) + autobatcher::autobatch(enqueued, index_already_exists, primary_key.as_deref()) { return self.create_next_batch_index( rtxn, @@ -949,7 +962,7 @@ impl IndexScheduler { /// The list of processed tasks. fn apply_index_operation<'i>( &self, - index_wtxn: &'_ mut RwTxn<'i, '_>, + index_wtxn: &mut RwTxn<'i, '_>, index: &'i Index, operation: IndexOperation, ) -> Result> { @@ -985,17 +998,31 @@ impl IndexScheduler { let mut primary_key_has_been_set = false; let must_stop_processing = self.must_stop_processing.clone(); let indexer_config = self.index_mapper.indexer_config(); - // TODO use the code from the IndexCreate operation + if let Some(primary_key) = primary_key { - if index.primary_key(index_wtxn)?.is_none() { - let mut builder = - milli::update::Settings::new(index_wtxn, index, indexer_config); - builder.set_primary_key(primary_key); - builder.execute( - |indexing_step| debug!("update: {:?}", indexing_step), - || must_stop_processing.clone().get(), - )?; - primary_key_has_been_set = true; + match index.primary_key(index_wtxn)? { + // if a primary key was set AND had already been defined in the index + // but to a different value, we can make the whole batch fail. + Some(pk) => { + if primary_key != pk { + return Err(milli::Error::from( + milli::UserError::PrimaryKeyCannotBeChanged(pk.to_string()), + ) + .into()); + } + } + // if the primary key was set and there was no primary key set for this index + // we set it to the received value before starting the indexing process. + None => { + let mut builder = + milli::update::Settings::new(index_wtxn, index, indexer_config); + builder.set_primary_key(primary_key); + builder.execute( + |indexing_step| debug!("update: {:?}", indexing_step), + || must_stop_processing.clone().get(), + )?; + primary_key_has_been_set = true; + } } } @@ -1059,7 +1086,8 @@ impl IndexScheduler { task.status = Status::Failed; task.details = Some(Details::DocumentAdditionOrUpdate { received_documents: count, - indexed_documents: Some(count), + // if there was an error we indexed 0 documents. + indexed_documents: Some(0), }); task.error = Some(error.into()) } diff --git a/index-scheduler/src/error.rs b/index-scheduler/src/error.rs index 013bcf595..3264bda7a 100644 --- a/index-scheduler/src/error.rs +++ b/index-scheduler/src/error.rs @@ -100,9 +100,9 @@ pub enum Error { InvalidIndexUid { index_uid: String }, #[error("Task `{0}` not found.")] TaskNotFound(TaskId), - #[error("Query parameters to filter the tasks to delete are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.")] + #[error("Query parameters to filter the tasks to delete are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.")] TaskDeletionWithEmptyQuery, - #[error("Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.")] + #[error("Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.")] TaskCancelationWithEmptyQuery, #[error(transparent)] @@ -141,8 +141,8 @@ impl ErrorCode for Error { Error::IndexAlreadyExists(_) => Code::IndexAlreadyExists, Error::SwapDuplicateIndexesFound(_) => Code::InvalidSwapDuplicateIndexFound, Error::SwapDuplicateIndexFound(_) => Code::InvalidSwapDuplicateIndexFound, - Error::SwapIndexNotFound(_) => Code::InvalidSwapIndexes, - Error::SwapIndexesNotFound(_) => Code::InvalidSwapIndexes, + Error::SwapIndexNotFound(_) => Code::IndexNotFound, + Error::SwapIndexesNotFound(_) => Code::IndexNotFound, Error::InvalidTaskDate { field, .. } => (*field).into(), Error::InvalidTaskUids { .. } => Code::InvalidTaskUids, Error::InvalidTaskStatuses { .. } => Code::InvalidTaskStatuses, diff --git a/index-scheduler/src/insta_snapshot.rs b/index-scheduler/src/insta_snapshot.rs index 0f0c9953a..e8d07ee63 100644 --- a/index-scheduler/src/insta_snapshot.rs +++ b/index-scheduler/src/insta_snapshot.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::fmt::Write; use meilisearch_types::heed::types::{OwnedType, SerdeBincode, SerdeJson, Str}; @@ -92,7 +93,9 @@ pub fn snapshot_index_scheduler(scheduler: &IndexScheduler) -> String { pub fn snapshot_file_store(file_store: &file_store::FileStore) -> String { let mut snap = String::new(); - for uuid in file_store.__all_uuids() { + // we store the uuid in a `BTreeSet` to keep them ordered. + let all_uuids = file_store.all_uuids().unwrap().collect::, _>>().unwrap(); + for uuid in all_uuids { snap.push_str(&format!("{uuid}\n")); } snap diff --git a/index-scheduler/src/lib.rs b/index-scheduler/src/lib.rs index 0b9e856d2..387dac2d0 100644 --- a/index-scheduler/src/lib.rs +++ b/index-scheduler/src/lib.rs @@ -452,6 +452,10 @@ impl IndexScheduler { &self.index_mapper.indexer_config } + pub fn size(&self) -> Result { + Ok(self.env.real_disk_size()?) + } + /// Return the index corresponding to the name. /// /// * If the index wasn't opened before, the index will be opened. @@ -502,13 +506,22 @@ impl IndexScheduler { } if let Some(canceled_by) = &query.canceled_by { + let mut all_canceled_tasks = RoaringBitmap::new(); for cancel_task_uid in canceled_by { if let Some(canceled_by_uid) = self.canceled_by.get(rtxn, &BEU32::new(*cancel_task_uid))? { - tasks &= canceled_by_uid; + all_canceled_tasks |= canceled_by_uid; } } + + // if the canceled_by has been specified but no task + // matches then we prefer matching zero than all tasks. + if all_canceled_tasks.is_empty() { + return Ok(RoaringBitmap::new()); + } else { + tasks &= all_canceled_tasks; + } } if let Some(kind) = &query.types { @@ -889,6 +902,11 @@ impl IndexScheduler { Ok(self.file_store.new_update_with_uuid(uuid)?) } + /// The size on disk taken by all the updates files contained in the `IndexScheduler`, in bytes. + pub fn compute_update_file_size(&self) -> Result { + Ok(self.file_store.compute_total_size()?) + } + /// Delete a file from the index scheduler. /// /// Counterpart to the [`create_update_file`](IndexScheduler::create_update_file) method. @@ -1991,7 +2009,7 @@ mod tests { .unwrap() .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) .collect::>(); - snapshot!(serde_json::to_string_pretty(&documents).unwrap()); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); } #[test] @@ -2038,7 +2056,7 @@ mod tests { .unwrap() .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) .collect::>(); - snapshot!(serde_json::to_string_pretty(&documents).unwrap()); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); } #[test] @@ -2090,7 +2108,7 @@ mod tests { .unwrap() .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) .collect::>(); - snapshot!(serde_json::to_string_pretty(&documents).unwrap()); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); } #[test] @@ -2141,7 +2159,7 @@ mod tests { .unwrap() .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) .collect::>(); - snapshot!(serde_json::to_string_pretty(&documents).unwrap()); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); } #[test] @@ -2192,7 +2210,7 @@ mod tests { .unwrap() .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) .collect::>(); - snapshot!(serde_json::to_string_pretty(&documents).unwrap()); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); } #[macro_export] @@ -2831,7 +2849,7 @@ mod tests { .unwrap() .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) .collect::>(); - snapshot!(serde_json::to_string_pretty(&documents).unwrap()); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); } #[test] @@ -2894,7 +2912,7 @@ mod tests { .unwrap() .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) .collect::>(); - snapshot!(serde_json::to_string_pretty(&documents).unwrap()); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); } #[test] @@ -2954,7 +2972,7 @@ mod tests { .unwrap() .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) .collect::>(); - snapshot!(serde_json::to_string_pretty(&documents).unwrap()); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); } #[test] @@ -3011,7 +3029,361 @@ mod tests { .unwrap() .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) .collect::>(); - snapshot!(serde_json::to_string_pretty(&documents).unwrap()); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); + } + + #[test] + fn test_document_addition_with_multiple_primary_key() { + let (index_scheduler, mut handle) = IndexScheduler::test(true, vec![]); + + for (id, primary_key) in ["id", "bork", "bloup"].iter().enumerate() { + let content = format!( + r#"{{ + "id": {id}, + "doggo": "jean bob" + }}"#, + ); + let (uuid, mut file) = + index_scheduler.create_update_file_with_uuid(id as u128).unwrap(); + let documents_count = read_json(content.as_bytes(), file.as_file_mut()).unwrap(); + assert_eq!(documents_count, 1); + file.persist().unwrap(); + + index_scheduler + .register(KindWithContent::DocumentAdditionOrUpdate { + index_uid: S("doggos"), + primary_key: Some(S(primary_key)), + method: ReplaceDocuments, + content_file: uuid, + documents_count, + allow_index_creation: true, + }) + .unwrap(); + index_scheduler.assert_internally_consistent(); + } + + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "after_registering_the_3_tasks"); + + // A first batch should be processed with only the first documentAddition. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "only_first_task_succeed"); + + // The second batch should fail. + handle.advance_one_failed_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "second_task_fails"); + + // The second batch should fail. + handle.advance_one_failed_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "third_task_fails"); + + // Is the primary key still what we expect? + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap().unwrap(); + snapshot!(primary_key, @"id"); + + // Is the document still the one we expect?. + let field_ids_map = index.fields_ids_map(&rtxn).unwrap(); + let field_ids = field_ids_map.ids().collect::>(); + let documents = index + .all_documents(&rtxn) + .unwrap() + .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) + .collect::>(); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); + } + + #[test] + fn test_document_addition_with_multiple_primary_key_batch_wrong_key() { + let (index_scheduler, mut handle) = IndexScheduler::test(true, vec![]); + + for (id, primary_key) in ["id", "bork", "bork"].iter().enumerate() { + let content = format!( + r#"{{ + "id": {id}, + "doggo": "jean bob" + }}"#, + ); + let (uuid, mut file) = + index_scheduler.create_update_file_with_uuid(id as u128).unwrap(); + let documents_count = read_json(content.as_bytes(), file.as_file_mut()).unwrap(); + assert_eq!(documents_count, 1); + file.persist().unwrap(); + + index_scheduler + .register(KindWithContent::DocumentAdditionOrUpdate { + index_uid: S("doggos"), + primary_key: Some(S(primary_key)), + method: ReplaceDocuments, + content_file: uuid, + documents_count, + allow_index_creation: true, + }) + .unwrap(); + index_scheduler.assert_internally_consistent(); + } + + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "after_registering_the_3_tasks"); + + // A first batch should be processed with only the first documentAddition. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "only_first_task_succeed"); + + // The second batch should fail and contains two tasks. + handle.advance_one_failed_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "second_and_third_tasks_fails"); + + // Is the primary key still what we expect? + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap().unwrap(); + snapshot!(primary_key, @"id"); + + // Is the document still the one we expect?. + let field_ids_map = index.fields_ids_map(&rtxn).unwrap(); + let field_ids = field_ids_map.ids().collect::>(); + let documents = index + .all_documents(&rtxn) + .unwrap() + .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) + .collect::>(); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); + } + + #[test] + fn test_document_addition_with_bad_primary_key() { + let (index_scheduler, mut handle) = IndexScheduler::test(true, vec![]); + + for (id, primary_key) in ["bork", "bork", "id", "bork", "id"].iter().enumerate() { + let content = format!( + r#"{{ + "id": {id}, + "doggo": "jean bob" + }}"#, + ); + let (uuid, mut file) = + index_scheduler.create_update_file_with_uuid(id as u128).unwrap(); + let documents_count = read_json(content.as_bytes(), file.as_file_mut()).unwrap(); + assert_eq!(documents_count, 1); + file.persist().unwrap(); + + index_scheduler + .register(KindWithContent::DocumentAdditionOrUpdate { + index_uid: S("doggos"), + primary_key: Some(S(primary_key)), + method: ReplaceDocuments, + content_file: uuid, + documents_count, + allow_index_creation: true, + }) + .unwrap(); + index_scheduler.assert_internally_consistent(); + } + + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "after_registering_the_5_tasks"); + + // A first batch should be processed with only the first two documentAddition. + // it should fails because the documents don't contains any `bork` field. + // NOTE: it's marked as successful because the batch didn't fails, it's the individual tasks that failed. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "first_and_second_task_fails"); + + // The primary key should be set to none since we failed the batch. + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap(); + snapshot!(primary_key.is_none(), @"true"); + + // The second batch should succeed and only contains one task. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "third_task_succeeds"); + + // The primary key should be set to `id` since this batch succeeded. + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap().unwrap(); + snapshot!(primary_key, @"id"); + + // We're trying to `bork` again, but now there is already a primary key set for this index. + handle.advance_one_failed_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "fourth_task_fails"); + + // Finally the last task should succeed since its primary key is the same as the valid one. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "fifth_task_succeeds"); + + // Is the primary key still what we expect? + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap().unwrap(); + snapshot!(primary_key, @"id"); + + // Is the document still the one we expect?. + let field_ids_map = index.fields_ids_map(&rtxn).unwrap(); + let field_ids = field_ids_map.ids().collect::>(); + let documents = index + .all_documents(&rtxn) + .unwrap() + .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) + .collect::>(); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); + } + + #[test] + fn test_document_addition_with_set_and_null_primary_key() { + let (index_scheduler, mut handle) = IndexScheduler::test(true, vec![]); + + for (id, primary_key) in + [None, Some("bork"), Some("paw"), None, None, Some("paw")].into_iter().enumerate() + { + let content = format!( + r#"{{ + "paw": {id}, + "doggo": "jean bob" + }}"#, + ); + let (uuid, mut file) = + index_scheduler.create_update_file_with_uuid(id as u128).unwrap(); + let documents_count = read_json(content.as_bytes(), file.as_file_mut()).unwrap(); + assert_eq!(documents_count, 1); + file.persist().unwrap(); + + index_scheduler + .register(KindWithContent::DocumentAdditionOrUpdate { + index_uid: S("doggos"), + primary_key: primary_key.map(|pk| pk.to_string()), + method: ReplaceDocuments, + content_file: uuid, + documents_count, + allow_index_creation: true, + }) + .unwrap(); + index_scheduler.assert_internally_consistent(); + } + + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "after_registering_the_6_tasks"); + + // A first batch should contains only one task that fails because we can't infer the primary key. + // NOTE: it's marked as successful because the batch didn't fails, it's the individual tasks that failed. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "first_task_fails"); + + // The second batch should contains only one task that fails because we bork is not a valid primary key. + // NOTE: it's marked as successful because the batch didn't fails, it's the individual tasks that failed. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "second_task_fails"); + + // No primary key should be set at this point. + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap(); + snapshot!(primary_key.is_none(), @"true"); + + // The third batch should succeed and only contains one task. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "third_task_succeeds"); + + // The primary key should be set to `id` since this batch succeeded. + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap().unwrap(); + snapshot!(primary_key, @"paw"); + + // We should be able to batch together the next two tasks that don't specify any primary key + // + the last task that matches the current primary-key. Everything should succeed. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "all_other_tasks_succeeds"); + + // Is the primary key still what we expect? + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap().unwrap(); + snapshot!(primary_key, @"paw"); + + // Is the document still the one we expect?. + let field_ids_map = index.fields_ids_map(&rtxn).unwrap(); + let field_ids = field_ids_map.ids().collect::>(); + let documents = index + .all_documents(&rtxn) + .unwrap() + .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) + .collect::>(); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); + } + + #[test] + fn test_document_addition_with_set_and_null_primary_key_inference_works() { + let (index_scheduler, mut handle) = IndexScheduler::test(true, vec![]); + + for (id, primary_key) in [None, Some("bork"), Some("doggoid"), None, None, Some("doggoid")] + .into_iter() + .enumerate() + { + let content = format!( + r#"{{ + "doggoid": {id}, + "doggo": "jean bob" + }}"#, + ); + let (uuid, mut file) = + index_scheduler.create_update_file_with_uuid(id as u128).unwrap(); + let documents_count = read_json(content.as_bytes(), file.as_file_mut()).unwrap(); + assert_eq!(documents_count, 1); + file.persist().unwrap(); + + index_scheduler + .register(KindWithContent::DocumentAdditionOrUpdate { + index_uid: S("doggos"), + primary_key: primary_key.map(|pk| pk.to_string()), + method: ReplaceDocuments, + content_file: uuid, + documents_count, + allow_index_creation: true, + }) + .unwrap(); + index_scheduler.assert_internally_consistent(); + } + + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "after_registering_the_6_tasks"); + + // A first batch should contains only one task that succeed and sets the primary key to `doggoid`. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "first_task_succeed"); + + // Checking the primary key. + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap(); + snapshot!(primary_key.is_none(), @"false"); + + // The second batch should contains only one task that fails because it tries to update the primary key to `bork`. + handle.advance_one_failed_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "second_task_fails"); + + // The third batch should succeed and only contains one task. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "third_task_succeeds"); + + // We should be able to batch together the next two tasks that don't specify any primary key + // + the last task that matches the current primary-key. Everything should succeed. + handle.advance_one_successful_batch(); + snapshot!(snapshot_index_scheduler(&index_scheduler), name: "all_other_tasks_succeeds"); + + // Is the primary key still what we expect? + let index = index_scheduler.index("doggos").unwrap(); + let rtxn = index.read_txn().unwrap(); + let primary_key = index.primary_key(&rtxn).unwrap().unwrap(); + snapshot!(primary_key, @"doggoid"); + + // Is the document still the one we expect?. + let field_ids_map = index.fields_ids_map(&rtxn).unwrap(); + let field_ids = field_ids_map.ids().collect::>(); + let documents = index + .all_documents(&rtxn) + .unwrap() + .map(|ret| obkv_to_json(&field_ids, &field_ids_map, ret.unwrap().1).unwrap()) + .collect::>(); + snapshot!(serde_json::to_string_pretty(&documents).unwrap(), name: "documents"); } #[test] diff --git a/index-scheduler/src/snapshots/lib.rs/swap_indexes_errors/first_swap_failed.snap b/index-scheduler/src/snapshots/lib.rs/swap_indexes_errors/first_swap_failed.snap index 1a47f87fb..fd9790835 100644 --- a/index-scheduler/src/snapshots/lib.rs/swap_indexes_errors/first_swap_failed.snap +++ b/index-scheduler/src/snapshots/lib.rs/swap_indexes_errors/first_swap_failed.snap @@ -10,7 +10,7 @@ source: index-scheduler/src/lib.rs 1 {uid: 1, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "b", primary_key: Some("id") }} 2 {uid: 2, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "c", primary_key: Some("id") }} 3 {uid: 3, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "d", primary_key: Some("id") }} -4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Indexes `e`, `f` not found.", error_code: "invalid_swap_indexes", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-swap-indexes" }, details: { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }, kind: IndexSwap { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }} +4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Indexes `e`, `f` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }, kind: IndexSwap { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }} ---------------------------------------------------------------------- ### Status: enqueued [] diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_with_index/1.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_with_index/documents.snap similarity index 100% rename from index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_with_index/1.snap rename to index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_with_index/documents.snap diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_with_index_without_autobatching/1.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_with_index_without_autobatching/documents.snap similarity index 100% rename from index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_with_index_without_autobatching/1.snap rename to index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_with_index_without_autobatching/documents.snap diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index/after_processing_the_10_tasks.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index/after_processing_the_10_tasks.snap index 59e7a4509..ed28c121b 100644 --- a/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index/after_processing_the_10_tasks.snap +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index/after_processing_the_10_tasks.snap @@ -6,16 +6,16 @@ source: index-scheduler/src/lib.rs [] ---------------------------------------------------------------------- ### All Tasks: -0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} -1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }} -2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }} -3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }} -4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }} -5 {uid: 5, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }} -6 {uid: 6, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }} -7 {uid: 7, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }} -8 {uid: 8, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000008, documents_count: 1, allow_index_creation: false }} -9 {uid: 9, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000009, documents_count: 1, allow_index_creation: false }} +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }} +2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }} +3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }} +4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }} +5 {uid: 5, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }} +6 {uid: 6, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }} +7 {uid: 7, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }} +8 {uid: 8, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000008, documents_count: 1, allow_index_creation: false }} +9 {uid: 9, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000009, documents_count: 1, allow_index_creation: false }} ---------------------------------------------------------------------- ### Status: enqueued [] diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index_without_autobatching/all_tasks_processed.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index_without_autobatching/all_tasks_processed.snap index 05da8f83b..d995cab9e 100644 --- a/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index_without_autobatching/all_tasks_processed.snap +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index_without_autobatching/all_tasks_processed.snap @@ -6,16 +6,16 @@ source: index-scheduler/src/lib.rs [] ---------------------------------------------------------------------- ### All Tasks: -0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} -1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }} -2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }} -3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }} -4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }} -5 {uid: 5, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }} -6 {uid: 6, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }} -7 {uid: 7, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }} -8 {uid: 8, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000008, documents_count: 1, allow_index_creation: false }} -9 {uid: 9, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000009, documents_count: 1, allow_index_creation: false }} +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }} +2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }} +3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }} +4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }} +5 {uid: 5, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }} +6 {uid: 6, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }} +7 {uid: 7, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }} +8 {uid: 8, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000008, documents_count: 1, allow_index_creation: false }} +9 {uid: 9, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000009, documents_count: 1, allow_index_creation: false }} ---------------------------------------------------------------------- ### Status: enqueued [] diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index_without_autobatching/five_tasks_processed.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index_without_autobatching/five_tasks_processed.snap index 3c33a3f57..3ae875bff 100644 --- a/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index_without_autobatching/five_tasks_processed.snap +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_cant_create_index_without_index_without_autobatching/five_tasks_processed.snap @@ -6,11 +6,11 @@ source: index-scheduler/src/lib.rs [] ---------------------------------------------------------------------- ### All Tasks: -0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} -1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }} -2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }} -3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }} -4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }} +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }} +2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }} +3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }} +4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }} 5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }} 6 {uid: 6, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }} 7 {uid: 7, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }} diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/all_tasks_processed.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/all_tasks_processed.snap index d2ebddf08..19ee47359 100644 --- a/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/all_tasks_processed.snap +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/all_tasks_processed.snap @@ -6,7 +6,7 @@ source: index-scheduler/src/lib.rs [] ---------------------------------------------------------------------- ### All Tasks: -0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} 1 {uid: 1, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} 2 {uid: 2, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }} 3 {uid: 3, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/1.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/documents.snap similarity index 100% rename from index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/1.snap rename to index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/documents.snap diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/only_first_task_failed.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/only_first_task_failed.snap index 00edf1ef7..ed57bc4e3 100644 --- a/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/only_first_task_failed.snap +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_right_without_index_starts_with_cant_create/only_first_task_failed.snap @@ -6,7 +6,7 @@ source: index-scheduler/src/lib.rs [] ---------------------------------------------------------------------- ### All Tasks: -0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }} 1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} 2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }} 3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_rights_with_index/1.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_rights_with_index/documents.snap similarity index 100% rename from index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_rights_with_index/1.snap rename to index-scheduler/src/snapshots/lib.rs/test_document_addition_mixed_rights_with_index/documents.snap diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/after_registering_the_5_tasks.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/after_registering_the_5_tasks.snap new file mode 100644 index 000000000..53d3d28da --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/after_registering_the_5_tasks.snap @@ -0,0 +1,49 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [0,1,2,3,4,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Mapper: +[] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +---------------------------------------------------------------------- +### Started At: +---------------------------------------------------------------------- +### Finished At: +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000000 +00000000-0000-0000-0000-000000000001 +00000000-0000-0000-0000-000000000002 +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/documents.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/documents.snap new file mode 100644 index 000000000..dd1bbf8b0 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/documents.snap @@ -0,0 +1,13 @@ +--- +source: index-scheduler/src/lib.rs +--- +[ + { + "id": 2, + "doggo": "jean bob" + }, + { + "id": 4, + "doggo": "jean bob" + } +] diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/fifth_task_succeeds.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/fifth_task_succeeds.snap new file mode 100644 index 000000000..58e16fa55 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/fifth_task_succeeds.snap @@ -0,0 +1,54 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"id\":0,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"id\":1,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index already has a primary key: `id`.", error_code: "index_primary_key_already_exists", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [] +succeeded [2,4,] +failed [0,1,3,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +---------------------------------------------------------------------- +### File Store: + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/first_and_second_task_fails.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/first_and_second_task_fails.snap new file mode 100644 index 000000000..98bd2b580 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/first_and_second_task_fails.snap @@ -0,0 +1,50 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"id\":0,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"id\":1,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [2,3,4,] +failed [0,1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,1,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,1,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000002 +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/fourth_task_fails.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/fourth_task_fails.snap new file mode 100644 index 000000000..279040fdb --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/fourth_task_fails.snap @@ -0,0 +1,53 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"id\":0,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"id\":1,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index already has a primary key: `id`.", error_code: "index_primary_key_already_exists", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [4,] +succeeded [2,] +failed [0,1,3,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,1,] +[timestamp] [2,] +[timestamp] [3,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,1,] +[timestamp] [2,] +[timestamp] [3,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000004 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/third_task_succeeds.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/third_task_succeeds.snap new file mode 100644 index 000000000..441bb59e2 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_bad_primary_key/third_task_succeeds.snap @@ -0,0 +1,52 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"id\":0,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"id\":1,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [3,4,] +succeeded [2,] +failed [0,1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/after_registering_the_3_tasks.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/after_registering_the_3_tasks.snap new file mode 100644 index 000000000..cff9b0bd9 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/after_registering_the_3_tasks.snap @@ -0,0 +1,43 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bloup"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [0,1,2,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,] +---------------------------------------------------------------------- +### Index Mapper: +[] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Started At: +---------------------------------------------------------------------- +### Finished At: +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000000 +00000000-0000-0000-0000-000000000001 +00000000-0000-0000-0000-000000000002 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/documents.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/documents.snap new file mode 100644 index 000000000..96f9d447f --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/documents.snap @@ -0,0 +1,9 @@ +--- +source: index-scheduler/src/lib.rs +--- +[ + { + "id": 0, + "doggo": "jean bob" + } +] diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/only_first_task_succeed.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/only_first_task_succeed.snap new file mode 100644 index 000000000..d3888af01 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/only_first_task_succeed.snap @@ -0,0 +1,45 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bloup"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [1,2,] +succeeded [0,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000001 +00000000-0000-0000-0000-000000000002 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/second_task_fails.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/second_task_fails.snap new file mode 100644 index 000000000..84baeca92 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/second_task_fails.snap @@ -0,0 +1,47 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index already has a primary key: `id`.", error_code: "index_primary_key_already_exists", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bloup"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [2,] +succeeded [0,] +failed [1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +[timestamp] [1,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +[timestamp] [1,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000002 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/third_task_fails.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/third_task_fails.snap new file mode 100644 index 000000000..6b92f91d1 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key/third_task_fails.snap @@ -0,0 +1,48 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index already has a primary key: `id`.", error_code: "index_primary_key_already_exists", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index already has a primary key: `id`.", error_code: "index_primary_key_already_exists", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bloup"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [] +succeeded [0,] +failed [1,2,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### File Store: + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/after_registering_the_3_tasks.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/after_registering_the_3_tasks.snap new file mode 100644 index 000000000..4c4f88a30 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/after_registering_the_3_tasks.snap @@ -0,0 +1,43 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [0,1,2,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,] +---------------------------------------------------------------------- +### Index Mapper: +[] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Started At: +---------------------------------------------------------------------- +### Finished At: +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000000 +00000000-0000-0000-0000-000000000001 +00000000-0000-0000-0000-000000000002 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/documents.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/documents.snap new file mode 100644 index 000000000..96f9d447f --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/documents.snap @@ -0,0 +1,9 @@ +--- +source: index-scheduler/src/lib.rs +--- +[ + { + "id": 0, + "doggo": "jean bob" + } +] diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/only_first_task_succeed.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/only_first_task_succeed.snap new file mode 100644 index 000000000..76b814eab --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/only_first_task_succeed.snap @@ -0,0 +1,45 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [1,2,] +succeeded [0,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000001 +00000000-0000-0000-0000-000000000002 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/second_and_third_tasks_fails.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/second_and_third_tasks_fails.snap new file mode 100644 index 000000000..26b0f6584 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_multiple_primary_key_batch_wrong_key/second_and_third_tasks_fails.snap @@ -0,0 +1,47 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index already has a primary key: `id`.", error_code: "index_primary_key_already_exists", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [2,] +succeeded [0,] +failed [1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +[timestamp] [1,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +[timestamp] [1,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000002 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/after_registering_the_6_tasks.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/after_registering_the_6_tasks.snap new file mode 100644 index 000000000..078ba06d3 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/after_registering_the_6_tasks.snap @@ -0,0 +1,52 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +[] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +---------------------------------------------------------------------- +### Finished At: +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000000 +00000000-0000-0000-0000-000000000001 +00000000-0000-0000-0000-000000000002 +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 +00000000-0000-0000-0000-000000000005 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/all_other_tasks_succeeds.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/all_other_tasks_succeeds.snap new file mode 100644 index 000000000..69cbd3def --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/all_other_tasks_succeeds.snap @@ -0,0 +1,56 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "The primary key inference failed as the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", error_code: "index_primary_key_no_candidate_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_no_candidate_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"paw\":1,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [] +succeeded [2,3,4,5,] +failed [0,1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,4,5,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,4,5,] +---------------------------------------------------------------------- +### File Store: + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/documents.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/documents.snap new file mode 100644 index 000000000..a73c52da5 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/documents.snap @@ -0,0 +1,21 @@ +--- +source: index-scheduler/src/lib.rs +--- +[ + { + "paw": 2, + "doggo": "jean bob" + }, + { + "paw": 3, + "doggo": "jean bob" + }, + { + "paw": 4, + "doggo": "jean bob" + }, + { + "paw": 5, + "doggo": "jean bob" + } +] diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/first_task_fails.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/first_task_fails.snap new file mode 100644 index 000000000..ac63f3b58 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/first_task_fails.snap @@ -0,0 +1,54 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "The primary key inference failed as the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", error_code: "index_primary_key_no_candidate_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_no_candidate_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [1,2,3,4,5,] +failed [0,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000001 +00000000-0000-0000-0000-000000000002 +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 +00000000-0000-0000-0000-000000000005 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/second_task_fails.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/second_task_fails.snap new file mode 100644 index 000000000..538b4af93 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/second_task_fails.snap @@ -0,0 +1,55 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "The primary key inference failed as the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", error_code: "index_primary_key_no_candidate_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_no_candidate_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"paw\":1,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [2,3,4,5,] +failed [0,1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +[timestamp] [1,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +[timestamp] [1,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000002 +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 +00000000-0000-0000-0000-000000000005 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/third_task_succeeds.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/third_task_succeeds.snap new file mode 100644 index 000000000..1271b6f92 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key/third_task_succeeds.snap @@ -0,0 +1,57 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "The primary key inference failed as the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", error_code: "index_primary_key_no_candidate_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_no_candidate_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Document doesn't have a `bork` attribute: `{\"paw\":1,\"doggo\":\"jean bob\"}`.", error_code: "missing_document_id", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#missing_document_id" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("paw"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [3,4,5,] +succeeded [2,] +failed [0,1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 +00000000-0000-0000-0000-000000000005 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/after_registering_the_6_tasks.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/after_registering_the_6_tasks.snap new file mode 100644 index 000000000..0e9ecb81a --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/after_registering_the_6_tasks.snap @@ -0,0 +1,52 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +[] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +---------------------------------------------------------------------- +### Finished At: +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000000 +00000000-0000-0000-0000-000000000001 +00000000-0000-0000-0000-000000000002 +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 +00000000-0000-0000-0000-000000000005 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/all_other_tasks_succeeds.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/all_other_tasks_succeeds.snap new file mode 100644 index 000000000..437c6375e --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/all_other_tasks_succeeds.snap @@ -0,0 +1,56 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index already has a primary key: `doggoid`.", error_code: "index_primary_key_already_exists", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [] +succeeded [0,2,3,4,5,] +failed [1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,4,5,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,4,5,] +---------------------------------------------------------------------- +### File Store: + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/documents.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/documents.snap new file mode 100644 index 000000000..9c79853fa --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/documents.snap @@ -0,0 +1,25 @@ +--- +source: index-scheduler/src/lib.rs +--- +[ + { + "doggoid": 0, + "doggo": "jean bob" + }, + { + "doggoid": 2, + "doggo": "jean bob" + }, + { + "doggoid": 3, + "doggo": "jean bob" + }, + { + "doggoid": 4, + "doggo": "jean bob" + }, + { + "doggoid": 5, + "doggo": "jean bob" + } +] diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/first_task_succeed.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/first_task_succeed.snap new file mode 100644 index 000000000..fd480420a --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/first_task_succeed.snap @@ -0,0 +1,54 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [1,2,3,4,5,] +succeeded [0,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000001 +00000000-0000-0000-0000-000000000002 +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 +00000000-0000-0000-0000-000000000005 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/second_task_fails.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/second_task_fails.snap new file mode 100644 index 000000000..99001edb0 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/second_task_fails.snap @@ -0,0 +1,56 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index already has a primary key: `doggoid`.", error_code: "index_primary_key_already_exists", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [2,3,4,5,] +succeeded [0,] +failed [1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +[timestamp] [1,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +[timestamp] [1,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000002 +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 +00000000-0000-0000-0000-000000000005 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/third_task_succeeds.snap b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/third_task_succeeds.snap new file mode 100644 index 000000000..64625ca90 --- /dev/null +++ b/index-scheduler/src/snapshots/lib.rs/test_document_addition_with_set_and_null_primary_key_inference_works/third_task_succeeds.snap @@ -0,0 +1,57 @@ +--- +source: index-scheduler/src/lib.rs +--- +### Autobatching Enabled = true +### Processing Tasks: +[] +---------------------------------------------------------------------- +### All Tasks: +0 {uid: 0, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: true }} +1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index already has a primary key: `doggoid`.", error_code: "index_primary_key_already_exists", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("bork"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }} +2 {uid: 2, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: true }} +3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }} +4 {uid: 4, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: None, method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: true }} +5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("doggoid"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: true }} +---------------------------------------------------------------------- +### Status: +enqueued [3,4,5,] +succeeded [0,2,] +failed [1,] +---------------------------------------------------------------------- +### Kind: +"documentAdditionOrUpdate" [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Tasks: +doggos [0,1,2,3,4,5,] +---------------------------------------------------------------------- +### Index Mapper: +["doggos"] +---------------------------------------------------------------------- +### Canceled By: + +---------------------------------------------------------------------- +### Enqueued At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +[timestamp] [3,] +[timestamp] [4,] +[timestamp] [5,] +---------------------------------------------------------------------- +### Started At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### Finished At: +[timestamp] [0,] +[timestamp] [1,] +[timestamp] [2,] +---------------------------------------------------------------------- +### File Store: +00000000-0000-0000-0000-000000000003 +00000000-0000-0000-0000-000000000004 +00000000-0000-0000-0000-000000000005 + +---------------------------------------------------------------------- + diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_replace/3.snap b/index-scheduler/src/snapshots/lib.rs/test_document_replace/documents.snap similarity index 100% rename from index-scheduler/src/snapshots/lib.rs/test_document_replace/3.snap rename to index-scheduler/src/snapshots/lib.rs/test_document_replace/documents.snap diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_replace_without_autobatching/1.snap b/index-scheduler/src/snapshots/lib.rs/test_document_replace_without_autobatching/documents.snap similarity index 100% rename from index-scheduler/src/snapshots/lib.rs/test_document_replace_without_autobatching/1.snap rename to index-scheduler/src/snapshots/lib.rs/test_document_replace_without_autobatching/documents.snap diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_update/3.snap b/index-scheduler/src/snapshots/lib.rs/test_document_update/documents.snap similarity index 100% rename from index-scheduler/src/snapshots/lib.rs/test_document_update/3.snap rename to index-scheduler/src/snapshots/lib.rs/test_document_update/documents.snap diff --git a/index-scheduler/src/snapshots/lib.rs/test_document_update_without_autobatching/1.snap b/index-scheduler/src/snapshots/lib.rs/test_document_update_without_autobatching/documents.snap similarity index 100% rename from index-scheduler/src/snapshots/lib.rs/test_document_update_without_autobatching/1.snap rename to index-scheduler/src/snapshots/lib.rs/test_document_update_without_autobatching/documents.snap diff --git a/index-scheduler/src/snapshots/lib.rs/test_mixed_document_addition/1.snap b/index-scheduler/src/snapshots/lib.rs/test_mixed_document_addition/documents.snap similarity index 100% rename from index-scheduler/src/snapshots/lib.rs/test_mixed_document_addition/1.snap rename to index-scheduler/src/snapshots/lib.rs/test_mixed_document_addition/documents.snap diff --git a/index-scheduler/src/utils.rs b/index-scheduler/src/utils.rs index 0018ae1d0..c9b71b523 100644 --- a/index-scheduler/src/utils.rs +++ b/index-scheduler/src/utils.rs @@ -404,15 +404,19 @@ impl IndexScheduler { Details::DocumentAdditionOrUpdate { received_documents, indexed_documents } => { assert_eq!(kind.as_kind(), Kind::DocumentAdditionOrUpdate); match indexed_documents { - Some(0) => assert_ne!(status, Status::Enqueued), Some(indexed_documents) => { - assert_eq!(status, Status::Succeeded); - assert!(indexed_documents <= received_documents); + assert!(matches!( + status, + Status::Succeeded | Status::Failed | Status::Canceled + )); + match status { + Status::Succeeded => assert!(indexed_documents <= received_documents), + Status::Failed | Status::Canceled => assert_eq!(indexed_documents, 0), + status => panic!("DocumentAddition can't have an indexed_document set if it's {}", status), + } } None => { - assert_ne!(status, Status::Succeeded); - assert_ne!(status, Status::Canceled); - assert_ne!(status, Status::Failed); + assert!(matches!(status, Status::Enqueued | Status::Processing)) } } } @@ -504,10 +508,21 @@ impl IndexScheduler { if let KindWithContent::DocumentAdditionOrUpdate { content_file, .. } = kind { match status { Status::Enqueued | Status::Processing => { - assert!(self.file_store.__all_uuids().contains(&content_file)); + assert!(self + .file_store + .all_uuids() + .unwrap() + .any(|uuid| uuid.as_ref().unwrap() == &content_file), + "Could not find uuid `{content_file}` in the file_store. Available uuids are {:?}.", + self.file_store.all_uuids().unwrap().collect::, file_store::Error>>().unwrap(), + ); } Status::Succeeded | Status::Failed | Status::Canceled => { - assert!(!self.file_store.__all_uuids().contains(&content_file)); + assert!(self + .file_store + .all_uuids() + .unwrap() + .all(|uuid| uuid.as_ref().unwrap() != &content_file)); } } } diff --git a/meilisearch-auth/src/lib.rs b/meilisearch-auth/src/lib.rs index 8d4a7f2b7..adfd00ce5 100644 --- a/meilisearch-auth/src/lib.rs +++ b/meilisearch-auth/src/lib.rs @@ -3,12 +3,12 @@ pub mod error; mod store; use std::collections::{HashMap, HashSet}; -use std::ops::Deref; use std::path::Path; use std::sync::Arc; use error::{AuthControllerError, Result}; use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey}; +use meilisearch_types::milli::update::Setting; use meilisearch_types::star_or::StarOr; use serde::{Deserialize, Serialize}; pub use store::open_auth_store_env; @@ -33,6 +33,11 @@ impl AuthController { Ok(Self { store: Arc::new(store), master_key: master_key.clone() }) } + /// Return the size of the `AuthController` database in bytes. + pub fn size(&self) -> Result { + self.store.size() + } + pub fn create_key(&self, create_key: CreateApiKey) -> Result { match self.store.get_api_key(create_key.uid)? { Some(_) => Err(AuthControllerError::ApiKeyAlreadyExists(create_key.uid.to_string())), @@ -42,8 +47,14 @@ impl AuthController { pub fn update_key(&self, uid: Uuid, patch: PatchApiKey) -> Result { let mut key = self.get_key(uid)?; - key.description = patch.description; - key.name = patch.name; + match patch.description { + Setting::NotSet => (), + description => key.description = description.set(), + }; + match patch.name { + Setting::NotSet => (), + name => key.name = name.set(), + }; key.updated_at = OffsetDateTime::now_utc(); self.store.put_api_key(key) } @@ -86,15 +97,13 @@ impl AuthController { key.indexes .into_iter() .filter_map(|index| { - search_rules.get_index_search_rules(index.deref()).map( - |index_search_rules| { - (String::from(index), Some(index_search_rules)) - }, + search_rules.get_index_search_rules(&format!("{index}")).map( + |index_search_rules| (index.to_string(), Some(index_search_rules)), ) }) .collect(), ), - None => SearchRules::Set(key.indexes.into_iter().map(String::from).collect()), + None => SearchRules::Set(key.indexes.into_iter().map(|x| x.to_string()).collect()), }; } else if let Some(search_rules) = search_rules { filters.search_rules = search_rules; diff --git a/meilisearch-auth/src/store.rs b/meilisearch-auth/src/store.rs index b3f9ed672..c1cec0ede 100644 --- a/meilisearch-auth/src/store.rs +++ b/meilisearch-auth/src/store.rs @@ -3,7 +3,6 @@ use std::cmp::Reverse; use std::collections::HashSet; use std::convert::{TryFrom, TryInto}; use std::fs::create_dir_all; -use std::ops::Deref; use std::path::Path; use std::str; use std::sync::Arc; @@ -61,6 +60,11 @@ impl HeedAuthStore { Ok(Self { env, keys, action_keyid_index_expiration, should_close_on_drop: true }) } + /// Return the size in bytes of database + pub fn size(&self) -> Result { + Ok(self.env.real_disk_size()?) + } + pub fn set_drop_on_close(&mut self, v: bool) { self.should_close_on_drop = v; } @@ -135,7 +139,7 @@ impl HeedAuthStore { for index in key.indexes.iter() { db.put( &mut wtxn, - &(&uid, &action, Some(index.deref().as_bytes())), + &(&uid, &action, Some(index.to_string().as_bytes())), &key.expires_at, )?; } diff --git a/meilisearch-types/Cargo.toml b/meilisearch-types/Cargo.toml index cbb380874..7c30a34c5 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -9,7 +9,7 @@ actix-web = { version = "4.2.1", default-features = false } anyhow = "1.0.65" convert_case = "0.6.0" csv = "1.1.6" -deserr = "0.1.4" +deserr = "0.3.0" either = { version = "1.6.1", features = ["serde"] } enum-iterator = "1.1.3" file-store = { path = "../file-store" } @@ -17,10 +17,9 @@ flate2 = "1.0.24" fst = "0.4.7" memmap2 = "0.5.7" milli = { path = "../milli", default-features = false } -proptest = { version = "1.0.0", optional = true } -proptest-derive = { version = "0.3.0", optional = true } roaring = { version = "0.10.0", features = ["serde"] } serde = { version = "1.0.145", features = ["derive"] } +serde-cs = "0.2.4" serde_json = "1.0.85" tar = "0.4.38" tempfile = "3.3.0" @@ -32,8 +31,6 @@ uuid = { version = "1.1.2", features = ["serde", "v4"] } [dev-dependencies] insta = "1.19.1" meili-snap = { path = "../meili-snap" } -proptest = "1.0.0" -proptest-derive = "0.3.0" [features] # all specialized tokenizations @@ -47,4 +44,3 @@ hebrew = ["milli/hebrew"] japanese = ["milli/japanese"] # thai specialized tokenization thai = ["milli/thai"] -test-traits = ["proptest", "proptest-derive"] diff --git a/meilisearch-types/src/deserr/error_messages.rs b/meilisearch-types/src/deserr/error_messages.rs new file mode 100644 index 000000000..7e288085d --- /dev/null +++ b/meilisearch-types/src/deserr/error_messages.rs @@ -0,0 +1,328 @@ +/*! +This module implements the error messages of deserialization errors. + +We try to: +1. Give a human-readable description of where the error originated. +2. Use the correct terms depending on the format of the request (json/query param) +3. Categorise the type of the error (e.g. missing field, wrong value type, unexpected error, etc.) + */ +use deserr::{ErrorKind, IntoValue, ValueKind, ValuePointerRef}; + +use super::{DeserrJsonError, DeserrQueryParamError}; +use crate::error::{Code, ErrorCode}; + +/// Return a description of the given location in a Json, preceded by the given article. +/// e.g. `at .key1[8].key2`. If the location is the origin, the given article will not be +/// included in the description. +pub fn location_json_description(location: ValuePointerRef, article: &str) -> String { + fn rec(location: ValuePointerRef) -> String { + match location { + ValuePointerRef::Origin => String::new(), + ValuePointerRef::Key { key, prev } => rec(*prev) + "." + key, + ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)), + } + } + match location { + ValuePointerRef::Origin => String::new(), + _ => { + format!("{article} `{}`", rec(location)) + } + } +} + +/// Return a description of the list of value kinds for a Json payload. +fn value_kinds_description_json(kinds: &[ValueKind]) -> String { + // Rank each value kind so that they can be sorted (and deduplicated) + // Having a predictable order helps with pattern matching + fn order(kind: &ValueKind) -> u8 { + match kind { + ValueKind::Null => 0, + ValueKind::Boolean => 1, + ValueKind::Integer => 2, + ValueKind::NegativeInteger => 3, + ValueKind::Float => 4, + ValueKind::String => 5, + ValueKind::Sequence => 6, + ValueKind::Map => 7, + } + } + // Return a description of a single value kind, preceded by an article + fn single_description(kind: &ValueKind) -> &'static str { + match kind { + ValueKind::Null => "null", + ValueKind::Boolean => "a boolean", + ValueKind::Integer => "a positive integer", + ValueKind::NegativeInteger => "a negative integer", + ValueKind::Float => "a number", + ValueKind::String => "a string", + ValueKind::Sequence => "an array", + ValueKind::Map => "an object", + } + } + + fn description_rec(kinds: &[ValueKind], count_items: &mut usize, message: &mut String) { + let (msg_part, rest): (_, &[ValueKind]) = match kinds { + [] => (String::new(), &[]), + [ValueKind::Integer | ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => { + ("a number".to_owned(), rest) + } + [ValueKind::Integer, ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => { + ("a number".to_owned(), rest) + } + [ValueKind::Integer, ValueKind::NegativeInteger, rest @ ..] => { + ("an integer".to_owned(), rest) + } + [a] => (single_description(a).to_owned(), &[]), + [a, rest @ ..] => (single_description(a).to_owned(), rest), + }; + + if rest.is_empty() { + if *count_items == 0 { + message.push_str(&msg_part); + } else if *count_items == 1 { + message.push_str(&format!(" or {msg_part}")); + } else { + message.push_str(&format!(", or {msg_part}")); + } + } else { + if *count_items == 0 { + message.push_str(&msg_part); + } else { + message.push_str(&format!(", {msg_part}")); + } + + *count_items += 1; + description_rec(rest, count_items, message); + } + } + + let mut kinds = kinds.to_owned(); + kinds.sort_by_key(order); + kinds.dedup(); + + if kinds.is_empty() { + // Should not happen ideally + "a different value".to_owned() + } else { + let mut message = String::new(); + description_rec(kinds.as_slice(), &mut 0, &mut message); + message + } +} + +/// Return the JSON string of the value preceded by a description of its kind +fn value_description_with_kind_json(v: &serde_json::Value) -> String { + match v.kind() { + ValueKind::Null => "null".to_owned(), + kind => { + format!( + "{}: `{}`", + value_kinds_description_json(&[kind]), + serde_json::to_string(v).unwrap() + ) + } + } +} + +impl deserr::DeserializeError for DeserrJsonError { + fn error( + _self_: Option, + error: deserr::ErrorKind, + location: ValuePointerRef, + ) -> Result { + let mut message = String::new(); + + message.push_str(&match error { + ErrorKind::IncorrectValueKind { actual, accepted } => { + let expected = value_kinds_description_json(accepted); + let received = value_description_with_kind_json(&serde_json::Value::from(actual)); + + let location = location_json_description(location, " at"); + + format!("Invalid value type{location}: expected {expected}, but found {received}") + } + ErrorKind::MissingField { field } => { + let location = location_json_description(location, " inside"); + format!("Missing field `{field}`{location}") + } + ErrorKind::UnknownKey { key, accepted } => { + let location = location_json_description(location, " inside"); + format!( + "Unknown field `{}`{location}: expected one of {}", + key, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", ") + ) + } + ErrorKind::UnknownValue { value, accepted } => { + let location = location_json_description(location, " at"); + format!( + "Unknown value `{}`{location}: expected one of {}", + value, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", "), + ) + } + ErrorKind::Unexpected { msg } => { + let location = location_json_description(location, " at"); + format!("Invalid value{location}: {msg}") + } + }); + + Err(DeserrJsonError::new(message, C::default().error_code())) + } +} + +pub fn immutable_field_error(field: &str, accepted: &[&str], code: Code) -> DeserrJsonError { + let msg = format!( + "Immutable field `{field}`: expected one of {}", + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", ") + ); + + DeserrJsonError::new(msg, code) +} + +/// Return a description of the given location in query parameters, preceded by the +/// given article. e.g. `at key5[2]`. If the location is the origin, the given article +/// will not be included in the description. +pub fn location_query_param_description(location: ValuePointerRef, article: &str) -> String { + fn rec(location: ValuePointerRef) -> String { + match location { + ValuePointerRef::Origin => String::new(), + ValuePointerRef::Key { key, prev } => { + if matches!(prev, ValuePointerRef::Origin) { + key.to_owned() + } else { + rec(*prev) + "." + key + } + } + ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)), + } + } + match location { + ValuePointerRef::Origin => String::new(), + _ => { + format!("{article} `{}`", rec(location)) + } + } +} + +impl deserr::DeserializeError for DeserrQueryParamError { + fn error( + _self_: Option, + error: deserr::ErrorKind, + location: ValuePointerRef, + ) -> Result { + let mut message = String::new(); + + message.push_str(&match error { + ErrorKind::IncorrectValueKind { actual, accepted } => { + let expected = value_kinds_description_query_param(accepted); + let received = value_description_with_kind_query_param(actual); + + let location = location_query_param_description(location, " for parameter"); + + format!("Invalid value type{location}: expected {expected}, but found {received}") + } + ErrorKind::MissingField { field } => { + let location = location_query_param_description(location, " inside"); + format!("Missing parameter `{field}`{location}") + } + ErrorKind::UnknownKey { key, accepted } => { + let location = location_query_param_description(location, " inside"); + format!( + "Unknown parameter `{}`{location}: expected one of {}", + key, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", ") + ) + } + ErrorKind::UnknownValue { value, accepted } => { + let location = location_query_param_description(location, " for parameter"); + format!( + "Unknown value `{}`{location}: expected one of {}", + value, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", "), + ) + } + ErrorKind::Unexpected { msg } => { + let location = location_query_param_description(location, " in parameter"); + format!("Invalid value{location}: {msg}") + } + }); + + Err(DeserrQueryParamError::new(message, C::default().error_code())) + } +} + +/// Return a description of the list of value kinds for query parameters +/// Since query parameters are always treated as strings, we always return +/// "a string" for now. +fn value_kinds_description_query_param(_accepted: &[ValueKind]) -> String { + "a string".to_owned() +} + +fn value_description_with_kind_query_param(actual: deserr::Value) -> String { + match actual { + deserr::Value::Null => "null".to_owned(), + deserr::Value::Boolean(x) => format!("a boolean: `{x}`"), + deserr::Value::Integer(x) => format!("an integer: `{x}`"), + deserr::Value::NegativeInteger(x) => { + format!("an integer: `{x}`") + } + deserr::Value::Float(x) => { + format!("a number: `{x}`") + } + deserr::Value::String(x) => { + format!("a string: `{x}`") + } + deserr::Value::Sequence(_) => "multiple values".to_owned(), + deserr::Value::Map(_) => "multiple parameters".to_owned(), + } +} + +#[cfg(test)] +mod tests { + use deserr::ValueKind; + + use crate::deserr::error_messages::value_kinds_description_json; + + #[test] + fn test_value_kinds_description_json() { + insta::assert_display_snapshot!(value_kinds_description_json(&[]), @"a different value"); + + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean]), @"a boolean"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::NegativeInteger]), @"a negative integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::String]), @"a string"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence]), @"an array"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Map]), @"an object"); + + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Boolean]), @"a boolean or a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Integer]), @"null or a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence, ValueKind::NegativeInteger]), @"a negative integer or an array"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float]), @"a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger]), @"a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null or a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number"); + } +} diff --git a/meilisearch-types/src/deserr/mod.rs b/meilisearch-types/src/deserr/mod.rs new file mode 100644 index 000000000..c15b2c3a0 --- /dev/null +++ b/meilisearch-types/src/deserr/mod.rs @@ -0,0 +1,134 @@ +use std::convert::Infallible; +use std::fmt; +use std::marker::PhantomData; + +use deserr::{DeserializeError, MergeWithError, ValuePointerRef}; + +use crate::error::deserr_codes::{self, *}; +use crate::error::{ + unwrap_any, Code, DeserrParseBoolError, DeserrParseIntError, ErrorCode, InvalidTaskDateError, + ParseOffsetDateTimeError, +}; +use crate::index_uid::IndexUidFormatError; +use crate::tasks::{ParseTaskKindError, ParseTaskStatusError}; + +pub mod error_messages; +pub mod query_params; + +/// Marker type for the Json format +pub struct DeserrJson; +/// Marker type for the Query Parameter format +pub struct DeserrQueryParam; + +pub type DeserrJsonError = DeserrError; +pub type DeserrQueryParamError = DeserrError; + +/// A request deserialization error. +/// +/// The first generic paramater is a marker type describing the format of the request: either json (e.g. [`DeserrJson`] or [`DeserrQueryParam`]). +/// The second generic parameter is the default error code for the deserialization error, in case it is not given. +pub struct DeserrError { + pub msg: String, + pub code: Code, + _phantom: PhantomData<(Format, C)>, +} +impl DeserrError { + pub fn new(msg: String, code: Code) -> Self { + Self { msg, code, _phantom: PhantomData } + } +} +impl std::fmt::Debug for DeserrError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish() + } +} + +impl std::fmt::Display for DeserrError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.msg) + } +} + +impl std::error::Error for DeserrError {} +impl ErrorCode for DeserrError { + fn error_code(&self) -> Code { + self.code + } +} + +// For now, we don't accumulate errors. Only one deserialisation error is ever returned at a time. +impl + MergeWithError> for DeserrError +{ + fn merge( + _self_: Option, + other: DeserrError, + _merge_location: ValuePointerRef, + ) -> Result { + Err(DeserrError { msg: other.msg, code: other.code, _phantom: PhantomData }) + } +} + +impl MergeWithError for DeserrError { + fn merge( + _self_: Option, + _other: Infallible, + _merge_location: ValuePointerRef, + ) -> Result { + unreachable!() + } +} + +// Implement a convenience function to build a `missing_field` error +macro_rules! make_missing_field_convenience_builder { + ($err_code:ident, $fn_name:ident) => { + impl DeserrJsonError<$err_code> { + pub fn $fn_name(field: &str, location: ValuePointerRef) -> Self { + let x = unwrap_any(Self::error::( + None, + deserr::ErrorKind::MissingField { field }, + location, + )); + Self { msg: x.msg, code: $err_code.error_code(), _phantom: PhantomData } + } + } + }; +} +make_missing_field_convenience_builder!(MissingIndexUid, missing_index_uid); +make_missing_field_convenience_builder!(MissingApiKeyActions, missing_api_key_actions); +make_missing_field_convenience_builder!(MissingApiKeyExpiresAt, missing_api_key_expires_at); +make_missing_field_convenience_builder!(MissingApiKeyIndexes, missing_api_key_indexes); +make_missing_field_convenience_builder!(MissingSwapIndexes, missing_swap_indexes); + +// Integrate a sub-error into a [`DeserrError`] by taking its error message but using +// the default error code (C) from `Self` +macro_rules! merge_with_error_impl_take_error_message { + ($err_type:ty) => { + impl MergeWithError<$err_type> for DeserrError + where + DeserrError: deserr::DeserializeError, + { + fn merge( + _self_: Option, + other: $err_type, + merge_location: ValuePointerRef, + ) -> Result { + DeserrError::::error::( + None, + deserr::ErrorKind::Unexpected { msg: other.to_string() }, + merge_location, + ) + } + } + }; +} + +// All these errors can be merged into a `DeserrError` +merge_with_error_impl_take_error_message!(DeserrParseIntError); +merge_with_error_impl_take_error_message!(DeserrParseBoolError); +merge_with_error_impl_take_error_message!(uuid::Error); +merge_with_error_impl_take_error_message!(InvalidTaskDateError); +merge_with_error_impl_take_error_message!(ParseOffsetDateTimeError); +merge_with_error_impl_take_error_message!(ParseTaskKindError); +merge_with_error_impl_take_error_message!(ParseTaskStatusError); +merge_with_error_impl_take_error_message!(IndexUidFormatError); diff --git a/meilisearch-types/src/deserr/query_params.rs b/meilisearch-types/src/deserr/query_params.rs new file mode 100644 index 000000000..28629aa1b --- /dev/null +++ b/meilisearch-types/src/deserr/query_params.rs @@ -0,0 +1,115 @@ +/*! +This module provides helper traits, types, and functions to deserialize query parameters. + +The source of the problem is that query parameters only give us a string to work with. +This means `deserr` is never given a sequence or numbers, and thus the default deserialization +code for common types such as `usize` or `Vec` does not work. To work around it, we create a +wrapper type called `Param`, which is deserialised using the `from_query_param` method of the trait +`FromQueryParameter`. + +We also use other helper types such as `CS` (i.e. comma-separated) from `serde_cs` as well as +`StarOr`, `OptionStarOr`, and `OptionStarOrList`. +*/ + +use std::convert::Infallible; +use std::ops::Deref; +use std::str::FromStr; + +use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind}; + +use super::{DeserrParseBoolError, DeserrParseIntError}; +use crate::error::unwrap_any; +use crate::index_uid::IndexUid; +use crate::tasks::{Kind, Status}; + +/// A wrapper type indicating that the inner value should be +/// deserialised from a query parameter string. +/// +/// Note that if the field is optional, it is better to use +/// `Option>` instead of `Param>`. +#[derive(Default, Debug, Clone, Copy)] +pub struct Param(pub T); + +impl Deref for Param { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DeserializeFromValue for Param +where + E: DeserializeError + MergeWithError, + T: FromQueryParameter, +{ + fn deserialize_from_value( + value: deserr::Value, + location: deserr::ValuePointerRef, + ) -> Result { + match value { + deserr::Value::String(s) => match T::from_query_param(&s) { + Ok(x) => Ok(Param(x)), + Err(e) => Err(unwrap_any(E::merge(None, e, location))), + }, + _ => Err(unwrap_any(E::error( + None, + deserr::ErrorKind::IncorrectValueKind { + actual: value, + accepted: &[ValueKind::String], + }, + location, + ))), + } + } +} + +/// Parse a value from a query parameter string. +/// +/// This trait is functionally equivalent to `FromStr`. +/// Having a separate trait trait allows us to return better +/// deserializatio error messages. +pub trait FromQueryParameter: Sized { + type Err; + fn from_query_param(p: &str) -> Result; +} + +/// Implement `FromQueryParameter` for the given type using its `FromStr` +/// trait implementation. +macro_rules! impl_from_query_param_from_str { + ($type:ty) => { + impl FromQueryParameter for $type { + type Err = <$type as FromStr>::Err; + fn from_query_param(p: &str) -> Result { + p.parse() + } + } + }; +} +impl_from_query_param_from_str!(Kind); +impl_from_query_param_from_str!(Status); +impl_from_query_param_from_str!(IndexUid); + +/// Implement `FromQueryParameter` for the given type using its `FromStr` +/// trait implementation, replacing the returned error with a struct +/// that wraps the original query parameter. +macro_rules! impl_from_query_param_wrap_original_value_in_error { + ($type:ty, $err_type:path) => { + impl FromQueryParameter for $type { + type Err = $err_type; + fn from_query_param(p: &str) -> Result { + p.parse().map_err(|_| $err_type(p.to_owned())) + } + } + }; +} +impl_from_query_param_wrap_original_value_in_error!(usize, DeserrParseIntError); +impl_from_query_param_wrap_original_value_in_error!(u32, DeserrParseIntError); +impl_from_query_param_wrap_original_value_in_error!(bool, DeserrParseBoolError); + +impl FromQueryParameter for String { + type Err = Infallible; + fn from_query_param(p: &str) -> Result { + Ok(p.to_owned()) + } +} diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index bc29f9e82..39d9a1551 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -1,23 +1,16 @@ -use std::convert::Infallible; -use std::marker::PhantomData; use std::{fmt, io}; use actix_web::http::StatusCode; use actix_web::{self as aweb, HttpResponseBuilder}; use aweb::rt::task::JoinError; use convert_case::Casing; -use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef}; use milli::heed::{Error as HeedError, MdbError}; use serde::{Deserialize, Serialize}; -use self::deserr_codes::MissingIndexUid; - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] pub struct ResponseError { #[serde(skip)] - #[cfg_attr(feature = "test-traits", proptest(strategy = "strategy::status_code_strategy()"))] code: StatusCode, message: String, #[serde(rename = "code")] @@ -36,7 +29,7 @@ impl ResponseError { Self { code: code.http(), message, - error_code: code.err_code().error_name, + error_code: code.name(), error_type: code.type_(), error_link: code.url(), } @@ -97,9 +90,9 @@ pub trait ErrorCode { #[allow(clippy::enum_variant_names)] enum ErrorType { - InternalError, - InvalidRequestError, - AuthenticationError, + Internal, + InvalidRequest, + Auth, System, } @@ -108,14 +101,24 @@ impl fmt::Display for ErrorType { use ErrorType::*; match self { - InternalError => write!(f, "internal"), - InvalidRequestError => write!(f, "invalid_request"), - AuthenticationError => write!(f, "auth"), + Internal => write!(f, "internal"), + InvalidRequest => write!(f, "invalid_request"), + Auth => write!(f, "auth"), System => write!(f, "system"), } } } +/// Implement all the error codes. +/// +/// 1. Make an enum `Code` where each error code is a variant +/// 2. Implement the `http`, `name`, and `type_` method on the enum +/// 3. Make a unit type for each error code in the module `deserr_codes`. +/// +/// The unit type's purpose is to be used as a marker type parameter, e.g. +/// `DeserrJsonError`. It implements `Default` and `ErrorCode`, +/// so we can get a value of the `Code` enum with the correct variant by calling +/// `MyErrorCode::default().error_code()`. macro_rules! make_error_codes { ($($code_ident:ident, $err_type:ident, $status:ident);*) => { #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -123,37 +126,36 @@ macro_rules! make_error_codes { $($code_ident),* } impl Code { - /// associate a `Code` variant to the actual ErrCode - fn err_code(&self) -> ErrCode { - match self { - $( - Code::$code_ident => { - ErrCode::$err_type( stringify!($code_ident).to_case(convert_case::Case::Snake), StatusCode::$status) - } - )* - } - } /// return the HTTP status code associated with the `Code` fn http(&self) -> StatusCode { - self.err_code().status_code + match self { + $( + Code::$code_ident => StatusCode::$status + ),* + } } /// return error name, used as error code fn name(&self) -> String { - self.err_code().error_name.to_string() + match self { + $( + Code::$code_ident => stringify!($code_ident).to_case(convert_case::Case::Snake) + ),* + } } /// return the error type fn type_(&self) -> String { - self.err_code().error_type.to_string() + match self { + $( + Code::$code_ident => ErrorType::$err_type.to_string() + ),* + } } /// return the doc url associated with the error fn url(&self) -> String { - format!( - "https://docs.meilisearch.com/errors#{}", - self.name().to_case(convert_case::Case::Kebab) - ) + format!("https://docs.meilisearch.com/errors#{}", self.name()) } } pub mod deserr_codes { @@ -170,146 +172,120 @@ macro_rules! make_error_codes { } } } + +// An exhaustive list of all the error codes used by meilisearch. make_error_codes! { -ApiKeyAlreadyExists , invalid , CONFLICT ; -ApiKeyNotFound , invalid , NOT_FOUND ; -BadParameter , invalid , BAD_REQUEST; -BadRequest , invalid , BAD_REQUEST; -DatabaseSizeLimitReached , internal , INTERNAL_SERVER_ERROR; -DocumentNotFound , invalid , NOT_FOUND; -DumpAlreadyProcessing , invalid , CONFLICT; -DumpNotFound , invalid , NOT_FOUND; -DumpProcessFailed , internal , INTERNAL_SERVER_ERROR; -DuplicateIndexFound , invalid , BAD_REQUEST; - -ImmutableApiKeyUid , invalid , BAD_REQUEST; -ImmutableApiKeyKey , invalid , BAD_REQUEST; -ImmutableApiKeyActions , invalid , BAD_REQUEST; -ImmutableApiKeyIndexes , invalid , BAD_REQUEST; -ImmutableApiKeyExpiresAt , invalid , BAD_REQUEST; -ImmutableApiKeyCreatedAt , invalid , BAD_REQUEST; -ImmutableApiKeyUpdatedAt , invalid , BAD_REQUEST; - -ImmutableIndexUid , invalid , BAD_REQUEST; -ImmutableIndexCreatedAt , invalid , BAD_REQUEST; -ImmutableIndexUpdatedAt , invalid , BAD_REQUEST; - -IndexAlreadyExists , invalid , CONFLICT ; -IndexCreationFailed , internal , INTERNAL_SERVER_ERROR; -IndexNotFound , invalid , NOT_FOUND; -IndexPrimaryKeyAlreadyExists , invalid , BAD_REQUEST ; -IndexPrimaryKeyNoCandidateFound , invalid , BAD_REQUEST ; -IndexPrimaryKeyMultipleCandidatesFound, invalid , BAD_REQUEST; -Internal , internal , INTERNAL_SERVER_ERROR ; -InvalidApiKeyActions , invalid , BAD_REQUEST ; -InvalidApiKeyDescription , invalid , BAD_REQUEST ; -InvalidApiKeyExpiresAt , invalid , BAD_REQUEST ; -InvalidApiKeyIndexes , invalid , BAD_REQUEST ; -InvalidApiKeyLimit , invalid , BAD_REQUEST ; -InvalidApiKeyName , invalid , BAD_REQUEST ; -InvalidApiKeyOffset , invalid , BAD_REQUEST ; -InvalidApiKeyUid , invalid , BAD_REQUEST ; -InvalidApiKey , authentication, FORBIDDEN ; -InvalidContentType , invalid , UNSUPPORTED_MEDIA_TYPE ; -InvalidDocumentFields , invalid , BAD_REQUEST ; -InvalidDocumentGeoField , invalid , BAD_REQUEST ; -InvalidDocumentId , invalid , BAD_REQUEST ; -InvalidDocumentLimit , invalid , BAD_REQUEST ; -InvalidDocumentOffset , invalid , BAD_REQUEST ; -InvalidIndexLimit , invalid , BAD_REQUEST ; -InvalidIndexOffset , invalid , BAD_REQUEST ; -InvalidIndexPrimaryKey , invalid , BAD_REQUEST ; -InvalidIndexUid , invalid , BAD_REQUEST ; -InvalidMinWordLengthForTypo , invalid , BAD_REQUEST ; -InvalidSearchAttributesToCrop , invalid , BAD_REQUEST ; -InvalidSearchAttributesToHighlight , invalid , BAD_REQUEST ; -InvalidSearchAttributesToRetrieve , invalid , BAD_REQUEST ; -InvalidSearchCropLength , invalid , BAD_REQUEST ; -InvalidSearchCropMarker , invalid , BAD_REQUEST ; -InvalidSearchFacets , invalid , BAD_REQUEST ; -InvalidSearchFilter , invalid , BAD_REQUEST ; -InvalidSearchHighlightPostTag , invalid , BAD_REQUEST ; -InvalidSearchHighlightPreTag , invalid , BAD_REQUEST ; -InvalidSearchHitsPerPage , invalid , BAD_REQUEST ; -InvalidSearchLimit , invalid , BAD_REQUEST ; -InvalidSearchMatchingStrategy , invalid , BAD_REQUEST ; -InvalidSearchOffset , invalid , BAD_REQUEST ; -InvalidSearchPage , invalid , BAD_REQUEST ; -InvalidSearchQ , invalid , BAD_REQUEST ; -InvalidSearchShowMatchesPosition , invalid , BAD_REQUEST ; -InvalidSearchSort , invalid , BAD_REQUEST ; -InvalidSettingsDisplayedAttributes , invalid , BAD_REQUEST ; -InvalidSettingsDistinctAttribute , invalid , BAD_REQUEST ; -InvalidSettingsFaceting , invalid , BAD_REQUEST ; -InvalidSettingsFilterableAttributes , invalid , BAD_REQUEST ; -InvalidSettingsPagination , invalid , BAD_REQUEST ; -InvalidSettingsRankingRules , invalid , BAD_REQUEST ; -InvalidSettingsSearchableAttributes , invalid , BAD_REQUEST ; -InvalidSettingsSortableAttributes , invalid , BAD_REQUEST ; -InvalidSettingsStopWords , invalid , BAD_REQUEST ; -InvalidSettingsSynonyms , invalid , BAD_REQUEST ; -InvalidSettingsTypoTolerance , invalid , BAD_REQUEST ; -InvalidState , internal , INTERNAL_SERVER_ERROR ; -InvalidStoreFile , internal , INTERNAL_SERVER_ERROR ; -InvalidSwapDuplicateIndexFound , invalid , BAD_REQUEST ; -InvalidSwapIndexes , invalid , BAD_REQUEST ; -InvalidTaskAfterEnqueuedAt , invalid , BAD_REQUEST ; -InvalidTaskAfterFinishedAt , invalid , BAD_REQUEST ; -InvalidTaskAfterStartedAt , invalid , BAD_REQUEST ; -InvalidTaskBeforeEnqueuedAt , invalid , BAD_REQUEST ; -InvalidTaskBeforeFinishedAt , invalid , BAD_REQUEST ; -InvalidTaskBeforeStartedAt , invalid , BAD_REQUEST ; -InvalidTaskCanceledBy , invalid , BAD_REQUEST ; -InvalidTaskFrom , invalid , BAD_REQUEST ; -InvalidTaskLimit , invalid , BAD_REQUEST ; -InvalidTaskStatuses , invalid , BAD_REQUEST ; -InvalidTaskTypes , invalid , BAD_REQUEST ; -InvalidTaskUids , invalid , BAD_REQUEST ; -IoError , system , UNPROCESSABLE_ENTITY; -MalformedPayload , invalid , BAD_REQUEST ; -MaxFieldsLimitExceeded , invalid , BAD_REQUEST ; -MissingApiKeyActions , invalid , BAD_REQUEST ; -MissingApiKeyExpiresAt , invalid , BAD_REQUEST ; -MissingApiKeyIndexes , invalid , BAD_REQUEST ; -MissingAuthorizationHeader , authentication, UNAUTHORIZED ; -MissingContentType , invalid , UNSUPPORTED_MEDIA_TYPE ; -MissingDocumentId , invalid , BAD_REQUEST ; -MissingIndexUid , invalid , BAD_REQUEST ; -MissingMasterKey , authentication, UNAUTHORIZED ; -MissingPayload , invalid , BAD_REQUEST ; -MissingTaskFilters , invalid , BAD_REQUEST ; -NoSpaceLeftOnDevice , system , UNPROCESSABLE_ENTITY; -PayloadTooLarge , invalid , PAYLOAD_TOO_LARGE ; -TaskNotFound , invalid , NOT_FOUND ; -TooManyOpenFiles , system , UNPROCESSABLE_ENTITY ; -UnretrievableDocument , internal , BAD_REQUEST ; -UnretrievableErrorCode , invalid , BAD_REQUEST ; -UnsupportedMediaType , invalid , UNSUPPORTED_MEDIA_TYPE -} - -/// Internal structure providing a convenient way to create error codes -struct ErrCode { - status_code: StatusCode, - error_type: ErrorType, - error_name: String, -} - -impl ErrCode { - fn authentication(error_name: String, status_code: StatusCode) -> ErrCode { - ErrCode { status_code, error_name, error_type: ErrorType::AuthenticationError } - } - - fn internal(error_name: String, status_code: StatusCode) -> ErrCode { - ErrCode { status_code, error_name, error_type: ErrorType::InternalError } - } - - fn invalid(error_name: String, status_code: StatusCode) -> ErrCode { - ErrCode { status_code, error_name, error_type: ErrorType::InvalidRequestError } - } - - fn system(error_name: String, status_code: StatusCode) -> ErrCode { - ErrCode { status_code, error_name, error_type: ErrorType::System } - } +ApiKeyAlreadyExists , InvalidRequest , CONFLICT ; +ApiKeyNotFound , InvalidRequest , NOT_FOUND ; +BadParameter , InvalidRequest , BAD_REQUEST; +BadRequest , InvalidRequest , BAD_REQUEST; +DatabaseSizeLimitReached , Internal , INTERNAL_SERVER_ERROR; +DocumentNotFound , InvalidRequest , NOT_FOUND; +DumpAlreadyProcessing , InvalidRequest , CONFLICT; +DumpNotFound , InvalidRequest , NOT_FOUND; +DumpProcessFailed , Internal , INTERNAL_SERVER_ERROR; +DuplicateIndexFound , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyActions , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyCreatedAt , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyExpiresAt , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyIndexes , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyKey , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyUid , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyUpdatedAt , InvalidRequest , BAD_REQUEST; +ImmutableIndexCreatedAt , InvalidRequest , BAD_REQUEST; +ImmutableIndexUid , InvalidRequest , BAD_REQUEST; +ImmutableIndexUpdatedAt , InvalidRequest , BAD_REQUEST; +IndexAlreadyExists , InvalidRequest , CONFLICT ; +IndexCreationFailed , Internal , INTERNAL_SERVER_ERROR; +IndexNotFound , InvalidRequest , NOT_FOUND; +IndexPrimaryKeyAlreadyExists , InvalidRequest , BAD_REQUEST ; +IndexPrimaryKeyMultipleCandidatesFound, InvalidRequest , BAD_REQUEST; +IndexPrimaryKeyNoCandidateFound , InvalidRequest , BAD_REQUEST ; +Internal , Internal , INTERNAL_SERVER_ERROR ; +InvalidApiKey , Auth , FORBIDDEN ; +InvalidApiKeyActions , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyDescription , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyIndexes , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyLimit , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyName , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyOffset , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyUid , InvalidRequest , BAD_REQUEST ; +InvalidContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ; +InvalidDocumentFields , InvalidRequest , BAD_REQUEST ; +InvalidDocumentGeoField , InvalidRequest , BAD_REQUEST ; +InvalidDocumentId , InvalidRequest , BAD_REQUEST ; +InvalidDocumentLimit , InvalidRequest , BAD_REQUEST ; +InvalidDocumentOffset , InvalidRequest , BAD_REQUEST ; +InvalidIndexLimit , InvalidRequest , BAD_REQUEST ; +InvalidIndexOffset , InvalidRequest , BAD_REQUEST ; +InvalidIndexPrimaryKey , InvalidRequest , BAD_REQUEST ; +InvalidIndexUid , InvalidRequest , BAD_REQUEST ; +InvalidSearchAttributesToCrop , InvalidRequest , BAD_REQUEST ; +InvalidSearchAttributesToHighlight , InvalidRequest , BAD_REQUEST ; +InvalidSearchAttributesToRetrieve , InvalidRequest , BAD_REQUEST ; +InvalidSearchCropLength , InvalidRequest , BAD_REQUEST ; +InvalidSearchCropMarker , InvalidRequest , BAD_REQUEST ; +InvalidSearchFacets , InvalidRequest , BAD_REQUEST ; +InvalidSearchFilter , InvalidRequest , BAD_REQUEST ; +InvalidSearchHighlightPostTag , InvalidRequest , BAD_REQUEST ; +InvalidSearchHighlightPreTag , InvalidRequest , BAD_REQUEST ; +InvalidSearchHitsPerPage , InvalidRequest , BAD_REQUEST ; +InvalidSearchLimit , InvalidRequest , BAD_REQUEST ; +InvalidSearchMatchingStrategy , InvalidRequest , BAD_REQUEST ; +InvalidSearchOffset , InvalidRequest , BAD_REQUEST ; +InvalidSearchPage , InvalidRequest , BAD_REQUEST ; +InvalidSearchQ , InvalidRequest , BAD_REQUEST ; +InvalidSearchShowMatchesPosition , InvalidRequest , BAD_REQUEST ; +InvalidSearchSort , InvalidRequest , BAD_REQUEST ; +InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ; +InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ; +InvalidSettingsFaceting , InvalidRequest , BAD_REQUEST ; +InvalidSettingsFilterableAttributes , InvalidRequest , BAD_REQUEST ; +InvalidSettingsPagination , InvalidRequest , BAD_REQUEST ; +InvalidSettingsRankingRules , InvalidRequest , BAD_REQUEST ; +InvalidSettingsSearchableAttributes , InvalidRequest , BAD_REQUEST ; +InvalidSettingsSortableAttributes , InvalidRequest , BAD_REQUEST ; +InvalidSettingsStopWords , InvalidRequest , BAD_REQUEST ; +InvalidSettingsSynonyms , InvalidRequest , BAD_REQUEST ; +InvalidSettingsTypoTolerance , InvalidRequest , BAD_REQUEST ; +InvalidState , Internal , INTERNAL_SERVER_ERROR ; +InvalidStoreFile , Internal , INTERNAL_SERVER_ERROR ; +InvalidSwapDuplicateIndexFound , InvalidRequest , BAD_REQUEST ; +InvalidSwapIndexes , InvalidRequest , BAD_REQUEST ; +InvalidTaskAfterEnqueuedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskAfterFinishedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskAfterStartedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskBeforeEnqueuedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskBeforeFinishedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskBeforeStartedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskCanceledBy , InvalidRequest , BAD_REQUEST ; +InvalidTaskFrom , InvalidRequest , BAD_REQUEST ; +InvalidTaskLimit , InvalidRequest , BAD_REQUEST ; +InvalidTaskStatuses , InvalidRequest , BAD_REQUEST ; +InvalidTaskTypes , InvalidRequest , BAD_REQUEST ; +InvalidTaskUids , InvalidRequest , BAD_REQUEST ; +IoError , System , UNPROCESSABLE_ENTITY; +MalformedPayload , InvalidRequest , BAD_REQUEST ; +MaxFieldsLimitExceeded , InvalidRequest , BAD_REQUEST ; +MissingApiKeyActions , InvalidRequest , BAD_REQUEST ; +MissingApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ; +MissingApiKeyIndexes , InvalidRequest , BAD_REQUEST ; +MissingAuthorizationHeader , Auth , UNAUTHORIZED ; +MissingContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ; +MissingDocumentId , InvalidRequest , BAD_REQUEST ; +MissingIndexUid , InvalidRequest , BAD_REQUEST ; +MissingMasterKey , Auth , UNAUTHORIZED ; +MissingPayload , InvalidRequest , BAD_REQUEST ; +MissingSwapIndexes , InvalidRequest , BAD_REQUEST ; +MissingTaskFilters , InvalidRequest , BAD_REQUEST ; +NoSpaceLeftOnDevice , System , UNPROCESSABLE_ENTITY; +PayloadTooLarge , InvalidRequest , PAYLOAD_TOO_LARGE ; +TaskNotFound , InvalidRequest , NOT_FOUND ; +TooManyOpenFiles , System , UNPROCESSABLE_ENTITY ; +UnretrievableDocument , Internal , BAD_REQUEST ; +UnretrievableErrorCode , InvalidRequest , BAD_REQUEST ; +UnsupportedMediaType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE } impl ErrorCode for JoinError { @@ -348,13 +324,13 @@ impl ErrorCode for milli::Error { } UserError::PrimaryKeyCannotBeChanged(_) => Code::IndexPrimaryKeyAlreadyExists, UserError::SortRankingRuleMissing => Code::InvalidSearchSort, - UserError::InvalidFacetsDistribution { .. } => Code::BadRequest, + UserError::InvalidFacetsDistribution { .. } => Code::InvalidSearchFacets, UserError::InvalidSortableAttribute { .. } => Code::InvalidSearchSort, UserError::CriterionError(_) => Code::InvalidSettingsRankingRules, UserError::InvalidGeoField { .. } => Code::InvalidDocumentGeoField, UserError::SortError(_) => Code::InvalidSearchSort, UserError::InvalidMinTypoWordLenSetting(_, _) => { - Code::InvalidMinWordLengthForTypo + Code::InvalidSettingsTypoTolerance } } } @@ -367,6 +343,7 @@ impl ErrorCode for file_store::Error { match self { Self::IoError(e) => e.error_code(), Self::PersistError(e) => e.error_code(), + Self::CouldNotParseFileNameAsUtf8 | Self::UuidError(_) => Code::Internal, } } } @@ -404,6 +381,7 @@ impl ErrorCode for io::Error { } } +/// Unwrap a result, either its Ok or Err value. pub fn unwrap_any(any: Result) -> T { match any { Ok(any) => any, @@ -411,90 +389,41 @@ pub fn unwrap_any(any: Result) -> T { } } -#[cfg(feature = "test-traits")] -mod strategy { - use proptest::strategy::Strategy; - - use super::*; - - pub(super) fn status_code_strategy() -> impl Strategy { - (100..999u16).prop_map(|i| StatusCode::from_u16(i).unwrap()) - } -} - -pub struct DeserrError { - pub msg: String, - pub code: Code, - _phantom: PhantomData, -} -impl std::fmt::Debug for DeserrError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish() - } -} - -impl std::fmt::Display for DeserrError { +/// Deserialization when `deserr` cannot parse an API key date. +#[derive(Debug)] +pub struct ParseOffsetDateTimeError(pub String); +impl fmt::Display for ParseOffsetDateTimeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.msg) + writeln!(f, "`{original}` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", original = self.0) } } -impl std::error::Error for DeserrError {} -impl ErrorCode for DeserrError { - fn error_code(&self) -> Code { - self.code +/// Deserialization when `deserr` cannot parse a task date. +#[derive(Debug)] +pub struct InvalidTaskDateError(pub String); +impl std::fmt::Display for InvalidTaskDateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "`{}` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", self.0) } } -impl MergeWithError> for DeserrError { - fn merge( - _self_: Option, - other: DeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(DeserrError { msg: other.msg, code: other.code, _phantom: PhantomData }) +/// Deserialization error when `deserr` cannot parse a String +/// into a bool. +#[derive(Debug)] +pub struct DeserrParseBoolError(pub String); +impl fmt::Display for DeserrParseBoolError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "could not parse `{}` as a boolean, expected either `true` or `false`", self.0) } } -impl DeserrError { - pub fn missing_index_uid(field: &str, location: ValuePointerRef) -> Self { - let x = unwrap_any(Self::error::( - None, - deserr::ErrorKind::MissingField { field }, - location, - )); - Self { msg: x.msg, code: MissingIndexUid.error_code(), _phantom: PhantomData } - } -} - -impl deserr::DeserializeError for DeserrError { - fn error( - _self_: Option, - error: deserr::ErrorKind, - location: ValuePointerRef, - ) -> Result { - let msg = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - Err(DeserrError { msg, code: C::default().error_code(), _phantom: PhantomData }) - } -} - -pub struct TakeErrorMessage(pub T); - -impl MergeWithError> for DeserrError -where - T: std::error::Error, -{ - fn merge( - _self_: Option, - other: TakeErrorMessage, - merge_location: ValuePointerRef, - ) -> Result { - DeserrError::error::( - None, - deserr::ErrorKind::Unexpected { msg: other.0.to_string() }, - merge_location, - ) +/// Deserialization error when `deserr` cannot parse a String +/// into an integer. +#[derive(Debug)] +pub struct DeserrParseIntError(pub String); +impl fmt::Display for DeserrParseIntError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "could not parse `{}` as a positive integer", self.0) } } diff --git a/meilisearch-types/src/index_uid.rs b/meilisearch-types/src/index_uid.rs index 945a57e9e..2f3f6e5df 100644 --- a/meilisearch-types/src/index_uid.rs +++ b/meilisearch-types/src/index_uid.rs @@ -2,17 +2,15 @@ use std::error::Error; use std::fmt; use std::str::FromStr; -use serde::{Deserialize, Serialize}; +use deserr::DeserializeFromValue; use crate::error::{Code, ErrorCode}; /// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400 /// bytes long -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] -pub struct IndexUid( - #[cfg_attr(feature = "test-traits", proptest(regex("[a-zA-Z0-9_-]{1,400}")))] String, -); +#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromValue)] +#[deserr(from(String) = IndexUid::try_from -> IndexUidFormatError)] +pub struct IndexUid(String); impl IndexUid { pub fn new_unchecked(s: impl AsRef) -> Self { @@ -29,6 +27,12 @@ impl IndexUid { } } +impl fmt::Display for IndexUid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + impl std::ops::Deref for IndexUid { type Target = str; diff --git a/meilisearch-types/src/keys.rs b/meilisearch-types/src/keys.rs index b41bb06b6..31561c848 100644 --- a/meilisearch-types/src/keys.rs +++ b/meilisearch-types/src/keys.rs @@ -1,54 +1,39 @@ use std::convert::Infallible; -use std::fmt::Display; use std::hash::Hash; +use std::str::FromStr; -use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValuePointerRef}; +use deserr::{DeserializeError, DeserializeFromValue, ValuePointerRef}; use enum_iterator::Sequence; +use milli::update::Setting; use serde::{Deserialize, Serialize}; use time::format_description::well_known::Rfc3339; use time::macros::{format_description, time}; use time::{Date, OffsetDateTime, PrimitiveDateTime}; use uuid::Uuid; +use crate::deserr::error_messages::immutable_field_error; +use crate::deserr::DeserrJsonError; use crate::error::deserr_codes::*; -use crate::error::{unwrap_any, Code, DeserrError, ErrorCode, TakeErrorMessage}; -use crate::index_uid::{IndexUid, IndexUidFormatError}; +use crate::error::{unwrap_any, Code, ParseOffsetDateTimeError}; +use crate::index_uid::IndexUid; use crate::star_or::StarOr; pub type KeyId = Uuid; -impl MergeWithError for DeserrError { - fn merge( - _self_: Option, - other: IndexUidFormatError, - merge_location: deserr::ValuePointerRef, - ) -> std::result::Result { - DeserrError::error::( - None, - deserr::ErrorKind::Unexpected { msg: other.to_string() }, - merge_location, - ) - } -} - -fn parse_uuid_from_str(s: &str) -> Result> { - Uuid::parse_str(s).map_err(TakeErrorMessage) -} - #[derive(Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct CreateApiKey { - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub description: Option, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub name: Option, - #[deserr(default = Uuid::new_v4(), error = DeserrError, from(&String) = parse_uuid_from_str -> TakeErrorMessage)] + #[deserr(default = Uuid::new_v4(), error = DeserrJsonError, from(&String) = Uuid::from_str -> uuid::Error)] pub uid: KeyId, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_api_key_actions)] pub actions: Vec, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_api_key_indexes)] pub indexes: Vec>, - #[deserr(error = DeserrError, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage)] + #[deserr(error = DeserrJsonError, from(Option) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)] pub expires_at: Option, } impl CreateApiKey { @@ -72,32 +57,29 @@ fn deny_immutable_fields_api_key( field: &str, accepted: &[&str], location: ValuePointerRef, -) -> DeserrError { - let mut error = unwrap_any(DeserrError::::error::( - None, - deserr::ErrorKind::UnknownKey { key: field, accepted }, - location, - )); - - error.code = match field { - "uid" => Code::ImmutableApiKeyUid, - "actions" => Code::ImmutableApiKeyActions, - "indexes" => Code::ImmutableApiKeyIndexes, - "expiresAt" => Code::ImmutableApiKeyExpiresAt, - "createdAt" => Code::ImmutableApiKeyCreatedAt, - "updatedAt" => Code::ImmutableApiKeyUpdatedAt, - _ => Code::BadRequest, - }; - error +) -> DeserrJsonError { + match field { + "uid" => immutable_field_error(field, accepted, Code::ImmutableApiKeyUid), + "actions" => immutable_field_error(field, accepted, Code::ImmutableApiKeyActions), + "indexes" => immutable_field_error(field, accepted, Code::ImmutableApiKeyIndexes), + "expiresAt" => immutable_field_error(field, accepted, Code::ImmutableApiKeyExpiresAt), + "createdAt" => immutable_field_error(field, accepted, Code::ImmutableApiKeyCreatedAt), + "updatedAt" => immutable_field_error(field, accepted, Code::ImmutableApiKeyUpdatedAt), + _ => unwrap_any(DeserrJsonError::::error::( + None, + deserr::ErrorKind::UnknownKey { key: field, accepted }, + location, + )), + } } #[derive(Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)] pub struct PatchApiKey { - #[deserr(error = DeserrError)] - pub description: Option, - #[deserr(error = DeserrError)] - pub name: Option, + #[deserr(default, error = DeserrJsonError)] + pub description: Setting, + #[deserr(default, error = DeserrJsonError)] + pub name: Setting, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] @@ -149,46 +131,40 @@ impl Key { } } -#[derive(Debug)] -pub struct ParseOffsetDateTimeError(String); -impl Display for ParseOffsetDateTimeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "`{original}` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", original = self.0) - } -} -impl std::error::Error for ParseOffsetDateTimeError {} - fn parse_expiration_date( - string: &str, -) -> std::result::Result, TakeErrorMessage> { - let datetime = if let Ok(datetime) = OffsetDateTime::parse(string, &Rfc3339) { + string: Option, +) -> std::result::Result, ParseOffsetDateTimeError> { + let Some(string) = string else { + return Ok(None) + }; + let datetime = if let Ok(datetime) = OffsetDateTime::parse(&string, &Rfc3339) { datetime } else if let Ok(primitive_datetime) = PrimitiveDateTime::parse( - string, + &string, format_description!( "[year repr:full base:calendar]-[month repr:numerical]-[day]T[hour]:[minute]:[second]" ), ) { primitive_datetime.assume_utc() } else if let Ok(primitive_datetime) = PrimitiveDateTime::parse( - string, + &string, format_description!( "[year repr:full base:calendar]-[month repr:numerical]-[day] [hour]:[minute]:[second]" ), ) { primitive_datetime.assume_utc() } else if let Ok(date) = Date::parse( - string, + &string, format_description!("[year repr:full base:calendar]-[month repr:numerical]-[day]"), ) { PrimitiveDateTime::new(date, time!(00:00)).assume_utc() } else { - return Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned()))); + return Err(ParseOffsetDateTimeError(string)); }; if datetime > OffsetDateTime::now_utc() { Ok(Some(datetime)) } else { - Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned()))) + Err(ParseOffsetDateTimeError(string)) } } diff --git a/meilisearch-types/src/lib.rs b/meilisearch-types/src/lib.rs index c7f7ca7f5..14602b5aa 100644 --- a/meilisearch-types/src/lib.rs +++ b/meilisearch-types/src/lib.rs @@ -1,4 +1,5 @@ pub mod compression; +pub mod deserr; pub mod document_formats; pub mod error; pub mod index_uid; @@ -7,11 +8,10 @@ pub mod settings; pub mod star_or; pub mod tasks; pub mod versioning; - -pub use milli; pub use milli::{heed, Index}; use uuid::Uuid; pub use versioning::VERSION_FILE_NAME; +pub use {milli, serde_cs}; pub type Document = serde_json::Map; pub type InstanceUid = Uuid; diff --git a/meilisearch-types/src/settings.rs b/meilisearch-types/src/settings.rs index 3169920a6..b4ab1eff6 100644 --- a/meilisearch-types/src/settings.rs +++ b/meilisearch-types/src/settings.rs @@ -11,8 +11,9 @@ use milli::update::Setting; use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET}; use serde::{Deserialize, Serialize, Serializer}; +use crate::deserr::DeserrJsonError; use crate::error::deserr_codes::*; -use crate::error::{unwrap_any, DeserrError}; +use crate::error::unwrap_any; /// The maximimum number of results that the engine /// will be able to return in one search call. @@ -66,26 +67,31 @@ fn validate_min_word_size_for_typo_setting( #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] #[serde(deny_unknown_fields, rename_all = "camelCase")] -#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrError)] +#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError)] pub struct MinWordSizeTyposSetting { #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub one_typo: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub two_typos: Setting, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] #[serde(deny_unknown_fields, rename_all = "camelCase")] -#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError>)] +#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError>)] pub struct TypoSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub enabled: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub min_word_size_for_typos: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub disable_on_words: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub disable_on_attributes: Setting>, } @@ -94,6 +100,7 @@ pub struct TypoSettings { #[deserr(rename_all = camelCase, deny_unknown_fields)] pub struct FacetingSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub max_values_per_facet: Setting, } @@ -102,10 +109,11 @@ pub struct FacetingSettings { #[deserr(rename_all = camelCase, deny_unknown_fields)] pub struct PaginationSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub max_total_hits: Setting, } -impl MergeWithError for DeserrError { +impl MergeWithError for DeserrJsonError { fn merge( _self_: Option, other: milli::CriterionError, @@ -128,14 +136,14 @@ impl MergeWithError for DeserrError { #[serde( default, serialize_with = "serialize_with_wildcard", skip_serializing_if = "Setting::is_not_set" )] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub displayed_attributes: Setting>, #[serde( @@ -143,35 +151,35 @@ pub struct Settings { serialize_with = "serialize_with_wildcard", skip_serializing_if = "Setting::is_not_set" )] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub searchable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub filterable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub sortable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub ranking_rules: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub stop_words: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub synonyms: Setting>>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub distinct_attribute: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub typo_tolerance: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub faceting: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub pagination: Setting, #[serde(skip)] diff --git a/meilisearch-types/src/star_or.rs b/meilisearch-types/src/star_or.rs index f56c30b4e..135f610c4 100644 --- a/meilisearch-types/src/star_or.rs +++ b/meilisearch-types/src/star_or.rs @@ -1,12 +1,12 @@ -use std::fmt::{Display, Formatter}; +use std::fmt; use std::marker::PhantomData; -use std::ops::Deref; use std::str::FromStr; use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind}; use serde::de::Visitor; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use crate::deserr::query_params::FromQueryParameter; use crate::error::unwrap_any; /// A type that tries to match either a star (*) or @@ -17,35 +17,6 @@ pub enum StarOr { Other(T), } -impl DeserializeFromValue for StarOr -where - T: FromStr, - E: MergeWithError, -{ - fn deserialize_from_value( - value: deserr::Value, - location: deserr::ValuePointerRef, - ) -> Result { - match value { - deserr::Value::String(v) => match v.as_str() { - "*" => Ok(StarOr::Star), - v => match FromStr::from_str(v) { - Ok(x) => Ok(StarOr::Other(x)), - Err(e) => Err(unwrap_any(E::merge(None, e, location))), - }, - }, - _ => Err(unwrap_any(E::error::( - None, - deserr::ErrorKind::IncorrectValueKind { - actual: value, - accepted: &[ValueKind::String], - }, - location, - ))), - } - } -} - impl FromStr for StarOr { type Err = T::Err; @@ -57,23 +28,11 @@ impl FromStr for StarOr { } } } - -impl> Deref for StarOr { - type Target = str; - - fn deref(&self) -> &Self::Target { +impl fmt::Display for StarOr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Star => "*", - Self::Other(t) => t.deref(), - } - } -} - -impl> From> for String { - fn from(s: StarOr) -> Self { - match s { - StarOr::Star => "*".to_string(), - StarOr::Other(t) => t.into(), + StarOr::Star => write!(f, "*"), + StarOr::Other(x) => fmt::Display::fmt(x, f), } } } @@ -93,7 +52,7 @@ impl Eq for StarOr {} impl<'de, T, E> Deserialize<'de> for StarOr where T: FromStr, - E: Display, + E: fmt::Display, { fn deserialize(deserializer: D) -> Result where @@ -109,11 +68,11 @@ where impl<'de, T, FE> Visitor<'de> for StarOrVisitor where T: FromStr, - FE: Display, + FE: fmt::Display, { type Value = StarOr; - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + fn expecting(&self, formatter: &mut fmt::Formatter) -> std::fmt::Result { formatter.write_str("a string") } @@ -139,7 +98,7 @@ where impl Serialize for StarOr where - T: Deref, + T: ToString, { fn serialize(&self, serializer: S) -> Result where @@ -147,7 +106,222 @@ where { match self { StarOr::Star => serializer.serialize_str("*"), - StarOr::Other(other) => serializer.serialize_str(other.deref()), + StarOr::Other(other) => serializer.serialize_str(&other.to_string()), + } + } +} + +impl DeserializeFromValue for StarOr +where + T: FromStr, + E: DeserializeError + MergeWithError, +{ + fn deserialize_from_value( + value: deserr::Value, + location: deserr::ValuePointerRef, + ) -> Result { + match value { + deserr::Value::String(v) => { + if v == "*" { + Ok(StarOr::Star) + } else { + match T::from_str(&v) { + Ok(parsed) => Ok(StarOr::Other(parsed)), + Err(e) => Err(unwrap_any(E::merge(None, e, location))), + } + } + } + _ => Err(unwrap_any(E::error::( + None, + deserr::ErrorKind::IncorrectValueKind { + actual: value, + accepted: &[ValueKind::String], + }, + location, + ))), + } + } +} + +/// A type representing the content of a query parameter that can either not exist, +/// be equal to a star (*), or another value +/// +/// It is a convenient alternative to `Option>`. +#[derive(Debug, Default, Clone, Copy)] +pub enum OptionStarOr { + #[default] + None, + Star, + Other(T), +} + +impl OptionStarOr { + pub fn is_some(&self) -> bool { + match self { + Self::None => false, + Self::Star => false, + Self::Other(_) => true, + } + } + pub fn merge_star_and_none(self) -> Option { + match self { + Self::None | Self::Star => None, + Self::Other(x) => Some(x), + } + } + pub fn try_map Result>(self, map_f: F) -> Result, E> { + match self { + OptionStarOr::None => Ok(OptionStarOr::None), + OptionStarOr::Star => Ok(OptionStarOr::Star), + OptionStarOr::Other(x) => map_f(x).map(OptionStarOr::Other), + } + } +} + +impl FromQueryParameter for OptionStarOr +where + T: FromQueryParameter, +{ + type Err = T::Err; + fn from_query_param(p: &str) -> Result { + match p { + "*" => Ok(OptionStarOr::Star), + s => T::from_query_param(s).map(OptionStarOr::Other), + } + } +} + +impl DeserializeFromValue for OptionStarOr +where + E: DeserializeError + MergeWithError, + T: FromQueryParameter, +{ + fn deserialize_from_value( + value: deserr::Value, + location: deserr::ValuePointerRef, + ) -> Result { + match value { + deserr::Value::String(s) => match s.as_str() { + "*" => Ok(OptionStarOr::Star), + s => match T::from_query_param(s) { + Ok(x) => Ok(OptionStarOr::Other(x)), + Err(e) => Err(unwrap_any(E::merge(None, e, location))), + }, + }, + _ => Err(unwrap_any(E::error::( + None, + deserr::ErrorKind::IncorrectValueKind { + actual: value, + accepted: &[ValueKind::String], + }, + location, + ))), + } + } +} + +/// A type representing the content of a query parameter that can either not exist, be equal to a star (*), or represent a list of other values +#[derive(Debug, Default, Clone)] +pub enum OptionStarOrList { + #[default] + None, + Star, + List(Vec), +} + +impl OptionStarOrList { + pub fn is_some(&self) -> bool { + match self { + Self::None => false, + Self::Star => false, + Self::List(_) => true, + } + } + pub fn map U>(self, map_f: F) -> OptionStarOrList { + match self { + Self::None => OptionStarOrList::None, + Self::Star => OptionStarOrList::Star, + Self::List(xs) => OptionStarOrList::List(xs.into_iter().map(map_f).collect()), + } + } + pub fn try_map Result>( + self, + map_f: F, + ) -> Result, E> { + match self { + Self::None => Ok(OptionStarOrList::None), + Self::Star => Ok(OptionStarOrList::Star), + Self::List(xs) => { + xs.into_iter().map(map_f).collect::, _>>().map(OptionStarOrList::List) + } + } + } + pub fn merge_star_and_none(self) -> Option> { + match self { + Self::None | Self::Star => None, + Self::List(xs) => Some(xs), + } + } + pub fn push(&mut self, el: T) { + match self { + Self::None => *self = Self::List(vec![el]), + Self::Star => (), + Self::List(xs) => xs.push(el), + } + } +} + +impl DeserializeFromValue for OptionStarOrList +where + E: DeserializeError + MergeWithError, + T: FromQueryParameter, +{ + fn deserialize_from_value( + value: deserr::Value, + location: deserr::ValuePointerRef, + ) -> Result { + match value { + deserr::Value::String(s) => { + let mut error = None; + let mut is_star = false; + // CS::::from_str is infaillible + let cs = serde_cs::vec::CS::::from_str(&s).unwrap(); + let len_cs = cs.0.len(); + let mut els = vec![]; + for (i, el_str) in cs.into_iter().enumerate() { + if el_str == "*" { + is_star = true; + } else { + match T::from_query_param(&el_str) { + Ok(el) => { + els.push(el); + } + Err(e) => { + let location = + if len_cs > 1 { location.push_index(i) } else { location }; + error = Some(E::merge(error, e, location)?); + } + } + } + } + if let Some(error) = error { + return Err(error); + } + + if is_star { + Ok(OptionStarOrList::Star) + } else { + Ok(OptionStarOrList::List(els)) + } + } + _ => Err(unwrap_any(E::error::( + None, + deserr::ErrorKind::IncorrectValueKind { + actual: value, + accepted: &[ValueKind::String], + }, + location, + ))), } } } diff --git a/meilisearch-types/src/tasks.rs b/meilisearch-types/src/tasks.rs index ceddbd51c..3b7efb97b 100644 --- a/meilisearch-types/src/tasks.rs +++ b/meilisearch-types/src/tasks.rs @@ -1,3 +1,4 @@ +use core::fmt; use std::collections::HashSet; use std::fmt::{Display, Write}; use std::str::FromStr; @@ -9,7 +10,7 @@ use serde::{Deserialize, Serialize, Serializer}; use time::{Duration, OffsetDateTime}; use uuid::Uuid; -use crate::error::{Code, ResponseError}; +use crate::error::ResponseError; use crate::keys::Key; use crate::settings::{Settings, Unchecked}; use crate::InstanceUid; @@ -332,7 +333,7 @@ impl Display for Status { } impl FromStr for Status { - type Err = ResponseError; + type Err = ParseTaskStatusError; fn from_str(status: &str) -> Result { if status.eq_ignore_ascii_case("enqueued") { @@ -346,21 +347,28 @@ impl FromStr for Status { } else if status.eq_ignore_ascii_case("canceled") { Ok(Status::Canceled) } else { - Err(ResponseError::from_msg( - format!( - "`{}` is not a status. Available status are {}.", - status, - enum_iterator::all::() - .map(|s| format!("`{s}`")) - .collect::>() - .join(", ") - ), - Code::BadRequest, - )) + Err(ParseTaskStatusError(status.to_owned())) } } } +#[derive(Debug)] +pub struct ParseTaskStatusError(pub String); +impl fmt::Display for ParseTaskStatusError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "`{}` is not a valid task status. Available statuses are {}.", + self.0, + enum_iterator::all::() + .map(|s| format!("`{s}`")) + .collect::>() + .join(", ") + ) + } +} +impl std::error::Error for ParseTaskStatusError {} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence)] #[serde(rename_all = "camelCase")] pub enum Kind { @@ -412,7 +420,7 @@ impl Display for Kind { } } impl FromStr for Kind { - type Err = ResponseError; + type Err = ParseTaskKindError; fn from_str(kind: &str) -> Result { if kind.eq_ignore_ascii_case("indexCreation") { @@ -438,25 +446,32 @@ impl FromStr for Kind { } else if kind.eq_ignore_ascii_case("snapshotCreation") { Ok(Kind::SnapshotCreation) } else { - Err(ResponseError::from_msg( - format!( - "`{}` is not a type. Available types are {}.", - kind, - enum_iterator::all::() - .map(|k| format!( - "`{}`", - // by default serde is going to insert `"` around the value. - serde_json::to_string(&k).unwrap().trim_matches('"') - )) - .collect::>() - .join(", ") - ), - Code::BadRequest, - )) + Err(ParseTaskKindError(kind.to_owned())) } } } +#[derive(Debug)] +pub struct ParseTaskKindError(pub String); +impl fmt::Display for ParseTaskKindError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "`{}` is not a valid task type. Available types are {}.", + self.0, + enum_iterator::all::() + .map(|k| format!( + "`{}`", + // by default serde is going to insert `"` around the value. + serde_json::to_string(&k).unwrap().trim_matches('"') + )) + .collect::>() + .join(", ") + ) + } +} +impl std::error::Error for ParseTaskKindError {} + #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub enum Details { DocumentAdditionOrUpdate { received_documents: u64, indexed_documents: Option }, diff --git a/meilisearch/Cargo.toml b/meilisearch/Cargo.toml index a42e5cc7b..0c95142aa 100644 --- a/meilisearch/Cargo.toml +++ b/meilisearch/Cargo.toml @@ -19,7 +19,7 @@ byte-unit = { version = "4.0.14", default-features = false, features = ["std", " bytes = "1.2.1" clap = { version = "4.0.9", features = ["derive", "env"] } crossbeam-channel = "0.5.6" -deserr = "0.1.4" +deserr = "0.3.0" dump = { path = "../dump" } either = "1.8.0" env_logger = "0.9.1" @@ -55,7 +55,6 @@ rustls = "0.20.6" rustls-pemfile = "1.0.1" segment = { version = "0.2.1", optional = true } serde = { version = "1.0.145", features = ["derive"] } -serde-cs = "0.2.4" serde_json = { version = "1.0.85", features = ["preserve_order"] } sha2 = "0.10.6" siphasher = "0.3.10" @@ -74,6 +73,8 @@ walkdir = "2.3.2" yaup = "0.2.0" serde_urlencoded = "0.7.1" actix-utils = "3.0.1" +atty = "0.2.14" +termcolor = "1.1.3" [dev-dependencies] actix-rt = "2.7.0" diff --git a/meilisearch/src/analytics/segment_analytics.rs b/meilisearch/src/analytics/segment_analytics.rs index 1b5a1d73f..21b6696e7 100644 --- a/meilisearch/src/analytics/segment_analytics.rs +++ b/meilisearch/src/analytics/segment_analytics.rs @@ -9,7 +9,7 @@ use actix_web::HttpRequest; use byte_unit::Byte; use http::header::CONTENT_TYPE; use index_scheduler::IndexScheduler; -use meilisearch_auth::SearchRules; +use meilisearch_auth::{AuthController, SearchRules}; use meilisearch_types::InstanceUid; use once_cell::sync::Lazy; use regex::Regex; @@ -82,7 +82,11 @@ pub struct SegmentAnalytics { } impl SegmentAnalytics { - pub async fn new(opt: &Opt, index_scheduler: Arc) -> Arc { + pub async fn new( + opt: &Opt, + index_scheduler: Arc, + auth_controller: AuthController, + ) -> Arc { let instance_uid = super::find_user_id(&opt.db_path); let first_time_run = instance_uid.is_none(); let instance_uid = instance_uid.unwrap_or_else(|| Uuid::new_v4()); @@ -136,7 +140,7 @@ impl SegmentAnalytics { get_tasks_aggregator: TasksAggregator::default(), health_aggregator: HealthAggregator::default(), }); - tokio::spawn(segment.run(index_scheduler.clone())); + tokio::spawn(segment.run(index_scheduler.clone(), auth_controller.clone())); let this = Self { instance_uid, sender, user: user.clone() }; @@ -361,7 +365,7 @@ impl Segment { }) } - async fn run(mut self, index_scheduler: Arc) { + async fn run(mut self, index_scheduler: Arc, auth_controller: AuthController) { const INTERVAL: Duration = Duration::from_secs(60 * 60); // one hour // The first batch must be sent after one hour. let mut interval = @@ -370,7 +374,7 @@ impl Segment { loop { select! { _ = interval.tick() => { - self.tick(index_scheduler.clone()).await; + self.tick(index_scheduler.clone(), auth_controller.clone()).await; }, msg = self.inbox.recv() => { match msg { @@ -389,8 +393,14 @@ impl Segment { } } - async fn tick(&mut self, index_scheduler: Arc) { - if let Ok(stats) = create_all_stats(index_scheduler.into(), &SearchRules::default()) { + async fn tick( + &mut self, + index_scheduler: Arc, + auth_controller: AuthController, + ) { + if let Ok(stats) = + create_all_stats(index_scheduler.into(), auth_controller, &SearchRules::default()) + { let _ = self .batcher .push(Identify { diff --git a/meilisearch/src/error.rs b/meilisearch/src/error.rs index 23a101080..fcfe4b7fc 100644 --- a/meilisearch/src/error.rs +++ b/meilisearch/src/error.rs @@ -2,7 +2,7 @@ use actix_web as aweb; use aweb::error::{JsonPayloadError, QueryPayloadError}; use meilisearch_types::document_formats::{DocumentFormatError, PayloadType}; use meilisearch_types::error::{Code, ErrorCode, ResponseError}; -use meilisearch_types::index_uid::IndexUidFormatError; +use meilisearch_types::index_uid::{IndexUid, IndexUidFormatError}; use serde_json::Value; use tokio::task::JoinError; @@ -24,10 +24,10 @@ pub enum MeilisearchHttpError { MissingPayload(PayloadType), #[error("The provided payload reached the size limit.")] PayloadTooLarge, - #[error("Two indexes must be given for each swap. The list `{:?}` contains {} indexes.", - .0, .0.len() + #[error("Two indexes must be given for each swap. The list `[{}]` contains {} indexes.", + .0.iter().map(|uid| format!("\"{uid}\"")).collect::>().join(", "), .0.len() )] - SwapIndexPayloadWrongLength(Vec), + SwapIndexPayloadWrongLength(Vec), #[error(transparent)] IndexUid(#[from] IndexUidFormatError), #[error(transparent)] diff --git a/meilisearch/src/main.rs b/meilisearch/src/main.rs index 050c825a8..b78362ec1 100644 --- a/meilisearch/src/main.rs +++ b/meilisearch/src/main.rs @@ -1,4 +1,5 @@ use std::env; +use std::io::Write; use std::path::PathBuf; use std::sync::Arc; @@ -9,6 +10,7 @@ use index_scheduler::IndexScheduler; use meilisearch::analytics::Analytics; use meilisearch::{analytics, create_app, setup_meilisearch, Opt}; use meilisearch_auth::{generate_master_key, AuthController, MASTER_KEY_MIN_SIZE}; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; #[global_allocator] static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; @@ -32,24 +34,19 @@ async fn main() -> anyhow::Result<()> { match (opt.env.as_ref(), &opt.master_key) { ("production", Some(master_key)) if master_key.len() < MASTER_KEY_MIN_SIZE => { anyhow::bail!( - "In production mode, the master key must be of at least {MASTER_KEY_MIN_SIZE} bytes, but the provided key is only {} bytes long + "The master key must be at least {MASTER_KEY_MIN_SIZE} bytes in a production environment. The provided key is only {} bytes. -We generated a secure master key for you (you can safely copy this token): - ->> export MEILI_MASTER_KEY={} <<", +{}", master_key.len(), - generate_master_key(), + generated_master_key_message(), ) } ("production", None) => { anyhow::bail!( - "In production mode, you must provide a master key to secure your instance. It can be specified via the MEILI_MASTER_KEY environment variable or the --master-key launch option. + "You must provide a master key to secure your instance in a production environment. It can be specified via the MEILI_MASTER_KEY environment variable or the --master-key launch option. -We generated a secure master key for you (you can safely copy this token): - ->> export MEILI_MASTER_KEY={} << -", - generate_master_key() +{}", + generated_master_key_message() ) } // No error; continue @@ -60,7 +57,8 @@ We generated a secure master key for you (you can safely copy this token): #[cfg(all(not(debug_assertions), feature = "analytics"))] let analytics = if !opt.no_analytics { - analytics::SegmentAnalytics::new(&opt, index_scheduler.clone()).await + analytics::SegmentAnalytics::new(&opt, index_scheduler.clone(), auth_controller.clone()) + .await } else { analytics::MockAnalytics::new(&opt) }; @@ -147,7 +145,7 @@ pub fn print_launch_resume( " Thank you for using Meilisearch! -We collect anonymized analytics to improve our product and your experience. To learn more, including how to turn off analytics, visit our dedicated documentation page: https://docs.meilisearch.com/learn/what_is_meilisearch/telemetry.html +\nWe collect anonymized analytics to improve our product and your experience. To learn more, including how to turn off analytics, visit our dedicated documentation page: https://docs.meilisearch.com/learn/what_is_meilisearch/telemetry.html Anonymous telemetry:\t\"Enabled\"" ); @@ -170,16 +168,10 @@ Anonymous telemetry:\t\"Enabled\"" eprintln!("A master key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key."); if master_key.len() < MASTER_KEY_MIN_SIZE { - eprintln!(); - log::warn!("The provided master key is too short (< {MASTER_KEY_MIN_SIZE} bytes)"); - eprintln!("A master key of at least {MASTER_KEY_MIN_SIZE} bytes will be required when switching to the production environment."); + print_master_key_too_short_warning() } } - ("development", None) => { - log::warn!("No master key found; The server will accept unidentified requests"); - eprintln!("If you need some protection in development mode, please export a key:\n\nexport MEILI_MASTER_KEY={}", generate_master_key()); - eprintln!("\nA master key of at least {MASTER_KEY_MIN_SIZE} bytes will be required when switching to the production environment."); - } + ("development", None) => print_missing_master_key_warning(), // unreachable because Opt::try_build above would have failed already if any other value had been produced _ => unreachable!(), } @@ -190,3 +182,67 @@ Anonymous telemetry:\t\"Enabled\"" eprintln!("Contact:\t\thttps://docs.meilisearch.com/resources/contact.html"); eprintln!(); } + +const WARNING_BG_COLOR: Option = Some(Color::Ansi256(178)); +const WARNING_FG_COLOR: Option = Some(Color::Ansi256(0)); + +fn print_master_key_too_short_warning() { + let choice = + if atty::is(atty::Stream::Stderr) { ColorChoice::Auto } else { ColorChoice::Never }; + let mut stderr = StandardStream::stderr(choice); + stderr + .set_color( + ColorSpec::new().set_bg(WARNING_BG_COLOR).set_fg(WARNING_FG_COLOR).set_bold(true), + ) + .unwrap(); + writeln!(stderr, "\n").unwrap(); + writeln!( + stderr, + " Meilisearch started with a master key considered unsafe for use in a production environment. + + A master key of at least {MASTER_KEY_MIN_SIZE} bytes will be required when switching to a production environment." + ) + .unwrap(); + stderr.reset().unwrap(); + writeln!(stderr).unwrap(); + + eprintln!("\n{}", generated_master_key_message()); + eprintln!( + "\nRestart Meilisearch with the argument above to use this new and secure master key." + ) +} + +fn print_missing_master_key_warning() { + let choice = + if atty::is(atty::Stream::Stderr) { ColorChoice::Auto } else { ColorChoice::Never }; + let mut stderr = StandardStream::stderr(choice); + stderr + .set_color( + ColorSpec::new().set_bg(WARNING_BG_COLOR).set_fg(WARNING_FG_COLOR).set_bold(true), + ) + .unwrap(); + writeln!(stderr, "\n").unwrap(); + writeln!( + stderr, + " No master key was found. The server will accept unidentified requests. + + A master key of at least {MASTER_KEY_MIN_SIZE} bytes will be required when switching to a production environment." +) +.unwrap(); + stderr.reset().unwrap(); + writeln!(stderr).unwrap(); + + eprintln!("\n{}", generated_master_key_message()); + eprintln!( + "\nRestart Meilisearch with the argument above to use this new and secure master key." + ) +} + +fn generated_master_key_message() -> String { + format!( + "We generated a new secure master key for you (you can safely use this token): + +>> --master-key {} <<", + generate_master_key() + ) +} diff --git a/meilisearch/src/routes/api_key.rs b/meilisearch/src/routes/api_key.rs index 76912bbaa..cd5bfe0c7 100644 --- a/meilisearch/src/routes/api_key.rs +++ b/meilisearch/src/routes/api_key.rs @@ -4,14 +4,15 @@ use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use meilisearch_auth::error::AuthControllerError; use meilisearch_auth::AuthController; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{Code, DeserrError, ResponseError, TakeErrorMessage}; +use meilisearch_types::error::{Code, ResponseError}; use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; -use super::indexes::search::parse_usize_take_error_message; use super::PAGINATION_DEFAULT_LIMIT; use crate::extractors::authentication::policies::*; use crate::extractors::authentication::GuardedData; @@ -36,7 +37,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { pub async fn create_api_key( auth_controller: GuardedData, AuthController>, - body: ValidatedJson, + body: ValidatedJson, _req: HttpRequest, ) -> Result { let v = body.into_inner(); @@ -50,26 +51,23 @@ pub async fn create_api_key( Ok(HttpResponse::Created().json(res)) } -#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[derive(DeserializeFromValue, Debug, Clone, Copy)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct ListApiKeys { - #[serde(default)] - #[deserr(error = DeserrError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - pub offset: usize, - #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(error = DeserrError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - pub limit: usize, + #[deserr(default, error = DeserrQueryParamError)] + pub offset: Param, + #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError)] + pub limit: Param, } impl ListApiKeys { fn as_pagination(self) -> Pagination { - Pagination { offset: self.offset, limit: self.limit } + Pagination { offset: self.offset.0, limit: self.limit.0 } } } pub async fn list_api_keys( auth_controller: GuardedData, AuthController>, - list_api_keys: QueryParameter, + list_api_keys: QueryParameter, ) -> Result { let paginate = list_api_keys.into_inner().as_pagination(); let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { @@ -106,7 +104,7 @@ pub async fn get_api_key( pub async fn patch_api_key( auth_controller: GuardedData, AuthController>, - body: ValidatedJson, + body: ValidatedJson, path: web::Path, ) -> Result { let key = path.into_inner().key; @@ -172,7 +170,7 @@ impl KeyView { key: generated_key, uid: key.uid, actions: key.actions, - indexes: key.indexes.into_iter().map(String::from).collect(), + indexes: key.indexes.into_iter().map(|x| x.to_string()).collect(), expires_at: key.expires_at, created_at: key.created_at, updated_at: key.updated_at, diff --git a/meilisearch/src/routes/indexes/documents.rs b/meilisearch/src/routes/indexes/documents.rs index 3a54bba6a..0ec1057ae 100644 --- a/meilisearch/src/routes/indexes/documents.rs +++ b/meilisearch/src/routes/indexes/documents.rs @@ -1,5 +1,4 @@ use std::io::ErrorKind; -use std::num::ParseIntError; use actix_web::http::header::CONTENT_TYPE; use actix_web::web::Data; @@ -9,25 +8,25 @@ use deserr::DeserializeFromValue; use futures::StreamExt; use index_scheduler::IndexScheduler; use log::debug; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; use meilisearch_types::document_formats::{read_csv, read_json, read_ndjson, PayloadType}; use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; +use meilisearch_types::error::ResponseError; use meilisearch_types::heed::RoTxn; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::update::IndexDocumentsMethod; -use meilisearch_types::star_or::StarOr; +use meilisearch_types::star_or::OptionStarOrList; use meilisearch_types::tasks::KindWithContent; use meilisearch_types::{milli, Document, Index}; use mime::Mime; use once_cell::sync::Lazy; use serde::Deserialize; -use serde_cs::vec::CS; use serde_json::Value; use tempfile::tempfile; use tokio::fs::File; use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter}; -use super::search::parse_usize_take_error_message; use crate::analytics::{Analytics, DocumentDeletionKind}; use crate::error::MeilisearchHttpError; use crate::error::PayloadError::ReceivePayload; @@ -36,7 +35,7 @@ use crate::extractors::authentication::GuardedData; use crate::extractors::payload::Payload; use crate::extractors::query_parameters::QueryParameter; use crate::extractors::sequential_extractor::SeqHandler; -use crate::routes::{fold_star_or, PaginationView, SummarizedTaskView}; +use crate::routes::{PaginationView, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT}; static ACCEPTED_CONTENT_TYPE: Lazy> = Lazy::new(|| { vec!["application/json".to_string(), "application/x-ndjson".to_string(), "text/csv".to_string()] @@ -81,23 +80,26 @@ pub fn configure(cfg: &mut web::ServiceConfig) { ); } -#[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[derive(Debug, DeserializeFromValue)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct GetDocument { - #[deserr(error = DeserrError)] - fields: Option>>, + #[deserr(default, error = DeserrQueryParamError)] + fields: OptionStarOrList, } pub async fn get_document( index_scheduler: GuardedData, Data>, - path: web::Path, - params: QueryParameter, + document_param: web::Path, + params: QueryParameter, ) -> Result { - let GetDocument { fields } = params.into_inner(); - let attributes_to_retrieve = fields.and_then(fold_star_or); + let DocumentParam { index_uid, document_id } = document_param.into_inner(); + let index_uid = IndexUid::try_from(index_uid)?; - let index = index_scheduler.index(&path.index_uid)?; - let document = retrieve_document(&index, &path.document_id, attributes_to_retrieve)?; + let GetDocument { fields } = params.into_inner(); + let attributes_to_retrieve = fields.merge_star_and_none(); + + let index = index_scheduler.index(&index_uid)?; + let document = retrieve_document(&index, &document_id, attributes_to_retrieve)?; debug!("returns: {:?}", document); Ok(HttpResponse::Ok().json(document)) } @@ -108,60 +110,68 @@ pub async fn delete_document( req: HttpRequest, analytics: web::Data, ) -> Result { + let DocumentParam { index_uid, document_id } = path.into_inner(); + let index_uid = IndexUid::try_from(index_uid)?; + analytics.delete_documents(DocumentDeletionKind::PerDocumentId, &req); - let DocumentParam { document_id, index_uid } = path.into_inner(); - let task = KindWithContent::DocumentDeletion { index_uid, documents_ids: vec![document_id] }; + let task = KindWithContent::DocumentDeletion { + index_uid: index_uid.to_string(), + documents_ids: vec![document_id], + }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); debug!("returns: {:?}", task); Ok(HttpResponse::Accepted().json(task)) } -#[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[derive(Debug, DeserializeFromValue)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct BrowseQuery { - #[deserr(error = DeserrError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - offset: usize, - #[deserr(error = DeserrError, default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - limit: usize, - #[deserr(error = DeserrError)] - fields: Option>>, + #[deserr(default, error = DeserrQueryParamError)] + offset: Param, + #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError)] + limit: Param, + #[deserr(default, error = DeserrQueryParamError)] + fields: OptionStarOrList, } pub async fn get_all_documents( index_scheduler: GuardedData, Data>, index_uid: web::Path, - params: QueryParameter, + params: QueryParameter, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; debug!("called with params: {:?}", params); let BrowseQuery { limit, offset, fields } = params.into_inner(); - let attributes_to_retrieve = fields.and_then(fold_star_or); + let attributes_to_retrieve = fields.merge_star_and_none(); let index = index_scheduler.index(&index_uid)?; - let (total, documents) = retrieve_documents(&index, offset, limit, attributes_to_retrieve)?; + let (total, documents) = retrieve_documents(&index, offset.0, limit.0, attributes_to_retrieve)?; - let ret = PaginationView::new(offset, limit, total as usize, documents); + let ret = PaginationView::new(offset.0, limit.0, total as usize, documents); debug!("returns: {:?}", ret); Ok(HttpResponse::Ok().json(ret)) } #[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct UpdateDocumentsQuery { - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub primary_key: Option, } pub async fn add_documents( index_scheduler: GuardedData, Data>, index_uid: web::Path, - params: QueryParameter, + params: QueryParameter, body: Payload, req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + debug!("called with params: {:?}", params); let params = params.into_inner(); @@ -171,7 +181,7 @@ pub async fn add_documents( let task = document_addition( extract_mime_type(&req)?, index_scheduler, - index_uid.into_inner(), + index_uid, params.primary_key, body, IndexDocumentsMethod::ReplaceDocuments, @@ -184,14 +194,15 @@ pub async fn add_documents( pub async fn update_documents( index_scheduler: GuardedData, Data>, - path: web::Path, - params: QueryParameter, + index_uid: web::Path, + params: QueryParameter, body: Payload, req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + debug!("called with params: {:?}", params); - let index_uid = path.into_inner(); analytics.update_documents(¶ms, index_scheduler.index(&index_uid).is_err(), &req); @@ -213,7 +224,7 @@ pub async fn update_documents( async fn document_addition( mime_type: Option, index_scheduler: GuardedData, Data>, - index_uid: String, + index_uid: IndexUid, primary_key: Option, mut body: Payload, method: IndexDocumentsMethod, @@ -234,9 +245,6 @@ async fn document_addition( } }; - // is your indexUid valid? - let index_uid = IndexUid::try_from(index_uid)?.into_inner(); - let (uuid, mut update_file) = index_scheduler.create_update_file()?; let temp_file = match tempfile() { @@ -312,7 +320,7 @@ async fn document_addition( documents_count, primary_key, allow_index_creation, - index_uid, + index_uid: index_uid.to_string(), }; let scheduler = index_scheduler.clone(); @@ -330,12 +338,13 @@ async fn document_addition( pub async fn delete_documents( index_scheduler: GuardedData, Data>, - path: web::Path, + index_uid: web::Path, body: web::Json>, req: HttpRequest, analytics: web::Data, ) -> Result { debug!("called with params: {:?}", body); + let index_uid = IndexUid::try_from(index_uid.into_inner())?; analytics.delete_documents(DocumentDeletionKind::PerBatch, &req); @@ -345,7 +354,7 @@ pub async fn delete_documents( .collect(); let task = - KindWithContent::DocumentDeletion { index_uid: path.into_inner(), documents_ids: ids }; + KindWithContent::DocumentDeletion { index_uid: index_uid.to_string(), documents_ids: ids }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); @@ -355,13 +364,14 @@ pub async fn delete_documents( pub async fn clear_all_documents( index_scheduler: GuardedData, Data>, - path: web::Path, + index_uid: web::Path, req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; analytics.delete_documents(DocumentDeletionKind::ClearAll, &req); - let task = KindWithContent::DocumentClear { index_uid: path.into_inner() }; + let task = KindWithContent::DocumentClear { index_uid: index_uid.to_string() }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); diff --git a/meilisearch/src/routes/indexes/mod.rs b/meilisearch/src/routes/indexes/mod.rs index 7a3a97c1f..2d352bfe5 100644 --- a/meilisearch/src/routes/indexes/mod.rs +++ b/meilisearch/src/routes/indexes/mod.rs @@ -5,16 +5,18 @@ use actix_web::{web, HttpRequest, HttpResponse}; use deserr::{DeserializeError, DeserializeFromValue, ValuePointerRef}; use index_scheduler::IndexScheduler; use log::debug; +use meilisearch_types::deserr::error_messages::immutable_field_error; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{unwrap_any, Code, DeserrError, ResponseError, TakeErrorMessage}; +use meilisearch_types::error::{unwrap_any, Code, ResponseError}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::{self, FieldDistribution, Index}; use meilisearch_types::tasks::KindWithContent; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use serde_json::json; use time::OffsetDateTime; -use self::search::parse_usize_take_error_message; use super::{Pagination, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT}; use crate::analytics::Analytics; use crate::extractors::authentication::policies::*; @@ -48,7 +50,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { ); } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct IndexView { pub uid: String, @@ -71,26 +73,23 @@ impl IndexView { } } -#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[derive(DeserializeFromValue, Debug, Clone, Copy)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct ListIndexes { - #[serde(default)] - #[deserr(error = DeserrError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - pub offset: usize, - #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(error = DeserrError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - pub limit: usize, + #[deserr(default, error = DeserrQueryParamError)] + pub offset: Param, + #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError)] + pub limit: Param, } impl ListIndexes { fn as_pagination(self) -> Pagination { - Pagination { offset: self.offset, limit: self.limit } + Pagination { offset: self.offset.0, limit: self.limit.0 } } } pub async fn list_indexes( index_scheduler: GuardedData, Data>, - paginate: QueryParameter, + paginate: QueryParameter, ) -> Result { let search_rules = &index_scheduler.filters().search_rules; let indexes: Vec<_> = index_scheduler.indexes()?; @@ -107,22 +106,21 @@ pub async fn list_indexes( } #[derive(DeserializeFromValue, Debug)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct IndexCreateRequest { - #[deserr(error = DeserrError, missing_field_error = DeserrError::missing_index_uid)] - uid: String, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_index_uid)] + uid: IndexUid, + #[deserr(default, error = DeserrJsonError)] primary_key: Option, } pub async fn create_index( index_scheduler: GuardedData, Data>, - body: ValidatedJson, + body: ValidatedJson, req: HttpRequest, analytics: web::Data, ) -> Result { let IndexCreateRequest { primary_key, uid } = body.into_inner(); - let uid = IndexUid::try_from(uid)?.into_inner(); let allow_index_creation = index_scheduler.filters().search_rules.is_index_authorized(&uid); if allow_index_creation { @@ -132,7 +130,7 @@ pub async fn create_index( Some(&req), ); - let task = KindWithContent::IndexCreation { index_uid: uid, primary_key }; + let task = KindWithContent::IndexCreation { index_uid: uid.to_string(), primary_key }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); @@ -146,25 +144,23 @@ fn deny_immutable_fields_index( field: &str, accepted: &[&str], location: ValuePointerRef, -) -> DeserrError { - let mut error = unwrap_any(DeserrError::::error::( - None, - deserr::ErrorKind::UnknownKey { key: field, accepted }, - location, - )); - - error.code = match field { - "uid" => Code::ImmutableIndexUid, - "createdAt" => Code::ImmutableIndexCreatedAt, - "updatedAt" => Code::ImmutableIndexUpdatedAt, - _ => Code::BadRequest, - }; - error +) -> DeserrJsonError { + match field { + "uid" => immutable_field_error(field, accepted, Code::ImmutableIndexUid), + "createdAt" => immutable_field_error(field, accepted, Code::ImmutableIndexCreatedAt), + "updatedAt" => immutable_field_error(field, accepted, Code::ImmutableIndexUpdatedAt), + _ => unwrap_any(DeserrJsonError::::error::( + None, + deserr::ErrorKind::UnknownKey { key: field, accepted }, + location, + )), + } } + #[derive(DeserializeFromValue, Debug)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)] pub struct UpdateIndexRequest { - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] primary_key: Option, } @@ -172,6 +168,8 @@ pub async fn get_index( index_scheduler: GuardedData, Data>, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let index = index_scheduler.index(&index_uid)?; let index_view = IndexView::new(index_uid.into_inner(), &index)?; @@ -182,12 +180,13 @@ pub async fn get_index( pub async fn update_index( index_scheduler: GuardedData, Data>, - path: web::Path, - body: ValidatedJson, + index_uid: web::Path, + body: ValidatedJson, req: HttpRequest, analytics: web::Data, ) -> Result { debug!("called with params: {:?}", body); + let index_uid = IndexUid::try_from(index_uid.into_inner())?; let body = body.into_inner(); analytics.publish( "Index Updated".to_string(), @@ -196,7 +195,7 @@ pub async fn update_index( ); let task = KindWithContent::IndexUpdate { - index_uid: path.into_inner(), + index_uid: index_uid.into_inner(), primary_key: body.primary_key, }; @@ -211,6 +210,7 @@ pub async fn delete_index( index_scheduler: GuardedData, Data>, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; let task = KindWithContent::IndexDeletion { index_uid: index_uid.into_inner() }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); @@ -224,6 +224,7 @@ pub async fn get_index_stats( req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; analytics.publish("Stats Seen".to_string(), json!({ "per_index_uid": true }), Some(&req)); let stats = IndexStats::new((*index_scheduler).clone(), index_uid.into_inner())?; diff --git a/meilisearch/src/routes/indexes/search.rs b/meilisearch/src/routes/indexes/search.rs index 6296772e0..545c69ec5 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -1,13 +1,14 @@ -use std::str::FromStr; - use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::IndexScheduler; use log::debug; use meilisearch_auth::IndexSearchRules; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; -use serde_cs::vec::CS; +use meilisearch_types::error::ResponseError; +use meilisearch_types::index_uid::IndexUid; +use meilisearch_types::serde_cs::vec::CS; use serde_json::Value; use crate::analytics::{Analytics, SearchAggregator}; @@ -16,7 +17,6 @@ use crate::extractors::authentication::GuardedData; use crate::extractors::json::ValidatedJson; use crate::extractors::query_parameters::QueryParameter; use crate::extractors::sequential_extractor::SeqHandler; -use crate::routes::from_string_to_option_take_error_message; use crate::search::{ perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, @@ -31,54 +31,42 @@ pub fn configure(cfg: &mut web::ServiceConfig) { ); } -pub fn parse_usize_take_error_message( - s: &str, -) -> Result> { - usize::from_str(s).map_err(TakeErrorMessage) -} - -pub fn parse_bool_take_error_message( - s: &str, -) -> Result> { - s.parse().map_err(TakeErrorMessage) -} - #[derive(Debug, deserr::DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQueryGet { - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrQueryParamError)] q: Option, - #[deserr(error = DeserrError, default = DEFAULT_SEARCH_OFFSET(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - offset: usize, - #[deserr(error = DeserrError, default = DEFAULT_SEARCH_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - limit: usize, - #[deserr(error = DeserrError, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage)] - page: Option, - #[deserr(error = DeserrError, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage)] - hits_per_page: Option, - #[deserr(error = DeserrError)] + #[deserr(default = Param(DEFAULT_SEARCH_OFFSET()), error = DeserrQueryParamError)] + offset: Param, + #[deserr(default = Param(DEFAULT_SEARCH_LIMIT()), error = DeserrQueryParamError)] + limit: Param, + #[deserr(default, error = DeserrQueryParamError)] + page: Option>, + #[deserr(default, error = DeserrQueryParamError)] + hits_per_page: Option>, + #[deserr(default, error = DeserrQueryParamError)] attributes_to_retrieve: Option>, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrQueryParamError)] attributes_to_crop: Option>, - #[deserr(error = DeserrError, default = DEFAULT_CROP_LENGTH(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - crop_length: usize, - #[deserr(error = DeserrError)] + #[deserr(default = Param(DEFAULT_CROP_LENGTH()), error = DeserrQueryParamError)] + crop_length: Param, + #[deserr(default, error = DeserrQueryParamError)] attributes_to_highlight: Option>, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrQueryParamError)] filter: Option, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrQueryParamError)] sort: Option, - #[deserr(error = DeserrError, default, from(&String) = parse_bool_take_error_message -> TakeErrorMessage)] - show_matches_position: bool, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrQueryParamError)] + show_matches_position: Param, + #[deserr(default, error = DeserrQueryParamError)] facets: Option>, - #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] + #[deserr( default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError)] highlight_pre_tag: String, - #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_POST_TAG())] + #[deserr( default = DEFAULT_HIGHLIGHT_POST_TAG(), error = DeserrQueryParamError)] highlight_post_tag: String, - #[deserr(error = DeserrError, default = DEFAULT_CROP_MARKER())] + #[deserr(default = DEFAULT_CROP_MARKER(), error = DeserrQueryParamError)] crop_marker: String, - #[deserr(error = DeserrError, default)] + #[deserr(default, error = DeserrQueryParamError)] matching_strategy: MatchingStrategy, } @@ -94,17 +82,17 @@ impl From for SearchQuery { Self { q: other.q, - offset: other.offset, - limit: other.limit, - page: other.page, - hits_per_page: other.hits_per_page, + offset: other.offset.0, + limit: other.limit.0, + page: other.page.as_deref().copied(), + hits_per_page: other.hits_per_page.as_deref().copied(), attributes_to_retrieve: other.attributes_to_retrieve.map(|o| o.into_iter().collect()), attributes_to_crop: other.attributes_to_crop.map(|o| o.into_iter().collect()), - crop_length: other.crop_length, + crop_length: other.crop_length.0, attributes_to_highlight: other.attributes_to_highlight.map(|o| o.into_iter().collect()), filter, sort: other.sort.map(|attr| fix_sort_query_parameters(&attr)), - show_matches_position: other.show_matches_position, + show_matches_position: other.show_matches_position.0, facets: other.facets.map(|o| o.into_iter().collect()), highlight_pre_tag: other.highlight_pre_tag, highlight_post_tag: other.highlight_post_tag, @@ -162,11 +150,13 @@ fn fix_sort_query_parameters(sort_query: &str) -> Vec { pub async fn search_with_url_query( index_scheduler: GuardedData, Data>, index_uid: web::Path, - params: QueryParameter, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { debug!("called with params: {:?}", params); + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let mut query: SearchQuery = params.into_inner().into(); // Tenant token search_rules. @@ -194,10 +184,12 @@ pub async fn search_with_url_query( pub async fn search_with_post( index_scheduler: GuardedData, Data>, index_uid: web::Path, - params: ValidatedJson, + params: ValidatedJson, req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let mut query = params.into_inner(); debug!("search called with params: {:?}", query); diff --git a/meilisearch/src/routes/indexes/settings.rs b/meilisearch/src/routes/indexes/settings.rs index 13c280d63..0c864cc73 100644 --- a/meilisearch/src/routes/indexes/settings.rs +++ b/meilisearch/src/routes/indexes/settings.rs @@ -2,7 +2,8 @@ use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::IndexScheduler; use log::debug; -use meilisearch_types::error::{DeserrError, ResponseError}; +use meilisearch_types::deserr::DeserrJsonError; +use meilisearch_types::error::ResponseError; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::settings::{settings, RankingRuleView, Settings, Unchecked}; use meilisearch_types::tasks::KindWithContent; @@ -40,12 +41,14 @@ macro_rules! make_setting_route { >, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let new_settings = Settings { $attr: Setting::Reset.into(), ..Default::default() }; let allow_index_creation = index_scheduler.filters().allow_index_creation; - let index_uid = IndexUid::try_from(index_uid.into_inner())?.into_inner(); + let task = KindWithContent::SettingsUpdate { - index_uid, + index_uid: index_uid.to_string(), new_settings: Box::new(new_settings), is_deletion: true, allow_index_creation, @@ -69,6 +72,8 @@ macro_rules! make_setting_route { req: HttpRequest, $analytics_var: web::Data, ) -> std::result::Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let body = body.into_inner(); $analytics(&body, &req); @@ -82,9 +87,9 @@ macro_rules! make_setting_route { }; let allow_index_creation = index_scheduler.filters().allow_index_creation; - let index_uid = IndexUid::try_from(index_uid.into_inner())?.into_inner(); + let task = KindWithContent::SettingsUpdate { - index_uid, + index_uid: index_uid.to_string(), new_settings: Box::new(new_settings), is_deletion: false, allow_index_creation, @@ -105,6 +110,8 @@ macro_rules! make_setting_route { >, index_uid: actix_web::web::Path, ) -> std::result::Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let index = index_scheduler.index(&index_uid)?; let rtxn = index.read_txn()?; let settings = settings(&index, &rtxn)?; @@ -130,7 +137,7 @@ make_setting_route!( "/filterable-attributes", put, std::collections::BTreeSet, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes, >, filterable_attributes, @@ -156,7 +163,7 @@ make_setting_route!( "/sortable-attributes", put, std::collections::BTreeSet, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsSortableAttributes, >, sortable_attributes, @@ -182,7 +189,7 @@ make_setting_route!( "/displayed-attributes", put, Vec, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsDisplayedAttributes, >, displayed_attributes, @@ -208,7 +215,7 @@ make_setting_route!( "/typo-tolerance", patch, meilisearch_types::settings::TypoSettings, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsTypoTolerance, >, typo_tolerance, @@ -253,7 +260,7 @@ make_setting_route!( "/searchable-attributes", put, Vec, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsSearchableAttributes, >, searchable_attributes, @@ -279,7 +286,7 @@ make_setting_route!( "/stop-words", put, std::collections::BTreeSet, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsStopWords, >, stop_words, @@ -304,7 +311,7 @@ make_setting_route!( "/synonyms", put, std::collections::BTreeMap>, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsSynonyms, >, synonyms, @@ -329,7 +336,7 @@ make_setting_route!( "/distinct-attribute", put, String, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsDistinctAttribute, >, distinct_attribute, @@ -353,7 +360,7 @@ make_setting_route!( "/ranking-rules", put, Vec, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsRankingRules, >, ranking_rules, @@ -384,7 +391,7 @@ make_setting_route!( "/faceting", patch, meilisearch_types::settings::FacetingSettings, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsFaceting, >, faceting, @@ -409,7 +416,7 @@ make_setting_route!( "/pagination", patch, meilisearch_types::settings::PaginationSettings, - meilisearch_types::error::DeserrError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsPagination, >, pagination, @@ -461,10 +468,12 @@ generate_configure!( pub async fn update_all( index_scheduler: GuardedData, Data>, index_uid: web::Path, - body: ValidatedJson, DeserrError>, + body: ValidatedJson, DeserrJsonError>, req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let new_settings = body.into_inner(); analytics.publish( @@ -570,6 +579,8 @@ pub async fn get_all( index_scheduler: GuardedData, Data>, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let index = index_scheduler.index(&index_uid)?; let rtxn = index.read_txn()?; let new_settings = settings(&index, &rtxn)?; @@ -581,6 +592,8 @@ pub async fn delete_all( index_scheduler: GuardedData, Data>, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let new_settings = Settings::cleared().into_unchecked(); let allow_index_creation = index_scheduler.filters().allow_index_creation; diff --git a/meilisearch/src/routes/mod.rs b/meilisearch/src/routes/mod.rs index 2e619540a..7aaad7125 100644 --- a/meilisearch/src/routes/mod.rs +++ b/meilisearch/src/routes/mod.rs @@ -1,13 +1,12 @@ use std::collections::BTreeMap; -use std::str::FromStr; use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::{IndexScheduler, Query}; use log::debug; -use meilisearch_types::error::{ResponseError, TakeErrorMessage}; +use meilisearch_auth::AuthController; +use meilisearch_types::error::ResponseError; use meilisearch_types::settings::{Settings, Unchecked}; -use meilisearch_types::star_or::StarOr; use meilisearch_types::tasks::{Kind, Status, Task, TaskId}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -35,37 +34,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(web::scope("/swap-indexes").configure(swap_indexes::configure)); } -/// Extracts the raw values from the `StarOr` types and -/// return None if a `StarOr::Star` is encountered. -pub fn fold_star_or(content: impl IntoIterator>) -> Option -where - O: FromIterator, -{ - content - .into_iter() - .map(|value| match value { - StarOr::Star => None, - StarOr::Other(val) => Some(val), - }) - .collect() -} - -pub fn from_string_to_option(input: &str) -> Result, E> -where - T: FromStr, -{ - Ok(Some(input.parse()?)) -} -pub fn from_string_to_option_take_error_message( - input: &str, -) -> Result, TakeErrorMessage> -where - T: FromStr, -{ - Ok(Some(input.parse().map_err(TakeErrorMessage)?)) -} - -const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20; +const PAGINATION_DEFAULT_LIMIT: usize = 20; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -262,13 +231,15 @@ pub struct Stats { async fn get_stats( index_scheduler: GuardedData, Data>, + auth_controller: GuardedData, AuthController>, req: HttpRequest, analytics: web::Data, ) -> Result { analytics.publish("Stats Seen".to_string(), json!({ "per_index_uid": false }), Some(&req)); let search_rules = &index_scheduler.filters().search_rules; - let stats = create_all_stats((*index_scheduler).clone(), search_rules)?; + let stats = + create_all_stats((*index_scheduler).clone(), (*auth_controller).clone(), search_rules)?; debug!("returns: {:?}", stats); Ok(HttpResponse::Ok().json(stats)) @@ -276,6 +247,7 @@ async fn get_stats( pub fn create_all_stats( index_scheduler: Data, + auth_controller: AuthController, search_rules: &meilisearch_auth::SearchRules, ) -> Result { let mut last_task: Option = None; @@ -285,6 +257,7 @@ pub fn create_all_stats( Query { statuses: Some(vec![Status::Processing]), limit: Some(1), ..Query::default() }, search_rules.authorized_indexes(), )?; + // accumulate the size of each indexes let processing_index = processing_task.first().and_then(|task| task.index_uid()); for (name, index) in index_scheduler.indexes()? { if !search_rules.is_index_authorized(&name) { @@ -305,6 +278,11 @@ pub fn create_all_stats( indexes.insert(name, stats); } + + database_size += index_scheduler.size()?; + database_size += auth_controller.size()?; + database_size += index_scheduler.compute_update_file_size()?; + let stats = Stats { database_size, last_update: last_task, indexes }; Ok(stats) } diff --git a/meilisearch/src/routes/swap_indexes.rs b/meilisearch/src/routes/swap_indexes.rs index c5b371cd9..4a7802f2e 100644 --- a/meilisearch/src/routes/swap_indexes.rs +++ b/meilisearch/src/routes/swap_indexes.rs @@ -2,8 +2,10 @@ use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use index_scheduler::IndexScheduler; +use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::error::deserr_codes::InvalidSwapIndexes; -use meilisearch_types::error::{DeserrError, ResponseError}; +use meilisearch_types::error::ResponseError; +use meilisearch_types::index_uid::IndexUid; use meilisearch_types::tasks::{IndexSwap, KindWithContent}; use serde_json::json; @@ -20,15 +22,15 @@ pub fn configure(cfg: &mut web::ServiceConfig) { } #[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct SwapIndexesPayload { - #[deserr(error = DeserrError)] - indexes: Vec, + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_swap_indexes)] + indexes: Vec, } pub async fn swap_indexes( index_scheduler: GuardedData, Data>, - params: ValidatedJson, DeserrError>, + params: ValidatedJson, DeserrJsonError>, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -44,6 +46,7 @@ pub async fn swap_indexes( let mut swaps = vec![]; for SwapIndexesPayload { indexes } in params.into_iter() { + // TODO: switch to deserr let (lhs, rhs) = match indexes.as_slice() { [lhs, rhs] => (lhs, rhs), _ => { @@ -53,7 +56,7 @@ pub async fn swap_indexes( if !search_rules.is_index_authorized(lhs) || !search_rules.is_index_authorized(rhs) { return Err(AuthenticationError::InvalidToken.into()); } - swaps.push(IndexSwap { indexes: (lhs.clone(), rhs.clone()) }); + swaps.push(IndexSwap { indexes: (lhs.to_string(), rhs.to_string()) }); } let task = KindWithContent::IndexSwap { swaps }; diff --git a/meilisearch/src/routes/tasks.rs b/meilisearch/src/routes/tasks.rs index 09723623f..b78be7876 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -1,34 +1,32 @@ -use std::num::ParseIntError; -use std::str::FromStr; - use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use index_scheduler::{IndexScheduler, Query, TaskId}; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::DeserrQueryParamError; use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; +use meilisearch_types::error::{InvalidTaskDateError, ResponseError}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::settings::{Settings, Unchecked}; -use meilisearch_types::star_or::StarOr; +use meilisearch_types::star_or::{OptionStarOr, OptionStarOrList}; use meilisearch_types::tasks::{ serialize_duration, Details, IndexSwap, Kind, KindWithContent, Status, Task, }; -use serde::{Deserialize, Serialize}; -use serde_cs::vec::CS; +use serde::Serialize; use serde_json::json; use time::format_description::well_known::Rfc3339; use time::macros::format_description; use time::{Date, Duration, OffsetDateTime, Time}; use tokio::task; -use super::{fold_star_or, SummarizedTaskView}; +use super::SummarizedTaskView; use crate::analytics::Analytics; use crate::extractors::authentication::policies::*; use crate::extractors::authentication::GuardedData; use crate::extractors::query_parameters::QueryParameter; use crate::extractors::sequential_extractor::SeqHandler; -const DEFAULT_LIMIT: fn() -> u32 = || 20; +const DEFAULT_LIMIT: u32 = 20; pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( @@ -164,162 +162,157 @@ impl From
for DetailsView { } } -fn parse_option_cs( - s: Option>, -) -> Result>, TakeErrorMessage> { - if let Some(s) = s { - s.into_iter() - .map(|s| T::from_str(&s)) - .collect::, T::Err>>() - .map_err(TakeErrorMessage) - .map(Some) - } else { - Ok(None) - } +#[derive(Debug, DeserializeFromValue)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] +pub struct TasksFilterQuery { + #[deserr(default = Param(DEFAULT_LIMIT), error = DeserrQueryParamError)] + pub limit: Param, + #[deserr(default, error = DeserrQueryParamError)] + pub from: Option>, + + #[deserr(default, error = DeserrQueryParamError)] + pub uids: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub canceled_by: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub types: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub statuses: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub index_uids: OptionStarOrList, + + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_enqueued_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_enqueued_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_started_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_started_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_finished_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_finished_at: OptionStarOr, } -fn parse_option_cs_star_or( - s: Option>>, -) -> Result>, TakeErrorMessage> { - if let Some(s) = s.and_then(fold_star_or) as Option> { - s.into_iter() - .map(|s| T::from_str(&s)) - .collect::, T::Err>>() - .map_err(TakeErrorMessage) - .map(Some) - } else { - Ok(None) - } -} -fn parse_option_str(s: Option) -> Result, TakeErrorMessage> { - if let Some(s) = s { - T::from_str(&s).map_err(TakeErrorMessage).map(Some) - } else { - Ok(None) +impl TasksFilterQuery { + fn into_query(self) -> Query { + Query { + limit: Some(self.limit.0), + from: self.from.as_deref().copied(), + statuses: self.statuses.merge_star_and_none(), + types: self.types.merge_star_and_none(), + index_uids: self.index_uids.map(|x| x.to_string()).merge_star_and_none(), + uids: self.uids.merge_star_and_none(), + canceled_by: self.canceled_by.merge_star_and_none(), + before_enqueued_at: self.before_enqueued_at.merge_star_and_none(), + after_enqueued_at: self.after_enqueued_at.merge_star_and_none(), + before_started_at: self.before_started_at.merge_star_and_none(), + after_started_at: self.after_started_at.merge_star_and_none(), + before_finished_at: self.before_finished_at.merge_star_and_none(), + after_finished_at: self.after_finished_at.merge_star_and_none(), + } } } -fn parse_str(s: String) -> Result> { - T::from_str(&s).map_err(TakeErrorMessage) +impl TaskDeletionOrCancelationQuery { + fn is_empty(&self) -> bool { + matches!( + self, + TaskDeletionOrCancelationQuery { + uids: OptionStarOrList::None, + canceled_by: OptionStarOrList::None, + types: OptionStarOrList::None, + statuses: OptionStarOrList::None, + index_uids: OptionStarOrList::None, + after_enqueued_at: OptionStarOr::None, + before_enqueued_at: OptionStarOr::None, + after_started_at: OptionStarOr::None, + before_started_at: OptionStarOr::None, + after_finished_at: OptionStarOr::None, + before_finished_at: OptionStarOr::None + } + ) + } } #[derive(Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] -pub struct TasksFilterQuery { - #[deserr(error = DeserrError, default = DEFAULT_LIMIT(), from(String) = parse_str:: -> TakeErrorMessage)] - pub limit: u32, - #[deserr(error = DeserrError, from(Option) = parse_option_str:: -> TakeErrorMessage)] - pub from: Option, - - #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] - pub uids: Option>, - #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] - pub canceled_by: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub types: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub statuses: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub index_uids: Option>, - - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_enqueued_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_enqueued_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_started_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_started_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_finished_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_finished_at: Option, -} - -#[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct TaskDeletionOrCancelationQuery { - #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] - pub uids: Option>, - #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] - pub canceled_by: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub types: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub statuses: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub index_uids: Option>, + #[deserr(default, error = DeserrQueryParamError)] + pub uids: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub canceled_by: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub types: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub statuses: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub index_uids: OptionStarOrList, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_enqueued_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_enqueued_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_started_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_started_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_finished_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_finished_at: Option, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_enqueued_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_enqueued_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_started_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_started_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_finished_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_finished_at: OptionStarOr, +} +impl TaskDeletionOrCancelationQuery { + fn into_query(self) -> Query { + Query { + limit: None, + from: None, + statuses: self.statuses.merge_star_and_none(), + types: self.types.merge_star_and_none(), + index_uids: self.index_uids.map(|x| x.to_string()).merge_star_and_none(), + uids: self.uids.merge_star_and_none(), + canceled_by: self.canceled_by.merge_star_and_none(), + before_enqueued_at: self.before_enqueued_at.merge_star_and_none(), + after_enqueued_at: self.after_enqueued_at.merge_star_and_none(), + before_started_at: self.before_started_at.merge_star_and_none(), + after_started_at: self.after_started_at.merge_star_and_none(), + before_finished_at: self.before_finished_at.merge_star_and_none(), + after_finished_at: self.after_finished_at.merge_star_and_none(), + } + } } async fn cancel_tasks( index_scheduler: GuardedData, Data>, - params: QueryParameter, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { - let TaskDeletionOrCancelationQuery { - types, - uids, - canceled_by, - statuses, - index_uids, - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - } = params.into_inner(); + let params = params.into_inner(); + + if params.is_empty() { + return Err(index_scheduler::Error::TaskCancelationWithEmptyQuery.into()); + } analytics.publish( "Tasks Canceled".to_string(), json!({ - "filtered_by_uid": uids.is_some(), - "filtered_by_index_uid": index_uids.is_some(), - "filtered_by_type": types.is_some(), - "filtered_by_status": statuses.is_some(), - "filtered_by_canceled_by": canceled_by.is_some(), - "filtered_by_before_enqueued_at": before_enqueued_at.is_some(), - "filtered_by_after_enqueued_at": after_enqueued_at.is_some(), - "filtered_by_before_started_at": before_started_at.is_some(), - "filtered_by_after_started_at": after_started_at.is_some(), - "filtered_by_before_finished_at": before_finished_at.is_some(), - "filtered_by_after_finished_at": after_finished_at.is_some(), + "filtered_by_uid": params.uids.is_some(), + "filtered_by_index_uid": params.index_uids.is_some(), + "filtered_by_type": params.types.is_some(), + "filtered_by_status": params.statuses.is_some(), + "filtered_by_canceled_by": params.canceled_by.is_some(), + "filtered_by_before_enqueued_at": params.before_enqueued_at.is_some(), + "filtered_by_after_enqueued_at": params.after_enqueued_at.is_some(), + "filtered_by_before_started_at": params.before_started_at.is_some(), + "filtered_by_after_started_at": params.after_started_at.is_some(), + "filtered_by_before_finished_at": params.before_finished_at.is_some(), + "filtered_by_after_finished_at": params.after_finished_at.is_some(), }), Some(&req), ); - let query = Query { - limit: None, - from: None, - statuses, - types, - index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()), - uids, - canceled_by, - before_enqueued_at, - after_enqueued_at, - before_started_at, - after_started_at, - before_finished_at, - after_finished_at, - }; - - if query.is_empty() { - return Err(index_scheduler::Error::TaskCancelationWithEmptyQuery.into()); - } + let query = params.into_query(); let tasks = index_scheduler.get_task_ids_from_authorized_indexes( &index_scheduler.read_txn()?, @@ -337,62 +330,34 @@ async fn cancel_tasks( async fn delete_tasks( index_scheduler: GuardedData, Data>, - params: QueryParameter, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { - let TaskDeletionOrCancelationQuery { - types, - uids, - canceled_by, - statuses, - index_uids, + let params = params.into_inner(); - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - } = params.into_inner(); + if params.is_empty() { + return Err(index_scheduler::Error::TaskDeletionWithEmptyQuery.into()); + } analytics.publish( "Tasks Deleted".to_string(), json!({ - "filtered_by_uid": uids.is_some(), - "filtered_by_index_uid": index_uids.is_some(), - "filtered_by_type": types.is_some(), - "filtered_by_status": statuses.is_some(), - "filtered_by_canceled_by": canceled_by.is_some(), - "filtered_by_before_enqueued_at": before_enqueued_at.is_some(), - "filtered_by_after_enqueued_at": after_enqueued_at.is_some(), - "filtered_by_before_started_at": before_started_at.is_some(), - "filtered_by_after_started_at": after_started_at.is_some(), - "filtered_by_before_finished_at": before_finished_at.is_some(), - "filtered_by_after_finished_at": after_finished_at.is_some(), + "filtered_by_uid": params.uids.is_some(), + "filtered_by_index_uid": params.index_uids.is_some(), + "filtered_by_type": params.types.is_some(), + "filtered_by_status": params.statuses.is_some(), + "filtered_by_canceled_by": params.canceled_by.is_some(), + "filtered_by_before_enqueued_at": params.before_enqueued_at.is_some(), + "filtered_by_after_enqueued_at": params.after_enqueued_at.is_some(), + "filtered_by_before_started_at": params.before_started_at.is_some(), + "filtered_by_after_started_at": params.after_started_at.is_some(), + "filtered_by_before_finished_at": params.before_finished_at.is_some(), + "filtered_by_after_finished_at": params.after_finished_at.is_some(), }), Some(&req), ); - - let query = Query { - limit: None, - from: None, - statuses, - types, - index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()), - uids, - canceled_by, - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - }; - - if query.is_empty() { - return Err(index_scheduler::Error::TaskDeletionWithEmptyQuery.into()); - } + let query = params.into_query(); let tasks = index_scheduler.get_task_ids_from_authorized_indexes( &index_scheduler.read_txn()?, @@ -418,47 +383,17 @@ pub struct AllTasks { async fn get_tasks( index_scheduler: GuardedData, Data>, - params: QueryParameter, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { - let params = params.into_inner(); + let mut params = params.into_inner(); analytics.get_tasks(¶ms, &req); - let TasksFilterQuery { - types, - uids, - canceled_by, - statuses, - index_uids, - limit, - from, - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - } = params; - // We +1 just to know if there is more after this "page" or not. - let limit = limit.saturating_add(1); - - let query = index_scheduler::Query { - limit: Some(limit), - from, - statuses, - types, - index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()), - uids, - canceled_by, - before_enqueued_at, - after_enqueued_at, - before_started_at, - after_started_at, - before_finished_at, - after_finished_at, - }; + params.limit.0 = params.limit.0.saturating_add(1); + let limit = params.limit.0; + let query = params.into_query(); let mut tasks_results: Vec = index_scheduler .get_tasks_from_authorized_indexes( @@ -524,7 +459,7 @@ pub enum DeserializeDateOption { pub fn deserialize_date( value: &str, option: DeserializeDateOption, -) -> std::result::Result> { +) -> std::result::Result { // We can't parse using time's rfc3339 format, since then we won't know what part of the // datetime was not explicitly specified, and thus we won't be able to increment it to the // next step. @@ -546,54 +481,41 @@ pub fn deserialize_date( } } } else { - Err(TakeErrorMessage(InvalidTaskDateError(value.to_owned()))) + Err(InvalidTaskDateError(value.to_owned())) } } -pub fn deserialize_date_before( - value: Option, -) -> std::result::Result, TakeErrorMessage> { - if let Some(value) = value { - let date = deserialize_date(&value, DeserializeDateOption::Before)?; - Ok(Some(date)) - } else { - Ok(None) - } -} pub fn deserialize_date_after( - value: Option, -) -> std::result::Result, TakeErrorMessage> { - if let Some(value) = value { - let date = deserialize_date(&value, DeserializeDateOption::After)?; - Ok(Some(date)) - } else { - Ok(None) - } + value: OptionStarOr, +) -> std::result::Result, InvalidTaskDateError> { + value.try_map(|x| deserialize_date(&x, DeserializeDateOption::After)) } - -#[derive(Debug)] -pub struct InvalidTaskDateError(String); -impl std::fmt::Display for InvalidTaskDateError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "`{}` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", self.0) - } +pub fn deserialize_date_before( + value: OptionStarOr, +) -> std::result::Result, InvalidTaskDateError> { + value.try_map(|x| deserialize_date(&x, DeserializeDateOption::Before)) } -impl std::error::Error for InvalidTaskDateError {} #[cfg(test)] mod tests { use deserr::DeserializeFromValue; use meili_snap::snapshot; - use meilisearch_types::error::DeserrError; + use meilisearch_types::deserr::DeserrQueryParamError; + use meilisearch_types::error::{Code, ResponseError}; - use crate::extractors::query_parameters::QueryParameter; use crate::routes::tasks::{TaskDeletionOrCancelationQuery, TasksFilterQuery}; - fn deserr_query_params(j: &str) -> Result + fn deserr_query_params(j: &str) -> Result where - T: DeserializeFromValue, + T: DeserializeFromValue, { - QueryParameter::::from_query(j).map(|p| p.0) + let value = serde_urlencoded::from_str::(j) + .map_err(|e| ResponseError::from_msg(e.to_string(), Code::BadRequest))?; + + match deserr::deserialize::<_, _, DeserrQueryParamError>(value) { + Ok(data) => Ok(data), + Err(e) => Err(ResponseError::from(e)), + } } #[test] @@ -602,65 +524,113 @@ mod tests { let params = "afterEnqueuedAt=2021-12-03&beforeEnqueuedAt=2021-12-03&afterStartedAt=2021-12-03&beforeStartedAt=2021-12-03&afterFinishedAt=2021-12-03&beforeFinishedAt=2021-12-03"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.before_enqueued_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.after_started_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.before_started_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.after_finished_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.before_finished_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(2021-12-04 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.before_enqueued_at), @"Other(2021-12-03 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.after_started_at), @"Other(2021-12-04 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.before_started_at), @"Other(2021-12-03 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.after_finished_at), @"Other(2021-12-04 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.before_finished_at), @"Other(2021-12-03 0:00:00.0 +00:00:00)"); } { let params = "afterEnqueuedAt=2021-12-03T23:45:23Z&beforeEnqueuedAt=2021-12-03T23:45:23Z"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); - snapshot!(format!("{:?}", query.before_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(2021-12-03 23:45:23.0 +00:00:00)"); + snapshot!(format!("{:?}", query.before_enqueued_at), @"Other(2021-12-03 23:45:23.0 +00:00:00)"); } { let params = "afterEnqueuedAt=1997-11-12T09:55:06-06:20"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 -06:20:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(1997-11-12 9:55:06.0 -06:20:00)"); } { let params = "afterEnqueuedAt=1997-11-12T09:55:06%2B00:00"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 +00:00:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(1997-11-12 9:55:06.0 +00:00:00)"); } { let params = "afterEnqueuedAt=1997-11-12T09:55:06.200000300Z"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.2000003 +00:00:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(1997-11-12 9:55:06.2000003 +00:00:00)"); + } + { + // Stars are allowed in date fields as well + let params = "afterEnqueuedAt=*&beforeStartedAt=*&afterFinishedAt=*&beforeFinishedAt=*&afterStartedAt=*&beforeEnqueuedAt=*"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: None, canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Star, before_enqueued_at: Star, after_started_at: Star, before_started_at: Star, after_finished_at: Star, before_finished_at: Star }"); } { let params = "afterFinishedAt=2021"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `afterFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_after_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_after_finished_at" + } + "###); } { let params = "beforeFinishedAt=2021"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `beforeFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_before_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_before_finished_at" + } + "###); } { let params = "afterEnqueuedAt=2021-12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `afterEnqueuedAt`: `2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_after_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_after_enqueued_at" + } + "###); } { let params = "beforeEnqueuedAt=2021-12-03T23"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `beforeEnqueuedAt`: `2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_before_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_before_enqueued_at" + } + "###); } { let params = "afterStartedAt=2021-12-03T23:45"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `afterStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_after_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_after_started_at" + } + "###); } { let params = "beforeStartedAt=2021-12-03T23:45"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `beforeStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_before_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_before_started_at" + } + "###); } } @@ -669,22 +639,48 @@ mod tests { { let params = "uids=78,1,12,73"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.uids.unwrap()), @"[78, 1, 12, 73]"); + snapshot!(format!("{:?}", query.uids), @"List([78, 1, 12, 73])"); } { let params = "uids=1"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.uids.unwrap()), @"[1]"); + snapshot!(format!("{:?}", query.uids), @"List([1])"); + } + { + let params = "uids=cat,*,dog"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `uids[0]`: could not parse `cat` as a positive integer", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" + } + "###); } { let params = "uids=78,hello,world"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `uids[1]`: could not parse `hello` as a positive integer", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" + } + "###); } { let params = "uids=cat"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `uids`: could not parse `cat` as a positive integer", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" + } + "###); } } @@ -693,17 +689,24 @@ mod tests { { let params = "statuses=succeeded,failed,enqueued,processing,canceled"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.statuses.unwrap()), @"[Succeeded, Failed, Enqueued, Processing, Canceled]"); + snapshot!(format!("{:?}", query.statuses), @"List([Succeeded, Failed, Enqueued, Processing, Canceled])"); } { let params = "statuses=enqueued"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.statuses.unwrap()), @"[Enqueued]"); + snapshot!(format!("{:?}", query.statuses), @"List([Enqueued])"); } { let params = "statuses=finished"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`finished` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `statuses`: `finished` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", + "code": "invalid_task_statuses", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_statuses" + } + "###); } } #[test] @@ -711,17 +714,24 @@ mod tests { { let params = "types=documentAdditionOrUpdate,documentDeletion,settingsUpdate,indexCreation,indexDeletion,indexUpdate,indexSwap,taskCancelation,taskDeletion,dumpCreation,snapshotCreation"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.types.unwrap()), @"[DocumentAdditionOrUpdate, DocumentDeletion, SettingsUpdate, IndexCreation, IndexDeletion, IndexUpdate, IndexSwap, TaskCancelation, TaskDeletion, DumpCreation, SnapshotCreation]"); + snapshot!(format!("{:?}", query.types), @"List([DocumentAdditionOrUpdate, DocumentDeletion, SettingsUpdate, IndexCreation, IndexDeletion, IndexUpdate, IndexSwap, TaskCancelation, TaskDeletion, DumpCreation, SnapshotCreation])"); } { let params = "types=settingsUpdate"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.types.unwrap()), @"[SettingsUpdate]"); + snapshot!(format!("{:?}", query.types), @"List([SettingsUpdate])"); } { let params = "types=createIndex"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`createIndex` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `types`: `createIndex` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", + "code": "invalid_task_types", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_types" + } + "###); } } #[test] @@ -729,22 +739,36 @@ mod tests { { let params = "indexUids=toto,tata-78"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.index_uids.unwrap()), @r###"[IndexUid("toto"), IndexUid("tata-78")]"###); + snapshot!(format!("{:?}", query.index_uids), @r###"List([IndexUid("toto"), IndexUid("tata-78")])"###); } { let params = "indexUids=index_a"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.index_uids.unwrap()), @r###"[IndexUid("index_a")]"###); + snapshot!(format!("{:?}", query.index_uids), @r###"List([IndexUid("index_a")])"###); } { let params = "indexUids=1,hé"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `indexUids[1]`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); } { let params = "indexUids=hé"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `indexUids`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); } } @@ -753,38 +777,74 @@ mod tests { { let params = "from=12&limit=15&indexUids=toto,tata-78&statuses=succeeded,enqueued&afterEnqueuedAt=2012-04-23&uids=1,2,3"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query), @r###"TasksFilterQuery { limit: 15, from: Some(12), uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: Some([Succeeded, Enqueued]), index_uids: Some([IndexUid("toto"), IndexUid("tata-78")]), after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"###); + snapshot!(format!("{:?}", query), @r###"TasksFilterQuery { limit: Param(15), from: Some(Param(12)), uids: List([1, 2, 3]), canceled_by: None, types: None, statuses: List([Succeeded, Enqueued]), index_uids: List([IndexUid("toto"), IndexUid("tata-78")]), after_enqueued_at: Other(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"###); } { // Stars should translate to `None` in the query // Verify value of the default limit let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query), @"TasksFilterQuery { limit: 20, from: None, uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); + snapshot!(format!("{:?}", query), @"TasksFilterQuery { limit: Param(20), from: None, uids: List([1, 2, 3]), canceled_by: None, types: None, statuses: Star, index_uids: Star, after_enqueued_at: Other(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); } { // Stars should also translate to `None` in task deletion/cancelation queries let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); + snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: List([1, 2, 3]), canceled_by: None, types: None, statuses: Star, index_uids: Star, after_enqueued_at: Other(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); } { - // Stars in uids not allowed - let params = "uids=*"; + // Star in from not allowed + let params = "uids=*&from=*"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `from`: could not parse `*` as a positive integer", + "code": "invalid_task_from", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_from" + } + "###); } { // From not allowed in task deletion/cancelation queries let params = "from=12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); } { // Limit not allowed in task deletion/cancelation queries let params = "limit=12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``."); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); + } + } + + #[test] + fn deserialize_task_delete_or_cancel_empty() { + { + let params = ""; + let query = deserr_query_params::(params).unwrap(); + assert!(query.is_empty()); + } + { + let params = "statuses=*"; + let query = deserr_query_params::(params).unwrap(); + assert!(!query.is_empty()); + snapshot!(format!("{query:?}"), @"TaskDeletionOrCancelationQuery { uids: None, canceled_by: None, types: None, statuses: Star, index_uids: None, after_enqueued_at: None, before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); } } } diff --git a/meilisearch/src/search.rs b/meilisearch/src/search.rs index 9e4d81ffd..d4a65cc39 100644 --- a/meilisearch/src/search.rs +++ b/meilisearch/src/search.rs @@ -5,8 +5,8 @@ use std::time::Instant; use deserr::DeserializeFromValue; use either::Either; +use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::DeserrError; use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS; use meilisearch_types::{milli, Document}; use milli::tokenizer::TokenizerBuilder; @@ -15,7 +15,7 @@ use milli::{ SortError, TermsMatchingStrategy, DEFAULT_VALUES_PER_FACET, }; use regex::Regex; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use serde_json::{json, Value}; use crate::error::MeilisearchHttpError; @@ -30,41 +30,41 @@ pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "".to_string(); pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "".to_string(); #[derive(Debug, Clone, Default, PartialEq, Eq, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQuery { - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub q: Option, - #[deserr(error = DeserrError, default = DEFAULT_SEARCH_OFFSET())] + #[deserr(default = DEFAULT_SEARCH_OFFSET(), error = DeserrJsonError)] pub offset: usize, - #[deserr(error = DeserrError, default = DEFAULT_SEARCH_LIMIT())] + #[deserr(default = DEFAULT_SEARCH_LIMIT(), error = DeserrJsonError)] pub limit: usize, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub page: Option, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub hits_per_page: Option, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub attributes_to_retrieve: Option>, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub attributes_to_crop: Option>, - #[deserr(error = DeserrError, default = DEFAULT_CROP_LENGTH())] + #[deserr(default, error = DeserrJsonError, default = DEFAULT_CROP_LENGTH())] pub crop_length: usize, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub attributes_to_highlight: Option>, - #[deserr(error = DeserrError, default)] + #[deserr(default, error = DeserrJsonError, default)] pub show_matches_position: bool, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub filter: Option, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub sort: Option>, - #[deserr(error = DeserrError)] + #[deserr(default, error = DeserrJsonError)] pub facets: Option>, - #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] + #[deserr(default, error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] pub highlight_pre_tag: String, - #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_POST_TAG())] + #[deserr(default, error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_POST_TAG())] pub highlight_post_tag: String, - #[deserr(error = DeserrError, default = DEFAULT_CROP_MARKER())] + #[deserr(default, error = DeserrJsonError, default = DEFAULT_CROP_MARKER())] pub crop_marker: String, - #[deserr(error = DeserrError, default)] + #[deserr(default, error = DeserrJsonError, default)] pub matching_strategy: MatchingStrategy, } @@ -74,9 +74,8 @@ impl SearchQuery { } } -#[derive(Deserialize, Debug, Clone, PartialEq, Eq, DeserializeFromValue)] +#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromValue)] #[deserr(rename_all = camelCase)] -#[serde(rename_all = "camelCase")] pub enum MatchingStrategy { /// Remove query words from last to first Last, diff --git a/meilisearch/tests/auth/api_keys.rs b/meilisearch/tests/auth/api_keys.rs index 0a14107a8..0ae57d726 100644 --- a/meilisearch/tests/auth/api_keys.rs +++ b/meilisearch/tests/auth/api_keys.rs @@ -205,7 +205,7 @@ async fn error_add_api_key_no_header() { "message": "The Authorization header is missing. It must use the bearer authorization method.", "code": "missing_authorization_header", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" } "###); } @@ -228,7 +228,7 @@ async fn error_add_api_key_bad_key() { "message": "The provided API key is invalid.", "code": "invalid_api_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" + "link": "https://docs.meilisearch.com/errors#invalid_api_key" } "###); } @@ -248,10 +248,10 @@ async fn error_add_api_key_missing_parameter() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: missing field `indexes` at ``", - "code": "bad_request", + "message": "Missing field `indexes`", + "code": "missing_api_key_indexes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#missing_api_key_indexes" } "###); @@ -265,10 +265,10 @@ async fn error_add_api_key_missing_parameter() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: missing field `actions` at ``", - "code": "bad_request", + "message": "Missing field `actions`", + "code": "missing_api_key_actions", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#missing_api_key_actions" } "###); @@ -279,22 +279,13 @@ async fn error_add_api_key_missing_parameter() { "actions": ["documents.add"], }); let (response, code) = server.add_api_key(content).await; - meili_snap::snapshot!(code, @"201 Created"); + meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" { - "name": null, - "description": "Indexing API key", - "key": "[ignored]", - "uid": "[ignored]", - "actions": [ - "documents.add" - ], - "indexes": [ - "products" - ], - "expiresAt": null, - "createdAt": "[ignored]", - "updatedAt": "[ignored]" + "message": "Missing field `expiresAt`", + "code": "missing_api_key_expires_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_api_key_expires_at" } "###); } @@ -314,10 +305,10 @@ async fn error_add_api_key_invalid_parameters_description() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.description`.", + "message": "Invalid value type at `.description`: expected a string, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_description", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-description" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_description" } "###); } @@ -337,10 +328,10 @@ async fn error_add_api_key_invalid_parameters_name() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.name`.", + "message": "Invalid value type at `.name`: expected a string, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_name", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-name" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_name" } "###); } @@ -360,10 +351,10 @@ async fn error_add_api_key_invalid_parameters_indexes() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.indexes`.", + "message": "Invalid value type at `.indexes`: expected an array, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_indexes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes" } "###); } @@ -386,10 +377,10 @@ async fn error_add_api_key_invalid_index_uids() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "`invalid index # / \\name with spaces` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexes[0]`.", + "message": "Invalid value at `.indexes[0]`: `invalid index # / \\name with spaces` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_api_key_indexes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes" } "###); } @@ -411,10 +402,10 @@ async fn error_add_api_key_invalid_parameters_actions() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.actions`.", + "message": "Invalid value type at `.actions`: expected an array, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_actions", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_actions" } "###); @@ -431,10 +422,10 @@ async fn error_add_api_key_invalid_parameters_actions() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: unknown value `doc.add`, expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete` at `.actions[0]`.", + "message": "Unknown value `doc.add` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`", "code": "invalid_api_key_actions", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_actions" } "###); } @@ -455,10 +446,10 @@ async fn error_add_api_key_invalid_parameters_expires_at() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.expiresAt`.", + "message": "Invalid value type at `.expiresAt`: expected a string, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_expires_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_expires_at" } "###); } @@ -478,10 +469,10 @@ async fn error_add_api_key_invalid_parameters_expires_at_in_the_past() { meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "`2010-11-13T00:00:00Z` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.\n at `.expiresAt`.", + "message": "Invalid value at `.expiresAt`: `2010-11-13T00:00:00Z` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.\n", "code": "invalid_api_key_expires_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_expires_at" } "###); meili_snap::snapshot!(code, @"400 Bad Request"); @@ -503,10 +494,10 @@ async fn error_add_api_key_invalid_parameters_uid() { meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid length: expected length 32 for simple format, found 13 at `.uid`.", + "message": "Invalid value at `.uid`: invalid length: expected length 32 for simple format, found 13", "code": "invalid_api_key_uid", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-uid" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_uid" } "###); meili_snap::snapshot!(code, @"400 Bad Request"); @@ -551,7 +542,7 @@ async fn error_add_api_key_parameters_uid_already_exist() { "message": "`uid` field value `4bc0887a-0e41-4f3b-935d-0c451dcee9c8` is already an existing API key.", "code": "api_key_already_exists", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#api-key-already-exists" + "link": "https://docs.meilisearch.com/errors#api_key_already_exists" } "###); meili_snap::snapshot!(code, @"409 Conflict"); @@ -697,7 +688,7 @@ async fn error_get_api_key_no_header() { "message": "The Authorization header is missing. It must use the bearer authorization method.", "code": "missing_authorization_header", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); @@ -716,7 +707,7 @@ async fn error_get_api_key_bad_key() { "message": "The provided API key is invalid.", "code": "invalid_api_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" + "link": "https://docs.meilisearch.com/errors#invalid_api_key" } "###); meili_snap::snapshot!(code, @"403 Forbidden"); @@ -735,7 +726,7 @@ async fn error_get_api_key_not_found() { "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", "code": "api_key_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#api-key-not-found" + "link": "https://docs.meilisearch.com/errors#api_key_not_found" } "###); meili_snap::snapshot!(code, @"404 Not Found"); @@ -799,7 +790,7 @@ async fn list_api_keys() { "###); meili_snap::snapshot!(code, @"201 Created"); - let (response, code) = server.list_api_keys().await; + let (response, code) = server.list_api_keys("").await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".results[].createdAt" => "[ignored]", ".results[].updatedAt" => "[ignored]", ".results[].uid" => "[ignored]", ".results[].key" => "[ignored]" }), @r###" { "results": [ @@ -873,13 +864,13 @@ async fn list_api_keys() { async fn error_list_api_keys_no_header() { let server = Server::new_auth().await; - let (response, code) = server.list_api_keys().await; + let (response, code) = server.list_api_keys("").await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { "message": "The Authorization header is missing. It must use the bearer authorization method.", "code": "missing_authorization_header", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); @@ -890,13 +881,13 @@ async fn error_list_api_keys_bad_key() { let mut server = Server::new_auth().await; server.use_api_key("d4000bd7225f77d1eb22cc706ed36772bbc36767c016a27f76def7537b68600d"); - let (response, code) = server.list_api_keys().await; + let (response, code) = server.list_api_keys("").await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { "message": "The provided API key is invalid.", "code": "invalid_api_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" + "link": "https://docs.meilisearch.com/errors#invalid_api_key" } "###); meili_snap::snapshot!(code, @"403 Forbidden"); @@ -973,7 +964,7 @@ async fn delete_api_key() { "message": "[ignored]", "code": "api_key_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#api-key-not-found" + "link": "https://docs.meilisearch.com/errors#api_key_not_found" } "###); meili_snap::snapshot!(code, @"404 Not Found"); @@ -992,7 +983,7 @@ async fn error_delete_api_key_no_header() { "message": "The Authorization header is missing. It must use the bearer authorization method.", "code": "missing_authorization_header", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); @@ -1011,7 +1002,7 @@ async fn error_delete_api_key_bad_key() { "message": "The provided API key is invalid.", "code": "invalid_api_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" + "link": "https://docs.meilisearch.com/errors#invalid_api_key" } "###); meili_snap::snapshot!(code, @"403 Forbidden"); @@ -1030,7 +1021,7 @@ async fn error_delete_api_key_not_found() { "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", "code": "api_key_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#api-key-not-found" + "link": "https://docs.meilisearch.com/errors#api_key_not_found" } "###); meili_snap::snapshot!(code, @"404 Not Found"); @@ -1089,14 +1080,14 @@ async fn patch_api_key_description() { let uid = response["uid"].as_str().unwrap(); - // Add a description - let content = json!({ "description": "Indexing API key" }); + // Add a description and a name + let content = json!({ "description": "Indexing API key", "name": "bob" }); thread::sleep(time::Duration::new(1, 0)); let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" { - "name": null, + "name": "bob", "description": "Indexing API key", "key": "[ignored]", "uid": "[ignored]", @@ -1128,7 +1119,7 @@ async fn patch_api_key_description() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" { - "name": null, + "name": "bob", "description": "Product API key", "key": "[ignored]", "uid": "[ignored]", @@ -1160,7 +1151,7 @@ async fn patch_api_key_description() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" { - "name": null, + "name": "bob", "description": null, "key": "[ignored]", "uid": "[ignored]", @@ -1242,15 +1233,15 @@ async fn patch_api_key_name() { let created_at = response["createdAt"].as_str().unwrap(); let updated_at = response["updatedAt"].as_str().unwrap(); - // Add a name - let content = json!({ "name": "Indexing API key" }); + // Add a name and description + let content = json!({ "name": "Indexing API key", "description": "The doggoscription" }); thread::sleep(time::Duration::new(1, 0)); let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" { "name": "Indexing API key", - "description": null, + "description": "The doggoscription", "key": "[ignored]", "uid": "[ignored]", "actions": [ @@ -1285,7 +1276,7 @@ async fn patch_api_key_name() { meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" { "name": "Product API key", - "description": null, + "description": "The doggoscription", "key": "[ignored]", "uid": "[ignored]", "actions": [ @@ -1311,13 +1302,13 @@ async fn patch_api_key_name() { meili_snap::snapshot!(code, @"200 OK"); // Remove the name - let content = json!({ "name": serde_json::Value::Null }); + let content = json!({ "name": null }); let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" { "name": null, - "description": null, + "description": "The doggoscription", "key": "[ignored]", "uid": "[ignored]", "actions": [ @@ -1403,10 +1394,10 @@ async fn error_patch_api_key_indexes() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: unknown field `indexes`, expected one of `description`, `name` at ``.", + "message": "Immutable field `indexes`: expected one of `description`, `name`", "code": "immutable_api_key_indexes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#immutable-api-key-indexes" + "link": "https://docs.meilisearch.com/errors#immutable_api_key_indexes" } "###); meili_snap::snapshot!(code, @"400 Bad Request"); @@ -1480,10 +1471,10 @@ async fn error_patch_api_key_actions() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: unknown field `actions`, expected one of `description`, `name` at ``.", + "message": "Immutable field `actions`: expected one of `description`, `name`", "code": "immutable_api_key_actions", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#immutable-api-key-actions" + "link": "https://docs.meilisearch.com/errors#immutable_api_key_actions" } "###); meili_snap::snapshot!(code, @"400 Bad Request"); @@ -1549,10 +1540,10 @@ async fn error_patch_api_key_expiration_date() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: unknown field `expiresAt`, expected one of `description`, `name` at ``.", + "message": "Immutable field `expiresAt`: expected one of `description`, `name`", "code": "immutable_api_key_expires_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#immutable-api-key-expires-at" + "link": "https://docs.meilisearch.com/errors#immutable_api_key_expires_at" } "###); meili_snap::snapshot!(code, @"400 Bad Request"); @@ -1574,7 +1565,7 @@ async fn error_patch_api_key_no_header() { "message": "The Authorization header is missing. It must use the bearer authorization method.", "code": "missing_authorization_header", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); @@ -1597,7 +1588,7 @@ async fn error_patch_api_key_bad_key() { "message": "The provided API key is invalid.", "code": "invalid_api_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" + "link": "https://docs.meilisearch.com/errors#invalid_api_key" } "###); meili_snap::snapshot!(code, @"403 Forbidden"); @@ -1620,7 +1611,7 @@ async fn error_patch_api_key_not_found() { "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", "code": "api_key_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#api-key-not-found" + "link": "https://docs.meilisearch.com/errors#api_key_not_found" } "###); meili_snap::snapshot!(code, @"404 Not Found"); @@ -1670,10 +1661,10 @@ async fn error_patch_api_key_indexes_invalid_parameters() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Integer `13`, expected a String at `.description`.", + "message": "Invalid value type at `.description`: expected a string, but found a positive integer: `13`", "code": "invalid_api_key_description", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-description" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_description" } "###); meili_snap::snapshot!(code, @"400 Bad Request"); @@ -1686,10 +1677,10 @@ async fn error_patch_api_key_indexes_invalid_parameters() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Integer `13`, expected a String at `.name`.", + "message": "Invalid value type at `.name`: expected a string, but found a positive integer: `13`", "code": "invalid_api_key_name", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-name" + "link": "https://docs.meilisearch.com/errors#invalid_api_key_name" } "###); meili_snap::snapshot!(code, @"400 Bad Request"); @@ -1705,7 +1696,7 @@ async fn error_access_api_key_routes_no_master_key_set() { "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", "code": "missing_master_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" + "link": "https://docs.meilisearch.com/errors#missing_master_key" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); @@ -1716,7 +1707,7 @@ async fn error_access_api_key_routes_no_master_key_set() { "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", "code": "missing_master_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" + "link": "https://docs.meilisearch.com/errors#missing_master_key" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); @@ -1727,18 +1718,18 @@ async fn error_access_api_key_routes_no_master_key_set() { "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", "code": "missing_master_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" + "link": "https://docs.meilisearch.com/errors#missing_master_key" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); - let (response, code) = server.list_api_keys().await; + let (response, code) = server.list_api_keys("").await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", "code": "missing_master_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" + "link": "https://docs.meilisearch.com/errors#missing_master_key" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); @@ -1751,7 +1742,7 @@ async fn error_access_api_key_routes_no_master_key_set() { "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", "code": "missing_master_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" + "link": "https://docs.meilisearch.com/errors#missing_master_key" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); @@ -1762,7 +1753,7 @@ async fn error_access_api_key_routes_no_master_key_set() { "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", "code": "missing_master_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" + "link": "https://docs.meilisearch.com/errors#missing_master_key" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); @@ -1773,18 +1764,18 @@ async fn error_access_api_key_routes_no_master_key_set() { "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", "code": "missing_master_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" + "link": "https://docs.meilisearch.com/errors#missing_master_key" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); - let (response, code) = server.list_api_keys().await; + let (response, code) = server.list_api_keys("").await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", "code": "missing_master_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" + "link": "https://docs.meilisearch.com/errors#missing_master_key" } "###); meili_snap::snapshot!(code, @"401 Unauthorized"); diff --git a/meilisearch/tests/auth/authorization.rs b/meilisearch/tests/auth/authorization.rs index 8ef2d108d..fae6ee7e1 100644 --- a/meilisearch/tests/auth/authorization.rs +++ b/meilisearch/tests/auth/authorization.rs @@ -73,7 +73,7 @@ static INVALID_RESPONSE: Lazy = Lazy::new(|| { json!({"message": "The provided API key is invalid.", "code": "invalid_api_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" + "link": "https://docs.meilisearch.com/errors#invalid_api_key" }) }); @@ -520,7 +520,7 @@ async fn error_creating_index_without_action() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }); // try to create a index via add documents route diff --git a/meilisearch/tests/auth/errors.rs b/meilisearch/tests/auth/errors.rs new file mode 100644 index 000000000..2ef853d72 --- /dev/null +++ b/meilisearch/tests/auth/errors.rs @@ -0,0 +1,438 @@ +use meili_snap::*; +use serde_json::json; +use uuid::Uuid; + +use crate::common::Server; + +#[actix_rt::test] +async fn create_api_key_bad_description() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.add_api_key(json!({ "description": ["doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.description`: expected a string, but found an array: `[\"doggo\"]`", + "code": "invalid_api_key_description", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_description" + } + "###); +} + +#[actix_rt::test] +async fn create_api_key_bad_name() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.add_api_key(json!({ "name": ["doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.name`: expected a string, but found an array: `[\"doggo\"]`", + "code": "invalid_api_key_name", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_name" + } + "###); +} + +#[actix_rt::test] +async fn create_api_key_bad_uid() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + // bad type + let (response, code) = server.add_api_key(json!({ "uid": ["doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.uid`: expected a string, but found an array: `[\"doggo\"]`", + "code": "invalid_api_key_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_uid" + } + "###); + + // can't parse + let (response, code) = server.add_api_key(json!({ "uid": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value at `.uid`: invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-zA-Z], found `o` at 2", + "code": "invalid_api_key_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_uid" + } + "###); +} + +#[actix_rt::test] +async fn create_api_key_bad_actions() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + // bad type + let (response, code) = server.add_api_key(json!({ "actions": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.actions`: expected an array, but found a string: `\"doggo\"`", + "code": "invalid_api_key_actions", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_actions" + } + "###); + + // can't parse + let (response, code) = server.add_api_key(json!({ "actions": ["doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown value `doggo` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`", + "code": "invalid_api_key_actions", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_actions" + } + "###); +} + +#[actix_rt::test] +async fn create_api_key_bad_indexes() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + // bad type + let (response, code) = server.add_api_key(json!({ "indexes": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.indexes`: expected an array, but found a string: `\"doggo\"`", + "code": "invalid_api_key_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes" + } + "###); + + // can't parse + let (response, code) = server.add_api_key(json!({ "indexes": ["good doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value at `.indexes[0]`: `good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_api_key_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes" + } + "###); +} + +#[actix_rt::test] +async fn create_api_key_bad_expires_at() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + // bad type + let (response, code) = server.add_api_key(json!({ "expires_at": ["doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown field `expires_at`: expected one of `description`, `name`, `uid`, `actions`, `indexes`, `expiresAt`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); + + // can't parse + let (response, code) = server.add_api_key(json!({ "expires_at": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown field `expires_at`: expected one of `description`, `name`, `uid`, `actions`, `indexes`, `expiresAt`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); +} + +#[actix_rt::test] +async fn create_api_key_missing_action() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = + server.add_api_key(json!({ "indexes": ["doggo"], "expiresAt": null })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Missing field `actions`", + "code": "missing_api_key_actions", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_api_key_actions" + } + "###); +} + +#[actix_rt::test] +async fn create_api_key_missing_indexes() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server + .add_api_key(json!({ "uid": Uuid::nil() , "actions": ["*"], "expiresAt": null })) + .await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Missing field `indexes`", + "code": "missing_api_key_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_api_key_indexes" + } + "###); +} + +#[actix_rt::test] +async fn create_api_key_missing_expires_at() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server + .add_api_key(json!({ "uid": Uuid::nil(), "actions": ["*"], "indexes": ["doggo"] })) + .await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Missing field `expiresAt`", + "code": "missing_api_key_expires_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_api_key_expires_at" + } + "###); +} + +#[actix_rt::test] +async fn create_api_key_unexpected_field() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server + .add_api_key(json!({ "uid": Uuid::nil(), "actions": ["*"], "indexes": ["doggo"], "expiresAt": null, "doggo": "bork" })) + .await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown field `doggo`: expected one of `description`, `name`, `uid`, `actions`, `indexes`, `expiresAt`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); +} + +#[actix_rt::test] +async fn list_api_keys_bad_offset() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.list_api_keys("?offset=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `offset`: could not parse `doggo` as a positive integer", + "code": "invalid_api_key_offset", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_offset" + } + "###); +} + +#[actix_rt::test] +async fn list_api_keys_bad_limit() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.list_api_keys("?limit=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer", + "code": "invalid_api_key_limit", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_limit" + } + "###); +} + +#[actix_rt::test] +async fn list_api_keys_unexpected_field() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.list_api_keys("?doggo=no_limit").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown parameter `doggo`: expected one of `offset`, `limit`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); +} + +#[actix_rt::test] +async fn patch_api_keys_bad_description() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.patch_api_key("doggo", json!({ "description": ["doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.description`: expected a string, but found an array: `[\"doggo\"]`", + "code": "invalid_api_key_description", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_description" + } + "###); +} + +#[actix_rt::test] +async fn patch_api_keys_bad_name() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.patch_api_key("doggo", json!({ "name": ["doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.name`: expected a string, but found an array: `[\"doggo\"]`", + "code": "invalid_api_key_name", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_api_key_name" + } + "###); +} + +#[actix_rt::test] +async fn patch_api_keys_immutable_uid() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.patch_api_key("doggo", json!({ "uid": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Immutable field `uid`: expected one of `description`, `name`", + "code": "immutable_api_key_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_api_key_uid" + } + "###); +} + +#[actix_rt::test] +async fn patch_api_keys_immutable_actions() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.patch_api_key("doggo", json!({ "actions": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Immutable field `actions`: expected one of `description`, `name`", + "code": "immutable_api_key_actions", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_api_key_actions" + } + "###); +} + +#[actix_rt::test] +async fn patch_api_keys_immutable_indexes() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.patch_api_key("doggo", json!({ "indexes": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Immutable field `indexes`: expected one of `description`, `name`", + "code": "immutable_api_key_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_api_key_indexes" + } + "###); +} + +#[actix_rt::test] +async fn patch_api_keys_immutable_expires_at() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.patch_api_key("doggo", json!({ "expiresAt": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Immutable field `expiresAt`: expected one of `description`, `name`", + "code": "immutable_api_key_expires_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_api_key_expires_at" + } + "###); +} + +#[actix_rt::test] +async fn patch_api_keys_immutable_created_at() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.patch_api_key("doggo", json!({ "createdAt": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Immutable field `createdAt`: expected one of `description`, `name`", + "code": "immutable_api_key_created_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_api_key_created_at" + } + "###); +} + +#[actix_rt::test] +async fn patch_api_keys_immutable_updated_at() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.patch_api_key("doggo", json!({ "updatedAt": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Immutable field `updatedAt`: expected one of `description`, `name`", + "code": "immutable_api_key_updated_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_api_key_updated_at" + } + "###); +} + +#[actix_rt::test] +async fn patch_api_keys_unknown_field() { + let mut server = Server::new_auth().await; + server.use_admin_key("MASTER_KEY").await; + + let (response, code) = server.patch_api_key("doggo", json!({ "doggo": "bork" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown field `doggo`: expected one of `description`, `name`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); +} diff --git a/meilisearch/tests/auth/mod.rs b/meilisearch/tests/auth/mod.rs index dec02cf1f..422f92d6e 100644 --- a/meilisearch/tests/auth/mod.rs +++ b/meilisearch/tests/auth/mod.rs @@ -1,5 +1,6 @@ mod api_keys; mod authorization; +mod errors; mod payload; mod tenant_token; @@ -16,7 +17,7 @@ impl Server { /// Fetch and use the default admin key for nexts http requests. pub async fn use_admin_key(&mut self, master_key: impl AsRef) { self.use_api_key(master_key); - let (response, code) = self.list_api_keys().await; + let (response, code) = self.list_api_keys("").await; assert_eq!(200, code, "{:?}", response); let admin_key = &response["results"][1]["key"]; self.use_api_key(admin_key.as_str().unwrap()); @@ -37,8 +38,8 @@ impl Server { self.service.patch(url, content).await } - pub async fn list_api_keys(&self) -> (Value, StatusCode) { - let url = "/keys"; + pub async fn list_api_keys(&self, params: &str) -> (Value, StatusCode) { + let url = format!("/keys{params}"); self.service.get(url).await } diff --git a/meilisearch/tests/auth/payload.rs b/meilisearch/tests/auth/payload.rs index 8164acf48..78eec3eb2 100644 --- a/meilisearch/tests/auth/payload.rs +++ b/meilisearch/tests/auth/payload.rs @@ -37,7 +37,7 @@ async fn error_api_key_bad_content_types() { ); assert_eq!(response["code"], "invalid_content_type"); assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type"); + assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type"); // patch let req = test::TestRequest::patch() @@ -59,7 +59,7 @@ async fn error_api_key_bad_content_types() { ); assert_eq!(response["code"], "invalid_content_type"); assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type"); + assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type"); } #[actix_rt::test] @@ -96,7 +96,7 @@ async fn error_api_key_empty_content_types() { ); assert_eq!(response["code"], "invalid_content_type"); assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type"); + assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type"); // patch let req = test::TestRequest::patch() @@ -118,7 +118,7 @@ async fn error_api_key_empty_content_types() { ); assert_eq!(response["code"], "invalid_content_type"); assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type"); + assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type"); } #[actix_rt::test] @@ -154,7 +154,7 @@ async fn error_api_key_missing_content_types() { ); assert_eq!(response["code"], "missing_content_type"); assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing-content-type"); + assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing_content_type"); // patch let req = test::TestRequest::patch() @@ -175,7 +175,7 @@ async fn error_api_key_missing_content_types() { ); assert_eq!(response["code"], "missing_content_type"); assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing-content-type"); + assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing_content_type"); } #[actix_rt::test] @@ -200,7 +200,7 @@ async fn error_api_key_empty_payload() { assert_eq!(status_code, 400); assert_eq!(response["code"], json!("missing_payload")); assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload")); + assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload")); assert_eq!(response["message"], json!(r#"A json payload is missing."#)); // patch @@ -217,7 +217,7 @@ async fn error_api_key_empty_payload() { assert_eq!(status_code, 400); assert_eq!(response["code"], json!("missing_payload")); assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload")); + assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload")); assert_eq!(response["message"], json!(r#"A json payload is missing."#)); } @@ -243,7 +243,7 @@ async fn error_api_key_malformed_payload() { assert_eq!(status_code, 400); assert_eq!(response["code"], json!("malformed_payload")); assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload")); assert_eq!( response["message"], json!( @@ -265,7 +265,7 @@ async fn error_api_key_malformed_payload() { assert_eq!(status_code, 400); assert_eq!(response["code"], json!("malformed_payload")); assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload")); assert_eq!( response["message"], json!( diff --git a/meilisearch/tests/auth/tenant_token.rs b/meilisearch/tests/auth/tenant_token.rs index af3e7c2a5..fbf9d2b49 100644 --- a/meilisearch/tests/auth/tenant_token.rs +++ b/meilisearch/tests/auth/tenant_token.rs @@ -56,7 +56,7 @@ static INVALID_RESPONSE: Lazy = Lazy::new(|| { json!({"message": "The provided API key is invalid.", "code": "invalid_api_key", "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" + "link": "https://docs.meilisearch.com/errors#invalid_api_key" }) }); diff --git a/meilisearch/tests/common/index.rs b/meilisearch/tests/common/index.rs index d42f18d2c..c127af921 100644 --- a/meilisearch/tests/common/index.rs +++ b/meilisearch/tests/common/index.rs @@ -63,6 +63,11 @@ impl Index<'_> { self.service.post_encoded("/indexes", body, self.encoder).await } + pub async fn update_raw(&self, body: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}", urlencode(self.uid.as_ref())); + self.service.patch_encoded(url, body, self.encoder).await + } + pub async fn update(&self, primary_key: Option<&str>) -> (Value, StatusCode) { let body = json!({ "primaryKey": primary_key, @@ -132,7 +137,12 @@ impl Index<'_> { self.service.get(url).await } - pub async fn filtered_tasks(&self, types: &[&str], statuses: &[&str]) -> (Value, StatusCode) { + pub async fn filtered_tasks( + &self, + types: &[&str], + statuses: &[&str], + canceled_by: &[&str], + ) -> (Value, StatusCode) { let mut url = format!("/tasks?indexUids={}", self.uid); if !types.is_empty() { let _ = write!(url, "&types={}", types.join(",")); @@ -140,6 +150,9 @@ impl Index<'_> { if !statuses.is_empty() { let _ = write!(url, "&statuses={}", statuses.join(",")); } + if !canceled_by.is_empty() { + let _ = write!(url, "&canceledBy={}", canceled_by.join(",")); + } self.service.get(url).await } @@ -155,6 +168,11 @@ impl Index<'_> { self.service.get(url).await } + pub async fn get_all_documents_raw(&self, options: &str) -> (Value, StatusCode) { + let url = format!("/indexes/{}/documents{}", urlencode(self.uid.as_ref()), options); + self.service.get(url).await + } + pub async fn get_all_documents(&self, options: GetAllDocumentsOptions) -> (Value, StatusCode) { let mut url = format!("/indexes/{}/documents?", urlencode(self.uid.as_ref())); if let Some(limit) = options.limit { @@ -187,6 +205,11 @@ impl Index<'_> { self.service.post_encoded(url, serde_json::to_value(&ids).unwrap(), self.encoder).await } + pub async fn delete_batch_raw(&self, body: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/documents/delete-batch", urlencode(self.uid.as_ref())); + self.service.post_encoded(url, body, self.encoder).await + } + pub async fn settings(&self) -> (Value, StatusCode) { let url = format!("/indexes/{}/settings", urlencode(self.uid.as_ref())); self.service.get(url).await @@ -289,8 +312,8 @@ impl Index<'_> { eprintln!("Error with post search"); resume_unwind(e); } - - let (response, code) = self.search_get(query).await; + let query = yaup::to_string(&query).unwrap(); + let (response, code) = self.search_get(&query).await; if let Err(e) = catch_unwind(move || test(response, code)) { eprintln!("Error with get search"); resume_unwind(e); @@ -302,9 +325,8 @@ impl Index<'_> { self.service.post_encoded(url, query, self.encoder).await } - pub async fn search_get(&self, query: Value) -> (Value, StatusCode) { - let params = yaup::to_string(&query).unwrap(); - let url = format!("/indexes/{}/search?{}", urlencode(self.uid.as_ref()), params); + pub async fn search_get(&self, query: &str) -> (Value, StatusCode) { + let url = format!("/indexes/{}/search?{}", urlencode(self.uid.as_ref()), query); self.service.get(url).await } diff --git a/meilisearch/tests/common/server.rs b/meilisearch/tests/common/server.rs index c3c9b7c60..f2d645563 100644 --- a/meilisearch/tests/common/server.rs +++ b/meilisearch/tests/common/server.rs @@ -95,10 +95,18 @@ impl Server { self.index_with_encoder(uid, Encoder::Plain) } + pub async fn create_index(&self, body: Value) -> (Value, StatusCode) { + self.service.post("/indexes", body).await + } + pub fn index_with_encoder(&self, uid: impl AsRef, encoder: Encoder) -> Index<'_> { Index { uid: uid.as_ref().to_string(), service: &self.service, encoder } } + pub async fn list_indexes_raw(&self, parameters: &str) -> (Value, StatusCode) { + self.service.get(format!("/indexes{parameters}")).await + } + pub async fn list_indexes( &self, offset: Option, @@ -132,8 +140,8 @@ impl Server { self.service.get("/tasks").await } - pub async fn tasks_filter(&self, filter: Value) -> (Value, StatusCode) { - self.service.get(format!("/tasks?{}", yaup::to_string(&filter).unwrap())).await + pub async fn tasks_filter(&self, filter: &str) -> (Value, StatusCode) { + self.service.get(format!("/tasks?{}", filter)).await } pub async fn get_dump_status(&self, uid: &str) -> (Value, StatusCode) { @@ -148,14 +156,12 @@ impl Server { self.service.post("/swap-indexes", value).await } - pub async fn cancel_tasks(&self, value: Value) -> (Value, StatusCode) { - self.service - .post(format!("/tasks/cancel?{}", yaup::to_string(&value).unwrap()), json!(null)) - .await + pub async fn cancel_tasks(&self, value: &str) -> (Value, StatusCode) { + self.service.post(format!("/tasks/cancel?{}", value), json!(null)).await } - pub async fn delete_tasks(&self, value: Value) -> (Value, StatusCode) { - self.service.delete(format!("/tasks?{}", yaup::to_string(&value).unwrap())).await + pub async fn delete_tasks(&self, value: &str) -> (Value, StatusCode) { + self.service.delete(format!("/tasks?{}", value)).await } pub async fn wait_task(&self, update_id: u64) -> Value { diff --git a/meilisearch/tests/content_type.rs b/meilisearch/tests/content_type.rs index 5759d4d9e..e16a83c06 100644 --- a/meilisearch/tests/content_type.rs +++ b/meilisearch/tests/content_type.rs @@ -88,7 +88,7 @@ async fn error_json_bad_content_type() { "message": r#"A Content-Type header is missing. Accepted values for the Content-Type header are: `application/json`"#, "code": "missing_content_type", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#missing-content-type", + "link": "https://docs.meilisearch.com/errors#missing_content_type", }), "when calling the route `{}` with no content-type", route, @@ -117,7 +117,7 @@ async fn error_json_bad_content_type() { "message": expected_error_message, "code": "invalid_content_type", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-content-type", + "link": "https://docs.meilisearch.com/errors#invalid_content_type", }), "when calling the route `{}` with a content-type of `{}`", route, diff --git a/meilisearch/tests/documents/add_documents.rs b/meilisearch/tests/documents/add_documents.rs index 4af365a7e..e553dcacd 100644 --- a/meilisearch/tests/documents/add_documents.rs +++ b/meilisearch/tests/documents/add_documents.rs @@ -1,4 +1,5 @@ use actix_web::test; +use meili_snap::{json_string, snapshot}; use serde_json::{json, Value}; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; @@ -30,8 +31,17 @@ async fn add_documents_test_json_content_types() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 202); - assert_eq!(response["taskUid"], 0); + snapshot!(status_code, @"202 Accepted"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "taskUid": 0, + "indexUid": "dog", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "enqueuedAt": "[date]" + } + "###); // put let req = test::TestRequest::put() @@ -43,8 +53,17 @@ async fn add_documents_test_json_content_types() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 202); - assert_eq!(response["taskUid"], 1); + snapshot!(status_code, @"202 Accepted"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "taskUid": 1, + "indexUid": "dog", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "enqueuedAt": "[date]" + } + "###); } /// Here we try to send a single document instead of an array with a single document inside. @@ -69,8 +88,17 @@ async fn add_single_document_test_json_content_types() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 202); - assert_eq!(response["taskUid"], 0); + snapshot!(status_code, @"202 Accepted"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "taskUid": 0, + "indexUid": "dog", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "enqueuedAt": "[date]" + } + "###); // put let req = test::TestRequest::put() @@ -82,8 +110,17 @@ async fn add_single_document_test_json_content_types() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 202); - assert_eq!(response["taskUid"], 1); + snapshot!(status_code, @"202 Accepted"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "taskUid": 1, + "indexUid": "dog", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "enqueuedAt": "[date]" + } + "###); } /// Here we try sending encoded (compressed) document request @@ -110,8 +147,17 @@ async fn add_single_document_gzip_encoded() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 202); - assert_eq!(response["taskUid"], 0); + snapshot!(status_code, @"202 Accepted"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "taskUid": 0, + "indexUid": "dog", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "enqueuedAt": "[date]" + } + "###); // put let req = test::TestRequest::put() @@ -124,8 +170,17 @@ async fn add_single_document_gzip_encoded() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 202); - assert_eq!(response["taskUid"], 1); + snapshot!(status_code, @"202 Accepted"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "taskUid": 1, + "indexUid": "dog", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "enqueuedAt": "[date]" + } + "###); } /// Here we try document request with every encoding @@ -184,16 +239,16 @@ async fn error_add_documents_test_bad_content_types() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 415); - assert_eq!( - response["message"], - json!( - r#"The Content-Type `text/plain` is invalid. Accepted values for the Content-Type header are: `application/json`, `application/x-ndjson`, `text/csv`"# - ) - ); - assert_eq!(response["code"], "invalid_content_type"); - assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type"); + snapshot!(status_code, @"415 Unsupported Media Type"); + snapshot!(json_string!(response), + @r###" + { + "message": "The Content-Type `text/plain` is invalid. Accepted values for the Content-Type header are: `application/json`, `application/x-ndjson`, `text/csv`", + "code": "invalid_content_type", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_content_type" + } + "###); // put let req = test::TestRequest::put() @@ -205,16 +260,16 @@ async fn error_add_documents_test_bad_content_types() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 415); - assert_eq!( - response["message"], - json!( - r#"The Content-Type `text/plain` is invalid. Accepted values for the Content-Type header are: `application/json`, `application/x-ndjson`, `text/csv`"# - ) - ); - assert_eq!(response["code"], "invalid_content_type"); - assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type"); + snapshot!(status_code, @"415 Unsupported Media Type"); + snapshot!(json_string!(response), + @r###" + { + "message": "The Content-Type `text/plain` is invalid. Accepted values for the Content-Type header are: `application/json`, `application/x-ndjson`, `text/csv`", + "code": "invalid_content_type", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_content_type" + } + "###); } /// missing content-type must be refused @@ -239,16 +294,16 @@ async fn error_add_documents_test_no_content_type() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 415); - assert_eq!( - response["message"], - json!( - r#"A Content-Type header is missing. Accepted values for the Content-Type header are: `application/json`, `application/x-ndjson`, `text/csv`"# - ) - ); - assert_eq!(response["code"], "missing_content_type"); - assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing-content-type"); + snapshot!(status_code, @"415 Unsupported Media Type"); + snapshot!(json_string!(response), + @r###" + { + "message": "A Content-Type header is missing. Accepted values for the Content-Type header are: `application/json`, `application/x-ndjson`, `text/csv`", + "code": "missing_content_type", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_content_type" + } + "###); // put let req = test::TestRequest::put() @@ -259,16 +314,16 @@ async fn error_add_documents_test_no_content_type() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 415); - assert_eq!( - response["message"], - json!( - r#"A Content-Type header is missing. Accepted values for the Content-Type header are: `application/json`, `application/x-ndjson`, `text/csv`"# - ) - ); - assert_eq!(response["code"], "missing_content_type"); - assert_eq!(response["type"], "invalid_request"); - assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing-content-type"); + snapshot!(status_code, @"415 Unsupported Media Type"); + snapshot!(json_string!(response), + @r###" + { + "message": "A Content-Type header is missing. Accepted values for the Content-Type header are: `application/json`, `application/x-ndjson`, `text/csv`", + "code": "missing_content_type", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_content_type" + } + "###); } #[actix_rt::test] @@ -288,16 +343,16 @@ async fn error_add_malformed_csv_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!( - response["message"], - json!( - r#"The `csv` payload provided is malformed: `CSV error: record 1 (line: 2, byte: 12): found record with 3 fields, but the previous record has 2 fields`."# - ) - ); - assert_eq!(response["code"], json!("malformed_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "The `csv` payload provided is malformed: `CSV error: record 1 (line: 2, byte: 12): found record with 3 fields, but the previous record has 2 fields`.", + "code": "malformed_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#malformed_payload" + } + "###); // put let req = test::TestRequest::put() @@ -309,16 +364,16 @@ async fn error_add_malformed_csv_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!( - response["message"], - json!( - r#"The `csv` payload provided is malformed: `CSV error: record 1 (line: 2, byte: 12): found record with 3 fields, but the previous record has 2 fields`."# - ) - ); - assert_eq!(response["code"], json!("malformed_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "The `csv` payload provided is malformed: `CSV error: record 1 (line: 2, byte: 12): found record with 3 fields, but the previous record has 2 fields`.", + "code": "malformed_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#malformed_payload" + } + "###); } #[actix_rt::test] @@ -338,16 +393,16 @@ async fn error_add_malformed_json_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!( - response["message"], - json!( - r#"The `json` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 1 column 14`."# - ) - ); - assert_eq!(response["code"], json!("malformed_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "The `json` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 1 column 14`.", + "code": "malformed_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#malformed_payload" + } + "###); // put let req = test::TestRequest::put() @@ -359,16 +414,16 @@ async fn error_add_malformed_json_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!( - response["message"], - json!( - r#"The `json` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 1 column 14`."# - ) - ); - assert_eq!(response["code"], json!("malformed_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "The `json` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 1 column 14`.", + "code": "malformed_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#malformed_payload" + } + "###); // truncate @@ -384,16 +439,16 @@ async fn error_add_malformed_json_documents() { let res = test::call_service(&app, req).await; let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!( - response["message"], - json!( - r#"The `json` payload provided is malformed. `Couldn't serialize document value: data are neither an object nor a list of objects`."# - ) - ); - assert_eq!(response["code"], json!("malformed_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "The `json` payload provided is malformed. `Couldn't serialize document value: data are neither an object nor a list of objects`.", + "code": "malformed_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#malformed_payload" + } + "###); // add one more char to the long string to test if the truncating works. let document = format!("\"{}m\"", long); @@ -405,14 +460,16 @@ async fn error_add_malformed_json_documents() { let res = test::call_service(&app, req).await; let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!( - response["message"], - json!("The `json` payload provided is malformed. `Couldn't serialize document value: data are neither an object nor a list of objects`.") - ); - assert_eq!(response["code"], json!("malformed_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "The `json` payload provided is malformed. `Couldn't serialize document value: data are neither an object nor a list of objects`.", + "code": "malformed_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#malformed_payload" + } + "###); } #[actix_rt::test] @@ -432,16 +489,16 @@ async fn error_add_malformed_ndjson_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!( - response["message"], - json!( - r#"The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 2 column 2`."# - ) - ); - assert_eq!(response["code"], json!("malformed_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 2 column 2`.", + "code": "malformed_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#malformed_payload" + } + "###); // put let req = test::TestRequest::put() @@ -453,14 +510,16 @@ async fn error_add_malformed_ndjson_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!( - response["message"], - json!("The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 2 column 2`.") - ); - assert_eq!(response["code"], json!("malformed_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 2 column 2`.", + "code": "malformed_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#malformed_payload" + } + "###); } #[actix_rt::test] @@ -480,11 +539,16 @@ async fn error_add_missing_payload_csv_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!(response["message"], json!(r#"A csv payload is missing."#)); - assert_eq!(response["code"], json!("missing_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "A csv payload is missing.", + "code": "missing_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_payload" + } + "###); // put let req = test::TestRequest::put() @@ -496,11 +560,16 @@ async fn error_add_missing_payload_csv_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!(response["message"], json!(r#"A csv payload is missing."#)); - assert_eq!(response["code"], json!("missing_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "A csv payload is missing.", + "code": "missing_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_payload" + } + "###); } #[actix_rt::test] @@ -520,11 +589,16 @@ async fn error_add_missing_payload_json_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!(response["message"], json!(r#"A json payload is missing."#)); - assert_eq!(response["code"], json!("missing_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "A json payload is missing.", + "code": "missing_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_payload" + } + "###); // put let req = test::TestRequest::put() @@ -536,11 +610,16 @@ async fn error_add_missing_payload_json_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!(response["message"], json!(r#"A json payload is missing."#)); - assert_eq!(response["code"], json!("missing_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "A json payload is missing.", + "code": "missing_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_payload" + } + "###); } #[actix_rt::test] @@ -560,11 +639,16 @@ async fn error_add_missing_payload_ndjson_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!(response["message"], json!(r#"A ndjson payload is missing."#)); - assert_eq!(response["code"], json!("missing_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "A ndjson payload is missing.", + "code": "missing_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_payload" + } + "###); // put let req = test::TestRequest::put() @@ -576,11 +660,16 @@ async fn error_add_missing_payload_ndjson_documents() { let status_code = res.status(); let body = test::read_body(res).await; let response: Value = serde_json::from_slice(&body).unwrap_or_default(); - assert_eq!(status_code, 400); - assert_eq!(response["message"], json!(r#"A ndjson payload is missing."#)); - assert_eq!(response["code"], json!("missing_payload")); - assert_eq!(response["type"], json!("invalid_request")); - assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload")); + snapshot!(status_code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "A ndjson payload is missing.", + "code": "missing_payload", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_payload" + } + "###); } #[actix_rt::test] @@ -596,26 +685,32 @@ async fn add_documents_no_index_creation() { ]); let (response, code) = index.add_documents(documents, None).await; - assert_eq!(code, 202); + snapshot!(code, @"202 Accepted"); assert_eq!(response["taskUid"], 0); - /* - * currently we don’t check these field to stay ISO with meilisearch - * assert_eq!(response["status"], "pending"); - * assert_eq!(response["meta"]["type"], "DocumentsAddition"); - * assert_eq!(response["meta"]["format"], "Json"); - * assert_eq!(response["meta"]["primaryKey"], Value::Null); - * assert!(response.get("enqueuedAt").is_some()); - */ index.wait_task(0).await; let (response, code) = index.get_task(0).await; - assert_eq!(code, 200); - assert_eq!(response["status"], "succeeded"); - assert_eq!(response["uid"], 0); - assert_eq!(response["type"], "documentAdditionOrUpdate"); - assert_eq!(response["details"]["receivedDocuments"], 1); - assert_eq!(response["details"]["indexedDocuments"], 1); + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 0, + "indexUid": "test", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); let processed_at = OffsetDateTime::parse(response["finishedAt"].as_str().unwrap(), &Rfc3339).unwrap(); @@ -625,7 +720,7 @@ async fn add_documents_no_index_creation() { // index was created, and primary key was inferred. let (response, code) = index.get().await; - assert_eq!(code, 200); + snapshot!(code, @"200 OK"); assert_eq!(response["primaryKey"], "id"); } @@ -635,15 +730,16 @@ async fn error_document_add_create_index_bad_uid() { let index = server.index("883 fj!"); let (response, code) = index.add_documents(json!([{"id": 1}]), None).await; - let expected_response = json!({ - "message": "`883 fj!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", - "code": "invalid_index_uid", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-index-uid" - }); - - assert_eq!(code, 400); - assert_eq!(response, expected_response); + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), + @r###" + { + "message": "`883 fj!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); } #[actix_rt::test] @@ -658,21 +754,53 @@ async fn document_addition_with_primary_key() { } ]); let (response, code) = index.add_documents(documents, Some("primary")).await; - assert_eq!(code, 202, "response: {}", response); + snapshot!(code, @"202 Accepted"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "taskUid": 0, + "indexUid": "test", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "enqueuedAt": "[date]" + } + "###); index.wait_task(0).await; let (response, code) = index.get_task(0).await; - assert_eq!(code, 200); - assert_eq!(response["status"], "succeeded"); - assert_eq!(response["uid"], 0); - assert_eq!(response["type"], "documentAdditionOrUpdate"); - assert_eq!(response["details"]["receivedDocuments"], 1); - assert_eq!(response["details"]["indexedDocuments"], 1); + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 0, + "indexUid": "test", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); let (response, code) = index.get().await; - assert_eq!(code, 200); - assert_eq!(response["primaryKey"], "primary"); + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".createdAt" => "[date]", ".updatedAt" => "[date]" }), + @r###" + { + "uid": "test", + "createdAt": "[date]", + "updatedAt": "[date]", + "primaryKey": "primary" + } + "###); } #[actix_rt::test] @@ -688,7 +816,17 @@ async fn replace_document() { ]); let (response, code) = index.add_documents(documents, None).await; - assert_eq!(code, 202, "response: {}", response); + snapshot!(code,@"202 Accepted"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "taskUid": 0, + "indexUid": "test", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "enqueuedAt": "[date]" + } + "###); index.wait_task(0).await; @@ -700,17 +838,41 @@ async fn replace_document() { ]); let (_response, code) = index.add_documents(documents, None).await; - assert_eq!(code, 202); + snapshot!(code,@"202 Accepted"); index.wait_task(1).await; let (response, code) = index.get_task(1).await; - assert_eq!(code, 200); - assert_eq!(response["status"], "succeeded"); + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 1, + "indexUid": "test", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); let (response, code) = index.get_document(1, None).await; - assert_eq!(code, 200); - assert_eq!(response.to_string(), r##"{"doc_id":1,"other":"bar"}"##); + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response), + @r###" + { + "doc_id": 1, + "other": "bar" + } + "###); } #[actix_rt::test] @@ -718,7 +880,7 @@ async fn add_no_documents() { let server = Server::new().await; let index = server.index("test"); let (_response, code) = index.add_documents(json!([]), None).await; - assert_eq!(code, 202); + snapshot!(code, @"202 Accepted"); } #[actix_rt::test] @@ -769,20 +931,31 @@ async fn error_add_documents_bad_document_id() { index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = index.get_task(1).await; - assert_eq!(code, 200); - assert_eq!(response["status"], json!("failed")); - assert_eq!( - response["error"]["message"], - json!( - r#"Document identifier `"foo & bar"` is invalid. A document identifier can be of type integer or string, only composed of alphanumeric characters (a-z A-Z 0-9), hyphens (-) and underscores (_)."# - ) - ); - assert_eq!(response["error"]["code"], json!("invalid_document_id")); - assert_eq!(response["error"]["type"], json!("invalid_request")); - assert_eq!( - response["error"]["link"], - json!("https://docs.meilisearch.com/errors#invalid-document-id") - ); + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 1, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Document identifier `\"foo & bar\"` is invalid. A document identifier can be of type integer or string, only composed of alphanumeric characters (a-z A-Z 0-9), hyphens (-) and underscores (_).", + "code": "invalid_document_id", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_id" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); } #[actix_rt::test] @@ -799,18 +972,31 @@ async fn error_add_documents_missing_document_id() { index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = index.get_task(1).await; - assert_eq!(code, 200); - assert_eq!(response["status"], "failed"); - assert_eq!( - response["error"]["message"], - json!(r#"Document doesn't have a `docid` attribute: `{"id":"11","content":"foobar"}`."#) - ); - assert_eq!(response["error"]["code"], json!("missing_document_id")); - assert_eq!(response["error"]["type"], json!("invalid_request")); - assert_eq!( - response["error"]["link"], - json!("https://docs.meilisearch.com/errors#missing-document-id") - ); + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 1, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Document doesn't have a `docid` attribute: `{\"id\":\"11\",\"content\":\"foobar\"}`.", + "code": "missing_document_id", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_document_id" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); } #[actix_rt::test] @@ -831,22 +1017,14 @@ async fn error_document_field_limit_reached() { let documents = json!([big_object]); let (_response, code) = index.update_documents(documents, Some("id")).await; - assert_eq!(code, 202); + snapshot!(code, @"202"); index.wait_task(0).await; let (response, code) = index.get_task(0).await; - assert_eq!(code, 200); + snapshot!(code, @"200"); // Documents without a primary key are not accepted. - assert_eq!(response["status"], "failed"); - - let expected_error = json!({ - "message": "A document cannot contain more than 65,535 fields.", - "code": "document_fields_limit_reached", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#document-fields-limit-reached" - }); - - assert_eq!(response["error"], expected_error); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @""); } #[actix_rt::test] @@ -856,6 +1034,7 @@ async fn add_documents_invalid_geo_field() { index.create(Some("id")).await; index.update_settings(json!({"sortableAttributes": ["_geo"]})).await; + // _geo is not an object let documents = json!([ { "id": "11", @@ -866,8 +1045,438 @@ async fn add_documents_invalid_geo_field() { index.add_documents(documents, None).await; index.wait_task(2).await; let (response, code) = index.get_task(2).await; - assert_eq!(code, 200); - assert_eq!(response["status"], "failed"); + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 2, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "The `_geo` field in the document with the id: `11` is not an object. Was expecting an object with the `_geo.lat` and `_geo.lng` fields but instead got `\"foobar\"`.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but is missing both the lat and lng + let documents = json!([ + { + "id": "11", + "_geo": {} + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(3).await; + let (response, code) = index.get_task(3).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 3, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not find latitude nor longitude in the document with the id: `11`. Was expecting `_geo.lat` and `_geo.lng` fields.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but is missing both the lat and lng and contains an unexpected field + let documents = json!([ + { + "id": "11", + "_geo": { "doggos": "are good" } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(4).await; + let (response, code) = index.get_task(4).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 4, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not find latitude nor longitude in the document with the id: `11`. Was expecting `_geo.lat` and `_geo.lng` fields.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but only contains the lat + let documents = json!([ + { + "id": "11", + "_geo": { "lat": 12 } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(5).await; + let (response, code) = index.get_task(5).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 5, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not find longitude in the document with the id: `11`. Was expecting a `_geo.lng` field.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but only contains the lng + let documents = json!([ + { + "id": "11", + "_geo": { "lng": 12 } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(6).await; + let (response, code) = index.get_task(6).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 6, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not find latitude in the document with the id: `11`. Was expecting a `_geo.lat` field.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but the lat has a wrong type + let documents = json!([ + { + "id": "11", + "_geo": { "lat": true } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(7).await; + let (response, code) = index.get_task(7).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 7, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not find longitude in the document with the id: `11`. Was expecting a `_geo.lng` field.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but the lng has a wrong type + let documents = json!([ + { + "id": "11", + "_geo": { "lng": true } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(8).await; + let (response, code) = index.get_task(8).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 8, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not find latitude in the document with the id: `11`. Was expecting a `_geo.lat` field.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but the lat and lng have a wrong type + let documents = json!([ + { + "id": "11", + "_geo": { "lat": false, "lng": true } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(9).await; + let (response, code) = index.get_task(9).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 9, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not parse latitude nor longitude in the document with the id: `11`. Was expecting finite numbers but instead got `false` and `true`.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but the lat can't be parsed as a float + let documents = json!([ + { + "id": "11", + "_geo": { "lat": "doggo" } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(10).await; + let (response, code) = index.get_task(10).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 10, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not find longitude in the document with the id: `11`. Was expecting a `_geo.lng` field.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but the lng can't be parsed as a float + let documents = json!([ + { + "id": "11", + "_geo": { "lng": "doggo" } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(11).await; + let (response, code) = index.get_task(11).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 11, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not find latitude in the document with the id: `11`. Was expecting a `_geo.lat` field.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is an object but the lat and lng can't be parsed as a float + let documents = json!([ + { + "id": "11", + "_geo": { "lat": "doggo", "lng": "doggo" } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(12).await; + let (response, code) = index.get_task(12).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 12, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "Could not parse latitude nor longitude in the document with the id: `11`. Was expecting finite numbers but instead got `\"doggo\"` and `\"doggo\"`.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + // _geo is a valid object but contains one extra unknown field + let documents = json!([ + { + "id": "11", + "_geo": { "lat": 1, "lng": 2, "doggo": "are the best" } + } + ]); + + index.add_documents(documents, None).await; + index.wait_task(13).await; + let (response, code) = index.get_task(13).await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "uid": 13, + "indexUid": "test", + "status": "failed", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 0 + }, + "error": { + "message": "The `_geo` field in the document with the id: `11` contains the following unexpected fields: `{\"doggo\":\"are the best\"}`.", + "code": "invalid_document_geo_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_geo_field" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); } #[actix_rt::test] @@ -885,15 +1494,16 @@ async fn error_add_documents_payload_size() { let documents = json!(documents); let (response, code) = index.add_documents(documents, None).await; - let expected_response = json!({ - "message": "The provided payload reached the size limit.", - "code": "payload_too_large", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#payload-too-large" - }); - - assert_eq!(response, expected_response); - assert_eq!(code, 413); + snapshot!(code, @"413 Payload Too Large"); + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), + @r###" + { + "message": "The provided payload reached the size limit.", + "code": "payload_too_large", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#payload_too_large" + } + "###); } #[actix_rt::test] @@ -913,7 +1523,7 @@ async fn error_primary_key_inference() { let (response, code) = index.get_task(0).await; assert_eq!(code, 200); - insta::assert_json_snapshot!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), @r###" { "uid": 0, @@ -923,13 +1533,13 @@ async fn error_primary_key_inference() { "canceledBy": null, "details": { "receivedDocuments": 1, - "indexedDocuments": 1 + "indexedDocuments": 0 }, "error": { "message": "The primary key inference failed as the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", "code": "index_primary_key_no_candidate_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-primary-key-no-candidate-found" + "link": "https://docs.meilisearch.com/errors#index_primary_key_no_candidate_found" }, "duration": "[duration]", "enqueuedAt": "[date]", @@ -953,7 +1563,7 @@ async fn error_primary_key_inference() { let (response, code) = index.get_task(1).await; assert_eq!(code, 200); - insta::assert_json_snapshot!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), @r###" { "uid": 1, @@ -963,13 +1573,13 @@ async fn error_primary_key_inference() { "canceledBy": null, "details": { "receivedDocuments": 1, - "indexedDocuments": 1 + "indexedDocuments": 0 }, "error": { "message": "The primary key inference failed as the engine found 3 fields ending with `id` in their names: 'id' and 'object_id'. Please specify the primary key manually using the `primaryKey` query parameter.", "code": "index_primary_key_multiple_candidates_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-primary-key-multiple-candidates-found" + "link": "https://docs.meilisearch.com/errors#index_primary_key_multiple_candidates_found" }, "duration": "[duration]", "enqueuedAt": "[date]", @@ -991,7 +1601,7 @@ async fn error_primary_key_inference() { let (response, code) = index.get_task(2).await; assert_eq!(code, 200); - insta::assert_json_snapshot!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, + snapshot!(json_string!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }), @r###" { "uid": 2, @@ -1077,7 +1687,7 @@ async fn batch_several_documents_addition() { futures::future::join_all(waiter).await; index.wait_task(9).await; - let (response, _code) = index.filtered_tasks(&[], &["failed"]).await; + let (response, _code) = index.filtered_tasks(&[], &["failed"], &[]).await; // Check if only the 6th task failed println!("{}", &response); diff --git a/meilisearch/tests/documents/delete_documents.rs b/meilisearch/tests/documents/delete_documents.rs index 0abc24eca..e36e2f033 100644 --- a/meilisearch/tests/documents/delete_documents.rs +++ b/meilisearch/tests/documents/delete_documents.rs @@ -95,7 +95,7 @@ async fn error_delete_batch_unexisting_index() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }); assert_eq!(code, 202); diff --git a/meilisearch/tests/documents/errors.rs b/meilisearch/tests/documents/errors.rs new file mode 100644 index 000000000..ffec01062 --- /dev/null +++ b/meilisearch/tests/documents/errors.rs @@ -0,0 +1,99 @@ +use meili_snap::*; +use serde_json::json; + +use crate::common::Server; + +#[actix_rt::test] +async fn get_all_documents_bad_offset() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.get_all_documents_raw("?offset").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `offset`: could not parse `` as a positive integer", + "code": "invalid_document_offset", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_offset" + } + "###); + + let (response, code) = index.get_all_documents_raw("?offset=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `offset`: could not parse `doggo` as a positive integer", + "code": "invalid_document_offset", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_offset" + } + "###); + + let (response, code) = index.get_all_documents_raw("?offset=-1").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `offset`: could not parse `-1` as a positive integer", + "code": "invalid_document_offset", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_offset" + } + "###); +} + +#[actix_rt::test] +async fn get_all_documents_bad_limit() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.get_all_documents_raw("?limit").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `limit`: could not parse `` as a positive integer", + "code": "invalid_document_limit", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_limit" + } + "###); + + let (response, code) = index.get_all_documents_raw("?limit=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer", + "code": "invalid_document_limit", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_limit" + } + "###); + + let (response, code) = index.get_all_documents_raw("?limit=-1").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `limit`: could not parse `-1` as a positive integer", + "code": "invalid_document_limit", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_limit" + } + "###); +} + +#[actix_rt::test] +async fn delete_documents_batch() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.delete_batch_raw(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Json deserialize error: invalid type: string \"doggo\", expected a sequence at line 1 column 7", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); +} diff --git a/meilisearch/tests/documents/get_documents.rs b/meilisearch/tests/documents/get_documents.rs index 69c60dbb4..9bc54973e 100644 --- a/meilisearch/tests/documents/get_documents.rs +++ b/meilisearch/tests/documents/get_documents.rs @@ -27,7 +27,7 @@ async fn error_get_unexisting_document() { "message": "Document `1` not found.", "code": "document_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#document-not-found" + "link": "https://docs.meilisearch.com/errors#document_not_found" }); assert_eq!(response, expected_response); @@ -90,7 +90,7 @@ async fn error_get_unexisting_index_all_documents() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }); assert_eq!(response, expected_response); diff --git a/meilisearch/tests/documents/mod.rs b/meilisearch/tests/documents/mod.rs index 794b57c3a..f6430b108 100644 --- a/meilisearch/tests/documents/mod.rs +++ b/meilisearch/tests/documents/mod.rs @@ -1,4 +1,5 @@ mod add_documents; mod delete_documents; +mod errors; mod get_documents; mod update_documents; diff --git a/meilisearch/tests/documents/update_documents.rs b/meilisearch/tests/documents/update_documents.rs index 3f31eb2e7..688605861 100644 --- a/meilisearch/tests/documents/update_documents.rs +++ b/meilisearch/tests/documents/update_documents.rs @@ -13,7 +13,7 @@ async fn error_document_update_create_index_bad_uid() { "message": "`883 fj!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_index_uid", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" }); assert_eq!(code, 400); @@ -167,7 +167,7 @@ async fn error_update_documents_bad_document_id() { assert_eq!(response["error"]["type"], json!("invalid_request")); assert_eq!( response["error"]["link"], - json!("https://docs.meilisearch.com/errors#invalid-document-id") + json!("https://docs.meilisearch.com/errors#invalid_document_id") ); } @@ -193,6 +193,6 @@ async fn error_update_documents_missing_document_id() { assert_eq!(response["error"]["type"], "invalid_request"); assert_eq!( response["error"]["link"], - "https://docs.meilisearch.com/errors#missing-document-id" + "https://docs.meilisearch.com/errors#missing_document_id" ); } diff --git a/meilisearch/tests/dumps/mod.rs b/meilisearch/tests/dumps/mod.rs index 0759454e8..cbc895f32 100644 --- a/meilisearch/tests/dumps/mod.rs +++ b/meilisearch/tests/dumps/mod.rs @@ -98,14 +98,14 @@ async fn import_dump_v1_movie_with_settings() { assert_eq!(code, 200); assert_eq!( settings, - json!({ "displayedAttributes": ["genres", "id", "overview", "poster", "release_date", "title"], "searchableAttributes": ["title", "overview"], "filterableAttributes": ["genres"], "sortableAttributes": [], "rankingRules": ["typo", "words", "proximity", "attribute", "exactness"], "stopWords": ["of", "the"], "synonyms": {}, "distinctAttribute": null, "typoTolerance": {"enabled": true, "minWordSizeForTypos": { "oneTypo": 5, "twoTypos": 9 }, "disableOnWords": [], "disableOnAttributes": [] }, "faceting": { "maxValuesPerFacet": 100 }, "pagination": { "maxTotalHits": 1000 } }) + json!({ "displayedAttributes": ["genres", "id", "overview", "poster", "release_date", "title"], "searchableAttributes": ["title", "overview"], "filterableAttributes": ["genres"], "sortableAttributes": ["genres"], "rankingRules": ["typo", "words", "proximity", "attribute", "exactness"], "stopWords": ["of", "the"], "synonyms": {}, "distinctAttribute": null, "typoTolerance": {"enabled": true, "minWordSizeForTypos": { "oneTypo": 5, "twoTypos": 9 }, "disableOnWords": [], "disableOnAttributes": [] }, "faceting": { "maxValuesPerFacet": 100 }, "pagination": { "maxTotalHits": 1000 } }) ); let (tasks, code) = index.list_tasks().await; assert_eq!(code, 200); assert_eq!( tasks, - json!({ "results": [{ "uid": 1, "indexUid": "indexUID", "status": "succeeded", "type": "settingsUpdate", "canceledBy": null, "details": { "displayedAttributes": ["genres", "id", "overview", "poster", "release_date", "title"], "searchableAttributes": ["title", "overview"], "filterableAttributes": ["genres"], "stopWords": ["of", "the"] }, "error": null, "duration": "PT7.288826907S", "enqueuedAt": "2021-09-08T09:34:40.882977Z", "startedAt": "2021-09-08T09:34:40.883073093Z", "finishedAt": "2021-09-08T09:34:48.1719Z"}, { "uid": 0, "indexUid": "indexUID", "status": "succeeded", "type": "documentAdditionOrUpdate", "canceledBy": null, "details": { "receivedDocuments": 0, "indexedDocuments": 31968 }, "error": null, "duration": "PT9.090735774S", "enqueuedAt": "2021-09-08T09:34:16.036101Z", "startedAt": "2021-09-08T09:34:16.261191226Z", "finishedAt": "2021-09-08T09:34:25.351927Z" }], "limit": 20, "from": 1, "next": null }) + json!({ "results": [{ "uid": 1, "indexUid": "indexUID", "status": "succeeded", "type": "settingsUpdate", "canceledBy": null, "details": { "displayedAttributes": ["genres", "id", "overview", "poster", "release_date", "title"], "searchableAttributes": ["title", "overview"], "filterableAttributes": ["genres"], "sortableAttributes": ["genres"], "stopWords": ["of", "the"] }, "error": null, "duration": "PT7.288826907S", "enqueuedAt": "2021-09-08T09:34:40.882977Z", "startedAt": "2021-09-08T09:34:40.883073093Z", "finishedAt": "2021-09-08T09:34:48.1719Z"}, { "uid": 0, "indexUid": "indexUID", "status": "succeeded", "type": "documentAdditionOrUpdate", "canceledBy": null, "details": { "receivedDocuments": 0, "indexedDocuments": 31968 }, "error": null, "duration": "PT9.090735774S", "enqueuedAt": "2021-09-08T09:34:16.036101Z", "startedAt": "2021-09-08T09:34:16.261191226Z", "finishedAt": "2021-09-08T09:34:25.351927Z" }], "limit": 20, "from": 1, "next": null }) ); // finally we're just going to check that we can still get a few documents by id @@ -161,7 +161,7 @@ async fn import_dump_v1_rubygems_with_settings() { assert_eq!(code, 200); assert_eq!( settings, - json!({"displayedAttributes": ["description", "id", "name", "summary", "total_downloads", "version"], "searchableAttributes": ["name", "summary"], "filterableAttributes": ["version"], "sortableAttributes": [], "rankingRules": ["typo", "words", "fame:desc", "proximity", "attribute", "exactness", "total_downloads:desc"], "stopWords": [], "synonyms": {}, "distinctAttribute": null, "typoTolerance": {"enabled": true, "minWordSizeForTypos": {"oneTypo": 5, "twoTypos": 9}, "disableOnWords": [], "disableOnAttributes": [] }, "faceting": { "maxValuesPerFacet": 100 }, "pagination": { "maxTotalHits": 1000 }}) + json!({"displayedAttributes": ["description", "id", "name", "summary", "total_downloads", "version"], "searchableAttributes": ["name", "summary"], "filterableAttributes": ["version"], "sortableAttributes": ["version"], "rankingRules": ["typo", "words", "fame:desc", "proximity", "attribute", "exactness", "total_downloads:desc"], "stopWords": [], "synonyms": {}, "distinctAttribute": null, "typoTolerance": {"enabled": true, "minWordSizeForTypos": {"oneTypo": 5, "twoTypos": 9}, "disableOnWords": [], "disableOnAttributes": [] }, "faceting": { "maxValuesPerFacet": 100 }, "pagination": { "maxTotalHits": 1000 }}) ); let (tasks, code) = index.list_tasks().await; @@ -811,7 +811,7 @@ async fn import_dump_v5() { assert_eq!(code, 200); assert_eq!(stats, expected_stats); - let (keys, code) = server.list_api_keys().await; + let (keys, code) = server.list_api_keys("").await; assert_eq!(code, 200); let key = &keys["results"][0]; diff --git a/meilisearch/tests/index/create_index.rs b/meilisearch/tests/index/create_index.rs index 6c5adb5c6..48a07b67f 100644 --- a/meilisearch/tests/index/create_index.rs +++ b/meilisearch/tests/index/create_index.rs @@ -1,6 +1,7 @@ use actix_web::http::header::ContentType; use actix_web::test; use http::header::ACCEPT_ENCODING; +use meili_snap::{json_string, snapshot}; use serde_json::{json, Value}; use crate::common::encoder::Encoder; @@ -176,7 +177,7 @@ async fn error_create_existing_index() { "message": "Index `test` already exists.", "code": "index_already_exists", "type": "invalid_request", - "link":"https://docs.meilisearch.com/errors#index-already-exists" + "link":"https://docs.meilisearch.com/errors#index_already_exists" }); assert_eq!(response["error"], expected_response); @@ -188,13 +189,13 @@ async fn error_create_with_invalid_index_uid() { let index = server.index("test test#!"); let (response, code) = index.create(None).await; - let expected_response = json!({ - "message": "`test test#!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", - "code": "invalid_index_uid", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-index-uid" - }); - - assert_eq!(response, expected_response); - assert_eq!(code, 400); + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value at `.uid`: `test test#!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); } diff --git a/meilisearch/tests/index/delete_index.rs b/meilisearch/tests/index/delete_index.rs index 2c5fb51b2..b6efc7a68 100644 --- a/meilisearch/tests/index/delete_index.rs +++ b/meilisearch/tests/index/delete_index.rs @@ -35,7 +35,7 @@ async fn error_delete_unexisting_index() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }); let response = index.wait_task(0).await; diff --git a/meilisearch/tests/index/errors.rs b/meilisearch/tests/index/errors.rs new file mode 100644 index 000000000..ae17a68f1 --- /dev/null +++ b/meilisearch/tests/index/errors.rs @@ -0,0 +1,265 @@ +use meili_snap::*; +use serde_json::json; + +use crate::common::Server; + +#[actix_rt::test] +async fn get_indexes_bad_offset() { + let server = Server::new().await; + + let (response, code) = server.list_indexes_raw("?offset=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `offset`: could not parse `doggo` as a positive integer", + "code": "invalid_index_offset", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_offset" + } + "###); +} + +#[actix_rt::test] +async fn get_indexes_bad_limit() { + let server = Server::new().await; + + let (response, code) = server.list_indexes_raw("?limit=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer", + "code": "invalid_index_limit", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_limit" + } + "###); +} + +#[actix_rt::test] +async fn get_indexes_unknown_field() { + let server = Server::new().await; + + let (response, code) = server.list_indexes_raw("?doggo=nolimit").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown parameter `doggo`: expected one of `offset`, `limit`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); +} + +#[actix_rt::test] +async fn create_index_missing_uid() { + let server = Server::new().await; + + let (response, code) = server.create_index(json!({ "primaryKey": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Missing field `uid`", + "code": "missing_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_index_uid" + } + "###); +} + +#[actix_rt::test] +async fn create_index_bad_uid() { + let server = Server::new().await; + + let (response, code) = server.create_index(json!({ "uid": "the best doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value at `.uid`: `the best doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); + + let (response, code) = server.create_index(json!({ "uid": true })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.uid`: expected a string, but found a boolean: `true`", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); +} + +#[actix_rt::test] +async fn create_index_bad_primary_key() { + let server = Server::new().await; + + let (response, code) = server + .create_index(json!({ "uid": "doggo", "primaryKey": ["the", "best", "doggo"] })) + .await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.primaryKey`: expected a string, but found an array: `[\"the\",\"best\",\"doggo\"]`", + "code": "invalid_index_primary_key", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_primary_key" + } + "###); +} + +#[actix_rt::test] +async fn create_index_unknown_field() { + let server = Server::new().await; + + let (response, code) = server.create_index(json!({ "uid": "doggo", "doggo": "bernese" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown field `doggo`: expected one of `uid`, `primaryKey`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); +} + +#[actix_rt::test] +async fn get_index_bad_uid() { + let server = Server::new().await; + let index = server.index("the good doggo"); + + let (response, code) = index.get().await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); +} + +#[actix_rt::test] +async fn update_index_bad_primary_key() { + let server = Server::new().await; + let index = server.index("doggo"); + + let (response, code) = index.update_raw(json!({ "primaryKey": ["doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.primaryKey`: expected a string, but found an array: `[\"doggo\"]`", + "code": "invalid_index_primary_key", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_primary_key" + } + "###); +} + +#[actix_rt::test] +async fn update_index_immutable_uid() { + let server = Server::new().await; + let index = server.index("doggo"); + + let (response, code) = index.update_raw(json!({ "uid": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Immutable field `uid`: expected one of `primaryKey`", + "code": "immutable_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_index_uid" + } + "###); +} + +#[actix_rt::test] +async fn update_index_immutable_created_at() { + let server = Server::new().await; + let index = server.index("doggo"); + + let (response, code) = index.update_raw(json!({ "createdAt": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Immutable field `createdAt`: expected one of `primaryKey`", + "code": "immutable_index_created_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_index_created_at" + } + "###); +} + +#[actix_rt::test] +async fn update_index_immutable_updated_at() { + let server = Server::new().await; + let index = server.index("doggo"); + + let (response, code) = index.update_raw(json!({ "updatedAt": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Immutable field `updatedAt`: expected one of `primaryKey`", + "code": "immutable_index_updated_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable_index_updated_at" + } + "###); +} + +#[actix_rt::test] +async fn update_index_unknown_field() { + let server = Server::new().await; + let index = server.index("doggo"); + + let (response, code) = index.update_raw(json!({ "doggo": "bork" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown field `doggo`: expected one of `primaryKey`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); +} + +#[actix_rt::test] +async fn update_index_bad_uid() { + let server = Server::new().await; + let index = server.index("the good doggo"); + + let (response, code) = index.update_raw(json!({ "primaryKey": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); +} + +#[actix_rt::test] +async fn delete_index_bad_uid() { + let server = Server::new().await; + let index = server.index("the good doggo"); + + let (response, code) = index.delete().await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); +} diff --git a/meilisearch/tests/index/get_index.rs b/meilisearch/tests/index/get_index.rs index 7bd8a0184..5a184c8ce 100644 --- a/meilisearch/tests/index/get_index.rs +++ b/meilisearch/tests/index/get_index.rs @@ -1,3 +1,4 @@ +use meili_snap::{json_string, snapshot}; use serde_json::{json, Value}; use crate::common::Server; @@ -34,7 +35,7 @@ async fn error_get_unexisting_index() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }); assert_eq!(response, expected_response); @@ -182,15 +183,13 @@ async fn get_invalid_index_uid() { let index = server.index("this is not a valid index name"); let (response, code) = index.get().await; - assert_eq!(code, 404); - assert_eq!( - response, - json!( - { - "message": "Index `this is not a valid index name` not found.", - "code": "index_not_found", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" - }) - ); + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`this is not a valid index name` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" + } + "###); } diff --git a/meilisearch/tests/index/mod.rs b/meilisearch/tests/index/mod.rs index 9996df2e7..5df5e7e97 100644 --- a/meilisearch/tests/index/mod.rs +++ b/meilisearch/tests/index/mod.rs @@ -1,5 +1,6 @@ mod create_index; mod delete_index; +mod errors; mod get_index; mod stats; mod update_index; diff --git a/meilisearch/tests/index/stats.rs b/meilisearch/tests/index/stats.rs index a5828e32b..813f05b4a 100644 --- a/meilisearch/tests/index/stats.rs +++ b/meilisearch/tests/index/stats.rs @@ -55,7 +55,7 @@ async fn error_get_stats_unexisting_index() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }); assert_eq!(response, expected_response); diff --git a/meilisearch/tests/index/update_index.rs b/meilisearch/tests/index/update_index.rs index cad55babe..3c283407c 100644 --- a/meilisearch/tests/index/update_index.rs +++ b/meilisearch/tests/index/update_index.rs @@ -98,7 +98,7 @@ async fn error_update_existing_primary_key() { "message": "Index already has a primary key: `id`.", "code": "index_primary_key_already_exists", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-primary-key-already-exists" + "link": "https://docs.meilisearch.com/errors#index_primary_key_already_exists" }); assert_eq!(response["error"], expected_response); @@ -117,7 +117,7 @@ async fn error_update_unexisting_index() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }); assert_eq!(response["error"], expected_response); diff --git a/meilisearch/tests/integration.rs b/meilisearch/tests/integration.rs index 25b4e49b6..4383aea57 100644 --- a/meilisearch/tests/integration.rs +++ b/meilisearch/tests/integration.rs @@ -8,6 +8,7 @@ mod search; mod settings; mod snapshot; mod stats; +mod swap_indexes; mod tasks; // Tests are isolated by features in different modules to allow better readability, test diff --git a/meilisearch/tests/search/errors.rs b/meilisearch/tests/search/errors.rs index 3ef342171..9e4dbdcf5 100644 --- a/meilisearch/tests/search/errors.rs +++ b/meilisearch/tests/search/errors.rs @@ -13,7 +13,7 @@ async fn search_unexisting_index() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }); index @@ -46,10 +46,10 @@ async fn search_bad_q() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.q`.", + "message": "Invalid value type at `.q`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_search_q", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-q" + "link": "https://docs.meilisearch.com/errors#invalid_search_q" } "###); // Can't make the `q` fail with a get search since it'll accept anything as a string. @@ -64,21 +64,21 @@ async fn search_bad_offset() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.offset`.", + "message": "Invalid value type at `.offset`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_offset", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-offset" + "link": "https://docs.meilisearch.com/errors#invalid_search_offset" } "###); - let (response, code) = index.search_get(json!({"offset": "doggo"})).await; + let (response, code) = index.search_get("offset=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.offset`.", + "message": "Invalid value in parameter `offset`: could not parse `doggo` as a positive integer", "code": "invalid_search_offset", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-offset" + "link": "https://docs.meilisearch.com/errors#invalid_search_offset" } "###); } @@ -92,21 +92,21 @@ async fn search_bad_limit() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.limit`.", + "message": "Invalid value type at `.limit`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_limit", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-limit" + "link": "https://docs.meilisearch.com/errors#invalid_search_limit" } "###); - let (response, code) = index.search_get(json!({"limit": "doggo"})).await; + let (response, code) = index.search_get("limit=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.limit`.", + "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer", "code": "invalid_search_limit", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-limit" + "link": "https://docs.meilisearch.com/errors#invalid_search_limit" } "###); } @@ -120,21 +120,21 @@ async fn search_bad_page() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.page`.", + "message": "Invalid value type at `.page`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_page", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-page" + "link": "https://docs.meilisearch.com/errors#invalid_search_page" } "###); - let (response, code) = index.search_get(json!({"page": "doggo"})).await; + let (response, code) = index.search_get("page=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.page`.", + "message": "Invalid value in parameter `page`: could not parse `doggo` as a positive integer", "code": "invalid_search_page", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-page" + "link": "https://docs.meilisearch.com/errors#invalid_search_page" } "###); } @@ -148,21 +148,21 @@ async fn search_bad_hits_per_page() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.hitsPerPage`.", + "message": "Invalid value type at `.hitsPerPage`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_hits_per_page", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page" + "link": "https://docs.meilisearch.com/errors#invalid_search_hits_per_page" } "###); - let (response, code) = index.search_get(json!({"hitsPerPage": "doggo"})).await; + let (response, code) = index.search_get("hitsPerPage=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.hitsPerPage`.", + "message": "Invalid value in parameter `hitsPerPage`: could not parse `doggo` as a positive integer", "code": "invalid_search_hits_per_page", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page" + "link": "https://docs.meilisearch.com/errors#invalid_search_hits_per_page" } "###); } @@ -176,10 +176,10 @@ async fn search_bad_attributes_to_crop() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.attributesToCrop`.", + "message": "Invalid value type at `.attributesToCrop`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_search_attributes_to_crop", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-crop" + "link": "https://docs.meilisearch.com/errors#invalid_search_attributes_to_crop" } "###); // Can't make the `attributes_to_crop` fail with a get search since it'll accept anything as an array of strings. @@ -194,21 +194,21 @@ async fn search_bad_crop_length() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.cropLength`.", + "message": "Invalid value type at `.cropLength`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_crop_length", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-crop-length" + "link": "https://docs.meilisearch.com/errors#invalid_search_crop_length" } "###); - let (response, code) = index.search_get(json!({"cropLength": "doggo"})).await; + let (response, code) = index.search_get("cropLength=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.cropLength`.", + "message": "Invalid value in parameter `cropLength`: could not parse `doggo` as a positive integer", "code": "invalid_search_crop_length", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-crop-length" + "link": "https://docs.meilisearch.com/errors#invalid_search_crop_length" } "###); } @@ -222,10 +222,10 @@ async fn search_bad_attributes_to_highlight() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.attributesToHighlight`.", + "message": "Invalid value type at `.attributesToHighlight`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_search_attributes_to_highlight", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-highlight" + "link": "https://docs.meilisearch.com/errors#invalid_search_attributes_to_highlight" } "###); // Can't make the `attributes_to_highlight` fail with a get search since it'll accept anything as an array of strings. @@ -251,7 +251,7 @@ async fn search_bad_filter() { "message": "Invalid syntax for the filter parameter: `expected String, Array, found: true`.", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" } "###); // Can't make the `filter` fail with a get search since it'll accept anything as a strings. @@ -266,10 +266,10 @@ async fn search_bad_sort() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.sort`.", + "message": "Invalid value type at `.sort`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-sort" + "link": "https://docs.meilisearch.com/errors#invalid_search_sort" } "###); // Can't make the `sort` fail with a get search since it'll accept anything as a strings. @@ -284,21 +284,21 @@ async fn search_bad_show_matches_position() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Boolean at `.showMatchesPosition`.", + "message": "Invalid value type at `.showMatchesPosition`: expected a boolean, but found a string: `\"doggo\"`", "code": "invalid_search_show_matches_position", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position" + "link": "https://docs.meilisearch.com/errors#invalid_search_show_matches_position" } "###); - let (response, code) = index.search_get(json!({"showMatchesPosition": "doggo"})).await; + let (response, code) = index.search_get("showMatchesPosition=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "provided string was not `true` or `false` at `.showMatchesPosition`.", + "message": "Invalid value in parameter `showMatchesPosition`: could not parse `doggo` as a boolean, expected either `true` or `false`", "code": "invalid_search_show_matches_position", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position" + "link": "https://docs.meilisearch.com/errors#invalid_search_show_matches_position" } "###); } @@ -312,15 +312,136 @@ async fn search_bad_facets() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.facets`.", + "message": "Invalid value type at `.facets`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_search_facets", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-facets" + "link": "https://docs.meilisearch.com/errors#invalid_search_facets" } "###); // Can't make the `attributes_to_highlight` fail with a get search since it'll accept anything as an array of strings. } +#[actix_rt::test] +async fn search_non_filterable_facets() { + let server = Server::new().await; + let index = server.index("test"); + index.update_settings(json!({"filterableAttributes": ["title"]})).await; + // Wait for the settings update to complete + index.wait_task(0).await; + + let (response, code) = index.search_post(json!({"facets": ["doggo"]})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attribute is `title`.", + "code": "invalid_search_facets", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_facets" + } + "###); + + let (response, code) = index.search_get("facets=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attribute is `title`.", + "code": "invalid_search_facets", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_facets" + } + "###); +} + +#[actix_rt::test] +async fn search_non_filterable_facets_multiple_filterable() { + let server = Server::new().await; + let index = server.index("test"); + index.update_settings(json!({"filterableAttributes": ["title", "genres"]})).await; + index.wait_task(0).await; + + let (response, code) = index.search_post(json!({"facets": ["doggo"]})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attributes are `genres, title`.", + "code": "invalid_search_facets", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_facets" + } + "###); + + let (response, code) = index.search_get("facets=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attributes are `genres, title`.", + "code": "invalid_search_facets", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_facets" + } + "###); +} + +#[actix_rt::test] +async fn search_non_filterable_facets_no_filterable() { + let server = Server::new().await; + let index = server.index("test"); + index.update_settings(json!({"filterableAttributes": []})).await; + index.wait_task(0).await; + + let (response, code) = index.search_post(json!({"facets": ["doggo"]})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid facet distribution, this index does not have configured filterable attributes.", + "code": "invalid_search_facets", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_facets" + } + "###); + + let (response, code) = index.search_get("facets=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid facet distribution, this index does not have configured filterable attributes.", + "code": "invalid_search_facets", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_facets" + } + "###); +} + +#[actix_rt::test] +async fn search_non_filterable_facets_multiple_facets() { + let server = Server::new().await; + let index = server.index("test"); + index.update_settings(json!({"filterableAttributes": ["title", "genres"]})).await; + index.wait_task(0).await; + + let (response, code) = index.search_post(json!({"facets": ["doggo", "neko"]})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid facet distribution, attributes `doggo, neko` are not filterable. The available filterable attributes are `genres, title`.", + "code": "invalid_search_facets", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_facets" + } + "###); + + let (response, code) = index.search_get("facets=doggo,neko").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid facet distribution, attributes `doggo, neko` are not filterable. The available filterable attributes are `genres, title`.", + "code": "invalid_search_facets", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_facets" + } + "###); +} + #[actix_rt::test] async fn search_bad_highlight_pre_tag() { let server = Server::new().await; @@ -330,10 +451,10 @@ async fn search_bad_highlight_pre_tag() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.highlightPreTag`.", + "message": "Invalid value type at `.highlightPreTag`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_search_highlight_pre_tag", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag" + "link": "https://docs.meilisearch.com/errors#invalid_search_highlight_pre_tag" } "###); // Can't make the `highlight_pre_tag` fail with a get search since it'll accept anything as a strings. @@ -348,10 +469,10 @@ async fn search_bad_highlight_post_tag() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.highlightPostTag`.", + "message": "Invalid value type at `.highlightPostTag`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_search_highlight_post_tag", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag" + "link": "https://docs.meilisearch.com/errors#invalid_search_highlight_post_tag" } "###); // Can't make the `highlight_post_tag` fail with a get search since it'll accept anything as a strings. @@ -366,10 +487,10 @@ async fn search_bad_crop_marker() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.cropMarker`.", + "message": "Invalid value type at `.cropMarker`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_search_crop_marker", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-crop-marker" + "link": "https://docs.meilisearch.com/errors#invalid_search_crop_marker" } "###); // Can't make the `crop_marker` fail with a get search since it'll accept anything as a strings. @@ -384,21 +505,32 @@ async fn search_bad_matching_strategy() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown value `doggo`, expected one of `last`, `all` at `.matchingStrategy`.", + "message": "Unknown value `doggo` at `.matchingStrategy`: expected one of `last`, `all`", "code": "invalid_search_matching_strategy", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy" + "link": "https://docs.meilisearch.com/errors#invalid_search_matching_strategy" } "###); - let (response, code) = index.search_get(json!({"matchingStrategy": "doggo"})).await; + let (response, code) = index.search_post(json!({"matchingStrategy": {"doggo": "doggo"}})).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown value `doggo`, expected one of `last`, `all` at `.matchingStrategy`.", + "message": "Invalid value type at `.matchingStrategy`: expected a string, but found an object: `{\"doggo\":\"doggo\"}`", "code": "invalid_search_matching_strategy", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy" + "link": "https://docs.meilisearch.com/errors#invalid_search_matching_strategy" + } + "###); + + let (response, code) = index.search_get("matchingStrategy=doggo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown value `doggo` for parameter `matchingStrategy`: expected one of `last`, `all`", + "code": "invalid_search_matching_strategy", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_matching_strategy" } "###); } @@ -418,7 +550,7 @@ async fn filter_invalid_syntax_object() { "message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `IN`, `NOT IN`, `TO`, `EXISTS`, `NOT EXISTS`, `_geoRadius`, or `_geoBoundingBox` at `title & Glass`.\n1:14 title & Glass", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" }); index .search(json!({"filter": "title & Glass"}), |response, code| { @@ -443,7 +575,7 @@ async fn filter_invalid_syntax_array() { "message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `IN`, `NOT IN`, `TO`, `EXISTS`, `NOT EXISTS`, `_geoRadius`, or `_geoBoundingBox` at `title & Glass`.\n1:14 title & Glass", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" }); index .search(json!({"filter": ["title & Glass"]}), |response, code| { @@ -468,7 +600,7 @@ async fn filter_invalid_syntax_string() { "message": "Found unexpected characters at the end of the filter: `XOR title = Glass`. You probably forgot an `OR` or an `AND` rule.\n15:32 title = Glass XOR title = Glass", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" }); index .search(json!({"filter": "title = Glass XOR title = Glass"}), |response, code| { @@ -493,7 +625,7 @@ async fn filter_invalid_attribute_array() { "message": "Attribute `many` is not filterable. Available filterable attributes are: `title`.\n1:5 many = Glass", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" }); index .search(json!({"filter": ["many = Glass"]}), |response, code| { @@ -518,7 +650,7 @@ async fn filter_invalid_attribute_string() { "message": "Attribute `many` is not filterable. Available filterable attributes are: `title`.\n1:5 many = Glass", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" }); index .search(json!({"filter": "many = Glass"}), |response, code| { @@ -543,7 +675,7 @@ async fn filter_reserved_geo_attribute_array() { "message": "`_geo` is a reserved keyword and thus can't be used as a filter expression. Use the `_geoRadius(latitude, longitude, distance)` or `_geoBoundingBox([latitude, longitude], [latitude, longitude])` built-in rules to filter on `_geo` field coordinates.\n1:5 _geo = Glass", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" }); index .search(json!({"filter": ["_geo = Glass"]}), |response, code| { @@ -568,7 +700,7 @@ async fn filter_reserved_geo_attribute_string() { "message": "`_geo` is a reserved keyword and thus can't be used as a filter expression. Use the `_geoRadius(latitude, longitude, distance)` or `_geoBoundingBox([latitude, longitude], [latitude, longitude])` built-in rules to filter on `_geo` field coordinates.\n1:5 _geo = Glass", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" }); index .search(json!({"filter": "_geo = Glass"}), |response, code| { @@ -593,7 +725,7 @@ async fn filter_reserved_attribute_array() { "message": "`_geoDistance` is a reserved keyword and thus can't be used as a filter expression.\n1:13 _geoDistance = Glass", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" }); index .search(json!({"filter": ["_geoDistance = Glass"]}), |response, code| { @@ -618,7 +750,7 @@ async fn filter_reserved_attribute_string() { "message": "`_geoDistance` is a reserved keyword and thus can't be used as a filter expression.\n1:13 _geoDistance = Glass", "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" }); index .search(json!({"filter": "_geoDistance = Glass"}), |response, code| { @@ -643,7 +775,7 @@ async fn sort_geo_reserved_attribute() { "message": "`_geo` is a reserved keyword and thus can't be used as a sort expression. Use the _geoPoint(latitude, longitude) built-in rule to sort on _geo field coordinates.", "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-sort" + "link": "https://docs.meilisearch.com/errors#invalid_search_sort" }); index .search( @@ -673,7 +805,7 @@ async fn sort_reserved_attribute() { "message": "`_geoDistance` is a reserved keyword and thus can't be used as a sort expression.", "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-sort" + "link": "https://docs.meilisearch.com/errors#invalid_search_sort" }); index .search( @@ -703,7 +835,7 @@ async fn sort_unsortable_attribute() { "message": "Attribute `title` is not sortable. Available sortable attributes are: `id`.", "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-sort" + "link": "https://docs.meilisearch.com/errors#invalid_search_sort" }); index .search( @@ -733,7 +865,7 @@ async fn sort_invalid_syntax() { "message": "Invalid syntax for the sort parameter: expected expression ending by `:asc` or `:desc`, found `title`.", "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-sort" + "link": "https://docs.meilisearch.com/errors#invalid_search_sort" }); index .search( @@ -767,7 +899,7 @@ async fn sort_unset_ranking_rule() { "message": "The sort ranking rule must be specified in the ranking rules settings to use the sort parameter at search time.", "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-search-sort" + "link": "https://docs.meilisearch.com/errors#invalid_search_sort" }); index .search( diff --git a/meilisearch/tests/settings/errors.rs b/meilisearch/tests/settings/errors.rs index 77e62303a..52dadcf98 100644 --- a/meilisearch/tests/settings/errors.rs +++ b/meilisearch/tests/settings/errors.rs @@ -12,10 +12,10 @@ async fn settings_bad_displayed_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.displayedAttributes`.", + "message": "Invalid value type at `.displayedAttributes`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_displayed_attributes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes" + "link": "https://docs.meilisearch.com/errors#invalid_settings_displayed_attributes" } "###); @@ -23,10 +23,10 @@ async fn settings_bad_displayed_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_displayed_attributes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes" + "link": "https://docs.meilisearch.com/errors#invalid_settings_displayed_attributes" } "###); } @@ -40,10 +40,10 @@ async fn settings_bad_searchable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.searchableAttributes`.", + "message": "Invalid value type at `.searchableAttributes`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_searchable_attributes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes" + "link": "https://docs.meilisearch.com/errors#invalid_settings_searchable_attributes" } "###); @@ -51,10 +51,10 @@ async fn settings_bad_searchable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_searchable_attributes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes" + "link": "https://docs.meilisearch.com/errors#invalid_settings_searchable_attributes" } "###); } @@ -68,10 +68,10 @@ async fn settings_bad_filterable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.filterableAttributes`.", + "message": "Invalid value type at `.filterableAttributes`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_filterable_attributes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes" + "link": "https://docs.meilisearch.com/errors#invalid_settings_filterable_attributes" } "###); @@ -79,10 +79,10 @@ async fn settings_bad_filterable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_filterable_attributes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes" + "link": "https://docs.meilisearch.com/errors#invalid_settings_filterable_attributes" } "###); } @@ -96,10 +96,10 @@ async fn settings_bad_sortable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.sortableAttributes`.", + "message": "Invalid value type at `.sortableAttributes`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_sortable_attributes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes" + "link": "https://docs.meilisearch.com/errors#invalid_settings_sortable_attributes" } "###); @@ -107,10 +107,10 @@ async fn settings_bad_sortable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_sortable_attributes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes" + "link": "https://docs.meilisearch.com/errors#invalid_settings_sortable_attributes" } "###); } @@ -124,10 +124,10 @@ async fn settings_bad_ranking_rules() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.rankingRules`.", + "message": "Invalid value type at `.rankingRules`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_ranking_rules", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" + "link": "https://docs.meilisearch.com/errors#invalid_settings_ranking_rules" } "###); @@ -135,10 +135,10 @@ async fn settings_bad_ranking_rules() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_ranking_rules", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" + "link": "https://docs.meilisearch.com/errors#invalid_settings_ranking_rules" } "###); } @@ -152,10 +152,10 @@ async fn settings_bad_stop_words() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.stopWords`.", + "message": "Invalid value type at `.stopWords`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_stop_words", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words" + "link": "https://docs.meilisearch.com/errors#invalid_settings_stop_words" } "###); @@ -163,10 +163,10 @@ async fn settings_bad_stop_words() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_stop_words", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words" + "link": "https://docs.meilisearch.com/errors#invalid_settings_stop_words" } "###); } @@ -180,10 +180,10 @@ async fn settings_bad_synonyms() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at `.synonyms`.", + "message": "Invalid value type at `.synonyms`: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_synonyms", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms" + "link": "https://docs.meilisearch.com/errors#invalid_settings_synonyms" } "###); @@ -191,10 +191,10 @@ async fn settings_bad_synonyms() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_synonyms", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms" + "link": "https://docs.meilisearch.com/errors#invalid_settings_synonyms" } "###); } @@ -208,10 +208,10 @@ async fn settings_bad_distinct_attribute() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.distinctAttribute`.", + "message": "Invalid value type at `.distinctAttribute`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_settings_distinct_attribute", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute" + "link": "https://docs.meilisearch.com/errors#invalid_settings_distinct_attribute" } "###); @@ -219,10 +219,10 @@ async fn settings_bad_distinct_attribute() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at ``.", + "message": "Invalid value type: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_settings_distinct_attribute", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute" + "link": "https://docs.meilisearch.com/errors#invalid_settings_distinct_attribute" } "###); } @@ -236,10 +236,22 @@ async fn settings_bad_typo_tolerance() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at `.typoTolerance`.", + "message": "Invalid value type at `.typoTolerance`: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_typo_tolerance", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance" + "link": "https://docs.meilisearch.com/errors#invalid_settings_typo_tolerance" + } + "###); + + let (response, code) = + index.update_settings(json!({ "typoTolerance": { "minWordSizeForTypos": "doggo" }})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `.typoTolerance.minWordSizeForTypos`: expected an object, but found a string: `\"doggo\"`", + "code": "invalid_settings_typo_tolerance", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_settings_typo_tolerance" } "###); @@ -247,10 +259,25 @@ async fn settings_bad_typo_tolerance() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_typo_tolerance", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance" + "link": "https://docs.meilisearch.com/errors#invalid_settings_typo_tolerance" + } + "###); + + let (response, code) = index + .update_settings_typo_tolerance( + json!({ "typoTolerance": { "minWordSizeForTypos": "doggo" }}), + ) + .await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Unknown field `typoTolerance`: expected one of `enabled`, `minWordSizeForTypos`, `disableOnWords`, `disableOnAttributes`", + "code": "invalid_settings_typo_tolerance", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_settings_typo_tolerance" } "###); } @@ -264,10 +291,10 @@ async fn settings_bad_faceting() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at `.faceting`.", + "message": "Invalid value type at `.faceting`: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_faceting", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-faceting" + "link": "https://docs.meilisearch.com/errors#invalid_settings_faceting" } "###); @@ -275,10 +302,10 @@ async fn settings_bad_faceting() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_faceting", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-faceting" + "link": "https://docs.meilisearch.com/errors#invalid_settings_faceting" } "###); } @@ -292,10 +319,10 @@ async fn settings_bad_pagination() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at `.pagination`.", + "message": "Invalid value type at `.pagination`: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_pagination", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-pagination" + "link": "https://docs.meilisearch.com/errors#invalid_settings_pagination" } "###); @@ -303,10 +330,10 @@ async fn settings_bad_pagination() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_pagination", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-pagination" + "link": "https://docs.meilisearch.com/errors#invalid_settings_pagination" } "###); } diff --git a/meilisearch/tests/settings/get_settings.rs b/meilisearch/tests/settings/get_settings.rs index 3ac7d3801..88e395b57 100644 --- a/meilisearch/tests/settings/get_settings.rs +++ b/meilisearch/tests/settings/get_settings.rs @@ -185,7 +185,7 @@ async fn error_update_setting_unexisting_index_invalid_uid() { "message": "`test##! ` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_index_uid", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" } "###); } @@ -282,10 +282,10 @@ async fn error_set_invalid_ranking_rules() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "`manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.", + "message": "Invalid value at `.rankingRules[0]`: `manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules.", "code": "invalid_settings_ranking_rules", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" + "link": "https://docs.meilisearch.com/errors#invalid_settings_ranking_rules" } "###); } diff --git a/meilisearch/tests/swap_indexes/errors.rs b/meilisearch/tests/swap_indexes/errors.rs new file mode 100644 index 000000000..03625fd08 --- /dev/null +++ b/meilisearch/tests/swap_indexes/errors.rs @@ -0,0 +1,94 @@ +use meili_snap::*; +use serde_json::json; + +use crate::common::Server; + +#[actix_rt::test] +async fn swap_indexes_bad_format() { + let server = Server::new().await; + + let (response, code) = server.index_swap(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); + + let (response, code) = server.index_swap(json!(["doggo"])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `[0]`: expected an object, but found a string: `\"doggo\"`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad_request" + } + "###); +} + +#[actix_rt::test] +async fn swap_indexes_bad_indexes() { + let server = Server::new().await; + + let (response, code) = server.index_swap(json!([{ "indexes": "doggo"}])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value type at `[0].indexes`: expected an array, but found a string: `\"doggo\"`", + "code": "invalid_swap_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_swap_indexes" + } + "###); + + let (response, code) = server.index_swap(json!([{ "indexes": ["doggo"]}])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Two indexes must be given for each swap. The list `[\"doggo\"]` contains 1 indexes.", + "code": "invalid_swap_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_swap_indexes" + } + "###); + + let (response, code) = + server.index_swap(json!([{ "indexes": ["doggo", "crabo", "croco"]}])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Two indexes must be given for each swap. The list `[\"doggo\", \"crabo\", \"croco\"]` contains 3 indexes.", + "code": "invalid_swap_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_swap_indexes" + } + "###); + + let (response, code) = server.index_swap(json!([{ "indexes": ["doggo", "doggo"]}])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Indexes must be declared only once during a swap. `doggo` was specified several times.", + "code": "invalid_swap_duplicate_index_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_swap_duplicate_index_found" + } + "###); + + let (response, code) = server + .index_swap(json!([{ "indexes": ["doggo", "catto"]}, { "indexes": ["girafo", "doggo"]}])) + .await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Indexes must be declared only once during a swap. `doggo` was specified several times.", + "code": "invalid_swap_duplicate_index_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_swap_duplicate_index_found" + } + "###); +} diff --git a/meilisearch/tests/swap_indexes/mod.rs b/meilisearch/tests/swap_indexes/mod.rs new file mode 100644 index 000000000..42d92e2af --- /dev/null +++ b/meilisearch/tests/swap_indexes/mod.rs @@ -0,0 +1,357 @@ +mod errors; + +use meili_snap::{json_string, snapshot}; +use serde_json::json; + +use crate::common::{GetAllDocumentsOptions, Server}; + +#[actix_rt::test] +async fn swap_indexes() { + let server = Server::new().await; + let a = server.index("a"); + let (_, code) = a.add_documents(json!({ "id": 1, "index": "a"}), None).await; + snapshot!(code, @"202 Accepted"); + let b = server.index("b"); + let (res, code) = b.add_documents(json!({ "id": 1, "index": "b"}), None).await; + snapshot!(code, @"202 Accepted"); + snapshot!(res["taskUid"], @"1"); + server.wait_task(1).await; + + let (tasks, code) = server.tasks().await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(tasks, { ".results[].duration" => "[duration]", ".results[].enqueuedAt" => "[date]", ".results[].startedAt" => "[date]", ".results[].finishedAt" => "[date]" }), @r###" + { + "results": [ + { + "uid": 1, + "indexUid": "b", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 0, + "indexUid": "a", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + ], + "limit": 20, + "from": 1, + "next": null + } + "###); + + let (res, code) = server.index_swap(json!([{ "indexes": ["a", "b"] }])).await; + snapshot!(code, @"202 Accepted"); + snapshot!(res["taskUid"], @"2"); + server.wait_task(2).await; + + let (tasks, code) = server.tasks().await; + snapshot!(code, @"200 OK"); + + // Notice how the task 0 which was initially representing the creation of the index `A` now represents the creation of the index `B`. + snapshot!(json_string!(tasks, { ".results[].duration" => "[duration]", ".results[].enqueuedAt" => "[date]", ".results[].startedAt" => "[date]", ".results[].finishedAt" => "[date]" }), @r###" + { + "results": [ + { + "uid": 2, + "indexUid": null, + "status": "succeeded", + "type": "indexSwap", + "canceledBy": null, + "details": { + "swaps": [ + { + "indexes": [ + "a", + "b" + ] + } + ] + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 1, + "indexUid": "a", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 0, + "indexUid": "b", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + ], + "limit": 20, + "from": 2, + "next": null + } + "###); + + // BUT, the data in `a` should now points to the data that was in `b`. + // And the opposite is true as well + let (res, _) = a.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"b"}]"###); + let (res, _) = b.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"a"}]"###); + + // ================ + // And now we're going to attempt the famous and dangerous DOUBLE index swap 🤞 + + let c = server.index("c"); + let (res, code) = c.add_documents(json!({ "id": 1, "index": "c"}), None).await; + snapshot!(code, @"202 Accepted"); + snapshot!(res["taskUid"], @"3"); + let d = server.index("d"); + let (res, code) = d.add_documents(json!({ "id": 1, "index": "d"}), None).await; + snapshot!(code, @"202 Accepted"); + snapshot!(res["taskUid"], @"4"); + server.wait_task(4).await; + + // ensure the index creation worked properly + let (tasks, code) = server.tasks_filter("limit=2").await; + snapshot!(code, @"200 OK"); + snapshot!(json_string!(tasks, { ".results[].duration" => "[duration]", ".results[].enqueuedAt" => "[date]", ".results[].startedAt" => "[date]", ".results[].finishedAt" => "[date]" }), @r###" + { + "results": [ + { + "uid": 4, + "indexUid": "d", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 3, + "indexUid": "c", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + ], + "limit": 2, + "from": 4, + "next": 2 + } + "###); + + // It's happening 😲 + + let (res, code) = + server.index_swap(json!([{ "indexes": ["a", "b"] }, { "indexes": ["c", "d"] } ])).await; + snapshot!(res["taskUid"], @"5"); + snapshot!(code, @"202 Accepted"); + server.wait_task(5).await; + + // ensure the index creation worked properly + let (tasks, code) = server.tasks().await; + snapshot!(code, @"200 OK"); + + // What should we check for each tasks in this test: + // Task number; + // 0. should have the indexUid `a` again + // 1. should have the indexUid `b` again + // 2. stays unchanged + // 3. now have the indexUid `d` instead of `c` + // 4. now have the indexUid `c` instead of `d` + snapshot!(json_string!(tasks, { ".results[].duration" => "[duration]", ".results[].enqueuedAt" => "[date]", ".results[].startedAt" => "[date]", ".results[].finishedAt" => "[date]" }), @r###" + { + "results": [ + { + "uid": 5, + "indexUid": null, + "status": "succeeded", + "type": "indexSwap", + "canceledBy": null, + "details": { + "swaps": [ + { + "indexes": [ + "a", + "b" + ] + }, + { + "indexes": [ + "c", + "d" + ] + } + ] + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 4, + "indexUid": "c", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 3, + "indexUid": "d", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 2, + "indexUid": null, + "status": "succeeded", + "type": "indexSwap", + "canceledBy": null, + "details": { + "swaps": [ + { + "indexes": [ + "b", + "a" + ] + } + ] + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 1, + "indexUid": "b", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + }, + { + "uid": 0, + "indexUid": "a", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "canceledBy": null, + "details": { + "receivedDocuments": 1, + "indexedDocuments": 1 + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + ], + "limit": 20, + "from": 5, + "next": null + } + "###); + + // - The data in `a` should point to `a`. + // - The data in `b` should point to `b`. + // - The data in `c` should point to `d`. + // - The data in `d` should point to `c`. + let (res, _) = a.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"a"}]"###); + let (res, _) = b.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"b"}]"###); + let (res, _) = c.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"d"}]"###); + let (res, _) = d.get_all_documents(GetAllDocumentsOptions::default()).await; + snapshot!(res["results"], @r###"[{"id":1,"index":"c"}]"###); +} diff --git a/meilisearch/tests/tasks/errors.rs b/meilisearch/tests/tasks/errors.rs index 305ab8b9c..830c4c8e7 100644 --- a/meilisearch/tests/tasks/errors.rs +++ b/meilisearch/tests/tasks/errors.rs @@ -1,5 +1,4 @@ use meili_snap::*; -use serde_json::json; use crate::common::Server; @@ -7,36 +6,47 @@ use crate::common::Server; async fn task_bad_uids() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"uids": "doggo"})).await; + let (response, code) = server.tasks_filter("uids=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `doggo` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" } "###); - let (response, code) = server.cancel_tasks(json!({"uids": "doggo"})).await; + let (response, code) = server.cancel_tasks("uids=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `doggo` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" } "###); - let (response, code) = server.delete_tasks(json!({"uids": "doggo"})).await; + let (response, code) = server.delete_tasks("uids=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `doggo` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" + } + "###); + + let (response, code) = server.delete_tasks("uids=1,dogo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `uids[1]`: could not parse `dogo` as a positive integer", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" } "###); } @@ -45,36 +55,36 @@ async fn task_bad_uids() { async fn task_bad_canceled_by() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"canceledBy": "doggo"})).await; + let (response, code) = server.tasks_filter("canceledBy=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.canceledBy`.", + "message": "Invalid value in parameter `canceledBy`: could not parse `doggo` as a positive integer", "code": "invalid_task_canceled_by", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" + "link": "https://docs.meilisearch.com/errors#invalid_task_canceled_by" } "###); - let (response, code) = server.cancel_tasks(json!({"canceledBy": "doggo"})).await; + let (response, code) = server.cancel_tasks("canceledBy=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.canceledBy`.", + "message": "Invalid value in parameter `canceledBy`: could not parse `doggo` as a positive integer", "code": "invalid_task_canceled_by", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" + "link": "https://docs.meilisearch.com/errors#invalid_task_canceled_by" } "###); - let (response, code) = server.delete_tasks(json!({"canceledBy": "doggo"})).await; + let (response, code) = server.delete_tasks("canceledBy=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.canceledBy`.", + "message": "Invalid value in parameter `canceledBy`: could not parse `doggo` as a positive integer", "code": "invalid_task_canceled_by", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" + "link": "https://docs.meilisearch.com/errors#invalid_task_canceled_by" } "###); } @@ -83,36 +93,36 @@ async fn task_bad_canceled_by() { async fn task_bad_types() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"types": "doggo"})).await; + let (response, code) = server.tasks_filter("types=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", + "message": "Invalid value in parameter `types`: `doggo` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", "code": "invalid_task_types", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-types" + "link": "https://docs.meilisearch.com/errors#invalid_task_types" } "###); - let (response, code) = server.cancel_tasks(json!({"types": "doggo"})).await; + let (response, code) = server.cancel_tasks("types=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", + "message": "Invalid value in parameter `types`: `doggo` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", "code": "invalid_task_types", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-types" + "link": "https://docs.meilisearch.com/errors#invalid_task_types" } "###); - let (response, code) = server.delete_tasks(json!({"types": "doggo"})).await; + let (response, code) = server.delete_tasks("types=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", + "message": "Invalid value in parameter `types`: `doggo` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", "code": "invalid_task_types", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-types" + "link": "https://docs.meilisearch.com/errors#invalid_task_types" } "###); } @@ -121,36 +131,36 @@ async fn task_bad_types() { async fn task_bad_statuses() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"statuses": "doggo"})).await; + let (response, code) = server.tasks_filter("statuses=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", + "message": "Invalid value in parameter `statuses`: `doggo` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", "code": "invalid_task_statuses", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" + "link": "https://docs.meilisearch.com/errors#invalid_task_statuses" } "###); - let (response, code) = server.cancel_tasks(json!({"statuses": "doggo"})).await; + let (response, code) = server.cancel_tasks("statuses=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", + "message": "Invalid value in parameter `statuses`: `doggo` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", "code": "invalid_task_statuses", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" + "link": "https://docs.meilisearch.com/errors#invalid_task_statuses" } "###); - let (response, code) = server.delete_tasks(json!({"statuses": "doggo"})).await; + let (response, code) = server.delete_tasks("statuses=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", + "message": "Invalid value in parameter `statuses`: `doggo` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", "code": "invalid_task_statuses", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" + "link": "https://docs.meilisearch.com/errors#invalid_task_statuses" } "###); } @@ -159,36 +169,36 @@ async fn task_bad_statuses() { async fn task_bad_index_uids() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"indexUids": "the good doggo"})).await; + let (response, code) = server.tasks_filter("indexUids=the%20good%20doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", + "message": "Invalid value in parameter `indexUids`: `the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_index_uid", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" } "###); - let (response, code) = server.cancel_tasks(json!({"indexUids": "the good doggo"})).await; + let (response, code) = server.cancel_tasks("indexUids=the%20good%20doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", + "message": "Invalid value in parameter `indexUids`: `the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_index_uid", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" } "###); - let (response, code) = server.delete_tasks(json!({"indexUids": "the good doggo"})).await; + let (response, code) = server.delete_tasks("indexUids=the%20good%20doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", + "message": "Invalid value in parameter `indexUids`: `the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_index_uid", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + "link": "https://docs.meilisearch.com/errors#invalid_index_uid" } "###); } @@ -197,36 +207,36 @@ async fn task_bad_index_uids() { async fn task_bad_limit() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"limit": "doggo"})).await; + let (response, code) = server.tasks_filter("limit=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.limit`.", + "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer", "code": "invalid_task_limit", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-limit" + "link": "https://docs.meilisearch.com/errors#invalid_task_limit" } "###); - let (response, code) = server.cancel_tasks(json!({"limit": "doggo"})).await; + let (response, code) = server.cancel_tasks("limit=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#bad_request" } "###); - let (response, code) = server.delete_tasks(json!({"limit": "doggo"})).await; + let (response, code) = server.delete_tasks("limit=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#bad_request" } "###); } @@ -235,36 +245,36 @@ async fn task_bad_limit() { async fn task_bad_from() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"from": "doggo"})).await; + let (response, code) = server.tasks_filter("from=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.from`.", + "message": "Invalid value in parameter `from`: could not parse `doggo` as a positive integer", "code": "invalid_task_from", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-from" + "link": "https://docs.meilisearch.com/errors#invalid_task_from" } "###); - let (response, code) = server.cancel_tasks(json!({"from": "doggo"})).await; + let (response, code) = server.cancel_tasks("from=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#bad_request" } "###); - let (response, code) = server.delete_tasks(json!({"from": "doggo"})).await; + let (response, code) = server.delete_tasks("from=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#bad_request" } "###); } @@ -273,36 +283,36 @@ async fn task_bad_from() { async fn task_bad_after_enqueued_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"afterEnqueuedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("afterEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", + "message": "Invalid value in parameter `afterEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_enqueued_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_after_enqueued_at" } "###); - let (response, code) = server.cancel_tasks(json!({"afterEnqueuedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("afterEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", + "message": "Invalid value in parameter `afterEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_enqueued_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_after_enqueued_at" } "###); - let (response, code) = server.delete_tasks(json!({"afterEnqueuedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("afterEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", + "message": "Invalid value in parameter `afterEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_enqueued_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_after_enqueued_at" } "###); } @@ -311,36 +321,36 @@ async fn task_bad_after_enqueued_at() { async fn task_bad_before_enqueued_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"beforeEnqueuedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("beforeEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", + "message": "Invalid value in parameter `beforeEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_enqueued_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_enqueued_at" } "###); - let (response, code) = server.cancel_tasks(json!({"beforeEnqueuedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("beforeEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", + "message": "Invalid value in parameter `beforeEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_enqueued_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_enqueued_at" } "###); - let (response, code) = server.delete_tasks(json!({"beforeEnqueuedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("beforeEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", + "message": "Invalid value in parameter `beforeEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_enqueued_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_enqueued_at" } "###); } @@ -349,36 +359,36 @@ async fn task_bad_before_enqueued_at() { async fn task_bad_after_started_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"afterStartedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("afterStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", + "message": "Invalid value in parameter `afterStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_started_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_after_started_at" } "###); - let (response, code) = server.cancel_tasks(json!({"afterStartedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("afterStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", + "message": "Invalid value in parameter `afterStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_started_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_after_started_at" } "###); - let (response, code) = server.delete_tasks(json!({"afterStartedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("afterStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", + "message": "Invalid value in parameter `afterStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_started_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_after_started_at" } "###); } @@ -387,36 +397,36 @@ async fn task_bad_after_started_at() { async fn task_bad_before_started_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"beforeStartedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("beforeStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "message": "Invalid value in parameter `beforeStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_started_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_started_at" } "###); - let (response, code) = server.cancel_tasks(json!({"beforeStartedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("beforeStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "message": "Invalid value in parameter `beforeStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_started_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_started_at" } "###); - let (response, code) = server.delete_tasks(json!({"beforeStartedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("beforeStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "message": "Invalid value in parameter `beforeStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_started_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_started_at" } "###); } @@ -425,36 +435,36 @@ async fn task_bad_before_started_at() { async fn task_bad_after_finished_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"afterFinishedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("afterFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", + "message": "Invalid value in parameter `afterFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_finished_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_after_finished_at" } "###); - let (response, code) = server.cancel_tasks(json!({"afterFinishedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("afterFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", + "message": "Invalid value in parameter `afterFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_finished_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_after_finished_at" } "###); - let (response, code) = server.delete_tasks(json!({"afterFinishedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("afterFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", + "message": "Invalid value in parameter `afterFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_finished_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_after_finished_at" } "###); } @@ -463,36 +473,36 @@ async fn task_bad_after_finished_at() { async fn task_bad_before_finished_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"beforeFinishedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("beforeFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", + "message": "Invalid value in parameter `beforeFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_finished_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_finished_at" } "###); - let (response, code) = server.cancel_tasks(json!({"beforeFinishedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("beforeFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", + "message": "Invalid value in parameter `beforeFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_finished_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_finished_at" } "###); - let (response, code) = server.delete_tasks(json!({"beforeFinishedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("beforeFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", + "message": "Invalid value in parameter `beforeFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_finished_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_finished_at" } "###); } diff --git a/meilisearch/tests/tasks/mod.rs b/meilisearch/tests/tasks/mod.rs index 46775e05f..e9b5a2325 100644 --- a/meilisearch/tests/tasks/mod.rs +++ b/meilisearch/tests/tasks/mod.rs @@ -19,7 +19,7 @@ async fn error_get_unexisting_task_status() { "message": "Task `1` not found.", "code": "task_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#task-not-found" + "link": "https://docs.meilisearch.com/errors#task_not_found" }); assert_eq!(response, expected_response); @@ -115,7 +115,7 @@ async fn list_tasks_status_filtered() { .add_documents(serde_json::from_str(include_str!("../assets/test_set.json")).unwrap(), None) .await; - let (response, code) = index.filtered_tasks(&[], &["succeeded"]).await; + let (response, code) = index.filtered_tasks(&[], &["succeeded"], &[]).await; assert_eq!(code, 200, "{}", response); assert_eq!(response["results"].as_array().unwrap().len(), 1); @@ -126,7 +126,7 @@ async fn list_tasks_status_filtered() { index.wait_task(1).await; - let (response, code) = index.filtered_tasks(&[], &["succeeded"]).await; + let (response, code) = index.filtered_tasks(&[], &["succeeded"], &[]).await; assert_eq!(code, 200, "{}", response); assert_eq!(response["results"].as_array().unwrap().len(), 2); } @@ -141,16 +141,31 @@ async fn list_tasks_type_filtered() { .add_documents(serde_json::from_str(include_str!("../assets/test_set.json")).unwrap(), None) .await; - let (response, code) = index.filtered_tasks(&["indexCreation"], &[]).await; + let (response, code) = index.filtered_tasks(&["indexCreation"], &[], &[]).await; assert_eq!(code, 200, "{}", response); assert_eq!(response["results"].as_array().unwrap().len(), 1); let (response, code) = - index.filtered_tasks(&["indexCreation", "documentAdditionOrUpdate"], &[]).await; + index.filtered_tasks(&["indexCreation", "documentAdditionOrUpdate"], &[], &[]).await; assert_eq!(code, 200, "{}", response); assert_eq!(response["results"].as_array().unwrap().len(), 2); } +#[actix_rt::test] +async fn list_tasks_invalid_canceled_by_filter() { + let server = Server::new().await; + let index = server.index("test"); + index.create(None).await; + index.wait_task(0).await; + index + .add_documents(serde_json::from_str(include_str!("../assets/test_set.json")).unwrap(), None) + .await; + + let (response, code) = index.filtered_tasks(&[], &[], &["0"]).await; + assert_eq!(code, 200, "{}", response); + assert_eq!(response["results"].as_array().unwrap().len(), 0); +} + #[actix_rt::test] async fn list_tasks_status_and_type_filtered() { let server = Server::new().await; @@ -161,7 +176,7 @@ async fn list_tasks_status_and_type_filtered() { .add_documents(serde_json::from_str(include_str!("../assets/test_set.json")).unwrap(), None) .await; - let (response, code) = index.filtered_tasks(&["indexCreation"], &["failed"]).await; + let (response, code) = index.filtered_tasks(&["indexCreation"], &["failed"], &[]).await; assert_eq!(code, 200, "{}", response); assert_eq!(response["results"].as_array().unwrap().len(), 0); @@ -169,6 +184,7 @@ async fn list_tasks_status_and_type_filtered() { .filtered_tasks( &["indexCreation", "documentAdditionOrUpdate"], &["succeeded", "processing", "enqueued"], + &[], ) .await; assert_eq!(code, 200, "{}", response); @@ -179,47 +195,47 @@ async fn list_tasks_status_and_type_filtered() { async fn get_task_filter_error() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!( { "lol": "pied" })).await; + let (response, code) = server.tasks_filter("lol=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `lol`, expected one of `limit`, `from`, `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `lol`: expected one of `limit`, `from`, `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#bad_request" } "###); - let (response, code) = server.tasks_filter(json!( { "uids": "pied" })).await; + let (response, code) = server.tasks_filter("uids=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `pied` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" } "###); - let (response, code) = server.tasks_filter(json!( { "from": "pied" })).await; + let (response, code) = server.tasks_filter("from=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "invalid digit found in string at `.from`.", + "message": "Invalid value in parameter `from`: could not parse `pied` as a positive integer", "code": "invalid_task_from", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-from" + "link": "https://docs.meilisearch.com/errors#invalid_task_from" } "###); - let (response, code) = server.tasks_filter(json!( { "beforeStartedAt": "pied" })).await; + let (response, code) = server.tasks_filter("beforeStartedAt=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "`pied` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "message": "Invalid value in parameter `beforeStartedAt`: `pied` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_started_at", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" + "link": "https://docs.meilisearch.com/errors#invalid_task_before_started_at" } "###); } @@ -228,36 +244,36 @@ async fn get_task_filter_error() { async fn delete_task_filter_error() { let server = Server::new().await; - let (response, code) = server.delete_tasks(json!(null)).await; + let (response, code) = server.delete_tasks("").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "Query parameters to filter the tasks to delete are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.", + "message": "Query parameters to filter the tasks to delete are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.", "code": "missing_task_filters", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#missing-task-filters" + "link": "https://docs.meilisearch.com/errors#missing_task_filters" } "###); - let (response, code) = server.delete_tasks(json!({ "lol": "pied" })).await; + let (response, code) = server.delete_tasks("lol=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `lol`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `lol`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#bad_request" } "###); - let (response, code) = server.delete_tasks(json!({ "uids": "pied" })).await; + let (response, code) = server.delete_tasks("uids=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `pied` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" } "###); } @@ -266,36 +282,36 @@ async fn delete_task_filter_error() { async fn cancel_task_filter_error() { let server = Server::new().await; - let (response, code) = server.cancel_tasks(json!(null)).await; + let (response, code) = server.cancel_tasks("").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.", + "message": "Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.", "code": "missing_task_filters", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#missing-task-filters" + "link": "https://docs.meilisearch.com/errors#missing_task_filters" } "###); - let (response, code) = server.cancel_tasks(json!({ "lol": "pied" })).await; + let (response, code) = server.cancel_tasks("lol=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `lol`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `lol`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#bad_request" } "###); - let (response, code) = server.cancel_tasks(json!({ "uids": "pied" })).await; + let (response, code) = server.cancel_tasks("uids=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `pied` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + "link": "https://docs.meilisearch.com/errors#invalid_task_uids" } "###); } @@ -350,7 +366,7 @@ async fn test_summarized_document_addition_or_update() { index.add_documents(json!({ "id": 42, "content": "doggos & fluff" }), None).await; index.wait_task(0).await; let (task, _) = index.get_task(0).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -374,7 +390,7 @@ async fn test_summarized_document_addition_or_update() { index.add_documents(json!({ "id": 42, "content": "doggos & fluff" }), Some("id")).await; index.wait_task(1).await; let (task, _) = index.get_task(1).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -403,7 +419,7 @@ async fn test_summarized_delete_batch() { index.delete_batch(vec![1, 2, 3]).await; index.wait_task(0).await; let (task, _) = index.get_task(0).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -420,7 +436,7 @@ async fn test_summarized_delete_batch() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }, "duration": "[duration]", "enqueuedAt": "[date]", @@ -433,7 +449,7 @@ async fn test_summarized_delete_batch() { index.delete_batch(vec![42]).await; index.wait_task(2).await; let (task, _) = index.get_task(2).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -462,7 +478,7 @@ async fn test_summarized_delete_document() { index.delete_document(1).await; index.wait_task(0).await; let (task, _) = index.get_task(0).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -479,7 +495,7 @@ async fn test_summarized_delete_document() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }, "duration": "[duration]", "enqueuedAt": "[date]", @@ -492,7 +508,7 @@ async fn test_summarized_delete_document() { index.delete_document(42).await; index.wait_task(2).await; let (task, _) = index.get_task(2).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -523,17 +539,17 @@ async fn test_summarized_settings_update() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "`custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.", + "message": "Invalid value at `.rankingRules[0]`: `custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules.", "code": "invalid_settings_ranking_rules", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" + "link": "https://docs.meilisearch.com/errors#invalid_settings_ranking_rules" } "###); index.update_settings(json!({ "displayedAttributes": ["doggos", "name"], "filterableAttributes": ["age", "nb_paw_pads"], "sortableAttributes": ["iq"] })).await; index.wait_task(0).await; let (task, _) = index.get_task(0).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -571,7 +587,7 @@ async fn test_summarized_index_creation() { index.create(None).await; index.wait_task(0).await; let (task, _) = index.get_task(0).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -594,7 +610,7 @@ async fn test_summarized_index_creation() { index.create(Some("doggos")).await; index.wait_task(1).await; let (task, _) = index.get_task(1).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -610,7 +626,7 @@ async fn test_summarized_index_creation() { "message": "Index `test` already exists.", "code": "index_already_exists", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-already-exists" + "link": "https://docs.meilisearch.com/errors#index_already_exists" }, "duration": "[duration]", "enqueuedAt": "[date]", @@ -627,7 +643,7 @@ async fn test_summarized_index_deletion() { index.delete().await; index.wait_task(0).await; let (task, _) = index.get_task(0).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -643,7 +659,7 @@ async fn test_summarized_index_deletion() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }, "duration": "[duration]", "enqueuedAt": "[date]", @@ -657,7 +673,7 @@ async fn test_summarized_index_deletion() { index.delete().await; index.wait_task(2).await; let (task, _) = index.get_task(2).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -681,7 +697,7 @@ async fn test_summarized_index_deletion() { index.delete().await; index.wait_task(2).await; let (task, _) = index.get_task(2).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -710,7 +726,7 @@ async fn test_summarized_index_update() { index.update(None).await; index.wait_task(0).await; let (task, _) = index.get_task(0).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -726,7 +742,7 @@ async fn test_summarized_index_update() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }, "duration": "[duration]", "enqueuedAt": "[date]", @@ -738,7 +754,7 @@ async fn test_summarized_index_update() { index.update(Some("bones")).await; index.wait_task(1).await; let (task, _) = index.get_task(1).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -754,7 +770,7 @@ async fn test_summarized_index_update() { "message": "Index `test` not found.", "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" + "link": "https://docs.meilisearch.com/errors#index_not_found" }, "duration": "[duration]", "enqueuedAt": "[date]", @@ -769,7 +785,7 @@ async fn test_summarized_index_update() { index.update(None).await; index.wait_task(3).await; let (task, _) = index.get_task(3).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -792,7 +808,7 @@ async fn test_summarized_index_update() { index.update(Some("bones")).await; index.wait_task(4).await; let (task, _) = index.get_task(4).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -823,7 +839,7 @@ async fn test_summarized_index_swap() { .await; server.wait_task(0).await; let (task, _) = server.get_task(0).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -844,9 +860,9 @@ async fn test_summarized_index_swap() { }, "error": { "message": "Indexes `cattos`, `doggos` not found.", - "code": "invalid_swap_indexes", + "code": "index_not_found", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-swap-indexes" + "link": "https://docs.meilisearch.com/errors#index_not_found" }, "duration": "[duration]", "enqueuedAt": "[date]", @@ -864,7 +880,7 @@ async fn test_summarized_index_swap() { .await; server.wait_task(3).await; let (task, _) = server.get_task(3).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -899,10 +915,10 @@ async fn test_summarized_task_cancelation() { // to avoid being flaky we're only going to cancel an already finished task :( index.create(None).await; index.wait_task(0).await; - server.cancel_tasks(json!({ "uids": [0] })).await; + server.cancel_tasks("uids=0").await; index.wait_task(1).await; let (task, _) = index.get_task(1).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -932,10 +948,10 @@ async fn test_summarized_task_deletion() { // to avoid being flaky we're only going to delete an already finished task :( index.create(None).await; index.wait_task(0).await; - server.delete_tasks(json!({ "uids": [0] })).await; + server.delete_tasks("uids=0").await; index.wait_task(1).await; let (task, _) = index.get_task(1).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { @@ -964,7 +980,7 @@ async fn test_summarized_dump_creation() { server.create_dump().await; server.wait_task(0).await; let (task, _) = server.get_task(0).await; - assert_json_snapshot!(task, + assert_json_snapshot!(task, { ".details.dumpUid" => "[dumpUid]", ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { diff --git a/milli/Cargo.toml b/milli/Cargo.toml index 0ba44a819..d9458ca70 100644 --- a/milli/Cargo.toml +++ b/milli/Cargo.toml @@ -12,7 +12,7 @@ byteorder = "1.4.3" charabia = { version = "0.7.0", default-features = false } concat-arrays = "0.1.2" crossbeam-channel = "0.5.6" -deserr = "0.1.4" +deserr = "0.3.0" either = "1.8.0" flatten-serde-json = { path = "../flatten-serde-json" } fst = "0.4.7" diff --git a/milli/src/error.rs b/milli/src/error.rs index 92c238814..1685f984a 100644 --- a/milli/src/error.rs +++ b/milli/src/error.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; use std::convert::Infallible; +use std::fmt::Write; use std::{io, str}; use heed::{Error as HeedError, MdbError}; @@ -100,10 +101,11 @@ A document identifier can be of type integer or string, \ only composed of alphanumeric characters (a-z A-Z 0-9), hyphens (-) and underscores (_).", .document_id.to_string() )] InvalidDocumentId { document_id: Value }, - #[error("Invalid facet distribution, the fields `{}` are not set as filterable.", - .invalid_facets_name.iter().map(AsRef::as_ref).collect::>().join(", ") - )] - InvalidFacetsDistribution { invalid_facets_name: BTreeSet }, + #[error("Invalid facet distribution, {}", format_invalid_filter_distribution(.invalid_facets_name, .valid_facets_name))] + InvalidFacetsDistribution { + invalid_facets_name: BTreeSet, + valid_facets_name: BTreeSet, + }, #[error(transparent)] InvalidGeoField(#[from] GeoError), #[error("{0}")] @@ -152,6 +154,8 @@ only composed of alphanumeric characters (a-z A-Z 0-9), hyphens (-) and undersco pub enum GeoError { #[error("The `_geo` field in the document with the id: `{document_id}` is not an object. Was expecting an object with the `_geo.lat` and `_geo.lng` fields but instead got `{value}`.")] NotAnObject { document_id: Value, value: Value }, + #[error("The `_geo` field in the document with the id: `{document_id}` contains the following unexpected fields: `{value}`.")] + UnexpectedExtraFields { document_id: Value, value: Value }, #[error("Could not find latitude nor longitude in the document with the id: `{document_id}`. Was expecting `_geo.lat` and `_geo.lng` fields.")] MissingLatitudeAndLongitude { document_id: Value }, #[error("Could not find latitude in the document with the id: `{document_id}`. Was expecting a `_geo.lat` field.")] @@ -166,6 +170,50 @@ pub enum GeoError { BadLongitude { document_id: Value, value: Value }, } +fn format_invalid_filter_distribution( + invalid_facets_name: &BTreeSet, + valid_facets_name: &BTreeSet, +) -> String { + if valid_facets_name.is_empty() { + return "this index does not have configured filterable attributes.".into(); + } + + let mut result = String::new(); + + match invalid_facets_name.len() { + 0 => (), + 1 => write!( + result, + "attribute `{}` is not filterable.", + invalid_facets_name.first().unwrap() + ) + .unwrap(), + _ => write!( + result, + "attributes `{}` are not filterable.", + invalid_facets_name.iter().map(AsRef::as_ref).collect::>().join(", ") + ) + .unwrap(), + }; + + match valid_facets_name.len() { + 1 => write!( + result, + " The available filterable attribute is `{}`.", + valid_facets_name.first().unwrap() + ) + .unwrap(), + _ => write!( + result, + " The available filterable attributes are `{}`.", + valid_facets_name.iter().map(AsRef::as_ref).collect::>().join(", ") + ) + .unwrap(), + } + + result +} + /// A little macro helper to autogenerate From implementation that needs two `Into`. /// Given the following parameters: `error_from_sub_error!(FieldIdMapMissingEntry => InternalError)` /// the macro will create the following code: diff --git a/milli/src/index.rs b/milli/src/index.rs index 9f5b30cd6..47bf54e2d 100644 --- a/milli/src/index.rs +++ b/milli/src/index.rs @@ -426,7 +426,7 @@ impl Index { } /// Returns the `rtree` which associates coordinates to documents ids. - pub fn geo_rtree(&self, rtxn: &'_ RoTxn) -> Result>> { + pub fn geo_rtree(&self, rtxn: &RoTxn) -> Result>> { match self .main .get::<_, Str, SerdeBincode>>(rtxn, main_key::GEO_RTREE_KEY)? @@ -2394,4 +2394,69 @@ pub(crate) mod tests { assert!(all_ids.insert(id)); } } + + #[test] + fn bug_3007() { + // https://github.com/meilisearch/meilisearch/issues/3007 + + use crate::error::{GeoError, UserError}; + let index = TempIndex::new(); + + // Given is an index with a geo field NOT contained in the sortable_fields of the settings + index + .update_settings(|settings| { + settings.set_primary_key("id".to_string()); + settings.set_filterable_fields(HashSet::from(["_geo".to_string()])); + }) + .unwrap(); + + // happy path + index.add_documents(documents!({ "id" : 5, "_geo": {"lat": 12.0, "lng": 11.0}})).unwrap(); + + db_snap!(index, geo_faceted_documents_ids); + + // both are unparseable, we expect GeoError::BadLatitudeAndLongitude + let err1 = index + .add_documents( + documents!({ "id" : 6, "_geo": {"lat": "unparseable", "lng": "unparseable"}}), + ) + .unwrap_err(); + assert!(matches!( + err1, + Error::UserError(UserError::InvalidGeoField(GeoError::BadLatitudeAndLongitude { .. })) + )); + + db_snap!(index, geo_faceted_documents_ids); // ensure that no more document was inserted + } + + #[test] + fn unexpected_extra_fields_in_geo_field() { + let index = TempIndex::new(); + + index + .update_settings(|settings| { + settings.set_primary_key("id".to_string()); + settings.set_filterable_fields(HashSet::from(["_geo".to_string()])); + }) + .unwrap(); + + let err = index + .add_documents( + documents!({ "id" : "doggo", "_geo": { "lat": 1, "lng": 2, "doggo": "are the best" }}), + ) + .unwrap_err(); + insta::assert_display_snapshot!(err, @r###"The `_geo` field in the document with the id: `"\"doggo\""` contains the following unexpected fields: `{"doggo":"are the best"}`."###); + + db_snap!(index, geo_faceted_documents_ids); // ensure that no documents were inserted + + // multiple fields and complex values + let err = index + .add_documents( + documents!({ "id" : "doggo", "_geo": { "lat": 1, "lng": 2, "doggo": "are the best", "and": { "all": ["cats", { "are": "beautiful" } ] } } }), + ) + .unwrap_err(); + insta::assert_display_snapshot!(err, @r###"The `_geo` field in the document with the id: `"\"doggo\""` contains the following unexpected fields: `{"and":{"all":["cats",{"are":"beautiful"}]},"doggo":"are the best"}`."###); + + db_snap!(index, geo_faceted_documents_ids); // ensure that no documents were inserted + } } diff --git a/milli/src/search/criteria/proximity.rs b/milli/src/search/criteria/proximity.rs index 0f5b340bf..182f9fbea 100644 --- a/milli/src/search/criteria/proximity.rs +++ b/milli/src/search/criteria/proximity.rs @@ -183,14 +183,14 @@ impl<'t> Criterion for Proximity<'t> { } fn resolve_candidates( - ctx: &'_ dyn Context, + ctx: &dyn Context, query_tree: &Operation, proximity: u8, cache: &mut Cache, wdcache: &mut WordDerivationsCache, ) -> Result { fn resolve_operation( - ctx: &'_ dyn Context, + ctx: &dyn Context, query_tree: &Operation, proximity: u8, cache: &mut Cache, @@ -244,7 +244,7 @@ fn resolve_candidates( } fn mdfs_pair( - ctx: &'_ dyn Context, + ctx: &dyn Context, left: &Operation, right: &Operation, proximity: u8, @@ -299,7 +299,7 @@ fn resolve_candidates( } fn mdfs( - ctx: &'_ dyn Context, + ctx: &dyn Context, branches: &[Operation], proximity: u8, cache: &mut Cache, diff --git a/milli/src/search/criteria/typo.rs b/milli/src/search/criteria/typo.rs index 95e722074..69a210e7b 100644 --- a/milli/src/search/criteria/typo.rs +++ b/milli/src/search/criteria/typo.rs @@ -240,14 +240,14 @@ fn alterate_query_tree( } fn resolve_candidates( - ctx: &'_ dyn Context, + ctx: &dyn Context, query_tree: &Operation, number_typos: u8, cache: &mut HashMap<(Operation, u8), RoaringBitmap>, wdcache: &mut WordDerivationsCache, ) -> Result { fn resolve_operation( - ctx: &'_ dyn Context, + ctx: &dyn Context, query_tree: &Operation, number_typos: u8, cache: &mut HashMap<(Operation, u8), RoaringBitmap>, @@ -277,7 +277,7 @@ fn resolve_candidates( } fn mdfs( - ctx: &'_ dyn Context, + ctx: &dyn Context, branches: &[Operation], mana: u8, cache: &mut HashMap<(Operation, u8), RoaringBitmap>, diff --git a/milli/src/search/facet/facet_distribution.rs b/milli/src/search/facet/facet_distribution.rs index 43367abbb..4d5028ce0 100644 --- a/milli/src/search/facet/facet_distribution.rs +++ b/milli/src/search/facet/facet_distribution.rs @@ -291,6 +291,7 @@ impl<'a> FacetDistribution<'a> { if !invalid_fields.is_empty() { return Err(UserError::InvalidFacetsDistribution { invalid_facets_name: invalid_fields.into_iter().cloned().collect(), + valid_facets_name: filterable_fields.into_iter().collect(), } .into()); } else { diff --git a/milli/src/snapshots/index.rs/bug_3007/geo_faceted_documents_ids.snap b/milli/src/snapshots/index.rs/bug_3007/geo_faceted_documents_ids.snap new file mode 100644 index 000000000..f9ebc0c20 --- /dev/null +++ b/milli/src/snapshots/index.rs/bug_3007/geo_faceted_documents_ids.snap @@ -0,0 +1,4 @@ +--- +source: milli/src/index.rs +--- +[0, ] diff --git a/milli/src/snapshots/index.rs/unexpected_extra_fields_in_geo_field/geo_faceted_documents_ids.snap b/milli/src/snapshots/index.rs/unexpected_extra_fields_in_geo_field/geo_faceted_documents_ids.snap new file mode 100644 index 000000000..89fb1856a --- /dev/null +++ b/milli/src/snapshots/index.rs/unexpected_extra_fields_in_geo_field/geo_faceted_documents_ids.snap @@ -0,0 +1,4 @@ +--- +source: milli/src/index.rs +--- +[] diff --git a/milli/src/update/delete_documents.rs b/milli/src/update/delete_documents.rs index 4ba7eb08f..daf186683 100644 --- a/milli/src/update/delete_documents.rs +++ b/milli/src/update/delete_documents.rs @@ -575,8 +575,8 @@ fn remove_from_word_docids( } fn remove_docids_from_field_id_docid_facet_value( - index: &'_ Index, - wtxn: &'_ mut heed::RwTxn, + index: &Index, + wtxn: &mut heed::RwTxn, facet_type: FacetType, field_id: FieldId, to_remove: &RoaringBitmap, diff --git a/milli/src/update/facet/incremental.rs b/milli/src/update/facet/incremental.rs index d5f09c783..aaef93b48 100644 --- a/milli/src/update/facet/incremental.rs +++ b/milli/src/update/facet/incremental.rs @@ -159,7 +159,7 @@ impl FacetsUpdateIncrementalInner { /// See documentation of `insert_in_level` fn insert_in_level_0( &self, - txn: &'_ mut RwTxn, + txn: &mut RwTxn, field_id: u16, facet_value: &[u8], docids: &RoaringBitmap, @@ -213,7 +213,7 @@ impl FacetsUpdateIncrementalInner { /// of the parent node should be incremented. fn insert_in_level( &self, - txn: &'_ mut RwTxn, + txn: &mut RwTxn, field_id: u16, level: u8, facet_value: &[u8], @@ -350,7 +350,7 @@ impl FacetsUpdateIncrementalInner { /// Insert the given facet value and corresponding document ids in the database. pub fn insert( &self, - txn: &'_ mut RwTxn, + txn: &mut RwTxn, field_id: u16, facet_value: &[u8], docids: &RoaringBitmap, @@ -472,7 +472,7 @@ impl FacetsUpdateIncrementalInner { /// its left bound as well. fn delete_in_level( &self, - txn: &'_ mut RwTxn, + txn: &mut RwTxn, field_id: u16, level: u8, facet_value: &[u8], @@ -531,7 +531,7 @@ impl FacetsUpdateIncrementalInner { fn delete_in_level_0( &self, - txn: &'_ mut RwTxn, + txn: &mut RwTxn, field_id: u16, facet_value: &[u8], docids: &RoaringBitmap, @@ -559,7 +559,7 @@ impl FacetsUpdateIncrementalInner { pub fn delete( &self, - txn: &'_ mut RwTxn, + txn: &mut RwTxn, field_id: u16, facet_value: &[u8], docids: &RoaringBitmap, diff --git a/milli/src/update/index_documents/enrich.rs b/milli/src/update/index_documents/enrich.rs index 3331497c9..ed04e9962 100644 --- a/milli/src/update/index_documents/enrich.rs +++ b/milli/src/update/index_documents/enrich.rs @@ -98,7 +98,12 @@ pub fn enrich_documents_batch( // If the settings specifies that a _geo field must be used therefore we must check the // validity of it in all the documents of this batch and this is when we return `Some`. let geo_field_id = match documents_batch_index.id("_geo") { - Some(geo_field_id) if index.sortable_fields(rtxn)?.contains("_geo") => Some(geo_field_id), + Some(geo_field_id) + if index.sortable_fields(rtxn)?.contains("_geo") + || index.filterable_fields(rtxn)?.contains("_geo") => + { + Some(geo_field_id) + } _otherwise => None, }; @@ -367,11 +372,17 @@ pub fn extract_finite_float_from_value(value: Value) -> StdResult { pub fn validate_geo_from_json(id: &DocumentId, bytes: &[u8]) -> Result> { use GeoError::*; - let debug_id = || Value::from(id.debug()); + let debug_id = || { + serde_json::from_slice(id.value().as_bytes()).unwrap_or_else(|_| Value::from(id.debug())) + }; match serde_json::from_slice(bytes).map_err(InternalError::SerdeJson)? { Value::Object(mut object) => match (object.remove("lat"), object.remove("lng")) { (Some(lat), Some(lng)) => { match (extract_finite_float_from_value(lat), extract_finite_float_from_value(lng)) { + (Ok(_), Ok(_)) if !object.is_empty() => Ok(Err(UnexpectedExtraFields { + document_id: debug_id(), + value: object.into(), + })), (Ok(_), Ok(_)) => Ok(Ok(())), (Err(value), Ok(_)) => Ok(Err(BadLatitude { document_id: debug_id(), value })), (Ok(_), Err(value)) => Ok(Err(BadLongitude { document_id: debug_id(), value })), diff --git a/milli/src/update/index_documents/mod.rs b/milli/src/update/index_documents/mod.rs index be97defbd..3e9edf3a2 100644 --- a/milli/src/update/index_documents/mod.rs +++ b/milli/src/update/index_documents/mod.rs @@ -965,34 +965,6 @@ mod tests { .unwrap(); } - #[test] - fn index_all_flavour_of_geo() { - let mut index = TempIndex::new(); - index.index_documents_config.update_method = IndexDocumentsMethod::ReplaceDocuments; - - index - .update_settings(|settings| { - settings.set_filterable_fields(hashset!(S("_geo"))); - }) - .unwrap(); - - index - .add_documents(documents!([ - { "id": 0, "_geo": { "lat": 31, "lng": [42] } }, - { "id": 1, "_geo": { "lat": "31" }, "_geo.lng": 42 }, - { "id": 2, "_geo": { "lng": "42" }, "_geo.lat": "31" }, - { "id": 3, "_geo.lat": 31, "_geo.lng": "42" }, - ])) - .unwrap(); - - let rtxn = index.read_txn().unwrap(); - - let mut search = crate::Search::new(&rtxn, &index); - search.filter(crate::Filter::from_str("_geoRadius(31, 42, 0.000001)").unwrap().unwrap()); - let crate::SearchResult { documents_ids, .. } = search.execute().unwrap(); - assert_eq!(documents_ids, vec![0, 1, 2, 3]); - } - #[test] fn geo_error() { let mut index = TempIndex::new(); diff --git a/milli/src/update/settings.rs b/milli/src/update/settings.rs index 55f6c66fe..1646ab5b2 100644 --- a/milli/src/update/settings.rs +++ b/milli/src/update/settings.rs @@ -37,9 +37,6 @@ where _ => T::deserialize_from_value(value, location).map(Setting::Set), } } - fn default() -> Option { - Some(Self::NotSet) - } } impl Default for Setting { diff --git a/milli/src/update/words_prefix_position_docids.rs b/milli/src/update/words_prefix_position_docids.rs index 5dbc9f89b..6f12dde38 100644 --- a/milli/src/update/words_prefix_position_docids.rs +++ b/milli/src/update/words_prefix_position_docids.rs @@ -140,16 +140,20 @@ impl<'t, 'u, 'i> WordPrefixPositionDocids<'t, 'u, 'i> { // We remove all the entries that are no more required in this word prefix position // docids database. - let mut iter = - self.index.word_prefix_position_docids.iter_mut(self.wtxn)?.lazily_decode_data(); - while let Some(((prefix, _), _)) = iter.next().transpose()? { - if del_prefix_fst_words.contains(prefix.as_bytes()) { - unsafe { iter.del_current()? }; + // We also avoid iterating over the whole `word_prefix_position_docids` database if we know in + // advance that the `if del_prefix_fst_words.contains(prefix.as_bytes()) {` condition below + // will always be false (i.e. if `del_prefix_fst_words` is empty). + if !del_prefix_fst_words.is_empty() { + let mut iter = + self.index.word_prefix_position_docids.iter_mut(self.wtxn)?.lazily_decode_data(); + while let Some(((prefix, _), _)) = iter.next().transpose()? { + if del_prefix_fst_words.contains(prefix.as_bytes()) { + unsafe { iter.del_current()? }; + } } + drop(iter); } - drop(iter); - // We finally write all the word prefix position docids into the LMDB database. sorter_into_lmdb_database( self.wtxn,