sequel_migration_builder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Roland Swingler
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,55 @@
1
+ = Sequel Migration Builder
2
+
3
+ Builds sequel migrations based on the differences between an abstract
4
+ representation of the desired schema and a database instance.
5
+
6
+ == Example
7
+
8
+ require 'sequel/migration_builder'
9
+
10
+ desired_schema = { ... } # see below
11
+ builder = Sequel::MigrationBuilder.new(DB)
12
+ migration_code = builder.generate_migration(desired_schema)
13
+ File.write("001_some_migration.rb", migration_code)
14
+
15
+ == Schema format
16
+
17
+ The schema is an abstract representation of the tables in your database,
18
+ as a hash. A sample YAML version might look like:
19
+
20
+ example_table:
21
+ primary_key: id
22
+ columns:
23
+ - name: id
24
+ column_type: integer
25
+ - name: foo
26
+ column_type: varchar
27
+ default: "bar"
28
+ null: true
29
+ size: 30
30
+ another_table:
31
+ ...
32
+
33
+ == Requirements
34
+
35
+ * Sequel 3.12.0 or higher
36
+
37
+ == TODO
38
+
39
+ * Dropping tables when they are removed from the schema
40
+ * Automigrate functionality
41
+ * Dealing with renames in some way (even if just logging that they would be possible).
42
+
43
+ == Note on Patches/Pull Requests
44
+
45
+ * Fork the project.
46
+ * Make your feature addition or bug fix.
47
+ * Add tests for it. This is important so I don't break it in a
48
+ future version unintentionally.
49
+ * Commit, do not mess with rakefile, version, or history.
50
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
51
+ * Send me a pull request. Bonus points for topic branches.
52
+
53
+ == Copyright
54
+
55
+ Copyright (c) 2010 Roland Swingler. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "sequel_migration_builder"
8
+ gem.summary = "Build Sequel Migrations based on the differences between two schemas"
9
+ gem.description = "Build Sequel Migrations based on the differences between two schemas"
10
+ gem.email = "roland.swingler@gmail.com"
11
+ gem.homepage = "http://github.com/knaveofdiamonds/sequel_migration_builder"
12
+ gem.authors = ["Roland Swingler"]
13
+ gem.add_dependency "sequel", ">= 3.12.0"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
41
+
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = "sequel_migration_builder #{version}"
44
+ rdoc.rdoc_files.include('README*')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,140 @@
1
+ require 'sequel/schema/db_column'
2
+ require 'sequel/schema/db_schema_parser'
3
+ require 'sequel/schema/alter_table_operations'
4
+
5
+ module Sequel
6
+ # Generates a Sequel migration to bring a database inline with an
7
+ # abstract schema.
8
+ #
9
+ class MigrationBuilder
10
+ INDENT_SPACES = ' '
11
+
12
+ # Creates a migration builder for the given database.
13
+ #
14
+ def initialize(db)
15
+ @db = db
16
+ @db_tables = Schema::DbSchemaParser.for_db(db).parse_db_schema
17
+ @db_table_names = @db.tables
18
+ @indent = 0
19
+ @result = []
20
+ end
21
+
22
+ # Generates a string of ruby code to define a sequel
23
+ # migration, based on the differences between the database schema
24
+ # of this MigrationBuilder and the tables passed.
25
+ #
26
+ def generate_migration(tables)
27
+ return if tables.empty? && @db_tables.empty?
28
+ result.clear
29
+
30
+ add_line "Sequel.migration do"
31
+ indent do
32
+ generate_up(tables)
33
+ generate_down(tables)
34
+ end
35
+ add_line "end\n"
36
+
37
+ result.join("\n")
38
+ end
39
+
40
+ # Generates the 'up' part of the migration.
41
+ #
42
+ def generate_up(tables)
43
+ current_tables, new_tables = table_names(tables).partition do |table_name|
44
+ @db_table_names.include?(table_name)
45
+ end
46
+
47
+ add_line "up do"
48
+ create_new_tables(new_tables, tables)
49
+ alter_tables(current_tables, tables, :up)
50
+ add_line "end"
51
+ add_blank_line
52
+ end
53
+
54
+ # Generates the down part of the migration.
55
+ #
56
+ def generate_down(tables)
57
+ current_tables, new_tables = table_names(tables).partition do |table_name|
58
+ @db_table_names.include?(table_name)
59
+ end
60
+
61
+ add_line "down do"
62
+ alter_tables(current_tables, tables, :down)
63
+ indent do
64
+ new_tables.reverse.each {|table_name| add_line "drop_table #{table_name.inspect}" }
65
+ end
66
+ add_line "end"
67
+ end
68
+
69
+ # Generates any create table statements for new tables.
70
+ #
71
+ def create_new_tables(new_tables, tables)
72
+ i = 0
73
+ new_tables.each do |table_name|
74
+ i += 1
75
+ indent { create_table_statement table_name, tables[table_name] }
76
+ add_blank_line unless i == tables.size
77
+ end
78
+ end
79
+
80
+ # Generates any alter table statements for current tables.
81
+ #
82
+ def alter_tables(current_tables, tables, direction)
83
+ i = 0
84
+ indent do
85
+ current_tables.each do |table_name|
86
+ i += 1
87
+ hsh = tables[table_name].dup
88
+ hsh[:columns] = hsh[:columns].map {|c| Schema::DbColumn.build_from_hash(c) }
89
+ operations = Schema::AlterTableOperations.build(@db_tables[table_name], hsh)
90
+ unless operations.empty?
91
+ add_line "alter_table #{table_name.inspect} do"
92
+ indent do
93
+ operations.each {|op| add_line op.__send__(direction) }
94
+ end
95
+ add_line "end"
96
+ add_blank_line unless i == tables.size
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ # Generates an individual create_table statement.
103
+ #
104
+ def create_table_statement(table_name, table)
105
+ add_line "create_table #{table_name.inspect} do"
106
+ indent do
107
+ table[:columns].map {|c| Schema::DbColumn.build_from_hash(c) }.each do |column|
108
+ add_line column.define_statement
109
+ end
110
+ if table[:primary_key]
111
+ add_blank_line
112
+ add_line "primary_key #{table[:primary_key].inspect}"
113
+ end
114
+ end
115
+ add_line "end"
116
+ end
117
+
118
+ private
119
+
120
+ attr_reader :result
121
+
122
+ def table_names(tables)
123
+ tables.keys.map {|n| n.to_s }.sort.map {|n| n.to_sym }
124
+ end
125
+
126
+ def indent
127
+ @indent += 1
128
+ yield
129
+ @indent -= 1
130
+ end
131
+
132
+ def add_line(line)
133
+ @result << (INDENT_SPACES * @indent + line)
134
+ end
135
+
136
+ def add_blank_line
137
+ @result << ''
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,85 @@
1
+ module Sequel
2
+ module Schema
3
+ module AlterTableOperations
4
+
5
+ # Returns an array of operations to change the current database
6
+ # table to be like the defined table.
7
+ #
8
+ def self.build(db_table, new_table)
9
+ db_columns = db_table[:columns].inject({}) {|hsh, column| hsh[column.name] = column; hsh }
10
+
11
+ operations = new_table[:columns].map do |column|
12
+ if db_columns[column.name]
13
+ build_column_operations db_columns[column.name], column
14
+ else
15
+ AddColumn.new(column)
16
+ end
17
+ end.flatten
18
+
19
+ new_column_names = new_table[:columns].map {|c| c.name }
20
+ operations + (db_columns.keys - new_column_names).map {|column| DropColumn.new(column) }
21
+ end
22
+
23
+ # Returns an array of operations to change the current database
24
+ # column to be like the defined column.
25
+ #
26
+ def self.build_column_operations(db_column, new_column)
27
+ result = []
28
+
29
+ diffs = db_column.diff(new_column)
30
+ result << :change_type_statement if [:column_type, :size, :unsigned].any? {|sym| diffs.include?(sym) }
31
+ # only need to explicitly set the default if we're not changing the column type.
32
+ result << :change_default_statement if diffs.include?(:default) && result.empty?
33
+ result << :change_null_statement if diffs.include?(:null)
34
+
35
+ result.map {|k| ChangeColumn.new(db_column, new_column, k) }
36
+ end
37
+
38
+ # Changes a column.
39
+ class ChangeColumn
40
+ def initialize(old_column, new_column, statement)
41
+ @old_column, @new_column = old_column, new_column
42
+ @statement_method = statement
43
+ end
44
+
45
+ def up
46
+ @new_column.__send__(@statement_method)
47
+ end
48
+
49
+ def down
50
+ @old_column.__send__(@statement_method)
51
+ end
52
+ end
53
+
54
+ # Adds a column.
55
+ class AddColumn
56
+ def initialize(column)
57
+ @column = column
58
+ end
59
+
60
+ def up
61
+ @column.add_statement
62
+ end
63
+
64
+ def down
65
+ @column.drop_statement
66
+ end
67
+ end
68
+
69
+ # Drops a column.
70
+ class DropColumn
71
+ def initialize(column)
72
+ @column = column
73
+ end
74
+
75
+ def up
76
+ @column.drop_statement
77
+ end
78
+
79
+ def down
80
+ @column.add_statement
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,96 @@
1
+ module Sequel
2
+ module Schema
3
+ DbColumn = Struct.new(:name, :column_type, :null, :default, :unsigned, :size, :elements)
4
+
5
+ # A column in a database table.
6
+ #
7
+ # Responsible for generating all migration method calls used by
8
+ # migration operations.
9
+ #
10
+ class DbColumn
11
+ # Builds a DbColumn from a Hash of attribute values. Keys
12
+ # can be strings or symbols.
13
+ #
14
+ def self.build_from_hash(attrs={})
15
+ new *members.map {|key| attrs[key] || attrs[key.to_sym] }
16
+ end
17
+
18
+ # Returns a Sequel migration statement to define a column in a
19
+ # create_table block.
20
+ #
21
+ def define_statement
22
+ ["#{column_type} #{name.inspect}", options].compact.join(", ")
23
+ end
24
+
25
+ # Returns a Sequel migration statement to remove the column.
26
+ #
27
+ def drop_statement
28
+ "drop_column #{name.inspect}"
29
+ end
30
+
31
+ # Returns a Sequel migration statement to add the column to a
32
+ # table in an alter_table block.
33
+ #
34
+ def add_statement
35
+ ["add_column #{name.inspect}", column_type.inspect, options].compact.join(", ")
36
+ end
37
+
38
+ # Returns a Sequel migration statement to change whether a column
39
+ # allows null values.
40
+ #
41
+ def change_null_statement
42
+ "set_column_allow_null #{name.inspect}, #{(!!null).inspect}"
43
+ end
44
+
45
+ # Returns a Sequel migration statement to change a column's default
46
+ # value.
47
+ #
48
+ def change_default_statement
49
+ "set_column_default #{name.inspect}, #{default.inspect}"
50
+ end
51
+
52
+ # Returns a Sequel migration statement to change the type of an
53
+ # existing column. Null changes must be handled separately.
54
+ #
55
+ def change_type_statement
56
+ ["set_column_type #{name.inspect}", column_type.inspect, change_options].compact.join(", ")
57
+ end
58
+
59
+ # Returns an Array of attributes that are different between this
60
+ # and another column.
61
+ #
62
+ def diff(other)
63
+ result = []
64
+ each_pair {|key, value| result << key if other[key] != value }
65
+ result
66
+ end
67
+
68
+ private
69
+
70
+ def change_options
71
+ opts = []
72
+
73
+ opts << ":default => #{default.inspect}"
74
+ # seems odd, but we only want to output if unsigned is a true
75
+ # boolean, not if it is nil.
76
+ opts << ":unsigned => #{unsigned.inspect}" if unsigned == true || unsigned == false
77
+ opts << ":size => #{size.inspect}" if size
78
+ opts << ":elements => #{elements.inspect}" if elements
79
+
80
+ opts.join(", ") unless opts.empty?
81
+ end
82
+
83
+ def options
84
+ opts = []
85
+
86
+ opts << ":null => false" unless null == true
87
+ opts << ":default => #{default.inspect}" if default
88
+ opts << ":unsigned => true" if unsigned
89
+ opts << ":size => #{size.inspect}" if size
90
+ opts << ":elements => #{elements.inspect}" if elements
91
+
92
+ opts.join(", ") unless opts.empty?
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,94 @@
1
+ module Sequel
2
+ module Schema
3
+ # Builds an abstract representation of a database schema.
4
+ #
5
+ # Sample usage:
6
+ #
7
+ # parser = DbSchemaParser.for_db( sequel_db_connection )
8
+ # parser.parse_db_schema
9
+ # # => Returns an array of table definitions
10
+ #
11
+ class DbSchemaParser
12
+ # Returns an appropriate schema parser for the database
13
+ # connection.
14
+ #
15
+ def self.for_db(db)
16
+ self.new(db)
17
+ end
18
+
19
+ # Parses the schema from a Sequel Database connection.
20
+ #
21
+ # Returns a hash of table representations.
22
+ #
23
+ # Example:
24
+ #
25
+ # builder.parse_db_schema(db)
26
+ # # => {:table1 => { :columns => [ DbColumns ... ] }
27
+ # :table2 => { ... } }
28
+ #
29
+ def parse_db_schema
30
+ result = {}
31
+ @db.tables.each do |table_name|
32
+ result[table_name] = {:columns => parse_table_schema(@db.schema(table_name))}
33
+ end
34
+ result
35
+ end
36
+
37
+ # Extracts an array of hashes representing the columns in the
38
+ # table, given an Array of Arrays returned by DB.schema(:table).
39
+ #
40
+ def parse_table_schema(db_schema)
41
+ db_schema.map do |column|
42
+ attrs = {
43
+ :name => column.first,
44
+ :default => column.last[:ruby_default],
45
+ :null => column.last[:allow_null],
46
+ :column_type => parse_type(column.last[:db_type]),
47
+ :unsigned => column.last[:db_type].include?(" unsigned")
48
+ }
49
+ attrs[:size] = extract_size(column) if column.last[:type] == :string
50
+ attrs[:elements] = extract_enum_elements(column) if attrs[:column_type] == :enum
51
+
52
+ DbColumn.build_from_hash(attrs)
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ # Creates a new schema parser for the given database
59
+ # connection. Use for_db instead.
60
+ #
61
+ def initialize(db)
62
+ @db = db
63
+ end
64
+
65
+ # Returns a type symbol for a given db_type string, suitable for
66
+ # use in a Sequel migration.
67
+ #
68
+ # Examples:
69
+ #
70
+ # parse_type("int(11)") # => :integer
71
+ # parse_type("varchar(20)") # => :varchar
72
+ #
73
+ def parse_type(type)
74
+ case type
75
+ when /^int/ then :integer
76
+ when /^tinyint\(1\)/ then :boolean
77
+ when /^([^(]+)/ then $1.to_sym
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def extract_size(column)
84
+ match = column.last[:db_type].match(/\((\d+)\)/)
85
+ match[1].to_i if match[1]
86
+ end
87
+
88
+ def extract_enum_elements(column)
89
+ match = column.last[:db_type].match(/\(([^)]+)\)/)
90
+ eval('[' + match[1] + ']') if match[1]
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,67 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{sequel_migration_builder}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Roland Swingler"]
12
+ s.date = %q{2010-06-13}
13
+ s.description = %q{Build Sequel Migrations based on the differences between two schemas}
14
+ s.email = %q{roland.swingler@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/sequel/migration_builder.rb",
27
+ "lib/sequel/schema/alter_table_operations.rb",
28
+ "lib/sequel/schema/db_column.rb",
29
+ "lib/sequel/schema/db_schema_parser.rb",
30
+ "sequel_migration_builder.gemspec",
31
+ "spec/alter_table_operations_spec.rb",
32
+ "spec/db_column_spec.rb",
33
+ "spec/db_schema_parser_spec.rb",
34
+ "spec/migration_builder_spec.rb",
35
+ "spec/spec.opts",
36
+ "spec/spec_helper.rb"
37
+ ]
38
+ s.homepage = %q{http://github.com/knaveofdiamonds/sequel_migration_builder}
39
+ s.rdoc_options = ["--charset=UTF-8"]
40
+ s.require_paths = ["lib"]
41
+ s.rubygems_version = %q{1.3.6}
42
+ s.summary = %q{Build Sequel Migrations based on the differences between two schemas}
43
+ s.test_files = [
44
+ "spec/alter_table_operations_spec.rb",
45
+ "spec/db_column_spec.rb",
46
+ "spec/db_schema_parser_spec.rb",
47
+ "spec/migration_builder_spec.rb",
48
+ "spec/spec_helper.rb"
49
+ ]
50
+
51
+ if s.respond_to? :specification_version then
52
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
53
+ s.specification_version = 3
54
+
55
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
56
+ s.add_runtime_dependency(%q<sequel>, [">= 3.12.0"])
57
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
58
+ else
59
+ s.add_dependency(%q<sequel>, [">= 3.12.0"])
60
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
61
+ end
62
+ else
63
+ s.add_dependency(%q<sequel>, [">= 3.12.0"])
64
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
65
+ end
66
+ end
67
+
@@ -0,0 +1,152 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ def build_column(hash)
4
+ Sequel::Schema::DbColumn.build_from_hash(hash)
5
+ end
6
+
7
+ describe "Sequel::Schema::AlterTableOperations.build_column_operations" do
8
+ it "should return an empty Array if there are no differences between column definitions" do
9
+ a = build_column(:name => :foo, :column_type => :integer)
10
+ b = build_column(:name => :foo, :column_type => :integer)
11
+
12
+ Sequel::Schema::AlterTableOperations.build_column_operations(a,b).should == []
13
+ end
14
+
15
+ it "should return a ChangeColumn operation if the types are different" do
16
+ a = build_column(:name => :foo, :column_type => :integer)
17
+ b = build_column(:name => :foo, :column_type => :smallint)
18
+ ops = Sequel::Schema::AlterTableOperations.build_column_operations(a,b)
19
+
20
+ ops.first.up.should == "set_column_type :foo, :smallint, :default => nil"
21
+ end
22
+
23
+ it "should return a ChangeColumn operation if the sizes are different" do
24
+ a = build_column(:name => :foo, :column_type => :char, :size => 20)
25
+ b = build_column(:name => :foo, :column_type => :char, :size => 10)
26
+ ops = Sequel::Schema::AlterTableOperations.build_column_operations(a,b)
27
+
28
+ ops.first.up.should == "set_column_type :foo, :char, :default => nil, :size => 10"
29
+ end
30
+
31
+ it "should return a ChangeColumn operation if the unsigned value is different" do
32
+ a = build_column(:name => :foo, :column_type => :integer, :unsigned => true)
33
+ b = build_column(:name => :foo, :column_type => :integer, :unsigned => false)
34
+ ops = Sequel::Schema::AlterTableOperations.build_column_operations(a,b)
35
+
36
+ ops.first.up.should == "set_column_type :foo, :integer, :default => nil, :unsigned => false"
37
+ end
38
+
39
+ it "should return a ChangeColumn operation to set the null value if the null value is different" do
40
+ a = build_column(:name => :foo, :column_type => :integer, :null => true)
41
+ b = build_column(:name => :foo, :column_type => :integer, :null => false)
42
+ ops = Sequel::Schema::AlterTableOperations.build_column_operations(a,b)
43
+
44
+ ops.first.up.should == "set_column_allow_null :foo, false"
45
+ end
46
+
47
+ it "should return a ChangeColumn operation to set the default if the default value is different" do
48
+ a = build_column(:name => :foo, :column_type => :integer, :default => 1)
49
+ b = build_column(:name => :foo, :column_type => :integer, :default => 2)
50
+ ops = Sequel::Schema::AlterTableOperations.build_column_operations(a,b)
51
+
52
+ ops.first.up.should == "set_column_default :foo, 2"
53
+ end
54
+
55
+ it "should only return 1 operation if the default and other values are different" do
56
+ a = build_column(:name => :foo, :column_type => :integer, :default => 1)
57
+ b = build_column(:name => :foo, :column_type => :smallint, :default => 2)
58
+ ops = Sequel::Schema::AlterTableOperations.build_column_operations(a,b)
59
+
60
+ ops.size.should == 1
61
+ ops.first.up.should == "set_column_type :foo, :smallint, :default => 2"
62
+ end
63
+ end
64
+
65
+ describe "Sequel::Schema::AlterTableOperations.build" do
66
+ it "should return an empty array if nothing is different" do
67
+ table_a = {:name => :example_table,
68
+ :columns => [build_column(:name => :foo, :column_type => :integer)]}
69
+ table_b = {:name => :example_table,
70
+ :columns => [build_column(:name => :foo, :column_type => :integer)]}
71
+ ops = Sequel::Schema::AlterTableOperations.build(table_a,table_b)
72
+
73
+ ops.should == []
74
+ end
75
+
76
+ it "should return an add column operation if the column is new" do
77
+ table_a = {:name => :example_table,
78
+ :columns => []}
79
+ table_b = {:name => :example_table,
80
+ :columns => [build_column(:name => :foo, :column_type => :integer)]}
81
+ ops = Sequel::Schema::AlterTableOperations.build(table_a,table_b)
82
+
83
+ ops.size.should == 1
84
+ ops.first.should be_kind_of(Sequel::Schema::AlterTableOperations::AddColumn)
85
+ end
86
+
87
+ it "should return a drop column operation if the column has been removed" do
88
+ table_a = {:name => :example_table,
89
+ :columns => [build_column(:name => :foo, :column_type => :integer)]}
90
+ table_b = {:name => :example_table,
91
+ :columns => []}
92
+ ops = Sequel::Schema::AlterTableOperations.build(table_a,table_b)
93
+
94
+ ops.size.should == 1
95
+ ops.first.should be_kind_of(Sequel::Schema::AlterTableOperations::DropColumn)
96
+ end
97
+
98
+ it "should return a change column operation if columns are different" do
99
+ table_a = {:name => :example_table,
100
+ :columns => [build_column(:name => :foo, :column_type => :integer)]}
101
+ table_b = {:name => :example_table,
102
+ :columns => [build_column(:name => :foo, :column_type => :smallint)]}
103
+ ops = Sequel::Schema::AlterTableOperations.build(table_a,table_b)
104
+
105
+ ops.size.should == 1
106
+ ops.first.should be_kind_of(Sequel::Schema::AlterTableOperations::ChangeColumn)
107
+ end
108
+ end
109
+
110
+ describe Sequel::Schema::AlterTableOperations::AddColumn do
111
+ before(:each) { @mock_column = mock() }
112
+
113
+ it "should ask the column for its add column statement on #up" do
114
+ @mock_column.should_receive(:add_statement)
115
+ Sequel::Schema::AlterTableOperations::AddColumn.new(@mock_column).up
116
+ end
117
+
118
+ it "should ask the column for its drop column statement on #down" do
119
+ @mock_column.should_receive(:drop_statement)
120
+ Sequel::Schema::AlterTableOperations::AddColumn.new(@mock_column).down
121
+ end
122
+ end
123
+
124
+ describe Sequel::Schema::AlterTableOperations::DropColumn do
125
+ before(:each) { @mock_column = mock() }
126
+
127
+ it "should ask the column for its drop column statement on #up" do
128
+ @mock_column.should_receive(:drop_statement)
129
+ Sequel::Schema::AlterTableOperations::DropColumn.new(@mock_column).up
130
+ end
131
+
132
+ it "should ask the column for its add column statement on #down" do
133
+ @mock_column.should_receive(:add_statement)
134
+ Sequel::Schema::AlterTableOperations::DropColumn.new(@mock_column).down
135
+ end
136
+ end
137
+
138
+ describe Sequel::Schema::AlterTableOperations::ChangeColumn do
139
+ it "should ask the new column for statement on #up" do
140
+ new = mock(:new)
141
+ old = mock(:old)
142
+ new.should_receive(:change_type_statement)
143
+ Sequel::Schema::AlterTableOperations::ChangeColumn.new(old, new, :change_type_statement).up
144
+ end
145
+
146
+ it "should ask the new column for statement on #down" do
147
+ new = mock(:new)
148
+ old = mock(:old)
149
+ old.should_receive(:change_type_statement)
150
+ Sequel::Schema::AlterTableOperations::ChangeColumn.new(old, new, :change_type_statement).down
151
+ end
152
+ end
@@ -0,0 +1,47 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ describe Sequel::Schema::DbColumn do
4
+
5
+ before :each do
6
+ @column = Sequel::Schema::DbColumn.new(:foo, :integer, false, 10, true, 10, nil)
7
+ end
8
+
9
+ it "should return a #define_statement" do
10
+ @column.define_statement.should == "integer :foo, :null => false, :default => 10, :unsigned => true, :size => 10"
11
+ end
12
+
13
+ it "should return a #drop_statement" do
14
+ @column.drop_statement.should == "drop_column :foo"
15
+ end
16
+
17
+ it "should return an #add_statement" do
18
+ @column.add_statement.should == "add_column :foo, :integer, :null => false, :default => 10, :unsigned => true, :size => 10"
19
+ end
20
+
21
+ it "should return a #change_null statement" do
22
+ @column.change_null_statement.should == "set_column_allow_null :foo, false"
23
+ end
24
+
25
+ it "should return a #change_default statement" do
26
+ @column.change_default_statement.should == "set_column_default :foo, 10"
27
+ end
28
+
29
+ it "should return a #change_type statement" do
30
+ @column.change_type_statement.should == "set_column_type :foo, :integer, :default => 10, :unsigned => true, :size => 10"
31
+ end
32
+
33
+ it "should be diffable with another DbColumn" do
34
+ other = Sequel::Schema::DbColumn.new(:foo, :smallint, false, 10, true, 10, nil)
35
+ @column.diff(other).should == [:column_type]
36
+
37
+ other = Sequel::Schema::DbColumn.new(:foo, :integer, true, 11, true, 10, nil)
38
+ @column.diff(other).should == [:null, :default]
39
+ end
40
+
41
+ it "should be buildable from a Hash" do
42
+ Sequel::Schema::DbColumn.build_from_hash(:name => "foo",
43
+ :column_type => "integer").column_type.should == "integer"
44
+ Sequel::Schema::DbColumn.build_from_hash('name' => "foo",
45
+ 'column_type' => "integer").name.should == "foo"
46
+ end
47
+ end
@@ -0,0 +1,99 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ describe "Sequel::Schema::DbSchemaParser.for_db" do
4
+ it "should return a DbSchemaParser" do
5
+ Sequel::Schema::DbSchemaParser.for_db(stub(:database)).should \
6
+ be_kind_of(Sequel::Schema::DbSchemaParser)
7
+ end
8
+ end
9
+
10
+ describe "A hash in the array returned by Sequel::Schema::DbSchemaParser#parse_table_schema" do
11
+ before :each do
12
+ @parser = Sequel::Schema::DbSchemaParser.for_db(stub(:database))
13
+ @schema = [[:example_column,
14
+ { :type => :integer,
15
+ :default => "1",
16
+ :ruby_default => 1,
17
+ :primary_key => false,
18
+ :db_type => "int(11)",
19
+ :allow_null => true }]]
20
+ end
21
+
22
+ it "should contain the :name of the column" do
23
+ @parser.parse_table_schema(@schema).first.name.should == :example_column
24
+ end
25
+
26
+ it "should contain the ruby_default as the :default" do
27
+ @parser.parse_table_schema(@schema).first.default.should == 1
28
+ end
29
+
30
+ it "should contain whether the column can be :null" do
31
+ @parser.parse_table_schema(@schema).first.null.should == true
32
+ end
33
+
34
+ it "should contain a type of :integer given a int column" do
35
+ set_db_type "int(11)"
36
+ @parser.parse_table_schema(@schema).first.column_type.should == :integer
37
+ end
38
+
39
+ it "should contain a type of :boolean given a tinyint(1) column" do
40
+ set_db_type "tinyint(1)"
41
+ @parser.parse_table_schema(@schema).first.column_type.should == :boolean
42
+ end
43
+
44
+ it "should contain a type of :tinyint given a tinyint column" do
45
+ set_db_type "tinyint(4)"
46
+ @parser.parse_table_schema(@schema).first.column_type.should == :tinyint
47
+ end
48
+
49
+ it "should contain a type of :smallint given a smallint column" do
50
+ set_db_type "smallint(5)"
51
+ @parser.parse_table_schema(@schema).first.column_type.should == :smallint
52
+ end
53
+
54
+ it "should contain a type of :mediumint given a mediumint column" do
55
+ set_db_type "mediumint(5)"
56
+ @parser.parse_table_schema(@schema).first.column_type.should == :mediumint
57
+ end
58
+
59
+ it "should contain a type of :bigint given a bigint column" do
60
+ set_db_type "bigint(10)"
61
+ @parser.parse_table_schema(@schema).first.column_type.should == :bigint
62
+ end
63
+
64
+ it "should contain a :size attribute for text-like columns" do
65
+ set_db_type "varchar(20)", :string
66
+ @parser.parse_table_schema(@schema).first.size.should == 20
67
+ end
68
+
69
+ it "should contain :unsigned false if a numeric column is not unsigned" do
70
+ set_db_type "int(10)"
71
+ @parser.parse_table_schema(@schema).first.unsigned.should == false
72
+ end
73
+
74
+ it "should contain :unsigned true if a numeric column is unsigned" do
75
+ set_db_type "int(10) unsigned"
76
+ @parser.parse_table_schema(@schema).first.unsigned.should == true
77
+ end
78
+
79
+ it "should contain the elements of an enum column" do
80
+ set_db_type "enum('foo','bar')"
81
+ @parser.parse_table_schema(@schema).first.elements.should == ['foo', 'bar']
82
+ end
83
+
84
+ def set_db_type(type, ruby_type=nil)
85
+ @schema.first.last.merge!(:db_type => type)
86
+ @schema.first.last.merge!(:type => ruby_type) if ruby_type
87
+ end
88
+ end
89
+
90
+ describe "Sequel::Schema::DbSchemaParser#parse_db_schema" do
91
+ it "should extract a list of table definitions from a database" do
92
+ mock_db = mock(:db)
93
+ mock_db.should_receive(:tables).at_least(:once).and_return([:table1])
94
+ mock_db.should_receive(:schema).with(:table1).and_return([])
95
+
96
+ @parser = Sequel::Schema::DbSchemaParser.for_db(mock_db)
97
+ @parser.parse_db_schema.keys.should == [:table1]
98
+ end
99
+ end
@@ -0,0 +1,143 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ describe Sequel::MigrationBuilder do
4
+
5
+ it "should return nil if the table hash is empty and the database has no tables" do
6
+ mock_db = mock(:database)
7
+ mock_db.should_receive(:tables).at_least(:once).and_return([])
8
+ Sequel::MigrationBuilder.new(mock_db).generate_migration({}).should be_nil
9
+ end
10
+
11
+ it "should produce a simple migration string given a database connection and a hash of tables" do
12
+ tables = {}
13
+ tables[:example_table] = {
14
+ :columns => [{:name => :foo, :column_type => :integer}]
15
+ }
16
+
17
+ expected = <<-END
18
+ Sequel.migration do
19
+ up do
20
+ create_table :example_table do
21
+ integer :foo, :null => false
22
+ end
23
+ end
24
+
25
+ down do
26
+ drop_table :example_table
27
+ end
28
+ end
29
+ END
30
+
31
+ mock_db = mock(:database)
32
+ mock_db.should_receive(:tables).at_least(:once).and_return([])
33
+ Sequel::MigrationBuilder.new(mock_db).generate_migration(tables).should == expected
34
+ end
35
+
36
+ it "should produce statements for multiple new tables" do
37
+ tables = {}
38
+ tables[:example_table] = {
39
+ :columns => [{:name => :foo, :column_type => :integer}, {:name => :bar, :column_type => :varchar}]
40
+ }
41
+
42
+ tables[:example_table_2] = {
43
+ :columns => [{:name => :foo, :column_type => :integer, :null => true}]
44
+ }
45
+
46
+ expected = <<-END
47
+ Sequel.migration do
48
+ up do
49
+ create_table :example_table do
50
+ integer :foo, :null => false
51
+ varchar :bar, :null => false
52
+ end
53
+
54
+ create_table :example_table_2 do
55
+ integer :foo
56
+ end
57
+ end
58
+
59
+ down do
60
+ drop_table :example_table_2
61
+ drop_table :example_table
62
+ end
63
+ end
64
+ END
65
+
66
+ mock_db = mock(:database)
67
+ mock_db.should_receive(:tables).at_least(:once).and_return([])
68
+ Sequel::MigrationBuilder.new(mock_db).generate_migration(tables).should == expected
69
+ end
70
+
71
+ it "should add the primary key of the table" do
72
+ mock_db = mock(:database)
73
+ mock_db.should_receive(:tables).at_least(:once).and_return([])
74
+ table = {
75
+ :primary_key => :foo,
76
+ :columns => [{:name => :foo, :column_type => :integer}, {:name => :bar, :column_type => :varchar}]
77
+ }
78
+
79
+ expected = <<-END
80
+ create_table :example_table do
81
+ integer :foo, :null => false
82
+ varchar :bar, :null => false
83
+
84
+ primary_key :foo
85
+ end
86
+ END
87
+
88
+ Sequel::MigrationBuilder.new(mock_db).create_table_statement(:example_table, table).join("\n").
89
+ should == expected.strip
90
+ end
91
+
92
+
93
+ context "when a table needs to be altered" do
94
+ before :each do
95
+ @tables = { :example_table =>
96
+ {:columns => [{:name => :foo, :column_type => :integer}, {:name => :bar, :column_type => :varchar}]}
97
+ }
98
+ @mock_db = mock(:database)
99
+ @mock_db.should_receive(:tables).at_least(:once).and_return([:example_table])
100
+ @mock_db.should_receive(:schema).with(:example_table).and_return([[:foo, {:type => :integer, :db_type => "smallint(5) unsigned", :allow_null => true, :ruby_default => 10}]])
101
+
102
+ end
103
+
104
+ it "should return an alter table statement with column changes for #generate_up" do
105
+ expected = <<-END
106
+ up do
107
+ alter_table :example_table do
108
+ set_column_type :foo, :integer, :default => nil
109
+ set_column_allow_null :foo, false
110
+ add_column :bar, :varchar, :null => false
111
+ end
112
+ end
113
+ END
114
+ Sequel::MigrationBuilder.new(@mock_db).generate_up(@tables).join("\n").should == expected
115
+ end
116
+
117
+ it "should return an alter table statement with column changes for #generate_down" do
118
+ expected = <<-END
119
+ down do
120
+ alter_table :example_table do
121
+ set_column_type :foo, :smallint, :default => 10, :unsigned => true
122
+ set_column_allow_null :foo, true
123
+ drop_column :bar
124
+ end
125
+ end
126
+ END
127
+ Sequel::MigrationBuilder.new(@mock_db).generate_down(@tables).join("\n").should == expected.strip
128
+ end
129
+ end
130
+
131
+ it "should drop the table if the table exists in the database but not the table hash" do
132
+ pending # Deal with in a later version.
133
+ mock_db = mock(:database)
134
+ mock_db.should_receive(:tables).at_least(:once).and_return([:example_table])
135
+
136
+ expected = <<-END
137
+ up do
138
+ drop_table :example_table
139
+ end
140
+ END
141
+ Sequel::MigrationBuilder.new(mock_db).generate_up({}).join("\n").should == expected
142
+ end
143
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'sequel/migration_builder'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequel_migration_builder
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Roland Swingler
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-06-13 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: sequel
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 3
29
+ - 12
30
+ - 0
31
+ version: 3.12.0
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 2
44
+ - 9
45
+ version: 1.2.9
46
+ type: :development
47
+ version_requirements: *id002
48
+ description: Build Sequel Migrations based on the differences between two schemas
49
+ email: roland.swingler@gmail.com
50
+ executables: []
51
+
52
+ extensions: []
53
+
54
+ extra_rdoc_files:
55
+ - LICENSE
56
+ - README.rdoc
57
+ files:
58
+ - .document
59
+ - .gitignore
60
+ - LICENSE
61
+ - README.rdoc
62
+ - Rakefile
63
+ - VERSION
64
+ - lib/sequel/migration_builder.rb
65
+ - lib/sequel/schema/alter_table_operations.rb
66
+ - lib/sequel/schema/db_column.rb
67
+ - lib/sequel/schema/db_schema_parser.rb
68
+ - sequel_migration_builder.gemspec
69
+ - spec/alter_table_operations_spec.rb
70
+ - spec/db_column_spec.rb
71
+ - spec/db_schema_parser_spec.rb
72
+ - spec/migration_builder_spec.rb
73
+ - spec/spec.opts
74
+ - spec/spec_helper.rb
75
+ has_rdoc: true
76
+ homepage: http://github.com/knaveofdiamonds/sequel_migration_builder
77
+ licenses: []
78
+
79
+ post_install_message:
80
+ rdoc_options:
81
+ - --charset=UTF-8
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ segments:
96
+ - 0
97
+ version: "0"
98
+ requirements: []
99
+
100
+ rubyforge_project:
101
+ rubygems_version: 1.3.6
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: Build Sequel Migrations based on the differences between two schemas
105
+ test_files:
106
+ - spec/alter_table_operations_spec.rb
107
+ - spec/db_column_spec.rb
108
+ - spec/db_schema_parser_spec.rb
109
+ - spec/migration_builder_spec.rb
110
+ - spec/spec_helper.rb