sequel 3.11.0 → 3.12.0

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