Skip to main content

teaql_sql/
dialect.rs

1use teaql_core::{
2    AggregateFunction, BinaryOp, DataType, DeleteCommand, EntityDescriptor, Expr, ExprFunction,
3    InsertCommand, OrderBy, PropertyDescriptor, RecoverCommand, SelectQuery, SortDirection,
4    UpdateCommand, Value,
5};
6
7use crate::{CompiledQuery, DatabaseKind, SqlCompileError};
8
9fn with_comment(sql: String, comment: Option<&str>) -> String {
10    match comment {
11        Some(comment) if !comment.is_empty() => {
12            let escaped = comment.replace("*/", "* /");
13            format!("/* {escaped} */ {sql}")
14        }
15        _ => sql,
16    }
17}
18
19pub trait SqlDialect {
20    fn kind(&self) -> DatabaseKind;
21    fn quote_ident(&self, ident: &str) -> String;
22    fn placeholder(&self, index: usize) -> String;
23
24    fn schema_setup_sqls(&self) -> &'static [&'static str] {
25        &[]
26    }
27
28    fn schema_type_sql(
29        &self,
30        data_type: DataType,
31        _property: &PropertyDescriptor,
32    ) -> Result<&'static str, SqlCompileError> {
33        match data_type {
34            DataType::Bool => Ok("BOOLEAN"),
35            DataType::I64 | DataType::U64 => Ok("INTEGER"),
36            DataType::F64 => Ok("REAL"),
37            DataType::Decimal => Ok("NUMERIC"),
38            DataType::Text | DataType::Json | DataType::Date | DataType::Timestamp => Ok("TEXT"),
39        }
40    }
41
42    fn column_definition_sql(
43        &self,
44        property: &PropertyDescriptor,
45    ) -> Result<String, SqlCompileError> {
46        let mut parts = vec![
47            self.quote_ident(&property.column_name),
48            self.schema_type_sql(property.data_type, property)?
49                .to_owned(),
50        ];
51
52        if property.is_id {
53            parts.push("PRIMARY KEY".to_owned());
54        }
55        if property.is_id || !property.nullable {
56            parts.push("NOT NULL".to_owned());
57        }
58
59        Ok(parts.join(" "))
60    }
61
62    fn compile_create_table(&self, entity: &EntityDescriptor) -> Result<String, SqlCompileError> {
63        let columns = entity
64            .properties
65            .iter()
66            .map(|property| self.column_definition_sql(property))
67            .collect::<Result<Vec<_>, _>>()?
68            .join(", ");
69        Ok(format!(
70            "CREATE TABLE IF NOT EXISTS {} ({columns})",
71            self.quote_ident(&entity.table_name)
72        ))
73    }
74
75    fn compile_add_column(
76        &self,
77        entity: &EntityDescriptor,
78        property: &PropertyDescriptor,
79    ) -> Result<String, SqlCompileError> {
80        Ok(format!(
81            "ALTER TABLE {} ADD COLUMN {}",
82            self.quote_ident(&entity.table_name),
83            self.column_definition_sql(property)?
84        ))
85    }
86
87    fn compile_select(
88        &self,
89        entity: &EntityDescriptor,
90        query: &SelectQuery,
91    ) -> Result<CompiledQuery, SqlCompileError> {
92        let mut params = Vec::new();
93        let sql = self.compile_select_sql(entity, query, &mut params)?;
94        Ok(CompiledQuery { sql, params })
95    }
96
97    fn compile_select_sql(
98        &self,
99        entity: &EntityDescriptor,
100        query: &SelectQuery,
101        params: &mut Vec<Value>,
102    ) -> Result<String, SqlCompileError> {
103        if let Some(raw_sql) = &query.raw_sql {
104            return Ok(with_comment(raw_sql.clone(), query.comment.as_deref()));
105        }
106
107        let projection = if query.aggregates.is_empty() {
108            self.select_projection(entity, query, params)?
109        } else {
110            self.aggregate_projection(entity, query, params)?
111        };
112
113        let mut sql = format!(
114            "SELECT {projection} FROM {}",
115            self.quote_ident(&entity.table_name)
116        );
117
118        let mut where_parts = Vec::new();
119        if let Some(filter) = &query.filter {
120            where_parts.push(self.compile_expr(entity, filter, params)?);
121        }
122        where_parts.extend(query.raw_sql_search_criteria.iter().cloned());
123        if let Some(json_expr) = &query.json_expr {
124            where_parts.push(json_expr.clone());
125        }
126        if !where_parts.is_empty() {
127            sql.push_str(" WHERE ");
128            sql.push_str(&where_parts.join(" AND "));
129        }
130
131        if !query.group_by.is_empty() {
132            let group_by = query
133                .group_by
134                .iter()
135                .map(|field| self.column_sql(entity, field))
136                .collect::<Result<Vec<_>, _>>()?
137                .join(", ");
138            sql.push_str(" GROUP BY ");
139            sql.push_str(&group_by);
140        }
141
142        if let Some(having) = &query.having {
143            let having_sql = self.compile_expr(entity, having, params)?;
144            sql.push_str(" HAVING ");
145            sql.push_str(&having_sql);
146        }
147
148        if !query.order_by.is_empty() {
149            let order_by = query
150                .order_by
151                .iter()
152                .map(|order| self.order_by_sql(entity, order, params))
153                .collect::<Result<Vec<_>, _>>()?
154                .join(", ");
155            sql.push_str(" ORDER BY ");
156            sql.push_str(&order_by);
157        }
158
159        if let Some(slice) = query.slice {
160            if let Some(limit) = slice.limit {
161                sql.push_str(&format!(" LIMIT {limit}"));
162            }
163            if slice.offset > 0 {
164                sql.push_str(&format!(" OFFSET {}", slice.offset));
165            }
166        }
167
168        Ok(with_comment(sql, query.comment.as_deref()))
169    }
170
171    fn compile_insert(
172        &self,
173        entity: &EntityDescriptor,
174        command: &InsertCommand,
175    ) -> Result<CompiledQuery, SqlCompileError> {
176        let mut columns = Vec::new();
177        let mut placeholders = Vec::new();
178        let mut params = Vec::new();
179
180        for property in &entity.properties {
181            if let Some(value) = command.values.get(&property.name) {
182                columns.push(self.quote_ident(&property.column_name));
183                params.push(value.clone());
184                placeholders.push(self.placeholder(params.len()));
185            }
186        }
187
188        if columns.is_empty() {
189            return Err(SqlCompileError::EmptyMutation("insert".to_owned()));
190        }
191
192        Ok(CompiledQuery {
193            sql: format!(
194                "INSERT INTO {} ({}) VALUES ({})",
195                self.quote_ident(&entity.table_name),
196                columns.join(", "),
197                placeholders.join(", ")
198            ),
199            params,
200        })
201    }
202
203    fn compile_update(
204        &self,
205        entity: &EntityDescriptor,
206        command: &UpdateCommand,
207    ) -> Result<CompiledQuery, SqlCompileError> {
208        let id_property = entity
209            .id_property()
210            .ok_or_else(|| SqlCompileError::MissingIdProperty(entity.name.clone()))?;
211        let mut assignments = Vec::new();
212        let mut params = Vec::new();
213
214        for property in &entity.properties {
215            if property.is_id {
216                continue;
217            }
218            if let Some(value) = command.values.get(&property.name) {
219                params.push(value.clone());
220                assignments.push(format!(
221                    "{} = {}",
222                    self.quote_ident(&property.column_name),
223                    self.placeholder(params.len())
224                ));
225            }
226        }
227
228        if let Some(expected_version) = command.expected_version {
229            let version_property = entity
230                .version_property()
231                .ok_or_else(|| SqlCompileError::MissingVersionProperty(entity.name.clone()))?;
232            params.push(Value::I64(expected_version + 1));
233            assignments.push(format!(
234                "{} = {}",
235                self.quote_ident(&version_property.column_name),
236                self.placeholder(params.len())
237            ));
238        }
239
240        if assignments.is_empty() {
241            return Err(SqlCompileError::EmptyMutation("update".to_owned()));
242        }
243
244        params.push(command.id.clone());
245        let mut predicates = vec![format!(
246            "{} = {}",
247            self.quote_ident(&id_property.column_name),
248            self.placeholder(params.len())
249        )];
250
251        if let Some(expected_version) = command.expected_version {
252            let version_property = entity
253                .version_property()
254                .ok_or_else(|| SqlCompileError::MissingVersionProperty(entity.name.clone()))?;
255            params.push(Value::I64(expected_version));
256            predicates.push(format!(
257                "{} = {}",
258                self.quote_ident(&version_property.column_name),
259                self.placeholder(params.len())
260            ));
261        }
262
263        Ok(CompiledQuery {
264            sql: format!(
265                "UPDATE {} SET {} WHERE {}",
266                self.quote_ident(&entity.table_name),
267                assignments.join(", "),
268                predicates.join(" AND ")
269            ),
270            params,
271        })
272    }
273
274    fn compile_delete(
275        &self,
276        entity: &EntityDescriptor,
277        command: &DeleteCommand,
278    ) -> Result<CompiledQuery, SqlCompileError> {
279        let id_property = entity
280            .id_property()
281            .ok_or_else(|| SqlCompileError::MissingIdProperty(entity.name.clone()))?;
282        let mut params = Vec::new();
283
284        if command.soft_delete {
285            let version_property = entity
286                .version_property()
287                .ok_or_else(|| SqlCompileError::MissingVersionProperty(entity.name.clone()))?;
288            params.push(match command.expected_version {
289                Some(version) => Value::I64(-(version + 1)),
290                None => Value::I64(-1),
291            });
292
293            params.push(command.id.clone());
294            let mut predicates = vec![format!(
295                "{} = {}",
296                self.quote_ident(&id_property.column_name),
297                self.placeholder(params.len())
298            )];
299
300            if let Some(expected_version) = command.expected_version {
301                params.push(Value::I64(expected_version));
302                predicates.push(format!(
303                    "{} = {}",
304                    self.quote_ident(&version_property.column_name),
305                    self.placeholder(params.len())
306                ));
307            }
308
309            return Ok(CompiledQuery {
310                sql: format!(
311                    "UPDATE {} SET {} = {} WHERE {}",
312                    self.quote_ident(&entity.table_name),
313                    self.quote_ident(&version_property.column_name),
314                    self.placeholder(1),
315                    predicates.join(" AND ")
316                ),
317                params,
318            });
319        }
320
321        params.push(command.id.clone());
322        let mut predicates = vec![format!(
323            "{} = {}",
324            self.quote_ident(&id_property.column_name),
325            self.placeholder(params.len())
326        )];
327
328        if let Some(expected_version) = command.expected_version {
329            let version_property = entity
330                .version_property()
331                .ok_or_else(|| SqlCompileError::MissingVersionProperty(entity.name.clone()))?;
332            params.push(Value::I64(expected_version));
333            predicates.push(format!(
334                "{} = {}",
335                self.quote_ident(&version_property.column_name),
336                self.placeholder(params.len())
337            ));
338        }
339
340        Ok(CompiledQuery {
341            sql: format!(
342                "DELETE FROM {} WHERE {}",
343                self.quote_ident(&entity.table_name),
344                predicates.join(" AND ")
345            ),
346            params,
347        })
348    }
349
350    fn compile_recover(
351        &self,
352        entity: &EntityDescriptor,
353        command: &RecoverCommand,
354    ) -> Result<CompiledQuery, SqlCompileError> {
355        if command.expected_version >= 0 {
356            return Err(SqlCompileError::InvalidRecoverVersion(
357                command.expected_version,
358            ));
359        }
360
361        let id_property = entity
362            .id_property()
363            .ok_or_else(|| SqlCompileError::MissingIdProperty(entity.name.clone()))?;
364        let version_property = entity
365            .version_property()
366            .ok_or_else(|| SqlCompileError::MissingVersionProperty(entity.name.clone()))?;
367        let params = vec![
368            Value::I64(-command.expected_version + 1),
369            command.id.clone(),
370            Value::I64(command.expected_version),
371        ];
372
373        Ok(CompiledQuery {
374            sql: format!(
375                "UPDATE {} SET {} = {} WHERE {} = {} AND {} = {}",
376                self.quote_ident(&entity.table_name),
377                self.quote_ident(&version_property.column_name),
378                self.placeholder(1),
379                self.quote_ident(&id_property.column_name),
380                self.placeholder(2),
381                self.quote_ident(&version_property.column_name),
382                self.placeholder(3),
383            ),
384            params,
385        })
386    }
387
388    fn column_sql(
389        &self,
390        entity: &EntityDescriptor,
391        field: &str,
392    ) -> Result<String, SqlCompileError> {
393        let property = entity
394            .property_by_name(field)
395            .ok_or_else(|| SqlCompileError::UnknownField(field.to_owned()))?;
396        Ok(self.quote_ident(&property.column_name))
397    }
398
399    fn order_by_sql(
400        &self,
401        entity: &EntityDescriptor,
402        order_by: &OrderBy,
403        params: &mut Vec<Value>,
404    ) -> Result<String, SqlCompileError> {
405        let field = if let Some(expr) = &order_by.expr {
406            self.compile_expr(entity, expr, params)?
407        } else {
408            self.column_sql(entity, &order_by.field)?
409        };
410        let direction = match order_by.direction {
411            SortDirection::Asc => "ASC",
412            SortDirection::Desc => "DESC",
413        };
414        Ok(format!("{field} {direction}"))
415    }
416
417    fn select_projection(
418        &self,
419        entity: &EntityDescriptor,
420        query: &SelectQuery,
421        params: &mut Vec<Value>,
422    ) -> Result<String, SqlCompileError> {
423        if query.projection.is_empty()
424            && query.expr_projection.is_empty()
425            && query.raw_projections.is_empty()
426            && query.dynamic_properties.is_empty()
427        {
428            return Ok("*".to_owned());
429        }
430        let mut parts = query
431            .projection
432            .iter()
433            .map(|field| self.column_sql(entity, field))
434            .collect::<Result<Vec<_>, _>>()?;
435        for projection in &query.expr_projection {
436            let expr = self.compile_expr(entity, &projection.expr, params)?;
437            parts.push(format!("{expr} AS {}", self.quote_ident(&projection.alias)));
438        }
439        for projection in query
440            .raw_projections
441            .iter()
442            .chain(query.dynamic_properties.iter())
443        {
444            parts.push(format!(
445                "{} AS {}",
446                projection.raw_sql_segment,
447                self.quote_ident(&projection.property_name)
448            ));
449        }
450        Ok(parts.join(", "))
451    }
452
453    fn aggregate_projection(
454        &self,
455        entity: &EntityDescriptor,
456        query: &SelectQuery,
457        params: &mut Vec<Value>,
458    ) -> Result<String, SqlCompileError> {
459        let mut parts = Vec::new();
460        for field in query.group_by.iter().chain(query.projection.iter()) {
461            let column = self.column_sql(entity, field)?;
462            if !parts.contains(&column) {
463                parts.push(column);
464            }
465        }
466        for projection in &query.expr_projection {
467            let expr = self.compile_expr(entity, &projection.expr, params)?;
468            let aliased = format!("{expr} AS {}", self.quote_ident(&projection.alias));
469            if !parts.contains(&aliased) {
470                parts.push(aliased);
471            }
472        }
473        for projection in query
474            .raw_projections
475            .iter()
476            .chain(query.dynamic_properties.iter())
477        {
478            let aliased = format!(
479                "{} AS {}",
480                projection.raw_sql_segment,
481                self.quote_ident(&projection.property_name)
482            );
483            if !parts.contains(&aliased) {
484                parts.push(aliased);
485            }
486        }
487        parts.extend(
488            query
489                .aggregates
490                .iter()
491                .map(|aggregate| {
492                    let field = if aggregate.function == AggregateFunction::Count
493                        && aggregate.field == "*"
494                    {
495                        "*".to_owned()
496                    } else {
497                        self.column_sql(entity, &aggregate.field)?
498                    };
499                    let call = self.aggregate_call_sql(aggregate.function, &field);
500                    Ok(format!("{call} AS {}", self.quote_ident(&aggregate.alias)))
501                })
502                .collect::<Result<Vec<_>, _>>()?,
503        );
504        Ok(parts.join(", "))
505    }
506
507    fn aggregate_call_sql(&self, function: AggregateFunction, field: &str) -> String {
508        let function_sql = self.aggregate_function_sql(function);
509        format!("{function_sql}({field})")
510    }
511
512    fn aggregate_function_sql(&self, function: AggregateFunction) -> &'static str {
513        match function {
514            AggregateFunction::Count => "COUNT",
515            AggregateFunction::Sum => "SUM",
516            AggregateFunction::Avg => "AVG",
517            AggregateFunction::Min => "MIN",
518            AggregateFunction::Max => "MAX",
519            AggregateFunction::Stddev => "STDDEV",
520            AggregateFunction::StddevPop => "STDDEV_POP",
521            AggregateFunction::VarSamp => "VAR_SAMP",
522            AggregateFunction::VarPop => "VAR_POP",
523            AggregateFunction::BitAnd => "BIT_AND",
524            AggregateFunction::BitOr => "BIT_OR",
525            AggregateFunction::BitXor => "BIT_XOR",
526        }
527    }
528
529    fn compile_expr(
530        &self,
531        entity: &EntityDescriptor,
532        expr: &Expr,
533        params: &mut Vec<Value>,
534    ) -> Result<String, SqlCompileError> {
535        match expr {
536            Expr::Column(name) => self.column_sql(entity, name),
537            Expr::Value(value) => {
538                params.push(value.clone());
539                Ok(self.placeholder(params.len()))
540            }
541            Expr::Function { function, args } => {
542                self.compile_function(entity, *function, args, params)
543            }
544            Expr::Binary { left, op, right } => {
545                if matches!(
546                    op,
547                    BinaryOp::In | BinaryOp::NotIn | BinaryOp::InLarge | BinaryOp::NotInLarge
548                ) {
549                    return self.compile_in(entity, left, *op, right, params);
550                }
551                let lhs = self.compile_expr(entity, left, params)?;
552                let rhs = self.compile_expr(entity, right, params)?;
553                let op = match op {
554                    BinaryOp::Eq => "=",
555                    BinaryOp::Ne => "!=",
556                    BinaryOp::Gt => ">",
557                    BinaryOp::Gte => ">=",
558                    BinaryOp::Lt => "<",
559                    BinaryOp::Lte => "<=",
560                    BinaryOp::Like => "LIKE",
561                    BinaryOp::NotLike => "NOT LIKE",
562                    BinaryOp::In | BinaryOp::NotIn | BinaryOp::InLarge | BinaryOp::NotInLarge => {
563                        unreachable!()
564                    }
565                };
566                Ok(format!("({lhs} {op} {rhs})"))
567            }
568            Expr::SubQuery {
569                left,
570                op,
571                entity: sub_entity,
572                query,
573            } => self.compile_subquery(entity, left, *op, sub_entity, query, params),
574            Expr::Between { expr, lower, upper } => {
575                let expr = self.compile_expr(entity, expr, params)?;
576                let lower = self.compile_expr(entity, lower, params)?;
577                let upper = self.compile_expr(entity, upper, params)?;
578                Ok(format!("({expr} BETWEEN {lower} AND {upper})"))
579            }
580            Expr::IsNull(expr) => {
581                let expr = self.compile_expr(entity, expr, params)?;
582                Ok(format!("({expr} IS NULL)"))
583            }
584            Expr::IsNotNull(expr) => {
585                let expr = self.compile_expr(entity, expr, params)?;
586                Ok(format!("({expr} IS NOT NULL)"))
587            }
588            Expr::And(parts) => self.compile_joined(entity, parts, "AND", params),
589            Expr::Or(parts) => self.compile_joined(entity, parts, "OR", params),
590            Expr::Not(expr) => {
591                let expr = self.compile_expr(entity, expr, params)?;
592                Ok(format!("(NOT {expr})"))
593            }
594        }
595    }
596
597    fn compile_function(
598        &self,
599        entity: &EntityDescriptor,
600        function: ExprFunction,
601        args: &[Expr],
602        params: &mut Vec<Value>,
603    ) -> Result<String, SqlCompileError> {
604        match function {
605            ExprFunction::Soundex => {
606                let [arg] = args else {
607                    return Err(SqlCompileError::InvalidFunctionArguments(
608                        "SOUNDEX expects exactly one argument".to_owned(),
609                    ));
610                };
611                let arg = self.compile_expr(entity, arg, params)?;
612                Ok(format!("SOUNDEX({arg})"))
613            }
614            ExprFunction::Gbk => {
615                let [arg] = args else {
616                    return Err(SqlCompileError::InvalidFunctionArguments(
617                        "GBK expects exactly one argument".to_owned(),
618                    ));
619                };
620                let arg = self.compile_expr(entity, arg, params)?;
621                Ok(format!("convert_to({arg}, 'GBK')"))
622            }
623            ExprFunction::Count if args.is_empty() => Ok("COUNT(*)".to_owned()),
624            ExprFunction::Count => self.compile_single_arg_function(entity, "COUNT", args, params),
625            ExprFunction::Sum => self.compile_single_arg_function(entity, "SUM", args, params),
626            ExprFunction::Avg => self.compile_single_arg_function(entity, "AVG", args, params),
627            ExprFunction::Min => self.compile_single_arg_function(entity, "MIN", args, params),
628            ExprFunction::Max => self.compile_single_arg_function(entity, "MAX", args, params),
629            ExprFunction::Stddev => {
630                self.compile_single_arg_function(entity, "STDDEV", args, params)
631            }
632            ExprFunction::StddevPop => {
633                self.compile_single_arg_function(entity, "STDDEV_POP", args, params)
634            }
635            ExprFunction::VarSamp => {
636                self.compile_single_arg_function(entity, "VAR_SAMP", args, params)
637            }
638            ExprFunction::VarPop => {
639                self.compile_single_arg_function(entity, "VAR_POP", args, params)
640            }
641            ExprFunction::BitAnd => {
642                self.compile_single_arg_function(entity, "BIT_AND", args, params)
643            }
644            ExprFunction::BitOr => self.compile_single_arg_function(entity, "BIT_OR", args, params),
645            ExprFunction::BitXor => {
646                self.compile_single_arg_function(entity, "BIT_XOR", args, params)
647            }
648        }
649    }
650
651    fn compile_single_arg_function(
652        &self,
653        entity: &EntityDescriptor,
654        function: &str,
655        args: &[Expr],
656        params: &mut Vec<Value>,
657    ) -> Result<String, SqlCompileError> {
658        let [arg] = args else {
659            return Err(SqlCompileError::InvalidFunctionArguments(format!(
660                "{function} expects exactly one argument"
661            )));
662        };
663        let arg = self.compile_expr(entity, arg, params)?;
664        Ok(format!("{function}({arg})"))
665    }
666
667    fn compile_subquery(
668        &self,
669        entity: &EntityDescriptor,
670        left: &Expr,
671        op: BinaryOp,
672        sub_entity: &EntityDescriptor,
673        query: &SelectQuery,
674        params: &mut Vec<Value>,
675    ) -> Result<String, SqlCompileError> {
676        let lhs = self.compile_expr(entity, left, params)?;
677        let operator = match op {
678            BinaryOp::In | BinaryOp::InLarge => "IN",
679            BinaryOp::NotIn | BinaryOp::NotInLarge => "NOT IN",
680            _ => return Err(SqlCompileError::InvalidSubQueryOperator(format!("{op:?}"))),
681        };
682        let subquery = self.compile_select_sql(sub_entity, query, params)?;
683        Ok(format!("({lhs} {operator} ({subquery}))"))
684    }
685
686    fn compile_joined(
687        &self,
688        entity: &EntityDescriptor,
689        parts: &[Expr],
690        joiner: &str,
691        params: &mut Vec<Value>,
692    ) -> Result<String, SqlCompileError> {
693        let compiled = parts
694            .iter()
695            .map(|part| self.compile_expr(entity, part, params))
696            .collect::<Result<Vec<_>, _>>()?;
697        Ok(format!("({})", compiled.join(&format!(" {joiner} "))))
698    }
699
700    fn compile_in(
701        &self,
702        entity: &EntityDescriptor,
703        left: &Expr,
704        op: BinaryOp,
705        right: &Expr,
706        params: &mut Vec<Value>,
707    ) -> Result<String, SqlCompileError> {
708        let lhs = self.compile_expr(entity, left, params)?;
709        let operator = match op {
710            BinaryOp::In | BinaryOp::InLarge => "IN",
711            BinaryOp::NotIn | BinaryOp::NotInLarge => "NOT IN",
712            _ => unreachable!(),
713        };
714        match right {
715            Expr::Value(Value::List(values)) => {
716                if values.is_empty() {
717                    return Err(SqlCompileError::EmptyInList);
718                }
719                let mut placeholders = Vec::with_capacity(values.len());
720                for value in values {
721                    params.push(value.clone());
722                    placeholders.push(self.placeholder(params.len()));
723                }
724                Ok(format!("({lhs} {operator} ({}))", placeholders.join(", ")))
725            }
726            _ => {
727                let rhs = self.compile_expr(entity, right, params)?;
728                Ok(format!("({lhs} {operator} ({rhs}))"))
729            }
730        }
731    }
732}