Repository: duckdb/duckdb (language: python)

Issue:
Regression: Replacement Scans Misbehave in v1.1
### What happens?

When importing functions that use `duckdb.sql("...")` from modules, they don't "see" the namespace of in-memory dataframes anymore.

This was working fine in v1.0.

### To Reproduce

## 1) create file `ddb.py` defining a query string with a method to run a DuckDB query:

```python
%%writefile ddb.py
import duckdb
print(f"DuckDB v{duckdb.__version__}")
class Q_IMPORTED(str):
    def run(self):
        return duckdb.sql(self)
```

## 2) create another version of a query string locally, a sample dataframe, use imported and local query on a dataframe:
```
import duckdb
import pandas as pd
df = pd.DataFrame({"a": [42]})

from ddb import Q_IMPORTED

class Q_LOCAL(str):
    def run(self):
        return duckdb.sql(self)

try:
    Q("FROM df").run()
except Exception as e:
    print("Q from module", e)

Q_LOCAL("FROM df").run()
```

```
import duckdb
import pandas as pd
df = pd.DataFrame({"a": [42]})

from ddb import Q_IMPORTED

class Q_LOCAL(str):
    def run(self):
        return duckdb.sql(self)

try:
    df_from_imported = Q_IMPORTED("FROM df").run().df()
except Exception as e:
    print("Q from module", e)
    df_from_imported = None

df_from_local = Q_LOCAL("FROM df").run().df()
pd.testing.assert_frame_equal(df_from_imported, df_from_local)
```

### output in v1.0.0
```
DuckDB v1.0.0
```


### Output in v1.1.1
```
DuckDB v1.1.1
Q from module Catalog Error: Table with name df does not exist!
Did you mean "pg_depend"?
---------------------------------------------------------------------------
AssertionError 
```

### OS:

Linux

### DuckDB Version:

1.1

### DuckDB Client:

python

### Hardware:

_No response_

### Full Name:

Alex

### Affiliation:

self

### 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:
Likely duplicate of #13836 
Looks like it.  Similar behavior: works in the same environment but not from outside.

> Previously we would scan all stack frames in an effort to find a suitable replacement, now we limit it to only the current frame (which is also what Python itself does)
 - #12425 

So how do you work around it?
 `set python_scan_all_frames=true` to get the old, and frankly, worse behavior
Thanks, that's useful.  I have a substantial number of reports where I'm building data stepwise starting with a non-DuckDB query to MSSQL followed by DuckDB queries to other resources; I'd either have to pin to 1.0 or revise. 

FWIW, I've never had issues with the old behavior.
> `set python_scan_all_frames=true` to get the old, and frankly, worse behavior

Actually, further down the line stuff is breaking with 
`AttributeError: 'NoneType' object has no attribute 'f_locals'`; this piece was introduced in this PR
https://github.com/duckdb/duckdb/pull/13896

Fix patch:
diff --git a/tools/pythonpkg/src/python_replacement_scan.cpp b/tools/pythonpkg/src/python_replacement_scan.cpp
index 8d1610534789..65eb8c009329 100644
--- a/tools/pythonpkg/src/python_replacement_scan.cpp
+++ b/tools/pythonpkg/src/python_replacement_scan.cpp
@@ -225,6 +225,9 @@ static unique_ptr<TableRef> ReplaceInternal(ClientContext &context, const string
 			}
 		}
 		current_frame = current_frame.attr("f_back");
+		if (py::none().is(current_frame)) {
+			break;
+		}
 	} while (scan_all_frames && (has_locals || has_globals));
 	return nullptr;
 }


Test patch:
diff --git a/tools/pythonpkg/tests/fast/test_replacement_scan.py b/tools/pythonpkg/tests/fast/test_replacement_scan.py
index 5255c8cac902..c7287bba3ffe 100644
--- a/tools/pythonpkg/tests/fast/test_replacement_scan.py
+++ b/tools/pythonpkg/tests/fast/test_replacement_scan.py
@@ -165,6 +165,12 @@ def test_replacement_scan_relapi(self):
         assert type(pyrel3) == duckdb.DuckDBPyRelation
         assert pyrel3.fetchall() == [(142,), (184,)]
 
+    def test_replacement_scan_not_found(self):
+        con = duckdb.connect()
+        con.execute("set python_scan_all_frames=true")
+        with pytest.raises(duckdb.CatalogException, match='Table with name non_existant does not exist'):
+            res = con.sql("select * from non_existant").fetchall()
+
     def test_replacement_scan_alias(self):
         con = duckdb.connect()
         pyrel1 = con.query('from (values (1, 2)) t(i, j)')