drive/query/
vote_polls_by_end_date_query.rs

1use crate::drive::votes::paths::vote_end_date_queries_tree_path_vec;
2#[cfg(feature = "server")]
3use crate::drive::Drive;
4#[cfg(feature = "server")]
5use crate::error::drive::DriveError;
6#[cfg(feature = "server")]
7use crate::error::Error;
8#[cfg(feature = "server")]
9use crate::fees::op::LowLevelDriveOperation;
10#[cfg(feature = "server")]
11use crate::query::GroveError;
12use crate::query::Query;
13#[cfg(feature = "server")]
14use crate::util::common::encode::decode_u64;
15use crate::util::common::encode::encode_u64;
16use bincode::{Decode, Encode};
17#[cfg(feature = "server")]
18use dpp::block::block_info::BlockInfo;
19#[cfg(feature = "server")]
20use dpp::fee::Credits;
21use dpp::prelude::{TimestampIncluded, TimestampMillis};
22#[cfg(feature = "server")]
23use dpp::serialization::PlatformDeserializable;
24#[cfg(feature = "server")]
25use dpp::voting::vote_polls::VotePoll;
26#[cfg(feature = "server")]
27use grovedb::query_result_type::{QueryResultElements, QueryResultType};
28#[cfg(feature = "server")]
29use grovedb::TransactionArg;
30use grovedb::{PathQuery, SizedQuery};
31#[cfg(feature = "server")]
32use platform_version::version::PlatformVersion;
33#[cfg(feature = "server")]
34use std::collections::BTreeMap;
35
36/// Vote Poll Drive Query struct
37#[derive(Debug, PartialEq, Clone, Encode, Decode)]
38pub struct VotePollsByEndDateDriveQuery {
39    /// What is the start time we are asking for
40    pub start_time: Option<(TimestampMillis, TimestampIncluded)>,
41    /// What vote poll are we asking for?
42    pub end_time: Option<(TimestampMillis, TimestampIncluded)>,
43    /// Limit
44    pub limit: Option<u16>,
45    /// Offset
46    pub offset: Option<u16>,
47    /// Ascending
48    pub order_ascending: bool,
49}
50
51impl VotePollsByEndDateDriveQuery {
52    /// Get the path query for an abci query that gets vote polls until an end time
53    pub fn path_query_for_end_time_included(end_time: TimestampMillis, limit: u16) -> PathQuery {
54        let path = vote_end_date_queries_tree_path_vec();
55
56        let mut query = Query::new_with_direction(true);
57
58        let encoded_time = encode_u64(end_time);
59
60        query.insert_range_to_inclusive(..=encoded_time);
61
62        let mut sub_query = Query::new();
63
64        sub_query.insert_all();
65
66        query.default_subquery_branch.subquery = Some(sub_query.into());
67
68        PathQuery {
69            path,
70            query: SizedQuery {
71                query,
72                limit: Some(limit),
73                offset: None,
74            },
75        }
76    }
77
78    /// Get the path query for an abci query that gets vote polls at an the end time
79    pub fn path_query_for_single_end_time(end_time: TimestampMillis, limit: u16) -> PathQuery {
80        let path = vote_end_date_queries_tree_path_vec();
81
82        let mut query = Query::new_with_direction(true);
83
84        let encoded_time = encode_u64(end_time);
85
86        query.insert_key(encoded_time);
87
88        let mut sub_query = Query::new();
89
90        sub_query.insert_all();
91
92        query.default_subquery_branch.subquery = Some(sub_query.into());
93
94        PathQuery {
95            path,
96            query: SizedQuery {
97                query,
98                limit: Some(limit),
99                offset: None,
100            },
101        }
102    }
103
104    #[cfg(feature = "server")]
105    /// Executes a special query with no proof to get contested document resource vote polls.
106    /// This is meant for platform abci to get votes that have finished
107    pub fn execute_no_proof_for_specialized_end_time_query(
108        end_time: TimestampMillis,
109        limit: u16,
110        drive: &Drive,
111        transaction: TransactionArg,
112        drive_operations: &mut Vec<LowLevelDriveOperation>,
113        platform_version: &PlatformVersion,
114    ) -> Result<BTreeMap<TimestampMillis, Vec<VotePoll>>, Error> {
115        let path_query = Self::path_query_for_end_time_included(end_time, limit);
116        let query_result = drive.grove_get_path_query(
117            &path_query,
118            transaction,
119            QueryResultType::QueryPathKeyElementTrioResultType,
120            drive_operations,
121            &platform_version.drive,
122        );
123        match query_result {
124            Err(Error::GroveDB(e))
125                if matches!(
126                    e.as_ref(),
127                    GroveError::PathKeyNotFound(_)
128                        | GroveError::PathNotFound(_)
129                        | GroveError::PathParentLayerNotFound(_)
130                ) =>
131            {
132                Ok(BTreeMap::new())
133            }
134            Err(e) => Err(e),
135            Ok((query_result_elements, _)) => {
136                let vote_polls_by_end_date = query_result_elements
137                    .to_path_key_elements()
138                    .into_iter()
139                    .map(|(path, _, element)| {
140                        let Some(last_path_component) = path.last() else {
141                            return Err(Error::Drive(DriveError::CorruptedDriveState(
142                                "we should always have a path not be null".to_string(),
143                            )));
144                        };
145                        let timestamp = decode_u64(last_path_component)?;
146                        let contested_document_resource_vote_poll_bytes =
147                            element.into_item_bytes().map_err(Error::from)?;
148                        let vote_poll = VotePoll::deserialize_from_bytes(
149                            &contested_document_resource_vote_poll_bytes,
150                        )?;
151                        Ok((timestamp, vote_poll))
152                    })
153                    .collect::<Result<Vec<_>, Error>>()?
154                    .into_iter()
155                    .fold(
156                        BTreeMap::new(),
157                        |mut acc: BTreeMap<u64, Vec<VotePoll>>, (timestamp, vote_poll)| {
158                            acc.entry(timestamp).or_default().push(vote_poll);
159                            acc
160                        },
161                    );
162                Ok(vote_polls_by_end_date)
163            }
164        }
165    }
166
167    #[cfg(feature = "server")]
168    /// Executes a special query with no proof to get contested document resource vote polls.
169    /// This is meant for platform abci to get votes that have finished
170    pub fn execute_no_proof_for_specialized_end_time_query_only_check_end_time(
171        end_time: TimestampMillis,
172        limit: u16,
173        drive: &Drive,
174        transaction: TransactionArg,
175        drive_operations: &mut Vec<LowLevelDriveOperation>,
176        platform_version: &PlatformVersion,
177    ) -> Result<Vec<VotePoll>, Error> {
178        let path_query = Self::path_query_for_single_end_time(end_time, limit);
179        let query_result = drive.grove_get_path_query(
180            &path_query,
181            transaction,
182            QueryResultType::QueryPathKeyElementTrioResultType,
183            drive_operations,
184            &platform_version.drive,
185        );
186        match query_result {
187            Err(Error::GroveDB(e))
188                if matches!(
189                    e.as_ref(),
190                    GroveError::PathKeyNotFound(_)
191                        | GroveError::PathNotFound(_)
192                        | GroveError::PathParentLayerNotFound(_)
193                ) =>
194            {
195                Ok(vec![])
196            }
197            Err(e) => Err(e),
198            Ok((query_result_elements, _)) => {
199                // Process the query result elements and collect VotePolls
200                let vote_polls = query_result_elements
201                    .to_path_key_elements()
202                    .into_iter()
203                    .map(|(_, _, element)| {
204                        // Extract the bytes from the element
205                        let vote_poll_bytes = element.into_item_bytes().map_err(Error::from)?;
206                        // Deserialize the bytes into a VotePoll
207                        let vote_poll = VotePoll::deserialize_from_bytes(&vote_poll_bytes)?;
208                        Ok(vote_poll)
209                    })
210                    .collect::<Result<Vec<_>, Error>>()?;
211                Ok(vote_polls)
212            }
213        }
214    }
215
216    /// Operations to construct a path query.
217    pub fn construct_path_query(&self) -> PathQuery {
218        let path = vote_end_date_queries_tree_path_vec();
219
220        let mut query = Query::new_with_direction(self.order_ascending);
221
222        // this is a range on all elements
223        match &(self.start_time, self.end_time) {
224            (None, None) => {
225                query.insert_all();
226            }
227            (Some((starts_at_key_bytes, start_at_included)), None) => {
228                let starts_at_key = encode_u64(*starts_at_key_bytes);
229                match start_at_included {
230                    true => query.insert_range_from(starts_at_key..),
231                    false => query.insert_range_after(starts_at_key..),
232                }
233            }
234            (None, Some((ends_at_key_bytes, ends_at_included))) => {
235                let ends_at_key = encode_u64(*ends_at_key_bytes);
236                match ends_at_included {
237                    true => query.insert_range_to_inclusive(..=ends_at_key),
238                    false => query.insert_range_to(..ends_at_key),
239                }
240            }
241            (
242                Some((starts_at_key_bytes, start_at_included)),
243                Some((ends_at_key_bytes, ends_at_included)),
244            ) => {
245                let starts_at_key = encode_u64(*starts_at_key_bytes);
246                let ends_at_key = encode_u64(*ends_at_key_bytes);
247                match (start_at_included, ends_at_included) {
248                    (true, true) => query.insert_range_inclusive(starts_at_key..=ends_at_key),
249                    (true, false) => query.insert_range(starts_at_key..ends_at_key),
250                    (false, true) => {
251                        query.insert_range_after_to_inclusive(starts_at_key..=ends_at_key)
252                    }
253                    (false, false) => query.insert_range_after_to(starts_at_key..ends_at_key),
254                }
255            }
256        }
257
258        let mut sub_query = Query::new();
259
260        sub_query.insert_all();
261
262        query.default_subquery_branch.subquery = Some(sub_query.into());
263
264        PathQuery {
265            path,
266            query: SizedQuery {
267                query,
268                limit: self.limit,
269                offset: None,
270            },
271        }
272    }
273    #[cfg(feature = "server")]
274    /// Executes a query with proof and returns the items and fee.
275    pub fn execute_with_proof(
276        self,
277        drive: &Drive,
278        block_info: Option<BlockInfo>,
279        transaction: TransactionArg,
280        platform_version: &PlatformVersion,
281    ) -> Result<(Vec<u8>, u64), Error> {
282        let mut drive_operations = vec![];
283        let items = self.execute_with_proof_internal(
284            drive,
285            transaction,
286            &mut drive_operations,
287            platform_version,
288        )?;
289        let cost = if let Some(block_info) = block_info {
290            let fee_result = Drive::calculate_fee(
291                None,
292                Some(drive_operations),
293                &block_info.epoch,
294                drive.config.epochs_per_era,
295                platform_version,
296                None,
297            )?;
298            fee_result.processing_fee
299        } else {
300            0
301        };
302        Ok((items, cost))
303    }
304
305    #[cfg(feature = "server")]
306    /// Executes an internal query with proof and returns the items.
307    pub(crate) fn execute_with_proof_internal(
308        self,
309        drive: &Drive,
310        transaction: TransactionArg,
311        drive_operations: &mut Vec<LowLevelDriveOperation>,
312        platform_version: &PlatformVersion,
313    ) -> Result<Vec<u8>, Error> {
314        let path_query = self.construct_path_query();
315        drive.grove_get_proved_path_query(
316            &path_query,
317            transaction,
318            drive_operations,
319            &platform_version.drive,
320        )
321    }
322    #[cfg(feature = "server")]
323    /// Executes a query with no proof and returns the items, skipped items, and fee.
324    pub fn execute_no_proof_with_cost(
325        &self,
326        drive: &Drive,
327        block_info: Option<BlockInfo>,
328        transaction: TransactionArg,
329        platform_version: &PlatformVersion,
330    ) -> Result<(BTreeMap<TimestampMillis, Vec<VotePoll>>, Credits), Error> {
331        let mut drive_operations = vec![];
332        let result =
333            self.execute_no_proof(drive, transaction, &mut drive_operations, platform_version)?;
334        let cost = if let Some(block_info) = block_info {
335            let fee_result = Drive::calculate_fee(
336                None,
337                Some(drive_operations),
338                &block_info.epoch,
339                drive.config.epochs_per_era,
340                platform_version,
341                None,
342            )?;
343            fee_result.processing_fee
344        } else {
345            0
346        };
347        Ok((result, cost))
348    }
349
350    #[cfg(feature = "server")]
351    /// Executes an internal query with no proof and returns the values and skipped items.
352    pub fn execute_no_proof(
353        &self,
354        drive: &Drive,
355        transaction: TransactionArg,
356        drive_operations: &mut Vec<LowLevelDriveOperation>,
357        platform_version: &PlatformVersion,
358    ) -> Result<BTreeMap<TimestampMillis, Vec<VotePoll>>, Error> {
359        let path_query = self.construct_path_query();
360        let query_result = drive.grove_get_path_query(
361            &path_query,
362            transaction,
363            QueryResultType::QueryPathKeyElementTrioResultType,
364            drive_operations,
365            &platform_version.drive,
366        );
367        match query_result {
368            Err(Error::GroveDB(e))
369                if matches!(
370                    e.as_ref(),
371                    GroveError::PathKeyNotFound(_)
372                        | GroveError::PathNotFound(_)
373                        | GroveError::PathParentLayerNotFound(_)
374                ) =>
375            {
376                Ok(BTreeMap::new())
377            }
378            Err(e) => Err(e),
379            Ok((query_result_elements, _)) => {
380                let vote_polls_by_end_date = query_result_elements
381                    .to_path_key_elements()
382                    .into_iter()
383                    .map(|(path, _, element)| {
384                        let Some(last_path_component) = path.last() else {
385                            return Err(Error::Drive(DriveError::CorruptedDriveState(
386                                "we should always have a path not be null".to_string(),
387                            )));
388                        };
389                        let timestamp = decode_u64(last_path_component)?;
390                        let contested_document_resource_vote_poll_bytes =
391                            element.into_item_bytes().map_err(Error::from)?;
392                        let vote_poll = VotePoll::deserialize_from_bytes(
393                            &contested_document_resource_vote_poll_bytes,
394                        )?;
395                        Ok((timestamp, vote_poll))
396                    })
397                    .collect::<Result<Vec<_>, Error>>()?
398                    .into_iter()
399                    .fold(
400                        BTreeMap::new(),
401                        |mut acc: BTreeMap<u64, Vec<VotePoll>>, (timestamp, vote_poll)| {
402                            acc.entry(timestamp).or_default().push(vote_poll);
403                            acc
404                        },
405                    );
406                Ok(vote_polls_by_end_date)
407            }
408        }
409    }
410
411    #[cfg(feature = "server")]
412    /// Executes an internal query with no proof and returns the values and skipped items.
413    pub fn execute_no_proof_keep_serialized(
414        &self,
415        drive: &Drive,
416        transaction: TransactionArg,
417        drive_operations: &mut Vec<LowLevelDriveOperation>,
418        platform_version: &PlatformVersion,
419    ) -> Result<BTreeMap<TimestampMillis, Vec<Vec<u8>>>, Error> {
420        let path_query = self.construct_path_query();
421        let query_result = drive.grove_get_path_query(
422            &path_query,
423            transaction,
424            QueryResultType::QueryPathKeyElementTrioResultType,
425            drive_operations,
426            &platform_version.drive,
427        );
428        match query_result {
429            Err(Error::GroveDB(e))
430                if matches!(
431                    e.as_ref(),
432                    GroveError::PathKeyNotFound(_)
433                        | GroveError::PathNotFound(_)
434                        | GroveError::PathParentLayerNotFound(_)
435                ) =>
436            {
437                Ok(BTreeMap::new())
438            }
439            Err(e) => Err(e),
440            Ok((query_result_elements, _)) => {
441                let vote_polls_by_end_date = query_result_elements
442                    .to_path_key_elements()
443                    .into_iter()
444                    .map(|(path, _, element)| {
445                        let Some(last_path_component) = path.last() else {
446                            return Err(Error::Drive(DriveError::CorruptedDriveState(
447                                "we should always have a path not be null".to_string(),
448                            )));
449                        };
450                        let timestamp = decode_u64(last_path_component)?;
451                        let contested_document_resource_vote_poll_bytes =
452                            element.into_item_bytes().map_err(Error::from)?;
453                        Ok((timestamp, contested_document_resource_vote_poll_bytes))
454                    })
455                    .collect::<Result<Vec<_>, Error>>()?
456                    .into_iter()
457                    .fold(
458                        BTreeMap::new(),
459                        |mut acc: BTreeMap<u64, Vec<Vec<u8>>>,
460                         (timestamp, vote_poll_serialized)| {
461                            acc.entry(timestamp).or_default().push(vote_poll_serialized);
462                            acc
463                        },
464                    );
465                Ok(vote_polls_by_end_date)
466            }
467        }
468    }
469
470    #[cfg(feature = "server")]
471    #[allow(unused)]
472    /// Executes an internal query with no proof and returns the values and skipped items.
473    pub(crate) fn execute_no_proof_internal(
474        &self,
475        drive: &Drive,
476        result_type: QueryResultType,
477        transaction: TransactionArg,
478        drive_operations: &mut Vec<LowLevelDriveOperation>,
479        platform_version: &PlatformVersion,
480    ) -> Result<QueryResultElements, Error> {
481        let path_query = self.construct_path_query();
482        let query_result = drive.grove_get_path_query(
483            &path_query,
484            transaction,
485            result_type,
486            drive_operations,
487            &platform_version.drive,
488        );
489        match query_result {
490            Err(Error::GroveDB(e))
491                if matches!(
492                    e.as_ref(),
493                    GroveError::PathKeyNotFound(_)
494                        | GroveError::PathNotFound(_)
495                        | GroveError::PathParentLayerNotFound(_)
496                ) =>
497            {
498                Ok(QueryResultElements::new())
499            }
500            _ => {
501                let (data, _) = query_result?;
502                {
503                    Ok(data)
504                }
505            }
506        }
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::drive::votes::paths::END_DATE_QUERIES_TREE_KEY;
514    use crate::drive::RootTree;
515    use grovedb::QueryItem;
516
517    fn expected_base_path() -> Vec<Vec<u8>> {
518        vec![
519            vec![RootTree::Votes as u8],
520            vec![END_DATE_QUERIES_TREE_KEY as u8],
521        ]
522    }
523
524    // -----------------------------------------------------------------------
525    // construct_path_query
526    // -----------------------------------------------------------------------
527
528    #[test]
529    fn construct_path_query_no_bounds_ascending() {
530        let query = VotePollsByEndDateDriveQuery {
531            start_time: None,
532            end_time: None,
533            limit: Some(10),
534            offset: None,
535            order_ascending: true,
536        };
537
538        let pq = query.construct_path_query();
539        assert_eq!(pq.path, expected_base_path());
540        assert_eq!(pq.query.limit, Some(10));
541        assert_eq!(pq.query.offset, None);
542
543        // Should be RangeFull (insert_all)
544        assert_eq!(pq.query.query.items.len(), 1);
545        assert!(matches!(&pq.query.query.items[0], QueryItem::RangeFull(..)));
546
547        // Direction should be ascending
548        assert!(pq.query.query.left_to_right);
549
550        // Should have a subquery for all items at each timestamp
551        assert!(pq.query.query.default_subquery_branch.subquery.is_some());
552    }
553
554    #[test]
555    fn construct_path_query_no_bounds_descending() {
556        let query = VotePollsByEndDateDriveQuery {
557            start_time: None,
558            end_time: None,
559            limit: None,
560            offset: None,
561            order_ascending: false,
562        };
563
564        let pq = query.construct_path_query();
565        assert!(!pq.query.query.left_to_right);
566        assert_eq!(pq.query.limit, None);
567    }
568
569    #[test]
570    fn construct_path_query_start_time_included() {
571        let query = VotePollsByEndDateDriveQuery {
572            start_time: Some((1000, true)),
573            end_time: None,
574            limit: Some(5),
575            offset: None,
576            order_ascending: true,
577        };
578
579        let pq = query.construct_path_query();
580        let items = &pq.query.query.items;
581        assert_eq!(items.len(), 1);
582        let encoded_1000 = encode_u64(1000);
583        assert!(
584            matches!(&items[0], QueryItem::RangeFrom(r) if r.start == encoded_1000),
585            "expected RangeFrom for included start time"
586        );
587    }
588
589    #[test]
590    fn construct_path_query_start_time_excluded() {
591        let query = VotePollsByEndDateDriveQuery {
592            start_time: Some((1000, false)),
593            end_time: None,
594            limit: Some(5),
595            offset: None,
596            order_ascending: true,
597        };
598
599        let pq = query.construct_path_query();
600        let items = &pq.query.query.items;
601        assert_eq!(items.len(), 1);
602        let encoded_1000 = encode_u64(1000);
603        assert!(
604            matches!(&items[0], QueryItem::RangeAfter(r) if r.start == encoded_1000),
605            "expected RangeAfter for excluded start time"
606        );
607    }
608
609    #[test]
610    fn construct_path_query_end_time_included() {
611        let query = VotePollsByEndDateDriveQuery {
612            start_time: None,
613            end_time: Some((2000, true)),
614            limit: Some(5),
615            offset: None,
616            order_ascending: true,
617        };
618
619        let pq = query.construct_path_query();
620        let items = &pq.query.query.items;
621        assert_eq!(items.len(), 1);
622        let encoded_2000 = encode_u64(2000);
623        assert!(
624            matches!(&items[0], QueryItem::RangeToInclusive(r) if r.end == encoded_2000),
625            "expected RangeToInclusive for included end time"
626        );
627    }
628
629    #[test]
630    fn construct_path_query_end_time_excluded() {
631        let query = VotePollsByEndDateDriveQuery {
632            start_time: None,
633            end_time: Some((2000, false)),
634            limit: Some(5),
635            offset: None,
636            order_ascending: true,
637        };
638
639        let pq = query.construct_path_query();
640        let items = &pq.query.query.items;
641        assert_eq!(items.len(), 1);
642        let encoded_2000 = encode_u64(2000);
643        assert!(
644            matches!(&items[0], QueryItem::RangeTo(r) if r.end == encoded_2000),
645            "expected RangeTo for excluded end time"
646        );
647    }
648
649    #[test]
650    fn construct_path_query_both_bounds_included() {
651        let query = VotePollsByEndDateDriveQuery {
652            start_time: Some((1000, true)),
653            end_time: Some((2000, true)),
654            limit: Some(20),
655            offset: None,
656            order_ascending: true,
657        };
658
659        let pq = query.construct_path_query();
660        let items = &pq.query.query.items;
661        assert_eq!(items.len(), 1);
662        let encoded_1000 = encode_u64(1000);
663        let encoded_2000 = encode_u64(2000);
664        assert!(
665            matches!(&items[0], QueryItem::RangeInclusive(r) if *r.start() == encoded_1000 && *r.end() == encoded_2000),
666            "expected RangeInclusive for both bounds included"
667        );
668    }
669
670    #[test]
671    fn construct_path_query_start_included_end_excluded() {
672        let query = VotePollsByEndDateDriveQuery {
673            start_time: Some((1000, true)),
674            end_time: Some((2000, false)),
675            limit: None,
676            offset: None,
677            order_ascending: true,
678        };
679
680        let pq = query.construct_path_query();
681        let items = &pq.query.query.items;
682        assert_eq!(items.len(), 1);
683        let encoded_1000 = encode_u64(1000);
684        let encoded_2000 = encode_u64(2000);
685        assert!(
686            matches!(&items[0], QueryItem::Range(r) if r.start == encoded_1000 && r.end == encoded_2000),
687            "expected Range (half-open) for start included, end excluded"
688        );
689    }
690
691    #[test]
692    fn construct_path_query_start_excluded_end_included() {
693        let query = VotePollsByEndDateDriveQuery {
694            start_time: Some((1000, false)),
695            end_time: Some((2000, true)),
696            limit: None,
697            offset: None,
698            order_ascending: true,
699        };
700
701        let pq = query.construct_path_query();
702        let items = &pq.query.query.items;
703        assert_eq!(items.len(), 1);
704        let encoded_1000 = encode_u64(1000);
705        let encoded_2000 = encode_u64(2000);
706        assert!(
707            matches!(&items[0], QueryItem::RangeAfterToInclusive(r) if *r.start() == encoded_1000 && *r.end() == encoded_2000),
708            "expected RangeAfterToInclusive"
709        );
710    }
711
712    #[test]
713    fn construct_path_query_both_bounds_excluded() {
714        let query = VotePollsByEndDateDriveQuery {
715            start_time: Some((1000, false)),
716            end_time: Some((2000, false)),
717            limit: None,
718            offset: None,
719            order_ascending: true,
720        };
721
722        let pq = query.construct_path_query();
723        let items = &pq.query.query.items;
724        assert_eq!(items.len(), 1);
725        let encoded_1000 = encode_u64(1000);
726        let encoded_2000 = encode_u64(2000);
727        assert!(
728            matches!(&items[0], QueryItem::RangeAfterTo(r) if r.start == encoded_1000 && r.end == encoded_2000),
729            "expected RangeAfterTo for both excluded"
730        );
731    }
732
733    // -----------------------------------------------------------------------
734    // path_query_for_end_time_included
735    // -----------------------------------------------------------------------
736
737    #[test]
738    fn path_query_for_end_time_included_builds_correct_query() {
739        let end_time: u64 = 5000;
740        let limit: u16 = 50;
741
742        let pq = VotePollsByEndDateDriveQuery::path_query_for_end_time_included(end_time, limit);
743        assert_eq!(pq.path, expected_base_path());
744        assert_eq!(pq.query.limit, Some(limit));
745        assert!(pq.query.query.left_to_right);
746
747        let items = &pq.query.query.items;
748        assert_eq!(items.len(), 1);
749        let encoded_5000 = encode_u64(5000);
750        assert!(
751            matches!(&items[0], QueryItem::RangeToInclusive(r) if r.end == encoded_5000),
752            "expected RangeToInclusive up to end_time"
753        );
754
755        // Should have a sub-query for all items
756        assert!(pq.query.query.default_subquery_branch.subquery.is_some());
757    }
758
759    // -----------------------------------------------------------------------
760    // path_query_for_single_end_time
761    // -----------------------------------------------------------------------
762
763    #[test]
764    fn path_query_for_single_end_time_builds_key_query() {
765        let end_time: u64 = 7777;
766        let limit: u16 = 100;
767
768        let pq = VotePollsByEndDateDriveQuery::path_query_for_single_end_time(end_time, limit);
769        assert_eq!(pq.path, expected_base_path());
770        assert_eq!(pq.query.limit, Some(limit));
771
772        let items = &pq.query.query.items;
773        assert_eq!(items.len(), 1);
774        let encoded_7777 = encode_u64(7777);
775        assert!(
776            matches!(&items[0], QueryItem::Key(k) if *k == encoded_7777),
777            "expected Key query for single end time"
778        );
779    }
780}