1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
|
# frozen-string-literal: true
module Sequel
module Plugins
# The primary_key_lookup_check_values plugin typecasts given primary key
# values before performing a lookup by primary key. If the given primary
# key value cannot be typecasted correctly, the lookup returns nil
# without issuing a query. If the schema for the primary key column
# includes minimum and maximum values, this also checks the given value
# is not outside the range. If the given value is outside the allowed
# range, the lookup returns nil without issuing a query.
#
# This affects the following Model methods:
#
# * Model.[] (when called with non-Hash)
# * Model.with_pk
# * Model.with_pk!
#
# It also affects the following Model dataset methods:
#
# * Dataset#[] (when called with Integer)
# * Dataset#with_pk
# * dataset#with_pk!
#
# Note that this can break working code. The above methods accept
# any filter condition by default, not just primary key values. The
# plugin will handle Symbol, Sequel::SQL::Expression, and
# Sequel::LiteralString objects, but code such as the following will break:
#
# # Return first Album where primary key is one of the given values
# Album.dataset.with_pk([1, 2, 3])
#
# Usage:
#
# # Make all model subclasses support checking primary key values before
# # lookup # (called before loading subclasses)
# Sequel::Model.plugin :primary_key_lookup_check_values
#
# # Make the Album class support checking primary key values before lookup
# Album.plugin :primary_key_lookup_check_values
module PrimaryKeyLookupCheckValues
def self.configure(model)
model.instance_exec do
setup_primary_key_lookup_check_values if @dataset
end
end
module ClassMethods
Plugins.after_set_dataset(self, :setup_primary_key_lookup_check_values)
Plugins.inherited_instance_variables(self,
:@primary_key_type=>nil,
:@primary_key_value_range=>nil)
private
# Check the given primary key value. Typecast it to the appropriate
# database type if the database type is known. If it cannot be
# typecasted, or the typecasted value is outside the range of column
# values, return nil.
def _check_pk_lookup_value(pk)
return if nil == pk
case pk
when SQL::Expression, LiteralString, Symbol
return pk
end
return pk unless pk_type = @primary_key_type
if pk_type.is_a?(Array)
return unless pk.is_a?(Array)
return unless pk.size == pk_type.size
return if pk.any?(&:nil?)
pk_value_range = @primary_key_value_range
i = 0
pk.map do |v|
if type = pk_type[i]
v = _typecast_pk_lookup_value(v, type)
return if nil == v
if pk_value_range
min, max = pk_value_range[i]
return if min && v < min
return if max && v > max
end
end
i += 1
v
end
elsif pk.is_a?(Array)
return
elsif nil != (pk = _typecast_pk_lookup_value(pk, pk_type))
min, max = @primary_key_value_range
return if min && pk < min
return if max && pk > max
pk
end
end
# Typecast the value to the appropriate type,
# returning nil if it cannot be typecasted.
def _typecast_pk_lookup_value(value, type)
db.typecast_value(type, value)
rescue InvalidValue
nil
end
# Skip the primary key lookup if the typecasted and checked
# primary key value is nil.
def primary_key_lookup(pk)
unless nil == (pk = _check_pk_lookup_value(pk))
super
end
end
# Setup the primary key type and value range used for checking
# primary key values during lookup.
def setup_primary_key_lookup_check_values
if primary_key.is_a?(Array)
types = []
value_ranges = []
primary_key.each do |pk|
type, min, max = _type_min_max_values_for_column(pk)
types << type
value_ranges << ([min, max].freeze if min || max)
end
@primary_key_type = (types.freeze if types.any?)
@primary_key_value_range = (value_ranges.freeze if @primary_key_type && value_ranges.any?)
else
@primary_key_type, min, max = _type_min_max_values_for_column(primary_key)
@primary_key_value_range = ([min, max].freeze if @primary_key_type && (min || max))
end
end
# Return the type, min_value, and max_value schema entries
# for the column, if they exist.
def _type_min_max_values_for_column(column)
if schema = db_schema[column]
schema.values_at(:type, :min_value, :max_value)
end
end
end
module DatasetMethods
# Skip the primary key lookup if the typecasted and checked
# primary key value is nil.
def with_pk(pk)
unless nil == (pk = model.send(:_check_pk_lookup_value, pk))
super
end
end
end
end
end
end
|