Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a756239
[SPARK-58093][SQL] Add ASOF JOIN SQL syntax with MATCH_CONDITION
srielau Jul 11, 2026
1154ac5
[SPARK-58093][SQL] Fix AsOfJoin pattern arity and ResolveAsOfJoin com…
srielau Jul 11, 2026
e615b3b
[SPARK-58093][SQL] Fix asOfJoinMatchConditionTableReferenceError call…
srielau Jul 11, 2026
0cb6577
[SPARK-58093][SQL] Remove redundant ResolveAsOfJoin ruleId override
srielau Jul 11, 2026
9bfb7a6
[SPARK-58093][SQL] Fix Scalastyle issues in ASOF join analysis rules
srielau Jul 11, 2026
98195fa
[SPARK-58093][SQL] Fix Scalastyle line length in AsOfJoinSQLSuite
srielau Jul 11, 2026
708e5ad
[SPARK-58093][SQL] Fix MATCH_CONDITION resolution for SQL ASOF JOIN
srielau Jul 11, 2026
da8f8ac
[SPARK-58093][SQL] Gate SQL ASOF JOIN behind spark.sql.join.asofJoin.…
srielau Jul 11, 2026
a86d123
[SPARK-58093][SQL] Document ASOF and MATCH_CONDITION in keyword compl…
srielau Jul 11, 2026
77e0a0e
[SPARK-58093][SQL] List ASOF as ANSI non-reserved keyword
srielau Jul 11, 2026
2e23443
[SPARK-58093][SQL] Harden SQL ASOF MATCH_CONDITION validation and par…
srielau Jul 11, 2026
b0cfefd
[SPARK-58093][SQL] Resolve MATCH_CONDITION operands with non-trivial …
srielau Jul 11, 2026
3fa6298
[SPARK-58093][SQL] Support nested STRUCT and ARRAY MATCH_CONDITION op…
srielau Jul 12, 2026
b8e5ef6
[SPARK-58093][SQL] Fix Scalastyle import order in basicLogicalOperators
srielau Jul 12, 2026
876b95d
[SPARK-58093][SQL] Update SQL golden files for ASOF keywords and bitwise
srielau Jul 12, 2026
dc383a9
[SPARK-58093][SQL] Update JDBC ODBC keywords test for ASOF tokens
srielau Jul 12, 2026
aeaec17
[SPARK-58093][SQL] Sort ASOF error conditions alphabetically in JSON
srielau Jul 12, 2026
0c154ba
[SPARK-58093][SQL] Update Spark Connect JDBC SQL keywords test for ASOF
srielau Jul 12, 2026
f000f4c
[SPARK-58093][SQL] Fix parser regressions from ASOF JOIN grammar
srielau Jul 12, 2026
2b4aad1
[SPARK-58093][SQL] Restore bitwise.sql golden for > >> parse error
srielau Jul 12, 2026
a7524c5
[SPARK-58093][SQL] Require sort-merge for SQL ASOF JOIN execution
srielau Jul 13, 2026
2994aa0
[SPARK-58093][SQL] Address ASOF JOIN self-review feedback
srielau Jul 13, 2026
d426ab1
[SPARK-58093][SQL] Report TABLE_REFERENCE for cross-side MATCH_CONDITION
srielau Jul 13, 2026
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
34 changes: 34 additions & 0 deletions common/utils/src/main/resources/error/error-conditions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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>."
Expand All @@ -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."
Expand Down Expand Up @@ -8231,6 +8260,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."
Expand Down
2 changes: 2 additions & 0 deletions docs/sql-ref-ansi-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down Expand Up @@ -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|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ APPROX: 'APPROX';
ARCHIVE: 'ARCHIVE';
ARRAY: 'ARRAY' {incComplexTypeLevelCounter();};
AS: 'AS';
ASOF: 'ASOF';
ASC: 'ASC';
ASENSITIVE: 'ASENSITIVE';
AT: 'AT';
Expand Down Expand Up @@ -358,6 +359,7 @@ LOOP: 'LOOP';
MACRO: 'MACRO';
MAP: 'MAP' {incComplexTypeLevelCounter();};
MATCHED: 'MATCHED';
MATCH_CONDITION: 'MATCH_CONDITION';
MATERIALIZED: 'MATERIALIZED';
MAX: 'MAX';
MEASURE: 'MEASURE';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1104,8 +1104,24 @@ relationExtension
;

joinRelation
: (joinType) JOIN LATERAL? right=relationPrimary (joinCriteria | nearestByClause)?
: (joinType) JOIN LATERAL? right=relationPrimary asofJoinPostfix?
| NATURAL joinType JOIN LATERAL? right=relationPrimary
| asofJoinType ASOF JOIN right=relationPrimary asofJoinCriteria
;

asofJoinType
: INNER?
| LEFT OUTER?
;

asofJoinPostfix
: joinCriteria
| nearestByClause
;

asofJoinCriteria
: MATCH_CONDITION LEFT_PAREN matchExpr=booleanExpression RIGHT_PAREN
( ON onExpr=booleanExpression | USING identifierList )?
;

joinType
Expand Down Expand Up @@ -2013,6 +2029,7 @@ ansiNonReserved
| ARRAY
| ASC
| ASENSITIVE
| ASOF
| AT
| ATOMIC
| AUTO
Expand Down Expand Up @@ -2180,6 +2197,7 @@ ansiNonReserved
| MACRO
| MAP
| MATCHED
| MATCH_CONDITION
| MATERIALIZED
| MAX
| MEASURE
Expand Down Expand Up @@ -2404,6 +2422,7 @@ nonReserved
| ARRAY
| AS
| ASC
| ASOF
| ASENSITIVE
| AT
| ATOMIC
Expand Down Expand Up @@ -2610,6 +2629,7 @@ nonReserved
| MACRO
| MAP
| MATCHED
| MATCH_CONDITION
| MATERIALIZED
| MAX
| MEASURE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,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 invalidNameForDropTempFunc(name: Seq[String], ctx: ParserRuleContext): Throwable = {
new ParseException(
errorClass = "INVALID_SQL_SYNTAX.MULTI_PART_NAME",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,8 @@ class Analyzer(
typeCoercionRules() ++
Seq(
ResolveWithCTE,
ExtractDistributedSequenceID) ++
ExtractDistributedSequenceID,
ResolveAsOfJoin) ++
Seq(ResolveUpdateEventTimeWatermarkColumn) ++
extendedResolutionRules ++
Seq(NameStreamingSources) : _*),
Expand Down Expand Up @@ -1931,6 +1932,29 @@ class Analyzer(
case d: DataFrameDropColumns if !d.resolved =>
resolveDataFrameDropColumns(d)

case j @ AsOfJoin(left, right, _, condition, _, _, _, _, matchCmp, _, _, _)
if left.resolved && right.resolved &&
(matchCmp.exists(mc => !mc.left.resolved || !mc.right.resolved) ||
condition.exists(!_.resolved)) =>
var updated = j
matchCmp.foreach { mc =>
val resolvedLeft = resolveExpressionByPlanChildren(mc.left, j, includeLastResort = true)
val resolvedRight = resolveExpressionByPlanChildren(mc.right, j, includeLastResort = true)
if (!resolvedLeft.fastEquals(mc.left) || !resolvedRight.fastEquals(mc.right)) {
updated = updated.copy(
matchComparison = Some(AsOfMatchCondition(resolvedLeft, mc.operator, resolvedRight)))
}
}
condition.foreach { cond =>
if (!cond.resolved) {
val resolvedCond = resolveExpressionByPlanChildren(cond, j, includeLastResort = true)
if (!resolvedCond.fastEquals(cond)) {
updated = updated.copy(condition = Some(resolvedCond))
}
}
}
if (updated.fastEquals(j)) j else updated

case q: LogicalPlan =>
logTrace(s"Attempting to resolve ${q.simpleString(conf.maxToStringFields)}")
q.mapExpressions(resolveExpressionByPlanChildren(_, q, includeLastResort = true))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,15 +676,22 @@ trait CheckAnalysis extends LookupCatalog with QueryErrorsBase with PlanToString
"joinCondition" -> toSQLExpr(condition),
"conditionType" -> toSQLType(condition.dataType)))

case j @ AsOfJoin(_, _, _, Some(condition), _, _, _)
case j @ AsOfJoin(_, _, _, _, _, _, _, _, _, _, _, true)
if !SQLConf.get.sortMergeAsOfJoinEnabled =>
j.failAnalysis(
errorClass = "AS_OF_JOIN.SORT_MERGE_REQUIRED",
messageParameters = Map(
"config" -> SQLConf.SORT_MERGE_AS_OF_JOIN_ENABLED.key))

case j @ AsOfJoin(_, _, _, Some(condition), _, _, _, _, _, _, _, _)
if condition.dataType != BooleanType =>
throw SparkException.internalError(
msg = s"join condition '${toSQLExpr(condition)}' " +
s"of type ${toSQLType(condition.dataType)} is not a boolean.",
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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading