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 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
@@ -44,3 +44,8 @@ Rake::RDocTask.new do |rdoc|
44
44
  rdoc.rdoc_files.include('README*')
45
45
  rdoc.rdoc_files.include('lib/**/*.rb')
46
46
  end
47
+
48
+ desc "Flog this baby!"
49
+ task :flog do
50
+ sh 'find lib -name "*.rb" | xargs flog'
51
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.4
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
- indent do
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(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
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 alter table statements for current tables.
77
+ # Generates any drop table statements for new tables.
81
78
  #
82
- def alter_tables(current_tables, tables, direction)
83
- i = 0
79
+ def drop_new_tables(new_table_names)
84
80
  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
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}#{options_str(table)} do"
112
+ add_line "create_table #{table_name.inspect}#{pretty_hash(table[:table_options])} do"
106
113
  indent do
107
- table[:columns].map {|c| Schema::DbColumn.build_from_hash(c) }.each do |column|
108
- add_line column.define_statement
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 options_str(table)
123
- ", " + table[:table_options].inspect.gsub(/^\{|\}$/,'').gsub("=>", " => ") if table[:table_options]
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 Array of attributes that are different between this
70
+ # Returns an Set of attributes that are different between this
60
71
  # and another column.
61
72
  #
62
73
  def diff(other)
63
- result = []
64
- each_pair {|key, value| result << key if other[key] != value }
65
- result
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 << ":default => #{default.inspect}"
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 << ":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?
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 << ":null => #{(!!null).inspect}" if null != true || column_type == :timestamp
87
- opts << ":default => #{default.inspect}" if default || column_type == :timestamp
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
- opts.join(", ") unless opts.empty?
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
- 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)
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 extract_size(column)
84
- match = column.last[:db_type].match(/\((\d+)\)/)
85
- match[1].to_i if match && match[1]
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 extract_enum_elements(column)
89
- match = column.last[:db_type].match(/\(([^)]+)\)/)
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.4"
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-22}
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 = [
@@ -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 a numeric column is unsigned" do
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
@@ -1,5 +1,7 @@
1
+ require 'rubygems'
1
2
  $LOAD_PATH.unshift(File.dirname(__FILE__))
2
3
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'sequel'
3
5
  require 'sequel/migration_builder'
4
6
  require 'spec'
5
7
  require 'spec/autorun'
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 4
9
- version: 0.0.4
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-22 00:00:00 +01:00
17
+ date: 2010-06-23 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency