1use super::{Expr, FromClause, MergeWhen, Projection, Result, SelectStmt, Statement, Value, CTE};
10use crate::ast::InternalColumnRef;
11
12#[derive(Debug, Clone)]
14pub struct ResolvedVariable {
15 pub value: Value,
16 pub declared_type: Option<String>,
17 pub collation_oid: Option<u32>,
19}
20
21impl ResolvedVariable {
22 #[must_use]
23 pub fn untyped(value: Value) -> Self {
24 Self {
25 value,
26 declared_type: None,
27 collation_oid: None,
28 }
29 }
30
31 pub(crate) fn into_expression(self) -> Expr {
32 match self.declared_type {
33 Some(ty) => Expr::TypedLiteral {
34 collation_oid: self.collation_oid,
35 composite_source: None,
36 value: self.value,
37 ty,
38 },
39 None => Expr::Literal(self.value),
40 }
41 }
42}
43
44pub trait VariableResolver {
47 fn parameter_type(&self, name: &str) -> Option<crate::ColumnType> {
50 crate::ColumnType::from_sql_name(name).ok()
51 }
52
53 fn resolve_name(&mut self, name: &str) -> Result<Option<ResolvedVariable>>;
56 fn resolve_qualified(
58 &mut self,
59 qualifier: &str,
60 column: &str,
61 ) -> Result<Option<ResolvedVariable>>;
62 fn resolve_param(&mut self, index: usize) -> Result<Option<ResolvedVariable>>;
64
65 fn rewrite_name(&mut self, name: &str) -> Result<Option<Expr>> {
67 Ok(self
68 .resolve_name(name)?
69 .map(ResolvedVariable::into_expression))
70 }
71
72 fn rewrite_qualified(&mut self, qualifier: &str, column: &str) -> Result<Option<Expr>> {
74 Ok(self
75 .resolve_qualified(qualifier, column)?
76 .map(ResolvedVariable::into_expression))
77 }
78
79 fn rewrite_qualified_star(&mut self, _qualifier: &str) -> Result<Option<Vec<Expr>>> {
84 Ok(None)
85 }
86
87 fn rewrite_qualified_whole_row(&mut self, _qualifier: &str) -> Result<Option<Expr>> {
89 Ok(None)
90 }
91
92 fn rewrite_param(&mut self, index: usize) -> Result<Option<Expr>> {
94 Ok(self
95 .resolve_param(index)?
96 .map(ResolvedVariable::into_expression))
97 }
98
99 fn rewrite_internal(&mut self, _column: InternalColumnRef) -> Result<Option<Expr>> {
102 Ok(None)
103 }
104}
105
106#[expect(
109 clippy::too_many_lines,
110 reason = "PL/pgSQL lowering preserves parser order and datum validation"
111)]
112pub fn bind_expr(expr: &Expr, r: &mut dyn VariableResolver) -> Result<Expr> {
113 Ok(match expr {
114 Expr::Column(name) => match r.rewrite_name(name)? {
115 Some(value) => value,
116 None => expr.clone(),
117 },
118 Expr::QualifiedColumn {
119 qualifier, column, ..
120 } => match r.rewrite_qualified(qualifier, column)? {
121 Some(value) => value,
122 None => expr.clone(),
123 },
124 Expr::Param(index) => match r.rewrite_param(*index)? {
125 Some(value) => value,
126 None => expr.clone(),
127 },
128 Expr::InternalColumn(column) => match r.rewrite_internal(*column)? {
129 Some(value) => value,
130 None => expr.clone(),
131 },
132 Expr::QualifiedStar(qualifier) => r
133 .rewrite_qualified_whole_row(qualifier)?
134 .unwrap_or_else(|| expr.clone()),
135 Expr::Default | Expr::Literal(_) | Expr::TypedLiteral { .. } | Expr::Star => expr.clone(),
136 Expr::Func {
137 order_syntax,
138 name,
139 binding,
140 args,
141 distinct,
142 order_by,
143 filter,
144 } => Expr::Func {
145 order_syntax: *order_syntax,
146 name: name.clone(),
147 binding: binding.clone(),
148 args: bind_exprs(args, r)?,
149 distinct: *distinct,
150 order_by: bind_order_by(order_by, r)?,
151 filter: match filter {
152 Some(f) => Some(Box::new(bind_expr(f, r)?)),
153 None => None,
154 },
155 },
156 Expr::Array(items) => Expr::Array(bind_exprs(items, r)?),
157 Expr::Row(items) => Expr::Row(bind_exprs(items, r)?),
158 Expr::CompositeRow { items, binding } => Expr::CompositeRow {
159 items: bind_exprs(items, r)?,
160 binding: binding.clone(),
161 },
162 Expr::Binary { op, lhs, rhs } => Expr::Binary {
163 op: *op,
164 lhs: Box::new(bind_expr(lhs, r)?),
165 rhs: Box::new(bind_expr(rhs, r)?),
166 },
167 Expr::UnaryMinus(inner) => Expr::UnaryMinus(Box::new(bind_expr(inner, r)?)),
168 Expr::Not(inner) => Expr::Not(Box::new(bind_expr(inner, r)?)),
169 Expr::And(items) => Expr::And(bind_exprs(items, r)?),
170 Expr::Or(items) => Expr::Or(bind_exprs(items, r)?),
171 Expr::IsNull { expr, negated } => Expr::IsNull {
172 expr: Box::new(bind_expr(expr, r)?),
173 negated: *negated,
174 },
175 Expr::Between { expr, low, high } => Expr::Between {
176 expr: Box::new(bind_expr(expr, r)?),
177 low: Box::new(bind_expr(low, r)?),
178 high: Box::new(bind_expr(high, r)?),
179 },
180 Expr::InList {
181 expr,
182 list,
183 negated,
184 } => Expr::InList {
185 expr: Box::new(bind_expr(expr, r)?),
186 list: bind_exprs(list, r)?,
187 negated: *negated,
188 },
189 Expr::WindowCall {
190 name,
191 args,
192 spec,
193 filter,
194 modifiers,
195 } => Expr::WindowCall {
196 modifiers: *modifiers,
197 name: name.clone(),
198 args: bind_exprs(args, r)?,
199 spec: Box::new(bind_window_spec(spec, r)?),
200 filter: match filter {
201 Some(f) => Some(Box::new(bind_expr(f, r)?)),
202 None => None,
203 },
204 },
205 Expr::Case {
206 base,
207 when,
208 else_branch,
209 } => Expr::Case {
210 base: match base {
211 Some(b) => Some(Box::new(bind_expr(b, r)?)),
212 None => None,
213 },
214 when: when
215 .iter()
216 .map(|(c, v)| Ok((bind_expr(c, r)?, bind_expr(v, r)?)))
217 .collect::<Result<Vec<_>>>()?,
218 else_branch: match else_branch {
219 Some(e) => Some(Box::new(bind_expr(e, r)?)),
220 None => None,
221 },
222 },
223 Expr::Cast { expr, ty, implicit } => Expr::Cast {
224 implicit: *implicit,
225 expr: Box::new(bind_expr(expr, r)?),
226 ty: ty.clone(),
227 },
228 Expr::ScalarSubquery(body) => Expr::ScalarSubquery(Box::new(bind_select(body, r)?)),
229 Expr::Exists { body, negated } => Expr::Exists {
230 body: Box::new(bind_select(body, r)?),
231 negated: *negated,
232 },
233 Expr::InSubquery {
234 expr,
235 body,
236 negated,
237 } => Expr::InSubquery {
238 expr: Box::new(bind_expr(expr, r)?),
239 body: Box::new(bind_select(body, r)?),
240 negated: *negated,
241 },
242 })
243}
244
245fn bind_frame_bound(
247 bound: &crate::ast::FrameBound,
248 r: &mut dyn VariableResolver,
249) -> Result<crate::ast::FrameBound> {
250 use crate::ast::FrameBound;
251 Ok(match bound {
252 FrameBound::Preceding(offset) => FrameBound::Preceding(Box::new(bind_expr(offset, r)?)),
253 FrameBound::Following(offset) => FrameBound::Following(Box::new(bind_expr(offset, r)?)),
254 FrameBound::UnboundedPreceding => FrameBound::UnboundedPreceding,
255 FrameBound::UnboundedFollowing => FrameBound::UnboundedFollowing,
256 FrameBound::CurrentRow => FrameBound::CurrentRow,
257 })
258}
259
260pub(super) fn bind_exprs(exprs: &[Expr], r: &mut dyn VariableResolver) -> Result<Vec<Expr>> {
261 exprs.iter().map(|e| bind_expr(e, r)).collect()
262}
263
264pub(super) fn bind_opt_expr(
265 expr: Option<&Expr>,
266 r: &mut dyn VariableResolver,
267) -> Result<Option<Expr>> {
268 match expr {
269 Some(e) => Ok(Some(bind_expr(e, r)?)),
270 None => Ok(None),
271 }
272}
273
274pub(super) fn bind_order_by(
275 items: &[crate::ast::OrderBy],
276 r: &mut dyn VariableResolver,
277) -> Result<Vec<crate::ast::OrderBy>> {
278 items
279 .iter()
280 .map(|o| {
281 Ok(crate::ast::OrderBy {
282 expr: bind_expr(&o.expr, r)?,
283 descending: o.descending,
284 nulls: o.nulls,
285 })
286 })
287 .collect()
288}
289
290pub(super) fn bind_projections(
291 items: &[Projection],
292 r: &mut dyn VariableResolver,
293) -> Result<Vec<Projection>> {
294 items
295 .iter()
296 .map(|p| {
297 Ok(Projection {
298 expr: bind_expr(&p.expr, r)?,
299 alias: p.alias.clone(),
300 })
301 })
302 .collect()
303}
304
305pub(super) fn bind_assignments(
306 items: &[(crate::ast::AssignmentTargets, Expr)],
307 r: &mut dyn VariableResolver,
308) -> Result<Vec<(crate::ast::AssignmentTargets, Expr)>> {
309 items
310 .iter()
311 .map(|(target, expression)| {
312 Ok((
313 {
314 let mut group = target.clone();
315 for target in group.targets_mut() {
316 *target = bind_assignment_target(target, r)?;
317 }
318 group
319 },
320 bind_expr(expression, r)?,
321 ))
322 })
323 .collect()
324}
325
326fn bind_assignment_target(
327 target: &crate::ast::AssignmentTarget,
328 resolver: &mut dyn VariableResolver,
329) -> Result<crate::ast::AssignmentTarget> {
330 let mut target = target.clone();
331 for expression in target.expressions_mut() {
332 *expression = bind_expr(expression, resolver)?;
333 }
334 Ok(target)
335}
336
337pub(super) fn bind_ctes(items: &[CTE], r: &mut dyn VariableResolver) -> Result<Vec<CTE>> {
338 items
339 .iter()
340 .map(|cte| {
341 Ok(CTE {
342 name: cte.name.clone(),
343 columns: cte.columns.clone(),
344 recursive: cte.recursive,
345 materialization: cte.materialization,
346 search: cte.search.clone(),
347 cycle: cte
348 .cycle
349 .as_ref()
350 .map(|cycle| -> Result<crate::ast::CteCycleClause> {
351 Ok(crate::ast::CteCycleClause {
352 columns: cycle.columns.clone(),
353 mark_column: cycle.mark_column.clone(),
354 mark_value: bind_expr(&cycle.mark_value, r)?,
355 mark_default: bind_expr(&cycle.mark_default, r)?,
356 path_column: cycle.path_column.clone(),
357 })
358 })
359 .transpose()?,
360 body: crate::ast::CteBody::try_from(bind_statement(
361 &cte.body.clone().into_statement(),
362 r,
363 )?)?,
364 })
365 })
366 .collect()
367}
368
369pub(super) fn bind_rows(
370 rows: &[Vec<Expr>],
371 r: &mut dyn VariableResolver,
372) -> Result<Vec<Vec<Expr>>> {
373 rows.iter().map(|row| bind_exprs(row, r)).collect()
374}
375
376fn bind_window_spec(
377 spec: &crate::ast::WindowSpec,
378 r: &mut dyn VariableResolver,
379) -> Result<crate::ast::WindowSpec> {
380 Ok(crate::ast::WindowSpec {
381 definition: spec.definition,
382 raw_definition: None,
383 reference: spec.reference.clone(),
384 partition_by: bind_exprs(&spec.partition_by, r)?,
385 order_by: bind_order_by(&spec.order_by, r)?,
386 frame: spec
387 .frame
388 .as_ref()
389 .map(|frame| -> Result<crate::ast::WindowFrame> {
390 Ok(crate::ast::WindowFrame {
391 start: bind_frame_bound(&frame.start, r)?,
392 end: bind_frame_bound(&frame.end, r)?,
393 ..frame.clone()
394 })
395 })
396 .transpose()?,
397 })
398}
399
400pub fn bind_select(stmt: &SelectStmt, r: &mut dyn VariableResolver) -> Result<SelectStmt> {
402 Ok(SelectStmt {
403 windows: stmt
404 .windows
405 .iter()
406 .map(|window| {
407 Ok(crate::ast::WindowDefinition {
408 name: window.name.clone(),
409 inherited: window.inherited,
410 spec: bind_window_spec(&window.spec, r)?,
411 })
412 })
413 .collect::<Result<Vec<_>>>()?,
414 projections: bind_projections(&stmt.projections, r)?,
415 values: bind_rows(&stmt.values, r)?,
416 from: match stmt.from.as_ref() {
417 Some(f) => Some(bind_from(f, r)?),
418 None => None,
419 },
420 r#where: bind_opt_expr(stmt.r#where.as_ref(), r)?,
421 group_by: bind_exprs(&stmt.group_by, r)?,
422 grouping_sets: stmt
423 .grouping_sets
424 .iter()
425 .map(|set| bind_exprs(set, r))
426 .collect::<Result<Vec<_>>>()?,
427 group_distinct: stmt.group_distinct,
428 having: bind_opt_expr(stmt.having.as_ref(), r)?,
429 order_by: bind_order_by(&stmt.order_by, r)?,
430 limit: bind_opt_expr(stmt.limit.as_ref(), r)?,
431 with_ties: stmt.with_ties,
432 offset: bind_opt_expr(stmt.offset.as_ref(), r)?,
433 with: bind_ctes(&stmt.with, r)?,
434 set_op: match stmt.set_op.as_ref() {
435 Some(op) => Some(Box::new(crate::ast::SetOp {
436 kind: op.kind,
437 all: op.all,
438 left: op
439 .left
440 .as_ref()
441 .map(|left| bind_select(left, r).map(Box::new))
442 .transpose()?,
443 right: bind_select(&op.right, r)?,
444 combined_order_by: bind_order_by(&op.combined_order_by, r)?,
445 combined_limit: bind_opt_expr(op.combined_limit.as_ref(), r)?,
446 combined_with_ties: op.combined_with_ties,
447 combined_offset: bind_opt_expr(op.combined_offset.as_ref(), r)?,
448 })),
449 None => None,
450 },
451 distinct: stmt.distinct,
452 distinct_on: bind_exprs(&stmt.distinct_on, r)?,
453 locking: stmt.locking.clone(),
454 })
455}
456
457pub(super) fn bind_from(from: &FromClause, r: &mut dyn VariableResolver) -> Result<FromClause> {
458 Ok(match from {
459 FromClause::Table { .. } => from.clone(),
460 FromClause::Join {
461 left,
462 right,
463 kind,
464 on,
465 using,
466 natural,
467 alias,
468 column_aliases,
469 lateral,
470 } => FromClause::Join {
471 left: Box::new(bind_from(left, r)?),
472 right: Box::new(bind_from(right, r)?),
473 kind: *kind,
474 on: bind_opt_expr(on.as_ref(), r)?,
475 using: using.clone(),
476 natural: *natural,
477 alias: alias.clone(),
478 column_aliases: column_aliases.clone(),
479 lateral: *lateral,
480 },
481 FromClause::Values {
482 rows,
483 alias,
484 column_aliases,
485 internal_relation,
486 internal_column_types,
487 internal_column_collations,
488 } => FromClause::Values {
489 rows: bind_rows(rows, r)?,
490 alias: alias.clone(),
491 column_aliases: column_aliases.clone(),
492 internal_relation: *internal_relation,
493 internal_column_types: internal_column_types.clone(),
494 internal_column_collations: internal_column_collations.clone(),
495 },
496 FromClause::Function {
497 name,
498 binding,
499 output_name,
500 relations,
501 args,
502 alias,
503 column_aliases,
504 ordinality,
505 column_types,
506 } => FromClause::Function {
507 name: name.clone(),
508 binding: binding.clone(),
509 output_name: output_name.clone(),
510 relations: relations.clone(),
511 args: bind_exprs(args, r)?,
512 alias: alias.clone(),
513 column_aliases: column_aliases.clone(),
514 ordinality: *ordinality,
515 column_types: column_types.clone(),
516 },
517 FromClause::FunctionGroup {
518 functions,
519 alias,
520 column_aliases,
521 ordinality,
522 } => FromClause::FunctionGroup {
523 functions: functions
524 .iter()
525 .map(|function| {
526 Ok(crate::ast::TableFunction {
527 name: function.name.clone(),
528 binding: function.binding.clone(),
529 output_name: function.output_name.clone(),
530 relations: function.relations.clone(),
531 args: bind_exprs(&function.args, r)?,
532 column_aliases: function.column_aliases.clone(),
533 column_types: function.column_types.clone(),
534 })
535 })
536 .collect::<Result<Vec<_>>>()?,
537 alias: alias.clone(),
538 column_aliases: column_aliases.clone(),
539 ordinality: *ordinality,
540 },
541 FromClause::Subquery {
542 body,
543 alias,
544 column_aliases,
545 } => FromClause::Subquery {
546 body: Box::new(bind_select(body, r)?),
547 alias: alias.clone(),
548 column_aliases: column_aliases.clone(),
549 },
550 })
551}
552
553#[expect(
557 clippy::too_many_lines,
558 reason = "PL/pgSQL lowering preserves parser order and datum validation"
559)]
560pub fn bind_statement(stmt: &Statement, r: &mut dyn VariableResolver) -> Result<Statement> {
561 Ok(match stmt {
562 Statement::Select(body) => Statement::Select(Box::new(bind_select(body, r)?)),
563 Statement::Insert(insert) => {
564 let mut out = insert.clone();
565 out.columns = insert
566 .columns
567 .iter()
568 .map(|target| bind_assignment_target(target, r))
569 .collect::<Result<_>>()?;
570 out.with = bind_ctes(&insert.with, r)?;
571 out.rows = bind_rows(&insert.rows, r)?;
572 out.select_source = match insert.select_source.as_ref() {
573 Some(body) => Some(Box::new(bind_select(body, r)?)),
574 None => None,
575 };
576 out.on_conflict = match insert.on_conflict.as_ref() {
577 Some(oc) => Some(crate::ast::OnConflict {
578 predicate: bind_opt_expr(oc.predicate.as_deref(), r)?.map(Box::new),
579 constraint: oc.constraint.clone(),
580 conflict_columns: oc.conflict_columns.clone(),
581 expressions: oc
582 .expressions
583 .iter()
584 .map(|expr| bind_expr(expr, r))
585 .collect::<Result<Vec<_>>>()?,
586 action: match &oc.action {
587 crate::ast::OnConflictAction::Nothing => {
588 crate::ast::OnConflictAction::Nothing
589 }
590 crate::ast::OnConflictAction::Update {
591 assignments,
592 r#where,
593 } => crate::ast::OnConflictAction::Update {
594 assignments: bind_assignments(assignments, r)?,
595 r#where: bind_opt_expr(r#where.as_deref(), r)?.map(Box::new),
596 },
597 },
598 }),
599 None => None,
600 };
601 out.returning = bind_projections(&insert.returning, r)?;
602 Statement::Insert(out)
603 }
604 Statement::Update(update) => {
605 let mut out = update.clone();
606 out.assignments = bind_assignments(&update.assignments, r)?;
607 out.r#where = bind_opt_expr(update.r#where.as_ref(), r)?;
608 out.with = bind_ctes(&update.with, r)?;
609 out.from = match update.from.as_ref() {
610 Some(f) => Some(bind_from(f, r)?),
611 None => None,
612 };
613 out.returning = bind_projections(&update.returning, r)?;
614 Statement::Update(out)
615 }
616 Statement::Delete(delete) => {
617 let mut out = delete.clone();
618 out.r#where = bind_opt_expr(delete.r#where.as_ref(), r)?;
619 out.with = bind_ctes(&delete.with, r)?;
620 out.using = match delete.using.as_ref() {
621 Some(f) => Some(bind_from(f, r)?),
622 None => None,
623 };
624 out.returning = bind_projections(&delete.returning, r)?;
625 Statement::Delete(out)
626 }
627 Statement::Values { rows } => Statement::Values {
628 rows: bind_rows(rows, r)?,
629 },
630 Statement::CreateTableAs {
631 name,
632 if_not_exists,
633 column_names,
634 with_no_data,
635 select_into,
636 persistence,
637 on_commit,
638 body,
639 } => Statement::CreateTableAs {
640 name: name.clone(),
641 if_not_exists: *if_not_exists,
642 column_names: column_names.clone(),
643 with_no_data: *with_no_data,
644 select_into: *select_into,
645 persistence: *persistence,
646 on_commit: *on_commit,
647 body: Box::new(bind_select(body, r)?),
648 },
649 Statement::CreateMaterializedView {
650 name,
651 column_names,
652 if_not_exists,
653 with_no_data,
654 options,
655 body,
656 } => Statement::CreateMaterializedView {
657 name: name.clone(),
658 column_names: column_names.clone(),
659 if_not_exists: *if_not_exists,
660 with_no_data: *with_no_data,
661 options: options.clone(),
662 body: Box::new(bind_select(body, r)?),
663 },
664 Statement::Explain {
665 analyze,
666 verbose,
667 format,
668 body,
669 } => Statement::Explain {
670 analyze: *analyze,
671 verbose: *verbose,
672 format: format.clone(),
673 body: Box::new(bind_statement(body, r)?),
674 },
675 Statement::DeclareCursor(cursor) => {
676 let mut out = cursor.clone();
677 out.query = Box::new(bind_select(&cursor.query, r)?);
678 Statement::DeclareCursor(out)
679 }
680 Statement::Merge(merge) => {
681 let mut out = merge.clone();
682 out.with = bind_ctes(&merge.with, r)?;
683 out.source = bind_from(&merge.source, r)?;
684 out.join_condition = bind_expr(&merge.join_condition, r)?;
685 out.when_clauses = merge
686 .when_clauses
687 .iter()
688 .map(|w| bind_merge_when(w, r))
689 .collect::<Result<Vec<_>>>()?;
690 out.returning = bind_projections(&merge.returning, r)?;
691 Statement::Merge(out)
692 }
693 Statement::Call { name, args } => Statement::Call {
694 name: name.clone(),
695 args: bind_exprs(args, r)?,
696 },
697 other => other.clone(),
698 })
699}
700
701pub(super) fn bind_merge_when(when: &MergeWhen, r: &mut dyn VariableResolver) -> Result<MergeWhen> {
702 Ok(match when {
703 MergeWhen::UpdateMatched {
704 condition,
705 assignments,
706 } => MergeWhen::UpdateMatched {
707 condition: bind_opt_expr(condition.as_ref(), r)?,
708 assignments: bind_assignments(assignments, r)?,
709 },
710 MergeWhen::DeleteMatched { condition } => MergeWhen::DeleteMatched {
711 condition: bind_opt_expr(condition.as_ref(), r)?,
712 },
713 MergeWhen::UpdateNotMatchedBySource {
714 condition,
715 assignments,
716 } => MergeWhen::UpdateNotMatchedBySource {
717 condition: bind_opt_expr(condition.as_ref(), r)?,
718 assignments: bind_assignments(assignments, r)?,
719 },
720 MergeWhen::DeleteNotMatchedBySource { condition } => MergeWhen::DeleteNotMatchedBySource {
721 condition: bind_opt_expr(condition.as_ref(), r)?,
722 },
723 MergeWhen::InsertNotMatched {
724 condition,
725 columns,
726 overriding,
727 values,
728 } => MergeWhen::InsertNotMatched {
729 condition: bind_opt_expr(condition.as_ref(), r)?,
730 columns: columns
731 .iter()
732 .map(|target| bind_assignment_target(target, r))
733 .collect::<Result<_>>()?,
734 overriding: *overriding,
735 values: bind_exprs(values, r)?,
736 },
737 MergeWhen::NothingMatched { condition } => MergeWhen::NothingMatched {
738 condition: bind_opt_expr(condition.as_ref(), r)?,
739 },
740 MergeWhen::NothingNotMatched { condition } => MergeWhen::NothingNotMatched {
741 condition: bind_opt_expr(condition.as_ref(), r)?,
742 },
743 MergeWhen::NothingNotMatchedBySource { condition } => {
744 MergeWhen::NothingNotMatchedBySource {
745 condition: bind_opt_expr(condition.as_ref(), r)?,
746 }
747 }
748 })
749}