sequel 5.101.0 → 5.104.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/lib/sequel/adapters/jdbc/derby.rb +2 -0
  4. data/lib/sequel/adapters/jdbc/h2.rb +2 -2
  5. data/lib/sequel/adapters/postgres.rb +1 -1
  6. data/lib/sequel/adapters/shared/mssql.rb +3 -3
  7. data/lib/sequel/adapters/shared/mysql.rb +5 -4
  8. data/lib/sequel/adapters/shared/postgres.rb +16 -16
  9. data/lib/sequel/adapters/shared/sqlite.rb +3 -3
  10. data/lib/sequel/adapters/sqlite.rb +1 -1
  11. data/lib/sequel/adapters/tinytds.rb +1 -1
  12. data/lib/sequel/connection_pool/sharded_timed_queue.rb +36 -11
  13. data/lib/sequel/connection_pool/timed_queue.rb +27 -9
  14. data/lib/sequel/connection_pool.rb +6 -0
  15. data/lib/sequel/database/schema_generator.rb +36 -5
  16. data/lib/sequel/database/schema_methods.rb +1 -1
  17. data/lib/sequel/dataset/placeholder_literalizer.rb +3 -0
  18. data/lib/sequel/dataset/prepared_statements.rb +7 -4
  19. data/lib/sequel/dataset/query.rb +6 -3
  20. data/lib/sequel/dataset/sql.rb +6 -1
  21. data/lib/sequel/extensions/connection_checkout_event_callback.rb +151 -0
  22. data/lib/sequel/extensions/connection_expiration.rb +1 -1
  23. data/lib/sequel/extensions/connection_validator.rb +1 -1
  24. data/lib/sequel/extensions/dataset_run.rb +2 -2
  25. data/lib/sequel/extensions/date_arithmetic.rb +6 -6
  26. data/lib/sequel/extensions/lit_require_frozen.rb +131 -0
  27. data/lib/sequel/extensions/migration.rb +14 -17
  28. data/lib/sequel/extensions/pg_enum.rb +1 -1
  29. data/lib/sequel/model/associations.rb +180 -6
  30. data/lib/sequel/plugins/dataset_associations.rb +20 -1
  31. data/lib/sequel/plugins/dirty.rb +5 -2
  32. data/lib/sequel/plugins/many_through_many.rb +21 -0
  33. data/lib/sequel/plugins/mssql_optimistic_locking.rb +1 -1
  34. data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +1 -1
  35. data/lib/sequel/plugins/serialization.rb +10 -1
  36. data/lib/sequel/version.rb +1 -1
  37. metadata +3 -1
@@ -54,7 +54,7 @@ module Sequel
54
54
 
55
55
  # Set the bind arguments based on the hash and call super.
56
56
  def call(bind_vars=OPTS, &block)
57
- sql = prepared_sql
57
+ sql = prepared_sql.freeze
58
58
  prepared_args.freeze
59
59
  ps = bind(bind_vars)
60
60
  ps.clone(:bind_arguments=>ps.map_to_prepared_args(ps.opts[:bind_vars]), :sql=>sql, :prepared_sql=>sql).run(&block)
@@ -223,7 +223,7 @@ module Sequel
223
223
  # with the prepared SQL, to ensure the prepared_sql_type is respected.
224
224
  def force_prepared_sql
225
225
  if prepared_sql_type != prepared_type
226
- with_sql(prepared_sql)
226
+ with_sql(prepared_sql.freeze)
227
227
  else
228
228
  self
229
229
  end
@@ -287,7 +287,7 @@ module Sequel
287
287
 
288
288
  def run(&block)
289
289
  if @opts[:prepared_sql_frags]
290
- sql = literal(Sequel::SQL::PlaceholderLiteralString.new(@opts[:prepared_sql_frags], @opts[:bind_arguments], false))
290
+ sql = literal(Sequel::SQL::PlaceholderLiteralString.new(@opts[:prepared_sql_frags], @opts[:bind_arguments], false)).freeze
291
291
  clone(:prepared_sql_frags=>nil, :sql=>sql, :prepared_sql=>sql).run(&block)
292
292
  else
293
293
  super
@@ -320,6 +320,9 @@ module Sequel
320
320
  end
321
321
 
322
322
  prepared_args.freeze
323
+ frags.freeze
324
+ frags.each(&:freeze)
325
+ prepared_sql.freeze
323
326
  clone(:prepared_sql_frags=>frags, :prepared_sql=>prepared_sql, :sql=>prepared_sql)
324
327
  end
325
328
 
@@ -409,7 +412,7 @@ module Sequel
409
412
  ps = ps.with_extend(EmulatePreparedStatementMethods)
410
413
  ps.send(:emulated_prepared_statement, type, name, values)
411
414
  else
412
- sql = ps.prepared_sql
415
+ sql = ps.prepared_sql.freeze
413
416
  ps.prepared_args.freeze
414
417
  ps.clone(:prepared_sql=>sql, :sql=>sql)
415
418
  end
@@ -1301,7 +1301,7 @@ module Sequel
1301
1301
  # * truncate (if a TRUNCATE statement, with no arguments)
1302
1302
  def with_sql(sql, *args)
1303
1303
  if sql.is_a?(Symbol)
1304
- sql = public_send(sql, *args)
1304
+ sql = public_send(sql, *args).freeze
1305
1305
  else
1306
1306
  sql = SQL::PlaceholderLiteralString.new(sql, args) unless args.empty?
1307
1307
  end
@@ -1474,7 +1474,10 @@ module Sequel
1474
1474
  def default_join_table_qualification
1475
1475
  :symbol
1476
1476
  end
1477
-
1477
+
1478
+ PAREN_WRAPPER = ["(".freeze, ")".freeze].freeze
1479
+ private_constant :PAREN_WRAPPER
1480
+
1478
1481
  # SQL expression object based on the expr type. See +where+.
1479
1482
  def filter_expr(expr = nil, &block)
1480
1483
  expr = nil if expr == EMPTY_ARRAY
@@ -1495,7 +1498,7 @@ module Sequel
1495
1498
  raise Error, "Invalid filter expression: #{expr.inspect}"
1496
1499
  end
1497
1500
  when LiteralString
1498
- LiteralString.new("(#{expr})")
1501
+ SQL::PlaceholderLiteralString.new(PAREN_WRAPPER, [expr])
1499
1502
  when Numeric, SQL::NumericExpression, SQL::StringExpression, Proc, String, Set
1500
1503
  raise Error, "Invalid filter expression: #{expr.inspect}"
1501
1504
  when TrueClass, FalseClass
@@ -53,7 +53,7 @@ module Sequel
53
53
  when String
54
54
  case v
55
55
  when LiteralString
56
- sql << v
56
+ literal_literal_string_append(sql, v)
57
57
  when SQL::Blob
58
58
  literal_blob_append(sql, v)
59
59
  else
@@ -1424,6 +1424,11 @@ module Sequel
1424
1424
  v.to_s
1425
1425
  end
1426
1426
 
1427
+ # Append string to SQL string.
1428
+ def literal_literal_string_append(sql, v)
1429
+ sql << v
1430
+ end
1431
+
1427
1432
  # SQL fragment for nil
1428
1433
  def literal_nil
1429
1434
  "NULL"
@@ -0,0 +1,151 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The connection_checkout_event_callback extension modifies a database's
4
+ # connection pool to allow for a checkout event callback. This callback is
5
+ # called with the following arguments:
6
+ #
7
+ # :immediately_available :: Connection immediately available and returned
8
+ # :not_immediately_available :: Connection not immediately available
9
+ # :new_connection :: New connection created and returned
10
+ # Float :: Number of seconds waiting to acquire a connection
11
+ #
12
+ # This is a low-level extension that allows for building telemetry
13
+ # information. It doesn't implement any telemetry reporting itself. The
14
+ # main reason for recording this information is to use it to determine the
15
+ # appropriate size for the connection pool. Having too large a connection
16
+ # pool can waste resources, while having too small a connection pool can
17
+ # result in substantial time to check out a connection. In general, you
18
+ # want to use as small a pool as possible while keeping the time to
19
+ # checkout a connection low.
20
+ #
21
+ # To use the connection checkout event callback, you must first load the
22
+ # extension:
23
+ #
24
+ # DB.extension(:connection_checkout_event_callback)
25
+ #
26
+ # By default, an empty proc is used as the callback so that loading the
27
+ # support doesn't break anything. If you are using the extension, you
28
+ # should set the callback at some point during application startup:
29
+ #
30
+ # DB.pool.on_checkout_event = proc do |event|
31
+ # # ...
32
+ # end
33
+ #
34
+ # When using the sharded connection pool, the callback is also
35
+ # passed a second argument, the requested server shard (generally a
36
+ # symbol), allowing for collection of per-shard telemetry:
37
+ #
38
+ # DB.pool.on_checkout_event = proc do |event, server|
39
+ # # ...
40
+ # end
41
+ #
42
+ # Note that the callback may be called currently by multiple threads.
43
+ # You should use some form of concurrency control inside the callback,
44
+ # such as a mutex or queue.
45
+ #
46
+ # Below is a brief example of usage to determine the percentage of
47
+ # connection requests where a connection was immediately available:
48
+ #
49
+ # mutex = Mutex.new
50
+ # total = immediates = 0
51
+ #
52
+ # DB.pool.on_checkout_event = proc do |event|
53
+ # case event
54
+ # when :immediately_available
55
+ # mutex.synchronize do
56
+ # total += 1
57
+ # immediates += 1
58
+ # end
59
+ # when :not_immediately_available
60
+ # mutex.synchronize do
61
+ # total += 1
62
+ # end
63
+ # end
64
+ # end
65
+ #
66
+ # immediate_percentage = lambda do
67
+ # mutex.synchronize do
68
+ # 100.0 * immediates / total
69
+ # end
70
+ # end
71
+ #
72
+ # Note that this extension only works with the timed_queue
73
+ # and sharded_timed_queue connection pools (the default
74
+ # connection pools when using Ruby 3.2+).
75
+ #
76
+ # Related modules: Sequel::ConnectionCheckoutEventCallbacks::TimedQueue,
77
+ # Sequel::ConnectionCheckoutEventCallbacks::ShardedTimedQueue
78
+
79
+ #
80
+ module Sequel
81
+ module ConnectionCheckoutEventCallbacks
82
+ module TimedQueue
83
+ # The callback that is called with connection checkout events.
84
+ attr_accessor :on_checkout_event
85
+
86
+ private
87
+
88
+ def available
89
+ conn = super
90
+ @on_checkout_event.call(conn ? :immediately_available : :not_immediately_available)
91
+ conn
92
+ end
93
+
94
+ def try_make_new
95
+ conn = super
96
+ @on_checkout_event.call(:new_connection) if conn
97
+ conn
98
+ end
99
+
100
+ def wait_until_available
101
+ timer = Sequel.start_timer
102
+ conn = super
103
+ @on_checkout_event.call(Sequel.elapsed_seconds_since(timer))
104
+ conn
105
+ end
106
+ end
107
+
108
+ module ShardedTimedQueue
109
+ # The callback that is called with connection checkout events.
110
+ attr_accessor :on_checkout_event
111
+
112
+ private
113
+
114
+ def available(queue, server)
115
+ conn = super
116
+ @on_checkout_event.call(conn ? :immediately_available : :not_immediately_available, server)
117
+ conn
118
+ end
119
+
120
+ def try_make_new(server)
121
+ conn = super
122
+ @on_checkout_event.call(:new_connection, server) if conn
123
+ conn
124
+ end
125
+
126
+ def wait_until_available(queue, server)
127
+ timer = Sequel.start_timer
128
+ conn = super
129
+ @on_checkout_event.call(Sequel.elapsed_seconds_since(timer), server)
130
+ conn
131
+ end
132
+ end
133
+ end
134
+
135
+ default_callback = proc{}
136
+
137
+ Database.register_extension(:connection_checkout_event_callback) do |db|
138
+ pool = db.pool
139
+
140
+ case pool.pool_type
141
+ when :timed_queue
142
+ db.pool.extend(ConnectionCheckoutEventCallbacks::TimedQueue)
143
+ when :sharded_timed_queue
144
+ db.pool.extend(ConnectionCheckoutEventCallbacks::ShardedTimedQueue)
145
+ else
146
+ raise Error, "the connection_checkout_event_callback extension is only supported when using a timed_queue connection pool"
147
+ end
148
+
149
+ pool.on_checkout_event ||= default_callback
150
+ end
151
+ end
@@ -91,7 +91,7 @@ module Sequel
91
91
  sync{@allocated.delete(Sequel.current)}
92
92
  end
93
93
 
94
- disconnect_connection(conn)
94
+ disconnect_acquired_connection(conn, *a)
95
95
  redo
96
96
  end
97
97
  end
@@ -118,7 +118,7 @@ module Sequel
118
118
  sync{@allocated.delete(Sequel.current)}
119
119
  end
120
120
 
121
- disconnect_connection(conn)
121
+ disconnect_acquired_connection(conn, *a)
122
122
  redo if valid == false
123
123
  end
124
124
  end
@@ -30,9 +30,9 @@ module Sequel
30
30
  # # GRANT SELECT ON "table" TO "user"
31
31
  def run
32
32
  if server = @opts[:server]
33
- db.run(sql, :server=>server)
33
+ db.run(sql.freeze, :server=>server)
34
34
  else
35
- db.run(sql)
35
+ db.run(sql.freeze)
36
36
  end
37
37
  end
38
38
  end
@@ -82,12 +82,12 @@ module Sequel
82
82
  DURATION_UNITS = [:years, :months, :days, :hours, :minutes, :seconds].freeze
83
83
  DEF_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| s.to_s.freeze}).freeze
84
84
  POSTGRES_DURATION_UNITS = DURATION_UNITS.zip([:years, :months, :days, :hours, :mins, :secs].map{|s| s.to_s.freeze}).freeze
85
- MYSQL_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| Sequel.lit(s.to_s.upcase[0...-1]).freeze}).freeze
86
- MSSQL_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| Sequel.lit(s.to_s[0...-1]).freeze}).freeze
85
+ MYSQL_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| Sequel.lit(s.to_s.upcase[0...-1].freeze).freeze}).freeze
86
+ MSSQL_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| Sequel.lit(s.to_s[0...-1].freeze).freeze}).freeze
87
87
  H2_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| s.to_s[0...-1].freeze}).freeze
88
- DERBY_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| Sequel.lit("SQL_TSI_#{s.to_s.upcase[0...-1]}").freeze}).freeze
88
+ DERBY_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| Sequel.lit("SQL_TSI_#{s.to_s.upcase[0...-1]}".freeze).freeze}).freeze
89
89
  ACCESS_DURATION_UNITS = DURATION_UNITS.zip(%w'yyyy m d h n s'.map(&:freeze)).freeze
90
- DB2_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| Sequel.lit(s.to_s).freeze}).freeze
90
+ DB2_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| Sequel.lit(s.to_s.freeze).freeze}).freeze
91
91
 
92
92
  # Append the SQL fragment for the DateAdd expression to the SQL query.
93
93
  def date_add_sql_append(sql, da)
@@ -107,7 +107,7 @@ module Sequel
107
107
  placeholder = []
108
108
  vals = []
109
109
  each_valid_interval_unit(h, POSTGRES_DURATION_UNITS) do |value, sql_unit|
110
- placeholder << "#{', ' unless placeholder.empty?}#{sql_unit} := "
110
+ placeholder << "#{', ' unless placeholder.empty?}#{sql_unit} := ".freeze
111
111
  vals << value
112
112
  end
113
113
  interval = Sequel.function(:make_interval, Sequel.lit(placeholder, *vals)) unless vals.empty?
@@ -156,7 +156,7 @@ module Sequel
156
156
  expr = Sequel.cast_string(expr) + ' 00:00:00'
157
157
  end
158
158
  each_valid_interval_unit(h, DERBY_DURATION_UNITS) do |value, sql_unit|
159
- expr = Sequel.lit(["{fn timestampadd(#{sql_unit}, ", ", timestamp(", "))}"], value, expr)
159
+ expr = Sequel.lit(["{fn timestampadd(#{sql_unit}, ".freeze, ", timestamp(", "))}"], value, expr)
160
160
  end
161
161
  when :oracle
162
162
  each_valid_interval_unit(h, MYSQL_DURATION_UNITS) do |value, sql_unit|
@@ -0,0 +1,131 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The lit_require_frozen extension disallows the use of unfrozen strings
4
+ # as literal strings in database and dataset methods. If you try to use an
5
+ # unfrozen string as a literal string for a dataset using this extension,
6
+ # an exception will be raised.
7
+ #
8
+ # While this works for all Ruby versions, it is designed for use on Ruby 3+
9
+ # where all files are using the frozen-string-literal magic comment. In this
10
+ # case, uninterpolated literal strings are frozen, but interpolated strings
11
+ # are not frozen. This allows you to catch potentially dangerous code:
12
+ #
13
+ # # Probably safe, no exception raised
14
+ # DB["SELECT * FROM t WHERE c > :v", v: user_provided_string)
15
+ #
16
+ # # Potentially unsafe, raises Sequel::LitRequireFrozen::Error
17
+ # DB["SELECT * FROM t WHERE c > '#{user_provided_string}'"]
18
+ #
19
+ # The assumption made is that a frozen string is unlikely to contain unsafe
20
+ # input, while an unfrozen string has potentially been interpolated and may
21
+ # contain unsafe input.
22
+ #
23
+ # This disallows the the following cases:
24
+ #
25
+ # * Sequel::LiteralString instances that are unfrozen and are not based on a
26
+ # frozen string
27
+ # * Sequel::SQL::PlaceholderLiteralString instances when the placeholder string
28
+ # is not frozen
29
+ # * Unfrozen strings passed to Database#<< or #[] or Dataset#with_sql
30
+ #
31
+ # To use this extension, load it into the database:
32
+ #
33
+ # DB.extension :lit_require_frozen
34
+ #
35
+ # It can also be loaded into individual datasets:
36
+ #
37
+ # ds = DB[:t].extension(:lit_require_frozen)
38
+ #
39
+ # Assuming you have good test coverage, it is recommended to only load
40
+ # this extension when testing.
41
+ #
42
+ # Related module: Sequel::LitRequireFrozen
43
+
44
+ #
45
+ module Sequel
46
+ class LiteralString
47
+ # The string used when creating the literal string (first argument to
48
+ # Sequel::LiteralString.new). This may be nil if no string was provided,
49
+ # or if the litral string was created before this extension was required.
50
+ attr_reader :source
51
+
52
+ def initialize(*a)
53
+ @source = a.first
54
+ super
55
+ end
56
+ # :nocov:
57
+ ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
58
+ # :nocov:
59
+ end
60
+
61
+ module LitRequireFrozen
62
+ # Error class raised for using unfrozen literal string.
63
+ class Error < Sequel::Error
64
+ end
65
+
66
+ module DatabaseMethods
67
+ def self.extended(db)
68
+ db.extend_datasets(DatasetMethods)
69
+ end
70
+
71
+ # Check given SQL is frozen before running it.
72
+ def run(sql, opts=OPTS)
73
+ @default_dataset.with_sql(sql)
74
+ super
75
+ end
76
+ end
77
+
78
+ module DatasetMethods
79
+ # Check given SQL is not an unfrozen string.
80
+ def with_sql(sql, *args)
81
+ _check_unfrozen_literal_string(sql)
82
+ super
83
+ end
84
+
85
+ # Check that placeholder string is frozen (or all entries
86
+ # in placeholder array are frozen).
87
+ def placeholder_literal_string_sql_append(sql, pls)
88
+ str = pls.str
89
+
90
+ if str.is_a?(Array)
91
+ str.each do |s|
92
+ _check_unfrozen_literal_string(s)
93
+ end
94
+ else
95
+ _check_unfrozen_literal_string(str)
96
+ end
97
+
98
+ super
99
+ end
100
+
101
+ private
102
+
103
+ # Base method that other methods used to check for whether a string should be allowed
104
+ # as literal SQL. Allows non-strings as well as frozen strings.
105
+ def _check_unfrozen_literal_string(str)
106
+ return if !str.is_a?(String) || str.frozen?
107
+
108
+ if str.is_a?(LiteralString)
109
+ _check_unfrozen_literal_string(str.source)
110
+ else
111
+ raise Error, "cannot treat unfrozen string as literal SQL: #{str.inspect}"
112
+ end
113
+ end
114
+
115
+ # Check literal strings appended to SQL.
116
+ def literal_literal_string_append(sql, v)
117
+ _check_unfrozen_literal_string(v)
118
+ super
119
+ end
120
+
121
+ # Check static SQL is not frozen.
122
+ def static_sql(sql)
123
+ _check_unfrozen_literal_string(sql)
124
+ super
125
+ end
126
+ end
127
+ end
128
+
129
+ Dataset.register_extension(:lit_require_frozen, LitRequireFrozen::DatasetMethods)
130
+ Database.register_extension(:lit_require_frozen, LitRequireFrozen::DatabaseMethods)
131
+ end
@@ -186,22 +186,19 @@ module Sequel
186
186
  # and returns a new block that reverses the actions taken by
187
187
  # the given block.
188
188
  def reverse(&block)
189
- begin
190
- instance_exec(&block)
191
- rescue
192
- just_raise = true
193
- end
194
- if just_raise
195
- Proc.new{raise Sequel::Error, "irreversible migration method used in #{block.source_location.first}, you may need to write your own down method"}
196
- else
197
- actions = @actions.reverse
198
- Proc.new do
199
- actions.each do |a|
200
- pr = a.last.is_a?(Proc) ? a.pop : nil
201
- # Allow calling private methods as the reversing methods are private
202
- send(*a, &pr)
203
- end
204
- end
189
+ instance_exec(&block)
190
+ rescue NoMethodError => e
191
+ Proc.new{raise Sequel::Error, "irreversible migration method \"#{e.name}\" used in #{block.source_location.first}, you may need to write your own down method"}
192
+ rescue => e
193
+ Proc.new{raise Sequel::Error, "unable to reverse migration due to #{e.class} in #{block.source_location.first}, you may need to write your own down method"}
194
+ else
195
+ actions = @actions.reverse
196
+ Proc.new do
197
+ actions.each do |a|
198
+ pr = a.last.is_a?(Proc) ? a.pop : nil
199
+ # Allow calling private methods as the reversing methods are private
200
+ send(*a, &pr)
201
+ end
205
202
  end
206
203
  end
207
204
 
@@ -270,7 +267,7 @@ module Sequel
270
267
  end
271
268
 
272
269
  def add_primary_key(*args)
273
- raise if args.first.is_a?(Array)
270
+ super if args.first.is_a?(Array)
274
271
  @actions << [:drop_column, args.first]
275
272
  end
276
273
 
@@ -127,7 +127,7 @@ module Sequel
127
127
 
128
128
  # Run the SQL on the database, reparsing the enum labels after it is run.
129
129
  def _process_enum_change_sql(sql)
130
- run(sql)
130
+ run(sql.freeze)
131
131
  parse_enum_labels
132
132
  nil
133
133
  end