fix(python): support exist_ok in local create_table (#4421)

Synchronous local Python `create_table` callers using `mode="exist_ok"`
received `ValueError` instead of creating or opening a table.

Related to [#2900](https://github.com/lancedb/lancedb/issues/2900). The
local wrapper rejected this mode before the shared Rust parser could
validate it. Removing that guard delegates mode validation to the
parser, and the sync and async docs now describe `exist_ok`. Remote
option gaps and the TypeScript table type gap remain outside this
change.

The added tests retain both calls from [@wjones127's
comment](https://github.com/lancedb/lancedb/issues/2900#issuecomment-5964167206).
This builds on the code @wjones127 posted there. Three regression tests
fail before the change, pass after it, fail with the original `db.py`
restored, then pass again on HEAD.

Tested on Linux x86_64 with Python 3.14.7. From the Python directory,
`uv run --no-sync --extra tests --extra dev python -m pytest
python/tests/test_db.py -q` reports 53 passed. `uv run --no-sync --extra
tests --extra dev python -m pytest python/tests -q -rs -m "not slow and
not s3_test"` reports 1428 passed, 34 skipped, 68 deselected, and one
failure: the existing `test_get_lsm_write_spec`, also failing on clean
base (1425 passed, one failed). Ruff 0.9.9 `ruff format --check .` and
`ruff check .` pass. Tests used worktree Python code with the released
`lancedb==0.40.0b12` native wheel. No native source build was run.

Co-authored-by: Will Jones <willjones127@gmail.com>
This commit is contained in:
Chi-Wei Lee
2026-10-03 10:12:29 -05:00
committed by GitHub
co-authored by Will Jones
parent 9c3e1cbae7
commit 0be3ae960e
2 changed files with 39 additions and 5 deletions
+5 -5
View File
@@ -409,9 +409,10 @@ class DBConnection(EnforceOverrides):
- [LanceModel][lancedb.pydantic.LanceModel]
mode: str; default "create"
The mode to use when creating the table.
Can be either "create" or "overwrite".
Can be "create", "overwrite", or "exist_ok".
By default, if the table already exists, an exception is raised.
If you want to overwrite the table, use mode="overwrite".
To open an existing table without adding data, use mode="exist_ok".
exist_ok: bool, default False
If a table by the same name already exists, then raise an exception
if exist_ok=False. If exist_ok=True, then open the existing table;
@@ -1546,8 +1547,6 @@ class LanceDBConnection(DBConnection):
"""
if namespace_path is None:
namespace_path = []
if mode.lower() not in ["create", "overwrite"]:
raise ValueError("mode must be either 'create' or 'overwrite'")
validate_table_name(name)
tbl = LanceTable.create(
@@ -2350,11 +2349,12 @@ class AsyncConnection(object):
- pyarrow.Schema
- [LanceModel][lancedb.pydantic.LanceModel]
mode: Literal["create", "overwrite"]; default "create"
mode: Literal["create", "overwrite", "exist_ok"]; default "create"
The mode to use when creating the table.
Can be either "create" or "overwrite".
Can be "create", "overwrite", or "exist_ok".
By default, if the table already exists, an exception is raised.
If you want to overwrite the table, use mode="overwrite".
To open an existing table without adding data, use mode="exist_ok".
exist_ok: bool, default False
If a table by the same name already exists, then raise an exception
if exist_ok=False. If exist_ok=True, then open the existing table;
+34
View File
@@ -351,6 +351,40 @@ async def test_create_table_from_iterator_async(mem_db_async: lancedb.AsyncConne
assert await table.count_rows() == 10
def test_create_exist_ok_mode(tmp_db: lancedb.DBConnection):
db = tmp_db
data = [{"vector": [1.1, 1.2]}, {"vector": [0.2, 1.8]}]
table = db.create_table("t1", data, mode="exist_ok")
assert table.count_rows() == 2
original = table.to_arrow()
reopened = db.create_table("t1", [{"vector": [9.0, 8.0]}], mode="exist_ok")
assert reopened.to_arrow().equals(original)
def test_create_empty_exist_ok_mode(tmp_db: lancedb.DBConnection):
schema = pa.schema([pa.field("id", pa.int64())])
table = tmp_db.create_table("empty", schema=schema, mode="exist_ok")
assert table.schema == schema
assert table.count_rows() == 0
table.add([{"id": 7}])
reopened = tmp_db.create_table("empty", schema=schema, mode="exist_ok")
assert reopened.to_arrow().to_pylist() == [{"id": 7}]
bad_schema = pa.schema([pa.field("other", pa.int64())])
with pytest.raises(ValueError):
tmp_db.create_table("empty", schema=bad_schema, mode="exist_ok")
def test_create_table_invalid_mode(tmp_db: lancedb.DBConnection):
db = tmp_db
data = [{"vector": [1.1, 1.2]}, {"vector": [0.2, 1.8]}]
with pytest.raises(ValueError, match="Invalid mode bogus"):
db.create_table("tb", data, mode="bogus")
assert "tb" not in db
def test_create_exist_ok(tmp_db: lancedb.DBConnection):
from conftest import pandas_string_type