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