sequel 5.97.0 → 5.98.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 236cf31a3c7848a6699dc871558a387748593a83dda8a722aad2e1c6beaad2d9
4
- data.tar.gz: 974c8617fcecc74f10fab9c7fa8dd9e2f74a7a56767cf47cafd95e1108cc5f57
3
+ metadata.gz: 28641a0240d39f16a5b6f2d30986d44a8744c26e24de5e78b8e3823a409c48d2
4
+ data.tar.gz: 25435173509b5058e781f4b06b1ebf8005244e86864c5a72ac35bc739656a6b1
5
5
  SHA512:
6
- metadata.gz: 5b396689d99af2a0882358e80150dc452b96a9cd09a73a9cdace42b903dd0958d341bf67e0570bd382dd051df54dc66b2d2b3e396506518c619d08bd006ebd1b
7
- data.tar.gz: '0839e0177ccbb9d67880835b92e0b8767ea339b96375da914d0471a2cc24f25cdffbd8ada3b45ba69bdfc167a4609ed44fde1a7fd1db4c7574dff3f2efbfd80f'
6
+ metadata.gz: 11c7f62e3e296c43bef8af4de71ecaaf01e2e4d7e034910403991b91bc2075778dde0d16e899bc3ddacc1351e38da0f61f43476272e3b90c9a2aee5b09d82599
7
+ data.tar.gz: 173dfc5d3bbc19846a2d27b077d2241e6f64473f8a8f16ba052d9fd021d514036af1cf42cade4ea8974f2d920e126d4c23e824ab0e2d49a9062857ecd7e93342
@@ -62,11 +62,26 @@ module Sequel
62
62
 
63
63
  # Mimic the file:// uri, by having 2 preceding slashes specify a relative
64
64
  # path, and 3 preceding slashes specify an absolute path.
65
+ # Also support no preceding slashes to specify a relative path.
65
66
  def self.uri_to_options(uri) # :nodoc:
66
- { :database => (uri.host.nil? && uri.path == '/') ? nil : "#{uri.host}#{uri.path}" }
67
+ database = if uri.host.nil?
68
+ case uri.path
69
+ when '/'
70
+ nil
71
+ when nil
72
+ uri.opaque
73
+ else
74
+ uri.path
75
+ end
76
+ else
77
+ "#{uri.host}#{uri.path}"
78
+ end
79
+
80
+ { :database => database }
67
81
  end
68
82
  private_class_method :uri_to_options
69
83
 
84
+
70
85
  # Connect to the database. Since SQLite is a file based database,
71
86
  # the only options available are :database (to specify the database
72
87
  # name), and :timeout, to specify how long to wait for the database to
@@ -516,6 +516,7 @@ module Sequel
516
516
  end
517
517
  ensure
518
518
  conn.execute("UNLISTEN *")
519
+ true while conn.wait_for_notify(0)
519
520
  end
520
521
  end
521
522
  end
@@ -89,8 +89,22 @@ module Sequel
89
89
 
90
90
  # Mimic the file:// uri, by having 2 preceding slashes specify a relative
91
91
  # path, and 3 preceding slashes specify an absolute path.
92
+ # Also support no preceding slashes to specify a relative path.
92
93
  def self.uri_to_options(uri) # :nodoc:
93
- { :database => (uri.host.nil? && uri.path == '/') ? nil : "#{uri.host}#{uri.path}" }
94
+ database = if uri.host.nil?
95
+ case uri.path
96
+ when '/'
97
+ nil
98
+ when nil
99
+ uri.opaque
100
+ else
101
+ uri.path
102
+ end
103
+ else
104
+ "#{uri.host}#{uri.path}"
105
+ end
106
+
107
+ { :database => database }
94
108
  end
95
109
 
96
110
  private_class_method :uri_to_options
@@ -0,0 +1,191 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The pg_auto_parameterize_duplicate_query_detection extension builds on the
4
+ # pg_auto_parameterize extension, adding support for detecting duplicate
5
+ # queries inside a block that occur at the same location. This is designed
6
+ # mostly to catch duplicate query issues (e.g. N+1 queries) during testing.
7
+ #
8
+ # To detect duplicate queries inside a block of code, wrap the code with
9
+ # +detect_duplicate_queries+:
10
+ #
11
+ # DB.detect_duplicate_queries{your_code}
12
+ #
13
+ # With this approach, if the test runs code where the same query is executed
14
+ # more than once with the same call stack, a
15
+ # Sequel::Postgres::AutoParameterizeDuplicateQueryDetection::DuplicateQueries
16
+ # exception will be raised.
17
+ #
18
+ # You can pass the +:warn+ option to +detect_duplicate_queries+ to warn
19
+ # instead of raising. Note that if the block passed to +detect_duplicate_queries+
20
+ # raises, this extension will warn, and raise the original exception.
21
+ #
22
+ # For more control, you can pass the +:handler+ option to
23
+ # +detect_duplicate_queries+. If the +:handler+ option is provided, this
24
+ # extension will call the +:handler+ option with the hash of duplicate
25
+ # query information, and will not raise or warn. This can be useful in
26
+ # production environments, to record duplicate queries for later analysis.
27
+ #
28
+ # For accuracy, the entire call stack is always used as part of the hash key
29
+ # to determine whether a query is duplicate. However, you can filter the
30
+ # displayed backtrace by using the +:backtrace_filter+ option.
31
+ #
32
+ # +detect_duplicate_queries+ is concurrency aware, it uses the same approach
33
+ # that Sequel's default connection pools use. So if you are running multiple
34
+ # threads, +detect_duplicate_queries+ will only report duplicate queries for
35
+ # the current thread (or fiber if the fiber_concurrency extension is used).
36
+ #
37
+ # When testing applications, it's probably best to use this to wrap the
38
+ # application being tested. For example, testing with rack-test, if your app
39
+ # is +App+, you would want to wrap it:
40
+ #
41
+ # include Rack::Test::Methods
42
+ #
43
+ # WrappedApp = lambda do |env|
44
+ # DB.detect_duplicate_queries{App.call(env)}
45
+ # end
46
+ #
47
+ # def app
48
+ # WrappedApp
49
+ # end
50
+ #
51
+ # It is possible to use this to wrap each separate spec using an around hook,
52
+ # but that can result in false positives when using libraries that have
53
+ # implicit retrying (such as Capybara), as you can have the same call stack
54
+ # for multiple requests.
55
+ #
56
+ # Related module: Sequel::Postgres::AutoParameterizeDuplicateQueryDetection
57
+
58
+ module Sequel
59
+ module Postgres
60
+ # Enable detecting duplicate queries inside a block
61
+ module AutoParameterizeDuplicateQueryDetection
62
+ def self.extended(db)
63
+ db.instance_exec do
64
+ @duplicate_query_detection_contexts = {}
65
+ @duplicate_query_detection_mutex = Mutex.new
66
+ end
67
+ end
68
+
69
+ # Exception class raised when duplicate queries are detected.
70
+ class DuplicateQueries < Sequel::Error
71
+ # A hash of queries that were duplicate. Keys are arrays
72
+ # with 2 entries, the first being the query SQL, and the
73
+ # second being the related call stack (backtrace).
74
+ # The values are the number of query executions.
75
+ attr_reader :queries
76
+
77
+ def initialize(message, queries)
78
+ @queries = queries
79
+ super(message)
80
+ end
81
+ end
82
+
83
+ # Record each query executed so duplicates can be detected,
84
+ # if queries are being recorded.
85
+ def execute(sql, opts=OPTS, &block)
86
+ record, queries = duplicate_query_recorder_state
87
+
88
+ if record
89
+ queries[[sql.is_a?(Symbol) ? sql : sql.to_s, caller].freeze] += 1
90
+ end
91
+
92
+ super
93
+ end
94
+
95
+ # Ignore (do not record) queries inside given block. This can
96
+ # be useful in situations where you want to run your entire test suite
97
+ # with duplicate query detection, but you have duplicate queries in
98
+ # some parts of your application where it is not trivial to use a
99
+ # different approach. You can mark those specific sections with
100
+ # +ignore_duplicate_queries+, and still get duplicate query detection
101
+ # for the rest of the application.
102
+ def ignore_duplicate_queries(&block)
103
+ if state = duplicate_query_recorder_state
104
+ change_duplicate_query_recorder_state(state, false, &block)
105
+ else
106
+ # If we are not inside a detect_duplicate_queries block, there is
107
+ # no need to do anything, since we are not recording queries.
108
+ yield
109
+ end
110
+ end
111
+
112
+ # Run the duplicate query detector during the block.
113
+ # Options:
114
+ #
115
+ # :backtrace_filter :: Regexp used to filter the displayed backtrace.
116
+ # :handler :: If present, called with hash of duplicate query information,
117
+ # instead of raising or warning.
118
+ # :warn :: Always warn instead of raising for duplicate queries.
119
+ #
120
+ # Note that if you nest calls to this method, only the top
121
+ # level call will respect the passed options.
122
+ def detect_duplicate_queries(opts=OPTS, &block)
123
+ current = Sequel.current
124
+ if state = duplicate_query_recorder_state(current)
125
+ return change_duplicate_query_recorder_state(state, true, &block)
126
+ end
127
+
128
+ @duplicate_query_detection_mutex.synchronize do
129
+ @duplicate_query_detection_contexts[current] = [true, Hash.new(0)]
130
+ end
131
+
132
+ begin
133
+ yield
134
+ rescue Exception => e
135
+ raise
136
+ ensure
137
+ _, queries = @duplicate_query_detection_mutex.synchronize do
138
+ @duplicate_query_detection_contexts.delete(current)
139
+ end
140
+ queries.delete_if{|_,v| v < 2}
141
+
142
+ unless queries.empty?
143
+ if handler = opts[:handler]
144
+ handler.call(queries)
145
+ else
146
+ backtrace_filter = opts[:backtrace_filter]
147
+ backtrace_filter_note = backtrace_filter ? " (filtered)" : ""
148
+ query_info = queries.map do |k,v|
149
+ backtrace = k[1]
150
+ backtrace = backtrace.grep(backtrace_filter) if backtrace_filter
151
+ "times:#{v}\nsql:#{k[0]}\nbacktrace#{backtrace_filter_note}:\n#{backtrace.join("\n")}\n"
152
+ end
153
+ message = "duplicate queries detected:\n\n#{query_info.join("\n")}"
154
+
155
+ if e || opts[:warn]
156
+ warn(message)
157
+ else
158
+ raise DuplicateQueries.new(message, queries)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ # Get the query record state for the given context.
168
+ def duplicate_query_recorder_state(current=Sequel.current)
169
+ @duplicate_query_detection_mutex.synchronize{@duplicate_query_detection_contexts[current]}
170
+ end
171
+
172
+ # Temporarily change whether to record queries for the block, resetting the
173
+ # previous setting after the block exits.
174
+ def change_duplicate_query_recorder_state(state, setting)
175
+ prev = state[0]
176
+ state[0] = setting
177
+
178
+ begin
179
+ yield
180
+ ensure
181
+ state[0] = prev
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ Database.register_extension(:pg_auto_parameterize_duplicate_query_detection) do |db|
188
+ db.extension(:pg_auto_parameterize)
189
+ db.extend(Postgres::AutoParameterizeDuplicateQueryDetection)
190
+ end
191
+ end
@@ -302,7 +302,7 @@ module Sequel
302
302
 
303
303
  if strategy == :window_function
304
304
  delete_rn = ds.row_number_column
305
- objects.each{|obj| obj.values.delete(delete_rn)}
305
+ objects.each{|obj| obj.remove_key!(delete_rn)}
306
306
  end
307
307
  elsif strategy == :union
308
308
  objects = []
@@ -1291,11 +1291,11 @@ module Sequel
1291
1291
  name = self[:name]
1292
1292
 
1293
1293
  self[:model].eager_load_results(self, eo) do |assoc_record|
1294
- assoc_record.values.delete(delete_rn) if delete_rn
1294
+ assoc_record.remove_key!(delete_rn) if delete_rn
1295
1295
  hash_key = if uses_lcks
1296
- left_key_alias.map{|k| assoc_record.values.delete(k)}
1296
+ left_key_alias.map{|k| assoc_record.remove_key!(k)}
1297
1297
  else
1298
- assoc_record.values.delete(left_key_alias)
1298
+ assoc_record.remove_key!(left_key_alias)
1299
1299
  end
1300
1300
 
1301
1301
  objects = h[hash_key]
@@ -2387,7 +2387,7 @@ module Sequel
2387
2387
  delete_rn = opts.delete_row_number_column
2388
2388
 
2389
2389
  eager_load_results(opts, eo) do |assoc_record|
2390
- assoc_record.values.delete(delete_rn) if delete_rn
2390
+ assoc_record.remove_key!(delete_rn) if delete_rn
2391
2391
  hash_key = uses_cks ? km.map{|k| assoc_record.get_column_value(k)} : assoc_record.get_column_value(km)
2392
2392
  objects = h[hash_key]
2393
2393
  if assign_singular
@@ -2,13 +2,15 @@
2
2
 
3
3
  module Sequel
4
4
  class Model
5
+ # SEQUEL6: Remove Enumerable here, and send all Enumerable methods to dataset
6
+ # by default, with a plugin for the current behavior.
5
7
  extend Enumerable
6
8
  extend Inflections
7
9
 
8
10
  # Class methods for Sequel::Model that implement basic model functionality.
9
11
  #
10
12
  # * All of the following methods have class methods created that send the method
11
- # to the model's dataset: all, as_hash, avg, count, cross_join, distinct, each,
13
+ # to the model's dataset: all, any?, as_hash, avg, count, cross_join, distinct, each,
12
14
  # each_server, empty?, except, exclude, exclude_having, fetch_rows,
13
15
  # filter, first, first!, for_update, from, from_self, full_join, full_outer_join,
14
16
  # get, graph, grep, group, group_and_count, group_append, group_by, having, import,
@@ -695,7 +697,7 @@ module Sequel
695
697
  end
696
698
 
697
699
  # Add model methods that call dataset methods
698
- Plugins.def_dataset_methods(self, (Dataset::ACTION_METHODS + Dataset::QUERY_METHODS + [:each_server]) - [:<<, :or, :[], :columns, :columns!, :delete, :update, :set_graph_aliases, :add_graph_aliases])
700
+ Plugins.def_dataset_methods(self, (Dataset::ACTION_METHODS + Dataset::QUERY_METHODS + [:any?, :each_server]) - [:<<, :or, :[], :columns, :columns!, :delete, :update, :set_graph_aliases, :add_graph_aliases])
699
701
 
700
702
  private
701
703
 
@@ -1499,6 +1501,20 @@ END
1499
1501
  refresh
1500
1502
  end
1501
1503
 
1504
+ # Remove a key from the instances values, and return the value
1505
+ # of the key.
1506
+ #
1507
+ # a = Album[1]
1508
+ # a.values
1509
+ # # => {id: 1, artist_id: 2}
1510
+ # a.remove_key!(:artist_id)
1511
+ # # => 2
1512
+ # a.values
1513
+ # # => {id: 1}
1514
+ def remove_key!(key)
1515
+ @values.delete(key)
1516
+ end
1517
+
1502
1518
  # Creates or updates the record, after making sure the record
1503
1519
  # is valid and before hooks execute successfully. Fails if:
1504
1520
  #
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Sequel
4
4
  module Plugins
5
- # If the model's dataset selects explicit columns and the
5
+ # If the model's dataset selects explicit columns (or table.*) and the
6
6
  # database supports it, the insert_returning_select plugin will
7
7
  # automatically set the RETURNING clause on the dataset used to
8
8
  # insert rows to the columns selected, which allows the default model
@@ -45,6 +45,9 @@ module Sequel
45
45
  ret
46
46
  end
47
47
 
48
+ RETURN_ALL = [Sequel::Dataset::WILDCARD].freeze
49
+ private_constant :RETURN_ALL
50
+
48
51
  # Determine the columns to use for the returning clause, or return nil
49
52
  # if they can't be determined and a returning clause should not be
50
53
  # added automatically.
@@ -52,6 +55,12 @@ module Sequel
52
55
  return unless ds.supports_returning?(:insert)
53
56
  return unless values = ds.opts[:select]
54
57
 
58
+ # SELECT table.* -> RETURNING *
59
+ if values.length == 1 && values[0].is_a?(Sequel::SQL::ColumnAll)
60
+ return RETURN_ALL
61
+ end
62
+
63
+ # SELECT column1, table.column2, ... -> RETURNING column1, column2, ...
55
64
  values = values.map{|v| ds.unqualified_column_for(v)}
56
65
  if values.all?
57
66
  values
@@ -111,7 +111,7 @@ module Sequel
111
111
  ancestor_base_case_columns = prkey_array.zip(key_aliases).map{|k, ka_| SQL::AliasedExpression.new(k, ka_)} + c_all
112
112
  descendant_base_case_columns = key_array.zip(key_aliases).map{|k, ka_| SQL::AliasedExpression.new(k, ka_)} + c_all
113
113
  recursive_case_columns = prkey_array.zip(key_aliases).map{|k, ka_| SQL::QualifiedIdentifier.new(t, ka_)} + c_all
114
- extract_key_alias = lambda{|m| key_aliases.map{|ka_| bd_conv[m.values.delete(ka_)]}}
114
+ extract_key_alias = lambda{|m| key_aliases.map{|ka_| bd_conv[m.remove_key!(ka_)]}}
115
115
  else
116
116
  key_present = key_conv = lambda{|m| m[key]}
117
117
  prkey_conv = lambda{|m| m[prkey]}
@@ -119,7 +119,7 @@ module Sequel
119
119
  ancestor_base_case_columns = [SQL::AliasedExpression.new(prkey, ka)] + c_all
120
120
  descendant_base_case_columns = [SQL::AliasedExpression.new(key, ka)] + c_all
121
121
  recursive_case_columns = [SQL::QualifiedIdentifier.new(t, ka)] + c_all
122
- extract_key_alias = lambda{|m| bd_conv[m.values.delete(ka)]}
122
+ extract_key_alias = lambda{|m| bd_conv[m.remove_key!(ka)]}
123
123
  end
124
124
 
125
125
  parent = opts.merge(opts.fetch(:parent, OPTS)).fetch(:name, :parent)
@@ -200,7 +200,7 @@ module Sequel
200
200
  model.eager_load_results(r, eo.merge(:loader=>false, :initialize_rows=>false, :dataset=>ds, :id_map=>nil)) do |obj|
201
201
  opk = prkey_conv[obj]
202
202
  if idm_obj = parent_map[opk]
203
- key_aliases.each{|ka_| idm_obj.values[ka_] = obj.values[ka_]}
203
+ key_aliases.each{|ka_| idm_obj[ka_] = obj[ka_]}
204
204
  obj = idm_obj
205
205
  else
206
206
  obj.associations[parent] = nil
@@ -307,12 +307,12 @@ module Sequel
307
307
  ds = ds.select_append(ka) unless ds.opts[:select] == nil
308
308
  model.eager_load_results(r, eo.merge(:loader=>false, :initialize_rows=>false, :dataset=>ds, :id_map=>nil, :associations=>OPTS)) do |obj|
309
309
  if level
310
- no_cache = no_cache_level == obj.values.delete(la)
310
+ no_cache = no_cache_level == obj.remove_key!(la)
311
311
  end
312
312
 
313
313
  opk = prkey_conv[obj]
314
314
  if idm_obj = parent_map[opk]
315
- key_aliases.each{|ka_| idm_obj.values[ka_] = obj.values[ka_]}
315
+ key_aliases.each{|ka_| idm_obj[ka_] = obj[ka_]}
316
316
  obj = idm_obj
317
317
  else
318
318
  obj.associations[childrena] = [] unless no_cache
@@ -54,6 +54,16 @@ module Sequel
54
54
  end
55
55
  end
56
56
 
57
+ # Remove the key from noncolumn values if it is present there. If it is not
58
+ # present there, then use the default behavior of removing it from values.
59
+ def remove_key!(key)
60
+ if @noncolumn_values && @noncolumn_values.key?(key)
61
+ @noncolumn_values.delete(key)
62
+ else
63
+ super
64
+ end
65
+ end
66
+
57
67
  # Check all entries in the values hash. If any of the keys are not columns,
58
68
  # move the entry into the noncolumn_values hash.
59
69
  def split_noncolumn_values
@@ -9,6 +9,13 @@ module Sequel
9
9
  # in the result sets (and possibly overwrite columns in the
10
10
  # current model with the same name).
11
11
  #
12
+ # Note that by default on databases that supporting RETURNING,
13
+ # using this plugin will cause instance creations
14
+ # to use two queries (insert and refresh) instead of a single
15
+ # query using RETURNING. You can use the insert_returning_select
16
+ # plugin to automatically use RETURNING for instance creations
17
+ # for models using this plugin.
18
+ #
12
19
  # Usage:
13
20
  #
14
21
  # # Make all model subclasses select table.*
@@ -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 = 97
9
+ MINOR = 98
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.97.0
4
+ version: 5.98.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
@@ -250,6 +250,7 @@ files:
250
250
  - lib/sequel/extensions/pg_array.rb
251
251
  - lib/sequel/extensions/pg_array_ops.rb
252
252
  - lib/sequel/extensions/pg_auto_parameterize.rb
253
+ - lib/sequel/extensions/pg_auto_parameterize_duplicate_query_detection.rb
253
254
  - lib/sequel/extensions/pg_auto_parameterize_in_array.rb
254
255
  - lib/sequel/extensions/pg_enum.rb
255
256
  - lib/sequel/extensions/pg_extended_date_support.rb