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}