Skip to main content

drive/query/
vote_polls_by_end_date_query.rs

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