diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f6b0c3..49fbf567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.9.15] - 2024-02-20 ### Changed -- removing the coalesce of the privacy unit with since it looses the UNIQUE property and changing the JOIN to left instead of INNER after the clipping. +- SELECT * with JOINs preserve the column names when there is no ambiguity [#268](https://github.com/Qrlew/qrlew/pull/268) +- removing the coalesce of the privacy unit with since it looses the UNIQUE property and changing the JOIN to left instead of INNER after the clipping. [#272](https://github.com/Qrlew/qrlew/pull/272) ## [0.9.14] - 2024-01-30 ### Added diff --git a/Cargo.toml b/Cargo.toml index a855c3cf..e06bfe7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Nicolas Grislain "] name = "qrlew" -version = "0.9.14" +version = "0.9.15" edition = "2021" description = "Sarus Qrlew Engine" documentation = "https://docs.rs/qrlew" diff --git a/src/dialect_translation/postgresql.rs b/src/dialect_translation/postgresql.rs index df8a74ab..1fe094fe 100644 --- a/src/dialect_translation/postgresql.rs +++ b/src/dialect_translation/postgresql.rs @@ -99,13 +99,7 @@ mod tests { use super::*; use crate::{ - builder::{Ready, With}, - data_type::{DataType, Value as _}, - display::Dot, - expr::Expr, - namer, - relation::{schema::Schema, Relation, TableBuilder}, - sql::{parse, relation::QueryWithRelations}, + builder::{Ready, With}, data_type::{DataType, Value as _}, display::Dot, expr::Expr, io::{postgresql, Database as _}, namer, relation::{schema::Schema, Relation, TableBuilder}, sql::{parse, relation::QueryWithRelations} }; use std::sync::Arc; @@ -154,19 +148,8 @@ mod tests { #[test] fn test_table_special() -> Result<()> { - let table: Relation = TableBuilder::new() - .path(["MY SPECIAL TABLE"]) - .name("my_table") - .size(100) - .schema( - Schema::empty() - .with(("Id", DataType::integer_interval(0, 1000))) - .with(("Na.Me", DataType::text())) - .with(("inc&ome", DataType::float_interval(100.0, 200000.0))) - .with(("normal_col", DataType::text())), - ) - .build(); - let relations = Hierarchy::from([(["schema", "MY SPECIAL TABLE"], Arc::new(table))]); + let mut database = postgresql::test_database(); + let relations = database.relations(); let query_str = r#"SELECT "Id", NORMAL_COL, "Na.Me" FROM "MY SPECIAL TABLE" ORDER BY "Id" "#; let translator = PostgreSqlTranslator; let query = parse_with_dialect(query_str, translator.dialect())?; @@ -174,16 +157,13 @@ mod tests { let relation = Relation::try_from((query_with_relation, translator))?; println!("\n {} \n", relation); let rel_with_traslator = RelationWithTranslator(&relation, translator); - let retranslated = ast::Query::from(rel_with_traslator); - print!("{}", retranslated); - let translated = r#" - WITH "map_mou5" ("Id","normal_col","Na.Me") AS ( - SELECT "Id" AS "Id", "normal_col" AS "normal_col", "Na.Me" AS "Na.Me" FROM "MY SPECIAL TABLE" - ), "map_0swv"("Id","normal_col","Na.Me") AS ( - SELECT "Id" AS "Id", "normal_col" AS "normal_col", "Na.Me" AS "Na.Me" FROM "map_mou5" ORDER BY "Id" ASC - ) SELECT * FROM "map_0swv" - "#; - assert_same_query_str(&retranslated.to_string(), translated); + let translated = ast::Query::from(rel_with_traslator); + print!("{}", translated); + _ = database + .query(translated.to_string().as_str()) + .unwrap() + .iter() + .map(ToString::to_string); Ok(()) } } diff --git a/src/relation/rewriting.rs b/src/relation/rewriting.rs index 4de3dfe0..6703649a 100644 --- a/src/relation/rewriting.rs +++ b/src/relation/rewriting.rs @@ -202,38 +202,52 @@ impl Join { self } - /// Replace the duplicates fields specified in `columns` by their coalesce expression - /// Its mimicks teh behaviour of USING in SQL + /// To mimic the behavior of USING(col) and NATURAL JOIN in SQL we create + /// a map where join columns identified by `vec` are coalesced. + /// vec: vector of string identifying input columns present in both _LEFT_ + /// and _RIGHT_ relation of the join. + /// columns: is the Hierarchy mapping input names in the JOIN to name field + /// + /// It returns a: + /// - Map build on top the Join with coalesced column along with + /// the other fields of the join and + /// - coalesced columns mapping (name in join -> name in map) pub fn remove_duplicates_and_coalesce( self, vec: Vec, columns: &Hierarchy, - ) -> Relation { - let fields = self + ) -> (Relation, Hierarchy) { + let mut coalesced_cols: Vec<(Identifier, Identifier)> = vec![]; + let coalesced = self .field_inputs() - .filter_map(|(name, id)| { - let col = id.as_ref().last().unwrap(); - if id.as_ref().first().unwrap().as_str() == LEFT_INPUT_NAME && vec.contains(col) { + .filter_map(|(_, input_id)| { + let col = input_id.as_ref().last().unwrap(); + if input_id.as_ref().first().unwrap().as_str() == LEFT_INPUT_NAME && vec.contains(col) { + let left_col = columns[[LEFT_INPUT_NAME, col]].as_ref().last().unwrap(); + let right_col = columns[[RIGHT_INPUT_NAME, col]].as_ref().last().unwrap(); + coalesced_cols.push((left_col.as_str().into(), col[..].into())); + coalesced_cols.push((right_col.as_str().into(), col[..].into())); Some(( - name, + col.clone(), Expr::coalesce( - Expr::col(columns[[LEFT_INPUT_NAME, col]].as_ref().last().unwrap()), - Expr::col(columns[[RIGHT_INPUT_NAME, col]].as_ref().last().unwrap()), + Expr::col(left_col), + Expr::col(right_col), ), )) } else { None } - }) + }); + let coalesced_with_others = coalesced .chain(self.field_inputs().filter_map(|(name, id)| { let col = id.as_ref().last().unwrap(); (!vec.contains(col)).then_some((name.clone(), Expr::col(name))) })) .collect::>(); - Relation::map() + (Relation::map() .input(Relation::from(self)) - .with_iter(fields) - .build() + .with_iter(coalesced_with_others) + .build(), coalesced_cols.into_iter().collect()) } } diff --git a/src/sql/mod.rs b/src/sql/mod.rs index e68dff94..8a1da91d 100644 --- a/src/sql/mod.rs +++ b/src/sql/mod.rs @@ -118,6 +118,7 @@ mod tests { use itertools::Itertools; use sqlparser::dialect::BigQueryDialect; + #[test] fn test_display() { let database = postgresql::test_database(); @@ -132,12 +133,12 @@ mod tests { t2 AS (SELECT * FROM table_2) SELECT max(a), sum(d) FROM t1 INNER JOIN t2 ON t1.d = t2.x CROSS JOIN table_2 GROUP BY t2.y, t1.a", " - WITH t1 AS (SELECT a,d FROM table_1), + WITH t1 AS (SELECT a, d FROM table_1), t2 AS (SELECT * FROM table_2) SELECT * FROM t1 INNER JOIN t2 ON t1.d = t2.x INNER JOIN table_2 ON t1.d=table_2.x ORDER BY t1.a LIMIT 10", ] { let relation = Relation::try_from(parse(query).unwrap().with(&database.relations())).unwrap(); - relation.display_dot(); + relation.display_dot().unwrap(); } } diff --git a/src/sql/relation.rs b/src/sql/relation.rs index 4dbfb4ae..5422d300 100644 --- a/src/sql/relation.rs +++ b/src/sql/relation.rs @@ -8,31 +8,16 @@ use super::{ Error, Result, }; use crate::{ - ast, - builder::{Ready, With, WithIterator, WithoutContext}, - dialect::{Dialect, GenericDialect}, - expr::{Expr, Identifier, Split, Reduce}, - hierarchy::{Hierarchy, Path}, - namer::{self, FIELD}, - parser::Parser, - relation::{ + ast, builder::{Ready, With, WithIterator, WithoutContext}, dialect::{Dialect, GenericDialect}, dialect_translation::{postgresql::PostgreSqlTranslator, QueryToRelationTranslator}, display::Dot, expr::{Expr, Identifier, Reduce, Split}, hierarchy::{Hierarchy, Path}, namer::{self, FIELD}, parser::Parser, relation::{ Join, JoinOperator, MapBuilder, Relation, SetOperator, SetQuantifier, Variant as _, WithInput, LEFT_INPUT_NAME, RIGHT_INPUT_NAME - }, - tokenizer::Tokenizer, - visitor::{Acceptor, Dependencies, Visited}, - dialect_translation::{QueryToRelationTranslator, postgresql::PostgreSqlTranslator}, - types::And, display::Dot + }, tokenizer::Tokenizer, types::And, visitor::{Acceptor, Dependencies, Visited} }; +use dot::Id; use itertools::Itertools; use std::{ - convert::TryFrom, - iter::{once, Iterator}, - result, - str::FromStr, - sync::Arc, - ops::Deref + collections::HashMap, convert::TryFrom, iter::{once, Iterator}, ops::Deref, result, str::FromStr, sync::Arc }; /* @@ -183,11 +168,12 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> VisitedQueryRelations<'a, columns: &'a Hierarchy, ) -> Result { Ok(match join_constraint { - ast::JoinConstraint::On(expr) => expr.with(columns).try_into()?, + ast::JoinConstraint::On(expr) => self.translator.try_expr(expr, columns)?, ast::JoinConstraint::Using(idents) => { // the "Using (id)" condition is equivalent to "ON _LEFT_.id = _RIGHT_.id" Expr::and_iter( idents.into_iter() - .map(|id| Expr::eq( + .map(|id| + Expr::eq( Expr::Column(Identifier::from(vec![LEFT_INPUT_NAME.to_string(), id.value.to_string()])), Expr::Column(Identifier::from(vec![RIGHT_INPUT_NAME.to_string(), id.value.to_string()])), )) @@ -247,6 +233,76 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> VisitedQueryRelations<'a, } } + /// Build a RelationWithColumns with a JOIN + fn try_from_join( + &self, + left: RelationWithColumns, + ast_join: &'a ast::Join + ) -> Result { + let RelationWithColumns(left_relation, left_columns) = left; + let RelationWithColumns(right_relation, right_columns) = + self.try_from_table_factor(&ast_join.relation)?; + let left_columns: Hierarchy = left_columns.map(|i| { + let mut v = vec![Join::left_name().to_string()]; + v.extend(i.to_vec()); + v.into() + }); + let right_columns = right_columns.map(|i| { + let mut v = vec![Join::right_name().to_string()]; + v.extend(i.to_vec()); + v.into() + }); + // fully qualified input names -> fully qualified JOIN names + let all_columns: Hierarchy = left_columns.with(right_columns); + let operator = self.try_from_join_operator_with_columns( + &ast_join.join_operator, + &all_columns, + )?; + let join: Join = Relation::join() + .operator(operator) + .left(left_relation) + .right(right_relation) + .build(); + + let join_columns: Hierarchy = join + .field_inputs() + .map(|(f, i)| (i, f.into())) + .collect(); + + // If the join constraint is of type "USING" or "NATURAL", add a map to coalesce the duplicate columns + let (relation, coalesced) = match &ast_join.join_operator { + ast::JoinOperator::Inner(ast::JoinConstraint::Using(v)) + | ast::JoinOperator::LeftOuter(ast::JoinConstraint::Using(v)) + | ast::JoinOperator::RightOuter(ast::JoinConstraint::Using(v)) + | ast::JoinOperator::FullOuter(ast::JoinConstraint::Using(v)) => { + // Do we need to change all_columns? + let to_be_coalesced: Vec = v.into_iter().map(|id| id.value.to_string()).collect(); + join.remove_duplicates_and_coalesce(to_be_coalesced, &join_columns) + }, + ast::JoinOperator::Inner(ast::JoinConstraint::Natural) + | ast::JoinOperator::LeftOuter(ast::JoinConstraint::Natural) + | ast::JoinOperator::RightOuter(ast::JoinConstraint::Natural) + | ast::JoinOperator::FullOuter(ast::JoinConstraint::Natural) => { + let v: Vec = join.left().fields() + .into_iter() + .filter_map(|f| join.right().schema().field(f.name()).is_ok().then_some(f.name().to_string())) + .collect(); + join.remove_duplicates_and_coalesce(v, &join_columns) + }, + ast::JoinOperator::LeftSemi(_) => todo!(), + ast::JoinOperator::RightSemi(_) => todo!(), + ast::JoinOperator::LeftAnti(_) => todo!(), + ast::JoinOperator::RightAnti(_) => todo!(), + _ => { + let empty: Vec<(Identifier, Identifier)> = vec![]; + (Relation::from(join), empty.into_iter().collect()) + } + }; + let with_coalesced = join_columns.clone().with(join_columns.and_then(coalesced)); + let composed = all_columns.and_then(with_coalesced); + Ok(RelationWithColumns::new(Arc::new(relation), composed)) + } + /// Convert a TableWithJoins into a RelationWithColumns fn try_from_table_with_joins( &self, @@ -254,71 +310,11 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> VisitedQueryRelations<'a, ) -> Result { // Process the relation // Then the JOIN if needed - let result = table_with_joins.joins.iter().fold( - self.try_from_table_factor(&table_with_joins.relation), - |left, ast_join| { - let RelationWithColumns(left_relation, left_columns) = left?; - let RelationWithColumns(right_relation, right_columns) = - self.try_from_table_factor(&ast_join.relation)?; - let left_columns = left_columns.map(|i| { - let mut v = vec![Join::left_name().to_string()]; - v.extend(i.to_vec()); - v.into() - }); - let right_columns = right_columns.map(|i| { - let mut v = vec![Join::right_name().to_string()]; - v.extend(i.to_vec()); - v.into() - }); - let all_columns = left_columns.with(right_columns); - let operator = self.try_from_join_operator_with_columns( - &ast_join.join_operator, - // &all_columns.filter_map(|i| Some(i.split_last().ok()?.0)),//TODO remove this - &all_columns, - )?; - // We build a Join - let join: Join = Relation::join() - .operator(operator) - .left(left_relation) - .right(right_relation) - .build(); - - // We collect column mapping inputs should map to new names (hence the inversion) - let new_columns: Hierarchy = - join.field_inputs().map(|(f, i)| (i, f.into())).collect(); - let composed_columns = all_columns.and_then(new_columns.clone()); - - // If the join contraint is of type "USING" or "NATURAL", add a map to coalesce the duplicate columns - let relation = match &ast_join.join_operator { - ast::JoinOperator::Inner(ast::JoinConstraint::Using(v)) - | ast::JoinOperator::LeftOuter(ast::JoinConstraint::Using(v)) - | ast::JoinOperator::RightOuter(ast::JoinConstraint::Using(v)) - | ast::JoinOperator::FullOuter(ast::JoinConstraint::Using(v)) => { - join.remove_duplicates_and_coalesce( - v.into_iter().map(|id| id.value.to_string()).collect(), - &new_columns - ) - }, - ast::JoinOperator::Inner(ast::JoinConstraint::Natural) - | ast::JoinOperator::LeftOuter(ast::JoinConstraint::Natural) - | ast::JoinOperator::RightOuter(ast::JoinConstraint::Natural) - | ast::JoinOperator::FullOuter(ast::JoinConstraint::Natural) => { - let v = join.left().fields() - .into_iter() - .filter_map(|f| join.right().schema().field(f.name()).is_ok().then_some(f.name().to_string())) - .collect(); - join.remove_duplicates_and_coalesce(v,&new_columns) - }, - ast::JoinOperator::LeftSemi(_) => todo!(), - ast::JoinOperator::RightSemi(_) => todo!(), - ast::JoinOperator::LeftAnti(_) => todo!(), - ast::JoinOperator::RightAnti(_) => todo!(), - _ => Relation::from(join), - }; - - // We should compose hierarchies - Ok(RelationWithColumns::new(Arc::new(relation), composed_columns)) - }, + let result = table_with_joins.joins + .iter() + .fold(self.try_from_table_factor(&table_with_joins.relation), + |left, ast_join| + self.try_from_join(left?, &ast_join), ); result } @@ -331,24 +327,26 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> VisitedQueryRelations<'a, // TODO consider more tables // For now, only consider the first element // It should eventually be cross joined as described in: https://www.postgresql.org/docs/current/queries-table-expressions.html - self.try_from_table_with_joins(&tables_with_joins[0]) + self.try_from_table_with_joins( + &tables_with_joins[0] + ) } - /// Build a relation from the - fn try_from_select_items_selection_and_group_by( + /// Extracts named expressions from the from relation and the select items + fn try_named_expr_columns_from_select_items( &self, - names: &'a Hierarchy, + columns: &'a Hierarchy, select_items: &'a [ast::SelectItem], - selection: &'a Option, - group_by: &'a ast::GroupByExpr, - from: Arc, - having: &'a Option, - distinct: &'a Option, - ) -> Result> { - // Collect all expressions with their aliases + from: &'a Arc, + ) -> Result<(Vec<(String, Expr)>, Hierarchy)> { + let mut named_exprs: Vec<(String, Expr)> = vec![]; - // Columns from names - let columns = &names.map(|s| s.clone().into()); + + // The select all forces the preservation of names for non ambiguous + // columns. In this vector we collect those names. They are needed + // to update the column mapping for order by limit and offset. + let mut renamed_columns: Vec<(Identifier, Identifier)> = vec![]; + for select_item in select_items { match select_item { ast::SelectItem::UnnamedExpr(expr) => named_exprs.push(( @@ -372,30 +370,66 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> VisitedQueryRelations<'a, expr => namer::name_from_content(FIELD, &expr), }, self.translator.try_expr(expr,columns)? - //Expr::try_from(expr.with(columns))?, )), ast::SelectItem::ExprWithAlias { expr, alias } => { named_exprs.push((alias.clone().value, self.translator.try_expr(expr,columns)?)) - //named_exprs.push((alias.clone().value, Expr::try_from(expr.with(columns))?)) } ast::SelectItem::QualifiedWildcard(_, _) => todo!(), ast::SelectItem::Wildcard(_) => { + // push all names that are present in the from into named_exprs. + // for non ambiguous col names preserve the input name + // for the ambiguous ones used the name present in the relation. + let non_ambiguous_cols = last(columns); + // Invert mapping of non_ambiguous_cols + let new_aliases: Hierarchy = non_ambiguous_cols.iter() + .map(|(p, i)|(i.deref(), p.last().unwrap().clone())) + .collect(); + for field in from.schema().iter() { - named_exprs.push((field.name().to_string(), Expr::col(field.name()))) + let field_name = field.name().to_string(); + let alias = new_aliases + .get_key_value(&[field.name().to_string()]) + .and_then(|(k, v)|{ + renamed_columns.push((k.to_vec().into(), v.clone().into())); + Some(v.clone()) + } ); + named_exprs.push((alias.unwrap_or(field_name), Expr::col(field.name()))); } } } } + Ok((named_exprs, renamed_columns.into_iter().collect())) + } + + /// Build a RelationWithColumns from select_items selection group_by having and distinct + fn try_from_select_items_selection_and_group_by( + &self, + names: &'a Hierarchy, + select_items: &'a [ast::SelectItem], + selection: &'a Option, + group_by: &'a ast::GroupByExpr, + from: Arc, + having: &'a Option, + distinct: &'a Option, + ) -> Result { + // Collect all expressions with their aliases + let mut named_exprs: Vec<(String, Expr)> = vec![]; + // Columns from names + let columns = &names.map(|s| s.clone().into()); + + let (named_expr_from_select, new_columns) = self.try_named_expr_columns_from_select_items(columns, select_items, &from)?; + named_exprs.extend(named_expr_from_select.into_iter()); + // Prepare the GROUP BY let group_by = match group_by { ast::GroupByExpr::All => todo!(), ast::GroupByExpr::Expressions(group_by_exprs) => group_by_exprs .iter() - .map(|e| e.with(columns).try_into()) + .map(|e| self.translator.try_expr(e, columns)) .collect::>>()?, }; // If the GROUP BY contains aliases, then replace them by the corresponding expression in `named_exprs`. - // Note that we mimic postgres behaviour and support only GROUP BY alias column (no other expressions containing aliases are allowed) + // Note that we mimic postgres behavior and support only GROUP BY alias column (no other expressions containing aliases are allowed) // The aliases cannot be used in HAVING let group_by = group_by.into_iter() .map(|x| match &x { @@ -412,7 +446,7 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> VisitedQueryRelations<'a, // Add the having in named_exprs let having = if let Some(expr) = having { let having_name = namer::name_from_content(FIELD, &expr); - let mut expr = self.translator.try_expr(expr,columns)?; //Expr::try_from(expr.with(columns))?; + let mut expr = self.translator.try_expr(expr,columns)?; let columns = named_exprs .iter() .map(|(s, x)| (Expr::col(s.to_string()), x.clone())) @@ -447,8 +481,10 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> VisitedQueryRelations<'a, // Prepare the WHERE let filter: Option = selection .as_ref() - .map(|e| e.with(columns).try_into()) + // todo. Use pass the expression through the translator + .map(|e| self.translator.try_expr(e, columns)) .map_or(Ok(None), |r| r.map(Some))?; + // Build a Relation let mut relation: Relation = match split { Split::Map(map) => { @@ -483,7 +519,9 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> VisitedQueryRelations<'a, } relation = relation.distinct() } - Ok(Arc::new(relation)) + // preserve old columns while composing with new ones + let columns = &columns.clone().with(columns.and_then(new_columns)); + Ok(RelationWithColumns::new(Arc::new(relation), columns.clone())) } /// Convert a Select into a Relation @@ -528,8 +566,11 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> VisitedQueryRelations<'a, if qualify.is_some() { return Err(Error::other("QUALIFY is not supported")); } - let RelationWithColumns(from, columns) = self.try_from_tables_with_joins(from)?; - let relation = self.try_from_select_items_selection_and_group_by( + + let RelationWithColumns(from, columns) = self.try_from_tables_with_joins( + from + )?; + let RelationWithColumns(relation, columns) = self.try_from_select_items_selection_and_group_by( &columns.filter_map(|i| Some(i.split_last().ok()?.0)), projection, selection, @@ -722,6 +763,19 @@ impl<'a, T: QueryToRelationTranslator + Copy + Clone> TryFrom<(QueryWithRelation } } +/// It creates a new hierarchy with Identifier for which the last part of their +/// path is not ambiguous. The new hierarchy will contain one-element paths +fn last(columns: &Hierarchy) -> Hierarchy { + columns + .iter() + .filter_map(|(path, _)|{ + let path_last = path.last().unwrap().clone(); + columns + .get(&[path_last.clone()]) + .and_then( |t| Some((path_last, t.clone())) ) + }) + .collect() +} /// A simple SQL query parser with dialect @@ -1407,6 +1461,113 @@ mod tests { .map(ToString::to_string); } + #[test] + fn test_select_all_with_joins() { + let mut database = postgresql::test_database(); + let relations = database.relations(); + + let query_str = r#" + SELECT * + FROM table_2 AS t1 INNER JOIN table_2 AS t2 USING(x) INNER JOIN table_2 AS t3 USING(x) + WHERE x > 50 + ORDER BY x, t2.y, t2.z + "#; + let query = parse(query_str).unwrap(); + let relation = Relation::try_from(QueryWithRelations::new( + &query, + &relations + )) + .unwrap(); + relation.display_dot().unwrap(); + let query: &str = &ast::Query::from(&relation).to_string(); + println!("{query}"); + _ = database + .query(query) + .unwrap() + .iter() + .map(ToString::to_string); + + let query_str = r#" + WITH my_tab AS (SELECT * FROM user_table u JOIN order_table o USING (id)) + SELECT * FROM my_tab WHERE id > 50; + "#; + let query = parse(query_str).unwrap(); + let relation = Relation::try_from(QueryWithRelations::new( + &query, + &relations + )) + .unwrap(); + relation.display_dot().unwrap(); + let query: &str = &ast::Query::from(&relation).to_string(); + println!("{query}"); + _ = database + .query(query) + .unwrap() + .iter() + .map(ToString::to_string); + + let query_str = r#" + WITH my_tab AS (SELECT id, age FROM user_table u JOIN order_table o USING (id)) + SELECT * FROM my_tab WHERE id > 50; + "#; + let query = parse(query_str).unwrap(); + let relation = Relation::try_from(QueryWithRelations::new( + &query, + &relations + )) + .unwrap(); + relation.display_dot().unwrap(); + let query: &str = &ast::Query::from(&relation).to_string(); + println!("{query}"); + _ = database + .query(query) + .unwrap() + .iter() + .map(ToString::to_string); + + let query_str = r#" + WITH my_tab AS (SELECT * FROM user_table u JOIN order_table o ON (u.id=o.id)) + SELECT * FROM my_tab WHERE user_id > 50; + "#; + let query = parse(query_str).unwrap(); + let relation = Relation::try_from(QueryWithRelations::new( + &query, + &relations + )) + .unwrap(); + // id becomes an ambiguous column since is present in both tables + assert!(relation.schema().field("id").is_err()); + relation.display_dot().unwrap(); + println!("relation = {relation}"); + let query: &str = &ast::Query::from(&relation).to_string(); + println!("{query}"); + _ = database + .query(query) + .unwrap() + .iter() + .map(ToString::to_string); + + let query_str = r#" + WITH my_tab AS (SELECT u.id, user_id, age FROM user_table u JOIN order_table o ON (u.id=o.id)) + SELECT * FROM my_tab WHERE user_id > 50; + "#; + let query = parse(query_str).unwrap(); + let relation = Relation::try_from(QueryWithRelations::new( + &query, + &relations + )) + .unwrap(); + relation.display_dot().unwrap(); + println!("relation = {relation}"); + let query: &str = &ast::Query::from(&relation).to_string(); + println!("{query}"); + _ = database + .query(query) + .unwrap() + .iter() + .map(ToString::to_string); + } + #[test] fn test_distinct_in_select() { let query = parse("SELECT DISTINCT a, b FROM table_1;").unwrap();