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.raw_sql_search_criteria = options.raw_sql_search_criteria.clone();
368    query.dynamic_properties = options
369        .dynamic_properties
370        .iter()
371        .map(|projection| {
372            CoreRawSqlProjection::new(
373                projection.property_name.clone(),
374                projection.raw_sql_segment.clone(),
375            )
376        })
377        .collect();
378    query.raw_projections = options
379        .raw_projections
380        .iter()
381        .map(|projection| {
382            CoreRawSqlProjection::new(
383                projection.property_name.clone(),
384                projection.raw_sql_segment.clone(),
385            )
386        })
387        .collect();
388    query.object_group_bys = options
389        .object_group_bys
390        .iter()
391        .map(|group_by| {
392            CoreObjectGroupBy::new(
393                group_by.property_name.clone(),
394                group_by.storage_field.clone(),
395                group_by.query.clone().into_query(),
396            )
397        })
398        .collect();
399    query.child_enhancements = child_enhancements
400        .iter()
401        .cloned()
402        .map(QuerySelection::into_query)
403        .collect();
404    query
405}
406
407// ---------------------------------------------------------------------------
408// runtime_relation_aggregates — converts builder → core RelationAggregate
409// ---------------------------------------------------------------------------
410
411pub fn runtime_relation_aggregates(options: &QueryOptions) -> Vec<RuntimeRelationAggregate> {
412    options
413        .relation_aggregates
414        .iter()
415        .map(|aggregate| {
416            RuntimeRelationAggregate::new(
417                aggregate.relation_name.clone(),
418                aggregate.alias.clone(),
419                aggregate.query.clone().into_query(),
420                aggregate.single_result,
421            )
422        })
423        .collect()
424}
425
426// ---------------------------------------------------------------------------
427// Facet helpers
428// ---------------------------------------------------------------------------
429
430pub fn merge_outer_filter_into_facet_aggregates(
431    selection: &mut QuerySelection,
432    outer_query: &SelectQuery,
433) {
434    let Some(filter) = outer_query.filter.clone() else {
435        return;
436    };
437    for aggregate in &mut selection.query_options.relation_aggregates {
438        if aggregate.query.query.entity == outer_query.entity {
439            aggregate.query.query = aggregate.query.query.clone().and_filter(filter.clone());
440        }
441    }
442}
443
444pub fn attach_facets<T>(rows: &mut SmartList<T>, facets: BTreeMap<String, SmartList<CompactRow>>) {
445    for (name, facet) in facets {
446        rows.add_facet(name, facet);
447    }
448}
449
450// ---------------------------------------------------------------------------
451// field_operator_expr / field_operator_column_expr
452// ---------------------------------------------------------------------------
453
454pub fn field_operator_expr(field: &str, operator: FieldOperator, values: Vec<Value>) -> Expr {
455    match operator {
456        FieldOperator::Equal => Expr::eq(field, required_value(operator, &values, 0)),
457        FieldOperator::NotEqual => Expr::ne(field, required_value(operator, &values, 0)),
458        FieldOperator::GreaterThan => Expr::gt(field, required_value(operator, &values, 0)),
459        FieldOperator::GreaterThanOrEqual => Expr::gte(field, required_value(operator, &values, 0)),
460        FieldOperator::LessThan => Expr::lt(field, required_value(operator, &values, 0)),
461        FieldOperator::LessThanOrEqual => Expr::lte(field, required_value(operator, &values, 0)),
462        FieldOperator::Between => Expr::between(
463            field,
464            required_value(operator, &values, 0),
465            required_value(operator, &values, 1),
466        ),
467        FieldOperator::In => Expr::in_list(field, values),
468        FieldOperator::NotIn => Expr::not_in_list(field, values),
469        FieldOperator::Contain => Expr::contain(field, required_text(operator, &values, 0)),
470        FieldOperator::NotContain => Expr::not_contain(field, required_text(operator, &values, 0)),
471        FieldOperator::BeginWith => Expr::begin_with(field, required_text(operator, &values, 0)),
472        FieldOperator::NotBeginWith => {
473            Expr::not_begin_with(field, required_text(operator, &values, 0))
474        }
475        FieldOperator::EndWith => Expr::end_with(field, required_text(operator, &values, 0)),
476        FieldOperator::NotEndWith => Expr::not_end_with(field, required_text(operator, &values, 0)),
477        FieldOperator::SoundsLike => Expr::sound_like(field, required_value(operator, &values, 0)),
478        FieldOperator::IsNull => Expr::is_null(field),
479        FieldOperator::IsNotNull => Expr::is_not_null(field),
480    }
481}
482
483pub fn field_operator_column_expr(field: &str, operator: FieldOperator, other_field: &str) -> Expr {
484    let binary_op = match operator {
485        FieldOperator::Equal => BinaryOp::Eq,
486        FieldOperator::NotEqual => BinaryOp::Ne,
487        FieldOperator::GreaterThan => BinaryOp::Gt,
488        FieldOperator::GreaterThanOrEqual => BinaryOp::Gte,
489        FieldOperator::LessThan => BinaryOp::Lt,
490        FieldOperator::LessThanOrEqual => BinaryOp::Lte,
491        FieldOperator::Contain => BinaryOp::Like,
492        FieldOperator::NotContain => BinaryOp::NotLike,
493        FieldOperator::BeginWith => BinaryOp::Like,
494        FieldOperator::NotBeginWith => BinaryOp::NotLike,
495        FieldOperator::EndWith => BinaryOp::Like,
496        FieldOperator::NotEndWith => BinaryOp::NotLike,
497        unsupported => panic!("{unsupported:?} is not supported for property-to-property filters"),
498    };
499    Expr::compare_columns(field, binary_op, other_field)
500}
501
502// ---------------------------------------------------------------------------
503// required_value / required_text
504// ---------------------------------------------------------------------------
505
506pub fn required_value(operator: FieldOperator, values: &[Value], index: usize) -> Value {
507    values
508        .get(index)
509        .cloned()
510        .unwrap_or_else(|| panic!("{operator:?} requires value at index {index}"))
511}
512
513pub fn required_text(operator: FieldOperator, values: &[Value], index: usize) -> String {
514    match required_value(operator, values, index) {
515        Value::Text(value) => value,
516        value => panic!("{operator:?} requires text value, got {value:?}"),
517    }
518}
519
520// ---------------------------------------------------------------------------
521// remove_default_live_filter / remove_filter_expr
522// ---------------------------------------------------------------------------
523
524pub fn remove_default_live_filter(filter: Option<Expr>) -> Option<Expr> {
525    let default_filter = Expr::gt("version", 0_i64);
526    remove_filter_expr(filter?, &default_filter)
527}
528
529pub fn remove_filter_expr(filter: Expr, target: &Expr) -> Option<Expr> {
530    if &filter == target {
531        return None;
532    }
533    match filter {
534        Expr::And(parts) => {
535            let mut retained = parts
536                .into_iter()
537                .filter_map(|part| remove_filter_expr(part, target))
538                .collect::<Vec<_>>();
539            match retained.len() {
540                0 => None,
541                1 => retained.pop(),
542                _ => Some(Expr::And(retained)),
543            }
544        }
545        other => Some(other),
546    }
547}
548
549// ---------------------------------------------------------------------------
550// Dynamic JSON helpers
551// ---------------------------------------------------------------------------
552
553pub fn dynamic_json_value_to_teaql_value(value: &JsonValue) -> Value {
554    match value {
555        JsonValue::Null => Value::Null,
556        JsonValue::Bool(value) => Value::Bool(*value),
557        JsonValue::Number(value) => value
558            .as_i64()
559            .map(Value::I64)
560            .or_else(|| value.as_u64().map(Value::U64))
561            .or_else(|| value.as_f64().map(Value::F64))
562            .unwrap_or(Value::Null),
563        JsonValue::String(value) => Value::Text(value.trim().to_owned()),
564        JsonValue::Array(values) => Value::List(
565            values
566                .iter()
567                .map(dynamic_json_value_to_teaql_value)
568                .collect(),
569        ),
570        JsonValue::Object(object) => object
571            .get("id")
572            .map(dynamic_json_value_to_teaql_value)
573            .unwrap_or(Value::Null),
574    }
575}
576
577pub fn dynamic_json_values(value: &JsonValue) -> Vec<Value> {
578    match value {
579        JsonValue::Array(values) => values
580            .iter()
581            .map(dynamic_json_value_to_teaql_value)
582            .collect(),
583        value => vec![dynamic_json_value_to_teaql_value(value)],
584    }
585}
586
587pub fn dynamic_json_operator(value: &JsonValue) -> FieldOperator {
588    match value {
589        JsonValue::String(value) if value.eq_ignore_ascii_case("__is_null__") => {
590            FieldOperator::IsNull
591        }
592        JsonValue::String(value) if value.eq_ignore_ascii_case("__is_not_null__") => {
593            FieldOperator::IsNotNull
594        }
595        JsonValue::String(_) => FieldOperator::Contain,
596        JsonValue::Number(_) | JsonValue::Bool(_) => FieldOperator::Equal,
597        JsonValue::Array(values) if values.first().map(JsonValue::is_string).unwrap_or(false) => {
598            FieldOperator::In
599        }
600        JsonValue::Array(values) if values.first().map(JsonValue::is_object).unwrap_or(false) => {
601            FieldOperator::In
602        }
603        JsonValue::Array(values) if values.len() == 2 => FieldOperator::Between,
604        _ => FieldOperator::Equal,
605    }
606}
607
608pub fn dynamic_json_filter_expr(field: &str, value: &JsonValue) -> Expr {
609    let operator = dynamic_json_operator(value);
610    field_operator_expr(field, operator, dynamic_json_values(value))
611}
612
613pub fn dynamic_json_u64_field(
614    object: &serde_json::Map<String, JsonValue>,
615    field: &str,
616) -> Option<u64> {
617    object.get(field).and_then(|value| {
618        value
619            .as_u64()
620            .or_else(|| value.as_i64().and_then(|value| u64::try_from(value).ok()))
621    })
622}