Two related ORM bugs found while running a downstream PostgreSQL service against tina4-python 3.13.1. Both break basic ORM use for common model shapes (per-row timestamp defaults, user-supplied string primary keys). A downstream BaseORM shim works around both, but the proper fix belongs in the framework.
Per the project's parity rules, both should be fixed across all 4 frameworks (Python, PHP, Ruby, Node.js) with tests for each; Python is the master reference.
Bug 1 — Callable field defaults are not resolved
A field declared with a callable default stores the function object itself instead of its return value:
class GiftCard(ORM):
created_at = DateTimeField(default=lambda: datetime.datetime.now())
On save this reaches the driver unresolved and fails:
psycopg2.ProgrammingError: can't adapt type 'function'
Root cause (Python):
orm/model.py, model __init__: setattr(self, name, field.default) (~L182) assigns the default verbatim.
orm/fields.py, Field.validate(): return self.default (~L59) — neither checks callable(...).
Expected: when default is callable, call it at the point of use so each instance gets a freshly evaluated value (e.g. a distinct per-row created_at).
Repro:
gc = GiftCard()
type(gc.created_at) # -> <class 'function'> (should be datetime)
gc.save() # -> raises "can't adapt type 'function'"
Bug 2 — save() cannot INSERT a row with a natural (non-auto-increment) primary key
save() decides INSERT vs UPDATE purely on whether the PK is set:
# orm/model.py, save() (~L320)
if pk_value is not None:
db.update(...) # UPDATE
else:
db.insert(...) # INSERT
This only works for auto-increment keys. For a model whose primary key is a user-supplied/natural key (e.g. a string gift_card_number set before the first save), the PK is always set, so save() issues an UPDATE that matches zero rows and silently persists nothing — while still returning success.
Repro:
gc = GiftCard()
gc.gift_card_number = "GC-100" # natural string PK, set by caller
gc.created_by_email = "a@b.com"
gc.owned_by_email = "a@b.com"
gc.save() # returns success...
GiftCard.find_by_id("GC-100") # ...but returns None — nothing was inserted
Expected: for a non-auto-increment PK, decide INSERT vs UPDATE by whether the row exists (the ORM already has exists() / find_by_id()), or track a persisted flag. Auto-increment behaviour must be unchanged. Saving a new natural-key row must INSERT and be retrievable; saving again must UPDATE (no duplicate, no error).
Downstream workaround (for reference)
class BaseORM(ORM):
def __init__(self, data=None, **kwargs):
super().__init__(data, **kwargs)
for name in self._fields:
value = getattr(self, name, None)
if callable(value):
setattr(self, name, value()) # Bug 1
def save(self):
pk = self._get_pk()
pk_value = getattr(self, pk, None)
pk_field = self._fields[pk]
if pk_value is not None and not pk_field.auto_increment and not type(self).exists(pk_value):
return self._insert_new() # Bug 2: force INSERT for new natural-key rows
return super().save()
Acceptance criteria
Two related ORM bugs found while running a downstream PostgreSQL service against tina4-python 3.13.1. Both break basic ORM use for common model shapes (per-row timestamp defaults, user-supplied string primary keys). A downstream
BaseORMshim works around both, but the proper fix belongs in the framework.Per the project's parity rules, both should be fixed across all 4 frameworks (Python, PHP, Ruby, Node.js) with tests for each; Python is the master reference.
Bug 1 — Callable field defaults are not resolved
A field declared with a callable default stores the function object itself instead of its return value:
On save this reaches the driver unresolved and fails:
Root cause (Python):
orm/model.py, model__init__:setattr(self, name, field.default)(~L182) assigns the default verbatim.orm/fields.py,Field.validate():return self.default(~L59) — neither checkscallable(...).Expected: when
defaultis callable, call it at the point of use so each instance gets a freshly evaluated value (e.g. a distinct per-rowcreated_at).Repro:
Bug 2 —
save()cannot INSERT a row with a natural (non-auto-increment) primary keysave()decides INSERT vs UPDATE purely on whether the PK is set:This only works for auto-increment keys. For a model whose primary key is a user-supplied/natural key (e.g. a string
gift_card_numberset before the first save), the PK is always set, sosave()issues an UPDATE that matches zero rows and silently persists nothing — while still returning success.Repro:
Expected: for a non-auto-increment PK, decide INSERT vs UPDATE by whether the row exists (the ORM already has
exists()/find_by_id()), or track a persisted flag. Auto-increment behaviour must be unchanged. Saving a new natural-key row must INSERT and be retrievable; saving again must UPDATE (no duplicate, no error).Downstream workaround (for reference)
Acceptance criteria
save()INSERTs a new natural-key row and UPDATEs an existing one; covered by tests.