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