Repository: duckdb/duckdb (language: python)

Issue:
`json_extract` Fails with Python 3.11.9 but Works with Python 3.11.0  
### What happens?

When running the same DuckDB query using Python 3.11.0 and Python 3.11.9, `json_extract` produces inconsistent results. Specifically, in Python 3.11.9, `json_extract('data', '/')` and `json_extract(t.test, '/')` both return `NULL`, while they work correctly in Python 3.11.0.  


### To Reproduce

**To Reproduce:**  

Run the following Python script using both Python 3.11.0 and 3.11.9:  

```python
import duckdb

query = duckdb.query("""
WITH t AS (
    SELECT '{ "hello": 1 }' AS test
)
SELECT
    t.test::json AS data,
    json_extract('data', '/') AS literal_extract,
    json_extract(t.test, '/') AS column_extract
FROM t;
""")
print(query)

```

**Observed Behavior:**  

**With Python 3.11.0:**  

literal_extract: {"hello": 1}
column_extract: {"hello": 1} 


**With Python 3.11.9:**  

literal_extract: `NULL`
column_extract: `NULL`

### OS:

WINDOWS 10,  x86_64

### DuckDB Version:

1.1.3

### DuckDB Client:

Python

### Hardware:

Python Versions: 3.11.0 and 3.11.9

### Full Name:

Rowan Farquharson

### Affiliation:

MAIT DEV

### 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:
Thanks for the report, can you run this on both python versions?

```py
import duckdb

duckdb.sql("pragma version;").show()
```

I suspect these are not using the same duckdb version, as I don't see anything python-client specific here

Fix patch:
diff --git a/extension/json/include/json_common.hpp b/extension/json/include/json_common.hpp
index 1387bf4f5d96..1da6088ffb4b 100644
--- a/extension/json/include/json_common.hpp
+++ b/extension/json/include/json_common.hpp
@@ -283,7 +283,7 @@ struct JSONCommon {
 	//! Get JSON value using JSON path query (unsafe)
 	static inline yyjson_val *GetUnsafe(yyjson_val *val, const char *ptr, const idx_t &len) {
 		if (len == 0) {
-			return nullptr;
+			return val;
 		}
 		switch (*ptr) {
 		case '/':
diff --git a/extension/json/json_functions.cpp b/extension/json/json_functions.cpp
index 2b8b7828e17c..83aebb6a7991 100644
--- a/extension/json/json_functions.cpp
+++ b/extension/json/json_functions.cpp
@@ -22,24 +22,22 @@ static JSONPathType CheckPath(const Value &path_val, string &path, size_t &len)
 	auto path_str = path_str_val.GetValueUnsafe<string_t>();
 	len = path_str.GetSize();
 	const auto ptr = path_str.GetData();
-	// Empty strings and invalid $ paths yield an error
-	if (len == 0) {
-		throw BinderException("Empty JSON path");
-	}
 	JSONPathType path_type = JSONPathType::REGULAR;
 	// Copy over string to the bind data
-	if (*ptr == '/' || *ptr == '$') {
-		path = string(ptr, len);
-	} else if (path_val.type().IsIntegral()) {
-		path = "$[" + string(ptr, len) + "]";
-	} else if (memchr(ptr, '"', len)) {
-		path = "/" + string(ptr, len);
-	} else {
-		path = "$.\"" + string(ptr, len) + "\"";
-	}
-	len = path.length();
-	if (*path.c_str() == '$') {
-		path_type = JSONCommon::ValidatePath(path.c_str(), len, true);
+	if (len != 0) {
+		if (*ptr == '/' || *ptr == '$') {
+			path = string(ptr, len);
+		} else if (path_val.type().IsIntegral()) {
+			path = "$[" + string(ptr, len) + "]";
+		} else if (memchr(ptr, '"', len)) {
+			path = "/" + string(ptr, len);
+		} else {
+			path = "$.\"" + string(ptr, len) + "\"";
+		}
+		len = path.length();
+		if (*path.c_str() == '$') {
+			path_type = JSONCommon::ValidatePath(path.c_str(), len, true);
+		}
 	}
 	return path_type;
 }
diff --git a/extension/json/json_functions/json_create.cpp b/extension/json/json_functions/json_create.cpp
index 8c587fd0eccd..29f99cc01528 100644
--- a/extension/json/json_functions/json_create.cpp
+++ b/extension/json/json_functions/json_create.cpp
@@ -688,9 +688,22 @@ static void ToJSONFunctionInternal(const StructNames &names, Vector &input, cons
 	CreateValues(names, doc, vals, input, count);
 
 	// Write JSON values to string
-	UnaryExecutor::ExecuteWithNulls<data_t, string_t>(input, result, count, [&](data_t, ValidityMask &, idx_t index) {
-		return JSONCommon::WriteVal<yyjson_mut_val>(vals[index], alc);
-	});
+	auto objects = FlatVector::GetData<string_t>(result);
+	auto &result_validity = FlatVector::Validity(result);
+	UnifiedVectorFormat input_data;
+	input.ToUnifiedFormat(count, input_data);
+	for (idx_t i = 0; i < count; i++) {
+		idx_t idx = input_data.sel->get_index(i);
+		if (input_data.validity.RowIsValid(idx)) {
+			objects[i] = JSONCommon::WriteVal<yyjson_mut_val>(vals[i], alc);
+		} else {
+			result_validity.SetInvalid(i);
+		}
+	}
+
+	if (input.GetVectorType() == VectorType::CONSTANT_VECTOR || count == 1) {
+		result.SetVectorType(VectorType::CONSTANT_VECTOR);
+	}
 }
 
 static void ToJSONFunction(DataChunk &args, ExpressionState &state, Vector &result) {


Test patch:
diff --git a/test/sql/json/scalar/test_json_extract.test b/test/sql/json/scalar/test_json_extract.test
index 460ebe2d1bf6..98d712ce1d82 100644
--- a/test/sql/json/scalar/test_json_extract.test
+++ b/test/sql/json/scalar/test_json_extract.test
@@ -364,3 +364,11 @@ query I
 select '{"duck":null}'->>'$.duck'
 ----
 NULL
+
+# issue 15217 (we should be able to extract the root using JSONPointer)
+# the issue is wrong - using '/' should extract the field with an empty string as key
+# but we should still be able to extract the root using a completely empty string
+query I
+select json_extract('{"hello":1}', '')
+----
+{"hello":1}
diff --git a/test/sql/json/scalar/test_json_path.test b/test/sql/json/scalar/test_json_path.test
index 05301e1ef057..acd0b98f16e6 100644
--- a/test/sql/json/scalar/test_json_path.test
+++ b/test/sql/json/scalar/test_json_path.test
@@ -7,10 +7,11 @@ require json
 statement ok
 pragma enable_verification
 
-statement error
+# this used to error but the json pointer spec says we should be able to extract the root using the empty string
+query I
 select json_extract('{"my_field": "duck"}', '')
 ----
-Binder Error
+{"my_field":"duck"}
 
 query T
 select json_extract('{"my_field": "duck"}', '$.my_field')