Cache data needed to build the feed and feeds overviews.
authorNeal H. Walfield <neal@walfield.org>
Tue, 6 Sep 2011 12:58:36 +0000 (14:58 +0200)
committerNeal H. Walfield <neal@walfield.org>
Tue, 6 Sep 2011 15:32:19 +0000 (17:32 +0200)
src/rss_sqlite.py

index 6551d93..5735de2 100644 (file)
@@ -81,7 +81,108 @@ update_feeds_iface = None
 
 jobs_at_start = 0
 
-class Feed:
+class BaseObject(object):
+    # Columns to cache.  Classes that inherit from this and use the
+    # cache mechanism should set this to a list of tuples, each of
+    # which contains two entries: the table and the column.  Note that
+    # both are case sensitive.
+    cached_columns = ()
+
+    def cache_invalidate(self, table=None):
+        """
+        Invalidate the cache.
+
+        If table is not None, invalidate only the specified table.
+        Otherwise, drop the whole cache.
+        """
+        if not hasattr(self, 'cache'):
+            return
+
+        if table is None:
+            del self.cache
+        else:
+            if table in self.cache:
+                del self.cache[table]
+
+    def lookup(self, table, column, id=None):
+        """
+        Look up a column or value.  Uses a cache for columns in
+        cached_columns.  Note: the column is returned unsorted.
+        """
+        if not hasattr(self, 'cache'):
+            self.cache = {}
+
+        # Cache data for at most 60 seconds.
+        now = time.time()
+        try:
+            cache = self.cache[table]
+
+            if time.time() - cache[None] > 60:
+                self.cache[table].clear()
+        except KeyError:
+            cache = None
+
+        if (cache is None
+            or (table, column) not in self.cached_columns):
+            # The cache is empty or the caller wants a column that we
+            # don't cache.
+            if (table, column) in self.cached_columns:
+                do_cache = True
+
+                self.cache[table] = cache = {}
+                columns = []
+                for t, c in self.cached_columns:
+                    if table == t:
+                        cache[c] = {}
+                        columns.append(c)
+
+                columns.append('id')
+                where = ""
+            else:
+                do_cache = False
+
+                columns = (colums,)
+                if id is not None:
+                    where = "where id = '%s'" % id
+                else:
+                    where = ""
+
+            results = self.db.execute(
+                "SELECT %s FROM %s %s" % (','.join(columns), table, where))
+
+            if do_cache:
+                for r in results:
+                    values = list(r)
+                    i = values.pop()
+                    for index, value in enumerate(values):
+                        cache[columns[index]][i] = value
+
+                cache[None] = now
+            else:
+                results = []
+                for r in results:
+                    if id is not None:
+                        return values[0]
+
+                    results.append(values[0])
+
+                return results
+        else:
+            cache = self.cache[table]
+
+        try:
+            if id is not None:
+                return cache[column][id]
+            else:
+                return cache[column].values()
+        except KeyError:
+            return None
+
+class Feed(BaseObject):
+    # Columns to cache.
+    cached_columns = (('feed', 'read'),
+                      ('feed', 'title'))
+
     serial_execution_lock = threading.Lock()
 
     def _getdb(self):
@@ -528,6 +629,8 @@ class Feed:
                     postFeedUpdateFunc (self.key, updateTime, etag, modified,
                                         title, *postFeedUpdateFuncArgs)
 
+        self.cache_invalidate()
+
     def setEntryRead(self, id):
         self.db.execute("UPDATE feed SET read=1 WHERE id=?;", (id,) )
         self.db.commit()
@@ -539,21 +642,23 @@ class Feed:
                 pass
         if wc().available():
             mainthread.execute(doit, async=True)
+        self.cache_invalidate('feed')
 
     def setEntryUnread(self, id):
         self.db.execute("UPDATE feed SET read=0 WHERE id=?;", (id,) )
         self.db.commit()     
+        self.cache_invalidate('feed')
         
     def markAllAsRead(self):
         self.db.execute("UPDATE feed SET read=1 WHERE read=0;")
         self.db.commit()
+        self.cache_invalidate('feed')
 
     def isEntryRead(self, id):
-        read_status = self.db.execute("SELECT read FROM feed WHERE id=?;", (id,) ).fetchone()[0]
-        return read_status==1  # Returns True if read==1, and False if read==0
+        return self.lookup('feed', 'read', id) == 1
     
     def getTitle(self, id):
-        return self.db.execute("SELECT title FROM feed WHERE id=?;", (id,) ).fetchone()[0]
+        return self.lookup('feed', 'title', id)
     
     def getContentLink(self, id):
         return self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,) ).fetchone()[0]
@@ -754,7 +859,13 @@ class ArchivedArticles(Feed):
                 pass
         self.removeEntry(id)
 
-class Listing:
+class Listing(BaseObject):
+    # Columns to cache.
+    cached_columns = (('feeds', 'updateTime'),
+                      ('feeds', 'unread'),
+                      ('feeds', 'title'),
+                      ('categories', 'title'))
+
     def _getdb(self):
         try:
             db = self.tls.db
@@ -925,6 +1036,7 @@ class Listing:
             self.db.execute("UPDATE feeds SET title=(case WHEN title=='' THEN ? ELSE title END) where id=?;",
                             (title, key))
         self.db.commit()
+        self.cache_invalidate('feeds')
         self.updateUnread(key)
 
         update_server_object().ArticleCountUpdated()
@@ -955,6 +1067,7 @@ class Listing:
         else:
             self.db.execute("UPDATE feeds SET title=?, url=? WHERE id=?;", (title, url, key))
         self.db.commit()
+        self.cache_invalidate('feeds')
 
         if wc().available():
             try:
@@ -963,8 +1076,7 @@ class Listing:
                 logger.debug("Feed %s (%s) unknown." % (key, title))
         
     def getFeedUpdateTime(self, key):
-        update_time = self.db.execute(
-            "SELECT updateTime FROM feeds WHERE id=?;", (key,)).fetchone()[0]
+        update_time = self.lookup('feeds', 'updateTime', key)
 
         if not update_time:
             return "Never"
@@ -998,13 +1110,14 @@ class Listing:
         return time.strftime("%x", time.gmtime(update_time))
         
     def getFeedNumberOfUnreadItems(self, key):
-        return self.db.execute("SELECT unread FROM feeds WHERE id=?;", (key,)).fetchone()[0]
+        return self.lookup('feeds', 'unread', key)
         
     def getFeedTitle(self, key):
-        (title, url) = self.db.execute("SELECT title, url FROM feeds WHERE id=?;", (key,)).fetchone()
+        title = self.lookup('feeds', 'title', key)
         if title:
             return title
-        return url
+
+        return self.getFeedUrl(key)
         
     def getFeedUrl(self, key):
         return self.db.execute("SELECT url FROM feeds WHERE id=?;", (key,)).fetchone()[0]
@@ -1024,16 +1137,11 @@ class Listing:
         return keys
     
     def getListOfCategories(self):
-        rows = self.db.execute("SELECT id FROM categories ORDER BY rank;" )
-        keys = []
-        for row in rows:
-            if row[0]:
-                keys.append(row[0])
-        return keys
+        return list(row[0] for row in self.db.execute(
+                "SELECT id FROM categories ORDER BY rank;"))
     
     def getCategoryTitle(self, id):
-        row = self.db.execute("SELECT title FROM categories WHERE id=?;", (id, )).fetchone()
-        return row[0]
+        return self.lookup('categories', 'title', id)
     
     def getSortedListOfKeys(self, order, onlyUnread=False, category=1):
         if   order == "Most unread":
@@ -1073,6 +1181,7 @@ class Listing:
         feed = self.getFeed(key)
         self.db.execute("UPDATE feeds SET unread=? WHERE id=?;", (feed.getNumberOfUnreadItems(), key))
         self.db.commit()
+        self.cache_invalidate('feeds')
 
     def addFeed(self, title, url, id=None, category=1):
         if not id: