milli_core/
error.rs

1use std::collections::BTreeSet;
2use std::collections::HashMap;
3use std::convert::Infallible;
4use std::fmt::Write;
5use std::{io, str};
6
7use bstr::BString;
8use heed::{Error as HeedError, MdbError};
9use rayon::ThreadPoolBuildError;
10use rhai::EvalAltResult;
11use serde_json::Value;
12use thiserror::Error;
13
14use crate::constants::RESERVED_GEO_FIELD_NAME;
15use crate::documents::{self, DocumentsBatchCursorError};
16use crate::thread_pool_no_abort::PanicCatched;
17use crate::vector::settings::EmbeddingSettings;
18use crate::{CriterionError, DocumentId, FieldId, Object, SortError};
19
20pub fn is_reserved_keyword(keyword: &str) -> bool {
21    [RESERVED_GEO_FIELD_NAME, "_geoDistance", "_geoPoint", "_geoRadius", "_geoBoundingBox"]
22        .contains(&keyword)
23}
24
25#[derive(Error, Debug)]
26pub enum Error {
27    #[error("internal: {0}.")]
28    InternalError(#[from] InternalError),
29    #[error(transparent)]
30    IoError(#[from] io::Error),
31    #[error(transparent)]
32    UserError(#[from] UserError),
33}
34
35#[derive(Error, Debug)]
36pub enum InternalError {
37    #[error("missing {} in the {db_name} database", key.unwrap_or("key"))]
38    DatabaseMissingEntry { db_name: &'static str, key: Option<&'static str> },
39    #[error("missing {key} in the fieldids weights mapping")]
40    FieldidsWeightsMapMissingEntry { key: FieldId },
41    #[error(transparent)]
42    FieldIdMapMissingEntry(#[from] FieldIdMapMissingEntry),
43    #[error("missing {key} in the field id mapping")]
44    FieldIdMappingMissingEntry { key: FieldId },
45    #[error(transparent)]
46    Fst(#[from] fst::Error),
47    #[error(transparent)]
48    DocumentsError(#[from] documents::Error),
49    #[error("invalid compression type have been specified to grenad")]
50    GrenadInvalidCompressionType,
51    #[error("invalid grenad file with an invalid version format")]
52    GrenadInvalidFormatVersion,
53    #[error("invalid merge while processing {process}")]
54    IndexingMergingKeys { process: &'static str },
55    #[error(transparent)]
56    RayonThreadPool(#[from] ThreadPoolBuildError),
57    #[error(transparent)]
58    PanicInThreadPool(#[from] PanicCatched),
59    #[error(transparent)]
60    SerdeJson(#[from] serde_json::Error),
61    #[error(transparent)]
62    BincodeError(#[from] bincode::Error),
63    #[error(transparent)]
64    Serialization(#[from] SerializationError),
65    #[error(transparent)]
66    Store(#[from] MdbError),
67    #[error("Cannot delete {key:?} from database {database_name}: {error}")]
68    StoreDeletion { database_name: &'static str, key: BString, error: heed::Error },
69    #[error("Cannot insert {key:?} and value with length {value_length} into database {database_name}: {error}")]
70    StorePut { database_name: &'static str, key: BString, value_length: usize, error: heed::Error },
71    #[error(transparent)]
72    Utf8(#[from] str::Utf8Error),
73    #[error("An indexation process was explicitly aborted")]
74    AbortedIndexation,
75    #[error("The matching words list contains at least one invalid member")]
76    InvalidMatchingWords,
77    #[error("Cannot upgrade to the following version: v{0}.{1}.{2}.")]
78    CannotUpgradeToVersion(u32, u32, u32),
79    #[error(transparent)]
80    ArroyError(#[from] arroy::Error),
81    #[error(transparent)]
82    VectorEmbeddingError(#[from] crate::vector::Error),
83}
84
85#[derive(Error, Debug)]
86pub enum SerializationError {
87    #[error("{}", match .db_name {
88        Some(name) => format!("decoding from the {name} database failed"),
89        None => "decoding failed".to_string(),
90    })]
91    Decoding { db_name: Option<&'static str> },
92    #[error("{}", match .db_name {
93        Some(name) => format!("encoding into the {name} database failed"),
94        None => "encoding failed".to_string(),
95    })]
96    Encoding { db_name: Option<&'static str> },
97    #[error("number is not a valid finite number")]
98    InvalidNumberSerialization,
99}
100
101#[derive(Error, Debug)]
102pub enum FieldIdMapMissingEntry {
103    #[error("unknown field id {field_id} coming from the {process} process")]
104    FieldId { field_id: FieldId, process: &'static str },
105    #[error("unknown field name {field_name} coming from the {process} process")]
106    FieldName { field_name: String, process: &'static str },
107}
108
109#[derive(Error, Debug)]
110pub enum UserError {
111    #[error("A document cannot contain more than 65,535 fields.")]
112    AttributeLimitReached,
113    #[error(transparent)]
114    CriterionError(#[from] CriterionError),
115    #[error("Maximum number of documents reached.")]
116    DocumentLimitReached,
117    #[error(
118        "Document identifier `{}` is invalid. \
119A document identifier can be of type integer or string, \
120only composed of alphanumeric characters (a-z A-Z 0-9), hyphens (-) and underscores (_), \
121and can not be more than 511 bytes.", .document_id.to_string()
122    )]
123    InvalidDocumentId { document_id: Value },
124    #[error("Invalid facet distribution: {}",
125        if .invalid_facets_name.len() == 1 {
126            let field = .invalid_facets_name.iter().next().unwrap();
127            match .matching_rule_indices.get(field) {
128                Some(rule_index) => format!("Attribute `{}` matched rule #{} in filterableAttributes, but this rule does not enable filtering.\nHint: enable filtering in rule #{} by modifying the features.filter object\nHint: prepend another rule matching `{}` with appropriate filter features before rule #{}",
129                    field, rule_index, rule_index, field, rule_index),
130                None => match .valid_patterns.is_empty() {
131                    true => format!("Attribute `{}` is not filterable. This index does not have configured filterable attributes.", field),
132                    false => format!("Attribute `{}` is not filterable. Available filterable attributes patterns are: `{}`.",
133                        field,
134                        .valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")),
135                }
136            }
137        } else {
138            format!("Attributes `{}` are not filterable. {}",
139                .invalid_facets_name.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", "),
140                match .valid_patterns.is_empty() {
141                    true => "This index does not have configured filterable attributes.".to_string(),
142                    false => format!("Available filterable attributes patterns are: `{}`.",
143                        .valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")),
144                }
145            )
146        }
147    )]
148    InvalidFacetsDistribution {
149        invalid_facets_name: BTreeSet<String>,
150        valid_patterns: BTreeSet<String>,
151        matching_rule_indices: HashMap<String, usize>,
152    },
153    #[error(transparent)]
154    InvalidGeoField(#[from] Box<GeoError>),
155    #[error("Invalid vector dimensions: expected: `{}`, found: `{}`.", .expected, .found)]
156    InvalidVectorDimensions { expected: usize, found: usize },
157    #[error("Invalid vector dimensions in document with id `{document_id}` in `._vectors.{embedder_name}`.\n  - note: embedding #{embedding_index} has dimensions {found}\n  - note: embedder `{embedder_name}` requires {expected}")]
158    InvalidIndexingVectorDimensions {
159        embedder_name: String,
160        document_id: String,
161        embedding_index: usize,
162        expected: usize,
163        found: usize,
164    },
165    #[error("The `_vectors` field in the document with id: `{document_id}` is not an object. Was expecting an object with a key for each embedder with manually provided vectors, but instead got `{value}`")]
166    InvalidVectorsMapType { document_id: String, value: Value },
167    #[error("Bad embedder configuration in the document with id: `{document_id}`. {error}")]
168    InvalidVectorsEmbedderConf { document_id: String, error: String },
169    #[error("{0}")]
170    InvalidFilter(String),
171    #[error("Invalid type for filter subexpression: expected: {}, found: {}.", .0.join(", "), .1)]
172    InvalidFilterExpression(&'static [&'static str], Value),
173    #[error("Filter operator `{operator}` is not allowed for the attribute `{field}`.\n  - Note: allowed operators: {}.\n  - Note: field `{field}` matched rule #{rule_index} in `filterableAttributes`\n  - Hint: enable {} in rule #{rule_index} by modifying the features.filter object\n  - Hint: prepend another rule matching `{field}` with appropriate filter features before rule #{rule_index}",
174        allowed_operators.join(", "),
175        if operator == "=" || operator == "!=" || operator == "IN" {"equality"}
176        else if operator == "<" || operator == ">" || operator == "<=" || operator == ">=" || operator == "TO" {"comparison"}
177        else {"the appropriate filter operators"}
178    )]
179    FilterOperatorNotAllowed {
180        field: String,
181        allowed_operators: Vec<String>,
182        operator: String,
183        rule_index: usize,
184    },
185    #[error("Attribute `{}` is not sortable. {}",
186        .field,
187        match .valid_fields.is_empty() {
188            true => "This index does not have configured sortable attributes.".to_string(),
189            false => format!("Available sortable attributes are: `{}{}`.",
190                    valid_fields.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", "),
191                    .hidden_fields.then_some(", <..hidden-attributes>").unwrap_or(""),
192                ),
193        }
194    )]
195    InvalidSortableAttribute { field: String, valid_fields: BTreeSet<String>, hidden_fields: bool },
196    #[error("Attribute `{}` is not filterable and thus, cannot be used as distinct attribute. {}",
197        .field,
198        match (.valid_patterns.is_empty(), .matching_rule_index) {
199            // No rules match and no filterable attributes
200            (true, None) => "This index does not have configured filterable attributes.".to_string(),
201
202            // No rules match but there are some filterable attributes
203            (false, None) => format!("Available filterable attributes patterns are: `{}{}`.",
204                    valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", "),
205                    .hidden_fields.then_some(", <..hidden-attributes>").unwrap_or(""),
206                ),
207
208            // A rule matched but filtering isn't enabled
209            (_, Some(rule_index)) => format!("Note: this attribute matches rule #{} in filterableAttributes, but this rule does not enable filtering.\nHint: enable filtering in rule #{} by adding appropriate filter features.\nHint: prepend another rule matching {} with filter features before rule #{}",
210                    rule_index, rule_index, .field, rule_index
211                ),
212        }
213    )]
214    InvalidDistinctAttribute {
215        field: String,
216        valid_patterns: BTreeSet<String>,
217        hidden_fields: bool,
218        matching_rule_index: Option<usize>,
219    },
220    #[error("Attribute `{}` is not facet-searchable. {}",
221        .field,
222        match (.valid_patterns.is_empty(), .matching_rule_index) {
223            // No rules match and no facet searchable attributes
224            (true, None) => "This index does not have configured facet-searchable attributes. To make it facet-searchable add it to the `filterableAttributes` index settings.".to_string(),
225
226            // No rules match but there are some facet searchable attributes
227            (false, None) => format!("Available facet-searchable attributes patterns are: `{}{}`. To make it facet-searchable add it to the `filterableAttributes` index settings.",
228                    valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", "),
229                    .hidden_fields.then_some(", <..hidden-attributes>").unwrap_or(""),
230                ),
231
232            // A rule matched but facet search isn't enabled
233            (_, Some(rule_index)) => format!("Note: this attribute matches rule #{} in filterableAttributes, but this rule does not enable facetSearch.\nHint: enable facetSearch in rule #{} by adding `\"facetSearch\": true` to the rule.\nHint: prepend another rule matching {} with facetSearch: true before rule #{}",
234                    rule_index, rule_index, .field, rule_index
235                ),
236        }
237    )]
238    InvalidFacetSearchFacetName {
239        field: String,
240        valid_patterns: BTreeSet<String>,
241        hidden_fields: bool,
242        matching_rule_index: Option<usize>,
243    },
244    #[error("Attribute `{}` is not searchable. Available searchable attributes are: `{}{}`.",
245        .field,
246        .valid_fields.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", "),
247        .hidden_fields.then_some(", <..hidden-attributes>").unwrap_or(""),
248    )]
249    InvalidSearchableAttribute {
250        field: String,
251        valid_fields: BTreeSet<String>,
252        hidden_fields: bool,
253    },
254    #[error("An LMDB environment is already opened")]
255    EnvAlreadyOpened,
256    #[error("You must specify where `sort` is listed in the rankingRules setting to use the sort parameter at search time.")]
257    SortRankingRuleMissing,
258    #[error("The database file is in an invalid state.")]
259    InvalidStoreFile,
260    #[error("Maximum database size has been reached.")]
261    MaxDatabaseSizeReached,
262    #[error("Document doesn't have a `{}` attribute: `{}`.", .primary_key, serde_json::to_string(.document).unwrap())]
263    MissingDocumentId { primary_key: String, document: Object },
264    #[error("Document have too many matching `{}` attribute: `{}`.", .primary_key, serde_json::to_string(.document).unwrap())]
265    TooManyDocumentIds { primary_key: String, document: Object },
266    #[error("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.")]
267    NoPrimaryKeyCandidateFound,
268    #[error("The primary key inference failed as the engine found {} fields ending with `id` in their names: '{}' and '{}'. Please specify the primary key manually using the `primaryKey` query parameter.", .candidates.len(), .candidates.first().unwrap(), .candidates.get(1).unwrap())]
269    MultiplePrimaryKeyCandidatesFound { candidates: Vec<String> },
270    #[error("There is no more space left on the device. Consider increasing the size of the disk/partition.")]
271    NoSpaceLeftOnDevice,
272    #[error("Index already has a primary key: `{0}`.")]
273    PrimaryKeyCannotBeChanged(String),
274    #[error(transparent)]
275    SerdeJson(serde_json::Error),
276    #[error(transparent)]
277    SortError(#[from] SortError),
278    #[error("An unknown internal document id have been used: `{document_id}`.")]
279    UnknownInternalDocumentId { document_id: DocumentId },
280    #[error("`minWordSizeForTypos` setting is invalid. `oneTypo` and `twoTypos` fields should be between `0` and `255`, and `twoTypos` should be greater or equals to `oneTypo` but found `oneTypo: {0}` and twoTypos: {1}`.")]
281    InvalidMinTypoWordLenSetting(u8, u8),
282    #[error(transparent)]
283    VectorEmbeddingError(#[from] crate::vector::Error),
284    #[error(transparent)]
285    MissingDocumentField(#[from] crate::prompt::error::RenderPromptError),
286    #[error(transparent)]
287    InvalidPrompt(#[from] crate::prompt::error::NewPromptError),
288    #[error("`.embedders.{0}.documentTemplate`: Invalid template: {1}.")]
289    InvalidPromptForEmbeddings(String, crate::prompt::error::NewPromptError),
290    #[error("Too many embedders in the configuration. Found {0}, but limited to 256.")]
291    TooManyEmbedders(usize),
292    #[error("Cannot find embedder with name `{0}`.")]
293    InvalidSearchEmbedder(String),
294    #[error("Cannot find embedder with name `{0}`.")]
295    InvalidSimilarEmbedder(String),
296    #[error("Too many vectors for document with id {0}: found {1}, but limited to 256.")]
297    TooManyVectors(String, usize),
298    #[error("`.embedders.{embedder_name}`: Field `{field}` unavailable for source `{source_}`{for_context}.{available_sources}{available_fields}{available_contexts}",
299    field=field.name(),
300        for_context={
301            context.in_context()
302        },
303        available_sources={
304            let allowed_sources_for_field = EmbeddingSettings::allowed_sources_for_field(*field, *context);
305            if allowed_sources_for_field.is_empty() {
306                String::new()
307            } else {
308                format!("\n  - note: `{}` is available for sources: {}",
309                field.name(),
310                allowed_sources_for_field
311                .iter()
312                .map(|accepted| format!("`{}`", accepted))
313                .collect::<Vec<String>>()
314                .join(", "),
315            )
316            }
317        },
318        available_fields={
319            let allowed_fields_for_source = EmbeddingSettings::allowed_fields_for_source(*source_, *context);
320            format!("\n  - note: available fields for source `{source_}`{}: {}",context.in_context(), allowed_fields_for_source
321            .iter()
322            .map(|accepted| format!("`{}`", accepted))
323            .collect::<Vec<String>>()
324            .join(", "),)
325        },
326        available_contexts={
327            let available_not_nested = !matches!(EmbeddingSettings::field_status(*source_, *field, crate::vector::settings::NestingContext::NotNested), crate::vector::settings::FieldStatus::Disallowed);
328            if available_not_nested {
329                format!("\n  - note: `{}` is available when source `{source_}` is not{}", field.name(), context.in_context())
330            } else {
331                String::new()
332            }
333        }
334    )]
335    InvalidFieldForSource {
336        embedder_name: String,
337        source_: crate::vector::settings::EmbedderSource,
338        context: crate::vector::settings::NestingContext,
339        field: crate::vector::settings::MetaEmbeddingSetting,
340    },
341    #[error("`.embedders.{embedder_name}.model`: Invalid model `{model}` for OpenAI. Supported models: {:?}", crate::vector::openai::EmbeddingModel::supported_models())]
342    InvalidOpenAiModel { embedder_name: String, model: String },
343    #[error("`.embedders.{embedder_name}`: Missing field `{field}` (note: this field is mandatory for source `{source_}`)")]
344    MissingFieldForSource {
345        field: &'static str,
346        source_: crate::vector::settings::EmbedderSource,
347        embedder_name: String,
348    },
349    #[error("`.embedders.{embedder_name}.dimensions`: Model `{model}` does not support overriding its native dimensions of {expected_dimensions}. Found {dimensions}")]
350    InvalidOpenAiModelDimensions {
351        embedder_name: String,
352        model: &'static str,
353        dimensions: usize,
354        expected_dimensions: usize,
355    },
356    #[error("`.embedders.{embedder_name}.dimensions`: Model `{model}` does not support overriding its dimensions to a value higher than {max_dimensions}. Found {dimensions}")]
357    InvalidOpenAiModelDimensionsMax {
358        embedder_name: String,
359        model: &'static str,
360        dimensions: usize,
361        max_dimensions: usize,
362    },
363    #[error("`.embedders.{embedder_name}.source`: Source `{source_}` is not available in a nested embedder")]
364    InvalidSourceForNested {
365        embedder_name: String,
366        source_: crate::vector::settings::EmbedderSource,
367    },
368    #[error("`.embedders.{embedder_name}`: Missing field `source`.\n  - note: this field is mandatory for nested embedders")]
369    MissingSourceForNested { embedder_name: String },
370    #[error("`.embedders.{embedder_name}`: {message}")]
371    InvalidSettingsEmbedder { embedder_name: String, message: String },
372    #[error("`.embedders.{embedder_name}.dimensions`: `dimensions` cannot be zero")]
373    InvalidSettingsDimensions { embedder_name: String },
374    #[error(
375        "`.embedders.{embedder_name}.binaryQuantized`: Cannot disable the binary quantization.\n - Note: Binary quantization is a lossy operation that cannot be reverted.\n - Hint: Add a new embedder that is non-quantized and regenerate the vectors."
376    )]
377    InvalidDisableBinaryQuantization { embedder_name: String },
378    #[error("`.embedders.{embedder_name}.documentTemplateMaxBytes`: `documentTemplateMaxBytes` cannot be zero")]
379    InvalidSettingsDocumentTemplateMaxBytes { embedder_name: String },
380    #[error("`.embedders.{embedder_name}.url`: could not parse `{url}`: {inner_error}")]
381    InvalidUrl { embedder_name: String, inner_error: url::ParseError, url: String },
382    #[error("Document editions cannot modify a document's primary key")]
383    DocumentEditionCannotModifyPrimaryKey,
384    #[error("Document editions must keep documents as objects")]
385    DocumentEditionDocumentMustBeObject,
386    #[error("Document edition runtime error encountered while running the function: {0}")]
387    DocumentEditionRuntimeError(Box<EvalAltResult>),
388    #[error("Document edition runtime error encountered while compiling the function: {0}")]
389    DocumentEditionCompilationError(rhai::ParseError),
390    #[error("{0}")]
391    DocumentEmbeddingError(String),
392}
393
394impl From<crate::vector::Error> for Error {
395    fn from(value: crate::vector::Error) -> Self {
396        match value.fault() {
397            FaultSource::User => Error::UserError(value.into()),
398            FaultSource::Runtime => Error::UserError(value.into()),
399            FaultSource::Bug => Error::InternalError(value.into()),
400            FaultSource::Undecided => Error::UserError(value.into()),
401        }
402    }
403}
404
405impl From<arroy::Error> for Error {
406    fn from(value: arroy::Error) -> Self {
407        match value {
408            arroy::Error::Heed(heed) => heed.into(),
409            arroy::Error::Io(io) => io.into(),
410            arroy::Error::InvalidVecDimension { expected, received } => {
411                Error::UserError(UserError::InvalidVectorDimensions { expected, found: received })
412            }
413            arroy::Error::BuildCancelled => Error::InternalError(InternalError::AbortedIndexation),
414            arroy::Error::DatabaseFull
415            | arroy::Error::InvalidItemAppend
416            | arroy::Error::UnmatchingDistance { .. }
417            | arroy::Error::NeedBuild(_)
418            | arroy::Error::MissingKey { .. }
419            | arroy::Error::MissingMetadata(_)
420            | arroy::Error::CannotDecodeKeyMode { .. } => {
421                Error::InternalError(InternalError::ArroyError(value))
422            }
423        }
424    }
425}
426
427#[derive(Error, Debug)]
428pub enum GeoError {
429    #[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}`.")]
430    NotAnObject { document_id: Value, value: Value },
431    #[error("The `_geo` field in the document with the id: `{document_id}` contains the following unexpected fields: `{value}`.")]
432    UnexpectedExtraFields { document_id: Value, value: Value },
433    #[error("Could not find latitude nor longitude in the document with the id: `{document_id}`. Was expecting `_geo.lat` and `_geo.lng` fields.")]
434    MissingLatitudeAndLongitude { document_id: Value },
435    #[error("Could not find latitude in the document with the id: `{document_id}`. Was expecting a `_geo.lat` field.")]
436    MissingLatitude { document_id: Value },
437    #[error("Could not find longitude in the document with the id: `{document_id}`. Was expecting a `_geo.lng` field.")]
438    MissingLongitude { document_id: Value },
439    #[error("Could not parse latitude nor longitude in the document with the id: `{document_id}`. Was expecting finite numbers but instead got `{lat}` and `{lng}`.")]
440    BadLatitudeAndLongitude { document_id: Value, lat: Value, lng: Value },
441    #[error("Could not parse latitude in the document with the id: `{document_id}`. Was expecting a finite number but instead got `{value}`.")]
442    BadLatitude { document_id: Value, value: Value },
443    #[error("Could not parse longitude in the document with the id: `{document_id}`. Was expecting a finite number but instead got `{value}`.")]
444    BadLongitude { document_id: Value, value: Value },
445}
446
447#[allow(dead_code)]
448fn format_invalid_filter_distribution(
449    invalid_facets_name: &BTreeSet<String>,
450    valid_patterns: &BTreeSet<String>,
451) -> String {
452    let mut result = String::new();
453
454    if invalid_facets_name.is_empty() {
455        if valid_patterns.is_empty() {
456            return "this index does not have configured filterable attributes.".into();
457        }
458    } else {
459        match invalid_facets_name.len() {
460            1 => write!(
461                result,
462                "Attribute `{}` is not filterable.",
463                invalid_facets_name.first().unwrap()
464            )
465            .unwrap(),
466            _ => write!(
467                result,
468                "Attributes `{}` are not filterable.",
469                invalid_facets_name.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")
470            )
471            .unwrap(),
472        };
473    }
474
475    if valid_patterns.is_empty() {
476        if !invalid_facets_name.is_empty() {
477            write!(result, " This index does not have configured filterable attributes.").unwrap();
478        }
479    } else {
480        match valid_patterns.len() {
481            1 => write!(
482                result,
483                " Available filterable attributes patterns are: `{}`.",
484                valid_patterns.first().unwrap()
485            )
486            .unwrap(),
487            _ => write!(
488                result,
489                " Available filterable attributes patterns are: `{}`.",
490                valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")
491            )
492            .unwrap(),
493        }
494    }
495
496    result
497}
498
499/// A little macro helper to autogenerate From implementation that needs two `Into`.
500/// Given the following parameters: `error_from_sub_error!(FieldIdMapMissingEntry => InternalError)`
501/// the macro will create the following code:
502/// ```ignore
503/// impl From<FieldIdMapMissingEntry> for Error {
504///     fn from(error: FieldIdMapMissingEntry) -> Error {
505///         Error::from(<InternalError>::from(error))
506///     }
507/// }
508/// ```
509macro_rules! error_from_sub_error {
510    () => {};
511    ($sub:ty => $intermediate:ty) => {
512        impl From<$sub> for Error {
513            fn from(error: $sub) -> Error {
514                Error::from(<$intermediate>::from(error))
515            }
516        }
517    };
518    ($($sub:ty => $intermediate:ty $(,)?),+) => {
519        $(error_from_sub_error!($sub => $intermediate);)+
520    };
521}
522
523error_from_sub_error! {
524    FieldIdMapMissingEntry => InternalError,
525    fst::Error => InternalError,
526    documents::Error => InternalError,
527    str::Utf8Error => InternalError,
528    ThreadPoolBuildError => InternalError,
529    SerializationError => InternalError,
530    Box<GeoError> => UserError,
531    CriterionError => UserError,
532}
533
534impl<E> From<grenad::Error<E>> for Error
535where
536    Error: From<E>,
537{
538    fn from(error: grenad::Error<E>) -> Error {
539        match error {
540            grenad::Error::Io(error) => Error::IoError(error),
541            grenad::Error::Merge(error) => Error::from(error),
542            grenad::Error::InvalidCompressionType => {
543                Error::InternalError(InternalError::GrenadInvalidCompressionType)
544            }
545            grenad::Error::InvalidFormatVersion => {
546                Error::InternalError(InternalError::GrenadInvalidFormatVersion)
547            }
548        }
549    }
550}
551
552impl From<DocumentsBatchCursorError> for Error {
553    fn from(error: DocumentsBatchCursorError) -> Error {
554        match error {
555            DocumentsBatchCursorError::Grenad(e) => Error::from(e),
556            DocumentsBatchCursorError::SerdeJson(e) => Error::from(InternalError::from(e)),
557        }
558    }
559}
560
561impl From<Infallible> for Error {
562    fn from(_error: Infallible) -> Error {
563        unreachable!()
564    }
565}
566
567impl From<HeedError> for Error {
568    fn from(error: HeedError) -> Error {
569        use self::Error::*;
570        use self::InternalError::*;
571        use self::SerializationError::*;
572        use self::UserError::*;
573
574        match error {
575            HeedError::Io(error) => Error::from(error),
576            HeedError::Mdb(MdbError::MapFull) => UserError(MaxDatabaseSizeReached),
577            HeedError::Mdb(MdbError::Invalid) => UserError(InvalidStoreFile),
578            HeedError::Mdb(error) => InternalError(Store(error)),
579            // TODO use the encoding
580            HeedError::Encoding(_) => InternalError(Serialization(Encoding { db_name: None })),
581            HeedError::Decoding(_) => InternalError(Serialization(Decoding { db_name: None })),
582            HeedError::EnvAlreadyOpened { .. } => UserError(EnvAlreadyOpened),
583        }
584    }
585}
586
587#[derive(Debug, Clone, Copy)]
588pub enum FaultSource {
589    User,
590    Runtime,
591    Bug,
592    Undecided,
593}
594
595impl std::fmt::Display for FaultSource {
596    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
597        let s = match self {
598            FaultSource::User => "user error",
599            FaultSource::Runtime => "runtime error",
600            FaultSource::Bug => "coding error",
601            FaultSource::Undecided => "error",
602        };
603        f.write_str(s)
604    }
605}
606
607#[test]
608fn conditionally_lookup_for_error_message() {
609    let prefix = "Attribute `name` is not sortable.";
610    let messages = vec![
611        (BTreeSet::new(), "This index does not have configured sortable attributes."),
612        (BTreeSet::from(["age".to_string()]), "Available sortable attributes are: `age`."),
613    ];
614
615    for (list, suffix) in messages {
616        let err = UserError::InvalidSortableAttribute {
617            field: "name".to_string(),
618            valid_fields: list,
619            hidden_fields: false,
620        };
621
622        assert_eq!(err.to_string(), format!("{} {}", prefix, suffix));
623    }
624}