1use std::marker::PhantomData;
2
3use crate::{FieldId, FieldRef, IntoScalarValue, ParamError, ScalarValue, SearchEntity};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum TimeBucketUnit {
7 Hour,
8 Day,
9 Week,
10 Month,
11}
12
13impl TimeBucketUnit {
14 #[must_use]
15 pub const fn sql_name(self) -> &'static str {
16 match self {
17 Self::Hour => "hour",
18 Self::Day => "day",
19 Self::Week => "week",
20 Self::Month => "month",
21 }
22 }
23}
24
25#[derive(Debug, Clone)]
26pub struct TimeBucketRequest<E: SearchEntity> {
27 field: FieldId,
28 schema_fingerprint: u64,
29 unit: TimeBucketUnit,
30 timezone: &'static str,
31 _entity: PhantomData<fn() -> E>,
32}
33
34impl<E: SearchEntity> TimeBucketRequest<E> {
35 pub fn new<T, K>(
36 field: FieldRef<E, T, K>,
37 unit: TimeBucketUnit,
38 timezone: &'static str,
39 ) -> Result<Self, ParamError> {
40 if E::META.field(field.id()).is_none() {
41 return Err(ParamError::UnknownField(format!("#{}", field.id().0)));
42 }
43 if timezone.is_empty()
44 || timezone.contains('\'')
45 || timezone.contains(';')
46 || timezone.contains("--")
47 || timezone.contains("/*")
48 {
49 return Err(ParamError::InvalidValue {
50 field: "timezone".to_owned(),
51 reason: "timezone contains an unsafe token".to_owned(),
52 });
53 }
54 Ok(Self {
55 field: field.id(),
56 schema_fingerprint: E::META.schema_fingerprint,
57 unit,
58 timezone,
59 _entity: PhantomData,
60 })
61 }
62
63 #[must_use]
64 pub const fn field_id(&self) -> FieldId {
65 self.field
66 }
67 #[must_use]
68 pub const fn schema_fingerprint(&self) -> u64 {
69 self.schema_fingerprint
70 }
71 #[must_use]
72 pub const fn unit(&self) -> TimeBucketUnit {
73 self.unit
74 }
75 #[must_use]
76 pub const fn timezone(&self) -> &'static str {
77 self.timezone
78 }
79}
80
81#[derive(Debug, Clone)]
82pub struct NumericBucketRequest<E: SearchEntity> {
83 field: FieldId,
84 schema_fingerprint: u64,
85 boundaries: Vec<ScalarValue>,
86 _entity: PhantomData<fn() -> E>,
87}
88
89impl<E: SearchEntity> NumericBucketRequest<E> {
90 pub fn new<T, K, V, I>(field: FieldRef<E, T, K>, boundaries: I) -> Result<Self, ParamError>
91 where
92 V: IntoScalarValue,
93 I: IntoIterator<Item = V>,
94 {
95 let boundaries = boundaries
96 .into_iter()
97 .map(IntoScalarValue::into_scalar_value)
98 .collect::<Vec<_>>();
99 if boundaries.is_empty() || boundaries.len() > 1_000 {
100 return Err(ParamError::InvalidValue {
101 field: "bucket.boundaries".to_owned(),
102 reason: "boundaries must contain between 1 and 1000 values".to_owned(),
103 });
104 }
105 let value_type = boundaries[0].value_type();
106 let Some(field_meta) = E::META.field(field.id()) else {
107 return Err(ParamError::UnknownField(format!("#{}", field.id().0)));
108 };
109 if !matches!(
110 value_type,
111 crate::ValueType::I32
112 | crate::ValueType::I64
113 | crate::ValueType::F32
114 | crate::ValueType::F64
115 ) || field_meta.value_type != value_type
116 || boundaries
117 .iter()
118 .any(|value| value.value_type() != value_type)
119 {
120 return Err(ParamError::InvalidValue {
121 field: "bucket.boundaries".to_owned(),
122 reason: "numeric boundaries must use one numeric value type".to_owned(),
123 });
124 }
125 if boundaries
126 .windows(2)
127 .any(|pair| !scalar_lt(&pair[0], &pair[1]))
128 {
129 return Err(ParamError::InvalidValue {
130 field: "bucket.boundaries".to_owned(),
131 reason: "boundaries must be strictly increasing".to_owned(),
132 });
133 }
134 Ok(Self {
135 field: field.id(),
136 schema_fingerprint: E::META.schema_fingerprint,
137 boundaries,
138 _entity: PhantomData,
139 })
140 }
141
142 #[must_use]
143 pub const fn field_id(&self) -> FieldId {
144 self.field
145 }
146 #[must_use]
147 pub const fn schema_fingerprint(&self) -> u64 {
148 self.schema_fingerprint
149 }
150 #[must_use]
151 pub fn boundaries(&self) -> &[ScalarValue] {
152 &self.boundaries
153 }
154}
155
156#[derive(Debug, Clone)]
157pub enum BucketRequest<E: SearchEntity> {
158 Time(TimeBucketRequest<E>),
159 Numeric(NumericBucketRequest<E>),
160}
161
162impl<E: SearchEntity> BucketRequest<E> {
163 #[must_use]
164 pub const fn field_id(&self) -> FieldId {
165 match self {
166 Self::Time(request) => request.field_id(),
167 Self::Numeric(request) => request.field_id(),
168 }
169 }
170
171 #[must_use]
172 pub const fn schema_fingerprint(&self) -> u64 {
173 match self {
174 Self::Time(request) => request.schema_fingerprint(),
175 Self::Numeric(request) => request.schema_fingerprint(),
176 }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize))]
182pub struct BucketEntry {
183 pub label: String,
184 pub count: i64,
185}
186
187#[derive(Debug, Clone, PartialEq)]
188#[cfg_attr(feature = "serde", derive(serde::Serialize))]
189pub struct BucketResult {
190 pub field: &'static str,
191 pub buckets: Vec<BucketEntry>,
192}
193
194fn scalar_lt(left: &ScalarValue, right: &ScalarValue) -> bool {
195 match (left, right) {
196 (ScalarValue::I32(left), ScalarValue::I32(right)) => left < right,
197 (ScalarValue::I64(left), ScalarValue::I64(right)) => left < right,
198 (ScalarValue::F32(left), ScalarValue::F32(right)) => left < right,
199 (ScalarValue::F64(left), ScalarValue::F64(right)) => left < right,
200 _ => false,
201 }
202}
203
204#[doc(hidden)]
205pub fn numeric_label(boundaries: &[ScalarValue], index: usize) -> String {
206 if index == 0 {
207 format!("<{}", boundaries[0].to_external_text())
208 } else if index < boundaries.len() {
209 format!(
210 "[{}, {})",
211 boundaries[index - 1].to_external_text(),
212 boundaries[index].to_external_text()
213 )
214 } else {
215 format!(">={}", boundaries[boundaries.len() - 1].to_external_text())
216 }
217}