diff --git a/dm-migrations/examples/sample_migration.rb b/dm-migrations/examples/sample_migration.rb index b7d79c76..bc51342b 100644 --- a/dm-migrations/examples/sample_migration.rb +++ b/dm-migrations/examples/sample_migration.rb @@ -1,10 +1,13 @@ require File.dirname(__FILE__) + '/../lib/migration_runner' DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/migration_test.db") +DataMapper::Logger.new(STDOUT, :debug) +DataMapper.logger.debug( "Starting Migration" ) migration 1, :create_people_table do up do create_table :people do + column :id, :int, :primary_key => true column :name, :string column :age, :int end diff --git a/dm-migrations/examples/sample_migration_spec.rb b/dm-migrations/examples/sample_migration_spec.rb index 86e9deee..b4fbf6e8 100644 --- a/dm-migrations/examples/sample_migration_spec.rb +++ b/dm-migrations/examples/sample_migration_spec.rb @@ -1,14 +1,34 @@ require File.dirname(__FILE__) + '/sample_migration' -require File.dirname(__FILE__) + '/../lib/spec/example/migration_example_group.rb' +require File.dirname(__FILE__) + '/../lib/spec/example/migration_example_group' describe :create_people_table, :type => :migration do - before(:all) do - puts "Inserts here" + before do + run_migration end - it 'should do something' do - puts "hi" + it 'should create a people table' do + repository(:default).should have_table(:people) + end + + it 'should have an id column as the primary key' do + table(:people).should have_column(:id) + table(:people).column(:id).type.should == 'int' + table(:people).column(:id).should be_primary_key + end + + it 'should have a name column as a string' do + puts table(:people).inspect + puts query("PRAGMA table_info(people)") + table(:people).should have_column(:name) + table(:people).column(:name).type.should == 'string' + table(:people).column(:name).should_not permit_null + end + + it 'should have a nullable age column as a int' do + table(:people).should have_column(:age) + table(:people).column(:age).type.should == 'int' + table(:people).column(:age).should_not permit_null end end diff --git a/dm-migrations/lib/migration.rb b/dm-migrations/lib/migration.rb index 8b711067..f3230ec5 100644 --- a/dm-migrations/lib/migration.rb +++ b/dm-migrations/lib/migration.rb @@ -13,7 +13,7 @@ def initialize(migration) class Migration include SQL - attr_accessor :position, :name + attr_accessor :position, :name, :database, :adapter def initialize( position, name, opts = {}, &block ) @position, @name = position, name @@ -22,10 +22,11 @@ def initialize( position, name, opts = {}, &block ) @database = DataMapper.repository(@options[:database] || :default) @adapter = @database.adapter + puts @adapter.class case @adapter.class.to_s - when /Sqlite3/ then extend(SQL::Sqlite3) - when /Mysql/ then extend(SQL::Mysql) - when /Postgres/ then extend(SQL::Postgres) + when /Sqlite3/ then @adapter.extend(SQL::Sqlite3) + when /Mysql/ then @adapter.extend(SQL::Mysql) + when /Postgres/ then @adapter.extend(SQL::Postgres) else raise "Unsupported Migration Adapter #{@adapter.class}" end @@ -156,7 +157,7 @@ def quoted_name end def migration_info_table_exists? - table_exists?('migration_info') + adapter.table_exists?('migration_info') end # Fetch the record for this migration out of the migration_info table diff --git a/dm-migrations/lib/spec/example/migration_example_group.rb b/dm-migrations/lib/spec/example/migration_example_group.rb index 79022e6b..b5a0e607 100644 --- a/dm-migrations/lib/spec/example/migration_example_group.rb +++ b/dm-migrations/lib/spec/example/migration_example_group.rb @@ -1,23 +1,42 @@ require 'spec' +require File.dirname(__FILE__) + '/../lib/spec/matchers/migration_matchers' + module Spec module Example class MigrationExampleGroup < Spec::Example::ExampleGroup + include Spec::Matchers::Migration before(:all) do - # drop & create db - run_prereq_migrations + if this_migration.adapter.supports_schema_transactions? + run_prereq_migrations + end end before(:each) do - run_migration + if ! this_migration.adapter.supports_schema_transactions? + run_prereq_migrations + else + this_migration.adapter.begin_transaction + end + end + + after(:each) do + if this_migration.adapter.supports_schema_transactions? + this_migration.adapter.rollback_transaction + end end after(:all) do - # drop db + this_migration.adapter.drop_database end def run_prereq_migrations + "running n-1 migrations" + all_databases.each do |db| + db.adapter.drop_database + db.adapter.create_database + end @@migrations.sort.each do |migration| break if migration.name.to_s == migration_name.to_s migration.perform_up @@ -25,15 +44,29 @@ def run_prereq_migrations end def run_migration - @@migrations.sort.each do |migration| - migration.perform_up if migration.name.to_s == migration_name - end + this_migration.perform_up end def migration_name @migration_name ||= self.class.instance_variable_get("@description_text").to_s end + def all_databases + @@migrations.map(&:database).uniq + end + + def this_migration + @@migrations.select { |m| m.name.to_s == migration_name }.first + end + + def query(sql) + this_migration.adapter.query(sql) + end + + def table(table_name) + this_migration.adapter.table(table_name) + end + Spec::Example::ExampleGroupFactory.register(:migration, self) end diff --git a/dm-migrations/lib/spec/matchers/migration_matchers.rb b/dm-migrations/lib/spec/matchers/migration_matchers.rb new file mode 100644 index 00000000..2a28d4e5 --- /dev/null +++ b/dm-migrations/lib/spec/matchers/migration_matchers.rb @@ -0,0 +1,100 @@ + +module Spec + module Matchers + module Migration + + def have_table(table_name) + HaveTableMatcher.new(table_name) + end + + def have_column(column_name) + HaveColumnMatcher.new(column_name) + end + + def permit_null + NullableColumnMatcher.new + end + + def be_primary_key + PrimaryKeyMatcher.new + end + + class HaveTableMatcher + + attr_accessor :table_name, :repository + + def initialize(table_name) + @table_name = table_name + end + + def matches?(repository) + repository.adapter.table_exists?(table_name) + end + + def failure_message + %(expected #{repository} to have table '#{table_name}') + end + + def negative_failure_message + %(expected #{repository} to not have table '#{table_name}') + end + + end + + class HaveColumnMatcher + + attr_accessor :table, :column_name + + def initialize(column_name) + @column_name = column_name + end + + def matches?(table) + @table = table + table.columns.map(&:name).include?(column_name.to_s) + end + + def failure_message + %(expected #{table} to have column '#{column_name}') + end + + def negative_failure_message + %(expected #{table} to not have column '#{column_name}') + end + + end + + class NullableColumnMatcher + + attr_accessor :column + + def matches?(column) + @column = column + ! column.not_null + end + + def failure_message + %(expected #{column.name} to permit NULL) + end + + def negative_failure_message + %(expected #{column.name} to be NOT NULL) + end + + end + + class PrimaryKeyMatcher + + attr_accessor :column + + def matches?(column) + @column = column + column.primary_key + end + + end + + end + end +end + diff --git a/dm-migrations/lib/sql.rb b/dm-migrations/lib/sql.rb index 82ac4a56..ef480c4c 100644 --- a/dm-migrations/lib/sql.rb +++ b/dm-migrations/lib/sql.rb @@ -1,117 +1,185 @@ module SQL - class TableCreator - attr_accessor :table_name, :opts + class TableCreator + attr_accessor :table_name, :opts - def initialize(adapter, table_name, opts = {}, &block) - @adapter = adapter - @table_name = table_name.to_s - @opts = opts + def initialize(adapter, table_name, opts = {}, &block) + @adapter = adapter + @table_name = table_name.to_s + @opts = opts - @columns = [] + @columns = [] - self.instance_eval &block - end + self.instance_eval &block + end - def quoted_table_name - @adapter.quote_table_name(table_name) - end + def quoted_table_name + @adapter.quote_table_name(table_name) + end + + def column(name, type, opts = {}) + @columns << Column.new(@adapter, name, type, opts) + end - def column(name, type, opts = {}) - @columns << Column.new(@adapter, name, type, opts) + def to_sql + "CREATE TABLE #{quoted_table_name} (#{@columns.map(&:to_sql).join(', ')})" + end + + class Column + attr_accessor :name, :type + + def initialize(adapter, name, type, opts = {}) + @adapter = adapter + @name, @type = name.to_s, type.to_s + @opts = opts end def to_sql - "CREATE TABLE #{quoted_table_name} (#{@columns.map(&:to_sql).join(', ')})" + "#{quoted_name} #{type}" + end + + def quoted_name + @adapter.quote_column_name(name) end + end - class Column - attr_accessor :name, :type + end - def initialize(adapter, name, type, opts = {}) - @adapter = adapter - @name, @type = name.to_s, type.to_s - @opts = opts - end + class TableModifier + attr_accessor :table_name, :opts, :statements - def to_sql - "#{quoted_name} #{type}" - end + def initialize(*args) + @adapter = adapter + @table_name = table_name.to_s + @opts = opts + + @statements = [] + + self.instance_eval &block + end + + def add_column(name, type, opts = {}) + @statements << "ALTER TABLE #{quoted_table_name} ADD COLUMN #{quote_column_name(name)} #{type.to_s}" + end - def quoted_name - @adapter.quote_column_name(name) - end + def drop_column(name) + # raise NotImplemented for SQLite3. Can't ALTER TABLE, need to copy table. + # We'd have to inspect it, and we can't, since we aren't executing any queries yet. + # Just write the sql yourself. + if name.is_a?(Array) + name.each{ |n| drop_column(n) } + else + @statements << "ALTER TABLE #{quoted_table_name} DROP COLUMN #{quote_column_name(name)}" end + end + alias drop_columns drop_column + def rename_column(name, new_name, opts = {}) + # raise NotImplemented for SQLite3 + @statements << "ALTER TABLE #{quoted_table_name} RENAME COLUMN #{quote_column_name(name)} TO #{quote_column_name(new_name)}" end - class TableModifier - attr_accessor :table_name, :opts, :statements + def change_column(name, type, opts = {}) + # raise NotImplemented for SQLite3 + @statements << "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(name)} TYPE #{type}" + end - def initialize(*args) - @adapter = adapter - @table_name = table_name.to_s - @opts = opts + def quote_column_name(name) + @adapter.quote_column_name(name.to_s) + end - @statements = [] + def quoted_table_name + @adapter.quote_table_name(table_name) + end - self.instance_eval &block - end + end - def add_column(name, type, opts = {}) - @statements << "ALTER TABLE #{quoted_table_name} ADD COLUMN #{quote_column_name(name)} #{type.to_s}" - end + class Table - def drop_column(name) - # raise NotImplemented for SQLite3. Can't ALTER TABLE, need to copy table. - # We'd have to inspect it, and we can't, since we aren't executing any queries yet. - # Just write the sql yourself. - if name.is_a?(Array) - name.each{ |n| drop_column(n) } - else - @statements << "ALTER TABLE #{quoted_table_name} DROP COLUMN #{quote_column_name(name)}" - end - end - alias drop_columns drop_column + attr_accessor :name, :columns - def rename_column(name, new_name, opts = {}) - # raise NotImplemented for SQLite3 - @statements << "ALTER TABLE #{quoted_table_name} RENAME COLUMN #{quote_column_name(name)} TO #{quote_column_name(new_name)}" - end + def initialize(adapter, table_name) + @columns = [] + adapter.query_table(table_name).each do |col_struct| + @columns << SQL::Column.new(col_struct) + end + end - def change_column(name, type, opts = {}) - # raise NotImplemented for SQLite3 - @statements << "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(name)} TYPE #{type}" - end + def to_s + name + end - def quote_column_name(name) - @adapter.quote_column_name(name.to_s) - end + def column(column_name) + @columns.select { |c| c.name == column_name.to_s }.first + end - def quoted_table_name - @adapter.quote_table_name(table_name) - end + end + + class Column + + attr_accessor :name, :type, :not_null, :default_value, :primary_key + + def initialize(col_struct) + @name, @type, @default_value, @primary_key = col_struct.name, col_struct.type, col_struct.dflt_value, col_struct.pk + @not_null = col_struct.notnull == 0 end + end + module Sqlite3 def table_exists?(table_name) - @adapter.query("SELECT * FROM sqlite_master - WHERE name='#{table_name}' AND type = 'table'").size > 0 + query_table(table_name).size > 0 + end + + def supports_schema_transactions? + true end + + def drop_database + DataMapper.logger.info "Dropping #{@uri.path}" + system "rm #{@uri.path}" + end + + def create_database + # do nothing, sqlite will automatically create the database file + end + + def table(table_name) + SQL::Table.new(self, table_name) + end + + def query_table(table_name) + query("PRAGMA table_info('#{table_name}')") + end + end module Mysql def table_exists?(table_name) end + + def supports_schema_transactions? + false + end + + def drop_database + end end module Postgresql def table_exists?(table_name) end + + def supports_schema_transactions? + true + end + + def drop_database + end end end diff --git a/dm-migrations/spec/spec.opts b/dm-migrations/spec/spec.opts new file mode 100644 index 00000000..ff5051c3 --- /dev/null +++ b/dm-migrations/spec/spec.opts @@ -0,0 +1,3 @@ +--colour +--format +specdoc