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.
13//! - Transfer: evaluates `original_document_clauses` and a new `owner_clause` against
14//!   the `recipient_owner_id`.
15//! - UpdatePrice: evaluates `original_document_clauses` and a `price_clause` against
16//!   the new price in the transition.
17//! - Purchase: evaluates `original_document_clauses` and an `owner_clause` against the
18//!   batch owner (purchaser) ID.
19//!
20//! Usage:
21//! - First check: call `matches_document_transition()` per transition to
22//!   evaluate applicable constraints before fetching the original document. Decide
23//!   whether to fetch the original document (returns Pass/Fail/NeedsOriginal).
24//! - Second check: only if the first check returned `NeedsOriginal`, fetch the original
25//!   `Document` and call `matches_original_document()` to evaluate original-dependent clauses.
26//!
27//! Validation:
28//! - `validate()` performs structural checks: confirms the document type exists for the
29//!   contract, enforces action-specific composition rules (e.g., at least one non-empty
30//!   clause where required), and validates operator/value compatibility for scalar clauses
31//!   like `owner_clause` and `price_clause`.
32
33use std::collections::BTreeMap;
34use dpp::data_contract::accessors::v0::DataContractV0Getters;
35use dpp::data_contract::DataContract;
36use dpp::platform_value::Value;
37use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods;
38use dpp::document::{Document, DocumentV0Getters};
39use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition;
40use dpp::state_transition::batch_transition::document_create_transition::v0::v0_methods::DocumentCreateTransitionV0Methods;
41use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods;
42use dpp::state_transition::batch_transition::document_replace_transition::v0::v0_methods::DocumentReplaceTransitionV0Methods;
43use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::v0_methods::DocumentTransferTransitionV0Methods;
44use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::v0_methods::DocumentUpdatePriceTransitionV0Methods;
45use crate::query::{InternalClauses, QuerySyntaxSimpleValidationResult, ValueClause, WhereOperator};
46use crate::error::query::QuerySyntaxError;
47use dpp::platform_value::ValueMapHelper;
48
49/// Filter used to match document transitions for subscriptions.
50///
51/// Targets a specific data contract and document type, and carries action-specific
52/// match clauses via `DocumentActionMatchClauses`. Use `matches_document_transition()`
53/// and `matches_original_document()` to evaluate document transitions.
54/// `validate()` performs structural checks (document type exists, clause composition rules).
55#[cfg(any(feature = "server", feature = "verify"))]
56#[derive(Debug, PartialEq, Clone)]
57pub struct DriveDocumentQueryFilter<'a> {
58    /// DataContract
59    pub contract: &'a DataContract,
60    /// Document type name
61    pub document_type_name: String,
62    /// Action-specific clauses
63    pub action_clauses: DocumentActionMatchClauses,
64}
65
66/// Result of evaluating constraints for a transition before potentially fetching the original document.
67#[cfg(any(feature = "server", feature = "verify"))]
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum TransitionCheckResult {
70    /// All applicable transition-level checks pass and no original is required.
71    Pass,
72    /// Some transition-level check fails; do not fetch original.
73    Fail,
74    /// Transition-level checks pass, original clauses are non-empty and must be evaluated.
75    NeedsOriginal,
76}
77
78/// Action-specific filter clauses for matching document transitions.
79///
80/// These clauses are used to evaluate whether a given document transition
81/// (Create/Replace/Delete/Transfer/UpdatePrice/Purchase) matches a subscription
82/// filter.
83///
84/// Conventions:
85/// - Empty `InternalClauses` = no constraint for document-data checks.
86/// - `Option<ValueClause>` = optional scalar constraint (owner/price); `None` = no constraint.
87/// - Action-specific “at least one present” rules are enforced by `validate()`.
88#[allow(clippy::large_enum_variant)]
89#[derive(Debug, PartialEq, Clone)]
90pub enum DocumentActionMatchClauses {
91    /// Create: filters on the new document data.
92    Create {
93        /// Clauses on the new document data.
94        new_document_clauses: InternalClauses,
95    },
96    /// Replace: filters on original and/or new document data.
97    Replace {
98        /// Clauses on the original document data (pre-change).
99        original_document_clauses: InternalClauses,
100        /// Clauses on the new document data (replacement).
101        new_document_clauses: InternalClauses,
102    },
103    /// Delete: filters on the original (existing) document.
104    Delete {
105        /// Clauses on the original document data.
106        original_document_clauses: InternalClauses,
107    },
108    /// Transfer: filters on original data and/or recipient owner id.
109    Transfer {
110        /// Clauses on the original document data.
111        original_document_clauses: InternalClauses,
112        /// Constraint on the recipient owner id.
113        owner_clause: Option<ValueClause>,
114    },
115    /// UpdatePrice: filters on original data and/or the new price.
116    UpdatePrice {
117        /// Clauses on the original document data.
118        original_document_clauses: InternalClauses,
119        /// Constraint on the new price.
120        price_clause: Option<ValueClause>,
121    },
122    /// Purchase: filters on original data and/or batch owner id.
123    Purchase {
124        /// Clauses on the original document data.
125        original_document_clauses: InternalClauses,
126        /// Constraint on the batch owner (purchaser) id.
127        owner_clause: Option<ValueClause>,
128    },
129}
130
131impl DriveDocumentQueryFilter<'_> {
132    /// Check a transition using only transition-level constraints.
133    ///
134    /// When to run:
135    /// - Call this for each incoming transition before
136    ///   fetching the original document. It short-circuits on obvious mismatches and
137    ///   tells you if an original is needed at all for the final decision.
138    ///
139    /// Returns:
140    /// - `Pass` if all applicable transition-level checks pass and no original is needed.
141    /// - `Fail` if any transition-level check fails (no need to fetch original).
142    /// - `NeedsOriginal` if transition-level checks pass but original clauses are non-empty
143    ///   and must be evaluated with the original document.
144    #[cfg(any(feature = "server", feature = "verify"))]
145    pub fn matches_document_transition(
146        &self,
147        document_transition: &DocumentTransition,
148        batch_owner_value: Option<&Value>, // Only used for Purchase
149    ) -> TransitionCheckResult {
150        // Fast reject on contract/type mismatch common to all transitions
151        if document_transition.base().data_contract_id() != self.contract.id()
152            || document_transition.base().document_type_name() != &self.document_type_name
153        {
154            return TransitionCheckResult::Fail;
155        }
156
157        // Document ID value used by clause evaluation paths
158        let id_value: Value = document_transition.base().id().into();
159
160        match document_transition {
161            DocumentTransition::Create(create) => {
162                if let DocumentActionMatchClauses::Create {
163                    new_document_clauses,
164                } = &self.action_clauses
165                {
166                    if self.evaluate_clauses(new_document_clauses, &id_value, create.data()) {
167                        TransitionCheckResult::Pass
168                    } else {
169                        TransitionCheckResult::Fail
170                    }
171                } else {
172                    TransitionCheckResult::Fail
173                }
174            }
175            DocumentTransition::Replace(replace) => {
176                if let DocumentActionMatchClauses::Replace {
177                    original_document_clauses,
178                    new_document_clauses,
179                } = &self.action_clauses
180                {
181                    let final_ok = if new_document_clauses.is_empty() {
182                        true
183                    } else {
184                        self.evaluate_clauses(new_document_clauses, &id_value, replace.data())
185                    };
186                    if !final_ok {
187                        return TransitionCheckResult::Fail;
188                    }
189                    if original_document_clauses.is_empty() {
190                        return TransitionCheckResult::Pass;
191                    }
192                    if original_document_clauses.is_for_primary_key() {
193                        if self.evaluate_clauses(
194                            original_document_clauses,
195                            &id_value,
196                            &BTreeMap::new(),
197                        ) {
198                            return TransitionCheckResult::Pass;
199                        }
200                        return TransitionCheckResult::Fail;
201                    }
202                    TransitionCheckResult::NeedsOriginal
203                } else {
204                    TransitionCheckResult::Fail
205                }
206            }
207            DocumentTransition::Delete(_) => {
208                if let DocumentActionMatchClauses::Delete {
209                    original_document_clauses,
210                } = &self.action_clauses
211                {
212                    if original_document_clauses.is_empty() {
213                        return TransitionCheckResult::Pass;
214                    }
215                    if original_document_clauses.is_for_primary_key() {
216                        if self.evaluate_clauses(
217                            original_document_clauses,
218                            &id_value,
219                            &BTreeMap::new(),
220                        ) {
221                            return TransitionCheckResult::Pass;
222                        }
223                        return TransitionCheckResult::Fail;
224                    }
225                    TransitionCheckResult::NeedsOriginal
226                } else {
227                    TransitionCheckResult::Fail
228                }
229            }
230            DocumentTransition::Transfer(transfer) => {
231                if let DocumentActionMatchClauses::Transfer {
232                    original_document_clauses,
233                    owner_clause,
234                } = &self.action_clauses
235                {
236                    let new_owner_value: Value = transfer.recipient_owner_id().into();
237                    let owner_ok = match owner_clause {
238                        Some(clause) => clause.matches_value(&new_owner_value),
239                        None => true,
240                    };
241                    if !owner_ok {
242                        return TransitionCheckResult::Fail;
243                    }
244                    if original_document_clauses.is_empty() {
245                        return TransitionCheckResult::Pass;
246                    }
247                    if original_document_clauses.is_for_primary_key() {
248                        if self.evaluate_clauses(
249                            original_document_clauses,
250                            &id_value,
251                            &BTreeMap::new(),
252                        ) {
253                            return TransitionCheckResult::Pass;
254                        }
255                        return TransitionCheckResult::Fail;
256                    }
257                    TransitionCheckResult::NeedsOriginal
258                } else {
259                    TransitionCheckResult::Fail
260                }
261            }
262            DocumentTransition::UpdatePrice(update_price) => {
263                if let DocumentActionMatchClauses::UpdatePrice {
264                    original_document_clauses,
265                    price_clause,
266                } = &self.action_clauses
267                {
268                    let price_value = Value::U64(update_price.price());
269                    let price_ok = match price_clause {
270                        Some(clause) => clause.matches_value(&price_value),
271                        None => true,
272                    };
273                    if !price_ok {
274                        return TransitionCheckResult::Fail;
275                    }
276                    if original_document_clauses.is_empty() {
277                        return TransitionCheckResult::Pass;
278                    }
279                    if original_document_clauses.is_for_primary_key() {
280                        if self.evaluate_clauses(
281                            original_document_clauses,
282                            &id_value,
283                            &BTreeMap::new(),
284                        ) {
285                            return TransitionCheckResult::Pass;
286                        }
287                        return TransitionCheckResult::Fail;
288                    }
289                    TransitionCheckResult::NeedsOriginal
290                } else {
291                    TransitionCheckResult::Fail
292                }
293            }
294            DocumentTransition::Purchase(_) => {
295                if let DocumentActionMatchClauses::Purchase {
296                    original_document_clauses,
297                    owner_clause,
298                } = &self.action_clauses
299                {
300                    let owner_ok = match (owner_clause, batch_owner_value) {
301                        (Some(clause), Some(val)) => clause.matches_value(val),
302                        (Some(_), None) => return TransitionCheckResult::Fail,
303                        (None, _) => true,
304                    };
305                    if !owner_ok {
306                        return TransitionCheckResult::Fail;
307                    }
308                    if original_document_clauses.is_empty() {
309                        return TransitionCheckResult::Pass;
310                    }
311                    if original_document_clauses.is_for_primary_key() {
312                        if self.evaluate_clauses(
313                            original_document_clauses,
314                            &id_value,
315                            &BTreeMap::new(),
316                        ) {
317                            return TransitionCheckResult::Pass;
318                        }
319                        return TransitionCheckResult::Fail;
320                    }
321                    TransitionCheckResult::NeedsOriginal
322                } else {
323                    TransitionCheckResult::Fail
324                }
325            }
326        }
327    }
328
329    /// Evaluates original-dependent clauses against the provided original `Document`.
330    ///
331    /// When to run:
332    /// - After `matches_document_transition` returns `NeedsOriginal` and the caller fetches
333    ///   the original document.
334    /// - This evaluates only original-dependent clauses; transition-level checks were
335    ///   already applied during the first phase.
336    #[cfg(any(feature = "server", feature = "verify"))]
337    pub fn matches_original_document(&self, original_document: &Document) -> bool {
338        // Evaluate only original-dependent clauses. Transition base was validated earlier.
339        match &self.action_clauses {
340            DocumentActionMatchClauses::Replace {
341                original_document_clauses,
342                ..
343            }
344            | DocumentActionMatchClauses::Delete {
345                original_document_clauses,
346            }
347            | DocumentActionMatchClauses::Transfer {
348                original_document_clauses,
349                ..
350            }
351            | DocumentActionMatchClauses::UpdatePrice {
352                original_document_clauses,
353                ..
354            }
355            | DocumentActionMatchClauses::Purchase {
356                original_document_clauses,
357                ..
358            } => {
359                let id_value: Value = original_document.id().into();
360                self.evaluate_clauses(
361                    original_document_clauses,
362                    &id_value,
363                    original_document.properties(),
364                )
365            }
366            _ => false,
367        }
368    }
369
370    /// Single clause evaluator used by both transition and original-document paths.
371    #[cfg(any(feature = "server", feature = "verify"))]
372    fn evaluate_clauses(
373        &self,
374        clauses: &InternalClauses,
375        document_id_value: &Value,
376        document_data: &BTreeMap<String, Value>,
377    ) -> bool {
378        // Primary key IN clause
379        if let Some(primary_key_in_clause) = &clauses.primary_key_in_clause {
380            if !primary_key_in_clause.matches_value(document_id_value) {
381                return false;
382            }
383        }
384
385        // Primary key EQUAL clause
386        if let Some(primary_key_equal_clause) = &clauses.primary_key_equal_clause {
387            if !primary_key_equal_clause.matches_value(document_id_value) {
388                return false;
389            }
390        }
391
392        // In clause
393        if let Some(in_clause) = &clauses.in_clause {
394            let field_value = get_value_by_path(document_data, &in_clause.field);
395            if let Some(value) = field_value {
396                if !in_clause.matches_value(value) {
397                    return false;
398                }
399            } else {
400                return false;
401            }
402        }
403
404        // Range clause
405        if let Some(range_clause) = &clauses.range_clause {
406            let field_value = get_value_by_path(document_data, &range_clause.field);
407            if let Some(value) = field_value {
408                if !range_clause.matches_value(value) {
409                    return false;
410                }
411            } else {
412                return false;
413            }
414        }
415
416        // Equal clauses
417        for (field, equal_clause) in &clauses.equal_clauses {
418            let field_value = get_value_by_path(document_data, field);
419            if let Some(value) = field_value {
420                if !equal_clause.matches_value(value) {
421                    return false;
422                }
423            } else {
424                return false;
425            }
426        }
427
428        true
429    }
430
431    /// Validate the filter structure and clauses.
432    ///
433    /// In addition to these validations, the subscription host should check the contract's existence.
434    #[cfg(any(feature = "server", feature = "verify"))]
435    pub fn validate(&self) -> QuerySyntaxSimpleValidationResult {
436        // Ensure the document type exists
437        let Some(document_type) = self
438            .contract
439            .document_type_optional_for_name(&self.document_type_name)
440        else {
441            return QuerySyntaxSimpleValidationResult::new_with_error(
442                QuerySyntaxError::DocumentTypeNotFound("unknown document type"),
443            );
444        };
445
446        match &self.action_clauses {
447            DocumentActionMatchClauses::Create {
448                new_document_clauses,
449            } => new_document_clauses.validate_against_schema(document_type),
450            DocumentActionMatchClauses::Replace {
451                original_document_clauses,
452                new_document_clauses,
453            } => {
454                if !original_document_clauses.is_empty() {
455                    let result = original_document_clauses.validate_against_schema(document_type);
456                    if result.is_err() {
457                        return result;
458                    }
459                }
460                if !new_document_clauses.is_empty() {
461                    new_document_clauses.validate_against_schema(document_type)
462                } else {
463                    QuerySyntaxSimpleValidationResult::new()
464                }
465            }
466            DocumentActionMatchClauses::Delete {
467                original_document_clauses,
468            } => original_document_clauses.validate_against_schema(document_type),
469            DocumentActionMatchClauses::Transfer {
470                original_document_clauses,
471                owner_clause,
472            } => {
473                if !original_document_clauses.is_empty() {
474                    let result = original_document_clauses.validate_against_schema(document_type);
475                    if result.is_err() {
476                        return result;
477                    }
478                }
479                if let Some(owner) = owner_clause {
480                    let ok = match owner.operator {
481                        WhereOperator::Equal => matches!(owner.value, Value::Identifier(_)),
482                        WhereOperator::In => match &owner.value {
483                            Value::Array(arr) => {
484                                arr.iter().all(|v| matches!(v, Value::Identifier(_)))
485                            }
486                            _ => false,
487                        },
488                        _ => false,
489                    };
490                    if ok {
491                        QuerySyntaxSimpleValidationResult::new()
492                    } else {
493                        QuerySyntaxSimpleValidationResult::new_with_error(
494                            QuerySyntaxError::InvalidWhereClauseComponents("invalid owner clause"),
495                        )
496                    }
497                } else {
498                    QuerySyntaxSimpleValidationResult::new()
499                }
500            }
501            DocumentActionMatchClauses::UpdatePrice {
502                original_document_clauses,
503                price_clause,
504            } => {
505                if !original_document_clauses.is_empty() {
506                    let result = original_document_clauses.validate_against_schema(document_type);
507                    if result.is_err() {
508                        return result;
509                    }
510                }
511                if let Some(price) = price_clause {
512                    let ok = match price.operator {
513                        WhereOperator::Equal
514                        | WhereOperator::GreaterThan
515                        | WhereOperator::GreaterThanOrEquals
516                        | WhereOperator::LessThan
517                        | WhereOperator::LessThanOrEquals => {
518                            price.value.is_integer_can_fit_in_64_bits()
519                        }
520                        WhereOperator::Between
521                        | WhereOperator::BetweenExcludeBounds
522                        | WhereOperator::BetweenExcludeLeft
523                        | WhereOperator::BetweenExcludeRight => match &price.value {
524                            Value::Array(arr) => {
525                                arr.len() == 2
526                                    && arr.iter().all(|v| v.is_integer_can_fit_in_64_bits())
527                                    && arr[0] < arr[1]
528                            }
529                            _ => false,
530                        },
531                        WhereOperator::In => match &price.value {
532                            Value::Array(arr) => {
533                                arr.iter().all(|v| v.is_integer_can_fit_in_64_bits())
534                            }
535                            _ => false,
536                        },
537                        WhereOperator::StartsWith => false,
538                    };
539                    if ok {
540                        QuerySyntaxSimpleValidationResult::new()
541                    } else {
542                        QuerySyntaxSimpleValidationResult::new_with_error(
543                            QuerySyntaxError::InvalidWhereClauseComponents("invalid price clause"),
544                        )
545                    }
546                } else {
547                    QuerySyntaxSimpleValidationResult::new()
548                }
549            }
550            DocumentActionMatchClauses::Purchase {
551                original_document_clauses,
552                owner_clause,
553            } => {
554                if !original_document_clauses.is_empty() {
555                    let result = original_document_clauses.validate_against_schema(document_type);
556                    if result.is_err() {
557                        return result;
558                    }
559                }
560                if let Some(owner) = owner_clause {
561                    let ok = match owner.operator {
562                        WhereOperator::Equal => matches!(owner.value, Value::Identifier(_)),
563                        WhereOperator::In => match &owner.value {
564                            Value::Array(arr) => {
565                                arr.iter().all(|v| matches!(v, Value::Identifier(_)))
566                            }
567                            _ => false,
568                        },
569                        _ => false,
570                    };
571                    if ok {
572                        QuerySyntaxSimpleValidationResult::new()
573                    } else {
574                        QuerySyntaxSimpleValidationResult::new_with_error(
575                            QuerySyntaxError::InvalidWhereClauseComponents("invalid owner clause"),
576                        )
577                    }
578                } else {
579                    QuerySyntaxSimpleValidationResult::new()
580                }
581            }
582        }
583    }
584}
585
586/// Resolve a dot-notated path into a nested `BTreeMap<String, Value>` payload.
587///
588/// Supports dot notation like `meta.status` by walking `Value::Map` entries
589/// using `ValueMapHelper`. Returns `None` if any segment is missing or if a
590/// non-map value is encountered before the final segment. An empty `path`
591/// returns `None`.
592#[cfg(any(feature = "server", feature = "verify"))]
593fn get_value_by_path<'a>(root: &'a BTreeMap<String, Value>, path: &str) -> Option<&'a Value> {
594    if path.is_empty() {
595        return None;
596    }
597    let mut current: Option<&Value> = None;
598    let mut segments = path.split('.');
599    if let Some(first) = segments.next() {
600        current = root.get(first);
601    }
602    for seg in segments {
603        match current {
604            Some(Value::Map(ref vm)) => {
605                current = vm.get_optional_key(seg);
606            }
607            _ => return None,
608        }
609    }
610    current
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use crate::query::{ValueClause, WhereClause, WhereOperator};
617    use dpp::document::{Document, DocumentV0};
618    use dpp::prelude::Identifier;
619    use dpp::state_transition::batch_transition::document_base_transition::v1::DocumentBaseTransitionV1;
620    use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition;
621    use dpp::tests::fixtures::get_data_contract_fixture;
622    use dpp::version::LATEST_PLATFORM_VERSION;
623
624    #[test]
625    fn test_matches_document_basic() {
626        // Get a test contract from fixtures
627        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
628        let contract = fixture.data_contract_owned();
629
630        // Create a filter with no clauses (should match if contract and type match)
631        let internal_clauses = InternalClauses::default();
632        let filter = DriveDocumentQueryFilter {
633            contract: &contract,
634            document_type_name: "niceDocument".to_string(),
635            action_clauses: DocumentActionMatchClauses::Create {
636                new_document_clauses: internal_clauses.clone(),
637            },
638        };
639
640        // Create matching document base
641        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
642            id: Identifier::from([3u8; 32]),
643            document_type_name: "niceDocument".to_string(),
644            data_contract_id: contract.id(),
645            identity_contract_nonce: 0,
646            token_payment_info: None,
647        });
648
649        let document_data = BTreeMap::new();
650
651        // With no clauses, evaluation should be true regardless of data
652        let id_value: Value = document_base.id().into();
653        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &document_data));
654    }
655
656    #[test]
657    fn test_matches_document_with_primary_key_equal() {
658        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
659        let contract = fixture.data_contract_owned();
660
661        let target_id = Identifier::from([42u8; 32]);
662
663        let internal_clauses = InternalClauses {
664            primary_key_equal_clause: Some(WhereClause {
665                field: "$id".to_string(),
666                operator: WhereOperator::Equal,
667                value: target_id.into(),
668            }),
669            ..Default::default()
670        };
671
672        let filter = DriveDocumentQueryFilter {
673            contract: &contract,
674            document_type_name: "niceDocument".to_string(),
675            action_clauses: DocumentActionMatchClauses::Create {
676                new_document_clauses: internal_clauses.clone(),
677            },
678        };
679
680        // Test with matching ID
681        let matching_doc = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
682            id: target_id,
683            document_type_name: "niceDocument".to_string(),
684            data_contract_id: contract.id(),
685            identity_contract_nonce: 0,
686            token_payment_info: None,
687        });
688
689        let document_data = BTreeMap::new();
690
691        let id_value: Value = matching_doc.id().into();
692        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &document_data));
693
694        // Test with different ID
695        let non_matching_doc = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
696            id: Identifier::from([99u8; 32]),
697            document_type_name: "niceDocument".to_string(),
698            data_contract_id: contract.id(),
699            identity_contract_nonce: 0,
700            token_payment_info: None,
701        });
702
703        let non_id_value: Value = non_matching_doc.id().into();
704        assert!(!filter.evaluate_clauses(&internal_clauses, &non_id_value, &document_data));
705    }
706
707    #[test]
708    fn test_matches_document_with_field_filters() {
709        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
710        let contract = fixture.data_contract_owned();
711
712        // Test Equal operator
713        let mut equal_clauses = BTreeMap::new();
714        equal_clauses.insert(
715            "name".to_string(),
716            WhereClause {
717                field: "name".to_string(),
718                operator: WhereOperator::Equal,
719                value: Value::Text("example".to_string()),
720            },
721        );
722
723        let internal_clauses = InternalClauses {
724            equal_clauses,
725            ..Default::default()
726        };
727
728        let filter = DriveDocumentQueryFilter {
729            contract: &contract,
730            document_type_name: "niceDocument".to_string(),
731            action_clauses: DocumentActionMatchClauses::Create {
732                new_document_clauses: internal_clauses.clone(),
733            },
734        };
735
736        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
737            id: Identifier::from([3u8; 32]),
738            document_type_name: "niceDocument".to_string(),
739            data_contract_id: contract.id(),
740            identity_contract_nonce: 0,
741            token_payment_info: None,
742        });
743
744        // Test with matching data
745        let mut matching_data = BTreeMap::new();
746        matching_data.insert("name".to_string(), Value::Text("example".to_string()));
747
748        let id_value: Value = document_base.id().into();
749        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &matching_data));
750
751        // Test with non-matching data
752        let mut non_matching_data = BTreeMap::new();
753        non_matching_data.insert("name".to_string(), Value::Text("different".to_string()));
754
755        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &non_matching_data));
756
757        // Test with missing field
758        let empty_data = BTreeMap::new();
759        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &empty_data));
760    }
761
762    #[test]
763    fn test_matches_document_with_in_operator() {
764        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
765        let contract = fixture.data_contract_owned();
766
767        let allowed_values = vec![
768            Value::Text("active".to_string()),
769            Value::Text("pending".to_string()),
770        ];
771
772        let internal_clauses = InternalClauses {
773            in_clause: Some(WhereClause {
774                field: "status".to_string(),
775                operator: WhereOperator::In,
776                value: Value::Array(allowed_values),
777            }),
778            ..Default::default()
779        };
780
781        let filter = DriveDocumentQueryFilter {
782            contract: &contract,
783            document_type_name: "niceDocument".to_string(),
784            action_clauses: DocumentActionMatchClauses::Create {
785                new_document_clauses: internal_clauses.clone(),
786            },
787        };
788
789        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
790            id: Identifier::from([3u8; 32]),
791            document_type_name: "niceDocument".to_string(),
792            data_contract_id: contract.id(),
793            identity_contract_nonce: 0,
794            token_payment_info: None,
795        });
796
797        // Test with value in list
798        let mut matching_data = BTreeMap::new();
799        matching_data.insert("status".to_string(), Value::Text("active".to_string()));
800        let id_value: Value = document_base.id().into();
801        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &matching_data));
802
803        // Test with value not in list
804        let mut non_matching_data = BTreeMap::new();
805        non_matching_data.insert("status".to_string(), Value::Text("completed".to_string()));
806        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &non_matching_data));
807    }
808
809    #[test]
810    fn test_matches_document_with_range_operators() {
811        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
812        let contract = fixture.data_contract_owned();
813
814        // Test GreaterThan
815        let internal_clauses = InternalClauses {
816            range_clause: Some(WhereClause {
817                field: "score".to_string(),
818                operator: WhereOperator::GreaterThan,
819                value: Value::U64(50),
820            }),
821            ..Default::default()
822        };
823
824        let filter = DriveDocumentQueryFilter {
825            contract: &contract,
826            document_type_name: "niceDocument".to_string(),
827            action_clauses: DocumentActionMatchClauses::Create {
828                new_document_clauses: internal_clauses.clone(),
829            },
830        };
831
832        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
833            id: Identifier::from([3u8; 32]),
834            document_type_name: "niceDocument".to_string(),
835            data_contract_id: contract.id(),
836            identity_contract_nonce: 0,
837            token_payment_info: None,
838        });
839
840        // Test with value greater than threshold
841        let mut greater_data = BTreeMap::new();
842        greater_data.insert("score".to_string(), Value::U64(75));
843        let id_value: Value = document_base.id().into();
844        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &greater_data));
845
846        // Test with value equal to threshold (should fail for GreaterThan)
847        let mut equal_data = BTreeMap::new();
848        equal_data.insert("score".to_string(), Value::U64(50));
849        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &equal_data));
850
851        // Test with value less than threshold
852        let mut less_data = BTreeMap::new();
853        less_data.insert("score".to_string(), Value::U64(25));
854        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &less_data));
855    }
856
857    #[test]
858    fn test_matches_document_with_nested_field() {
859        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
860        let contract = fixture.data_contract_owned();
861
862        // Equal on nested field: meta.status == "active"
863        let mut equal_clauses = BTreeMap::new();
864        equal_clauses.insert(
865            "meta.status".to_string(),
866            WhereClause {
867                field: "meta.status".to_string(),
868                operator: WhereOperator::Equal,
869                value: Value::Text("active".to_string()),
870            },
871        );
872
873        let internal_clauses = InternalClauses {
874            equal_clauses,
875            ..Default::default()
876        };
877
878        let filter = DriveDocumentQueryFilter {
879            contract: &contract,
880            document_type_name: "niceDocument".to_string(),
881            action_clauses: DocumentActionMatchClauses::Create {
882                new_document_clauses: internal_clauses.clone(),
883            },
884        };
885
886        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
887            id: Identifier::from([3u8; 32]),
888            document_type_name: "niceDocument".to_string(),
889            data_contract_id: contract.id(),
890            identity_contract_nonce: 0,
891            token_payment_info: None,
892        });
893
894        // Build nested data: { meta: { status: "active" } }
895        let nested = vec![(
896            Value::Text("status".to_string()),
897            Value::Text("active".to_string()),
898        )];
899        let mut data = BTreeMap::new();
900        data.insert("meta".to_string(), Value::Map(nested));
901
902        let id_value: Value = document_base.id().into();
903        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &data));
904    }
905
906    #[test]
907    fn test_validate_optional_actions_allow_empty_clauses() {
908        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
909        let contract = fixture.data_contract_owned();
910
911        // Replace with none/none -> allowed
912        let filter = DriveDocumentQueryFilter {
913            contract: &contract,
914            document_type_name: "niceDocument".to_string(),
915            action_clauses: DocumentActionMatchClauses::Replace {
916                original_document_clauses: InternalClauses::default(),
917                new_document_clauses: InternalClauses::default(),
918            },
919        };
920        assert!(filter.validate().is_valid());
921
922        // Replace with final only -> valid (non-empty final clauses)
923        let filter = DriveDocumentQueryFilter {
924            contract: &contract,
925            document_type_name: "niceDocument".to_string(),
926            action_clauses: DocumentActionMatchClauses::Replace {
927                original_document_clauses: InternalClauses::default(),
928                new_document_clauses: InternalClauses {
929                    primary_key_equal_clause: Some(WhereClause {
930                        field: "$id".to_string(),
931                        operator: WhereOperator::Equal,
932                        value: Value::Identifier([3u8; 32]),
933                    }),
934                    ..Default::default()
935                },
936            },
937        };
938        assert!(filter.validate().is_valid());
939
940        // Transfer with none/none -> allowed
941        let filter = DriveDocumentQueryFilter {
942            contract: &contract,
943            document_type_name: "niceDocument".to_string(),
944            action_clauses: DocumentActionMatchClauses::Transfer {
945                original_document_clauses: InternalClauses::default(),
946                owner_clause: None,
947            },
948        };
949        assert!(filter.validate().is_valid());
950
951        // Transfer with owner only -> valid
952        let filter = DriveDocumentQueryFilter {
953            contract: &contract,
954            document_type_name: "niceDocument".to_string(),
955            action_clauses: DocumentActionMatchClauses::Transfer {
956                original_document_clauses: InternalClauses::default(),
957                owner_clause: Some(ValueClause {
958                    operator: WhereOperator::Equal,
959                    value: Value::Identifier([1u8; 32]),
960                }),
961            },
962        };
963        assert!(filter.validate().is_valid());
964
965        // UpdatePrice with none/none -> allowed
966        let filter = DriveDocumentQueryFilter {
967            contract: &contract,
968            document_type_name: "niceDocument".to_string(),
969            action_clauses: DocumentActionMatchClauses::UpdatePrice {
970                original_document_clauses: InternalClauses::default(),
971                price_clause: None,
972            },
973        };
974        assert!(filter.validate().is_valid());
975
976        // UpdatePrice with price only -> valid
977        let filter = DriveDocumentQueryFilter {
978            contract: &contract,
979            document_type_name: "niceDocument".to_string(),
980            action_clauses: DocumentActionMatchClauses::UpdatePrice {
981                original_document_clauses: InternalClauses::default(),
982                price_clause: Some(ValueClause {
983                    operator: WhereOperator::GreaterThan,
984                    value: Value::U64(0),
985                }),
986            },
987        };
988        assert!(filter.validate().is_valid());
989
990        // Purchase with none/none -> allowed
991        let filter = DriveDocumentQueryFilter {
992            contract: &contract,
993            document_type_name: "niceDocument".to_string(),
994            action_clauses: DocumentActionMatchClauses::Purchase {
995                original_document_clauses: InternalClauses::default(),
996                owner_clause: None,
997            },
998        };
999        assert!(filter.validate().is_valid());
1000
1001        // Purchase with owner only -> valid
1002        let filter = DriveDocumentQueryFilter {
1003            contract: &contract,
1004            document_type_name: "niceDocument".to_string(),
1005            action_clauses: DocumentActionMatchClauses::Purchase {
1006                original_document_clauses: InternalClauses::default(),
1007                owner_clause: Some(ValueClause {
1008                    operator: WhereOperator::Equal,
1009                    value: Value::Identifier([2u8; 32]),
1010                }),
1011            },
1012        };
1013        assert!(filter.validate().is_valid());
1014    }
1015
1016    #[test]
1017    fn test_transfer_owner_clause_only_matches() {
1018        use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::v0::DocumentTransferTransitionV0;
1019        use dpp::state_transition::batch_transition::batched_transition::document_transfer_transition::DocumentTransferTransition;
1020
1021        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1022        let contract = fixture.data_contract_owned();
1023
1024        let new_owner = Identifier::from([5u8; 32]);
1025
1026        // Filter checks only new owner
1027        let filter = DriveDocumentQueryFilter {
1028            contract: &contract,
1029            document_type_name: "niceDocument".to_string(),
1030            action_clauses: DocumentActionMatchClauses::Transfer {
1031                original_document_clauses: InternalClauses::default(),
1032                owner_clause: Some(ValueClause {
1033                    operator: WhereOperator::Equal,
1034                    value: new_owner.into(),
1035                }),
1036            },
1037        };
1038
1039        // Transfer transition with recipient = new_owner
1040        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1041            id: Identifier::from([3u8; 32]),
1042            document_type_name: "niceDocument".to_string(),
1043            data_contract_id: contract.id(),
1044            identity_contract_nonce: 0,
1045            token_payment_info: None,
1046        });
1047
1048        let transfer_v0 = DocumentTransferTransitionV0 {
1049            base: document_base.clone(),
1050            revision: 1_u64,
1051            recipient_owner_id: new_owner,
1052        };
1053        let transfer = DocumentTransition::Transfer(DocumentTransferTransition::V0(transfer_v0));
1054
1055        // First check should pass without needing original
1056        assert_eq!(
1057            filter.matches_document_transition(&transfer, None),
1058            TransitionCheckResult::Pass
1059        );
1060
1061        // Mismatch owner
1062        let other_owner = Identifier::from([6u8; 32]);
1063        let transfer_v0_mismatch = DocumentTransferTransitionV0 {
1064            base: document_base,
1065            revision: 1_u64,
1066            recipient_owner_id: other_owner,
1067        };
1068        let transfer_mismatch =
1069            DocumentTransition::Transfer(DocumentTransferTransition::V0(transfer_v0_mismatch));
1070        assert_eq!(
1071            filter.matches_document_transition(&transfer_mismatch, None),
1072            TransitionCheckResult::Fail
1073        );
1074    }
1075
1076    #[test]
1077    fn test_purchase_owner_clause_only_matches_and_requires_owner_context() {
1078        use dpp::fee::Credits;
1079        use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::DocumentPurchaseTransitionV0;
1080        use dpp::state_transition::batch_transition::batched_transition::document_purchase_transition::DocumentPurchaseTransition;
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 purchaser = Identifier::from([7u8; 32]);
1086
1087        // Filter checks batch owner (purchaser)
1088        let filter = DriveDocumentQueryFilter {
1089            contract: &contract,
1090            document_type_name: "niceDocument".to_string(),
1091            action_clauses: DocumentActionMatchClauses::Purchase {
1092                original_document_clauses: InternalClauses::default(),
1093                owner_clause: Some(ValueClause {
1094                    operator: WhereOperator::Equal,
1095                    value: purchaser.into(),
1096                }),
1097            },
1098        };
1099
1100        // Purchase transition
1101        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1102            id: Identifier::from([4u8; 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 purchase_v0 = DocumentPurchaseTransitionV0 {
1110            base: document_base,
1111            revision: 1_u64,
1112            price: 10 as Credits,
1113        };
1114        let purchase = DocumentTransition::Purchase(DocumentPurchaseTransition::V0(purchase_v0));
1115
1116        // Without batch owner context, should fail (owner clause requires it)
1117        assert_eq!(
1118            filter.matches_document_transition(&purchase, None),
1119            TransitionCheckResult::Fail
1120        );
1121        // With batch owner context, should pass
1122        let owner_value = Value::Identifier(purchaser.to_buffer());
1123        assert_eq!(
1124            filter.matches_document_transition(&purchase, Some(&owner_value)),
1125            TransitionCheckResult::Pass
1126        );
1127    }
1128
1129    #[test]
1130    fn test_transfer_original_clause_only_matches_with_original_document() {
1131        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1132        let contract = fixture.data_contract_owned();
1133
1134        // Filter checks only original document field
1135        let mut eq = BTreeMap::new();
1136        eq.insert(
1137            "status".to_string(),
1138            WhereClause {
1139                field: "status".to_string(),
1140                operator: WhereOperator::Equal,
1141                value: Value::Text("active".to_string()),
1142            },
1143        );
1144        let filter = DriveDocumentQueryFilter {
1145            contract: &contract,
1146            document_type_name: "niceDocument".to_string(),
1147            action_clauses: DocumentActionMatchClauses::Transfer {
1148                original_document_clauses: InternalClauses {
1149                    equal_clauses: eq,
1150                    ..Default::default()
1151                },
1152                owner_clause: None,
1153            },
1154        };
1155
1156        // Original doc present and matching
1157        let mut original = BTreeMap::new();
1158        original.insert("status".to_string(), Value::Text("active".to_string()));
1159        let original_doc = Document::V0(DocumentV0 {
1160            id: Identifier::from([9u8; 32]),
1161            owner_id: Identifier::from([0u8; 32]),
1162            properties: original,
1163            ..Default::default()
1164        });
1165        assert!(filter.matches_original_document(&original_doc));
1166
1167        // Without original doc, clause is required -> no match
1168        // No call without original: first pass already signaled it is required
1169    }
1170
1171    #[test]
1172    fn test_delete_original_clause_only_matches_with_original_document() {
1173        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1174        let contract = fixture.data_contract_owned();
1175
1176        // Filter checks only original document field
1177        let mut eq = BTreeMap::new();
1178        eq.insert(
1179            "status".to_string(),
1180            WhereClause {
1181                field: "status".to_string(),
1182                operator: WhereOperator::Equal,
1183                value: Value::Text("active".to_string()),
1184            },
1185        );
1186        let filter = DriveDocumentQueryFilter {
1187            contract: &contract,
1188            document_type_name: "niceDocument".to_string(),
1189            action_clauses: DocumentActionMatchClauses::Delete {
1190                original_document_clauses: InternalClauses {
1191                    equal_clauses: eq,
1192                    ..Default::default()
1193                },
1194            },
1195        };
1196
1197        // Original doc present and matching
1198        let mut original = BTreeMap::new();
1199        original.insert("status".to_string(), Value::Text("active".to_string()));
1200        let original_doc = Document::V0(DocumentV0 {
1201            id: Identifier::from([12u8; 32]),
1202            owner_id: Identifier::from([0u8; 32]),
1203            properties: original,
1204            ..Default::default()
1205        });
1206        assert!(filter.matches_original_document(&original_doc));
1207
1208        // Without original doc -> no match (required for Delete)
1209        // No call without original: first pass already signaled it is required
1210
1211        // Original mismatching -> no match
1212        let mut original_bad = BTreeMap::new();
1213        original_bad.insert("status".to_string(), Value::Text("inactive".to_string()));
1214        let original_doc_bad = Document::V0(DocumentV0 {
1215            id: Identifier::from([12u8; 32]),
1216            owner_id: Identifier::from([0u8; 32]),
1217            properties: original_bad,
1218            ..Default::default()
1219        });
1220        assert!(!filter.matches_original_document(&original_doc_bad));
1221    }
1222
1223    #[test]
1224    fn test_update_price_price_clause_only_matches_and_with_original_clause() {
1225        use dpp::fee::Credits;
1226        use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::v0::DocumentUpdatePriceTransitionV0;
1227        use dpp::state_transition::batch_transition::batched_transition::document_update_price_transition::DocumentUpdatePriceTransition;
1228
1229        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1230        let contract = fixture.data_contract_owned();
1231
1232        let base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1233            id: Identifier::from([10u8; 32]),
1234            document_type_name: "niceDocument".to_string(),
1235            data_contract_id: contract.id(),
1236            identity_contract_nonce: 0,
1237            token_payment_info: None,
1238        });
1239
1240        // Price-only clause
1241        let update_v0 = DocumentUpdatePriceTransitionV0 {
1242            base: base.clone(),
1243            revision: 1,
1244            price: 10 as Credits,
1245        };
1246        let update = DocumentTransition::UpdatePrice(DocumentUpdatePriceTransition::V0(update_v0));
1247
1248        let filter_price_only = DriveDocumentQueryFilter {
1249            contract: &contract,
1250            document_type_name: "niceDocument".to_string(),
1251            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1252                original_document_clauses: InternalClauses::default(),
1253                price_clause: Some(ValueClause {
1254                    operator: WhereOperator::GreaterThan,
1255                    value: Value::U64(5),
1256                }),
1257            },
1258        };
1259        // Price-only clause is decided in first check
1260        assert_eq!(
1261            filter_price_only.matches_document_transition(&update, None),
1262            TransitionCheckResult::Pass
1263        );
1264
1265        let filter_price_only_fail = DriveDocumentQueryFilter {
1266            contract: &contract,
1267            document_type_name: "niceDocument".to_string(),
1268            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1269                original_document_clauses: InternalClauses::default(),
1270                price_clause: Some(ValueClause {
1271                    operator: WhereOperator::GreaterThan,
1272                    value: Value::U64(15),
1273                }),
1274            },
1275        };
1276        assert_eq!(
1277            filter_price_only_fail.matches_document_transition(&update, None),
1278            TransitionCheckResult::Fail
1279        );
1280
1281        // With original clauses as well
1282        let mut eq = BTreeMap::new();
1283        eq.insert(
1284            "kind".to_string(),
1285            WhereClause {
1286                field: "kind".to_string(),
1287                operator: WhereOperator::Equal,
1288                value: Value::Text("sale".to_string()),
1289            },
1290        );
1291        let filter_with_orig = DriveDocumentQueryFilter {
1292            contract: &contract,
1293            document_type_name: "niceDocument".to_string(),
1294            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1295                original_document_clauses: InternalClauses {
1296                    equal_clauses: eq,
1297                    ..Default::default()
1298                },
1299                price_clause: Some(ValueClause {
1300                    operator: WhereOperator::GreaterThanOrEquals,
1301                    value: Value::U64(10),
1302                }),
1303            },
1304        };
1305        let mut original_doc = BTreeMap::new();
1306        original_doc.insert("kind".to_string(), Value::Text("sale".to_string()));
1307        assert_eq!(
1308            filter_with_orig.matches_document_transition(&update, None),
1309            TransitionCheckResult::NeedsOriginal
1310        );
1311        let original_document = Document::V0(DocumentV0 {
1312            id: Identifier::from([10u8; 32]),
1313            owner_id: Identifier::from([0u8; 32]),
1314            properties: original_doc,
1315            ..Default::default()
1316        });
1317        assert!(filter_with_orig.matches_original_document(&original_document));
1318
1319        // Missing original doc -> required -> no match
1320        // No call without original: first pass already signaled it is required
1321    }
1322
1323    #[test]
1324    fn test_replace_with_both_original_and_new_document_clauses() {
1325        use dpp::state_transition::batch_transition::batched_transition::document_replace_transition::v0::DocumentReplaceTransitionV0;
1326        use dpp::state_transition::batch_transition::batched_transition::document_replace_transition::DocumentReplaceTransition;
1327
1328        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1329        let contract = fixture.data_contract_owned();
1330
1331        let base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1332            id: Identifier::from([11u8; 32]),
1333            document_type_name: "niceDocument".to_string(),
1334            data_contract_id: contract.id(),
1335            identity_contract_nonce: 0,
1336            token_payment_info: None,
1337        });
1338
1339        // Original must have status=active; New must have score=10
1340        let mut orig_eq = BTreeMap::new();
1341        orig_eq.insert(
1342            "status".to_string(),
1343            WhereClause {
1344                field: "status".to_string(),
1345                operator: WhereOperator::Equal,
1346                value: Value::Text("active".to_string()),
1347            },
1348        );
1349        let original_clauses = InternalClauses {
1350            equal_clauses: orig_eq,
1351            ..Default::default()
1352        };
1353
1354        let mut final_eq = BTreeMap::new();
1355        final_eq.insert(
1356            "score".to_string(),
1357            WhereClause {
1358                field: "score".to_string(),
1359                operator: WhereOperator::Equal,
1360                value: Value::U64(10),
1361            },
1362        );
1363        let new_document_clauses = InternalClauses {
1364            equal_clauses: final_eq,
1365            ..Default::default()
1366        };
1367
1368        let filter = DriveDocumentQueryFilter {
1369            contract: &contract,
1370            document_type_name: "niceDocument".to_string(),
1371            action_clauses: DocumentActionMatchClauses::Replace {
1372                original_document_clauses: original_clauses,
1373                new_document_clauses,
1374            },
1375        };
1376
1377        // Build Replace transition with new data
1378        let mut data = BTreeMap::new();
1379        data.insert("score".to_string(), Value::U64(10));
1380        let replace_v0 = DocumentReplaceTransitionV0 {
1381            base,
1382            revision: 1,
1383            data,
1384        };
1385        let replace = DocumentTransition::Replace(DocumentReplaceTransition::V0(replace_v0));
1386
1387        // Original provided and matching; final matches (requires original)
1388        let mut original_doc = BTreeMap::new();
1389        original_doc.insert("status".to_string(), Value::Text("active".to_string()));
1390        assert_eq!(
1391            filter.matches_document_transition(&replace, None),
1392            TransitionCheckResult::NeedsOriginal
1393        );
1394        let original_document = Document::V0(DocumentV0 {
1395            id: Identifier::from([11u8; 32]),
1396            owner_id: Identifier::from([0u8; 32]),
1397            properties: original_doc,
1398            ..Default::default()
1399        });
1400        assert!(filter.matches_original_document(&original_document));
1401
1402        // Original missing -> should fail as it's required
1403        // No call without original: first pass already signaled it is required
1404
1405        // Original mismatching -> fail
1406        let mut original_doc_bad = BTreeMap::new();
1407        original_doc_bad.insert("status".to_string(), Value::Text("inactive".to_string()));
1408        let original_document_bad = Document::V0(DocumentV0 {
1409            id: Identifier::from([11u8; 32]),
1410            owner_id: Identifier::from([0u8; 32]),
1411            properties: original_doc_bad,
1412            ..Default::default()
1413        });
1414        assert!(!filter.matches_original_document(&original_document_bad));
1415
1416        // New-data mismatching should fail in first check (do not call final)
1417        if let DocumentTransition::Replace(mut rep) = replace.clone() {
1418            let DocumentReplaceTransition::V0(ref mut v0) = rep;
1419            v0.data.insert("score".to_string(), Value::U64(9));
1420            let bad_final = DocumentTransition::Replace(rep);
1421            assert_eq!(
1422                filter.matches_document_transition(&bad_final, None),
1423                TransitionCheckResult::Fail
1424            );
1425        }
1426    }
1427
1428    #[test]
1429    fn test_matches_document_with_between_operator() {
1430        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1431        let contract = fixture.data_contract_owned();
1432
1433        let internal_clauses = InternalClauses {
1434            range_clause: Some(WhereClause {
1435                field: "value".to_string(),
1436                operator: WhereOperator::Between,
1437                value: Value::Array(vec![Value::U64(10), Value::U64(20)]),
1438            }),
1439            ..Default::default()
1440        };
1441
1442        let filter = DriveDocumentQueryFilter {
1443            contract: &contract,
1444            document_type_name: "niceDocument".to_string(),
1445            action_clauses: DocumentActionMatchClauses::Create {
1446                new_document_clauses: internal_clauses.clone(),
1447            },
1448        };
1449
1450        let document_base = DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
1451            id: Identifier::from([3u8; 32]),
1452            document_type_name: "niceDocument".to_string(),
1453            data_contract_id: contract.id(),
1454            identity_contract_nonce: 0,
1455            token_payment_info: None,
1456        });
1457
1458        // Test value in range
1459        let mut in_range = BTreeMap::new();
1460        in_range.insert("value".to_string(), Value::U64(15));
1461        let id_value: Value = document_base.id().into();
1462        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &in_range));
1463
1464        // Test lower bound (inclusive)
1465        let mut lower_bound = BTreeMap::new();
1466        lower_bound.insert("value".to_string(), Value::U64(10));
1467        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &lower_bound));
1468
1469        // Test upper bound (inclusive)
1470        let mut upper_bound = BTreeMap::new();
1471        upper_bound.insert("value".to_string(), Value::U64(20));
1472        assert!(filter.evaluate_clauses(&internal_clauses, &id_value, &upper_bound));
1473
1474        // Test below range
1475        let mut below = BTreeMap::new();
1476        below.insert("value".to_string(), Value::U64(5));
1477        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &below));
1478
1479        // Test above range
1480        let mut above = BTreeMap::new();
1481        above.insert("value".to_string(), Value::U64(25));
1482        assert!(!filter.evaluate_clauses(&internal_clauses, &id_value, &above));
1483    }
1484
1485    #[test]
1486    fn test_validate_filter() {
1487        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1488        let contract = fixture.data_contract_owned();
1489
1490        // Test valid filter with indexed field
1491        let mut equal_clauses = BTreeMap::new();
1492        equal_clauses.insert(
1493            "firstName".to_string(),
1494            WhereClause {
1495                field: "firstName".to_string(),
1496                operator: WhereOperator::Equal,
1497                value: Value::Text("Alice".to_string()),
1498            },
1499        );
1500        let internal_clauses = InternalClauses {
1501            equal_clauses,
1502            ..Default::default()
1503        };
1504
1505        let valid_filter = DriveDocumentQueryFilter {
1506            contract: &contract,
1507            document_type_name: "indexedDocument".to_string(),
1508            action_clauses: DocumentActionMatchClauses::Create {
1509                new_document_clauses: internal_clauses,
1510            },
1511        };
1512
1513        assert!(
1514            valid_filter.validate().is_valid(),
1515            "Filter with indexed field should be valid"
1516        );
1517
1518        // Test filter with non-indexed field: structural validation should pass
1519        // (indexes are not considered by subscription filters).
1520        let mut equal_clauses = BTreeMap::new();
1521        equal_clauses.insert(
1522            "name".to_string(),
1523            WhereClause {
1524                field: "name".to_string(),
1525                operator: WhereOperator::Equal,
1526                value: Value::Text("value".to_string()),
1527            },
1528        );
1529        let internal_clauses = InternalClauses {
1530            equal_clauses,
1531            ..Default::default()
1532        };
1533
1534        let invalid_filter = DriveDocumentQueryFilter {
1535            contract: &contract,
1536            document_type_name: "niceDocument".to_string(),
1537            action_clauses: DocumentActionMatchClauses::Create {
1538                new_document_clauses: internal_clauses,
1539            },
1540        };
1541
1542        assert!(
1543            invalid_filter.validate().is_valid(),
1544            "Structural validate should ignore indexes"
1545        );
1546        // Index-aware validation removed; structural validation suffices for subscriptions.
1547
1548        // Test valid filter with only primary key
1549        let internal_clauses = InternalClauses {
1550            primary_key_equal_clause: Some(WhereClause {
1551                field: "$id".to_string(),
1552                operator: WhereOperator::Equal,
1553                value: Value::Identifier([42u8; 32]),
1554            }),
1555            ..Default::default()
1556        };
1557
1558        let primary_key_filter = DriveDocumentQueryFilter {
1559            contract: &contract,
1560            document_type_name: "indexedDocument".to_string(),
1561            action_clauses: DocumentActionMatchClauses::Create {
1562                new_document_clauses: internal_clauses,
1563            },
1564        };
1565
1566        assert!(
1567            primary_key_filter.validate().is_valid(),
1568            "Filter with only primary key should be valid"
1569        );
1570    }
1571
1572    #[test]
1573    fn test_validate_rejects_id_in_generic_clauses() {
1574        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1575        let contract = fixture.data_contract_owned();
1576
1577        // $id in equal_clauses should be rejected
1578        let mut eq = BTreeMap::new();
1579        eq.insert(
1580            "$id".to_string(),
1581            WhereClause {
1582                field: "$id".to_string(),
1583                operator: WhereOperator::Equal,
1584                value: Value::Identifier([1u8; 32]),
1585            },
1586        );
1587        let filter = DriveDocumentQueryFilter {
1588            contract: &contract,
1589            document_type_name: "niceDocument".to_string(),
1590            action_clauses: DocumentActionMatchClauses::Create {
1591                new_document_clauses: InternalClauses {
1592                    equal_clauses: eq,
1593                    ..Default::default()
1594                },
1595            },
1596        };
1597        assert!(filter.validate().is_err());
1598
1599        // $id in range clause should be rejected
1600        let filter = DriveDocumentQueryFilter {
1601            contract: &contract,
1602            document_type_name: "niceDocument".to_string(),
1603            action_clauses: DocumentActionMatchClauses::Create {
1604                new_document_clauses: InternalClauses {
1605                    range_clause: Some(WhereClause {
1606                        field: "$id".to_string(),
1607                        operator: WhereOperator::GreaterThan,
1608                        value: Value::U64(0),
1609                    }),
1610                    ..Default::default()
1611                },
1612            },
1613        };
1614        assert!(filter.validate().is_err());
1615    }
1616
1617    #[test]
1618    fn test_validate_owner_and_price_clause_types() {
1619        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1620        let contract = fixture.data_contract_owned();
1621
1622        // Owner clause must be Identifier
1623        let filter = DriveDocumentQueryFilter {
1624            contract: &contract,
1625            document_type_name: "niceDocument".to_string(),
1626            action_clauses: DocumentActionMatchClauses::Transfer {
1627                original_document_clauses: InternalClauses::default(),
1628                owner_clause: Some(ValueClause {
1629                    operator: WhereOperator::Equal,
1630                    value: Value::Text("not-id".to_string()),
1631                }),
1632            },
1633        };
1634        assert!(filter.validate().is_err());
1635
1636        // Price clause must be integer-like, not float
1637        let filter = DriveDocumentQueryFilter {
1638            contract: &contract,
1639            document_type_name: "niceDocument".to_string(),
1640            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1641                original_document_clauses: InternalClauses::default(),
1642                price_clause: Some(ValueClause {
1643                    operator: WhereOperator::Equal,
1644                    value: Value::Float(1.23),
1645                }),
1646            },
1647        };
1648        assert!(filter.validate().is_err());
1649
1650        // Price Between must be 2 integer-like values
1651        let filter = DriveDocumentQueryFilter {
1652            contract: &contract,
1653            document_type_name: "niceDocument".to_string(),
1654            action_clauses: DocumentActionMatchClauses::UpdatePrice {
1655                original_document_clauses: InternalClauses::default(),
1656                price_clause: Some(ValueClause {
1657                    operator: WhereOperator::Between,
1658                    value: Value::Array(vec![Value::U64(1), Value::Float(2.0)]),
1659                }),
1660            },
1661        };
1662        assert!(filter.validate().is_err());
1663
1664        // Price Between variants must reject equal bounds
1665        for operator in [
1666            WhereOperator::Between,
1667            WhereOperator::BetweenExcludeBounds,
1668            WhereOperator::BetweenExcludeLeft,
1669            WhereOperator::BetweenExcludeRight,
1670        ] {
1671            let filter = DriveDocumentQueryFilter {
1672                contract: &contract,
1673                document_type_name: "niceDocument".to_string(),
1674                action_clauses: DocumentActionMatchClauses::UpdatePrice {
1675                    original_document_clauses: InternalClauses::default(),
1676                    price_clause: Some(ValueClause {
1677                        operator,
1678                        value: Value::Array(vec![Value::U64(10), Value::U64(10)]),
1679                    }),
1680                },
1681            };
1682            assert!(
1683                filter.validate().is_err(),
1684                "{operator:?} should reject equal price bounds"
1685            );
1686        }
1687    }
1688
1689    #[test]
1690    fn test_validate_startswith_on_numeric_field_rejected() {
1691        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1692        let contract = fixture.data_contract_owned();
1693
1694        // numeric field 'score' with StartsWith should be rejected
1695        let filter = DriveDocumentQueryFilter {
1696            contract: &contract,
1697            document_type_name: "niceDocument".to_string(),
1698            action_clauses: DocumentActionMatchClauses::Create {
1699                new_document_clauses: InternalClauses {
1700                    range_clause: Some(WhereClause {
1701                        field: "score".to_string(),
1702                        operator: WhereOperator::StartsWith,
1703                        value: Value::Text("1".to_string()),
1704                    }),
1705                    ..Default::default()
1706                },
1707            },
1708        };
1709        assert!(filter.validate().is_err());
1710    }
1711
1712    #[test]
1713    fn test_conversion_between_filter_and_query() {
1714        let fixture = get_data_contract_fixture(None, 0, LATEST_PLATFORM_VERSION.protocol_version);
1715        let contract = fixture.data_contract_owned();
1716
1717        let internal_clauses = InternalClauses {
1718            primary_key_equal_clause: Some(WhereClause {
1719                field: "$id".to_string(),
1720                operator: WhereOperator::Equal,
1721                value: Value::Identifier([42u8; 32]),
1722            }),
1723            ..Default::default()
1724        };
1725
1726        let original_filter = DriveDocumentQueryFilter {
1727            contract: &contract,
1728            document_type_name: "niceDocument".to_string(),
1729            action_clauses: DocumentActionMatchClauses::Create {
1730                new_document_clauses: internal_clauses.clone(),
1731            },
1732        };
1733
1734        // No conversion helpers; verify the filter holds the expected clauses
1735        if let DocumentActionMatchClauses::Create {
1736            new_document_clauses,
1737        } = original_filter.action_clauses
1738        {
1739            assert_eq!(new_document_clauses, internal_clauses);
1740        } else {
1741            panic!("expected Create action clauses");
1742        }
1743    }
1744}