Skip to content
3 changes: 3 additions & 0 deletions fuzz/fuzz_seeds/sqlite_2680_in_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SELECT a IN t, a NOT IN t, a IN main.t, a IN f(), a NOT IN f(1, 2), a IN main.f(1) FROM t;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫰

SELECT 1 IN view, 1 IN offset, 1 IN key, 1 IN abort FROM t;
SELECT 1 IN "from" FROM t
16 changes: 16 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,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 @@ -892,6 +901,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 @@ -1542,6 +1542,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 @@ -3351,4 +3356,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 @@ -193,6 +193,10 @@ impl Dialect for GenericDialect {
true
}

fn supports_in_table_expression(&self) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenericDialect now accepts reserved words as an IN table name, because it
keeps the default is_table_factor (which reserves only INTO, LIMIT,
HAVING, WHERE). Inputs that failed with Expected: ( before now parse:

  • SELECT 1 IN NULL -> table named NULL
  • SELECT 1 IN select -> table named select
  • SELECT 1 IN case -> table named case

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 @@ -531,6 +531,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 @@ -244,6 +244,77 @@ impl Dialect for SQLiteDialect {
fn supports_bitwise_shift_operators(&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
)
}
}

/// SQLite permits signed and decimal numeric type modifiers.
Expand Down
27 changes: 27 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4531,6 +4531,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 @@ -4548,6 +4561,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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be next_word_is_table_factor?

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 @@ -2692,8 +2692,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 All @@ -2712,6 +2714,130 @@ fn parse_in_unparenthesized_expr() {
dialects.verified_stmt("SELECT * FROM t WHERE x IN (SELECT y FROM u)");
}

#[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:?}"
);
}
}

#[test]
fn parse_string_agg() {
let sql = "SELECT a || b";
Expand Down
29 changes: 29 additions & 0 deletions tests/sqlparser_sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,35 @@ fn parse_where_in_empty_list() {
);
}

#[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""#);
}

#[test]
fn invalid_empty_list() {
let sql = "SELECT * FROM t1 WHERE a IN (,,)";
Expand Down
Loading