sequel 5.86.0 → 5.88.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2945ab081dad1d3dc6d910e2902247b18750215023e0cfbb5b8f8159811b0d25
4
- data.tar.gz: 6b2933b4ac2023a71526a76f40b949e873460e2a7f8aed1524bef47b71f91e79
3
+ metadata.gz: 6a4a563fddfd5332195e8b9ba2588aef535f27ea0cee15f43afdc877910775ba
4
+ data.tar.gz: 47cb743d96f031e4fa7e415708473158b2f564b3faa155335a4787e79bce34bd
5
5
  SHA512:
6
- metadata.gz: 988bfe69b6b4953a7792431e3dd26bcb7ad13f01b8b1dc822fd9d79917608cc2f60bdb7e9a75cfa975bcd297ada83f00a2d80343424f9e4096c029437a9250ad
7
- data.tar.gz: 51c2591400946af23395f0a3f49dfc87d4ce6f536e9cbc045ae7c86915007938c72b90f899dd347d749918218b86edc8dbaa63e8f9430d7075872a431d91b777
6
+ metadata.gz: b3dd656f73cdf525bda28a2cb5f136f91ae2935f48cfd94f8dc06f042b7f682c2faf48a48625b7845ae058b92189447433c152ab11eeebb8baae925d5371326d
7
+ data.tar.gz: 84ae987e64b8c872351d259d88a1e8b63b7926e06f9bda7a0d71dda4826e4e17c83fdc5538bcde63cc0d16f77ffd75c57b504947a692898d552a77e1814a7c44
@@ -567,7 +567,7 @@ module Sequel
567
567
  im = input_identifier_meth(opts[:dataset])
568
568
  table = SQL::Identifier.new(im.call(table_name))
569
569
  table = SQL::QualifiedIdentifier.new(im.call(opts[:schema]), table) if opts[:schema]
570
- metadata_dataset.with_sql("DESCRIBE ?", table).map do |row|
570
+ metadata_dataset.with_sql("SHOW FULL COLUMNS FROM ?", table).map do |row|
571
571
  extra = row.delete(:Extra)
572
572
  if row[:primary_key] = row.delete(:Key) == 'PRI'
573
573
  row[:auto_increment] = !!(extra.to_s =~ /auto_increment/i)
@@ -577,10 +577,14 @@ module Sequel
577
577
  row[:generated] = !!(extra.to_s =~ /VIRTUAL|STORED|PERSISTENT/i)
578
578
  end
579
579
  row[:allow_null] = row.delete(:Null) == 'YES'
580
+ row[:comment] = row.delete(:Comment)
581
+ row[:comment] = nil if row[:comment] == ""
580
582
  row[:default] = row.delete(:Default)
581
583
  row[:db_type] = row.delete(:Type)
582
584
  row[:type] = schema_column_type(row[:db_type])
583
585
  row[:extra] = extra
586
+ row.delete(:Collation)
587
+ row.delete(:Privileges)
584
588
  [m.call(row.delete(:Field)), row]
585
589
  end
586
590
  end
@@ -1075,6 +1075,7 @@ module Sequel
1075
1075
  pg_attribute[:attname].as(:name),
1076
1076
  SQL::Cast.new(pg_attribute[:atttypid], :integer).as(:oid),
1077
1077
  SQL::Cast.new(basetype[:oid], :integer).as(:base_oid),
1078
+ SQL::Function.new(:col_description, pg_class[:oid], pg_attribute[:attnum]).as(:comment),
1078
1079
  SQL::Function.new(:format_type, basetype[:oid], pg_type[:typtypmod]).as(:db_base_type),
1079
1080
  SQL::Function.new(:format_type, pg_type[:oid], pg_attribute[:atttypmod]).as(:db_type),
1080
1081
  SQL::Function.new(:pg_get_expr, pg_attrdef[:adbin], pg_class[:oid]).as(:default),
@@ -2387,6 +2388,25 @@ module Sequel
2387
2388
  join_from_sql(:USING, sql)
2388
2389
  end
2389
2390
 
2391
+ # Handle column aliases containing data types, useful for selecting from functions
2392
+ # that return the record data type.
2393
+ def derived_column_list_sql_append(sql, column_aliases)
2394
+ c = false
2395
+ comma = ', '
2396
+ column_aliases.each do |a|
2397
+ sql << comma if c
2398
+ if a.is_a?(Array)
2399
+ 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
2400
+ a, type = a
2401
+ identifier_append(sql, a)
2402
+ sql << " " << db.cast_type_literal(type).to_s
2403
+ else
2404
+ identifier_append(sql, a)
2405
+ end
2406
+ c ||= true
2407
+ end
2408
+ end
2409
+
2390
2410
  # Add ON CONFLICT clause if it should be used
2391
2411
  def insert_conflict_sql(sql)
2392
2412
  if opts = @opts[:insert_conflict]
@@ -62,8 +62,7 @@ module Sequel
62
62
  private
63
63
 
64
64
  def database_specific_error_class(exception, opts)
65
- case exception.message
66
- when /1205 - Lock wait timeout exceeded; try restarting transaction\z/
65
+ if exception.error_code == 1205
67
66
  DatabaseLockTimeout
68
67
  else
69
68
  super
@@ -246,9 +246,13 @@ module Sequel
246
246
  # extension does not have specific support for Database objects, an Error will be raised.
247
247
  # Returns self.
248
248
  def extension(*exts)
249
- Sequel.extension(*exts)
250
249
  exts.each do |ext|
251
- if pr = Sequel.synchronize{EXTENSIONS[ext]}
250
+ unless pr = Sequel.synchronize{EXTENSIONS[ext]}
251
+ Sequel.extension(ext)
252
+ pr = Sequel.synchronize{EXTENSIONS[ext]}
253
+ end
254
+
255
+ if pr
252
256
  if Sequel.synchronize{@loaded_extensions.include?(ext) ? false : (@loaded_extensions << ext)}
253
257
  pr.call(self)
254
258
  end
@@ -204,7 +204,7 @@ module Sequel
204
204
  # If no related extension file exists or the extension does not have
205
205
  # specific support for Dataset objects, an error will be raised.
206
206
  def extension(*exts)
207
- Sequel.extension(*exts)
207
+ exts.each{|ext| Sequel.extension(ext) unless Sequel.synchronize{EXTENSIONS[ext]}}
208
208
  mods = exts.map{|ext| Sequel.synchronize{EXTENSION_MODULES[ext]}}
209
209
  if mods.all?
210
210
  with_extend(*mods)
@@ -1359,9 +1359,13 @@ module Sequel
1359
1359
  unless TRUE_FREEZE
1360
1360
  # Load the extensions into the receiver, without checking if the receiver is frozen.
1361
1361
  def _extension!(exts)
1362
- Sequel.extension(*exts)
1363
1362
  exts.each do |ext|
1364
- if pr = Sequel.synchronize{EXTENSIONS[ext]}
1363
+ unless pr = Sequel.synchronize{EXTENSIONS[ext]}
1364
+ Sequel.extension(ext)
1365
+ pr = Sequel.synchronize{EXTENSIONS[ext]}
1366
+ end
1367
+
1368
+ if pr
1365
1369
  pr.call(self)
1366
1370
  else
1367
1371
  raise(Error, "Extension #{ext} does not have specific support handling individual datasets (try: Sequel.extension #{ext.inspect})")
@@ -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)
@@ -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
 
@@ -394,7 +394,7 @@ module Sequel
394
394
  # there can be more than one parameter per column, so this doesn't prevent going
395
395
  # over the limit, though it does make it less likely.
396
396
  def default_import_slice
397
- 40
397
+ @opts[:no_auto_parameterize] ? super : 40
398
398
  end
399
399
 
400
400
  # Handle parameterization of multi_insert_sql
@@ -0,0 +1,90 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The pg_schema_caching extension builds on top of the schema_caching
4
+ # extension, and allows it to handle custom PostgreSQL types. On
5
+ # PostgreSQL, column schema hashes include an :oid entry for the OID
6
+ # for the column's type. For custom types, this OID is dependent on
7
+ # the PostgreSQL database, so in most cases, test and development
8
+ # versions of the same database, created with the same migrations,
9
+ # will have different OIDs.
10
+ #
11
+ # To fix this case, the pg_schema_caching extension removes custom
12
+ # OIDs from the schema cache when dumping the schema, replacing them
13
+ # with a placeholder. When loading the cached schema, the Database
14
+ # object makes a single query to get the OIDs for all custom types
15
+ # used by the cached schema, and it updates all related column
16
+ # schema hashes to set the correct :oid entry for the current
17
+ # database.
18
+ #
19
+ # Related module: Sequel::Postgres::SchemaCaching
20
+
21
+ require_relative "schema_caching"
22
+
23
+ module Sequel
24
+ module Postgres
25
+ module SchemaCaching
26
+ include Sequel::SchemaCaching
27
+
28
+ private
29
+
30
+ # Load custom oids from database when loading schema cache file.
31
+ def load_schema_cache_file(file)
32
+ set_custom_oids_for_cached_schema(super)
33
+ end
34
+
35
+ # Find all column schema hashes that use custom types.
36
+ # Load the oids for custom types in a single query, and update
37
+ # each related column schema hash with the correct oid.
38
+ def set_custom_oids_for_cached_schema(schemas)
39
+ custom_oid_rows = {}
40
+
41
+ schemas.each_value do |cols|
42
+ cols.each do |_, h|
43
+ if h[:oid] == :custom
44
+ (custom_oid_rows[h[:db_type]] ||= []) << h
45
+ end
46
+ end
47
+ end
48
+
49
+ unless custom_oid_rows.empty?
50
+ from(:pg_type).where(:typname=>custom_oid_rows.keys).select_hash(:typname, :oid).each do |name, oid|
51
+ custom_oid_rows.delete(name).each do |row|
52
+ row[:oid] = oid
53
+ end
54
+ end
55
+ end
56
+
57
+ unless custom_oid_rows.empty?
58
+ warn "Could not load OIDs for the following custom types: #{custom_oid_rows.keys.sort.join(", ")}", uplevel: 3
59
+
60
+ schemas.keys.each do |k|
61
+ if schemas[k].any?{|_,h| h[:oid] == :custom}
62
+ # Remove schema entry for table, so it will be queried at runtime to get the correct oids
63
+ schemas.delete(k)
64
+ end
65
+ end
66
+ end
67
+
68
+ schemas
69
+ end
70
+
71
+ # Replace :oid entries for custom types with :custom.
72
+ def dumpable_schema_cache
73
+ sch = super
74
+
75
+ sch.each_value do |cols|
76
+ cols.each do |_, h|
77
+ if (oid = h[:oid]) && oid >= 10000
78
+ h[:oid] = :custom
79
+ end
80
+ end
81
+ end
82
+
83
+ sch
84
+ end
85
+ end
86
+ end
87
+
88
+ Database.register_extension(:pg_schema_caching, Postgres::SchemaCaching)
89
+ end
90
+
@@ -51,14 +51,7 @@ module Sequel
51
51
  module SchemaCaching
52
52
  # Dump the cached schema to the filename given in Marshal format.
53
53
  def dump_schema_cache(file)
54
- sch = {}
55
- @schemas.sort.each do |k,v|
56
- sch[k] = v.map do |c, h|
57
- h = Hash[h]
58
- h.delete(:callable_default)
59
- [c, h]
60
- end
61
- end
54
+ sch = dumpable_schema_cache
62
55
  File.open(file, 'wb'){|f| f.write(Marshal.dump(sch))}
63
56
  nil
64
57
  end
@@ -72,7 +65,7 @@ module Sequel
72
65
  # Replace the schema cache with the data from the given file, which
73
66
  # should be in Marshal format.
74
67
  def load_schema_cache(file)
75
- @schemas = Marshal.load(File.read(file))
68
+ @schemas = load_schema_cache_file(file)
76
69
  @schemas.each_value{|v| schema_post_process(v)}
77
70
  nil
78
71
  end
@@ -82,6 +75,28 @@ module Sequel
82
75
  def load_schema_cache?(file)
83
76
  load_schema_cache(file) if File.exist?(file)
84
77
  end
78
+
79
+ private
80
+
81
+ # Return the deserialized schema cache file.
82
+ def load_schema_cache_file(file)
83
+ Marshal.load(File.read(file))
84
+ end
85
+
86
+ # A dumpable version of the schema cache.
87
+ def dumpable_schema_cache
88
+ sch = {}
89
+
90
+ @schemas.sort.each do |k,v|
91
+ sch[k] = v.map do |c, h|
92
+ h = Hash[h]
93
+ h.delete(:callable_default)
94
+ [c, h]
95
+ end
96
+ end
97
+
98
+ sch
99
+ end
85
100
  end
86
101
 
87
102
  Database.register_extension(:schema_caching, SchemaCaching)
@@ -35,7 +35,7 @@
35
35
  #
36
36
  # j[1] # (json_column ->> 1)
37
37
  # j.get(1) # (json_column ->> 1)
38
- # j.get_text(1) # (json_column -> 1)
38
+ # j.get_json(1) # (json_column -> 1)
39
39
  # j.extract('$.a') # json_extract(json_column, '$.a')
40
40
  # jb.extract('$.a') # jsonb_extract(jsonb_column, '$.a')
41
41
  #
@@ -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
 
@@ -87,7 +87,7 @@ module Sequel
87
87
  attr_reader :simple_pk
88
88
 
89
89
  # Should be the literal table name if this Model's dataset is a simple table (no select, order, join, etc.),
90
- # or nil otherwise. This and simple_pk are used for an optimization in Model.[].
90
+ # or nil otherwise. This and simple_pk are used for an optimization in Model[].
91
91
  attr_reader :simple_table
92
92
 
93
93
  # Whether mass assigning via .create/.new/#set/#update should raise an error
@@ -398,7 +398,7 @@ module Sequel
398
398
  end
399
399
 
400
400
  # Finds a single record according to the supplied filter.
401
- # You are encouraged to use Model.[] or Model.first instead of this method.
401
+ # You are encouraged to use Model[] or Model.first instead of this method.
402
402
  #
403
403
  # Artist.find(name: 'Bob')
404
404
  # # SELECT * FROM artists WHERE (name = 'Bob') LIMIT 1
@@ -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 ||= Module.new}
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
@@ -1311,7 +1324,7 @@ module Sequel
1311
1324
  # Returns a string representation of the model instance including
1312
1325
  # the class name and values.
1313
1326
  def inspect
1314
- "#<#{model.name} @values=#{inspect_values}>"
1327
+ "#<#{inspect_prefix} @values=#{inspect_values}>"
1315
1328
  end
1316
1329
 
1317
1330
  # Returns the keys in +values+. May not include all column names.
@@ -1994,7 +2007,12 @@ module Sequel
1994
2007
  set(h) unless h.empty?
1995
2008
  end
1996
2009
 
1997
- # Default inspection output for the values hash, overwrite to change what #inspect displays.
2010
+ # Default inspect output for the inspect, by default, just showing the class.
2011
+ def inspect_prefix
2012
+ model.name
2013
+ end
2014
+
2015
+ # Default inspect output for the values hash, overwrite to change what #inspect displays.
1998
2016
  def inspect_values
1999
2017
  @values.inspect
2000
2018
  end
@@ -0,0 +1,44 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The inspect_pk plugin includes the pk right next to the
6
+ # model name in inspect, allowing for easily copying and
7
+ # pasting to retrieve a copy of the object:
8
+ #
9
+ # Album.with_pk(1).inspect
10
+ # # default: #<Album @values={...}>
11
+ # # with inspect_pk: #<Album[1] @values={...}>
12
+ #
13
+ # Usage:
14
+ #
15
+ # # Make all model instances include pk in inspect output
16
+ # Sequel::Model.plugin :inspect_pk
17
+ #
18
+ # # Make Album instances include pk in inspect output
19
+ # Album.plugin :inspect_pk
20
+ module InspectPk
21
+ module InstanceMethods
22
+ private
23
+
24
+ # The primary key value to include in the inspect output, if any.
25
+ # For composite primary keys, this only includes a value if all
26
+ # fields are present.
27
+ def inspect_pk
28
+ if primary_key && (pk = self.pk) && (!(Array === pk) || pk.all?)
29
+ pk
30
+ end
31
+ end
32
+
33
+ # Include the instance's primary key in the output.
34
+ def inspect_prefix
35
+ if v = inspect_pk
36
+ "#{super}[#{v.inspect}]"
37
+ else
38
+ super
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -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]}
@@ -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
@@ -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 ||= Module.new}
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
@@ -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 = 86
9
+ MINOR = 88
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.86.0
4
+ version: 5.88.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-11-01 00:00:00.000000000 Z
10
+ date: 2025-01-01 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bigdecimal
@@ -268,6 +267,7 @@ files:
268
267
  - lib/sequel/extensions/pg_range_ops.rb
269
268
  - lib/sequel/extensions/pg_row.rb
270
269
  - lib/sequel/extensions/pg_row_ops.rb
270
+ - lib/sequel/extensions/pg_schema_caching.rb
271
271
  - lib/sequel/extensions/pg_static_cache_updater.rb
272
272
  - lib/sequel/extensions/pg_timestamptz.rb
273
273
  - lib/sequel/extensions/pretty_table.rb
@@ -353,6 +353,7 @@ files:
353
353
  - lib/sequel/plugins/input_transformer.rb
354
354
  - lib/sequel/plugins/insert_conflict.rb
355
355
  - lib/sequel/plugins/insert_returning_select.rb
356
+ - lib/sequel/plugins/inspect_pk.rb
356
357
  - lib/sequel/plugins/instance_filters.rb
357
358
  - lib/sequel/plugins/instance_hooks.rb
358
359
  - lib/sequel/plugins/instance_specific_default.rb
@@ -390,6 +391,7 @@ files:
390
391
  - lib/sequel/plugins/string_stripper.rb
391
392
  - lib/sequel/plugins/subclasses.rb
392
393
  - lib/sequel/plugins/subset_conditions.rb
394
+ - lib/sequel/plugins/subset_static_cache.rb
393
395
  - lib/sequel/plugins/table_select.rb
394
396
  - lib/sequel/plugins/tactical_eager_loading.rb
395
397
  - lib/sequel/plugins/throw_failures.rb
@@ -422,7 +424,6 @@ metadata:
422
424
  documentation_uri: https://sequel.jeremyevans.net/documentation.html
423
425
  mailing_list_uri: https://github.com/jeremyevans/sequel/discussions
424
426
  source_code_uri: https://github.com/jeremyevans/sequel
425
- post_install_message:
426
427
  rdoc_options:
427
428
  - "--quiet"
428
429
  - "--line-numbers"
@@ -444,8 +445,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
444
445
  - !ruby/object:Gem::Version
445
446
  version: '0'
446
447
  requirements: []
447
- rubygems_version: 3.5.16
448
- signing_key:
448
+ rubygems_version: 3.6.2
449
449
  specification_version: 4
450
450
  summary: The Database Toolkit for Ruby
451
451
  test_files: []