1use crate::ast_transforms::get_output_column_names;
9use crate::dialects::{Dialect, DialectType};
10use crate::expressions::{DataType, Expression, JoinKind, TableRef, With};
11use crate::lineage::{lineage_by_index_from_expression, LineageNode};
12use crate::optimizer::annotate_types::annotate_types;
13use crate::optimizer::qualify_columns::{qualify_columns, QualifyColumnsOptions};
14use crate::schema::{MappingSchema, Schema};
15use crate::scope::{build_scope, Scope, SourceInfo, SourceKind};
16use crate::traversal::{contains_aggregate, ExpressionWalk};
17use crate::validation::{mapping_schema_from_validation_schema_with_dialect, ValidationSchema};
18use crate::{parse_one, Error, Result};
19use serde::{Deserialize, Serialize};
20use std::collections::{HashMap, HashSet};
21
22#[derive(Debug, Clone, Serialize, Deserialize, Default)]
24#[serde(rename_all = "camelCase", default)]
25pub struct AnalyzeQueryOptions {
26 pub dialect: DialectType,
28 pub schema: Option<ValidationSchema>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct QueryAnalysis {
36 pub shape: QueryShape,
37 pub ctes: Vec<String>,
38 pub cte_facts: Vec<CteFact>,
39 pub projections: Vec<ProjectionFact>,
40 pub relations: Vec<RelationFact>,
41 pub base_tables: Vec<RelationFact>,
42 pub star_projections: Vec<StarProjectionFact>,
43 pub set_operations: Vec<SetOperationFact>,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum QueryShape {
50 Select,
51 SetOperation,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct ProjectionFact {
58 pub index: usize,
59 pub name: Option<String>,
60 pub is_star: bool,
61 pub star_table: Option<String>,
62 pub transform_kind: TransformKind,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub transform_function: Option<TransformFunctionFact>,
65 pub cast_type: Option<String>,
66 pub type_hint: Option<String>,
67 pub nullability: ProjectionNullability,
68 pub upstream: Vec<ColumnReferenceFact>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct TransformFunctionFact {
75 pub name: String,
76 pub literal_args: Vec<String>,
77 pub column_args: Vec<ColumnReferenceFact>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct CteFact {
84 pub name: String,
85 pub columns: Vec<String>,
86 pub body_sql: String,
87 pub output_columns: Vec<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct StarProjectionFact {
94 pub index: usize,
95 pub table: Option<String>,
96 pub expanded_columns: Vec<String>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct ColumnReferenceFact {
103 pub source_name: Option<String>,
104 pub source_alias: Option<String>,
105 pub source_kind: SourceKind,
106 pub table: Option<String>,
107 pub column: String,
108 pub unqualified: bool,
109 pub confidence: ReferenceConfidence,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct RelationFact {
116 pub name: String,
117 pub alias: Option<String>,
118 pub kind: SourceKind,
119 pub columns: Vec<String>,
120 pub catalog: Option<String>,
121 pub schema: Option<String>,
122 pub table: Option<String>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct SetOperationFact {
129 pub kind: String,
130 pub all: bool,
131 pub distinct: bool,
132 pub output_columns: Vec<String>,
133 pub branches: Vec<SetOperationBranchFact>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct SetOperationBranchFact {
140 pub index: usize,
141 pub role: SetOperationBranchRole,
142 pub projections: Vec<ProjectionFact>,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum SetOperationBranchRole {
149 Value,
150 Filter,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(rename_all = "snake_case")]
156pub enum TransformKind {
157 Direct,
158 Cast,
159 Aggregation,
160 Constant,
161 Expression,
162 Star,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "snake_case")]
168pub enum ReferenceConfidence {
169 Resolved,
170 Ambiguous,
171 Unknown,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "snake_case")]
177pub enum ProjectionNullability {
178 NonNull,
179 Nullable,
180 Unknown,
181}
182
183pub fn analyze_query(sql: &str, options: AnalyzeQueryOptions) -> Result<QueryAnalysis> {
185 let mut expression = parse_one(sql, options.dialect)?;
186 expression = effective_query(expression);
187 ensure_query(&expression)?;
188 let original_expression = expression.clone();
189
190 let mapping_schema = options
191 .schema
192 .as_ref()
193 .map(|schema| analysis_mapping_schema(schema, options.dialect));
194 let schema_info = options.schema.as_ref().map(AnalysisSchemaInfo::from_schema);
195 let cte_facts = top_level_cte_facts(&original_expression, options.dialect)?;
196 let star_projections = star_projection_facts(&original_expression, mapping_schema.as_ref());
197
198 if let Some(schema) = mapping_schema.as_ref() {
199 let qualify_options = QualifyColumnsOptions::new()
200 .with_dialect(options.dialect)
201 .with_allow_partial(true);
202 expression = qualify_columns(expression, schema, &qualify_options)
203 .map_err(|e| Error::internal(format!("query analysis qualification failed: {e}")))?;
204 }
205
206 annotate_types(
207 &mut expression,
208 mapping_schema.as_ref().map(|schema| schema as &dyn Schema),
209 Some(options.dialect),
210 );
211 crate::lineage::expand_cte_stars(
212 &mut expression,
213 mapping_schema.as_ref().map(|schema| schema as &dyn Schema),
214 );
215
216 let scope = build_scope(&expression);
217 let nullability_context = NullabilityContext {
218 schema: schema_info.as_ref(),
219 nullable_sources: nullable_source_names(&expression),
220 };
221 let shape = if is_set_operation(&expression) {
222 QueryShape::SetOperation
223 } else {
224 QueryShape::Select
225 };
226
227 Ok(QueryAnalysis {
228 shape,
229 ctes: collect_cte_names(&expression),
230 cte_facts,
231 projections: projection_facts_for_query(
232 &expression,
233 &scope,
234 options.dialect,
235 &nullability_context,
236 ),
237 relations: relation_facts(&scope, mapping_schema.as_ref()),
238 base_tables: base_table_facts(&scope, mapping_schema.as_ref()),
239 star_projections,
240 set_operations: set_operation_facts(&expression, &scope, options.dialect),
241 })
242}
243
244fn analysis_mapping_schema(schema: &ValidationSchema, dialect: DialectType) -> MappingSchema {
245 mapping_schema_from_validation_schema_with_dialect(schema, dialect)
246}
247
248fn validation_table_names(table: &crate::validation::SchemaTable) -> Vec<String> {
249 let mut names = Vec::new();
250
251 names.push(table.name.to_ascii_lowercase());
252 if let Some(schema_name) = &table.schema {
253 names.push(format!(
254 "{}.{}",
255 schema_name.to_ascii_lowercase(),
256 table.name.to_ascii_lowercase()
257 ));
258 }
259 for alias in &table.aliases {
260 names.push(alias.to_ascii_lowercase());
261 }
262
263 names.sort();
264 names.dedup();
265 names
266}
267
268#[derive(Debug, Clone)]
269struct AnalysisColumnInfo {
270 nullable: Option<bool>,
271 primary_key: bool,
272}
273
274#[derive(Debug, Clone)]
275struct AnalysisSchemaInfo {
276 columns: HashMap<(String, String), AnalysisColumnInfo>,
277}
278
279impl AnalysisSchemaInfo {
280 fn from_schema(schema: &ValidationSchema) -> Self {
281 let mut columns = HashMap::new();
282
283 for table in &schema.tables {
284 let table_names = validation_table_names(table);
285 let primary_keys: HashSet<String> = table
286 .primary_key
287 .iter()
288 .map(|column| column.to_ascii_lowercase())
289 .collect();
290
291 for column in &table.columns {
292 let info = AnalysisColumnInfo {
293 nullable: column.nullable,
294 primary_key: column.primary_key
295 || primary_keys.contains(&column.name.to_ascii_lowercase()),
296 };
297
298 for table_name in &table_names {
299 columns.insert(
300 (
301 normalize_lookup_name(table_name),
302 normalize_lookup_name(&column.name),
303 ),
304 info.clone(),
305 );
306 }
307 }
308 }
309
310 Self { columns }
311 }
312
313 fn column(&self, table: &str, column: &str) -> Option<&AnalysisColumnInfo> {
314 self.columns
315 .get(&(normalize_lookup_name(table), normalize_lookup_name(column)))
316 }
317}
318
319struct NullabilityContext<'a> {
320 schema: Option<&'a AnalysisSchemaInfo>,
321 nullable_sources: HashSet<String>,
322}
323
324fn top_level_cte_facts(expression: &Expression, dialect: DialectType) -> Result<Vec<CteFact>> {
325 let Some(with_clause) = with_clause(expression) else {
326 return Ok(Vec::new());
327 };
328
329 with_clause
330 .ctes
331 .iter()
332 .map(|cte| {
333 Ok(CteFact {
334 name: cte.alias.name.clone(),
335 columns: cte
336 .columns
337 .iter()
338 .map(|column| column.name.clone())
339 .collect(),
340 body_sql: Dialect::get(dialect).generate(&cte.this)?,
341 output_columns: get_output_column_names(&cte.this),
342 })
343 })
344 .collect()
345}
346
347fn star_projection_facts(
348 expression: &Expression,
349 mapping_schema: Option<&MappingSchema>,
350) -> Vec<StarProjectionFact> {
351 let scope = build_scope(expression);
352 let ordered_sources = ordered_source_names_for_query(expression);
353
354 select_expressions_for_query(expression)
355 .iter()
356 .enumerate()
357 .filter_map(|(index, projection)| {
358 let inner = unwrap_projection_alias(projection);
359 if !projection_is_star(inner) {
360 return None;
361 }
362
363 let table = projection_star_table(inner);
364 let expanded_columns =
365 expanded_star_columns(table.as_deref(), &scope, &ordered_sources, mapping_schema);
366
367 Some(StarProjectionFact {
368 index,
369 table,
370 expanded_columns,
371 })
372 })
373 .collect()
374}
375
376fn expanded_star_columns(
377 star_table: Option<&str>,
378 scope: &Scope,
379 ordered_sources: &[String],
380 mapping_schema: Option<&MappingSchema>,
381) -> Vec<String> {
382 let mut columns = Vec::new();
383 let mut source_names: Vec<String> = if ordered_sources.is_empty() {
384 let mut names: Vec<_> = scope.sources.keys().cloned().collect();
385 names.sort();
386 names
387 } else {
388 ordered_sources.to_vec()
389 };
390
391 source_names.dedup();
392
393 for source_name in source_names {
394 let Some(source) = scope.sources.get(&source_name) else {
395 continue;
396 };
397
398 if let Some(star_table) = star_table {
399 let matches = source_name.eq_ignore_ascii_case(star_table)
400 || source
401 .alias
402 .as_deref()
403 .is_some_and(|alias| alias.eq_ignore_ascii_case(star_table))
404 || source_table_name(source)
405 .is_some_and(|table| table.eq_ignore_ascii_case(star_table));
406
407 if !matches {
408 continue;
409 }
410 }
411
412 columns.extend(source_columns(source, mapping_schema));
413 }
414
415 columns
416}
417
418fn ordered_source_names_for_query(expression: &Expression) -> Vec<String> {
419 match expression {
420 Expression::Select(select) => ordered_source_names_for_select(select),
421 Expression::Union(union) => ordered_source_names_for_query(&union.left),
422 Expression::Intersect(intersect) => ordered_source_names_for_query(&intersect.left),
423 Expression::Except(except) => ordered_source_names_for_query(&except.left),
424 Expression::Subquery(subquery) => ordered_source_names_for_query(&subquery.this),
425 _ => Vec::new(),
426 }
427}
428
429fn ordered_source_names_for_select(select: &crate::expressions::Select) -> Vec<String> {
430 let mut sources = Vec::new();
431
432 if let Some(from) = &select.from {
433 for expression in &from.expressions {
434 if let Some(source_name) = expression_source_name(expression) {
435 sources.push(source_name);
436 }
437 }
438 }
439
440 for join in &select.joins {
441 if let Some(source_name) = expression_source_name(&join.this) {
442 sources.push(source_name);
443 }
444 }
445
446 sources
447}
448
449fn nullable_source_names(expression: &Expression) -> HashSet<String> {
450 match expression {
451 Expression::Select(select) => nullable_source_names_for_select(select),
452 Expression::Union(union) => nullable_source_names(&union.left),
453 Expression::Intersect(intersect) => nullable_source_names(&intersect.left),
454 Expression::Except(except) => nullable_source_names(&except.left),
455 Expression::Subquery(subquery) => nullable_source_names(&subquery.this),
456 _ => HashSet::new(),
457 }
458}
459
460fn nullable_source_names_for_select(select: &crate::expressions::Select) -> HashSet<String> {
461 let mut nullable = HashSet::new();
462 let mut left_sources = Vec::new();
463
464 if let Some(from) = &select.from {
465 for expression in &from.expressions {
466 if let Some(source_name) = expression_source_name(expression) {
467 left_sources.push(source_name);
468 }
469 }
470 }
471
472 for join in &select.joins {
473 let right_source = expression_source_name(&join.this);
474
475 if join_nullable_left(join.kind) {
476 for source_name in &left_sources {
477 nullable.insert(normalize_lookup_name(source_name));
478 }
479 }
480
481 if join_nullable_right(join.kind) {
482 if let Some(source_name) = &right_source {
483 nullable.insert(normalize_lookup_name(source_name));
484 }
485 }
486
487 if let Some(source_name) = right_source {
488 left_sources.push(source_name);
489 }
490 }
491
492 nullable
493}
494
495fn join_nullable_left(kind: JoinKind) -> bool {
496 matches!(
497 kind,
498 JoinKind::Right
499 | JoinKind::NaturalRight
500 | JoinKind::AsOfRight
501 | JoinKind::Full
502 | JoinKind::NaturalFull
503 | JoinKind::Outer
504 )
505}
506
507fn join_nullable_right(kind: JoinKind) -> bool {
508 matches!(
509 kind,
510 JoinKind::Left
511 | JoinKind::NaturalLeft
512 | JoinKind::AsOfLeft
513 | JoinKind::LeftLateral
514 | JoinKind::OuterApply
515 | JoinKind::LeftArray
516 | JoinKind::Full
517 | JoinKind::NaturalFull
518 | JoinKind::Outer
519 )
520}
521
522fn expression_source_name(expression: &Expression) -> Option<String> {
523 match expression {
524 Expression::Table(table) => table
525 .alias
526 .as_ref()
527 .map(|alias| alias.name.clone())
528 .or_else(|| Some(table.name.name.clone())),
529 Expression::Subquery(subquery) => subquery.alias.as_ref().map(|alias| alias.name.clone()),
530 Expression::Alias(alias) => Some(alias.alias.name.clone()),
531 Expression::Cte(cte) => Some(cte.alias.name.clone()),
532 _ => None,
533 }
534}
535
536fn normalize_lookup_name(name: &str) -> String {
537 name.to_ascii_lowercase()
538}
539
540fn effective_query(expression: Expression) -> Expression {
541 match expression {
542 Expression::Prepare(prepare) => prepare.statement,
543 Expression::Subquery(subquery) if subquery.alias.is_none() => subquery.this,
544 other => other,
545 }
546}
547
548fn ensure_query(expression: &Expression) -> Result<()> {
549 if matches!(
550 expression,
551 Expression::Select(_)
552 | Expression::Union(_)
553 | Expression::Intersect(_)
554 | Expression::Except(_)
555 ) {
556 Ok(())
557 } else {
558 Err(Error::internal(
559 "analyze_query requires a SELECT or set operation query",
560 ))
561 }
562}
563
564fn is_set_operation(expression: &Expression) -> bool {
565 matches!(
566 expression,
567 Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_)
568 )
569}
570
571fn collect_cte_names(expression: &Expression) -> Vec<String> {
572 let mut names = Vec::new();
573 let mut seen = HashSet::new();
574 collect_cte_names_inner(expression, &mut names, &mut seen);
575 names
576}
577
578fn collect_cte_names_inner(
579 expression: &Expression,
580 names: &mut Vec<String>,
581 seen: &mut HashSet<String>,
582) {
583 if let Some(with_clause) = with_clause(expression) {
584 collect_with_names(with_clause, names, seen);
585 }
586
587 match expression {
588 Expression::Union(union) => {
589 collect_cte_names_inner(&union.left, names, seen);
590 collect_cte_names_inner(&union.right, names, seen);
591 }
592 Expression::Intersect(intersect) => {
593 collect_cte_names_inner(&intersect.left, names, seen);
594 collect_cte_names_inner(&intersect.right, names, seen);
595 }
596 Expression::Except(except) => {
597 collect_cte_names_inner(&except.left, names, seen);
598 collect_cte_names_inner(&except.right, names, seen);
599 }
600 Expression::Subquery(subquery) => collect_cte_names_inner(&subquery.this, names, seen),
601 _ => {}
602 }
603}
604
605fn collect_with_names(with_clause: &With, names: &mut Vec<String>, seen: &mut HashSet<String>) {
606 for cte in &with_clause.ctes {
607 if seen.insert(cte.alias.name.clone()) {
608 names.push(cte.alias.name.clone());
609 }
610 collect_cte_names_inner(&cte.this, names, seen);
611 }
612}
613
614fn with_clause(expression: &Expression) -> Option<&With> {
615 match expression {
616 Expression::Select(select) => select.with.as_ref(),
617 Expression::Union(union) => union.with.as_ref(),
618 Expression::Intersect(intersect) => intersect.with.as_ref(),
619 Expression::Except(except) => except.with.as_ref(),
620 _ => None,
621 }
622}
623
624fn projection_facts_for_query(
625 expression: &Expression,
626 scope: &Scope,
627 dialect: DialectType,
628 nullability_context: &NullabilityContext<'_>,
629) -> Vec<ProjectionFact> {
630 let expressions = select_expressions_for_query(expression);
631 let names = get_output_column_names(expression);
632
633 expressions
634 .iter()
635 .enumerate()
636 .map(|(index, projection)| {
637 projection_fact(
638 index,
639 names
640 .get(index)
641 .cloned()
642 .or_else(|| projection_name(projection)),
643 projection,
644 expression,
645 scope,
646 dialect,
647 nullability_context,
648 )
649 })
650 .collect()
651}
652
653fn select_expressions_for_query(expression: &Expression) -> Vec<&Expression> {
654 match expression {
655 Expression::Select(select) => select.expressions.iter().collect(),
656 Expression::Union(union) => select_expressions_for_query(&union.left),
657 Expression::Intersect(intersect) => select_expressions_for_query(&intersect.left),
658 Expression::Except(except) => select_expressions_for_query(&except.left),
659 Expression::Subquery(subquery) => select_expressions_for_query(&subquery.this),
660 _ => Vec::new(),
661 }
662}
663
664fn projection_fact(
665 index: usize,
666 name: Option<String>,
667 projection: &Expression,
668 query: &Expression,
669 scope: &Scope,
670 dialect: DialectType,
671 nullability_context: &NullabilityContext<'_>,
672) -> ProjectionFact {
673 let inner = unwrap_projection_alias(projection);
674 let is_star = projection_is_star(inner);
675 let upstream = lineage_by_index_from_expression(index, query, Some(dialect), false)
676 .map(|node| terminal_references_from_lineage(&node))
677 .ok()
678 .filter(|refs| !refs.is_empty())
679 .unwrap_or_else(|| fallback_column_references(inner, scope));
680
681 ProjectionFact {
682 index,
683 name,
684 is_star,
685 star_table: projection_star_table(inner),
686 transform_kind: transform_kind(inner),
687 transform_function: transform_function_fact(inner, scope, dialect),
688 cast_type: cast_type(inner, dialect),
689 type_hint: projection
690 .inferred_type()
691 .or_else(|| inner.inferred_type())
692 .and_then(|data_type| render_data_type(data_type, dialect)),
693 nullability: projection_nullability(inner, scope, nullability_context),
694 upstream,
695 }
696}
697
698fn transform_function_fact(
699 expression: &Expression,
700 scope: &Scope,
701 dialect: DialectType,
702) -> Option<TransformFunctionFact> {
703 let mut matches = expression
704 .find_all(|candidate| transform_function_fact_for_node(candidate, scope, dialect).is_some())
705 .into_iter();
706
707 let first = matches.next()?;
708 if matches.next().is_some() {
709 return None;
710 }
711
712 transform_function_fact_for_node(first, scope, dialect)
713}
714
715fn transform_function_fact_for_node(
716 expression: &Expression,
717 scope: &Scope,
718 dialect: DialectType,
719) -> Option<TransformFunctionFact> {
720 match expression {
721 Expression::Function(function) => Some(transform_function_from_args(
722 &function.name,
723 &function.args,
724 scope,
725 dialect,
726 )),
727 Expression::AggregateFunction(function) => Some(transform_function_from_args(
728 &function.name,
729 &function.args,
730 scope,
731 dialect,
732 )),
733 Expression::DateTrunc(function) => Some(transform_function_from_parts(
734 "DATE_TRUNC",
735 vec![datetime_field_name(&function.unit)],
736 vec![&function.this],
737 scope,
738 dialect,
739 )),
740 Expression::TimestampTrunc(function) => Some(transform_function_from_parts(
741 "TIMESTAMP_TRUNC",
742 vec![datetime_field_name(&function.unit)],
743 vec![&function.this],
744 scope,
745 dialect,
746 )),
747 Expression::TimeTrunc(function) => {
748 let mut args = vec![function.this.as_ref()];
749 if let Some(zone) = function.zone.as_deref() {
750 args.push(zone);
751 }
752 Some(transform_function_from_parts(
753 "TIME_TRUNC",
754 vec![function.unit.clone()],
755 args,
756 scope,
757 dialect,
758 ))
759 }
760 Expression::Extract(function) => Some(transform_function_from_parts(
761 "EXTRACT",
762 vec![datetime_field_name(&function.field)],
763 vec![&function.this],
764 scope,
765 dialect,
766 )),
767 Expression::DateAdd(function) => Some(transform_function_from_parts(
768 "DATE_ADD",
769 Vec::new(),
770 vec![&function.this, &function.interval],
771 scope,
772 dialect,
773 )),
774 Expression::DateSub(function) => Some(transform_function_from_parts(
775 "DATE_SUB",
776 Vec::new(),
777 vec![&function.this, &function.interval],
778 scope,
779 dialect,
780 )),
781 Expression::DateDiff(function) => Some(transform_function_from_parts(
782 "DATE_DIFF",
783 Vec::new(),
784 vec![&function.this, &function.expression],
785 scope,
786 dialect,
787 )),
788 _ => None,
789 }
790}
791
792fn transform_function_from_args(
793 name: &str,
794 args: &[Expression],
795 scope: &Scope,
796 dialect: DialectType,
797) -> TransformFunctionFact {
798 let literal_args = args
799 .iter()
800 .filter_map(|arg| literal_argument(arg, dialect))
801 .collect();
802 transform_function_from_parts(name, literal_args, args.iter().collect(), scope, dialect)
803}
804
805fn transform_function_from_parts(
806 name: &str,
807 literal_args: Vec<String>,
808 args: Vec<&Expression>,
809 scope: &Scope,
810 _dialect: DialectType,
811) -> TransformFunctionFact {
812 let column_args = dedupe_column_refs(
813 args.into_iter()
814 .flat_map(|arg| fallback_column_references(arg, scope))
815 .collect(),
816 );
817
818 TransformFunctionFact {
819 name: name.to_string(),
820 literal_args,
821 column_args,
822 }
823}
824
825fn literal_argument(expression: &Expression, dialect: DialectType) -> Option<String> {
826 match expression {
827 Expression::Literal(literal) => Some(literal.value_str().to_string()),
828 Expression::Boolean(boolean) => Some(boolean.value.to_string()),
829 Expression::Null(_) => Some("NULL".to_string()),
830 Expression::Identifier(identifier) => Some(identifier.name.clone()),
831 Expression::Var(var) => Some(var.this.clone()),
832 Expression::DataType(data_type) => render_data_type(data_type, dialect),
833 _ => None,
834 }
835}
836
837fn datetime_field_name(field: &crate::expressions::DateTimeField) -> String {
838 match field {
839 crate::expressions::DateTimeField::Year => "year".to_string(),
840 crate::expressions::DateTimeField::Month => "month".to_string(),
841 crate::expressions::DateTimeField::Day => "day".to_string(),
842 crate::expressions::DateTimeField::Hour => "hour".to_string(),
843 crate::expressions::DateTimeField::Minute => "minute".to_string(),
844 crate::expressions::DateTimeField::Second => "second".to_string(),
845 crate::expressions::DateTimeField::Millisecond => "millisecond".to_string(),
846 crate::expressions::DateTimeField::Microsecond => "microsecond".to_string(),
847 crate::expressions::DateTimeField::DayOfWeek => "day_of_week".to_string(),
848 crate::expressions::DateTimeField::DayOfYear => "day_of_year".to_string(),
849 crate::expressions::DateTimeField::Week => "week".to_string(),
850 crate::expressions::DateTimeField::WeekWithModifier(modifier) => {
851 format!("week({modifier})")
852 }
853 crate::expressions::DateTimeField::Quarter => "quarter".to_string(),
854 crate::expressions::DateTimeField::Epoch => "epoch".to_string(),
855 crate::expressions::DateTimeField::Timezone => "timezone".to_string(),
856 crate::expressions::DateTimeField::TimezoneHour => "timezone_hour".to_string(),
857 crate::expressions::DateTimeField::TimezoneMinute => "timezone_minute".to_string(),
858 crate::expressions::DateTimeField::Date => "date".to_string(),
859 crate::expressions::DateTimeField::Time => "time".to_string(),
860 crate::expressions::DateTimeField::Custom(name) => name.clone(),
861 }
862}
863
864fn unwrap_projection_alias(expression: &Expression) -> &Expression {
865 match expression {
866 Expression::Alias(alias) => unwrap_projection_alias(&alias.this),
867 Expression::Annotated(annotated) => unwrap_projection_alias(&annotated.this),
868 Expression::Paren(paren) => unwrap_projection_alias(&paren.this),
869 _ => expression,
870 }
871}
872
873fn projection_name(expression: &Expression) -> Option<String> {
874 match expression {
875 Expression::Alias(alias) => Some(alias.alias.name.clone()),
876 Expression::Column(column) => Some(column.name.name.clone()),
877 Expression::Identifier(identifier) => Some(identifier.name.clone()),
878 Expression::Star(_) => Some("*".to_string()),
879 Expression::Annotated(annotated) => projection_name(&annotated.this),
880 _ => None,
881 }
882}
883
884fn projection_is_star(expression: &Expression) -> bool {
885 matches!(expression, Expression::Star(_))
886 || matches!(expression, Expression::Column(column) if column.name.name == "*")
887}
888
889fn projection_star_table(expression: &Expression) -> Option<String> {
890 match expression {
891 Expression::Star(star) => star
892 .table
893 .as_ref()
894 .map(|identifier| identifier.name.clone()),
895 Expression::Column(column) if column.name.name == "*" => column
896 .table
897 .as_ref()
898 .map(|identifier| identifier.name.clone()),
899 _ => None,
900 }
901}
902
903fn transform_kind(expression: &Expression) -> TransformKind {
904 if projection_is_star(expression) {
905 TransformKind::Star
906 } else if is_cast_expression(expression) {
907 TransformKind::Cast
908 } else if contains_aggregate(expression) {
909 TransformKind::Aggregation
910 } else if matches!(
911 expression,
912 Expression::Column(_) | Expression::Identifier(_)
913 ) {
914 TransformKind::Direct
915 } else if is_simple_constant(expression) {
916 TransformKind::Constant
917 } else {
918 TransformKind::Expression
919 }
920}
921
922fn is_cast_expression(expression: &Expression) -> bool {
923 matches!(
924 expression,
925 Expression::Cast(_) | Expression::TryCast(_) | Expression::SafeCast(_)
926 )
927}
928
929fn cast_type(expression: &Expression, dialect: DialectType) -> Option<String> {
930 match expression {
931 Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
932 render_data_type(&cast.to, dialect)
933 }
934 _ => None,
935 }
936}
937
938fn render_data_type(data_type: &DataType, dialect: DialectType) -> Option<String> {
939 Dialect::get(dialect)
940 .generate(&Expression::DataType(data_type.clone()))
941 .ok()
942}
943
944fn is_simple_constant(expression: &Expression) -> bool {
945 match expression {
946 Expression::Literal(_) | Expression::Boolean(_) | Expression::Null(_) => true,
947 Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
948 is_simple_constant(&cast.this)
949 }
950 Expression::Neg(unary) | Expression::BitwiseNot(unary) => is_simple_constant(&unary.this),
951 _ => false,
952 }
953}
954
955fn projection_nullability(
956 expression: &Expression,
957 scope: &Scope,
958 context: &NullabilityContext<'_>,
959) -> ProjectionNullability {
960 match expression {
961 Expression::Alias(alias) => projection_nullability(&alias.this, scope, context),
962 Expression::Annotated(annotated) => projection_nullability(&annotated.this, scope, context),
963 Expression::Paren(paren) => projection_nullability(&paren.this, scope, context),
964 Expression::Literal(_) | Expression::Boolean(_) => ProjectionNullability::NonNull,
965 Expression::Null(_) => ProjectionNullability::Nullable,
966 Expression::Count(_) | Expression::CountIf(_) => ProjectionNullability::NonNull,
967 Expression::Cast(cast) => projection_nullability(&cast.this, scope, context),
968 Expression::TryCast(_) | Expression::SafeCast(_) => ProjectionNullability::Unknown,
969 Expression::Column(column) => column_nullability(
970 &column.name.name,
971 column.table.as_ref().map(|table| table.name.as_str()),
972 scope,
973 context,
974 ),
975 Expression::Identifier(identifier) => {
976 column_nullability(&identifier.name, None, scope, context)
977 }
978 Expression::Coalesce(func) => coalesce_nullability(&func.expressions, scope, context),
979 _ => ProjectionNullability::Unknown,
980 }
981}
982
983fn column_nullability(
984 column_name: &str,
985 source_name: Option<&str>,
986 scope: &Scope,
987 context: &NullabilityContext<'_>,
988) -> ProjectionNullability {
989 let resolved_source_name = source_name
990 .map(str::to_string)
991 .or_else(|| single_scope_source_name(scope));
992
993 if let Some(source_name) = &resolved_source_name {
994 if context
995 .nullable_sources
996 .contains(&normalize_lookup_name(source_name))
997 {
998 return ProjectionNullability::Nullable;
999 }
1000 }
1001
1002 let Some(schema) = context.schema else {
1003 return ProjectionNullability::Unknown;
1004 };
1005
1006 let table_name = resolved_source_name
1007 .as_ref()
1008 .and_then(|name| scope.sources.get(name).and_then(source_table_name))
1009 .or(resolved_source_name);
1010
1011 let Some(table_name) = table_name else {
1012 return ProjectionNullability::Unknown;
1013 };
1014
1015 match schema.column(&table_name, column_name) {
1016 Some(info) if info.primary_key || info.nullable == Some(false) => {
1017 ProjectionNullability::NonNull
1018 }
1019 Some(info) if info.nullable == Some(true) => ProjectionNullability::Nullable,
1020 Some(_) | None => ProjectionNullability::Unknown,
1021 }
1022}
1023
1024fn single_scope_source_name(scope: &Scope) -> Option<String> {
1025 if scope.sources.len() == 1 {
1026 scope.sources.keys().next().cloned()
1027 } else {
1028 None
1029 }
1030}
1031
1032fn coalesce_nullability(
1033 expressions: &[Expression],
1034 scope: &Scope,
1035 context: &NullabilityContext<'_>,
1036) -> ProjectionNullability {
1037 if expressions.is_empty() {
1038 return ProjectionNullability::Unknown;
1039 }
1040
1041 let mut all_nullable = true;
1042
1043 for expression in expressions {
1044 match projection_nullability(unwrap_projection_alias(expression), scope, context) {
1045 ProjectionNullability::NonNull => return ProjectionNullability::NonNull,
1046 ProjectionNullability::Nullable => {}
1047 ProjectionNullability::Unknown => all_nullable = false,
1048 }
1049 }
1050
1051 if all_nullable {
1052 ProjectionNullability::Nullable
1053 } else {
1054 ProjectionNullability::Unknown
1055 }
1056}
1057
1058fn terminal_references_from_lineage(node: &LineageNode) -> Vec<ColumnReferenceFact> {
1059 let mut refs = Vec::new();
1060 collect_terminal_references(node, &mut refs);
1061 dedupe_column_refs(refs)
1062}
1063
1064fn collect_terminal_references(node: &LineageNode, refs: &mut Vec<ColumnReferenceFact>) {
1065 if node.downstream.is_empty() {
1066 if let Some(reference) = column_reference_from_lineage_node(node) {
1067 refs.push(reference);
1068 }
1069 return;
1070 }
1071
1072 for child in &node.downstream {
1073 collect_terminal_references(child, refs);
1074 }
1075}
1076
1077fn column_reference_from_lineage_node(node: &LineageNode) -> Option<ColumnReferenceFact> {
1078 match &node.expression {
1079 Expression::Column(column) => {
1080 let source_name = non_empty_string(node.source_name.clone());
1081 let table =
1082 lineage_node_table(node).or_else(|| column.table.as_ref().map(|t| t.name.clone()));
1083 let confidence = if node.source_kind == SourceKind::Unknown && source_name.is_none() {
1084 ReferenceConfidence::Unknown
1085 } else {
1086 ReferenceConfidence::Resolved
1087 };
1088 Some(ColumnReferenceFact {
1089 source_name,
1090 source_alias: node.source_alias.clone(),
1091 source_kind: node.source_kind,
1092 table,
1093 column: column.name.name.clone(),
1094 unqualified: column.table.is_none(),
1095 confidence,
1096 })
1097 }
1098 Expression::Star(_) => Some(ColumnReferenceFact {
1099 source_name: non_empty_string(node.source_name.clone()),
1100 source_alias: node.source_alias.clone(),
1101 source_kind: node.source_kind,
1102 table: lineage_node_table(node),
1103 column: "*".to_string(),
1104 unqualified: true,
1105 confidence: if node.source_kind == SourceKind::Unknown {
1106 ReferenceConfidence::Unknown
1107 } else {
1108 ReferenceConfidence::Resolved
1109 },
1110 }),
1111 _ => None,
1112 }
1113}
1114
1115fn lineage_node_table(node: &LineageNode) -> Option<String> {
1116 match &node.source {
1117 Expression::Table(table) => Some(table_name(table)),
1118 _ => None,
1119 }
1120}
1121
1122fn fallback_column_references(expression: &Expression, scope: &Scope) -> Vec<ColumnReferenceFact> {
1123 let mut refs = Vec::new();
1124 let source_count = scope.sources.len();
1125 let single_source = if source_count == 1 {
1126 scope.sources.iter().next()
1127 } else {
1128 None
1129 };
1130
1131 for column_expr in expression.find_all(|candidate| matches!(candidate, Expression::Column(_))) {
1132 if let Expression::Column(column) = column_expr {
1133 if column.name.name == "*" {
1134 continue;
1135 }
1136 let source = column
1137 .table
1138 .as_ref()
1139 .and_then(|table| scope.sources.get(&table.name));
1140 let (source_name, source_alias, source_kind, table, confidence) =
1141 if let Some(table_identifier) = &column.table {
1142 if let Some(source) = source {
1143 (
1144 Some(table_identifier.name.clone()),
1145 source.alias.clone(),
1146 source.kind,
1147 source_table_name(source)
1148 .or_else(|| Some(table_identifier.name.clone())),
1149 ReferenceConfidence::Resolved,
1150 )
1151 } else {
1152 (
1153 Some(table_identifier.name.clone()),
1154 None,
1155 SourceKind::Unknown,
1156 Some(table_identifier.name.clone()),
1157 ReferenceConfidence::Unknown,
1158 )
1159 }
1160 } else if let Some((name, source)) = single_source {
1161 (
1162 Some(name.clone()),
1163 source.alias.clone(),
1164 source.kind,
1165 source_table_name(source).or_else(|| Some(name.clone())),
1166 ReferenceConfidence::Resolved,
1167 )
1168 } else if source_count > 1 {
1169 (
1170 None,
1171 None,
1172 SourceKind::Unknown,
1173 None,
1174 ReferenceConfidence::Ambiguous,
1175 )
1176 } else {
1177 (
1178 None,
1179 None,
1180 SourceKind::Unknown,
1181 None,
1182 ReferenceConfidence::Unknown,
1183 )
1184 };
1185
1186 refs.push(ColumnReferenceFact {
1187 source_name,
1188 source_alias,
1189 source_kind,
1190 table,
1191 column: column.name.name.clone(),
1192 unqualified: column.table.is_none(),
1193 confidence,
1194 });
1195 }
1196 }
1197
1198 dedupe_column_refs(refs)
1199}
1200
1201fn dedupe_column_refs(refs: Vec<ColumnReferenceFact>) -> Vec<ColumnReferenceFact> {
1202 let mut seen = HashSet::new();
1203 let mut deduped = Vec::new();
1204
1205 for reference in refs {
1206 let key = (
1207 reference.source_name.clone(),
1208 reference.source_alias.clone(),
1209 reference.table.clone(),
1210 reference.column.clone(),
1211 format!("{:?}", reference.source_kind),
1212 reference.unqualified,
1213 format!("{:?}", reference.confidence),
1214 );
1215 if seen.insert(key) {
1216 deduped.push(reference);
1217 }
1218 }
1219
1220 deduped
1221}
1222
1223fn relation_facts(
1224 scope: &Scope,
1225 mapping_schema: Option<&crate::schema::MappingSchema>,
1226) -> Vec<RelationFact> {
1227 let mut relations = Vec::new();
1228 let mut seen = HashSet::new();
1229 collect_relation_facts(scope, mapping_schema, &mut seen, &mut relations);
1230
1231 relations.sort_by(|left, right| {
1232 left.name
1233 .cmp(&right.name)
1234 .then_with(|| left.alias.cmp(&right.alias))
1235 });
1236 relations
1237}
1238
1239fn collect_relation_facts(
1240 scope: &Scope,
1241 mapping_schema: Option<&crate::schema::MappingSchema>,
1242 seen: &mut HashSet<String>,
1243 relations: &mut Vec<RelationFact>,
1244) {
1245 for relation in scope.sources.iter().map(|(source_name, source)| {
1246 let identity = source_table_identity(source);
1247 RelationFact {
1248 name: source
1249 .lineage_name
1250 .clone()
1251 .or_else(|| identity.as_ref().map(|identity| identity.name.clone()))
1252 .unwrap_or_else(|| source_name.clone()),
1253 alias: source.alias.clone().or_else(|| source_alias(source)),
1254 kind: source.kind,
1255 columns: source_columns(source, mapping_schema),
1256 catalog: identity
1257 .as_ref()
1258 .and_then(|identity| identity.catalog.clone()),
1259 schema: identity
1260 .as_ref()
1261 .and_then(|identity| identity.schema.clone()),
1262 table: identity
1263 .as_ref()
1264 .and_then(|identity| identity.table.clone()),
1265 }
1266 }) {
1267 let key = format!("{:?}|{}|{:?}", relation.kind, relation.name, relation.alias);
1268 if seen.insert(key) {
1269 relations.push(relation);
1270 }
1271 }
1272
1273 for branch_scope in &scope.union_scopes {
1274 collect_relation_facts(branch_scope, mapping_schema, seen, relations);
1275 }
1276}
1277
1278fn base_table_facts(
1279 scope: &Scope,
1280 mapping_schema: Option<&crate::schema::MappingSchema>,
1281) -> Vec<RelationFact> {
1282 let mut relations = Vec::new();
1283 let mut seen = HashSet::new();
1284
1285 collect_base_table_facts(scope, mapping_schema, &mut seen, &mut relations);
1286
1287 relations.sort_by(|left, right| left.name.cmp(&right.name));
1288 relations
1289}
1290
1291fn collect_base_table_facts(
1292 scope: &Scope,
1293 mapping_schema: Option<&crate::schema::MappingSchema>,
1294 seen: &mut HashSet<String>,
1295 relations: &mut Vec<RelationFact>,
1296) {
1297 for source in scope.sources.values() {
1298 if source.kind != SourceKind::Table {
1299 continue;
1300 }
1301
1302 let Some(identity) = source_table_identity(source) else {
1303 continue;
1304 };
1305
1306 if seen.insert(identity.name.clone()) {
1307 relations.push(RelationFact {
1308 name: identity.name,
1309 alias: source.alias.clone().or_else(|| source_alias(source)),
1310 kind: SourceKind::Table,
1311 columns: source_columns(source, mapping_schema),
1312 catalog: identity.catalog,
1313 schema: identity.schema,
1314 table: identity.table,
1315 });
1316 }
1317 }
1318
1319 for child_scope in scope
1320 .cte_scopes
1321 .iter()
1322 .chain(scope.union_scopes.iter())
1323 .chain(scope.table_scopes.iter())
1324 .chain(scope.derived_table_scopes.iter())
1325 .chain(scope.subquery_scopes.iter())
1326 {
1327 collect_base_table_facts(child_scope, mapping_schema, seen, relations);
1328 }
1329}
1330
1331fn source_columns(
1332 source: &SourceInfo,
1333 mapping_schema: Option<&crate::schema::MappingSchema>,
1334) -> Vec<String> {
1335 match &source.expression {
1336 Expression::Table(table) => mapping_schema
1337 .and_then(|schema| schema.column_names(&table_name(table)).ok())
1338 .unwrap_or_default(),
1339 Expression::Select(_)
1340 | Expression::Union(_)
1341 | Expression::Intersect(_)
1342 | Expression::Except(_) => get_output_column_names(&source.expression),
1343 Expression::Subquery(subquery) => get_output_column_names(&subquery.this),
1344 Expression::Cte(cte) if !cte.columns.is_empty() => cte
1345 .columns
1346 .iter()
1347 .map(|column| column.name.clone())
1348 .collect(),
1349 Expression::Cte(cte) => get_output_column_names(&cte.this),
1350 _ => Vec::new(),
1351 }
1352}
1353
1354fn source_table_name(source: &SourceInfo) -> Option<String> {
1355 source_table_identity(source).map(|identity| identity.name)
1356}
1357
1358fn source_alias(source: &SourceInfo) -> Option<String> {
1359 match &source.expression {
1360 Expression::Table(table) => table.alias.as_ref().map(|alias| alias.name.clone()),
1361 Expression::Subquery(subquery) => subquery.alias.as_ref().map(|alias| alias.name.clone()),
1362 _ => None,
1363 }
1364}
1365
1366fn table_name(table: &TableRef) -> String {
1367 let mut parts = Vec::new();
1368 if let Some(catalog) = &table.catalog {
1369 parts.push(catalog.name.clone());
1370 }
1371 if let Some(schema) = &table.schema {
1372 parts.push(schema.name.clone());
1373 }
1374 parts.push(table.name.name.clone());
1375 parts.join(".")
1376}
1377
1378#[derive(Debug, Clone)]
1379struct RelationIdentity {
1380 name: String,
1381 catalog: Option<String>,
1382 schema: Option<String>,
1383 table: Option<String>,
1384}
1385
1386fn source_table_identity(source: &SourceInfo) -> Option<RelationIdentity> {
1387 match &source.expression {
1388 Expression::Table(table) => Some(table_identity(table)),
1389 _ => None,
1390 }
1391}
1392
1393fn table_identity(table: &TableRef) -> RelationIdentity {
1394 RelationIdentity {
1395 name: table_name(table),
1396 catalog: table.catalog.as_ref().map(|catalog| catalog.name.clone()),
1397 schema: table.schema.as_ref().map(|schema| schema.name.clone()),
1398 table: Some(table.name.name.clone()),
1399 }
1400}
1401
1402fn set_operation_facts(
1403 expression: &Expression,
1404 scope: &Scope,
1405 dialect: DialectType,
1406) -> Vec<SetOperationFact> {
1407 let mut facts = Vec::new();
1408 collect_set_operation_facts(expression, scope, dialect, &mut facts);
1409 facts
1410}
1411
1412fn collect_set_operation_facts(
1413 expression: &Expression,
1414 scope: &Scope,
1415 dialect: DialectType,
1416 facts: &mut Vec<SetOperationFact>,
1417) {
1418 match expression {
1419 Expression::Union(union) => {
1420 facts.push(SetOperationFact {
1421 kind: "union".to_string(),
1422 all: union.all,
1423 distinct: union.distinct,
1424 output_columns: get_output_column_names(expression),
1425 branches: set_operation_branches(
1426 &union.left,
1427 &union.right,
1428 scope,
1429 dialect,
1430 SetOperationBranchRole::Value,
1431 ),
1432 });
1433 collect_set_operation_facts(&union.left, scope, dialect, facts);
1434 collect_set_operation_facts(&union.right, scope, dialect, facts);
1435 }
1436 Expression::Intersect(intersect) => {
1437 facts.push(SetOperationFact {
1438 kind: "intersect".to_string(),
1439 all: intersect.all,
1440 distinct: intersect.distinct,
1441 output_columns: get_output_column_names(expression),
1442 branches: set_operation_branches(
1443 &intersect.left,
1444 &intersect.right,
1445 scope,
1446 dialect,
1447 SetOperationBranchRole::Filter,
1448 ),
1449 });
1450 collect_set_operation_facts(&intersect.left, scope, dialect, facts);
1451 collect_set_operation_facts(&intersect.right, scope, dialect, facts);
1452 }
1453 Expression::Except(except) => {
1454 facts.push(SetOperationFact {
1455 kind: "except".to_string(),
1456 all: except.all,
1457 distinct: except.distinct,
1458 output_columns: get_output_column_names(expression),
1459 branches: set_operation_branches(
1460 &except.left,
1461 &except.right,
1462 scope,
1463 dialect,
1464 SetOperationBranchRole::Filter,
1465 ),
1466 });
1467 collect_set_operation_facts(&except.left, scope, dialect, facts);
1468 collect_set_operation_facts(&except.right, scope, dialect, facts);
1469 }
1470 Expression::Subquery(subquery) => {
1471 collect_set_operation_facts(&subquery.this, scope, dialect, facts);
1472 }
1473 _ => {}
1474 }
1475}
1476
1477fn set_operation_branches(
1478 left: &Expression,
1479 right: &Expression,
1480 scope: &Scope,
1481 dialect: DialectType,
1482 right_role: SetOperationBranchRole,
1483) -> Vec<SetOperationBranchFact> {
1484 vec![
1485 SetOperationBranchFact {
1486 index: 0,
1487 role: SetOperationBranchRole::Value,
1488 projections: projection_facts_for_branch(left, scope, dialect),
1489 },
1490 SetOperationBranchFact {
1491 index: 1,
1492 role: right_role,
1493 projections: projection_facts_for_branch(right, scope, dialect),
1494 },
1495 ]
1496}
1497
1498fn projection_facts_for_branch(
1499 expression: &Expression,
1500 root_scope: &Scope,
1501 dialect: DialectType,
1502) -> Vec<ProjectionFact> {
1503 let branch_scope = build_scope(expression);
1504 let scope = if branch_scope.sources.is_empty() {
1505 root_scope
1506 } else {
1507 &branch_scope
1508 };
1509 let nullability_context = NullabilityContext {
1510 schema: None,
1511 nullable_sources: nullable_source_names(expression),
1512 };
1513 projection_facts_for_query(expression, scope, dialect, &nullability_context)
1514}
1515
1516fn non_empty_string(value: String) -> Option<String> {
1517 if value.is_empty() {
1518 None
1519 } else {
1520 Some(value)
1521 }
1522}