##// END OF EJS Templates
Synced with latest sqlalchemy-migrate, added new upcomming migration for 1.3
marcink -
r1632:5b2cf21b beta
parent child Browse files
Show More
@@ -0,0 +1,24 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.model.db
4 ~~~~~~~~~~~~~~~~~~
5
6 Database Models for RhodeCode
7
8 :created_on: Apr 08, 2010
9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>. No newline at end of file
@@ -0,0 +1,28 b''
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper
7 from sqlalchemy.orm.session import Session
8
9 from rhodecode.lib.dbmigrate.migrate import *
10 from rhodecode.lib.dbmigrate.migrate.changeset import *
11
12 from rhodecode.model.meta import Base
13
14 log = logging.getLogger(__name__)
15
16 def upgrade(migrate_engine):
17 """ Upgrade operations go here.
18 Don't create your own engine; bind migrate_engine to your metadata
19 """
20
21
22
23 return
24
25
26 def downgrade(migrate_engine):
27 meta = MetaData()
28 meta.bind = migrate_engine
@@ -1,11 +1,11 b''
1 """
1 """
2 SQLAlchemy migrate provides two APIs :mod:`migrate.versioning` for
2 SQLAlchemy migrate provides two APIs :mod:`migrate.versioning` for
3 database schema version and repository management and
3 database schema version and repository management and
4 :mod:`migrate.changeset` that allows to define database schema changes
4 :mod:`migrate.changeset` that allows to define database schema changes
5 using Python.
5 using Python.
6 """
6 """
7
7
8 from rhodecode.lib.dbmigrate.migrate.versioning import *
8 from rhodecode.lib.dbmigrate.migrate.versioning import *
9 from rhodecode.lib.dbmigrate.migrate.changeset import *
9 from rhodecode.lib.dbmigrate.migrate.changeset import *
10
10
11 __version__ = '0.7.2.dev' No newline at end of file
11 __version__ = '0.7.3.dev' No newline at end of file
@@ -1,155 +1,155 b''
1 """
1 """
2 `SQLite`_ database specific implementations of changeset classes.
2 `SQLite`_ database specific implementations of changeset classes.
3
3
4 .. _`SQLite`: http://www.sqlite.org/
4 .. _`SQLite`: http://www.sqlite.org/
5 """
5 """
6 from UserDict import DictMixin
6 from UserDict import DictMixin
7 from copy import copy
7 from copy import copy
8
8
9 from sqlalchemy.databases import sqlite as sa_base
9 from sqlalchemy.databases import sqlite as sa_base
10
10
11 from rhodecode.lib.dbmigrate.migrate import exceptions
11 from rhodecode.lib.dbmigrate.migrate import exceptions
12 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
12 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
13
13
14
14
15 if not SQLA_06:
15 if not SQLA_06:
16 SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator
16 SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator
17 else:
17 else:
18 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
18 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
19
19
20 class SQLiteCommon(object):
20 class SQLiteCommon(object):
21
21
22 def _not_supported(self, op):
22 def _not_supported(self, op):
23 raise exceptions.NotSupportedError("SQLite does not support "
23 raise exceptions.NotSupportedError("SQLite does not support "
24 "%s; see http://www.sqlite.org/lang_altertable.html" % op)
24 "%s; see http://www.sqlite.org/lang_altertable.html" % op)
25
25
26
26
27 class SQLiteHelper(SQLiteCommon):
27 class SQLiteHelper(SQLiteCommon):
28
28
29 def recreate_table(self,table,column=None,delta=None):
29 def recreate_table(self,table,column=None,delta=None):
30 table_name = self.preparer.format_table(table)
30 table_name = self.preparer.format_table(table)
31
31
32 # we remove all indexes so as not to have
32 # we remove all indexes so as not to have
33 # problems during copy and re-create
33 # problems during copy and re-create
34 for index in table.indexes:
34 for index in table.indexes:
35 index.drop()
35 index.drop()
36
36
37 self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
37 self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
38 self.execute()
38 self.execute()
39
39
40 insertion_string = self._modify_table(table, column, delta)
40 insertion_string = self._modify_table(table, column, delta)
41
41
42 table.create()
42 table.create(bind=self.connection)
43 self.append(insertion_string % {'table_name': table_name})
43 self.append(insertion_string % {'table_name': table_name})
44 self.execute()
44 self.execute()
45 self.append('DROP TABLE migration_tmp')
45 self.append('DROP TABLE migration_tmp')
46 self.execute()
46 self.execute()
47
47
48 def visit_column(self, delta):
48 def visit_column(self, delta):
49 if isinstance(delta, DictMixin):
49 if isinstance(delta, DictMixin):
50 column = delta.result_column
50 column = delta.result_column
51 table = self._to_table(delta.table)
51 table = self._to_table(delta.table)
52 else:
52 else:
53 column = delta
53 column = delta
54 table = self._to_table(column.table)
54 table = self._to_table(column.table)
55 self.recreate_table(table,column,delta)
55 self.recreate_table(table,column,delta)
56
56
57 class SQLiteColumnGenerator(SQLiteSchemaGenerator,
57 class SQLiteColumnGenerator(SQLiteSchemaGenerator,
58 ansisql.ANSIColumnGenerator,
58 ansisql.ANSIColumnGenerator,
59 # at the end so we get the normal
59 # at the end so we get the normal
60 # visit_column by default
60 # visit_column by default
61 SQLiteHelper,
61 SQLiteHelper,
62 SQLiteCommon
62 SQLiteCommon
63 ):
63 ):
64 """SQLite ColumnGenerator"""
64 """SQLite ColumnGenerator"""
65
65
66 def _modify_table(self, table, column, delta):
66 def _modify_table(self, table, column, delta):
67 columns = ' ,'.join(map(
67 columns = ' ,'.join(map(
68 self.preparer.format_column,
68 self.preparer.format_column,
69 [c for c in table.columns if c.name!=column.name]))
69 [c for c in table.columns if c.name!=column.name]))
70 return ('INSERT INTO %%(table_name)s (%(cols)s) '
70 return ('INSERT INTO %%(table_name)s (%(cols)s) '
71 'SELECT %(cols)s from migration_tmp')%{'cols':columns}
71 'SELECT %(cols)s from migration_tmp')%{'cols':columns}
72
72
73 def visit_column(self,column):
73 def visit_column(self,column):
74 if column.foreign_keys:
74 if column.foreign_keys:
75 SQLiteHelper.visit_column(self,column)
75 SQLiteHelper.visit_column(self,column)
76 else:
76 else:
77 super(SQLiteColumnGenerator,self).visit_column(column)
77 super(SQLiteColumnGenerator,self).visit_column(column)
78
78
79 class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
79 class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
80 """SQLite ColumnDropper"""
80 """SQLite ColumnDropper"""
81
81
82 def _modify_table(self, table, column, delta):
82 def _modify_table(self, table, column, delta):
83
83
84 columns = ' ,'.join(map(self.preparer.format_column, table.columns))
84 columns = ' ,'.join(map(self.preparer.format_column, table.columns))
85 return 'INSERT INTO %(table_name)s SELECT ' + columns + \
85 return 'INSERT INTO %(table_name)s SELECT ' + columns + \
86 ' from migration_tmp'
86 ' from migration_tmp'
87
87
88 def visit_column(self,column):
88 def visit_column(self,column):
89 # For SQLite, we *have* to remove the column here so the table
89 # For SQLite, we *have* to remove the column here so the table
90 # is re-created properly.
90 # is re-created properly.
91 column.remove_from_table(column.table,unset_table=False)
91 column.remove_from_table(column.table,unset_table=False)
92 super(SQLiteColumnDropper,self).visit_column(column)
92 super(SQLiteColumnDropper,self).visit_column(column)
93
93
94
94
95 class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
95 class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
96 """SQLite SchemaChanger"""
96 """SQLite SchemaChanger"""
97
97
98 def _modify_table(self, table, column, delta):
98 def _modify_table(self, table, column, delta):
99 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
99 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
100
100
101 def visit_index(self, index):
101 def visit_index(self, index):
102 """Does not support ALTER INDEX"""
102 """Does not support ALTER INDEX"""
103 self._not_supported('ALTER INDEX')
103 self._not_supported('ALTER INDEX')
104
104
105
105
106 class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon):
106 class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon):
107
107
108 def visit_migrate_primary_key_constraint(self, constraint):
108 def visit_migrate_primary_key_constraint(self, constraint):
109 tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )"
109 tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )"
110 cols = ', '.join(map(self.preparer.format_column, constraint.columns))
110 cols = ', '.join(map(self.preparer.format_column, constraint.columns))
111 tname = self.preparer.format_table(constraint.table)
111 tname = self.preparer.format_table(constraint.table)
112 name = self.get_constraint_name(constraint)
112 name = self.get_constraint_name(constraint)
113 msg = tmpl % (name, tname, cols)
113 msg = tmpl % (name, tname, cols)
114 self.append(msg)
114 self.append(msg)
115 self.execute()
115 self.execute()
116
116
117 def _modify_table(self, table, column, delta):
117 def _modify_table(self, table, column, delta):
118 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
118 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
119
119
120 def visit_migrate_foreign_key_constraint(self, *p, **k):
120 def visit_migrate_foreign_key_constraint(self, *p, **k):
121 self.recreate_table(p[0].table)
121 self.recreate_table(p[0].table)
122
122
123 def visit_migrate_unique_constraint(self, *p, **k):
123 def visit_migrate_unique_constraint(self, *p, **k):
124 self.recreate_table(p[0].table)
124 self.recreate_table(p[0].table)
125
125
126
126
127 class SQLiteConstraintDropper(ansisql.ANSIColumnDropper,
127 class SQLiteConstraintDropper(ansisql.ANSIColumnDropper,
128 SQLiteCommon,
128 SQLiteCommon,
129 ansisql.ANSIConstraintCommon):
129 ansisql.ANSIConstraintCommon):
130
130
131 def visit_migrate_primary_key_constraint(self, constraint):
131 def visit_migrate_primary_key_constraint(self, constraint):
132 tmpl = "DROP INDEX %s "
132 tmpl = "DROP INDEX %s "
133 name = self.get_constraint_name(constraint)
133 name = self.get_constraint_name(constraint)
134 msg = tmpl % (name)
134 msg = tmpl % (name)
135 self.append(msg)
135 self.append(msg)
136 self.execute()
136 self.execute()
137
137
138 def visit_migrate_foreign_key_constraint(self, *p, **k):
138 def visit_migrate_foreign_key_constraint(self, *p, **k):
139 self._not_supported('ALTER TABLE DROP CONSTRAINT')
139 self._not_supported('ALTER TABLE DROP CONSTRAINT')
140
140
141 def visit_migrate_check_constraint(self, *p, **k):
141 def visit_migrate_check_constraint(self, *p, **k):
142 self._not_supported('ALTER TABLE DROP CONSTRAINT')
142 self._not_supported('ALTER TABLE DROP CONSTRAINT')
143
143
144 def visit_migrate_unique_constraint(self, *p, **k):
144 def visit_migrate_unique_constraint(self, *p, **k):
145 self._not_supported('ALTER TABLE DROP CONSTRAINT')
145 self._not_supported('ALTER TABLE DROP CONSTRAINT')
146
146
147
147
148 # TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index
148 # TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index
149
149
150 class SQLiteDialect(ansisql.ANSIDialect):
150 class SQLiteDialect(ansisql.ANSIDialect):
151 columngenerator = SQLiteColumnGenerator
151 columngenerator = SQLiteColumnGenerator
152 columndropper = SQLiteColumnDropper
152 columndropper = SQLiteColumnDropper
153 schemachanger = SQLiteSchemaChanger
153 schemachanger = SQLiteSchemaChanger
154 constraintgenerator = SQLiteConstraintGenerator
154 constraintgenerator = SQLiteConstraintGenerator
155 constraintdropper = SQLiteConstraintDropper
155 constraintdropper = SQLiteConstraintDropper
@@ -1,88 +1,87 b''
1 """
1 """
2 Provide exception classes for :mod:`migrate`
2 Provide exception classes for :mod:`migrate`
3 """
3 """
4
4
5
5
6 class Error(Exception):
6 class Error(Exception):
7 """Error base class."""
7 """Error base class."""
8
8
9
9
10 class ApiError(Error):
10 class ApiError(Error):
11 """Base class for API errors."""
11 """Base class for API errors."""
12
12
13
13
14 class KnownError(ApiError):
14 class KnownError(ApiError):
15 """A known error condition."""
15 """A known error condition."""
16
16
17
17
18 class UsageError(ApiError):
18 class UsageError(ApiError):
19 """A known error condition where help should be displayed."""
19 """A known error condition where help should be displayed."""
20
20
21
21
22 class ControlledSchemaError(Error):
22 class ControlledSchemaError(Error):
23 """Base class for controlled schema errors."""
23 """Base class for controlled schema errors."""
24
24
25
25
26 class InvalidVersionError(ControlledSchemaError):
26 class InvalidVersionError(ControlledSchemaError):
27 """Invalid version number."""
27 """Invalid version number."""
28
28
29
29
30 class DatabaseNotControlledError(ControlledSchemaError):
30 class DatabaseNotControlledError(ControlledSchemaError):
31 """Database should be under version control, but it's not."""
31 """Database should be under version control, but it's not."""
32
32
33
33
34 class DatabaseAlreadyControlledError(ControlledSchemaError):
34 class DatabaseAlreadyControlledError(ControlledSchemaError):
35 """Database shouldn't be under version control, but it is"""
35 """Database shouldn't be under version control, but it is"""
36
36
37
37
38 class WrongRepositoryError(ControlledSchemaError):
38 class WrongRepositoryError(ControlledSchemaError):
39 """This database is under version control by another repository."""
39 """This database is under version control by another repository."""
40
40
41
41
42 class NoSuchTableError(ControlledSchemaError):
42 class NoSuchTableError(ControlledSchemaError):
43 """The table does not exist."""
43 """The table does not exist."""
44
44
45
45
46 class PathError(Error):
46 class PathError(Error):
47 """Base class for path errors."""
47 """Base class for path errors."""
48
48
49
49
50 class PathNotFoundError(PathError):
50 class PathNotFoundError(PathError):
51 """A path with no file was required; found a file."""
51 """A path with no file was required; found a file."""
52
52
53
53
54 class PathFoundError(PathError):
54 class PathFoundError(PathError):
55 """A path with a file was required; found no file."""
55 """A path with a file was required; found no file."""
56
56
57
57
58 class RepositoryError(Error):
58 class RepositoryError(Error):
59 """Base class for repository errors."""
59 """Base class for repository errors."""
60
60
61
61
62 class InvalidRepositoryError(RepositoryError):
62 class InvalidRepositoryError(RepositoryError):
63 """Invalid repository error."""
63 """Invalid repository error."""
64
64
65
65
66 class ScriptError(Error):
66 class ScriptError(Error):
67 """Base class for script errors."""
67 """Base class for script errors."""
68
68
69
69
70 class InvalidScriptError(ScriptError):
70 class InvalidScriptError(ScriptError):
71 """Invalid script error."""
71 """Invalid script error."""
72
72
73
73
74 class InvalidVersionError(Error):
74 class InvalidVersionError(Error):
75 """Invalid version error."""
75 """Invalid version error."""
76
76
77 # migrate.changeset
77 # migrate.changeset
78
78
79 class NotSupportedError(Error):
79 class NotSupportedError(Error):
80 """Not supported error"""
80 """Not supported error"""
81
81
82
82
83 class InvalidConstraintError(Error):
83 class InvalidConstraintError(Error):
84 """Invalid constraint error"""
84 """Invalid constraint error"""
85
85
86
87 class MigrateDeprecationWarning(DeprecationWarning):
86 class MigrateDeprecationWarning(DeprecationWarning):
88 """Warning for deprecated features in Migrate"""
87 """Warning for deprecated features in Migrate"""
@@ -1,383 +1,384 b''
1 """
1 """
2 This module provides an external API to the versioning system.
2 This module provides an external API to the versioning system.
3
3
4 .. versionchanged:: 0.6.0
4 .. versionchanged:: 0.6.0
5 :func:`migrate.versioning.api.test` and schema diff functions
5 :func:`migrate.versioning.api.test` and schema diff functions
6 changed order of positional arguments so all accept `url` and `repository`
6 changed order of positional arguments so all accept `url` and `repository`
7 as first arguments.
7 as first arguments.
8
8
9 .. versionchanged:: 0.5.4
9 .. versionchanged:: 0.5.4
10 ``--preview_sql`` displays source file when using SQL scripts.
10 ``--preview_sql`` displays source file when using SQL scripts.
11 If Python script is used, it runs the action with mocked engine and
11 If Python script is used, it runs the action with mocked engine and
12 returns captured SQL statements.
12 returns captured SQL statements.
13
13
14 .. versionchanged:: 0.5.4
14 .. versionchanged:: 0.5.4
15 Deprecated ``--echo`` parameter in favour of new
15 Deprecated ``--echo`` parameter in favour of new
16 :func:`migrate.versioning.util.construct_engine` behavior.
16 :func:`migrate.versioning.util.construct_engine` behavior.
17 """
17 """
18
18
19 # Dear migrate developers,
19 # Dear migrate developers,
20 #
20 #
21 # please do not comment this module using sphinx syntax because its
21 # please do not comment this module using sphinx syntax because its
22 # docstrings are presented as user help and most users cannot
22 # docstrings are presented as user help and most users cannot
23 # interpret sphinx annotated ReStructuredText.
23 # interpret sphinx annotated ReStructuredText.
24 #
24 #
25 # Thanks,
25 # Thanks,
26 # Jan Dittberner
26 # Jan Dittberner
27
27
28 import sys
28 import sys
29 import inspect
29 import inspect
30 import logging
30 import logging
31
31
32 from rhodecode.lib.dbmigrate.migrate import exceptions
32 from rhodecode.lib.dbmigrate.migrate import exceptions
33 from rhodecode.lib.dbmigrate.migrate.versioning import repository, schema, version, \
33 from rhodecode.lib.dbmigrate.migrate.versioning import repository, schema, version, \
34 script as script_ # command name conflict
34 script as script_ # command name conflict
35 from rhodecode.lib.dbmigrate.migrate.versioning.util import catch_known_errors, with_engine
35 from rhodecode.lib.dbmigrate.migrate.versioning.util import catch_known_errors, with_engine
36
36
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39 command_desc = {
39 command_desc = {
40 'help': 'displays help on a given command',
40 'help': 'displays help on a given command',
41 'create': 'create an empty repository at the specified path',
41 'create': 'create an empty repository at the specified path',
42 'script': 'create an empty change Python script',
42 'script': 'create an empty change Python script',
43 'script_sql': 'create empty change SQL scripts for given database',
43 'script_sql': 'create empty change SQL scripts for given database',
44 'version': 'display the latest version available in a repository',
44 'version': 'display the latest version available in a repository',
45 'db_version': 'show the current version of the repository under version control',
45 'db_version': 'show the current version of the repository under version control',
46 'source': 'display the Python code for a particular version in this repository',
46 'source': 'display the Python code for a particular version in this repository',
47 'version_control': 'mark a database as under this repository\'s version control',
47 'version_control': 'mark a database as under this repository\'s version control',
48 'upgrade': 'upgrade a database to a later version',
48 'upgrade': 'upgrade a database to a later version',
49 'downgrade': 'downgrade a database to an earlier version',
49 'downgrade': 'downgrade a database to an earlier version',
50 'drop_version_control': 'removes version control from a database',
50 'drop_version_control': 'removes version control from a database',
51 'manage': 'creates a Python script that runs Migrate with a set of default values',
51 'manage': 'creates a Python script that runs Migrate with a set of default values',
52 'test': 'performs the upgrade and downgrade command on the given database',
52 'test': 'performs the upgrade and downgrade command on the given database',
53 'compare_model_to_db': 'compare MetaData against the current database state',
53 'compare_model_to_db': 'compare MetaData against the current database state',
54 'create_model': 'dump the current database as a Python model to stdout',
54 'create_model': 'dump the current database as a Python model to stdout',
55 'make_update_script_for_model': 'create a script changing the old MetaData to the new (current) MetaData',
55 'make_update_script_for_model': 'create a script changing the old MetaData to the new (current) MetaData',
56 'update_db_from_model': 'modify the database to match the structure of the current MetaData',
56 'update_db_from_model': 'modify the database to match the structure of the current MetaData',
57 }
57 }
58 __all__ = command_desc.keys()
58 __all__ = command_desc.keys()
59
59
60 Repository = repository.Repository
60 Repository = repository.Repository
61 ControlledSchema = schema.ControlledSchema
61 ControlledSchema = schema.ControlledSchema
62 VerNum = version.VerNum
62 VerNum = version.VerNum
63 PythonScript = script_.PythonScript
63 PythonScript = script_.PythonScript
64 SqlScript = script_.SqlScript
64 SqlScript = script_.SqlScript
65
65
66
66
67 # deprecated
67 # deprecated
68 def help(cmd=None, **opts):
68 def help(cmd=None, **opts):
69 """%prog help COMMAND
69 """%prog help COMMAND
70
70
71 Displays help on a given command.
71 Displays help on a given command.
72 """
72 """
73 if cmd is None:
73 if cmd is None:
74 raise exceptions.UsageError(None)
74 raise exceptions.UsageError(None)
75 try:
75 try:
76 func = globals()[cmd]
76 func = globals()[cmd]
77 except:
77 except:
78 raise exceptions.UsageError(
78 raise exceptions.UsageError(
79 "'%s' isn't a valid command. Try 'help COMMAND'" % cmd)
79 "'%s' isn't a valid command. Try 'help COMMAND'" % cmd)
80 ret = func.__doc__
80 ret = func.__doc__
81 if sys.argv[0]:
81 if sys.argv[0]:
82 ret = ret.replace('%prog', sys.argv[0])
82 ret = ret.replace('%prog', sys.argv[0])
83 return ret
83 return ret
84
84
85 @catch_known_errors
85 @catch_known_errors
86 def create(repository, name, **opts):
86 def create(repository, name, **opts):
87 """%prog create REPOSITORY_PATH NAME [--table=TABLE]
87 """%prog create REPOSITORY_PATH NAME [--table=TABLE]
88
88
89 Create an empty repository at the specified path.
89 Create an empty repository at the specified path.
90
90
91 You can specify the version_table to be used; by default, it is
91 You can specify the version_table to be used; by default, it is
92 'migrate_version'. This table is created in all version-controlled
92 'migrate_version'. This table is created in all version-controlled
93 databases.
93 databases.
94 """
94 """
95 repo_path = Repository.create(repository, name, **opts)
95 repo_path = Repository.create(repository, name, **opts)
96
96
97
97
98 @catch_known_errors
98 @catch_known_errors
99 def script(description, repository, **opts):
99 def script(description, repository, **opts):
100 """%prog script DESCRIPTION REPOSITORY_PATH
100 """%prog script DESCRIPTION REPOSITORY_PATH
101
101
102 Create an empty change script using the next unused version number
102 Create an empty change script using the next unused version number
103 appended with the given description.
103 appended with the given description.
104
104
105 For instance, manage.py script "Add initial tables" creates:
105 For instance, manage.py script "Add initial tables" creates:
106 repository/versions/001_Add_initial_tables.py
106 repository/versions/001_Add_initial_tables.py
107 """
107 """
108 repo = Repository(repository)
108 repo = Repository(repository)
109 repo.create_script(description, **opts)
109 repo.create_script(description, **opts)
110
110
111
111
112 @catch_known_errors
112 @catch_known_errors
113 def script_sql(database, description, repository, **opts):
113 def script_sql(database, description, repository, **opts):
114 """%prog script_sql DATABASE DESCRIPTION REPOSITORY_PATH
114 """%prog script_sql DATABASE DESCRIPTION REPOSITORY_PATH
115
115
116 Create empty change SQL scripts for given DATABASE, where DATABASE
116 Create empty change SQL scripts for given DATABASE, where DATABASE
117 is either specific ('postgresql', 'mysql', 'oracle', 'sqlite', etc.)
117 is either specific ('postgresql', 'mysql', 'oracle', 'sqlite', etc.)
118 or generic ('default').
118 or generic ('default').
119
119
120 For instance, manage.py script_sql postgresql description creates:
120 For instance, manage.py script_sql postgresql description creates:
121 repository/versions/001_description_postgresql_upgrade.sql and
121 repository/versions/001_description_postgresql_upgrade.sql and
122 repository/versions/001_description_postgresql_postgres.sql
122 repository/versions/001_description_postgresql_downgrade.sql
123 """
123 """
124 repo = Repository(repository)
124 repo = Repository(repository)
125 repo.create_script_sql(database, description, **opts)
125 repo.create_script_sql(database, description, **opts)
126
126
127
127
128 def version(repository, **opts):
128 def version(repository, **opts):
129 """%prog version REPOSITORY_PATH
129 """%prog version REPOSITORY_PATH
130
130
131 Display the latest version available in a repository.
131 Display the latest version available in a repository.
132 """
132 """
133 repo = Repository(repository)
133 repo = Repository(repository)
134 return repo.latest
134 return repo.latest
135
135
136
136
137 @with_engine
137 @with_engine
138 def db_version(url, repository, **opts):
138 def db_version(url, repository, **opts):
139 """%prog db_version URL REPOSITORY_PATH
139 """%prog db_version URL REPOSITORY_PATH
140
140
141 Show the current version of the repository with the given
141 Show the current version of the repository with the given
142 connection string, under version control of the specified
142 connection string, under version control of the specified
143 repository.
143 repository.
144
144
145 The url should be any valid SQLAlchemy connection string.
145 The url should be any valid SQLAlchemy connection string.
146 """
146 """
147 engine = opts.pop('engine')
147 engine = opts.pop('engine')
148 schema = ControlledSchema(engine, repository)
148 schema = ControlledSchema(engine, repository)
149 return schema.version
149 return schema.version
150
150
151
151
152 def source(version, dest=None, repository=None, **opts):
152 def source(version, dest=None, repository=None, **opts):
153 """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH
153 """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH
154
154
155 Display the Python code for a particular version in this
155 Display the Python code for a particular version in this
156 repository. Save it to the file at DESTINATION or, if omitted,
156 repository. Save it to the file at DESTINATION or, if omitted,
157 send to stdout.
157 send to stdout.
158 """
158 """
159 if repository is None:
159 if repository is None:
160 raise exceptions.UsageError("A repository must be specified")
160 raise exceptions.UsageError("A repository must be specified")
161 repo = Repository(repository)
161 repo = Repository(repository)
162 ret = repo.version(version).script().source()
162 ret = repo.version(version).script().source()
163 if dest is not None:
163 if dest is not None:
164 dest = open(dest, 'w')
164 dest = open(dest, 'w')
165 dest.write(ret)
165 dest.write(ret)
166 dest.close()
166 dest.close()
167 ret = None
167 ret = None
168 return ret
168 return ret
169
169
170
170
171 def upgrade(url, repository, version=None, **opts):
171 def upgrade(url, repository, version=None, **opts):
172 """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql]
172 """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql]
173
173
174 Upgrade a database to a later version.
174 Upgrade a database to a later version.
175
175
176 This runs the upgrade() function defined in your change scripts.
176 This runs the upgrade() function defined in your change scripts.
177
177
178 By default, the database is updated to the latest available
178 By default, the database is updated to the latest available
179 version. You may specify a version instead, if you wish.
179 version. You may specify a version instead, if you wish.
180
180
181 You may preview the Python or SQL code to be executed, rather than
181 You may preview the Python or SQL code to be executed, rather than
182 actually executing it, using the appropriate 'preview' option.
182 actually executing it, using the appropriate 'preview' option.
183 """
183 """
184 err = "Cannot upgrade a database of version %s to version %s. "\
184 err = "Cannot upgrade a database of version %s to version %s. "\
185 "Try 'downgrade' instead."
185 "Try 'downgrade' instead."
186 return _migrate(url, repository, version, upgrade=True, err=err, **opts)
186 return _migrate(url, repository, version, upgrade=True, err=err, **opts)
187
187
188
188
189 def downgrade(url, repository, version, **opts):
189 def downgrade(url, repository, version, **opts):
190 """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql]
190 """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql]
191
191
192 Downgrade a database to an earlier version.
192 Downgrade a database to an earlier version.
193
193
194 This is the reverse of upgrade; this runs the downgrade() function
194 This is the reverse of upgrade; this runs the downgrade() function
195 defined in your change scripts.
195 defined in your change scripts.
196
196
197 You may preview the Python or SQL code to be executed, rather than
197 You may preview the Python or SQL code to be executed, rather than
198 actually executing it, using the appropriate 'preview' option.
198 actually executing it, using the appropriate 'preview' option.
199 """
199 """
200 err = "Cannot downgrade a database of version %s to version %s. "\
200 err = "Cannot downgrade a database of version %s to version %s. "\
201 "Try 'upgrade' instead."
201 "Try 'upgrade' instead."
202 return _migrate(url, repository, version, upgrade=False, err=err, **opts)
202 return _migrate(url, repository, version, upgrade=False, err=err, **opts)
203
203
204 @with_engine
204 @with_engine
205 def test(url, repository, **opts):
205 def test(url, repository, **opts):
206 """%prog test URL REPOSITORY_PATH [VERSION]
206 """%prog test URL REPOSITORY_PATH [VERSION]
207
207
208 Performs the upgrade and downgrade option on the given
208 Performs the upgrade and downgrade option on the given
209 database. This is not a real test and may leave the database in a
209 database. This is not a real test and may leave the database in a
210 bad state. You should therefore better run the test on a copy of
210 bad state. You should therefore better run the test on a copy of
211 your database.
211 your database.
212 """
212 """
213 engine = opts.pop('engine')
213 engine = opts.pop('engine')
214 repos = Repository(repository)
214 repos = Repository(repository)
215 script = repos.version(None).script()
216
215
217 # Upgrade
216 # Upgrade
218 log.info("Upgrading...")
217 log.info("Upgrading...")
218 script = repos.version(None).script(engine.name, 'upgrade')
219 script.run(engine, 1)
219 script.run(engine, 1)
220 log.info("done")
220 log.info("done")
221
221
222 log.info("Downgrading...")
222 log.info("Downgrading...")
223 script = repos.version(None).script(engine.name, 'downgrade')
223 script.run(engine, -1)
224 script.run(engine, -1)
224 log.info("done")
225 log.info("done")
225 log.info("Success")
226 log.info("Success")
226
227
227
228
228 @with_engine
229 @with_engine
229 def version_control(url, repository, version=None, **opts):
230 def version_control(url, repository, version=None, **opts):
230 """%prog version_control URL REPOSITORY_PATH [VERSION]
231 """%prog version_control URL REPOSITORY_PATH [VERSION]
231
232
232 Mark a database as under this repository's version control.
233 Mark a database as under this repository's version control.
233
234
234 Once a database is under version control, schema changes should
235 Once a database is under version control, schema changes should
235 only be done via change scripts in this repository.
236 only be done via change scripts in this repository.
236
237
237 This creates the table version_table in the database.
238 This creates the table version_table in the database.
238
239
239 The url should be any valid SQLAlchemy connection string.
240 The url should be any valid SQLAlchemy connection string.
240
241
241 By default, the database begins at version 0 and is assumed to be
242 By default, the database begins at version 0 and is assumed to be
242 empty. If the database is not empty, you may specify a version at
243 empty. If the database is not empty, you may specify a version at
243 which to begin instead. No attempt is made to verify this
244 which to begin instead. No attempt is made to verify this
244 version's correctness - the database schema is expected to be
245 version's correctness - the database schema is expected to be
245 identical to what it would be if the database were created from
246 identical to what it would be if the database were created from
246 scratch.
247 scratch.
247 """
248 """
248 engine = opts.pop('engine')
249 engine = opts.pop('engine')
249 ControlledSchema.create(engine, repository, version)
250 ControlledSchema.create(engine, repository, version)
250
251
251
252
252 @with_engine
253 @with_engine
253 def drop_version_control(url, repository, **opts):
254 def drop_version_control(url, repository, **opts):
254 """%prog drop_version_control URL REPOSITORY_PATH
255 """%prog drop_version_control URL REPOSITORY_PATH
255
256
256 Removes version control from a database.
257 Removes version control from a database.
257 """
258 """
258 engine = opts.pop('engine')
259 engine = opts.pop('engine')
259 schema = ControlledSchema(engine, repository)
260 schema = ControlledSchema(engine, repository)
260 schema.drop()
261 schema.drop()
261
262
262
263
263 def manage(file, **opts):
264 def manage(file, **opts):
264 """%prog manage FILENAME [VARIABLES...]
265 """%prog manage FILENAME [VARIABLES...]
265
266
266 Creates a script that runs Migrate with a set of default values.
267 Creates a script that runs Migrate with a set of default values.
267
268
268 For example::
269 For example::
269
270
270 %prog manage manage.py --repository=/path/to/repository \
271 %prog manage manage.py --repository=/path/to/repository \
271 --url=sqlite:///project.db
272 --url=sqlite:///project.db
272
273
273 would create the script manage.py. The following two commands
274 would create the script manage.py. The following two commands
274 would then have exactly the same results::
275 would then have exactly the same results::
275
276
276 python manage.py version
277 python manage.py version
277 %prog version --repository=/path/to/repository
278 %prog version --repository=/path/to/repository
278 """
279 """
279 Repository.create_manage_file(file, **opts)
280 Repository.create_manage_file(file, **opts)
280
281
281
282
282 @with_engine
283 @with_engine
283 def compare_model_to_db(url, repository, model, **opts):
284 def compare_model_to_db(url, repository, model, **opts):
284 """%prog compare_model_to_db URL REPOSITORY_PATH MODEL
285 """%prog compare_model_to_db URL REPOSITORY_PATH MODEL
285
286
286 Compare the current model (assumed to be a module level variable
287 Compare the current model (assumed to be a module level variable
287 of type sqlalchemy.MetaData) against the current database.
288 of type sqlalchemy.MetaData) against the current database.
288
289
289 NOTE: This is EXPERIMENTAL.
290 NOTE: This is EXPERIMENTAL.
290 """ # TODO: get rid of EXPERIMENTAL label
291 """ # TODO: get rid of EXPERIMENTAL label
291 engine = opts.pop('engine')
292 engine = opts.pop('engine')
292 return ControlledSchema.compare_model_to_db(engine, model, repository)
293 return ControlledSchema.compare_model_to_db(engine, model, repository)
293
294
294
295
295 @with_engine
296 @with_engine
296 def create_model(url, repository, **opts):
297 def create_model(url, repository, **opts):
297 """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True]
298 """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True]
298
299
299 Dump the current database as a Python model to stdout.
300 Dump the current database as a Python model to stdout.
300
301
301 NOTE: This is EXPERIMENTAL.
302 NOTE: This is EXPERIMENTAL.
302 """ # TODO: get rid of EXPERIMENTAL label
303 """ # TODO: get rid of EXPERIMENTAL label
303 engine = opts.pop('engine')
304 engine = opts.pop('engine')
304 declarative = opts.get('declarative', False)
305 declarative = opts.get('declarative', False)
305 return ControlledSchema.create_model(engine, repository, declarative)
306 return ControlledSchema.create_model(engine, repository, declarative)
306
307
307
308
308 @catch_known_errors
309 @catch_known_errors
309 @with_engine
310 @with_engine
310 def make_update_script_for_model(url, repository, oldmodel, model, **opts):
311 def make_update_script_for_model(url, repository, oldmodel, model, **opts):
311 """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
312 """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
312
313
313 Create a script changing the old Python model to the new (current)
314 Create a script changing the old Python model to the new (current)
314 Python model, sending to stdout.
315 Python model, sending to stdout.
315
316
316 NOTE: This is EXPERIMENTAL.
317 NOTE: This is EXPERIMENTAL.
317 """ # TODO: get rid of EXPERIMENTAL label
318 """ # TODO: get rid of EXPERIMENTAL label
318 engine = opts.pop('engine')
319 engine = opts.pop('engine')
319 return PythonScript.make_update_script_for_model(
320 return PythonScript.make_update_script_for_model(
320 engine, oldmodel, model, repository, **opts)
321 engine, oldmodel, model, repository, **opts)
321
322
322
323
323 @with_engine
324 @with_engine
324 def update_db_from_model(url, repository, model, **opts):
325 def update_db_from_model(url, repository, model, **opts):
325 """%prog update_db_from_model URL REPOSITORY_PATH MODEL
326 """%prog update_db_from_model URL REPOSITORY_PATH MODEL
326
327
327 Modify the database to match the structure of the current Python
328 Modify the database to match the structure of the current Python
328 model. This also sets the db_version number to the latest in the
329 model. This also sets the db_version number to the latest in the
329 repository.
330 repository.
330
331
331 NOTE: This is EXPERIMENTAL.
332 NOTE: This is EXPERIMENTAL.
332 """ # TODO: get rid of EXPERIMENTAL label
333 """ # TODO: get rid of EXPERIMENTAL label
333 engine = opts.pop('engine')
334 engine = opts.pop('engine')
334 schema = ControlledSchema(engine, repository)
335 schema = ControlledSchema(engine, repository)
335 schema.update_db_from_model(model)
336 schema.update_db_from_model(model)
336
337
337 @with_engine
338 @with_engine
338 def _migrate(url, repository, version, upgrade, err, **opts):
339 def _migrate(url, repository, version, upgrade, err, **opts):
339 engine = opts.pop('engine')
340 engine = opts.pop('engine')
340 url = str(engine.url)
341 url = str(engine.url)
341 schema = ControlledSchema(engine, repository)
342 schema = ControlledSchema(engine, repository)
342 version = _migrate_version(schema, version, upgrade, err)
343 version = _migrate_version(schema, version, upgrade, err)
343
344
344 changeset = schema.changeset(version)
345 changeset = schema.changeset(version)
345 for ver, change in changeset:
346 for ver, change in changeset:
346 nextver = ver + changeset.step
347 nextver = ver + changeset.step
347 log.info('%s -> %s... ', ver, nextver)
348 log.info('%s -> %s... ', ver, nextver)
348
349
349 if opts.get('preview_sql'):
350 if opts.get('preview_sql'):
350 if isinstance(change, PythonScript):
351 if isinstance(change, PythonScript):
351 log.info(change.preview_sql(url, changeset.step, **opts))
352 log.info(change.preview_sql(url, changeset.step, **opts))
352 elif isinstance(change, SqlScript):
353 elif isinstance(change, SqlScript):
353 log.info(change.source())
354 log.info(change.source())
354
355
355 elif opts.get('preview_py'):
356 elif opts.get('preview_py'):
356 if not isinstance(change, PythonScript):
357 if not isinstance(change, PythonScript):
357 raise exceptions.UsageError("Python source can be only displayed"
358 raise exceptions.UsageError("Python source can be only displayed"
358 " for python migration files")
359 " for python migration files")
359 source_ver = max(ver, nextver)
360 source_ver = max(ver, nextver)
360 module = schema.repository.version(source_ver).script().module
361 module = schema.repository.version(source_ver).script().module
361 funcname = upgrade and "upgrade" or "downgrade"
362 funcname = upgrade and "upgrade" or "downgrade"
362 func = getattr(module, funcname)
363 func = getattr(module, funcname)
363 log.info(inspect.getsource(func))
364 log.info(inspect.getsource(func))
364 else:
365 else:
365 schema.runchange(ver, change, changeset.step)
366 schema.runchange(ver, change, changeset.step)
366 log.info('done')
367 log.info('done')
367
368
368
369
369 def _migrate_version(schema, version, upgrade, err):
370 def _migrate_version(schema, version, upgrade, err):
370 if version is None:
371 if version is None:
371 return version
372 return version
372 # Version is specified: ensure we're upgrading in the right direction
373 # Version is specified: ensure we're upgrading in the right direction
373 # (current version < target version for upgrading; reverse for down)
374 # (current version < target version for upgrading; reverse for down)
374 version = VerNum(version)
375 version = VerNum(version)
375 cur = schema.version
376 cur = schema.version
376 if upgrade is not None:
377 if upgrade is not None:
377 if upgrade:
378 if upgrade:
378 direction = cur <= version
379 direction = cur <= version
379 else:
380 else:
380 direction = cur >= version
381 direction = cur >= version
381 if not direction:
382 if not direction:
382 raise exceptions.KnownError(err % (cur, version))
383 raise exceptions.KnownError(err % (cur, version))
383 return version
384 return version
@@ -1,242 +1,242 b''
1 """
1 """
2 SQLAlchemy migrate repository management.
2 SQLAlchemy migrate repository management.
3 """
3 """
4 import os
4 import os
5 import shutil
5 import shutil
6 import string
6 import string
7 import logging
7 import logging
8
8
9 from pkg_resources import resource_filename
9 from pkg_resources import resource_filename
10 from tempita import Template as TempitaTemplate
10 from tempita import Template as TempitaTemplate
11
11
12 from rhodecode.lib.dbmigrate.migrate import exceptions
12 from rhodecode.lib.dbmigrate.migrate import exceptions
13 from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse
13 from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse
14 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
14 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
15 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
15 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
16
16
17
17
18 log = logging.getLogger(__name__)
18 log = logging.getLogger(__name__)
19
19
20 class Changeset(dict):
20 class Changeset(dict):
21 """A collection of changes to be applied to a database.
21 """A collection of changes to be applied to a database.
22
22
23 Changesets are bound to a repository and manage a set of
23 Changesets are bound to a repository and manage a set of
24 scripts from that repository.
24 scripts from that repository.
25
25
26 Behaves like a dict, for the most part. Keys are ordered based on step value.
26 Behaves like a dict, for the most part. Keys are ordered based on step value.
27 """
27 """
28
28
29 def __init__(self, start, *changes, **k):
29 def __init__(self, start, *changes, **k):
30 """
30 """
31 Give a start version; step must be explicitly stated.
31 Give a start version; step must be explicitly stated.
32 """
32 """
33 self.step = k.pop('step', 1)
33 self.step = k.pop('step', 1)
34 self.start = version.VerNum(start)
34 self.start = version.VerNum(start)
35 self.end = self.start
35 self.end = self.start
36 for change in changes:
36 for change in changes:
37 self.add(change)
37 self.add(change)
38
38
39 def __iter__(self):
39 def __iter__(self):
40 return iter(self.items())
40 return iter(self.items())
41
41
42 def keys(self):
42 def keys(self):
43 """
43 """
44 In a series of upgrades x -> y, keys are version x. Sorted.
44 In a series of upgrades x -> y, keys are version x. Sorted.
45 """
45 """
46 ret = super(Changeset, self).keys()
46 ret = super(Changeset, self).keys()
47 # Reverse order if downgrading
47 # Reverse order if downgrading
48 ret.sort(reverse=(self.step < 1))
48 ret.sort(reverse=(self.step < 1))
49 return ret
49 return ret
50
50
51 def values(self):
51 def values(self):
52 return [self[k] for k in self.keys()]
52 return [self[k] for k in self.keys()]
53
53
54 def items(self):
54 def items(self):
55 return zip(self.keys(), self.values())
55 return zip(self.keys(), self.values())
56
56
57 def add(self, change):
57 def add(self, change):
58 """Add new change to changeset"""
58 """Add new change to changeset"""
59 key = self.end
59 key = self.end
60 self.end += self.step
60 self.end += self.step
61 self[key] = change
61 self[key] = change
62
62
63 def run(self, *p, **k):
63 def run(self, *p, **k):
64 """Run the changeset scripts"""
64 """Run the changeset scripts"""
65 for version, script in self:
65 for version, script in self:
66 script.run(*p, **k)
66 script.run(*p, **k)
67
67
68
68
69 class Repository(pathed.Pathed):
69 class Repository(pathed.Pathed):
70 """A project's change script repository"""
70 """A project's change script repository"""
71
71
72 _config = 'migrate.cfg'
72 _config = 'migrate.cfg'
73 _versions = 'versions'
73 _versions = 'versions'
74
74
75 def __init__(self, path):
75 def __init__(self, path):
76 log.debug('Loading repository %s...' % path)
76 log.debug('Loading repository %s...' % path)
77 self.verify(path)
77 self.verify(path)
78 super(Repository, self).__init__(path)
78 super(Repository, self).__init__(path)
79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
80 self.versions = version.Collection(os.path.join(self.path,
80 self.versions = version.Collection(os.path.join(self.path,
81 self._versions))
81 self._versions))
82 log.debug('Repository %s loaded successfully' % path)
82 log.debug('Repository %s loaded successfully' % path)
83 log.debug('Config: %r' % self.config.to_dict())
83 log.debug('Config: %r' % self.config.to_dict())
84
84
85 @classmethod
85 @classmethod
86 def verify(cls, path):
86 def verify(cls, path):
87 """
87 """
88 Ensure the target path is a valid repository.
88 Ensure the target path is a valid repository.
89
89
90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
91 """
91 """
92 # Ensure the existence of required files
92 # Ensure the existence of required files
93 try:
93 try:
94 cls.require_found(path)
94 cls.require_found(path)
95 cls.require_found(os.path.join(path, cls._config))
95 cls.require_found(os.path.join(path, cls._config))
96 cls.require_found(os.path.join(path, cls._versions))
96 cls.require_found(os.path.join(path, cls._versions))
97 except exceptions.PathNotFoundError, e:
97 except exceptions.PathNotFoundError, e:
98 raise exceptions.InvalidRepositoryError(path)
98 raise exceptions.InvalidRepositoryError(path)
99
99
100 @classmethod
100 @classmethod
101 def prepare_config(cls, tmpl_dir, name, options=None):
101 def prepare_config(cls, tmpl_dir, name, options=None):
102 """
102 """
103 Prepare a project configuration file for a new project.
103 Prepare a project configuration file for a new project.
104
104
105 :param tmpl_dir: Path to Repository template
105 :param tmpl_dir: Path to Repository template
106 :param config_file: Name of the config file in Repository template
106 :param config_file: Name of the config file in Repository template
107 :param name: Repository name
107 :param name: Repository name
108 :type tmpl_dir: string
108 :type tmpl_dir: string
109 :type config_file: string
109 :type config_file: string
110 :type name: string
110 :type name: string
111 :returns: Populated config file
111 :returns: Populated config file
112 """
112 """
113 if options is None:
113 if options is None:
114 options = {}
114 options = {}
115 options.setdefault('version_table', 'migrate_version')
115 options.setdefault('version_table', 'migrate_version')
116 options.setdefault('repository_id', name)
116 options.setdefault('repository_id', name)
117 options.setdefault('required_dbs', [])
117 options.setdefault('required_dbs', [])
118 options.setdefault('use_timestamp_numbering', '0')
118 options.setdefault('use_timestamp_numbering', False)
119
119
120 tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
120 tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
121 ret = TempitaTemplate(tmpl).substitute(options)
121 ret = TempitaTemplate(tmpl).substitute(options)
122
122
123 # cleanup
123 # cleanup
124 del options['__template_name__']
124 del options['__template_name__']
125
125
126 return ret
126 return ret
127
127
128 @classmethod
128 @classmethod
129 def create(cls, path, name, **opts):
129 def create(cls, path, name, **opts):
130 """Create a repository at a specified path"""
130 """Create a repository at a specified path"""
131 cls.require_notfound(path)
131 cls.require_notfound(path)
132 theme = opts.pop('templates_theme', None)
132 theme = opts.pop('templates_theme', None)
133 t_path = opts.pop('templates_path', None)
133 t_path = opts.pop('templates_path', None)
134
134
135 # Create repository
135 # Create repository
136 tmpl_dir = Template(t_path).get_repository(theme=theme)
136 tmpl_dir = Template(t_path).get_repository(theme=theme)
137 shutil.copytree(tmpl_dir, path)
137 shutil.copytree(tmpl_dir, path)
138
138
139 # Edit config defaults
139 # Edit config defaults
140 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
140 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
141 fd = open(os.path.join(path, cls._config), 'w')
141 fd = open(os.path.join(path, cls._config), 'w')
142 fd.write(config_text)
142 fd.write(config_text)
143 fd.close()
143 fd.close()
144
144
145 opts['repository_name'] = name
145 opts['repository_name'] = name
146
146
147 # Create a management script
147 # Create a management script
148 manager = os.path.join(path, 'manage.py')
148 manager = os.path.join(path, 'manage.py')
149 Repository.create_manage_file(manager, templates_theme=theme,
149 Repository.create_manage_file(manager, templates_theme=theme,
150 templates_path=t_path, **opts)
150 templates_path=t_path, **opts)
151
151
152 return cls(path)
152 return cls(path)
153
153
154 def create_script(self, description, **k):
154 def create_script(self, description, **k):
155 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
155 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
156
156
157 k['use_timestamp_numbering'] = self.use_timestamp_numbering
157 k['use_timestamp_numbering'] = self.use_timestamp_numbering
158 self.versions.create_new_python_version(description, **k)
158 self.versions.create_new_python_version(description, **k)
159
159
160 def create_script_sql(self, database, description, **k):
160 def create_script_sql(self, database, description, **k):
161 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
161 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
162 k['use_timestamp_numbering'] = self.use_timestamp_numbering
162 k['use_timestamp_numbering'] = self.use_timestamp_numbering
163 self.versions.create_new_sql_version(database, description, **k)
163 self.versions.create_new_sql_version(database, description, **k)
164
164
165 @property
165 @property
166 def latest(self):
166 def latest(self):
167 """API to :attr:`migrate.versioning.version.Collection.latest`"""
167 """API to :attr:`migrate.versioning.version.Collection.latest`"""
168 return self.versions.latest
168 return self.versions.latest
169
169
170 @property
170 @property
171 def version_table(self):
171 def version_table(self):
172 """Returns version_table name specified in config"""
172 """Returns version_table name specified in config"""
173 return self.config.get('db_settings', 'version_table')
173 return self.config.get('db_settings', 'version_table')
174
174
175 @property
175 @property
176 def id(self):
176 def id(self):
177 """Returns repository id specified in config"""
177 """Returns repository id specified in config"""
178 return self.config.get('db_settings', 'repository_id')
178 return self.config.get('db_settings', 'repository_id')
179
179
180 @property
180 @property
181 def use_timestamp_numbering(self):
181 def use_timestamp_numbering(self):
182 """Returns use_timestamp_numbering specified in config"""
182 """Returns use_timestamp_numbering specified in config"""
183 ts_numbering = self.config.get('db_settings', 'use_timestamp_numbering', raw=True)
183 if self.config.has_option('db_settings', 'use_timestamp_numbering'):
184
184 return self.config.getboolean('db_settings', 'use_timestamp_numbering')
185 return ts_numbering
185 return False
186
186
187 def version(self, *p, **k):
187 def version(self, *p, **k):
188 """API to :attr:`migrate.versioning.version.Collection.version`"""
188 """API to :attr:`migrate.versioning.version.Collection.version`"""
189 return self.versions.version(*p, **k)
189 return self.versions.version(*p, **k)
190
190
191 @classmethod
191 @classmethod
192 def clear(cls):
192 def clear(cls):
193 # TODO: deletes repo
193 # TODO: deletes repo
194 super(Repository, cls).clear()
194 super(Repository, cls).clear()
195 version.Collection.clear()
195 version.Collection.clear()
196
196
197 def changeset(self, database, start, end=None):
197 def changeset(self, database, start, end=None):
198 """Create a changeset to migrate this database from ver. start to end/latest.
198 """Create a changeset to migrate this database from ver. start to end/latest.
199
199
200 :param database: name of database to generate changeset
200 :param database: name of database to generate changeset
201 :param start: version to start at
201 :param start: version to start at
202 :param end: version to end at (latest if None given)
202 :param end: version to end at (latest if None given)
203 :type database: string
203 :type database: string
204 :type start: int
204 :type start: int
205 :type end: int
205 :type end: int
206 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
206 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
207 """
207 """
208 start = version.VerNum(start)
208 start = version.VerNum(start)
209
209
210 if end is None:
210 if end is None:
211 end = self.latest
211 end = self.latest
212 else:
212 else:
213 end = version.VerNum(end)
213 end = version.VerNum(end)
214
214
215 if start <= end:
215 if start <= end:
216 step = 1
216 step = 1
217 range_mod = 1
217 range_mod = 1
218 op = 'upgrade'
218 op = 'upgrade'
219 else:
219 else:
220 step = -1
220 step = -1
221 range_mod = 0
221 range_mod = 0
222 op = 'downgrade'
222 op = 'downgrade'
223
223
224 versions = range(start + range_mod, end + range_mod, step)
224 versions = range(start + range_mod, end + range_mod, step)
225 changes = [self.version(v).script(database, op) for v in versions]
225 changes = [self.version(v).script(database, op) for v in versions]
226 ret = Changeset(start, step=step, *changes)
226 ret = Changeset(start, step=step, *changes)
227 return ret
227 return ret
228
228
229 @classmethod
229 @classmethod
230 def create_manage_file(cls, file_, **opts):
230 def create_manage_file(cls, file_, **opts):
231 """Create a project management script (manage.py)
231 """Create a project management script (manage.py)
232
232
233 :param file_: Destination file to be written
233 :param file_: Destination file to be written
234 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
234 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
235 """
235 """
236 mng_file = Template(opts.pop('templates_path', None))\
236 mng_file = Template(opts.pop('templates_path', None))\
237 .get_manage(theme=opts.pop('templates_theme', None))
237 .get_manage(theme=opts.pop('templates_theme', None))
238
238
239 tmpl = open(mng_file).read()
239 tmpl = open(mng_file).read()
240 fd = open(file_, 'w')
240 fd = open(file_, 'w')
241 fd.write(TempitaTemplate(tmpl).substitute(opts))
241 fd.write(TempitaTemplate(tmpl).substitute(opts))
242 fd.close()
242 fd.close()
@@ -1,285 +1,293 b''
1 """
1 """
2 Schema differencing support.
2 Schema differencing support.
3 """
3 """
4
4
5 import logging
5 import logging
6 import sqlalchemy
6 import sqlalchemy
7
7
8 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06
8 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06
9 from sqlalchemy.types import Float
9 from sqlalchemy.types import Float
10
10
11 log = logging.getLogger(__name__)
11 log = logging.getLogger(__name__)
12
12
13 def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None):
13 def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None):
14 """
14 """
15 Return differences of model against database.
15 Return differences of model against database.
16
16
17 :return: object which will evaluate to :keyword:`True` if there \
17 :return: object which will evaluate to :keyword:`True` if there \
18 are differences else :keyword:`False`.
18 are differences else :keyword:`False`.
19 """
19 """
20 return SchemaDiff(metadata,
20 db_metadata = sqlalchemy.MetaData(engine, reflect=True)
21 sqlalchemy.MetaData(engine, reflect=True),
21
22 # sqlite will include a dynamically generated 'sqlite_sequence' table if
23 # there are autoincrement sequences in the database; this should not be
24 # compared.
25 if engine.dialect.name == 'sqlite':
26 if 'sqlite_sequence' in db_metadata.tables:
27 db_metadata.remove(db_metadata.tables['sqlite_sequence'])
28
29 return SchemaDiff(metadata, db_metadata,
22 labelA='model',
30 labelA='model',
23 labelB='database',
31 labelB='database',
24 excludeTables=excludeTables)
32 excludeTables=excludeTables)
25
33
26
34
27 def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None):
35 def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None):
28 """
36 """
29 Return differences of model against another model.
37 Return differences of model against another model.
30
38
31 :return: object which will evaluate to :keyword:`True` if there \
39 :return: object which will evaluate to :keyword:`True` if there \
32 are differences else :keyword:`False`.
40 are differences else :keyword:`False`.
33 """
41 """
34 return SchemaDiff(metadataA, metadataB, excludeTables)
42 return SchemaDiff(metadataA, metadataB, excludeTables)
35
43
36
44
37 class ColDiff(object):
45 class ColDiff(object):
38 """
46 """
39 Container for differences in one :class:`~sqlalchemy.schema.Column`
47 Container for differences in one :class:`~sqlalchemy.schema.Column`
40 between two :class:`~sqlalchemy.schema.Table` instances, ``A``
48 between two :class:`~sqlalchemy.schema.Table` instances, ``A``
41 and ``B``.
49 and ``B``.
42
50
43 .. attribute:: col_A
51 .. attribute:: col_A
44
52
45 The :class:`~sqlalchemy.schema.Column` object for A.
53 The :class:`~sqlalchemy.schema.Column` object for A.
46
54
47 .. attribute:: col_B
55 .. attribute:: col_B
48
56
49 The :class:`~sqlalchemy.schema.Column` object for B.
57 The :class:`~sqlalchemy.schema.Column` object for B.
50
58
51 .. attribute:: type_A
59 .. attribute:: type_A
52
60
53 The most generic type of the :class:`~sqlalchemy.schema.Column`
61 The most generic type of the :class:`~sqlalchemy.schema.Column`
54 object in A.
62 object in A.
55
63
56 .. attribute:: type_B
64 .. attribute:: type_B
57
65
58 The most generic type of the :class:`~sqlalchemy.schema.Column`
66 The most generic type of the :class:`~sqlalchemy.schema.Column`
59 object in A.
67 object in A.
60
68
61 """
69 """
62
70
63 diff = False
71 diff = False
64
72
65 def __init__(self,col_A,col_B):
73 def __init__(self,col_A,col_B):
66 self.col_A = col_A
74 self.col_A = col_A
67 self.col_B = col_B
75 self.col_B = col_B
68
76
69 self.type_A = col_A.type
77 self.type_A = col_A.type
70 self.type_B = col_B.type
78 self.type_B = col_B.type
71
79
72 self.affinity_A = self.type_A._type_affinity
80 self.affinity_A = self.type_A._type_affinity
73 self.affinity_B = self.type_B._type_affinity
81 self.affinity_B = self.type_B._type_affinity
74
82
75 if self.affinity_A is not self.affinity_B:
83 if self.affinity_A is not self.affinity_B:
76 self.diff = True
84 self.diff = True
77 return
85 return
78
86
79 if isinstance(self.type_A,Float) or isinstance(self.type_B,Float):
87 if isinstance(self.type_A,Float) or isinstance(self.type_B,Float):
80 if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)):
88 if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)):
81 self.diff=True
89 self.diff=True
82 return
90 return
83
91
84 for attr in ('precision','scale','length'):
92 for attr in ('precision','scale','length'):
85 A = getattr(self.type_A,attr,None)
93 A = getattr(self.type_A,attr,None)
86 B = getattr(self.type_B,attr,None)
94 B = getattr(self.type_B,attr,None)
87 if not (A is None or B is None) and A!=B:
95 if not (A is None or B is None) and A!=B:
88 self.diff=True
96 self.diff=True
89 return
97 return
90
98
91 def __nonzero__(self):
99 def __nonzero__(self):
92 return self.diff
100 return self.diff
93
101
94 class TableDiff(object):
102 class TableDiff(object):
95 """
103 """
96 Container for differences in one :class:`~sqlalchemy.schema.Table`
104 Container for differences in one :class:`~sqlalchemy.schema.Table`
97 between two :class:`~sqlalchemy.schema.MetaData` instances, ``A``
105 between two :class:`~sqlalchemy.schema.MetaData` instances, ``A``
98 and ``B``.
106 and ``B``.
99
107
100 .. attribute:: columns_missing_from_A
108 .. attribute:: columns_missing_from_A
101
109
102 A sequence of column names that were found in B but weren't in
110 A sequence of column names that were found in B but weren't in
103 A.
111 A.
104
112
105 .. attribute:: columns_missing_from_B
113 .. attribute:: columns_missing_from_B
106
114
107 A sequence of column names that were found in A but weren't in
115 A sequence of column names that were found in A but weren't in
108 B.
116 B.
109
117
110 .. attribute:: columns_different
118 .. attribute:: columns_different
111
119
112 A dictionary containing information about columns that were
120 A dictionary containing information about columns that were
113 found to be different.
121 found to be different.
114 It maps column names to a :class:`ColDiff` objects describing the
122 It maps column names to a :class:`ColDiff` objects describing the
115 differences found.
123 differences found.
116 """
124 """
117 __slots__ = (
125 __slots__ = (
118 'columns_missing_from_A',
126 'columns_missing_from_A',
119 'columns_missing_from_B',
127 'columns_missing_from_B',
120 'columns_different',
128 'columns_different',
121 )
129 )
122
130
123 def __nonzero__(self):
131 def __nonzero__(self):
124 return bool(
132 return bool(
125 self.columns_missing_from_A or
133 self.columns_missing_from_A or
126 self.columns_missing_from_B or
134 self.columns_missing_from_B or
127 self.columns_different
135 self.columns_different
128 )
136 )
129
137
130 class SchemaDiff(object):
138 class SchemaDiff(object):
131 """
139 """
132 Compute the difference between two :class:`~sqlalchemy.schema.MetaData`
140 Compute the difference between two :class:`~sqlalchemy.schema.MetaData`
133 objects.
141 objects.
134
142
135 The string representation of a :class:`SchemaDiff` will summarise
143 The string representation of a :class:`SchemaDiff` will summarise
136 the changes found between the two
144 the changes found between the two
137 :class:`~sqlalchemy.schema.MetaData` objects.
145 :class:`~sqlalchemy.schema.MetaData` objects.
138
146
139 The length of a :class:`SchemaDiff` will give the number of
147 The length of a :class:`SchemaDiff` will give the number of
140 changes found, enabling it to be used much like a boolean in
148 changes found, enabling it to be used much like a boolean in
141 expressions.
149 expressions.
142
150
143 :param metadataA:
151 :param metadataA:
144 First :class:`~sqlalchemy.schema.MetaData` to compare.
152 First :class:`~sqlalchemy.schema.MetaData` to compare.
145
153
146 :param metadataB:
154 :param metadataB:
147 Second :class:`~sqlalchemy.schema.MetaData` to compare.
155 Second :class:`~sqlalchemy.schema.MetaData` to compare.
148
156
149 :param labelA:
157 :param labelA:
150 The label to use in messages about the first
158 The label to use in messages about the first
151 :class:`~sqlalchemy.schema.MetaData`.
159 :class:`~sqlalchemy.schema.MetaData`.
152
160
153 :param labelB:
161 :param labelB:
154 The label to use in messages about the second
162 The label to use in messages about the second
155 :class:`~sqlalchemy.schema.MetaData`.
163 :class:`~sqlalchemy.schema.MetaData`.
156
164
157 :param excludeTables:
165 :param excludeTables:
158 A sequence of table names to exclude.
166 A sequence of table names to exclude.
159
167
160 .. attribute:: tables_missing_from_A
168 .. attribute:: tables_missing_from_A
161
169
162 A sequence of table names that were found in B but weren't in
170 A sequence of table names that were found in B but weren't in
163 A.
171 A.
164
172
165 .. attribute:: tables_missing_from_B
173 .. attribute:: tables_missing_from_B
166
174
167 A sequence of table names that were found in A but weren't in
175 A sequence of table names that were found in A but weren't in
168 B.
176 B.
169
177
170 .. attribute:: tables_different
178 .. attribute:: tables_different
171
179
172 A dictionary containing information about tables that were found
180 A dictionary containing information about tables that were found
173 to be different.
181 to be different.
174 It maps table names to a :class:`TableDiff` objects describing the
182 It maps table names to a :class:`TableDiff` objects describing the
175 differences found.
183 differences found.
176 """
184 """
177
185
178 def __init__(self,
186 def __init__(self,
179 metadataA, metadataB,
187 metadataA, metadataB,
180 labelA='metadataA',
188 labelA='metadataA',
181 labelB='metadataB',
189 labelB='metadataB',
182 excludeTables=None):
190 excludeTables=None):
183
191
184 self.metadataA, self.metadataB = metadataA, metadataB
192 self.metadataA, self.metadataB = metadataA, metadataB
185 self.labelA, self.labelB = labelA, labelB
193 self.labelA, self.labelB = labelA, labelB
186 self.label_width = max(len(labelA),len(labelB))
194 self.label_width = max(len(labelA),len(labelB))
187 excludeTables = set(excludeTables or [])
195 excludeTables = set(excludeTables or [])
188
196
189 A_table_names = set(metadataA.tables.keys())
197 A_table_names = set(metadataA.tables.keys())
190 B_table_names = set(metadataB.tables.keys())
198 B_table_names = set(metadataB.tables.keys())
191
199
192 self.tables_missing_from_A = sorted(
200 self.tables_missing_from_A = sorted(
193 B_table_names - A_table_names - excludeTables
201 B_table_names - A_table_names - excludeTables
194 )
202 )
195 self.tables_missing_from_B = sorted(
203 self.tables_missing_from_B = sorted(
196 A_table_names - B_table_names - excludeTables
204 A_table_names - B_table_names - excludeTables
197 )
205 )
198
206
199 self.tables_different = {}
207 self.tables_different = {}
200 for table_name in A_table_names.intersection(B_table_names):
208 for table_name in A_table_names.intersection(B_table_names):
201
209
202 td = TableDiff()
210 td = TableDiff()
203
211
204 A_table = metadataA.tables[table_name]
212 A_table = metadataA.tables[table_name]
205 B_table = metadataB.tables[table_name]
213 B_table = metadataB.tables[table_name]
206
214
207 A_column_names = set(A_table.columns.keys())
215 A_column_names = set(A_table.columns.keys())
208 B_column_names = set(B_table.columns.keys())
216 B_column_names = set(B_table.columns.keys())
209
217
210 td.columns_missing_from_A = sorted(
218 td.columns_missing_from_A = sorted(
211 B_column_names - A_column_names
219 B_column_names - A_column_names
212 )
220 )
213
221
214 td.columns_missing_from_B = sorted(
222 td.columns_missing_from_B = sorted(
215 A_column_names - B_column_names
223 A_column_names - B_column_names
216 )
224 )
217
225
218 td.columns_different = {}
226 td.columns_different = {}
219
227
220 for col_name in A_column_names.intersection(B_column_names):
228 for col_name in A_column_names.intersection(B_column_names):
221
229
222 cd = ColDiff(
230 cd = ColDiff(
223 A_table.columns.get(col_name),
231 A_table.columns.get(col_name),
224 B_table.columns.get(col_name)
232 B_table.columns.get(col_name)
225 )
233 )
226
234
227 if cd:
235 if cd:
228 td.columns_different[col_name]=cd
236 td.columns_different[col_name]=cd
229
237
230 # XXX - index and constraint differences should
238 # XXX - index and constraint differences should
231 # be checked for here
239 # be checked for here
232
240
233 if td:
241 if td:
234 self.tables_different[table_name]=td
242 self.tables_different[table_name]=td
235
243
236 def __str__(self):
244 def __str__(self):
237 ''' Summarize differences. '''
245 ''' Summarize differences. '''
238 out = []
246 out = []
239 column_template =' %%%is: %%r' % self.label_width
247 column_template =' %%%is: %%r' % self.label_width
240
248
241 for names,label in (
249 for names,label in (
242 (self.tables_missing_from_A,self.labelA),
250 (self.tables_missing_from_A,self.labelA),
243 (self.tables_missing_from_B,self.labelB),
251 (self.tables_missing_from_B,self.labelB),
244 ):
252 ):
245 if names:
253 if names:
246 out.append(
254 out.append(
247 ' tables missing from %s: %s' % (
255 ' tables missing from %s: %s' % (
248 label,', '.join(sorted(names))
256 label,', '.join(sorted(names))
249 )
257 )
250 )
258 )
251
259
252 for name,td in sorted(self.tables_different.items()):
260 for name,td in sorted(self.tables_different.items()):
253 out.append(
261 out.append(
254 ' table with differences: %s' % name
262 ' table with differences: %s' % name
255 )
263 )
256 for names,label in (
264 for names,label in (
257 (td.columns_missing_from_A,self.labelA),
265 (td.columns_missing_from_A,self.labelA),
258 (td.columns_missing_from_B,self.labelB),
266 (td.columns_missing_from_B,self.labelB),
259 ):
267 ):
260 if names:
268 if names:
261 out.append(
269 out.append(
262 ' %s missing these columns: %s' % (
270 ' %s missing these columns: %s' % (
263 label,', '.join(sorted(names))
271 label,', '.join(sorted(names))
264 )
272 )
265 )
273 )
266 for name,cd in td.columns_different.items():
274 for name,cd in td.columns_different.items():
267 out.append(' column with differences: %s' % name)
275 out.append(' column with differences: %s' % name)
268 out.append(column_template % (self.labelA,cd.col_A))
276 out.append(column_template % (self.labelA,cd.col_A))
269 out.append(column_template % (self.labelB,cd.col_B))
277 out.append(column_template % (self.labelB,cd.col_B))
270
278
271 if out:
279 if out:
272 out.insert(0, 'Schema diffs:')
280 out.insert(0, 'Schema diffs:')
273 return '\n'.join(out)
281 return '\n'.join(out)
274 else:
282 else:
275 return 'No schema diffs'
283 return 'No schema diffs'
276
284
277 def __len__(self):
285 def __len__(self):
278 """
286 """
279 Used in bool evaluation, return of 0 means no diffs.
287 Used in bool evaluation, return of 0 means no diffs.
280 """
288 """
281 return (
289 return (
282 len(self.tables_missing_from_A) +
290 len(self.tables_missing_from_A) +
283 len(self.tables_missing_from_B) +
291 len(self.tables_missing_from_B) +
284 len(self.tables_different)
292 len(self.tables_different)
285 )
293 )
@@ -1,10 +1,12 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 from migrate.versioning.shell import main
2 from migrate.versioning.shell import main
3
3
4 {{py:
4 {{py:
5 _vars = locals().copy()
5 _vars = locals().copy()
6 del _vars['__template_name__']
6 del _vars['__template_name__']
7 _vars.pop('repository_name', None)
7 _vars.pop('repository_name', None)
8 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
8 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
9 }}
9 }}
10 main({{ defaults }})
10
11 if __name__ == '__main__':
12 main({{ defaults }})
@@ -1,29 +1,30 b''
1 #!/usr/bin/python
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3 import sys
3 import sys
4
4
5 from sqlalchemy import engine_from_config
5 from sqlalchemy import engine_from_config
6 from paste.deploy.loadwsgi import ConfigLoader
6 from paste.deploy.loadwsgi import ConfigLoader
7
7
8 from migrate.versioning.shell import main
8 from migrate.versioning.shell import main
9 from {{ locals().pop('repository_name') }}.model import migrations
9 from {{ locals().pop('repository_name') }}.model import migrations
10
10
11
11
12 if '-c' in sys.argv:
12 if '-c' in sys.argv:
13 pos = sys.argv.index('-c')
13 pos = sys.argv.index('-c')
14 conf_path = sys.argv[pos + 1]
14 conf_path = sys.argv[pos + 1]
15 del sys.argv[pos:pos + 2]
15 del sys.argv[pos:pos + 2]
16 else:
16 else:
17 conf_path = 'development.ini'
17 conf_path = 'development.ini'
18
18
19 {{py:
19 {{py:
20 _vars = locals().copy()
20 _vars = locals().copy()
21 del _vars['__template_name__']
21 del _vars['__template_name__']
22 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
22 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
23 }}
23 }}
24
24
25 conf_dict = ConfigLoader(conf_path).parser._sections['app:main']
25 conf_dict = ConfigLoader(conf_path).parser._sections['app:main']
26
26
27 # migrate supports passing url as an existing Engine instance (since 0.6.0)
27 # migrate supports passing url as an existing Engine instance (since 0.6.0)
28 # usage: migrate -c path/to/config.ini COMMANDS
28 # usage: migrate -c path/to/config.ini COMMANDS
29 main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
29 if __name__ == '__main__':
30 main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
@@ -1,240 +1,238 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3
3
4 import os
4 import os
5 import re
5 import re
6 import shutil
6 import shutil
7 import logging
7 import logging
8
8
9 from rhodecode.lib.dbmigrate.migrate import exceptions
9 from rhodecode.lib.dbmigrate.migrate import exceptions
10 from rhodecode.lib.dbmigrate.migrate.versioning import pathed, script
10 from rhodecode.lib.dbmigrate.migrate.versioning import pathed, script
11 from datetime import datetime
11 from datetime import datetime
12
12
13
13
14 log = logging.getLogger(__name__)
14 log = logging.getLogger(__name__)
15
15
16 class VerNum(object):
16 class VerNum(object):
17 """A version number that behaves like a string and int at the same time"""
17 """A version number that behaves like a string and int at the same time"""
18
18
19 _instances = dict()
19 _instances = dict()
20
20
21 def __new__(cls, value):
21 def __new__(cls, value):
22 val = str(value)
22 val = str(value)
23 if val not in cls._instances:
23 if val not in cls._instances:
24 cls._instances[val] = super(VerNum, cls).__new__(cls)
24 cls._instances[val] = super(VerNum, cls).__new__(cls)
25 ret = cls._instances[val]
25 ret = cls._instances[val]
26 return ret
26 return ret
27
27
28 def __init__(self,value):
28 def __init__(self,value):
29 self.value = str(int(value))
29 self.value = str(int(value))
30 if self < 0:
30 if self < 0:
31 raise ValueError("Version number cannot be negative")
31 raise ValueError("Version number cannot be negative")
32
32
33 def __add__(self, value):
33 def __add__(self, value):
34 ret = int(self) + int(value)
34 ret = int(self) + int(value)
35 return VerNum(ret)
35 return VerNum(ret)
36
36
37 def __sub__(self, value):
37 def __sub__(self, value):
38 return self + (int(value) * -1)
38 return self + (int(value) * -1)
39
39
40 def __cmp__(self, value):
40 def __cmp__(self, value):
41 return int(self) - int(value)
41 return int(self) - int(value)
42
42
43 def __repr__(self):
43 def __repr__(self):
44 return "<VerNum(%s)>" % self.value
44 return "<VerNum(%s)>" % self.value
45
45
46 def __str__(self):
46 def __str__(self):
47 return str(self.value)
47 return str(self.value)
48
48
49 def __int__(self):
49 def __int__(self):
50 return int(self.value)
50 return int(self.value)
51
51
52
52
53 class Collection(pathed.Pathed):
53 class Collection(pathed.Pathed):
54 """A collection of versioning scripts in a repository"""
54 """A collection of versioning scripts in a repository"""
55
55
56 FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*')
56 FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*')
57
57
58 def __init__(self, path):
58 def __init__(self, path):
59 """Collect current version scripts in repository
59 """Collect current version scripts in repository
60 and store them in self.versions
60 and store them in self.versions
61 """
61 """
62 super(Collection, self).__init__(path)
62 super(Collection, self).__init__(path)
63
63
64 # Create temporary list of files, allowing skipped version numbers.
64 # Create temporary list of files, allowing skipped version numbers.
65 files = os.listdir(path)
65 files = os.listdir(path)
66 if '1' in files:
66 if '1' in files:
67 # deprecation
67 # deprecation
68 raise Exception('It looks like you have a repository in the old '
68 raise Exception('It looks like you have a repository in the old '
69 'format (with directories for each version). '
69 'format (with directories for each version). '
70 'Please convert repository before proceeding.')
70 'Please convert repository before proceeding.')
71
71
72 tempVersions = dict()
72 tempVersions = dict()
73 for filename in files:
73 for filename in files:
74 match = self.FILENAME_WITH_VERSION.match(filename)
74 match = self.FILENAME_WITH_VERSION.match(filename)
75 if match:
75 if match:
76 num = int(match.group(1))
76 num = int(match.group(1))
77 tempVersions.setdefault(num, []).append(filename)
77 tempVersions.setdefault(num, []).append(filename)
78 else:
78 else:
79 pass # Must be a helper file or something, let's ignore it.
79 pass # Must be a helper file or something, let's ignore it.
80
80
81 # Create the versions member where the keys
81 # Create the versions member where the keys
82 # are VerNum's and the values are Version's.
82 # are VerNum's and the values are Version's.
83 self.versions = dict()
83 self.versions = dict()
84 for num, files in tempVersions.items():
84 for num, files in tempVersions.items():
85 self.versions[VerNum(num)] = Version(num, path, files)
85 self.versions[VerNum(num)] = Version(num, path, files)
86
86
87 @property
87 @property
88 def latest(self):
88 def latest(self):
89 """:returns: Latest version in Collection"""
89 """:returns: Latest version in Collection"""
90 return max([VerNum(0)] + self.versions.keys())
90 return max([VerNum(0)] + self.versions.keys())
91
91
92 def _next_ver_num(self, use_timestamp_numbering):
92 def _next_ver_num(self, use_timestamp_numbering):
93 print use_timestamp_numbering
94 if use_timestamp_numbering == True:
93 if use_timestamp_numbering == True:
95 print "Creating new timestamp version!"
96 return VerNum(int(datetime.utcnow().strftime('%Y%m%d%H%M%S')))
94 return VerNum(int(datetime.utcnow().strftime('%Y%m%d%H%M%S')))
97 else:
95 else:
98 return self.latest + 1
96 return self.latest + 1
99
97
100 def create_new_python_version(self, description, **k):
98 def create_new_python_version(self, description, **k):
101 """Create Python files for new version"""
99 """Create Python files for new version"""
102 ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
100 ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
103 extra = str_to_filename(description)
101 extra = str_to_filename(description)
104
102
105 if extra:
103 if extra:
106 if extra == '_':
104 if extra == '_':
107 extra = ''
105 extra = ''
108 elif not extra.startswith('_'):
106 elif not extra.startswith('_'):
109 extra = '_%s' % extra
107 extra = '_%s' % extra
110
108
111 filename = '%03d%s.py' % (ver, extra)
109 filename = '%03d%s.py' % (ver, extra)
112 filepath = self._version_path(filename)
110 filepath = self._version_path(filename)
113
111
114 script.PythonScript.create(filepath, **k)
112 script.PythonScript.create(filepath, **k)
115 self.versions[ver] = Version(ver, self.path, [filename])
113 self.versions[ver] = Version(ver, self.path, [filename])
116
114
117 def create_new_sql_version(self, database, description, **k):
115 def create_new_sql_version(self, database, description, **k):
118 """Create SQL files for new version"""
116 """Create SQL files for new version"""
119 ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
117 ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
120 self.versions[ver] = Version(ver, self.path, [])
118 self.versions[ver] = Version(ver, self.path, [])
121
119
122 extra = str_to_filename(description)
120 extra = str_to_filename(description)
123
121
124 if extra:
122 if extra:
125 if extra == '_':
123 if extra == '_':
126 extra = ''
124 extra = ''
127 elif not extra.startswith('_'):
125 elif not extra.startswith('_'):
128 extra = '_%s' % extra
126 extra = '_%s' % extra
129
127
130 # Create new files.
128 # Create new files.
131 for op in ('upgrade', 'downgrade'):
129 for op in ('upgrade', 'downgrade'):
132 filename = '%03d%s_%s_%s.sql' % (ver, extra, database, op)
130 filename = '%03d%s_%s_%s.sql' % (ver, extra, database, op)
133 filepath = self._version_path(filename)
131 filepath = self._version_path(filename)
134 script.SqlScript.create(filepath, **k)
132 script.SqlScript.create(filepath, **k)
135 self.versions[ver].add_script(filepath)
133 self.versions[ver].add_script(filepath)
136
134
137 def version(self, vernum=None):
135 def version(self, vernum=None):
138 """Returns latest Version if vernum is not given.
136 """Returns latest Version if vernum is not given.
139 Otherwise, returns wanted version"""
137 Otherwise, returns wanted version"""
140 if vernum is None:
138 if vernum is None:
141 vernum = self.latest
139 vernum = self.latest
142 return self.versions[VerNum(vernum)]
140 return self.versions[VerNum(vernum)]
143
141
144 @classmethod
142 @classmethod
145 def clear(cls):
143 def clear(cls):
146 super(Collection, cls).clear()
144 super(Collection, cls).clear()
147
145
148 def _version_path(self, ver):
146 def _version_path(self, ver):
149 """Returns path of file in versions repository"""
147 """Returns path of file in versions repository"""
150 return os.path.join(self.path, str(ver))
148 return os.path.join(self.path, str(ver))
151
149
152
150
153 class Version(object):
151 class Version(object):
154 """A single version in a collection
152 """A single version in a collection
155 :param vernum: Version Number
153 :param vernum: Version Number
156 :param path: Path to script files
154 :param path: Path to script files
157 :param filelist: List of scripts
155 :param filelist: List of scripts
158 :type vernum: int, VerNum
156 :type vernum: int, VerNum
159 :type path: string
157 :type path: string
160 :type filelist: list
158 :type filelist: list
161 """
159 """
162
160
163 def __init__(self, vernum, path, filelist):
161 def __init__(self, vernum, path, filelist):
164 self.version = VerNum(vernum)
162 self.version = VerNum(vernum)
165
163
166 # Collect scripts in this folder
164 # Collect scripts in this folder
167 self.sql = dict()
165 self.sql = dict()
168 self.python = None
166 self.python = None
169
167
170 for script in filelist:
168 for script in filelist:
171 self.add_script(os.path.join(path, script))
169 self.add_script(os.path.join(path, script))
172
170
173 def script(self, database=None, operation=None):
171 def script(self, database=None, operation=None):
174 """Returns SQL or Python Script"""
172 """Returns SQL or Python Script"""
175 for db in (database, 'default'):
173 for db in (database, 'default'):
176 # Try to return a .sql script first
174 # Try to return a .sql script first
177 try:
175 try:
178 return self.sql[db][operation]
176 return self.sql[db][operation]
179 except KeyError:
177 except KeyError:
180 continue # No .sql script exists
178 continue # No .sql script exists
181
179
182 # TODO: maybe add force Python parameter?
180 # TODO: maybe add force Python parameter?
183 ret = self.python
181 ret = self.python
184
182
185 assert ret is not None, \
183 assert ret is not None, \
186 "There is no script for %d version" % self.version
184 "There is no script for %d version" % self.version
187 return ret
185 return ret
188
186
189 def add_script(self, path):
187 def add_script(self, path):
190 """Add script to Collection/Version"""
188 """Add script to Collection/Version"""
191 if path.endswith(Extensions.py):
189 if path.endswith(Extensions.py):
192 self._add_script_py(path)
190 self._add_script_py(path)
193 elif path.endswith(Extensions.sql):
191 elif path.endswith(Extensions.sql):
194 self._add_script_sql(path)
192 self._add_script_sql(path)
195
193
196 SQL_FILENAME = re.compile(r'^.*\.sql')
194 SQL_FILENAME = re.compile(r'^.*\.sql')
197
195
198 def _add_script_sql(self, path):
196 def _add_script_sql(self, path):
199 basename = os.path.basename(path)
197 basename = os.path.basename(path)
200 match = self.SQL_FILENAME.match(basename)
198 match = self.SQL_FILENAME.match(basename)
201
199
202 if match:
200 if match:
203 basename = basename.replace('.sql', '')
201 basename = basename.replace('.sql', '')
204 parts = basename.split('_')
202 parts = basename.split('_')
205 if len(parts) < 3:
203 if len(parts) < 3:
206 raise exceptions.ScriptError(
204 raise exceptions.ScriptError(
207 "Invalid SQL script name %s " % basename + \
205 "Invalid SQL script name %s " % basename + \
208 "(needs to be ###_description_database_operation.sql)")
206 "(needs to be ###_description_database_operation.sql)")
209 version = parts[0]
207 version = parts[0]
210 op = parts[-1]
208 op = parts[-1]
211 dbms = parts[-2]
209 dbms = parts[-2]
212 else:
210 else:
213 raise exceptions.ScriptError(
211 raise exceptions.ScriptError(
214 "Invalid SQL script name %s " % basename + \
212 "Invalid SQL script name %s " % basename + \
215 "(needs to be ###_description_database_operation.sql)")
213 "(needs to be ###_description_database_operation.sql)")
216
214
217 # File the script into a dictionary
215 # File the script into a dictionary
218 self.sql.setdefault(dbms, {})[op] = script.SqlScript(path)
216 self.sql.setdefault(dbms, {})[op] = script.SqlScript(path)
219
217
220 def _add_script_py(self, path):
218 def _add_script_py(self, path):
221 if self.python is not None:
219 if self.python is not None:
222 raise exceptions.ScriptError('You can only have one Python script '
220 raise exceptions.ScriptError('You can only have one Python script '
223 'per version, but you have: %s and %s' % (self.python, path))
221 'per version, but you have: %s and %s' % (self.python, path))
224 self.python = script.PythonScript(path)
222 self.python = script.PythonScript(path)
225
223
226
224
227 class Extensions:
225 class Extensions:
228 """A namespace for file extensions"""
226 """A namespace for file extensions"""
229 py = 'py'
227 py = 'py'
230 sql = 'sql'
228 sql = 'sql'
231
229
232 def str_to_filename(s):
230 def str_to_filename(s):
233 """Replaces spaces, (double and single) quotes
231 """Replaces spaces, (double and single) quotes
234 and double underscores to underscores
232 and double underscores to underscores
235 """
233 """
236
234
237 s = s.replace(' ', '_').replace('"', '_').replace("'", '_').replace(".", "_")
235 s = s.replace(' ', '_').replace('"', '_').replace("'", '_').replace(".", "_")
238 while '__' in s:
236 while '__' in s:
239 s = s.replace('__', '_')
237 s = s.replace('__', '_')
240 return s
238 return s
@@ -1,119 +1,119 b''
1 import logging
1 import logging
2 import datetime
2 import datetime
3
3
4 from sqlalchemy import *
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper
6 from sqlalchemy.orm import relation, backref, class_mapper
7 from sqlalchemy.orm.session import Session
7 from sqlalchemy.orm.session import Session
8
8
9 from rhodecode.lib.dbmigrate.migrate import *
9 from rhodecode.lib.dbmigrate.migrate import *
10 from rhodecode.lib.dbmigrate.migrate.changeset import *
10 from rhodecode.lib.dbmigrate.migrate.changeset import *
11
11
12 from rhodecode.model.meta import Base
12 from rhodecode.model.meta import Base
13
13
14 log = logging.getLogger(__name__)
14 log = logging.getLogger(__name__)
15
15
16 def upgrade(migrate_engine):
16 def upgrade(migrate_engine):
17 """ Upgrade operations go here.
17 """ Upgrade operations go here.
18 Don't create your own engine; bind migrate_engine to your metadata
18 Don't create your own engine; bind migrate_engine to your metadata
19 """
19 """
20
20
21 #==========================================================================
21 #==========================================================================
22 # Add table `groups``
22 # Add table `groups``
23 #==========================================================================
23 #==========================================================================
24 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import Group
24 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import RepoGroup as Group
25 Group().__table__.create()
25 Group().__table__.create()
26
26
27 #==========================================================================
27 #==========================================================================
28 # Add table `group_to_perm`
28 # Add table `group_to_perm`
29 #==========================================================================
29 #==========================================================================
30 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserRepoGroupToPerm
30 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserRepoGroupToPerm
31 UserRepoGroupToPerm().__table__.create()
31 UserRepoGroupToPerm().__table__.create()
32
32
33 #==========================================================================
33 #==========================================================================
34 # Add table `users_groups`
34 # Add table `users_groups`
35 #==========================================================================
35 #==========================================================================
36 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroup
36 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroup
37 UsersGroup().__table__.create()
37 UsersGroup().__table__.create()
38
38
39 #==========================================================================
39 #==========================================================================
40 # Add table `users_groups_members`
40 # Add table `users_groups_members`
41 #==========================================================================
41 #==========================================================================
42 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupMember
42 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupMember
43 UsersGroupMember().__table__.create()
43 UsersGroupMember().__table__.create()
44
44
45 #==========================================================================
45 #==========================================================================
46 # Add table `users_group_repo_to_perm`
46 # Add table `users_group_repo_to_perm`
47 #==========================================================================
47 #==========================================================================
48 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupRepoToPerm
48 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupRepoToPerm
49 UsersGroupRepoToPerm().__table__.create()
49 UsersGroupRepoToPerm().__table__.create()
50
50
51 #==========================================================================
51 #==========================================================================
52 # Add table `users_group_to_perm`
52 # Add table `users_group_to_perm`
53 #==========================================================================
53 #==========================================================================
54 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupToPerm
54 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupToPerm
55 UsersGroupToPerm().__table__.create()
55 UsersGroupToPerm().__table__.create()
56
56
57 #==========================================================================
57 #==========================================================================
58 # Upgrade of `users` table
58 # Upgrade of `users` table
59 #==========================================================================
59 #==========================================================================
60 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import User
60 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import User
61
61
62 #add column
62 #add column
63 ldap_dn = Column("ldap_dn", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
63 ldap_dn = Column("ldap_dn", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
64 ldap_dn.create(User().__table__)
64 ldap_dn.create(User().__table__)
65
65
66 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
66 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
67 api_key.create(User().__table__)
67 api_key.create(User().__table__)
68
68
69 #remove old column
69 #remove old column
70 is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
70 is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
71 is_ldap.drop(User().__table__)
71 is_ldap.drop(User().__table__)
72
72
73
73
74 #==========================================================================
74 #==========================================================================
75 # Upgrade of `repositories` table
75 # Upgrade of `repositories` table
76 #==========================================================================
76 #==========================================================================
77 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import Repository
77 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import Repository
78
78
79 #ADD clone_uri column#
79 #ADD clone_uri column#
80
80
81 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False,
81 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False,
82 assert_unicode=None),
82 assert_unicode=None),
83 nullable=True, unique=False, default=None)
83 nullable=True, unique=False, default=None)
84
84
85 clone_uri.create(Repository().__table__)
85 clone_uri.create(Repository().__table__)
86
86
87 #ADD downloads column#
87 #ADD downloads column#
88 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
88 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
89 enable_downloads.create(Repository().__table__)
89 enable_downloads.create(Repository().__table__)
90
90
91 #ADD column created_on
91 #ADD column created_on
92 created_on = Column('created_on', DateTime(timezone=False), nullable=True,
92 created_on = Column('created_on', DateTime(timezone=False), nullable=True,
93 unique=None, default=datetime.datetime.now)
93 unique=None, default=datetime.datetime.now)
94 created_on.create(Repository().__table__)
94 created_on.create(Repository().__table__)
95
95
96 #ADD group_id column#
96 #ADD group_id column#
97 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'),
97 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'),
98 nullable=True, unique=False, default=None)
98 nullable=True, unique=False, default=None)
99
99
100 group_id.create(Repository().__table__)
100 group_id.create(Repository().__table__)
101
101
102
102
103 #==========================================================================
103 #==========================================================================
104 # Upgrade of `user_followings` table
104 # Upgrade of `user_followings` table
105 #==========================================================================
105 #==========================================================================
106
106
107 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserFollowing
107 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserFollowing
108
108
109 follows_from = Column('follows_from', DateTime(timezone=False),
109 follows_from = Column('follows_from', DateTime(timezone=False),
110 nullable=True, unique=None,
110 nullable=True, unique=None,
111 default=datetime.datetime.now)
111 default=datetime.datetime.now)
112 follows_from.create(UserFollowing().__table__)
112 follows_from.create(UserFollowing().__table__)
113
113
114 return
114 return
115
115
116
116
117 def downgrade(migrate_engine):
117 def downgrade(migrate_engine):
118 meta = MetaData()
118 meta = MetaData()
119 meta.bind = migrate_engine
119 meta.bind = migrate_engine
General Comments 0
You need to be logged in to leave comments. Login now