sequel 5.87.0 → 5.90.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 (41) 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_auto_parameterize_in_array.rb +9 -4
  20. data/lib/sequel/extensions/pg_enum.rb +3 -3
  21. data/lib/sequel/extensions/pg_row.rb +3 -1
  22. data/lib/sequel/extensions/query_blocker.rb +172 -0
  23. data/lib/sequel/extensions/string_agg.rb +2 -2
  24. data/lib/sequel/extensions/virtual_row_method_block.rb +1 -0
  25. data/lib/sequel/model/associations.rb +19 -2
  26. data/lib/sequel/model/base.rb +29 -12
  27. data/lib/sequel/plugins/composition.rb +1 -1
  28. data/lib/sequel/plugins/enum.rb +1 -1
  29. data/lib/sequel/plugins/inverted_subsets.rb +1 -0
  30. data/lib/sequel/plugins/lazy_attributes.rb +1 -1
  31. data/lib/sequel/plugins/nested_attributes.rb +1 -1
  32. data/lib/sequel/plugins/pg_eager_any_typed_array.rb +95 -0
  33. data/lib/sequel/plugins/rcte_tree.rb +1 -1
  34. data/lib/sequel/plugins/serialization.rb +11 -5
  35. data/lib/sequel/plugins/sql_comments.rb +7 -2
  36. data/lib/sequel/plugins/static_cache_cache.rb +50 -13
  37. data/lib/sequel/plugins/subset_conditions.rb +1 -0
  38. data/lib/sequel/plugins/subset_static_cache.rb +263 -0
  39. data/lib/sequel/sql.rb +8 -1
  40. data/lib/sequel/version.rb +1 -1
  41. metadata +6 -6
@@ -693,6 +693,9 @@ module Sequel
693
693
 
694
694
  # The predicate condition to use for the eager_loader.
695
695
  def eager_loading_predicate_condition(keys)
696
+ if transform = self[:eager_loading_predicate_transform]
697
+ keys = transform.call(keys, self)
698
+ end
696
699
  {predicate_key=>keys}
697
700
  end
698
701
 
@@ -759,7 +762,15 @@ module Sequel
759
762
  def placeholder_eager_loader
760
763
  cached_fetch(:placeholder_eager_loader) do
761
764
  eager_loading_dataset.placeholder_literalizer_loader do |pl, ds|
762
- apply_eager_limit_strategy(ds.where(predicate_key=>pl.arg), eager_limit_strategy)
765
+ arg = pl.arg
766
+
767
+ if transform = self[:eager_loading_predicate_transform]
768
+ arg = arg.transform do |v|
769
+ transform.call(v, self)
770
+ end
771
+ end
772
+
773
+ apply_eager_limit_strategy(ds.where(predicate_key=>arg), eager_limit_strategy)
763
774
  end
764
775
  end
765
776
  end
@@ -1707,6 +1718,9 @@ module Sequel
1707
1718
  # record should be populated.
1708
1719
  # :eager_loader_key :: A symbol for the key column to use to populate the key_hash
1709
1720
  # for the eager loader. Can be set to nil to not populate the key_hash.
1721
+ # :eager_loading_predicate_transform :: A callable object with which to transform the predicate key values used
1722
+ # when eager loading. Called with two arguments, the array of predicate key
1723
+ # values, and a the reflection for the association being eager loaded.
1710
1724
  # :extend :: A module or array of modules to extend the dataset with.
1711
1725
  # :filter_limit_strategy :: Determines the strategy used for enforcing limits and offsets when filtering by
1712
1726
  # limited associations. Possible options are :window_function, :distinct_on, or
@@ -1769,6 +1783,9 @@ module Sequel
1769
1783
  # Set to nil to not define a setter method for the association.
1770
1784
  # :subqueries_per_union :: The number of subqueries to use in each UNION query, for eager
1771
1785
  # loading limited associations using the default :union strategy.
1786
+ # :use_placeholder_loader :: Whether to use a placeholder loader when eager loading the
1787
+ # association. Can be set to false to disable the use of a placeholder
1788
+ # loader if one would be used by default.
1772
1789
  # :validate :: Set to false to not validate when implicitly saving any associated object.
1773
1790
  # === :many_to_one
1774
1791
  # :key :: foreign key in current model's table that references
@@ -1891,7 +1908,7 @@ module Sequel
1891
1908
  raise(Error, "cannot clone an association to an association of different type (association #{name} with type #{type} cloning #{opts[:clone]} with type #{cloned_assoc[:type]})")
1892
1909
  end
1893
1910
 
1894
- opts[:use_placeholder_loader] = !opts[:instance_specific] && !opts[:eager_graph]
1911
+ opts[:use_placeholder_loader] = !opts[:instance_specific] && !opts[:eager_graph] unless opts.include?(:use_placeholder_loader)
1895
1912
  opts[:eager_block] = opts[:block] unless opts.include?(:eager_block)
1896
1913
  opts[:graph_join_type] ||= :left_outer
1897
1914
  opts[:order_eager_graph] = true unless opts.include?(:order_eager_graph)
@@ -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,36 @@ 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
+ mod_name = "#{name}::@dataset_methods_module"
772
+ Sequel.synchronize{@dataset_methods_module ||= Sequel.set_temp_name(Module.new){mod_name}}
773
+ extend(@dataset_methods_module)
774
+ @dataset_methods_module
775
+ end
765
776
 
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.
777
+ # Define a model method that calls the dataset method with the same name.
769
778
  def def_model_dataset_method(meth)
770
779
  return if respond_to?(meth, true)
771
780
 
781
+ mod = dataset_methods_module
782
+
772
783
  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__)
784
+ mod.module_eval(<<END, __FILE__, __LINE__ + 1)
785
+ def #{meth}(*args, &block); dataset.#{meth}(*args, &block) end
786
+ ruby2_keywords :#{meth} if respond_to?(:ruby2_keywords, true)
787
+ END
774
788
  else
775
- define_singleton_method(meth){|*args, &block| dataset.public_send(meth, *args, &block)}
789
+ mod.send(:define_method, meth){|*args, &block| dataset.public_send(meth, *args, &block)}
790
+ # :nocov:
791
+ mod.send(:ruby2_keywords, meth) if respond_to?(:ruby2_keywords, true)
792
+ # :nocov:
776
793
  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:
794
+ mod.send(:alias_method, meth, meth)
781
795
  end
782
796
 
783
797
  # Get the schema from the database, fall back on checking the columns
@@ -943,7 +957,10 @@ module Sequel
943
957
  # Module that the class includes that holds methods the class adds for column accessors and
944
958
  # associations so that the methods can be overridden with +super+.
945
959
  def overridable_methods_module
946
- include(@overridable_methods_module = Module.new) unless @overridable_methods_module
960
+ return @overridable_methods_module if defined?(@overridable_methods_module)
961
+ mod_name = "#{name}::@overridable_methods_module"
962
+ Sequel.synchronize{@overridable_methods_module ||= Sequel.set_temp_name(Module.new){mod_name}}
963
+ include(@overridable_methods_module)
947
964
  @overridable_methods_module
948
965
  end
949
966
 
@@ -1597,7 +1614,7 @@ module Sequel
1597
1614
  @skip_validation_on_next_save = true
1598
1615
  end
1599
1616
 
1600
- # Returns (naked) dataset that should return only this instance.
1617
+ # Returns naked dataset that should return only the row related to this instance.
1601
1618
  #
1602
1619
  # Artist[1].this
1603
1620
  # # SELECT * FROM artists WHERE (id = 1) LIMIT 1
@@ -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|
@@ -0,0 +1,95 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The pg_eager_any_typed_array plugin automatically converts
6
+ # the predicate expressions used for eager loading from:
7
+ #
8
+ # table.column IN (value_list)
9
+ #
10
+ # to:
11
+ #
12
+ # table.column = ANY(array_expr::type[])
13
+ #
14
+ # This makes it easier to use the pg_auto_parameterize_in_array
15
+ # extension with the :treat_string_list_as_text_array option,
16
+ # when using foreign keys with non-text database types that are represented
17
+ # by Ruby strings, such as enum and uuid types.
18
+ #
19
+ # Most association types that ship with Sequel have their predicate
20
+ # expressions converted by this plugin. Here are the exceptions:
21
+ #
22
+ # * associations using composite predicate keys
23
+ # * many_to_pg_array associations
24
+ # * many_to_many/one_through_one associations using :join_table_db option
25
+ # * many_through_many/one_through_many associations using
26
+ # :separate_table_per_query option
27
+ #
28
+ # To avoid predicate conversion for particular associations, set the
29
+ # :eager_loading_predicate_transform association option to nil/false.
30
+ #
31
+ # This plugin loads the pg_array extension into the model's Database.
32
+ module PgEagerAnyTypedArray
33
+ # Add the pg_array extension to the database
34
+ def self.apply(model)
35
+ model.db.extension(:pg_array)
36
+ end
37
+
38
+ module ClassMethods
39
+ TRANSFORM = proc do |values, ref|
40
+ type = ref.send(:cached_fetch, :_pg_eager_any_typed_array_type) do
41
+ key = ref.predicate_key
42
+ next if key.is_a?(Array)
43
+
44
+ while key.is_a?(SQL::QualifiedIdentifier)
45
+ key = key.column
46
+ end
47
+
48
+ # :nocov:
49
+ # many_to_pg_array association type does not need changes, as it
50
+ # already converts the values to a typed postgres array, it does
51
+ # not call the code that uses :eager_loading_predicate_transform.
52
+ #
53
+ # No association type that ships with Sequel can reach this code
54
+ # unless it is one of these types, but external association types
55
+ # could potentially reach it.
56
+ sch = case ref[:type]
57
+ # :nocov:
58
+ when :many_to_one, :one_to_one, :one_to_many, :pg_array_to_many
59
+ ref.associated_class.db_schema
60
+ when :many_to_many, :one_through_one
61
+ # Not compatible with the :join_table_db option, but that option
62
+ # does not call into this code.
63
+ Hash[ref.associated_class.db.schema(ref.join_table_source)]
64
+ when :many_through_many, :one_through_many
65
+ # Not compatible with the :separate_query_per_table option, but
66
+ # that option does not call into this code.
67
+ Hash[ref.associated_class.db.schema(ref[:through][0][:table])]
68
+ end
69
+
70
+ if sch && (sch = sch[key])
71
+ sch[:db_type]
72
+ end
73
+ end
74
+
75
+ if type
76
+ Sequel.function(:ANY, Sequel.pg_array(values, type))
77
+ else
78
+ values
79
+ end
80
+ end
81
+
82
+ # Set the :eager_loading_predicate_transform option if not already set
83
+ def associate(type, name, opts = OPTS, &block)
84
+ res = super
85
+
86
+ unless res.has_key?(:eager_loading_predicate_transform)
87
+ res[:eager_loading_predicate_transform] = TRANSFORM
88
+ end
89
+
90
+ res
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -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
@@ -40,7 +40,8 @@ module Sequel
40
40
  # Album.sql_comments_dataset_methods :to_csv # csv_serializer plugin
41
41
  #
42
42
  # In order for the sql_comments plugin to work, the sql_comments
43
- # Database extension must be loaded into the model's database.
43
+ # Database extension must be loaded into the model's database, so
44
+ # loading the plugin does this automatically.
44
45
  #
45
46
  # Note that in order to make sure SQL comments are included, some
46
47
  # optimizations are disabled if this plugin is loaded.
@@ -67,6 +68,10 @@ module Sequel
67
68
  # :nocov:
68
69
  end
69
70
 
71
+ def self.apply(model)
72
+ model.db.extension(:sql_comments)
73
+ end
74
+
70
75
  def self.configure(model)
71
76
  model.send(:reset_fast_pk_lookup_sql)
72
77
  end
@@ -85,7 +90,7 @@ module Sequel
85
90
  # Use automatic SQL comments for the given dataset methods.
86
91
  def sql_comments_dataset_methods(*meths)
87
92
  unless @_sql_comments_dataset_module
88
- dataset_module(@_sql_comments_dataset_module = Module.new)
93
+ dataset_module(@_sql_comments_dataset_module = Sequel.set_temp_name(Module.new){"#{name}::@_sql_comments_dataset_module"})
89
94
  end
90
95
  _sql_comments_methods(@_sql_comments_dataset_module, :dataset, meths)
91
96
  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