From 1851fb540f47116aeabd88b3d387dc7bc8e6515e Mon Sep 17 00:00:00 2001 From: Angela Xie Date: Tue, 18 Nov 2025 14:32:17 -0800 Subject: [PATCH 1/2] make IsNullable return true for nullable datetime functions --- enginetest/queries/script_queries.go | 7 ++++ sql/expression/function/extract.go | 5 +++ sql/expression/function/time.go | 54 ++++++++++++++++++++++++++-- sql/expression/function/time_math.go | 7 +++- sql/types/datetime.go | 4 +-- 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/enginetest/queries/script_queries.go b/enginetest/queries/script_queries.go index 5de345fa06..1f8d9d9769 100644 --- a/enginetest/queries/script_queries.go +++ b/enginetest/queries/script_queries.go @@ -13441,16 +13441,23 @@ select * from t1 except ( }, { // https://github.com/dolthub/dolt/issues/10070 + // https://github.com/dolthub/dolt/issues/10092 Name: "NOT EXISTS with nullable filter", SetUpScript: []string{ "CREATE TABLE t0(c0 INT , c1 INT);", "INSERT INTO t0(c0, c1) VALUES (1, -2);", + "create table t1(c0 int, primary key(c0))", + "insert into t1 values (1)", }, Assertions: []ScriptTestAssertion{ { Query: `SELECT * FROM t0 WHERE NOT EXISTS (SELECT 1 FROM (SELECT 1) alias0 WHERE (CASE -1 WHEN t0.c1 THEN false END));`, Expected: []sql.Row{{1, -2}}, }, + { + Query: "select * from t1 where not exists (select 1 from (select 1) as subquery where weekday(t1.c0))", + Expected: []sql.Row{{1}}, + }, }, }, } diff --git a/sql/expression/function/extract.go b/sql/expression/function/extract.go index 7332a03570..bc86f5ab40 100644 --- a/sql/expression/function/extract.go +++ b/sql/expression/function/extract.go @@ -54,6 +54,11 @@ func (td *Extract) Description() string { // Type implements the Expression interface. func (td *Extract) Type() sql.Type { return types.Int64 } +// IsNullable implements the Expression interface +func (td *Extract) IsNullable() bool { + return true +} + // CollationCoercibility implements the interface sql.CollationCoercible. func (*Extract) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 diff --git a/sql/expression/function/time.go b/sql/expression/function/time.go index 76d3026fe0..01f9065dde 100644 --- a/sql/expression/function/time.go +++ b/sql/expression/function/time.go @@ -146,6 +146,11 @@ func (q *Quarter) String() string { return fmt.Sprintf("%s(%s)", q.FunctionName( // Type implements the Expression interface. func (q *Quarter) Type() sql.Type { return types.Int32 } +// IsNullable implements the Expression interface +func (q *Quarter) IsNullable() bool { + return true +} + // CollationCoercibility implements the interface sql.CollationCoercible. func (q *Quarter) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 @@ -192,6 +197,11 @@ func (m *Month) String() string { return fmt.Sprintf("%s(%s)", m.FunctionName(), // Type implements the Expression interface. func (m *Month) Type() sql.Type { return types.Int32 } +// IsNullable implements the Expression interface +func (d *Month) IsNullable() bool { + return true +} + // CollationCoercibility implements the interface sql.CollationCoercible. func (*Month) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 @@ -238,6 +248,11 @@ func (d *Day) String() string { return fmt.Sprintf("%s(%s)", d.FunctionName(), d // Type implements the Expression interface. func (d *Day) Type() sql.Type { return types.Int32 } +// IsNullable implements the Expression interface +func (d *Day) IsNullable() bool { + return true +} + // CollationCoercibility implements the interface sql.CollationCoercible. func (*Day) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 @@ -285,6 +300,11 @@ func (d *Weekday) String() string { return fmt.Sprintf("%s(%s)", d.FunctionName( // Type implements the Expression interface. func (d *Weekday) Type() sql.Type { return types.Int32 } +// IsNullable implements the Expression interface +func (d *Weekday) IsNullable() bool { + return true +} + // CollationCoercibility implements the interface sql.CollationCoercible. func (*Weekday) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 @@ -470,6 +490,11 @@ func (d *DayOfWeek) String() string { return fmt.Sprintf("DAYOFWEEK(%s)", d.Chil // Type implements the Expression interface. func (d *DayOfWeek) Type() sql.Type { return types.Int32 } +// IsNullable implements the Expression interface +func (d *DayOfWeek) IsNullable() bool { + return true +} + // CollationCoercibility implements the interface sql.CollationCoercible. func (*DayOfWeek) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 @@ -516,6 +541,11 @@ func (d *DayOfYear) String() string { return fmt.Sprintf("DAYOFYEAR(%s)", d.Chil // Type implements the Expression interface. func (d *DayOfYear) Type() sql.Type { return types.Int32 } +// IsNullable implements the Expression interface +func (d *DayOfYear) IsNullable() bool { + return true +} + // CollationCoercibility implements the interface sql.CollationCoercible. func (*DayOfYear) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 @@ -653,7 +683,7 @@ func (d *YearWeek) Children() []sql.Expression { return []sql.Expression{d.date, // IsNullable implements the Expression interface. func (d *YearWeek) IsNullable() bool { - return d.date.IsNullable() + return true } // WithChildren implements the Expression interface. @@ -781,7 +811,7 @@ func (d *Week) Children() []sql.Expression { return []sql.Expression{d.date, d.m // IsNullable implements the Expression interface. func (d *Week) IsNullable() bool { - return d.date.IsNullable() + return true } // WithChildren implements the Expression interface. @@ -1288,6 +1318,11 @@ func (d *Date) String() string { return fmt.Sprintf("DATE(%s)", d.Child) } // Type implements the Expression interface. func (d *Date) Type() sql.Type { return types.Date } +// IsNullable implements the Expression interface +func (d *Date) IsNullable() bool { + return true +} + // CollationCoercibility implements the interface sql.CollationCoercible. func (*Date) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 @@ -1370,6 +1405,11 @@ func (dtf *UnaryDatetimeFunc) String() string { return fmt.Sprintf("%s(%s)", strings.ToUpper(dtf.Name), dtf.Child.String()) } +// IsNullable implements the Expression interface +func (dtf *UnaryDatetimeFunc) IsNullable() bool { + return true +} + // Type implements the Expression interface. func (dtf *UnaryDatetimeFunc) Type() sql.Type { return dtf.SQLType @@ -1401,6 +1441,11 @@ func (*DayName) CollationCoercibility(ctx *sql.Context) (collation sql.Collation return ctx.GetCollation(), 4 } +// IsNullable implements the Expression interface +func (d *DayName) IsNullable() bool { + return true +} + func (d *DayName) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { val, err := d.EvalChild(ctx, row) if err != nil { @@ -1487,6 +1532,11 @@ func (*MonthName) CollationCoercibility(ctx *sql.Context) (collation sql.Collati return ctx.GetCollation(), 4 } +// IsNullable implements the Expression interface +func (d *MonthName) IsNullable() bool { + return true +} + func (d *MonthName) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { val, err := d.EvalChild(ctx, row) if err != nil { diff --git a/sql/expression/function/time_math.go b/sql/expression/function/time_math.go index 56db5ffd69..340fa92a39 100644 --- a/sql/expression/function/time_math.go +++ b/sql/expression/function/time_math.go @@ -63,6 +63,11 @@ func (d *DateDiff) String() string { // Type implements the sql.Expression interface. func (d *DateDiff) Type() sql.Type { return types.Int64 } +// IsNullable implements the Expression interface +func (d *DateDiff) IsNullable() bool { + return true +} + // CollationCoercibility implements the interface sql.CollationCoercible. func (*DateDiff) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { return sql.Collation_binary, 5 @@ -600,7 +605,7 @@ func (t *TimestampDiff) Resolved() bool { // IsNullable implements the sql.Expression interface. func (t *TimestampDiff) IsNullable() bool { - return t.unit.IsNullable() && t.expr1.IsNullable() && t.expr2.IsNullable() + return true } // Type implements the sql.Expression interface. diff --git a/sql/types/datetime.go b/sql/types/datetime.go index aca5ac146c..6669aeb8fd 100644 --- a/sql/types/datetime.go +++ b/sql/types/datetime.go @@ -66,8 +66,8 @@ var ( // datetimeMaxTime is the maximum representable time value, MYSQL: 9999-12-31 23:59:59.999999 (microseconds) datetimeMaxTime = time.Date(9999, 12, 31, 23, 59, 59, 999999000, time.UTC) - // datetimeMinTime is the minimum representable time value, MYSQL: 0000-01-01 00:00:00.000000 (microseconds) - datetimeMinTime = time.Date(0000, 0, 0, 0, 0, 0, 0, time.UTC) + // datetimeMinTime is the minimum representable time value, MYSQL: 0000-00-00 00:00:00.000000 (microseconds) + datetimeMinTime = ZeroTime DateOnlyLayouts = []string{ "2006-01-02", From 0d1594ccfd55c8566707226d66203cf0afb8f60a Mon Sep 17 00:00:00 2001 From: Angela Xie Date: Tue, 18 Nov 2025 14:45:06 -0800 Subject: [PATCH 2/2] update query plans --- enginetest/queries/tpch_plans.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/enginetest/queries/tpch_plans.go b/enginetest/queries/tpch_plans.go index e4c64805dd..200939f4c0 100644 --- a/enginetest/queries/tpch_plans.go +++ b/enginetest/queries/tpch_plans.go @@ -848,11 +848,11 @@ order by cust_nation, l_year;`, ExpectedPlan: "Project\n" + - " ├─ columns: [shipping.supp_nation:1!null, shipping.cust_nation:2!null, shipping.l_year:3!null, sum(shipping.volume):0!null->revenue:0]\n" + - " └─ Sort(shipping.supp_nation:1!null ASC nullsFirst, shipping.cust_nation:2!null ASC nullsFirst, shipping.l_year:3!null ASC nullsFirst)\n" + + " ├─ columns: [shipping.supp_nation:1!null, shipping.cust_nation:2!null, shipping.l_year:3, sum(shipping.volume):0!null->revenue:0]\n" + + " └─ Sort(shipping.supp_nation:1!null ASC nullsFirst, shipping.cust_nation:2!null ASC nullsFirst, shipping.l_year:3 ASC nullsFirst)\n" + " └─ GroupBy\n" + - " ├─ select: SUM(shipping.volume:3!null), shipping.supp_nation:0!null, shipping.cust_nation:1!null, shipping.l_year:2!null\n" + - " ├─ group: shipping.supp_nation:0!null, shipping.cust_nation:1!null, shipping.l_year:2!null\n" + + " ├─ select: SUM(shipping.volume:3!null), shipping.supp_nation:0!null, shipping.cust_nation:1!null, shipping.l_year:2\n" + + " ├─ group: shipping.supp_nation:0!null, shipping.cust_nation:1!null, shipping.l_year:2\n" + " └─ SubqueryAlias\n" + " ├─ name: shipping\n" + " ├─ outerVisibility: false\n" + @@ -1084,14 +1084,14 @@ group by order by o_year;`, ExpectedPlan: "Project\n" + - " ├─ columns: [all_nations.o_year:2!null, (sum(case when (all_nations.nation = 'brazil') then all_nations.volume else 0 end):0!null / sum(all_nations.volume):1!null)->mkt_share:0]\n" + - " └─ Sort(all_nations.o_year:2!null ASC nullsFirst)\n" + + " ├─ columns: [all_nations.o_year:2, (sum(case when (all_nations.nation = 'brazil') then all_nations.volume else 0 end):0!null / sum(all_nations.volume):1!null)->mkt_share:0]\n" + + " └─ Sort(all_nations.o_year:2 ASC nullsFirst)\n" + " └─ GroupBy\n" + " ├─ select: SUM(CASE WHEN Eq\n" + " │ ├─ all_nations.nation:2!null\n" + " │ └─ BRAZIL (longtext)\n" + - " │ THEN all_nations.volume:1!null ELSE 0 (tinyint) END), SUM(all_nations.volume:1!null), all_nations.o_year:0!null\n" + - " ├─ group: all_nations.o_year:0!null\n" + + " │ THEN all_nations.volume:1!null ELSE 0 (tinyint) END), SUM(all_nations.volume:1!null), all_nations.o_year:0\n" + + " ├─ group: all_nations.o_year:0\n" + " └─ SubqueryAlias\n" + " ├─ name: all_nations\n" + " ├─ outerVisibility: false\n" + @@ -1345,11 +1345,11 @@ order by nation, o_year desc;`, ExpectedPlan: "Project\n" + - " ├─ columns: [profit.nation:1!null, profit.o_year:2!null, sum(profit.amount):0!null->sum_profit:0]\n" + - " └─ Sort(profit.nation:1!null ASC nullsFirst, profit.o_year:2!null DESC nullsFirst)\n" + + " ├─ columns: [profit.nation:1!null, profit.o_year:2, sum(profit.amount):0!null->sum_profit:0]\n" + + " └─ Sort(profit.nation:1!null ASC nullsFirst, profit.o_year:2 DESC nullsFirst)\n" + " └─ GroupBy\n" + - " ├─ select: SUM(profit.amount:2!null), profit.nation:0!null, profit.o_year:1!null\n" + - " ├─ group: profit.nation:0!null, profit.o_year:1!null\n" + + " ├─ select: SUM(profit.amount:2!null), profit.nation:0!null, profit.o_year:1\n" + + " ├─ group: profit.nation:0!null, profit.o_year:1\n" + " └─ SubqueryAlias\n" + " ├─ name: profit\n" + " ├─ outerVisibility: false\n" +