sequel_migration_builder 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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