Permalink
Browse files

adding support to ms-sql server

  • Loading branch information...
1 parent 107ec00 commit 5bd4e68758fc0f3bda8bd7159c41288b071ca5c7 @wandenberg wandenberg committed Apr 10, 2012
View
@@ -1,3 +1,6 @@
+### 1.4.4 - Wandenberg Vieira Peixoto <wandenberg@gmail.com> Mon, 09 Apr 2012 23:20:00 -0300
+- Adding MS-SQL Server support, made by Bruno Caimar <bruno.caimar@gmail.com>
+
### 1.4.3 - Wandenberg Vieira Peixoto <wandenberg@gmail.com> Wed, 07 Mar 2012 12:00:00 -0300
- Removing support to make debian package installation
- Refactoring tests
View
@@ -3,7 +3,7 @@ The simple-db-migrate is developed by:
Guilherme Chapiewski (http://gc.blog.br)
Contact: guilherme.chapiewski at gmail.com
Project Website: http://guilhermechapiewski.github.com/simple-db-migrate/
- Issue tracker: http://simple-db-migrate.lighthouseapp.com
+ Issue tracker: https://github.com/guilhermechapiewski/simple-db-migrate/issues
This software is distributed for free, under Apache License version 2.0.
See LICENSE.txt for more details.
View
@@ -108,7 +108,7 @@ You can set default values to internals configurations on your configuration fil
| DATABASE_USER | username used to connect to database and execute the commands | - | - |
| DATABASE_PASSWORD | password used to connect to database and execute the commands | - | - |
| DATABASE_NAME | database name used where the commands will be executed | - | - |
-| DATABASE_ENGINE | the database type where migrations will me executed | mysql | oracle,mysql |
+| DATABASE_ENGINE | the database type where migrations will me executed | mysql | oracle,mysql,mssql |
| DATABASE_VERSION_TABLE | the table name used to save database versions | __db_version__ | any name supported by the database |
| UTC_TIMESTAMP | create migrations files using UTC time to format the name | False | True,False |
| DATABASE_MIGRATIONS_DIR | directories to look for migrations files separated by _:_ | - | - |
@@ -125,9 +125,14 @@ You can set default values to internals configurations on your configuration fil
| show_sql | if True show executed sql commands | False | True,False |
| show_sql_only | if True only show the sqls, but not execute them | False | True,False |
+h2. Supported databases engines
+
+You can use this project to run migrations on MySQL, Oracle and MS-SQL server databases.
+The default database engine is MySQL, to use the others databases set the DATABASE_ENGINE constant on the configuration file.
+
h2. Roadmap, bug reporting and feature requests
-For detailed info about future versions, bug reporting and feature requests, go to the project's ticket system at "Lighthouse":http://simple-db-migrate.lighthouseapp.com/projects/25437-simple-db-migrate/overview.
+For detailed info about future versions, bug reporting and feature requests, go to "issues":https://github.com/guilhermechapiewski/simple-db-migrate/issues page.
h2. Other questions
@@ -0,0 +1,14 @@
+SQL_UP = """
+CREATE TABLE users (
+ id int NOT NULL identity,
+ oid varchar(500) default NULL,
+ first_name varchar(255) default NULL,
+ last_name varchar(255) default NULL,
+ email varchar(255) default NULL,
+ PRIMARY KEY (id)
+);
+"""
+
+SQL_DOWN = """
+DROP TABLE users;
+"""
@@ -0,0 +1,7 @@
+SQL_UP = """
+ALTER TABLE users add age int NULL;
+"""
+
+SQL_DOWN = """
+ALTER TABLE users drop column age;
+"""
@@ -0,0 +1,7 @@
+SQL_UP = """
+ALTER TABLE users add mother_age int NULL;
+"""
+
+SQL_DOWN = """
+ALTER TABLE users drop column mother_age;
+"""
@@ -0,0 +1,11 @@
+SQL_UP = """
+CREATE TABLE chimps (
+ id int NOT NULL identity,
+ first_name varchar(255) default NULL,
+ PRIMARY KEY (id)
+);
+"""
+
+SQL_DOWN = """
+DROP TABLE chimps;
+"""
@@ -0,0 +1,11 @@
+SQL_UP = """
+CREATE TABLE aleatory (
+ id int NOT NULL identity,
+ name varchar(255) default NULL,
+ PRIMARY KEY (id)
+);
+"""
+
+SQL_DOWN = """
+DROP TABLE aleatory;
+"""
@@ -0,0 +1,56 @@
+# example config file
+import os
+
+DATABASE_ENGINE = os.getenv("DB_ENGINE") or "mssql"
+DATABASE_HOST = os.getenv("DB_HOST") or "192.168.254.5:1038"
+DATABASE_USER = os.getenv("DB_USERNAME") or "sa"
+DATABASE_PASSWORD = os.getenv("DB_PASSWORD") or "root"
+DATABASE = os.getenv("DB_DATABASE") or "migration_example"
+
+###############################################################################
+
+# There is an alternative and convenient way to create your configuration
+# files making it possible to override values. If you use 'os.getenv' like
+# in the example below, you can pass value to override the default settings
+# through the command line using:
+#
+# [shell]$ DATABASE_HOST='my.database.host' db-migrate (... parameters ...)
+#
+# The example above will override the DATABASE_HOST with 'my.database.host'
+# instead of using 'localhost'.
+#
+# * Example of overridable config:
+# DATABASE_HOST = os.getenv("DATABASE_HOST") or "localhost"
+# DATABASE_USER = os.getenv("DATABASE_USER") or "root"
+# DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD") or ""
+# DATABASE_NAME = os.getenv("DATABASE_NAME") or "migration_example"
+# DATABASE_ENGINE = os.getenv("DB_ENGINE") or "oracle"
+# VERSION_TABLE = os.getenv("DB_VERSION_TABLE") or "db_version"
+
+###############################################################################
+
+# In some cases you will not want to write database passwords in the config
+# files (e.g. production databases passwords). You can configure the password
+# to be asked for you in the command line.
+#
+# DATABASE_PASSWORD = "<<ask_me>>"
+
+###############################################################################
+
+# Relative path from the location of this file.
+# You can put multiple directories if you want, separated by ':'.
+# The path can be absolute or relative (recommended).
+#
+# * Example:
+# DATABASE_MIGRATIONS_DIR = os.getenv("MIG_DIR") or "./migrations/dir:./another/dir"
+
+DATABASE_MIGRATIONS_DIR = os.getenv("MIG_DIR") or "."
+
+###############################################################################
+
+# Script format
+#
+# UTC_TIMESTAMP = True will cause creation scripts to utilize a UTC timestamp
+# instead of a local timestamp (default)
+#
+# UTC_TIMESTAMP = os.getenv("UTC_TIMESTAMP") or True
@@ -38,6 +38,9 @@ def __init__(self, config, sgdb=None):
elif self.config.get("db_engine") == 'oracle':
from oracle import Oracle
self.sgdb = Oracle(config)
+ elif self.config.get("db_engine") == 'mssql':
+ from mssql import MSSQL
+ self.sgdb = MSSQL(config)
else:
raise Exception("engine not supported '%s'" % self.config.get("db_engine"))
@@ -0,0 +1,196 @@
+from core import Migration
+from core.exceptions import MigrationException
+from helpers import Utils
+
+class MSSQL(object):
+
+ def __init__(self, config=None, mssql_driver=None):
+ self.__mssql_script_encoding = config.get("db_script_encoding", "utf8")
+ self.__mssql_encoding = config.get("db_encoding", "utf8")
+ self.__mssql_host = config.get("db_host")
+ self.__mssql_user = config.get("db_user")
+ self.__mssql_passwd = config.get("db_password")
+ self.__mssql_db = config.get("db_name")
+ self.__version_table = config.get("db_version_table")
+
+ self.__mssql_driver = mssql_driver
+ if not mssql_driver:
+ import _mssql
+ self.__mssql_driver = _mssql
+
+ if config.get("drop_db_first"):
+ self._drop_database()
+
+ self._create_database_if_not_exists()
+ self._create_version_table_if_not_exists()
+
+ def __mssql_connect(self, connect_using_db_name=True):
+ try:
+ conn = self.__mssql_driver.connect(server=self.__mssql_host, user=self.__mssql_user, password=self.__mssql_passwd, charset=self.__mssql_encoding)
+ if connect_using_db_name:
+ conn.select_db(self.__mssql_db)
+ return conn
+ except Exception, e:
+ raise Exception("could not connect to database: %s" % e)
+
+ def __execute(self, sql, execution_log=None):
+ db = self.__mssql_connect()
+ curr_statement = None
+ try:
+ for statement in MSSQL._parse_sql_statements(sql):
+ curr_statement = statement
+ db.execute_non_query(statement)
+ affected_rows = db.rows_affected
+ if execution_log:
+ execution_log("%s\n-- %d row(s) affected\n" % (statement, affected_rows and int(affected_rows) or 0))
+ except Exception, e:
+ db.cancel()
+ raise MigrationException("error executing migration: %s" % e, curr_statement)
+ finally:
+ db.close()
+
+ @classmethod
+ def _parse_sql_statements(cls, migration_sql):
+ all_statements = []
+ last_statement = ''
+
+ for statement in migration_sql.split(';'):
+ if len(last_statement) > 0:
+ curr_statement = '%s;%s' % (last_statement, statement)
+ else:
+ curr_statement = statement
+
+ count = Utils.count_occurrences(curr_statement)
+ single_quotes = count.get("'", 0)
+ double_quotes = count.get('"', 0)
+ left_parenthesis = count.get('(', 0)
+ right_parenthesis = count.get(')', 0)
+
+ if single_quotes % 2 == 0 and double_quotes % 2 == 0 and left_parenthesis == right_parenthesis:
+ all_statements.append(curr_statement)
+ last_statement = ''
+ else:
+ last_statement = curr_statement
+
+ return [s.strip() for s in all_statements if s.strip() != ""]
+
+ def _drop_database(self):
+ db = self.__mssql_connect(False)
+ try:
+ db.execute_non_query("if exists ( select 1 from sysdatabases where name = '%s' ) drop database %s;" % (self.__mssql_db, self.__mssql_db))
+ except Exception, e:
+ raise Exception("can't drop database '%s'; \n%s" % (self.__mssql_db, str(e)))
+ finally:
+ db.close()
+
+ def _create_database_if_not_exists(self):
+ db = self.__mssql_connect(False)
+ db.execute_non_query("if not exists ( select 1 from sysdatabases where name = '%s' ) create database %s;" % (self.__mssql_db, self.__mssql_db))
+ db.close()
+
+ def _create_version_table_if_not_exists(self):
+ # create version table
+ sql = "if not exists ( select 1 from sysobjects where name = '%s' and type = 'u' ) create table %s ( id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, version varchar(20) NOT NULL default '0', label varchar(255), name varchar(255), sql_up NTEXT, sql_down NTEXT);" % (self.__version_table, self.__version_table)
+ self.__execute(sql)
+
+ self._check_version_table_if_is_updated()
+
+ # check if there is a register there
+ db = self.__mssql_connect()
+ count = db.execute_scalar("select count(*) from %s;" % self.__version_table)
+ db.close()
+
+ # if there is not a version register, insert one
+ if count == 0:
+ sql = "insert into %s (version) values ('0');" % self.__version_table
+ self.__execute(sql)
+
+ def _check_version_table_if_is_updated(self):
+ # try to query a column wich not exists on the old version of simple-db-migrate
+ # has to have one check of this to each version of simple-db-migrate
+ db = self.__mssql_connect()
+ try:
+ db.execute_non_query("select id from %s;" % self.__version_table)
+ except Exception:
+ # update version table
+ sql = "alter table %s add id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, label varchar(255), name varchar(255), sql_up ntext, sql_down ntext;" % self.__version_table
+ self.__execute(sql)
+
+ db.close()
+
+ def __change_db_version(self, version, migration_file_name, sql_up, sql_down, up=True, execution_log=None, label_version=None):
+ params = []
+ params.append(version)
+
+ if up:
+ # moving up and storing history
+ sql = "insert into %s (version, label, name, sql_up, sql_down) values (%%s, %%s, %%s, %%s, %%s);" % (self.__version_table)
+ params.append(label_version)
+ params.append(migration_file_name)
+ params.append(sql_up and sql_up.encode(self.__mssql_script_encoding) or "")
+ params.append(sql_down and sql_down.encode(self.__mssql_script_encoding) or "")
+ else:
+ # moving down and deleting from history
+ sql = "delete from %s where version = %%s;" % (self.__version_table)
+
+ db = self.__mssql_connect()
+ try:
+ db.execute_non_query(sql.encode(self.__mssql_script_encoding), tuple(params))
+ if execution_log:
+ execution_log("migration %s registered\n" % (migration_file_name))
+ except Exception, e:
+ db.cancel()
+ raise MigrationException("error logging migration: %s" % e, migration_file_name)
+ finally:
+ db.close()
+
+ def change(self, sql, new_db_version, migration_file_name, sql_up, sql_down, up=True, execution_log=None, label_version=None):
+ self.__execute(sql, execution_log)
+ self.__change_db_version(new_db_version, migration_file_name, sql_up, sql_down, up, execution_log, label_version)
+
+ def get_current_schema_version(self):
+ db = self.__mssql_connect()
+ version = db.execute_scalar("select top 1 version from %s order by id desc" % self.__version_table) or 0
+ db.close()
+ return version
+
+ def get_all_schema_versions(self):
+ versions = []
+ db = self.__mssql_connect()
+ db.execute_query("select version from %s order by id;" % self.__version_table)
+ all_versions = db
+ for version in all_versions:
+ versions.append(version['version'])
+ db.close()
+ versions.sort()
+ return versions
+
+ def get_version_id_from_version_number(self, version):
+ db = self.__mssql_connect()
+ result = db.execute_row("select id from %s where version = '%s';" % (self.__version_table, version))
+ id = result and int(result['id']) or None
+ db.close()
+ return id
+
+ def get_version_number_from_label(self, label):
+ db = self.__mssql_connect()
+ result = db.execute_row("select version from %s where label = '%s' order by id desc" % (self.__version_table, label))
+ version = result and result['version'] or None
+ db.close()
+ return version
+
+ def get_all_schema_migrations(self):
+ migrations = []
+ db = self.__mssql_connect()
+ db.execute_query("select id, version, label, name, cast(sql_up as text) as sql_up, cast(sql_down as text) as sql_down from %s order by id;" % self.__version_table)
+ all_migrations = db
+ for migration_db in all_migrations:
+ migration = Migration(id = int(migration_db['id']),
+ version = migration_db['version'] and str(migration_db['version']) or None,
+ label = migration_db['label'] and str(migration_db['label']) or None,
+ file_name = migration_db['name'] and str(migration_db['name']) or None,
+ sql_up = Migration.ensure_sql_unicode(migration_db['sql_up'], self.__mssql_script_encoding),
+ sql_down = Migration.ensure_sql_unicode(migration_db['sql_down'], self.__mssql_script_encoding))
+ migrations.append(migration)
+ db.close()
+ return migrations
View
@@ -49,6 +49,12 @@ def test_it_should_use_oracle_class_if_choose_this_engine(self, oracle_mock):
Main(config=config)
oracle_mock.assert_called_with(config)
+ @patch('simple_db_migrate.mssql.MSSQL')
+ def test_it_should_use_mssql_class_if_choose_this_engine(self, mssql_mock):
+ config=Config({'log_dir':'.', 'db_engine': 'mssql', "migrations_dir":['.']})
+ Main(config=config)
+ mssql_mock.assert_called_with(config)
+
def test_it_should_raise_error_if_config_is_not_an_instance_of_simple_db_migrate_config(self):
self.assertRaisesWithMessage(Exception, "config must be an instance of simple_db_migrate.config.Config", Main, config={})
Oops, something went wrong.

0 comments on commit 5bd4e68

Please sign in to comment.