diff --git a/unwind/db.py b/unwind/db.py index bb3e6f0..99839cb 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -165,12 +165,9 @@ def shared_connection() -> Database: async def add(item): - # Support late initializing of `id` (used for optimization). - if hasattr(item, "id") and getattr(item, "id") is None: - for f in fields(item): - if f.name == "id": - item.id = f.default_factory() - break + # Support late initializing - used for optimization. + if getattr(item, "_is_lazy", False): + item._lazy_init() values = asplain(item) keys = ", ".join(f"{k}" for k in values) diff --git a/unwind/imdb_import.py b/unwind/imdb_import.py index 94b6830..c43bbf3 100644 --- a/unwind/imdb_import.py +++ b/unwind/imdb_import.py @@ -50,7 +50,7 @@ class BasicRow: def as_movie(self): assert self.startYear is not None - return Movie( + return Movie.lazy( title=self.primaryTitle, original_title=self.originalTitle, release_year=self.startYear, @@ -59,8 +59,6 @@ class BasicRow: imdb_score=None, runtime=self.runtimeMinutes, genres=self.genres or set(), - updated=None, # optimization: skip default factory - id=None, # optimization: skip default factory ) @@ -77,12 +75,10 @@ class RatingRow: return inst def as_movie(self): - return Movie( + return Movie.lazy( imdb_id=self.tconst, imdb_score=score_from_imdb_rating(self.averageRating), imdb_votes=self.numVotes, - updated=None, # optimization: skip default factory - id=None, # optimization: skip default factory ) diff --git a/unwind/models.py b/unwind/models.py index f6c2b7e..56ad2c5 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -35,6 +35,9 @@ def fields(class_or_instance): for f in _fields(class_or_instance): + if f.name == "_is_lazy": + continue + if (attn := annotations(f.type)) and _RelationSentinel in attn: continue # Relations are ignored @@ -167,8 +170,36 @@ class Movie: imdb_votes: Optional[int] = None runtime: Optional[int] = None # minutes genres: set[str] = None + created: datetime = field(default_factory=utcnow) updated: datetime = field(default_factory=utcnow) + _is_lazy: bool = field(default=False, init=False, repr=False, compare=False) + + @classmethod + def lazy(cls, **kwds): + """Return a new instance without running default factories. + + This is meant purely for optimization purposes, to postpone possibly + expensive initialization operations. + """ + # XXX optimize using a metaclass & storing field refs on the class + kwds.setdefault("id", None) + kwds.setdefault("created", None) + kwds.setdefault("updated", None) + movie = cls(**kwds) + movie._is_lazy = True + return movie + + def _lazy_init(self): + if not self._is_lazy: + return + + for field in fields(Movie): + if getattr(self, field.name) is None and field.default_factory: + setattr(self, field.name, field.default_factory()) + + self._is_lazy = False + T = TypeVar("T") _RelationSentinel = object() diff --git a/unwind/sql/20210721-213417.sql b/unwind/sql/20210721-213417.sql new file mode 100644 index 0000000..33e891a --- /dev/null +++ b/unwind/sql/20210721-213417.sql @@ -0,0 +1,38 @@ +-- add creation timestamp to movies + +CREATE TABLE _migrate_movies ( + id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + original_title TEXT, + release_year INTEGER NOT NULL, + media_type TEXT NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + imdb_score INTEGER, + imdb_votes INTEGER, + runtime INTEGER, + genres TEXT NOT NULL, + created TEXT NOT NULL, + updated TEXT NOT NULL +);; + +INSERT INTO _migrate_movies +SELECT + id, + title, + original_title, + release_year, + media_type, + imdb_id, + imdb_score, + imdb_votes, + runtime, + genres, + updated AS created, + updated +FROM movies +WHERE true;; + +DROP TABLE movies;; + +ALTER TABLE _migrate_movies +RENAME TO movies;;