From fafa966300462605ceaf6acf4048695048604ad8 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 23 Nov 2025 14:51:12 -0800 Subject: [PATCH] Table.create() now stores configuration in _defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When configuration parameters like pk, foreign_keys, not_null, etc. are passed to Table.create(), they are now stored in self._defaults so that subsequent operations on the same Table instance (like insert, upsert) will use those settings automatically. Previously, calling db["table"].insert({...}, pk="id") would create the table correctly but subsequent calls to insert() or upsert() on the same table object would not know about the pk setting, causing issues like upsert() failing with "requires a pk" error. Closes #655 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- sqlite_utils/db.py | 50 ++++++++++++++++++---- tests/test_create.py | 98 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 9 deletions(-) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index dd83b4c..04a4c01 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -1703,19 +1703,19 @@ class Table(Queryable): def create( self, columns: Dict[str, Any], - pk: Optional[Any] = None, - foreign_keys: Optional[ForeignKeysType] = None, - column_order: Optional[List[str]] = None, - not_null: Optional[Iterable[str]] = None, - defaults: Optional[Dict[str, Any]] = None, - hash_id: Optional[str] = None, - hash_id_columns: Optional[Iterable[str]] = None, - extracts: Optional[Union[Dict[str, str], List[str]]] = None, + pk: Optional[Any] = DEFAULT, + foreign_keys: Optional[ForeignKeysType] = DEFAULT, + column_order: Optional[List[str]] = DEFAULT, + not_null: Optional[Iterable[str]] = DEFAULT, + defaults: Optional[Dict[str, Any]] = DEFAULT, + hash_id: Optional[str] = DEFAULT, + hash_id_columns: Optional[Iterable[str]] = DEFAULT, + extracts: Optional[Union[Dict[str, str], List[str]]] = DEFAULT, if_not_exists: bool = False, replace: bool = False, ignore: bool = False, transform: bool = False, - strict: bool = False, + strict: bool = DEFAULT, ) -> "Table": """ Create a table with the specified columns. @@ -1737,6 +1737,38 @@ class Table(Queryable): :param transform: If table already exists transform it to fit the specified schema :param strict: Apply STRICT mode to table """ + # Resolve defaults from _defaults (issue #655) + pk = self.value_or_default("pk", pk) + foreign_keys = self.value_or_default("foreign_keys", foreign_keys) + column_order = self.value_or_default("column_order", column_order) + not_null = self.value_or_default("not_null", not_null) + defaults = self.value_or_default("defaults", defaults) + hash_id = self.value_or_default("hash_id", hash_id) + hash_id_columns = self.value_or_default("hash_id_columns", hash_id_columns) + extracts = self.value_or_default("extracts", extracts) + strict = self.value_or_default("strict", strict) + + # Store configuration in _defaults for subsequent operations (issue #655) + # Don't store pk if hash_id is set, since pk is derived from hash_id in that case + if pk is not None and hash_id is None: + self._defaults["pk"] = pk + if foreign_keys is not None: + self._defaults["foreign_keys"] = foreign_keys + if column_order is not None: + self._defaults["column_order"] = column_order + if not_null is not None: + self._defaults["not_null"] = not_null + if defaults is not None: + self._defaults["defaults"] = defaults + if hash_id is not None: + self._defaults["hash_id"] = hash_id + if hash_id_columns is not None: + self._defaults["hash_id_columns"] = hash_id_columns + if extracts is not None: + self._defaults["extracts"] = extracts + if strict: + self._defaults["strict"] = strict + columns = {name: value for (name, value) in columns.items()} with self.db.conn: self.db.create_table( diff --git a/tests/test_create.py b/tests/test_create.py index 7043ca8..1c45723 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -1380,3 +1380,101 @@ def test_bad_table_and_view_exceptions(fresh_db): with pytest.raises(NoView) as ex2: fresh_db.view("t") assert ex2.value.args[0] == "View t does not exist" + + +# Tests for issue #655: Table configuration should be stored in _defaults +# after table creation, so subsequent operations use the same settings. + + +def test_pk_persists_after_insert_655(fresh_db): + """When pk is passed to insert(), subsequent inserts should use it.""" + table = fresh_db["users"] + table.insert({"id": 1, "name": "Alice"}, pk="id") + # Second insert should use pk="id" from _defaults + table.insert({"id": 2, "name": "Bob"}) + assert table.pks == ["id"] + # Verify both rows exist (not overwritten due to missing pk) + assert table.count == 2 + + +def test_pk_persists_after_insert_all_655(fresh_db): + """When pk is passed to insert_all(), subsequent inserts should use it.""" + table = fresh_db["users"] + table.insert_all([{"id": 1, "name": "Alice"}], pk="id") + # Second insert_all should use pk="id" from _defaults + table.insert_all([{"id": 2, "name": "Bob"}]) + assert table.pks == ["id"] + assert table.count == 2 + + +def test_pk_persists_after_create_655(fresh_db): + """When pk is passed to create(), it should be stored in _defaults.""" + table = fresh_db["users"] + table.create({"id": int, "name": str}, pk="id") + assert table._defaults["pk"] == "id" + # Subsequent insert should use the pk + table.insert({"id": 1, "name": "Alice"}) + table.insert({"id": 2, "name": "Bob"}) + assert table.count == 2 + + +def test_foreign_keys_persist_after_create_655(fresh_db): + """When foreign_keys is passed to create(), it should be stored in _defaults.""" + fresh_db["authors"].insert({"id": 1, "name": "Alice"}, pk="id") + table = fresh_db["books"] + table.create( + {"id": int, "title": str, "author_id": int}, + pk="id", + foreign_keys=[("author_id", "authors", "id")], + ) + assert table._defaults["pk"] == "id" + assert table._defaults["foreign_keys"] == [("author_id", "authors", "id")] + + +def test_not_null_persists_after_create_655(fresh_db): + """When not_null is passed to create(), it should be stored in _defaults.""" + table = fresh_db["users"] + table.create({"id": int, "name": str}, pk="id", not_null=["name"]) + assert table._defaults["not_null"] == ["name"] + + +def test_defaults_persist_after_create_655(fresh_db): + """When defaults is passed to create(), it should be stored in _defaults.""" + table = fresh_db["users"] + table.create({"id": int, "score": int}, pk="id", defaults={"score": 0}) + assert table._defaults["defaults"] == {"score": 0} + + +def test_strict_persists_after_create_655(fresh_db): + """When strict is passed to create(), it should be stored in _defaults.""" + table = fresh_db["users"] + table.create({"id": int, "name": str}, pk="id", strict=True) + assert table._defaults["strict"] == True + + +def test_upsert_uses_pk_from_prior_insert_655(fresh_db): + """After insert with pk, upsert should use the same pk.""" + table = fresh_db["users"] + table.insert({"id": 1, "name": "Alice"}, pk="id") + # Upsert should work without specifying pk again + table.upsert({"id": 1, "name": "Alice Updated"}) + assert table.count == 1 + assert list(table.rows)[0]["name"] == "Alice Updated" + + +def test_upsert_all_uses_pk_from_prior_insert_655(fresh_db): + """After insert with pk, upsert_all should use the same pk.""" + table = fresh_db["users"] + table.insert({"id": 1, "name": "Alice"}, pk="id") + # Upsert_all should work without specifying pk again + table.upsert_all([{"id": 1, "name": "Alice Updated"}, {"id": 2, "name": "Bob"}]) + assert table.count == 2 + rows = {row["id"]: row["name"] for row in table.rows} + assert rows == {1: "Alice Updated", 2: "Bob"} + + +def test_chained_create_sets_pks(fresh_db): + table = fresh_db.table("dogs3", pk="id").create( + {"id": int, "name": str, "color": str} + ) + assert table.pks == ["id"]