sequel 5.62.0 → 5.63.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df5fea5cf2d6dae7a5721c549c89a190df5a1cf79f2e7d48921dcdaeae398839
4
- data.tar.gz: e1155c60bb4c2845a96cddf0a7d5498358f1cba826fdc9d7f00754a017b4310f
3
+ metadata.gz: abbe367cded9066a40aee5dd911a9b915f580264d00e6b668fad956bb94e38ef
4
+ data.tar.gz: 40ab8f35b0129177b550469f5ce9b6b6fccbcc97bdcfb3b06a2d8528bca37432
5
5
  SHA512:
6
- metadata.gz: 8e52498bbcc1c2d472592b213be8e6e508f5881de5c1ae0579b426211aa448960e06bdb297f43eb26ee3024923f78e54bb17f9a886710c3ffe60f0c134746029
7
- data.tar.gz: 0d5ae732cde0ad90a4c331f40628628e346386c483267173eb50eaad067d7de14ba6ff3c83f598d7440a6fcc752e4439cf1856706489cc3d3b7cffaf5b6d8de8
6
+ metadata.gz: 87a17fc09f790b81df75fb7d170f851c9607a0f45abf6fc50c21f50243e418362fe7c804521bfe06a12e32656e234c7e79469d40d365436edad0a2e9c176a335
7
+ data.tar.gz: ee366f061a808d7983c2996dd969f834b74815e2a1ed77410f0bdc32f822dad72dee3c48a9ad22b6f77d791d8dc43de8bc0b4619929ac5870976495774959a7b
data/CHANGELOG CHANGED
@@ -1,3 +1,17 @@
1
+ === 5.63.0 (2022-12-01)
2
+
3
+ * Make validates_associated plugin avoid database type errors for non-integer association keys (jeremyevans) (#1968)
4
+
5
+ * Make tactical_eager_loading plugin work better with table inheritance plugins (rolftimmermans, jeremyevans) (#1962)
6
+
7
+ * Add support for pool_class: :timed_queue on Ruby 3.2+, using a Queue for available connections (jeremyevans)
8
+
9
+ * Allow :pool_class Database option to be specified as a string to more easily choose a different pool type (jeremyevans)
10
+
11
+ * Use compare_by_identity hashes for Thread-keyed hashes in threaded connection pools (jeremyevans)
12
+
13
+ * Skip use of JRuby workaround on JRuby 9.3.9.0+ in named_timezones extension as JRuby fixed the related bug (jeremyevans)
14
+
1
15
  === 5.62.0 (2022-11-01)
2
16
 
3
17
  * Add back the pg_auto_parameterize extension for automatically using bound variables when using postgres adapter with pg driver (jeremyevans)
@@ -0,0 +1,33 @@
1
+ = New Features
2
+
3
+ * On Ruby 3.2, the pool_class: :timed_queue Database option can now
4
+ be used to use an alternative connection pool that stores
5
+ connections in a queue, and uses the new Queue#pop :timeout option
6
+ in Ruby 3.2 to implement the pool timeout. This new connection
7
+ pool is simpler than the default connection pool. It is not yet
8
+ the default connection pool on Ruby 3.2, but it may become the
9
+ default in a later version. Users of Ruby 3.2 are encouraged to
10
+ try out the pool_class: :timed_queue Database option and provide
11
+ feedback on how it works in their application.
12
+
13
+ = Other Improvements
14
+
15
+ * The tactical_eager_loading plugin now works in combination with the
16
+ single_table_inheritance and class_table_inheritance plugins, when
17
+ loading an association only defined in a specific subclass.
18
+ Previously, eager loading would be skipped in such a case. Now,
19
+ an eager load will be attempted for all instances supporting the
20
+ association.
21
+
22
+ * The validate_associated plugin now avoids database type errors for
23
+ non-integer association keys. In cases where the associated object
24
+ doesn't have a value for the associated key, and the current object
25
+ does not have a key value that can be set in the associated object,
26
+ validation errors in the associated object related to the associated
27
+ key will be ignored.
28
+
29
+ * Thread-keyed connection pool hashes now use compare_by_identity for
30
+ better performance.
31
+
32
+ * The JRuby workaround in the named_timezones extension is no longer
33
+ used on JRuby 9.3.9.0+, as JRuby fixed the related bug.
@@ -22,6 +22,8 @@ class Sequel::ShardedThreadedConnectionPool < Sequel::ThreadedConnectionPool
22
22
  @connections_to_disconnect = []
23
23
  @servers = opts.fetch(:servers_hash, Hash.new(:default))
24
24
  remove_instance_variable(:@waiter)
25
+ remove_instance_variable(:@allocated)
26
+ @allocated = {}
25
27
  @waiters = {}
26
28
 
27
29
  add_servers([:default])
@@ -36,7 +38,9 @@ class Sequel::ShardedThreadedConnectionPool < Sequel::ThreadedConnectionPool
36
38
  unless @servers.has_key?(server)
37
39
  @servers[server] = server
38
40
  @available_connections[server] = []
39
- @allocated[server] = {}
41
+ allocated = {}
42
+ allocated.compare_by_identity
43
+ @allocated[server] = allocated
40
44
  @waiters[server] = ConditionVariable.new
41
45
  end
42
46
  end
@@ -3,7 +3,7 @@
3
3
  # A connection pool allowing multi-threaded access to a pool of connections.
4
4
  # This is the default connection pool used by Sequel.
5
5
  class Sequel::ThreadedConnectionPool < Sequel::ConnectionPool
6
- USE_WAITER = true
6
+ USE_WAITER = true # SEQUEL6: Remove
7
7
  Sequel::Deprecation.deprecate_constant(self, :USE_WAITER)
8
8
 
9
9
  # The maximum number of connections this pool will create (per shard/server
@@ -12,17 +12,17 @@ class Sequel::ThreadedConnectionPool < Sequel::ConnectionPool
12
12
 
13
13
  # An array of connections that are available for use by the pool.
14
14
  # The calling code should already have the mutex before calling this.
15
- attr_reader :available_connections
15
+ attr_reader :available_connections # SEQUEL6: Remove
16
16
 
17
- # A hash with thread keys and connection values for currently allocated connections.
17
+ # A hash with thread/fiber keys and connection values for currently allocated connections.
18
18
  # The calling code should already have the mutex before calling this.
19
- attr_reader :allocated
19
+ attr_reader :allocated # SEQUEL6: Remove
20
20
 
21
21
  # The following additional options are respected:
22
22
  # :max_connections :: The maximum number of connections the connection pool
23
23
  # will open (default 4)
24
24
  # :pool_timeout :: The amount of seconds to wait to acquire a connection
25
- # before raising a PoolTimeoutError (default 5)
25
+ # before raising a PoolTimeout error (default 5)
26
26
  def initialize(db, opts = OPTS)
27
27
  super
28
28
  @max_size = Integer(opts[:max_connections] || 4)
@@ -31,6 +31,7 @@ class Sequel::ThreadedConnectionPool < Sequel::ConnectionPool
31
31
  @connection_handling = opts[:connection_handling]
32
32
  @available_connections = []
33
33
  @allocated = {}
34
+ @allocated.compare_by_identity
34
35
  @timeout = Float(opts[:pool_timeout] || 5)
35
36
  @waiter = ConditionVariable.new
36
37
  end
@@ -49,8 +50,7 @@ class Sequel::ThreadedConnectionPool < Sequel::ConnectionPool
49
50
  end
50
51
  end
51
52
 
52
- # Removes all connections currently available, optionally
53
- # yielding each connection to the given block. This method has the effect of
53
+ # Removes all connections currently available. This method has the effect of
54
54
  # disconnecting from the database, assuming that no connections are currently
55
55
  # being used. If you want to be able to disconnect connections that are
56
56
  # currently in use, use the ShardedThreadedConnectionPool, which can do that.
@@ -134,7 +134,7 @@ class Sequel::ThreadedConnectionPool < Sequel::ConnectionPool
134
134
  # calling this.
135
135
  #
136
136
  # This should return a connection is one is available within the timeout,
137
- # or nil if a connection could not be acquired within the timeout.
137
+ # or raise PoolTimeout if a connection could not be acquired within the timeout.
138
138
  def acquire(thread)
139
139
  if conn = assign_connection(thread)
140
140
  return conn
@@ -0,0 +1,257 @@
1
+ # frozen-string-literal: true
2
+
3
+ # :nocov:
4
+ raise LoadError, "Sequel::TimedQueueConnectionPool is only available on Ruby 3.2+" unless RUBY_VERSION >= '3.2'
5
+ # :nocov:
6
+
7
+ # A connection pool allowing multi-threaded access to a pool of connections,
8
+ # using a timed queue (only available in Ruby 3.2+).
9
+ class Sequel::TimedQueueConnectionPool < Sequel::ConnectionPool
10
+ # The maximum number of connections this pool will create.
11
+ attr_reader :max_size
12
+
13
+ # The following additional options are respected:
14
+ # :max_connections :: The maximum number of connections the connection pool
15
+ # will open (default 4)
16
+ # :pool_timeout :: The amount of seconds to wait to acquire a connection
17
+ # before raising a PoolTimeout (default 5)
18
+ def initialize(db, opts = OPTS)
19
+ super
20
+ @max_size = Integer(opts[:max_connections] || 4)
21
+ raise(Sequel::Error, ':max_connections must be positive') if @max_size < 1
22
+ @mutex = Mutex.new
23
+ # Size inside array so this still works while the pool is frozen.
24
+ @size = [0]
25
+ @allocated = {}
26
+ @allocated.compare_by_identity
27
+ @timeout = Float(opts[:pool_timeout] || 5)
28
+ @queue = Queue.new
29
+ end
30
+
31
+ # Yield all of the available connections, and the one currently allocated to
32
+ # this thread. This will not yield connections currently allocated to other
33
+ # threads, as it is not safe to operate on them.
34
+ def all_connections
35
+ hold do |conn|
36
+ yield conn
37
+
38
+ # Use a hash to record all connections already seen. As soon as we
39
+ # come across a connection we've already seen, we stop the loop.
40
+ conns = {}
41
+ conns.compare_by_identity
42
+ while true
43
+ conn = nil
44
+ begin
45
+ break unless (conn = @queue.pop(timeout: 0)) && !conns[conn]
46
+ conns[conn] = true
47
+ yield conn
48
+ ensure
49
+ @queue.push(conn) if conn
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ # Removes all connections currently in the pool's queue. This method has the effect of
56
+ # disconnecting from the database, assuming that no connections are currently
57
+ # being used.
58
+ #
59
+ # Once a connection is requested using #hold, the connection pool
60
+ # creates new connections to the database.
61
+ def disconnect(opts=OPTS)
62
+ while conn = @queue.pop(timeout: 0)
63
+ disconnect_connection(conn)
64
+ end
65
+ fill_queue
66
+ nil
67
+ end
68
+
69
+ # Chooses the first available connection, or if none are
70
+ # available, creates a new connection. Passes the connection to the supplied
71
+ # block:
72
+ #
73
+ # pool.hold {|conn| conn.execute('DROP TABLE posts')}
74
+ #
75
+ # Pool#hold is re-entrant, meaning it can be called recursively in
76
+ # the same thread without blocking.
77
+ #
78
+ # If no connection is immediately available and the pool is already using the maximum
79
+ # number of connections, Pool#hold will block until a connection
80
+ # is available or the timeout expires. If the timeout expires before a
81
+ # connection can be acquired, a Sequel::PoolTimeout is raised.
82
+ def hold(server=nil)
83
+ t = Sequel.current
84
+ if conn = sync{@allocated[t]}
85
+ return yield(conn)
86
+ end
87
+
88
+ begin
89
+ conn = acquire(t)
90
+ yield conn
91
+ rescue Sequel::DatabaseDisconnectError, *@error_classes => e
92
+ if disconnect_error?(e)
93
+ oconn = conn
94
+ conn = nil
95
+ disconnect_connection(oconn) if oconn
96
+ sync{@allocated.delete(t)}
97
+ fill_queue
98
+ end
99
+ raise
100
+ ensure
101
+ release(t) if conn
102
+ end
103
+ end
104
+
105
+ def pool_type
106
+ :timed_queue
107
+ end
108
+
109
+ # The total number of connections in the pool.
110
+ def size
111
+ sync{@size[0]}
112
+ end
113
+
114
+ private
115
+
116
+ # Create a new connection, after the pool's current size has already
117
+ # been updated to account for the new connection. If there is an exception
118
+ # when creating the connection, decrement the current size.
119
+ #
120
+ # This should only be called after can_make_new?. If there is an exception
121
+ # between when can_make_new? is called and when preallocated_make_new
122
+ # is called, it has the effect of reducing the maximum size of the
123
+ # connection pool by 1, since the current size of the pool will show a
124
+ # higher number than the number of connections allocated or
125
+ # in the queue.
126
+ #
127
+ # Calling code should not have the mutex when calling this.
128
+ def preallocated_make_new
129
+ make_new(:default)
130
+ rescue Exception
131
+ sync{@size[0] -= 1}
132
+ raise
133
+ end
134
+
135
+ # Decrement the current size of the pool when disconnecting connections.
136
+ #
137
+ # Calling code should not have the mutex when calling this.
138
+ def disconnect_connection(conn)
139
+ sync{@size[0] -= 1}
140
+ super
141
+ end
142
+
143
+ # If there are any threads waiting on the queue, try to create
144
+ # new connections in a separate thread if the pool is not yet at the
145
+ # maximum size.
146
+ #
147
+ # The reason for this method is to handle cases where acquire
148
+ # could not retrieve a connection immediately, and the pool
149
+ # was already at the maximum size. In that case, the acquire will
150
+ # wait on the queue until the timeout. This method is called
151
+ # after disconnecting to potentially add new connections to the
152
+ # pool, so the threads that are currently waiting for connections
153
+ # do not timeout after the pool is no longer full.
154
+ def fill_queue
155
+ if @queue.num_waiting > 0
156
+ Thread.new do
157
+ while @queue.num_waiting > 0 && (conn = try_make_new)
158
+ @queue.push(conn)
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ # Whether the given size is less than the maximum size of the pool.
165
+ # In that case, the pool's current size is incremented. If this
166
+ # method returns true, space in the pool for the connection is
167
+ # preallocated, and preallocated_make_new should be called to
168
+ # create the connection.
169
+ #
170
+ # Calling code should have the mutex when calling this.
171
+ def can_make_new?(current_size)
172
+ if @max_size > current_size
173
+ @size[0] += 1
174
+ end
175
+ end
176
+
177
+ # Try to make a new connection if there is space in the pool.
178
+ # If the pool is already full, look for dead threads/fibers and
179
+ # disconnect the related connections.
180
+ #
181
+ # Calling code should not have the mutex when calling this.
182
+ def try_make_new
183
+ return preallocated_make_new if sync{can_make_new?(@size[0])}
184
+
185
+ to_disconnect = nil
186
+ do_make_new = false
187
+
188
+ sync do
189
+ current_size = @size[0]
190
+ @allocated.keys.each do |t|
191
+ unless t.alive?
192
+ (to_disconnect ||= []) << @allocated.delete(t)
193
+ current_size -= 1
194
+ end
195
+ end
196
+
197
+ do_make_new = true if can_make_new?(current_size)
198
+ end
199
+
200
+ begin
201
+ preallocated_make_new if do_make_new
202
+ ensure
203
+ if to_disconnect
204
+ to_disconnect.each{|conn| disconnect_connection(conn)}
205
+ fill_queue
206
+ end
207
+ end
208
+ end
209
+
210
+ # Assigns a connection to the supplied thread, if one
211
+ # is available.
212
+ #
213
+ # This should return a connection is one is available within the timeout,
214
+ # or raise PoolTimeout if a connection could not be acquired within the timeout.
215
+ #
216
+ # Calling code should not have the mutex when calling this.
217
+ def acquire(thread)
218
+ if conn = @queue.pop(timeout: 0) || try_make_new || @queue.pop(timeout: @timeout)
219
+ sync{@allocated[thread] = conn}
220
+ else
221
+ name = db.opts[:name]
222
+ raise ::Sequel::PoolTimeout, "timeout: #{@timeout}#{", database name: #{name}" if name}"
223
+ end
224
+ end
225
+
226
+ # Create the maximum number of connections immediately. This should not be called
227
+ # with a true argument unles no code is currently operating on the database.
228
+ #
229
+ # Calling code should not have the mutex when calling this.
230
+ def preconnect(concurrent = false)
231
+ if concurrent
232
+ if times = sync{@max_size > (size = @size[0]) ? @max_size - size : false}
233
+ times.times.map{Thread.new{if conn = try_make_new; @queue.push(conn) end}}.map(&:value)
234
+ end
235
+ else
236
+ while conn = try_make_new
237
+ @queue.push(conn)
238
+ end
239
+ end
240
+
241
+ nil
242
+ end
243
+
244
+ # Releases the connection assigned to the supplied thread back to the pool.
245
+ #
246
+ # Calling code should not have the mutex when calling this.
247
+ def release(thread)
248
+ @queue.push(sync{@allocated.delete(thread)})
249
+ end
250
+
251
+ # Yield to the block while inside the mutex.
252
+ #
253
+ # Calling code should not have the mutex when calling this.
254
+ def sync
255
+ @mutex.synchronize{yield}
256
+ end
257
+ end
@@ -30,8 +30,11 @@ class Sequel::ConnectionPool
30
30
  :threaded => :ThreadedConnectionPool,
31
31
  :single => :SingleConnectionPool,
32
32
  :sharded_threaded => :ShardedThreadedConnectionPool,
33
- :sharded_single => :ShardedSingleConnectionPool
34
- }.freeze
33
+ :sharded_single => :ShardedSingleConnectionPool,
34
+ :timed_queue => :TimedQueueConnectionPool,
35
+ }
36
+ POOL_CLASS_MAP.to_a.each{|k, v| POOL_CLASS_MAP[k.to_s] = v}
37
+ POOL_CLASS_MAP.freeze
35
38
 
36
39
  # Class methods used to return an appropriate pool subclass, separated
37
40
  # into a module for easier overridding by extensions.
@@ -127,6 +127,18 @@ module Sequel
127
127
  #
128
128
  # DB[:table].delete # DELETE * FROM table
129
129
  # # => 3
130
+ #
131
+ # Some databases support using multiple tables in a DELETE query. This requires
132
+ # multiple FROM tables (JOINs can also be used). As multiple FROM tables use
133
+ # an implicit CROSS JOIN, you should make sure your WHERE condition uses the
134
+ # appropriate filters for the FROM tables:
135
+ #
136
+ # DB.from(:a, :b).join(:c, :d=>Sequel[:b][:e]).where{{a[:f]=>b[:g], a[:id]=>c[:h]}}.
137
+ # delete
138
+ # # DELETE FROM a
139
+ # # USING b
140
+ # # INNER JOIN c ON (c.d = b.e)
141
+ # # WHERE ((a.f = b.g) AND (a.id = c.h))
130
142
  def delete(&block)
131
143
  sql = delete_sql
132
144
  if uses_returning?(:delete)
@@ -926,6 +938,19 @@ module Sequel
926
938
  #
927
939
  # DB[:table].update(x: Sequel[:x]+1, y: 0) # UPDATE table SET x = (x + 1), y = 0
928
940
  # # => 10
941
+ #
942
+ # Some databases support using multiple tables in an UPDATE query. This requires
943
+ # multiple FROM tables (JOINs can also be used). As multiple FROM tables use
944
+ # an implicit CROSS JOIN, you should make sure your WHERE condition uses the
945
+ # appropriate filters for the FROM tables:
946
+ #
947
+ # DB.from(:a, :b).join(:c, :d=>Sequel[:b][:e]).where{{a[:f]=>b[:g], a[:id]=>10}}.
948
+ # update(:f=>Sequel[:c][:h])
949
+ # # UPDATE a
950
+ # # SET f = c.h
951
+ # # FROM b
952
+ # # INNER JOIN c ON (c.d = b.e)
953
+ # # WHERE ((a.f = b.g) AND (a.id = 10))
929
954
  def update(values=OPTS, &block)
930
955
  sql = update_sql(values)
931
956
  if uses_returning?(:update)
@@ -5,7 +5,7 @@
5
5
  # code
6
6
  #
7
7
  # DB.extension :async_thread_pool
8
- # foos = DB[:foos].async.where{name: 'A'..'M'}.all
8
+ # foos = DB[:foos].async.where(name: 'A'..'M').all
9
9
  # bar_names = DB[:bar].async.select_order_map(:name)
10
10
  # baz_1 = DB[:bazes].async.first(id: 1)
11
11
  #
@@ -15,7 +15,7 @@
15
15
  # of calling that method on the result of the query method. For example,
16
16
  # if you run:
17
17
  #
18
- # foos = DB[:foos].async.where{name: 'A'..'M'}.all
18
+ # foos = DB[:foos].async.where(name: 'A'..'M').all
19
19
  # bar_names = DB[:bars].async.select_order_map(:name)
20
20
  # baz_1 = DB[:bazes].async.first(id: 1)
21
21
  # sleep(1)
@@ -26,7 +26,7 @@
26
26
  # These three queries will generally be run concurrently in separate
27
27
  # threads. If you instead run:
28
28
  #
29
- # DB[:foos].async.where{name: 'A'..'M'}.all.size
29
+ # DB[:foos].async.where(name: 'A'..'M').all.size
30
30
  # DB[:bars].async.select_order_map(:name).first
31
31
  # DB[:bazes].async.first(id: 1).name
32
32
  #
@@ -37,7 +37,7 @@
37
37
  # What is run in the separate thread is the entire method call that
38
38
  # returns results. So with the original example:
39
39
  #
40
- # foos = DB[:foos].async.where{name: 'A'..'M'}.all
40
+ # foos = DB[:foos].async.where(name: 'A'..'M').all
41
41
  # bar_names = DB[:bars].async.select_order_map(:name)
42
42
  # baz_1 = DB[:bazes].async.first(id: 1)
43
43
  #
@@ -156,7 +156,7 @@
156
156
  # so that the query will run in the current thread instead of waiting
157
157
  # for an async thread to become available. With the following code:
158
158
  #
159
- # foos = DB[:foos].async.where{name: 'A'..'M'}.all
159
+ # foos = DB[:foos].async.where(name: 'A'..'M').all
160
160
  # bar_names = DB[:bar].async.select_order_map(:name)
161
161
  # if foos.length > 4
162
162
  # baz_1 = DB[:bazes].async.first(id: 1)
@@ -68,6 +68,10 @@ module Sequel
68
68
  private
69
69
 
70
70
  if RUBY_VERSION >= '2.6'
71
+ # Whether Time.at with :nsec and :in is broken. True on JRuby < 9.3.9.0.
72
+ BROKEN_TIME_AT_WITH_NSEC = defined?(JRUBY_VERSION) && (JRUBY_VERSION < '9.3' || (JRUBY_VERSION < '9.4' && JRUBY_VERSION.split('.')[2].to_i < 9))
73
+ private_constant :BROKEN_TIME_AT_WITH_NSEC
74
+
71
75
  # Convert the given input Time (which must be in UTC) to the given input timezone,
72
76
  # which should be a TZInfo::Timezone instance.
73
77
  def convert_input_time_other(v, input_timezone)
@@ -77,7 +81,7 @@ module Sequel
77
81
  period = input_timezone.period_for_local(v, &disamb)
78
82
  offset = period.utc_total_offset
79
83
  # :nocov:
80
- if defined?(JRUBY_VERSION)
84
+ if BROKEN_TIME_AT_WITH_NSEC
81
85
  Time.at(v.to_i - offset, :in => input_timezone) + v.nsec/1000000000.0
82
86
  # :nocov:
83
87
  else
@@ -89,7 +93,7 @@ module Sequel
89
93
  # which should be a TZInfo::Timezone instance.
90
94
  def convert_output_time_other(v, output_timezone)
91
95
  # :nocov:
92
- if defined?(JRUBY_VERSION)
96
+ if BROKEN_TIME_AT_WITH_NSEC
93
97
  Time.at(v.to_i, :in => output_timezone) + v.nsec/1000000000.0
94
98
  # :nocov:
95
99
  else
@@ -3552,11 +3552,11 @@ module Sequel
3552
3552
  end
3553
3553
 
3554
3554
  # Eagerly load all specified associations.
3555
- def eager_load(a, eager_assoc=@opts[:eager])
3555
+ def eager_load(a, eager_assoc=@opts[:eager], m=model)
3556
3556
  return if a.empty?
3557
3557
 
3558
3558
  # Reflections for all associations to eager load
3559
- reflections = eager_assoc.keys.map{|assoc| model.association_reflection(assoc) || (raise Sequel::UndefinedAssociation, "Model: #{self}, Association: #{assoc}")}
3559
+ reflections = eager_assoc.keys.map{|assoc| m.association_reflection(assoc) || (raise Sequel::UndefinedAssociation, "Model: #{self}, Association: #{assoc}")}
3560
3560
 
3561
3561
  perform_eager_loads(prepare_eager_load(a, reflections, eager_assoc))
3562
3562
 
@@ -253,6 +253,14 @@ module Sequel
253
253
 
254
254
  private
255
255
 
256
+ # Limit tactical eager loading objects to objects that support the same association.
257
+ def _filter_tactical_eager_load_objects(opts)
258
+ objects = defined?(super) ? super : retrieved_with.dup
259
+ name = opts[:name]
260
+ objects.select!{|x| x.model.association_reflections.include?(name)}
261
+ objects
262
+ end
263
+
256
264
  # Don't allow use of prepared statements.
257
265
  def use_prepared_statements_for?(type)
258
266
  false
@@ -152,23 +152,23 @@ module Sequel
152
152
  name = opts[:name]
153
153
  eager_reload = dynamic_opts[:eager_reload]
154
154
  if (!associations.include?(name) || eager_reload) && opts[:allow_eager] != false && retrieved_by && !frozen? && !dynamic_opts[:callback] && !dynamic_opts[:reload]
155
- begin
156
- objects = if eager_reload
157
- retrieved_with.reject(&:frozen?)
158
- else
159
- retrieved_with.reject{|x| x.frozen? || x.associations.include?(name)}
160
- end
161
- retrieved_by.send(:eager_load, objects, name=>dynamic_opts[:eager] || OPTS)
162
- rescue Sequel::UndefinedAssociation
163
- # This can happen if class table inheritance is used and the association
164
- # is only defined in a subclass. This particular instance can use the
165
- # association, but it can't be eagerly loaded as the parent class doesn't
166
- # have access to the association, and that's the class doing the eager loading.
167
- nil
168
- end
155
+ retrieved_by.send(:eager_load, _filter_tactical_eager_load_objects(:eager_reload=>eager_reload, :name=>name), {name=>dynamic_opts[:eager] || OPTS}, model)
169
156
  end
170
157
  super
171
158
  end
159
+
160
+ # Filter the objects used when tactical eager loading.
161
+ # By default, this removes frozen objects and objects that alreayd have the association loaded
162
+ def _filter_tactical_eager_load_objects(opts)
163
+ objects = defined?(super) ? super : retrieved_with.dup
164
+ if opts[:eager_reload]
165
+ objects.reject!(&:frozen?)
166
+ else
167
+ name = opts[:name]
168
+ objects.reject!{|x| x.frozen? || x.associations.include?(name)}
169
+ end
170
+ objects
171
+ end
172
172
  end
173
173
 
174
174
  module DatasetMethods
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Sequel
4
4
  module Plugins
5
- # The validates_associated plugin allows you to validate associated
5
+ # The validate_associated plugin allows you to validate associated
6
6
  # objects. It also offers the ability to delay the validation of
7
7
  # associated objects until the current object is validated.
8
8
  # If the associated object is invalid, validation error messages
@@ -54,22 +54,32 @@ module Sequel
54
54
  return if reflection[:validate] == false
55
55
  association = reflection[:name]
56
56
  if (reflection[:type] == :one_to_many || reflection[:type] == :one_to_one) && (key = reflection[:key]).is_a?(Symbol) && !(pk_val = obj.values[key])
57
- # There could be a presence validation on the foreign key in the associated model,
58
- # which will fail if we validate before saving the current object. If there is
59
- # no value for the foreign key, set it to the current primary key value, or a dummy
60
- # value of 0 if we haven't saved the current object.
61
57
  p_key = pk unless pk.is_a?(Array)
62
- obj.values[key] = p_key || 0
63
- key = nil if p_key
58
+ if p_key
59
+ obj.values[key] = p_key
60
+ else
61
+ ignore_key_errors = true
62
+ end
64
63
  end
65
- obj.errors.full_messages.each{|m| errors.add(association, m)} unless obj.valid?
66
- if key && !pk_val
67
- # If we used a dummy value of 0, remove it so it doesn't accidently remain.
68
- obj.values.delete(key)
64
+
65
+ unless obj.valid?
66
+ if ignore_key_errors
67
+ # Ignore errors on the key column in the associated object. This column
68
+ # will be set when saving to a presumably valid value using a column
69
+ # in the current object (which may not be available until after the current
70
+ # object is saved).
71
+ obj.errors.delete(key)
72
+ obj.errors.delete_if{|k,| Array === k && k.include?(key)}
73
+ end
74
+
75
+ obj.errors.full_messages.each do |m|
76
+ errors.add(association, m)
77
+ end
69
78
  end
79
+
80
+ nil
70
81
  end
71
82
  end
72
83
  end
73
84
  end
74
85
  end
75
-
@@ -6,7 +6,7 @@ module Sequel
6
6
 
7
7
  # The minor version of Sequel. Bumped for every non-patch level
8
8
  # release, generally around once a month.
9
- MINOR = 62
9
+ MINOR = 63
10
10
 
11
11
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
12
12
  # releases that fix regressions from previous versions.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.62.0
4
+ version: 5.63.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-01 00:00:00.000000000 Z
11
+ date: 2022-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -194,6 +194,7 @@ extra_rdoc_files:
194
194
  - doc/release_notes/5.60.0.txt
195
195
  - doc/release_notes/5.61.0.txt
196
196
  - doc/release_notes/5.62.0.txt
197
+ - doc/release_notes/5.63.0.txt
197
198
  - doc/release_notes/5.7.0.txt
198
199
  - doc/release_notes/5.8.0.txt
199
200
  - doc/release_notes/5.9.0.txt
@@ -284,6 +285,7 @@ files:
284
285
  - doc/release_notes/5.60.0.txt
285
286
  - doc/release_notes/5.61.0.txt
286
287
  - doc/release_notes/5.62.0.txt
288
+ - doc/release_notes/5.63.0.txt
287
289
  - doc/release_notes/5.7.0.txt
288
290
  - doc/release_notes/5.8.0.txt
289
291
  - doc/release_notes/5.9.0.txt
@@ -352,6 +354,7 @@ files:
352
354
  - lib/sequel/connection_pool/sharded_threaded.rb
353
355
  - lib/sequel/connection_pool/single.rb
354
356
  - lib/sequel/connection_pool/threaded.rb
357
+ - lib/sequel/connection_pool/timed_queue.rb
355
358
  - lib/sequel/core.rb
356
359
  - lib/sequel/database.rb
357
360
  - lib/sequel/database/connecting.rb
@@ -606,7 +609,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
606
609
  - !ruby/object:Gem::Version
607
610
  version: '0'
608
611
  requirements: []
609
- rubygems_version: 3.3.7
612
+ rubygems_version: 3.3.26
610
613
  signing_key:
611
614
  specification_version: 4
612
615
  summary: The Database Toolkit for Ruby