sequel 5.87.0 → 5.89.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/lib/sequel/adapters/ibmdb.rb +1 -0
  3. data/lib/sequel/adapters/mysql2.rb +8 -1
  4. data/lib/sequel/adapters/shared/access.rb +1 -0
  5. data/lib/sequel/adapters/shared/mssql.rb +1 -0
  6. data/lib/sequel/adapters/shared/oracle.rb +1 -0
  7. data/lib/sequel/adapters/shared/postgres.rb +34 -4
  8. data/lib/sequel/core.rb +15 -0
  9. data/lib/sequel/database/dataset_defaults.rb +3 -3
  10. data/lib/sequel/database/misc.rb +5 -1
  11. data/lib/sequel/database/schema_generator.rb +8 -0
  12. data/lib/sequel/dataset/deprecated_singleton_class_methods.rb +1 -1
  13. data/lib/sequel/dataset/prepared_statements.rb +2 -1
  14. data/lib/sequel/dataset/query.rb +2 -2
  15. data/lib/sequel/dataset/sql.rb +6 -1
  16. data/lib/sequel/extensions/connection_validator.rb +15 -10
  17. data/lib/sequel/extensions/migration.rb +19 -3
  18. data/lib/sequel/extensions/null_dataset.rb +2 -2
  19. data/lib/sequel/extensions/pg_row.rb +3 -1
  20. data/lib/sequel/extensions/query_blocker.rb +172 -0
  21. data/lib/sequel/extensions/string_agg.rb +2 -2
  22. data/lib/sequel/extensions/virtual_row_method_block.rb +1 -0
  23. data/lib/sequel/model/base.rb +24 -11
  24. data/lib/sequel/plugins/composition.rb +1 -1
  25. data/lib/sequel/plugins/enum.rb +1 -1
  26. data/lib/sequel/plugins/inverted_subsets.rb +1 -0
  27. data/lib/sequel/plugins/lazy_attributes.rb +1 -1
  28. data/lib/sequel/plugins/nested_attributes.rb +1 -1
  29. data/lib/sequel/plugins/rcte_tree.rb +1 -1
  30. data/lib/sequel/plugins/serialization.rb +11 -5
  31. data/lib/sequel/plugins/sql_comments.rb +1 -1
  32. data/lib/sequel/plugins/static_cache_cache.rb +50 -13
  33. data/lib/sequel/plugins/subset_conditions.rb +1 -0
  34. data/lib/sequel/plugins/subset_static_cache.rb +262 -0
  35. data/lib/sequel/sql.rb +1 -0
  36. data/lib/sequel/version.rb +1 -1
  37. metadata +5 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8be61078e55201001f1041d09be334411bfaef071e6dbeb3c9082c21b0e70dce
4
- data.tar.gz: c877f3832d2ef28b14d7037e6bbf7bbe90d2d6f6391736bc3b9e6bdc142b5ba5
3
+ metadata.gz: 8248e272e580f0e89e508e2aa97ebea83ac083b3f1bd347fc9d34700fd98ae42
4
+ data.tar.gz: a74658ae42f7e087a541056d5f0cac2db4cc67b11ca56367b2bea309e502df49
5
5
  SHA512:
6
- metadata.gz: 7b032d7f8987e0727cc79b98f7f6d5319cd1dc13657c3a2ebc16bff4276d26eb368f1a498d51e27df0aea89184d0b811f6a05355cf3f7a400b45cd8707061f63
7
- data.tar.gz: f2a1326e3635a7840e220b368459407409391507a3e87cb85ac01f4360d5762a9bdf15ee350032875f9c6d202f03b868fd6c7af43e72bf80cca007edd34cc14e
6
+ metadata.gz: c2875a0f53a46efa87b3380b3fe2c1bbae46542076e3e5615ddcd53b55ac6dfefa53fc828ec4f1277df85a7852eb0e2934377d074e58a6410837138ed2e2a7db
7
+ data.tar.gz: 8c1a3591085be13aef51f3bc63f0407249858c57e91c3f314b92d929d362c4aafd0b801ee52ca0f96acb18f6fa133d33c4532b5a0fc13fc61e599ba3e05f4e6c
@@ -7,6 +7,7 @@ module Sequel
7
7
 
8
8
  module IBMDB
9
9
  tt = Class.new do
10
+ Sequel.set_temp_name(self){"Sequel::IBMDB::_TypeTranslator"}
10
11
  def boolean(s) !s.to_i.zero? end
11
12
  def int(s) s.to_i end
12
13
  end.new
@@ -97,7 +97,14 @@ module Sequel
97
97
  synchronize(opts[:server]) do |conn|
98
98
  stmt, ps_sql = conn.prepared_statements[ps_name]
99
99
  unless ps_sql == sql
100
- stmt.close if stmt
100
+ if stmt
101
+ begin
102
+ stmt.close
103
+ rescue ::Mysql2::Error
104
+ # probably Invalid statement handle, can happen from dropping
105
+ # related table, ignore as we won't be using it again.
106
+ end
107
+ end
101
108
  stmt = log_connection_yield("Preparing #{ps_name}: #{sql}", conn){conn.prepare(sql)}
102
109
  conn.prepared_statements[ps_name] = [stmt, sql]
103
110
  end
@@ -88,6 +88,7 @@ module Sequel
88
88
 
89
89
  module DatasetMethods
90
90
  include(Module.new do
91
+ Sequel.set_temp_name(self){"Sequel::Access::DatasetMethods::_SQLMethods"}
91
92
  Dataset.def_sql_method(self, :select, %w'select distinct limit columns into from join where group order having compounds')
92
93
  end)
93
94
  include EmulateOffsetWithReverseAndCount
@@ -574,6 +574,7 @@ module Sequel
574
574
 
575
575
  module DatasetMethods
576
576
  include(Module.new do
577
+ Sequel.set_temp_name(self){"Sequel::MSSQL::DatasetMethods::_SQLMethods"}
577
578
  Dataset.def_sql_method(self, :select, %w'with select distinct limit columns into from lock join where group having compounds order')
578
579
  end)
579
580
  include EmulateOffsetWithRowNumber
@@ -333,6 +333,7 @@ module Sequel
333
333
  BITAND_PROC = lambda{|a, b| Sequel.lit(["CAST(BITAND(", ", ", ") AS INTEGER)"], a, b)}
334
334
 
335
335
  include(Module.new do
336
+ Sequel.set_temp_name(self){"Sequel::Oracle::DatasetMethods::_SQLMethods"}
336
337
  Dataset.def_sql_method(self, :select, %w'with select distinct columns from join where group having compounds order limit lock')
337
338
  end)
338
339
 
@@ -1271,21 +1271,32 @@ module Sequel
1271
1271
 
1272
1272
  # Handle exclusion constraints.
1273
1273
  def constraint_definition_sql(constraint)
1274
- case constraint[:type]
1274
+ case type = constraint[:type]
1275
1275
  when :exclude
1276
1276
  elements = constraint[:elements].map{|c, op| "#{literal(c)} WITH #{op}"}.join(', ')
1277
1277
  sql = String.new
1278
1278
  sql << "#{"CONSTRAINT #{quote_identifier(constraint[:name])} " if constraint[:name]}EXCLUDE USING #{constraint[:using]||'gist'} (#{elements})#{" WHERE #{filter_expr(constraint[:where])}" if constraint[:where]}"
1279
1279
  constraint_deferrable_sql_append(sql, constraint[:deferrable])
1280
1280
  sql
1281
- when :foreign_key, :check
1281
+ when :primary_key, :unique
1282
+ if using_index = constraint[:using_index]
1283
+ sql = String.new
1284
+ sql << "CONSTRAINT #{quote_identifier(constraint[:name])} " if constraint[:name]
1285
+ if type == :primary_key
1286
+ sql << primary_key_constraint_sql_fragment(constraint)
1287
+ else
1288
+ sql << unique_constraint_sql_fragment(constraint)
1289
+ end
1290
+ sql << " USING INDEX " << quote_identifier(using_index)
1291
+ else
1292
+ super
1293
+ end
1294
+ else # when :foreign_key, :check
1282
1295
  sql = super
1283
1296
  if constraint[:not_valid]
1284
1297
  sql << " NOT VALID"
1285
1298
  end
1286
1299
  sql
1287
- else
1288
- super
1289
1300
  end
1290
1301
  end
1291
1302
 
@@ -2388,6 +2399,25 @@ module Sequel
2388
2399
  join_from_sql(:USING, sql)
2389
2400
  end
2390
2401
 
2402
+ # Handle column aliases containing data types, useful for selecting from functions
2403
+ # that return the record data type.
2404
+ def derived_column_list_sql_append(sql, column_aliases)
2405
+ c = false
2406
+ comma = ', '
2407
+ column_aliases.each do |a|
2408
+ sql << comma if c
2409
+ if a.is_a?(Array)
2410
+ raise Error, "column aliases specified as arrays must have only 2 elements, the first is alias name and the second is data type" unless a.length == 2
2411
+ a, type = a
2412
+ identifier_append(sql, a)
2413
+ sql << " " << db.cast_type_literal(type).to_s
2414
+ else
2415
+ identifier_append(sql, a)
2416
+ end
2417
+ c ||= true
2418
+ end
2419
+ end
2420
+
2391
2421
  # Add ON CONFLICT clause if it should be used
2392
2422
  def insert_conflict_sql(sql)
2393
2423
  if opts = @opts[:insert_conflict]
data/lib/sequel/core.rb CHANGED
@@ -164,6 +164,21 @@ module Sequel
164
164
  JSON::ParserError
165
165
  end
166
166
 
167
+ if RUBY_VERSION >= '3.3'
168
+ # Create a new module using the block, and set the temporary name
169
+ # on it using the given a containing module and name.
170
+ def set_temp_name(mod)
171
+ mod.set_temporary_name(yield)
172
+ mod
173
+ end
174
+ # :nocov:
175
+ else
176
+ def set_temp_name(mod)
177
+ mod
178
+ end
179
+ end
180
+ # :nocov:
181
+
167
182
  # Convert given object to json and return the result.
168
183
  # This can be overridden to use an alternative json implementation.
169
184
  def object_to_json(obj, *args, &block)
@@ -17,7 +17,7 @@ module Sequel
17
17
  # as the dataset class.
18
18
  def dataset_class=(c)
19
19
  unless @dataset_modules.empty?
20
- c = Class.new(c)
20
+ c = Sequel.set_temp_name(Class.new(c)){"Sequel::Dataset::_Subclass"}
21
21
  @dataset_modules.each{|m| c.send(:include, m)}
22
22
  end
23
23
  @dataset_class = c
@@ -61,10 +61,10 @@ module Sequel
61
61
  # # SELECT id, name FROM table WHERE active ORDER BY id
62
62
  def extend_datasets(mod=nil, &block)
63
63
  raise(Error, "must provide either mod or block, not both") if mod && block
64
- mod = Dataset::DatasetModule.new(&block) if block
64
+ mod = Sequel.set_temp_name(Dataset::DatasetModule.new(&block)){"Sequel::Dataset::_DatasetModule(#{block.source_location.join(':')})"} if block
65
65
  if @dataset_modules.empty?
66
66
  @dataset_modules = [mod]
67
- @dataset_class = Class.new(@dataset_class)
67
+ @dataset_class = Sequel.set_temp_name(Class.new(@dataset_class)){"Sequel::Dataset::_Subclass"}
68
68
  else
69
69
  @dataset_modules << mod
70
70
  end
@@ -116,6 +116,10 @@ module Sequel
116
116
  # :after_connect :: A callable object called after each new connection is made, with the
117
117
  # connection object (and server argument if the callable accepts 2 arguments),
118
118
  # useful for customizations that you want to apply to all connections.
119
+ # :compare_connections_by_identity :: Whether to use compare_by_identity on hashes that use
120
+ # connection objects as keys. Defaults to true. This should only
121
+ # be set to false to work around bugs in libraries or
122
+ # ruby implementations.
119
123
  # :before_preconnect :: Callable that runs after extensions from :preconnect_extensions are loaded,
120
124
  # but before any connections are created.
121
125
  # :cache_schema :: Whether schema should be cached for this Database instance
@@ -165,7 +169,7 @@ module Sequel
165
169
  @schemas = {}
166
170
  @prepared_statements = {}
167
171
  @transactions = {}
168
- @transactions.compare_by_identity
172
+ @transactions.compare_by_identity if typecast_value_boolean(@opts.fetch(:compare_connections_by_identity, true))
169
173
  @symbol_literal_cache = {}
170
174
 
171
175
  @timezone = nil
@@ -430,6 +430,10 @@ module Sequel
430
430
  # add_unique_constraint(:name, name: :unique_name) # ADD CONSTRAINT unique_name UNIQUE (name)
431
431
  #
432
432
  # Supports the same :deferrable option as CreateTableGenerator#column.
433
+ #
434
+ # PostgreSQL specific options:
435
+ #
436
+ # :using_index :: Use the USING INDEX clause to specify an existing unique index
433
437
  def add_unique_constraint(columns, opts = OPTS)
434
438
  @operations << {:op => :add_constraint, :type => :unique, :columns => Array(columns)}.merge!(opts)
435
439
  nil
@@ -483,6 +487,10 @@ module Sequel
483
487
  #
484
488
  # add_primary_key(:id) # ADD COLUMN id serial PRIMARY KEY
485
489
  # add_primary_key([:artist_id, :name]) # ADD PRIMARY KEY (artist_id, name)
490
+ #
491
+ # PostgreSQL specific options:
492
+ #
493
+ # :using_index :: Use the USING INDEX clause to specify an existing unique index
486
494
  def add_primary_key(name, opts = OPTS)
487
495
  return add_composite_primary_key(name, opts) if name.is_a?(Array)
488
496
  opts = @db.serial_primary_key_options.merge(opts)
@@ -19,7 +19,7 @@ module Sequel
19
19
  def with_extend(*mods, &block)
20
20
  c = _clone(:freeze=>false)
21
21
  c.extend(*mods) unless mods.empty?
22
- c.extend(DatasetModule.new(&block)) if block
22
+ c.extend(Sequel.set_temp_name(DatasetModule.new(&block)){"Sequel::Dataset::_DatasetModule(#{block.source_location.join(':')})"}) if block
23
23
  c.freeze
24
24
  end
25
25
 
@@ -20,7 +20,8 @@ module Sequel
20
20
  def self.prepared_statements_module(code, mods, meths=DEFAULT_PREPARED_STATEMENT_MODULE_METHODS, &block)
21
21
  code = PREPARED_STATEMENT_MODULE_CODE[code] || code
22
22
 
23
- Module.new do
23
+ Module.new do
24
+ Sequel.set_temp_name(self){"Sequel::Dataset::_PreparedStatementsModule(#{block.source_location.join(':') if block})"}
24
25
  Array(mods).each do |mod|
25
26
  include mod
26
27
  end
@@ -1238,9 +1238,9 @@ module Sequel
1238
1238
  # Note that like Object#extend, when multiple modules are provided
1239
1239
  # as arguments the subclass includes the modules in reverse order.
1240
1240
  def with_extend(*mods, &block)
1241
- c = Class.new(self.class)
1241
+ c = Sequel.set_temp_name(Class.new(self.class)){"Sequel::Dataset::_Subclass"}
1242
1242
  c.include(*mods) unless mods.empty?
1243
- c.include(DatasetModule.new(&block)) if block
1243
+ c.include(Sequel.set_temp_name(DatasetModule.new(&block)){"Sequel::Dataset::_DatasetModule(#{block.source_location.join(':')})"}) if block
1244
1244
  o = c.freeze.allocate
1245
1245
  o.instance_variable_set(:@db, @db)
1246
1246
  o.instance_variable_set(:@opts, @opts)
@@ -1032,7 +1032,7 @@ module Sequel
1032
1032
  if column_aliases
1033
1033
  raise Error, "#{db.database_type} does not support derived column lists" unless supports_derived_column_lists?
1034
1034
  sql << '('
1035
- identifier_list_append(sql, column_aliases)
1035
+ derived_column_list_sql_append(sql, column_aliases)
1036
1036
  sql << ')'
1037
1037
  end
1038
1038
  end
@@ -1165,6 +1165,11 @@ module Sequel
1165
1165
  end
1166
1166
  end
1167
1167
 
1168
+ # Append the column aliases to the SQL.
1169
+ def derived_column_list_sql_append(sql, column_aliases)
1170
+ identifier_list_append(sql, column_aliases)
1171
+ end
1172
+
1168
1173
  # Disable caching of SQL for the current dataset
1169
1174
  def disable_sql_caching!
1170
1175
  cache_set(:_no_cache_sql, true)
@@ -105,18 +105,23 @@ module Sequel
105
105
  1.times do
106
106
  if (conn = super) &&
107
107
  (timer = sync{@connection_timestamps.delete(conn)}) &&
108
- Sequel.elapsed_seconds_since(timer) > @connection_validation_timeout &&
109
- !db.valid_connection?(conn)
108
+ Sequel.elapsed_seconds_since(timer) > @connection_validation_timeout
110
109
 
111
- case pool_type
112
- when :sharded_threaded, :sharded_timed_queue
113
- sync{@allocated[a.last].delete(Sequel.current)}
114
- else
115
- sync{@allocated.delete(Sequel.current)}
116
- end
110
+ begin
111
+ valid = db.valid_connection?(conn)
112
+ ensure
113
+ unless valid
114
+ case pool_type
115
+ when :sharded_threaded, :sharded_timed_queue
116
+ sync{@allocated[a.last].delete(Sequel.current)}
117
+ else
118
+ sync{@allocated.delete(Sequel.current)}
119
+ end
117
120
 
118
- disconnect_connection(conn)
119
- redo
121
+ disconnect_connection(conn)
122
+ redo
123
+ end
124
+ end
120
125
  end
121
126
  end
122
127
 
@@ -223,7 +223,7 @@ module Sequel
223
223
  @actions << [:drop_join_table, *args]
224
224
  end
225
225
 
226
- def create_table(name, opts=OPTS)
226
+ def create_table(name, opts=OPTS, &_)
227
227
  @actions << [:drop_table, name, opts]
228
228
  end
229
229
 
@@ -371,7 +371,7 @@ module Sequel
371
371
  #
372
372
  # Part of the +migration+ extension.
373
373
  class Migrator
374
- MIGRATION_FILE_PATTERN = /\A(\d+)_.+\.rb\z/i.freeze
374
+ MIGRATION_FILE_PATTERN = /\A(\d+)_(.+)\.rb\z/i.freeze
375
375
 
376
376
  # Mutex used around migration file loading
377
377
  MUTEX = Mutex.new
@@ -791,7 +791,23 @@ module Sequel
791
791
  next unless MIGRATION_FILE_PATTERN.match(file)
792
792
  files << File.join(directory, file)
793
793
  end
794
- files.sort_by{|f| MIGRATION_FILE_PATTERN.match(File.basename(f))[1].to_i}
794
+ files.sort! do |a, b|
795
+ a_ver, a_name = split_migration_filename(a)
796
+ b_ver, b_name = split_migration_filename(b)
797
+ x = a_ver <=> b_ver
798
+ if x.zero?
799
+ x = a_name <=> b_name
800
+ end
801
+ x
802
+ end
803
+ files
804
+ end
805
+
806
+ # Return an integer and name (without extension) for the given path.
807
+ def split_migration_filename(path)
808
+ version, name = MIGRATION_FILE_PATTERN.match(File.basename(path)).captures
809
+ version = version.to_i
810
+ [version, name]
795
811
  end
796
812
 
797
813
  # Returns tuples of migration, filename, and direction
@@ -63,12 +63,12 @@ module Sequel
63
63
  end
64
64
 
65
65
  # Return self without sending a database query, never yielding.
66
- def each
66
+ def each(&_)
67
67
  self
68
68
  end
69
69
 
70
70
  # Return nil without sending a database query, never yielding.
71
- def fetch_rows(sql)
71
+ def fetch_rows(sql, &_)
72
72
  nil
73
73
  end
74
74
 
@@ -113,6 +113,7 @@ module Sequel
113
113
  # automatically casted to the database type when literalizing.
114
114
  def self.subclass(db_type)
115
115
  Class.new(self) do
116
+ Sequel.set_temp_name(self){"Sequel::Postgres::PGRow::ArrayRow::_Subclass(#{db_type})"}
116
117
  @db_type = db_type
117
118
  end
118
119
  end
@@ -170,6 +171,7 @@ module Sequel
170
171
  # type and columns.
171
172
  def self.subclass(db_type, columns)
172
173
  Class.new(self) do
174
+ Sequel.set_temp_name(self){"Sequel::Postgres::PGRow::HashRow::_Subclass(#{db_type})"}
173
175
  @db_type = db_type
174
176
  @columns = columns
175
177
  end
@@ -391,7 +393,7 @@ module Sequel
391
393
  db.instance_exec do
392
394
  @row_types = {}
393
395
  @row_schema_types = {}
394
- extend(@row_type_method_module = Module.new)
396
+ extend(@row_type_method_module = Sequel.set_temp_name(Module.new){"Sequel::Postgres::PGRow::DatabaseMethods::_RowTypeMethodModule"})
395
397
  add_conversion_proc(2249, PGRow::Parser.new(:converter=>PGRow::ArrayRow))
396
398
  if respond_to?(:register_array_type)
397
399
  register_array_type('record', :oid=>2287, :scalar_oid=>2249)
@@ -0,0 +1,172 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The query_blocker extension adds Database#block_queries.
4
+ # Inside the block passed to #block_queries, any attempts to
5
+ # execute a query/statement on the database will raise a
6
+ # Sequel::QueryBlocker::BlockedQuery exception.
7
+ #
8
+ # DB.extension :query_blocker
9
+ # DB.block_queries do
10
+ # ds = DB[:table] # No exception
11
+ # ds = ds.where(column: 1) # No exception
12
+ # ds.all # Exception raised
13
+ # end
14
+ #
15
+ # To handle concurrency, you can pass a :scope option:
16
+ #
17
+ # # Current Thread
18
+ # DB.block_queries(scope: :thread){}
19
+ #
20
+ # # Current Fiber
21
+ # DB.block_queries(scope: :fiber){}
22
+ #
23
+ # # Specific Thread
24
+ # DB.block_queries(scope: Thread.current){}
25
+ #
26
+ # # Specific Fiber
27
+ # DB.block_queries(scope: Fiber.current){}
28
+ #
29
+ # Database#block_queries is useful for blocking queries inside
30
+ # the block. However, there may be cases where you want to
31
+ # allow queries in specific places inside a block_queries block.
32
+ # You can use Database#allow_queries for that:
33
+ #
34
+ # DB.block_queries do
35
+ # DB.allow_queries do
36
+ # DB[:table].all # Query allowed
37
+ # end
38
+ #
39
+ # DB[:table].all # Exception raised
40
+ # end
41
+ #
42
+ # When mixing block_queries and allow_queries with scopes, the
43
+ # narrowest scope has priority. So if you are blocking with
44
+ # :thread scope, and allowing with :fiber scope, queries in the
45
+ # current fiber will be allowed, but queries in different fibers of
46
+ # the current thread will be blocked.
47
+ #
48
+ # Note that this should catch all queries executed through the
49
+ # Database instance. Whether it catches queries executed directly
50
+ # on a connection object depends on the adapter in use.
51
+ #
52
+ # Related module: Sequel::QueryBlocker
53
+
54
+ require "fiber"
55
+
56
+ #
57
+ module Sequel
58
+ module QueryBlocker
59
+ # Exception class raised if there is an attempt to execute a
60
+ # query/statement on the database inside a block passed to
61
+ # block_queries.
62
+ class BlockedQuery < Sequel::Error
63
+ end
64
+
65
+ def self.extended(db)
66
+ db.instance_exec do
67
+ @blocked_query_scopes ||= {}
68
+ end
69
+ end
70
+
71
+ # If checking a connection for validity, and a BlockedQuery exception is
72
+ # raised, treat it as a valid connection. You cannot check whether the
73
+ # connection is valid without issuing a query, and if queries are blocked,
74
+ # you need to assume it is valid or assume it is not. Since it most cases
75
+ # it will be valid, this assumes validity.
76
+ def valid_connection?(conn)
77
+ super
78
+ rescue BlockedQuery
79
+ true
80
+ end
81
+
82
+ # Check whether queries are blocked before executing them.
83
+ def log_connection_yield(sql, conn, args=nil)
84
+ # All database adapters should be calling this method around
85
+ # query execution (otherwise the queries would not get logged),
86
+ # ensuring the blocking is checked. Any database adapter issuing
87
+ # a query without calling this method is considered buggy.
88
+ check_blocked_queries!
89
+ super
90
+ end
91
+
92
+ # Whether queries are currently blocked.
93
+ def block_queries?
94
+ b = @blocked_query_scopes
95
+ b.fetch(Fiber.current) do
96
+ b.fetch(Thread.current) do
97
+ b.fetch(:global, false)
98
+ end
99
+ end
100
+ end
101
+
102
+ # Allow queries inside the block. Only useful if they are already blocked
103
+ # for the same scope. Useful for blocking queries generally, and only allowing
104
+ # them in specific places. Takes the same :scope option as #block_queries.
105
+ def allow_queries(opts=OPTS, &block)
106
+ _allow_or_block_queries(false, opts, &block)
107
+ end
108
+
109
+ # Reject (raise an BlockedQuery exception) if there is an attempt to execute
110
+ # a query/statement inside the block.
111
+ #
112
+ # The :scope option indicates which queries are rejected inside the block:
113
+ #
114
+ # :global :: This is the default, and rejects all queries.
115
+ # :thread :: Reject all queries in the current thread.
116
+ # :fiber :: Reject all queries in the current fiber.
117
+ # Thread :: Reject all queries in the given thread.
118
+ # Fiber :: Reject all queries in the given fiber.
119
+ def block_queries(opts=OPTS, &block)
120
+ _allow_or_block_queries(true, opts, &block)
121
+ end
122
+
123
+ private
124
+
125
+ # Internals of block_queries and allow_queries.
126
+ def _allow_or_block_queries(value, opts)
127
+ scope = query_blocker_scope(opts)
128
+ prev_value = nil
129
+ scopes = @blocked_query_scopes
130
+
131
+ begin
132
+ Sequel.synchronize do
133
+ prev_value = scopes[scope]
134
+ scopes[scope] = value
135
+ end
136
+
137
+ yield
138
+ ensure
139
+ Sequel.synchronize do
140
+ if prev_value.nil?
141
+ scopes.delete(scope)
142
+ else
143
+ scopes[scope] = prev_value
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ # The scope for the query block, either :global, or a Thread or Fiber instance.
150
+ def query_blocker_scope(opts)
151
+ case scope = opts[:scope]
152
+ when nil
153
+ :global
154
+ when :global, Thread, Fiber
155
+ scope
156
+ when :thread
157
+ Thread.current
158
+ when :fiber
159
+ Fiber.current
160
+ else
161
+ raise Sequel::Error, "invalid scope given to block_queries: #{scope.inspect}"
162
+ end
163
+ end
164
+
165
+ # Raise a BlockQuery exception if queries are currently blocked.
166
+ def check_blocked_queries!
167
+ raise BlockedQuery, "cannot execute query inside a block_queries block" if block_queries?
168
+ end
169
+ end
170
+
171
+ Database.register_extension(:query_blocker, QueryBlocker)
172
+ end
@@ -173,7 +173,7 @@ module Sequel
173
173
  # Return a modified StringAgg that uses distinct expressions
174
174
  def distinct
175
175
  self.class.new(@expr, @separator) do |sa|
176
- sa.instance_variable_set(:@order_expr, @order_expr) if @order_expr
176
+ sa.instance_variable_set(:@order_expr, @order_expr)
177
177
  sa.instance_variable_set(:@distinct, true)
178
178
  end
179
179
  end
@@ -181,8 +181,8 @@ module Sequel
181
181
  # Return a modified StringAgg with the given order
182
182
  def order(*o)
183
183
  self.class.new(@expr, @separator) do |sa|
184
- sa.instance_variable_set(:@distinct, @distinct) if @distinct
185
184
  sa.instance_variable_set(:@order_expr, o.empty? ? nil : o.freeze)
185
+ sa.instance_variable_set(:@distinct, @distinct)
186
186
  end
187
187
  end
188
188
 
@@ -14,6 +14,7 @@ module Sequel
14
14
  module SQL
15
15
  class VirtualRow < BasicObject
16
16
  include(Module.new do
17
+ Sequel.set_temp_name(self){"Sequel::SQL:VirtualRow::_MethodBlockMethodMissing"}
17
18
  # Handle blocks passed to methods and change the behavior.
18
19
  def method_missing(m, *args, &block)
19
20
  if block
@@ -184,7 +184,7 @@ module Sequel
184
184
  end
185
185
  end
186
186
 
187
- klass = Class.new(self)
187
+ klass = Sequel.set_temp_name(Class.new(self)){"Sequel::_Model(#{source.inspect})"}
188
188
 
189
189
  if source.is_a?(::Sequel::Database)
190
190
  klass.db = source
@@ -762,22 +762,35 @@ module Sequel
762
762
  end
763
763
  end
764
764
  end
765
+
766
+ # Module that the class methods that call dataset methods are kept in.
767
+ # This allows the methods to be overridden and call super with the
768
+ # default behavior.
769
+ def dataset_methods_module
770
+ return @dataset_methods_module if defined?(@dataset_methods_module)
771
+ Sequel.synchronize{@dataset_methods_module ||= Sequel.set_temp_name(Module.new){"#{name}::@dataset_methods_module"}}
772
+ extend(@dataset_methods_module)
773
+ @dataset_methods_module
774
+ end
765
775
 
766
- # Define a model method that calls the dataset method with the same name,
767
- # only used for methods with names that can't be represented directly in
768
- # ruby code.
776
+ # Define a model method that calls the dataset method with the same name.
769
777
  def def_model_dataset_method(meth)
770
778
  return if respond_to?(meth, true)
771
779
 
780
+ mod = dataset_methods_module
781
+
772
782
  if meth.to_s =~ /\A[A-Za-z_][A-Za-z0-9_]*\z/
773
- instance_eval("def #{meth}(*args, &block); dataset.#{meth}(*args, &block) end", __FILE__, __LINE__)
783
+ mod.module_eval(<<END, __FILE__, __LINE__ + 1)
784
+ def #{meth}(*args, &block); dataset.#{meth}(*args, &block) end
785
+ ruby2_keywords :#{meth} if respond_to?(:ruby2_keywords, true)
786
+ END
774
787
  else
775
- define_singleton_method(meth){|*args, &block| dataset.public_send(meth, *args, &block)}
788
+ mod.send(:define_method, meth){|*args, &block| dataset.public_send(meth, *args, &block)}
789
+ # :nocov:
790
+ mod.send(:ruby2_keywords, meth) if respond_to?(:ruby2_keywords, true)
791
+ # :nocov:
776
792
  end
777
- singleton_class.send(:alias_method, meth, meth)
778
- # :nocov:
779
- singleton_class.send(:ruby2_keywords, meth) if respond_to?(:ruby2_keywords, true)
780
- # :nocov:
793
+ mod.send(:alias_method, meth, meth)
781
794
  end
782
795
 
783
796
  # Get the schema from the database, fall back on checking the columns
@@ -943,7 +956,7 @@ module Sequel
943
956
  # Module that the class includes that holds methods the class adds for column accessors and
944
957
  # associations so that the methods can be overridden with +super+.
945
958
  def overridable_methods_module
946
- include(@overridable_methods_module = Module.new) unless @overridable_methods_module
959
+ include(@overridable_methods_module = Sequel.set_temp_name(Module.new){"#{name}::@overridable_methods_module"}) unless @overridable_methods_module
947
960
  @overridable_methods_module
948
961
  end
949
962
 
@@ -61,7 +61,7 @@ module Sequel
61
61
  def self.apply(model)
62
62
  model.instance_exec do
63
63
  @compositions = {}
64
- include(@composition_module ||= Module.new)
64
+ include(@composition_module ||= Sequel.set_temp_name(Module.new){"#{name}::@composition_module"})
65
65
  end
66
66
  end
67
67
 
@@ -80,7 +80,7 @@ module Sequel
80
80
  inverted = values.invert.freeze
81
81
 
82
82
  unless @enum_methods
83
- @enum_methods = Module.new
83
+ @enum_methods = Sequel.set_temp_name(Module.new){"#{name}::@enum_methods"}
84
84
  include @enum_methods
85
85
  end
86
86
 
@@ -32,6 +32,7 @@ module Sequel
32
32
  def self.apply(model, &block)
33
33
  model.instance_exec do
34
34
  @dataset_module_class = Class.new(@dataset_module_class) do
35
+ Sequel.set_temp_name(self){"#{model.name}::@dataset_module_class(InvertedSubsets)"}
35
36
  include DatasetModuleMethods
36
37
  if block
37
38
  define_method(:inverted_subset_name, &block)
@@ -64,7 +64,7 @@ module Sequel
64
64
  # :dataset :: The base dataset to use for the lazy attribute lookup
65
65
  # :table :: The table name to use to qualify the attribute and primary key columns.
66
66
  def define_lazy_attribute_getter(a, opts=OPTS)
67
- include(@lazy_attributes_module ||= Module.new) unless @lazy_attributes_module
67
+ include(@lazy_attributes_module ||= Sequel.set_temp_name(Module.new){"#{name}::@lazy_attributes_module"}) unless @lazy_attributes_module
68
68
  @lazy_attributes_module.class_eval do
69
69
  define_method(a) do
70
70
  if !values.has_key?(a) && !new?
@@ -129,7 +129,7 @@ module Sequel
129
129
  #
130
130
  # If a block is provided, it is used to set the :reject_if option.
131
131
  def nested_attributes(*associations, &block)
132
- include(@nested_attributes_module ||= Module.new) unless @nested_attributes_module
132
+ include(@nested_attributes_module ||= Sequel.set_temp_name(Module.new){"#{name}::@nested_attributes_module"}) unless @nested_attributes_module
133
133
  opts = associations.last.is_a?(Hash) ? associations.pop : OPTS
134
134
  reflections = associations.map{|a| association_reflection(a) || raise(Error, "no association named #{a} for #{self}")}
135
135
  reflections.each do |r|
@@ -81,7 +81,7 @@ module Sequel
81
81
 
82
82
  opts = opts.dup
83
83
  opts[:class] = model
84
- opts[:methods_module] = Module.new
84
+ opts[:methods_module] = Sequel.set_temp_name(Module.new){"#{model.name}::_rcte_tree[:methods_module]"}
85
85
  opts[:union_all] = opts[:union_all].nil? ? true : opts[:union_all]
86
86
  model.send(:include, opts[:methods_module])
87
87
 
@@ -37,7 +37,8 @@ module Sequel
37
37
  #
38
38
  # # Register custom serializer/deserializer pair, if desired
39
39
  # require 'sequel/plugins/serialization'
40
- # Sequel::Plugins::Serialization.register_format(:reverse, :reverse.to_proc, :reverse.to_proc)
40
+ # require 'base64'
41
+ # Sequel::Plugins::Serialization.register_format(:base64, Base64.method(:encode64), Base64.method(:decode64))
41
42
  #
42
43
  # class User < Sequel::Model
43
44
  # # Built-in format support when loading the plugin
@@ -48,10 +49,10 @@ module Sequel
48
49
  # serialize_attributes :marshal, :permissions
49
50
  #
50
51
  # # Use custom registered serialization format just like built-in format
51
- # serialize_attributes :reverse, :password
52
+ # serialize_attributes :base64, :password
52
53
  #
53
54
  # # Use a custom serializer/deserializer pair without registering
54
- # serialize_attributes [:reverse.to_proc, :reverse.to_proc], :password
55
+ # serialize_attributes [ Base64.method(:encode64), Base64.method(:decode64)], :password
55
56
  # end
56
57
  # user = User.create
57
58
  # user.permissions = {global: 'read-only'}
@@ -123,7 +124,12 @@ module Sequel
123
124
  end
124
125
 
125
126
  # Create instance level reader that deserializes column values on request,
126
- # and instance level writer that stores new deserialized values.
127
+ # and instance level writer that stores new deserialized values. If +format+
128
+ # is a symbol, it should correspond to a previously-registered format using +register_format+.
129
+ # Otherwise, +format+ is expected to be a 2-element array of callables,
130
+ # with the first element being the serializer, used to convert the value used by the application
131
+ # to the value that will be stored in the database, and the second element being the deserializer,
132
+ # used to convert the value stored the database to the value used by the application.
127
133
  def serialize_attributes(format, *columns)
128
134
  if format.is_a?(Symbol)
129
135
  unless format = Sequel.synchronize{REGISTERED_FORMATS[format]}
@@ -140,7 +146,7 @@ module Sequel
140
146
  # Add serializated attribute acessor methods to the serialization_module
141
147
  def define_serialized_attribute_accessor(serializer, deserializer, *columns)
142
148
  m = self
143
- include(@serialization_module ||= Module.new) unless @serialization_module
149
+ include(@serialization_module ||= Sequel.set_temp_name(Module.new){"#{name}::@serialization_module"}) unless @serialization_module
144
150
  @serialization_module.class_eval do
145
151
  columns.each do |column|
146
152
  m.serialization_map[column] = serializer
@@ -85,7 +85,7 @@ module Sequel
85
85
  # Use automatic SQL comments for the given dataset methods.
86
86
  def sql_comments_dataset_methods(*meths)
87
87
  unless @_sql_comments_dataset_module
88
- dataset_module(@_sql_comments_dataset_module = Module.new)
88
+ dataset_module(@_sql_comments_dataset_module = Sequel.set_temp_name(Module.new){"#{name}::@_sql_comments_dataset_module"})
89
89
  end
90
90
  _sql_comments_methods(@_sql_comments_dataset_module, :dataset, meths)
91
91
  end
@@ -2,11 +2,11 @@
2
2
 
3
3
  module Sequel
4
4
  module Plugins
5
- # The static_cache_cache plugin allows for caching the row content for subclasses
6
- # that use the static cache plugin (or just the current class). Using this plugin
7
- # can avoid the need to query the database every time loading the plugin into a
8
- # model, which can save time when you have a lot of models using the static_cache
9
- # plugin.
5
+ # The static_cache_cache plugin allows for caching the row content for the current
6
+ # class and subclasses that use the static_cache or subset_static_cache plugins.
7
+ # Using this plugin can avoid the need to query the database every time loading
8
+ # the static_cache plugin into a model (static_cache plugin) or using the
9
+ # cache_subset method (subset_static_cache plugin).
10
10
  #
11
11
  # Usage:
12
12
  #
@@ -26,11 +26,7 @@ module Sequel
26
26
  module ClassMethods
27
27
  # Dump the in-memory cached rows to the cache file.
28
28
  def dump_static_cache_cache
29
- static_cache_cache = {}
30
- @static_cache_cache.sort.each do |k, v|
31
- static_cache_cache[k] = v
32
- end
33
- File.open(@static_cache_cache_file, 'wb'){|f| f.write(Marshal.dump(static_cache_cache))}
29
+ File.open(@static_cache_cache_file, 'wb'){|f| f.write(Marshal.dump(sort_static_cache_hash(@static_cache_cache)))}
34
30
  nil
35
31
  end
36
32
 
@@ -38,16 +34,57 @@ module Sequel
38
34
 
39
35
  private
40
36
 
37
+ # Sort the given static cache hash in a deterministic way, so that
38
+ # the same static cache values will result in the same marshal file.
39
+ def sort_static_cache_hash(cache)
40
+ cache = cache.sort do |a, b|
41
+ a, = a
42
+ b, = b
43
+ if a.is_a?(Array)
44
+ if b.is_a?(Array)
45
+ a_name, a_meth = a
46
+ b_name, b_meth = b
47
+ x = a_name <=> b_name
48
+ if x.zero?
49
+ x = a_meth <=> b_meth
50
+ end
51
+ x
52
+ else
53
+ 1
54
+ end
55
+ elsif b.is_a?(Array)
56
+ -1
57
+ else
58
+ a <=> b
59
+ end
60
+ end
61
+ Hash[cache]
62
+ end
63
+
41
64
  # Load the rows for the model from the cache if available.
42
65
  # If not available, load the rows from the database, and
43
66
  # then update the cache with the raw rows.
44
67
  def load_static_cache_rows
45
- if rows = Sequel.synchronize{@static_cache_cache[name]}
68
+ _load_static_cache_rows(dataset, name)
69
+ end
70
+
71
+ # Load the rows for the subset from the cache if available.
72
+ # If not available, load the rows from the database, and
73
+ # then update the cache with the raw rows.
74
+ def load_subset_static_cache_rows(ds, meth)
75
+ _load_static_cache_rows(ds, [name, meth].freeze)
76
+ end
77
+
78
+ # Check the cache first for the key, and return rows without a database
79
+ # query if present. Otherwise, get all records in the provided dataset,
80
+ # and update the cache with them.
81
+ def _load_static_cache_rows(ds, key)
82
+ if rows = Sequel.synchronize{@static_cache_cache[key]}
46
83
  rows.map{|row| call(row)}.freeze
47
84
  else
48
- rows = dataset.all.freeze
85
+ rows = ds.all.freeze
49
86
  raw_rows = rows.map(&:values)
50
- Sequel.synchronize{@static_cache_cache[name] = raw_rows}
87
+ Sequel.synchronize{@static_cache_cache[key] = raw_rows}
51
88
  rows
52
89
  end
53
90
  end
@@ -48,6 +48,7 @@ module Sequel
48
48
  def self.apply(model, &block)
49
49
  model.instance_exec do
50
50
  @dataset_module_class = Class.new(@dataset_module_class) do
51
+ Sequel.set_temp_name(self){"#{model.name}::@dataset_module_class(SubsetConditions)"}
51
52
  include DatasetModuleMethods
52
53
  end
53
54
  end
@@ -0,0 +1,262 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The subset_static_cache plugin is designed for model subsets that are not modified at all
6
+ # in production use cases, or at least where modifications to them would usually
7
+ # coincide with an application restart. When caching a model subset, it
8
+ # retrieves all rows in the database and statically caches a ruby array and hash
9
+ # keyed on primary key containing all of the model instances. All of these cached
10
+ # instances are frozen so they won't be modified unexpectedly.
11
+ #
12
+ # With the following code:
13
+ #
14
+ # class StatusType < Sequel::Model
15
+ # dataset_module do
16
+ # where :available, hidden: false
17
+ # end
18
+ # cache_subset :available
19
+ # end
20
+ #
21
+ # The following methods will use the cache and not issue a database query:
22
+ #
23
+ # * StatusType.available.with_pk
24
+ # * StatusType.available.all
25
+ # * StatusType.available.each
26
+ # * StatusType.available.first (without block, only supporting no arguments or single integer argument)
27
+ # * StatusType.available.count (without an argument or block)
28
+ # * StatusType.available.map
29
+ # * StatusType.available.as_hash
30
+ # * StatusType.available.to_hash
31
+ # * StatusType.available.to_hash_groups
32
+ #
33
+ # The cache is not used if you chain methods before or after calling the cached
34
+ # method, as doing so would not be safe:
35
+ #
36
+ # StatusType.where{number > 1}.available.all
37
+ # StatusType.available.where{number > 1}.all
38
+ #
39
+ # The cache is also not used if you change the class's dataset after caching
40
+ # the subset, or in subclasses of the model.
41
+ #
42
+ # You should not modify any row that is statically cached when using this plugin,
43
+ # as otherwise you will get different results for cached and uncached method
44
+ # calls.
45
+ module SubsetStaticCache
46
+ def self.configure(model)
47
+ model.class_exec do
48
+ @subset_static_caches ||= ({}.compare_by_identity)
49
+ end
50
+ end
51
+
52
+ module ClassMethods
53
+ # Cache the given subset statically, so that calling the subset method on
54
+ # the model will return a dataset that will return cached results instead
55
+ # of issuing database queries (assuming the cache has the necessary
56
+ # information).
57
+ #
58
+ # The model must already respond to the given method before cache_subset
59
+ # is called.
60
+ def cache_subset(meth)
61
+ ds = send(meth).with_extend(CachedDatasetMethods)
62
+ cache = ds.instance_variable_get(:@cache)
63
+
64
+ rows, hash = subset_static_cache_rows(ds, meth)
65
+ cache[:subset_static_cache_all] = rows
66
+ cache[:subset_static_cache_map] = hash
67
+
68
+ caches = @subset_static_caches
69
+ caches[meth] = ds
70
+ model = self
71
+ subset_static_cache_module.send(:define_method, meth) do
72
+ if (model == self) && (cached_dataset = caches[meth])
73
+ cached_dataset
74
+ else
75
+ super()
76
+ end
77
+ end
78
+ nil
79
+ end
80
+
81
+ Plugins.after_set_dataset(self, :clear_subset_static_caches)
82
+ Plugins.inherited_instance_variables(self, :@subset_static_caches=>proc{{}.compare_by_identity})
83
+
84
+ private
85
+
86
+ # Clear the subset_static_caches. This is used if the model dataset
87
+ # changes, to prevent cached values from being used.
88
+ def clear_subset_static_caches
89
+ @subset_static_caches.clear
90
+ end
91
+
92
+ # A module for the subset static cache methods, so that you can define
93
+ # a singleton method in the class with the same name, and call super
94
+ # to get default behavior.
95
+ def subset_static_cache_module
96
+ return @subset_static_cache_module if @subset_static_cache_module
97
+
98
+ # Ensure dataset_methods module is defined and class is extended with
99
+ # it before calling creating this module.
100
+ dataset_methods_module
101
+
102
+ Sequel.synchronize{@subset_static_cache_module ||= Sequel.set_temp_name(Module.new){"#{name}::@subset_static_cache_module"}}
103
+ extend(@subset_static_cache_module)
104
+ @subset_static_cache_module
105
+ end
106
+
107
+ # Return the frozen array and hash used for caching the subset
108
+ # of the given dataset.
109
+ def subset_static_cache_rows(ds, meth)
110
+ all = load_subset_static_cache_rows(ds, meth)
111
+ h = {}
112
+ all.each do |o|
113
+ o.errors.freeze
114
+ h[o.pk.freeze] = o.freeze
115
+ end
116
+ [all, h.freeze]
117
+ end
118
+
119
+ # Return a frozen array for all rows in the dataset.
120
+ def load_subset_static_cache_rows(ds, meth)
121
+ ret = super if defined?(super)
122
+ ret || ds.all.freeze
123
+ end
124
+ end
125
+
126
+ module CachedDatasetMethods
127
+ # An array of all of the dataset's instances, without issuing a database
128
+ # query. If a block is given, yields each instance to the block.
129
+ def all(&block)
130
+ return super unless all = @cache[:subset_static_cache_all]
131
+
132
+ array = all.dup
133
+ array.each(&block) if block
134
+ array
135
+ end
136
+
137
+ # Get the number of records in the cache, without issuing a database query,
138
+ # if no arguments or block are provided.
139
+ def count(*a, &block)
140
+ if a.empty? && !block && (all = @cache[:subset_static_cache_all])
141
+ all.size
142
+ else
143
+ super
144
+ end
145
+ end
146
+
147
+ # If a block is given, multiple arguments are given, or a single
148
+ # non-Integer argument is given, performs the default behavior of
149
+ # issuing a database query. Otherwise, uses the cached values
150
+ # to return either the first cached instance (no arguments) or an
151
+ # array containing the number of instances specified (single integer
152
+ # argument).
153
+ def first(*args)
154
+ if !defined?(yield) && args.length <= 1 && (args.length == 0 || args[0].is_a?(Integer)) && (all = @cache[:subset_static_cache_all])
155
+ all.first(*args)
156
+ else
157
+ super
158
+ end
159
+ end
160
+
161
+ # Return the frozen object with the given pk, or nil if no such object exists
162
+ # in the cache, without issuing a database query.
163
+ def with_pk(pk)
164
+ if cache = @cache[:subset_static_cache_map]
165
+ cache[pk]
166
+ else
167
+ super
168
+ end
169
+ end
170
+
171
+ # Yield each of the dataset's frozen instances to the block, without issuing a database
172
+ # query.
173
+ def each(&block)
174
+ return super unless all = @cache[:subset_static_cache_all]
175
+ all.each(&block)
176
+ end
177
+
178
+ # Use the cache instead of a query to get the results.
179
+ def map(column=nil, &block)
180
+ return super unless all = @cache[:subset_static_cache_all]
181
+ if column
182
+ raise(Error, "Cannot provide both column and block to map") if block
183
+ if column.is_a?(Array)
184
+ all.map{|r| r.values.values_at(*column)}
185
+ else
186
+ all.map{|r| r[column]}
187
+ end
188
+ else
189
+ all.map(&block)
190
+ end
191
+ end
192
+
193
+ # Use the cache instead of a query to get the results if possible
194
+ def as_hash(key_column = nil, value_column = nil, opts = OPTS)
195
+ return super unless all = @cache[:subset_static_cache_all]
196
+
197
+ if key_column.nil? && value_column.nil?
198
+ if opts[:hash]
199
+ key_column = model.primary_key
200
+ else
201
+ return Hash[@cache[:subset_static_cache_map]]
202
+ end
203
+ end
204
+
205
+ h = opts[:hash] || {}
206
+ if value_column
207
+ if value_column.is_a?(Array)
208
+ if key_column.is_a?(Array)
209
+ all.each{|r| h[r.values.values_at(*key_column)] = r.values.values_at(*value_column)}
210
+ else
211
+ all.each{|r| h[r[key_column]] = r.values.values_at(*value_column)}
212
+ end
213
+ else
214
+ if key_column.is_a?(Array)
215
+ all.each{|r| h[r.values.values_at(*key_column)] = r[value_column]}
216
+ else
217
+ all.each{|r| h[r[key_column]] = r[value_column]}
218
+ end
219
+ end
220
+ elsif key_column.is_a?(Array)
221
+ all.each{|r| h[r.values.values_at(*key_column)] = r}
222
+ else
223
+ all.each{|r| h[r[key_column]] = r}
224
+ end
225
+ h
226
+ end
227
+
228
+ # Alias of as_hash for backwards compatibility.
229
+ def to_hash(*a)
230
+ as_hash(*a)
231
+ end
232
+
233
+ # Use the cache instead of a query to get the results
234
+ def to_hash_groups(key_column, value_column = nil, opts = OPTS)
235
+ return super unless all = @cache[:subset_static_cache_all]
236
+
237
+ h = opts[:hash] || {}
238
+ if value_column
239
+ if value_column.is_a?(Array)
240
+ if key_column.is_a?(Array)
241
+ all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r.values.values_at(*value_column)}
242
+ else
243
+ all.each{|r| (h[r[key_column]] ||= []) << r.values.values_at(*value_column)}
244
+ end
245
+ else
246
+ if key_column.is_a?(Array)
247
+ all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r[value_column]}
248
+ else
249
+ all.each{|r| (h[r[key_column]] ||= []) << r[value_column]}
250
+ end
251
+ end
252
+ elsif key_column.is_a?(Array)
253
+ all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r}
254
+ else
255
+ all.each{|r| (h[r[key_column]] ||= []) << r}
256
+ end
257
+ h
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
data/lib/sequel/sql.rb CHANGED
@@ -1915,6 +1915,7 @@ module Sequel
1915
1915
  end
1916
1916
 
1917
1917
  m = Module.new do
1918
+ Sequel.set_temp_name(Module.new){"Sequel::SQL::VirtualRow::_BaseMethodMissing"}
1918
1919
  # Return an +Identifier+, +QualifiedIdentifier+, or +Function+, depending
1919
1920
  # on arguments and whether a block is provided. Does not currently call the block.
1920
1921
  # See the class level documentation.
@@ -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 = 87
9
+ MINOR = 89
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,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.87.0
4
+ version: 5.89.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-01 00:00:00.000000000 Z
10
+ date: 2025-02-01 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bigdecimal
@@ -274,6 +273,7 @@ files:
274
273
  - lib/sequel/extensions/pretty_table.rb
275
274
  - lib/sequel/extensions/provenance.rb
276
275
  - lib/sequel/extensions/query.rb
276
+ - lib/sequel/extensions/query_blocker.rb
277
277
  - lib/sequel/extensions/round_timestamps.rb
278
278
  - lib/sequel/extensions/run_transaction_hooks.rb
279
279
  - lib/sequel/extensions/s.rb
@@ -392,6 +392,7 @@ files:
392
392
  - lib/sequel/plugins/string_stripper.rb
393
393
  - lib/sequel/plugins/subclasses.rb
394
394
  - lib/sequel/plugins/subset_conditions.rb
395
+ - lib/sequel/plugins/subset_static_cache.rb
395
396
  - lib/sequel/plugins/table_select.rb
396
397
  - lib/sequel/plugins/tactical_eager_loading.rb
397
398
  - lib/sequel/plugins/throw_failures.rb
@@ -424,7 +425,6 @@ metadata:
424
425
  documentation_uri: https://sequel.jeremyevans.net/documentation.html
425
426
  mailing_list_uri: https://github.com/jeremyevans/sequel/discussions
426
427
  source_code_uri: https://github.com/jeremyevans/sequel
427
- post_install_message:
428
428
  rdoc_options:
429
429
  - "--quiet"
430
430
  - "--line-numbers"
@@ -446,8 +446,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
446
446
  - !ruby/object:Gem::Version
447
447
  version: '0'
448
448
  requirements: []
449
- rubygems_version: 3.5.22
450
- signing_key:
449
+ rubygems_version: 3.6.2
451
450
  specification_version: 4
452
451
  summary: The Database Toolkit for Ruby
453
452
  test_files: []