Repository: duckdb/duckdb (language: python)

Issue:
[Python Arrow Scan] cannot join blob columns
### What happens?

when you join by binary column you get an error, duckdb v1.1.1.  works in v1.0.0


### To Reproduce

```
import duckdb
import pyarrow as pa
import hashlib
my_table = pa.Table.from_pydict({"foo": pa.array([hashlib.sha256("foo".encode()).digest()], type=pa.binary())})
my_table2 = pa.Table.from_pydict({"foo": pa.array([hashlib.sha256("foo".encode()).digest()], type=pa.binary()),"a": ["123"]})
duckdb.sql(f"""SELECT my_table2.* EXCLUDE (foo) FROM my_table LEFT JOIN my_table2 USING (foo)""").show()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
duckdb.duckdb.Error: UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb4 in position 2: invalid start byte
```

### OS:

mac

### DuckDB Version:

1..1.1

### DuckDB Client:

Python

### Hardware:

_No response_

### Full Name:

Jack Zhao

### Affiliation:

Delphina.ai

### 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:

Fix patch:
diff --git a/src/include/duckdb/planner/table_filter.hpp b/src/include/duckdb/planner/table_filter.hpp
index ab4d754c40ea..edcb975cd26a 100644
--- a/src/include/duckdb/planner/table_filter.hpp
+++ b/src/include/duckdb/planner/table_filter.hpp
@@ -45,6 +45,7 @@ class TableFilter {
 	//! Returns true if the statistics indicate that the segment can contain values that satisfy that filter
 	virtual FilterPropagateResult CheckStatistics(BaseStatistics &stats) = 0;
 	virtual string ToString(const string &column_name) = 0;
+	string DebugToString();
 	virtual unique_ptr<TableFilter> Copy() const = 0;
 	virtual bool Equals(const TableFilter &other) const {
 		return filter_type != other.filter_type;
diff --git a/src/planner/table_filter.cpp b/src/planner/table_filter.cpp
index 48af721a8020..636a7671d253 100644
--- a/src/planner/table_filter.cpp
+++ b/src/planner/table_filter.cpp
@@ -26,6 +26,10 @@ void TableFilterSet::PushFilter(idx_t column_index, unique_ptr<TableFilter> filt
 	}
 }
 
+string TableFilter::DebugToString() {
+	return ToString("c0");
+}
+
 void DynamicTableFilterSet::ClearFilters(const PhysicalOperator &op) {
 	lock_guard<mutex> l(lock);
 	filters.erase(op);
diff --git a/tools/pythonpkg/src/arrow/arrow_array_stream.cpp b/tools/pythonpkg/src/arrow/arrow_array_stream.cpp
index 8a78927aeea2..31b24e86086d 100644
--- a/tools/pythonpkg/src/arrow/arrow_array_stream.cpp
+++ b/tools/pythonpkg/src/arrow/arrow_array_stream.cpp
@@ -272,7 +272,7 @@ py::object GetScalar(Value &constant, const string &timezone_config, const Arrow
 	case LogicalTypeId::VARCHAR:
 		return dataset_scalar(constant.ToString());
 	case LogicalTypeId::BLOB:
-		return dataset_scalar(constant.GetValueUnsafe<string>());
+		return dataset_scalar(py::bytes(constant.GetValueUnsafe<string>()));
 	case LogicalTypeId::DECIMAL: {
 		py::object date_type = py::module_::import("pyarrow").attr("decimal128");
 		uint8_t width;


Test patch:
diff --git a/tools/pythonpkg/tests/fast/arrow/test_14344.py b/tools/pythonpkg/tests/fast/arrow/test_14344.py
new file mode 100644
index 000000000000..522228c0fdb2
--- /dev/null
+++ b/tools/pythonpkg/tests/fast/arrow/test_14344.py
@@ -0,0 +1,25 @@
+import duckdb
+import pytest
+
+pa = pytest.importorskip("pyarrow")
+import hashlib
+
+
+def test_14344(duckdb_cursor):
+    my_table = pa.Table.from_pydict({"foo": pa.array([hashlib.sha256("foo".encode()).digest()], type=pa.binary())})
+    my_table2 = pa.Table.from_pydict(
+        {"foo": pa.array([hashlib.sha256("foo".encode()).digest()], type=pa.binary()), "a": ["123"]}
+    )
+
+    res = duckdb_cursor.sql(
+        f"""
+		SELECT
+			my_table2.* EXCLUDE (foo)
+		FROM
+			my_table
+		LEFT JOIN
+			my_table2
+		USING (foo)
+	"""
+    ).fetchall()
+    assert res == [('123',)]