mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-07-07 17:04:59 +00:00
Add support for mysql table hints (#1675)
This commit is contained in:
parent
fdbe864d0d
commit
f7b0812b01
12 changed files with 266 additions and 5 deletions
|
@ -69,11 +69,11 @@ pub use self::query::{
|
|||
OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem,
|
||||
RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select,
|
||||
SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table,
|
||||
TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs, TableSample,
|
||||
TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier,
|
||||
TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion,
|
||||
TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values,
|
||||
WildcardAdditionalOptions, With, WithFill,
|
||||
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,
|
||||
};
|
||||
|
||||
pub use self::trigger::{
|
||||
|
|
|
@ -975,6 +975,81 @@ pub struct TableFunctionArgs {
|
|||
pub settings: Option<Vec<Setting>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
|
||||
pub enum TableIndexHintType {
|
||||
Use,
|
||||
Ignore,
|
||||
Force,
|
||||
}
|
||||
|
||||
impl fmt::Display for TableIndexHintType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
TableIndexHintType::Use => "USE",
|
||||
TableIndexHintType::Ignore => "IGNORE",
|
||||
TableIndexHintType::Force => "FORCE",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
|
||||
pub enum TableIndexType {
|
||||
Index,
|
||||
Key,
|
||||
}
|
||||
|
||||
impl fmt::Display for TableIndexType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
TableIndexType::Index => "INDEX",
|
||||
TableIndexType::Key => "KEY",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
|
||||
pub enum TableIndexHintForClause {
|
||||
Join,
|
||||
OrderBy,
|
||||
GroupBy,
|
||||
}
|
||||
|
||||
impl fmt::Display for TableIndexHintForClause {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
TableIndexHintForClause::Join => "JOIN",
|
||||
TableIndexHintForClause::OrderBy => "ORDER BY",
|
||||
TableIndexHintForClause::GroupBy => "GROUP BY",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
|
||||
pub struct TableIndexHints {
|
||||
pub hint_type: TableIndexHintType,
|
||||
pub index_type: TableIndexType,
|
||||
pub for_clause: Option<TableIndexHintForClause>,
|
||||
pub index_names: Vec<Ident>,
|
||||
}
|
||||
|
||||
impl fmt::Display for TableIndexHints {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{} {} ", self.hint_type, self.index_type)?;
|
||||
if let Some(for_clause) = &self.for_clause {
|
||||
write!(f, "FOR {} ", for_clause)?;
|
||||
}
|
||||
write!(f, "({})", display_comma_separated(&self.index_names))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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))]
|
||||
|
@ -1009,6 +1084,9 @@ pub enum TableFactor {
|
|||
/// Optional table sample modifier
|
||||
/// See: <https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#sample-clause>
|
||||
sample: Option<TableSampleKind>,
|
||||
/// Optional index hints(mysql)
|
||||
/// See: <https://dev.mysql.com/doc/refman/8.4/en/index-hints.html>
|
||||
index_hints: Vec<TableIndexHints>,
|
||||
},
|
||||
Derived {
|
||||
lateral: bool,
|
||||
|
@ -1590,6 +1668,7 @@ impl fmt::Display for TableFactor {
|
|||
with_ordinality,
|
||||
json_path,
|
||||
sample,
|
||||
index_hints,
|
||||
} => {
|
||||
write!(f, "{name}")?;
|
||||
if let Some(json_path) = json_path {
|
||||
|
@ -1618,6 +1697,9 @@ impl fmt::Display for TableFactor {
|
|||
if let Some(alias) = alias {
|
||||
write!(f, " AS {alias}")?;
|
||||
}
|
||||
if !index_hints.is_empty() {
|
||||
write!(f, " {}", display_separated(index_hints, " "))?;
|
||||
}
|
||||
if !with_hints.is_empty() {
|
||||
write!(f, " WITH ({})", display_comma_separated(with_hints))?;
|
||||
}
|
||||
|
|
|
@ -1739,6 +1739,7 @@ impl Spanned for TableFactor {
|
|||
partitions: _,
|
||||
json_path: _,
|
||||
sample: _,
|
||||
index_hints: _,
|
||||
} => union_spans(
|
||||
name.0
|
||||
.iter()
|
||||
|
|
|
@ -854,6 +854,10 @@ pub trait Dialect: Debug + Any {
|
|||
fn supports_string_escape_constant(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Returns true if the dialect supports the table hints in the `FROM` clause.
|
||||
fn supports_table_hints(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents the operators for which precedence must be defined
|
||||
|
|
|
@ -25,6 +25,10 @@ use crate::{
|
|||
parser::{Parser, ParserError},
|
||||
};
|
||||
|
||||
use super::keywords;
|
||||
|
||||
const RESERVED_FOR_TABLE_ALIAS_MYSQL: &[Keyword] = &[Keyword::USE, Keyword::IGNORE, Keyword::FORCE];
|
||||
|
||||
/// A [`Dialect`] for [MySQL](https://www.mysql.com/)
|
||||
#[derive(Debug)]
|
||||
pub struct MySqlDialect {}
|
||||
|
@ -111,6 +115,16 @@ impl Dialect for MySqlDialect {
|
|||
fn supports_user_host_grantee(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool {
|
||||
explicit
|
||||
|| (!keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw)
|
||||
&& !RESERVED_FOR_TABLE_ALIAS_MYSQL.contains(kw))
|
||||
}
|
||||
|
||||
fn supports_table_hints(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// `LOCK TABLES`
|
||||
|
|
|
@ -8910,6 +8910,64 @@ impl<'a> Parser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_table_index_hints(&mut self) -> Result<Vec<TableIndexHints>, ParserError> {
|
||||
let mut hints = vec![];
|
||||
while let Some(hint_type) =
|
||||
self.parse_one_of_keywords(&[Keyword::USE, Keyword::IGNORE, Keyword::FORCE])
|
||||
{
|
||||
let hint_type = match hint_type {
|
||||
Keyword::USE => TableIndexHintType::Use,
|
||||
Keyword::IGNORE => TableIndexHintType::Ignore,
|
||||
Keyword::FORCE => TableIndexHintType::Force,
|
||||
_ => {
|
||||
return self.expected(
|
||||
"expected to match USE/IGNORE/FORCE keyword",
|
||||
self.peek_token(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let index_type = match self.parse_one_of_keywords(&[Keyword::INDEX, Keyword::KEY]) {
|
||||
Some(Keyword::INDEX) => TableIndexType::Index,
|
||||
Some(Keyword::KEY) => TableIndexType::Key,
|
||||
_ => {
|
||||
return self.expected("expected to match INDEX/KEY keyword", self.peek_token())
|
||||
}
|
||||
};
|
||||
let for_clause = if self.parse_keyword(Keyword::FOR) {
|
||||
let clause = if self.parse_keyword(Keyword::JOIN) {
|
||||
TableIndexHintForClause::Join
|
||||
} else if self.parse_keywords(&[Keyword::ORDER, Keyword::BY]) {
|
||||
TableIndexHintForClause::OrderBy
|
||||
} else if self.parse_keywords(&[Keyword::GROUP, Keyword::BY]) {
|
||||
TableIndexHintForClause::GroupBy
|
||||
} else {
|
||||
return self.expected(
|
||||
"expected to match FOR/ORDER BY/GROUP BY table hint in for clause",
|
||||
self.peek_token(),
|
||||
);
|
||||
};
|
||||
Some(clause)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.expect_token(&Token::LParen)?;
|
||||
let index_names = if self.peek_token().token != Token::RParen {
|
||||
self.parse_comma_separated(Parser::parse_identifier)?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
self.expect_token(&Token::RParen)?;
|
||||
hints.push(TableIndexHints {
|
||||
hint_type,
|
||||
index_type,
|
||||
for_clause,
|
||||
index_names,
|
||||
});
|
||||
}
|
||||
Ok(hints)
|
||||
}
|
||||
|
||||
/// Wrapper for parse_optional_alias_inner, left for backwards-compatibility
|
||||
/// but new flows should use the context-specific methods such as `maybe_parse_select_item_alias`
|
||||
/// and `maybe_parse_table_alias`.
|
||||
|
@ -11257,6 +11315,14 @@ impl<'a> Parser<'a> {
|
|||
|
||||
let alias = self.maybe_parse_table_alias()?;
|
||||
|
||||
// MYSQL-specific table hints:
|
||||
let index_hints = if self.dialect.supports_table_hints() {
|
||||
self.maybe_parse(|p| p.parse_table_index_hints())?
|
||||
.unwrap_or(vec![])
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// MSSQL-specific table hints:
|
||||
let mut with_hints = vec![];
|
||||
if self.parse_keyword(Keyword::WITH) {
|
||||
|
@ -11285,6 +11351,7 @@ impl<'a> Parser<'a> {
|
|||
with_ordinality,
|
||||
json_path,
|
||||
sample,
|
||||
index_hints,
|
||||
};
|
||||
|
||||
while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) {
|
||||
|
|
|
@ -362,6 +362,7 @@ pub fn table(name: impl Into<String>) -> TableFactor {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -376,6 +377,7 @@ pub fn table_from_name(name: ObjectName) -> TableFactor {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -393,6 +395,7 @@ pub fn table_with_alias(name: impl Into<String>, alias: impl Into<String>) -> Ta
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1565,6 +1565,7 @@ fn parse_table_time_travel() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
joins: vec![]
|
||||
},]
|
||||
|
@ -1665,6 +1666,7 @@ fn parse_merge() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
table
|
||||
);
|
||||
|
@ -1682,6 +1684,7 @@ fn parse_merge() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
source
|
||||
);
|
||||
|
|
|
@ -503,6 +503,7 @@ fn parse_update_with_table_alias() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
joins: vec![],
|
||||
},
|
||||
|
@ -596,6 +597,7 @@ fn parse_select_with_table_alias() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
joins: vec![],
|
||||
}]
|
||||
|
@ -792,6 +794,7 @@ fn parse_where_delete_with_alias_statement() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
from[0].relation,
|
||||
);
|
||||
|
@ -810,6 +813,7 @@ fn parse_where_delete_with_alias_statement() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
joins: vec![],
|
||||
}]),
|
||||
|
@ -6416,6 +6420,7 @@ fn parse_joins_on() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
global,
|
||||
join_operator: f(JoinConstraint::On(Expr::BinaryOp {
|
||||
|
@ -6545,6 +6550,7 @@ fn parse_joins_using() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
global: false,
|
||||
join_operator: f(JoinConstraint::Using(vec![ObjectName::from(vec![
|
||||
|
@ -6623,6 +6629,7 @@ fn parse_natural_join() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
global: false,
|
||||
join_operator: f(JoinConstraint::Natural),
|
||||
|
@ -8718,6 +8725,7 @@ fn parse_merge() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
}
|
||||
);
|
||||
assert_eq!(table, table_no_into);
|
||||
|
@ -9901,6 +9909,7 @@ fn parse_pivot_table() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
}),
|
||||
aggregate_functions: vec![
|
||||
expected_function("a", None),
|
||||
|
@ -9977,6 +9986,7 @@ fn parse_unpivot_table() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
}),
|
||||
value: Ident {
|
||||
value: "quantity".to_string(),
|
||||
|
@ -10023,6 +10033,73 @@ fn parse_unpivot_table() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_select_table_with_index_hints() {
|
||||
let supported_dialects = all_dialects_where(|d| d.supports_table_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 {
|
||||
assert_eq!(
|
||||
vec![
|
||||
TableIndexHints {
|
||||
hint_type: TableIndexHintType::Use,
|
||||
index_names: vec!["i1".into()],
|
||||
index_type: TableIndexType::Index,
|
||||
for_clause: None,
|
||||
},
|
||||
TableIndexHints {
|
||||
hint_type: TableIndexHintType::Ignore,
|
||||
index_names: vec!["i2".into()],
|
||||
index_type: TableIndexType::Index,
|
||||
for_clause: Some(TableIndexHintForClause::OrderBy),
|
||||
},
|
||||
],
|
||||
*index_hints
|
||||
);
|
||||
} else {
|
||||
panic!("Expected TableFactor::Table");
|
||||
}
|
||||
supported_dialects.verified_stmt("SELECT * FROM t1 USE INDEX (i1) USE INDEX (i1, i1)");
|
||||
supported_dialects.verified_stmt(
|
||||
"SELECT * FROM t1 USE INDEX () IGNORE INDEX (i2) USE INDEX (i1) USE INDEX (i2)",
|
||||
);
|
||||
supported_dialects.verified_stmt("SELECT * FROM t1 FORCE INDEX FOR JOIN (i2)");
|
||||
supported_dialects.verified_stmt("SELECT * FROM t1 IGNORE INDEX FOR JOIN (i2)");
|
||||
supported_dialects.verified_stmt(
|
||||
"SELECT * FROM t USE INDEX (index1) IGNORE INDEX FOR ORDER BY (index1) IGNORE INDEX FOR GROUP BY (index1) WHERE A = B",
|
||||
);
|
||||
|
||||
// Test that dialects that don't support table hints will keep parsing the USE as table alias
|
||||
let sql = "SELECT * FROM T USE LIMIT 1";
|
||||
let unsupported_dialects = all_dialects_where(|d| !d.supports_table_hints());
|
||||
let select = unsupported_dialects
|
||||
.verified_only_select_with_canonical(sql, "SELECT * FROM T AS USE LIMIT 1");
|
||||
assert_eq!(
|
||||
select.from,
|
||||
vec![TableWithJoins {
|
||||
relation: TableFactor::Table {
|
||||
name: ObjectName(vec![sqlparser::ast::ObjectNamePart::Identifier(
|
||||
Ident::new("T")
|
||||
)]),
|
||||
alias: Some(TableAlias {
|
||||
name: Ident::new("USE"),
|
||||
columns: vec![],
|
||||
}),
|
||||
args: None,
|
||||
with_hints: vec![],
|
||||
version: None,
|
||||
partitions: vec![],
|
||||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
joins: vec![],
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pivot_unpivot_table() {
|
||||
let sql = concat!(
|
||||
|
@ -10048,6 +10125,7 @@ fn parse_pivot_unpivot_table() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
}),
|
||||
value: Ident {
|
||||
value: "population".to_string(),
|
||||
|
|
|
@ -460,6 +460,7 @@ fn parse_delimited_identifiers() {
|
|||
partitions: _,
|
||||
json_path: _,
|
||||
sample: _,
|
||||
index_hints: _,
|
||||
} => {
|
||||
assert_eq!(
|
||||
ObjectName::from(vec![Ident::with_quote('"', "a table")]),
|
||||
|
|
|
@ -74,6 +74,7 @@ fn parse_table_time_travel() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![]
|
||||
},
|
||||
joins: vec![]
|
||||
},]
|
||||
|
@ -223,6 +224,7 @@ fn parse_mssql_openjson() {
|
|||
partitions: vec![],
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![]
|
||||
},
|
||||
joins: vec![Join {
|
||||
relation: TableFactor::OpenJsonTable {
|
||||
|
@ -282,6 +284,7 @@ fn parse_mssql_openjson() {
|
|||
partitions: vec![],
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![]
|
||||
},
|
||||
joins: vec![Join {
|
||||
relation: TableFactor::OpenJsonTable {
|
||||
|
@ -341,6 +344,7 @@ fn parse_mssql_openjson() {
|
|||
partitions: vec![],
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![]
|
||||
},
|
||||
joins: vec![Join {
|
||||
relation: TableFactor::OpenJsonTable {
|
||||
|
@ -400,6 +404,7 @@ fn parse_mssql_openjson() {
|
|||
partitions: vec![],
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
joins: vec![Join {
|
||||
relation: TableFactor::OpenJsonTable {
|
||||
|
@ -439,6 +444,7 @@ fn parse_mssql_openjson() {
|
|||
partitions: vec![],
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
joins: vec![Join {
|
||||
relation: TableFactor::OpenJsonTable {
|
||||
|
|
|
@ -2036,6 +2036,7 @@ fn parse_update_with_joins() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
joins: vec![Join {
|
||||
relation: TableFactor::Table {
|
||||
|
@ -2051,6 +2052,7 @@ fn parse_update_with_joins() {
|
|||
with_ordinality: false,
|
||||
json_path: None,
|
||||
sample: None,
|
||||
index_hints: vec![],
|
||||
},
|
||||
global: false,
|
||||
join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue