sequel 5.11.0 → 5.12.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/advanced_associations.rdoc +132 -14
  4. data/doc/postgresql.rdoc +14 -0
  5. data/doc/release_notes/5.12.0.txt +141 -0
  6. data/lib/sequel/adapters/ado/mssql.rb +1 -1
  7. data/lib/sequel/adapters/oracle.rb +5 -6
  8. data/lib/sequel/adapters/postgres.rb +18 -5
  9. data/lib/sequel/adapters/shared/mysql.rb +5 -5
  10. data/lib/sequel/adapters/sqlite.rb +0 -5
  11. data/lib/sequel/adapters/tinytds.rb +0 -5
  12. data/lib/sequel/adapters/utils/emulate_offset_with_reverse_and_count.rb +2 -5
  13. data/lib/sequel/core.rb +6 -1
  14. data/lib/sequel/dataset/graph.rb +25 -9
  15. data/lib/sequel/dataset/placeholder_literalizer.rb +47 -17
  16. data/lib/sequel/dataset/prepared_statements.rb +86 -18
  17. data/lib/sequel/dataset/sql.rb +5 -1
  18. data/lib/sequel/extensions/caller_logging.rb +79 -0
  19. data/lib/sequel/extensions/constraint_validations.rb +1 -1
  20. data/lib/sequel/extensions/pg_static_cache_updater.rb +2 -2
  21. data/lib/sequel/model/associations.rb +56 -23
  22. data/lib/sequel/model/base.rb +3 -3
  23. data/lib/sequel/plugins/eager_graph_eager.rb +139 -0
  24. data/lib/sequel/plugins/static_cache.rb +9 -8
  25. data/lib/sequel/plugins/tactical_eager_loading.rb +63 -1
  26. data/lib/sequel/version.rb +1 -1
  27. data/spec/adapters/oracle_spec.rb +44 -0
  28. data/spec/adapters/postgres_spec.rb +39 -0
  29. data/spec/core/dataset_spec.rb +23 -9
  30. data/spec/core/object_graph_spec.rb +314 -284
  31. data/spec/extensions/caller_logging_spec.rb +52 -0
  32. data/spec/extensions/eager_graph_eager_spec.rb +100 -0
  33. data/spec/extensions/finder_spec.rb +1 -1
  34. data/spec/extensions/prepared_statements_spec.rb +7 -12
  35. data/spec/extensions/static_cache_spec.rb +14 -0
  36. data/spec/extensions/tactical_eager_loading_spec.rb +262 -1
  37. data/spec/integration/associations_test.rb +72 -0
  38. data/spec/integration/dataset_test.rb +3 -3
  39. data/spec/model/eager_loading_spec.rb +90 -0
  40. metadata +8 -2
@@ -441,7 +441,7 @@ module Sequel
441
441
  /cannot be null/ => NotNullConstraintViolation,
442
442
  /Deadlock found when trying to get lock; try restarting transaction/ => SerializationFailure,
443
443
  /CONSTRAINT .+ failed for/ => CheckConstraintViolation,
444
- /\AStatement aborted because lock\(s\) could not be acquired immediately and NOWAIT is set\./ => DatabaseLockTimeout,
444
+ /\A(Statement aborted because lock\(s\) could not be acquired immediately and NOWAIT is set\.|Lock wait timeout exceeded; try restarting transaction)/ => DatabaseLockTimeout,
445
445
  }.freeze
446
446
  def database_error_regexps
447
447
  DATABASE_ERROR_REGEXPS
@@ -804,9 +804,9 @@ module Sequel
804
804
  true
805
805
  end
806
806
 
807
- # MySQL does not support INTERSECT or EXCEPT
807
+ # MariaDB 10.3+ supports INTERSECT or EXCEPT
808
808
  def supports_intersect_except?
809
- false
809
+ db.mariadb? && db.server_version >= 100300
810
810
  end
811
811
 
812
812
  # MySQL does not support limits in correlated subqueries (or any subqueries that use IN).
@@ -819,9 +819,9 @@ module Sequel
819
819
  true
820
820
  end
821
821
 
822
- # MySQL 8+ supports NOWAIT.
822
+ # MySQL 8+ and MariaDB 10.3+ support NOWAIT.
823
823
  def supports_nowait?
824
- !db.mariadb? && db.server_version >= 80000
824
+ db.server_version >= (db.mariadb? ? 100300 : 80000)
825
825
  end
826
826
 
827
827
  # MySQL's DISTINCT ON emulation using GROUP BY does not respect the
@@ -306,11 +306,6 @@ module Sequel
306
306
  def prepared_arg(k)
307
307
  LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}")
308
308
  end
309
-
310
- # Always assume a prepared argument.
311
- def prepared_arg?(k)
312
- true
313
- end
314
309
  end
315
310
 
316
311
  BindArgumentMethods = prepared_statements_module(:bind, ArgumentMapper)
@@ -194,11 +194,6 @@ module Sequel
194
194
  def prepared_arg(k)
195
195
  LiteralString.new("@#{k.to_s.gsub('.', '__')}")
196
196
  end
197
-
198
- # Always assume a prepared argument.
199
- def prepared_arg?(k)
200
- true
201
- end
202
197
  end
203
198
 
204
199
  PreparedStatementMethods = prepared_statements_module("sql = prepared_sql; opts = Hash[opts]; opts[:arguments] = bind_arguments", ArgumentMapper)
@@ -32,14 +32,11 @@ module Sequel
32
32
  row_count = @opts[:offset_total_count] || ds.clone(:append_sql=>String.new, :placeholder_literal_null=>true).count
33
33
  dsa1 = dataset_alias(1)
34
34
 
35
- if o.is_a?(Symbol) && @opts[:bind_vars] && (match = /\A\$(.*)\z/.match(o.to_s))
35
+ if o.is_a?(Symbol) && @opts[:bind_vars] && /\A\$(.*)\z/ =~ o
36
36
  # Handle use of bound variable offsets. Unfortunately, prepared statement
37
37
  # bound variable offsets cannot be handled, since the bound variable value
38
38
  # isn't available until later.
39
- s = match[1].to_sym
40
- if prepared_arg?(s)
41
- o = prepared_arg(s)
42
- end
39
+ o = prepared_arg($1.to_sym)
43
40
  end
44
41
 
45
42
  reverse_offset = row_count - o
@@ -58,6 +58,11 @@ module Sequel
58
58
  #
59
59
  # Sequel.single_threaded = true
60
60
  attr_accessor :single_threaded
61
+
62
+ # Alias of original require method, as Sequel.require is does a relative
63
+ # require for backwards compatibility.
64
+ alias orig_require require
65
+ private :orig_require
61
66
  end
62
67
 
63
68
  # Returns true if the passed object could be a specifier of conditions, false otherwise.
@@ -142,7 +147,7 @@ module Sequel
142
147
  # Sequel.extension(:blank)
143
148
  # Sequel.extension(:core_extensions, :named_timezones)
144
149
  def self.extension(*extensions)
145
- extensions.each{|e| Kernel.require "sequel/extensions/#{e}"}
150
+ extensions.each{|e| orig_require("sequel/extensions/#{e}")}
146
151
  end
147
152
 
148
153
  # The exception classed raised if there is an error parsing JSON.
@@ -96,10 +96,30 @@ module Sequel
96
96
 
97
97
  table_alias_qualifier = qualifier_from_alias_symbol(table_alias, table)
98
98
  implicit_qualifier = options[:implicit_qualifier]
99
+ joined_dataset = joined_dataset?
99
100
  ds = self
101
+ graph = opts[:graph]
102
+
103
+ if !graph && (select = @opts[:select]) && !select.empty?
104
+ select_columns = nil
105
+
106
+ unless !joined_dataset && select.length == 1 && (select[0].is_a?(SQL::ColumnAll))
107
+ force_from_self = false
108
+ select_columns = select.map do |sel|
109
+ unless col = _hash_key_symbol(sel)
110
+ force_from_self = true
111
+ break
112
+ end
113
+
114
+ [sel, col]
115
+ end
116
+
117
+ select_columns = nil if force_from_self
118
+ end
119
+ end
100
120
 
101
121
  # Use a from_self if this is already a joined table (or from_self specifically disabled for graphs)
102
- if (@opts[:graph_from_self] != false && !@opts[:graph] && joined_dataset?)
122
+ if (@opts[:graph_from_self] != false && !graph && (joined_dataset || force_from_self))
103
123
  from_selfed = true
104
124
  implicit_qualifier = options[:from_self_alias] || first_source
105
125
  ds = ds.from_self(:alias=>implicit_qualifier)
@@ -115,7 +135,7 @@ module Sequel
115
135
  # Whether to include the table in the result set
116
136
  add_table = options[:select] == false ? false : true
117
137
 
118
- if graph = opts[:graph]
138
+ if graph
119
139
  graph = graph.dup
120
140
  select = opts[:select].dup
121
141
  [:column_aliases, :table_aliases, :column_alias_num].each{|k| graph[k] = graph[k].dup}
@@ -137,12 +157,8 @@ module Sequel
137
157
  # Keep track of the alias numbers used
138
158
  ca_num = graph[:column_alias_num] = Hash.new(0)
139
159
 
140
- # All columns in the master table are never
141
- # aliased, but are not included if set_graph_aliases
142
- # has been used.
143
- if (select = @opts[:select]) && !select.empty? && !(select.length == 1 && (select.first.is_a?(SQL::ColumnAll)))
144
- select = select.map do |sel|
145
- raise Error, "can't figure out alias to use for graphing for #{sel.inspect}" unless column = _hash_key_symbol(sel)
160
+ select = if select_columns
161
+ select_columns.map do |sel, column|
146
162
  column_aliases[column] = [master, column]
147
163
  if from_selfed
148
164
  # Initial dataset was wrapped in subselect, selected all
@@ -155,7 +171,7 @@ module Sequel
155
171
  end
156
172
  end
157
173
  else
158
- select = columns.map do |column|
174
+ columns.map do |column|
159
175
  column_aliases[column] = [master, column]
160
176
  SQL::QualifiedIdentifier.new(qualifier, column)
161
177
  end
@@ -77,23 +77,8 @@ module Sequel
77
77
  # Yields the receiver and the dataset to the block, which should
78
78
  # call #arg on the receiver for each placeholder argument, and
79
79
  # return the dataset that you want to load.
80
- def loader(dataset)
81
- @argn = -1
82
- @args = []
83
- ds = yield self, dataset
84
- sql = ds.clone(:placeholder_literalizer=>self).sql
85
-
86
- last_offset = 0
87
- fragments = @args.map do |used_sql, offset, arg, t|
88
- raise Error, "placeholder literalizer argument literalized into different string than dataset returned" unless used_sql.equal?(sql)
89
- a = [sql[last_offset...offset], arg, t]
90
- last_offset = offset
91
- a
92
- end
93
- final_sql = sql[last_offset..-1]
94
-
95
- arity = @argn+1
96
- PlaceholderLiteralizer.new(ds.clone, fragments, final_sql, arity)
80
+ def loader(dataset, &block)
81
+ PlaceholderLiteralizer.new(*process(dataset, &block))
97
82
  end
98
83
 
99
84
  # Return an Argument with the specified position, or the next position. In
@@ -111,6 +96,51 @@ module Sequel
111
96
  def use(sql, arg, transformer)
112
97
  @args << [sql, sql.length, arg, transformer]
113
98
  end
99
+
100
+ private
101
+
102
+ # Return an array with two elements, the first being an
103
+ # SQL string with interpolated prepared argument placeholders
104
+ # (suitable for inspect), the the second being an array of
105
+ # SQL fragments suitable for using for creating a
106
+ # Sequel::SQL::PlaceholderLiteralString. Designed for use with
107
+ # emulated prepared statements.
108
+ def prepared_sql_and_frags(dataset, prepared_args, &block)
109
+ _, frags, final_sql, _ = process(dataset, &block)
110
+
111
+ frags = frags.map(&:first)
112
+ prepared_sql = String.new
113
+ frags.each_with_index do |sql, i|
114
+ prepared_sql << sql
115
+ prepared_sql << "$#{prepared_args[i]}"
116
+ end
117
+ if final_sql
118
+ frags << final_sql
119
+ prepared_sql << final_sql
120
+ end
121
+
122
+ [prepared_sql, frags]
123
+ end
124
+
125
+ # Internals of #loader and #prepared_sql_and_frags.
126
+ def process(dataset)
127
+ @argn = -1
128
+ @args = []
129
+ ds = yield self, dataset
130
+ sql = ds.clone(:placeholder_literalizer=>self).sql
131
+
132
+ last_offset = 0
133
+ fragments = @args.map do |used_sql, offset, arg, t|
134
+ raise Error, "placeholder literalizer argument literalized into different string than dataset returned" unless used_sql.equal?(sql)
135
+ a = [sql[last_offset...offset], arg, t]
136
+ last_offset = offset
137
+ a
138
+ end
139
+ final_sql = sql[last_offset..-1]
140
+
141
+ arity = @argn+1
142
+ [ds, fragments, final_sql, arity]
143
+ end
114
144
  end
115
145
 
116
146
  # Create a PlaceholderLiteralizer by yielding a Recorder and dataset to the
@@ -67,6 +67,14 @@ module Sequel
67
67
  end
68
68
  cache_set(:_prepared_sql, super)
69
69
  end
70
+
71
+ private
72
+
73
+ # Report that prepared statements are not emulated, since
74
+ # all adapters that use this use native prepared statements.
75
+ def emulate_prepared_statements?
76
+ false
77
+ end
70
78
  end
71
79
 
72
80
  # Backbone of the prepared statement support. Grafts bind variable
@@ -155,13 +163,8 @@ module Sequel
155
163
  # prepared_args is present. If so, they are considered placeholders,
156
164
  # and they are substituted using prepared_arg.
157
165
  def literal_symbol_append(sql, v)
158
- if @opts[:bind_vars] and match = /\A\$(.*)\z/.match(v.to_s)
159
- s = match[1].to_sym
160
- if prepared_arg?(s)
161
- literal_append(sql, prepared_arg(s))
162
- else
163
- sql << v.to_s
164
- end
166
+ if @opts[:bind_vars] && /\A\$(.*)\z/ =~ v
167
+ literal_append(sql, prepared_arg($1.to_sym))
165
168
  else
166
169
  super
167
170
  end
@@ -214,11 +217,6 @@ module Sequel
214
217
  @opts[:bind_vars][k]
215
218
  end
216
219
 
217
- # Whether there is a bound value for the given key.
218
- def prepared_arg?(k)
219
- @opts[:bind_vars].has_key?(k)
220
- end
221
-
222
220
  # The symbol cache should always be skipped, since placeholders are symbols.
223
221
  def skip_symbol_cache?
224
222
  true
@@ -228,9 +226,12 @@ module Sequel
228
226
  # support and using the same argument hash so that you can use
229
227
  # bind variables/prepared arguments in subselects.
230
228
  def subselect_sql_append(sql, ds)
231
- ds.clone(:append_sql=>sql, :prepared_args=>prepared_args, :bind_vars=>@opts[:bind_vars]).
232
- send(:to_prepared_statement, :select, nil, :extend=>prepared_statement_modules).
233
- prepared_sql
229
+ subselect_sql_dataset(sql, ds).prepared_sql
230
+ end
231
+
232
+ def subselect_sql_dataset(sql, ds)
233
+ super.clone(:prepared_args=>prepared_args, :bind_vars=>@opts[:bind_vars]).
234
+ send(:to_prepared_statement, :select, nil, :extend=>prepared_statement_modules)
234
235
  end
235
236
  end
236
237
 
@@ -258,11 +259,63 @@ module Sequel
258
259
  prepared_args << k
259
260
  prepared_arg_placeholder
260
261
  end
262
+ end
263
+
264
+ # Prepared statements emulation support for adapters that don't
265
+ # support native prepared statements. Uses a placeholder
266
+ # literalizer to hold the prepared sql with the ability to
267
+ # interpolate arguments to prepare the final SQL string.
268
+ module EmulatePreparedStatementMethods
269
+ include UnnumberedArgumentMapper
270
+
271
+ def run(&block)
272
+ if @opts[:prepared_sql_frags]
273
+ sql = literal(Sequel::SQL::PlaceholderLiteralString.new(@opts[:prepared_sql_frags], @opts[:bind_arguments], false))
274
+ clone(:prepared_sql_frags=>nil, :sql=>sql, :prepared_sql=>sql).run(&block)
275
+ else
276
+ super
277
+ end
278
+ end
279
+
280
+ private
261
281
 
262
- # Always assume there is a prepared arg in the argument mapper.
263
- def prepared_arg?(k)
282
+ # Turn emulation of prepared statements back on, since ArgumentMapper
283
+ # turns it off.
284
+ def emulate_prepared_statements?
264
285
  true
265
286
  end
287
+
288
+ def emulated_prepared_statement(type, name, values)
289
+ prepared_sql, frags = Sequel::Dataset::PlaceholderLiteralizer::Recorder.new.send(:prepared_sql_and_frags, self, prepared_args) do |pl, ds|
290
+ ds = ds.clone(:recorder=>pl)
291
+
292
+ case type
293
+ when :first
294
+ ds.limit(1)
295
+ when :update, :insert, :insert_select, :delete
296
+ ds.with_sql(:"#{type}_sql", *values)
297
+ when :insert_pk
298
+ ds.with_sql(:insert_sql, *values)
299
+ else
300
+ ds
301
+ end
302
+ end
303
+
304
+ prepared_args.freeze
305
+ clone(:prepared_sql_frags=>frags, :prepared_sql=>prepared_sql, :sql=>prepared_sql)
306
+ end
307
+
308
+ # Associates the argument with name k with the next position in
309
+ # the output array.
310
+ def prepared_arg(k)
311
+ prepared_args << k
312
+ @opts[:recorder].arg
313
+ end
314
+
315
+ def subselect_sql_dataset(sql, ds)
316
+ super.clone(:recorder=>@opts[:recorder]).
317
+ with_extend(EmulatePreparedStatementMethods)
318
+ end
266
319
  end
267
320
 
268
321
  # Set the bind variables to use for the call. If bind variables have
@@ -315,7 +368,16 @@ module Sequel
315
368
  # DB.call(:select_by_name, name: 'Blah') # Same thing
316
369
  def prepare(type, name, *values)
317
370
  ps = to_prepared_statement(type, values, :name=>name, :extend=>prepared_statement_modules, :no_delayed_evaluations=>true)
318
- ps.prepared_sql
371
+
372
+ ps = if ps.send(:emulate_prepared_statements?)
373
+ ps = ps.with_extend(EmulatePreparedStatementMethods)
374
+ ps.send(:emulated_prepared_statement, type, name, values)
375
+ else
376
+ sql = ps.prepared_sql
377
+ ps.prepared_args.freeze
378
+ ps.clone(:prepared_sql=>sql, :sql=>sql)
379
+ end
380
+
319
381
  db.set_prepared_statement(name, ps)
320
382
  ps
321
383
  end
@@ -344,6 +406,12 @@ module Sequel
344
406
  prepared_statement_modules
345
407
  end
346
408
 
409
+ # Whether prepared statements should be emulated. True by
410
+ # default so that adapters have to opt in.
411
+ def emulate_prepared_statements?
412
+ true
413
+ end
414
+
347
415
  def prepared_statement_modules
348
416
  []
349
417
  end
@@ -1553,7 +1553,11 @@ module Sequel
1553
1553
 
1554
1554
  # Append literalization of the subselect to SQL string.
1555
1555
  def subselect_sql_append(sql, ds)
1556
- ds.clone(:append_sql=>sql).sql
1556
+ subselect_sql_dataset(sql, ds).sql
1557
+ end
1558
+
1559
+ def subselect_sql_dataset(sql, ds)
1560
+ ds.clone(:append_sql=>sql)
1557
1561
  end
1558
1562
 
1559
1563
  # The number of decimal digits of precision to use in timestamps.
@@ -0,0 +1,79 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The caller_logging extension includes caller information before
4
+ # query logging, showing which code caused the query. It skips
5
+ # internal Sequel code, showing the first non-Sequel caller line.
6
+ #
7
+ # DB.extension :caller_logging
8
+ # DB[:table].first
9
+ # # Logger:
10
+ # # (0.000041s) (source: /path/to/app/foo/t.rb:12 in `get_first`) SELECT * FROM table LIMIT 1
11
+ #
12
+ # You can further filter the caller lines by setting
13
+ # <tt>Database#caller_logging_ignore</tt> to a regexp of additional
14
+ # caller lines to ignore. This is useful if you have specific
15
+ # methods or internal extensions/plugins that you would also
16
+ # like to ignore as they obscure the code actually making the
17
+ # request.
18
+ #
19
+ # DB.caller_logging_ignore = %r{/path/to/app/lib/plugins}
20
+ #
21
+ # You can also format the caller before it is placed in the logger,
22
+ # using +caller_logging_formatter+:
23
+ #
24
+ # DB.caller_logging_formatter = lambda do |caller|
25
+ # "(#{caller.sub(/\A\/path\/to\/app\//, '')})"
26
+ # end
27
+ # DB[:table].first
28
+ # # Logger:
29
+ # # (0.000041s) (foo/t.rb:12 in `get_first`) SELECT * FROM table LIMIT 1
30
+ #
31
+ # Related module: Sequel::CallerLogging
32
+
33
+ require 'rbconfig'
34
+
35
+ #
36
+ module Sequel
37
+ module CallerLogging
38
+ SEQUEL_LIB_PATH = (File.expand_path('../../..', __FILE__) + '/').freeze
39
+
40
+ # A regexp of caller lines to ignore, in addition to internal Sequel and Ruby code.
41
+ attr_accessor :caller_logging_ignore
42
+
43
+ # A callable to format the external caller
44
+ attr_accessor :caller_logging_formatter
45
+
46
+ # Include caller information when logging query.
47
+ def log_connection_yield(sql, conn, args=nil)
48
+ if !@loggers.empty? && (external_caller = external_caller_for_log)
49
+ sql = "#{external_caller} #{sql}"
50
+ end
51
+ super
52
+ end
53
+
54
+ private
55
+
56
+ # The caller to log, ignoring internal Sequel and Ruby code, and user specified
57
+ # lines to ignore.
58
+ def external_caller_for_log
59
+ ignore = caller_logging_ignore
60
+ c = caller.find do |line|
61
+ !(line.start_with?(SEQUEL_LIB_PATH) ||
62
+ line.start_with?(RbConfig::CONFIG["rubylibdir"]) ||
63
+ (ignore && line =~ ignore))
64
+ end
65
+
66
+ if c
67
+ c = if formatter = caller_logging_formatter
68
+ formatter.call(c)
69
+ else
70
+ "(source: #{c})"
71
+ end
72
+ end
73
+
74
+ c
75
+ end
76
+ end
77
+
78
+ Database.register_extension(:caller_logging, CallerLogging)
79
+ end