sequel 4.35.0 → 4.36.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|