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