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