Repository: duckdb/duckdb (2026-07-21T08:53:05Z)

Issue:
Parquet reader doesn't prune row group with non-existent UUID
### What happens?

See repro for details.

### To Reproduce

```sql
memory D COPY (
             SELECT
                 printf(
                     '00000000-0000-0000-0000-%012d',
                     (i % 100) * 2
                 )::UUID AS u,
                 i::BIGINT AS payload
             FROM range(20480) AS t(i)
         )
         TO '/tmp/pruning_types.parquet'
         (
             FORMAT PARQUET,
             ROW_GROUP_SIZE 2048,
             DICTIONARY_SIZE_LIMIT 1000,
             WRITE_BLOOM_FILTER TRUE,
             OVERWRITE TRUE
         );

-- shouldn't read any row groups
memory D EXPLAIN ANALYZE
         SELECT sum(payload)
         FROM read_parquet('/tmp/pruning_types.parquet')
         WHERE u = '00000000-0000-0000-0000-000000000101'::UUID;
╭─ Summary ───────────╮
│ Total Time: 0.0070s │
│ Data Read: 108.0 kB │
╰─────────────────────╯
╭──────────────────────────────────────────────────╮
│ Ungrouped Aggregate                  1 row · 0µs │
│ Projection                          0 rows · 0µs │
╰─────────────────────────┬────────────────────────╯
╭─ Table Scan ────────────┴────────────────────────╮
│ Function: READ_PARQUET                           │
│ Projections: payload                             │
│ Filters:                                         │
│ u = '00000000-0000-0000-0000-000000000101'::UUID │
│ Total Files Read: 1                              │
│ Filename(s): /tmp/pruning_types.parquet          │
│ Row Groups Scanned: 10 / 10                      │
│ 0 rows                                    20.0ms │
╰──────────────────────────────────────────────────╯
```

### OS:

macos

### DuckDB Version:

9ebdd1ee5279885dd2a89d4ac8f37034c05de203

### 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/extension/parquet/parquet_statistics.cpp b/extension/parquet/parquet_statistics.cpp
index c4decb58d2a0..87169a0cdf91 100644
--- a/extension/parquet/parquet_statistics.cpp
+++ b/extension/parquet/parquet_statistics.cpp
@@ -13,6 +13,7 @@
 #include "zstd/common/xxhash.hpp"
 #include "duckdb/common/types/blob.hpp"
 #include "duckdb/common/types/time.hpp"
+#include "duckdb/common/types/uuid.hpp"
 #include "duckdb/common/types/value.hpp"
 #include "duckdb/common/helper.hpp"
 #include "duckdb/storage/statistics/struct_stats.hpp"
@@ -804,8 +805,13 @@ static optional<uint64_t> TryHashTimestamp(const Value &constant, const ParquetC
 // TODO TEST THIS!
 // TODO perhaps we can re-use some writer infra here
 static optional<uint64_t> ValueXXH64(const Value &constant, const ParquetColumnSchema &schema) {
-	// Handle time-related types.
+	// Handle logical types whose Parquet representation needs special hashing.
 	switch (constant.type().id()) {
+	case LogicalTypeId::UUID: {
+		data_t bytes[16];
+		BaseUUID::ToBlob(constant.GetValue<hugeint_t>(), bytes);
+		return duckdb_zstd::XXH64(bytes, sizeof(bytes), 0);
+	}
 	case LogicalTypeId::TIME:
 	case LogicalTypeId::TIME_NS:
 	case LogicalTypeId::TIME_TZ:
@@ -900,6 +906,8 @@ bool ParquetStatisticsUtils::BloomFilterSupported(const ParquetColumnSchema &sch
 	case LogicalTypeId::BLOB:
 	case LogicalTypeId::DATE:
 		return true;
+	case LogicalTypeId::UUID:
+		return schema.parquet_type == duckdb_parquet::Type::FIXED_LEN_BYTE_ARRAY && schema.type_length == 16;
 	case LogicalTypeId::TIMESTAMP:
 	case LogicalTypeId::TIMESTAMP_TZ:
 		// When type info is UNIT_NS, DuckDB type TIMESTAMP_NS/TIMESTAMP_TZ_NS is used.


Test patch:
diff --git a/test/sql/copy/parquet/bloom_filters.test b/test/sql/copy/parquet/bloom_filters.test
index 8f80c3ce984e..cc2882f5e489 100644
--- a/test/sql/copy/parquet/bloom_filters.test
+++ b/test/sql/copy/parquet/bloom_filters.test
@@ -38,6 +38,45 @@ SELECT BOOL_AND(bloom_filter_excludes) FROM parquet_bloom_probe('{TEST_DIR}/bloo
 ----
 true
 
+statement ok
+COPY (
+    SELECT
+        printf('00000000-0000-0000-0000-%012d', (i % 100) * 2)::UUID AS u,
+        i::BIGINT AS payload
+    FROM range(20480) t(i)
+) TO '{TEST_DIR}/bloom_uuid.parquet' (
+    FORMAT PARQUET,
+    ROW_GROUP_SIZE 2048,
+    DICTIONARY_SIZE_LIMIT 1000,
+    WRITE_BLOOM_FILTER TRUE
+);
+
+# The UUID is absent from every row group but falls within every min/max range
+query I
+SELECT BOOL_AND(bloom_filter_excludes)
+FROM parquet_bloom_probe(
+    '{TEST_DIR}/bloom_uuid.parquet',
+    'u',
+    '00000000-0000-0000-0000-000000000101'
+);
+----
+true
+
+query II
+EXPLAIN ANALYZE
+SELECT sum(payload)
+FROM read_parquet('{TEST_DIR}/bloom_uuid.parquet')
+WHERE u = '00000000-0000-0000-0000-000000000101'::UUID;
+----
+analyzed_plan	<REGEX>:(?s).*Row Groups Scanned: 0 / 10.*
+
+query I
+SELECT count(*)
+FROM read_parquet('{TEST_DIR}/bloom_uuid.parquet')
+WHERE u = '00000000-0000-0000-0000-000000000101'::UUID;
+----
+0
+
 statement ok
 CREATE MACRO assert_bloom_filter_hit(file, col, val) AS TABLE
     SELECT COUNT(*) > 0 AND COUNT(*) < MAX(row_group_id+1) FROM parquet_bloom_probe(file, col, val) WHERE NOT bloom_filter_excludes;