sequel 5.86.0 → 5.88.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.
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: []