sequel_migration_builder 0.0.4 → 0.0.5

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/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