sequel 4.35.0 → 4.36.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.
- checksums.yaml +4 -4
- data/CHANGELOG +32 -0
- data/doc/association_basics.rdoc +27 -4
- data/doc/migration.rdoc +24 -0
- data/doc/release_notes/4.36.0.txt +116 -0
- data/lib/sequel/adapters/jdbc/h2.rb +1 -1
- data/lib/sequel/adapters/mysql2.rb +11 -1
- data/lib/sequel/adapters/oracle.rb +3 -5
- data/lib/sequel/adapters/postgres.rb +2 -2
- data/lib/sequel/adapters/shared/access.rb +1 -1
- data/lib/sequel/adapters/shared/oracle.rb +1 -1
- data/lib/sequel/adapters/shared/postgres.rb +1 -1
- data/lib/sequel/adapters/shared/sqlite.rb +1 -1
- data/lib/sequel/connection_pool.rb +5 -0
- data/lib/sequel/connection_pool/sharded_single.rb +1 -1
- data/lib/sequel/connection_pool/sharded_threaded.rb +29 -14
- data/lib/sequel/connection_pool/single.rb +1 -1
- data/lib/sequel/connection_pool/threaded.rb +5 -3
- data/lib/sequel/database/schema_methods.rb +7 -1
- data/lib/sequel/dataset/sql.rb +4 -0
- data/lib/sequel/extensions/arbitrary_servers.rb +1 -1
- data/lib/sequel/extensions/connection_expiration.rb +89 -0
- data/lib/sequel/extensions/connection_validator.rb +11 -3
- data/lib/sequel/extensions/constraint_validations.rb +28 -0
- data/lib/sequel/extensions/string_agg.rb +178 -0
- data/lib/sequel/model.rb +13 -56
- data/lib/sequel/model/associations.rb +3 -1
- data/lib/sequel/model/base.rb +104 -7
- data/lib/sequel/plugins/constraint_validations.rb +17 -3
- data/lib/sequel/plugins/validation_helpers.rb +1 -1
- data/lib/sequel/sql.rb +8 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +4 -0
- data/spec/core/dataset_spec.rb +4 -0
- data/spec/core/expression_filters_spec.rb +4 -0
- data/spec/extensions/connection_expiration_spec.rb +121 -0
- data/spec/extensions/connection_validator_spec.rb +7 -0
- data/spec/extensions/constraint_validations_plugin_spec.rb +14 -0
- data/spec/extensions/constraint_validations_spec.rb +64 -0
- data/spec/extensions/string_agg_spec.rb +85 -0
- data/spec/extensions/validation_helpers_spec.rb +2 -0
- data/spec/integration/plugin_test.rb +37 -2
- data/spec/model/association_reflection_spec.rb +10 -0
- data/spec/model/model_spec.rb +49 -0
- metadata +8 -2
data/lib/sequel/model.rb
CHANGED
@@ -3,61 +3,14 @@
|
|
3
3
|
require 'sequel/core'
|
4
4
|
|
5
5
|
module Sequel
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
# Database :: Sets the database for this model to +source+.
|
10
|
-
# Generally only useful when subclassing directly
|
11
|
-
# from the returned class, where the name of the
|
12
|
-
# subclass sets the table name (which is combined
|
13
|
-
# with the +Database+ in +source+ to create the
|
14
|
-
# dataset to use)
|
15
|
-
# Dataset :: Sets the dataset for this model to +source+.
|
16
|
-
# other :: Sets the table name for this model to +source+. The
|
17
|
-
# class will use the default database for model
|
18
|
-
# classes in order to create the dataset.
|
19
|
-
#
|
20
|
-
# The purpose of this method is to set the dataset/database automatically
|
21
|
-
# for a model class, if the table name doesn't match the implicit
|
22
|
-
# name. This is neater than using set_dataset inside the class,
|
23
|
-
# doesn't require a bogus query for the schema.
|
24
|
-
#
|
25
|
-
# # Using a symbol
|
26
|
-
# class Comment < Sequel::Model(:something)
|
27
|
-
# table_name # => :something
|
28
|
-
# end
|
29
|
-
#
|
30
|
-
# # Using a dataset
|
31
|
-
# class Comment < Sequel::Model(DB1[:something])
|
32
|
-
# dataset # => DB1[:something]
|
33
|
-
# end
|
34
|
-
#
|
35
|
-
# # Using a database
|
36
|
-
# class Comment < Sequel::Model(DB1)
|
37
|
-
# dataset # => DB1[:comments]
|
38
|
-
# end
|
39
|
-
def self.Model(source)
|
40
|
-
if cache_anonymous_models && (klass = Model::ANONYMOUS_MODEL_CLASSES_MUTEX.synchronize{Model::ANONYMOUS_MODEL_CLASSES[source]})
|
41
|
-
return klass
|
42
|
-
end
|
43
|
-
klass = if source.is_a?(Database)
|
44
|
-
c = Class.new(Model)
|
45
|
-
c.db = source
|
46
|
-
c
|
47
|
-
else
|
48
|
-
Class.new(Model).set_dataset(source)
|
49
|
-
end
|
50
|
-
Model::ANONYMOUS_MODEL_CLASSES_MUTEX.synchronize{Model::ANONYMOUS_MODEL_CLASSES[source] = klass} if cache_anonymous_models
|
51
|
-
klass
|
6
|
+
# Delegate to Sequel::Model, only for backwards compatibility.
|
7
|
+
def self.cache_anonymous_models
|
8
|
+
Model.cache_anonymous_models
|
52
9
|
end
|
53
10
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
# Whether to cache the anonymous models created by Sequel::Model(). This is
|
58
|
-
# required for reloading them correctly (avoiding the superclass mismatch). True
|
59
|
-
# by default for backwards compatibility.
|
60
|
-
attr_accessor :cache_anonymous_models
|
11
|
+
# Delegate to Sequel::Model, only for backwards compatibility.
|
12
|
+
def self.cache_anonymous_models=(v)
|
13
|
+
Model.cache_anonymous_models = v
|
61
14
|
end
|
62
15
|
|
63
16
|
# <tt>Sequel::Model</tt> is an object relational mapper built on top of Sequel core. Each
|
@@ -78,10 +31,10 @@ module Sequel
|
|
78
31
|
|
79
32
|
# Map that stores model classes created with <tt>Sequel::Model()</tt>, to allow the reopening
|
80
33
|
# of classes when dealing with code reloading.
|
81
|
-
ANONYMOUS_MODEL_CLASSES = {}
|
34
|
+
ANONYMOUS_MODEL_CLASSES = @Model_cache = {}
|
82
35
|
|
83
36
|
# Mutex protecting access to ANONYMOUS_MODEL_CLASSES
|
84
|
-
ANONYMOUS_MODEL_CLASSES_MUTEX = Mutex.new
|
37
|
+
ANONYMOUS_MODEL_CLASSES_MUTEX = @Model_mutex = Mutex.new
|
85
38
|
|
86
39
|
# Class methods added to model that call the method of the same name on the dataset
|
87
40
|
DATASET_METHODS = (Dataset::ACTION_METHODS + Dataset::QUERY_METHODS +
|
@@ -125,7 +78,8 @@ module Sequel
|
|
125
78
|
:@raise_on_typecast_failure=>nil, :@plugins=>:dup, :@setter_methods=>nil,
|
126
79
|
:@use_after_commit_rollback=>nil, :@fast_pk_lookup_sql=>nil,
|
127
80
|
:@fast_instance_delete_sql=>nil, :@finders=>:dup, :@finder_loaders=>:dup,
|
128
|
-
:@db=>nil, :@default_set_fields_options=>:dup, :@require_valid_table=>nil
|
81
|
+
:@db=>nil, :@default_set_fields_options=>:dup, :@require_valid_table=>nil,
|
82
|
+
:@cache_anonymous_models=>nil, :@Model_mutex=>nil}
|
129
83
|
|
130
84
|
# Regular expression that determines if a method name is normal in the sense that
|
131
85
|
# it could be used literally in ruby code without using send. Used to
|
@@ -137,6 +91,7 @@ module Sequel
|
|
137
91
|
SETTER_METHOD_REGEXP = /=\z/
|
138
92
|
|
139
93
|
@allowed_columns = nil
|
94
|
+
@cache_anonymous_models = true
|
140
95
|
@db = nil
|
141
96
|
@db_schema = nil
|
142
97
|
@dataset = nil
|
@@ -174,5 +129,7 @@ module Sequel
|
|
174
129
|
# The setter methods (methods ending with =) that are never allowed
|
175
130
|
# to be called automatically via +set+/+update+/+new+/etc..
|
176
131
|
RESTRICTED_SETTER_METHODS = instance_methods.map(&:to_s).grep(SETTER_METHOD_REGEXP)
|
132
|
+
|
133
|
+
def_Model(::Sequel)
|
177
134
|
end
|
178
135
|
end
|
@@ -1486,7 +1486,9 @@ module Sequel
|
|
1486
1486
|
# :class :: The associated class or its name as a string or symbol. If not
|
1487
1487
|
# given, uses the association's name, which is camelized (and
|
1488
1488
|
# singularized unless the type is :many_to_one, :one_to_one, or one_through_one). If this is specified
|
1489
|
-
# as a string or symbol, you must specify the full class name (e.g. "SomeModule::MyModel").
|
1489
|
+
# as a string or symbol, you must specify the full class name (e.g. "::SomeModule::MyModel").
|
1490
|
+
# :class_namespace :: If :class is given as a string or symbol, sets the default namespace in which to look for
|
1491
|
+
# the class. <tt>:class=>'Foo', :class_namespace=>'Bar'</tt> looks for <tt>::Bar::Foo</tt>.)
|
1490
1492
|
# :clearer :: Proc used to define the private _remove_all_* method for doing the database work
|
1491
1493
|
# to remove all objects associated to the current object (*_to_many assocations).
|
1492
1494
|
# :clone :: Merge the current options and block into the options and block used in defining
|
data/lib/sequel/model/base.rb
CHANGED
@@ -14,6 +14,11 @@ module Sequel
|
|
14
14
|
# (default: not set, so all columns not otherwise restricted are allowed).
|
15
15
|
attr_reader :allowed_columns
|
16
16
|
|
17
|
+
# Whether to cache the anonymous models created by Sequel::Model(). This is
|
18
|
+
# required for reloading them correctly (avoiding the superclass mismatch). True
|
19
|
+
# by default for backwards compatibility.
|
20
|
+
attr_accessor :cache_anonymous_models
|
21
|
+
|
17
22
|
# Array of modules that extend this model's dataset. Stored
|
18
23
|
# so that if the model's dataset is changed, it will be extended
|
19
24
|
# with all of these modules.
|
@@ -48,11 +53,8 @@ module Sequel
|
|
48
53
|
attr_accessor :raise_on_save_failure
|
49
54
|
|
50
55
|
# Whether to raise an error when unable to typecast data for a column
|
51
|
-
# (default:
|
52
|
-
#
|
53
|
-
# web applications). You can use the validates_schema_types validation
|
54
|
-
# (from the validation_helpers plugin) in connection with this setting to
|
55
|
-
# check for typecast failures during validation.
|
56
|
+
# (default: false). This should be set to true if you want to have model
|
57
|
+
# setter methods raise errors if the argument cannot be typecast properly.
|
56
58
|
attr_accessor :raise_on_typecast_failure
|
57
59
|
|
58
60
|
# Whether to raise an error if an UPDATE or DELETE query related to
|
@@ -116,6 +118,94 @@ module Sequel
|
|
116
118
|
# If you are sending database queries in before_* or after_* hooks, you shouldn't change
|
117
119
|
# the default setting without a good reason.
|
118
120
|
attr_accessor :use_transactions
|
121
|
+
|
122
|
+
# Define a Model method on the given module that calls the Model
|
123
|
+
# method on the receiver. This is how the Sequel::Model() method is
|
124
|
+
# defined, and allows you to define Model() methods on other modules,
|
125
|
+
# making it easier to have custom model settings for all models under
|
126
|
+
# a namespace. Example:
|
127
|
+
#
|
128
|
+
# module Foo
|
129
|
+
# Model = Class.new(Sequel::Model)
|
130
|
+
# Model.def_Model(self)
|
131
|
+
# DB = Model.db = Sequel.connect(ENV['FOO_DATABASE_URL'])
|
132
|
+
# Model.plugin :prepared_statements
|
133
|
+
#
|
134
|
+
# class Bar < Model
|
135
|
+
# # Uses Foo::DB[:bars]
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# class Baz < Model(:my_baz)
|
139
|
+
# # Uses Foo::DB[:my_baz]
|
140
|
+
# end
|
141
|
+
# end
|
142
|
+
def def_Model(mod)
|
143
|
+
model = self
|
144
|
+
(class << mod; self; end).send(:define_method, :Model) do |source|
|
145
|
+
model.Model(source)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Lets you create a Model subclass with its dataset already set.
|
150
|
+
# +source+ should be an instance of one of the following classes:
|
151
|
+
#
|
152
|
+
# Database :: Sets the database for this model to +source+.
|
153
|
+
# Generally only useful when subclassing directly
|
154
|
+
# from the returned class, where the name of the
|
155
|
+
# subclass sets the table name (which is combined
|
156
|
+
# with the +Database+ in +source+ to create the
|
157
|
+
# dataset to use)
|
158
|
+
# Dataset :: Sets the dataset for this model to +source+.
|
159
|
+
# other :: Sets the table name for this model to +source+. The
|
160
|
+
# class will use the default database for model
|
161
|
+
# classes in order to create the dataset.
|
162
|
+
#
|
163
|
+
# The purpose of this method is to set the dataset/database automatically
|
164
|
+
# for a model class, if the table name doesn't match the implicit
|
165
|
+
# name. This is neater than using set_dataset inside the class,
|
166
|
+
# doesn't require a bogus query for the schema.
|
167
|
+
#
|
168
|
+
# When creating subclasses of Sequel::Model itself, this method is usually
|
169
|
+
# called on Sequel itself, using <tt>Sequel::Model(:something)</tt>.
|
170
|
+
#
|
171
|
+
# # Using a symbol
|
172
|
+
# class Comment < Sequel::Model(:something)
|
173
|
+
# table_name # => :something
|
174
|
+
# end
|
175
|
+
#
|
176
|
+
# # Using a dataset
|
177
|
+
# class Comment < Sequel::Model(DB1[:something])
|
178
|
+
# dataset # => DB1[:something]
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
# # Using a database
|
182
|
+
# class Comment < Sequel::Model(DB1)
|
183
|
+
# dataset # => DB1[:comments]
|
184
|
+
# end
|
185
|
+
def Model(source)
|
186
|
+
if cache_anonymous_models
|
187
|
+
mutex = @Model_mutex
|
188
|
+
cache = mutex.synchronize{@Model_cache ||= {}}
|
189
|
+
|
190
|
+
if klass = mutex.synchronize{cache[source]}
|
191
|
+
return klass
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
klass = Class.new(self)
|
196
|
+
|
197
|
+
if source.is_a?(::Sequel::Database)
|
198
|
+
klass.db = source
|
199
|
+
else
|
200
|
+
klass.set_dataset(source)
|
201
|
+
end
|
202
|
+
|
203
|
+
if cache_anonymous_models
|
204
|
+
mutex.synchronize{cache[source] = klass}
|
205
|
+
end
|
206
|
+
|
207
|
+
klass
|
208
|
+
end
|
119
209
|
|
120
210
|
# Returns the first record from the database matching the conditions.
|
121
211
|
# If a hash is given, it is used as the conditions. If another
|
@@ -992,11 +1082,18 @@ module Sequel
|
|
992
1082
|
case opts[:class]
|
993
1083
|
when String, Symbol
|
994
1084
|
# Delete :class to allow late binding
|
995
|
-
|
1085
|
+
class_name = opts.delete(:class).to_s
|
1086
|
+
|
1087
|
+
if (namespace = opts[:class_namespace]) && !class_name.start_with?('::')
|
1088
|
+
class_name = "::#{namespace}::#{class_name}"
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
opts[:class_name] ||= class_name
|
996
1092
|
when Class
|
997
1093
|
opts[:class_name] ||= opts[:class].name
|
998
1094
|
end
|
999
|
-
|
1095
|
+
|
1096
|
+
opts[:class_name] ||= '::' + ((name || '').split("::")[0..-2] + [camelize(default)]).join('::')
|
1000
1097
|
end
|
1001
1098
|
|
1002
1099
|
# Module that the class includes that holds methods the class adds for column accessors and
|
@@ -33,6 +33,10 @@ module Sequel
|
|
33
33
|
# The default constraint validation metadata table name.
|
34
34
|
DEFAULT_CONSTRAINT_VALIDATIONS_TABLE = :sequel_constraint_validations
|
35
35
|
|
36
|
+
# Mapping of operator names in table to ruby operators
|
37
|
+
OPERATOR_MAP = {:str_lt => :<, :str_lte => :<=, :str_gt => :>, :str_gte => :>=,
|
38
|
+
:int_lt => :<, :int_lte => :<=, :int_gt => :>, :int_gte => :>=}.freeze
|
39
|
+
|
36
40
|
# Automatically load the validation_helpers plugin to run the actual validations.
|
37
41
|
def self.apply(model, opts=OPTS)
|
38
42
|
model.instance_eval do
|
@@ -154,6 +158,10 @@ module Sequel
|
|
154
158
|
when :includes_int_range
|
155
159
|
arg = constraint_validation_int_range(arg)
|
156
160
|
type = :includes
|
161
|
+
when *OPERATOR_MAP.keys
|
162
|
+
arg = arg.to_i if type.to_s =~ /\Aint_/
|
163
|
+
operator = OPERATOR_MAP[type]
|
164
|
+
type = :operator
|
157
165
|
end
|
158
166
|
|
159
167
|
column = if type == :unique
|
@@ -163,16 +171,22 @@ module Sequel
|
|
163
171
|
end
|
164
172
|
|
165
173
|
if type_opts = @constraint_validation_options[type]
|
166
|
-
opts
|
174
|
+
opts.merge!(type_opts)
|
167
175
|
end
|
168
176
|
|
169
|
-
reflection_opts = opts
|
177
|
+
reflection_opts = opts.dup
|
170
178
|
a = [:"validates_#{type}"]
|
171
179
|
|
180
|
+
if operator
|
181
|
+
a << operator
|
182
|
+
reflection_opts[:operator] = operator
|
183
|
+
end
|
184
|
+
|
172
185
|
if arg
|
173
186
|
a << arg
|
174
|
-
reflection_opts =
|
187
|
+
reflection_opts[:argument] = arg
|
175
188
|
end
|
189
|
+
|
176
190
|
a << column
|
177
191
|
unless opts.empty?
|
178
192
|
a << opts
|
@@ -159,7 +159,7 @@ module Sequel
|
|
159
159
|
# Check attribute value(s) against a specified value and operation, e.g.
|
160
160
|
# validates_operator(:>, 3, :value) validates that value > 3.
|
161
161
|
def validates_operator(operator, rhs, atts, opts=OPTS)
|
162
|
-
validatable_attributes_for_type(:operator, atts, opts){|a,v,m| validation_error_message(m, operator, rhs)
|
162
|
+
validatable_attributes_for_type(:operator, atts, opts){|a,v,m| validation_error_message(m, operator, rhs) if v.nil? || !v.send(operator, rhs)}
|
163
163
|
end
|
164
164
|
|
165
165
|
# Validates for all of the model columns (or just the given columns)
|
data/lib/sequel/sql.rb
CHANGED
@@ -1333,6 +1333,14 @@ module Sequel
|
|
1333
1333
|
with_opts(:lateral=>true)
|
1334
1334
|
end
|
1335
1335
|
|
1336
|
+
# Return a new function where the function will be ordered. Only useful for aggregate
|
1337
|
+
# functions that are order dependent.
|
1338
|
+
#
|
1339
|
+
# Sequel.function(:foo, :a).order(:a, Sequel.desc(:b)) # foo(a ORDER BY a, b DESC)
|
1340
|
+
def order(*args)
|
1341
|
+
with_opts(:order=>args)
|
1342
|
+
end
|
1343
|
+
|
1336
1344
|
# Return a new function with an OVER clause (making it a window function).
|
1337
1345
|
#
|
1338
1346
|
# Sequel.function(:row_number).over(:partition=>:col) # row_number() OVER (PARTITION BY col)
|
data/lib/sequel/version.rb
CHANGED
@@ -5,7 +5,7 @@ module Sequel
|
|
5
5
|
MAJOR = 4
|
6
6
|
# The minor version of Sequel. Bumped for every non-patch level
|
7
7
|
# release, generally around once a month.
|
8
|
-
MINOR =
|
8
|
+
MINOR = 36
|
9
9
|
# The tiny version of Sequel. Usually 0, only bumped for bugfix
|
10
10
|
# releases that fix regressions from previous versions.
|
11
11
|
TINY = 0
|
@@ -243,6 +243,10 @@ describe "A PostgreSQL database" do
|
|
243
243
|
@db.values([[1, 2], [3, 4]]).map([:column1, :column2]).must_equal [[1, 2], [3, 4]]
|
244
244
|
end
|
245
245
|
|
246
|
+
it "should support ordering in aggregate functions" do
|
247
|
+
@db.from(@db.values([['1'], ['2']]).as(:t, [:a])).get{string_agg(:a, '-').order(Sequel.desc(:a)).as(:c)}.must_equal '2-1'
|
248
|
+
end if DB.server_version >= 90000
|
249
|
+
|
246
250
|
it "should support ordering and limiting with #values" do
|
247
251
|
@db.values([[1, 2], [3, 4]]).reverse(:column2, :column1).limit(1).map([:column1, :column2]).must_equal [[3, 4]]
|
248
252
|
@db.values([[1, 2], [3, 4]]).reverse(:column2, :column1).offset(1).map([:column1, :column2]).must_equal [[1, 2]]
|
data/spec/core/dataset_spec.rb
CHANGED
@@ -3884,6 +3884,10 @@ describe "Sequel::Dataset#qualify" do
|
|
3884
3884
|
@ds.select{sum(:a).over(:partition=>:b, :order=>:c)}.qualify.sql.must_equal 'SELECT sum(t.a) OVER (PARTITION BY t.b ORDER BY t.c) FROM t'
|
3885
3885
|
end
|
3886
3886
|
|
3887
|
+
it "should handle SQL::Functions with orders" do
|
3888
|
+
@ds.select{sum(:a).order(:a)}.qualify.sql.must_equal 'SELECT sum(t.a ORDER BY t.a) FROM t'
|
3889
|
+
end
|
3890
|
+
|
3887
3891
|
it "should handle SQL::DelayedEvaluation" do
|
3888
3892
|
t = :a
|
3889
3893
|
ds = @ds.filter(Sequel.delay{t}).qualify
|
@@ -557,6 +557,10 @@ describe Sequel::SQL::VirtualRow do
|
|
557
557
|
@d.l{count(:over, :* =>true, :partition=>a, :order=>b, :window=>:win, :frame=>:rows){}}.must_equal 'count(*) OVER ("win" PARTITION BY "a" ORDER BY "b" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)'
|
558
558
|
end
|
559
559
|
|
560
|
+
it "should support order method on functions to specify orders for aggregate functions" do
|
561
|
+
@d.l{rank(:c).order(:a, :b)}.must_equal 'rank("c" ORDER BY "a", "b")'
|
562
|
+
end
|
563
|
+
|
560
564
|
it "should support over method on functions to create window functions" do
|
561
565
|
@d.l{rank{}.over}.must_equal 'rank() OVER ()'
|
562
566
|
@d.l{sum(c).over(:partition=>a, :order=>b, :window=>:win, :frame=>:rows)}.must_equal 'sum("c") OVER ("win" PARTITION BY "a" ORDER BY "b" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)'
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
|
2
|
+
|
3
|
+
connection_expiration_specs = shared_description do
|
4
|
+
describe "connection expiration" do
|
5
|
+
before do
|
6
|
+
@db.extend(Module.new do
|
7
|
+
def disconnect_connection(conn)
|
8
|
+
@sqls << 'disconnect'
|
9
|
+
end
|
10
|
+
end)
|
11
|
+
@db.extension(:connection_expiration)
|
12
|
+
@db.pool.connection_expiration_timeout = 2
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should still allow new connections" do
|
16
|
+
@db.synchronize{|c| c}.must_be_kind_of(Sequel::Mock::Connection)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should not override connection_expiration_timeout when loading extension" do
|
20
|
+
@db.extension(:connection_expiration)
|
21
|
+
@db.pool.connection_expiration_timeout.must_equal 2
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should only expire if older than timeout" do
|
25
|
+
c1 = @db.synchronize{|c| c}
|
26
|
+
@db.sqls.must_equal []
|
27
|
+
@db.synchronize{|c| c}.must_be_same_as(c1)
|
28
|
+
@db.sqls.must_equal []
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should disconnect connection if expired" do
|
32
|
+
c1 = @db.synchronize{|c| c}
|
33
|
+
@db.sqls.must_equal []
|
34
|
+
simulate_sleep(c1)
|
35
|
+
c2 = @db.synchronize{|c| c}
|
36
|
+
@db.sqls.must_equal ['disconnect']
|
37
|
+
c2.wont_be_same_as(c1)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should disconnect only expired connections among multiple" do
|
41
|
+
c1, c2 = multiple_connections
|
42
|
+
|
43
|
+
# Expire c1 only.
|
44
|
+
simulate_sleep(c1)
|
45
|
+
simulate_sleep(c2, 1)
|
46
|
+
c1, c2 = multiple_connections
|
47
|
+
|
48
|
+
c3 = @db.synchronize{|c| c}
|
49
|
+
@db.sqls.must_equal ['disconnect']
|
50
|
+
c3.wont_be_same_as(c1)
|
51
|
+
c3.must_be_same_as(c2)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should disconnect connections repeatedly if they are expired" do
|
55
|
+
c1, c2 = multiple_connections
|
56
|
+
|
57
|
+
simulate_sleep(c1)
|
58
|
+
simulate_sleep(c2)
|
59
|
+
|
60
|
+
c3 = @db.synchronize{|c| c}
|
61
|
+
@db.sqls.must_equal ['disconnect', 'disconnect']
|
62
|
+
c3.wont_be_same_as(c1)
|
63
|
+
c3.wont_be_same_as(c2)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should not leak connection references to expiring connections" do
|
67
|
+
c1 = @db.synchronize{|c| c}
|
68
|
+
simulate_sleep(c1)
|
69
|
+
c2 = @db.synchronize{|c| c}
|
70
|
+
c2.wont_be_same_as(c1)
|
71
|
+
@db.pool.instance_variable_get(:@connection_expiration_timestamps).must_include(c2)
|
72
|
+
@db.pool.instance_variable_get(:@connection_expiration_timestamps).wont_include(c1)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should not leak connection references during disconnect" do
|
76
|
+
c1, c2 = multiple_connections
|
77
|
+
@db.pool.instance_variable_get(:@connection_expiration_timestamps).size.must_equal 2
|
78
|
+
@db.disconnect
|
79
|
+
@db.pool.instance_variable_get(:@connection_expiration_timestamps).size.must_equal 0
|
80
|
+
end
|
81
|
+
|
82
|
+
def multiple_connections
|
83
|
+
q, q1 = Queue.new, Queue.new
|
84
|
+
c1 = nil
|
85
|
+
c2 = nil
|
86
|
+
@db.synchronize do |c|
|
87
|
+
Thread.new do
|
88
|
+
@db.synchronize do |cc|
|
89
|
+
c2 = cc
|
90
|
+
end
|
91
|
+
q1.pop
|
92
|
+
q.push nil
|
93
|
+
end
|
94
|
+
q1.push nil
|
95
|
+
q.pop
|
96
|
+
c1 = c
|
97
|
+
end
|
98
|
+
[c1, c2]
|
99
|
+
end
|
100
|
+
|
101
|
+
# Set the timestamp back in time to simulate sleep / passage of time.
|
102
|
+
def simulate_sleep(conn, sleep_time = 3)
|
103
|
+
timestamps = @db.pool.instance_variable_get(:@connection_expiration_timestamps)
|
104
|
+
timestamps[conn] -= sleep_time
|
105
|
+
@db.pool.instance_variable_set(:@connection_expiration_timestamps, timestamps)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "Sequel::ConnectionExpiration with threaded pool" do
|
111
|
+
before do
|
112
|
+
@db = Sequel.mock
|
113
|
+
end
|
114
|
+
include connection_expiration_specs
|
115
|
+
end
|
116
|
+
describe "Sequel::ConnectionExpiration with sharded threaded pool" do
|
117
|
+
before do
|
118
|
+
@db = Sequel.mock(:servers=>{})
|
119
|
+
end
|
120
|
+
include connection_expiration_specs
|
121
|
+
end
|