Repository: duckdb/duckdb (language: python)

Issue:
Unexpected result when using `INTERVAL` and `INNER JOIN` subquery
### What happens?

Consider the following test case. The second query with a `WHERE` filter returns 36 rows, and it's more than the first query without a `WHERE` filter, which is unexpected. 

Considering PostgreSQL returns 36 rows for both queries, I suppose the first query is unexpected.

### To Reproduce

```sql
CREATE  TABLE  t0(c1 INTERVAL);
INSERT INTO t0(c1) VALUES ('2 years 3 months');
INSERT INTO t0(c1) VALUES ('-1734799452 DAYS'), ('2 DAYS');
INSERT INTO t0(c1) VALUES ('13 days');
INSERT INTO t0(c1) VALUES ('1 month');
INSERT INTO t0(c1) VALUES ('3 days');

SELECT * FROM t0 INNER  JOIN  (SELECT  INTERVAL 1000 DAY AS col0 FROM t0) AS sub0  ON (t0.c1 < sub0.col0); -- 24 rows (unexpected)
SELECT * FROM t0 INNER  JOIN  (SELECT  INTERVAL 1000 DAY AS col0 FROM t0) AS sub0  ON (t0.c1 < sub0.col0) WHERE (NOT (t0.c1 != t0.c1)); -- 36 rows


```

### OS:

Ubuntu 22.04

### DuckDB Version:

v1.1.4-dev1859 b484c2d96f

### DuckDB Client:

CLI

### Hardware:

_No response_

### Full Name:

Suyang Zhong

### Affiliation:

NUS

### What is the latest build you tested with? If possible, we recommend testing with the latest nightly build.

I have tested with a source build

### Did you include all relevant data sets for reproducing the issue?

Yes

### Did you include all code required to reproduce the issue?

- [X] Yes, I have

### Did you include all relevant configuration (e.g., CPU architecture, Python version, Linux distribution) to reproduce the issue?

- [X] Yes, I have


Discussion / hints:
Thanks!

Fix patch:
diff --git a/extension/core_functions/function_list.cpp b/extension/core_functions/function_list.cpp
index a534c43400af..c6b773ae3bc2 100644
--- a/extension/core_functions/function_list.cpp
+++ b/extension/core_functions/function_list.cpp
@@ -282,6 +282,7 @@ static const StaticFunctionDefinition core_functions[] = {
 	DUCKDB_SCALAR_FUNCTION_SET(MonthNameFun),
 	DUCKDB_SCALAR_FUNCTION_SET(NanosecondsFun),
 	DUCKDB_SCALAR_FUNCTION_SET(NextAfterFun),
+	DUCKDB_SCALAR_FUNCTION(NormalizedIntervalFun),
 	DUCKDB_SCALAR_FUNCTION_ALIAS(NowFun),
 	DUCKDB_SCALAR_FUNCTION_ALIAS(OrdFun),
 	DUCKDB_SCALAR_FUNCTION_SET(ParseDirnameFun),
diff --git a/extension/core_functions/include/core_functions/scalar/date_functions.hpp b/extension/core_functions/include/core_functions/scalar/date_functions.hpp
index 72057aceb118..c7192d89f29d 100644
--- a/extension/core_functions/include/core_functions/scalar/date_functions.hpp
+++ b/extension/core_functions/include/core_functions/scalar/date_functions.hpp
@@ -372,6 +372,15 @@ struct NanosecondsFun {
 	static ScalarFunctionSet GetFunctions();
 };
 
+struct NormalizedIntervalFun {
+	static constexpr const char *Name = "normalized_interval";
+	static constexpr const char *Parameters = "interval";
+	static constexpr const char *Description = "Normalizes an INTERVAL to an equivalent interval";
+	static constexpr const char *Example = "normalized_interval(INTERVAL '30 days')";
+
+	static ScalarFunction GetFunction();
+};
+
 struct QuarterFun {
 	static constexpr const char *Name = "quarter";
 	static constexpr const char *Parameters = "ts";
diff --git a/extension/core_functions/scalar/date/epoch.cpp b/extension/core_functions/scalar/date/epoch.cpp
index 01481d721381..c30530341afc 100644
--- a/extension/core_functions/scalar/date/epoch.cpp
+++ b/extension/core_functions/scalar/date/epoch.cpp
@@ -28,6 +28,23 @@ ScalarFunction ToTimestampFun::GetFunction() {
 	return ScalarFunction({LogicalType::DOUBLE}, LogicalType::TIMESTAMP_TZ, EpochSecFunction);
 }
 
+struct NormalizedIntervalOperator {
+	template <typename INPUT_TYPE, typename RESULT_TYPE>
+	static RESULT_TYPE Operation(INPUT_TYPE input) {
+		return input.Normalize();
+	}
+};
+
+static void NormalizedIntervalFunction(DataChunk &input, ExpressionState &state, Vector &result) {
+	D_ASSERT(input.ColumnCount() == 1);
+
+	UnaryExecutor::Execute<interval_t, interval_t, NormalizedIntervalOperator>(input.data[0], result, input.size());
+}
+
+ScalarFunction NormalizedIntervalFun::GetFunction() {
+	return ScalarFunction({LogicalType::INTERVAL}, LogicalType::INTERVAL, NormalizedIntervalFunction);
+}
+
 struct TimeTZSortKeyOperator {
 	template <typename INPUT_TYPE, typename RESULT_TYPE>
 	static RESULT_TYPE Operation(INPUT_TYPE input) {
@@ -44,5 +61,4 @@ static void TimeTZSortKeyFunction(DataChunk &input, ExpressionState &state, Vect
 ScalarFunction TimeTZSortKeyFun::GetFunction() {
 	return ScalarFunction({LogicalType::TIME_TZ}, LogicalType::UBIGINT, TimeTZSortKeyFunction);
 }
-
 } // namespace duckdb
diff --git a/extension/core_functions/scalar/date/functions.json b/extension/core_functions/scalar/date/functions.json
index cd74605174b5..6da0714bc1ee 100644
--- a/extension/core_functions/scalar/date/functions.json
+++ b/extension/core_functions/scalar/date/functions.json
@@ -276,6 +276,14 @@
         "example": "nanosecond(timestamp_ns '2021-08-03 11:59:44.123456789') => 44123456789",
         "type": "scalar_function_set"
     },
+    {
+        "struct": "NormalizedIntervalFun",
+        "name": "normalized_interval",
+        "parameters": "interval",
+        "description": "Normalizes an INTERVAL to an equivalent interval",
+        "example": "normalized_interval(INTERVAL '30 days')",
+        "type": "scalar_function"
+    },
     {
         "name": "quarter",
         "parameters": "ts",
diff --git a/src/include/duckdb/common/types/interval.hpp b/src/include/duckdb/common/types/interval.hpp
index 85a90666c698..fdda9ee2afe2 100644
--- a/src/include/duckdb/common/types/interval.hpp
+++ b/src/include/duckdb/common/types/interval.hpp
@@ -27,6 +27,11 @@ struct interval_t { // NOLINT
 	int64_t micros;
 
 	inline void Normalize(int64_t &months, int64_t &days, int64_t &micros) const;
+
+	// Normalize to interval bounds.
+	inline static void Borrow(const int64_t msf, int64_t &lsf, int32_t &f, const int64_t scale);
+	inline interval_t Normalize() const;
+
 	inline bool operator==(const interval_t &right) const {
 		//	Quick equality check
 		const auto &left = *this;
@@ -165,6 +170,7 @@ class Interval {
 		return left > right;
 	}
 };
+
 void interval_t::Normalize(int64_t &months, int64_t &days, int64_t &micros) const {
 	auto &input = *this;
 
@@ -182,4 +188,30 @@ void interval_t::Normalize(int64_t &months, int64_t &days, int64_t &micros) cons
 	months += carry_months;
 }
 
+void interval_t::Borrow(const int64_t msf, int64_t &lsf, int32_t &f, const int64_t scale) {
+	if (msf > NumericLimits<int32_t>::Maximum()) {
+		f = NumericLimits<int32_t>::Maximum();
+		lsf += (msf - f) * scale;
+	} else if (msf < NumericLimits<int32_t>::Minimum()) {
+		f = NumericLimits<int32_t>::Minimum();
+		lsf += (msf - f) * scale;
+	} else {
+		f = UnsafeNumericCast<int32_t>(msf);
+	}
+}
+
+interval_t interval_t::Normalize() const {
+	interval_t result;
+
+	int64_t mm;
+	int64_t dd;
+	Normalize(mm, dd, result.micros);
+
+	//  Borrow right on overflow
+	Borrow(mm, dd, result.months, Interval::DAYS_PER_MONTH);
+	Borrow(dd, result.micros, result.days, Interval::MICROS_PER_DAY);
+
+	return result;
+}
+
 } // namespace duckdb
diff --git a/src/planner/collation_binding.cpp b/src/planner/collation_binding.cpp
index d620b63fcbd7..aabde7f55b62 100644
--- a/src/planner/collation_binding.cpp
+++ b/src/planner/collation_binding.cpp
@@ -87,10 +87,32 @@ bool PushTimeTZCollation(ClientContext &context, unique_ptr<Expression> &source,
 	return true;
 }
 
+bool PushIntervalCollation(ClientContext &context, unique_ptr<Expression> &source, const LogicalType &sql_type,
+                           CollationType) {
+	if (sql_type.id() != LogicalTypeId::INTERVAL) {
+		return false;
+	}
+
+	auto &catalog = Catalog::GetSystemCatalog(context);
+	auto &function_entry = catalog.GetEntry<ScalarFunctionCatalogEntry>(context, DEFAULT_SCHEMA, "normalized_interval");
+	if (function_entry.functions.Size() != 1) {
+		throw InternalException("normalized_interval should only have a single overload");
+	}
+	auto &scalar_function = function_entry.functions.GetFunctionReferenceByOffset(0);
+	vector<unique_ptr<Expression>> children;
+	children.push_back(std::move(source));
+
+	FunctionBinder function_binder(context);
+	auto function = function_binder.BindScalarFunction(scalar_function, std::move(children));
+	source = std::move(function);
+	return true;
+}
+
 // timetz_byte_comparable
 CollationBinding::CollationBinding() {
 	RegisterCollation(CollationCallback(PushVarcharCollation));
 	RegisterCollation(CollationCallback(PushTimeTZCollation));
+	RegisterCollation(CollationCallback(PushIntervalCollation));
 }
 
 void CollationBinding::RegisterCollation(CollationCallback callback) {


Test patch:
diff --git a/test/sql/types/interval/test_interval_comparison.test b/test/sql/types/interval/test_interval_comparison.test
index 83a70972cd11..a56c815dbe22 100644
--- a/test/sql/types/interval/test_interval_comparison.test
+++ b/test/sql/types/interval/test_interval_comparison.test
@@ -51,3 +51,148 @@ query I
 select interval '28 days 432000 seconds' = interval '1 month 3 days';
 ----
 True
+
+# Sorting with normalisation
+statement ok
+CREATE TABLE issue14384(i INTERVAL);
+
+statement ok
+INSERT INTO issue14384(i) VALUES 
+	('2 years 3 months'),
+	('-1734799452 DAYS'), 
+	('2 DAYS'),
+	('13 days'),
+	('1 month'),
+	('3 days'),
+;
+
+query I
+SELECT i FROM issue14384 ORDER BY ALL;
+----
+-1734799452 days
+2 days
+3 days
+13 days
+1 month
+2 years 3 months
+
+# PiecewiseMergeJoin
+query II 
+SELECT * 
+FROM issue14384 
+INNER JOIN (
+	SELECT INTERVAL 1000 DAY AS col0 
+	FROM issue14384) AS sub0  
+	ON (issue14384.i < sub0.col0)
+ORDER BY ALL;
+----
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+2 days	1000 days
+2 days	1000 days
+2 days	1000 days
+2 days	1000 days
+2 days	1000 days
+2 days	1000 days
+3 days	1000 days
+3 days	1000 days
+3 days	1000 days
+3 days	1000 days
+3 days	1000 days
+3 days	1000 days
+13 days	1000 days
+13 days	1000 days
+13 days	1000 days
+13 days	1000 days
+13 days	1000 days
+13 days	1000 days
+1 month	1000 days
+1 month	1000 days
+1 month	1000 days
+1 month	1000 days
+1 month	1000 days
+1 month	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+
+# NestedLoopJoin
+query II
+SELECT * 
+FROM issue14384 
+INNER  JOIN  (
+	SELECT  INTERVAL 1000 DAY AS col0 FROM issue14384) AS sub0  
+	ON (issue14384.i < sub0.col0) 
+WHERE (NOT (issue14384.i != issue14384.i))
+ORDER BY ALL;
+----
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+-1734799452 days	1000 days
+2 days	1000 days
+2 days	1000 days
+2 days	1000 days
+2 days	1000 days
+2 days	1000 days
+2 days	1000 days
+3 days	1000 days
+3 days	1000 days
+3 days	1000 days
+3 days	1000 days
+3 days	1000 days
+3 days	1000 days
+13 days	1000 days
+13 days	1000 days
+13 days	1000 days
+13 days	1000 days
+13 days	1000 days
+13 days	1000 days
+1 month	1000 days
+1 month	1000 days
+1 month	1000 days
+1 month	1000 days
+1 month	1000 days
+1 month	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+2 years 3 months	1000 days
+
+
+statement ok
+INSERT INTO issue14384(i) VALUES 
+	('1000 DAY'),
+	('28 days 432000 seconds'),
+	('1 month 3 days'),
+	('2147483647 months 2147483647 days 9223372036854775807 microseconds'),
+	('-2147483648 months -2147483648 days -9223372036854775807 microseconds'),
+	(NULL)
+;
+
+query I
+SELECT i FROM issue14384 ORDER BY ALL;
+----
+-178956970 years -8 months -2147483648 days -2562047788:00:54.775807
+-1734799452 days
+2 days
+3 days
+13 days
+1 month
+28 days 120:00:00
+1 month 3 days
+2 years 3 months
+1000 days
+178956970 years 7 months 2147483647 days 2562047788:00:54.775807
+NULL