diff --git a/src/query/ast/src/ast/format/ast_format.rs b/src/query/ast/src/ast/format/ast_format.rs index 4fb6380d4366..608d4a06863a 100644 --- a/src/query/ast/src/ast/format/ast_format.rs +++ b/src/query/ast/src/ast/format/ast_format.rs @@ -2093,7 +2093,7 @@ impl<'ast> Visitor<'ast> for AstFormatVisitor { let node = FormatTreeNode::with_children(format_ctx, vec![child]); self.children.push(node); } - SelectTarget::QualifiedName(_) => { + SelectTarget::QualifiedName { .. } => { let name = format!("Target {}", target); let format_ctx = AstFormatContext::new(name); let node = FormatTreeNode::new(format_ctx); diff --git a/src/query/ast/src/ast/format/syntax/query.rs b/src/query/ast/src/ast/format/syntax/query.rs index f4e83f55c593..8364e7379188 100644 --- a/src/query/ast/src/ast/format/syntax/query.rs +++ b/src/query/ast/src/ast/format/syntax/query.rs @@ -20,6 +20,7 @@ use crate::ast::format::syntax::inline_dot; use crate::ast::format::syntax::interweave_comma; use crate::ast::format::syntax::parenthenized; use crate::ast::format::syntax::NEST_FACTOR; +use crate::ast::ExcludeCol; use crate::ast::Expr; use crate::ast::JoinCondition; use crate::ast::JoinOperator; @@ -108,26 +109,70 @@ fn pretty_select_list(select_list: Vec) -> RcDoc { } .nest(NEST_FACTOR) .append( - interweave_comma(select_list.into_iter().map(|select_target| { - match select_target { - SelectTarget::AliasedExpr { expr, alias } => { - pretty_expr(*expr).append(if let Some(alias) = alias { - RcDoc::space() - .append(RcDoc::text("AS")) - .append(RcDoc::space()) - .append(RcDoc::text(alias.to_string())) - } else { - RcDoc::nil() - }) - } - SelectTarget::QualifiedName(object_name) => inline_dot( - object_name - .into_iter() - .map(|indirection| RcDoc::text(indirection.to_string())), - ) - .group(), - } - })) + interweave_comma( + select_list + .into_iter() + .map(|select_target| match select_target { + SelectTarget::AliasedExpr { expr, alias } => { + pretty_expr(*expr).append(if let Some(alias) = alias { + RcDoc::space() + .append(RcDoc::text("AS")) + .append(RcDoc::space()) + .append(RcDoc::text(alias.to_string())) + } else { + RcDoc::nil() + }) + } + SelectTarget::QualifiedName { + qualified: object_name, + exclude, + } => { + let docs = inline_dot( + object_name + .into_iter() + .map(|indirection| RcDoc::text(indirection.to_string())), + ) + .group(); + docs.append(if let Some(exclude) = exclude { + match exclude { + ExcludeCol::Col(col) => RcDoc::line().append( + RcDoc::text("EXCEPT") + .append(RcDoc::space().nest(NEST_FACTOR)) + .append(RcDoc::text(col.to_string())), + ), + ExcludeCol::Cols(cols) => { + if !cols.is_empty() { + RcDoc::line() + .append( + RcDoc::text("EXCEPT").append( + if cols.len() > 1 { + RcDoc::line() + } else { + RcDoc::space() + } + .nest(NEST_FACTOR), + ), + ) + .append( + interweave_comma(cols.into_iter().map(|ident| { + RcDoc::space() + .append(RcDoc::space()) + .append(RcDoc::text(ident.to_string())) + })) + .nest(NEST_FACTOR) + .group(), + ) + } else { + RcDoc::nil() + } + } + } + } else { + RcDoc::nil() + }) + } + }), + ) .nest(NEST_FACTOR) .group(), ) diff --git a/src/query/ast/src/ast/query.rs b/src/query/ast/src/ast/query.rs index 255b1512cfe5..a99182fea768 100644 --- a/src/query/ast/src/ast/query.rs +++ b/src/query/ast/src/ast/query.rs @@ -119,9 +119,31 @@ pub enum SelectTarget<'a> { alias: Option>, }, - // Qualified name, e.g. `SELECT t.a, t.* FROM t`. + // Qualified name, e.g. `SELECT t.a, t.* exclude t.a FROM t`. // For simplicity, wildcard is involved. - QualifiedName(QualifiedName<'a>), + QualifiedName { + qualified: QualifiedName<'a>, + exclude: Option>, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ExcludeCol<'a> { + Col(Identifier<'a>), + Cols(Vec>), +} + +impl Display for ExcludeCol<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ExcludeCol::Col(col) => write!(f, "{col}"), + ExcludeCol::Cols(cols) => { + write!(f, "(")?; + write_comma_separated_list(f, cols)?; + write!(f, ")") + } + } + } } pub type QualifiedName<'a> = Vec>; @@ -370,8 +392,24 @@ impl<'a> Display for SelectTarget<'a> { write!(f, " AS {ident}")?; } } - SelectTarget::QualifiedName(indirections) => { - write_period_separated_list(f, indirections)?; + SelectTarget::QualifiedName { qualified, exclude } => { + write_period_separated_list(f, qualified)?; + if let Some(exclude) = exclude { + // ORDER BY clause + match exclude { + ExcludeCol::Col(col) => { + write!(f, " EXCLUDE")?; + write!(f, " {col}")?; + } + ExcludeCol::Cols(cols) => { + if !cols.is_empty() { + write!(f, " EXCLUDE (")?; + write_comma_separated_list(f, cols)?; + write!(f, ")")?; + } + } + } + } } } Ok(()) diff --git a/src/query/ast/src/parser/query.rs b/src/query/ast/src/parser/query.rs index bd2aa85cada3..840f550fbd2c 100644 --- a/src/query/ast/src/parser/query.rs +++ b/src/query/ast/src/parser/query.rs @@ -83,21 +83,48 @@ pub fn with(i: Input) -> IResult { )(i) } +pub fn exclude_col(i: Input) -> IResult { + let var = map( + rule! { + #ident + }, + ExcludeCol::Col, + ); + let vars = map( + rule! { + "(" ~ ^#comma_separated_list1(ident) ~ ")" + }, + |(_, cols, _)| ExcludeCol::Cols(cols), + ); + + rule!( + #var + | #vars + )(i) +} + pub fn select_target(i: Input) -> IResult { let qualified_wildcard = map( rule! { - ( #ident ~ "." ~ ( #ident ~ "." )? )? ~ "*" + ( #ident ~ "." ~ ( #ident ~ "." )? )? ~ "*" ~ ( ( EXCEPT | EXCLUDE ) ~ #exclude_col )? }, - |(res, _)| match res { - Some((fst, _, Some((snd, _)))) => SelectTarget::QualifiedName(vec![ - Indirection::Identifier(fst), - Indirection::Identifier(snd), - Indirection::Star, - ]), - Some((fst, _, None)) => { - SelectTarget::QualifiedName(vec![Indirection::Identifier(fst), Indirection::Star]) - } - None => SelectTarget::QualifiedName(vec![Indirection::Star]), + |(res, _, opt_exclude)| match res { + Some((fst, _, Some((snd, _)))) => SelectTarget::QualifiedName { + qualified: vec![ + Indirection::Identifier(fst), + Indirection::Identifier(snd), + Indirection::Star, + ], + exclude: opt_exclude.map(|(_, exclude)| exclude), + }, + Some((fst, _, None)) => SelectTarget::QualifiedName { + qualified: vec![Indirection::Identifier(fst), Indirection::Star], + exclude: opt_exclude.map(|(_, exclude)| exclude), + }, + None => SelectTarget::QualifiedName { + qualified: vec![Indirection::Star], + exclude: opt_exclude.map(|(_, exclude)| exclude), + }, }, ); let projection = map( diff --git a/src/query/ast/src/parser/token.rs b/src/query/ast/src/parser/token.rs index 7dd7b38fd95c..d3cdbad112b1 100644 --- a/src/query/ast/src/parser/token.rs +++ b/src/query/ast/src/parser/token.rs @@ -367,6 +367,8 @@ pub enum TokenKind { DROP, #[token("EXCEPT", ignore(ascii_case))] EXCEPT, + #[token("EXCLUDE", ignore(ascii_case))] + EXCLUDE, #[token("ELSE", ignore(ascii_case))] ELSE, #[token("END", ignore(ascii_case))] diff --git a/src/query/ast/src/visitors/walk.rs b/src/query/ast/src/visitors/walk.rs index d90ee62f3bec..ad01aefb2357 100644 --- a/src/query/ast/src/visitors/walk.rs +++ b/src/query/ast/src/visitors/walk.rs @@ -185,7 +185,10 @@ pub fn walk_select_target<'a, V: Visitor<'a>>(visitor: &mut V, target: &'a Selec visitor.visit_identifier(alias); } } - SelectTarget::QualifiedName(names) => { + SelectTarget::QualifiedName { + qualified: names, + exclude, + } => { for indirection in names { match indirection { Indirection::Identifier(ident) => { @@ -194,6 +197,16 @@ pub fn walk_select_target<'a, V: Visitor<'a>>(visitor: &mut V, target: &'a Selec Indirection::Star => {} } } + if let Some(exclude) = exclude { + match exclude { + ExcludeCol::Col(col) => visitor.visit_identifier(col), + ExcludeCol::Cols(cols) => { + for ident in cols.iter() { + visitor.visit_identifier(ident); + } + } + } + } } } } diff --git a/src/query/ast/src/visitors/walk_mut.rs b/src/query/ast/src/visitors/walk_mut.rs index 0b24d9c74432..3b37bd550a10 100644 --- a/src/query/ast/src/visitors/walk_mut.rs +++ b/src/query/ast/src/visitors/walk_mut.rs @@ -185,7 +185,10 @@ pub fn walk_select_target_mut<'a, V: VisitorMut>(visitor: &mut V, target: &mut S visitor.visit_identifier(alias); } } - SelectTarget::QualifiedName(names) => { + SelectTarget::QualifiedName { + qualified: names, + exclude, + } => { for indirection in names { match indirection { Indirection::Identifier(ident) => { @@ -194,6 +197,16 @@ pub fn walk_select_target_mut<'a, V: VisitorMut>(visitor: &mut V, target: &mut S Indirection::Star => {} } } + if let Some(exclude) = exclude { + match exclude { + ExcludeCol::Col(col) => visitor.visit_identifier(col), + ExcludeCol::Cols(cols) => { + for ident in cols { + visitor.visit_identifier(ident); + } + } + } + } } } } diff --git a/src/query/ast/tests/it/parser.rs b/src/query/ast/tests/it/parser.rs index 0792a7ab1c59..28e9e3e7e0ff 100644 --- a/src/query/ast/tests/it/parser.rs +++ b/src/query/ast/tests/it/parser.rs @@ -418,6 +418,7 @@ fn test_query() { let mut mint = Mint::new("tests/it/testdata"); let mut file = mint.new_goldenfile("query.txt").unwrap(); let cases = &[ + r#"select * except c1, b.* except (c2, c3, c4) from customer inner join orders on a = b limit 1"#, r#"select * from customer inner join orders"#, r#"select * from customer cross join orders"#, r#"select * from customer inner join orders on a = b limit 1"#, diff --git a/src/query/ast/tests/it/testdata/query.txt b/src/query/ast/tests/it/testdata/query.txt index 270af6a09503..9f764d0bff6e 100644 --- a/src/query/ast/tests/it/testdata/query.txt +++ b/src/query/ast/tests/it/testdata/query.txt @@ -1,3 +1,210 @@ +---------- Input ---------- +select * except c1, b.* except (c2, c3, c4) from customer inner join orders on a = b limit 1 +---------- Output --------- +SELECT * EXCLUDE c1, b.* EXCLUDE (c2, c3, c4) FROM customer INNER JOIN orders ON a = b LIMIT 1 +---------- AST ------------ +Query { + span: [ + SELECT(0..6), + Multiply(7..8), + EXCEPT(9..15), + Ident(16..18), + Comma(18..19), + Ident(20..21), + Period(21..22), + Multiply(22..23), + EXCEPT(24..30), + LParen(31..32), + Ident(32..34), + Comma(34..35), + Ident(36..38), + Comma(38..39), + Ident(40..42), + RParen(42..43), + FROM(44..48), + Ident(49..57), + INNER(58..63), + JOIN(64..68), + Ident(69..75), + ON(76..78), + Ident(79..80), + Eq(81..82), + Ident(83..84), + LIMIT(85..90), + LiteralInteger(91..92), + ], + with: None, + body: Select( + SelectStmt { + span: [ + SELECT(0..6), + Multiply(7..8), + EXCEPT(9..15), + Ident(16..18), + Comma(18..19), + Ident(20..21), + Period(21..22), + Multiply(22..23), + EXCEPT(24..30), + LParen(31..32), + Ident(32..34), + Comma(34..35), + Ident(36..38), + Comma(38..39), + Ident(40..42), + RParen(42..43), + FROM(44..48), + Ident(49..57), + INNER(58..63), + JOIN(64..68), + Ident(69..75), + ON(76..78), + Ident(79..80), + Eq(81..82), + Ident(83..84), + ], + distinct: false, + select_list: [ + QualifiedName { + qualified: [ + Star, + ], + exclude: Some( + Col( + Identifier { + name: "c1", + quote: None, + span: Ident(16..18), + }, + ), + ), + }, + QualifiedName { + qualified: [ + Identifier( + Identifier { + name: "b", + quote: None, + span: Ident(20..21), + }, + ), + Star, + ], + exclude: Some( + Cols( + [ + Identifier { + name: "c2", + quote: None, + span: Ident(32..34), + }, + Identifier { + name: "c3", + quote: None, + span: Ident(36..38), + }, + Identifier { + name: "c4", + quote: None, + span: Ident(40..42), + }, + ], + ), + ), + }, + ], + from: [ + Join { + span: [ + INNER(58..63), + JOIN(64..68), + ], + join: Join { + op: Inner, + condition: On( + BinaryOp { + span: [ + Eq(81..82), + ], + op: Eq, + left: ColumnRef { + span: [ + Ident(79..80), + ], + database: None, + table: None, + column: Identifier { + name: "a", + quote: None, + span: Ident(79..80), + }, + }, + right: ColumnRef { + span: [ + Ident(83..84), + ], + database: None, + table: None, + column: Identifier { + name: "b", + quote: None, + span: Ident(83..84), + }, + }, + }, + ), + left: Table { + span: [ + Ident(49..57), + ], + catalog: None, + database: None, + table: Identifier { + name: "customer", + quote: None, + span: Ident(49..57), + }, + alias: None, + travel_point: None, + }, + right: Table { + span: [ + Ident(69..75), + ], + catalog: None, + database: None, + table: Identifier { + name: "orders", + quote: None, + span: Ident(69..75), + }, + alias: None, + travel_point: None, + }, + }, + }, + ], + selection: None, + group_by: [], + having: None, + }, + ), + order_by: [], + limit: [ + Literal { + span: [ + LiteralInteger(91..92), + ], + lit: Integer( + 1, + ), + }, + ], + offset: None, + ignore_result: false, +} + + ---------- Input ---------- select * from customer inner join orders ---------- Output --------- @@ -27,11 +234,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -114,11 +322,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -211,11 +420,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -350,11 +560,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -488,11 +699,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -592,11 +804,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -1394,11 +1607,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -2709,11 +2923,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -2746,11 +2961,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -2817,11 +3033,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -2854,11 +3071,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -2937,11 +3155,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -2974,11 +3193,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3013,11 +3233,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3098,11 +3319,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3135,11 +3357,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3174,11 +3397,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3250,11 +3474,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3294,11 +3519,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3331,11 +3557,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3418,11 +3645,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3455,11 +3683,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3494,11 +3723,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3572,11 +3802,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3616,11 +3847,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -3653,11 +3885,12 @@ Query { ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { diff --git a/src/query/ast/tests/it/testdata/statement.txt b/src/query/ast/tests/it/testdata/statement.txt index 9dd11a12fbd7..09dfc54ce7c0 100644 --- a/src/query/ast/tests/it/testdata/statement.txt +++ b/src/query/ast/tests/it/testdata/statement.txt @@ -377,11 +377,12 @@ CreateTable( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -1909,11 +1910,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -1972,11 +1974,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -2047,11 +2050,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -2149,11 +2153,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -2295,11 +2300,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -2448,11 +2454,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -2603,11 +2610,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -2758,11 +2766,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -2911,11 +2920,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -3065,11 +3075,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -3218,11 +3229,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -3372,11 +3384,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -3527,11 +3540,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -3682,11 +3696,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -3835,11 +3850,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -3981,11 +3997,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -4089,11 +4106,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -4197,11 +4215,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -4303,11 +4322,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Join { @@ -4422,11 +4442,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -4624,11 +4645,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -4826,11 +4848,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -5026,11 +5049,12 @@ Query( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { @@ -5404,11 +5428,12 @@ Insert( ], distinct: false, select_list: [ - QualifiedName( - [ + QualifiedName { + qualified: [ Star, ], - ), + exclude: None, + }, ], from: [ Table { diff --git a/src/query/sql/src/planner/binder/project.rs b/src/query/sql/src/planner/binder/project.rs index 31925cb7e6ec..8a050afaa03d 100644 --- a/src/query/sql/src/planner/binder/project.rs +++ b/src/query/sql/src/planner/binder/project.rs @@ -13,7 +13,10 @@ // limitations under the License. use std::collections::HashMap; +use std::collections::HashSet; +use common_ast::ast::ExcludeCol; +use common_ast::ast::Identifier; use common_ast::ast::Indirection; use common_ast::ast::SelectTarget; use common_exception::ErrorCode; @@ -150,8 +153,107 @@ impl<'a> Binder { let mut output = SelectList::<'a>::default(); for select_target in select_list { match select_target { - SelectTarget::QualifiedName(names) => { + SelectTarget::QualifiedName { + qualified: names, + exclude, + } => { // Handle qualified name as select target + let mut exclude_cols: HashSet = HashSet::new(); + if let Some(exclude) = exclude { + match exclude { + ExcludeCol::Col(col) => { + exclude_cols.insert(col.name.clone()); + } + ExcludeCol::Cols(cols) => { + for col in cols { + exclude_cols.insert(col.name.clone()); + } + if exclude_cols.len() < cols.len() { + // * except (id, id) + return Err(ErrorCode::SemanticError("duplicate column name")); + } + } + } + } + + // Pre-check exclude_col is legal + let precheck_exclude_cols = |input_context: &BindContext, + exclude_cols: &HashSet, + db_name: Option<&Identifier>, + table_name: Option<&Identifier>| + -> Result<()> { + let all_columns_bind = input_context.all_column_bindings(); + let mut qualified_cols_name: HashMap = HashMap::new(); + for i in all_columns_bind { + if let (None, None) = (db_name, table_name) { + if i.visibility != Visibility::Visible { + continue; + } + if qualified_cols_name.contains_key(i.column_name.as_str()) { + qualified_cols_name.insert( + i.column_name.clone(), + *qualified_cols_name.get(i.column_name.as_str()).unwrap() + + 1, + ); + } else { + qualified_cols_name.insert(i.column_name.clone(), 0u8); + } + } else if let (None, Some(table_name)) = (db_name, table_name) { + if i.visibility != Visibility::Visible { + continue; + } + if i.table_name == Some(table_name.name.clone()) { + if qualified_cols_name.contains_key(i.column_name.as_str()) { + qualified_cols_name.insert( + i.column_name.clone(), + *qualified_cols_name + .get(i.column_name.as_str()) + .unwrap() + + 1, + ); + } else { + qualified_cols_name.insert(i.column_name.clone(), 0u8); + } + } + } else if let (Some(db_name), Some(table_name)) = (db_name, table_name) + { + if i.visibility != Visibility::Visible { + continue; + } + if i.table_name == Some(table_name.name.clone()) + && i.database_name == Some(db_name.name.clone()) + { + if qualified_cols_name.contains_key(i.column_name.as_str()) { + qualified_cols_name.insert( + i.column_name.clone(), + *qualified_cols_name + .get(i.column_name.as_str()) + .unwrap() + + 1, + ); + } else { + qualified_cols_name.insert(i.column_name.clone(), 0u8); + } + } + } + } + for exclude_col in exclude_cols { + if qualified_cols_name.get(exclude_col).is_none() { + return Err(ErrorCode::SemanticError(format!( + "column '{exclude_col}' doesn't exist" + ))); + } else if 0 < *qualified_cols_name.get(exclude_col).unwrap() { + return Err(ErrorCode::SemanticError(format!( + "ambiguous column name '{exclude_col}'" + ))); + } + } + if exclude_cols.len() == qualified_cols_name.len() { + return Err(ErrorCode::SemanticError("SELECT with no columns")); + } + Ok(()) + }; + if names.len() == 1 { // * let indirection = &names[0]; @@ -159,18 +261,42 @@ impl<'a> Binder { Indirection::Star => { // Expands wildcard star, for example we have a table `t(a INT, b INT)`: // The query `SELECT * FROM t` will be expanded into `SELECT t.a, t.b FROM t` - for column_binding in input_context.all_column_bindings() { - if column_binding.visibility != Visibility::Visible { - continue; + if exclude_cols.is_empty() { + for column_binding in input_context.all_column_bindings() { + if column_binding.visibility != Visibility::Visible { + continue; + } + output.items.push(SelectItem { + select_target, + scalar: BoundColumnRef { + column: column_binding.clone(), + } + .into(), + alias: column_binding.column_name.clone(), + }); } - output.items.push(SelectItem { - select_target, - scalar: BoundColumnRef { - column: column_binding.clone(), + } else { + precheck_exclude_cols( + input_context, + &exclude_cols, + None, + None, + )?; + for column_binding in input_context.all_column_bindings() { + if column_binding.visibility != Visibility::Visible { + continue; } - .into(), - alias: column_binding.column_name.clone(), - }); + if exclude_cols.get(&column_binding.column_name).is_none() { + output.items.push(SelectItem { + select_target, + scalar: BoundColumnRef { + column: column_binding.clone(), + } + .into(), + alias: column_binding.column_name.clone(), + }); + } + } } } _ => { @@ -185,21 +311,54 @@ impl<'a> Binder { match indirection { Indirection::Identifier(table_name) => { let mut match_table = false; - for column_binding in input_context.all_column_bindings() { - if column_binding.visibility != Visibility::Visible { - continue; + if exclude_cols.is_empty() { + for column_binding in input_context.all_column_bindings() { + if column_binding.visibility != Visibility::Visible { + continue; + } + if column_binding.table_name + == Some(table_name.name.clone()) + { + match_table = true; + output.items.push(SelectItem { + select_target, + scalar: BoundColumnRef { + column: column_binding.clone(), + } + .into(), + alias: column_binding.column_name.clone(), + }); + } } - if column_binding.table_name == Some(table_name.name.clone()) { - match_table = true; - let select_item = SelectItem { - select_target, - scalar: BoundColumnRef { - column: column_binding.clone(), + } else { + precheck_exclude_cols( + input_context, + &exclude_cols, + None, + Some(table_name), + )?; + for column_binding in input_context.all_column_bindings() { + if column_binding.visibility != Visibility::Visible { + continue; + } + if column_binding.table_name + == Some(table_name.name.clone()) + { + match_table = true; + if exclude_cols + .get(&column_binding.column_name) + .is_none() + { + output.items.push(SelectItem { + select_target, + scalar: BoundColumnRef { + column: column_binding.clone(), + } + .into(), + alias: column_binding.column_name.clone(), + }); } - .into(), - alias: column_binding.column_name.clone(), - }; - output.items.push(select_item); + } } } if !match_table { @@ -225,26 +384,62 @@ impl<'a> Binder { Indirection::Identifier(table_name), ) => { let mut match_table = false; - for column_binding in input_context.all_column_bindings() { - if column_binding.visibility != Visibility::Visible { - continue; + if exclude_cols.is_empty() { + for column_binding in input_context.all_column_bindings() { + if column_binding.visibility != Visibility::Visible { + continue; + } + if column_binding.database_name + == Some(db_name.name.clone()) + && column_binding.table_name + == Some(table_name.name.clone()) + { + match_table = true; + output.items.push(SelectItem { + select_target, + scalar: BoundColumnRef { + column: column_binding.clone(), + } + .into(), + alias: column_binding.column_name.clone(), + }); + } } - if column_binding.database_name == Some(db_name.name.clone()) - && column_binding.table_name - == Some(table_name.name.clone()) - { - match_table = true; - let select_item = SelectItem { - select_target, - scalar: BoundColumnRef { - column: column_binding.clone(), + } else { + precheck_exclude_cols( + input_context, + &exclude_cols, + Some(db_name), + Some(table_name), + )?; + + for column_binding in input_context.all_column_bindings() { + if column_binding.visibility != Visibility::Visible { + continue; + } + if column_binding.database_name + == Some(db_name.name.clone()) + && column_binding.table_name + == Some(table_name.name.clone()) + { + match_table = true; + if exclude_cols + .get(&column_binding.column_name) + .is_none() + { + output.items.push(SelectItem { + select_target, + scalar: BoundColumnRef { + column: column_binding.clone(), + } + .into(), + alias: column_binding.column_name.clone(), + }); } - .into(), - alias: column_binding.column_name.clone(), - }; - output.items.push(select_item); + } } } + if !match_table { return Err(ErrorCode::UnknownTable(format!( "Unknown table '{}'.'{}'", diff --git a/src/query/sql/src/planner/binder/table.rs b/src/query/sql/src/planner/binder/table.rs index 76a3ac265d25..e0b13b46fdf0 100644 --- a/src/query/sql/src/planner/binder/table.rs +++ b/src/query/sql/src/planner/binder/table.rs @@ -61,7 +61,10 @@ impl<'a> Binder { stmt: &SelectStmt<'a>, ) -> Result<(SExpr, BindContext)> { for select_target in &stmt.select_list { - if let SelectTarget::QualifiedName(names) = select_target { + if let SelectTarget::QualifiedName { + qualified: names, .. + } = select_target + { for indirect in names { if indirect == &Indirection::Star { return Err(ErrorCode::SemanticError(stmt.span.display_error( diff --git a/tests/logictest/suites/base/03_dml/03_0034_select_except_list b/tests/logictest/suites/base/03_dml/03_0034_select_except_list new file mode 100644 index 000000000000..476206e386a2 --- /dev/null +++ b/tests/logictest/suites/base/03_dml/03_0034_select_except_list @@ -0,0 +1,159 @@ +statement ok +drop database if exists db; + + +statement ok +drop table if exists default.t; + + +statement ok +drop table if exists default.t1; + + +statement ok +create table default.t (id int, c1 tuple(int, int)); + + +statement ok +create table default.t1 (id int, c2 tuple(int, int)); + + +statement ok +create database db; + + +statement ok +create table db.t (id int, c1 tuple(int, int)); + + +statement ok +create table db.t1 (id int, c1 tuple(int, int)); + + +statement ok +create table db.t2 (id2 int, c1 tuple(int, int)); + + +statement ok +insert into db.t(id) values(1); + + +statement ok +insert into db.t1(id) values(2); + + +statement ok +insert into db.t2(id2) values(3); + + +statement ok +insert into default.t values(1, (100,100)); + + +statement ok +insert into default.t1 values(2, (200,200)); + + +statement query I +select * except c1 from db.t; + +---- +1 + + +statement query T +select * except (id) from db.t; + +---- +(0,0) + + +statement query T +select t.* exclude (id) from db.t; + +---- +(0,0) + + +statement query T +select db.t.* except (id) from db.t; + +---- +(0,0) + + +statement error 1065 +select db.t.* except (id, c1) from db.t; + + +statement query T +select c.* except (id) from db.t as c; + +---- +(0,0) + + +statement error 1065 +select db.c.* except (id) from db.t as c; + + +statement error 1065 +select * except id from (select t.id as id, t.c1 as id from db.t) t1; + + +statement error 1065 +select t1.* except id from (select t.id as id, t.c1 as id from db.t) t1; + + +statement query ITITITIIT +select *, t.* except c1, t1.* except (c1), t2.* except id from db.t join db.t1 on t.id != t1.id join default.t as t2 on t.id=t2.id; + +---- +1 (0,0) 2 (0,0) 1 (100,100) 1 2 (100,100) + + +statement query IT +select db.t.* except (c1), default.t1.* except id from db.t join default.t1; + +---- +1 (200,200) + + +statement error 1065 +select * except id from default.t1 join db.t; + + +statement error 1065 +select db.t.* except (c1), default.t1.* except idcc from db.t join default.t1; + + +statement error 1065 +select * except c10 from (select * from db.t) t1; + + +statement error 1065 +select * except (id, c1) from (select * from db.t) t1; + + +statement error 1065 +select t1.* except (id, c1) from (select * from db.t) t1; + + +statement error 1065 +select * except (c1, c1) from db.t; + + +statement error 1065 +select * except (id, id) from db.t; + + +statement ok +drop database if exists db; + + +statement ok +drop table default.t; + + +statement ok +drop table default.t1; \ No newline at end of file