2010YOUY01 commented on code in PR #23830:
URL: https://github.com/apache/datafusion/pull/23830#discussion_r3942757156


##########
datafusion/core/tests/sql/joins.rs:
##########
@@ -297,3 +299,412 @@ async fn unparse_cross_join() -> Result<()> {
 
     Ok(())
 }
+
+fn register_asof_test_tables(ctx: &SessionContext) -> Result<()> {

Review Comment:
   Can we unify those tests into sqllogictests, this way I think it's easier to 
maintain. Unless there are some setups that is not possible to do in `slt`, we 
might want extra coverage here.
   
   In order to move those tests checking partition properties, I think we can 
assert the end behavior (final plan shape), rather than internal properties.



##########
datafusion/expr/src/logical_plan/builder.rs:
##########
@@ -1007,8 +1009,12 @@ impl LogicalPlanBuilder {
         )
     }
 
-    /// Apply a left-preserving ASOF join using equality expressions and one
-    /// ordered match condition.
+    /// Apply a left-preserving ASOF join using pre-separated equality
+    /// expressions and a structured ordered match condition.
+    ///
+    /// This is a low-level API. Most callers should use
+    /// [`asof_join_on`](Self::asof_join_on), which validates and separates
+    /// ordinary predicate expressions.
     pub fn asof_join(

Review Comment:
   It seems we can remove this function, since it directly calls a helper.



##########
datafusion/sql/src/unparser/plan.rs:
##########
@@ -1917,6 +1917,126 @@ impl Unparser<'_> {
         }
     }
 
+    // Keep ASOF-specific locals out of the recursive plan unparser's stack 
frame.
+    #[inline(never)]
+    fn asof_join_to_sql(

Review Comment:
   I don't fully understand the unparser now, but my AI tool suggest it is good 
to go, with some cleanup advices:
   
   ### AI review
   > The unparser is heavy relative to the join it mirrors. About 120 lines of 
branching on "already projected" versus not, and "needs a derived subquery" 
versus not, for each side. The regular join unparser has the same structural 
problem, so this is inherited rather than introduced. A cleaner alternative 
would be a small shared helper that takes a join input and returns both the 
relation and its projection items, used by both join kinds. That refactor is 
bigger than this PR should carry, but it would be a natural follow-up given the 
branch already touched the shared nesting helper.
   



##########
datafusion/sql/src/relation/join.rs:
##########
@@ -98,10 +98,77 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
             JoinOperator::CrossJoin(JoinConstraint::None) => {
                 self.parse_cross_join(left, right)
             }
+            JoinOperator::AsOf {
+                match_condition,
+                constraint,
+            } => self.parse_asof_join(
+                left,
+                right,
+                match_condition,
+                constraint,
+                planner_context,
+            ),
             other => not_impl_err!("Unsupported JOIN operator {other:?}"),
         }
     }
 
+    fn parse_asof_join(
+        &self,
+        left: LogicalPlan,
+        right: LogicalPlan,
+        sql_match_condition: sqlparser::ast::Expr,
+        constraint: JoinConstraint,
+        planner_context: &mut PlannerContext,
+    ) -> Result<LogicalPlan> {
+        let join_schema = left.schema().join(right.schema())?;
+        let match_condition =
+            self.sql_to_expr(sql_match_condition, &join_schema, 
planner_context)?;
+
+        match constraint {
+            JoinConstraint::On(sql_on) => {
+                let on = self.sql_to_expr(sql_on, &join_schema, 
planner_context)?;
+                LogicalPlanBuilder::from(left)
+                    .asof_join_on(right, [on], match_condition)?
+                    .build()
+            }
+            JoinConstraint::Using(object_names) => {
+                let keys = object_names
+                    .into_iter()
+                    .map(|object_name| {
+                        let ObjectName(mut object_names) = object_name;
+                        if object_names.len() != 1 {
+                            return not_impl_err!(
+                                "Invalid identifier in ASOF USING clause. 
Expected single identifier, got {}",
+                                ObjectName(object_names)
+                            );
+                        }
+                        let id = object_names.swap_remove(0);
+                        id.as_ident()
+                            .ok_or_else(|| {
+                                plan_datafusion_err!(
+                                    "Expected identifier in ASOF USING clause"
+                                )
+                            })
+                            .map(|ident| {
+                                Column::from_name(
+                                    
self.ident_normalizer.normalize(ident.clone()),
+                                )
+                            })
+                    })
+                    .collect::<Result<Vec<_>>>()?;
+                LogicalPlanBuilder::from(left)
+                    .asof_join_using(right, keys, 
AsOfMatch::try_from(match_condition)?)?
+                    .build()
+            }
+            JoinConstraint::None => LogicalPlanBuilder::from(left)
+                .asof_join_on(right, [], match_condition)?
+                .build(),
+            JoinConstraint::Natural => {
+                not_impl_err!("NATURAL ASOF JOIN is not supported")

Review Comment:
   It seem to be invalid syntax, then it should be `plan_err` instead



##########
datafusion/sql/src/unparser/plan.rs:
##########
@@ -1917,6 +1917,126 @@ impl Unparser<'_> {
         }
     }
 
+    // Keep ASOF-specific locals out of the recursive plan unparser's stack 
frame.
+    #[inline(never)]
+    fn asof_join_to_sql(
+        &self,

Review Comment:
   btw how do we test unparser now, is there any test infra for roundtrip tests?



##########
datafusion/sqllogictest/test_files/asof_join.slt:
##########
@@ -0,0 +1,245 @@
+# 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.
+
+statement ok
+CREATE TABLE asof_left(id INT, grp TEXT, ts TIMESTAMP) AS VALUES
+  (1, 'A', TIMESTAMP '2024-01-01 09:00:01'),
+  (2, 'A', TIMESTAMP '2024-01-01 09:00:04'),
+  (3, 'A', TIMESTAMP '2024-01-01 09:00:07'),
+  (4, 'B', TIMESTAMP '2024-01-01 09:00:02'),
+  (5, 'B', TIMESTAMP '2024-01-01 09:00:08'),
+  (6, NULL, TIMESTAMP '2024-01-01 09:00:03'),
+  (7, 'A', NULL);
+
+statement ok
+CREATE TABLE asof_right(grp TEXT, ts TIMESTAMP, val TEXT) AS VALUES
+  ('A', TIMESTAMP '2024-01-01 09:00:02', 'a2'),
+  ('A', TIMESTAMP '2024-01-01 09:00:04', 'a4'),
+  ('A', TIMESTAMP '2024-01-01 09:00:06', 'a6'),
+  ('B', TIMESTAMP '2024-01-01 09:00:01', 'b1'),
+  ('B', TIMESTAMP '2024-01-01 09:00:06', 'b6'),
+  (NULL, TIMESTAMP '2024-01-01 09:00:02', 'null-group'),
+  ('A', NULL, 'null-ts');
+
+# Inclusive predecessor per equality group. This also verifies unmatched left
+# rows and NULL behavior for equality keys and ordered expressions.
+query IPPT
+SELECT l.id, l.ts, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts >= r.ts)
+ON l.grp = r.grp
+ORDER BY l.id;
+----
+1 2024-01-01T09:00:01 NULL NULL
+2 2024-01-01T09:00:04 2024-01-01T09:00:04 a4
+3 2024-01-01T09:00:07 2024-01-01T09:00:06 a6
+4 2024-01-01T09:00:02 2024-01-01T09:00:01 b1
+5 2024-01-01T09:00:08 2024-01-01T09:00:06 b6
+6 2024-01-01T09:00:03 NULL NULL
+7 NULL NULL NULL
+
+# Strict predecessor per equality group.
+query IPPT
+SELECT l.id, l.ts, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts > r.ts)
+ON l.grp = r.grp
+ORDER BY l.id;
+----
+1 2024-01-01T09:00:01 NULL NULL
+2 2024-01-01T09:00:04 2024-01-01T09:00:02 a2
+3 2024-01-01T09:00:07 2024-01-01T09:00:06 a6
+4 2024-01-01T09:00:02 2024-01-01T09:00:01 b1
+5 2024-01-01T09:00:08 2024-01-01T09:00:06 b6
+6 2024-01-01T09:00:03 NULL NULL
+7 NULL NULL NULL
+
+# Inclusive successor per equality group.
+query IPPT
+SELECT l.id, l.ts, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts <= r.ts)
+ON l.grp = r.grp
+ORDER BY l.id;
+----
+1 2024-01-01T09:00:01 2024-01-01T09:00:02 a2
+2 2024-01-01T09:00:04 2024-01-01T09:00:04 a4
+3 2024-01-01T09:00:07 NULL NULL
+4 2024-01-01T09:00:02 2024-01-01T09:00:06 b6
+5 2024-01-01T09:00:08 NULL NULL
+6 2024-01-01T09:00:03 NULL NULL
+7 NULL NULL NULL
+
+# Strict successor per equality group.
+query IPPT
+SELECT l.id, l.ts, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts < r.ts)
+ON l.grp = r.grp
+ORDER BY l.id;
+----
+1 2024-01-01T09:00:01 2024-01-01T09:00:02 a2
+2 2024-01-01T09:00:04 2024-01-01T09:00:06 a6
+3 2024-01-01T09:00:07 NULL NULL
+4 2024-01-01T09:00:02 2024-01-01T09:00:06 b6
+5 2024-01-01T09:00:08 NULL NULL
+6 2024-01-01T09:00:03 NULL NULL
+7 NULL NULL NULL
+
+# USING exposes one unqualified equality key.
+query TIPT
+SELECT grp, l.id, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts >= r.ts)
+USING (grp)
+ORDER BY l.id;
+----
+A 1 NULL NULL
+A 2 2024-01-01T09:00:04 a4
+A 3 2024-01-01T09:00:06 a6
+B 4 2024-01-01T09:00:01 b1
+B 5 2024-01-01T09:00:06 b6
+NULL 6 NULL NULL
+A 7 NULL NULL
+
+# Both qualified equality keys remain addressable.
+query ITT
+SELECT l.id, l.grp, r.grp
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts >= r.ts)
+USING (grp)
+ORDER BY l.id;
+----
+1 A NULL
+2 A A
+3 A A
+4 B B
+5 B B
+6 NULL NULL
+7 A NULL
+
+# Equality keys are optional.
+query IT
+SELECT l.id, r.label
+FROM (VALUES (1, 1), (2, 5), (3, CAST(NULL AS INT))) AS l(id, ts)
+ASOF JOIN (VALUES (2, 'r2'), (4, 'r4')) AS r(ts, label)
+MATCH_CONDITION (l.ts >= r.ts)
+ORDER BY l.id;
+----
+1 NULL
+2 r4
+3 NULL
+
+# Multiple equality keys form one candidate group.
+query IT
+SELECT l.id, r.val
+FROM (VALUES
+  (1, 'X', 'A', TIMESTAMP '2024-01-01 09:00:04'),
+  (2, 'Y', 'A', TIMESTAMP '2024-01-01 09:00:04')
+) AS l(id, venue, grp, ts)
+ASOF JOIN (VALUES
+  ('X', 'A', TIMESTAMP '2024-01-01 09:00:02', 'x-a2'),
+  ('Y', 'A', TIMESTAMP '2024-01-01 09:00:03', 'y-a3'),
+  ('X', 'B', TIMESTAMP '2024-01-01 09:00:04', 'x-b4')
+) AS r(venue, grp, ts, val)
+MATCH_CONDITION (l.ts >= r.ts)
+ON l.venue = r.venue AND l.grp = r.grp
+ORDER BY l.id;
+----
+1 x-a2
+2 y-a3
+
+# Candidate selection sees the right input after subquery filtering.
+query IT
+SELECT l.id, r.val
+FROM asof_left l
+ASOF JOIN (SELECT * FROM asof_right WHERE val <> 'a6') r
+MATCH_CONDITION (l.ts >= r.ts)
+ON l.grp = r.grp
+WHERE l.id IN (3, 5)
+ORDER BY l.id;
+----
+3 a4
+5 b6
+
+# Equality and match operands use the planner's common coercion types.
+query II
+SELECT l.id, r.payload
+FROM (VALUES (1, CAST(5 AS SMALLINT), CAST(10 AS INT))) AS l(id, grp, ts)
+ASOF JOIN (
+  VALUES (CAST(5 AS BIGINT), CAST(9 AS BIGINT), 90)
+) AS r(grp, ts, payload)
+MATCH_CONDITION (l.ts >= r.ts)
+ON l.grp = r.grp;
+----
+1 90
+
+query TT
+EXPLAIN SELECT l.id, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts >= r.ts)
+ON l.grp = r.grp;
+----
+logical_plan
+01)Projection: l.id, r.val
+02)--AsOf Join: match=[l.ts >= r.ts], constraint=On, on=[l.grp = r.grp]
+03)----SubqueryAlias: l
+04)------TableScan: asof_left projection=[id, grp, ts]
+05)----SubqueryAlias: r
+06)------TableScan: asof_right projection=[grp, ts, val]
+physical_plan
+01)ProjectionExec: expr=[id@0 as id, val@5 as val]
+02)--AsOfJoinExec: on=[(grp = grp)], match=[ts >= ts]
+03)----SortExec: expr=[grp@1 ASC, ts@2 ASC], preserve_partitioning=[false]
+04)------DataSourceExec: partitions=1, partition_sizes=[1]
+05)----SortExec: expr=[grp@0 ASC, ts@1 ASC], preserve_partitioning=[false]
+06)------DataSourceExec: partitions=1, partition_sizes=[1]
+
+query error ASOF MATCH_CONDITION requires <, <=, >, or >=
+SELECT *
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts = r.ts)
+ON l.grp = r.grp;
+
+query error ASOF MATCH_CONDITION left operand must reference only the left 
input

Review Comment:
   Should this query get normalized internally, and make it valid? 🤔 



##########
datafusion/expr/src/logical_plan/builder.rs:
##########
@@ -1018,6 +1024,48 @@ impl LogicalPlanBuilder {
         self.asof_join_with_constraint(right, on, match_condition, 
JoinConstraint::On)
     }
 
+    /// Apply a left-preserving ASOF join using ordinary `ON` and
+    /// `MATCH_CONDITION` expressions.
+    ///
+    /// Equality predicates may be combined with `AND`. This method validates
+    /// and separates their left and right operands before constructing the
+    /// structured ASOF logical plan.
+    pub fn asof_join_on(
+        self,
+        right: LogicalPlan,
+        on_exprs: impl IntoIterator<Item = Expr>,

Review Comment:
   I think it's better to make it a single `Expr`, otherwise callers have to 
figure out 'vec of expr means ANDed conjunction', I think it's an extra 
complexity.



##########
datafusion/expr/src/logical_plan/builder.rs:
##########
@@ -1018,6 +1024,48 @@ impl LogicalPlanBuilder {
         self.asof_join_with_constraint(right, on, match_condition, 
JoinConstraint::On)
     }
 
+    /// Apply a left-preserving ASOF join using ordinary `ON` and
+    /// `MATCH_CONDITION` expressions.
+    ///
+    /// Equality predicates may be combined with `AND`. This method validates
+    /// and separates their left and right operands before constructing the
+    /// structured ASOF logical plan.

Review Comment:
   I think now the comments explains the internal mechanism, but it should be 
user-facing: like specifying the `on_expr` and `match_condition`
   
   e.g. match_condition must be in the form `l_expr cmp r_expr`, `l_expr` is an 
expression only referencing left relation columns, ...



##########
datafusion/expr/src/logical_plan/builder.rs:
##########


Review Comment:
   ```suggestion
           match_condition: Expr,
   ```
   to make it consistent with `asof_join_on`



##########
datafusion/sqllogictest/test_files/asof_join.slt:
##########
@@ -0,0 +1,245 @@
+# 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.
+
+statement ok
+CREATE TABLE asof_left(id INT, grp TEXT, ts TIMESTAMP) AS VALUES
+  (1, 'A', TIMESTAMP '2024-01-01 09:00:01'),
+  (2, 'A', TIMESTAMP '2024-01-01 09:00:04'),
+  (3, 'A', TIMESTAMP '2024-01-01 09:00:07'),
+  (4, 'B', TIMESTAMP '2024-01-01 09:00:02'),
+  (5, 'B', TIMESTAMP '2024-01-01 09:00:08'),
+  (6, NULL, TIMESTAMP '2024-01-01 09:00:03'),
+  (7, 'A', NULL);
+
+statement ok
+CREATE TABLE asof_right(grp TEXT, ts TIMESTAMP, val TEXT) AS VALUES
+  ('A', TIMESTAMP '2024-01-01 09:00:02', 'a2'),
+  ('A', TIMESTAMP '2024-01-01 09:00:04', 'a4'),
+  ('A', TIMESTAMP '2024-01-01 09:00:06', 'a6'),
+  ('B', TIMESTAMP '2024-01-01 09:00:01', 'b1'),
+  ('B', TIMESTAMP '2024-01-01 09:00:06', 'b6'),
+  (NULL, TIMESTAMP '2024-01-01 09:00:02', 'null-group'),
+  ('A', NULL, 'null-ts');
+
+# Inclusive predecessor per equality group. This also verifies unmatched left
+# rows and NULL behavior for equality keys and ordered expressions.
+query IPPT
+SELECT l.id, l.ts, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts >= r.ts)
+ON l.grp = r.grp
+ORDER BY l.id;
+----
+1 2024-01-01T09:00:01 NULL NULL
+2 2024-01-01T09:00:04 2024-01-01T09:00:04 a4
+3 2024-01-01T09:00:07 2024-01-01T09:00:06 a6
+4 2024-01-01T09:00:02 2024-01-01T09:00:01 b1
+5 2024-01-01T09:00:08 2024-01-01T09:00:06 b6
+6 2024-01-01T09:00:03 NULL NULL
+7 NULL NULL NULL
+
+# Strict predecessor per equality group.
+query IPPT
+SELECT l.id, l.ts, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts > r.ts)
+ON l.grp = r.grp
+ORDER BY l.id;
+----
+1 2024-01-01T09:00:01 NULL NULL
+2 2024-01-01T09:00:04 2024-01-01T09:00:02 a2
+3 2024-01-01T09:00:07 2024-01-01T09:00:06 a6
+4 2024-01-01T09:00:02 2024-01-01T09:00:01 b1
+5 2024-01-01T09:00:08 2024-01-01T09:00:06 b6
+6 2024-01-01T09:00:03 NULL NULL
+7 NULL NULL NULL
+
+# Inclusive successor per equality group.
+query IPPT
+SELECT l.id, l.ts, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts <= r.ts)
+ON l.grp = r.grp
+ORDER BY l.id;
+----
+1 2024-01-01T09:00:01 2024-01-01T09:00:02 a2
+2 2024-01-01T09:00:04 2024-01-01T09:00:04 a4
+3 2024-01-01T09:00:07 NULL NULL
+4 2024-01-01T09:00:02 2024-01-01T09:00:06 b6
+5 2024-01-01T09:00:08 NULL NULL
+6 2024-01-01T09:00:03 NULL NULL
+7 NULL NULL NULL
+
+# Strict successor per equality group.
+query IPPT
+SELECT l.id, l.ts, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts < r.ts)
+ON l.grp = r.grp
+ORDER BY l.id;
+----
+1 2024-01-01T09:00:01 2024-01-01T09:00:02 a2
+2 2024-01-01T09:00:04 2024-01-01T09:00:06 a6
+3 2024-01-01T09:00:07 NULL NULL
+4 2024-01-01T09:00:02 2024-01-01T09:00:06 b6
+5 2024-01-01T09:00:08 NULL NULL
+6 2024-01-01T09:00:03 NULL NULL
+7 NULL NULL NULL
+
+# USING exposes one unqualified equality key.
+query TIPT
+SELECT grp, l.id, r.ts, r.val
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts >= r.ts)
+USING (grp)
+ORDER BY l.id;
+----
+A 1 NULL NULL
+A 2 2024-01-01T09:00:04 a4
+A 3 2024-01-01T09:00:06 a6
+B 4 2024-01-01T09:00:01 b1
+B 5 2024-01-01T09:00:06 b6
+NULL 6 NULL NULL
+A 7 NULL NULL
+
+# Both qualified equality keys remain addressable.
+query ITT
+SELECT l.id, l.grp, r.grp
+FROM asof_left l
+ASOF JOIN asof_right r
+MATCH_CONDITION (l.ts >= r.ts)
+USING (grp)
+ORDER BY l.id;
+----
+1 A NULL
+2 A A
+3 A A
+4 B B
+5 B B
+6 NULL NULL
+7 A NULL
+
+# Equality keys are optional.
+query IT
+SELECT l.id, r.label
+FROM (VALUES (1, 1), (2, 5), (3, CAST(NULL AS INT))) AS l(id, ts)
+ASOF JOIN (VALUES (2, 'r2'), (4, 'r4')) AS r(ts, label)
+MATCH_CONDITION (l.ts >= r.ts)
+ORDER BY l.id;
+----
+1 NULL
+2 r4
+3 NULL
+
+# Multiple equality keys form one candidate group.
+query IT
+SELECT l.id, r.val
+FROM (VALUES
+  (1, 'X', 'A', TIMESTAMP '2024-01-01 09:00:04'),
+  (2, 'Y', 'A', TIMESTAMP '2024-01-01 09:00:04')
+) AS l(id, venue, grp, ts)
+ASOF JOIN (VALUES
+  ('X', 'A', TIMESTAMP '2024-01-01 09:00:02', 'x-a2'),
+  ('Y', 'A', TIMESTAMP '2024-01-01 09:00:03', 'y-a3'),
+  ('X', 'B', TIMESTAMP '2024-01-01 09:00:04', 'x-b4')
+) AS r(venue, grp, ts, val)
+MATCH_CONDITION (l.ts >= r.ts)
+ON l.venue = r.venue AND l.grp = r.grp
+ORDER BY l.id;
+----
+1 x-a2
+2 y-a3
+
+# Candidate selection sees the right input after subquery filtering.
+query IT
+SELECT l.id, r.val
+FROM asof_left l
+ASOF JOIN (SELECT * FROM asof_right WHERE val <> 'a6') r
+MATCH_CONDITION (l.ts >= r.ts)
+ON l.grp = r.grp
+WHERE l.id IN (3, 5)
+ORDER BY l.id;
+----
+3 a4
+5 b6
+
+# Equality and match operands use the planner's common coercion types.

Review Comment:
   I suggest to add an explain to this testcase, to ensure the coercion is on 
the right side



##########
docs/source/user-guide/sql/select.md:
##########
@@ -400,6 +401,45 @@ SELECT * FROM x LEFT JOIN x AS y ON x.column_1 = 
y.column_2;
 +----------+----------+----------+----------+
 ```
 
+### ASOF JOIN
+
+An `ASOF JOIN` matches each left row with at most one right row according to an
+ordered comparison. It preserves every left row and fills the right columns
+with `NULL` when no right row matches.

Review Comment:
   We can also mention DataFusion is following snowflake syntax, and add a 
reference link.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to