sequel_migration_builder 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +4 -0
- data/Rakefile +5 -0
- data/VERSION +1 -1
- data/lib/sequel/migration_builder.rb +49 -30
- data/lib/sequel/schema/db_column.rb +87 -19
- data/lib/sequel/schema/db_schema_parser.rb +27 -19
- data/sequel_migration_builder.gemspec +2 -2
- data/spec/db_column_spec.rb +51 -2
- data/spec/db_schema_parser_spec.rb +23 -1
- data/spec/spec_helper.rb +2 -0
- metadata +3 -3
data/README.rdoc
CHANGED
@@ -19,6 +19,8 @@ as a hash. A sample YAML version might look like:
|
|
19
19
|
|
20
20
|
example_table:
|
21
21
|
primary_key: id
|
22
|
+
table_options:
|
23
|
+
engine: myisam
|
22
24
|
columns:
|
23
25
|
- name: id
|
24
26
|
column_type: integer
|
@@ -39,6 +41,8 @@ as a hash. A sample YAML version might look like:
|
|
39
41
|
* Dropping tables when they are removed from the schema
|
40
42
|
* Automigrate functionality
|
41
43
|
* Dealing with renames in some way (even if just logging that they would be possible).
|
44
|
+
* Changing table options with alter table
|
45
|
+
* More database type parsers
|
42
46
|
|
43
47
|
== Note on Patches/Pull Requests
|
44
48
|
|
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.5
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'sequel/extensions/blank'
|
1
2
|
require 'sequel/schema/db_column'
|
2
3
|
require 'sequel/schema/db_schema_parser'
|
3
4
|
require 'sequel/schema/alter_table_operations'
|
@@ -60,52 +61,58 @@ module Sequel
|
|
60
61
|
|
61
62
|
add_line "down do"
|
62
63
|
alter_tables(current_tables, tables, :down)
|
63
|
-
|
64
|
-
new_tables.reverse.each {|table_name| add_line "drop_table #{table_name.inspect}" }
|
65
|
-
end
|
64
|
+
drop_new_tables(new_tables)
|
66
65
|
add_line "end"
|
67
66
|
end
|
68
67
|
|
69
68
|
# Generates any create table statements for new tables.
|
70
69
|
#
|
71
|
-
def create_new_tables(
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
indent { create_table_statement table_name, tables[table_name] }
|
76
|
-
add_blank_line unless i == tables.size
|
70
|
+
def create_new_tables(new_table_names, tables)
|
71
|
+
each_table(new_table_names, tables) do |table_name, table, last_table|
|
72
|
+
create_table_statement table_name, table
|
73
|
+
add_blank_line unless last_table
|
77
74
|
end
|
78
75
|
end
|
79
76
|
|
80
|
-
# Generates any
|
77
|
+
# Generates any drop table statements for new tables.
|
81
78
|
#
|
82
|
-
def
|
83
|
-
i = 0
|
79
|
+
def drop_new_tables(new_table_names)
|
84
80
|
indent do
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
81
|
+
new_table_names.reverse.each {|table_name| add_line "drop_table #{table_name.inspect}" }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Generates any alter table statements for current tables.
|
86
|
+
#
|
87
|
+
def alter_tables(current_table_names, tables, direction)
|
88
|
+
each_table(current_table_names, tables) do |table_name, table, last_table|
|
89
|
+
hsh = table.dup
|
90
|
+
hsh[:columns] = hsh[:columns].map {|c| Schema::DbColumn.build_from_hash(c) }
|
91
|
+
operations = Schema::AlterTableOperations.build(@db_tables[table_name], hsh)
|
92
|
+
unless operations.empty?
|
93
|
+
alter_table_statement table_name, operations, direction
|
94
|
+
add_blank_line unless last_table
|
98
95
|
end
|
99
96
|
end
|
100
97
|
end
|
101
98
|
|
99
|
+
# Generates an individual alter table statement.
|
100
|
+
#
|
101
|
+
def alter_table_statement(table_name, operations, direction)
|
102
|
+
add_line "alter_table #{table_name.inspect} do"
|
103
|
+
indent do
|
104
|
+
operations.each {|op| add_line op.__send__(direction) }
|
105
|
+
end
|
106
|
+
add_line "end"
|
107
|
+
end
|
108
|
+
|
102
109
|
# Generates an individual create_table statement.
|
103
110
|
#
|
104
111
|
def create_table_statement(table_name, table)
|
105
|
-
add_line "create_table #{table_name.inspect}#{
|
112
|
+
add_line "create_table #{table_name.inspect}#{pretty_hash(table[:table_options])} do"
|
106
113
|
indent do
|
107
|
-
table[:columns].
|
108
|
-
add_line
|
114
|
+
table[:columns].each do |c|
|
115
|
+
add_line Schema::DbColumn.build_from_hash(c).define_statement
|
109
116
|
end
|
110
117
|
if table[:primary_key]
|
111
118
|
add_blank_line
|
@@ -119,8 +126,20 @@ module Sequel
|
|
119
126
|
|
120
127
|
attr_reader :result
|
121
128
|
|
122
|
-
def
|
123
|
-
|
129
|
+
def each_table(table_names, tables)
|
130
|
+
i = 0
|
131
|
+
indent do
|
132
|
+
table_names.each do |table_name|
|
133
|
+
i += 1
|
134
|
+
yield table_name, tables[table_name], i == tables.size
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns a string representing a hash as ':foo => :bar'
|
140
|
+
# rather than '{:foo=.:bar}'
|
141
|
+
def pretty_hash(hash)
|
142
|
+
", " + hash.inspect.gsub(/^\{|\}$/,'').gsub("=>", " => ") if hash
|
124
143
|
end
|
125
144
|
|
126
145
|
def table_names(tables)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module Sequel
|
2
4
|
module Schema
|
3
5
|
DbColumn = Struct.new(:name, :column_type, :null, :default, :unsigned, :size, :elements)
|
@@ -8,11 +10,20 @@ module Sequel
|
|
8
10
|
# migration operations.
|
9
11
|
#
|
10
12
|
class DbColumn
|
13
|
+
# Database column types that hold integers.
|
14
|
+
INTEGER_TYPES = [:tinyint, :integer, :smallint, :mediumint, :bigint]
|
15
|
+
|
16
|
+
# Database column types that hold fractional values.
|
17
|
+
DECIMAL_TYPES = [:decimal, :float, :bigdecimal]
|
18
|
+
|
19
|
+
# All numeric database column types.
|
20
|
+
NUMERIC_TYPES = INTEGER_TYPES + DECIMAL_TYPES
|
21
|
+
|
11
22
|
# Builds a DbColumn from a Hash of attribute values. Keys
|
12
23
|
# can be strings or symbols.
|
13
24
|
#
|
14
25
|
def self.build_from_hash(attrs={})
|
15
|
-
new *members.map {|key| attrs[key] || attrs[key.to_sym] }
|
26
|
+
self.new *members.map {|key| attrs[key] || attrs[key.to_sym] }
|
16
27
|
end
|
17
28
|
|
18
29
|
# Returns a Sequel migration statement to define a column in a
|
@@ -56,40 +67,97 @@ module Sequel
|
|
56
67
|
["set_column_type #{name.inspect}", column_type.inspect, change_options].compact.join(", ")
|
57
68
|
end
|
58
69
|
|
59
|
-
# Returns an
|
70
|
+
# Returns an Set of attributes that are different between this
|
60
71
|
# and another column.
|
61
72
|
#
|
62
73
|
def diff(other)
|
63
|
-
|
64
|
-
|
65
|
-
|
74
|
+
{ :null => :boolean_attribute_different?,
|
75
|
+
:unsigned => :boolean_attribute_different?,
|
76
|
+
:name => :attribute_different?,
|
77
|
+
:column_type => :attribute_different?,
|
78
|
+
:elements => :attribute_different?,
|
79
|
+
:default => :defaults_different?,
|
80
|
+
:size => :sizes_different?
|
81
|
+
}.select {|attribute, method| __send__(method, attribute, other) }.map {|a| a.first }.to_set
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns true if this column is numeric
|
85
|
+
#
|
86
|
+
def numeric?
|
87
|
+
NUMERIC_TYPES.include?(column_type)
|
66
88
|
end
|
67
89
|
|
68
90
|
private
|
69
91
|
|
92
|
+
def attribute_different?(sym, other)
|
93
|
+
other[sym] != self[sym]
|
94
|
+
end
|
95
|
+
|
96
|
+
def boolean_attribute_different?(sym, other)
|
97
|
+
(!!other[sym]) != (!!self[sym])
|
98
|
+
end
|
99
|
+
|
100
|
+
def numeric_attribute_different?(sym, other)
|
101
|
+
(self[sym] || 0) != (other[sym] || 0)
|
102
|
+
end
|
103
|
+
|
104
|
+
def sizes_different?(_, other)
|
105
|
+
# Null size indicates 'lack of interest' in the size
|
106
|
+
size && other.size && other.size != size
|
107
|
+
end
|
108
|
+
|
109
|
+
def defaults_different?(_, other)
|
110
|
+
# Complicated by dealing with database defaults if the column
|
111
|
+
# does not allow null values.
|
112
|
+
if null == true || other.null == true
|
113
|
+
attribute_different?(:default, other)
|
114
|
+
elsif numeric? && other.numeric?
|
115
|
+
numeric_attribute_different?(:default, other)
|
116
|
+
else
|
117
|
+
! (default.blank? && other.default.blank?) && other.default != default
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
70
121
|
def change_options
|
71
|
-
opts =
|
122
|
+
opts = OptionBuilder.new
|
72
123
|
|
73
|
-
opts
|
124
|
+
opts.set :default, default
|
74
125
|
# seems odd, but we only want to output if unsigned is a true
|
75
126
|
# boolean, not if it is nil.
|
76
|
-
opts
|
77
|
-
opts
|
78
|
-
opts
|
79
|
-
|
80
|
-
opts.
|
127
|
+
opts.set :unsigned, unsigned if numeric? && (unsigned == true || unsigned == false)
|
128
|
+
opts.set :size, size if size
|
129
|
+
opts.set :elements, elements if elements
|
130
|
+
|
131
|
+
opts.render
|
81
132
|
end
|
82
133
|
|
83
134
|
def options
|
84
|
-
opts =
|
135
|
+
opts = OptionBuilder.new
|
136
|
+
|
137
|
+
opts.set :null, !!null if null != true || column_type == :timestamp
|
138
|
+
opts.set :default, default if default || column_type == :timestamp
|
139
|
+
opts.set :unsigned, true if numeric? && unsigned
|
140
|
+
opts.set :size, size if size
|
141
|
+
opts.set :elements, elements if elements
|
85
142
|
|
86
|
-
opts
|
87
|
-
|
88
|
-
opts << ":unsigned => true" if unsigned
|
89
|
-
opts << ":size => #{size.inspect}" if size
|
90
|
-
opts << ":elements => #{elements.inspect}" if elements
|
143
|
+
opts.render
|
144
|
+
end
|
91
145
|
|
92
|
-
|
146
|
+
# Formats column options in a Sequel migration
|
147
|
+
class OptionBuilder
|
148
|
+
def initialize
|
149
|
+
@opts = []
|
150
|
+
end
|
151
|
+
|
152
|
+
# Sets column option name to a value.
|
153
|
+
def set(name, value)
|
154
|
+
@opts << "#{name.inspect} => #{value.inspect}"
|
155
|
+
end
|
156
|
+
|
157
|
+
# Renders the column option hash in a pretty format.
|
158
|
+
def render
|
159
|
+
@opts.join(", ") unless @opts.empty?
|
160
|
+
end
|
93
161
|
end
|
94
162
|
end
|
95
163
|
end
|
@@ -27,11 +27,10 @@ module Sequel
|
|
27
27
|
# :table2 => { ... } }
|
28
28
|
#
|
29
29
|
def parse_db_schema
|
30
|
-
result
|
31
|
-
@db.tables.each do |table_name|
|
30
|
+
@db.tables.inject({}) do |result, table_name|
|
32
31
|
result[table_name] = {:columns => parse_table_schema(@db.schema(table_name))}
|
32
|
+
result
|
33
33
|
end
|
34
|
-
result
|
35
34
|
end
|
36
35
|
|
37
36
|
# Extracts an array of hashes representing the columns in the
|
@@ -39,17 +38,14 @@ module Sequel
|
|
39
38
|
#
|
40
39
|
def parse_table_schema(db_schema)
|
41
40
|
db_schema.map do |column|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
attrs[:elements] = extract_enum_elements(column) if attrs[:column_type] == :enum
|
51
|
-
|
52
|
-
DbColumn.build_from_hash(attrs)
|
41
|
+
type = parse_type(column.last[:db_type])
|
42
|
+
DbColumn.build_from_hash(:name => column.first,
|
43
|
+
:default => column.last[:ruby_default],
|
44
|
+
:null => column.last[:allow_null],
|
45
|
+
:column_type => type,
|
46
|
+
:unsigned => extract_unsigned(column.last[:db_type], type),
|
47
|
+
:size => extract_size(column.last[:db_type], type),
|
48
|
+
:elements => extract_enum_elements(column.last[:db_type], type))
|
53
49
|
end
|
54
50
|
end
|
55
51
|
|
@@ -80,13 +76,25 @@ module Sequel
|
|
80
76
|
|
81
77
|
private
|
82
78
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
79
|
+
def extract_unsigned(db_type_string, type)
|
80
|
+
return unless DbColumn::NUMERIC_TYPES.include?(type)
|
81
|
+
db_type_string.include?(" unsigned")
|
86
82
|
end
|
87
83
|
|
88
|
-
def
|
89
|
-
|
84
|
+
def extract_size(db_type_string, type)
|
85
|
+
return if DbColumn::INTEGER_TYPES.include?(type)
|
86
|
+
|
87
|
+
match = db_type_string.match(/\(([0-9, ]+)\)/)
|
88
|
+
if match && match[1]
|
89
|
+
n = match[1].split(/\s*,\s*/).map {|i| i.to_i }
|
90
|
+
n.size == 1 ? n.first : n
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def extract_enum_elements(db_type_string, type)
|
95
|
+
return unless type == :enum
|
96
|
+
|
97
|
+
match = db_type_string.match(/\(([^)]+)\)/)
|
90
98
|
eval('[' + match[1] + ']') if match[1]
|
91
99
|
end
|
92
100
|
end
|
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{sequel_migration_builder}
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.5"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Roland Swingler"]
|
12
|
-
s.date = %q{2010-06-
|
12
|
+
s.date = %q{2010-06-23}
|
13
13
|
s.description = %q{Build Sequel Migrations based on the differences between two schemas}
|
14
14
|
s.email = %q{roland.swingler@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
data/spec/db_column_spec.rb
CHANGED
@@ -32,10 +32,59 @@ describe Sequel::Schema::DbColumn do
|
|
32
32
|
|
33
33
|
it "should be diffable with another DbColumn" do
|
34
34
|
other = Sequel::Schema::DbColumn.new(:foo, :smallint, false, 10, true, 10, nil)
|
35
|
-
@column.diff(other).should == [:column_type]
|
35
|
+
@column.diff(other).should == [:column_type].to_set
|
36
36
|
|
37
37
|
other = Sequel::Schema::DbColumn.new(:foo, :integer, true, 11, true, 10, nil)
|
38
|
-
@column.diff(other).should == [:null, :default]
|
38
|
+
@column.diff(other).should == [:null, :default].to_set
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should not consider allowing null being nil different from false" do
|
42
|
+
a = Sequel::Schema::DbColumn.new(:foo, :smallint, false, 10, true, 10, nil)
|
43
|
+
b = Sequel::Schema::DbColumn.new(:foo, :smallint, nil, 10, true, 10, nil)
|
44
|
+
a.diff(b).should be_empty
|
45
|
+
b.diff(a).should be_empty
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should not consider size to be different if one of the sizes is nil" do
|
49
|
+
a = Sequel::Schema::DbColumn.new(:foo, :smallint, false, 10, true, 10, nil)
|
50
|
+
b = Sequel::Schema::DbColumn.new(:foo, :smallint, false, 10, true, nil, nil)
|
51
|
+
a.diff(b).should be_empty
|
52
|
+
b.diff(a).should be_empty
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should not consider 0 to be different from null if the column does not allow nulls" do
|
56
|
+
a = Sequel::Schema::DbColumn.new(:foo, :smallint, false, 0, true, 10, nil)
|
57
|
+
b = Sequel::Schema::DbColumn.new(:foo, :smallint, false, nil, true, nil, nil)
|
58
|
+
a.diff(b).should be_empty
|
59
|
+
b.diff(a).should be_empty
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should consider 0 to be different from null if the column does allow nulls" do
|
63
|
+
a = Sequel::Schema::DbColumn.new(:foo, :smallint, true, 0, true, 10, nil)
|
64
|
+
b = Sequel::Schema::DbColumn.new(:foo, :smallint, true, nil, true, nil, nil)
|
65
|
+
a.diff(b).should == [:default].to_set
|
66
|
+
b.diff(a).should == [:default].to_set
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should consider 1 to be different from null" do
|
70
|
+
a = Sequel::Schema::DbColumn.new(:foo, :smallint, false, 1, true, 10, nil)
|
71
|
+
b = Sequel::Schema::DbColumn.new(:foo, :smallint, false, nil, true, nil, nil)
|
72
|
+
a.diff(b).should == [:default].to_set
|
73
|
+
b.diff(a).should == [:default].to_set
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not consider '' to be different from null if the column does not allow nulls" do
|
77
|
+
a = Sequel::Schema::DbColumn.new(:foo, :varchar, false, '', true, 10, nil)
|
78
|
+
b = Sequel::Schema::DbColumn.new(:foo, :varchar, false, nil, true, nil, nil)
|
79
|
+
a.diff(b).should be_empty
|
80
|
+
b.diff(a).should be_empty
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should consider '' to be different from null if the column allows null" do
|
84
|
+
a = Sequel::Schema::DbColumn.new(:foo, :varchar, true, '', true, 10, nil)
|
85
|
+
b = Sequel::Schema::DbColumn.new(:foo, :varchar, true, nil, true, nil, nil)
|
86
|
+
a.diff(b).should == [:default].to_set
|
87
|
+
b.diff(a).should == [:default].to_set
|
39
88
|
end
|
40
89
|
|
41
90
|
it "should be buildable from a Hash" do
|
@@ -66,16 +66,38 @@ describe "A hash in the array returned by Sequel::Schema::DbSchemaParser#parse_t
|
|
66
66
|
@parser.parse_table_schema(@schema).first.size.should == 20
|
67
67
|
end
|
68
68
|
|
69
|
+
it "should contain a :size attribute for decimal columns" do
|
70
|
+
set_db_type "decimal(14,5)"
|
71
|
+
@parser.parse_table_schema(@schema).first.size.should == [14,5]
|
72
|
+
end
|
73
|
+
|
69
74
|
it "should contain :unsigned false if a numeric column is not unsigned" do
|
70
75
|
set_db_type "int(10)"
|
71
76
|
@parser.parse_table_schema(@schema).first.unsigned.should == false
|
72
77
|
end
|
73
78
|
|
74
|
-
it "should contain :unsigned true if
|
79
|
+
it "should contain :unsigned true if an integer column is unsigned" do
|
75
80
|
set_db_type "int(10) unsigned"
|
76
81
|
@parser.parse_table_schema(@schema).first.unsigned.should == true
|
77
82
|
end
|
78
83
|
|
84
|
+
it "should contain :unsigned true if a decimal column is unsigned" do
|
85
|
+
@schema = [[:example_column,
|
86
|
+
{ :type => nil,
|
87
|
+
:default => "1",
|
88
|
+
:ruby_default => 1,
|
89
|
+
:primary_key => false,
|
90
|
+
:db_type => "decimal(10,2) unsigned",
|
91
|
+
:allow_null => true }]]
|
92
|
+
|
93
|
+
@parser.parse_table_schema(@schema).first.unsigned.should == true
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should not contain an :unsigned value if not a numeric column" do
|
97
|
+
set_db_type "varchar(10)", :string
|
98
|
+
@parser.parse_table_schema(@schema).first.unsigned.should == nil
|
99
|
+
end
|
100
|
+
|
79
101
|
it "should contain the elements of an enum column" do
|
80
102
|
set_db_type "enum('foo','bar')"
|
81
103
|
@parser.parse_table_schema(@schema).first.elements.should == ['foo', 'bar']
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 5
|
9
|
+
version: 0.0.5
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Roland Swingler
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-06-
|
17
|
+
date: 2010-06-23 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|