Skip to main content

drive/query/
filter.rs

1//! Document subscription filtering
2//!
3//! This module provides primitives to express and evaluate subscription filters for
4//! document state transitions. The main entry point is `DriveDocumentQueryFilter`, which
5//! holds a contract reference, a document type name, and action-specific match clauses
6//! (`DocumentActionMatchClauses`).
7//!
8//! Filtering in brief:
9//! - Create: evaluates `new_document_clauses` on the transition's data payload.
10//! - Replace: evaluates `original_document_clauses` on the original document and
11//!   `new_document_clauses` on the replacement data.
12//! - Delete: evaluates `original_document_clauses` on the original document, or on an
13//!   indexOnly delete's values.
14//! - A create's and a replace's data, and an indexOnly delete's values, are read as the
15//!   platform stores them: with every `generatedFrom` property the transition leaves out
16//!   generated from its params (protocol version 14).
17//! - Transfer: evaluates `original_document_clauses` and a new `owner_clause` against
18//!   the `recipient_owner_id`.
19//! - UpdatePrice: evaluates `original_document_clauses` and a `price_clause` against
20//!   the new price in the transition.
21//! - Purchase: evaluates `original_document_clauses` and an `owner_clause` against the
22//!   batch owner (purchaser) ID.
23//!
24//! Usage:
25//! - First check: call `matches_document_transition()` per transition to
26//!   evaluate applicable constraints before fetching the original document. Decide
27//!   whether to fetch the original document (returns Pass/Fail/NeedsOriginal).
28//! - Second check: only if the first check returned `NeedsOriginal`, fetch the original
29//!   `Document` and call `matches_original_document()` to evaluate original-dependent clauses.
30//!
31//! Validation:
32//! - `validate()` performs structural checks: confirms the document type exists for the
33//!   contract, enforces action-specific composition rules (e.g., at least one non-empty
34//!   clause where required), and validates operator/value compatibility for scalar clauses
35//!   like `owner_clause` and `price_clause`.
36
37use std::borrow::Cow;
38use std::collections::BTreeMap;
39use dpp::data_contract::accessors::v0::DataContractV0Getters;
40use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
41use dpp::data_contract::document_type::methods::DocumentTypeBasicMethods;
42use dpp::data_contract::document_type::typed_array_property;
43use dpp::data_contract::DataContract;
44use dpp::platform_value::Value;
45use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods;
46use dpp::document::{Document, DocumentV0Getters};
47use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition;
48use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods;
49use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods;
50use dpp::state_transition::batch_transition::document_replace_transition::v0::v0_methods::DocumentReplaceTransitionV0Methods;
51use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods;
52use dpp::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods;
53use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods;
54use crate::query::{
55    InternalClauses, QuerySyntaxSimpleValidationResult, ValueClause, WhereClause, WhereOperator,
56};
57use crate::error::query::QuerySyntaxError;
58use dpp::platform_value::ValueMapHelper;
59use dpp::version::PlatformVersion;
60
61/// Filter used to match document transitions for subscriptions.
62///
63/// Targets a specific data contract and document type, and carries action-specific
64/// match clauses via `DocumentActionMatchClauses`. Use `matches_document_transition()`
65/// and `matches_original_document()` to evaluate document transitions.
66/// `validate()` performs structural checks (document type exists, clause composition rules).
67#[cfg(any(feature = "server", feature = "verify"))]
68#[derive(Debug, PartialEq, Clone)]
69pub struct DriveDocumentQueryFilter<'a> {
70    /// DataContract
71    pub contract: &'a DataContract,
72    /// Document type name
73    pub document_type_name: String,
74    /// Action-specific clauses
75    pub action_clauses: DocumentActionMatchClauses,
76}
77
78/// Result of evaluating constraints for a transition before potentially fetching the original document.
79#[cfg(any(feature = "server", feature = "verify"))]
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum TransitionCheckResult {
82    /// All applicable transition-level checks pass and no original is required.
83    Pass,
84    /// Some transition-level check fails; do not fetch original.
85    Fail,
86    /// Transition-level checks pass, original clauses are non-empty and must be evaluated.
87    NeedsOriginal,
88}
89
90/// Action-specific filter clauses for matching document transitions.
91///
92/// These clauses are used to evaluate whether a given document transition
93/// (Create/Replace/Delete/Transfer/UpdatePrice/Purchase) matches a subscription
94/// filter.
95///
96/// Conventions:
97/// - Empty `InternalClauses` = no constraint for document-data checks.
98/// - `Option<ValueClause>` = optional scalar constraint (owner/price); `None` = no constraint.
99/// - Action-specific “at least one present” rules are enforced by `validate()`.
100#[allow(clippy::large_enum_variant)]
101#[derive(Debug, PartialEq, Clone)]
102pub enum DocumentActionMatchClauses {
103    /// Create: filters on the new document data.
104    Create {
105        /// Clauses on the new document data.
106        new_document_clauses: InternalClauses,
107    },
108    /// Replace: filters on original and/or new document data.
109    Replace {
110        /// Clauses on the original document data (pre-change).
111        original_document_clauses: InternalClauses,
112        /// Clauses on the new document data (replacement).
113        new_document_clauses: InternalClauses,
114    },
115    /// Delete: filters on the original (existing) document.
116    Delete {
117        /// Clauses on the original document data.
118        original_document_clauses: InternalClauses,
119    },
120    /// Transfer: filters on original data and/or recipient owner id.
121    Transfer {
122        /// Clauses on the original document data.
123        original_document_clauses: InternalClauses,
124        /// Constraint on the recipient owner id.
125        owner_clause: Option<ValueClause>,
126    },
127    /// UpdatePrice: filters on original data and/or the new price.
128    UpdatePrice {
129        /// Clauses on the original document data.
130        original_document_clauses: InternalClauses,
131        /// Constraint on the new price.
132        price_clause: Option<ValueClause>,
133    },
134    /// Purchase: filters on original data and/or batch owner id.
135    Purchase {
136        /// Clauses on the original document data.
137        original_document_clauses: InternalClauses,
138        /// Constraint on the batch owner (purchaser) id.
139        owner_clause: Option<ValueClause>,
140    },
141}
142
143impl DriveDocumentQueryFilter<'_> {
144    /// Check a transition using only transition-level constraints.
145    ///
146    /// When to run:
147    /// - Call this for each incoming transition before
148    ///   fetching the original document. It short-circuits on obvious mismatches and
149    ///   tells you if an original is needed at all for the final decision.
150    ///
151    /// Returns:
152    /// - `Pass` if all applicable transition-level checks pass and no original is needed.
153    /// - `Fail` if any transition-level check fails (no need to fetch original).
154    /// - `NeedsOriginal` if transition-level checks pass but original clauses are non-empty
155    ///   and must be evaluated with the original document.
156    ///
157    /// A create's and a replace's data, and an indexOnly delete's values, are judged as
158    /// the platform stores them at `platform_version`: a `generatedFrom` property the
159    /// transition leaves out is generated from its params first.
160    #[cfg(any(feature = "server", feature = "verify"))]
161    pub fn matches_document_transition(
162        &self,
163        document_transition: &DocumentTransition,
164        batch_owner_value: Option<&Value>, // Only used for Purchase
165        platform_version: &PlatformVersion,
166    ) -> TransitionCheckResult {
167        // Fast reject on contract/type mismatch common to all transitions
168        if document_transition.base().data_contract_id() != self.contract.id()
169            || document_transition.base().document_type_name() != &self.document_type_name
170        {
171            return TransitionCheckResult::Fail;
172        }
173
174        // Document ID value used by clause evaluation paths
175        let id_value: Value = document_transition.base().id().into();
176
177        match document_transition {
178            DocumentTransition::Create(create) => {
179                if let DocumentActionMatchClauses::Create {
180                    new_document_clauses,
181                } = &self.action_clauses
182                {
183                    let Some(data) = self.data_as_stored(create.data(), platform_version) else {
184                        return TransitionCheckResult::Fail;
185                    };
186                    if self.evaluate_clauses(new_document_clauses, &id_value, &data) {
187                        TransitionCheckResult::Pass
188                    } else {
189                        TransitionCheckResult::Fail
190                    }
191                } else {
192                    TransitionCheckResult::Fail
193                }
194            }
195            DocumentTransition::Replace(replace) => {
196                if let DocumentActionMatchClauses::Replace {
197                    original_document_clauses,
198                    new_document_clauses,
199                } = &self.action_clauses
200                {
201                    let final_ok = if new_document_clauses.is_empty() {
202                        true
203                    } else {
204                        let Some(data) = self.data_as_stored(replace.data(), platform_version)
205                        else {
206                            return TransitionCheckResult::Fail;
207                        };
208                        self.evaluate_clauses(new_document_clauses, &id_value, &data)
209                    };
210                    if !final_ok {
211                        return TransitionCheckResult::Fail;
212                    }
213                    if original_document_clauses.is_empty() {
214                        return TransitionCheckResult::Pass;
215                    }
216                    if original_document_clauses.is_for_primary_key() {
217                        if self.evaluate_clauses(
218                            original_document_clauses,
219                            &id_value,
220                            &BTreeMap::new(),
221                        ) {
222                            return TransitionCheckResult::Pass;
223                        }
224                        return TransitionCheckResult::Fail;
225                    }
226                    TransitionCheckResult::NeedsOriginal
227                } else {
228                    TransitionCheckResult::Fail
229                }
230            }
231            DocumentTransition::Delete(_) => {
232                if let DocumentActionMatchClauses::Delete {
233                    original_document_clauses,
234                } = &self.action_clauses
235                {
236                    if original_document_clauses.is_empty() {
237                        return TransitionCheckResult::Pass;
238                    }
239                    if original_document_clauses.is_for_primary_key() {
240                        if self.evaluate_clauses(
241                            original_document_clauses,
242                            &id_value,
243                            &BTreeMap::new(),
244                        ) {
245                            return TransitionCheckResult::Pass;
246                        }
247                        return TransitionCheckResult::Fail;
248                    }
249                    TransitionCheckResult::NeedsOriginal
250                } else {
251                    TransitionCheckResult::Fail
252                }
253            }
254            DocumentTransition::Transfer(transfer) => {
255                if let DocumentActionMatchClauses::Transfer {
256                    original_document_clauses,
257                    owner_clause,
258                } = &self.action_clauses
259                {
260                    let new_owner_value: Value = transfer.recipient_owner_id().into();
261                    let owner_ok = match owner_clause {
262                        Some(clause) => clause.matches_value(&new_owner_value),
263                        None => true,
264                    };
265                    if !owner_ok {
266                        return TransitionCheckResult::Fail;
267                    }
268                    if original_document_clauses.is_empty() {
269                        return TransitionCheckResult::Pass;
270                    }
271                    if original_document_clauses.is_for_primary_key() {
272                        if self.evaluate_clauses(
273                            original_document_clauses,
274                            &id_value,
275                            &BTreeMap::new(),
276                        ) {
277                            return TransitionCheckResult::Pass;
278                        }
279                        return TransitionCheckResult::Fail;
280                    }
281                    TransitionCheckResult::NeedsOriginal
282                } else {
283                    TransitionCheckResult::Fail
284                }
285            }
286            DocumentTransition::UpdatePrice(update_price) => {
287                if let DocumentActionMatchClauses::UpdatePrice {
288                    original_document_clauses,
289                    price_clause,
290                } = &self.action_clauses
291                {
292                    let price_value = Value::U64(update_price.price());
293                    let price_ok = match price_clause {
294                        Some(clause) => clause.matches_value(&price_value),
295                        None => true,
296                    };
297                    if !price_ok {
298                        return TransitionCheckResult::Fail;
299                    }
300                    if original_document_clauses.is_empty() {
301                        return TransitionCheckResult::Pass;
302                    }
303                    if original_document_clauses.is_for_primary_key() {
304                        if self.evaluate_clauses(
305                            original_document_clauses,
306                            &id_value,
307                            &BTreeMap::new(),
308                        ) {
309                            return TransitionCheckResult::Pass;
310                        }
311                        return TransitionCheckResult::Fail;
312                    }
313                    TransitionCheckResult::NeedsOriginal
314                } else {
315                    TransitionCheckResult::Fail
316                }
317            }
318            DocumentTransition::Purchase(_) => {
319                if let DocumentActionMatchClauses::Purchase {
320                    original_document_clauses,
321                    owner_clause,
322                } = &self.action_clauses
323                {
324                    let owner_ok = match (owner_clause, batch_owner_value) {
325                        (Some(clause), Some(val)) => clause.matches_value(val),
326                        (Some(_), None) => return TransitionCheckResult::Fail,
327                        (None, _) => true,
328                    };
329                    if !owner_ok {
330                        return TransitionCheckResult::Fail;
331                    }
332                    if original_document_clauses.is_empty() {
333                        return TransitionCheckResult::Pass;
334                    }
335                    if original_document_clauses.is_for_primary_key() {
336                        if self.evaluate_clauses(
337                            original_document_clauses,
338                            &id_value,
339                            &BTreeMap::new(),
340                        ) {
341                            return TransitionCheckResult::Pass;
342                        }
343                        return TransitionCheckResult::Fail;
344                    }
345                    TransitionCheckResult::NeedsOriginal
346                } else {
347                    TransitionCheckResult::Fail
348                }
349            }
350            DocumentTransition::IndexOnlyDelete(index_only_delete) => {
351                if let DocumentActionMatchClauses::Delete {
352                    original_document_clauses,
353                } = &self.action_clauses
354                {
355                    // An indexOnly document has no stored row to fetch:
356                    // the transition carries the document's full values,
357                    // so the "original document" clauses evaluate
358                    // directly against them and no original is ever
359                    // needed.
360                    let Some(values) =
361                        self.data_as_stored(index_only_delete.data(), platform_version)
362                    else {
363                        return TransitionCheckResult::Fail;
364                    };
365                    if self.evaluate_clauses(original_document_clauses, &id_value, &values) {
366                        TransitionCheckResult::Pass
367                    } else {
368                        TransitionCheckResult::Fail
369                    }
370                } else {
371                    TransitionCheckResult::Fail
372                }
373            }
374        }
375    }
376
377    /// Evaluates original-dependent clauses against the provided original `Document`.
378    ///
379    /// When to run:
380    /// - After `matches_document_transition` returns `NeedsOriginal` and the caller fetches
381    ///   the original document.
382    /// - This evaluates only original-dependent clauses; transition-level checks were
383    ///   already applied during the first phase.
384    #[cfg(any(feature = "server", feature = "verify"))]
385    pub fn matches_original_document(&self, original_document: &Document) -> bool {
386        // Evaluate only original-dependent clauses. Transition base was validated earlier.
387        match &self.action_clauses {
388            DocumentActionMatchClauses::Replace {
389                original_document_clauses,
390                ..
391            }
392            | DocumentActionMatchClauses::Delete {
393                original_document_clauses,
394            }
395            | DocumentActionMatchClauses::Transfer {
396                original_document_clauses,
397                ..
398            }
399            | DocumentActionMatchClauses::UpdatePrice {
400                original_document_clauses,
401                ..
402            }
403            | DocumentActionMatchClauses::Purchase {
404                original_document_clauses,
405                ..
406            } => {
407                let id_value: Value = original_document.id().into();
408                self.evaluate_clauses(
409                    original_document_clauses,
410                    &id_value,
411                    original_document.properties(),
412                )
413            }
414            _ => false,
415        }
416    }
417
418    /// A transition's `data` as the platform stores it, every `generatedFrom` property it
419    /// leaves out generated ([`DocumentTypeBasicMethods::data_as_stored`]). `None`, which
420    /// fails the match, for a document type the contract lacks (`validate` refuses the
421    /// filter then) or a version table this build does not know.
422    #[cfg(any(feature = "server", feature = "verify"))]
423    fn data_as_stored<'d>(
424        &self,
425        data: &'d BTreeMap<String, Value>,
426        platform_version: &PlatformVersion,
427    ) -> Option<Cow<'d, BTreeMap<String, Value>>> {
428        let document_type = self
429            .contract
430            .document_type_for_name(&self.document_type_name)
431            .ok()?;
432        document_type.data_as_stored(data, platform_version).ok()
433    }
434
435    /// Whether `clause` holds for the document's `value` of its field. A clause
436    /// reading the elements of a typed array (`contains`, `containsAny`) matches
437    /// an element that keys as one of its values does, as an indexed documents
438    /// query matches it: an identifier given as bytes or base58 text, or an
439    /// integer of another width, matches the stored element it keys like. Every
440    /// other clause compares values as before.
441    #[cfg(any(feature = "server", feature = "verify"))]
442    fn clause_matches(&self, clause: &WhereClause, value: &Value) -> bool {
443        if !clause.operator.reads_array_elements() {
444            return clause.matches_value(value);
445        }
446        let Some(document_type) = self
447            .contract
448            .document_type_optional_for_name(&self.document_type_name)
449        else {
450            return false;
451        };
452        let Some(typed_array) =
453            typed_array_property(document_type.flattened_properties(), &clause.field)
454        else {
455            return false;
456        };
457        let key = |value: &Value| typed_array.item_type.encode_value_for_tree_keys(value).ok();
458        let wanted: Vec<Vec<u8>> = if clause.operator == WhereOperator::ContainsAny {
459            match clause.in_values().data {
460                Some(values) => values.iter().filter_map(key).collect(),
461                None => return false,
462            }
463        } else {
464            key(&clause.value).into_iter().collect()
465        };
466        let Value::Array(elements) = value else {
467            return false;
468        };
469        elements
470            .iter()
471            .filter_map(key)
472            .any(|element| wanted.contains(&element))
473    }
474
475    /// Single clause evaluator used by both transition and original-document paths.
476    #[cfg(any(feature = "server", feature = "verify"))]
477    fn evaluate_clauses(
478        &self,
479        clauses: &InternalClauses,
480        document_id_value: &Value,
481        document_data: &BTreeMap<String, Value>,
482    ) -> bool {
483        // Primary key IN clause
484        if let Some(primary_key_in_clause) = &clauses.primary_key_in_clause {
485            if !primary_key_in_clause.matches_value(document_id_value) {
486                return false;
487            }
488        }
489
490        // Primary key EQUAL clause
491        if let Some(primary_key_equal_clause) = &clauses.primary_key_equal_clause {
492            if !primary_key_equal_clause.matches_value(document_id_value) {
493                return false;
494            }
495        }
496
497        // In clauses
498        for in_clause in &clauses.in_clauses {
499            let field_value = get_value_by_path(document_data, &in_clause.field);
500            if let Some(value) = field_value {
501                if !self.clause_matches(in_clause, value) {
502                    return false;
503                }
504            } else {
505                return false;
506            }
507        }
508
509        // Range clause
510        if let Some(range_clause) = &clauses.range_clause {
511            let field_value = get_value_by_path(document_data, &range_clause.field);
512            if let Some(value) = field_value {
513                if !range_clause.matches_value(value) {
514                    return false;
515                }
516            } else {
517                return false;
518            }
519        }
520
521        // Equal clauses
522        for (field, equal_clause) in &clauses.equal_clauses {
523            let field_value = get_value_by_path(document_data, field);
524            if let Some(value) = field_value {
525                if !self.clause_matches(equal_clause, value) {
526                    return false;
527                }
528            } else {
529                return false;
530            }
531        }
532
533        true
534    }
535
536    /// Validate the filter structure and clauses.
537    ///
538    /// In addition to these validations, the subscription host should check the contract's existence.
539    #[cfg(any(feature = "server", feature = "verify"))]
540    pub fn validate(&self) -> QuerySyntaxSimpleValidationResult {
541        // Ensure the document type exists
542        let Some(document_type) = self
543            .contract
544            .document_type_optional_for_name(&self.document_type_name)
545        else {
546            return QuerySyntaxSimpleValidationResult::new_with_error(
547                QuerySyntaxError::DocumentTypeNotFound("unknown document type"),
548            );
549        };
550
551        match &self.action_clauses {
552            DocumentActionMatchClauses::Create {
553                new_document_clauses,
554            } => new_document_clauses.validate_against_schema(document_type),
555            DocumentActionMatchClauses::Replace {
556                original_document_clauses,
557                new_document_clauses,
558            } => {
559                if !original_document_clauses.is_empty() {
560                    let result = original_document_clauses.validate_against_schema(document_type);
561                    if result.is_err() {
562                        return result;
563                    }
564                }
565                if !new_document_clauses.is_empty() {
566                    new_document_clauses.validate_against_schema(document_type)
567                } else {
568                    QuerySyntaxSimpleValidationResult::new()
569                }
570            }
571            DocumentActionMatchClauses::Delete {
572                original_document_clauses,
573            } => original_document_clauses.validate_against_schema(document_type),
574            DocumentActionMatchClauses::Transfer {
575                original_document_clauses,
576                owner_clause,
577            } => {
578                if !original_document_clauses.is_empty() {
579                    let result = original_document_clauses.validate_against_schema(document_type);
580                    if result.is_err() {
581                        return result;
582                    }
583                }
584                if let Some(owner) = owner_clause {
585                    let ok = match owner.operator {
586                        WhereOperator::Equal => matches!(owner.value, Value::Identifier(_)),
587                        WhereOperator::In => match &owner.value {
588                            Value::Array(arr) => {
589                                arr.iter().all(|v| matches!(v, Value::Identifier(_)))
590                            }
591                            _ => false,
592                        },
593                        _ => false,
594                    };
595                    if ok {
596                        QuerySyntaxSimpleValidationResult::new()
597                    } else {
598                        QuerySyntaxSimpleValidationResult::new_with_error(
599                            QuerySyntaxError::InvalidWhereClauseComponents("invalid owner clause"),
600                        )
601                    }
602                } else {
603                    QuerySyntaxSimpleValidationResult::new()
604                }
605            }
606            DocumentActionMatchClauses::UpdatePrice {
607                original_document_clauses,
608                price_clause,
609            } => {
610                if !original_document_clauses.is_empty() {
611                    let result = original_document_clauses.validate_against_schema(document_type);
612                    if result.is_err() {
613                        return result;
614                    }
615                }
616                if let Some(price) = price_clause {
617                    let ok = match price.operator {
618                        WhereOperator::Equal
619                        | WhereOperator::GreaterThan
620                        | WhereOperator::GreaterThanOrEquals
621                        | WhereOperator::LessThan
622                        | WhereOperator::LessThanOrEquals => {
623                            price.value.is_integer_can_fit_in_64_bits()
624                        }
625                        WhereOperator::Between
626                        | WhereOperator::BetweenExcludeBounds
627                        | WhereOperator::BetweenExcludeLeft
628                        | WhereOperator::BetweenExcludeRight => match &price.value {
629                            Value::Array(arr) => {
630                                arr.len() == 2
631                                    && arr.iter().all(|v| v.is_integer_can_fit_in_64_bits())
632                                    && arr[0] < arr[1]
633                            }
634                            _ => false,
635                        },
636                        WhereOperator::In => match &price.value {
637                            Value::Array(arr) => {
638                                arr.iter().all(|v| v.is_integer_can_fit_in_64_bits())
639                            }
640                            _ => false,
641                        },
642                        WhereOperator::StartsWith
643                        | WhereOperator::Contains
644                        | WhereOperator::ContainsAny => false,
645                    };
646                    if ok {
647                        QuerySyntaxSimpleValidationResult::new()
648                    } else {
649                        QuerySyntaxSimpleValidationResult::new_with_error(
650                            QuerySyntaxError::InvalidWhereClauseComponents("invalid price clause"),
651                        )
652                    }
653                } else {
654                    QuerySyntaxSimpleValidationResult::new()
655                }
656            }
657            DocumentActionMatchClauses::Purchase {
658                original_document_clauses,
659                owner_clause,
660            } => {
661                if !original_document_clauses.is_empty() {
662                    let result = original_document_clauses.validate_against_schema(document_type);
663                    if result.is_err() {
664                        return result;
665                    }
666                }
667                if let Some(owner) = owner_clause {
668                    let ok = match owner.operator {
669                        WhereOperator::Equal => matches!(owner.value, Value::Identifier(_)),
670                        WhereOperator::In => match &owner.value {
671                            Value::Array(arr) => {
672                                arr.iter().all(|v| matches!(v, Value::Identifier(_)))
673                            }
674                            _ => false,
675                        },
676                        _ => false,
677                    };
678                    if ok {
679                        QuerySyntaxSimpleValidationResult::new()
680                    } else {
681                        QuerySyntaxSimpleValidationResult::new_with_error(
682                            QuerySyntaxError::InvalidWhereClauseComponents("invalid owner clause"),
683                        )
684                    }
685                } else {
686                    QuerySyntaxSimpleValidationResult::new()
687                }
688            }
689        }
690    }
691}
692
693/// Resolve a dot-notated path into a nested `BTreeMap<String, Value>` payload.
694///
695/// Supports dot notation like `meta.status` by walking `Value::Map` entries
696/// using `ValueMapHelper`. Returns `None` if any segment is missing or if a
697/// non-map value is encountered before the final segment. An empty `path`
698/// returns `None`.
699#[cfg(any(feature = "server", feature = "verify"))]
700fn get_value_by_path<'a>(root: &'a BTreeMap<String, Value>, path: &str) -> Option<&'a Value> {
701    if path.is_empty() {
702        return None;
703    }
704    let mut current: Option<&Value> = None;
705    let mut segments = path.split('.');
706    if let Some(first) = segments.next() {
707        current = root.get(first);
708    }
709    for seg in segments {
710        match current {
711            Some(Value::Map(ref vm)) => {
712                current = vm.get_optional_key(seg);
713            }
714            _ => return None,
715        }
716    }
717    current
718}
719
720#[cfg(test)]
721mod tests {
722    use super::*;
723    use crate::query::{ValueClause, WhereClause, WhereOperator};
724    use dpp::document::{Document, DocumentV0};
725    use dpp::prelude::Identifier;
726    use dpp::state_transition::batch_transition::document_base_transition::v1::DocumentBaseTransitionV1;
727    use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition;
728    use dpp::tests::fixtures::get_data_contract_fixture;
729    use dpp::version::LATEST_PLATFORM_VERSION;
730
731    #[test]
732    fn test_matches_document_basic() {
733        // Get a test contract from fixtures
734        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
735        let contract = fixture.data_contract_owned();
736
737        // Create a filter with no clauses (should match if contract and type match)
738        let internal_clauses = InternalClauses::default();
739        let filter = DriveDocumentQueryFilter {
740            contract: &contract,
741            document_type_name: "niceDocument".to_string(),
742            action_clauses: DocumentActionMatchClauses::Create {
743                new_document_clauses: internal_clauses.clone(),
744            },
745        };
746
747        // Create matching document base
748        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
749            id: Identifier::from([3u8; 32]),
750            document_type_name: "niceDocument".to_string(),
751            data_contract_id: contract.id(),
752            identity_contract_nonce: 0,
753            token_payment_info: None,
754        });
755
756        let document_data = BTreeMap::new();
757
758        // With no clauses, evaluation should be true regardless of data
759        let id_value: Value = document_base.id().into();
760        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &document_data));
761    }
762
763    #[test]
764    fn test_matches_document_with_primary_key_equal() {
765        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
766        let contract = fixture.data_contract_owned();
767
768        let target_id = Identifier::from([42u8; 32]);
769
770        let internal_clauses = InternalClauses {
771            primary_key_equal_clause: Some(WhereClause {
772                field: "$id".to_string(),
773                operator: WhereOperator::Equal,
774                value: target_id.into(),
775            }),
776            ..Default::default()
777        };
778
779        let filter = DriveDocumentQueryFilter {
780            contract: &contract,
781            document_type_name: "niceDocument".to_string(),
782            action_clauses: DocumentActionMatchClauses::Create {
783                new_document_clauses: internal_clauses.clone(),
784            },
785        };
786
787        // Test with matching ID
788        let matching_doc = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
789            id: target_id,
790            document_type_name: "niceDocument".to_string(),
791            data_contract_id: contract.id(),
792            identity_contract_nonce: 0,
793            token_payment_info: None,
794        });
795
796        let document_data = BTreeMap::new();
797
798        let id_value: Value = matching_doc.id().into();
799        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &document_data));
800
801        // Test with different ID
802        let non_matching_doc = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
803            id: Identifier::from([99u8; 32]),
804            document_type_name: "niceDocument".to_string(),
805            data_contract_id: contract.id(),
806            identity_contract_nonce: 0,
807            token_payment_info: None,
808        });
809
810        let non_id_value: Value = non_matching_doc.id().into();
811        assert!(!filter.evaluate_clauses(&internal_clauses, &non_id_value, &document_data));
812    }
813
814    #[test]
815    fn test_matches_document_with_field_filters() {
816        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
817        let contract = fixture.data_contract_owned();
818
819        // Test Equal operator
820        let mut equal_clauses = BTreeMap::new();
821        equal_clauses.insert(
822            "name".to_string(),
823            WhereClause {
824                field: "name".to_string(),
825                operator: WhereOperator::Equal,
826                value: Value::Text("example".to_string()),
827            },
828        );
829
830        let internal_clauses = InternalClauses {
831            equal_clauses,
832            ..Default::default()
833        };
834
835        let filter = DriveDocumentQueryFilter {
836            contract: &contract,
837            document_type_name: "niceDocument".to_string(),
838            action_clauses: DocumentActionMatchClauses::Create {
839                new_document_clauses: internal_clauses.clone(),
840            },
841        };
842
843        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
844            id: Identifier::from([3u8; 32]),
845            document_type_name: "niceDocument".to_string(),
846            data_contract_id: contract.id(),
847            identity_contract_nonce: 0,
848            token_payment_info: None,
849        });
850
851        // Test with matching data
852        let mut matching_data = BTreeMap::new();
853        matching_data.insert("name".to_string(), Value::Text("example".to_string()));
854
855        let id_value: Value = document_base.id().into();
856        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &matching_data));
857
858        // Test with non-matching data
859        let mut non_matching_data = BTreeMap::new();
860        non_matching_data.insert("name".to_string(), Value::Text("different".to_string()));
861
862        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &non_matching_data));
863
864        // Test with missing field
865        let empty_data = BTreeMap::new();
866        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &empty_data));
867    }
868
869    #[test]
870    fn test_matches_document_with_in_operator() {
871        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
872        let contract = fixture.data_contract_owned();
873
874        let allowed_values = vec![
875            Value::Text("active".to_string()),
876            Value::Text("pending".to_string()),
877        ];
878
879        let internal_clauses = InternalClauses {
880            in_clauses: vec![WhereClause {
881                field: "status".to_string(),
882                operator: WhereOperator::In,
883                value: Value::Array(allowed_values),
884            }],
885            ..Default::default()
886        };
887
888        let filter = DriveDocumentQueryFilter {
889            contract: &contract,
890            document_type_name: "niceDocument".to_string(),
891            action_clauses: DocumentActionMatchClauses::Create {
892                new_document_clauses: internal_clauses.clone(),
893            },
894        };
895
896        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
897            id: Identifier::from([3u8; 32]),
898            document_type_name: "niceDocument".to_string(),
899            data_contract_id: contract.id(),
900            identity_contract_nonce: 0,
901            token_payment_info: None,
902        });
903
904        // Test with value in list
905        let mut matching_data = BTreeMap::new();
906        matching_data.insert("status".to_string(), Value::Text("active".to_string()));
907        let id_value: Value = document_base.id().into();
908        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &matching_data));
909
910        // Test with value not in list
911        let mut non_matching_data = BTreeMap::new();
912        non_matching_data.insert("status".to_string(), Value::Text("completed".to_string()));
913        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &non_matching_data));
914    }
915
916    #[test]
917    fn test_matches_document_with_range_operators() {
918        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
919        let contract = fixture.data_contract_owned();
920
921        // Test GreaterThan
922        let internal_clauses = InternalClauses {
923            range_clause: Some(WhereClause {
924                field: "score".to_string(),
925                operator: WhereOperator::GreaterThan,
926                value: Value::U64(50),
927            }),
928            ..Default::default()
929        };
930
931        let filter = DriveDocumentQueryFilter {
932            contract: &contract,
933            document_type_name: "niceDocument".to_string(),
934            action_clauses: DocumentActionMatchClauses::Create {
935                new_document_clauses: internal_clauses.clone(),
936            },
937        };
938
939        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
940            id: Identifier::from([3u8; 32]),
941            document_type_name: "niceDocument".to_string(),
942            data_contract_id: contract.id(),
943            identity_contract_nonce: 0,
944            token_payment_info: None,
945        });
946
947        // Test with value greater than threshold
948        let mut greater_data = BTreeMap::new();
949        greater_data.insert("score".to_string(), Value::U64(75));
950        let id_value: Value = document_base.id().into();
951        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &greater_data));
952
953        // Test with value equal to threshold (should fail for GreaterThan)
954        let mut equal_data = BTreeMap::new();
955        equal_data.insert("score".to_string(), Value::U64(50));
956        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &equal_data));
957
958        // Test with value less than threshold
959        let mut less_data = BTreeMap::new();
960        less_data.insert("score".to_string(), Value::U64(25));
961        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &less_data));
962    }
963
964    #[test]
965    fn test_matches_document_with_nested_field() {
966        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
967        let contract = fixture.data_contract_owned();
968
969        // Equal on nested field: meta.status == "active"
970        let mut equal_clauses = BTreeMap::new();
971        equal_clauses.insert(
972            "meta.status".to_string(),
973            WhereClause {
974                field: "meta.status".to_string(),
975                operator: WhereOperator::Equal,
976                value: Value::Text("active".to_string()),
977            },
978        );
979
980        let internal_clauses = InternalClauses {
981            equal_clauses,
982            ..Default::default()
983        };
984
985        let filter = DriveDocumentQueryFilter {
986            contract: &contract,
987            document_type_name: "niceDocument".to_string(),
988            action_clauses: DocumentActionMatchClauses::Create {
989                new_document_clauses: internal_clauses.clone(),
990            },
991        };
992
993        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
994            id: Identifier::from([3u8; 32]),
995            document_type_name: "niceDocument".to_string(),
996            data_contract_id: contract.id(),
997            identity_contract_nonce: 0,
998            token_payment_info: None,
999        });
1000
1001        // Build nested data: { meta: { status: "active" } }
1002        let nested = vec![(
1003            Value::Text("status".to_string()),
1004            Value::Text("active".to_string()),
1005        )];
1006        let mut data = BTreeMap::new();
1007        data.insert("meta".to_string(), Value::Map(nested));
1008
1009        let id_value: Value = document_base.id().into();
1010        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &data));
1011    }
1012
1013    #[test]
1014    fn test_validate_optional_actions_allow_empty_clauses() {
1015        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1016        let contract = fixture.data_contract_owned();
1017
1018        // Replace with none/none -> allowed
1019        let filter = DriveDocumentQueryFilter {
1020            contract: &contract,
1021            document_type_name: "niceDocument".to_string(),
1022            action_clauses: DocumentActionMatchClauses::Replace {
1023                original_document_clauses: InternalClauses::default(),
1024                new_document_clauses: InternalClauses::default(),
1025            },
1026        };
1027        assert!(filter.validate().is_valid());
1028
1029        // Replace with final only -> valid (non-empty final clauses)
1030        let filter = DriveDocumentQueryFilter {
1031            contract: &contract,
1032            document_type_name: "niceDocument".to_string(),
1033            action_clauses: DocumentActionMatchClauses::Replace {
1034                original_document_clauses: InternalClauses::default(),
1035                new_document_clauses: InternalClauses {
1036                    primary_key_equal_clause: Some(WhereClause {
1037                        field: "$id".to_string(),
1038                        operator: WhereOperator::Equal,
1039                        value: Value::Identifier([3u8; 32]),
1040                    }),
1041                    ..Default::default()
1042                },
1043            },
1044        };
1045        assert!(filter.validate().is_valid());
1046
1047        // Transfer with none/none -> allowed
1048        let filter = DriveDocumentQueryFilter {
1049            contract: &contract,
1050            document_type_name: "niceDocument".to_string(),
1051            action_clauses: DocumentActionMatchClauses::Transfer {
1052                original_document_clauses: InternalClauses::default(),
1053                owner_clause: None,
1054            },
1055        };
1056        assert!(filter.validate().is_valid());
1057
1058        // Transfer with owner only -> valid
1059        let filter = DriveDocumentQueryFilter {
1060            contract: &contract,
1061            document_type_name: "niceDocument".to_string(),
1062            action_clauses: DocumentActionMatchClauses::Transfer {
1063                original_document_clauses: InternalClauses::default(),
1064                owner_clause: Some(ValueClause {
1065                    operator: WhereOperator::Equal,
1066                    value: Value::Identifier([1u8; 32]),
1067                }),
1068            },
1069        };
1070        assert!(filter.validate().is_valid());
1071
1072        // UpdatePrice with none/none -> allowed
1073        let filter = DriveDocumentQueryFilter {
1074            contract: &contract,
1075            document_type_name: "niceDocument".to_string(),
1076            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1077                original_document_clauses: InternalClauses::default(),
1078                price_clause: None,
1079            },
1080        };
1081        assert!(filter.validate().is_valid());
1082
1083        // UpdatePrice with price only -> valid
1084        let filter = DriveDocumentQueryFilter {
1085            contract: &contract,
1086            document_type_name: "niceDocument".to_string(),
1087            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1088                original_document_clauses: InternalClauses::default(),
1089                price_clause: Some(ValueClause {
1090                    operator: WhereOperator::GreaterThan,
1091                    value: Value::U64(0),
1092                }),
1093            },
1094        };
1095        assert!(filter.validate().is_valid());
1096
1097        // Purchase with none/none -> allowed
1098        let filter = DriveDocumentQueryFilter {
1099            contract: &contract,
1100            document_type_name: "niceDocument".to_string(),
1101            action_clauses: DocumentActionMatchClauses::Purchase {
1102                original_document_clauses: InternalClauses::default(),
1103                owner_clause: None,
1104            },
1105        };
1106        assert!(filter.validate().is_valid());
1107
1108        // Purchase with owner only -> valid
1109        let filter = DriveDocumentQueryFilter {
1110            contract: &contract,
1111            document_type_name: "niceDocument".to_string(),
1112            action_clauses: DocumentActionMatchClauses::Purchase {
1113                original_document_clauses: InternalClauses::default(),
1114                owner_clause: Some(ValueClause {
1115                    operator: WhereOperator::Equal,
1116                    value: Value::Identifier([2u8; 32]),
1117                }),
1118            },
1119        };
1120        assert!(filter.validate().is_valid());
1121    }
1122
1123    #[test]
1124    fn test_transfer_owner_clause_only_matches() {
1125        use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::DocumentTransferTransitionV0;
1126        use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransition;
1127
1128        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1129        let contract = fixture.data_contract_owned();
1130
1131        let new_owner = Identifier::from([5u8; 32]);
1132
1133        // Filter checks only new owner
1134        let filter = DriveDocumentQueryFilter {
1135            contract: &contract,
1136            document_type_name: "niceDocument".to_string(),
1137            action_clauses: DocumentActionMatchClauses::Transfer {
1138                original_document_clauses: InternalClauses::default(),
1139                owner_clause: Some(ValueClause {
1140                    operator: WhereOperator::Equal,
1141                    value: new_owner.into(),
1142                }),
1143            },
1144        };
1145
1146        // Transfer transition with recipient = new_owner
1147        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1148            id: Identifier::from([3u8; 32]),
1149            document_type_name: "niceDocument".to_string(),
1150            data_contract_id: contract.id(),
1151            identity_contract_nonce: 0,
1152            token_payment_info: None,
1153        });
1154
1155        let transfer_v0 = DocumentTransferTransitionV0 {
1156            base: document_base.clone(),
1157            revision: 1_u64,
1158            recipient_owner_id: new_owner,
1159        };
1160        let transfer = DocumentTransition::Transfer(DocumentTransferTransition::V0(transfer_v0));
1161
1162        // First check should pass without needing original
1163        assert_eq!(
1164            filter.matches_document_transition(&transfer, None, PlatformVersion::latest()),
1165            TransitionCheckResult::Pass
1166        );
1167
1168        // Mismatch owner
1169        let other_owner = Identifier::from([6u8; 32]);
1170        let transfer_v0_mismatch = DocumentTransferTransitionV0 {
1171            base: document_base,
1172            revision: 1_u64,
1173            recipient_owner_id: other_owner,
1174        };
1175        let transfer_mismatch =
1176            DocumentTransition::Transfer(DocumentTransferTransition::V0(transfer_v0_mismatch));
1177        assert_eq!(
1178            filter.matches_document_transition(&transfer_mismatch, None, PlatformVersion::latest()),
1179            TransitionCheckResult::Fail
1180        );
1181    }
1182
1183    #[test]
1184    fn test_purchase_owner_clause_only_matches_and_requires_owner_context() {
1185        use dpp::fee::Credits;
1186        use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::DocumentPurchaseTransitionV0;
1187        use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransition;
1188
1189        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1190        let contract = fixture.data_contract_owned();
1191
1192        let purchaser = Identifier::from([7u8; 32]);
1193
1194        // Filter checks batch owner (purchaser)
1195        let filter = DriveDocumentQueryFilter {
1196            contract: &contract,
1197            document_type_name: "niceDocument".to_string(),
1198            action_clauses: DocumentActionMatchClauses::Purchase {
1199                original_document_clauses: InternalClauses::default(),
1200                owner_clause: Some(ValueClause {
1201                    operator: WhereOperator::Equal,
1202                    value: purchaser.into(),
1203                }),
1204            },
1205        };
1206
1207        // Purchase transition
1208        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1209            id: Identifier::from([4u8; 32]),
1210            document_type_name: "niceDocument".to_string(),
1211            data_contract_id: contract.id(),
1212            identity_contract_nonce: 0,
1213            token_payment_info: None,
1214        });
1215
1216        let purchase_v0 = DocumentPurchaseTransitionV0 {
1217            base: document_base,
1218            revision: 1_u64,
1219            price: 10 as Credits,
1220        };
1221        let purchase = DocumentTransition::Purchase(DocumentPurchaseTransition::V0(purchase_v0));
1222
1223        // Without batch owner context, should fail (owner clause requires it)
1224        assert_eq!(
1225            filter.matches_document_transition(&purchase, None, PlatformVersion::latest()),
1226            TransitionCheckResult::Fail
1227        );
1228        // With batch owner context, should pass
1229        let owner_value = Value::Identifier(purchaser.to_buffer());
1230        assert_eq!(
1231            filter.matches_document_transition(
1232                &purchase,
1233                Some(&owner_value),
1234                PlatformVersion::latest()
1235            ),
1236            TransitionCheckResult::Pass
1237        );
1238    }
1239
1240    #[test]
1241    fn test_transfer_original_clause_only_matches_with_original_document() {
1242        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1243        let contract = fixture.data_contract_owned();
1244
1245        // Filter checks only original document field
1246        let mut eq = BTreeMap::new();
1247        eq.insert(
1248            "status".to_string(),
1249            WhereClause {
1250                field: "status".to_string(),
1251                operator: WhereOperator::Equal,
1252                value: Value::Text("active".to_string()),
1253            },
1254        );
1255        let filter = DriveDocumentQueryFilter {
1256            contract: &contract,
1257            document_type_name: "niceDocument".to_string(),
1258            action_clauses: DocumentActionMatchClauses::Transfer {
1259                original_document_clauses: InternalClauses {
1260                    equal_clauses: eq,
1261                    ..Default::default()
1262                },
1263                owner_clause: None,
1264            },
1265        };
1266
1267        // Original doc present and matching
1268        let mut original = BTreeMap::new();
1269        original.insert("status".to_string(), Value::Text("active".to_string()));
1270        let original_doc = Document::V0(DocumentV0 {
1271            id: Identifier::from([9u8; 32]),
1272            owner_id: Identifier::from([0u8; 32]),
1273            properties: original,
1274            ..Default::default()
1275        });
1276        assert!(filter.matches_original_document(&original_doc));
1277
1278        // Without original doc, clause is required -> no match
1279        // No call without original: first pass already signaled it is required
1280    }
1281
1282    #[test]
1283    fn test_delete_original_clause_only_matches_with_original_document() {
1284        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1285        let contract = fixture.data_contract_owned();
1286
1287        // Filter checks only original document field
1288        let mut eq = BTreeMap::new();
1289        eq.insert(
1290            "status".to_string(),
1291            WhereClause {
1292                field: "status".to_string(),
1293                operator: WhereOperator::Equal,
1294                value: Value::Text("active".to_string()),
1295            },
1296        );
1297        let filter = DriveDocumentQueryFilter {
1298            contract: &contract,
1299            document_type_name: "niceDocument".to_string(),
1300            action_clauses: DocumentActionMatchClauses::Delete {
1301                original_document_clauses: InternalClauses {
1302                    equal_clauses: eq,
1303                    ..Default::default()
1304                },
1305            },
1306        };
1307
1308        // Original doc present and matching
1309        let mut original = BTreeMap::new();
1310        original.insert("status".to_string(), Value::Text("active".to_string()));
1311        let original_doc = Document::V0(DocumentV0 {
1312            id: Identifier::from([12u8; 32]),
1313            owner_id: Identifier::from([0u8; 32]),
1314            properties: original,
1315            ..Default::default()
1316        });
1317        assert!(filter.matches_original_document(&original_doc));
1318
1319        // Without original doc -> no match (required for Delete)
1320        // No call without original: first pass already signaled it is required
1321
1322        // Original mismatching -> no match
1323        let mut original_bad = BTreeMap::new();
1324        original_bad.insert("status".to_string(), Value::Text("inactive".to_string()));
1325        let original_doc_bad = Document::V0(DocumentV0 {
1326            id: Identifier::from([12u8; 32]),
1327            owner_id: Identifier::from([0u8; 32]),
1328            properties: original_bad,
1329            ..Default::default()
1330        });
1331        assert!(!filter.matches_original_document(&original_doc_bad));
1332    }
1333
1334    #[test]
1335    fn test_update_price_price_clause_only_matches_and_with_original_clause() {
1336        use dpp::fee::Credits;
1337        use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::DocumentUpdatePriceTransitionV0;
1338        use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransition;
1339
1340        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1341        let contract = fixture.data_contract_owned();
1342
1343        let base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1344            id: Identifier::from([10u8; 32]),
1345            document_type_name: "niceDocument".to_string(),
1346            data_contract_id: contract.id(),
1347            identity_contract_nonce: 0,
1348            token_payment_info: None,
1349        });
1350
1351        // Price-only clause
1352        let update_v0 = DocumentUpdatePriceTransitionV0 {
1353            base: base.clone(),
1354            revision: 1,
1355            price: 10 as Credits,
1356        };
1357        let update = DocumentTransition::UpdatePrice(DocumentUpdatePriceTransition::V0(update_v0));
1358
1359        let filter_price_only = DriveDocumentQueryFilter {
1360            contract: &contract,
1361            document_type_name: "niceDocument".to_string(),
1362            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1363                original_document_clauses: InternalClauses::default(),
1364                price_clause: Some(ValueClause {
1365                    operator: WhereOperator::GreaterThan,
1366                    value: Value::U64(5),
1367                }),
1368            },
1369        };
1370        // Price-only clause is decided in first check
1371        assert_eq!(
1372            filter_price_only.matches_document_transition(&update, None, PlatformVersion::latest()),
1373            TransitionCheckResult::Pass
1374        );
1375
1376        let filter_price_only_fail = DriveDocumentQueryFilter {
1377            contract: &contract,
1378            document_type_name: "niceDocument".to_string(),
1379            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1380                original_document_clauses: InternalClauses::default(),
1381                price_clause: Some(ValueClause {
1382                    operator: WhereOperator::GreaterThan,
1383                    value: Value::U64(15),
1384                }),
1385            },
1386        };
1387        assert_eq!(
1388            filter_price_only_fail.matches_document_transition(
1389                &update,
1390                None,
1391                PlatformVersion::latest()
1392            ),
1393            TransitionCheckResult::Fail
1394        );
1395
1396        // With original clauses as well
1397        let mut eq = BTreeMap::new();
1398        eq.insert(
1399            "kind".to_string(),
1400            WhereClause {
1401                field: "kind".to_string(),
1402                operator: WhereOperator::Equal,
1403                value: Value::Text("sale".to_string()),
1404            },
1405        );
1406        let filter_with_orig = DriveDocumentQueryFilter {
1407            contract: &contract,
1408            document_type_name: "niceDocument".to_string(),
1409            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1410                original_document_clauses: InternalClauses {
1411                    equal_clauses: eq,
1412                    ..Default::default()
1413                },
1414                price_clause: Some(ValueClause {
1415                    operator: WhereOperator::GreaterThanOrEquals,
1416                    value: Value::U64(10),
1417                }),
1418            },
1419        };
1420        let mut original_doc = BTreeMap::new();
1421        original_doc.insert("kind".to_string(), Value::Text("sale".to_string()));
1422        assert_eq!(
1423            filter_with_orig.matches_document_transition(&update, None, PlatformVersion::latest()),
1424            TransitionCheckResult::NeedsOriginal
1425        );
1426        let original_document = Document::V0(DocumentV0 {
1427            id: Identifier::from([10u8; 32]),
1428            owner_id: Identifier::from([0u8; 32]),
1429            properties: original_doc,
1430            ..Default::default()
1431        });
1432        assert!(filter_with_orig.matches_original_document(&original_document));
1433
1434        // Missing original doc -> required -> no match
1435        // No call without original: first pass already signaled it is required
1436    }
1437
1438    #[test]
1439    fn test_replace_with_both_original_and_new_document_clauses() {
1440        use dpp::state_transition::batch_transition::batched_transition::document_replace_transition::v0::DocumentReplaceTransitionV0;
1441        use dpp::state_transition::batch_transition::batched_transition::document_replace_transition::DocumentReplaceTransition;
1442
1443        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1444        let contract = fixture.data_contract_owned();
1445
1446        let base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1447            id: Identifier::from([11u8; 32]),
1448            document_type_name: "niceDocument".to_string(),
1449            data_contract_id: contract.id(),
1450            identity_contract_nonce: 0,
1451            token_payment_info: None,
1452        });
1453
1454        // Original must have status=active; New must have score=10
1455        let mut orig_eq = BTreeMap::new();
1456        orig_eq.insert(
1457            "status".to_string(),
1458            WhereClause {
1459                field: "status".to_string(),
1460                operator: WhereOperator::Equal,
1461                value: Value::Text("active".to_string()),
1462            },
1463        );
1464        let original_clauses = InternalClauses {
1465            equal_clauses: orig_eq,
1466            ..Default::default()
1467        };
1468
1469        let mut final_eq = BTreeMap::new();
1470        final_eq.insert(
1471            "score".to_string(),
1472            WhereClause {
1473                field: "score".to_string(),
1474                operator: WhereOperator::Equal,
1475                value: Value::U64(10),
1476            },
1477        );
1478        let new_document_clauses = InternalClauses {
1479            equal_clauses: final_eq,
1480            ..Default::default()
1481        };
1482
1483        let filter = DriveDocumentQueryFilter {
1484            contract: &contract,
1485            document_type_name: "niceDocument".to_string(),
1486            action_clauses: DocumentActionMatchClauses::Replace {
1487                original_document_clauses: original_clauses,
1488                new_document_clauses,
1489            },
1490        };
1491
1492        // Build Replace transition with new data
1493        let mut data = BTreeMap::new();
1494        data.insert("score".to_string(), Value::U64(10));
1495        let replace_v0 = DocumentReplaceTransitionV0 {
1496            base,
1497            revision: 1,
1498            data,
1499        };
1500        let replace = DocumentTransition::Replace(DocumentReplaceTransition::V0(replace_v0));
1501
1502        // Original provided and matching; final matches (requires original)
1503        let mut original_doc = BTreeMap::new();
1504        original_doc.insert("status".to_string(), Value::Text("active".to_string()));
1505        assert_eq!(
1506            filter.matches_document_transition(&replace, None, PlatformVersion::latest()),
1507            TransitionCheckResult::NeedsOriginal
1508        );
1509        let original_document = Document::V0(DocumentV0 {
1510            id: Identifier::from([11u8; 32]),
1511            owner_id: Identifier::from([0u8; 32]),
1512            properties: original_doc,
1513            ..Default::default()
1514        });
1515        assert!(filter.matches_original_document(&original_document));
1516
1517        // Original missing -> should fail as it's required
1518        // No call without original: first pass already signaled it is required
1519
1520        // Original mismatching -> fail
1521        let mut original_doc_bad = BTreeMap::new();
1522        original_doc_bad.insert("status".to_string(), Value::Text("inactive".to_string()));
1523        let original_document_bad = Document::V0(DocumentV0 {
1524            id: Identifier::from([11u8; 32]),
1525            owner_id: Identifier::from([0u8; 32]),
1526            properties: original_doc_bad,
1527            ..Default::default()
1528        });
1529        assert!(!filter.matches_original_document(&original_document_bad));
1530
1531        // New-data mismatching should fail in first check (do not call final)
1532        if let DocumentTransition::Replace(mut rep) = replace.clone() {
1533            let DocumentReplaceTransition::V0(ref mut v0) = rep;
1534            v0.data.insert("score".to_string(), Value::U64(9));
1535            let bad_final = DocumentTransition::Replace(rep);
1536            assert_eq!(
1537                filter.matches_document_transition(&bad_final, None, PlatformVersion::latest()),
1538                TransitionCheckResult::Fail
1539            );
1540        }
1541    }
1542
1543    /// A `handle` type whose `normalizedLabel` is generated from `label`, and an
1544    /// indexOnly `entry` type whose `normalizedName` is generated from `name`
1545    /// (protocol version 14).
1546    fn generated_from_contract() -> DataContract {
1547        use dpp::data_contract::DataContractFactory;
1548        use dpp::platform_value::platform_value;
1549
1550        let generated = |source: &str| {
1551            platform_value!({
1552                "function": "sys.stringTransformations.homographSafeASCII",
1553                "params": [source]
1554            })
1555        };
1556        let documents = platform_value!({
1557            "handle": {
1558                "type": "object",
1559                "documentsMutable": true,
1560                "properties": {
1561                    "label": { "type": "string", "maxLength": 32, "position": 0 },
1562                    "normalizedLabel": {
1563                        "type": "string",
1564                        "maxLength": 32,
1565                        "position": 1,
1566                        "generatedFrom": generated("label")
1567                    }
1568                },
1569                "additionalProperties": false
1570            },
1571            "entry": {
1572                "type": "object",
1573                "indexOnly": true,
1574                "documentsMutable": false,
1575                "indices": [
1576                    {
1577                        "name": "byNormalizedName",
1578                        "properties": [{ "normalizedName": "asc" }, { "name": "asc" }],
1579                        "terminal": "$ownerId"
1580                    }
1581                ],
1582                "properties": {
1583                    "name": { "type": "string", "maxLength": 32, "position": 0 },
1584                    "normalizedName": {
1585                        "type": "string",
1586                        "maxLength": 32,
1587                        "position": 1,
1588                        "generatedFrom": generated("name")
1589                    }
1590                },
1591                "required": ["name", "normalizedName"],
1592                "additionalProperties": false
1593            }
1594        });
1595        DataContractFactory::new(PlatformVersion::latest().protocol_version)
1596            .expect("factory")
1597            .create_with_value_config(Identifier::from([1u8; 32]), 1, documents, None, None)
1598            .expect("the contract parses")
1599            .data_contract_owned()
1600    }
1601
1602    /// A clause on a `generatedFrom` property judges a create, a replace and an
1603    /// indexOnly delete that leave the property out as the platform stores them,
1604    /// with the value generated from its param.
1605    #[test]
1606    fn test_matches_a_generated_property_the_transition_leaves_out() {
1607        use dpp::state_transition::batch_transition::batched_transition::document_create_transition::v0::DocumentCreateTransitionV0;
1608        use dpp::state_transition::batch_transition::batched_transition::document_create_transition::DocumentCreateTransition;
1609        use dpp::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::DocumentIndexOnlyDeleteTransitionV0;
1610        use dpp::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::DocumentIndexOnlyDeleteTransition;
1611        use dpp::state_transition::batch_transition::batched_transition::document_replace_transition::v0::DocumentReplaceTransitionV0;
1612        use dpp::state_transition::batch_transition::batched_transition::document_replace_transition::DocumentReplaceTransition;
1613
1614        let contract = generated_from_contract();
1615        let platform_version = PlatformVersion::latest();
1616        let equal = |field: &str, value: &str| InternalClauses {
1617            equal_clauses: BTreeMap::from([(
1618                field.to_string(),
1619                WhereClause {
1620                    field: field.to_string(),
1621                    operator: WhereOperator::Equal,
1622                    value: Value::Text(value.to_string()),
1623                },
1624            )]),
1625            ..Default::default()
1626        };
1627        let base = |document_type_name: &str| {
1628            DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1629                id: Identifier::from([3u8; 32]),
1630                document_type_name: document_type_name.to_string(),
1631                data_contract_id: contract.id(),
1632                identity_contract_nonce: 0,
1633                token_payment_info: None,
1634            })
1635        };
1636        let data = |key: &str, value: &str| {
1637            BTreeMap::from([(key.to_string(), Value::Text(value.to_string()))])
1638        };
1639
1640        let create =
1641            DocumentTransition::Create(DocumentCreateTransition::V0(DocumentCreateTransitionV0 {
1642                base: base("handle"),
1643                entropy: [0u8; 32],
1644                data: data("label", "Bob"),
1645                prefunded_voting_balance: None,
1646            }));
1647        let replace = DocumentTransition::Replace(DocumentReplaceTransition::V0(
1648            DocumentReplaceTransitionV0 {
1649                base: base("handle"),
1650                revision: 2,
1651                data: data("label", "Bob"),
1652            },
1653        ));
1654        let index_only_delete = DocumentTransition::IndexOnlyDelete(
1655            DocumentIndexOnlyDeleteTransition::V0(DocumentIndexOnlyDeleteTransitionV0 {
1656                base: base("entry"),
1657                data: data("name", "Bob"),
1658            }),
1659        );
1660
1661        for (normalized, expected) in [
1662            ("b0b", TransitionCheckResult::Pass),
1663            ("bob", TransitionCheckResult::Fail),
1664        ] {
1665            let create_filter = DriveDocumentQueryFilter {
1666                contract: &contract,
1667                document_type_name: "handle".to_string(),
1668                action_clauses: DocumentActionMatchClauses::Create {
1669                    new_document_clauses: equal("normalizedLabel", normalized),
1670                },
1671            };
1672            assert_eq!(
1673                create_filter.matches_document_transition(&create, None, platform_version),
1674                expected,
1675                "create, normalizedLabel == {normalized:?}"
1676            );
1677
1678            let replace_filter = DriveDocumentQueryFilter {
1679                contract: &contract,
1680                document_type_name: "handle".to_string(),
1681                action_clauses: DocumentActionMatchClauses::Replace {
1682                    original_document_clauses: InternalClauses::default(),
1683                    new_document_clauses: equal("normalizedLabel", normalized),
1684                },
1685            };
1686            assert_eq!(
1687                replace_filter.matches_document_transition(&replace, None, platform_version),
1688                expected,
1689                "replace, normalizedLabel == {normalized:?}"
1690            );
1691
1692            let delete_filter = DriveDocumentQueryFilter {
1693                contract: &contract,
1694                document_type_name: "entry".to_string(),
1695                action_clauses: DocumentActionMatchClauses::Delete {
1696                    original_document_clauses: equal("normalizedName", normalized),
1697                },
1698            };
1699            assert_eq!(
1700                delete_filter.matches_document_transition(
1701                    &index_only_delete,
1702                    None,
1703                    platform_version
1704                ),
1705                expected,
1706                "indexOnly delete, normalizedName == {normalized:?}"
1707            );
1708        }
1709    }
1710
1711    #[test]
1712    fn test_matches_document_with_between_operator() {
1713        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1714        let contract = fixture.data_contract_owned();
1715
1716        let internal_clauses = InternalClauses {
1717            range_clause: Some(WhereClause {
1718                field: "value".to_string(),
1719                operator: WhereOperator::Between,
1720                value: Value::Array(vec![Value::U64(10), Value::U64(20)]),
1721            }),
1722            ..Default::default()
1723        };
1724
1725        let filter = DriveDocumentQueryFilter {
1726            contract: &contract,
1727            document_type_name: "niceDocument".to_string(),
1728            action_clauses: DocumentActionMatchClauses::Create {
1729                new_document_clauses: internal_clauses.clone(),
1730            },
1731        };
1732
1733        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1734            id: Identifier::from([3u8; 32]),
1735            document_type_name: "niceDocument".to_string(),
1736            data_contract_id: contract.id(),
1737            identity_contract_nonce: 0,
1738            token_payment_info: None,
1739        });
1740
1741        // Test value in range
1742        let mut in_range = BTreeMap::new();
1743        in_range.insert("value".to_string(), Value::U64(15));
1744        let id_value: Value = document_base.id().into();
1745        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &in_range));
1746
1747        // Test lower bound (inclusive)
1748        let mut lower_bound = BTreeMap::new();
1749        lower_bound.insert("value".to_string(), Value::U64(10));
1750        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &lower_bound));
1751
1752        // Test upper bound (inclusive)
1753        let mut upper_bound = BTreeMap::new();
1754        upper_bound.insert("value".to_string(), Value::U64(20));
1755        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &upper_bound));
1756
1757        // Test below range
1758        let mut below = BTreeMap::new();
1759        below.insert("value".to_string(), Value::U64(5));
1760        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &below));
1761
1762        // Test above range
1763        let mut above = BTreeMap::new();
1764        above.insert("value".to_string(), Value::U64(25));
1765        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &above));
1766    }
1767
1768    #[test]
1769    fn test_validate_filter() {
1770        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1771        let contract = fixture.data_contract_owned();
1772
1773        // Test valid filter with indexed field
1774        let mut equal_clauses = BTreeMap::new();
1775        equal_clauses.insert(
1776            "firstName".to_string(),
1777            WhereClause {
1778                field: "firstName".to_string(),
1779                operator: WhereOperator::Equal,
1780                value: Value::Text("Alice".to_string()),
1781            },
1782        );
1783        let internal_clauses = InternalClauses {
1784            equal_clauses,
1785            ..Default::default()
1786        };
1787
1788        let valid_filter = DriveDocumentQueryFilter {
1789            contract: &contract,
1790            document_type_name: "indexedDocument".to_string(),
1791            action_clauses: DocumentActionMatchClauses::Create {
1792                new_document_clauses: internal_clauses,
1793            },
1794        };
1795
1796        assert!(
1797            valid_filter.validate().is_valid(),
1798            "Filter with indexed field should be valid"
1799        );
1800
1801        // Test filter with non-indexed field: structural validation should pass
1802        // (indexes are not considered by subscription filters).
1803        let mut equal_clauses = BTreeMap::new();
1804        equal_clauses.insert(
1805            "name".to_string(),
1806            WhereClause {
1807                field: "name".to_string(),
1808                operator: WhereOperator::Equal,
1809                value: Value::Text("value".to_string()),
1810            },
1811        );
1812        let internal_clauses = InternalClauses {
1813            equal_clauses,
1814            ..Default::default()
1815        };
1816
1817        let invalid_filter = DriveDocumentQueryFilter {
1818            contract: &contract,
1819            document_type_name: "niceDocument".to_string(),
1820            action_clauses: DocumentActionMatchClauses::Create {
1821                new_document_clauses: internal_clauses,
1822            },
1823        };
1824
1825        assert!(
1826            invalid_filter.validate().is_valid(),
1827            "Structural validate should ignore indexes"
1828        );
1829        // Index-aware validation removed; structural validation suffices for subscriptions.
1830
1831        // Test valid filter with only primary key
1832        let internal_clauses = InternalClauses {
1833            primary_key_equal_clause: Some(WhereClause {
1834                field: "$id".to_string(),
1835                operator: WhereOperator::Equal,
1836                value: Value::Identifier([42u8; 32]),
1837            }),
1838            ..Default::default()
1839        };
1840
1841        let primary_key_filter = DriveDocumentQueryFilter {
1842            contract: &contract,
1843            document_type_name: "indexedDocument".to_string(),
1844            action_clauses: DocumentActionMatchClauses::Create {
1845                new_document_clauses: internal_clauses,
1846            },
1847        };
1848
1849        assert!(
1850            primary_key_filter.validate().is_valid(),
1851            "Filter with only primary key should be valid"
1852        );
1853    }
1854
1855    #[test]
1856    fn test_validate_rejects_id_in_generic_clauses() {
1857        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1858        let contract = fixture.data_contract_owned();
1859
1860        // $id in equal_clauses should be rejected
1861        let mut eq = BTreeMap::new();
1862        eq.insert(
1863            "$id".to_string(),
1864            WhereClause {
1865                field: "$id".to_string(),
1866                operator: WhereOperator::Equal,
1867                value: Value::Identifier([1u8; 32]),
1868            },
1869        );
1870        let filter = DriveDocumentQueryFilter {
1871            contract: &contract,
1872            document_type_name: "niceDocument".to_string(),
1873            action_clauses: DocumentActionMatchClauses::Create {
1874                new_document_clauses: InternalClauses {
1875                    equal_clauses: eq,
1876                    ..Default::default()
1877                },
1878            },
1879        };
1880        assert!(filter.validate().is_err());
1881
1882        // $id in range clause should be rejected
1883        let filter = DriveDocumentQueryFilter {
1884            contract: &contract,
1885            document_type_name: "niceDocument".to_string(),
1886            action_clauses: DocumentActionMatchClauses::Create {
1887                new_document_clauses: InternalClauses {
1888                    range_clause: Some(WhereClause {
1889                        field: "$id".to_string(),
1890                        operator: WhereOperator::GreaterThan,
1891                        value: Value::U64(0),
1892                    }),
1893                    ..Default::default()
1894                },
1895            },
1896        };
1897        assert!(filter.validate().is_err());
1898    }
1899
1900    #[test]
1901    fn test_validate_owner_and_price_clause_types() {
1902        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1903        let contract = fixture.data_contract_owned();
1904
1905        // Owner clause must be Identifier
1906        let filter = DriveDocumentQueryFilter {
1907            contract: &contract,
1908            document_type_name: "niceDocument".to_string(),
1909            action_clauses: DocumentActionMatchClauses::Transfer {
1910                original_document_clauses: InternalClauses::default(),
1911                owner_clause: Some(ValueClause {
1912                    operator: WhereOperator::Equal,
1913                    value: Value::Text("not-id".to_string()),
1914                }),
1915            },
1916        };
1917        assert!(filter.validate().is_err());
1918
1919        // Price clause must be integer-like, not float
1920        let filter = DriveDocumentQueryFilter {
1921            contract: &contract,
1922            document_type_name: "niceDocument".to_string(),
1923            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1924                original_document_clauses: InternalClauses::default(),
1925                price_clause: Some(ValueClause {
1926                    operator: WhereOperator::Equal,
1927                    value: Value::Float(1.23),
1928                }),
1929            },
1930        };
1931        assert!(filter.validate().is_err());
1932
1933        // Price Between must be 2 integer-like values
1934        let filter = DriveDocumentQueryFilter {
1935            contract: &contract,
1936            document_type_name: "niceDocument".to_string(),
1937            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1938                original_document_clauses: InternalClauses::default(),
1939                price_clause: Some(ValueClause {
1940                    operator: WhereOperator::Between,
1941                    value: Value::Array(vec![Value::U64(1), Value::Float(2.0)]),
1942                }),
1943            },
1944        };
1945        assert!(filter.validate().is_err());
1946
1947        // Price Between variants must reject equal bounds
1948        for operator in [
1949            WhereOperator::Between,
1950            WhereOperator::BetweenExcludeBounds,
1951            WhereOperator::BetweenExcludeLeft,
1952            WhereOperator::BetweenExcludeRight,
1953        ] {
1954            let filter = DriveDocumentQueryFilter {
1955                contract: &contract,
1956                document_type_name: "niceDocument".to_string(),
1957                action_clauses: DocumentActionMatchClauses::UpdatePrice {
1958                    original_document_clauses: InternalClauses::default(),
1959                    price_clause: Some(ValueClause {
1960                        operator,
1961                        value: Value::Array(vec![Value::U64(10), Value::U64(10)]),
1962                    }),
1963                },
1964            };
1965            assert!(
1966                filter.validate().is_err(),
1967                "{operator:?} should reject equal price bounds"
1968            );
1969        }
1970    }
1971
1972    #[test]
1973    fn test_validate_startswith_on_numeric_field_rejected() {
1974        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1975        let contract = fixture.data_contract_owned();
1976
1977        // numeric field 'score' with StartsWith should be rejected
1978        let filter = DriveDocumentQueryFilter {
1979            contract: &contract,
1980            document_type_name: "niceDocument".to_string(),
1981            action_clauses: DocumentActionMatchClauses::Create {
1982                new_document_clauses: InternalClauses {
1983                    range_clause: Some(WhereClause {
1984                        field: "score".to_string(),
1985                        operator: WhereOperator::StartsWith,
1986                        value: Value::Text("1".to_string()),
1987                    }),
1988                    ..Default::default()
1989                },
1990            },
1991        };
1992        assert!(filter.validate().is_err());
1993    }
1994
1995    #[test]
1996    fn test_conversion_between_filter_and_query() {
1997        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1998        let contract = fixture.data_contract_owned();
1999
2000        let internal_clauses = InternalClauses {
2001            primary_key_equal_clause: Some(WhereClause {
2002                field: "$id".to_string(),
2003                operator: WhereOperator::Equal,
2004                value: Value::Identifier([42u8; 32]),
2005            }),
2006            ..Default::default()
2007        };
2008
2009        let original_filter = DriveDocumentQueryFilter {
2010            contract: &contract,
2011            document_type_name: "niceDocument".to_string(),
2012            action_clauses: DocumentActionMatchClauses::Create {
2013                new_document_clauses: internal_clauses.clone(),
2014            },
2015        };
2016
2017        // No conversion helpers; verify the filter holds the expected clauses
2018        if let DocumentActionMatchClauses::Create {
2019            new_document_clauses,
2020        } = original_filter.action_clauses
2021        {
2022            assert_eq!(new_document_clauses, internal_clauses);
2023        } else {
2024            panic!("expected Create action clauses");
2025        }
2026    }
2027
2028    // ---- validate: unknown document type ----
2029
2030    #[test]
2031    fn validate_rejects_unknown_document_type() {
2032        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2033        let contract = fixture.data_contract_owned();
2034
2035        let filter = DriveDocumentQueryFilter {
2036            contract: &contract,
2037            document_type_name: "doesNotExist".to_string(),
2038            action_clauses: DocumentActionMatchClauses::Create {
2039                new_document_clauses: InternalClauses::default(),
2040            },
2041        };
2042        let result = filter.validate();
2043        assert!(result.is_err());
2044        assert!(matches!(
2045            result.first_error(),
2046            Some(QuerySyntaxError::DocumentTypeNotFound(_))
2047        ));
2048    }
2049
2050    // ---- validate: owner clause with In operator ----
2051
2052    #[test]
2053    fn validate_transfer_owner_clause_in_with_identifiers_is_valid() {
2054        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2055        let contract = fixture.data_contract_owned();
2056
2057        let filter = DriveDocumentQueryFilter {
2058            contract: &contract,
2059            document_type_name: "niceDocument".to_string(),
2060            action_clauses: DocumentActionMatchClauses::Transfer {
2061                original_document_clauses: InternalClauses::default(),
2062                owner_clause: Some(ValueClause {
2063                    operator: WhereOperator::In,
2064                    value: Value::Array(vec![
2065                        Value::Identifier([1u8; 32]),
2066                        Value::Identifier([2u8; 32]),
2067                    ]),
2068                }),
2069            },
2070        };
2071        assert!(filter.validate().is_valid());
2072    }
2073
2074    #[test]
2075    fn validate_transfer_owner_clause_in_with_non_identifiers_is_invalid() {
2076        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2077        let contract = fixture.data_contract_owned();
2078
2079        let filter = DriveDocumentQueryFilter {
2080            contract: &contract,
2081            document_type_name: "niceDocument".to_string(),
2082            action_clauses: DocumentActionMatchClauses::Transfer {
2083                original_document_clauses: InternalClauses::default(),
2084                owner_clause: Some(ValueClause {
2085                    operator: WhereOperator::In,
2086                    value: Value::Array(vec![Value::Text("not-an-id".to_string())]),
2087                }),
2088            },
2089        };
2090        assert!(filter.validate().is_err());
2091    }
2092
2093    #[test]
2094    fn validate_transfer_owner_clause_greater_than_is_invalid() {
2095        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2096        let contract = fixture.data_contract_owned();
2097
2098        let filter = DriveDocumentQueryFilter {
2099            contract: &contract,
2100            document_type_name: "niceDocument".to_string(),
2101            action_clauses: DocumentActionMatchClauses::Transfer {
2102                original_document_clauses: InternalClauses::default(),
2103                owner_clause: Some(ValueClause {
2104                    operator: WhereOperator::GreaterThan,
2105                    value: Value::Identifier([1u8; 32]),
2106                }),
2107            },
2108        };
2109        assert!(filter.validate().is_err());
2110    }
2111
2112    // ---- validate: purchase owner clause ----
2113
2114    #[test]
2115    fn validate_purchase_owner_clause_in_with_identifiers_is_valid() {
2116        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2117        let contract = fixture.data_contract_owned();
2118
2119        let filter = DriveDocumentQueryFilter {
2120            contract: &contract,
2121            document_type_name: "niceDocument".to_string(),
2122            action_clauses: DocumentActionMatchClauses::Purchase {
2123                original_document_clauses: InternalClauses::default(),
2124                owner_clause: Some(ValueClause {
2125                    operator: WhereOperator::In,
2126                    value: Value::Array(vec![
2127                        Value::Identifier([3u8; 32]),
2128                        Value::Identifier([4u8; 32]),
2129                    ]),
2130                }),
2131            },
2132        };
2133        assert!(filter.validate().is_valid());
2134    }
2135
2136    #[test]
2137    fn validate_purchase_owner_clause_non_identifier_is_invalid() {
2138        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2139        let contract = fixture.data_contract_owned();
2140
2141        let filter = DriveDocumentQueryFilter {
2142            contract: &contract,
2143            document_type_name: "niceDocument".to_string(),
2144            action_clauses: DocumentActionMatchClauses::Purchase {
2145                original_document_clauses: InternalClauses::default(),
2146                owner_clause: Some(ValueClause {
2147                    operator: WhereOperator::Equal,
2148                    value: Value::U64(42),
2149                }),
2150            },
2151        };
2152        assert!(filter.validate().is_err());
2153    }
2154
2155    #[test]
2156    fn validate_purchase_owner_clause_in_with_non_array_is_invalid() {
2157        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2158        let contract = fixture.data_contract_owned();
2159
2160        let filter = DriveDocumentQueryFilter {
2161            contract: &contract,
2162            document_type_name: "niceDocument".to_string(),
2163            action_clauses: DocumentActionMatchClauses::Purchase {
2164                original_document_clauses: InternalClauses::default(),
2165                owner_clause: Some(ValueClause {
2166                    operator: WhereOperator::In,
2167                    value: Value::Identifier([1u8; 32]),
2168                }),
2169            },
2170        };
2171        assert!(filter.validate().is_err());
2172    }
2173
2174    // ---- validate: price clause coverage ----
2175
2176    #[test]
2177    fn validate_price_clause_starts_with_is_invalid() {
2178        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2179        let contract = fixture.data_contract_owned();
2180
2181        let filter = DriveDocumentQueryFilter {
2182            contract: &contract,
2183            document_type_name: "niceDocument".to_string(),
2184            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2185                original_document_clauses: InternalClauses::default(),
2186                price_clause: Some(ValueClause {
2187                    operator: WhereOperator::StartsWith,
2188                    value: Value::Text("1".to_string()),
2189                }),
2190            },
2191        };
2192        assert!(filter.validate().is_err());
2193    }
2194
2195    #[test]
2196    fn validate_price_clause_in_with_integers_is_valid() {
2197        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2198        let contract = fixture.data_contract_owned();
2199
2200        let filter = DriveDocumentQueryFilter {
2201            contract: &contract,
2202            document_type_name: "niceDocument".to_string(),
2203            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2204                original_document_clauses: InternalClauses::default(),
2205                price_clause: Some(ValueClause {
2206                    operator: WhereOperator::In,
2207                    value: Value::Array(vec![Value::U64(10), Value::U64(20), Value::U64(30)]),
2208                }),
2209            },
2210        };
2211        assert!(filter.validate().is_valid());
2212    }
2213
2214    #[test]
2215    fn validate_price_clause_in_with_non_integer_is_invalid() {
2216        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2217        let contract = fixture.data_contract_owned();
2218
2219        let filter = DriveDocumentQueryFilter {
2220            contract: &contract,
2221            document_type_name: "niceDocument".to_string(),
2222            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2223                original_document_clauses: InternalClauses::default(),
2224                price_clause: Some(ValueClause {
2225                    operator: WhereOperator::In,
2226                    value: Value::Array(vec![Value::Text("not_int".to_string())]),
2227                }),
2228            },
2229        };
2230        assert!(filter.validate().is_err());
2231    }
2232
2233    #[test]
2234    fn validate_price_clause_in_with_non_array_is_invalid() {
2235        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2236        let contract = fixture.data_contract_owned();
2237
2238        let filter = DriveDocumentQueryFilter {
2239            contract: &contract,
2240            document_type_name: "niceDocument".to_string(),
2241            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2242                original_document_clauses: InternalClauses::default(),
2243                price_clause: Some(ValueClause {
2244                    operator: WhereOperator::In,
2245                    value: Value::U64(10),
2246                }),
2247            },
2248        };
2249        assert!(filter.validate().is_err());
2250    }
2251
2252    #[test]
2253    fn validate_price_clause_between_with_valid_integers_is_valid() {
2254        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2255        let contract = fixture.data_contract_owned();
2256
2257        let filter = DriveDocumentQueryFilter {
2258            contract: &contract,
2259            document_type_name: "niceDocument".to_string(),
2260            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2261                original_document_clauses: InternalClauses::default(),
2262                price_clause: Some(ValueClause {
2263                    operator: WhereOperator::Between,
2264                    value: Value::Array(vec![Value::U64(10), Value::U64(100)]),
2265                }),
2266            },
2267        };
2268        assert!(filter.validate().is_valid());
2269    }
2270
2271    #[test]
2272    fn validate_price_clause_between_with_descending_bounds_is_invalid() {
2273        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2274        let contract = fixture.data_contract_owned();
2275
2276        let filter = DriveDocumentQueryFilter {
2277            contract: &contract,
2278            document_type_name: "niceDocument".to_string(),
2279            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2280                original_document_clauses: InternalClauses::default(),
2281                price_clause: Some(ValueClause {
2282                    operator: WhereOperator::Between,
2283                    value: Value::Array(vec![Value::U64(100), Value::U64(10)]),
2284                }),
2285            },
2286        };
2287        assert!(filter.validate().is_err());
2288    }
2289
2290    #[test]
2291    fn validate_price_clause_between_with_non_array_is_invalid() {
2292        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2293        let contract = fixture.data_contract_owned();
2294
2295        let filter = DriveDocumentQueryFilter {
2296            contract: &contract,
2297            document_type_name: "niceDocument".to_string(),
2298            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2299                original_document_clauses: InternalClauses::default(),
2300                price_clause: Some(ValueClause {
2301                    operator: WhereOperator::Between,
2302                    value: Value::U64(50),
2303                }),
2304            },
2305        };
2306        assert!(filter.validate().is_err());
2307    }
2308
2309    #[test]
2310    fn validate_price_clause_less_than_with_integer_is_valid() {
2311        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2312        let contract = fixture.data_contract_owned();
2313
2314        let filter = DriveDocumentQueryFilter {
2315            contract: &contract,
2316            document_type_name: "niceDocument".to_string(),
2317            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2318                original_document_clauses: InternalClauses::default(),
2319                price_clause: Some(ValueClause {
2320                    operator: WhereOperator::LessThan,
2321                    value: Value::U64(1000),
2322                }),
2323            },
2324        };
2325        assert!(filter.validate().is_valid());
2326    }
2327
2328    #[test]
2329    fn validate_price_clause_less_than_or_equals_with_integer_is_valid() {
2330        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2331        let contract = fixture.data_contract_owned();
2332
2333        let filter = DriveDocumentQueryFilter {
2334            contract: &contract,
2335            document_type_name: "niceDocument".to_string(),
2336            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2337                original_document_clauses: InternalClauses::default(),
2338                price_clause: Some(ValueClause {
2339                    operator: WhereOperator::LessThanOrEquals,
2340                    value: Value::U64(500),
2341                }),
2342            },
2343        };
2344        assert!(filter.validate().is_valid());
2345    }
2346
2347    #[test]
2348    fn validate_price_clause_greater_than_or_equals_with_integer_is_valid() {
2349        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2350        let contract = fixture.data_contract_owned();
2351
2352        let filter = DriveDocumentQueryFilter {
2353            contract: &contract,
2354            document_type_name: "niceDocument".to_string(),
2355            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2356                original_document_clauses: InternalClauses::default(),
2357                price_clause: Some(ValueClause {
2358                    operator: WhereOperator::GreaterThanOrEquals,
2359                    value: Value::U64(50),
2360                }),
2361            },
2362        };
2363        assert!(filter.validate().is_valid());
2364    }
2365
2366    // ---- validate: delete action ----
2367
2368    #[test]
2369    fn validate_delete_with_empty_clauses_is_valid() {
2370        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2371        let contract = fixture.data_contract_owned();
2372
2373        let filter = DriveDocumentQueryFilter {
2374            contract: &contract,
2375            document_type_name: "niceDocument".to_string(),
2376            action_clauses: DocumentActionMatchClauses::Delete {
2377                original_document_clauses: InternalClauses::default(),
2378            },
2379        };
2380        assert!(filter.validate().is_valid());
2381    }
2382
2383    #[test]
2384    fn validate_delete_with_primary_key_clause_is_valid() {
2385        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2386        let contract = fixture.data_contract_owned();
2387
2388        let filter = DriveDocumentQueryFilter {
2389            contract: &contract,
2390            document_type_name: "niceDocument".to_string(),
2391            action_clauses: DocumentActionMatchClauses::Delete {
2392                original_document_clauses: InternalClauses {
2393                    primary_key_equal_clause: Some(WhereClause {
2394                        field: "$id".to_string(),
2395                        operator: WhereOperator::Equal,
2396                        value: Value::Identifier([99u8; 32]),
2397                    }),
2398                    ..Default::default()
2399                },
2400            },
2401        };
2402        assert!(filter.validate().is_valid());
2403    }
2404
2405    // ---- matches_original_document: Create action returns false ----
2406
2407    #[test]
2408    fn matches_original_document_returns_false_for_create_action() {
2409        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2410        let contract = fixture.data_contract_owned();
2411
2412        let filter = DriveDocumentQueryFilter {
2413            contract: &contract,
2414            document_type_name: "niceDocument".to_string(),
2415            action_clauses: DocumentActionMatchClauses::Create {
2416                new_document_clauses: InternalClauses::default(),
2417            },
2418        };
2419
2420        let doc = Document::V0(DocumentV0 {
2421            id: Identifier::from([1u8; 32]),
2422            owner_id: Identifier::from([0u8; 32]),
2423            properties: BTreeMap::new(),
2424            ..Default::default()
2425        });
2426        // Create has no original document path
2427        assert!(!filter.matches_original_document(&doc));
2428    }
2429
2430    // ---- evaluate_clauses: primary_key_in_clause ----
2431
2432    #[test]
2433    fn evaluate_clauses_primary_key_in_clause_matches() {
2434        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2435        let contract = fixture.data_contract_owned();
2436
2437        let target_id_1 = Identifier::from([10u8; 32]);
2438        let target_id_2 = Identifier::from([20u8; 32]);
2439
2440        let internal_clauses = InternalClauses {
2441            primary_key_in_clause: Some(WhereClause {
2442                field: "$id".to_string(),
2443                operator: WhereOperator::In,
2444                value: Value::Array(vec![target_id_1.into(), target_id_2.into()]),
2445            }),
2446            ..Default::default()
2447        };
2448
2449        let filter = DriveDocumentQueryFilter {
2450            contract: &contract,
2451            document_type_name: "niceDocument".to_string(),
2452            action_clauses: DocumentActionMatchClauses::Create {
2453                new_document_clauses: internal_clauses.clone(),
2454            },
2455        };
2456
2457        // Matching ID
2458        let id_value: Value = target_id_1.into();
2459        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &BTreeMap::new()));
2460
2461        // Non-matching ID
2462        let other_id: Value = Identifier::from([99u8; 32]).into();
2463        assert!(!filter.evaluate_clauses(&internal_clauses, &other_id, &BTreeMap::new()));
2464    }
2465
2466    // ---- evaluate_clauses: combined primary_key and field clauses ----
2467
2468    #[test]
2469    fn evaluate_clauses_primary_key_plus_equal_clause() {
2470        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2471        let contract = fixture.data_contract_owned();
2472
2473        let target_id = Identifier::from([50u8; 32]);
2474
2475        let mut equal_clauses = BTreeMap::new();
2476        equal_clauses.insert(
2477            "name".to_string(),
2478            WhereClause {
2479                field: "name".to_string(),
2480                operator: WhereOperator::Equal,
2481                value: Value::Text("test".to_string()),
2482            },
2483        );
2484
2485        let internal_clauses = InternalClauses {
2486            primary_key_equal_clause: Some(WhereClause {
2487                field: "$id".to_string(),
2488                operator: WhereOperator::Equal,
2489                value: target_id.into(),
2490            }),
2491            equal_clauses,
2492            ..Default::default()
2493        };
2494
2495        let filter = DriveDocumentQueryFilter {
2496            contract: &contract,
2497            document_type_name: "niceDocument".to_string(),
2498            action_clauses: DocumentActionMatchClauses::Create {
2499                new_document_clauses: internal_clauses.clone(),
2500            },
2501        };
2502
2503        // Both match
2504        let id_value: Value = target_id.into();
2505        let mut data = BTreeMap::new();
2506        data.insert("name".to_string(), Value::Text("test".to_string()));
2507        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &data));
2508
2509        // ID matches but field doesn't
2510        let mut bad_data = BTreeMap::new();
2511        bad_data.insert("name".to_string(), Value::Text("other".to_string()));
2512        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &bad_data));
2513
2514        // Field matches but ID doesn't
2515        let wrong_id: Value = Identifier::from([99u8; 32]).into();
2516        assert!(!filter.evaluate_clauses(&internal_clauses, &wrong_id, &data));
2517    }
2518
2519    // ---- get_value_by_path ----
2520
2521    #[test]
2522    fn get_value_by_path_simple_key() {
2523        let mut root = BTreeMap::new();
2524        root.insert("name".to_string(), Value::Text("alice".to_string()));
2525        let result = get_value_by_path(&root, "name");
2526        assert_eq!(result, Some(&Value::Text("alice".to_string())));
2527    }
2528
2529    #[test]
2530    fn get_value_by_path_missing_key_returns_none() {
2531        let root = BTreeMap::new();
2532        let result = get_value_by_path(&root, "nonexistent");
2533        assert!(result.is_none());
2534    }
2535
2536    #[test]
2537    fn get_value_by_path_empty_path_returns_none() {
2538        let mut root = BTreeMap::new();
2539        root.insert("x".to_string(), Value::I64(1));
2540        let result = get_value_by_path(&root, "");
2541        assert!(result.is_none());
2542    }
2543
2544    #[test]
2545    fn get_value_by_path_nested_map() {
2546        let nested = vec![(
2547            Value::Text("level2".to_string()),
2548            Value::Text("deep_value".to_string()),
2549        )];
2550        let mut root = BTreeMap::new();
2551        root.insert("level1".to_string(), Value::Map(nested));
2552
2553        let result = get_value_by_path(&root, "level1.level2");
2554        assert_eq!(result, Some(&Value::Text("deep_value".to_string())));
2555    }
2556
2557    #[test]
2558    fn get_value_by_path_intermediate_non_map_returns_none() {
2559        let mut root = BTreeMap::new();
2560        root.insert("scalar".to_string(), Value::I64(42));
2561
2562        // Trying to traverse through a scalar value
2563        let result = get_value_by_path(&root, "scalar.child");
2564        assert!(result.is_none());
2565    }
2566
2567    #[test]
2568    fn get_value_by_path_deeply_nested() {
2569        let level3 = vec![(Value::Text("val".to_string()), Value::U64(999))];
2570        let level2 = vec![(Value::Text("c".to_string()), Value::Map(level3))];
2571        let mut root = BTreeMap::new();
2572        root.insert("a".to_string(), Value::Map(level2));
2573
2574        let result = get_value_by_path(&root, "a.c.val");
2575        assert_eq!(result, Some(&Value::U64(999)));
2576    }
2577
2578    #[test]
2579    fn get_value_by_path_missing_intermediate_key_returns_none() {
2580        let nested = vec![(Value::Text("exists".to_string()), Value::I64(1))];
2581        let mut root = BTreeMap::new();
2582        root.insert("a".to_string(), Value::Map(nested));
2583
2584        let result = get_value_by_path(&root, "a.not_here.val");
2585        assert!(result.is_none());
2586    }
2587
2588    // ---- evaluate_clauses: range clause with missing field ----
2589
2590    #[test]
2591    fn evaluate_clauses_range_clause_missing_field_returns_false() {
2592        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2593        let contract = fixture.data_contract_owned();
2594
2595        let internal_clauses = InternalClauses {
2596            range_clause: Some(WhereClause {
2597                field: "nonexistent".to_string(),
2598                operator: WhereOperator::GreaterThan,
2599                value: Value::U64(0),
2600            }),
2601            ..Default::default()
2602        };
2603
2604        let filter = DriveDocumentQueryFilter {
2605            contract: &contract,
2606            document_type_name: "niceDocument".to_string(),
2607            action_clauses: DocumentActionMatchClauses::Create {
2608                new_document_clauses: internal_clauses.clone(),
2609            },
2610        };
2611
2612        let id_value: Value = Identifier::from([1u8; 32]).into();
2613        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &BTreeMap::new()));
2614    }
2615
2616    // ---- evaluate_clauses: in clause with missing field ----
2617
2618    #[test]
2619    fn evaluate_clauses_in_clause_missing_field_returns_false() {
2620        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2621        let contract = fixture.data_contract_owned();
2622
2623        let internal_clauses = InternalClauses {
2624            in_clauses: vec![WhereClause {
2625                field: "nonexistent".to_string(),
2626                operator: WhereOperator::In,
2627                value: Value::Array(vec![Value::I64(1)]),
2628            }],
2629            ..Default::default()
2630        };
2631
2632        let filter = DriveDocumentQueryFilter {
2633            contract: &contract,
2634            document_type_name: "niceDocument".to_string(),
2635            action_clauses: DocumentActionMatchClauses::Create {
2636                new_document_clauses: internal_clauses.clone(),
2637            },
2638        };
2639
2640        let id_value: Value = Identifier::from([1u8; 32]).into();
2641        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &BTreeMap::new()));
2642    }
2643
2644    // ---- matches_original_document: UpdatePrice with original clauses ----
2645
2646    #[test]
2647    fn matches_original_document_update_price_evaluates_original_clauses() {
2648        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2649        let contract = fixture.data_contract_owned();
2650
2651        let mut eq = BTreeMap::new();
2652        eq.insert(
2653            "kind".to_string(),
2654            WhereClause {
2655                field: "kind".to_string(),
2656                operator: WhereOperator::Equal,
2657                value: Value::Text("premium".to_string()),
2658            },
2659        );
2660
2661        let filter = DriveDocumentQueryFilter {
2662            contract: &contract,
2663            document_type_name: "niceDocument".to_string(),
2664            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2665                original_document_clauses: InternalClauses {
2666                    equal_clauses: eq,
2667                    ..Default::default()
2668                },
2669                price_clause: None,
2670            },
2671        };
2672
2673        // Matching original
2674        let mut props = BTreeMap::new();
2675        props.insert("kind".to_string(), Value::Text("premium".to_string()));
2676        let doc = Document::V0(DocumentV0 {
2677            id: Identifier::from([15u8; 32]),
2678            owner_id: Identifier::from([0u8; 32]),
2679            properties: props,
2680            ..Default::default()
2681        });
2682        assert!(filter.matches_original_document(&doc));
2683
2684        // Non-matching original
2685        let mut bad_props = BTreeMap::new();
2686        bad_props.insert("kind".to_string(), Value::Text("basic".to_string()));
2687        let bad_doc = Document::V0(DocumentV0 {
2688            id: Identifier::from([15u8; 32]),
2689            owner_id: Identifier::from([0u8; 32]),
2690            properties: bad_props,
2691            ..Default::default()
2692        });
2693        assert!(!filter.matches_original_document(&bad_doc));
2694    }
2695
2696    // ---- matches_original_document: Purchase with original clauses ----
2697
2698    #[test]
2699    fn matches_original_document_purchase_evaluates_original_clauses() {
2700        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2701        let contract = fixture.data_contract_owned();
2702
2703        let mut eq = BTreeMap::new();
2704        eq.insert(
2705            "status".to_string(),
2706            WhereClause {
2707                field: "status".to_string(),
2708                operator: WhereOperator::Equal,
2709                value: Value::Text("for_sale".to_string()),
2710            },
2711        );
2712
2713        let filter = DriveDocumentQueryFilter {
2714            contract: &contract,
2715            document_type_name: "niceDocument".to_string(),
2716            action_clauses: DocumentActionMatchClauses::Purchase {
2717                original_document_clauses: InternalClauses {
2718                    equal_clauses: eq,
2719                    ..Default::default()
2720                },
2721                owner_clause: None,
2722            },
2723        };
2724
2725        let mut props = BTreeMap::new();
2726        props.insert("status".to_string(), Value::Text("for_sale".to_string()));
2727        let doc = Document::V0(DocumentV0 {
2728            id: Identifier::from([20u8; 32]),
2729            owner_id: Identifier::from([0u8; 32]),
2730            properties: props,
2731            ..Default::default()
2732        });
2733        assert!(filter.matches_original_document(&doc));
2734    }
2735
2736    // ---- validate: Replace with invalid original clauses ----
2737
2738    #[test]
2739    fn validate_replace_with_invalid_original_clauses_fails() {
2740        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2741        let contract = fixture.data_contract_owned();
2742
2743        // Put an invalid field name in original clauses
2744        let mut eq = BTreeMap::new();
2745        eq.insert(
2746            "nonexistentField".to_string(),
2747            WhereClause {
2748                field: "nonexistentField".to_string(),
2749                operator: WhereOperator::Equal,
2750                value: Value::I64(1),
2751            },
2752        );
2753
2754        let filter = DriveDocumentQueryFilter {
2755            contract: &contract,
2756            document_type_name: "niceDocument".to_string(),
2757            action_clauses: DocumentActionMatchClauses::Replace {
2758                original_document_clauses: InternalClauses {
2759                    equal_clauses: eq,
2760                    ..Default::default()
2761                },
2762                new_document_clauses: InternalClauses::default(),
2763            },
2764        };
2765        assert!(filter.validate().is_err());
2766    }
2767
2768    // ---- validate: Transfer with invalid original clauses ----
2769
2770    #[test]
2771    fn validate_transfer_with_invalid_original_clauses_fails() {
2772        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2773        let contract = fixture.data_contract_owned();
2774
2775        let mut eq = BTreeMap::new();
2776        eq.insert(
2777            "badField".to_string(),
2778            WhereClause {
2779                field: "badField".to_string(),
2780                operator: WhereOperator::Equal,
2781                value: Value::I64(1),
2782            },
2783        );
2784
2785        let filter = DriveDocumentQueryFilter {
2786            contract: &contract,
2787            document_type_name: "niceDocument".to_string(),
2788            action_clauses: DocumentActionMatchClauses::Transfer {
2789                original_document_clauses: InternalClauses {
2790                    equal_clauses: eq,
2791                    ..Default::default()
2792                },
2793                owner_clause: None,
2794            },
2795        };
2796        assert!(filter.validate().is_err());
2797    }
2798
2799    // ---- validate: UpdatePrice with invalid original clauses ----
2800
2801    #[test]
2802    fn validate_update_price_with_invalid_original_clauses_fails() {
2803        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2804        let contract = fixture.data_contract_owned();
2805
2806        let mut eq = BTreeMap::new();
2807        eq.insert(
2808            "badField".to_string(),
2809            WhereClause {
2810                field: "badField".to_string(),
2811                operator: WhereOperator::Equal,
2812                value: Value::I64(1),
2813            },
2814        );
2815
2816        let filter = DriveDocumentQueryFilter {
2817            contract: &contract,
2818            document_type_name: "niceDocument".to_string(),
2819            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2820                original_document_clauses: InternalClauses {
2821                    equal_clauses: eq,
2822                    ..Default::default()
2823                },
2824                price_clause: None,
2825            },
2826        };
2827        assert!(filter.validate().is_err());
2828    }
2829
2830    // ---- validate: Purchase with invalid original clauses ----
2831
2832    #[test]
2833    fn validate_purchase_with_invalid_original_clauses_fails() {
2834        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2835        let contract = fixture.data_contract_owned();
2836
2837        let mut eq = BTreeMap::new();
2838        eq.insert(
2839            "badField".to_string(),
2840            WhereClause {
2841                field: "badField".to_string(),
2842                operator: WhereOperator::Equal,
2843                value: Value::I64(1),
2844            },
2845        );
2846
2847        let filter = DriveDocumentQueryFilter {
2848            contract: &contract,
2849            document_type_name: "niceDocument".to_string(),
2850            action_clauses: DocumentActionMatchClauses::Purchase {
2851                original_document_clauses: InternalClauses {
2852                    equal_clauses: eq,
2853                    ..Default::default()
2854                },
2855                owner_clause: None,
2856            },
2857        };
2858        assert!(filter.validate().is_err());
2859    }
2860
2861    // ---- evaluate_clauses: starts_with on field ----
2862
2863    #[test]
2864    fn evaluate_clauses_starts_with_range_clause() {
2865        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2866        let contract = fixture.data_contract_owned();
2867
2868        let internal_clauses = InternalClauses {
2869            range_clause: Some(WhereClause {
2870                field: "name".to_string(),
2871                operator: WhereOperator::StartsWith,
2872                value: Value::Text("Ali".to_string()),
2873            }),
2874            ..Default::default()
2875        };
2876
2877        let filter = DriveDocumentQueryFilter {
2878            contract: &contract,
2879            document_type_name: "niceDocument".to_string(),
2880            action_clauses: DocumentActionMatchClauses::Create {
2881                new_document_clauses: internal_clauses.clone(),
2882            },
2883        };
2884
2885        let id_value: Value = Identifier::from([1u8; 32]).into();
2886
2887        let mut matching = BTreeMap::new();
2888        matching.insert("name".to_string(), Value::Text("Alice".to_string()));
2889        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &matching));
2890
2891        let mut non_matching = BTreeMap::new();
2892        non_matching.insert("name".to_string(), Value::Text("Bob".to_string()));
2893        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &non_matching));
2894    }
2895
2896    // ---- validate: price clause between_exclude variants ----
2897
2898    #[test]
2899    fn validate_price_clause_between_exclude_left_valid() {
2900        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2901        let contract = fixture.data_contract_owned();
2902
2903        let filter = DriveDocumentQueryFilter {
2904            contract: &contract,
2905            document_type_name: "niceDocument".to_string(),
2906            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2907                original_document_clauses: InternalClauses::default(),
2908                price_clause: Some(ValueClause {
2909                    operator: WhereOperator::BetweenExcludeLeft,
2910                    value: Value::Array(vec![Value::U64(5), Value::U64(50)]),
2911                }),
2912            },
2913        };
2914        assert!(filter.validate().is_valid());
2915    }
2916
2917    #[test]
2918    fn validate_price_clause_between_exclude_right_valid() {
2919        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2920        let contract = fixture.data_contract_owned();
2921
2922        let filter = DriveDocumentQueryFilter {
2923            contract: &contract,
2924            document_type_name: "niceDocument".to_string(),
2925            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2926                original_document_clauses: InternalClauses::default(),
2927                price_clause: Some(ValueClause {
2928                    operator: WhereOperator::BetweenExcludeRight,
2929                    value: Value::Array(vec![Value::U64(5), Value::U64(50)]),
2930                }),
2931            },
2932        };
2933        assert!(filter.validate().is_valid());
2934    }
2935
2936    #[test]
2937    fn validate_price_clause_between_exclude_bounds_valid() {
2938        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2939        let contract = fixture.data_contract_owned();
2940
2941        let filter = DriveDocumentQueryFilter {
2942            contract: &contract,
2943            document_type_name: "niceDocument".to_string(),
2944            action_clauses: DocumentActionMatchClauses::UpdatePrice {
2945                original_document_clauses: InternalClauses::default(),
2946                price_clause: Some(ValueClause {
2947                    operator: WhereOperator::BetweenExcludeBounds,
2948                    value: Value::Array(vec![Value::U64(5), Value::U64(50)]),
2949                }),
2950            },
2951        };
2952        assert!(filter.validate().is_valid());
2953    }
2954
2955    // ---- validate: transfer In with non-array ----
2956
2957    #[test]
2958    fn validate_transfer_owner_clause_in_with_non_array_is_invalid() {
2959        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
2960        let contract = fixture.data_contract_owned();
2961
2962        let filter = DriveDocumentQueryFilter {
2963            contract: &contract,
2964            document_type_name: "niceDocument".to_string(),
2965            action_clauses: DocumentActionMatchClauses::Transfer {
2966                original_document_clauses: InternalClauses::default(),
2967                owner_clause: Some(ValueClause {
2968                    operator: WhereOperator::In,
2969                    value: Value::Identifier([1u8; 32]),
2970                }),
2971            },
2972        };
2973        assert!(filter.validate().is_err());
2974    }
2975
2976    /// A filter reads a typed array by its elements as a documents query does
2977    /// (protocol version 14): with the same operators, judging the same values.
2978    mod typed_array_elements {
2979        use super::*;
2980        use crate::error::Error;
2981        use dpp::data_contract::DataContractFactory;
2982        use dpp::platform_value::platform_value;
2983        use dpp::platform_value::string_encoding::Encoding;
2984
2985        /// A `post` type holding a boolean, a u8 and a string typed array,
2986        /// beside a boolean and a string property.
2987        fn element_contract() -> DataContract {
2988            DataContractFactory::new(PlatformVersion::latest().protocol_version)
2989                .expect("factory")
2990                .create_with_value_config(
2991                    Identifier::from([1; 32]),
2992                    0,
2993                    platform_value!({"post": {
2994                        "type": "object",
2995                        "properties": {
2996                            "flags": {
2997                                "type": "array", "maxItems": 4,
2998                                "items": {"type": "boolean"}, "position": 0
2999                            },
3000                            "scores": {
3001                                "type": "array", "maxItems": 8,
3002                                "items": {"type": "integer", "minimum": 0, "maximum": 255},
3003                                "position": 1
3004                            },
3005                            "hashtags": {
3006                                "type": "array", "maxItems": 8,
3007                                "items": {"type": "string", "maxLength": 61}, "position": 2
3008                            },
3009                            "pinned": {"type": "boolean", "position": 3},
3010                            "language": {"type": "string", "maxLength": 2, "position": 4},
3011                            "authors": {
3012                                "type": "array", "maxItems": 4,
3013                                "items": {
3014                                    "type": "array", "byteArray": true, "minItems": 32,
3015                                    "maxItems": 32,
3016                                    "contentMediaType": "application/x.dash.dpp.identifier"
3017                                },
3018                                "position": 5
3019                            }
3020                        },
3021                        "additionalProperties": false
3022                    }}),
3023                    None,
3024                    None,
3025                )
3026                .expect("contract")
3027                .data_contract_owned()
3028        }
3029
3030        fn clause(field: &str, operator: WhereOperator, value: Value) -> WhereClause {
3031            WhereClause {
3032                field: field.to_string(),
3033                operator,
3034                value,
3035            }
3036        }
3037
3038        /// `clause` as the one clause of a filter on created posts, placed
3039        /// where an equality (`contains`) or an `in` (`containsAny`) goes.
3040        fn filter_clauses(clause: WhereClause) -> InternalClauses {
3041            let mut clauses = InternalClauses::default();
3042            match clause.operator {
3043                WhereOperator::Equal | WhereOperator::Contains => {
3044                    clauses.equal_clauses.insert(clause.field.clone(), clause);
3045                }
3046                _ => clauses.in_clauses.push(clause),
3047            }
3048            clauses
3049        }
3050
3051        fn create_filter(
3052            contract: &DataContract,
3053            clause: WhereClause,
3054        ) -> DriveDocumentQueryFilter<'_> {
3055            DriveDocumentQueryFilter {
3056                contract,
3057                document_type_name: "post".to_string(),
3058                action_clauses: DocumentActionMatchClauses::Create {
3059                    new_document_clauses: filter_clauses(clause),
3060                },
3061            }
3062        }
3063
3064        /// The refusal of a filter whose one clause is `clause`.
3065        fn filter_refusal(contract: &DataContract, clause: WhereClause) -> Option<String> {
3066            create_filter(contract, clause)
3067                .validate()
3068                .first_error()
3069                .map(ToString::to_string)
3070        }
3071
3072        /// The refusal a documents query gives `clause`.
3073        fn query_refusal(contract: &DataContract, clause: &WhereClause) -> Option<String> {
3074            let document_type = contract.document_type_for_name("post").expect("post");
3075            clause
3076                .validate_array_element_clause(document_type, PlatformVersion::latest())
3077                .err()
3078                .map(|error| match error {
3079                    Error::Query(error) => error.to_string(),
3080                    other => other.to_string(),
3081                })
3082        }
3083
3084        /// One property of a created post.
3085        fn post_data(property: &str, value: Value) -> BTreeMap<String, Value> {
3086            BTreeMap::from([(property.to_string(), value)])
3087        }
3088
3089        /// Whether the filter whose one clause is `clause` matches a created
3090        /// post holding `data`.
3091        fn matches(
3092            contract: &DataContract,
3093            clause: WhereClause,
3094            document_data: BTreeMap<String, Value>,
3095        ) -> bool {
3096            let filter = create_filter(contract, clause);
3097            let DocumentActionMatchClauses::Create {
3098                new_document_clauses,
3099            } = &filter.action_clauses
3100            else {
3101                unreachable!("a create filter");
3102            };
3103            filter.evaluate_clauses(
3104                new_document_clauses,
3105                &Value::Identifier([3; 32]),
3106                &document_data,
3107            )
3108        }
3109
3110        #[test]
3111        fn should_admit_a_boolean_array_membership_as_documents_queries_do() {
3112            let contract = element_contract();
3113            for element_clause in [
3114                clause(
3115                    "flags",
3116                    WhereOperator::ContainsAny,
3117                    Value::Array(vec![Value::Bool(true)]),
3118                ),
3119                clause("flags", WhereOperator::Contains, Value::Bool(false)),
3120            ] {
3121                assert_eq!(query_refusal(&contract, &element_clause), None);
3122                assert_eq!(filter_refusal(&contract, element_clause), None);
3123            }
3124            let flagged = post_data(
3125                "flags",
3126                Value::Array(vec![Value::Bool(false), Value::Bool(true)]),
3127            );
3128            let unflagged = post_data("flags", Value::Array(vec![Value::Bool(false)]));
3129            let any_true = clause(
3130                "flags",
3131                WhereOperator::ContainsAny,
3132                Value::Array(vec![Value::Bool(true)]),
3133            );
3134            assert!(matches(&contract, any_true.clone(), flagged));
3135            assert!(!matches(&contract, any_true, unflagged));
3136        }
3137
3138        #[test]
3139        fn should_keep_refusing_in_on_a_boolean_property() {
3140            let contract = element_contract();
3141            let pinned_in = clause(
3142                "pinned",
3143                WhereOperator::In,
3144                Value::Array(vec![Value::Bool(true)]),
3145            );
3146            assert_eq!(
3147                filter_refusal(&contract, pinned_in).as_deref(),
3148                Some("invalid where clause components error: operator not allowed for field type")
3149            );
3150        }
3151
3152        #[test]
3153        fn should_match_a_u8_array_against_a_list_given_as_bytes() {
3154            let contract = element_contract();
3155            let any_score = clause(
3156                "scores",
3157                WhereOperator::ContainsAny,
3158                Value::Bytes(vec![1, 2]),
3159            );
3160            assert_eq!(query_refusal(&contract, &any_score), None);
3161            assert_eq!(filter_refusal(&contract, any_score.clone()), None);
3162            let scored = post_data("scores", Value::Array(vec![Value::U8(1), Value::U8(5)]));
3163            let unscored = post_data("scores", Value::Array(vec![Value::U8(3)]));
3164            assert!(matches(&contract, any_score.clone(), scored));
3165            assert!(!matches(&contract, any_score, unscored));
3166        }
3167
3168        #[test]
3169        fn should_match_an_element_given_in_another_representation() {
3170            let contract = element_contract();
3171            // An integer of another width keys as the stored u8 element does.
3172            let scored = post_data("scores", Value::Array(vec![Value::U8(1), Value::U8(5)]));
3173            assert!(matches(
3174                &contract,
3175                clause("scores", WhereOperator::Contains, Value::U64(5)),
3176                scored.clone()
3177            ));
3178            assert!(matches(
3179                &contract,
3180                clause(
3181                    "scores",
3182                    WhereOperator::ContainsAny,
3183                    Value::Array(vec![Value::I64(9), Value::U16(1)])
3184                ),
3185                scored.clone()
3186            ));
3187            assert!(!matches(
3188                &contract,
3189                clause("scores", WhereOperator::Contains, Value::U64(9)),
3190                scored
3191            ));
3192            // An identifier given as bytes or as base58 text keys as the stored
3193            // identifier does.
3194            let author = [8u8; 32];
3195            let authored = post_data(
3196                "authors",
3197                Value::Array(vec![Value::Identifier(author), Value::Identifier([9; 32])]),
3198            );
3199            for given in [
3200                Value::Bytes(author.to_vec()),
3201                Value::Text(Identifier::from(author).to_string(Encoding::Base58)),
3202                Value::Identifier(author),
3203            ] {
3204                assert_eq!(
3205                    query_refusal(
3206                        &contract,
3207                        &clause("authors", WhereOperator::Contains, given.clone())
3208                    ),
3209                    None
3210                );
3211                assert!(
3212                    matches(
3213                        &contract,
3214                        clause("authors", WhereOperator::Contains, given.clone()),
3215                        authored.clone()
3216                    ),
3217                    "{given:?}"
3218                );
3219            }
3220        }
3221
3222        #[test]
3223        fn should_refuse_what_documents_queries_refuse_with_the_same_error() {
3224            let contract = element_contract();
3225            for refused in [
3226                clause("hashtags", WhereOperator::Equal, Value::Text("dash".into())),
3227                clause(
3228                    "language",
3229                    WhereOperator::Contains,
3230                    Value::Text("en".into()),
3231                ),
3232                clause("hashtags", WhereOperator::Contains, Value::U64(5)),
3233                clause("hashtags", WhereOperator::ContainsAny, Value::Array(vec![])),
3234                clause(
3235                    "hashtags",
3236                    WhereOperator::ContainsAny,
3237                    Value::Array(vec![Value::Text("a".into()), Value::Text("a".into())]),
3238                ),
3239            ] {
3240                let query = query_refusal(&contract, &refused);
3241                assert!(query.is_some(), "a documents query refuses {refused:?}");
3242                assert_eq!(filter_refusal(&contract, refused), query);
3243            }
3244        }
3245    }
3246}