sequel 5.102.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 (33) 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 +13 -7
  13. data/lib/sequel/connection_pool/timed_queue.rb +11 -6
  14. data/lib/sequel/database/schema_generator.rb +36 -5
  15. data/lib/sequel/database/schema_methods.rb +1 -1
  16. data/lib/sequel/dataset/placeholder_literalizer.rb +3 -0
  17. data/lib/sequel/dataset/prepared_statements.rb +7 -4
  18. data/lib/sequel/dataset/query.rb +6 -3
  19. data/lib/sequel/dataset/sql.rb +6 -1
  20. data/lib/sequel/extensions/dataset_run.rb +2 -2
  21. data/lib/sequel/extensions/date_arithmetic.rb +6 -6
  22. data/lib/sequel/extensions/lit_require_frozen.rb +131 -0
  23. data/lib/sequel/extensions/migration.rb +14 -17
  24. data/lib/sequel/extensions/pg_enum.rb +1 -1
  25. data/lib/sequel/model/associations.rb +180 -6
  26. data/lib/sequel/plugins/dataset_associations.rb +20 -1
  27. data/lib/sequel/plugins/dirty.rb +5 -2
  28. data/lib/sequel/plugins/many_through_many.rb +21 -0
  29. data/lib/sequel/plugins/mssql_optimistic_locking.rb +1 -1
  30. data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +1 -1
  31. data/lib/sequel/plugins/serialization.rb +10 -1
  32. data/lib/sequel/version.rb +1 -1
  33. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92eed5c337b361dfde5b948323fb6a0a22ad89ecb2473c284d9e5b70a5eed8e2
4
- data.tar.gz: dd07fefb9f3d2821479be092a12b87c6e7b92f5d8d8c314ff656741d31e75531
3
+ metadata.gz: a28329284b305f2ea739dc8ca05aa690db11c9c68c0dcd5fb47035ee621edbf9
4
+ data.tar.gz: 8cdf6a6239baddff4fc926718409413713801d4d49481b7f151a439fa8a8bbe1
5
5
  SHA512:
6
- metadata.gz: ea53c48308a031896491a59e57ca3902a9a3478b8d09d12de6548a927991419054fa8698463c18c18cef9801a9c4629dc299b27df03245272bb2296f79ccdc1c
7
- data.tar.gz: adfd92660e225f0d67948dfdc4aa57c2c61c1118265908f8b6a26fe23179573aff4195aec759575abea984e46fa5d1b62701b75caa8ce7e78838f6434e208527
6
+ metadata.gz: 9f1f61fc96b631a8c081b831fa8a9dfa962908bcb11bde6975cc75c234e416941492ac140b6b9d995aa8fcd84ffec0c519eb11e4aba43b16fe779cf66e23973e
7
+ data.tar.gz: ae638f7d62552732ebe9fd7af00fb3bdb931c078cabb3cc134e3d370a3104b5ce711a454d57f14e427fb7e522b95755366d7128cb3d1de3f325da19dd3e8ce5f
data/MIT-LICENSE CHANGED
@@ -1,5 +1,5 @@
1
1
  Copyright (c) 2007-2008 Sharon Rosner
2
- Copyright (c) 2008-2023 Jeremy Evans
2
+ Copyright (c) 2008-2026 Jeremy Evans and contributors
3
3
 
4
4
  Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  of this software and associated documentation files (the "Software"), to
@@ -1,5 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ # SEQUEL6: Remove
4
+
3
5
  Sequel::JDBC.load_driver('Java::OrgApacheDerbyJdbc::EmbeddedDriver', :Derby)
4
6
  require_relative 'transactions'
5
7
  require_relative '../utils/columns_limit_1'
@@ -18,7 +18,7 @@ module Sequel
18
18
  include AutoCastDateAndTime
19
19
 
20
20
  def commit_prepared_transaction(transaction_id, opts=OPTS)
21
- run("COMMIT TRANSACTION #{transaction_id}", opts)
21
+ run("COMMIT TRANSACTION #{transaction_id}".freeze, opts)
22
22
  end
23
23
 
24
24
  def database_type
@@ -36,7 +36,7 @@ module Sequel
36
36
  end
37
37
 
38
38
  def rollback_prepared_transaction(transaction_id, opts=OPTS)
39
- run("ROLLBACK TRANSACTION #{transaction_id}", opts)
39
+ run("ROLLBACK TRANSACTION #{transaction_id}".freeze, opts)
40
40
  end
41
41
 
42
42
  # H2 uses an IDENTITY type for primary keys
@@ -727,7 +727,7 @@ module Sequel
727
727
  prepared_args << y
728
728
  i = prepared_args.length
729
729
  end
730
- LiteralString.new("#{prepared_arg_placeholder}#{i}")
730
+ LiteralString.new("#{prepared_arg_placeholder}#{i}".freeze)
731
731
  end
732
732
  end
733
733
 
@@ -106,7 +106,7 @@ module Sequel
106
106
  values << value
107
107
  end
108
108
 
109
- sql = "DECLARE #{declarations.join(', ')}; EXECUTE @RC = #{name} #{values.join(', ')}; SELECT #{names.join(', ')}"
109
+ sql = "DECLARE #{declarations.join(', ')}; EXECUTE @RC = #{name} #{values.join(', ')}; SELECT #{names.join(', ')}".freeze
110
110
 
111
111
  ds = dataset.with_sql(sql)
112
112
  ds = ds.server(opts[:server]) if opts[:server]
@@ -400,7 +400,7 @@ module Sequel
400
400
  # Error if a string is given.
401
401
  def create_table_as(name, ds, options)
402
402
  raise(Error, "must provide dataset instance as value of create_table :as option on MSSQL") unless ds.is_a?(Sequel::Dataset)
403
- run(ds.into(name).sql)
403
+ run(ds.into(name).sql.freeze)
404
404
  end
405
405
 
406
406
  DATABASE_ERROR_REGEXPS = {
@@ -878,7 +878,7 @@ module Sequel
878
878
  elsif @opts[:output]
879
879
  # no transaction: our multi_insert_sql_strategy should guarantee
880
880
  # that there's only ever a single statement.
881
- sql = multi_insert_sql(columns, values)[0]
881
+ sql = multi_insert_sql(columns, values)[0].freeze
882
882
  naked.with_sql(sql).map{|v| v.length == 1 ? v.values.first : v}
883
883
  else
884
884
  super
@@ -40,7 +40,7 @@ module Sequel
40
40
  end
41
41
 
42
42
  def commit_prepared_transaction(transaction_id, opts=OPTS)
43
- run("XA COMMIT #{literal(transaction_id)}", opts)
43
+ run("XA COMMIT #{literal(transaction_id)}".freeze, opts)
44
44
  end
45
45
 
46
46
  def database_type
@@ -103,7 +103,7 @@ module Sequel
103
103
  sql += " FROM #{literal(schema)}"
104
104
  end
105
105
 
106
- metadata_dataset.with_sql(sql).each do |r|
106
+ metadata_dataset.with_sql(sql.freeze).each do |r|
107
107
  name = r[:Key_name]
108
108
  next if name == 'PRIMARY'
109
109
  name = m.call(name)
@@ -115,7 +115,7 @@ module Sequel
115
115
  end
116
116
 
117
117
  def rollback_prepared_transaction(transaction_id, opts=OPTS)
118
- run("XA ROLLBACK #{literal(transaction_id)}", opts)
118
+ run("XA ROLLBACK #{literal(transaction_id)}".freeze, opts)
119
119
  end
120
120
 
121
121
  # Whether the database is MariaDB and not MySQL
@@ -780,7 +780,8 @@ module Sequel
780
780
  # Load the PrettyTable class, needed for explain output
781
781
  Sequel.extension(:_pretty_table) unless defined?(Sequel::PrettyTable)
782
782
 
783
- ds = db.send(:metadata_dataset).with_sql(((opts[:extended] && (db.mariadb? || db.server_version < 50700)) ? 'EXPLAIN EXTENDED ' : 'EXPLAIN ') + select_sql).naked
783
+ sql = ((opts[:extended] && (db.mariadb? || db.server_version < 50700)) ? 'EXPLAIN EXTENDED ' : 'EXPLAIN ') + select_sql
784
+ ds = db.send(:metadata_dataset).with_sql(sql.freeze).naked
784
785
  rows = ds.all
785
786
  Sequel::PrettyTable.string(rows, ds.columns)
786
787
  end
@@ -339,7 +339,7 @@ module Sequel
339
339
  end
340
340
 
341
341
  def commit_prepared_transaction(transaction_id, opts=OPTS)
342
- run("COMMIT PREPARED #{literal(transaction_id)}", opts)
342
+ run("COMMIT PREPARED #{literal(transaction_id)}".freeze, opts)
343
343
  end
344
344
 
345
345
  # A hash of metadata for CHECK constraints on the table.
@@ -416,7 +416,7 @@ module Sequel
416
416
  return if already_identity
417
417
 
418
418
  transaction(server_hash) do
419
- run("ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(column)} DROP DEFAULT", server_hash)
419
+ run("ALTER TABLE #{quote_schema_table(table)} ALTER COLUMN #{quote_identifier(column)} DROP DEFAULT".freeze, server_hash)
420
420
 
421
421
  ds.from(:pg_depend).
422
422
  where(:classid=>pg_class, :objid=>seq_oid, :objsubid=>0, :deptype=>'a').
@@ -453,7 +453,7 @@ module Sequel
453
453
  # often used here if :security_definer is used.
454
454
  # :strict :: Makes the function return NULL when any argument is NULL.
455
455
  def create_function(name, definition, opts=OPTS)
456
- self << create_function_sql(name, definition, opts)
456
+ self << create_function_sql(name, definition, opts).freeze
457
457
  end
458
458
 
459
459
  # Create the procedural language in the database. Arguments:
@@ -464,7 +464,7 @@ module Sequel
464
464
  # :trusted :: Marks the language being created as trusted, allowing unprivileged users to create functions using this language.
465
465
  # :validator :: The name of previously registered function used as a validator of functions defined in this language.
466
466
  def create_language(name, opts=OPTS)
467
- self << create_language_sql(name, opts)
467
+ self << create_language_sql(name, opts).freeze
468
468
  end
469
469
 
470
470
  # Create a schema in the database. Arguments:
@@ -473,7 +473,7 @@ module Sequel
473
473
  # :if_not_exists :: Don't raise an error if the schema already exists (PostgreSQL 9.3+)
474
474
  # :owner :: The owner to set for the schema (defaults to current user if not specified)
475
475
  def create_schema(name, opts=OPTS)
476
- self << create_schema_sql(name, opts)
476
+ self << create_schema_sql(name, opts).freeze
477
477
  end
478
478
 
479
479
  # Support partitions of tables using the :partition_of option.
@@ -509,7 +509,7 @@ module Sequel
509
509
  # :replace :: Replace the trigger with the same name if it already exists (PostgreSQL 14+).
510
510
  # :when :: A filter to use for the trigger
511
511
  def create_trigger(table, name, function, opts=OPTS)
512
- self << create_trigger_sql(table, name, function, opts)
512
+ self << create_trigger_sql(table, name, function, opts).freeze
513
513
  end
514
514
 
515
515
  def database_type
@@ -542,7 +542,7 @@ module Sequel
542
542
  # default is plpgsql. Can be specified as a string or a symbol.
543
543
  def do(code, opts=OPTS)
544
544
  language = opts[:language]
545
- run "DO #{"LANGUAGE #{literal(language.to_s)} " if language}#{literal(code)}"
545
+ run "DO #{"LANGUAGE #{literal(language.to_s)} " if language}#{literal(code)}".freeze
546
546
  end
547
547
 
548
548
  # Drops the function from the database. Arguments:
@@ -552,7 +552,7 @@ module Sequel
552
552
  # :cascade :: Drop other objects depending on this function.
553
553
  # :if_exists :: Don't raise an error if the function doesn't exist.
554
554
  def drop_function(name, opts=OPTS)
555
- self << drop_function_sql(name, opts)
555
+ self << drop_function_sql(name, opts).freeze
556
556
  end
557
557
 
558
558
  # Drops a procedural language from the database. Arguments:
@@ -561,7 +561,7 @@ module Sequel
561
561
  # :cascade :: Drop other objects depending on this function.
562
562
  # :if_exists :: Don't raise an error if the function doesn't exist.
563
563
  def drop_language(name, opts=OPTS)
564
- self << drop_language_sql(name, opts)
564
+ self << drop_language_sql(name, opts).freeze
565
565
  end
566
566
 
567
567
  # Drops a schema from the database. Arguments:
@@ -570,7 +570,7 @@ module Sequel
570
570
  # :cascade :: Drop all objects in this schema.
571
571
  # :if_exists :: Don't raise an error if the schema doesn't exist.
572
572
  def drop_schema(name, opts=OPTS)
573
- self << drop_schema_sql(name, opts)
573
+ self << drop_schema_sql(name, opts).freeze
574
574
  remove_all_cached_schemas
575
575
  end
576
576
 
@@ -581,7 +581,7 @@ module Sequel
581
581
  # :cascade :: Drop other objects depending on this function.
582
582
  # :if_exists :: Don't raise an error if the function doesn't exist.
583
583
  def drop_trigger(table, name, opts=OPTS)
584
- self << drop_trigger_sql(table, name, opts)
584
+ self << drop_trigger_sql(table, name, opts).freeze
585
585
  end
586
586
 
587
587
  # Return full foreign key information using the pg system tables, including
@@ -745,7 +745,7 @@ module Sequel
745
745
  # name :: Current name of the schema
746
746
  # opts :: New name for the schema
747
747
  def rename_schema(name, new_name)
748
- self << rename_schema_sql(name, new_name)
748
+ self << rename_schema_sql(name, new_name).freeze
749
749
  remove_all_cached_schemas
750
750
  end
751
751
 
@@ -756,7 +756,7 @@ module Sequel
756
756
  # DB.refresh_view(:items_view, concurrently: true)
757
757
  # # REFRESH MATERIALIZED VIEW CONCURRENTLY items_view
758
758
  def refresh_view(name, opts=OPTS)
759
- run "REFRESH MATERIALIZED VIEW#{' CONCURRENTLY' if opts[:concurrently]} #{quote_schema_table(name)}"
759
+ run "REFRESH MATERIALIZED VIEW#{' CONCURRENTLY' if opts[:concurrently]} #{quote_schema_table(name)}".freeze
760
760
  end
761
761
 
762
762
  # Reset the primary key sequence for the given table, basing it on the
@@ -769,7 +769,7 @@ module Sequel
769
769
  table = Sequel.qualify(s, t) if s
770
770
 
771
771
  if server_version >= 100000
772
- seq_ds = metadata_dataset.from(:pg_sequence).where(:seqrelid=>regclass_oid(LiteralString.new(seq)))
772
+ seq_ds = metadata_dataset.from(:pg_sequence).where(:seqrelid=>regclass_oid(LiteralString.new(seq.freeze)))
773
773
  increment_by = :seqincrement
774
774
  min_value = :seqmin
775
775
  # :nocov:
@@ -784,7 +784,7 @@ module Sequel
784
784
  end
785
785
 
786
786
  def rollback_prepared_transaction(transaction_id, opts=OPTS)
787
- run("ROLLBACK PREPARED #{literal(transaction_id)}", opts)
787
+ run("ROLLBACK PREPARED #{literal(transaction_id)}".freeze, opts)
788
788
  end
789
789
 
790
790
  # PostgreSQL uses SERIAL psuedo-type instead of AUTOINCREMENT for
@@ -1469,7 +1469,7 @@ module Sequel
1469
1469
 
1470
1470
  def column_references_add_period(cols)
1471
1471
  cols= cols.dup
1472
- cols[-1] = Sequel.lit("PERIOD #{quote_identifier(cols[-1])}")
1472
+ cols[-1] = Sequel.lit("PERIOD #{quote_identifier(cols[-1])}".freeze)
1473
1473
  cols
1474
1474
  end
1475
1475
 
@@ -195,7 +195,7 @@ module Sequel
195
195
 
196
196
  # Dataset used for parsing schema
197
197
  def _parse_pragma_ds(table_name, opts)
198
- metadata_dataset.with_sql("PRAGMA table_#{'x' if sqlite_version > 33100}info(?)", input_identifier_meth(opts[:dataset]).call(table_name))
198
+ metadata_dataset.with_sql("PRAGMA table_#{'x' if sqlite_version > 33100}info(?)".freeze, input_identifier_meth(opts[:dataset]).call(table_name))
199
199
  end
200
200
 
201
201
  # Run all alter_table commands in a transaction. This is technically only
@@ -407,7 +407,7 @@ module Sequel
407
407
  def defined_columns_for(table)
408
408
  cols = parse_pragma(table, OPTS)
409
409
  cols.each do |c|
410
- c[:default] = LiteralString.new(c[:default]) if c[:default]
410
+ c[:default] = LiteralString.new(c[:default]).freeze if c[:default]
411
411
  c[:type] = c[:db_type]
412
412
  end
413
413
  cols
@@ -684,7 +684,7 @@ module Sequel
684
684
  # Load the PrettyTable class, needed for explain output
685
685
  Sequel.extension(:_pretty_table) unless defined?(Sequel::PrettyTable)
686
686
 
687
- ds = db.send(:metadata_dataset).clone(:sql=>"EXPLAIN #{select_sql}")
687
+ ds = db.send(:metadata_dataset).clone(:sql=>"EXPLAIN #{select_sql}".freeze)
688
688
  rows = ds.all
689
689
  Sequel::PrettyTable.string(rows, ds.columns)
690
690
  end
@@ -388,7 +388,7 @@ module Sequel
388
388
  # SQLite uses a : before the name of the argument for named
389
389
  # arguments.
390
390
  def prepared_arg(k)
391
- LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}")
391
+ LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}".freeze)
392
392
  end
393
393
  end
394
394
 
@@ -205,7 +205,7 @@ module Sequel
205
205
  private
206
206
 
207
207
  def prepared_arg(k)
208
- LiteralString.new("@#{k.to_s.gsub('.', '__')}")
208
+ LiteralString.new("@#{k.to_s.gsub('.', '__')}".freeze)
209
209
  end
210
210
  end
211
211
 
@@ -91,14 +91,16 @@ class Sequel::ShardedTimedQueueConnectionPool < Sequel::ConnectionPool
91
91
  # creates new connections to the database.
92
92
  #
93
93
  # If the :server option is provided, it should be a symbol or array of symbols,
94
- # and then the method will only disconnect connectsion from those specified shards.
94
+ # and then the method will only disconnect connections from those specified shards.
95
95
  def disconnect(opts=OPTS)
96
96
  (opts[:server] ? Array(opts[:server]) : sync{@servers.keys}).each do |server|
97
97
  raise Sequel::Error, "invalid server" unless queue = sync{@queues[server]}
98
+ nconns = 0
98
99
  while conn = available(queue, server)
100
+ nconns += 1
99
101
  disconnect_pool_connection(conn, server)
100
102
  end
101
- fill_queue(server)
103
+ fill_queue(server, nconns)
102
104
  end
103
105
  nil
104
106
  end
@@ -132,7 +134,7 @@ class Sequel::ShardedTimedQueueConnectionPool < Sequel::ConnectionPool
132
134
  conn = nil
133
135
  disconnect_pool_connection(oconn, server) if oconn
134
136
  sync{@allocated[server].delete(t)}
135
- fill_queue(server)
137
+ fill_queue(server, 1)
136
138
  end
137
139
  raise
138
140
  ensure
@@ -250,11 +252,15 @@ class Sequel::ShardedTimedQueueConnectionPool < Sequel::ConnectionPool
250
252
  # after disconnecting to potentially add new connections to the
251
253
  # pool, so the threads that are currently waiting for connections
252
254
  # do not timeout after the pool is no longer full.
253
- def fill_queue(server)
255
+ #
256
+ # nconns specifies the maximum number of connections to add, which should
257
+ # be the number of connections that were disconnected.
258
+ def fill_queue(server, nconns)
254
259
  queue = sync{@queues[server]}
255
- if queue.num_waiting > 0
260
+ if nconns > 0 && queue.num_waiting > 0
256
261
  Thread.new do
257
- while queue.num_waiting > 0 && (conn = try_make_new(server))
262
+ while nconns > 0 && queue.num_waiting > 0 && (conn = try_make_new(server))
263
+ nconns -= 1
258
264
  queue.push(conn)
259
265
  end
260
266
  end
@@ -303,7 +309,7 @@ class Sequel::ShardedTimedQueueConnectionPool < Sequel::ConnectionPool
303
309
  ensure
304
310
  if to_disconnect
305
311
  to_disconnect.each{|conn| disconnect_pool_connection(conn, server)}
306
- fill_queue(server)
312
+ fill_queue(server, to_disconnect.size)
307
313
  end
308
314
  end
309
315
  end
@@ -59,10 +59,12 @@ class Sequel::TimedQueueConnectionPool < Sequel::ConnectionPool
59
59
  # Once a connection is requested using #hold, the connection pool
60
60
  # creates new connections to the database.
61
61
  def disconnect(opts=OPTS)
62
+ nconns = 0
62
63
  while conn = available
64
+ nconns += 1
63
65
  disconnect_connection(conn)
64
66
  end
65
- fill_queue
67
+ fill_queue(nconns)
66
68
  nil
67
69
  end
68
70
 
@@ -94,7 +96,7 @@ class Sequel::TimedQueueConnectionPool < Sequel::ConnectionPool
94
96
  conn = nil
95
97
  disconnect_connection(oconn) if oconn
96
98
  sync{@allocated.delete(t)}
97
- fill_queue
99
+ fill_queue(1)
98
100
  end
99
101
  raise
100
102
  ensure
@@ -156,10 +158,13 @@ class Sequel::TimedQueueConnectionPool < Sequel::ConnectionPool
156
158
  # after disconnecting to potentially add new connections to the
157
159
  # pool, so the threads that are currently waiting for connections
158
160
  # do not timeout after the pool is no longer full.
159
- def fill_queue
160
- if @queue.num_waiting > 0
161
+ #
162
+ # nconns specifies the maximum number of connections to add, which should
163
+ # be the number of connections that were disconnected.
164
+ def fill_queue(nconns)
165
+ if nconns > 0 && @queue.num_waiting > 0
161
166
  Thread.new do
162
- while @queue.num_waiting > 0 && (conn = try_make_new)
167
+ while nconns > 0 && @queue.num_waiting > 0 && (conn = try_make_new)
163
168
  @queue.push(conn)
164
169
  end
165
170
  end
@@ -207,7 +212,7 @@ class Sequel::TimedQueueConnectionPool < Sequel::ConnectionPool
207
212
  ensure
208
213
  if to_disconnect
209
214
  to_disconnect.each{|conn| disconnect_connection(conn)}
210
- fill_queue
215
+ fill_queue(to_disconnect.size)
211
216
  end
212
217
  end
213
218
  end
@@ -3,6 +3,33 @@
3
3
  module Sequel
4
4
  # The Schema module holds the schema generators.
5
5
  module Schema
6
+ module ColumnOptionMerger
7
+ private
8
+
9
+ # Merge given options into the column's default options. For backwards compatibility,
10
+ # the options take priority, but in cases where the option value overrides the argument
11
+ # value, and the values are different, we warn as this is likely to be an error in the
12
+ # code.
13
+ def _merge_column_options(defaults, opts)
14
+ defaults.merge!(opts) do |k, defv, v|
15
+ unless defv == v
16
+ # :nocov:
17
+ if RUBY_VERSION >= "3.2"
18
+ # :nocov:
19
+ caller_loc = Thread.each_caller_location do |loc|
20
+ break loc unless loc.path == __FILE__
21
+ end
22
+ caller_loc &&= "#{caller_loc.path}:#{caller_loc.lineno}: "
23
+ end
24
+ warn("#{caller_loc}#{k.inspect} option value (#{v.inspect}) overrides argument value (#{defv.inspect})")
25
+ end
26
+
27
+ v
28
+ end
29
+ end
30
+ end
31
+ private_constant :ColumnOptionMerger
32
+
6
33
  # Schema::CreateTableGenerator is an internal class that the user is not expected
7
34
  # to instantiate directly. Instances are created by Database#create_table.
8
35
  # It is used to specify table creation parameters. It takes a Database
@@ -17,6 +44,8 @@ module Sequel
17
44
  # For more information on Sequel's support for schema modification, see
18
45
  # the {"Schema Modification" guide}[rdoc-ref:doc/schema_modification.rdoc].
19
46
  class CreateTableGenerator
47
+ include ColumnOptionMerger
48
+
20
49
  # Classes specifying generic types that Sequel will convert to database-specific types.
21
50
  GENERIC_TYPES=%w'String Integer Float Numeric BigDecimal Date DateTime Time File TrueClass FalseClass'.freeze
22
51
 
@@ -173,13 +202,13 @@ module Sequel
173
202
  # :clustered :: When using :primary_key or :unique, marks the primary key or unique
174
203
  # constraint as CLUSTERED (if true), or NONCLUSTERED (if false).
175
204
  def column(name, type, opts = OPTS)
176
- columns << {:name => name, :type => type}.merge!(opts)
205
+ columns << _merge_column_options({:name => name, :type => type}, opts)
177
206
  if index_opts = opts[:index]
178
207
  index(name, index_opts.is_a?(Hash) ? index_opts : OPTS)
179
208
  end
180
209
  nil
181
210
  end
182
-
211
+
183
212
  # Adds a named CHECK constraint (or unnamed if name is nil),
184
213
  # with the given block or args. To provide options for the constraint, pass
185
214
  # a hash as the first argument.
@@ -246,7 +275,7 @@ module Sequel
246
275
  opts.merge(:table=>table)
247
276
  end
248
277
  return composite_foreign_key(name, opts) if name.is_a?(Array)
249
- column(name, Integer, opts)
278
+ column(name, opts.fetch(:type, Integer), opts)
250
279
  end
251
280
 
252
281
  # Add a full text index on the given columns.
@@ -429,6 +458,8 @@ module Sequel
429
458
  # For more information on Sequel's support for schema modification, see
430
459
  # the {"Schema Modification" guide}[link:files/doc/schema_modification_rdoc.html].
431
460
  class AlterTableGenerator
461
+ include ColumnOptionMerger
462
+
432
463
  # An array of operations to perform
433
464
  attr_reader :operations
434
465
 
@@ -454,7 +485,7 @@ module Sequel
454
485
  # :after :: The name of an existing column that the new column should be positioned after
455
486
  # :first :: Create this new column before all other existing columns
456
487
  def add_column(name, type, opts = OPTS)
457
- op = {:op => :add_column, :name => name, :type => type}.merge!(opts)
488
+ op = _merge_column_options({:op => :add_column, :name => name, :type => type}, opts)
458
489
  index_opts = op.delete(:index)
459
490
  @operations << op
460
491
  add_index(name, index_opts.is_a?(Hash) ? index_opts : OPTS) if index_opts
@@ -519,7 +550,7 @@ module Sequel
519
550
  # sense when using an array of columns.
520
551
  def add_foreign_key(name, table, opts = OPTS)
521
552
  return add_composite_foreign_key(name, table, opts) if name.is_a?(Array)
522
- add_column(name, Integer, {:table=>table}.merge!(opts))
553
+ add_column(name, opts.fetch(:type, Integer), {:table=>table}.merge!(opts))
523
554
  end
524
555
 
525
556
  # Add a full text index on the given columns.
@@ -819,7 +819,7 @@ module Sequel
819
819
  # SELECT sql statement.
820
820
  def create_table_as(name, sql, options)
821
821
  sql = sql.sql if sql.is_a?(Sequel::Dataset)
822
- run(create_table_as_sql(name, sql, options))
822
+ run(create_table_as_sql(name, sql, options).freeze)
823
823
  end
824
824
 
825
825
  # SQL statement for creating a table from the result of a SELECT statement.
@@ -117,6 +117,8 @@ module Sequel
117
117
  frags << final_sql
118
118
  prepared_sql << final_sql
119
119
 
120
+ frags.each(&:freeze)
121
+ frags.freeze
120
122
  [prepared_sql, frags]
121
123
  end
122
124
 
@@ -125,6 +127,7 @@ module Sequel
125
127
  @argn = -1
126
128
  @args = []
127
129
  ds = yield self, dataset
130
+ ds.opts[:sql].freeze
128
131
  sql = ds.clone(:placeholder_literalizer=>self).sql
129
132
 
130
133
  last_offset = 0
@@ -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"
@@ -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
@@ -112,6 +112,8 @@ module Sequel
112
112
  # the dataset unmodified if no SQL limit strategy is needed.
113
113
  def apply_eager_graph_limit_strategy(strategy, ds)
114
114
  case strategy
115
+ when :lateral_subquery
116
+ apply_lateral_subquery_eager_graph_limit_strategy(ds)
115
117
  when :distinct_on
116
118
  apply_distinct_on_eager_limit_strategy(ds.order_prepend(*self[:order]))
117
119
  when :window_function
@@ -298,7 +300,12 @@ module Sequel
298
300
  # Correlated subqueries are not supported for regular eager loading
299
301
  strategy = :ruby if strategy == :correlated_subquery
300
302
  strategy = nil if strategy == :ruby && assign_singular?
301
- objects = apply_eager_limit_strategy(ds, strategy, eager_limit).all
303
+
304
+ objects = if strategy == :lateral_subquery
305
+ apply_lateral_subquery_eager_limit_strategy(ds, ids, eager_limit).all
306
+ else
307
+ apply_eager_limit_strategy(ds, strategy, eager_limit).all
308
+ end
302
309
 
303
310
  if strategy == :window_function
304
311
  delete_rn = ds.row_number_column
@@ -320,7 +327,7 @@ module Sequel
320
327
  end
321
328
  loader.append_sql(sql, *k)
322
329
  end
323
- objects.concat(ds.with_sql(sql).to_a)
330
+ objects.concat(ds.with_sql(sql.freeze).to_a)
324
331
  end
325
332
  ds = ds.eager(cascade) if cascade
326
333
  ds.send(:post_load, objects)
@@ -366,7 +373,12 @@ module Sequel
366
373
  # filtered. Works by using a subquery to test that the objects passed
367
374
  # also meet the association filter criteria.
368
375
  def filter_by_associations_conditions_expression(obj)
369
- ds = filter_by_associations_conditions_dataset.where(filter_by_associations_conditions_subquery_conditions(obj))
376
+ ds = if filter_by_associations_limit_strategy == :lateral_subquery
377
+ apply_lateral_subquery_filter_limit_strategy(associated_eager_dataset, obj)
378
+ else
379
+ filter_by_associations_conditions_dataset.where(filter_by_associations_conditions_subquery_conditions(obj))
380
+ end
381
+
370
382
  {filter_by_associations_conditions_key=>ds}
371
383
  end
372
384
 
@@ -754,6 +766,61 @@ module Sequel
754
766
  end
755
767
  end
756
768
 
769
+ def lateral_subquery_eager_limit_strategy_lateral_dataset(ds, limit_and_offset)
770
+ ds.
771
+ where(Array(filter_by_associations_conditions_key).zip(Array(filter_by_associations_conditions_associated_keys))).
772
+ limit(*limit_and_offset).
773
+ lateral
774
+ end
775
+
776
+ def apply_lateral_subquery_eager_limit_strategy(ds, ids, limit_and_offset)
777
+ table_name = self[:model].table_name
778
+ associated_table_name = associated_class.table_name
779
+
780
+ associated_class.
781
+ from(table_name).
782
+ select_all(associated_table_name).
783
+ join(lateral_subquery_eager_limit_strategy_lateral_dataset(ds, limit_and_offset).as(associated_table_name), true).
784
+ order(*self[:order])
785
+ end
786
+
787
+ # Return an expression to filter the filter by associations dataset to only
788
+ # rows related to given objects.
789
+ def _lateral_subquery_filter_limit_strategy_conditions(obj, key, value_method, value_column)
790
+ value = case obj
791
+ when Array
792
+ if key.is_a?(Array)
793
+ key_methods = Array(value_method)
794
+ obj.map{|o| key_methods.map{|meth| o.send(meth)}}
795
+ else
796
+ obj.map{|o| o.send(value_method)}
797
+ end
798
+ when Sequel::Dataset
799
+ obj.select(*Array(qualify(associated_class.table_name, value_column)))
800
+ else
801
+ if key.is_a?(Array)
802
+ return Array(key).zip(Array(value_method).map{|meth| obj.send(meth)})
803
+ else
804
+ obj.send(value_method)
805
+ end
806
+ end
807
+
808
+ {key => value}
809
+ end
810
+
811
+ def lateral_subquery_filter_limit_strategy_lateral_dataset(ds, obj)
812
+ lateral_subquery_filter_limit_strategy_filter_lateral_dataset(ds.
813
+ select(*qualify(associated_class.table_name, associated_class.primary_key)).
814
+ limit(*limit_and_offset).
815
+ lateral)
816
+ end
817
+
818
+ def apply_lateral_subquery_filter_limit_strategy(ds, obj)
819
+ lateral_subquery_filter_limit_strategy_filter_dataset(self[:model].
820
+ select(*lateral_subquery_filter_limit_strategy_lateral_dataset_select).
821
+ join(lateral_subquery_filter_limit_strategy_lateral_dataset(ds, obj).as(associated_class.table_name), filter_by_associations_conditions_subquery_conditions(obj)), obj)
822
+ end
823
+
757
824
  # Whether to limit the associated dataset to a single row.
758
825
  def limit_to_single_row?
759
826
  !returns_array?
@@ -776,7 +843,12 @@ module Sequel
776
843
  end
777
844
  end
778
845
 
779
- apply_eager_limit_strategy(ds.where(predicate_key=>arg), eager_limit_strategy)
846
+ strategy = eager_limit_strategy
847
+ if strategy == :lateral_subquery
848
+ apply_lateral_subquery_eager_limit_strategy(ds, arg, limit_and_offset)
849
+ else
850
+ apply_eager_limit_strategy(ds.where(predicate_key=>arg), strategy)
851
+ end
780
852
  end
781
853
  end
782
854
  end
@@ -1018,7 +1090,7 @@ module Sequel
1018
1090
  class OneToManyAssociationReflection < AssociationReflection
1019
1091
  ASSOCIATION_TYPES[:one_to_many] = self
1020
1092
 
1021
- # Support a correlated subquery limit strategy when using eager_graph.
1093
+ # Support a lateral_subquery and correlated_subquery limit strategy when using eager_graph.
1022
1094
  def apply_eager_graph_limit_strategy(strategy, ds)
1023
1095
  case strategy
1024
1096
  when :correlated_subquery
@@ -1118,6 +1190,54 @@ module Sequel
1118
1190
  ds.where(qualify(table_alias, primary_key)=>cs)
1119
1191
  end
1120
1192
 
1193
+ # Use a LATERAL subquery to limit the dataset. Note that this will not
1194
+ # work correctly if the associated dataset uses qualified identifers in the WHERE clause,
1195
+ # as they would reference the containing query instead of the subquery.
1196
+ #
1197
+ # This does not contain the conditions that are necessary to join to the
1198
+ # query, since the necessary qualifier is not passed as an argument.
1199
+ def apply_lateral_subquery_eager_graph_limit_strategy(ds)
1200
+ table_name = ds.first_source_alias
1201
+ qualifier = ds.opts[:eager_options][:implicit_qualifier]
1202
+ graph_conditions = self[:_graph_conditions]
1203
+
1204
+ unless Sequel.condition_specifier?(graph_conditions)
1205
+ raise Error, "lateral_subquery eager graph limit strategy only supported when graph conditions are a hash or array of pairs"
1206
+ end
1207
+
1208
+ ds.
1209
+ limit(*limit_and_offset).
1210
+ order(*self[:order]).
1211
+ where(graph_conditions.map{|k, v| [qualify(table_name, k), qualify(qualifier, v)]}).
1212
+ lateral
1213
+ end
1214
+
1215
+ # Avoid setting duplicate predicate condition when using the lateral subquery
1216
+ # eager limit strategy.
1217
+ def eager_loading_set_predicate_condition(ds, eo)
1218
+ eager_limit_strategy == :lateral_subquery ? ds : super
1219
+ end
1220
+
1221
+ def apply_lateral_subquery_eager_limit_strategy(ds, ids, limit_and_offset)
1222
+ super.where(qualify(self[:model].table_name, self[:primary_key]) => ids)
1223
+ end
1224
+
1225
+ def lateral_subquery_filter_limit_strategy_conditions(obj)
1226
+ _lateral_subquery_filter_limit_strategy_conditions(obj, filter_by_associations_conditions_key, self[:key_method], self[:key])
1227
+ end
1228
+
1229
+ def lateral_subquery_filter_limit_strategy_filter_lateral_dataset(ds)
1230
+ ds.where(Array(filter_by_associations_conditions_key).zip(Array(filter_by_associations_conditions_associated_keys)))
1231
+ end
1232
+
1233
+ def lateral_subquery_filter_limit_strategy_filter_dataset(ds, obj)
1234
+ ds.where(lateral_subquery_filter_limit_strategy_conditions(obj))
1235
+ end
1236
+
1237
+ def lateral_subquery_filter_limit_strategy_lateral_dataset_select
1238
+ qualified_primary_key
1239
+ end
1240
+
1121
1241
  # Support correlated subquery strategy when filtering by limited associations.
1122
1242
  def apply_filter_by_associations_limit_strategy(ds)
1123
1243
  case filter_by_associations_limit_strategy
@@ -1432,6 +1552,52 @@ module Sequel
1432
1552
  end
1433
1553
  end
1434
1554
 
1555
+ def lateral_subquery_filter_limit_strategy_conditions(obj)
1556
+ _lateral_subquery_filter_limit_strategy_conditions(obj, lateral_subquery_filter_limit_strategy_conditions_key, right_primary_key_method, right_primary_key)
1557
+ end
1558
+
1559
+ def lateral_subquery_filter_limit_strategy_conditions_key
1560
+ self[:right_key]
1561
+ end
1562
+
1563
+ def lateral_subquery_filter_limit_strategy_filter_lateral_dataset(ds)
1564
+ ds.where(Array(filter_by_associations_conditions_key).zip(Array(filter_by_associations_conditions_associated_keys)))
1565
+ end
1566
+
1567
+ def lateral_subquery_filter_limit_strategy_filter_dataset(ds, obj)
1568
+ ds.where(filter_by_associations_conditions_key => ds.db.from(self[:join_table]).
1569
+ select(*self[:left_key]).
1570
+ where(lateral_subquery_filter_limit_strategy_conditions(obj)))
1571
+ end
1572
+
1573
+ def lateral_subquery_filter_limit_strategy_lateral_dataset_select
1574
+ qualify(self[:model].table_name, self[:left_primary_key])
1575
+ end
1576
+
1577
+ def lateral_subquery_eager_limit_strategy_lateral_dataset(ds, limit_and_offset)
1578
+ ds = super
1579
+ select = ds.opts[:select].dup
1580
+ left_key_alias = self[:left_key_alias]
1581
+ select.pop while (s = select.last).is_a?(Sequel::SQL::AliasedExpression) && (left_key_alias.is_a?(Array) ? left_key_alias.include?(s.alias) : s.alias == left_key_alias)
1582
+ ds = ds.clone(:select=>select)
1583
+ end
1584
+
1585
+ def apply_lateral_subquery_eager_limit_strategy(ds, ids, limit_and_offset)
1586
+ table_name = self[:model].table_name
1587
+ super.
1588
+ select_all(associated_class.table_name).
1589
+ select_append(*qualify(table_name, self[:left_primary_keys]).zip(Array(self[:left_key_alias])).map{|id, aliaz| Sequel.as(id, aliaz)}).
1590
+ where(qualify(table_name, self[:left_primary_key]) => ids)
1591
+ end
1592
+
1593
+ def apply_lateral_subquery_eager_graph_limit_strategy(ds)
1594
+ ds.
1595
+ limit(*limit_and_offset).
1596
+ where(qualify(join_table_alias, self[:left_keys]).zip(qualify(ds.opts[:eager_options][:implicit_qualifier], self[:left_primary_key_columns]))).
1597
+ order(*self[:order]).
1598
+ lateral
1599
+ end
1600
+
1435
1601
  # Use the right_keys from the eager loading options if
1436
1602
  # using a separate query per table.
1437
1603
  def eager_loading_set_predicate_condition(ds, eo)
@@ -2413,9 +2579,14 @@ module Sequel
2413
2579
  conditions = opts[:graph_conditions]
2414
2580
  opts[:cartesian_product_number] ||= one_to_one ? 0 : 1
2415
2581
  graph_block = opts[:graph_block]
2582
+ graph_conditions = opts[:_graph_conditions] = use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions
2416
2583
  opts[:eager_grapher] ||= proc do |eo|
2417
2584
  ds = eo[:self]
2418
- ds = ds.graph(opts.apply_eager_graph_limit_strategy(eo[:limit_strategy], eager_graph_dataset(opts, eo)), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep), &graph_block)
2585
+ graph_limit_strategy = eo[:limit_strategy]
2586
+ egds = opts.apply_eager_graph_limit_strategy(graph_limit_strategy, eager_graph_dataset(opts, eo))
2587
+ graph_conditions_true = true if graph_limit_strategy == :lateral_subquery
2588
+
2589
+ ds = ds.graph(egds, graph_conditions_true || graph_conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep), &graph_block)
2419
2590
  # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
2420
2591
  ds.opts[:eager_graph][:reciprocals][eo[:table_alias]] = opts.reciprocal
2421
2592
  ds
@@ -2498,6 +2669,9 @@ module Sequel
2498
2669
  # Return dataset to graph into given the association reflection, applying the :callback option if set.
2499
2670
  def eager_graph_dataset(opts, eager_options)
2500
2671
  ds = opts.associated_class.dataset
2672
+ if eager_options[:limit_strategy] == :lateral_subquery
2673
+ ds = ds.clone(:eager_options=>eager_options)
2674
+ end
2501
2675
  if opts[:graph_use_association_block] && (b = opts[:block])
2502
2676
  ds = b.call(ds)
2503
2677
  end
@@ -82,8 +82,27 @@ module Sequel
82
82
  # database supports window functions.
83
83
  def associated(name)
84
84
  raise Error, "unrecognized association name: #{name.inspect}" unless r = model.association_reflection(name)
85
- ds = r.associated_class.dataset
85
+ klass = r.associated_class
86
86
  sds = opts[:limit] ? self : unordered
87
+
88
+ if r.send(:filter_by_associations_limit_strategy) == :lateral_subquery
89
+ ds = r.send(:associated_eager_dataset)
90
+
91
+ case r[:type]
92
+ when :one_to_one, :one_to_many
93
+ sds = sds.select(*Array(r.qualified_primary_key))
94
+ else
95
+ sds = sds.select(*r[:left_primary_keys])
96
+ ds = ds.select_all(klass.table_name)
97
+ update_select = true
98
+ end
99
+
100
+ ds = r.send(:apply_lateral_subquery_eager_limit_strategy, ds, sds, r.limit_and_offset)
101
+ ds = ds.clone(:select=>ds.opts[:select][0,1]) if update_select
102
+ return ds.clone(:eager=>nil, :eager_graph=>nil)
103
+ end
104
+
105
+ ds = klass.dataset
87
106
  ds = case r[:type]
88
107
  when :many_to_one
89
108
  ds.where(r.qualified_primary_key=>sds.select(*Array(r[:qualified_key])))
@@ -234,8 +234,11 @@ module Sequel
234
234
  iv.delete(column)
235
235
  end
236
236
  else
237
- check_missing_initial_value(column)
238
- iv[column] = get_column_value(column)
237
+ if db_schema[column]
238
+ check_missing_initial_value(column)
239
+ iv[column] = get_column_value(column)
240
+ end
241
+
239
242
  super
240
243
  end
241
244
  end
@@ -146,6 +146,27 @@ module Sequel
146
146
  ds
147
147
  end
148
148
 
149
+ def lateral_subquery_filter_limit_strategy_conditions_key
150
+ qualify(reverse_edges.first[:table], reverse_edges.first[:left])
151
+ end
152
+
153
+ def lateral_subquery_filter_limit_strategy_filter_lateral_dataset(ds)
154
+ ds.where(Array(qualify(edges.first[:table], edges.first[:right])).zip(Array(qualify(self[:model].table_name, edges.first[:left]))))
155
+ end
156
+
157
+ def lateral_subquery_filter_limit_strategy_filter_dataset(ds, obj)
158
+ first_edge, *remaining_edges = edges
159
+ filter_ds = ds.db.from(first_edge[:table]).
160
+ select(*qualify(first_edge[:table], first_edge[:right])).
161
+ where(lateral_subquery_filter_limit_strategy_conditions(obj))
162
+
163
+ remaining_edges.each do |edge|
164
+ filter_ds = filter_ds.join(edge[:table], Array(edge[:right]).zip(Array(edge[:left])))
165
+ end
166
+
167
+ ds.where(qualify(self[:model].table_name, self[:left_primary_key]) => filter_ds)
168
+ end
169
+
149
170
  # Make sure to use unique table aliases when lazy loading or eager loading
150
171
  def calculate_reverse_edge_aliases(reverse_edges)
151
172
  aliases = [associated_class.table_name]
@@ -55,7 +55,7 @@ module Sequel
55
55
  def _update_without_checking(columns)
56
56
  ds = _update_dataset
57
57
  lc = model.lock_column
58
- rows = ds.clone(ds.send(:default_server_opts, :sql=>ds.output(nil, [Sequel[:inserted][lc]]).update_sql(columns))).all
58
+ rows = ds.clone(ds.send(:default_server_opts, :sql=>ds.output(nil, [Sequel[:inserted][lc]]).update_sql(columns).freeze)).all
59
59
  values[lc] = rows.first[lc] unless rows.empty?
60
60
  rows.length
61
61
  end
@@ -99,7 +99,7 @@ module Sequel
99
99
  # Add an RETURNING clause to fetch the updated xmin when updating the row.
100
100
  def _update_without_checking(columns)
101
101
  ds = _update_dataset
102
- rows = ds.clone(ds.send(:default_server_opts, :sql=>ds.returning(:xmin).update_sql(columns))).all
102
+ rows = ds.clone(ds.send(:default_server_opts, :sql=>ds.returning(:xmin).update_sql(columns).freeze)).all
103
103
  values[:xmin] = rows.first[:xmin] unless rows.empty?
104
104
  rows.length
105
105
  end
@@ -30,6 +30,15 @@ module Sequel
30
30
  # Otherwise, it is possible that the default column accessors will take
31
31
  # precedence.
32
32
  #
33
+ # Note that use of an unsafe serialization method can result in an attack vector
34
+ # (potentially allowing remote code execution) if an attacker has the ability to
35
+ # store data directly in the underlying column. This would affect the marshal
36
+ # serialization format, and on older versions of Ruby, potentially the yaml and
37
+ # json serialization formats as well. It can also affect custom formats. You
38
+ # should ensure that attackers do not have access to store data directly in the
39
+ # underlying column when using this plugin (especially when using an unsafe
40
+ # serialization method).
41
+ #
33
42
  # == Example
34
43
  #
35
44
  # # Require json if you plan to use it, as the plugin doesn't require it for you.
@@ -97,7 +106,7 @@ module Sequel
97
106
  register_format(:marshal, lambda{|v| [Marshal.dump(v)].pack('m')},
98
107
  lambda do |v|
99
108
  # Handle unpacked marshalled data for backwards compat
100
- v = v.unpack('m')[0] unless v[0..1] == "\x04\x08"
109
+ v = v.unpack('m')[0] unless v.start_with?("\x04\x08")
101
110
  Marshal.load(v)
102
111
  end)
103
112
  register_format(:yaml, :to_yaml.to_proc, lambda{|s| YAML.load(s)})
@@ -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 = 102
9
+ MINOR = 104
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.102.0
4
+ version: 5.104.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
@@ -241,6 +241,7 @@ files:
241
241
  - lib/sequel/extensions/inflector.rb
242
242
  - lib/sequel/extensions/integer64.rb
243
243
  - lib/sequel/extensions/is_distinct_from.rb
244
+ - lib/sequel/extensions/lit_require_frozen.rb
244
245
  - lib/sequel/extensions/looser_typecasting.rb
245
246
  - lib/sequel/extensions/migration.rb
246
247
  - lib/sequel/extensions/mssql_emulate_lateral_with_apply.rb