← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~raoul-snyman/openlp/db-upgrades into lp:openlp

 

Raoul Snyman has proposed merging lp:~raoul-snyman/openlp/db-upgrades into lp:openlp.

Requested reviews:
  Andreas Preikschat (googol)
  Tim Bentley (trb143)

For more details, see:
https://code.launchpad.net/~raoul-snyman/openlp/db-upgrades/+merge/72953

Database upgrade system.

1. Create a module to use for upgrading.
2. from migrate import changeset
3. Define some default tables, which use reflection, in an "upgrade_setup()" function.
4. Write your upgrade functions named "upgrade_X()" where X is the version number.
5. Write your upgrade statements using the extended methods from Migrate.
6. Make sure the DB manager object knows about the upgrade module.

Done!
-- 
https://code.launchpad.net/~raoul-snyman/openlp/db-upgrades/+merge/72953
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/db.py'
--- openlp/core/lib/db.py	2011-08-16 19:53:52 +0000
+++ openlp/core/lib/db.py	2011-08-25 20:16:24 +0000
@@ -31,11 +31,13 @@
 import os
 
 from PyQt4 import QtCore
-from sqlalchemy import create_engine, MetaData
-from sqlalchemy.exc import InvalidRequestError
-from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy import Table, MetaData, Column, types, create_engine
+from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError
+from sqlalchemy.orm import scoped_session, sessionmaker, mapper
 from sqlalchemy.pool import NullPool
 
+from openlp.core.lib import translate
+from openlp.core.lib.ui import critical_error_message_box
 from openlp.core.utils import AppLocation, delete_file
 
 log = logging.getLogger(__name__)
@@ -59,6 +61,48 @@
         autocommit=auto_commit, bind=engine))
     return session, metadata
 
+
+def upgrade_db(url, upgrade):
+    """
+    Upgrade a database.
+
+    ``url``
+        The url of the database to upgrade.
+
+    ``upgrade``
+        The python module that contains the upgrade instructions.
+    """
+    session, metadata = init_db(url)
+    tables = upgrade.upgrade_setup(metadata)
+    metadata_table = Table(u'metadata', metadata,
+        Column(u'key', types.Unicode(64), primary_key=True),
+        Column(u'value', types.UnicodeText(), default=None)
+    )
+    metadata_table.create(checkfirst=True)
+    mapper(Metadata, metadata_table)
+    version_meta = session.query(Metadata).get(u'version')
+    if version_meta is None:
+        version_meta = Metadata.populate(key=u'version', value=u'0')
+        version = 0
+    else:
+        version = int(version_meta.value)
+    if version > upgrade.__version__:
+        return version, upgrade.__version__
+    version += 1
+    while hasattr(upgrade, u'upgrade_%d' % version):
+        log.debug(u'Running upgrade_%d', version)
+        try:
+            getattr(upgrade, u'upgrade_%d' % version)(session, metadata, tables)
+            version_meta.value = unicode(version)
+        except SQLAlchemyError, DBAPIError:
+            log.exception(u'Could not run database upgrade script "upgrade_%s"'\
+                ', upgrade process has been halted.', version)
+            break
+        version += 1
+    session.add(version_meta)
+    session.commit()
+    return int(version_meta.value), upgrade.__version__
+
 def delete_database(plugin_name, db_file_name=None):
     """
     Remove a database file from the system.
@@ -79,6 +123,7 @@
             AppLocation.get_section_data_path(plugin_name), plugin_name)
     return delete_file(db_file_path)
 
+
 class BaseModel(object):
     """
     BaseModel provides a base object with a set of generic functions
@@ -94,11 +139,19 @@
         return instance
 
 
+class Metadata(BaseModel):
+    """
+    Provides a class for the metadata table.
+    """
+    pass
+
+
 class Manager(object):
     """
     Provide generic object persistence management
     """
-    def __init__(self, plugin_name, init_schema, db_file_name=None):
+    def __init__(self, plugin_name, init_schema, db_file_name=None,
+                 upgrade_mod=None):
         """
         Runs the initialisation process that includes creating the connection
         to the database and the tables if they don't exist.
@@ -109,6 +162,9 @@
         ``init_schema``
             The init_schema function for this database
 
+        ``upgrade_schema``
+            The upgrade_schema function for this database
+
         ``db_file_name``
             The file name to use for this database. Defaults to None resulting
             in the plugin_name being used.
@@ -134,7 +190,27 @@
                 unicode(settings.value(u'db hostname').toString()),
                 unicode(settings.value(u'db database').toString()))
         settings.endGroup()
-        self.session = init_schema(self.db_url)
+        if upgrade_mod:
+            db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
+            if db_ver > up_ver:
+                critical_error_message_box(
+                    translate('OpenLP.Manager', 'Database Error'),
+                    unicode(translate('OpenLP.Manager', 'The database being '
+                        'loaded was created in a more recent version of '
+                        'OpenLP. The database is version %d, while OpenLP '
+                        'expects version %d. The database will not be loaded.'
+                        '\n\nDatabase: %s')) % \
+                        (db_ver, up_ver, self.db_url)
+                )
+                return
+        try:
+            self.session = init_schema(self.db_url)
+        except:
+            critical_error_message_box(
+                translate('OpenLP.Manager', 'Database Error'),
+                unicode(translate('OpenLP.Manager', 'OpenLP cannot load your '
+                    'database.\n\nDatabase: %s')) % self.db_url
+            )
 
     def save_object(self, object_instance, commit=True):
         """

=== modified file 'openlp/plugins/songs/lib/db.py'
--- openlp/plugins/songs/lib/db.py	2011-07-07 18:03:12 +0000
+++ openlp/plugins/songs/lib/db.py	2011-08-25 20:16:24 +0000
@@ -70,7 +70,6 @@
     """
     pass
 
-
 def init_schema(url):
     """
     Setup the songs database connection and initialise the database schema.
@@ -111,10 +110,6 @@
         * file_name
         * type
 
-    **media_files_songs Table**
-        * media_file_id
-        * song_id
-
     **song_books Table**
         The *song_books* table holds a list of books that a congregation gets
         their songs from, or old hymnals now no longer used. This table has the
@@ -162,7 +157,7 @@
 
     # Definition of the "authors" table
     authors_table = Table(u'authors', metadata,
-        Column(u'id', types.Integer, primary_key=True),
+        Column(u'id', types.Integer(), primary_key=True),
         Column(u'first_name', types.Unicode(128)),
         Column(u'last_name', types.Unicode(128)),
         Column(u'display_name', types.Unicode(255), index=True, nullable=False)
@@ -170,22 +165,25 @@
 
     # Definition of the "media_files" table
     media_files_table = Table(u'media_files', metadata,
-        Column(u'id', types.Integer, primary_key=True),
+        Column(u'id', types.Integer(), primary_key=True),
+        Column(u'song_id', types.Integer(), ForeignKey(u'songs.id'),
+            default=None),
         Column(u'file_name', types.Unicode(255), nullable=False),
-        Column(u'type', types.Unicode(64), nullable=False, default=u'audio')
+        Column(u'type', types.Unicode(64), nullable=False, default=u'audio'),
+        Column(u'weight', types.Integer(), default=0)
     )
 
     # Definition of the "song_books" table
     song_books_table = Table(u'song_books', metadata,
-        Column(u'id', types.Integer, primary_key=True),
+        Column(u'id', types.Integer(), primary_key=True),
         Column(u'name', types.Unicode(128), nullable=False),
         Column(u'publisher', types.Unicode(128))
     )
 
     # Definition of the "songs" table
     songs_table = Table(u'songs', metadata,
-        Column(u'id', types.Integer, primary_key=True),
-        Column(u'song_book_id', types.Integer,
+        Column(u'id', types.Integer(), primary_key=True),
+        Column(u'song_book_id', types.Integer(),
             ForeignKey(u'song_books.id'), default=None),
         Column(u'title', types.Unicode(255), nullable=False),
         Column(u'alternate_title', types.Unicode(255)),
@@ -202,31 +200,23 @@
 
     # Definition of the "topics" table
     topics_table = Table(u'topics', metadata,
-        Column(u'id', types.Integer, primary_key=True),
+        Column(u'id', types.Integer(), primary_key=True),
         Column(u'name', types.Unicode(128), index=True, nullable=False)
     )
 
     # Definition of the "authors_songs" table
     authors_songs_table = Table(u'authors_songs', metadata,
-        Column(u'author_id', types.Integer,
+        Column(u'author_id', types.Integer(),
             ForeignKey(u'authors.id'), primary_key=True),
-        Column(u'song_id', types.Integer,
-            ForeignKey(u'songs.id'), primary_key=True)
-    )
-
-    # Definition of the "media_files_songs" table
-    media_files_songs_table = Table(u'media_files_songs', metadata,
-        Column(u'media_file_id', types.Integer,
-            ForeignKey(u'media_files.id'), primary_key=True),
-        Column(u'song_id', types.Integer,
+        Column(u'song_id', types.Integer(),
             ForeignKey(u'songs.id'), primary_key=True)
     )
 
     # Definition of the "songs_topics" table
     songs_topics_table = Table(u'songs_topics', metadata,
-        Column(u'song_id', types.Integer,
+        Column(u'song_id', types.Integer(),
             ForeignKey(u'songs.id'), primary_key=True),
-        Column(u'topic_id', types.Integer,
+        Column(u'topic_id', types.Integer(),
             ForeignKey(u'topics.id'), primary_key=True)
     )
 
@@ -238,8 +228,7 @@
             'authors': relation(Author, backref='songs',
                 secondary=authors_songs_table, lazy=False),
             'book': relation(Book, backref='songs'),
-            'media_files': relation(MediaFile, backref='songs',
-                secondary=media_files_songs_table),
+            'media_files': relation(MediaFile, backref='songs'),
             'topics': relation(Topic, backref='songs',
                 secondary=songs_topics_table)
         })

=== added file 'openlp/plugins/songs/lib/upgrade.py'
--- openlp/plugins/songs/lib/upgrade.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/songs/lib/upgrade.py	2011-08-25 20:16:24 +0000
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2011 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan      #
+# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan,      #
+# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias     #
+# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith,    #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund             #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`upgrade` module provides a way for the database and schema that is the backend for
+the Songs plugin
+"""
+
+from sqlalchemy import Column, ForeignKey, Table, types
+from migrate import changeset
+from migrate.changeset.constraint import ForeignKeyConstraint
+
+__version__ = 1
+
+def upgrade_setup(metadata):
+    """
+    Set up the latest revision all tables, with reflection, needed for the
+    upgrade process. If you want to drop a table, you need to remove it from
+    here, and add it to your upgrade function.
+    """
+    tables = {
+        u'authors': Table(u'authors', metadata, autoload=True),
+        u'media_files': Table(u'media_files', metadata, autoload=True),
+        u'song_books': Table(u'song_books', metadata, autoload=True),
+        u'songs': Table(u'songs', metadata, autoload=True),
+        u'topics': Table(u'topics', metadata, autoload=True),
+        u'authors_songs': Table(u'authors_songs', metadata, autoload=True),
+        u'songs_topics': Table(u'songs_topics', metadata, autoload=True)
+    }
+    return tables
+
+
+def upgrade_1(session, metadata, tables):
+    """
+    Version 1 upgrade.
+
+    This upgrade removes the many-to-many relationship between songs and 
+    media_files and replaces it with a one-to-many, which is far more
+    representative of the real relationship between the two entities.
+
+    In order to facilitate this one-to-many relationship, a song_id column is
+    added to the media_files table, and a weight column so that the media
+    files can be ordered.
+    """
+    Table(u'media_files_songs', metadata, autoload=True).drop(checkfirst=True)
+    Column(u'song_id', types.Integer(), default=None)\
+        .create(table=tables[u'media_files'], populate_default=True)
+    Column(u'weight', types.Integer(), default=0)\
+        .create(table=tables[u'media_files'], populate_default=True)
+    if metadata.bind.url.get_dialect().name != 'sqlite':
+        # SQLite doesn't support ALTER TABLE ADD CONSTRAINT
+        ForeignKeyConstraint([u'song_id'], [u'songs.id'],
+            table=tables[u'media_files']).create()
+

=== modified file 'openlp/plugins/songs/songsplugin.py'
--- openlp/plugins/songs/songsplugin.py	2011-07-23 21:29:24 +0000
+++ openlp/plugins/songs/songsplugin.py	2011-08-25 20:16:24 +0000
@@ -36,7 +36,8 @@
 from openlp.core.lib.db import Manager
 from openlp.core.lib.ui import UiStrings, base_action, icon_action
 from openlp.core.utils.actions import ActionList
-from openlp.plugins.songs.lib import clean_song, SongMediaItem, SongsTab
+from openlp.plugins.songs.lib import clean_song, upgrade, SongMediaItem, \
+    SongsTab
 from openlp.plugins.songs.lib.db import init_schema, Song
 from openlp.plugins.songs.lib.importer import SongFormat
 from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
@@ -58,8 +59,8 @@
         Create and set up the Songs plugin.
         """
         Plugin.__init__(self, u'songs', plugin_helpers, SongMediaItem, SongsTab)
+        self.manager = Manager(u'songs', init_schema, upgrade_mod=upgrade)
         self.weight = -10
-        self.manager = Manager(u'songs', init_schema)
         self.icon_path = u':/plugins/plugin_songs.png'
         self.icon = build_icon(self.icon_path)
 

=== modified file 'resources/debian/debian/control'
--- resources/debian/debian/control	2011-03-09 06:55:41 +0000
+++ resources/debian/debian/control	2011-08-25 20:16:24 +0000
@@ -11,7 +11,7 @@
 Architecture: all
 Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, python-qt4,
  python-qt4-phonon, python-sqlalchemy, python-chardet, python-beautifulsoup,
- python-lxml, python-sqlite, python-enchant
+ python-lxml, python-sqlite, python-enchant, python-migrate
 Conflicts: python-openlp
 Description: Church lyrics projection application
  OpenLP is free church presentation software, or lyrics projection software,

=== modified file 'scripts/check_dependencies.py'
--- scripts/check_dependencies.py	2011-07-15 17:38:09 +0000
+++ scripts/check_dependencies.py	2011-08-25 20:16:24 +0000
@@ -46,14 +46,14 @@
     'sqlalchemy': '0.5',
     # pyenchant 1.6 required on Windows
     'enchant': '1.6' if is_win else '1.3'
-    }
+}
 
 # pywin32
 WIN32_MODULES = [
     'win32com',
     'win32ui',
     'pywintypes',
-    ]
+]
 
 MODULES = [
     'PyQt4',
@@ -72,7 +72,8 @@
     'enchant',
     'BeautifulSoup',
     'mako',
-    ]
+    'migrate',
+]
 
 
 OPTIONAL_MODULES = [


Follow ups