sequel 3.11.0 → 3.12.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +70 -0
- data/Rakefile +1 -1
- data/doc/active_record.rdoc +896 -0
- data/doc/advanced_associations.rdoc +46 -31
- data/doc/association_basics.rdoc +14 -9
- data/doc/dataset_basics.rdoc +3 -3
- data/doc/migration.rdoc +1011 -0
- data/doc/model_hooks.rdoc +198 -0
- data/doc/querying.rdoc +811 -86
- data/doc/release_notes/3.12.0.txt +304 -0
- data/doc/sharding.rdoc +17 -0
- data/doc/sql.rdoc +537 -0
- data/doc/validations.rdoc +501 -0
- data/lib/sequel/adapters/jdbc.rb +19 -27
- data/lib/sequel/adapters/jdbc/postgresql.rb +0 -7
- data/lib/sequel/adapters/mysql.rb +5 -4
- data/lib/sequel/adapters/odbc.rb +3 -2
- data/lib/sequel/adapters/shared/mssql.rb +7 -6
- data/lib/sequel/adapters/shared/mysql.rb +2 -7
- data/lib/sequel/adapters/shared/postgres.rb +2 -8
- data/lib/sequel/adapters/shared/sqlite.rb +2 -5
- data/lib/sequel/adapters/sqlite.rb +4 -4
- data/lib/sequel/core.rb +0 -1
- data/lib/sequel/database.rb +2 -1060
- data/lib/sequel/database/connecting.rb +227 -0
- data/lib/sequel/database/dataset.rb +58 -0
- data/lib/sequel/database/dataset_defaults.rb +127 -0
- data/lib/sequel/database/logging.rb +62 -0
- data/lib/sequel/database/misc.rb +246 -0
- data/lib/sequel/database/query.rb +390 -0
- data/lib/sequel/database/schema_generator.rb +7 -3
- data/lib/sequel/database/schema_methods.rb +351 -7
- data/lib/sequel/dataset/actions.rb +9 -2
- data/lib/sequel/dataset/misc.rb +6 -2
- data/lib/sequel/dataset/mutation.rb +3 -11
- data/lib/sequel/dataset/query.rb +49 -6
- data/lib/sequel/exceptions.rb +3 -0
- data/lib/sequel/extensions/migration.rb +395 -113
- data/lib/sequel/extensions/schema_dumper.rb +21 -13
- data/lib/sequel/model.rb +27 -25
- data/lib/sequel/model/associations.rb +72 -34
- data/lib/sequel/model/base.rb +74 -18
- data/lib/sequel/model/errors.rb +8 -1
- data/lib/sequel/plugins/active_model.rb +8 -0
- data/lib/sequel/plugins/association_pks.rb +87 -0
- data/lib/sequel/plugins/association_proxies.rb +8 -0
- data/lib/sequel/plugins/boolean_readers.rb +12 -6
- data/lib/sequel/plugins/caching.rb +14 -7
- data/lib/sequel/plugins/class_table_inheritance.rb +15 -9
- data/lib/sequel/plugins/composition.rb +2 -1
- data/lib/sequel/plugins/force_encoding.rb +10 -7
- data/lib/sequel/plugins/hook_class_methods.rb +12 -11
- data/lib/sequel/plugins/identity_map.rb +9 -0
- data/lib/sequel/plugins/instance_hooks.rb +23 -13
- data/lib/sequel/plugins/lazy_attributes.rb +4 -1
- data/lib/sequel/plugins/many_through_many.rb +18 -4
- data/lib/sequel/plugins/nested_attributes.rb +1 -0
- data/lib/sequel/plugins/optimistic_locking.rb +1 -1
- data/lib/sequel/plugins/rcte_tree.rb +9 -8
- data/lib/sequel/plugins/schema.rb +8 -0
- data/lib/sequel/plugins/serialization.rb +1 -3
- data/lib/sequel/plugins/sharding.rb +135 -0
- data/lib/sequel/plugins/single_table_inheritance.rb +117 -25
- data/lib/sequel/plugins/skip_create_refresh.rb +35 -0
- data/lib/sequel/plugins/string_stripper.rb +26 -0
- data/lib/sequel/plugins/tactical_eager_loading.rb +8 -0
- data/lib/sequel/plugins/timestamps.rb +15 -2
- data/lib/sequel/plugins/touch.rb +13 -0
- data/lib/sequel/plugins/update_primary_key.rb +48 -0
- data/lib/sequel/plugins/validation_class_methods.rb +8 -0
- data/lib/sequel/plugins/validation_helpers.rb +1 -1
- data/lib/sequel/sql.rb +17 -20
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +5 -5
- data/spec/core/core_sql_spec.rb +17 -1
- data/spec/core/database_spec.rb +17 -5
- data/spec/core/dataset_spec.rb +31 -8
- data/spec/core/schema_generator_spec.rb +8 -1
- data/spec/core/schema_spec.rb +13 -0
- data/spec/extensions/association_pks_spec.rb +85 -0
- data/spec/extensions/hook_class_methods_spec.rb +9 -9
- data/spec/extensions/migration_spec.rb +339 -219
- data/spec/extensions/schema_dumper_spec.rb +28 -17
- data/spec/extensions/sharding_spec.rb +272 -0
- data/spec/extensions/single_table_inheritance_spec.rb +92 -4
- data/spec/extensions/skip_create_refresh_spec.rb +17 -0
- data/spec/extensions/string_stripper_spec.rb +23 -0
- data/spec/extensions/update_primary_key_spec.rb +65 -0
- data/spec/extensions/validation_class_methods_spec.rb +5 -5
- data/spec/files/bad_down_migration/001_create_alt_basic.rb +4 -0
- data/spec/files/bad_down_migration/002_create_alt_advanced.rb +4 -0
- data/spec/files/bad_timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/bad_timestamped_migrations/1273253851_create_nodes.rb +9 -0
- data/spec/files/bad_timestamped_migrations/1273253853_3_create_users.rb +3 -0
- data/spec/files/bad_up_migration/001_create_alt_basic.rb +4 -0
- data/spec/files/bad_up_migration/002_create_alt_advanced.rb +3 -0
- data/spec/files/convert_to_timestamp_migrations/001_create_sessions.rb +9 -0
- data/spec/files/convert_to_timestamp_migrations/002_create_nodes.rb +9 -0
- data/spec/files/convert_to_timestamp_migrations/003_3_create_users.rb +4 -0
- data/spec/files/convert_to_timestamp_migrations/1273253850_create_artists.rb +9 -0
- data/spec/files/convert_to_timestamp_migrations/1273253852_create_albums.rb +9 -0
- data/spec/files/duplicate_integer_migrations/001_create_alt_advanced.rb +4 -0
- data/spec/files/duplicate_integer_migrations/001_create_alt_basic.rb +4 -0
- data/spec/files/duplicate_timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/duplicate_timestamped_migrations/1273253853_create_nodes.rb +9 -0
- data/spec/files/duplicate_timestamped_migrations/1273253853_create_users.rb +4 -0
- data/spec/files/integer_migrations/001_create_sessions.rb +9 -0
- data/spec/files/integer_migrations/002_create_nodes.rb +9 -0
- data/spec/files/integer_migrations/003_3_create_users.rb +4 -0
- data/spec/files/interleaved_timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/interleaved_timestamped_migrations/1273253850_create_artists.rb +9 -0
- data/spec/files/interleaved_timestamped_migrations/1273253851_create_nodes.rb +9 -0
- data/spec/files/interleaved_timestamped_migrations/1273253852_create_albums.rb +9 -0
- data/spec/files/interleaved_timestamped_migrations/1273253853_3_create_users.rb +4 -0
- data/spec/files/missing_integer_migrations/001_create_alt_basic.rb +4 -0
- data/spec/files/missing_integer_migrations/003_create_alt_advanced.rb +4 -0
- data/spec/files/missing_timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/missing_timestamped_migrations/1273253853_3_create_users.rb +4 -0
- data/spec/files/timestamped_migrations/1273253849_create_sessions.rb +9 -0
- data/spec/files/timestamped_migrations/1273253851_create_nodes.rb +9 -0
- data/spec/files/timestamped_migrations/1273253853_3_create_users.rb +4 -0
- data/spec/files/uppercase_timestamped_migrations/1273253849_CREATE_SESSIONS.RB +9 -0
- data/spec/files/uppercase_timestamped_migrations/1273253851_CREATE_NODES.RB +9 -0
- data/spec/files/uppercase_timestamped_migrations/1273253853_3_CREATE_USERS.RB +4 -0
- data/spec/integration/eager_loader_test.rb +20 -20
- data/spec/integration/migrator_test.rb +187 -0
- data/spec/integration/plugin_test.rb +150 -0
- data/spec/integration/schema_test.rb +13 -2
- data/spec/model/associations_spec.rb +41 -14
- data/spec/model/base_spec.rb +69 -0
- data/spec/model/eager_loading_spec.rb +7 -3
- data/spec/model/record_spec.rb +79 -4
- data/spec/model/validations_spec.rb +21 -9
- metadata +66 -5
- data/doc/schema.rdoc +0 -36
- 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
|
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
|
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:
|
data/lib/sequel/dataset/misc.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/sequel/dataset/query.rb
CHANGED
@@ -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
|
-
|
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
|
511
|
+
def reverse(*order)
|
473
512
|
order(*invert_order(order.empty? ? @opts[:order] : order))
|
474
513
|
end
|
475
|
-
|
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,
|
data/lib/sequel/exceptions.rb
CHANGED
@@ -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
|
-
#
|
7
|
-
#
|
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
|
-
#
|
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
|
-
#
|
28
|
-
#
|
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
|
-
#
|
34
|
-
# drop_column :category
|
35
|
-
# end
|
18
|
+
# drop_table(:artists)
|
36
19
|
# end
|
37
20
|
# end
|
38
21
|
#
|
39
|
-
#
|
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
|
-
|
61
|
-
|
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
|
-
#
|
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
|
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
|
-
#
|
108
|
-
#
|
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
|
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
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
MIGRATION_FILE_PATTERN = /\A\d+_.+\.rb\z
|
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
|
-
#
|
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
|
146
|
-
# * :current
|
147
|
-
#
|
148
|
-
# * :table
|
149
|
-
# * :target
|
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
|
-
|
158
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
166
|
-
|
167
|
-
|
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
|
176
|
-
(
|
335
|
+
def current_migration_version
|
336
|
+
ds.get(column) || 0
|
177
337
|
end
|
178
338
|
|
179
|
-
# Returns
|
180
|
-
def
|
181
|
-
|
182
|
-
|
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
|
188
|
-
|
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
|
-
|
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
|
-
|
203
|
-
classes
|
362
|
+
up? ? classes : classes.reverse
|
204
363
|
end
|
205
364
|
|
206
|
-
# Returns
|
207
|
-
def
|
208
|
-
|
209
|
-
|
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
|
219
|
-
|
220
|
-
|
221
|
-
db.
|
222
|
-
|
223
|
-
|
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
|
228
|
-
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
|
-
#
|
234
|
-
def
|
235
|
-
|
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
|