first half-attempt at a flexible attribute schema

This uses two tables, item_attributes and album_attributes, as key/value
tables for the respective entities. Plugins register fields, at which point
they are magically materialized as properties on Items (Albums are not done).
This currently supports adding and modifying these fields but not retrieving
them (which will need some sort of join).
This commit is contained in:
Adrian Sampson 2013-02-26 13:35:26 -08:00
parent 4a35be5724
commit 0d762cd269
2 changed files with 71 additions and 16 deletions

View file

@ -273,8 +273,8 @@ class Item(object):
"""If key is an item attribute (i.e., a column in the database),
returns the record entry for that key.
"""
if key in ITEM_KEYS:
return self.record[key]
if key in ITEM_KEYS or key in plugins.item_fields():
return self.record.get(key)
else:
raise AttributeError(key + ' is not a valid item field')
@ -293,7 +293,7 @@ class Item(object):
elif isinstance(value, buffer):
value = str(value)
if key in ITEM_KEYS:
if key in ITEM_KEYS or key in plugins.item_fields():
# If the value changed, mark the field as dirty.
if (key not in self.record) or (self.record[key] != value):
self.record[key] = value
@ -1067,6 +1067,8 @@ class Library(BaseLibrary):
# Set up database schema.
self._make_table('items', item_fields)
self._make_table('albums', album_fields)
self._make_attribute_table('item')
self._make_attribute_table('album')
def _make_table(self, table, fields):
"""Set up the schema of the library file. fields is a list of
@ -1111,6 +1113,22 @@ class Library(BaseLibrary):
with self.transaction() as tx:
tx.script(setup_sql)
def _make_attribute_table(self, entity):
"""Create a table and associated index for flexible attributes
for the given entity (if they don't exist).
"""
with self.transaction() as tx:
tx.script("""
CREATE TABLE IF NOT EXISTS {0}_attributes (
id INTEGER PRIMARY KEY,
entity_id INTEGER,
key TEXT,
value TEXT,
UNIQUE(entity_id, key) ON CONFLICT REPLACE);
CREATE INDEX IF NOT EXISTS {0}_id_attribute
ON {0}_attributes (entity_id);
""".format(entity))
def _connection(self):
"""Get a SQLite connection object to the underlying database.
One connection object is created per thread.
@ -1235,9 +1253,22 @@ class Library(BaseLibrary):
subvars.append(value)
# Issue query.
query = 'INSERT INTO items (' + columns + ') VALUES (' + values + ')'
with self.transaction() as tx:
new_id = tx.mutate(query, subvars)
# Main table insertion.
new_id = tx.mutate(
'INSERT INTO items (' + columns + ') VALUES (' + values + ')',
subvars
)
# Flexible attributes.
for key in plugins.item_fields():
value = getattr(item, key)
if value is not None:
tx.mutate(
'INSERT INTO item_attributes (entity_id, key, value)'
' VALUES (?, ?, ?)',
(new_id, key, value)
)
item._clear_dirty()
item.id = new_id
@ -1269,21 +1300,25 @@ class Library(BaseLibrary):
if key == 'path' and isinstance(value, str):
value = buffer(value)
subvars.append(value)
if not assignments:
# nothing to store (i.e., nothing was dirty)
return
assignments = assignments[:-1] # Knock off last ,
# Finish the query.
query = 'UPDATE items SET ' + assignments + ' WHERE id=?'
subvars.append(store_id)
with self.transaction() as tx:
tx.mutate(query, subvars)
item._clear_dirty()
# Main table update.
if assignments:
query = 'UPDATE items SET ' + assignments + ' WHERE id=?'
subvars.append(store_id)
tx.mutate(query, subvars)
# Flexible attributes.
for key in plugins.item_fields():
if item.dirty.get(key) or store_all:
tx.mutate(
'INSERT INTO item_attributes (entity_id, key, value)'
' VALUES (?, ?, ?)',
(store_id, key, getattr(item, key))
)
item._clear_dirty()
self._memotable = {}
def remove(self, item, delete=False, with_album=True):

View file

@ -48,6 +48,8 @@ class BeetsPlugin(object):
self.template_funcs = {}
if not self.template_fields:
self.template_fields = {}
self.item_fields = []
self.album_fields = []
def commands(self):
"""Should return a list of beets.ui.Subcommand objects for
@ -288,6 +290,24 @@ def import_stages():
stages += plugin.import_stages
return stages
def item_fields():
"""Get a list of strings indicating registered flexible Item
attributes.
"""
fields = []
for plugin in find_plugins():
fields += plugin.item_fields
return fields
def album_fields():
"""Get a list of strings indicating registered flexible Album
attributes.
"""
fields = []
for plugin in find_plugins():
fields += plugin.album_fields
return fields
# Event dispatch.