diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb5bc6cc75..c2438e2c0c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -107,13 +107,13 @@ pub use self::query::{ RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, - TableAliasColumnDef, TableFactor, TableFunctionArgs, TableIndexHintForClause, - TableIndexHintType, TableIndexHints, TableIndexType, TableSample, TableSampleBucket, - TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed, - TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, - UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, - XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn, - XmlTableColumnOption, + TableAliasColumnDef, TableFactor, TableFunctionArgs, TableIndexDirective, + TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableSample, + TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier, + TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion, + TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values, + WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition, XmlPassingArgument, + XmlPassingClause, XmlTableColumn, XmlTableColumnOption, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index 4aa82c7c3b..b86d79092e 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1453,6 +1453,30 @@ impl fmt::Display for TableIndexHints { } } +/// A table-level index-selection directive, see [`TableFactor::Table::index_directives`]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableIndexDirective { + /// `{USE | IGNORE | FORCE} {INDEX | KEY} [FOR ...] (index_name, ...)`, see + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/index-hints.html) + MySql(TableIndexHints), + /// Require the named index with SQLite's [`INDEXED BY`](https://www.sqlite.org/lang_indexedby.html). + IndexedBy(Ident), + /// Prohibit index use except rowid lookups with SQLite's [`NOT INDEXED`](https://www.sqlite.org/lang_indexedby.html). + NotIndexed, +} + +impl fmt::Display for TableIndexDirective { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TableIndexDirective::MySql(hint) => hint.fmt(f), + TableIndexDirective::IndexedBy(name) => write!(f, "INDEXED BY {name}"), + TableIndexDirective::NotIndexed => f.write_str("NOT INDEXED"), + } + } +} + /// A table name or a parenthesized subquery with an optional alias #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -1490,9 +1514,8 @@ pub enum TableFactor { /// Optional table sample modifier /// See: sample: Option, - /// Optional index hints(mysql) - /// See: - index_hints: Vec, + /// Optional table-level index-selection hints or requirements. + index_directives: Vec, }, /// A derived table (a parenthesized subquery), optionally `LATERAL`. Derived { @@ -2226,7 +2249,7 @@ impl fmt::Display for TableFactor { with_ordinality, json_path, sample, - index_hints, + index_directives, } => { name.fmt(f)?; if let Some(json_path) = json_path { @@ -2255,8 +2278,8 @@ impl fmt::Display for TableFactor { if let Some(alias) = alias { write!(f, " {alias}")?; } - if !index_hints.is_empty() { - write!(f, " {}", display_separated(index_hints, " "))?; + if !index_directives.is_empty() { + write!(f, " {}", display_separated(index_directives, " "))?; } if !with_hints.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_hints))?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8267c71798..a1cf705d3a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1993,7 +1993,7 @@ impl Spanned for TableFactor { partitions: _, json_path: _, sample: _, - index_hints: _, + index_directives: _, } => union_spans( name.0 .iter() diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 3ce71eb0be..db10eabeaa 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1580,6 +1580,13 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports SQLite's `INDEXED BY` and `NOT INDEXED` index-selection clauses. + /// + /// See + fn supports_indexed_by(&self) -> bool { + false + } + /// Returns true if this dialect requires a whitespace character after `--` to start a single line comment. /// /// MySQL: diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 41b0e8be16..2f226c1c6d 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -21,7 +21,7 @@ use alloc::boxed::Box; use crate::ast::BinaryOperator; use crate::ast::{Expr, Statement}; use crate::dialect::{Dialect, Precedence}; -use crate::keywords::Keyword; +use crate::keywords::{self, Keyword}; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; @@ -160,4 +160,17 @@ impl Dialect for SQLiteDialect { fn supports_unterminated_block_comments(&self) -> bool { true } + + fn supports_indexed_by(&self) -> bool { + true + } + + /// `INDEXED BY` and `NOT INDEXED` follow the table name or alias. + fn is_table_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool { + match kw { + Keyword::INDEXED => false, + Keyword::NOT => !parser.peek_keyword(Keyword::INDEXED), + _ => !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw), + } + } } diff --git a/src/keywords.rs b/src/keywords.rs index 7e93822416..bde973be3a 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -519,6 +519,7 @@ define_keywords!( INCREMENT, INCREMENTAL, INDEX, + INDEXED, INDICATOR, INHERIT, INHERITS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 276b252ed2..9ed113ba08 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13670,7 +13670,7 @@ impl<'a> Parser<'a> { } } - fn parse_table_index_hints(&mut self) -> Result, ParserError> { + fn parse_table_index_hints(&mut self) -> Result, ParserError> { let mut hints = vec![]; while let Some(hint_type) = self.parse_one_of_keywords(&[Keyword::USE, Keyword::IGNORE, Keyword::FORCE]) @@ -13719,12 +13719,12 @@ impl<'a> Parser<'a> { vec![] }; self.expect_token(&Token::RParen)?; - hints.push(TableIndexHints { + hints.push(TableIndexDirective::MySql(TableIndexHints { hint_type, index_type, for_clause, index_names, - }); + })); } Ok(hints) } @@ -17112,13 +17112,19 @@ impl<'a> Parser<'a> { let alias = self.maybe_parse_table_alias()?; - // MYSQL-specific table hints: - let index_hints = if self.dialect.supports_table_hints() { + let mut index_directives = if self.dialect.supports_table_hints() { self.maybe_parse(|p| p.parse_table_index_hints())? .unwrap_or(vec![]) } else { vec![] }; + if self.dialect.supports_indexed_by() { + if self.parse_keywords(&[Keyword::INDEXED, Keyword::BY]) { + index_directives.push(TableIndexDirective::IndexedBy(self.parse_identifier()?)); + } else if self.parse_keywords(&[Keyword::NOT, Keyword::INDEXED]) { + index_directives.push(TableIndexDirective::NotIndexed); + } + } // MSSQL-specific table hints: let mut with_hints = vec![]; @@ -17148,7 +17154,7 @@ impl<'a> Parser<'a> { with_ordinality, json_path, sample, - index_hints, + index_directives, }; while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { @@ -17198,7 +17204,7 @@ impl<'a> Parser<'a> { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }) } diff --git a/src/test_utils.rs b/src/test_utils.rs index c4d1d0db2e..4c9bbb2bc2 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -390,7 +390,7 @@ pub fn table(name: impl Into) -> TableFactor { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], } } @@ -405,7 +405,7 @@ pub fn table_from_name(name: ObjectName) -> TableFactor { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], } } @@ -424,7 +424,7 @@ pub fn table_with_alias( with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 612d3c2b00..b110423370 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1767,7 +1767,7 @@ fn parse_table_time_travel() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![] },] @@ -1882,7 +1882,7 @@ fn parse_merge() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, table ); @@ -1897,7 +1897,7 @@ fn parse_merge() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, source ); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 87fb61dc17..ba74a55541 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -602,7 +602,7 @@ fn parse_update_with_table_alias() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![], }, @@ -704,7 +704,7 @@ fn parse_select_with_table_alias() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![], }] @@ -910,7 +910,7 @@ fn parse_where_delete_with_alias_statement() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, from[0].relation, ); @@ -926,7 +926,7 @@ fn parse_where_delete_with_alias_statement() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![], }]), @@ -7764,7 +7764,7 @@ fn parse_joins_on() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, global, join_operator: f(JoinConstraint::On(Expr::BinaryOp { @@ -7906,7 +7906,7 @@ fn parse_joins_using() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, global: false, join_operator: f(JoinConstraint::Using(vec![ObjectName::from(vec![ @@ -8001,7 +8001,7 @@ fn parse_natural_join() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, global: false, join_operator: f(JoinConstraint::Natural), @@ -10452,7 +10452,7 @@ fn parse_merge() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], } ); assert_eq!(table, table_no_into); @@ -11929,7 +11929,7 @@ fn parse_pivot_table() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }), aggregate_functions: vec![ expected_function("a", None), @@ -12009,7 +12009,7 @@ fn parse_pivot_table() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }), aggregate_functions: vec![ ExprWithAlias { @@ -12095,7 +12095,7 @@ fn parse_unpivot_table() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }), null_inclusion: None, value: Expr::Identifier(Ident::new("quantity")), @@ -12300,23 +12300,26 @@ fn parse_select_table_with_index_hints() { let s = supported_dialects.verified_only_select( "SELECT * FROM t1 USE INDEX (i1) IGNORE INDEX FOR ORDER BY (i2) ORDER BY a", ); - if let TableFactor::Table { index_hints, .. } = &s.from[0].relation { + if let TableFactor::Table { + index_directives, .. + } = &s.from[0].relation + { assert_eq!( vec![ - TableIndexHints { + TableIndexDirective::MySql(TableIndexHints { hint_type: TableIndexHintType::Use, index_names: vec!["i1".into()], index_type: TableIndexType::Index, for_clause: None, - }, - TableIndexHints { + }), + TableIndexDirective::MySql(TableIndexHints { hint_type: TableIndexHintType::Ignore, index_names: vec!["i2".into()], index_type: TableIndexType::Index, for_clause: Some(TableIndexHintForClause::OrderBy), - }, + }), ], - *index_hints + *index_directives ); } else { panic!("Expected TableFactor::Table"); @@ -12351,7 +12354,7 @@ fn parse_select_table_with_index_hints() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![], }] @@ -12380,7 +12383,7 @@ fn parse_pivot_unpivot_table() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }), null_inclusion: None, value: Expr::Identifier(Ident::new("population")), @@ -18226,7 +18229,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -18239,7 +18242,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, global: false, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { @@ -18293,7 +18296,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -18306,7 +18309,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, global: false, join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { @@ -18360,7 +18363,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -18373,7 +18376,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, global: false, join_operator: JoinOperator::Left(JoinConstraint::On(Expr::BinaryOp { @@ -18427,7 +18430,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -18440,7 +18443,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, global: false, join_operator: JoinOperator::Right(JoinConstraint::On(Expr::BinaryOp { @@ -18494,7 +18497,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -18507,7 +18510,7 @@ fn test_nested_join_without_parentheses() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, global: false, join_operator: JoinOperator::FullOuter(JoinConstraint::On( diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 21e76e23b6..6459b8fc42 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -471,7 +471,7 @@ fn parse_delimited_identifiers() { partitions: _, json_path: _, sample: _, - index_hints: _, + index_directives: _, } => { assert_eq!( ObjectName::from(vec![Ident::with_quote('"', "a table")]), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9bf015fc00..a42f216757 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -75,7 +75,7 @@ fn parse_table_time_travel() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![] + index_directives: vec![] }, joins: vec![] },] @@ -495,7 +495,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, - index_hints: vec![] + index_directives: vec![] }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -549,7 +549,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, - index_hints: vec![] + index_directives: vec![] }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -603,7 +603,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, - index_hints: vec![] + index_directives: vec![] }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -657,7 +657,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![Join { relation: TableFactor::OpenJsonTable { @@ -691,7 +691,7 @@ fn parse_mssql_openjson() { partitions: vec![], json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![Join { relation: TableFactor::OpenJsonTable { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 8baa83b6bb..233a5403a5 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2794,7 +2794,7 @@ fn parse_update_with_joins() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, joins: vec![Join { relation: TableFactor::Table { @@ -2807,7 +2807,7 @@ fn parse_update_with_joins() { with_ordinality: false, json_path: None, sample: None, - index_hints: vec![], + index_directives: vec![], }, global: false, join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp { diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 1752e976cb..60bb6aad82 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1161,6 +1161,84 @@ fn parse_unterminated_block_comment() { ); } +#[test] +fn parse_indexed_by() { + let index_directives = |relation: &TableFactor| match relation { + TableFactor::Table { + index_directives, .. + } => index_directives.clone(), + _ => panic!("expected TableFactor::Table"), + }; + + let select = sqlite().verified_only_select("SELECT * FROM t INDEXED BY i WHERE a = 1"); + assert_eq!( + index_directives(&select.from[0].relation), + vec![TableIndexDirective::IndexedBy(Ident::new("i"))] + ); + + for sql in [ + "SELECT * FROM t NOT INDEXED", + "SELECT * FROM t AS x NOT INDEXED", + ] { + let select = sqlite().verified_only_select(sql); + assert_eq!( + index_directives(&select.from[0].relation), + vec![TableIndexDirective::NotIndexed] + ); + } + + let select = sqlite().verified_only_select("SELECT * FROM t INDEXED BY i, t AS u NOT INDEXED"); + assert_eq!( + index_directives(&select.from[0].relation), + vec![TableIndexDirective::IndexedBy(Ident::new("i"))] + ); + assert_eq!( + index_directives(&select.from[1].relation), + vec![TableIndexDirective::NotIndexed] + ); + + sqlite().verified_stmt("SELECT * FROM t AS x INDEXED BY i WHERE a = 1"); + sqlite().verified_stmt("DELETE FROM t INDEXED BY i WHERE a = 1"); + sqlite().verified_stmt("UPDATE t INDEXED BY i SET a = 2 WHERE a = 1"); + + assert_eq!( + all_dialects_where(|d| !d.supports_indexed_by()) + .parse_sql_statements("SELECT * FROM t INDEXED BY i"), + Err(ParserError::ParserError( + "Expected: end of statement, found: BY".to_string() + )) + ); +} + +#[test] +fn display_index_directives_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(()) + } + } + } + + for (sql, rejected) in [ + ("SELECT * FROM t INDEXED BY idx_b", 'b'), + ("SELECT * FROM t NOT INDEXED", 'N'), + ] { + let select = sqlite().verified_only_select(sql); + let mut writer = RejectingWriter(rejected); + assert_eq!( + write!(writer, "{}", select.from[0].relation), + Err(fmt::Error) + ); + } +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) }