sequel 5.11.0 → 5.12.0

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