Skip to main content

sift_query_core/model/
meta.rs

1use std::fmt;
2use std::marker::PhantomData;
3
4use crate::{ConfigError, Operator, SortOrder};
5
6/// Stable, entity-local field identifier.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8#[repr(transparent)]
9pub struct FieldId(pub u16);
10
11/// Supported logical value types.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14#[non_exhaustive]
15pub enum ValueType {
16    Bool,
17    I32,
18    I64,
19    F32,
20    F64,
21    #[cfg(feature = "decimal")]
22    Decimal,
23    Text,
24    Bytes,
25    #[cfg(feature = "chrono")]
26    Date,
27    #[cfg(feature = "chrono")]
28    Time,
29    #[cfg(feature = "chrono")]
30    DateTime,
31    #[cfg(feature = "chrono")]
32    Timestamp,
33    #[cfg(feature = "uuid")]
34    Uuid,
35    #[cfg(feature = "json")]
36    Json,
37    #[cfg(feature = "postgres-types")]
38    ArrayI64,
39    #[cfg(feature = "postgres-types")]
40    ArrayText,
41    #[cfg(feature = "postgres-types")]
42    Int8Range,
43    #[cfg(feature = "postgres-types")]
44    Network,
45}
46
47/// Types with a defined cursor encoding and ordered comparison contract.
48impl ValueType {
49    pub fn supports_cursor(self) -> bool {
50        match self {
51            #[cfg(feature = "json")]
52            Self::Json => false,
53            #[cfg(feature = "postgres-types")]
54            Self::ArrayI64 | Self::ArrayText | Self::Int8Range | Self::Network => false,
55            _ => true,
56        }
57    }
58}
59
60/// Determines whether a condition is evaluated before or after grouping.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum FieldKind {
63    Scalar,
64    GroupKey,
65    Aggregate,
66    FilterOnly,
67}
68
69/// A compact static operator whitelist.
70#[derive(Debug, Clone, Copy)]
71pub struct OperatorSet(pub &'static [Operator]);
72
73impl OperatorSet {
74    #[must_use]
75    pub fn contains(self, operator: Operator) -> bool {
76        self.0.contains(&operator)
77    }
78}
79
80/// Metadata for one query-visible field.
81#[derive(Debug)]
82pub struct FieldMeta {
83    pub id: FieldId,
84    pub public_name: &'static str,
85    pub column: &'static str,
86    pub result_alias: Option<&'static str>,
87    pub value_type: ValueType,
88    pub kind: FieldKind,
89    pub filterable: bool,
90    pub selectable: bool,
91    pub sortable: bool,
92    pub operators: OperatorSet,
93}
94
95/// A validated static ordering item.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct OrderMeta {
98    pub field: FieldId,
99    pub direction: SortOrder,
100}
101
102/// Per-entity limits. Instance-level limits may only make these stricter.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct EntityLimits {
105    pub max_page_size: u32,
106    pub max_offset: u64,
107    pub timeout_ms: u64,
108}
109
110impl Default for EntityLimits {
111    fn default() -> Self {
112        Self {
113            max_page_size: 100,
114            max_offset: 20_000,
115            timeout_ms: 5_000,
116        }
117    }
118}
119
120/// Static metadata generated for a search entity.
121#[derive(Debug)]
122pub struct EntityMeta {
123    pub entity_name: &'static str,
124    pub from: &'static str,
125    pub base_where: Option<&'static str>,
126    pub fixed_having: Option<&'static str>,
127    pub distinct: bool,
128    pub fields: &'static [FieldMeta],
129    pub default_order: &'static [OrderMeta],
130    pub stable_order: &'static [OrderMeta],
131    pub limits: EntityLimits,
132    pub schema_fingerprint: u64,
133}
134
135impl EntityMeta {
136    #[must_use]
137    pub fn field(&'static self, id: FieldId) -> Option<&'static FieldMeta> {
138        self.fields.get(usize::from(id.0))
139    }
140
141    #[must_use]
142    pub fn resolve_field(&'static self, name: &str) -> Option<&'static FieldMeta> {
143        self.fields.iter().find(|field| field.public_name == name)
144    }
145
146    #[must_use]
147    pub fn is_grouped(&self) -> bool {
148        self.fields
149            .iter()
150            .any(|field| matches!(field.kind, FieldKind::GroupKey | FieldKind::Aggregate))
151    }
152
153    pub fn validate(&self) -> Result<(), ConfigError> {
154        if self.entity_name.is_empty() || self.from.trim().is_empty() {
155            return Err(ConfigError::Invalid(
156                "entity_name and from must not be empty".to_owned(),
157            ));
158        }
159        for (index, field) in self.fields.iter().enumerate() {
160            if usize::from(field.id.0) != index {
161                return Err(ConfigError::Invalid(format!(
162                    "field {} has non-contiguous FieldId {}",
163                    field.public_name, field.id.0
164                )));
165            }
166        }
167        if self.is_grouped()
168            && self.fields.iter().any(|field| {
169                field.result_alias.is_some() && matches!(field.kind, FieldKind::Scalar)
170            })
171        {
172            return Err(ConfigError::Invalid(
173                "grouped entities cannot return Scalar fields".to_owned(),
174            ));
175        }
176        Ok(())
177    }
178}
179
180/// Implemented by derive-generated query entities.
181pub trait SearchEntity: Sized + Send + Sync + 'static {
182    /// Server-declared ordering expression using one validated query condition.
183    fn condition_order(_field: FieldId) -> Option<ConditionOrder> {
184        None
185    }
186    /// HTTP-only custom operator whitelist; trusted programmatic operators
187    /// are independent of this registration boundary.
188    fn custom_operator_names(_field: FieldId) -> &'static [&'static str] {
189        &[]
190    }
191    /// Optional named codec binding generated by `#[search_field(codec = "name")]`.
192    fn codec_name(_field: FieldId) -> Option<&'static str> {
193        None
194    }
195    /// Declarative `(source field, dictionary key)` label bindings generated
196    /// from `#[search_field(label = "...")]`.
197    fn label_bindings() -> &'static [(&'static str, &'static str)] {
198        &[]
199    }
200    const META: &'static EntityMeta;
201
202    /// Server assertion that the ordering expression is never SQL NULL.
203    /// Defaults to false; Rust types alone do not establish SQL nullability.
204    fn cursor_key_non_null(_field: FieldId) -> bool {
205        false
206    }
207
208    fn cursor_value(&self, _field: FieldId) -> Option<crate::CursorValue> {
209        None
210    }
211}
212
213#[derive(Debug, Clone, Copy)]
214pub struct ConditionOrder {
215    pub source: FieldId,
216    pub operator: &'static str,
217    pub expression: crate::CustomOperator,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct ProjectionField {
222    pub source: FieldId,
223    pub result_alias: &'static str,
224}
225
226pub trait SearchProjection<E: SearchEntity>: Sized + Send + Sync + 'static {
227    const FIELDS: &'static [ProjectionField];
228
229    fn cursor_value(&self, _source: FieldId) -> Option<crate::CursorValue> {
230        None
231    }
232}
233
234/// Typed reference to one entity field.
235#[derive(PartialEq, Eq, Hash)]
236pub struct FieldRef<E, T, K> {
237    id: FieldId,
238    _marker: PhantomData<fn() -> (E, T, K)>,
239}
240
241impl<E, T, K> FieldRef<E, T, K> {
242    #[doc(hidden)]
243    pub const fn new(id: FieldId) -> Self {
244        Self {
245            id,
246            _marker: PhantomData,
247        }
248    }
249
250    #[must_use]
251    pub const fn id(self) -> FieldId {
252        self.id
253    }
254}
255
256impl<E, T, K> Clone for FieldRef<E, T, K> {
257    fn clone(&self) -> Self {
258        *self
259    }
260}
261
262impl<E, T, K> Copy for FieldRef<E, T, K> {}
263
264impl<E, T, K> fmt::Debug for FieldRef<E, T, K> {
265    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
266        formatter.debug_tuple("FieldRef").field(&self.id).finish()
267    }
268}
269
270pub enum ScalarKind {}
271pub enum GroupKeyKind {}
272pub enum AggregateKind {}
273pub enum FilterOnlyKind {}
274
275pub trait RowPhaseKind {}
276impl RowPhaseKind for ScalarKind {}
277impl RowPhaseKind for GroupKeyKind {}
278impl RowPhaseKind for FilterOnlyKind {}
279
280pub trait SummableKind {}
281impl SummableKind for ScalarKind {}
282impl SummableKind for GroupKeyKind {}
283impl SummableKind for AggregateKind {}
284
285/// Marker stored in result structs for fields that participate only in filtering.
286pub struct FilterOnly<T>(PhantomData<fn() -> T>);
287
288impl<T> Default for FilterOnly<T> {
289    fn default() -> Self {
290        Self(PhantomData)
291    }
292}
293
294impl<T> Clone for FilterOnly<T> {
295    fn clone(&self) -> Self {
296        *self
297    }
298}
299
300impl<T> Copy for FilterOnly<T> {}
301
302impl<T> fmt::Debug for FilterOnly<T> {
303    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
304        formatter.write_str("FilterOnly")
305    }
306}