sequel 3.17.0 → 3.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/CHANGELOG +22 -0
  2. data/doc/migration.rdoc +34 -0
  3. data/doc/release_notes/3.18.0.txt +121 -0
  4. data/lib/sequel/adapters/jdbc.rb +6 -1
  5. data/lib/sequel/adapters/mysql.rb +11 -1
  6. data/lib/sequel/adapters/mysql2.rb +3 -1
  7. data/lib/sequel/adapters/postgres.rb +1 -1
  8. data/lib/sequel/adapters/shared/sqlite.rb +13 -1
  9. data/lib/sequel/adapters/sqlite.rb +31 -26
  10. data/lib/sequel/connection_pool/sharded_single.rb +5 -1
  11. data/lib/sequel/connection_pool/sharded_threaded.rb +5 -1
  12. data/lib/sequel/dataset/sql.rb +0 -5
  13. data/lib/sequel/extensions/migration.rb +117 -1
  14. data/lib/sequel/extensions/to_dot.rb +137 -0
  15. data/lib/sequel/model/base.rb +1 -1
  16. data/lib/sequel/plugins/instance_hooks.rb +14 -9
  17. data/lib/sequel/plugins/json_serializer.rb +3 -3
  18. data/lib/sequel/sql.rb +1 -1
  19. data/lib/sequel/version.rb +1 -1
  20. data/spec/adapters/sqlite_spec.rb +15 -7
  21. data/spec/core/connection_pool_spec.rb +17 -1
  22. data/spec/core/dataset_spec.rb +9 -0
  23. data/spec/extensions/instance_hooks_spec.rb +46 -0
  24. data/spec/extensions/json_serializer_spec.rb +11 -0
  25. data/spec/extensions/migration_spec.rb +107 -0
  26. data/spec/extensions/spec_helper.rb +1 -1
  27. data/spec/extensions/to_dot_spec.rb +152 -0
  28. data/spec/files/reversible_migrations/001_reversible.rb +5 -0
  29. data/spec/files/reversible_migrations/002_reversible.rb +5 -0
  30. data/spec/files/reversible_migrations/003_reversible.rb +5 -0
  31. data/spec/files/reversible_migrations/004_reversible.rb +5 -0
  32. data/spec/files/reversible_migrations/005_reversible.rb +10 -0
  33. data/spec/integration/migrator_test.rb +54 -0
  34. data/spec/model/association_reflection_spec.rb +19 -0
  35. metadata +13 -4
@@ -0,0 +1,137 @@
1
+ # This adds a <tt>Sequel::Dataset#to_dot</tt> method. The +to_dot+ method
2
+ # returns a string that can be processed by graphviz's +dot+ program in
3
+ # order to get a visualization of the dataset. Basically, it shows a version
4
+ # of the dataset's abstract syntax tree.
5
+
6
+ module Sequel
7
+ class Dataset
8
+ # The option keys that should be included in the dot output.
9
+ TO_DOT_OPTIONS = [:with, :distinct, :select, :from, :join, :where, :group, :having, :compounds, :order, :limit, :offset, :lock].freeze
10
+
11
+ # Return a string that can be processed by the +dot+ program (included
12
+ # with graphviz) in order to see a visualization of the dataset's
13
+ # abstract syntax tree.
14
+ def to_dot
15
+ i = 0
16
+ dot = ["digraph G {", "#{i} [label=\"self\"];"]
17
+ _to_dot(dot, "", i, self, i)
18
+ dot << "}"
19
+ dot.join("\n")
20
+ end
21
+
22
+ private
23
+
24
+ # Internal recursive version that handles all object types understood
25
+ # by Sequel. Arguments:
26
+ # * dot :: An array of strings representing the lines in the returned
27
+ # output. This function just pushes strings onto this array.
28
+ # * l :: The transition label from the parent node of the AST to the
29
+ # current node.
30
+ # * c :: An integer representing the parent node of the AST.
31
+ # * e :: The current node of the AST.
32
+ # * i :: The integer representing the last created node of the AST.
33
+ #
34
+ # The basic algorithm is that the +i+ is incremented to get the current
35
+ # node's integer. Then the transition from the parent node to the
36
+ # current node is added to the +dot+ array. Finally, the current node
37
+ # is added to the +dot+ array, and if it is a compound node with children,
38
+ # its children are then added by recursively calling this method. The
39
+ # return value is the integer representing the last created node.
40
+ def _to_dot(dot, l, c, e, i)
41
+ i += 1
42
+ dot << "#{c} -> #{i} [label=\"#{l}\"];" if l
43
+ c = i
44
+ case e
45
+ when LiteralString
46
+ dot << "#{i} [label=\"#{e.inspect.gsub('"', '\\"')}.lit\"];"
47
+ i
48
+ when Symbol, Numeric, String, Class, TrueClass, FalseClass, NilClass
49
+ dot << "#{i} [label=\"#{e.inspect.gsub('"', '\\"')}\"];"
50
+ i
51
+ when Array
52
+ dot << "#{i} [label=\"Array\"];"
53
+ e.each_with_index do |v, j|
54
+ i = _to_dot(dot, j, c, v, i)
55
+ end
56
+ when Hash
57
+ dot << "#{i} [label=\"Hash\"];"
58
+ e.each do |k, v|
59
+ i = _to_dot(dot, k, c, v, i)
60
+ end
61
+ when SQL::ComplexExpression
62
+ dot << "#{i} [label=\"ComplexExpression: #{e.op}\"];"
63
+ e.args.each_with_index do |v, j|
64
+ i = _to_dot(dot, j, c, v, i)
65
+ end
66
+ when SQL::Identifier
67
+ dot << "#{i} [label=\"Identifier\"];"
68
+ i = _to_dot(dot, :value, c, e.value, i)
69
+ when SQL::QualifiedIdentifier
70
+ dot << "#{i} [label=\"QualifiedIdentifier\"];"
71
+ i = _to_dot(dot, :table, c, e.table, i)
72
+ i = _to_dot(dot, :column, c, e.column, i)
73
+ when SQL::OrderedExpression
74
+ dot << "#{i} [label=\"OrderedExpression: #{e.descending ? :DESC : :ASC}#{" NULLS #{e.nulls.to_s.upcase}" if e.nulls}\"];"
75
+ i = _to_dot(dot, :expression, c, e.expression, i)
76
+ when SQL::AliasedExpression
77
+ dot << "#{i} [label=\"AliasedExpression\"];"
78
+ i = _to_dot(dot, :expression, c, e.expression, i)
79
+ i = _to_dot(dot, :alias, c, e.aliaz, i)
80
+ when SQL::CaseExpression
81
+ dot << "#{i} [label=\"CaseExpression\"];"
82
+ i = _to_dot(dot, :expression, c, e.expression, i) if e.expression
83
+ i = _to_dot(dot, :conditions, c, e.conditions, i)
84
+ i = _to_dot(dot, :default, c, e.default, i)
85
+ when SQL::Cast
86
+ dot << "#{i} [label=\"Cast\"];"
87
+ i = _to_dot(dot, :expr, c, e.expr, i)
88
+ i = _to_dot(dot, :type, c, e.type, i)
89
+ when SQL::Function
90
+ dot << "#{i} [label=\"Function: #{e.f}\"];"
91
+ e.args.each_with_index do |v, j|
92
+ i = _to_dot(dot, j, c, v, i)
93
+ end
94
+ when SQL::Subscript
95
+ dot << "#{i} [label=\"Subscript: #{e.f}\"];"
96
+ i = _to_dot(dot, :f, c, e.f, i)
97
+ i = _to_dot(dot, :sub, c, e.sub, i)
98
+ when SQL::WindowFunction
99
+ dot << "#{i} [label=\"WindowFunction\"];"
100
+ i = _to_dot(dot, :function, c, e.function, i)
101
+ i = _to_dot(dot, :window, c, e.window, i)
102
+ when SQL::Window
103
+ dot << "#{i} [label=\"Window\"];"
104
+ i = _to_dot(dot, :opts, c, e.opts, i)
105
+ when SQL::PlaceholderLiteralString
106
+ str = e.str
107
+ str = "(#{str})" if e.parens
108
+ dot << "#{i} [label=\"PlaceholderLiteralString: #{str.inspect.gsub('"', '\\"')}\"];"
109
+ i = _to_dot(dot, :args, c, e.args, i)
110
+ when SQL::JoinClause
111
+ str = "#{e.join_type.to_s.upcase} JOIN"
112
+ if e.is_a?(SQL::JoinOnClause)
113
+ str << " ON"
114
+ elsif e.is_a?(SQL::JoinUsingClause)
115
+ str << " USING"
116
+ end
117
+ dot << "#{i} [label=\"#{str}\"];"
118
+ i = _to_dot(dot, :table, c, e.table, i)
119
+ i = _to_dot(dot, :alias, c, e.table_alias, i) if e.table_alias
120
+ if e.is_a?(SQL::JoinOnClause)
121
+ i = _to_dot(dot, :on, c, e.on, i)
122
+ elsif e.is_a?(SQL::JoinUsingClause)
123
+ i = _to_dot(dot, :using, c, e.using, i)
124
+ end
125
+ when Dataset
126
+ dot << "#{i} [label=\"Dataset\"];"
127
+ TO_DOT_OPTIONS.each do |k|
128
+ next unless e.opts[k]
129
+ i = _to_dot(dot, k, c, e.opts[k], i)
130
+ end
131
+ else
132
+ dot << "#{i} [label=\"Unhandled: #{e.inspect.gsub('"', "''")}\"];"
133
+ end
134
+ i
135
+ end
136
+ end
137
+ end
@@ -553,7 +553,7 @@ module Sequel
553
553
  single_table = ds_opts[:from] && (ds_opts[:from].length == 1) \
554
554
  && !ds_opts.include?(:join) && !ds_opts.include?(:sql)
555
555
  get_columns = proc{check_non_connection_error{columns} || []}
556
- if single_table && (schema_array = (db.schema(table_name, :reload=>reload) rescue nil))
556
+ if single_table && (schema_array = (db.schema(dataset.first_source_table, :reload=>reload) rescue nil))
557
557
  schema_array.each{|k,v| schema_hash[k] = v}
558
558
  if ds_opts.include?(:select)
559
559
  # We don't remove the columns from the schema_hash,
@@ -10,7 +10,12 @@ module Sequel
10
10
  # after calling super. If any of the instance level before hook blocks return
11
11
  # false, no more instance level before hooks are called and false is returned.
12
12
  #
13
- # Instance level hooks are cleared when the object is saved successfully.
13
+ # Instance level hooks for before and after are cleared after all related
14
+ # after level instance hooks have run. This means that if you add a before_create
15
+ # and before_update instance hooks to a new object, the before_create hook will
16
+ # be run the first time you save the object (creating it), and the before_update
17
+ # hook will be run the second time you save the object (updating it), and no
18
+ # hooks will be run the third time you save the object.
14
19
  #
15
20
  # Usage:
16
21
  #
@@ -27,14 +32,14 @@ module Sequel
27
32
  HOOKS.each{|h| class_eval("def #{h}_hook(&block); add_instance_hook(:#{h}, &block) end", __FILE__, __LINE__)}
28
33
 
29
34
  BEFORE_HOOKS.each{|h| class_eval("def #{h}; run_before_instance_hooks(:#{h}) == false ? false : super end", __FILE__, __LINE__)}
30
- (AFTER_HOOKS - [:after_save]).each{|h| class_eval("def #{h}; super; run_after_instance_hooks(:#{h}) end", __FILE__, __LINE__)}
31
-
32
- # Clear the instance level hooks after saving the object.
33
- def after_save
34
- super
35
- run_after_instance_hooks(:after_save)
36
- @instance_hooks.clear if @instance_hooks
37
- end
35
+ AFTER_HOOKS.each{|h| class_eval(<<-END, __FILE__, __LINE__ + 1)}
36
+ def #{h}
37
+ super
38
+ run_after_instance_hooks(:#{h})
39
+ @instance_hooks.delete(:#{h})
40
+ @instance_hooks.delete(:#{h.to_s.sub('after', 'before')})
41
+ end
42
+ END
38
43
 
39
44
  private
40
45
 
@@ -106,10 +106,10 @@ module Sequel
106
106
  hash.each do |k, v|
107
107
  if assocs.include?(k)
108
108
  obj.associations[k.to_sym] = v
109
- elsif cols.include?(k)
110
- obj.values[k.to_sym] = v
111
109
  elsif meths.include?("#{k}=")
112
110
  obj.send("#{k}=", v)
111
+ elsif cols.include?(k)
112
+ obj.values[k.to_sym] = v
113
113
  else
114
114
  raise Error, "Entry in JSON hash not an association or column and no setter method exists: #{k}"
115
115
  end
@@ -171,7 +171,7 @@ module Sequel
171
171
  vals.keys - Array(opts[:except])
172
172
  end
173
173
  h = (JSON.create_id && !opts[:naked] && !opts[:root]) ? {JSON.create_id=>model.name} : {}
174
- cols.each{|c| h[c.to_s] = vals[c]}
174
+ cols.each{|c| h[c.to_s] = send(c)}
175
175
  if inc = opts[:include]
176
176
  if inc.is_a?(Hash)
177
177
  inc.each do |k, v|
data/lib/sequel/sql.rb CHANGED
@@ -6,7 +6,7 @@ module Sequel
6
6
  class BasicObject
7
7
  # The instance methods to not remove from the class when removing
8
8
  # other methods.
9
- KEEP_METHODS = %w"__id__ __send__ __metaclass__ instance_eval == equal? initialize"
9
+ KEEP_METHODS = %w"__id__ __send__ __metaclass__ instance_eval == equal? initialize method_missing"
10
10
 
11
11
  # Remove all but the most basic instance methods from the class. A separate
12
12
  # method so that it can be called again if necessary if you load libraries
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 3
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 17
6
+ MINOR = 18
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0
@@ -101,14 +101,13 @@ context "An SQLite database" do
101
101
  cspecify "should support timestamps and datetimes and respect datetime_class", :do, :jdbc, :amalgalite do
102
102
  @db.create_table!(:time){timestamp :t; datetime :d}
103
103
  t1 = Time.at(1)
104
- @db[:time] << {:t => t1, :d => t1.to_i}
105
- @db[:time] << {:t => t1.to_i, :d => t1}
106
- @db[:time].map(:t).should == [t1, t1]
107
- @db[:time].map(:d).should == [t1, t1]
104
+ @db[:time] << {:t => t1, :d => t1}
105
+ @db[:time].map(:t).should == [t1]
106
+ @db[:time].map(:d).should == [t1]
108
107
  Sequel.datetime_class = DateTime
109
108
  t2 = Sequel.string_to_datetime(t1.iso8601)
110
- @db[:time].map(:t).should == [t2, t2]
111
- @db[:time].map(:d).should == [t2, t2]
109
+ @db[:time].map(:t).should == [t2]
110
+ @db[:time].map(:d).should == [t2]
112
111
  end
113
112
 
114
113
  specify "should support sequential primary keys" do
@@ -412,7 +411,7 @@ context "A SQLite database" do
412
411
 
413
412
  specify "should choose a temporary table name that isn't already used when dropping or renaming columns" do
414
413
  sqls = []
415
- @db.loggers << (l=Class.new{define_method(:info){|sql| sqls << sql}}.new)
414
+ @db.loggers << (l=Class.new{%w'info error'.each{|m| define_method(m){|sql| sqls << sql}}}.new)
416
415
  @db.create_table! :test3 do
417
416
  Integer :h
418
417
  Integer :i
@@ -459,4 +458,13 @@ context "A SQLite database" do
459
458
  @db.add_index :test2, :value, :unique => true
460
459
  @db.drop_index :test2, :value
461
460
  end
461
+
462
+ specify "should keep applicable indexes when emulating schema methods" do
463
+ @db.create_table!(:a){Integer :a; Integer :b}
464
+ @db.add_index :a, :a
465
+ @db.add_index :a, :b
466
+ @db.add_index :a, [:b, :a]
467
+ @db.drop_column :a, :b
468
+ @db.indexes(:a).should == {:a_a_index=>{:unique=>false, :columns=>[:a]}}
469
+ end
462
470
  end
@@ -315,7 +315,7 @@ context "Threaded Sharded Connection Pool" do
315
315
  @invoked_count = 0
316
316
  @pool = Sequel::ConnectionPool.get_pool(CONNECTION_POOL_DEFAULTS.merge(:max_connections=>5, :servers=>{})) {@invoked_count += 1}
317
317
  end
318
-
318
+
319
319
  it_should_behave_like "A threaded connection pool"
320
320
  end
321
321
 
@@ -418,6 +418,22 @@ context "A connection pool with multiple servers" do
418
418
  end
419
419
  end
420
420
 
421
+ specify "should support a :servers_hash option used for converting the server argument" do
422
+ @pool = Sequel::ConnectionPool.get_pool(CONNECTION_POOL_DEFAULTS.merge(:servers_hash=>Hash.new(:read_only), :servers=>{:read_only=>{}})){|server| "#{server}#{@invoked_counts[server] += 1}"}
423
+ @pool.hold(:blah) do |c1|
424
+ c1.should == "read_only1"
425
+ @pool.hold(:blah) do |c2|
426
+ c2.should == c1
427
+ @pool.hold(:blah2) do |c3|
428
+ c2.should == c3
429
+ end
430
+ end
431
+ end
432
+
433
+ @pool = Sequel::ConnectionPool.get_pool(CONNECTION_POOL_DEFAULTS.merge(:servers_hash=>Hash.new{|h,k| raise Sequel::Error}, :servers=>{:read_only=>{}})){|server| "#{server}#{@invoked_counts[server] += 1}"}
434
+ proc{@pool.hold(:blah){|c1|}}.should raise_error(Sequel::Error)
435
+ end
436
+
421
437
  specify "should use the requested server if server is given" do
422
438
  @pool.size(:read_only).should == 0
423
439
  @pool.hold(:read_only) do |c|
@@ -1552,6 +1552,10 @@ context "Dataset#qualified_column_name" do
1552
1552
  specify "should not changed the qualifed column's table if given a qualified symbol" do
1553
1553
  @dataset.literal(@dataset.send(:qualified_column_name, :ccc__b, :items)).should == 'ccc.b'
1554
1554
  end
1555
+
1556
+ specify "should handle an aliased identifier" do
1557
+ @dataset.literal(@dataset.send(:qualified_column_name, :ccc, :items.as(:i))).should == 'i.ccc'
1558
+ end
1555
1559
  end
1556
1560
 
1557
1561
  class DummyDataset < Sequel::Dataset
@@ -1937,6 +1941,11 @@ context "Dataset#join_table" do
1937
1941
  'SELECT * FROM "stats" INNER JOIN "players" AS "p" ON ("p"."id" = "stats"."player_id")'
1938
1942
  end
1939
1943
 
1944
+ specify "should support aliased tables using an implicit alias" do
1945
+ @d.from('stats').join(:players.as(:p), {:id => :player_id}).sql.should ==
1946
+ 'SELECT * FROM "stats" INNER JOIN "players" AS "p" ON ("p"."id" = "stats"."player_id")'
1947
+ end
1948
+
1940
1949
  specify "should support using an alias for the FROM when doing the first join with unqualified condition columns" do
1941
1950
  ds = MockDataset.new(nil).from(:foo => :f)
1942
1951
  ds.quote_identifiers = true
@@ -130,4 +130,50 @@ describe "InstanceHooks plugin" do
130
130
  @o.valid?.should == false
131
131
  @r.should == [4, false]
132
132
  end
133
+
134
+ it "should clear only related hooks on successful create" do
135
+ @o.after_destroy_hook{r 1}
136
+ @o.before_destroy_hook{r 2}
137
+ @o.after_update_hook{r 3}
138
+ @o.before_update_hook{r 4}
139
+ @o.before_save_hook{r 5}
140
+ @o.after_save_hook{r 6}
141
+ @o.before_create_hook{r 7}
142
+ @o.after_create_hook{r 8}
143
+ @o.save.should_not == nil
144
+ @r.should == [5, 7, 8, 6]
145
+ @o.instance_variable_set(:@new, false)
146
+ @o.save.should_not == nil
147
+ @r.should == [5, 7, 8, 6, 4, 3]
148
+ @o.save.should_not == nil
149
+ @r.should == [5, 7, 8, 6, 4, 3]
150
+ @o.destroy
151
+ @r.should == [5, 7, 8, 6, 4, 3, 2, 1]
152
+ end
153
+
154
+ it "should clear only related hooks on successful update" do
155
+ @x.after_destroy_hook{r 1}
156
+ @x.before_destroy_hook{r 2}
157
+ @x.before_update_hook{r 3}
158
+ @x.after_update_hook{r 4}
159
+ @x.before_save_hook{r 5}
160
+ @x.after_save_hook{r 6}
161
+ @x.save.should_not == nil
162
+ @r.should == [5, 3, 4, 6]
163
+ @x.save.should_not == nil
164
+ @r.should == [5, 3, 4, 6]
165
+ @x.destroy
166
+ @r.should == [5, 3, 4, 6, 2, 1]
167
+ end
168
+
169
+ it "should clear only related hooks on successful destroy" do
170
+ @x.after_destroy_hook{r 1}
171
+ @x.before_destroy_hook{r 2}
172
+ @x.before_update_hook{r 3}
173
+ @x.before_save_hook{r 4}
174
+ @x.destroy
175
+ @r.should == [2, 1]
176
+ @x.save.should_not == nil
177
+ @r.should == [2, 1, 4, 3]
178
+ end
133
179
  end
@@ -10,12 +10,14 @@ describe "Sequel::Plugins::JsonSerializer" do
10
10
  class ::Artist < Sequel::Model
11
11
  plugin :json_serializer
12
12
  columns :id, :name
13
+ def_column_accessor :id, :name
13
14
  one_to_many :albums
14
15
  end
15
16
  class ::Album < Sequel::Model
16
17
  attr_accessor :blah
17
18
  plugin :json_serializer
18
19
  columns :id, :name, :artist_id
20
+ def_column_accessor :id, :name, :artist_id
19
21
  many_to_one :artist
20
22
  end
21
23
  @artist = Artist.load(:id=>2, :name=>'YJM')
@@ -34,6 +36,15 @@ describe "Sequel::Plugins::JsonSerializer" do
34
36
  JSON.parse(@album.to_json).should == @album
35
37
  end
36
38
 
39
+ it "should handle ruby objects in values" do
40
+ class ::Artist
41
+ def name=(v)
42
+ super(Date.parse(v))
43
+ end
44
+ end
45
+ JSON.parse(Artist.load(:name=>Date.today).to_json).should == Artist.load(:name=>Date.today)
46
+ end
47
+
37
48
  it "should handle the :only option" do
38
49
  JSON.parse(@artist.to_json(:only=>:name)).should == Artist.load(:name=>@artist.name)
39
50
  JSON.parse(@album.to_json(:only=>[:id, :name])).should == Album.load(:id=>@album.id, :name=>@album.name)
@@ -86,6 +86,113 @@ context "SimpleMigration#apply" do
86
86
  end
87
87
  end
88
88
 
89
+ context "Reversible Migrations with Sequel.migration{change{}}" do
90
+ before do
91
+ @c = Class.new do
92
+ self::AT = Class.new do
93
+ attr_reader :actions
94
+ def initialize(&block)
95
+ @actions = []
96
+ instance_eval(&block)
97
+ end
98
+ def method_missing(*args)
99
+ @actions << args
100
+ end
101
+ self
102
+ end
103
+ attr_reader :actions
104
+ def initialize
105
+ @actions = []
106
+ end
107
+ def method_missing(*args)
108
+ @actions << args
109
+ end
110
+ def alter_table(*args, &block)
111
+ @actions << [:alter_table, self.class::AT.new(&block).actions]
112
+ end
113
+ end
114
+ @db = @c.new
115
+ @p = Proc.new do
116
+ create_table(:a){Integer :a}
117
+ add_column :a, :b, String
118
+ add_index :a, :b
119
+ rename_column :a, :b, :c
120
+ rename_table :a, :b
121
+ alter_table(:b) do
122
+ add_column :d, String
123
+ add_constraint :blah, 'd IS NOT NULL'
124
+ add_foreign_key :e, :b
125
+ add_primary_key :f, :b
126
+ add_index :e, :name=>'e_n'
127
+ add_full_text_index :e, :name=>'e_ft'
128
+ add_spatial_index :e, :name=>'e_s'
129
+ rename_column :e, :g
130
+ end
131
+ create_view(:c, 'SELECT * FROM b')
132
+ end
133
+ end
134
+
135
+ specify "should apply up with normal actions in normal order" do
136
+ p = @p
137
+ Sequel.migration{change(&p)}.apply(@db, :up)
138
+ @db.actions.should == [[:create_table, :a],
139
+ [:add_column, :a, :b, String],
140
+ [:add_index, :a, :b],
141
+ [:rename_column, :a, :b, :c],
142
+ [:rename_table, :a, :b],
143
+ [:alter_table, [
144
+ [:add_column, :d, String],
145
+ [:add_constraint, :blah, "d IS NOT NULL"],
146
+ [:add_foreign_key, :e, :b],
147
+ [:add_primary_key, :f, :b],
148
+ [:add_index, :e, {:name=>"e_n"}],
149
+ [:add_full_text_index, :e, {:name=>"e_ft"}],
150
+ [:add_spatial_index, :e, {:name=>"e_s"}],
151
+ [:rename_column, :e, :g]]
152
+ ],
153
+ [:create_view, :c, "SELECT * FROM b"]]
154
+ end
155
+
156
+ specify "should execute down with reversing actions in reverse order" do
157
+ p = @p
158
+ Sequel.migration{change(&p)}.apply(@db, :down)
159
+ @db.actions.should == [[:drop_view, :c],
160
+ [:alter_table, [
161
+ [:rename_column, :g, :e],
162
+ [:drop_index, :e, {:name=>"e_s"}],
163
+ [:drop_index, :e, {:name=>"e_ft"}],
164
+ [:drop_index, :e, {:name=>"e_n"}],
165
+ [:drop_column, :f],
166
+ [:drop_column, :e],
167
+ [:drop_constraint, :blah],
168
+ [:drop_column, :d]]
169
+ ],
170
+ [:rename_table, :b, :a],
171
+ [:rename_column, :a, :c, :b],
172
+ [:drop_index, :a, :b],
173
+ [:drop_column, :a, :b],
174
+ [:drop_table, :a]]
175
+ end
176
+
177
+ specify "should raise in the down direction if migration uses unsupported method" do
178
+ m = Sequel.migration{change{run 'SQL'}}
179
+ proc{m.apply(@db, :up)}.should_not raise_error(Sequel::Error)
180
+ proc{m.apply(@db, :down)}.should raise_error(Sequel::Error)
181
+ end
182
+
183
+ specify "should raise in the down direction if migration uses add_primary_key with an array" do
184
+ m = Sequel.migration{change{alter_table(:a){add_primary_key [:b]}}}
185
+ proc{m.apply(@db, :up)}.should_not raise_error(Sequel::Error)
186
+ proc{m.apply(@db, :down)}.should raise_error(Sequel::Error)
187
+ end
188
+
189
+ specify "should raise in the down direction if migration uses add_foreign_key with an array" do
190
+ m = Sequel.migration{change{alter_table(:a){add_foreign_key [:b]}}}
191
+ proc{m.apply(@db, :up)}.should_not raise_error(Sequel::Error)
192
+ proc{m.apply(@db, :down)}.should raise_error(Sequel::Error)
193
+ end
194
+ end
195
+
89
196
  context "Sequel::IntegerMigrator" do
90
197
  before do
91
198
  dbc = Class.new(MockDatabase) do