Changeset 12069


Ignore:
Timestamp:
Sep 27, 2012, 11:06:58 PM (12 years ago)
Author:
Steffen Hoffmann
Message:

TagsPlugin: Re-write db schema setup procedures, refs #9521.

Since early schema change before tags-0.2 in 2006 (see [1750]) this plugins
schema check relied on a SELECT raising an exeption for non-existing db table.
This has been discussed lately and found to be a flawed approach, that even
breaks installations for Trac 0.13dev and ultimately 1.0 too.

Now we introduce the common, recommended approach of tracking plugin db schema
versions in Trac db table system, so the table existence test is called one
last time, current schema version set, and only this gets checked further on.

This changeset requires a database upgrade.

Old checks could even be finally dropped after the next stable release,
because there's likely no pre-tags-0.2 installation left out there, isn't it?

Location:
tagsplugin/trunk
Files:
6 added
10 edited
1 moved

Legend:

Unmodified
Added
Removed
  • tagsplugin/trunk/changelog

    r11932 r12069  
    11Originally created by Muness Alrubaie, totally rewritten by
    22Author: Alec Thomas <alec@swapoff.org>
    3 Maintainer: n.n.
     3Maintainer: Steffen Hoffmann <hoff.st@web.de>
    44
    55tractags-0.7 (not yet released)
     
    3636 * #9062: Wiki-page level-1-title broken on 0.12
    3737 * #9210: /tags page should not have a contextual navigation link 'Cloud'
     38 * #9521: New install impossible on Trac 0.13dev
     39   by adding generic db schema upgrade support
    3840 * TagsQuery now supports a context object
    3941 * refactor cloud rendering so it can be used by other plugins
  • tagsplugin/trunk/tractags/__init__.py

    r10780 r12069  
    22#
    33# Copyright (C) 2006 Alec Thomas <alec@swapoff.org>
     4# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
    45#
    56# This software is licensed as described in the file COPYING, which
     
    1213
    1314import api
     15import db
    1416import wiki
    1517import ticket
    1618import macros
    1719import web_ui
    18 import model
    1920import admin
  • tagsplugin/trunk/tractags/model.py

    r11984 r12069  
    1 from trac.core import Component, TracError, implements
    2 from trac.env import IEnvironmentSetupParticipant
    3 from trac.db import Table, Column, Index
    4 from trac.db.api import DatabaseManager
    5 
    6 
    7 class TagModelProvider(Component):
    8 
    9     implements(IEnvironmentSetupParticipant)
    10 
    11     SCHEMA = [
    12         Table('tags', key = ('tagspace', 'name', 'tag'))[
    13               Column('tagspace'),
    14               Column('name'),
    15               Column('tag'),
    16               Index(['tagspace', 'name']),
    17               Index(['tagspace', 'tag']),]
    18         ]
    19     def __init__(self):
    20         # Preemptive check for rollback tolerance of read-only db connections.
    21         # This is required to avoid breaking `environment_needs_upgrade`,
    22         #   if the plugin uses intentional db transaction errors for the test.
    23         self.rollback_is_safe = True
    24         try:
    25             db = DatabaseManager(self.env).get_connection()
    26             if hasattr(db, 'readonly'):
    27                 db = DatabaseManager(self.env).get_connection(readonly=True)
    28                 cursor = db.cursor()
    29                 # Test needed for rollback on read-only connections.
    30                 cursor.execute("SELECT COUNT(*) FROM system")
    31                 cursor.fetchone()
    32                 try:
    33                     db.rollback()
    34                 except AttributeError:
    35                     # Avoid rollback on read-only connections.
    36                     self.rollback_is_safe = False
    37                     return
    38                 # Test passed.
    39         except TracError, e:
    40             # Trac too old - expect no constraints.
    41             return
    42 
    43     # IEnvironmentSetupParticipant methods
    44     def environment_created(self):
    45         self._upgrade_db(self.env.get_db_cnx())
    46 
    47     def environment_needs_upgrade(self, db):
    48         if self._need_migration(db):
    49             return True
    50         try:
    51             cursor = db.cursor()
    52             cursor.execute("SELECT COUNT(*) FROM tags")
    53             cursor.fetchone()
    54             return False
    55         except Exception, e:
    56             self.log.error("DatabaseError: %s", e)
    57             if self.rollback_is_safe:
    58                 db.rollback()
    59             return True
    60 
    61     def upgrade_environment(self, db):
    62         self._upgrade_db(db)
    63 
    64     def _need_migration(self, db):
    65         cursor = db.cursor()
    66         # Special handling for the PostgreSQL Trac db backend.
    67         if self.env.config.get('trac', 'database').startswith('postgres'):
    68             cursor.execute("""
    69                 SELECT relname
    70                   FROM pg_class
    71                  WHERE relname = 'wiki_namespace'
    72             """)
    73             if cursor.fetchone() is not None:
    74                 self.env.log.debug("tractags needs to migrate old data")
    75                 return True
    76             else:
    77                 return False
    78         try:
    79             cursor.execute("SELECT COUNT(*) FROM wiki_namespace")
    80             cursor.fetchone()
    81             self.env.log.debug("tractags needs to migrate old data")
    82             return True
    83         except Exception, e:
    84             # The expected outcome for any up-to-date installation.
    85             if self.rollback_is_safe:
    86                 db.rollback()
    87             return False
    88 
    89     def _upgrade_db(self, db):
    90         try:
    91             try:
    92                 from trac.db import DatabaseManager
    93                 db_backend, _ = DatabaseManager(self.env)._get_connector()
    94             except ImportError:
    95                 db_backend = self.env.get_db_cnx()
    96 
    97             cursor = db.cursor()
    98             for table in self.SCHEMA:
    99                 for stmt in db_backend.to_sql(table):
    100                     self.env.log.debug(stmt)
    101                     cursor.execute(stmt)
    102             db.commit()
    103 
    104             # Migrate old data
    105             if self._need_migration(db):
    106                 cursor = db.cursor()
    107                 cursor.execute("""
    108                     INSERT INTO tags
    109                             (tagspace, name, tag)
    110                         SELECT 'wiki', name, namespace
    111                         FROM    wiki_namespace
    112                     """)
    113                 cursor.execute("DROP TABLE wiki_namespace")
    114                 db.commit()
    115         except Exception, e:
    116             self.log.error("DatabaseError: %s", e)
    117             db.rollback()
    118             raise
    119 
     1#!/usr/bin/env python
     2# -*- coding: utf-8 -*-
     3#
     4# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
     5#
     6# This software is licensed as described in the file COPYING, which
     7# you should have received as part of this distribution.
  • tagsplugin/trunk/tractags/tests/__init__.py

    r10788 r12069  
    22#
    33# Copyright (C) 2011 Odd Simon Simonsen <oddsimons@gmail.com>
     4# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
    45#
    56# This software is licensed as described in the file COPYING, which
     
    1213def test_suite():
    1314    suite = unittest.TestSuite()
    14    
     15
    1516    import tractags.tests.admin
    1617    suite.addTest(tractags.tests.admin.test_suite())
    17    
     18
    1819    import tractags.tests.api
    1920    suite.addTest(tractags.tests.api.test_suite())
    20    
     21
     22    import tractags.tests.db
     23    suite.addTest(tractags.tests.db.test_suite())
     24
    2125    import tractags.tests.macros
    2226    suite.addTest(tractags.tests.macros.test_suite())
    23    
    24     import tractags.tests.model
    25     suite.addTest(tractags.tests.model.test_suite())
    26    
     27
    2728    import tractags.tests.query
    2829    suite.addTest(tractags.tests.query.test_suite())
    29    
     30
    3031    import tractags.tests.ticket
    3132    suite.addTest(tractags.tests.ticket.test_suite())
    32    
     33
    3334    import tractags.tests.web_ui
    3435    suite.addTest(tractags.tests.web_ui.test_suite())
    35    
     36
    3637    import tractags.tests.wiki
    3738    suite.addTest(tractags.tests.wiki.test_suite())
    38    
     39
    3940    return suite
    4041
     
    4445if __name__ == '__main__':
    4546    unittest.main(defaultTest='test_suite')
    46 
  • tagsplugin/trunk/tractags/tests/admin.py

    r10670 r12069  
    22#
    33# Copyright (C) 2011 Odd Simon Simonsen <oddsimons@gmail.com>
     4# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
    45#
    56# This software is licensed as described in the file COPYING, which
     
    78#
    89
    9 import unittest
    1010import shutil
    1111import tempfile
     12import unittest
    1213
    1314from trac.test import EnvironmentStub, Mock
    1415
    15 from tractags.model import TagModelProvider
    1616from tractags.admin import TagChangeAdminPanel
    1717
    1818
    1919class TagChangeAdminPanelTestCase(unittest.TestCase):
    20    
     20
    2121    def setUp(self):
    2222        self.env = EnvironmentStub(
    2323                enable=['trac.*', 'tractags.*'])
    2424        self.env.path = tempfile.mkdtemp()
    25         TagModelProvider(self.env).environment_created()
    26        
     25
    2726        self.tag_cap = TagChangeAdminPanel(self.env)
    28    
     27
    2928    def tearDown(self):
    3029        shutil.rmtree(self.env.path)
    31    
     30
    3231    def test_init(self):
    3332        # Empty test just to confirm that setUp and tearDown works
  • tagsplugin/trunk/tractags/tests/api.py

    r10670 r12069  
    22#
    33# Copyright (C) 2011 Odd Simon Simonsen <oddsimons@gmail.com>
     4# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
    45#
    56# This software is licensed as described in the file COPYING, which
     
    78#
    89
    9 import unittest
    1010import shutil
    1111import tempfile
     12import unittest
    1213
    1314from trac.test import EnvironmentStub, Mock
    1415
    15 from tractags.model import TagModelProvider
    1616from tractags.api import TagSystem
    1717
    1818
    1919class TagSystemTestCase(unittest.TestCase):
    20    
     20
    2121    def setUp(self):
    2222        self.env = EnvironmentStub(
    2323                enable=['trac.*', 'tractags.*'])
    2424        self.env.path = tempfile.mkdtemp()
    25         TagModelProvider(self.env).environment_created()
    26        
     25
    2726        self.tag_s = TagSystem(self.env)
    28    
     27
    2928    def tearDown(self):
    3029        shutil.rmtree(self.env.path)
    31    
     30
    3231    def test_init(self):
    3332        # Empty test just to confirm that setUp and tearDown works
  • tagsplugin/trunk/tractags/tests/db.py

    r12068 r12069  
    22#
    33# Copyright (C) 2011 Odd Simon Simonsen <oddsimons@gmail.com>
     4# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
    45#
    56# This software is licensed as described in the file COPYING, which
     
    78#
    89
    9 import unittest
    1010import shutil
    1111import tempfile
     12import unittest
    1213
     14from trac import __version__ as trac_version
     15from trac.db import Table, Column, Index
     16from trac.db.api import DatabaseManager
    1317from trac.test import EnvironmentStub, Mock
    1418
    15 from tractags.model import TagModelProvider
     19from tractags import db_default
     20from tractags.db import TagSetup
    1621
    17 class TagsProviderTestCase(unittest.TestCase):
    18    
     22
     23class TagSetupTestCase(unittest.TestCase):
     24
    1925    def setUp(self):
    2026        self.env = EnvironmentStub(
    21                 enable=['trac.*', 'tractags.*'])
     27                enable=['trac.*'])
    2228        self.env.path = tempfile.mkdtemp()
    23         self.tag_mp = TagModelProvider(self.env)
    24         self.tag_mp.environment_created()
    25    
     29        # Initialize default (SQLite) db in memory.
     30        self.db = DatabaseManager(self.env)
     31        # Workaround required for Trac 0.11 up to 0.12.4 .
     32        if trac_version < '0.13dev':
     33            self.db.init_db()
     34
    2635    def tearDown(self):
    2736        shutil.rmtree(self.env.path)
    28    
     37
    2938    # Helpers
    30    
     39
    3140    def _get_cursor_description(self, cursor):
    3241        # Cursors don't look the same across Trac versions
    33         from trac import __version__ as trac_version
    3442        if trac_version < '0.12':
    3543            return cursor.description
    3644        else:
    3745            return cursor.cursor.description
    38    
     46
    3947    # Tests
    40    
    41     def test_table_exists(self):
     48
     49    def test_new_install(self):
    4250        db = self.env.get_db_cnx()
     51        setup = TagSetup(self.env)
     52        self.assertEquals(0, setup.get_schema_version(db))
     53
     54        setup.upgrade_environment(self.env.get_db_cnx())
    4355        cursor = db.cursor()
    4456        tags = cursor.execute("SELECT * FROM tags").fetchall()
     
    4658        self.assertEquals(['tagspace', 'name', 'tag'],
    4759                [col[0] for col in self._get_cursor_description(cursor)])
     60        version = cursor.execute("""
     61                      SELECT value
     62                        FROM system
     63                       WHERE name='tags_version'
     64                  """).fetchone()
     65        self.assertEquals(db_default.schema_version, int(version[0]))
     66
     67    def test_upgrade_schema_v1(self):
     68        # Ancient, unversioned schema - wiki only.
     69        schema = [
     70            Table('wiki_namespace')[
     71                Column('name'),
     72                Column('namespace'),
     73                Index(['name', 'namespace']),
     74            ]
     75        ]
     76        connector = self.db._get_connector()[0]
     77        db = self.env.get_db_cnx()
     78        cursor = db.cursor()
     79
     80        for table in schema:
     81            for stmt in connector.to_sql(table):
     82                cursor.execute(stmt)
     83        # Populate table with migration test data.
     84        cursor.execute("""
     85            INSERT INTO wiki_namespace
     86                        (name, namespace)
     87                 VALUES ('WikiStart', 'tag')
     88            """)
     89        # Current tractags schema is setup with enabled component anyway.
     90        cursor.execute("DROP TABLE IF EXISTS tags")
     91
     92        tags = cursor.execute("SELECT * FROM wiki_namespace").fetchall()
     93        self.assertEquals([('WikiStart', 'tag')], tags)
     94        setup = TagSetup(self.env)
     95        self.assertEquals(1, setup.get_schema_version(db))
     96
     97        setup.upgrade_environment(self.env.get_db_cnx())
     98        cursor = db.cursor()
     99        tags = cursor.execute("SELECT * FROM tags").fetchall()
     100        # Db content should be migrated.
     101        self.assertEquals([('wiki', 'WikiStart', 'tag')], tags)
     102        self.assertEquals(['tagspace', 'name', 'tag'],
     103                [col[0] for col in self._get_cursor_description(cursor)])
     104        version = cursor.execute("""
     105                      SELECT value
     106                        FROM system
     107                       WHERE name='tags_version'
     108                  """).fetchone()
     109        self.assertEquals(db_default.schema_version, int(version[0]))
     110
     111    def test_upgrade_schema_v2(self):
     112        # Just register a current, but unversioned schema.
     113        schema = [
     114            Table('tags', key=('tagspace', 'name', 'tag'))[
     115                Column('tagspace'),
     116                Column('name'),
     117                Column('tag'),
     118                Index(['tagspace', 'name']),
     119                Index(['tagspace', 'tag']),
     120            ]
     121        ]
     122        connector = self.db._get_connector()[0]
     123        db = self.env.get_db_cnx()
     124        cursor = db.cursor()
     125        # Current tractags schema is setup with enabled component anyway.
     126        cursor.execute("DROP TABLE IF EXISTS tags")
     127
     128        for table in schema:
     129            for stmt in connector.to_sql(table):
     130                cursor.execute(stmt)
     131        # Populate table with test data.
     132        cursor.execute("""
     133            INSERT INTO tags
     134                        (tagspace, name, tag)
     135                 VALUES ('wiki', 'WikiStart', 'tag')
     136            """)
     137
     138        tags = cursor.execute("SELECT * FROM tags").fetchall()
     139        self.assertEquals([('wiki', 'WikiStart', 'tag')], tags)
     140        setup = TagSetup(self.env)
     141        self.assertEquals(2, setup.get_schema_version(db))
     142
     143        setup.upgrade_environment(self.env.get_db_cnx())
     144        cursor = db.cursor()
     145        tags = cursor.execute("SELECT * FROM tags").fetchall()
     146        # Db should be unchanged.
     147        self.assertEquals([('wiki', 'WikiStart', 'tag')], tags)
     148        self.assertEquals(['tagspace', 'name', 'tag'],
     149                [col[0] for col in self._get_cursor_description(cursor)])
     150        version = cursor.execute("""
     151                      SELECT value
     152                        FROM system
     153                       WHERE name='tags_version'
     154                  """).fetchone()
     155        self.assertEquals(db_default.schema_version, int(version[0]))
    48156
    49157
    50158def test_suite():
    51159    suite = unittest.TestSuite()
    52     suite.addTest(unittest.makeSuite(TagsProviderTestCase, 'test'))
     160    suite.addTest(unittest.makeSuite(TagSetupTestCase, 'test'))
    53161    return suite
    54162
  • tagsplugin/trunk/tractags/tests/macros.py

    r11934 r12069  
    22#
    33# Copyright (C) 2011 Odd Simon Simonsen <oddsimons@gmail.com>
     4# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
    45#
    56# This software is licensed as described in the file COPYING, which
     
    78#
    89
    9 import unittest
    1010import shutil
    1111import tempfile
     12import unittest
    1213
    1314from trac.test import EnvironmentStub, Mock
     
    1516from trac.web.href import Href
    1617
    17 from tractags.model import TagModelProvider
    1818from tractags.macros import TagTemplateProvider, TagWikiMacros
    1919
     
    2525                enable=['trac.*', 'tractags.*'])
    2626        self.env.path = tempfile.mkdtemp()
    27         TagModelProvider(self.env).environment_created()
    2827
    2928        # TagTemplateProvider is abstract, test using a subclass
     
    4443                enable=['trac.*', 'tractags.*'])
    4544        self.env.path = tempfile.mkdtemp()
    46         TagModelProvider(self.env).environment_created()
    4745        PermissionSystem(self.env).grant_permission('user', 'TAGS_VIEW')
    48        
     46
    4947        self.tag_twm = TagWikiMacros(self.env)
    50    
     48
    5149    def tearDown(self):
    5250        shutil.rmtree(self.env.path)
    53    
     51
    5452    def test_empty_content(self):
    5553        req = Mock(args={},
     
    6563                str(self.tag_twm.expand_macro(formatter, 'ListTagged', '')))
    6664
     65
    6766class TagCloudMacroTestCase(unittest.TestCase):
    68    
     67
    6968    def setUp(self):
    7069        self.env = EnvironmentStub(
    7170                enable=['trac.*', 'tractags.*'])
    7271        self.env.path = tempfile.mkdtemp()
    73         TagModelProvider(self.env).environment_created()
    74        
     72
    7573        self.tag_twm = TagWikiMacros(self.env)
    76    
     74
    7775    def tearDown(self):
    7876        shutil.rmtree(self.env.path)
    79    
     77
    8078    def test_init(self):
    8179        # Empty test just to confirm that setUp and tearDown works
  • tagsplugin/trunk/tractags/tests/ticket.py

    r10670 r12069  
    22#
    33# Copyright (C) 2011 Odd Simon Simonsen <oddsimons@gmail.com>
     4# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
    45#
    56# This software is licensed as described in the file COPYING, which
     
    78#
    89
    9 import unittest
    1010import shutil
    1111import tempfile
     12import unittest
    1213
    1314from trac.test import EnvironmentStub, Mock
    1415
    15 from tractags.model import TagModelProvider
    1616from tractags.ticket import TicketTagProvider
    1717
    1818
    1919class TicketTagProviderTestCase(unittest.TestCase):
    20    
     20
    2121    def setUp(self):
    2222        self.env = EnvironmentStub(
    2323                enable=['trac.*', 'tractags.*'])
    2424        self.env.path = tempfile.mkdtemp()
    25         TagModelProvider(self.env).environment_created()
    26        
     25
    2726        self.tag_tp = TicketTagProvider(self.env)
    28    
     27
    2928    def tearDown(self):
    3029        shutil.rmtree(self.env.path)
    31    
     30
    3231    def test_init(self):
    3332        # Empty test just to confirm that setUp and tearDown works
  • tagsplugin/trunk/tractags/tests/web_ui.py

    r11935 r12069  
    88#
    99
    10 import unittest
    1110import shutil
    1211import tempfile
     12import unittest
    1313
    1414from trac.test import EnvironmentStub, Mock
     
    1818
    1919from tractags.api import TagSystem
    20 from tractags.model import TagModelProvider
    2120from tractags.web_ui import TagRequestHandler
    2221
    2322
    2423class TagRequestHandlerTestCase(unittest.TestCase):
    25    
     24
    2625    def setUp(self):
    2726        self.env = EnvironmentStub(
    2827                enable=['trac.*', 'tractags.*'])
    2928        self.env.path = tempfile.mkdtemp()
    30         TagModelProvider(self.env).environment_created()
    31        
     29
    3230        self.tag_s = TagSystem(self.env)
    3331        self.tag_rh = TagRequestHandler(self.env)
    34        
     32
    3533        perm_system = PermissionSystem(self.env)
    3634        self.anonymous = PermissionCache(self.env, 'anonymous')
     
    4139        self.admin = PermissionCache(self.env, 'admin')
    4240        perm_system.grant_permission('admin', 'TAGS_ADMIN')
    43        
     41
    4442        self.href = Href('/trac')
    4543        self.abs_href = Href('http://example.org/trac')
    46    
     44
    4745    def tearDown(self):
    4846        shutil.rmtree(self.env.path)
    49    
     47
    5048    def test_matches(self):
    5149        req = Mock(path_info='/tags',
     
    5452                  )
    5553        self.assertEquals(True, self.tag_rh.match_request(req))
    56    
     54
    5755    def test_matches_no_permission(self):
    5856        req = Mock(path_info='/tags',
     
    6159                  )
    6260        self.assertEquals(False, self.tag_rh.match_request(req))
    63    
     61
    6462    def test_get_main_page(self):
    6563        req = Mock(path_info='/tags',
  • tagsplugin/trunk/tractags/tests/wiki.py

    r10670 r12069  
    22#
    33# Copyright (C) 2011 Odd Simon Simonsen <oddsimons@gmail.com>
     4# Copyright (C) 2012 Steffen Hoffmann <hoff.st@web.de>
    45#
    56# This software is licensed as described in the file COPYING, which
     
    78#
    89
    9 import unittest
    1010import shutil
    1111import tempfile
     12import unittest
    1213
    1314from trac.test import EnvironmentStub, Mock
    1415
    15 from tractags.model import TagModelProvider
    1616from tractags.wiki import WikiTagProvider
    1717
    1818
    1919class WikiTagProviderTestCase(unittest.TestCase):
    20    
     20
    2121    def setUp(self):
    2222        self.env = EnvironmentStub(
    2323                enable=['trac.*', 'tractags.*'])
    2424        self.env.path = tempfile.mkdtemp()
    25         TagModelProvider(self.env).environment_created()
    26        
     25
    2726        self.tag_wp = WikiTagProvider(self.env)
    28    
     27
    2928    def tearDown(self):
    3029        shutil.rmtree(self.env.path)
    31    
     30
    3231    def test_init(self):
    3332        # Empty test just to confirm that setUp and tearDown works
Note: See TracChangeset for help on using the changeset viewer.