Skip to content

[SPARK-58093][SQL] Add ASOF JOIN SQL syntax with MATCH_CONDITION#57194

Open
srielau wants to merge 19 commits into
apache:masterfrom
srielau:SPARK-58093-ASOF-JOIN
Open

[SPARK-58093][SQL] Add ASOF JOIN SQL syntax with MATCH_CONDITION#57194
srielau wants to merge 19 commits into
apache:masterfrom
srielau:SPARK-58093-ASOF-JOIN

Conversation

@srielau

@srielau srielau commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Wire the SQL Reference Spec ASOF JOIN surface into the existing AsOfJoin logical operator, reusing the DataFrame execution path (RewriteAsOfJoin / SortMergeAsOfJoinExec). Adds parser grammar, analysis resolution for USING and MATCH_CONDITION operands, spec error conditions, and parser/SQL integration tests.

What changes were proposed in this pull request?

This PR adds SQL syntax for ASOF JOIN with a required MATCH_CONDITION clause and optional ON / USING, per the SQL Language Reference spec (Snowflake / Calcite MATCH_CONDITION family).

Syntax supported (v1):

[ INNER | LEFT [ OUTER ] ] ASOF JOIN right_table
  MATCH_CONDITION ( left_expr comparison_operator right_expr )
  [ ON boolean_expression | USING ( column_name [, ...] ) ]

where comparison_operator is one of >=, >, <=, <.

Example:

SELECT t.trade_time, t.symbol, q.bid_price
FROM trades t
ASOF JOIN quotes q
  MATCH_CONDITION (t.trade_time >= q.quote_time)
  ON t.symbol = q.symbol;

Implementation layers (new vs reused):

Layer Change
Parser ASOF, MATCH_CONDITION keywords; asofJoinType, asofJoinCriteria, matchComparison grammar rules
AstBuilder withAsOfJoin() builds AsOfJoin.fromMatchCondition(...)
Analysis New ResolveAsOfJoin rule: expands USING via NaturalAndUsingJoinResolution, normalizes MATCH_CONDITION operands into asOfCondition + orderExpression, validates operand expressions/types
Logical plan Extends AsOfJoin with deferred matchComparison / usingColumns fields; adds MatchComparisonOperator and AsOfJoin.fromMatchCondition
Execution No changes — reuses existing RewriteAsOfJoin optimizer rule and SortMergeAsOfJoinExec physical operator already used by DataFrame.joinAsOf
Errors Four new error classes: ASOF_JOIN_MATCH_CONDITION_INVALID_EXPRESSION, ..._INVALID_OPERATOR, ..._INVALID_TYPE, ..._TABLE_REFERENCE

Explicitly out of scope (deferred per spec): TOLERANCE, NEAREST direction, RIGHT / FULL / CROSS / SEMI / ANTI ASOF forms.

Related prior art in Spark: DataFrame.joinAsOf (PySpark/Scala), AsOfJoin logical node, SortMergeAsOfJoinSuite, DataFrameAsOfJoinSuite.

Why are the changes needed?

ASOF (as-of / closest-match) join is a standard temporal/analytics pattern — attaching the nearest preceding quote to each trade, finding the next maintenance window for an alert, etc. Spark already implements the semantics via the DataFrame API, but SQL users must hand-write LEFT JOIN + ROW_NUMBER + QUALIFY rewrites. Native SQL syntax lowers the barrier and aligns with industry dialects (Snowflake, Calcite/Pinot/Doris MATCH_CONDITION family).

Does this PR introduce any user-facing change?

Yes.

Before: ASOF JOIN was not valid SQL; closest-match joins were only available through DataFrame.joinAsOf.

After: SQL queries may use ASOF JOIN / LEFT ASOF JOIN with MATCH_CONDITION. Behavior matches the spec: asymmetric left-driven lookup, at most one right row per left row, INNER drops unmatched left rows, LEFT preserves them with NULL right-side columns.

Operand order inside MATCH_CONDITION is immaterial (t.ts >= q.tsq.ts <= t.ts); the analyzer normalizes to a canonical left-table-on-left form before planning.

How was this patch tested?

Added unit and integration tests; full compile/test suite to run on CI (local build blocked by missing netty 4.2.16 artifacts on the dev Maven proxy after rebasing onto latest master).

Parser tests (PlanParserSuite):

  • INNER / LEFT ASOF JOIN with MATCH_CONDITION and ON
  • USING clause
  • Swapped operand order (u.a <= t.a)
  • Rejection of = in MATCH_CONDITION (parse error)

SQL integration tests (AsOfJoinSQLSuite):

  • Spec examples: trades/quotes with ON, LEFT ASOF preserving unmatched rows, USING equivalence, first-following match with <=

Existing coverage reused (unchanged): DataFrameAsOfJoinSuite, SortMergeAsOfJoinSuite, RewriteAsOfJoinSuite exercise the shared execution path.

Suggested CI commands for reviewers:

build/sbt "sql/catalyst/testOnly org.apache.spark.sql.catalyst.parser.PlanParserSuite -- -z \"asof join\""
build/sbt "sql/testOnly org.apache.spark.sql.AsOfJoinSQLSuite"
build/sbt "sql/testOnly org.apache.spark.sql.DataFrameAsOfJoinSuite"

Was this patch authored or co-authored using generative AI tooling?

Yes. Generated-by: Cursor (Claude agent), with human review and editing by the PR author.

Wire the SQL Reference Spec ASOF JOIN surface into the existing AsOfJoin
logical operator, reusing the DataFrame execution path (RewriteAsOfJoin /
SortMergeAsOfJoinExec). Adds parser grammar, analysis resolution for USING
and match operands, spec error conditions, and parser/SQL integration tests.
@srielau srielau marked this pull request as draft July 11, 2026 05:05
srielau added 12 commits July 11, 2026 05:47
…pile errors

Update case-class pattern matches for the two new AsOfJoin fields, fix
conf.resolver usage, replace expr.preOrder with expr.collect, and drop a
duplicate RowOrdering import.
… arity

The error helper only accepts the two MATCH_CONDITION operands; drop the
unused left/right AttributeSet arguments from the throw site.
Rule.ruleId is a lazy val; overriding it with a non-lazy val fails
compilation. The base Rule implementation already derives the same id
from the class name, so drop the override entirely.
Wrap long lines and reorder expression imports so the braced import
precedes expressions.aggregate.
Store parsed MATCH_CONDITION operands in AsOfMatchCondition so mapExpressions
does not corrupt the tuple shape. Resolve operands in ResolveReferences and
materialize asOfCondition in ResolveAsOfJoin, including USING column projection.
…enabled

Default the new SQL syntax off while under development. AstBuilder rejects
ASOF JOIN at parse time when disabled; tests opt in via SQLConf.
…iance doc

SQLKeywordSuite requires new lexer keywords to be listed in
sql-ref-ansi-compliance.md with correct reserved/non-reserved status.
ASOF is a Spark extension, not SQL standard, so it belongs in
ansiNonReserved rather than being reserved under ANSI mode.
…sing

Restrict matchComparison to valueExpression so boolean AND is rejected at
parse time. Tighten type validation for incompatible operand pairs and
prepare struct/tuple ordering for field-wise distance expressions.
…expressions

Expose parsed MATCH_CONDITION operand trees to analysis pruning and
expression rewriting so functions, interval arithmetic, and ON predicates
all resolve before ResolveAsOfJoin materializes the join condition.
…erands

Recursively build min_by order expressions for nested structs (leaf
flattening) and arrays (ZipWith over element-wise diffs). Use signed
comparison distance for orderable non-numeric types like STRING. Add
AsOfJoinSQLSuite coverage for nested struct, ARRAY<INT>, and struct tuple
operands on the min_by rewrite path.
@srielau srielau marked this pull request as ready for review July 12, 2026 04:04
srielau added 6 commits July 12, 2026 04:08
Place TreePattern before TreePattern._ and keep DataTypeUtils imports
adjacent per scalastyle rules.
Regenerate keywords golden files to include ASOF and MATCH_CONDITION
from the lexer vocabulary. Update bitwise parse-error goldens after the
ASOF JOIN grammar change shifts the reported token for `> >>` syntax.
Add ASOF and MATCH_CONDITION to the SPARK-43119 hardcoded keyword list
returned by GetInfo(CLI_ODBC_KEYWORDS).
Reorder ASOF_JOIN_MATCH_CONDITION_* entries before ASSIGNMENT_ARITY_MISMATCH
and AS_OF_JOIN in error-conditions.json to satisfy SparkThrowableSuite
key ordering (ORDER_MAP_ENTRIES_BY_KEYS).
Add ASOF and MATCH_CONDITION to the hardcoded getSQLKeywords expectation
after the new ASOF JOIN grammar keywords were registered.
Parse MATCH_CONDITION comparisons as booleanExpression instead of a
dedicated matchComparison rule, which disturbed unrelated expression
error reporting (e.g. CHECK constraints). Move ASOF JOIN to a trailing
grammar alternative and validate allowed comparison operators in
AstBuilder. Update the join_ parse-error golden after the new ASOF
alternative changes ANTLR error recovery.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant