Repository: ibis-project/ibis

Issue:
bug: schema of replaced view not refreshed on registration 
### What happened?

```python
[ins] In [1]: import ibis

[ins] In [2]: import pandas as pd

[ins] In [3]: df = pd.DataFrame([["a", 1], ["b", 2]], columns=["let", "num"])

[ins] In [4]: con = ibis.duckdb.connect()

[ins] In [5]: con.register(df, table_name="t")
Out[5]: 
AlchemyTable: t
  let string
  num int64

[ins] In [6]: df = pd.DataFrame([["a", "a"], ["b", "b"]], columns=["let", "num"])

[ins] In [7]: con.register(df, table_name="t")
Out[7]: 
AlchemyTable: t
  let string
  num int64

[ins] In [8]: con.tables.t.execute()
Out[8]: 
  let num
0   a   a
1   b   b
```

When we re-register something with the same name, the view in DuckDB is updated, but not the Ibis schema.  For something like the `int->str` example above, the schema is wrong but the result is correct.
If you do `int->float` however, ibis helpfully casts back results as integers (:grimacing:)

Or, if you have extra columns...

```python
[ins] In [9]: df = pd.DataFrame([["a", "a", 2], ["b", "b", 3]], columns=["let", "let2", "num"])

[ins] In [10]: con.register(df, table_name="t")
Out[10]: 
AlchemyTable: t
  let string
  num int64
```


I'm nearly certain this can be fixed with `reflect_table`, i just didn't have time to work up the unit tests right now and didn't want to forget about this.

### What version of ibis are you using?

master

### What backend(s) are you using, if any?

DuckDB

### Relevant log output

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:

Resolving patch:
diff --git a/ibis/backends/base/sql/alchemy/__init__.py b/ibis/backends/base/sql/alchemy/__init__.py
--- a/ibis/backends/base/sql/alchemy/__init__.py
+++ b/ibis/backends/base/sql/alchemy/__init__.py
@@ -387,6 +387,10 @@ def _log(self, sql):
     def _get_sqla_table(
         self, name: str, schema: str | None = None, autoload: bool = True, **kwargs: Any
     ) -> sa.Table:
+        # If the underlying table (or more likely, view) has changed, remove it
+        # to ensure a correct reflection
+        if autoload and self.inspector.has_table(name):
+            self.meta.remove(sa.Table(name, self.meta))
         with warnings.catch_warnings():
             warnings.filterwarnings(
                 "ignore", message="Did not recognize type", category=sa.exc.SAWarning


Test patch:
diff --git a/ibis/backends/duckdb/tests/test_register.py b/ibis/backends/duckdb/tests/test_register.py
--- a/ibis/backends/duckdb/tests/test_register.py
+++ b/ibis/backends/duckdb/tests/test_register.py
@@ -124,6 +124,21 @@ def test_read_in_memory():
     assert "df_pandas" in con.list_tables()
 
 
+def test_re_read_in_memory_overwrite():
+    con = ibis.duckdb.connect()
+
+    df_pandas_1 = pd.DataFrame({"a": ["a"], "b": [1], "d": ["hi"]})
+    df_pandas_2 = pd.DataFrame({"a": [1], "c": [1.4]})
+
+    table = con.read_in_memory(df_pandas_1, table_name="df")
+    assert len(table.columns) == 3
+    assert table.schema() == ibis.schema([("a", "str"), ("b", "int"), ("d", "str")])
+
+    table = con.read_in_memory(df_pandas_2, table_name="df")
+    assert len(table.columns) == 2
+    assert table.schema() == ibis.schema([("a", "int"), ("c", "float")])
+
+
 def test_memtable_with_nullable_dtypes():
     data = pd.DataFrame(
         {
diff --git a/ibis/backends/tests/test_register.py b/ibis/backends/tests/test_register.py
--- a/ibis/backends/tests/test_register.py
+++ b/ibis/backends/tests/test_register.py
@@ -329,7 +329,9 @@ def test_csv_reregister_schema(con, tmp_path):
         )
 
     # For a full file scan, expect correct schema based on final row
-    foo_table = con.register(foo)
+    # We also use the same `table_name` for both tests to ensure that
+    # the table is re-reflected in sqlalchemy
+    foo_table = con.register(foo, table_name="same")
     result_schema = foo_table.schema()
 
     assert result_schema.names == ("cola", "colb", "colc")
@@ -339,7 +341,7 @@ def test_csv_reregister_schema(con, tmp_path):
 
     # If file scan is limited to first two rows, should be all some kind of integer.
     # The specific type isn't so important, and may vary across backends/versions
-    foo_table = con.register(foo, SAMPLE_SIZE=2)
+    foo_table = con.register(foo, SAMPLE_SIZE=2, table_name="same")
     result_schema = foo_table.schema()
     assert result_schema.names == ("cola", "colb", "colc")
     assert result_schema["cola"].is_integer()