sequel 3.11.0 → 3.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. data/CHANGELOG +70 -0
  2. data/Rakefile +1 -1
  3. data/doc/active_record.rdoc +896 -0
  4. data/doc/advanced_associations.rdoc +46 -31
  5. data/doc/association_basics.rdoc +14 -9
  6. data/doc/dataset_basics.rdoc +3 -3
  7. data/doc/migration.rdoc +1011 -0
  8. data/doc/model_hooks.rdoc +198 -0
  9. data/doc/querying.rdoc +811 -86
  10. data/doc/release_notes/3.12.0.txt +304 -0
  11. data/doc/sharding.rdoc +17 -0
  12. data/doc/sql.rdoc +537 -0
  13. data/doc/validations.rdoc +501 -0
  14. data/lib/sequel/adapters/jdbc.rb +19 -27
  15. data/lib/sequel/adapters/jdbc/postgresql.rb +0 -7
  16. data/lib/sequel/adapters/mysql.rb +5 -4
  17. data/lib/sequel/adapters/odbc.rb +3 -2
  18. data/lib/sequel/adapters/shared/mssql.rb +7 -6
  19. data/lib/sequel/adapters/shared/mysql.rb +2 -7
  20. data/lib/sequel/adapters/shared/postgres.rb +2 -8
  21. data/lib/sequel/adapters/shared/sqlite.rb +2 -5
  22. data/lib/sequel/adapters/sqlite.rb +4 -4
  23. data/lib/sequel/core.rb +0 -1
  24. data/lib/sequel/database.rb +2 -1060
  25. data/lib/sequel/database/connecting.rb +227 -0
  26. data/lib/sequel/database/dataset.rb +58 -0
  27. data/lib/sequel/database/dataset_defaults.rb +127 -0
  28. data/lib/sequel/database/logging.rb +62 -0
  29. data/lib/sequel/database/misc.rb +246 -0
  30. data/lib/sequel/database/query.rb +390 -0
  31. data/lib/sequel/database/schema_generator.rb +7 -3
  32. data/lib/sequel/database/schema_methods.rb +351 -7
  33. data/lib/sequel/dataset/actions.rb +9 -2
  34. data/lib/sequel/dataset/misc.rb +6 -2
  35. data/lib/sequel/dataset/mutation.rb +3 -11
  36. data/lib/sequel/dataset/query.rb +49 -6
  37. data/lib/sequel/exceptions.rb +3 -0
  38. data/lib/sequel/extensions/migration.rb +395 -113
  39. data/lib/sequel/extensions/schema_dumper.rb +21 -13
  40. data/lib/sequel/model.rb +27 -25
  41. data/lib/sequel/model/associations.rb +72 -34
  42. data/lib/sequel/model/base.rb +74 -18
  43. data/lib/sequel/model/errors.rb +8 -1
  44. data/lib/sequel/plugins/active_model.rb +8 -0
  45. data/lib/sequel/plugins/association_pks.rb +87 -0
  46. data/lib/sequel/plugins/association_proxies.rb +8 -0
  47. data/lib/sequel/plugins/boolean_readers.rb +12 -6
  48. data/lib/sequel/plugins/caching.rb +14 -7
  49. data/lib/sequel/plugins/class_table_inheritance.rb +15 -9
  50. data/lib/sequel/plugins/composition.rb +2 -1
  51. data/lib/sequel/plugins/force_encoding.rb +10 -7
  52. data/lib/sequel/plugins/hook_class_methods.rb +12 -11
  53. data/lib/sequel/plugins/identity_map.rb +9 -0
  54. data/lib/sequel/plugins/instance_hooks.rb +23 -13
  55. data/lib/sequel/plugins/lazy_attributes.rb +4 -1
  56. data/lib/sequel/plugins/many_through_many.rb +18 -4
  57. data/lib/sequel/plugins/nested_attributes.rb +1 -0
  58. data/lib/sequel/plugins/optimistic_locking.rb +1 -1
  59. data/lib/sequel/plugins/rcte_tree.rb +9 -8
  60. data/lib/sequel/plugins/schema.rb +8 -0
  61. data/lib/sequel/plugins/serialization.rb +1 -3
  62. data/lib/sequel/plugins/sharding.rb +135 -0
  63. data/lib/sequel/plugins/single_table_inheritance.rb +117 -25
  64. data/lib/sequel/plugins/skip_create_refresh.rb +35 -0
  65. data/lib/sequel/plugins/string_stripper.rb +26 -0
  66. data/lib/sequel/plugins/tactical_eager_loading.rb +8 -0
  67. data/lib/sequel/plugins/timestamps.rb +15 -2
  68. data/lib/sequel/plugins/touch.rb +13 -0
  69. data/lib/sequel/plugins/update_primary_key.rb +48 -0
  70. data/lib/sequel/plugins/validation_class_methods.rb +8 -0
  71. data/lib/sequel/plugins/validation_helpers.rb +1 -1
  72. data/lib/sequel/sql.rb +17 -20
  73. data/lib/sequel/version.rb +1 -1
  74. data/spec/adapters/postgres_spec.rb +5 -5
  75. data/spec/core/core_sql_spec.rb +17 -1
  76. data/spec/core/database_spec.rb +17 -5
  77. data/spec/core/dataset_spec.rb +31 -8
  78. data/spec/core/schema_generator_spec.rb +8 -1
  79. data/spec/core/schema_spec.rb +13 -0
  80. data/spec/extensions/association_pks_spec.rb +85 -0
  81. data/spec/extensions/hook_class_methods_spec.rb +9 -9
  82. data/spec/extensions/migration_spec.rb +339 -219
  83. data/spec/extensions/schema_dumper_spec.rb +28 -17
  84. data/spec/extensions/sharding_spec.rb +272 -0
  85. data/spec/extensions/single_table_inheritance_spec.rb +92 -4
  86. data/spec/extensions/skip_create_refresh_spec.rb +17 -0
  87. data/spec/extensions/string_stripper_spec.rb +23 -0
  88. data/spec/extensions/update_primary_key_spec.rb +65 -0
  89. data/spec/extensions/validation_class_methods_spec.rb +5 -5
  90. data/spec/files/bad_down_migration/001_create_alt_basic.rb +4 -0
  91. data/spec/files/bad_down_migration/002_create_alt_advanced.rb +4 -0
  92. data/spec/files/bad_timestamped_migrations/1273253849_create_sessions.rb +9 -0
  93. data/spec/files/bad_timestamped_migrations/1273253851_create_nodes.rb +9 -0
  94. data/spec/files/bad_timestamped_migrations/1273253853_3_create_users.rb +3 -0
  95. data/spec/files/bad_up_migration/001_create_alt_basic.rb +4 -0
  96. data/spec/files/bad_up_migration/002_create_alt_advanced.rb +3 -0
  97. data/spec/files/convert_to_timestamp_migrations/001_create_sessions.rb +9 -0
  98. data/spec/files/convert_to_timestamp_migrations/002_create_nodes.rb +9 -0
  99. data/spec/files/convert_to_timestamp_migrations/003_3_create_users.rb +4 -0
  100. data/spec/files/convert_to_timestamp_migrations/1273253850_create_artists.rb +9 -0
  101. data/spec/files/convert_to_timestamp_migrations/1273253852_create_albums.rb +9 -0
  102. data/spec/files/duplicate_integer_migrations/001_create_alt_advanced.rb +4 -0
  103. data/spec/files/duplicate_integer_migrations/001_create_alt_basic.rb +4 -0
  104. data/spec/files/duplicate_timestamped_migrations/1273253849_create_sessions.rb +9 -0
  105. data/spec/files/duplicate_timestamped_migrations/1273253853_create_nodes.rb +9 -0
  106. data/spec/files/duplicate_timestamped_migrations/1273253853_create_users.rb +4 -0
  107. data/spec/files/integer_migrations/001_create_sessions.rb +9 -0
  108. data/spec/files/integer_migrations/002_create_nodes.rb +9 -0
  109. data/spec/files/integer_migrations/003_3_create_users.rb +4 -0
  110. data/spec/files/interleaved_timestamped_migrations/1273253849_create_sessions.rb +9 -0
  111. data/spec/files/interleaved_timestamped_migrations/1273253850_create_artists.rb +9 -0
  112. data/spec/files/interleaved_timestamped_migrations/1273253851_create_nodes.rb +9 -0
  113. data/spec/files/interleaved_timestamped_migrations/1273253852_create_albums.rb +9 -0
  114. data/spec/files/interleaved_timestamped_migrations/1273253853_3_create_users.rb +4 -0
  115. data/spec/files/missing_integer_migrations/001_create_alt_basic.rb +4 -0
  116. data/spec/files/missing_integer_migrations/003_create_alt_advanced.rb +4 -0
  117. data/spec/files/missing_timestamped_migrations/1273253849_create_sessions.rb +9 -0
  118. data/spec/files/missing_timestamped_migrations/1273253853_3_create_users.rb +4 -0
  119. data/spec/files/timestamped_migrations/1273253849_create_sessions.rb +9 -0
  120. data/spec/files/timestamped_migrations/1273253851_create_nodes.rb +9 -0
  121. data/spec/files/timestamped_migrations/1273253853_3_create_users.rb +4 -0
  122. data/spec/files/uppercase_timestamped_migrations/1273253849_CREATE_SESSIONS.RB +9 -0
  123. data/spec/files/uppercase_timestamped_migrations/1273253851_CREATE_NODES.RB +9 -0
  124. data/spec/files/uppercase_timestamped_migrations/1273253853_3_CREATE_USERS.RB +4 -0
  125. data/spec/integration/eager_loader_test.rb +20 -20
  126. data/spec/integration/migrator_test.rb +187 -0
  127. data/spec/integration/plugin_test.rb +150 -0
  128. data/spec/integration/schema_test.rb +13 -2
  129. data/spec/model/associations_spec.rb +41 -14
  130. data/spec/model/base_spec.rb +69 -0
  131. data/spec/model/eager_loading_spec.rb +7 -3
  132. data/spec/model/record_spec.rb +79 -4
  133. data/spec/model/validations_spec.rb +21 -9
  134. metadata +66 -5
  135. data/doc/schema.rdoc +0 -36
  136. data/lib/sequel/database/schema_sql.rb +0 -320
@@ -7,6 +7,12 @@ module Sequel
7
7
  # they should be the last method called.
8
8
  # ---------------------
9
9
 
10
+ # Action methods defined by Sequel that execute code on the database.
11
+ ACTION_METHODS = %w'<< [] []= all avg count columns columns! delete each
12
+ empty? fetch_rows first get import insert insert_multiple interval last
13
+ map max min multi_insert range select_hash select_map select_order_map
14
+ set single_record single_value sum to_csv to_hash truncate update'.map{|x| x.to_sym}
15
+
10
16
  # Alias for insert, but not aliased directly so subclasses
11
17
  # don't have to override both methods.
12
18
  def <<(*args)
@@ -104,7 +110,7 @@ module Sequel
104
110
  # Executes a select query and fetches records, passing each record to the
105
111
  # supplied block. The yielded records should be hashes with symbol keys.
106
112
  def fetch_rows(sql, &block)
107
- raise NotImplementedError, NOTIMPL_MSG
113
+ raise NotImplemented, NOTIMPL_MSG
108
114
  end
109
115
 
110
116
  # If a integer argument is
@@ -156,7 +162,8 @@ module Sequel
156
162
  end
157
163
 
158
164
  # Inserts multiple records into the associated table. This method can be
159
- # to efficiently insert a large amounts of records into a table. Inserts
165
+ # used to efficiently insert a large number of records into a table in a
166
+ # single query if the database supports it. Inserts
160
167
  # are automatically wrapped in a transaction.
161
168
  #
162
169
  # This method is called with a columns array and an array of value arrays:
@@ -49,6 +49,11 @@ module Sequel
49
49
  db.servers.each{|s| yield server(s)}
50
50
  end
51
51
 
52
+ # Alias of first_source_alias
53
+ def first_source
54
+ first_source_alias
55
+ end
56
+
52
57
  # The first source (primary table) for this dataset. If the dataset doesn't
53
58
  # have a table, raises an error. If the table is aliased, returns the aliased name.
54
59
  def first_source_alias
@@ -66,7 +71,6 @@ module Sequel
66
71
  s
67
72
  end
68
73
  end
69
- alias first_source first_source_alias
70
74
 
71
75
  # The first source (primary table) for this dataset. If the dataset doesn't
72
76
  # have a table, raises an error. If the table is aliased, returns the original
@@ -116,4 +120,4 @@ module Sequel
116
120
  end
117
121
  end
118
122
  end
119
- end
123
+ end
@@ -5,16 +5,8 @@ module Sequel
5
5
  # These methods modify the receiving dataset and should be used with care.
6
6
  # ---------------------
7
7
 
8
- # All methods that should have a ! method added that modifies
9
- # the receiver.
10
- MUTATION_METHODS = %w'add_graph_aliases and cross_join distinct except exclude
11
- filter for_update from from_self full_join full_outer_join graph
12
- group group_and_count group_by having inner_join intersect invert join join_table left_join
13
- left_outer_join limit lock_style naked natural_full_join natural_join
14
- natural_left_join natural_right_join or order order_by order_more paginate qualify query
15
- reverse reverse_order right_join right_outer_join select select_all select_append select_more server
16
- set_defaults set_graph_aliases set_overrides unfiltered ungraphed ungrouped union
17
- unlimited unordered where with with_recursive with_sql'.collect{|x| x.to_sym}
8
+ # All methods that should have a ! method added that modifies the receiver.
9
+ MUTATION_METHODS = QUERY_METHODS
18
10
 
19
11
  # Setup mutation (e.g. filter!) methods. These operate the same as the
20
12
  # non-! methods, but replace the options of the current dataset with the
@@ -61,4 +53,4 @@ module Sequel
61
53
  self
62
54
  end
63
55
  end
64
- end
56
+ end
@@ -4,6 +4,7 @@ module Sequel
4
4
  # :section: Methods that return modified datasets
5
5
  # These methods all return modified copies of the receiver.
6
6
  # ---------------------
7
+
7
8
  # The dataset options that require the removal of cached columns
8
9
  # if changed.
9
10
  COLUMN_CHANGE_OPTS = [:select, :sql, :from, :join].freeze
@@ -23,6 +24,17 @@ module Sequel
23
24
  # if called with a block.
24
25
  UNCONDITIONED_JOIN_TYPES = [:natural, :natural_left, :natural_right, :natural_full, :cross]
25
26
 
27
+ # All methods that return modified datasets with a joined table added.
28
+ JOIN_METHODS = (CONDITIONED_JOIN_TYPES + UNCONDITIONED_JOIN_TYPES).map{|x| "#{x}_join".to_sym} + [:join, :join_table]
29
+
30
+ # Methods that return modified datasets
31
+ QUERY_METHODS = %w'add_graph_aliases and distinct except exclude
32
+ filter for_update from from_self graph grep group group_and_count group_by having intersect invert
33
+ limit lock_style naked or order order_append order_by order_more order_prepend paginate qualify query
34
+ reverse reverse_order select select_all select_append select_more server
35
+ set_defaults set_graph_aliases set_overrides unfiltered ungraphed ungrouped union
36
+ unlimited unordered where with with_recursive with_sql'.collect{|x| x.to_sym} + JOIN_METHODS
37
+
26
38
  # Adds an further filter to an existing filter using AND. If no filter
27
39
  # exists an error is raised. This method is identical to #filter except
28
40
  # it expects an existing filter.
@@ -208,7 +220,11 @@ module Sequel
208
220
  def group(*columns)
209
221
  clone(:group => (columns.compact.empty? ? nil : columns))
210
222
  end
211
- alias group_by group
223
+
224
+ # Alias of group
225
+ def group_by(*columns)
226
+ group(*columns)
227
+ end
212
228
 
213
229
  # Returns a dataset grouped by the given column with count by group,
214
230
  # order by the count of records. Column aliases may be supplied, and will
@@ -260,6 +276,11 @@ module Sequel
260
276
  clone(o)
261
277
  end
262
278
 
279
+ # Alias of inner_join
280
+ def join(*args, &block)
281
+ inner_join(*args, &block)
282
+ end
283
+
263
284
  # Returns a joined dataset. Uses the following arguments:
264
285
  #
265
286
  # * type - The type of join to do (e.g. :inner)
@@ -352,7 +373,6 @@ module Sequel
352
373
  UNCONDITIONED_JOIN_TYPES.each do |jtype|
353
374
  class_eval("def #{jtype}_join(table); raise(Sequel::Error, '#{jtype}_join does not accept join table blocks') if block_given?; join_table(:#{jtype}, table) end", __FILE__, __LINE__)
354
375
  end
355
- alias join inner_join
356
376
 
357
377
  # If given an integer, the dataset will contain only the first l results.
358
378
  # If given a range, it will contain only those at offsets within that
@@ -426,10 +446,19 @@ module Sequel
426
446
  columns += Array(Sequel.virtual_row(&block)) if block
427
447
  clone(:order => (columns.compact.empty?) ? nil : columns)
428
448
  end
429
- alias order_by order
430
449
 
450
+ # Alias of order_more, for naming consistency with order_prepend.
451
+ def order_append(*columns, &block)
452
+ order_more(*columns, &block)
453
+ end
454
+
455
+ # Alias of order
456
+ def order_by(*columns, &block)
457
+ order(*columns, &block)
458
+ end
459
+
431
460
  # Returns a copy of the dataset with the order columns added
432
- # to the existing order.
461
+ # to the end of the existing order.
433
462
  #
434
463
  # ds.order(:a).order(:b).sql #=> 'SELECT * FROM items ORDER BY b'
435
464
  # ds.order(:a).order_more(:b).sql #=> 'SELECT * FROM items ORDER BY a, b'
@@ -438,6 +467,16 @@ module Sequel
438
467
  order(*columns, &block)
439
468
  end
440
469
 
470
+ # Returns a copy of the dataset with the order columns added
471
+ # to the beginning of the existing order.
472
+ #
473
+ # ds.order(:a).order(:b).sql #=> 'SELECT * FROM items ORDER BY b'
474
+ # ds.order(:a).order_prepend(:b).sql #=> 'SELECT * FROM items ORDER BY b, a'
475
+ def order_prepend(*columns, &block)
476
+ ds = order(*columns, &block)
477
+ @opts[:order] ? ds.order_more(*@opts[:order]) : ds
478
+ end
479
+
441
480
  # Qualify to the given table, or first source if not table is given.
442
481
  def qualify(table=first_source)
443
482
  qualify_to(table)
@@ -469,10 +508,14 @@ module Sequel
469
508
 
470
509
  # Returns a copy of the dataset with the order reversed. If no order is
471
510
  # given, the existing order is inverted.
472
- def reverse_order(*order)
511
+ def reverse(*order)
473
512
  order(*invert_order(order.empty? ? @opts[:order] : order))
474
513
  end
475
- alias reverse reverse_order
514
+
515
+ # Alias of reverse
516
+ def reverse_order(*order)
517
+ reverse(*order)
518
+ end
476
519
 
477
520
  # Returns a copy of the dataset with the columns selected changed
478
521
  # to the given columns. This also takes a virtual row block,
@@ -32,6 +32,9 @@ module Sequel
32
32
  # Raised when attempting an invalid type conversion.
33
33
  class InvalidValue < Error ; end
34
34
 
35
+ # Raised when the adapter adapter hasn't implemented a method such as +tables+:
36
+ class NotImplemented < Error; end
37
+
35
38
  # Raised when the connection pool cannot acquire a database connection
36
39
  # before the timeout.
37
40
  class PoolTimeout < Error ; end
@@ -3,51 +3,23 @@
3
3
  # to a newer version or revert to a previous version.
4
4
 
5
5
  module Sequel
6
- # The Migration class describes a database migration that can be reversed.
7
- # The migration looks very similar to ActiveRecord (Rails) migrations, e.g.:
8
- #
9
- # class CreateSessions < Sequel::Migration
10
- # def up
11
- # create_table :sessions do
12
- # primary_key :id
13
- # String :session_id, :size => 32, :unique => true
14
- # DateTime :created_at
15
- # text :data
16
- # end
17
- # end
6
+ # Sequel's older migration class, available for backward compatibility.
7
+ # Uses subclasses with up and down instance methods for each migration:
18
8
  #
19
- # def down
20
- # # You can use raw SQL if you need to
21
- # self << 'DROP TABLE sessions'
22
- # end
23
- # end
24
- #
25
- # class AlterItems < Sequel::Migration
9
+ # Class.new(Sequel::Migration) do
26
10
  # def up
27
- # alter_table :items do
28
- # add_column :category, String, :default => 'ruby'
11
+ # create_table(:artists) do
12
+ # primary_key :id
13
+ # String :name
29
14
  # end
30
15
  # end
31
- #
16
+ #
32
17
  # def down
33
- # alter_table :items do
34
- # drop_column :category
35
- # end
18
+ # drop_table(:artists)
36
19
  # end
37
20
  # end
38
21
  #
39
- # To apply a migration to a database, you can invoke the #apply with
40
- # the target database instance and the direction :up or :down, e.g.:
41
- #
42
- # DB = Sequel.connect('sqlite://mydb')
43
- # CreateSessions.apply(DB, :up)
44
- #
45
- # See Sequel::Schema::Generator for the syntax to use for creating tables,
46
- # and Sequel::Schema::AlterTableGenerator for the syntax to use when
47
- # altering existing tables. Migrations act as a proxy for the database
48
- # given in #apply, so inside #down and #up, you can act as though self
49
- # refers to the database. So you can use any of the Sequel::Database
50
- # instance methods directly.
22
+ # Part of the +migration+ extension.
51
23
  class Migration
52
24
  # Creates a new instance of the migration and sets the @db attribute.
53
25
  def initialize(db)
@@ -57,15 +29,8 @@ module Sequel
57
29
  # Applies the migration to the supplied database in the specified
58
30
  # direction.
59
31
  def self.apply(db, direction)
60
- obj = new(db)
61
- case direction
62
- when :up
63
- obj.up
64
- when :down
65
- obj.down
66
- else
67
- raise ArgumentError, "Invalid migration direction specified (#{direction.inspect})"
68
- end
32
+ raise(ArgumentError, "Invalid migration direction specified (#{direction.inspect})") unless [:up, :down].include?(direction)
33
+ new(db).send(direction)
69
34
  end
70
35
 
71
36
  # Returns the list of Migration descendants.
@@ -92,9 +57,77 @@ module Sequel
92
57
  end
93
58
  end
94
59
 
95
- # The Migrator module performs migrations based on migration files in a
60
+ # Migration class used by the Sequel.migration DSL,
61
+ # using instances for each migration, unlike the
62
+ # +Migration+ class, which uses subclasses for each
63
+ # migration. Part of the +migration+ extension.
64
+ class SimpleMigration
65
+ # Proc used for the down action
66
+ attr_accessor :down
67
+
68
+ # Proc used for the up action
69
+ attr_accessor :up
70
+
71
+ # Apply the appropriate block on the +Database+
72
+ # instance using instance_eval.
73
+ def apply(db, direction)
74
+ raise(ArgumentError, "Invalid migration direction specified (#{direction.inspect})") unless [:up, :down].include?(direction)
75
+ if prok = send(direction)
76
+ db.instance_eval(&prok)
77
+ end
78
+ end
79
+ end
80
+
81
+ # Internal class used by the Sequel.migration DSL, part of the +migration+ extension.
82
+ class MigrationDSL < BasicObject
83
+ # The underlying Migration class.
84
+ attr_reader :migration
85
+
86
+ def self.create(&block)
87
+ new(&block).migration
88
+ end
89
+
90
+ # Create a new migration class, and instance_eval the block.
91
+ def initialize(&block)
92
+ @migration = SimpleMigration.new
93
+ Migration.descendants << migration
94
+ instance_eval(&block)
95
+ end
96
+
97
+ # Defines the migration's down action.
98
+ def down(&block)
99
+ migration.down = block
100
+ end
101
+
102
+ # Defines the migration's up action.
103
+ def up(&block)
104
+ migration.up = block
105
+ end
106
+ end
107
+
108
+ # The preferred method for writing Sequel migrations, using a DSL:
109
+ #
110
+ # Sequel.migration do
111
+ # up do
112
+ # create_table(:artists) do
113
+ # primary_key :id
114
+ # String :name
115
+ # end
116
+ # end
117
+ #
118
+ # down do
119
+ # drop_table(:artists)
120
+ # end
121
+ # end
122
+ #
123
+ # Designed to be used with the +Migrator+ class, part of the +migration+ extension.
124
+ def self.migration(&block)
125
+ MigrationDSL.create(&block)
126
+ end
127
+
128
+ # The +Migrator+ class performs migrations based on migration files in a
96
129
  # specified directory. The migration files should be named using the
97
- # following pattern (in similar fashion to ActiveRecord migrations):
130
+ # following pattern:
98
131
  #
99
132
  # <version>_<title>.rb
100
133
  #
@@ -102,10 +135,21 @@ module Sequel
102
135
  #
103
136
  # 001_create_sessions.rb
104
137
  # 002_add_data_column.rb
105
- # ...
138
+ #
139
+ # You can also use timestamps as version numbers:
140
+ #
141
+ # 1273253850_create_sessions.rb
142
+ # 1273257248_add_data_column.rb
106
143
  #
107
- # The migration files should contain one or more migration classes based
108
- # on Sequel::Migration.
144
+ # If any migration filenames use timestamps as version numbers, Sequel
145
+ # uses the +TimestampMigrator+ to migrate, otherwise it uses the +IntegerMigrator+.
146
+ # The +TimestampMigrator+ can handle migrations that are run out of order
147
+ # as well as migrations with the same timestamp,
148
+ # while the +IntegerMigrator+ is more strict and raises exceptions for missing
149
+ # or duplicate migration files.
150
+ #
151
+ # The migration files should contain either one +Migration+
152
+ # subclass or one <tt>Sequel.migration</tt> call.
109
153
  #
110
154
  # Migrations are generally run via the sequel command line tool,
111
155
  # using the -m and -M switches. The -m switch specifies the migration
@@ -114,10 +158,11 @@ module Sequel
114
158
  # You can apply migrations using the Migrator API, as well (this is necessary
115
159
  # if you want to specify the version from which to migrate in addition to the version
116
160
  # to which to migrate).
117
- # To apply a migration, the #apply method must be invoked with the database
161
+ # To apply a migrator, the +apply+ method must be invoked with the database
118
162
  # instance, the directory of migration files and the target version. If
119
163
  # no current version is supplied, it is read from the database. The migrator
120
- # automatically creates a schema_info table in the database to keep track
164
+ # automatically creates a table (schema_info for integer migrations and
165
+ # schema_migrations for timestamped migrations). in the database to keep track
121
166
  # of the current migration version. If no migration version is stored in the
122
167
  # database, the version is considered to be 0. If no target version is
123
168
  # specified, the database is migrated to the latest version available in the
@@ -127,26 +172,40 @@ module Sequel
127
172
  #
128
173
  # Sequel::Migrator.apply(DB, '.')
129
174
  #
175
+ # For example, to migrate the database all the way down:
176
+ #
177
+ # Sequel::Migrator.apply(DB, '.', 0)
178
+ #
179
+ # For example, to migrate the database to version 4:
180
+ #
181
+ # Sequel::Migrator.apply(DB, '.', 4)
182
+ #
130
183
  # To migrate the database from version 1 to version 5:
131
184
  #
132
185
  # Sequel::Migrator.apply(DB, '.', 5, 1)
133
- module Migrator
134
- DEFAULT_SCHEMA_COLUMN = :version
135
- DEFAULT_SCHEMA_TABLE = :schema_info
136
- MIGRATION_FILE_PATTERN = /\A\d+_.+\.rb\z/.freeze
186
+ #
187
+ # Part of the +migration+ extension.
188
+ class Migrator
189
+ MIGRATION_FILE_PATTERN = /\A\d+_.+\.rb\z/i.freeze
137
190
  MIGRATION_SPLITTER = '_'.freeze
191
+ MINIMUM_TIMESTAMP = 20000101
138
192
 
139
- # Wrapper for run, maintaining backwards API compatibility
193
+ # Exception class raised when there is an error with the migrator's
194
+ # file structure, database, or arguments.
195
+ class Error < Sequel::Error
196
+ end
197
+
198
+ # Wrapper for +run+, maintaining backwards API compatibility
140
199
  def self.apply(db, directory, target = nil, current = nil)
141
200
  run(db, directory, :target => target, :current => current)
142
201
  end
143
202
 
144
203
  # Migrates the supplied database using the migration files in the the specified directory. Options:
145
- # * :column - The column in the :table argument storing the migration version (default: :version).
146
- # * :current - The current version of the database. If not given, it is retrieved from the database
147
- # using the :table and :column options.
148
- # * :table - The table containing the schema version (default: :schema_info).
149
- # * :target - The target version to which to migrate. If not given, migrates to the maximum version.
204
+ # * :column :: The column in the :table argument storing the migration version (default: :version).
205
+ # * :current :: The current version of the database. If not given, it is retrieved from the database
206
+ # using the :table and :column options.
207
+ # * :table :: The table containing the schema version (default: :schema_info).
208
+ # * :target :: The target version to which to migrate. If not given, migrates to the maximum version.
150
209
  #
151
210
  # Examples:
152
211
  # Sequel::Migrator.run(DB, "migrations")
@@ -154,86 +213,309 @@ module Sequel
154
213
  # Sequel::Migrator.run(DB, "app1/migrations", :column=> :app2_version)
155
214
  # Sequel::Migrator.run(DB, "app2/migrations", :column => :app2_version, :table=>:schema_info2)
156
215
  def self.run(db, directory, opts={})
157
- raise(Error, "Must supply a valid migration path") unless directory and File.directory?(directory)
158
- raise(Error, "No current version available") unless current = opts[:current] || get_current_migration_version(db, opts)
159
- raise(Error, "No target version available") unless target = opts[:target] || latest_migration_version(directory)
216
+ migrator_class(directory).new(db, directory, opts).run
217
+ end
160
218
 
161
- direction = current < target ? :up : :down
162
-
163
- classes = migration_classes(directory, target, current, direction)
219
+ # Choose the Migrator subclass to use. Uses the TimestampMigrator
220
+ # if the version number appears to be a unix time integer for a year
221
+ # after 2005, otherwise uses the IntegerMigrator.
222
+ def self.migrator_class(directory)
223
+ Dir.new(directory).each do |file|
224
+ next unless MIGRATION_FILE_PATTERN.match(file)
225
+ return TimestampMigrator if file.split(MIGRATION_SPLITTER, 2).first.to_i > MINIMUM_TIMESTAMP
226
+ end
227
+ IntegerMigrator
228
+ end
229
+ private_class_method :migrator_class
230
+
231
+ # The column to use to hold the migration version number for integer migrations or
232
+ # filename for timestamp migrations (defaults to :version for integer migrations and
233
+ # :filename for timestamp migrations)
234
+ attr_reader :column
235
+
236
+ # The database related to this migrator
237
+ attr_reader :db
238
+
239
+ # The directory for this migrator's files
240
+ attr_reader :directory
241
+
242
+ # The dataset for this migrator, representing the +schema_info+ table for integer
243
+ # migrations and the +schema_migrations+ table for timestamp migrations
244
+ attr_reader :ds
245
+
246
+ # All migration files in this migrator's directory
247
+ attr_reader :files
248
+
249
+ # The table to use to hold the applied migration data (defaults to :schema_info for
250
+ # integer migrations and :schema_migrations for timestamp migrations)
251
+ attr_reader :table
252
+
253
+ # The target version for this migrator
254
+ attr_reader :target
164
255
 
165
- db.transaction do
166
- classes.each {|c| c.apply(db, direction)}
167
- set_current_migration_version(db, target, opts)
256
+ # Setup the state for the migrator
257
+ def initialize(db, directory, opts={})
258
+ raise(Error, "Must supply a valid migration path") unless File.directory?(directory)
259
+ @db = db
260
+ @directory = directory
261
+ @files = get_migration_files
262
+ @table = opts[:table] || self.class.const_get(:DEFAULT_SCHEMA_TABLE)
263
+ @column = opts[:column] || self.class.const_get(:DEFAULT_SCHEMA_COLUMN)
264
+ @ds = schema_dataset
265
+ end
266
+
267
+ private
268
+
269
+ # Remove all migration classes. Done by the migrator to ensure that
270
+ # the correct migration classes are picked up.
271
+ def remove_migration_classes
272
+ # Remove class definitions
273
+ Migration.descendants.each do |c|
274
+ Object.send(:remove_const, c.to_s) rescue nil
275
+ end
276
+ Migration.descendants.clear # remove any defined migration classes
277
+ end
278
+
279
+ # Return the integer migration version based on the filename.
280
+ def migration_version_from_file(filename)
281
+ filename.split(MIGRATION_SPLITTER, 2).first.to_i
282
+ end
283
+ end
284
+
285
+ # The default migrator, recommended in most cases. Uses a simple incrementing
286
+ # version number starting with 1, where missing or duplicate migration file
287
+ # versions are not allowed. Part of the +migration+ extension.
288
+ class IntegerMigrator < Migrator
289
+ DEFAULT_SCHEMA_COLUMN = :version
290
+ DEFAULT_SCHEMA_TABLE = :schema_info
291
+
292
+ Error = Migrator::Error
293
+
294
+ # The current version for this migrator
295
+ attr_reader :current
296
+
297
+ # The direction of the migrator, either :up or :down
298
+ attr_reader :direction
299
+
300
+ # The migrations used by this migrator
301
+ attr_reader :migrations
302
+
303
+ # Set up all state for the migrator instance
304
+ def initialize(db, directory, opts={})
305
+ super
306
+ @target = opts[:target] || latest_migration_version
307
+ @current = opts[:current] || current_migration_version
308
+ @direction = current < target ? :up : :down
309
+ @migrations = get_migrations
310
+
311
+ raise(Error, "No current version available") unless current
312
+ raise(Error, "No target version available") unless target
313
+ end
314
+
315
+ # Apply all migrations on the database
316
+ def run
317
+ migrations.zip(version_numbers).each do |m, v|
318
+ t = Time.now
319
+ lv = up? ? v : v + 1
320
+ db.log_info("Begin applying migration version #{lv}, direction: #{direction}")
321
+ db.transaction do
322
+ m.apply(db, direction)
323
+ set_migration_version(v)
324
+ end
325
+ db.log_info("Finished applying migration version #{lv}, direction: #{direction}, took #{sprintf('%0.6f', Time.now - t)} seconds")
168
326
  end
169
327
 
170
328
  target
171
329
  end
172
330
 
331
+ private
332
+
173
333
  # Gets the current migration version stored in the database. If no version
174
334
  # number is stored, 0 is returned.
175
- def self.get_current_migration_version(db, opts={})
176
- (schema_info_dataset(db, opts).first || {})[opts[:column] || DEFAULT_SCHEMA_COLUMN] || 0
335
+ def current_migration_version
336
+ ds.get(column) || 0
177
337
  end
178
338
 
179
- # Returns the latest version available in the specified directory.
180
- def self.latest_migration_version(directory)
181
- l = migration_files(directory).last
182
- l ? migration_version_from_file(File.basename(l)) : nil
339
+ # Returns any found migration files in the supplied directory.
340
+ def get_migration_files
341
+ files = []
342
+ Dir.new(directory).each do |file|
343
+ next unless MIGRATION_FILE_PATTERN.match(file)
344
+ version = migration_version_from_file(file)
345
+ raise(Error, "Duplicate migration version: #{version}") if files[version]
346
+ files[version] = File.join(directory, file)
347
+ end
348
+ 1.upto(files.length - 1){|i| raise(Error, "Missing migration version: #{i}") unless files[i]}
349
+ files
183
350
  end
184
-
351
+
185
352
  # Returns a list of migration classes filtered for the migration range and
186
353
  # ordered according to the migration direction.
187
- def self.migration_classes(directory, target, current, direction)
188
- range = direction == :up ?
189
- (current + 1)..target : (target + 1)..current
190
-
191
- # Remove class definitions
192
- Migration.descendants.each do |c|
193
- Object.send(:remove_const, c.to_s) rescue nil
194
- end
195
- Migration.descendants.clear # remove any defined migration classes
354
+ def get_migrations
355
+ remove_migration_classes
196
356
 
197
357
  # load migration files
198
- migration_files(directory, range).each {|fn| load(fn)}
358
+ files[up? ? (current + 1)..target : (target + 1)..current].compact.each{|f| load(f)}
199
359
 
200
360
  # get migration classes
201
361
  classes = Migration.descendants
202
- classes.reverse! if direction == :down
203
- classes
362
+ up? ? classes : classes.reverse
204
363
  end
205
364
 
206
- # Returns any found migration files in the supplied directory.
207
- def self.migration_files(directory, range = nil)
208
- files = []
209
- Dir.new(directory).each do |file|
210
- files[migration_version_from_file(file)] = File.join(directory, file) if MIGRATION_FILE_PATTERN.match(file)
211
- end
212
- filtered = range ? files[range] : files
213
- filtered ? filtered.compact : []
365
+ # Returns the latest version available in the specified directory.
366
+ def latest_migration_version
367
+ l = files.last
368
+ l ? migration_version_from_file(File.basename(l)) : nil
214
369
  end
215
370
 
216
371
  # Returns the dataset for the schema_info table. If no such table
217
372
  # exists, it is automatically created.
218
- def self.schema_info_dataset(db, opts={})
219
- column = opts[:column] || DEFAULT_SCHEMA_COLUMN
220
- table = opts[:table] || DEFAULT_SCHEMA_TABLE
221
- db.create_table?(table){Integer column}
222
- db.alter_table(table){add_column column, Integer} unless db.from(table).columns.include?(column)
223
- db.from(table)
373
+ def schema_dataset
374
+ c = column
375
+ ds = db.from(table)
376
+ if !db.table_exists?(table)
377
+ db.create_table(table){Integer c, :default=>0, :null=>false}
378
+ elsif !ds.columns.include?(c)
379
+ db.alter_table(table){add_column c, Integer, :default=>0, :null=>false}
380
+ end
381
+ ds.insert(c=>0) if ds.empty?
382
+ raise(Error, "More than 1 row in migrator table") if ds.count > 1
383
+ ds
224
384
  end
225
385
 
226
386
  # Sets the current migration version stored in the database.
227
- def self.set_current_migration_version(db, version, opts={})
228
- column = opts[:column] || DEFAULT_SCHEMA_COLUMN
229
- dataset = schema_info_dataset(db, opts)
230
- dataset.send(dataset.first ? :update : :insert, column => version)
387
+ def set_migration_version(version)
388
+ ds.update(column=>version)
231
389
  end
232
390
 
233
- # Return the integer migration version based on the filename.
234
- def self.migration_version_from_file(filename) # :nodoc:
235
- filename.split(MIGRATION_SPLITTER, 2).first.to_i
391
+ # Whether or not this is an up migration
392
+ def up?
393
+ direction == :up
394
+ end
395
+
396
+ # An array of numbers corresponding to the migrations,
397
+ # so that each number in the array is the migration version
398
+ # that will be in affect after the migration is run.
399
+ def version_numbers
400
+ up? ? ((current+1)..target).to_a : (target..(current - 1)).to_a.reverse
401
+ end
402
+ end
403
+
404
+ # The migrator used if any migration file version appears to be a timestamp.
405
+ # Stores filenames of migration files, and can figure out which migrations
406
+ # have not been applied and apply them, even if earlier migrations are added
407
+ # after later migrations. If you plan to do that, the responsibility is on
408
+ # you to make sure the migrations don't conflict. Part of the +migration+ extension.
409
+ class TimestampMigrator < Migrator
410
+ DEFAULT_SCHEMA_COLUMN = :filename
411
+ DEFAULT_SCHEMA_TABLE = :schema_migrations
412
+
413
+ Error = Migrator::Error
414
+
415
+ # Array of strings of applied migration filenames
416
+ attr_reader :applied_migrations
417
+
418
+ # Get tuples of migrations, filenames, and actions for each migration
419
+ attr_reader :migration_tuples
420
+
421
+ # Set up all state for the migrator instance
422
+ def initialize(db, directory, opts={})
423
+ super
424
+ @target = opts[:target]
425
+ @applied_migrations = get_applied_migrations
426
+ @migration_tuples = get_migration_tuples
427
+ end
428
+
429
+ # Apply all migration tuples on the database
430
+ def run
431
+ migration_tuples.each do |m, f, direction|
432
+ t = Time.now
433
+ db.log_info("Begin applying migration #{f}, direction: #{direction}")
434
+ db.transaction do
435
+ m.apply(db, direction)
436
+ fi = f.downcase
437
+ direction == :up ? ds.insert(column=>fi) : ds.filter(column=>fi).delete
438
+ end
439
+ db.log_info("Finished applying migration #{f}, direction: #{direction}, took #{sprintf('%0.6f', Time.now - t)} seconds")
440
+ end
441
+ nil
442
+ end
443
+
444
+ private
445
+
446
+ # Convert the schema_info table to the new schema_migrations table format,
447
+ # using the version of the schema_info table and the current migration files.
448
+ def convert_from_schema_info
449
+ v = db[IntegerMigrator::DEFAULT_SCHEMA_TABLE].get(IntegerMigrator::DEFAULT_SCHEMA_COLUMN)
450
+ ds = db.from(table)
451
+ files.each do |path|
452
+ f = File.basename(path)
453
+ if migration_version_from_file(f) <= v
454
+ ds.insert(column=>f)
455
+ end
456
+ end
457
+ end
458
+
459
+ # Returns filenames of all applied migrations
460
+ def get_applied_migrations
461
+ am = ds.select_order_map(column)
462
+ missing_migration_files = am - files.map{|f| File.basename(f).downcase}
463
+ raise(Error, "Applied migration files not in file system: #{missing_migration_files.join(', ')}") if missing_migration_files.length > 0
464
+ am
465
+ end
466
+
467
+ # Returns any migration files found in the migrator's directory.
468
+ def get_migration_files
469
+ files = []
470
+ Dir.new(directory).each do |file|
471
+ next unless MIGRATION_FILE_PATTERN.match(file)
472
+ files << File.join(directory, file)
473
+ end
474
+ files.sort
475
+ end
476
+
477
+ # Returns tuples of migration, filename, and direction
478
+ def get_migration_tuples
479
+ remove_migration_classes
480
+ up_mts = []
481
+ down_mts = []
482
+ ms = Migration.descendants
483
+ files.each do |path|
484
+ f = File.basename(path)
485
+ fi = f.downcase
486
+ if target
487
+ if migration_version_from_file(f) > target
488
+ if applied_migrations.include?(fi)
489
+ load(path)
490
+ down_mts << [ms.last, f, :down]
491
+ end
492
+ elsif !applied_migrations.include?(fi)
493
+ load(path)
494
+ up_mts << [ms.last, f, :up]
495
+ end
496
+ elsif !applied_migrations.include?(fi)
497
+ load(path)
498
+ up_mts << [ms.last, f, :up]
499
+ end
500
+ end
501
+ up_mts + down_mts.reverse
502
+ end
503
+
504
+ # Returns the dataset for the schema_migrations table. If no such table
505
+ # exists, it is automatically created.
506
+ def schema_dataset
507
+ c = column
508
+ ds = db.from(table)
509
+ if !db.table_exists?(table)
510
+ db.create_table(table){String c, :primary_key=>true}
511
+ if db.table_exists?(:schema_info) and vha = db[:schema_info].all and vha.length == 1 and
512
+ vha.first.keys == [:version] and vha.first.values.first.is_a?(Integer)
513
+ convert_from_schema_info
514
+ end
515
+ elsif !ds.columns.include?(c)
516
+ raise(Error, "Migrator table #{table} does not contain column #{c}")
517
+ end
518
+ ds
236
519
  end
237
- private_class_method :migration_version_from_file
238
520
  end
239
521
  end