Skip to main content

teaql_core/
request.rs

1//! Query builder layer types for TeaQL.
2//!
3//! This module contains the builder-side query types that were previously generated
4//! by the code generator's StringTemplate. They are the static, domain-independent
5//! parts shared by every generated TeaQL crate.
6//!
7//! Several types here intentionally shadow names from the parent crate (e.g.
8//! [`RelationAggregate`], [`ObjectGroupBy`], [`RawProjection`]). The builder
9//! versions carry a [`QuerySelection`] while the core/query versions carry a
10//! [`SelectQuery`]. The conversion happens in [`QuerySelection::into_query`] and
11//! [`apply_runtime_metadata`].
12
13use std::collections::BTreeMap;
14
15use serde_json::Value as JsonValue;
16
17use crate::{
18    BinaryOp, Expr, ObjectGroupBy as CoreObjectGroupBy, RawSqlProjection as CoreRawSqlProjection,
19    Record, RelationAggregate as RuntimeRelationAggregate, SelectQuery, SmartList, Value,
20};
21
22// ---------------------------------------------------------------------------
23// Constants
24// ---------------------------------------------------------------------------
25
26pub const COUNT_ALIAS: &str = "count";
27pub const TYPE_FIELD: &str = "internal_type";
28pub const TYPE_GROUP_FIELD: &str = "type_group";
29
30// ---------------------------------------------------------------------------
31// FieldOperator
32// ---------------------------------------------------------------------------
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum FieldOperator {
36    Equal,
37    NotEqual,
38    GreaterThan,
39    GreaterThanOrEqual,
40    LessThan,
41    LessThanOrEqual,
42    Between,
43    In,
44    NotIn,
45    Contain,
46    NotContain,
47    BeginWith,
48    NotBeginWith,
49    EndWith,
50    NotEndWith,
51    SoundsLike,
52    IsNull,
53    IsNotNull,
54}
55
56// ---------------------------------------------------------------------------
57// DateRange
58// ---------------------------------------------------------------------------
59
60#[derive(Clone, Debug, PartialEq)]
61pub struct DateRange<T> {
62    pub start: T,
63    pub end: T,
64}
65
66impl<T> DateRange<T> {
67    pub fn new(start: T, end: T) -> Self {
68        Self { start, end }
69    }
70}
71
72// ---------------------------------------------------------------------------
73// EntityReference
74// ---------------------------------------------------------------------------
75
76pub trait EntityReference {
77    fn entity_id_value(self) -> Value;
78}
79
80impl EntityReference for Value {
81    fn entity_id_value(self) -> Value {
82        self
83    }
84}
85
86impl EntityReference for u64 {
87    fn entity_id_value(self) -> Value {
88        Value::U64(self)
89    }
90}
91
92// ---------------------------------------------------------------------------
93// QuerySelection
94// ---------------------------------------------------------------------------
95
96#[derive(Clone, Debug, PartialEq)]
97pub struct QuerySelection {
98    pub query: SelectQuery,
99    pub relation_selections: Vec<RelationSelection>,
100    pub relation_filters: Vec<RelationFilter>,
101    pub child_enhancements: Vec<QuerySelection>,
102    pub query_options: QueryOptions,
103}
104
105impl QuerySelection {
106    pub fn new(query: impl Into<SelectQuery>) -> Self {
107        Self {
108            query: query.into(),
109            relation_selections: Vec::new(),
110            relation_filters: Vec::new(),
111            child_enhancements: Vec::new(),
112            query_options: QueryOptions::default(),
113        }
114    }
115
116    pub fn into_query(self) -> SelectQuery {
117        let query = apply_relation_selections(self.query, self.relation_selections);
118        apply_runtime_metadata(query, &self.query_options, &self.child_enhancements)
119    }
120}
121
122impl From<SelectQuery> for QuerySelection {
123    fn from(query: SelectQuery) -> Self {
124        QuerySelection::new(query)
125    }
126}
127
128// ---------------------------------------------------------------------------
129// RelationSelection
130// ---------------------------------------------------------------------------
131
132#[derive(Clone, Debug, PartialEq)]
133pub struct RelationSelection {
134    pub name: String,
135    pub query: SelectQuery,
136    pub relation_selections: Vec<RelationSelection>,
137    pub relation_filters: Vec<RelationFilter>,
138    pub child_enhancements: Vec<QuerySelection>,
139    pub query_options: QueryOptions,
140}
141
142impl RelationSelection {
143    pub fn new(name: impl Into<String>, selection: impl Into<QuerySelection>) -> Self {
144        let selection = selection.into();
145        Self {
146            name: name.into(),
147            query: selection.query,
148            relation_selections: selection.relation_selections,
149            relation_filters: selection.relation_filters,
150            child_enhancements: selection.child_enhancements,
151            query_options: selection.query_options,
152        }
153    }
154
155    pub fn into_query(self) -> SelectQuery {
156        let query = apply_relation_selections(self.query, self.relation_selections);
157        apply_runtime_metadata(query, &self.query_options, &self.child_enhancements)
158    }
159}
160
161// ---------------------------------------------------------------------------
162// RelationFilter
163// ---------------------------------------------------------------------------
164
165#[derive(Clone, Debug, PartialEq)]
166pub struct RelationFilter {
167    pub name: String,
168    pub query: SelectQuery,
169    pub relation_selections: Vec<RelationSelection>,
170    pub relation_filters: Vec<RelationFilter>,
171    pub child_enhancements: Vec<QuerySelection>,
172    pub query_options: QueryOptions,
173}
174
175impl RelationFilter {
176    pub fn new(name: impl Into<String>, selection: impl Into<QuerySelection>) -> Self {
177        let selection = selection.into();
178        Self {
179            name: name.into(),
180            query: selection.query,
181            relation_selections: selection.relation_selections,
182            relation_filters: selection.relation_filters,
183            child_enhancements: selection.child_enhancements,
184            query_options: selection.query_options,
185        }
186    }
187}
188
189// ---------------------------------------------------------------------------
190// QueryOptions
191// ---------------------------------------------------------------------------
192
193#[derive(Clone, Debug, Default, PartialEq)]
194pub struct QueryOptions {
195    pub comment: Option<String>,
196    pub raw_sql: Option<String>,
197    pub raw_sql_search_criteria: Vec<String>,
198    pub dynamic_properties: Vec<RawDynamicProperty>,
199    pub raw_projections: Vec<RawProjection>,
200    pub relation_aggregates: Vec<RelationAggregate>,
201    pub object_group_bys: Vec<ObjectGroupBy>,
202    pub facets: Vec<FacetRequest>,
203}
204
205// ---------------------------------------------------------------------------
206// UnsafeRawSqlSegment
207// ---------------------------------------------------------------------------
208
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct UnsafeRawSqlSegment {
211    sql: String,
212}
213
214impl UnsafeRawSqlSegment {
215    pub fn trusted(sql: impl Into<String>) -> Self {
216        Self { sql: sql.into() }
217    }
218
219    pub fn into_sql(self) -> String {
220        self.sql
221    }
222}
223
224// ---------------------------------------------------------------------------
225// RawDynamicProperty
226// ---------------------------------------------------------------------------
227
228#[derive(Clone, Debug, PartialEq, Eq)]
229pub struct RawDynamicProperty {
230    pub property_name: String,
231    pub raw_sql_segment: String,
232}
233
234impl RawDynamicProperty {
235    pub fn new(property_name: impl Into<String>, raw_sql_segment: UnsafeRawSqlSegment) -> Self {
236        Self {
237            property_name: property_name.into(),
238            raw_sql_segment: raw_sql_segment.into_sql(),
239        }
240    }
241}
242
243// ---------------------------------------------------------------------------
244// RawProjection (builder version — distinct from crate::RawSqlProjection)
245// ---------------------------------------------------------------------------
246
247#[derive(Clone, Debug, PartialEq, Eq)]
248pub struct RawProjection {
249    pub property_name: String,
250    pub raw_sql_segment: String,
251}
252
253impl RawProjection {
254    pub fn new(property_name: impl Into<String>, raw_sql_segment: UnsafeRawSqlSegment) -> Self {
255        Self {
256            property_name: property_name.into(),
257            raw_sql_segment: raw_sql_segment.into_sql(),
258        }
259    }
260}
261
262// ---------------------------------------------------------------------------
263// RelationAggregate (builder version — carries QuerySelection, not SelectQuery)
264// ---------------------------------------------------------------------------
265
266#[derive(Clone, Debug, PartialEq)]
267pub struct RelationAggregate {
268    pub relation_name: String,
269    pub alias: String,
270    pub query: QuerySelection,
271    pub single_result: bool,
272}
273
274impl RelationAggregate {
275    pub fn new(
276        relation_name: impl Into<String>,
277        alias: impl Into<String>,
278        query: impl Into<QuerySelection>,
279        single_result: bool,
280    ) -> Self {
281        Self {
282            relation_name: relation_name.into(),
283            alias: alias.into(),
284            query: query.into(),
285            single_result,
286        }
287    }
288}
289
290// ---------------------------------------------------------------------------
291// FacetRequest
292// ---------------------------------------------------------------------------
293
294#[derive(Clone, Debug, PartialEq)]
295pub struct FacetRequest {
296    pub facet_name: String,
297    pub relation_name: String,
298    pub query: QuerySelection,
299    pub include_all_facets: bool,
300}
301
302impl FacetRequest {
303    pub fn new(
304        facet_name: impl Into<String>,
305        relation_name: impl Into<String>,
306        query: impl Into<QuerySelection>,
307        include_all_facets: bool,
308    ) -> Self {
309        Self {
310            facet_name: facet_name.into(),
311            relation_name: relation_name.into(),
312            query: query.into(),
313            include_all_facets,
314        }
315    }
316}
317
318// ---------------------------------------------------------------------------
319// ObjectGroupBy (builder version — carries QuerySelection, not SelectQuery)
320// ---------------------------------------------------------------------------
321
322#[derive(Clone, Debug, PartialEq)]
323pub struct ObjectGroupBy {
324    pub property_name: String,
325    pub storage_field: String,
326    pub query: QuerySelection,
327}
328
329impl ObjectGroupBy {
330    pub fn new(
331        property_name: impl Into<String>,
332        storage_field: impl Into<String>,
333        query: impl Into<QuerySelection>,
334    ) -> Self {
335        Self {
336            property_name: property_name.into(),
337            storage_field: storage_field.into(),
338            query: query.into(),
339        }
340    }
341}
342
343// ---------------------------------------------------------------------------
344// Relation selection / runtime metadata helpers
345// ---------------------------------------------------------------------------
346
347pub fn apply_relation_selections(
348    mut query: SelectQuery,
349    relation_selections: Vec<RelationSelection>,
350) -> SelectQuery {
351    for selection in relation_selections {
352        query = query.relation_query(selection.name.clone(), selection.into_query());
353    }
354    query
355}
356
357pub fn apply_runtime_metadata(
358    mut query: SelectQuery,
359    options: &QueryOptions,
360    child_enhancements: &[QuerySelection],
361) -> SelectQuery {
362    if let Some(c) = options.comment.clone() {
363        query = query.comment(c);
364    }
365    query.raw_sql = options.raw_sql.clone();
366    query.raw_sql_search_criteria = options.raw_sql_search_criteria.clone();
367    query.dynamic_properties = options
368        .dynamic_properties
369        .iter()
370        .map(|projection| {
371            CoreRawSqlProjection::new(
372                projection.property_name.clone(),
373                projection.raw_sql_segment.clone(),
374            )
375        })
376        .collect();
377    query.raw_projections = options
378        .raw_projections
379        .iter()
380        .map(|projection| {
381            CoreRawSqlProjection::new(
382                projection.property_name.clone(),
383                projection.raw_sql_segment.clone(),
384            )
385        })
386        .collect();
387    query.object_group_bys = options
388        .object_group_bys
389        .iter()
390        .map(|group_by| {
391            CoreObjectGroupBy::new(
392                group_by.property_name.clone(),
393                group_by.storage_field.clone(),
394                group_by.query.clone().into_query(),
395            )
396        })
397        .collect();
398    query.child_enhancements = child_enhancements
399        .iter()
400        .cloned()
401        .map(QuerySelection::into_query)
402        .collect();
403    query
404}
405
406// ---------------------------------------------------------------------------
407// runtime_relation_aggregates — converts builder → core RelationAggregate
408// ---------------------------------------------------------------------------
409
410pub fn runtime_relation_aggregates(options: &QueryOptions) -> Vec<RuntimeRelationAggregate> {
411    options
412        .relation_aggregates
413        .iter()
414        .map(|aggregate| {
415            RuntimeRelationAggregate::new(
416                aggregate.relation_name.clone(),
417                aggregate.alias.clone(),
418                aggregate.query.clone().into_query(),
419                aggregate.single_result,
420            )
421        })
422        .collect()
423}
424
425// ---------------------------------------------------------------------------
426// Facet helpers
427// ---------------------------------------------------------------------------
428
429pub fn merge_outer_filter_into_facet_aggregates(
430    selection: &mut QuerySelection,
431    outer_query: &SelectQuery,
432) {
433    let Some(filter) = outer_query.filter.clone() else {
434        return;
435    };
436    for aggregate in &mut selection.query_options.relation_aggregates {
437        if aggregate.query.query.entity == outer_query.entity {
438            aggregate.query.query = aggregate.query.query.clone().and_filter(filter.clone());
439        }
440    }
441}
442
443pub fn attach_facets<T>(rows: &mut SmartList<T>, facets: BTreeMap<String, SmartList<Record>>) {
444    for (name, facet) in facets {
445        rows.add_facet(name, facet);
446    }
447}
448
449// ---------------------------------------------------------------------------
450// field_operator_expr / field_operator_column_expr
451// ---------------------------------------------------------------------------
452
453pub fn field_operator_expr(field: &str, operator: FieldOperator, values: Vec<Value>) -> Expr {
454    match operator {
455        FieldOperator::Equal => Expr::eq(field, required_value(operator, &values, 0)),
456        FieldOperator::NotEqual => Expr::ne(field, required_value(operator, &values, 0)),
457        FieldOperator::GreaterThan => Expr::gt(field, required_value(operator, &values, 0)),
458        FieldOperator::GreaterThanOrEqual => Expr::gte(field, required_value(operator, &values, 0)),
459        FieldOperator::LessThan => Expr::lt(field, required_value(operator, &values, 0)),
460        FieldOperator::LessThanOrEqual => Expr::lte(field, required_value(operator, &values, 0)),
461        FieldOperator::Between => Expr::between(
462            field,
463            required_value(operator, &values, 0),
464            required_value(operator, &values, 1),
465        ),
466        FieldOperator::In => Expr::in_list(field, values),
467        FieldOperator::NotIn => Expr::not_in_list(field, values),
468        FieldOperator::Contain => Expr::contain(field, required_text(operator, &values, 0)),
469        FieldOperator::NotContain => Expr::not_contain(field, required_text(operator, &values, 0)),
470        FieldOperator::BeginWith => Expr::begin_with(field, required_text(operator, &values, 0)),
471        FieldOperator::NotBeginWith => {
472            Expr::not_begin_with(field, required_text(operator, &values, 0))
473        }
474        FieldOperator::EndWith => Expr::end_with(field, required_text(operator, &values, 0)),
475        FieldOperator::NotEndWith => Expr::not_end_with(field, required_text(operator, &values, 0)),
476        FieldOperator::SoundsLike => Expr::sound_like(field, required_value(operator, &values, 0)),
477        FieldOperator::IsNull => Expr::is_null(field),
478        FieldOperator::IsNotNull => Expr::is_not_null(field),
479    }
480}
481
482pub fn field_operator_column_expr(field: &str, operator: FieldOperator, other_field: &str) -> Expr {
483    let binary_op = match operator {
484        FieldOperator::Equal => BinaryOp::Eq,
485        FieldOperator::NotEqual => BinaryOp::Ne,
486        FieldOperator::GreaterThan => BinaryOp::Gt,
487        FieldOperator::GreaterThanOrEqual => BinaryOp::Gte,
488        FieldOperator::LessThan => BinaryOp::Lt,
489        FieldOperator::LessThanOrEqual => BinaryOp::Lte,
490        FieldOperator::Contain => BinaryOp::Like,
491        FieldOperator::NotContain => BinaryOp::NotLike,
492        FieldOperator::BeginWith => BinaryOp::Like,
493        FieldOperator::NotBeginWith => BinaryOp::NotLike,
494        FieldOperator::EndWith => BinaryOp::Like,
495        FieldOperator::NotEndWith => BinaryOp::NotLike,
496        unsupported => panic!("{unsupported:?} is not supported for property-to-property filters"),
497    };
498    Expr::compare_columns(field, binary_op, other_field)
499}
500
501// ---------------------------------------------------------------------------
502// required_value / required_text
503// ---------------------------------------------------------------------------
504
505pub fn required_value(operator: FieldOperator, values: &[Value], index: usize) -> Value {
506    values
507        .get(index)
508        .cloned()
509        .unwrap_or_else(|| panic!("{operator:?} requires value at index {index}"))
510}
511
512pub fn required_text(operator: FieldOperator, values: &[Value], index: usize) -> String {
513    match required_value(operator, values, index) {
514        Value::Text(value) => value,
515        value => panic!("{operator:?} requires text value, got {value:?}"),
516    }
517}
518
519// ---------------------------------------------------------------------------
520// remove_default_live_filter / remove_filter_expr
521// ---------------------------------------------------------------------------
522
523pub fn remove_default_live_filter(filter: Option<Expr>) -> Option<Expr> {
524    let default_filter = Expr::gt("version", 0_i64);
525    remove_filter_expr(filter?, &default_filter)
526}
527
528pub fn remove_filter_expr(filter: Expr, target: &Expr) -> Option<Expr> {
529    if &filter == target {
530        return None;
531    }
532    match filter {
533        Expr::And(parts) => {
534            let mut retained = parts
535                .into_iter()
536                .filter_map(|part| remove_filter_expr(part, target))
537                .collect::<Vec<_>>();
538            match retained.len() {
539                0 => None,
540                1 => retained.pop(),
541                _ => Some(Expr::And(retained)),
542            }
543        }
544        other => Some(other),
545    }
546}
547
548// ---------------------------------------------------------------------------
549// Dynamic JSON helpers
550// ---------------------------------------------------------------------------
551
552pub fn dynamic_json_value_to_teaql_value(value: &JsonValue) -> Value {
553    match value {
554        JsonValue::Null => Value::Null,
555        JsonValue::Bool(value) => Value::Bool(*value),
556        JsonValue::Number(value) => value
557            .as_i64()
558            .map(Value::I64)
559            .or_else(|| value.as_u64().map(Value::U64))
560            .or_else(|| value.as_f64().map(Value::F64))
561            .unwrap_or(Value::Null),
562        JsonValue::String(value) => Value::Text(value.trim().to_owned()),
563        JsonValue::Array(values) => Value::List(
564            values
565                .iter()
566                .map(dynamic_json_value_to_teaql_value)
567                .collect(),
568        ),
569        JsonValue::Object(object) => object
570            .get("id")
571            .map(dynamic_json_value_to_teaql_value)
572            .unwrap_or(Value::Null),
573    }
574}
575
576pub fn dynamic_json_values(value: &JsonValue) -> Vec<Value> {
577    match value {
578        JsonValue::Array(values) => values
579            .iter()
580            .map(dynamic_json_value_to_teaql_value)
581            .collect(),
582        value => vec![dynamic_json_value_to_teaql_value(value)],
583    }
584}
585
586pub fn dynamic_json_operator(value: &JsonValue) -> FieldOperator {
587    match value {
588        JsonValue::String(value) if value.eq_ignore_ascii_case("__is_null__") => {
589            FieldOperator::IsNull
590        }
591        JsonValue::String(value) if value.eq_ignore_ascii_case("__is_not_null__") => {
592            FieldOperator::IsNotNull
593        }
594        JsonValue::String(_) => FieldOperator::Contain,
595        JsonValue::Number(_) | JsonValue::Bool(_) => FieldOperator::Equal,
596        JsonValue::Array(values) if values.first().map(JsonValue::is_string).unwrap_or(false) => {
597            FieldOperator::In
598        }
599        JsonValue::Array(values) if values.first().map(JsonValue::is_object).unwrap_or(false) => {
600            FieldOperator::In
601        }
602        JsonValue::Array(values) if values.len() == 2 => FieldOperator::Between,
603        _ => FieldOperator::Equal,
604    }
605}
606
607pub fn dynamic_json_filter_expr(field: &str, value: &JsonValue) -> Expr {
608    let operator = dynamic_json_operator(value);
609    field_operator_expr(field, operator, dynamic_json_values(value))
610}
611
612pub fn dynamic_json_u64_field(
613    object: &serde_json::Map<String, JsonValue>,
614    field: &str,
615) -> Option<u64> {
616    object.get(field).and_then(|value| {
617        value
618            .as_u64()
619            .or_else(|| value.as_i64().and_then(|value| u64::try_from(value).ok()))
620    })
621}