Repository: duckdb/duckdb (2026-07-30T09:07:14Z)

Issue:
VARCHAR hive partition value 'NULL' round-trips as SQL NULL
### What happens?

See repro for details

### To Reproduce

```sql
memory D CREATE TABLE t AS SELECT * FROM (VALUES ('NULL', 1), ('ok', 2)) v(p, id);
memory D COPY t TO '/private/tmp/duckdb_partition_literal_NULL_only'
         (FORMAT PARQUET, PARTITION_BY (p), OVERWRITE);
memory D SELECT p IS NULL AS is_null, p='NULL' AS equals_literal, id
         FROM read_parquet('/private/tmp/duckdb_partition_literal_NULL_only/**/*.parquet',
                           hive_partitioning=true)
         ORDER BY id;
┌─────────┬────────────────┬───────┐
│ is_null │ equals_literal │  id   │
│ boolean │    boolean     │ int32 │
├─────────┼────────────────┼───────┤
│ true    │ NULL           │     1 │
│ false   │ false          │     2 │
└─────────┴────────────────┴───────┘
```

### OS:

macos

### DuckDB Version:

d54e4f6a20c75e4cf1bdd7bbecb24bd08a62a491

### DuckDB Client:

SQL

### Hardware:

_No response_

### Full Name:

dentiny

### Affiliation:

N/A

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

- [x] Yes, I have

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

- [x] Yes, I have

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

Yes


Discussion:

Patch:
diff --git a/src/common/hive_partitioning.cpp b/src/common/hive_partitioning.cpp
index e3c827f26769..76ffe210ef61 100644
--- a/src/common/hive_partitioning.cpp
+++ b/src/common/hive_partitioning.cpp
@@ -131,14 +131,18 @@ std::map<string, string> HivePartitioning::Parse(const string &filename) {
 
 Value HivePartitioning::GetValue(ClientContext &context, const string &key, const string &str_val,
                                  const LogicalType &type) {
-	// Handle nulls
-	if (IsNull(str_val)) {
+	// On SQLNULL, DuckDB writes "__HIVE_DEFAULT_PARTITION__", instead of string version "NULL".
+	if (str_val == "__HIVE_DEFAULT_PARTITION__") {
 		return Value(type);
 	}
 	if (type.id() == LogicalTypeId::VARCHAR) {
 		// for string values we can directly return the type
 		return Value(Unescape(str_val));
 	}
+	// Handle Hive NULL markers for non-string partition types
+	if (StringUtil::CIEquals(str_val, "NULL")) {
+		return Value(type);
+	}
 	if (str_val.empty()) {
 		// empty strings are NULL for non-string types
 		return Value(type);
diff --git a/src/include/duckdb/common/hive_partitioning.hpp b/src/include/duckdb/common/hive_partitioning.hpp
index 18ede7770054..a217359f7a7d 100644
--- a/src/include/duckdb/common/hive_partitioning.hpp
+++ b/src/include/duckdb/common/hive_partitioning.hpp
@@ -41,7 +41,7 @@ class HivePartitioning {
 	DUCKDB_API static string Escape(const string &input);
 	//! Unescape a hive partition key or value encoded using URL encoding
 	DUCKDB_API static string Unescape(const string &input);
-	//! Whether the column is "NULL"/"__HIVE_DEFAULT_PARTITION"
+	//! Whether the value is a null marker when detecting Hive partition types
 	DUCKDB_API static bool IsNull(const string &input);
 };
 


Test patch:
diff --git a/test/sql/copy/parquet/parquet_hive_empty.test b/test/sql/copy/parquet/parquet_hive_empty.test
index 6199e547a8e9..795b626a244a 100644
--- a/test/sql/copy/parquet/parquet_hive_empty.test
+++ b/test/sql/copy/parquet/parquet_hive_empty.test
@@ -13,11 +13,11 @@ a	a
 b	(empty)
 c	NULL
 
-# filter on hive partitioning with NULL values
+# filter on hive partitioning with literal 'NULL' values
 query II
 select *
 from parquet_scan('{DATA_DIR}/parquet-testing/hive-partitioning/empty_string/*/*.parquet')
-WHERE key IS NULL
+WHERE key='NULL'
 ----
 c	NULL
 
diff --git a/test/sql/copy/parquet/parquet_hive_null.test b/test/sql/copy/parquet/parquet_hive_null.test
index 692229a80af8..5e262ac94159 100644
--- a/test/sql/copy/parquet/parquet_hive_null.test
+++ b/test/sql/copy/parquet/parquet_hive_null.test
@@ -66,6 +66,24 @@ ORDER BY ALL
 ----
 1	NULL	NULL
 
+# Test that literal VARCHAR partition value 'NULL' is not read as SQL NULL
+statement ok
+CREATE TABLE test_literal_null_partition AS
+SELECT *
+FROM (VALUES ('NULL', 1), ('ok', 2)) v(p, id);
+
+statement ok
+COPY test_literal_null_partition TO '{TEST_DIR}/hive-literal-null-partition'
+(FORMAT PARQUET, PARTITION_BY (p), OVERWRITE);
+
+query III
+SELECT (p IS NULL)::INTEGER AS is_null, COALESCE((p='NULL')::INTEGER, -1) AS equals_literal, id
+FROM parquet_scan('{TEST_DIR}/hive-literal-null-partition/**/*.parquet', hive_partitioning=1)
+ORDER BY id
+----
+0	1	1
+0	0	2
+
 # Test that NULL values are written as __HIVE_DEFAULT_PARTITION__
 statement ok
 create table test_null_write as select 1 as c, NULL::INT as a, NULL::INT as b;