Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,15 @@ pub enum InTarget {
/// `UNNEST(array_expression)`, see
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/operators#in_operators)
Unnest(Box<Expr>),
/// A table name or table-valued function, read as `(SELECT * FROM name)`, see
/// [SQLite](https://www.sqlite.org/lang_expr.html#in_op)
Table {
#[cfg_attr(feature = "visitor", visit(with = "visit_relation"))]
/// Table or table-valued function name, visited as a relation.
name: ObjectName,
/// Table-valued function arguments, `None` for a plain table name.
args: Option<Vec<FunctionArg>>,
},
}

impl fmt::Display for InTarget {
Expand All @@ -891,6 +900,13 @@ impl fmt::Display for InTarget {
InTarget::List(list) => write!(f, "({})", display_comma_separated(list)),
InTarget::Subquery(subquery) => write!(f, "({subquery})"),
InTarget::Unnest(array_expr) => write!(f, "UNNEST({array_expr})"),
InTarget::Table { name, args } => {
write!(f, "{name}")?;
if let Some(args) = args {
write!(f, "({})", display_comma_separated(args))?;
}
Ok(())
}
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,11 @@ impl Spanned for Expr {
),
InTarget::Subquery(subquery) => expr.span().union(&subquery.span()),
InTarget::Unnest(array_expr) => expr.span().union(&array_expr.span()),
InTarget::Table { name, args } => union_spans(
[expr.span(), name.span()]
.into_iter()
.chain(args.iter().flatten().map(|arg| arg.span())),
),
},
Expr::Between {
expr,
Expand Down Expand Up @@ -3258,4 +3263,20 @@ SETTINGS update_parallel_mode = 'auto'"#;
assert_eq!(test.get_source(expr.span()), expected, "{sql}");
}
}

#[test]
fn test_in_table_expression_spans() {
let dialect = &crate::dialect::SQLiteDialect {};
for (sql, expected) in [
(" needle IN main.items", "needle IN main.items"),
(
" needle IN main.f(first, last)",
"needle IN main.f(first, last",
),
] {
let mut test = SpanTest::new(dialect, sql);
let expr = test.0.parse_expr().unwrap();
assert_eq!(test.get_source(expr.span()), expected, "{sql}");
}
}
}
4 changes: 4 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ impl Dialect for GenericDialect {
true
}

fn supports_in_table_expression(&self) -> bool {
true
}

fn supports_comment_on(&self) -> bool {
true
}
Expand Down
8 changes: 8 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,14 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect supports a table name or table-valued function
/// as the right-hand side of `IN`, e.g. `x IN t`, `x IN schema.t`, or `x IN f(args)`.
///
/// [SQLite](https://www.sqlite.org/lang_expr.html#in_op)
fn supports_in_table_expression(&self) -> bool {
false
}

/// Returns true if the dialect supports `BEGIN {DEFERRED | IMMEDIATE | EXCLUSIVE | TRY | CATCH} [TRANSACTION]` statements
fn supports_start_transaction_modifier(&self) -> bool {
false
Expand Down
71 changes: 71 additions & 0 deletions src/dialect/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,75 @@ impl Dialect for SQLiteDialect {
fn supports_unterminated_block_comments(&self) -> bool {
true
}

fn supports_in_table_expression(&self) -> bool {
true
}

/// The keywords SQLite does not accept as a name, as opposed to the
/// keywords its grammar lets fall back to an identifier.
/// See <https://www.sqlite.org/lang_keywords.html> and the `%fallback ID`
/// list in [parse.y](https://sqlite.org/src/file?name=src/parse.y)
fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
!matches!(
kw,
Keyword::ADD
| Keyword::ALL
| Keyword::ALTER
| Keyword::AND
| Keyword::AS
| Keyword::AUTOINCREMENT
| Keyword::BETWEEN
| Keyword::CASE
| Keyword::CHECK
| Keyword::COLLATE
| Keyword::COMMIT
| Keyword::CONSTRAINT
| Keyword::CREATE
| Keyword::DEFAULT
| Keyword::DEFERRABLE
| Keyword::DELETE
| Keyword::DISTINCT
| Keyword::DROP
| Keyword::ELSE
| Keyword::ESCAPE
| Keyword::EXCEPT
| Keyword::EXISTS
| Keyword::FOREIGN
| Keyword::FROM
| Keyword::GROUP
| Keyword::HAVING
| Keyword::IN
| Keyword::INDEX
| Keyword::INSERT
| Keyword::INTERSECT
| Keyword::INTO
| Keyword::IS
| Keyword::JOIN
| Keyword::LIMIT
| Keyword::NOT
| Keyword::NOTHING
| Keyword::NOTNULL
| Keyword::NULL
| Keyword::ON
| Keyword::OR
| Keyword::ORDER
| Keyword::PRIMARY
| Keyword::REFERENCES
| Keyword::RETURNING
| Keyword::SELECT
| Keyword::SET
| Keyword::TABLE
| Keyword::THEN
| Keyword::TO
| Keyword::TRANSACTION
| Keyword::UNION
| Keyword::UNIQUE
| Keyword::UPDATE
| Keyword::USING
| Keyword::VALUES
| Keyword::WHEN
| Keyword::WHERE
)
}
}
27 changes: 27 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4515,6 +4515,19 @@ impl<'a> Parser<'a> {
let array_expr = self.parse_expr()?;
self.expect_token(&Token::RParen)?;
InTarget::Unnest(Box::new(array_expr))
} else if !global
&& self.dialect.supports_in_table_expression()
&& self.next_word_is_table_name()
{
let name = self.parse_object_name(false)?;
let args = if self.consume_token(&Token::LParen) {
let args = self.parse_comma_separated0(Self::parse_function_args, Token::RParen)?;
self.expect_token(&Token::RParen)?;
Some(args)
} else {
None
};
InTarget::Table { name, args }
} else if self.dialect.supports_in_unparenthesized_expr()
&& self.peek_token_ref().token != Token::LParen
{
Expand All @@ -4532,6 +4545,20 @@ impl<'a> Parser<'a> {
})
}

/// Returns true if the next token is a word the dialect accepts as a table
/// name. The word is consumed while asking, so `is_table_factor` peeks past
/// it as it does at the trailing comma check.
fn next_word_is_table_name(&mut self) -> bool {
let Token::Word(word) = &self.peek_token_ref().token else {
return false;
};
let keyword = word.keyword;
self.advance_token();
let is_name = self.dialect.is_table_factor(&keyword, self);
self.prev_token();
is_name
}

/// Parses the parenthesized subquery or value list of an `IN` predicate.
fn parse_in_parenthesized_target(&mut self) -> Result<InTarget, ParserError> {
self.expect_token(&Token::LParen)?;
Expand Down
130 changes: 128 additions & 2 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2621,8 +2621,10 @@ fn parse_in_error() {
// <expr> IN <expr> is no valid, except in dialects that accept an
// unparenthesized expression as the IN right-hand side (e.g. ClickHouse).
let sql = "SELECT * FROM customers WHERE segment in segment";
let res =
all_dialects_except(|d| d.supports_in_unparenthesized_expr()).parse_sql_statements(sql);
let res = all_dialects_except(|d| {
d.supports_in_unparenthesized_expr() || d.supports_in_table_expression()
})
.parse_sql_statements(sql);
assert_eq!(
ParserError::ParserError("Expected: (, found: segment".to_string()),
res.unwrap_err()
Expand Down Expand Up @@ -21022,3 +21024,127 @@ fn parse_window_spec_keyword_base_window_name() {
"SELECT count(*) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t",
);
}

#[test]
fn parse_in_table_expression() {
let dialects = all_dialects_where(|d| d.supports_in_table_expression());
for sql in [
"SELECT a IN t FROM t",
"SELECT a NOT IN t FROM t",
"SELECT a IN main.t FROM t",
"SELECT a IN f() FROM t",
"SELECT a NOT IN f(1, 2) FROM t",
] {
dialects.verified_stmt(sql);
}
match dialects.verified_expr("a IN main.f(1)") {
Expr::In {
target:
InTarget::Table {
name,
args: Some(args),
},
negated: false,
global: false,
..
} => {
assert_eq!(name.to_string(), "main.f");
assert_eq!(args.len(), 1);
}
other => panic!("expected an IN table target, got {other:?}"),
}
dialects.verified_stmt("SELECT a IN (1, 2) FROM t");
dialects.verified_stmt("SELECT a IN (SELECT b FROM u) FROM t");
}

/// Malformed table and table-function targets of `IN` keep their precise
/// parse errors.
#[test]
fn parse_in_table_expression_malformed_targets() {
let dialects = all_dialects_where(|d| d.supports_in_table_expression());
for (sql, expected) in [
// A dangling dot leaves the qualified name without its last part.
("SELECT a IN t.", "Expected: identifier, found: EOF"),
// A trailing comma leaves the next argument without an expression.
("SELECT a IN f(x,)", "Expected: an expression, found: )"),
// The argument list never closes its parenthesis.
("SELECT a IN f(1, 2", "Expected: ), found: EOF"),
] {
assert_eq!(
ParserError::ParserError(expected.to_string()),
dialects.parse_sql_statements(sql).unwrap_err(),
"{sql}"
);
}
}

/// Display of an `IN` table target propagates formatter errors from the
/// name and the argument list writes.
#[test]
fn display_in_table_target_propagates_formatter_errors() {
use core::fmt::{self, Write};

struct RejectingWriter(char);

impl Write for RejectingWriter {
fn write_str(&mut self, value: &str) -> fmt::Result {
if value.contains(self.0) {
Err(fmt::Error)
} else {
Ok(())
}
}
}

let plain = InTarget::Table {
name: ObjectName::from(Ident::new("t")),
args: None,
};
let mut writer = RejectingWriter('t');
assert_eq!(write!(writer, "{plain}"), Err(fmt::Error));

let function = InTarget::Table {
name: ObjectName::from(Ident::new("t")),
args: Some(vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(
Expr::value(number("1")),
))]),
};
let mut writer = RejectingWriter('1');
assert_eq!(write!(writer, "{function}"), Err(fmt::Error));
}

#[test]
#[cfg(feature = "visitor")]
fn test_visit_relations_in_table_target() {
let dialects = all_dialects_where(|d| d.supports_in_table_expression());
for dialect in dialects.dialects {
let stmts = Parser::parse_sql(dialect.as_ref(), "SELECT a IN lookup FROM source").unwrap();
let mut relations = vec![];
let _ = sqlparser::ast::visit_relations(&stmts, |relation| {
relations.push(relation.to_string());
core::ops::ControlFlow::<()>::Continue(())
});
assert_eq!(relations, ["lookup", "source"], "{dialect:?}");
}
}

#[test]
#[cfg(feature = "visitor")]
fn test_visit_relations_mut_in_table_target() {
let dialects = all_dialects_where(|d| d.supports_in_table_expression());
for dialect in dialects.dialects {
let mut stmts =
Parser::parse_sql(dialect.as_ref(), "SELECT a IN lookup FROM source").unwrap();
let _ = sqlparser::ast::visit_relations_mut(&mut stmts, |relation| {
if relation.to_string() == "lookup" {
*relation = ObjectName::from(Ident::new("renamed"));
}
core::ops::ControlFlow::<()>::Continue(())
});
assert_eq!(
stmts[0].to_string(),
"SELECT a IN renamed FROM source",
"{dialect:?}"
);
}
}
29 changes: 29 additions & 0 deletions tests/sqlparser_sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1250,3 +1250,32 @@ fn parse_bitwise_and_or_share_precedence() {
assert_eq!(sqlite().verified_expr(sql), expected, "{sql}");
}
}

#[test]
fn parse_in_table_keyword_names() {
// Keywords SQLite lets fall back to an identifier name a table after `IN`.
for sql in [
"SELECT 1 IN view",
"SELECT 1 IN offset",
"SELECT 1 IN key",
"SELECT 1 IN abort",
] {
sqlite().verified_stmt(sql);
}
// Its reserved keywords do not, so the target falls back to a parenthesized list.
for (sql, found) in [
("SELECT 1 IN FROM t", "FROM"),
("SELECT 1 IN select", "select"),
("SELECT 1 IN where", "where"),
("SELECT 1 IN NULL", "NULL"),
("SELECT 1 IN table", "table"),
("SELECT a FROM t WHERE a IN AND b", "AND"),
] {
assert_eq!(
sqlite().parse_sql_statements(sql).unwrap_err(),
ParserError::ParserError(format!("Expected: (, found: {found}")),
);
}
// A quoted keyword is a plain name.
sqlite().verified_stmt(r#"SELECT 1 IN "from""#);
}
Loading