db: Give Tables an idle Queue

Tables use the idle queue to load their data or filter rows in the
background. Tables will create a new queue by default, but can accept a
pre-constructed queue through the queue= parameter.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2023-02-25 20:32:10 -05:00
parent e502a7e8cb
commit 9944d07bba
6 changed files with 84 additions and 14 deletions

View File

@ -24,6 +24,12 @@ class Connection(connection.Connection):
self.settings = settings.Table(self)
def close(self) -> None:
"""Close the database connection."""
self.settings.stop()
super().close()
def load(self) -> None:
"""Load the database tables."""
self.settings.load()

View File

@ -2,6 +2,7 @@
"""Easy access to the settings table in our database."""
import sqlite3
from gi.repository import GObject
from . import idle
from . import table
@ -43,6 +44,10 @@ class StringSetting(Setting):
class Table(table.Table):
"""Creates and manages our settings properties."""
def __init__(self, sql: GObject.TYPE_PYOBJECT):
"""Initialize the settings table."""
super().__init__(sql, queue=idle.Queue(enabled=False))
def __getitem__(self, key: str) -> int | float | str | bool | None:
"""Get the value for a specific settings key."""
if (setting := self.lookup(key)) is not None:

View File

@ -4,6 +4,7 @@ import sqlite3
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
from .idle import Queue
from .. import store
@ -113,19 +114,27 @@ class Table(Gtk.FilterListModel):
"""An object that represents a database Table."""
sql = GObject.Property(type=GObject.TYPE_PYOBJECT)
queue = GObject.Property(type=Queue)
store = GObject.Property(type=Gio.ListModel)
rows = GObject.Property(type=GObject.TYPE_PYOBJECT)
loaded = GObject.Property(type=bool, default=False)
def __init__(self, sql: GObject.TYPE_PYOBJECT,
filter: Filter | None = None, **kwargs):
filter: Filter | None = None,
queue: Queue | None = None, **kwargs):
"""Set up our Table object."""
super().__init__(sql=sql, incremental=True, rows=dict(),
super().__init__(sql=sql, rows=dict(), incremental=True,
store=store.SortedList(self.get_sort_key),
filter=(filter if filter else Filter()), **kwargs)
filter=(filter if filter else Filter()),
queue=(queue if queue else Queue()), **kwargs)
self.set_model(self.store)
def __clear_rows(self) -> None:
self.rows.clear()
self.store.clear()
self.loaded = False
def __contains__(self, row: Row) -> bool:
"""Check if the row is in the _rowid_map for this Table."""
return self.index(row) is not None
@ -164,9 +173,8 @@ class Table(Gtk.FilterListModel):
def clear(self) -> None:
"""Clear the table."""
self.rows.clear()
self.store.clear()
self.loaded = False
self.stop()
self.__clear_rows()
def construct(self, *args, **kwargs) -> Row:
"""Construct a new Row instance."""
@ -185,11 +193,15 @@ class Table(Gtk.FilterListModel):
return True
return False
def filter(self, glob: str | None) -> None:
def _filter_idle(self, glob: str) -> bool:
rows = self.do_sql_glob(glob).fetchall()
self.get_filter().keys = {row[0] for row in rows}
return True
def filter(self, glob: str | None, *, now: bool = False) -> None:
"""Filter the displayed Rows."""
if glob is not None:
rows = self.do_sql_glob(glob).fetchall()
self.get_filter().keys = {row[0] for row in rows}
self.queue.push(self._filter_idle, glob, now=now)
else:
self.get_filter().keys = None
@ -209,20 +221,28 @@ class Table(Gtk.FilterListModel):
self.store.append(row)
return self.rows.setdefault(row.primary_key, row)
def load(self) -> None:
"""Load the Table from the database."""
self.clear()
def _load_idle(self) -> bool:
self.__clear_rows()
cur = self.do_sql_select_all()
rows = [self.construct(**row) for row in cur.fetchall()]
self.store.extend(rows)
self.rows = {row.primary_key: row for row in rows}
self.sql.emit("table-loaded", self)
return True
def load(self, *, now: bool = False) -> None:
"""Load the Table from the database."""
self.queue.push(self._load_idle, now=now)
def lookup(self, *args, **kwargs) -> Row | None:
"""Look up a Row in the database."""
row = self.do_sql_select_one(*args, **kwargs).fetchone()
return self.rows.get(row[0]) if row else None
def stop(self) -> None:
"""Stop any background work."""
self.queue.cancel()
def update(self, row: Row, column: str, newval) -> bool:
"""Update a Row."""
return self.do_sql_update(row, column, newval) is not None

View File

@ -23,6 +23,16 @@ class TestConnection(tests.util.TestCase):
cur = self.sql("PRAGMA user_version")
self.assertEqual(cur.fetchone()["user_version"], 1)
def test_close(self):
"""Check closing the connection."""
self.sql.settings.queue.running = True
self.sql.close()
self.assertFalse(self.sql.connected)
self.assertFalse(self.sql.settings.queue.running)
self.sql.close()
def test_tables(self):
"""Check that the connection has pointers to our tables."""
self.assertIsInstance(self.sql.settings, emmental.db.settings.Table)

View File

@ -26,6 +26,7 @@ class TestSetting(tests.util.TestCase):
def test_init(self):
"""Test that the Settings Manager is set up properly."""
self.assertIsInstance(self.settings, emmental.db.table.Table)
self.assertFalse(self.settings.queue.enabled)
def test_get_sort_key(self):
"""Test comparing settings."""

View File

@ -176,6 +176,7 @@ class TestTable(tests.util.TestCase):
def test_init(self):
"""Test that the table is set up properly."""
self.assertIsInstance(self.table, Gtk.FilterListModel)
self.assertIsInstance(self.table.queue, emmental.db.idle.Queue)
self.assertIsInstance(self.table.get_filter(),
emmental.db.table.Filter)
self.assertIsInstance(self.table.store, emmental.store.SortedList)
@ -188,18 +189,23 @@ class TestTable(tests.util.TestCase):
self.assertTrue(self.table.get_incremental())
filter2 = emmental.db.table.Filter()
table2 = emmental.db.table.Table(self.sql, filter=filter2)
queue2 = emmental.db.idle.Queue()
table2 = emmental.db.table.Table(self.sql, filter=filter2,
queue=queue2)
self.assertEqual(table2.get_filter(), filter2)
self.assertEqual(table2.queue, queue2)
def test_clear(self):
"""Test clearing a table."""
row = tests.util.table.MockRow(number=1, table=self.table)
self.table.store.append(row)
self.table.loaded = True
self.table.queue.running = True
self.table.clear()
self.assertEqual(self.table.store.n_items, 0)
self.assertDictEqual(self.table.rows, dict())
self.assertFalse(self.table.queue.running)
self.assertFalse(self.table.loaded)
def test_contains(self):
@ -263,8 +269,10 @@ class TestTable(tests.util.TestCase):
self.table.do_sql_delete(None)
with self.assertRaises(NotImplementedError):
self.table.filter("*text*")
self.table.queue.complete()
with self.assertRaises(NotImplementedError):
self.table.load()
self.table.queue.complete()
with self.assertRaises(NotImplementedError):
self.table.lookup(1)
with self.assertRaises(NotImplementedError):
@ -312,8 +320,17 @@ class TestTableFunctions(tests.util.TestCase):
self.table.create(number=n)
self.table.filter("*2*")
self.assertIsNone(self.table.get_filter().keys)
self.assertEqual(self.table.queue[0], (self.table._filter_idle, "*2*"))
self.table.queue.complete()
self.assertSetEqual(self.table.get_filter().keys, {121, 212})
self.table.filter("*1*", now=True)
self.assertSetEqual(self.table.get_filter().keys, {1, 121, 212})
self.table.filter(None)
self.assertIsNone(self.table.queue[0])
self.assertIsNone(self.table.get_filter().keys)
def test_get_sort_key(self):
@ -331,6 +348,11 @@ class TestTableFunctions(tests.util.TestCase):
self.sql("INSERT INTO mock_table (number) VALUES (?)", 2)
self.table.load()
self.assertFalse(self.table.loaded)
self.assertEqual(len(self.table), 0)
self.assertEqual(self.table.queue[0], (self.table._load_idle,))
self.table.queue.complete()
self.assertTrue(self.table.loaded)
self.assertEqual(len(self.table), 2)
table_loaded.assert_called_with(self.sql, self.table)
@ -344,7 +366,7 @@ class TestTableFunctions(tests.util.TestCase):
self.assertEqual(self.table.rows, {1: row1, 2: row2})
self.table.load()
self.table.load(now=True)
self.assertNotEqual(self.table[0], row1)
self.assertNotEqual(self.table[1], row2)
@ -354,6 +376,12 @@ class TestTableFunctions(tests.util.TestCase):
self.assertEqual(self.table.lookup(1), row)
self.assertIsNone(self.table.lookup(2))
def test_stop(self):
"""Test the table.stop() function."""
with unittest.mock.patch.object(self.table.queue, "cancel") as cancel:
self.table.stop()
cancel.assert_called()
def test_update(self):
"""Test updating a Row."""
row = self.table.create(number=1)