sqlite-utils icon indicating copy to clipboard operation
sqlite-utils copied to clipboard

Whether or not a table remembers or detects its primary key is confusing and messy

Open simonw opened this issue 6 months ago • 4 comments

Creating a table should stash its settings in self._defaults?

I bodged around this in:

  • #653

See this snippet of suggestion from Gemini 2.5 Flash: https://gist.github.com/simonw/d867a1791b40b3a5fbfb66b75417c0a3#response-2

But really what should happen is that this code here: https://github.com/simonw/sqlite-utils/blob/72f6c820f683ba4bd312679f5302d92e75254a53/sqlite_utils/db.py#L1695-L1713

Should record ALL of the interesting things on self._defaults.

This matters because if you do table = db.table("name_of_table") (where that table does not exist yet) and then call table.insert({...}, pk="id", ...) the table gets created but self._defaults is not updated, so future methods on that table don't know what the settings are.

This differs from if you do:

table = db.insert({...}, pk="id")

Because in that case the returned table DOES know it settings (I think, maybe because it can introspect them? Not 100% sure.)

simonw avatar May 08 '25 23:05 simonw

I'm not 100% sure I'm right about all of this, needs more research before rewriting any code.

simonw avatar May 08 '25 23:05 simonw

... I ended up NOT landing that bodged fix, but I'm leaving this issue open because I think there still may be an improvement to be made here.

When a table is created by a .insert() call it would be good if that table object "remembered" the new settings that it was created with. I think. Unless that's unnecessary because introspection works fine already?

simonw avatar May 09 '25 03:05 simonw

OK this is a mess. In this example the .upsert() fails to detect the primary key that exists on the table:

>>> db = sqlite_utils.Database(memory=True)
>>> t = db.table("dogs").create({"id": int, "name": str, "color": str}, pk="id")
>>> t.pk
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Table' object has no attribute 'pk'. Did you mean: 'pks'?
>>> t.pks
['id']
>>> t.upsert({"id": 1, "name": "Cleo"})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/simon/.local/share/virtualenvs/sqlite-chronicle-jIdutuif/lib/python3.10/site-packages/sqlite_utils/db.py", line 3475, in upsert
    return self.upsert_all(
  File "/Users/simon/.local/share/virtualenvs/sqlite-chronicle-jIdutuif/lib/python3.10/site-packages/sqlite_utils/db.py", line 3512, in upsert_all
    return self.insert_all(
  File "/Users/simon/.local/share/virtualenvs/sqlite-chronicle-jIdutuif/lib/python3.10/site-packages/sqlite_utils/db.py", line 3331, in insert_all
    raise PrimaryKeyRequired("upsert() requires a pk")
sqlite_utils.db.PrimaryKeyRequired: upsert() requires a pk

But if I used db.table("dogs", pk="id") then it works:

>>> t2 = db.table("dogs2", pk="id").create({"id": int, "name": str, "color": str}, pk="id")
>>> t2.pks
['id']
>>> t2.upsert({"id": 1, "name": "Cleo"})
<Table dogs2 (id, name, color)>

But weirdly if I do db.table("dogs", pk="id") but don't pass pk="id" to create() this happens:

>>> t3 = db.table("dogs3", pk="id").create({"id": int, "name": str, "color": str})
>>> t3.pks
['rowid']

simonw avatar May 09 '25 16:05 simonw

There are three ways these settings (like the table primary key) might make it to the point in the code where they are needed:

  • Passed to the db.table() method, which forwards to the Table constructor.
  • Passes to methods that might create a table such as .insert()
  • Introspected from the database itself

Introspection has the advantage that it works even if something else has been messing with the table while the code is running - though I expect that to be vanishingly rare.

simonw avatar May 10 '25 03:05 simonw