Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/sql-ref-ansi-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,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|
|ASOF|reserved|strict-non-reserved|non-reserved|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ANSI mode should probably match SQL-2016, like in ANTI case

@LukaZdravic LukaZdravic Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ASOF cannot be non-reserved, because it can then be read as a table alias, same as the AS we talked about above. However ANTI that you mention has the same problem SELECT * FROM t ANTI JOIN u USING (k) this return the inner join rows not the anti join rows. Definitely worth a follow up, what do you think? Also probably an investigation as to what else can be written normally and have different behaviours

|AT|non-reserved|non-reserved|reserved|
|ATOMIC|non-reserved|non-reserved|non-reserved|
|AUTHORIZATION|reserved|non-reserved|reserved|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2156,7 +2156,6 @@ ansiNonReserved
| ARRAY
| ASC
| ASENSITIVE
| ASOF
| AT
| ATOMIC
| AUTO
Expand Down Expand Up @@ -2533,6 +2532,7 @@ ansiNonReserved
// These 2 together contain all the keywords.
strictNonReserved
: ANTI
| ASOF
| CROSS
| EXCEPT
| FULL
Expand Down Expand Up @@ -2571,7 +2571,6 @@ nonReserved
| AS
| ASC
| ASENSITIVE
| ASOF
| AT
| ATOMIC
| AUTHORIZATION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1376,16 +1376,53 @@ class PlanParserSuite extends AnalysisTest {
}
}

test("asof join - missing MATCH_CONDITION is rejected, not parsed as a plain join") {
// A missing MATCH_CONDITION must fail to parse. If `asof` were read as t's alias, the first
// query would parse as a plain INNER join instead. Check both keyword modes.
Seq("false", "true").foreach { enforceReservedKeywords =>
withSQLConf(
SQLConf.SQL_ASOF_JOIN_ENABLED.key -> "true",
SQLConf.ANSI_ENABLED.key -> "true",
SQLConf.ENFORCE_RESERVED_KEYWORDS.key -> enforceReservedKeywords) {
Seq(
"select * from t asof join u on t.a = u.a",
"select * from t x asof join u on t.a = u.a").foreach { query =>
checkError(
exception = parseException(query),
condition = "PARSE_SYNTAX_ERROR",
parameters = Map("error" -> "'on'", "hint" -> ""))
}
// A back-quoted `asof` is still a valid alias.
assertEqual(
"select * from t `asof` join u on t.a = u.a",
table("t").as("asof").join(table("u"), Inner, Option($"t.a" === $"u.a"))
.select(star()))
}
}
}

test("asof is strict-non-reserved in the default keyword mode") {
// Usable as a column and a table name, but not as an unquoted table alias.
assertEqual("select asof from t", table("t").select($"asof"))
assertEqual("select * from asof", table("asof").select(star()))
Seq("select * from t asof", "select * from t as asof").foreach { query =>
checkError(
exception = parseException(query),
condition = "PARSE_SYNTAX_ERROR",
parameters = Map("error" -> "end of input", "hint" -> ""))
}
}

test("nearest-by keywords are non-reserved (usable as identifiers)") {
// Spark-specific join keywords must remain non-reserved so they can be used as identifiers.
Seq("approx", "asof", "distance", "exact", "nearest", "similarity").foreach { kw =>
Seq("approx", "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 six together in a single SELECT list.
parsePlan("select approx, asof, distance, exact, nearest, similarity from t")
// All five together in a single SELECT list.
parsePlan("select approx, distance, exact, nearest, similarity from t")
}

test("sampled relations") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,20 @@ org.apache.spark.sql.catalyst.parser.ParseException
}


-- !query
SELECT * FROM trades ASOF JOIN quotes USING (symbol)
-- !query analysis
org.apache.spark.sql.catalyst.parser.ParseException
{
"errorClass" : "PARSE_SYNTAX_ERROR",
"sqlState" : "42601",
"messageParameters" : {
"error" : "'USING'",
"hint" : ""
}
}


-- !query
SELECT * FROM trades t ASOF JOIN quotes q
MATCH_CONDITION (t.trade_time >= q.quote_time AND t.symbol = q.symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ SELECT * FROM trades t LEFT SEMI ASOF JOIN quotes q
-- FVT-ASOF-1-017: missing MATCH_CONDITION
SELECT * FROM trades t ASOF JOIN quotes q ON t.symbol = q.symbol;

-- FVT-ASOF-1-020: missing MATCH_CONDITION with no left alias is rejected, not a plain join
SELECT * FROM trades ASOF JOIN quotes USING (symbol);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. what about AS ASOF ?
  2. what about AS ASOF with MATCH_CONDITION?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When ANSI is off, AS gets treated as an alias to the table because it's a non-reserved word, and that means
FROM trades AS ASOF JOIN quotes USING (symbol) this fails because it's an asof join

FROM trades AS ASOF JOIN quotes MATCH_CONDITION (trades.trade_time >= quotes.quote_time) USING(symbol) this fails because it expects the AS to be an alias, so no trades.trade_time allowed

When ANSI is on, AS is a reserved word so every attempt at writing AS ASOF is failing.

I should add goldens so we solidify this behaviour for someone else who may come in contact with this, what do you think?


-- FVT-ASOF-1-018: multiple comparisons in MATCH_CONDITION
SELECT * FROM trades t ASOF JOIN quotes q
MATCH_CONDITION (t.trade_time >= q.quote_time AND t.symbol = q.symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,22 @@ org.apache.spark.sql.catalyst.parser.ParseException
}


-- !query
SELECT * FROM trades ASOF JOIN quotes USING (symbol)
-- !query schema
struct<>
-- !query output
org.apache.spark.sql.catalyst.parser.ParseException
{
"errorClass" : "PARSE_SYNTAX_ERROR",
"sqlState" : "42601",
"messageParameters" : {
"error" : "'USING'",
"hint" : ""
}
}


-- !query
SELECT * FROM trades t ASOF JOIN quotes q
MATCH_CONDITION (t.trade_time >= q.quote_time AND t.symbol = q.symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ARRAY false
AS true
ASC false
ASENSITIVE false
ASOF false
ASOF true
AT false
ATOMIC false
AUTHORIZATION true
Expand Down Expand Up @@ -473,6 +473,7 @@ ALL
AND
ANY
AS
ASOF
AUTHORIZATION
BOTH
CALL
Expand Down