Repository: duckdb/duckdb (language: python)

Issue:
"out of range" error when reading parquet with unsigned tinyint or unsigned smallint
### What happens?

"out of range" error when reading parquet with unsigned tinyint (unsigned int8) or unsigned smallint (unsigned int16).  

Error message for tinyint:
```
Invalid Input Error: Failed to cast value: Type UINT32 with value 4294967295 can't be cast because the value is out of range for the destination type UINT8
```

Error message for smallint:
```
Invalid Input Error: Failed to cast value: Type UINT32 with value 4294967295 can't be cast because the value is out of range for the destination type UINT16
```

### To Reproduce

Create a **parquet** file **containing at least 1 row** with this **schema**:
```
message schema {
  OPTIONAL INT32 hcol0 (INTEGER(8,false));
}
```
or
```
message schema {
  OPTIONAL INT32 hcol0 (INTEGER(16,false));
}
```

Use Duckdb to query the parquet:
```sql
select hcol0 from READ_PARQUET('data.parquet');
```

Example parquet files (the extension is put as `.txt` to be able to upload here)
[utinyint.txt](https://github.com/user-attachments/files/18060837/utinyint.txt)
[usmallint.txt](https://github.com/user-attachments/files/18060836/usmallint.txt)

```sql
select hcol0 from read_parquet('utinyint.txt');
select hcol0 from read_parquet('usmallint.txt');
```

### OS:

linux

### DuckDB Version:

1.1.3

### DuckDB Client:

CLI

### Hardware:

_No response_

### Full Name:

Dat Bui

### Affiliation:

holistics.io

### What is the latest build you tested with? If possible, we recommend testing with the latest nightly build.

I have tested with a stable release

### 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
"out of range" error when reading parquet with unsigned tinyint or unsigned smallint
### What happens?

"out of range" error when reading parquet with unsigned tinyint (unsigned int8) or unsigned smallint (unsigned int16).  

Error message for tinyint:
```
Invalid Input Error: Failed to cast value: Type UINT32 with value 4294967295 can't be cast because the value is out of range for the destination type UINT8
```

Error message for smallint:
```
Invalid Input Error: Failed to cast value: Type UINT32 with value 4294967295 can't be cast because the value is out of range for the destination type UINT16
```

### To Reproduce

Create a **parquet** file **containing at least 1 row** with this **schema**:
```
message schema {
  OPTIONAL INT32 hcol0 (INTEGER(8,false));
}
```
or
```
message schema {
  OPTIONAL INT32 hcol0 (INTEGER(16,false));
}
```

Use Duckdb to query the parquet:
```sql
select hcol0 from READ_PARQUET('data.parquet');
```

Example parquet files (the extension is put as `.txt` to be able to upload here)
[utinyint.txt](https://github.com/user-attachments/files/18060837/utinyint.txt)
[usmallint.txt](https://github.com/user-attachments/files/18060836/usmallint.txt)

```sql
select hcol0 from read_parquet('utinyint.txt');
select hcol0 from read_parquet('usmallint.txt');
```

### OS:

linux

### DuckDB Version:

1.1.3

### DuckDB Client:

CLI

### Hardware:

_No response_

### Full Name:

Dat Bui

### Affiliation:

holistics.io

### What is the latest build you tested with? If possible, we recommend testing with the latest nightly build.

I have tested with a stable release

### 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:
also reproducible with duckdb cli `v1.1.4-dev2261 ed90e384ef`
Thanks, I could reproduce the issue!
also reproducible with duckdb cli `v1.1.4-dev2261 ed90e384ef`
Thanks, I could reproduce the issue!

Fix patch:
diff --git a/extension/parquet/include/parquet_statistics.hpp b/extension/parquet/include/parquet_statistics.hpp
index d4e0a4a4d0be..ad1f939c8ef6 100644
--- a/extension/parquet/include/parquet_statistics.hpp
+++ b/extension/parquet/include/parquet_statistics.hpp
@@ -35,6 +35,10 @@ struct ParquetStatisticsUtils {
 
 	static bool BloomFilterExcludes(const TableFilter &filter, const duckdb_parquet::ColumnMetaData &column_meta_data,
 	                                duckdb_apache::thrift::protocol::TProtocol &file_proto, Allocator &allocator);
+
+private:
+	static Value ConvertValueInternal(const LogicalType &type, const duckdb_parquet::SchemaElement &schema_ele,
+	                                  const std::string &stats);
 };
 
 class ParquetBloomFilter {
diff --git a/extension/parquet/parquet_statistics.cpp b/extension/parquet/parquet_statistics.cpp
index 477bda0a574d..b20510be9408 100644
--- a/extension/parquet/parquet_statistics.cpp
+++ b/extension/parquet/parquet_statistics.cpp
@@ -30,16 +30,16 @@ static unique_ptr<BaseStatistics> CreateNumericStats(const LogicalType &type,
 	Value min;
 	Value max;
 	if (parquet_stats.__isset.min_value) {
-		min = ParquetStatisticsUtils::ConvertValue(type, schema_ele, parquet_stats.min_value).DefaultCastAs(type);
+		min = ParquetStatisticsUtils::ConvertValue(type, schema_ele, parquet_stats.min_value);
 	} else if (parquet_stats.__isset.min) {
-		min = ParquetStatisticsUtils::ConvertValue(type, schema_ele, parquet_stats.min).DefaultCastAs(type);
+		min = ParquetStatisticsUtils::ConvertValue(type, schema_ele, parquet_stats.min);
 	} else {
 		min = Value(type);
 	}
 	if (parquet_stats.__isset.max_value) {
-		max = ParquetStatisticsUtils::ConvertValue(type, schema_ele, parquet_stats.max_value).DefaultCastAs(type);
+		max = ParquetStatisticsUtils::ConvertValue(type, schema_ele, parquet_stats.max_value);
 	} else if (parquet_stats.__isset.max) {
-		max = ParquetStatisticsUtils::ConvertValue(type, schema_ele, parquet_stats.max).DefaultCastAs(type);
+		max = ParquetStatisticsUtils::ConvertValue(type, schema_ele, parquet_stats.max);
 	} else {
 		max = Value(type);
 	}
@@ -50,6 +50,17 @@ static unique_ptr<BaseStatistics> CreateNumericStats(const LogicalType &type,
 
 Value ParquetStatisticsUtils::ConvertValue(const LogicalType &type, const duckdb_parquet::SchemaElement &schema_ele,
                                            const std::string &stats) {
+	Value result;
+	string error;
+	auto stats_val = ConvertValueInternal(type, schema_ele, stats);
+	if (!stats_val.DefaultTryCastAs(type, result, &error)) {
+		return Value(type);
+	}
+	return result;
+}
+Value ParquetStatisticsUtils::ConvertValueInternal(const LogicalType &type,
+                                                   const duckdb_parquet::SchemaElement &schema_ele,
+                                                   const std::string &stats) {
 	auto stats_data = const_data_ptr_cast(stats.c_str());
 	switch (type.id()) {
 	case LogicalTypeId::BOOLEAN: {


Test patch:
diff --git a/data/parquet-testing/out_of_range_stats.parquet b/data/parquet-testing/out_of_range_stats.parquet
new file mode 100644
index 000000000000..3c3be2cb7f11
Binary files /dev/null and b/data/parquet-testing/out_of_range_stats.parquet differ
diff --git a/test/sql/copy/parquet/parquet_corrupt_stats.test b/test/sql/copy/parquet/parquet_corrupt_stats.test
new file mode 100644
index 000000000000..fb5a5362dfcd
--- /dev/null
+++ b/test/sql/copy/parquet/parquet_corrupt_stats.test
@@ -0,0 +1,10 @@
+# name: test/sql/copy/parquet/parquet_corrupt_stats.test
+# description: Test reading a Parquet file with stats that are out-of-range of the type
+# group: [parquet]
+
+require parquet
+
+query I
+FROM 'data/parquet-testing/out_of_range_stats.parquet'
+----
+255