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