redcord 0.0.2.alpha → 0.1.2
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 +4 -4
- data/lib/redcord.rb +30 -2
- data/lib/redcord.rbi +0 -16
- data/lib/redcord/actions.rb +152 -45
- data/lib/redcord/attribute.rb +110 -13
- data/lib/redcord/base.rb +17 -3
- data/lib/redcord/configurations.rb +4 -0
- data/lib/redcord/logger.rb +1 -1
- data/lib/redcord/migration.rb +2 -0
- data/lib/redcord/migration/index.rb +57 -0
- data/lib/redcord/migration/ttl.rb +9 -4
- data/lib/redcord/railtie.rb +18 -0
- data/lib/redcord/redis.rb +200 -0
- data/lib/redcord/redis_connection.rb +16 -25
- data/lib/redcord/relation.rb +141 -33
- data/lib/redcord/serializer.rb +84 -33
- data/lib/redcord/server_scripts/create_hash.erb.lua +81 -0
- data/lib/redcord/server_scripts/delete_hash.erb.lua +17 -8
- data/lib/redcord/server_scripts/find_by_attr.erb.lua +51 -16
- data/lib/redcord/server_scripts/find_by_attr_count.erb.lua +45 -14
- data/lib/redcord/server_scripts/shared/index_helper_methods.erb.lua +45 -16
- data/lib/redcord/server_scripts/shared/lua_helper_methods.erb.lua +20 -4
- data/lib/redcord/server_scripts/shared/query_helper_methods.erb.lua +81 -14
- data/lib/redcord/server_scripts/update_hash.erb.lua +40 -26
- data/lib/redcord/tasks/redis.rake +15 -0
- data/lib/redcord/tracer.rb +48 -0
- data/lib/redcord/vacuum_helper.rb +90 -0
- metadata +9 -8
- data/lib/redcord/prepared_redis.rb +0 -18
- data/lib/redcord/server_scripts.rb +0 -78
- data/lib/redcord/server_scripts/create_hash_returning_id.erb.lua +0 -69
    
        data/lib/redcord/relation.rb
    CHANGED
    
    | @@ -5,42 +5,62 @@ | |
| 5 5 | 
             
            require 'active_support/core_ext/array'
         | 
| 6 6 | 
             
            require 'active_support/core_ext/module'
         | 
| 7 7 |  | 
| 8 | 
            +
            module Redcord
         | 
| 9 | 
            +
              class InvalidQuery < StandardError; end
         | 
| 10 | 
            +
            end
         | 
| 11 | 
            +
             | 
| 8 12 | 
             
            class Redcord::Relation
         | 
| 9 13 | 
             
              extend T::Sig
         | 
| 10 14 |  | 
| 11 15 | 
             
              sig { returns(T.class_of(Redcord::Base)) }
         | 
| 12 16 | 
             
              attr_reader :model
         | 
| 13 17 |  | 
| 14 | 
            -
              sig { returns(T::Hash[Symbol, T.untyped]) }
         | 
| 15 | 
            -
              attr_reader :query_conditions
         | 
| 16 | 
            -
             | 
| 17 18 | 
             
              sig { returns(T::Set[Symbol]) }
         | 
| 18 19 | 
             
              attr_reader :select_attrs
         | 
| 19 20 |  | 
| 21 | 
            +
              sig { returns(T.nilable(Symbol)) }
         | 
| 22 | 
            +
              attr_reader :custom_index_name
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              sig { returns(T::Hash[Symbol, T.untyped]) }
         | 
| 25 | 
            +
              attr_reader :regular_index_query_conditions
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              sig { returns(T::Hash[Symbol, T.untyped]) }
         | 
| 28 | 
            +
              attr_reader :custom_index_query_conditions
         | 
| 29 | 
            +
             | 
| 20 30 | 
             
              sig do
         | 
| 21 31 | 
             
                params(
         | 
| 22 32 | 
             
                  model: T.class_of(Redcord::Base),
         | 
| 23 | 
            -
                   | 
| 33 | 
            +
                  regular_index_query_conditions: T::Hash[Symbol, T.untyped],
         | 
| 34 | 
            +
                  custom_index_query_conditions: T::Hash[Symbol, T.untyped],
         | 
| 24 35 | 
             
                  select_attrs: T::Set[Symbol],
         | 
| 36 | 
            +
                  custom_index_name: T.nilable(Symbol)
         | 
| 25 37 | 
             
                ).void
         | 
| 26 38 | 
             
              end
         | 
| 27 39 | 
             
              def initialize(
         | 
| 28 40 | 
             
                model,
         | 
| 29 | 
            -
                 | 
| 30 | 
            -
                 | 
| 41 | 
            +
                regular_index_query_conditions = {},
         | 
| 42 | 
            +
                custom_index_query_conditions = {},
         | 
| 43 | 
            +
                select_attrs = Set.new,
         | 
| 44 | 
            +
                custom_index_name: nil
         | 
| 31 45 | 
             
              )
         | 
| 32 46 | 
             
                @model = model
         | 
| 33 | 
            -
                @ | 
| 47 | 
            +
                @regular_index_query_conditions = regular_index_query_conditions
         | 
| 48 | 
            +
                @custom_index_query_conditions = custom_index_query_conditions
         | 
| 34 49 | 
             
                @select_attrs = select_attrs
         | 
| 50 | 
            +
                @custom_index_name = custom_index_name
         | 
| 35 51 | 
             
              end
         | 
| 36 52 |  | 
| 37 53 | 
             
              sig { params(args: T::Hash[Symbol, T.untyped]).returns(Redcord::Relation) }
         | 
| 38 54 | 
             
              def where(args)
         | 
| 39 55 | 
             
                encoded_args = args.map do |attr_key, attr_val|
         | 
| 40 | 
            -
                  encoded_val = model. | 
| 56 | 
            +
                  encoded_val = model.validate_types_and_encode_query(attr_key, attr_val)
         | 
| 41 57 | 
             
                  [attr_key, encoded_val]
         | 
| 42 58 | 
             
                end
         | 
| 43 | 
            -
             | 
| 59 | 
            +
             | 
| 60 | 
            +
                regular_index_query_conditions.merge!(encoded_args.to_h)
         | 
| 61 | 
            +
                if custom_index_name
         | 
| 62 | 
            +
                  with_index(custom_index_name)
         | 
| 63 | 
            +
                end
         | 
| 44 64 | 
             
                self
         | 
| 45 65 | 
             
              end
         | 
| 46 66 |  | 
| @@ -51,19 +71,46 @@ class Redcord::Relation | |
| 51 71 | 
             
              ).returns(T.any(Redcord::Relation, T::Array[T.untyped]))
         | 
| 52 72 | 
             
              end
         | 
| 53 73 | 
             
              def select(*args, &blk)
         | 
| 54 | 
            -
                 | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 74 | 
            +
                Redcord::Base.trace(
         | 
| 75 | 
            +
                 'redcord_relation_select',
         | 
| 76 | 
            +
                 model_name: model.name,
         | 
| 77 | 
            +
                ) do
         | 
| 78 | 
            +
                  if block_given?
         | 
| 79 | 
            +
                    return execute_query.select do |*item|
         | 
| 80 | 
            +
                      blk.call(*item)
         | 
| 81 | 
            +
                    end
         | 
| 57 82 | 
             
                  end
         | 
| 58 | 
            -
                end
         | 
| 59 83 |  | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 84 | 
            +
                  select_attrs.merge(args)
         | 
| 85 | 
            +
                  self
         | 
| 86 | 
            +
                end
         | 
| 62 87 | 
             
              end
         | 
| 63 88 |  | 
| 64 89 | 
             
              sig { returns(Integer) }
         | 
| 65 90 | 
             
              def count
         | 
| 66 | 
            -
                 | 
| 91 | 
            +
                Redcord::Base.trace(
         | 
| 92 | 
            +
                 'redcord_relation_count',
         | 
| 93 | 
            +
                 model_name: model.name,
         | 
| 94 | 
            +
                ) do
         | 
| 95 | 
            +
                  model.validate_index_attributes(query_conditions.keys, custom_index_name: custom_index_name)
         | 
| 96 | 
            +
                  redis.find_by_attr_count(
         | 
| 97 | 
            +
                    model.model_key,
         | 
| 98 | 
            +
                    extract_query_conditions!,
         | 
| 99 | 
            +
                    index_attrs: model._script_arg_index_attrs,
         | 
| 100 | 
            +
                    range_index_attrs: model._script_arg_range_index_attrs,
         | 
| 101 | 
            +
                    custom_index_attrs: model._script_arg_custom_index_attrs[custom_index_name],
         | 
| 102 | 
            +
                    hash_tag: extract_hash_tag!,
         | 
| 103 | 
            +
                    custom_index_name: custom_index_name
         | 
| 104 | 
            +
                  )
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
              end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              sig { params(index_name: T.nilable(Symbol)).returns(Redcord::Relation) }
         | 
| 109 | 
            +
              def with_index(index_name)
         | 
| 110 | 
            +
                @custom_index_name = index_name
         | 
| 111 | 
            +
                adjusted_query_conditions = model.validate_and_adjust_custom_index_query_conditions(regular_index_query_conditions)
         | 
| 112 | 
            +
                custom_index_query_conditions.merge!(adjusted_query_conditions)
         | 
| 113 | 
            +
                self
         | 
| 67 114 | 
             
              end
         | 
| 68 115 |  | 
| 69 116 | 
             
              delegate(
         | 
| @@ -132,32 +179,93 @@ class Redcord::Relation | |
| 132 179 |  | 
| 133 180 | 
             
              private
         | 
| 134 181 |  | 
| 135 | 
            -
              sig { returns(T | 
| 136 | 
            -
              def  | 
| 137 | 
            -
                 | 
| 138 | 
            -
             | 
| 139 | 
            -
             | 
| 140 | 
            -
             | 
| 141 | 
            -
             | 
| 182 | 
            +
              sig { returns(T.nilable(String)) }
         | 
| 183 | 
            +
              def extract_hash_tag!
         | 
| 184 | 
            +
                attr = model.shard_by_attribute
         | 
| 185 | 
            +
                return nil if attr.nil?
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                if !query_conditions.keys.include?(attr)
         | 
| 188 | 
            +
                  raise(
         | 
| 189 | 
            +
                    Redcord::InvalidQuery,
         | 
| 190 | 
            +
                    "Queries must contain attribute '#{attr}' since model #{model.name} is sharded by this attribute"
         | 
| 142 191 | 
             
                  )
         | 
| 192 | 
            +
                end
         | 
| 143 193 |  | 
| 144 | 
            -
             | 
| 145 | 
            -
             | 
| 146 | 
            -
             | 
| 147 | 
            -
             | 
| 148 | 
            -
                   | 
| 194 | 
            +
                # Query conditions on custom index are always in form of range, even when query is by value condition is [value_x, value_x]
         | 
| 195 | 
            +
                # When in fact query is by value, range is trasformed to a single value to pass the validation.
         | 
| 196 | 
            +
                condition = query_conditions[attr]
         | 
| 197 | 
            +
                if custom_index_name and condition.first == condition.last
         | 
| 198 | 
            +
                  condition = condition.first
         | 
| 199 | 
            +
                end
         | 
| 200 | 
            +
                case condition
         | 
| 201 | 
            +
                when Integer, String
         | 
| 202 | 
            +
                  "{#{condition}}"
         | 
| 149 203 | 
             
                else
         | 
| 150 | 
            -
                   | 
| 151 | 
            -
                     | 
| 152 | 
            -
                     | 
| 204 | 
            +
                  raise(
         | 
| 205 | 
            +
                    Redcord::InvalidQuery,
         | 
| 206 | 
            +
                    "Does not support query condition #{condition} on a Redis Cluster",
         | 
| 153 207 | 
             
                  )
         | 
| 208 | 
            +
                end
         | 
| 209 | 
            +
              end
         | 
| 210 | 
            +
             | 
| 211 | 
            +
              sig { returns(T::Array[T.untyped]) }
         | 
| 212 | 
            +
              def execute_query
         | 
| 213 | 
            +
                Redcord::Base.trace(
         | 
| 214 | 
            +
                 'redcord_relation_execute_query',
         | 
| 215 | 
            +
                 model_name: model.name,
         | 
| 216 | 
            +
                ) do
         | 
| 217 | 
            +
                  model.validate_index_attributes(query_conditions.keys, custom_index_name: custom_index_name)
         | 
| 218 | 
            +
                  if !select_attrs.empty?
         | 
| 219 | 
            +
                    res_hash = redis.find_by_attr(
         | 
| 220 | 
            +
                      model.model_key,
         | 
| 221 | 
            +
                      extract_query_conditions!,
         | 
| 222 | 
            +
                      select_attrs: select_attrs,
         | 
| 223 | 
            +
                      index_attrs: model._script_arg_index_attrs,
         | 
| 224 | 
            +
                      range_index_attrs: model._script_arg_range_index_attrs,
         | 
| 225 | 
            +
                      custom_index_attrs: model._script_arg_custom_index_attrs[custom_index_name],
         | 
| 226 | 
            +
                      hash_tag: extract_hash_tag!,
         | 
| 227 | 
            +
                      custom_index_name: custom_index_name
         | 
| 228 | 
            +
                    )
         | 
| 154 229 |  | 
| 155 | 
            -
             | 
| 230 | 
            +
                    res_hash.map do |id, args|
         | 
| 231 | 
            +
                      model.from_redis_hash(args).map do |k, v|
         | 
| 232 | 
            +
                        [k.to_sym, TypeCoerce[model.get_attr_type(k.to_sym)].new.from(v)]
         | 
| 233 | 
            +
                      end.to_h.merge(id: id)
         | 
| 234 | 
            +
                    end
         | 
| 235 | 
            +
                  else
         | 
| 236 | 
            +
                    res_hash = redis.find_by_attr(
         | 
| 237 | 
            +
                      model.model_key,
         | 
| 238 | 
            +
                      extract_query_conditions!,
         | 
| 239 | 
            +
                      index_attrs: model._script_arg_index_attrs,
         | 
| 240 | 
            +
                      range_index_attrs: model._script_arg_range_index_attrs,
         | 
| 241 | 
            +
                      custom_index_attrs: model._script_arg_custom_index_attrs[custom_index_name],
         | 
| 242 | 
            +
                      hash_tag: extract_hash_tag!,
         | 
| 243 | 
            +
                      custom_index_name: custom_index_name
         | 
| 244 | 
            +
                    )
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                    res_hash.map { |id, args| model.coerce_and_set_id(args, id) }
         | 
| 247 | 
            +
                  end
         | 
| 156 248 | 
             
                end
         | 
| 157 249 | 
             
              end
         | 
| 158 250 |  | 
| 159 | 
            -
              sig { returns(Redcord:: | 
| 251 | 
            +
              sig { returns(Redcord::Redis) }
         | 
| 160 252 | 
             
              def redis
         | 
| 161 253 | 
             
                model.redis
         | 
| 162 254 | 
             
              end
         | 
| 255 | 
            +
             | 
| 256 | 
            +
              sig { returns(T::Hash[Symbol, T.untyped]) }
         | 
| 257 | 
            +
              def query_conditions
         | 
| 258 | 
            +
                custom_index_name ? custom_index_query_conditions : regular_index_query_conditions
         | 
| 259 | 
            +
              end
         | 
| 260 | 
            +
             | 
| 261 | 
            +
              sig { returns(T::Hash[Symbol, T.untyped]) }
         | 
| 262 | 
            +
              def extract_query_conditions!
         | 
| 263 | 
            +
                attr = model.shard_by_attribute
         | 
| 264 | 
            +
                return query_conditions if attr.nil?
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                cond = query_conditions.reject { |key| key == attr }
         | 
| 267 | 
            +
                raise Redcord::InvalidQuery, "Cannot query only by shard_by_attribute: #{attr}" if cond.empty?
         | 
| 268 | 
            +
             | 
| 269 | 
            +
                cond
         | 
| 270 | 
            +
              end
         | 
| 163 271 | 
             
            end
         | 
    
        data/lib/redcord/serializer.rb
    CHANGED
    
    | @@ -8,6 +8,8 @@ module Redcord | |
| 8 8 | 
             
              # Raised by Model.where
         | 
| 9 9 | 
             
              class AttributeNotIndexed < StandardError; end
         | 
| 10 10 | 
             
              class WrongAttributeType < TypeError; end
         | 
| 11 | 
            +
              class CustomIndexInvalidQuery < StandardError; end
         | 
| 12 | 
            +
              class CustomIndexInvalidDesign < StandardError; end
         | 
| 11 13 | 
             
            end
         | 
| 12 14 |  | 
| 13 15 | 
             
            # This module defines various helper methods on Redcord for serialization
         | 
| @@ -31,50 +33,36 @@ module Redcord::Serializer | |
| 31 33 | 
             
                sig { params(attribute: Symbol, val: T.untyped).returns(T.untyped) }
         | 
| 32 34 | 
             
                def encode_attr_value(attribute, val)
         | 
| 33 35 | 
             
                  if !val.blank? && TIME_TYPES.include?(props[attribute][:type])
         | 
| 34 | 
            -
                     | 
| 36 | 
            +
                    time_in_nano_sec = val.to_i * 1_000_000_000
         | 
| 37 | 
            +
                    time_in_nano_sec >= 0 ? time_in_nano_sec + val.nsec : time_in_nano_sec - val.nsec
         | 
| 38 | 
            +
                  elsif val.is_a?(Float)
         | 
| 39 | 
            +
                    # Encode as round-trippable float64
         | 
| 40 | 
            +
                    '%1.16e' % [val]
         | 
| 41 | 
            +
                  else
         | 
| 42 | 
            +
                    val
         | 
| 35 43 | 
             
                  end
         | 
| 36 | 
            -
             | 
| 37 | 
            -
                  val
         | 
| 38 44 | 
             
                end
         | 
| 39 45 |  | 
| 40 46 | 
             
                sig { params(attribute: Symbol, val: T.untyped).returns(T.untyped) }
         | 
| 41 47 | 
             
                def decode_attr_value(attribute, val)
         | 
| 42 48 | 
             
                  if !val.blank? && TIME_TYPES.include?(props[attribute][:type])
         | 
| 43 | 
            -
                    val =  | 
| 44 | 
            -
             | 
| 49 | 
            +
                    val = val.to_i
         | 
| 50 | 
            +
                    nsec = val >= 0 ? val % 1_000_000_000 : -val % 1_000_000_000
         | 
| 45 51 |  | 
| 46 | 
            -
             | 
| 52 | 
            +
                    Time.zone.at(val / 1_000_000_000).change(nsec: nsec)
         | 
| 53 | 
            +
                  else
         | 
| 54 | 
            +
                    val
         | 
| 55 | 
            +
                  end
         | 
| 47 56 | 
             
                end
         | 
| 48 57 |  | 
| 49 58 | 
             
                sig { params(attr_key: Symbol, attr_val: T.untyped).returns(T.untyped)}
         | 
| 50 | 
            -
                def  | 
| 51 | 
            -
                  # Validate  | 
| 52 | 
            -
                  if !class_variable_get(:@@index_attributes).include?(attr_key) &&
         | 
| 53 | 
            -
                     !class_variable_get(:@@range_index_attributes).include?(attr_key)
         | 
| 54 | 
            -
                    raise(
         | 
| 55 | 
            -
                      Redcord::AttributeNotIndexed,
         | 
| 56 | 
            -
                      "#{attr_key} is not an indexed attribute.",
         | 
| 57 | 
            -
                    )
         | 
| 58 | 
            -
                  end
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                  # Validate attribute types for normal index attributes
         | 
| 59 | 
            +
                def validate_types_and_encode_query(attr_key, attr_val)
         | 
| 60 | 
            +
                  # Validate attribute types for index attributes
         | 
| 61 61 | 
             
                  attr_type = get_attr_type(attr_key)
         | 
| 62 | 
            -
                  if class_variable_get(:@@index_attributes).include?(attr_key)
         | 
| 62 | 
            +
                  if class_variable_get(:@@index_attributes).include?(attr_key) || attr_key == shard_by_attribute
         | 
| 63 63 | 
             
                    validate_attr_type(attr_val, attr_type)
         | 
| 64 64 | 
             
                  else
         | 
| 65 | 
            -
                     | 
| 66 | 
            -
                    if attr_val.is_a?(Redcord::RangeInterval)
         | 
| 67 | 
            -
                      validate_attr_type(
         | 
| 68 | 
            -
                        attr_val.min,
         | 
| 69 | 
            -
                        T.cast(T.nilable(attr_type), T::Types::Base),
         | 
| 70 | 
            -
                      )
         | 
| 71 | 
            -
                      validate_attr_type(
         | 
| 72 | 
            -
                        attr_val.max,
         | 
| 73 | 
            -
                        T.cast(T.nilable(attr_type), T::Types::Base),
         | 
| 74 | 
            -
                      )
         | 
| 75 | 
            -
                    else
         | 
| 76 | 
            -
                      validate_attr_type(attr_val, attr_type)
         | 
| 77 | 
            -
                    end
         | 
| 65 | 
            +
                    validate_range_attr_types(attr_val, attr_type)
         | 
| 78 66 |  | 
| 79 67 | 
             
                    # Range index attributes need to be further encoded into a format
         | 
| 80 68 | 
             
                    # understood by the Lua script.
         | 
| @@ -82,10 +70,73 @@ module Redcord::Serializer | |
| 82 70 | 
             
                      attr_val = encode_range_index_attr_val(attr_key, attr_val)
         | 
| 83 71 | 
             
                    end
         | 
| 84 72 | 
             
                  end
         | 
| 85 | 
            -
             | 
| 86 73 | 
             
                  attr_val
         | 
| 87 74 | 
             
                end
         | 
| 88 75 |  | 
| 76 | 
            +
                # Validate that attributes queried for are index attributes
         | 
| 77 | 
            +
                # For custom index: validate that attributes are present in specified index
         | 
| 78 | 
            +
                sig { params(attr_keys: T::Array[Symbol], custom_index_name: T.nilable(Symbol)).void}
         | 
| 79 | 
            +
                def validate_index_attributes(attr_keys, custom_index_name: nil)
         | 
| 80 | 
            +
                  custom_index_attributes = class_variable_get(:@@custom_index_attributes)[custom_index_name]
         | 
| 81 | 
            +
                  attr_keys.each do |attr_key|
         | 
| 82 | 
            +
                    next if attr_key == shard_by_attribute
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    if !custom_index_attributes.empty?
         | 
| 85 | 
            +
                      if !custom_index_attributes.include?(attr_key)
         | 
| 86 | 
            +
                        raise(
         | 
| 87 | 
            +
                          Redcord::AttributeNotIndexed,
         | 
| 88 | 
            +
                          "#{attr_key} is not a part of #{custom_index_name} index.",
         | 
| 89 | 
            +
                        )
         | 
| 90 | 
            +
                      end
         | 
| 91 | 
            +
                    else
         | 
| 92 | 
            +
                      if !class_variable_get(:@@index_attributes).include?(attr_key) &&
         | 
| 93 | 
            +
                        !class_variable_get(:@@range_index_attributes).include?(attr_key)
         | 
| 94 | 
            +
                        raise(
         | 
| 95 | 
            +
                          Redcord::AttributeNotIndexed,
         | 
| 96 | 
            +
                          "#{attr_key} is not an indexed attribute.",
         | 
| 97 | 
            +
                        )
         | 
| 98 | 
            +
                      end
         | 
| 99 | 
            +
                    end
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                # Validate exclusive ranges not used; Change all query conditions to range form;
         | 
| 104 | 
            +
                # The position of the attribute and type of query is validated on Lua side
         | 
| 105 | 
            +
                sig { params(query_conditions: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped])}
         | 
| 106 | 
            +
                def validate_and_adjust_custom_index_query_conditions(query_conditions)
         | 
| 107 | 
            +
                  adjusted_query_conditions = query_conditions.clone
         | 
| 108 | 
            +
                  query_conditions.each do |attr_key, condition|
         | 
| 109 | 
            +
                    if !condition.is_a?(Array)
         | 
| 110 | 
            +
                      adjusted_query_conditions[attr_key] = [condition, condition]
         | 
| 111 | 
            +
                    elsif condition[0].to_s[0] == '(' or condition[1].to_s[0] == '('
         | 
| 112 | 
            +
                      raise(Redcord::CustomIndexInvalidQuery, "Custom index doesn't support exclusive ranges")
         | 
| 113 | 
            +
                    end
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
                  adjusted_query_conditions
         | 
| 116 | 
            +
                end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                sig {
         | 
| 119 | 
            +
                  params(
         | 
| 120 | 
            +
                    attr_val: T.untyped,
         | 
| 121 | 
            +
                    attr_type: T.any(Class, T::Types::Base),
         | 
| 122 | 
            +
                  ).void
         | 
| 123 | 
            +
                }
         | 
| 124 | 
            +
                def validate_range_attr_types(attr_val, attr_type)
         | 
| 125 | 
            +
                  # Validate attribute types for range index attributes
         | 
| 126 | 
            +
                  if attr_val.is_a?(Redcord::RangeInterval)
         | 
| 127 | 
            +
                    validate_attr_type(
         | 
| 128 | 
            +
                      attr_val.min,
         | 
| 129 | 
            +
                      T.cast(T.nilable(attr_type), T::Types::Base),
         | 
| 130 | 
            +
                    )
         | 
| 131 | 
            +
                    validate_attr_type(
         | 
| 132 | 
            +
                      attr_val.max,
         | 
| 133 | 
            +
                      T.cast(T.nilable(attr_type), T::Types::Base),
         | 
| 134 | 
            +
                    )
         | 
| 135 | 
            +
                  else
         | 
| 136 | 
            +
                    validate_attr_type(attr_val, attr_type)
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 89 140 | 
             
                sig {
         | 
| 90 141 | 
             
                  params(
         | 
| 91 142 | 
             
                    attr_val: T.untyped,
         | 
| @@ -137,7 +188,7 @@ module Redcord::Serializer | |
| 137 188 | 
             
                sig {
         | 
| 138 189 | 
             
                  params(
         | 
| 139 190 | 
             
                    redis_hash: T::Hash[T.untyped, T.untyped],
         | 
| 140 | 
            -
                    id:  | 
| 191 | 
            +
                    id: String,
         | 
| 141 192 | 
             
                  ).returns(T.untyped)
         | 
| 142 193 | 
             
                }
         | 
| 143 194 | 
             
                def coerce_and_set_id(redis_hash, id)
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            --[[
         | 
| 2 | 
            +
            EVALSHA SHA1(__FILE__) [field value ...]
         | 
| 3 | 
            +
            > Time complexity: O(N) where N is the number of fields being set.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Create a hash with the specified fields to their respective values stored at
         | 
| 6 | 
            +
            key when key does not exist.
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            # Return value
         | 
| 9 | 
            +
            The id of the created hash as a string.
         | 
| 10 | 
            +
            --]]
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            -- The arguments can be accessed by Lua using the KEYS global variable in the
         | 
| 13 | 
            +
            -- form of a one-based array (so KEYS[1], KEYS[2], ...).
         | 
| 14 | 
            +
            -- All the additional arguments should not represent key names and can be
         | 
| 15 | 
            +
            -- accessed by Lua using the ARGV global variable, very similarly to what
         | 
| 16 | 
            +
            -- happens with keys (so ARGV[1], ARGV[2], ...).
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            --   KEYS = id hash_tag
         | 
| 19 | 
            +
            --   ARGV = Model.name ttl index_attr_size range_index_attr_size custom_index_attrs_flat_size [index_attr_key ...] [range_index_attr_key ...]
         | 
| 20 | 
            +
            --          [custom_index_name attrs_size [custom_index_attr_key ...] ...] attr_key attr_val [attr_key attr_val ..]
         | 
| 21 | 
            +
            <%= include_lua 'shared/lua_helper_methods' %>
         | 
| 22 | 
            +
            <%= include_lua 'shared/index_helper_methods' %>
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            -- Validate input to script before making Redis db calls
         | 
| 25 | 
            +
            if #KEYS ~= 2 then
         | 
| 26 | 
            +
              error('Expected keys to be of size 2')
         | 
| 27 | 
            +
            end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            local id, hash_tag = unpack(KEYS)
         | 
| 30 | 
            +
            local model, ttl = unpack(ARGV)
         | 
| 31 | 
            +
            local key = model .. ':id:' .. id
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            local index_attr_pos = 6
         | 
| 34 | 
            +
            local range_attr_pos = index_attr_pos + ARGV[3]
         | 
| 35 | 
            +
            local custom_attr_pos = range_attr_pos + ARGV[4]
         | 
| 36 | 
            +
            -- Starting position of the attr_key-attr_val pairs
         | 
| 37 | 
            +
            local attr_pos = custom_attr_pos + ARGV[5]
         | 
| 38 | 
            +
             | 
| 39 | 
            +
             | 
| 40 | 
            +
            if redis.call('exists', key) ~= 0 then
         | 
| 41 | 
            +
              error(key .. ' already exists')
         | 
| 42 | 
            +
            end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            -- Forward the script arguments to the Redis command HSET.
         | 
| 45 | 
            +
            -- Call the Redis command: HSET "#{Model.name}:id:#{id}" field value ...
         | 
| 46 | 
            +
            redis.call('hset', key, unpack(ARGV, attr_pos))
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            -- Set TTL on key
         | 
| 49 | 
            +
            if ttl and ttl ~= '-1' then
         | 
| 50 | 
            +
              redis.call('expire', key, ttl)
         | 
| 51 | 
            +
            end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            -- Add id value for any index and range index attributes
         | 
| 54 | 
            +
            local attrs_hash = to_hash(unpack(ARGV, attr_pos))
         | 
| 55 | 
            +
            local index_attr_keys = {unpack(ARGV, index_attr_pos, range_attr_pos - 1)}
         | 
| 56 | 
            +
            if #index_attr_keys > 0 then
         | 
| 57 | 
            +
              for _, attr_key in ipairs(index_attr_keys) do
         | 
| 58 | 
            +
                add_id_to_index_attr(hash_tag, model, attr_key, attrs_hash[attr_key], id)
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
            end
         | 
| 61 | 
            +
            local range_index_attr_keys = {unpack(ARGV, range_attr_pos, custom_attr_pos - 1)}
         | 
| 62 | 
            +
            if #range_index_attr_keys > 0 then
         | 
| 63 | 
            +
              for _, attr_key in ipairs(range_index_attr_keys) do
         | 
| 64 | 
            +
                add_id_to_range_index_attr(hash_tag, model, attr_key, attrs_hash[attr_key], id)
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
            end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
            -- Add a record to every custom index
         | 
| 69 | 
            +
            local custom_index_attr_keys = {unpack(ARGV, custom_attr_pos, attr_pos - 1)}
         | 
| 70 | 
            +
            local i = 1
         | 
| 71 | 
            +
            while i < #custom_index_attr_keys do
         | 
| 72 | 
            +
              local index_name, attrs_num = custom_index_attr_keys[i], custom_index_attr_keys[i+1]
         | 
| 73 | 
            +
              local attr_values = {}
         | 
| 74 | 
            +
              for j, attr_key in ipairs({unpack(custom_index_attr_keys, i + 2, i + attrs_num + 1)}) do
         | 
| 75 | 
            +
                attr_values[j] = attrs_hash[attr_key]
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
              add_record_to_custom_index(hash_tag, model, index_name, attr_values, id)
         | 
| 78 | 
            +
              i = i + 2 + attrs_num
         | 
| 79 | 
            +
            end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
            return nil
         |