This is an automated email from the ASF dual-hosted git repository.
dtenedor pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 84bb88952f33 [SPARK-58122][SQL] Add SQL ASOF JOIN parser and error
conditions
84bb88952f33 is described below
commit 84bb88952f33955deb1aa6bbd4dff729973452f2
Author: Serge Rielau <[email protected]>
AuthorDate: Wed Jul 15 08:55:27 2026 -0700
[SPARK-58122][SQL] Add SQL ASOF JOIN parser and error conditions
## What changes were proposed in this pull request?
First stacked PR for
[SPARK-58092](https://issues.apache.org/jira/browse/SPARK-58092) /
[SPARK-58122](https://issues.apache.org/jira/browse/SPARK-58122): SQL surface
area for ASOF JOIN only (parser, keywords, errors, feature flag, minimal
logical plan).
### SQL syntax (introduced in this PR)
```sql
left_table [ AS alias ]
[ INNER | LEFT OUTER ] ASOF JOIN right_table [ AS alias ]
MATCH_CONDITION ( left_expr comparison_op right_expr )
[ ON boolean_expression | USING ( column_name [, ...] ) ]
```
- **`ASOF JOIN`** is a dedicated join form (not a modifier on `INNER JOIN`).
- **`MATCH_CONDITION`** is required and must be a single comparison using
one of `>=`, `>`, `<=`, `<`. Compound predicates (`AND` / `OR`), equality
(`=`), and other operators are rejected at parse time.
- **Join type**: `INNER` (default) or `LEFT OUTER` only.
- **Optional `ON` / `USING`**: same role as in a regular join — additional
equi-join (or general boolean) correlation between the two inputs. `ON` and
`USING` are mutually exclusive.
Example:
```sql
SELECT *
FROM trades t
ASOF JOIN quotes q
MATCH_CONDITION (t.trade_time >= q.quote_time)
ON t.symbol = q.symbol
```
### Intended semantics (full behavior lands in follow-up PRs)
ASOF join finds, for each row on the left, the best-matching row on the
right according to the `MATCH_CONDITION` ordering predicate (e.g. latest quote
at or before each trade time when using `>=`). Optional `ON` / `USING`
restricts candidates to rows that also satisfy the equi-join keys.
- **`INNER ASOF JOIN`**: left rows with no qualifying right match are
dropped.
- **`LEFT OUTER ASOF JOIN`**: left rows with no match are kept; right-side
columns are null-padded.
This PR parses SQL into an unresolved `AsOfJoin` logical node carrying
`matchComparison`, optional `ON`/`USING`, and join type. Analysis
(`ResolveAsOfJoin`), type/reference validation, and execution are **not** in
this PR.
### PySpark relationship
PySpark already exposes ASOF join via `DataFrame.joinAsOf` (as-of keys,
optional tolerance, direction, and join condition). This PR adds the SQL
counterpart and generalizes the join condition through an explicit
`MATCH_CONDITION` comparison (`left_expr op right_expr`) instead of tying
semantics to a single pair of as-of column arguments.
### In scope (this PR)
- ANTLR grammar and parser (`AstBuilder`)
- Keywords `ASOF` and `MATCH_CONDITION` (lexer, docs, golden files, JDBC
metadata test)
- `spark.sql.join.asofJoin.enabled` conf (default off)
- Parse-time errors for disabled feature and invalid `MATCH_CONDITION`
operators
- Minimal `AsOfJoin` extensions (`usingColumns`, `matchComparison`,
`fromMatchCondition`)
- Tests: `PlanParserSuite` (asof join), `AsOfJoinSQLSuite` (parse error)
### Out of scope (follow-up PRs)
- `ResolveAsOfJoin` / analysis validation (`TABLE_REFERENCE`, types,
operand normalization)
- Sort-merge execution (`SortMergeAsOfJoinExec`, planner wiring for SQL
`MATCH_CONDITION` plans)
- End-to-end SQL integration tests (`AsOfJoinSortMergeSQLSuite`)
## Why is this needed?
ASOF join is already supported in PySpark for temporal/event-style joins.
Adding SQL syntax completes API parity and makes the feature usable from
SQL-only workloads (notebooks, views, JDBC). The `MATCH_CONDITION` form also
generalizes beyond the PySpark API’s fixed left/right as-of column pair.
This is the first PR in a stacked series so reviewers can sign off on
grammar and error surface before analysis and execution land.
## Does this PR introduce any user-facing change?
No when `spark.sql.join.asofJoin.enabled` is false (default). When enabled,
SQL parses to an unresolved `AsOfJoin` logical node; analysis and execution are
not wired yet in this PR.
## How was this patch tested?
```
build/sbt "catalyst/testOnly
org.apache.spark.sql.catalyst.parser.PlanParserSuite -- -z \"asof join\""
"sql/testOnly org.apache.spark.sql.AsOfJoinSQLSuite"
```
## Was this patch authored or co-authored using generative AI tooling?
Co-authored with Cursor.
### SQL vs DataFrame API asymmetry
The SQL surface is **not** a strict superset of the DataFrame joinAsOf API:
- **SQL generalizes** the match predicate via MATCH_CONDITION (left_expr op
right_expr) instead of a fixed left/right as-of column pair.
- **SQL does not yet express** two DataFrame features in this stacked
series:
- tolerance (fromMatchCondition hard-codes toleranceAssertion = None)
- Nearest direction (only >=, >, <=, < are accepted, mapping to
Backward/Forward with exact-match; Nearest has no operator)
Follow-up PRs cover analysis/execution; tolerance/Nearest parity may be
separate follow-ups if we extend the grammar.
Closes #57251 from srielau/SPARK-58122.
Authored-by: Serge Rielau <[email protected]>
Signed-off-by: Daniel Tenedorio <[email protected]>
---
.../src/main/resources/error/error-conditions.json | 34 +++++++
docs/sql-ref-ansi-compliance.md | 2 +
.../spark/sql/catalyst/parser/SqlBaseLexer.g4 | 2 +
.../spark/sql/catalyst/parser/SqlBaseParser.g4 | 24 ++++-
.../spark/sql/errors/QueryParsingErrors.scala | 16 ++++
.../sql/catalyst/analysis/CheckAnalysis.scala | 4 +-
.../catalyst/analysis/DeduplicateRelations.scala | 2 +-
.../sql/catalyst/optimizer/RewriteAsOfJoin.scala | 3 +-
.../spark/sql/catalyst/parser/AstBuilder.scala | 67 ++++++++++++-
.../spark/sql/catalyst/plans/joinTypes.scala | 30 ++++++
.../plans/logical/basicLogicalOperators.scala | 38 +++++++-
.../org/apache/spark/sql/internal/SQLConf.scala | 11 +++
.../sql/catalyst/parser/PlanParserSuite.scala | 105 ++++++++++++++++++++-
.../jdbc/SparkConnectDatabaseMetaDataSuite.scala | 2 +-
.../spark/sql/execution/SparkStrategies.scala | 2 +-
.../sql-tests/results/keywords-enforced.sql.out | 2 +
.../resources/sql-tests/results/keywords.sql.out | 2 +
.../sql-tests/results/nonansi/keywords.sql.out | 2 +
.../org/apache/spark/sql/AsOfJoinSQLSuite.scala | 72 ++++++++++++++
.../spark/sql/errors/QueryParsingErrorsSuite.scala | 2 +-
.../ThriftServerWithSparkContextSuite.scala | 2 +-
21 files changed, 403 insertions(+), 21 deletions(-)
diff --git a/common/utils/src/main/resources/error/error-conditions.json
b/common/utils/src/main/resources/error/error-conditions.json
index a6150f5e9500..83ace844cfaa 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -156,6 +156,30 @@
],
"sqlState" : "42713"
},
+ "ASOF_JOIN_MATCH_CONDITION_INVALID_EXPRESSION" : {
+ "message" : [
+ "The MATCH_CONDITION operand contains an invalid expression: <expr>.
Rewrite the query to avoid subqueries, aggregate functions, window functions,
generator functions, or non-deterministic functions in MATCH_CONDITION
operands."
+ ],
+ "sqlState" : "42903"
+ },
+ "ASOF_JOIN_MATCH_CONDITION_INVALID_OPERATOR" : {
+ "message" : [
+ "The MATCH_CONDITION operator must be one of >=, >, <=, <. Got:
<operator>."
+ ],
+ "sqlState" : "42K0E"
+ },
+ "ASOF_JOIN_MATCH_CONDITION_INVALID_TYPE" : {
+ "message" : [
+ "The MATCH_CONDITION operands must be of an orderable, mutually
comparable type. Got: <type1> and <type2>."
+ ],
+ "sqlState" : "42K09"
+ },
+ "ASOF_JOIN_MATCH_CONDITION_TABLE_REFERENCE" : {
+ "message" : [
+ "Each operand of the MATCH_CONDITION comparison must reference only
columns of one join input. Got: <refs1> and <refs2>."
+ ],
+ "sqlState" : "42K0E"
+ },
"ASSIGNMENT_ARITY_MISMATCH" : {
"message" : [
"The number of columns or variables assigned or aliased: <numTarget>
does not match the number of source expressions: <numExpr>."
@@ -167,6 +191,11 @@
"Invalid as-of join."
],
"subClass" : {
+ "SORT_MERGE_REQUIRED" : {
+ "message" : [
+ "SQL ASOF JOIN requires the sort-merge physical operator. Set
<config> to true."
+ ]
+ },
"TOLERANCE_IS_NON_NEGATIVE" : {
"message" : [
"The input argument `tolerance` must be non-negative."
@@ -8261,6 +8290,11 @@
"The ANALYZE TABLE command does not support views."
]
},
+ "ASOF_JOIN" : {
+ "message" : [
+ "SQL ASOF JOIN syntax. Set <config> to true to enable it."
+ ]
+ },
"BIN_BY" : {
"message" : [
"The BIN BY relation operator is not yet supported."
diff --git a/docs/sql-ref-ansi-compliance.md b/docs/sql-ref-ansi-compliance.md
index c3eec6cc5ce3..367abce6cb8d 100644
--- a/docs/sql-ref-ansi-compliance.md
+++ b/docs/sql-ref-ansi-compliance.md
@@ -430,6 +430,7 @@ Below is a list of all the keywords in Spark SQL.
|AS|reserved|non-reserved|reserved|
|ASC|non-reserved|non-reserved|non-reserved|
|ASENSITIVE|non-reserved|non-reserved|non-reserved|
+|ASOF|non-reserved|non-reserved|non-reserved|
|AT|non-reserved|non-reserved|reserved|
|ATOMIC|non-reserved|non-reserved|non-reserved|
|AUTHORIZATION|reserved|non-reserved|reserved|
@@ -641,6 +642,7 @@ Below is a list of all the keywords in Spark SQL.
|MACRO|non-reserved|non-reserved|non-reserved|
|MAP|non-reserved|non-reserved|non-reserved|
|MATCHED|non-reserved|non-reserved|non-reserved|
+|MATCH_CONDITION|non-reserved|non-reserved|non-reserved|
|MATERIALIZED|non-reserved|non-reserved|non-reserved|
|MAX|non-reserved|non-reserved|non-reserved|
|MEASURE|non-reserved|non-reserved|non-reserved|
diff --git
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
index 3e5c79613f19..64a99e82053b 100644
---
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
+++
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
@@ -145,6 +145,7 @@ APPROX: 'APPROX';
ARCHIVE: 'ARCHIVE';
ARRAY: 'ARRAY' {incComplexTypeLevelCounter();};
AS: 'AS';
+ASOF: 'ASOF';
ASC: 'ASC';
ASENSITIVE: 'ASENSITIVE';
AT: 'AT';
@@ -358,6 +359,7 @@ LOOP: 'LOOP';
MACRO: 'MACRO';
MAP: 'MAP' {incComplexTypeLevelCounter();};
MATCHED: 'MATCHED';
+MATCH_CONDITION: 'MATCH_CONDITION';
MATERIALIZED: 'MATERIALIZED';
MAX: 'MAX';
MEASURE: 'MEASURE';
diff --git
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
index 6b2237f15cc4..fd0bba2f9684 100644
---
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
+++
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
@@ -75,7 +75,7 @@ options { tokenVocab = SqlBaseLexer; }
la == AS || la == WHERE || la == PIVOT || la == UNPIVOT ||
la == TABLESAMPLE || la == INNER || la == CROSS || la == LEFT ||
la == RIGHT || la == FULL || la == NATURAL || la == SEMI ||
- la == ANTI || la == JOIN || la == UNION || la == EXCEPT ||
+ la == ANTI || la == ASOF || la == JOIN || la == UNION || la ==
EXCEPT ||
la == SETMINUS || la == INTERSECT || la == ORDER || la == CLUSTER ||
la == DISTRIBUTE || la == SORT || la == LIMIT || la == OFFSET ||
la == AGGREGATE || la == WINDOW || la == LATERAL || la == BIN;
@@ -1104,8 +1104,24 @@ relationExtension
;
joinRelation
- : (joinType) JOIN LATERAL? right=relationPrimary (joinCriteria |
nearestByClause)?
+ : (joinType) JOIN LATERAL? right=relationPrimary joinPostfix?
| NATURAL joinType JOIN LATERAL? right=relationPrimary
+ | asofJoinType ASOF JOIN right=relationPrimary asofJoinCriteria
+ ;
+
+asofJoinType
+ : INNER?
+ | LEFT OUTER?
+ ;
+
+joinPostfix
+ : joinCriteria
+ | nearestByClause
+ ;
+
+asofJoinCriteria
+ : MATCH_CONDITION LEFT_PAREN matchExpr=booleanExpression RIGHT_PAREN
+ ( ON onExpr=booleanExpression | USING identifierList )?
;
joinType
@@ -2013,6 +2029,7 @@ ansiNonReserved
| ARRAY
| ASC
| ASENSITIVE
+ | ASOF
| AT
| ATOMIC
| AUTO
@@ -2180,6 +2197,7 @@ ansiNonReserved
| MACRO
| MAP
| MATCHED
+ | MATCH_CONDITION
| MATERIALIZED
| MAX
| MEASURE
@@ -2405,6 +2423,7 @@ nonReserved
| AS
| ASC
| ASENSITIVE
+ | ASOF
| AT
| ATOMIC
| AUTHORIZATION
@@ -2610,6 +2629,7 @@ nonReserved
| MACRO
| MAP
| MATCHED
+ | MATCH_CONDITION
| MATERIALIZED
| MAX
| MEASURE
diff --git
a/sql/api/src/main/scala/org/apache/spark/sql/errors/QueryParsingErrors.scala
b/sql/api/src/main/scala/org/apache/spark/sql/errors/QueryParsingErrors.scala
index 9c7eaadedd78..d7554838ead1 100644
---
a/sql/api/src/main/scala/org/apache/spark/sql/errors/QueryParsingErrors.scala
+++
b/sql/api/src/main/scala/org/apache/spark/sql/errors/QueryParsingErrors.scala
@@ -237,6 +237,22 @@ private[sql] object QueryParsingErrors extends
DataTypeErrorsBase {
ctx)
}
+ def sqlAsofJoinDisabled(configKey: String, ctx: ParserRuleContext):
Throwable = {
+ new ParseException(
+ errorClass = "UNSUPPORTED_FEATURE.ASOF_JOIN",
+ messageParameters = Map("config" -> toSQLConf(configKey)),
+ ctx)
+ }
+
+ def sqlAsOfJoinMatchConditionInvalidOperator(
+ operator: String,
+ ctx: ParserRuleContext): Throwable = {
+ new ParseException(
+ errorClass = "ASOF_JOIN_MATCH_CONDITION_INVALID_OPERATOR",
+ messageParameters = Map("operator" -> operator),
+ ctx)
+ }
+
def repetitiveWindowDefinitionError(name: String, ctx: WindowClauseContext):
Throwable = {
new ParseException(
errorClass = "INVALID_SQL_SYNTAX.REPETITIVE_WINDOW_DEFINITION",
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckAnalysis.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckAnalysis.scala
index ccc5da5ed8c3..f457ac1ba852 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckAnalysis.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckAnalysis.scala
@@ -676,7 +676,7 @@ trait CheckAnalysis extends LookupCatalog with
QueryErrorsBase with PlanToString
"joinCondition" -> toSQLExpr(condition),
"conditionType" -> toSQLType(condition.dataType)))
- case j @ AsOfJoin(_, _, _, Some(condition), _, _, _)
+ case j @ AsOfJoin(_, _, _, Some(condition), _, _, _, _, _, _, _)
if condition.dataType != BooleanType =>
throw SparkException.internalError(
msg = s"join condition '${toSQLExpr(condition)}' " +
@@ -684,7 +684,7 @@ trait CheckAnalysis extends LookupCatalog with
QueryErrorsBase with PlanToString
context = j.origin.getQueryContext,
summary = j.origin.context.summary)
- case j @ AsOfJoin(_, _, _, _, _, _, Some(toleranceAssertion)) =>
+ case j @ AsOfJoin(_, _, _, _, _, _, Some(toleranceAssertion), _, _,
_, _) =>
if (!toleranceAssertion.foldable) {
j.failAnalysis(
errorClass = "AS_OF_JOIN.TOLERANCE_IS_UNFOLDABLE",
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/DeduplicateRelations.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/DeduplicateRelations.scala
index 1fb703814fb9..18045214e565 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/DeduplicateRelations.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/DeduplicateRelations.scala
@@ -48,7 +48,7 @@ object DeduplicateRelations extends Rule[LogicalPlan] {
if right.resolved && !j.duplicateResolved &&
noMissingInput(right.plan) =>
j.copy(right = right.withNewPlan(dedupRight(left, right.plan)))
// Resolve duplicate output for AsOfJoin.
- case j @ AsOfJoin(left, right, _, _, _, _, _)
+ case j @ AsOfJoin(left, right, _, _, _, _, _, _, _, _, _)
if !j.duplicateResolved && noMissingInput(right) =>
j.copy(right = dedupRight(left, right))
// Resolve duplicate output for NearestByJoin.
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteAsOfJoin.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteAsOfJoin.scala
index 6f10cad108f7..31625b25b378 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteAsOfJoin.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteAsOfJoin.scala
@@ -58,7 +58,8 @@ object RewriteAsOfJoin extends Rule[LogicalPlan] {
if (conf.sortMergeAsOfJoinEnabled) return plan
plan.transformUpWithNewOutput {
- case j @ AsOfJoin(left, right, asOfCondition, condition, joinType,
orderExpression, _) =>
+ case j @ AsOfJoin(
+ left, right, asOfCondition, condition, joinType, orderExpression, _,
_, _, _, _) =>
val conditionWithOuterReference =
condition.map(And(_,
asOfCondition)).getOrElse(asOfCondition).transformUp {
case a: AttributeReference if left.outputSet.contains(a) =>
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
index f5d8e600cb70..e6f4b574b73a 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
@@ -2505,11 +2505,17 @@ class AstBuilder extends DataTypeAstBuilder
}
}
- if (ctx.nearestByClause != null) {
- withNearestByJoin(ctx, base, baseJoinType)
+ val joinPostfix = Option(ctx.joinPostfix)
+ val joinCriteriaCtx = joinPostfix.flatMap(p => Option(p.joinCriteria))
+ val nearestByClauseCtx = joinPostfix.flatMap(p =>
Option(p.nearestByClause))
+
+ if (ctx.asofJoinCriteria != null) {
+ withAsOfJoin(ctx, base, ctx.asofJoinCriteria)
+ } else if (nearestByClauseCtx.isDefined) {
+ withNearestByJoin(ctx, base, baseJoinType, nearestByClauseCtx.get)
} else {
// Resolve the join type and join condition
- val (joinType, condition) = Option(ctx.joinCriteria) match {
+ val (joinType, condition) = joinCriteriaCtx match {
case Some(c) if c.USING != null =>
if (ctx.LATERAL != null) {
throw
QueryParsingErrors.lateralJoinWithUsingJoinUnsupportedError(ctx)
@@ -2546,6 +2552,56 @@ class AstBuilder extends DataTypeAstBuilder
}
}
+ private def asOfMatchConditionFromExpression(
+ expr: Expression,
+ ctx: ParserRuleContext): (Expression, MatchComparisonOperator,
Expression) = {
+ expr match {
+ case GreaterThanOrEqual(left, right) => (left, GreaterThanOrEqualOp,
right)
+ case GreaterThan(left, right) => (left, GreaterThanOp, right)
+ case LessThanOrEqual(left, right) => (left, LessThanOrEqualOp, right)
+ case LessThan(left, right) => (left, LessThanOp, right)
+ case _ =>
+ throw QueryParsingErrors.sqlAsOfJoinMatchConditionInvalidOperator(
+ asOfMatchConditionInvalidOperatorText(expr), ctx)
+ }
+ }
+
+ private def asOfMatchConditionInvalidOperatorText(expr: Expression): String
= expr match {
+ case EqualTo(_, _) => "="
+ case And(_, _) => "AND"
+ case Or(_, _) => "OR"
+ case _ => expr.prettyName
+ }
+
+ /**
+ * Build an [[AsOfJoin]] from the parsed `ASOF JOIN ... MATCH_CONDITION`
clause.
+ */
+ private def withAsOfJoin(
+ ctx: JoinRelationContext,
+ base: LogicalPlan,
+ criteria: AsofJoinCriteriaContext): AsOfJoin = {
+ if (!conf.sqlAsOfJoinEnabled) {
+ throw
QueryParsingErrors.sqlAsofJoinDisabled(SQLConf.SQL_ASOF_JOIN_ENABLED.key, ctx)
+ }
+ val joinType = Option(ctx.asofJoinType) match {
+ case None => Inner
+ case Some(jt) if jt.LEFT != null => LeftOuter
+ case _ => Inner
+ }
+ val (leftExpr, operator, rightExpr) =
+ asOfMatchConditionFromExpression(expression(criteria.matchExpr),
criteria.matchExpr)
+ val (condition, usingColumns) =
+ (Option(criteria.onExpr), Option(criteria.identifierList)) match {
+ case (Some(expr), None) => (Some(expression(expr)), None)
+ case (None, Some(ids)) => (None, Some(visitIdentifierList(ids)))
+ case (None, None) => (None, None)
+ case _ =>
+ throw SparkException.internalError(s"Unimplemented asofJoinCriteria:
$criteria")
+ }
+ AsOfJoin.fromMatchCondition(
+ base, plan(ctx.right), leftExpr, operator, rightExpr, condition,
joinType, usingColumns)
+ }
+
/**
* Build a [[NearestByJoin]] from the parsed `NEAREST BY` clause attached to
a join relation.
* Validates that the clause is not combined with `LATERAL` and that the
base join type is one
@@ -2555,7 +2611,8 @@ class AstBuilder extends DataTypeAstBuilder
private def withNearestByJoin(
ctx: JoinRelationContext,
base: LogicalPlan,
- baseJoinType: JoinType): NearestByJoin = {
+ baseJoinType: JoinType,
+ nearestByClause: NearestByClauseContext): NearestByJoin = {
if (ctx.LATERAL != null) {
throw QueryParsingErrors.nearestByJoinWithLateralUnsupportedError(ctx)
}
@@ -2563,7 +2620,7 @@ class AstBuilder extends DataTypeAstBuilder
throw QueryParsingErrors.unsupportedNearestByJoinTypeError(
ctx, baseJoinType.sql, NearestByJoinType.supportedDisplay)
}
- val clause = ctx.nearestByClause
+ val clause = nearestByClause
val approx = clause.APPROX != null
val numResults = Option(clause.num).map { n =>
// Guard against literals that overflow Long.
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/joinTypes.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/joinTypes.scala
index 790307e44ec9..ae1228c3d7bd 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/joinTypes.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/joinTypes.scala
@@ -160,6 +160,31 @@ case object Forward extends AsOfJoinDirection
case object Backward extends AsOfJoinDirection
case object Nearest extends AsOfJoinDirection
+sealed abstract class MatchComparisonOperator {
+ def sql: String
+ def flip: MatchComparisonOperator
+}
+
+case object GreaterThanOrEqualOp extends MatchComparisonOperator {
+ override def sql: String = ">="
+ override def flip: MatchComparisonOperator = LessThanOrEqualOp
+}
+
+case object GreaterThanOp extends MatchComparisonOperator {
+ override def sql: String = ">"
+ override def flip: MatchComparisonOperator = LessThanOp
+}
+
+case object LessThanOrEqualOp extends MatchComparisonOperator {
+ override def sql: String = "<="
+ override def flip: MatchComparisonOperator = GreaterThanOrEqualOp
+}
+
+case object LessThanOp extends MatchComparisonOperator {
+ override def sql: String = "<"
+ override def flip: MatchComparisonOperator = GreaterThanOp
+}
+
object LateralJoinType {
val supported = Seq(
@@ -226,6 +251,11 @@ object NearestByJoinType {
}
}
+object AsOfJoinType {
+
+ val supportedDisplay: String = "'INNER', 'LEFT OUTER'"
+}
+
object NearestByJoinMode {
/** @see [[NearestByJoinValidation.SupportedModes]] */
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
index b7c441063874..244614a2c962 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala
@@ -2533,7 +2533,12 @@ case class AsOfJoin(
condition: Option[Expression],
joinType: JoinType,
orderExpression: Expression,
- toleranceAssertion: Option[Expression]) extends BinaryNode {
+ toleranceAssertion: Option[Expression],
+ usingColumns: Option[Seq[String]] = None,
+ matchLeftOperand: Option[Expression] = None,
+ matchOperator: Option[MatchComparisonOperator] = None,
+ matchRightOperand: Option[Expression] = None)
+ extends BinaryNode {
require(Seq(Inner, LeftOuter).contains(joinType),
s"Unsupported as-of join type $joinType")
@@ -2553,6 +2558,10 @@ case class AsOfJoin(
override lazy val resolved: Boolean = {
childrenResolved &&
+ usingColumns.isEmpty &&
+ matchLeftOperand.isEmpty &&
+ matchOperator.isEmpty &&
+ matchRightOperand.isEmpty &&
expressions.forall(_.resolved) &&
duplicateResolved &&
asOfCondition.dataType == BooleanType &&
@@ -2588,6 +2597,33 @@ object AsOfJoin {
orderingExpr, tolerance.map(t => GreaterThanOrEqual(t,
Literal.default(t.dataType))))
}
+ /**
+ * Build an [[AsOfJoin]] from a SQL `MATCH_CONDITION (left_expr op
right_expr)` clause.
+ * Operand normalization is deferred until analysis when join inputs are
resolved.
+ */
+ def fromMatchCondition(
+ left: LogicalPlan,
+ right: LogicalPlan,
+ leftExpr: Expression,
+ operator: MatchComparisonOperator,
+ rightExpr: Expression,
+ condition: Option[Expression],
+ joinType: JoinType,
+ usingColumns: Option[Seq[String]] = None): AsOfJoin = {
+ AsOfJoin(
+ left,
+ right,
+ asOfCondition = Literal.TrueLiteral,
+ condition = condition,
+ joinType = joinType,
+ orderExpression = Literal(0),
+ toleranceAssertion = None,
+ usingColumns = usingColumns,
+ matchLeftOperand = Some(leftExpr),
+ matchOperator = Some(operator),
+ matchRightOperand = Some(rightExpr))
+ }
+
private def makeAsOfCond(
leftAsOf: Expression,
rightAsOf: Expression,
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
index efa5b0352113..8b52f308403c 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
@@ -945,6 +945,15 @@ object SQLConf {
.booleanConf
.createWithDefault(false)
+ val SQL_ASOF_JOIN_ENABLED =
+ buildConf("spark.sql.join.asofJoin.enabled")
+ .doc("When true, enable SQL ASOF JOIN syntax with MATCH_CONDITION. When
false, " +
+ "ASOF JOIN fails at parse time.")
+ .version("4.3.0")
+ .withBindingPolicy(ConfigBindingPolicy.SESSION)
+ .booleanConf
+ .createWithDefault(false)
+
val REQUIRE_ALL_CLUSTER_KEYS_FOR_CO_PARTITION =
buildConf("spark.sql.requireAllClusterKeysForCoPartition")
.internal()
@@ -8562,6 +8571,8 @@ class SQLConf extends Serializable with Logging with
SqlApiConf {
def sortMergeAsOfJoinEnabled: Boolean =
getConf(SORT_MERGE_AS_OF_JOIN_ENABLED)
+ def sqlAsOfJoinEnabled: Boolean = getConf(SQL_ASOF_JOIN_ENABLED)
+
def enableRadixSort: Boolean = getConf(RADIX_SORT_ENABLED)
def isParquetSchemaMergingEnabled: Boolean =
getConf(PARQUET_SCHEMA_MERGING_ENABLED)
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/PlanParserSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/PlanParserSuite.scala
index 6e5239b3d206..ae931604f4ad 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/PlanParserSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/PlanParserSuite.scala
@@ -965,17 +965,112 @@ class PlanParserSuite extends AnalysisTest {
stop = 73))
}
+ test("asof join") {
+ withSQLConf(SQLConf.SQL_ASOF_JOIN_ENABLED.key -> "true") {
+ assertEqual(
+ "select * from t asof join u match_condition (t.a >= u.a)",
+ AsOfJoin.fromMatchCondition(
+ table("t"),
+ table("u"),
+ $"t.a",
+ GreaterThanOrEqualOp,
+ $"u.a",
+ None,
+ Inner).select(star()))
+
+ assertEqual(
+ "select * from t left asof join u match_condition (t.a >= u.a) on t.b
= u.b",
+ AsOfJoin.fromMatchCondition(
+ table("t"),
+ table("u"),
+ $"t.a",
+ GreaterThanOrEqualOp,
+ $"u.a",
+ Some($"t.b" === $"u.b"),
+ LeftOuter).select(star()))
+
+ assertEqual(
+ "select * from t asof join u match_condition (t.a >= u.a) using (b)",
+ AsOfJoin.fromMatchCondition(
+ table("t"),
+ table("u"),
+ $"t.a",
+ GreaterThanOrEqualOp,
+ $"u.a",
+ None,
+ Inner,
+ usingColumns = Some(Seq("b"))).select(star()))
+
+ assertEqual(
+ "select * from t asof join u match_condition (u.a <= t.a)",
+ AsOfJoin.fromMatchCondition(
+ table("t"),
+ table("u"),
+ $"u.a",
+ LessThanOrEqualOp,
+ $"t.a",
+ None,
+ Inner).select(star()))
+ }
+ }
+
+ test("asof join - invalid match operator") {
+ withSQLConf(SQLConf.SQL_ASOF_JOIN_ENABLED.key -> "true") {
+ val sql =
+ "select * from t asof join u match_condition (t.a = u.a)"
+ checkError(
+ exception = parseException(sql),
+ condition = "ASOF_JOIN_MATCH_CONDITION_INVALID_OPERATOR",
+ sqlState = Some("42K0E"),
+ parameters = Map("operator" -> "="),
+ queryContext = Array(
+ ExpectedContext(
+ fragment = "asof join u match_condition (t.a = u.a)",
+ start = 16,
+ stop = 54)))
+ }
+ }
+
+ test("asof join - compound match condition rejected") {
+ withSQLConf(SQLConf.SQL_ASOF_JOIN_ENABLED.key -> "true") {
+ checkError(
+ exception = parseException(
+ "select * from t asof join u match_condition (t.a >= u.a and t.b >=
u.b)"),
+ condition = "ASOF_JOIN_MATCH_CONDITION_INVALID_OPERATOR",
+ sqlState = Some("42K0E"),
+ parameters = Map("operator" -> "AND"),
+ queryContext = Array(
+ ExpectedContext(
+ fragment = "asof join u match_condition (t.a >= u.a and t.b >=
u.b)",
+ start = 16,
+ stop = 70)))
+ }
+ }
+
+ test("asof join disabled by default") {
+ withSQLConf(SQLConf.SQL_ASOF_JOIN_ENABLED.key -> "false") {
+ checkError(
+ exception = parseException("select * from t asof join u
match_condition (t.a >= u.a)"),
+ condition = "UNSUPPORTED_FEATURE.ASOF_JOIN",
+ sqlState = "0A000",
+ parameters = Map("config" -> "\"spark.sql.join.asofJoin.enabled\""),
+ context = ExpectedContext(
+ fragment = "asof join u match_condition (t.a >= u.a)",
+ start = 16,
+ stop = 55))
+ }
+ }
+
test("nearest-by keywords are non-reserved (usable as identifiers)") {
- // The five new keywords (APPROX, DISTANCE, EXACT, NEAREST, SIMILARITY)
must remain
- // non-reserved so they can continue to be used as column or table
identifiers.
- Seq("approx", "distance", "exact", "nearest", "similarity").foreach { kw =>
+ // Spark-specific join keywords must remain non-reserved so they can be
used as identifiers.
+ Seq("approx", "asof", "distance", "exact", "nearest",
"similarity").foreach { kw =>
// As a column identifier in the SELECT list.
parsePlan(s"select $kw from t")
// As a table identifier in the FROM clause.
parsePlan(s"select * from $kw")
}
- // All five together in a single SELECT list.
- parsePlan("select approx, distance, exact, nearest, similarity from t")
+ // All six together in a single SELECT list.
+ parsePlan("select approx, asof, distance, exact, nearest, similarity from
t")
}
test("sampled relations") {
diff --git
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
index 6c35f73f5742..babac5dd9486 100644
---
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
+++
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
@@ -210,7 +210,7 @@ class SparkConnectDatabaseMetaDataSuite extends
ConnectFunSuite with RemoteSpark
val metadata = conn.getMetaData
// scalastyle:off line.size.limit
// CURRENT_PATH and SYSTEM are excluded: getSQLKeywords drops SQL:2003
reserved words (see companion).
- assert(metadata.getSQLKeywords ===
"ADD,AFTER,AGGREGATE,ALIGN,ALWAYS,ANALYZE,ANTI,ANY_VALUE,APPLY,APPROX,ARCHIVE,ASC,AUTO,BERNOULLI,BIN,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BUCKET,BUCKETS,BYTE,CACHE,CASCADE,CATALOG,CATALOGS,CDC,CHANGE,CHANGES,CLEAR,CLUSTER,CLUSTERED,CODEGEN,COLLATION,COLLATIONS,COLLECTION,COLUMNS,COMMENT,COMPACT,COMPACTIONS,COMPENSATION,COMPUTE,CONCATENATE,CONTAINS,CONTINUE,COST,CURRENT_DATABASE,CURRENT_SCHEMA,DATA,DATABASE,DATABASES,DATEADD,DATEDIFF,DATE
[...]
+ assert(metadata.getSQLKeywords ===
"ADD,AFTER,AGGREGATE,ALIGN,ALWAYS,ANALYZE,ANTI,ANY_VALUE,APPLY,APPROX,ARCHIVE,ASC,ASOF,AUTO,BERNOULLI,BIN,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BUCKET,BUCKETS,BYTE,CACHE,CASCADE,CATALOG,CATALOGS,CDC,CHANGE,CHANGES,CLEAR,CLUSTER,CLUSTERED,CODEGEN,COLLATION,COLLATIONS,COLLECTION,COLUMNS,COMMENT,COMPACT,COMPACTIONS,COMPENSATION,COMPUTE,CONCATENATE,CONTAINS,CONTINUE,COST,CURRENT_DATABASE,CURRENT_SCHEMA,DATA,DATABASE,DATABASES,DATEADD,DATEDIFF
[...]
// scalastyle:on line.size.limit
}
}
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkStrategies.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkStrategies.scala
index f8f1e9eeeb35..1329e0d5d6b3 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkStrategies.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkStrategies.scala
@@ -431,7 +431,7 @@ abstract class SparkStrategies extends
QueryPlanner[SparkPlan] {
object AsOfJoinSelection extends Strategy with PredicateHelper {
def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match {
case j @ AsOfJoin(left, right, asOfCondition, condition, joinType,
- orderExpression, _) if conf.sortMergeAsOfJoinEnabled =>
+ orderExpression, _, _, _, _, _) if conf.sortMergeAsOfJoinEnabled =>
val (leftKeys, rightKeys, residual) = condition match {
case Some(cond) => extractEquiJoinKeys(cond, left, right)
case None => (Seq.empty[Expression], Seq.empty[Expression], None)
diff --git
a/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
b/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
index 0c57178048eb..989bfb725111 100644
--- a/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
@@ -23,6 +23,7 @@ ARRAY false
AS true
ASC false
ASENSITIVE false
+ASOF false
AT false
ATOMIC false
AUTHORIZATION true
@@ -234,6 +235,7 @@ LOOP false
MACRO false
MAP false
MATCHED false
+MATCH_CONDITION false
MATERIALIZED false
MAX false
MEASURE false
diff --git a/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
b/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
index e0528a75c6ef..ee127e584f17 100644
--- a/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
@@ -23,6 +23,7 @@ ARRAY false
AS false
ASC false
ASENSITIVE false
+ASOF false
AT false
ATOMIC false
AUTHORIZATION false
@@ -234,6 +235,7 @@ LOOP false
MACRO false
MAP false
MATCHED false
+MATCH_CONDITION false
MATERIALIZED false
MAX false
MEASURE false
diff --git
a/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
b/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
index e0528a75c6ef..ee127e584f17 100644
--- a/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
@@ -23,6 +23,7 @@ ARRAY false
AS false
ASC false
ASENSITIVE false
+ASOF false
AT false
ATOMIC false
AUTHORIZATION false
@@ -234,6 +235,7 @@ LOOP false
MACRO false
MAP false
MATCHED false
+MATCH_CONDITION false
MATERIALIZED false
MAX false
MEASURE false
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/AsOfJoinSQLSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/AsOfJoinSQLSuite.scala
new file mode 100644
index 000000000000..d8fead52af94
--- /dev/null
+++ b/sql/core/src/test/scala/org/apache/spark/sql/AsOfJoinSQLSuite.scala
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql
+
+import org.apache.spark.sql.catalyst.parser.ParseException
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.test.SharedSparkSession
+
+/**
+ * SQL ASOF JOIN surface tests (parser and feature gating).
+ * Execution semantics and complex MATCH_CONDITION types are covered by
+ * `AsOfJoinSortMergeSQLSuite`, which requires sort-merge ASOF join.
+ */
+class AsOfJoinSQLSuite extends QueryTest with SharedSparkSession {
+
+ override def beforeAll(): Unit = {
+ super.beforeAll()
+ spark.conf.set(SQLConf.SQL_ASOF_JOIN_ENABLED.key, "true")
+ }
+
+ override def afterAll(): Unit = {
+ spark.conf.unset(SQLConf.SQL_ASOF_JOIN_ENABLED.key)
+ super.afterAll()
+ }
+
+ test("equality operator is rejected in MATCH_CONDITION") {
+ sql(
+ """
+ |CREATE OR REPLACE TEMP VIEW trades(trade_time, symbol) AS
+ |VALUES (TIMESTAMP '2026-06-29 10:00:00', 'AAPL')
+ |""".stripMargin)
+ sql(
+ """
+ |CREATE OR REPLACE TEMP VIEW quotes(quote_time, symbol) AS
+ |VALUES (TIMESTAMP '2026-06-29 09:00:00', 'AAPL')
+ |""".stripMargin)
+ val sqlText =
+ """
+ |SELECT * FROM trades t
+ |ASOF JOIN quotes q
+ | MATCH_CONDITION (t.trade_time = q.quote_time)
+ | ON t.symbol = q.symbol
+ |""".stripMargin
+ checkError(
+ exception = intercept[ParseException](sql(sqlText)),
+ condition = "ASOF_JOIN_MATCH_CONDITION_INVALID_OPERATOR",
+ sqlState = Some("42K0E"),
+ parameters = Map("operator" -> "="),
+ queryContext = Array(
+ ExpectedContext(
+ fragment = """ASOF JOIN quotes q
+ | MATCH_CONDITION (t.trade_time = q.quote_time)
+ | ON t.symbol = q.symbol""".stripMargin,
+ start = 24,
+ stop = 114)))
+ }
+}
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryParsingErrorsSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryParsingErrorsSuite.scala
index f834db203b90..aab49da38ddc 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryParsingErrorsSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryParsingErrorsSuite.scala
@@ -472,7 +472,7 @@ class QueryParsingErrorsSuite extends SharedSparkSession {
exception = parseException("select * from a left join_ b on a.id =
b.id"),
condition = "PARSE_SYNTAX_ERROR",
sqlState = "42601",
- parameters = Map("error" -> "'join_'", "hint" -> ": missing 'JOIN'"))
+ parameters = Map("error" -> "'join_'", "hint" -> ""))
checkError(
exception = parseException("select * from test where test.t is like
'test'"),
diff --git
a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
index 4e8f117dc8a5..ff9e203f5915 100644
---
a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
+++
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
@@ -214,7 +214,7 @@ trait ThriftServerWithSparkContextSuite extends
SharedThriftServer {
val sessionHandle = client.openSession(user, "")
val infoValue = client.getInfo(sessionHandle,
GetInfoType.CLI_ODBC_KEYWORDS)
// scalastyle:off line.size.limit
- assert(infoValue.getStringValue ==
"ADD,AFTER,AGGREGATE,ALIGN,ALL,ALTER,ALWAYS,ANALYZE,AND,ANTI,ANY,ANY_VALUE,APPLY,APPROX,ARCHIVE,ARRAY,AS,ASC,ASENSITIVE,AT,ATOMIC,AUTHORIZATION,AUTO,BEGIN,BERNOULLI,BETWEEN,BIGINT,BIN,BINARY,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BOOLEAN,BOTH,BUCKET,BUCKETS,BY,BYTE,CACHE,CALL,CALLED,CASCADE,CASE,CAST,CATALOG,CATALOGS,CDC,CHANGE,CHANGES,CHAR,CHARACTER,CHECK,CLEAR,CLOSE,CLUSTER,CLUSTERED,CODEGEN,COLLATE,COLLATION,COLLATIONS,COLLECTION,COLUMN
[...]
+ assert(infoValue.getStringValue ==
"ADD,AFTER,AGGREGATE,ALIGN,ALL,ALTER,ALWAYS,ANALYZE,AND,ANTI,ANY,ANY_VALUE,APPLY,APPROX,ARCHIVE,ARRAY,AS,ASC,ASENSITIVE,ASOF,AT,ATOMIC,AUTHORIZATION,AUTO,BEGIN,BERNOULLI,BETWEEN,BIGINT,BIN,BINARY,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BOOLEAN,BOTH,BUCKET,BUCKETS,BY,BYTE,CACHE,CALL,CALLED,CASCADE,CASE,CAST,CATALOG,CATALOGS,CDC,CHANGE,CHANGES,CHAR,CHARACTER,CHECK,CLEAR,CLOSE,CLUSTER,CLUSTERED,CODEGEN,COLLATE,COLLATION,COLLATIONS,COLLECTION,C
[...]
// scalastyle:on line.size.limit
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]