redis-objects-preloadable 0.1.0 → 0.1.1

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: 2e46629bdca1958195bdb6eebd11446ae6842a574b8789855fdede06cac509fe
4
- data.tar.gz: 5730090a0ffd1a8697c061d6a714d6f48a75cc140b809d2377364925d2ace50b
3
+ metadata.gz: 2135bc0cffef091721bc51971f69adbfbe636ef10a372758db3e4de164bf7ec9
4
+ data.tar.gz: cae263e6dafa776af239cf3ce8d7a38d1f2eccbf2452b90e5a2a8afe82bf8680
5
5
  SHA512:
6
- metadata.gz: 79589dae4c2e987e95d3d9195f00752da234a5f5b0ec37bd0351c9f5d20be83136836784e2627fdde55fead3e30b384a5e0ff8705a6ccc63128c75f4f5f8a820
7
- data.tar.gz: 6601dcdf77506ebdc93e574ce9e9b3e469c9538a8376d70d134fa75b7b8fe968a97fd38158026ec66c172344b1f0f00ec03b1dbcd9db09595aaebe01d13e7a29
6
+ metadata.gz: 82c717b269c84a0c977b195ec5be729a177e83d00268506eb9d2efc641cf1d32938fd4f9eb9c6346746b55e11ba0b41d954051719140c7e1f99ecd54e345f085
7
+ data.tar.gz: 4b96bf14d1cce588cf96500819898aa0da85468f75fce4e9b9042d6ea22bd50c25ab392def2120323e39d0b47e7a295204a674ea3af48a811e26a6e4c50706e9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.1] - 2026-02-22
4
+
5
+ - Fix Rails 8.1 / Ruby 3.4 compatibility: forward all arguments in `ModelExtension.all` override to avoid `ArgumentError` when `ActiveRecord::Persistence#_find_record` calls `all(all_queries: ...)` ([#fix](https://github.com/kyohah/redis-objects-preloadable/issues))
6
+
3
7
  ## [0.1.0] - 2026-02-20
4
8
 
5
9
  - Initial release
@@ -3,17 +3,37 @@
3
3
  class Redis
4
4
  module Objects
5
5
  module Preloadable
6
+ # ActiveSupport::Concern that integrates Preloadable into ActiveRecord models.
7
+ #
8
+ # Included automatically when a model does +include Redis::Objects::Preloadable+.
9
+ # Overrides +.all+ to extend relations with {RelationExtension} and provides
10
+ # the backward-compatible +read_redis_counter+ helper.
11
+ #
6
12
  module ModelExtension
7
13
  extend ActiveSupport::Concern
8
14
 
9
15
  class_methods do
10
- def all
11
- super.extending(Redis::Objects::Preloadable::RelationExtension)
16
+ # @api private
17
+ def all(...)
18
+ super(...).extending(Redis::Objects::Preloadable::RelationExtension)
12
19
  end
13
20
  end
14
21
 
15
22
  private
16
23
 
24
+ # Backward-compatible helper for reading a counter with SQL fallback.
25
+ #
26
+ # If the counter has a preloaded value, it is returned directly.
27
+ # Otherwise, checks Redis and falls back to the block (SQL query).
28
+ #
29
+ # With transparent preloading, this method is no longer necessary.
30
+ # You can access +counter.value+ directly instead.
31
+ #
32
+ # @param _name [Symbol] the counter attribute name (unused in transparent mode)
33
+ # @param counter [Redis::Counter] the counter instance
34
+ # @yield SQL fallback block that returns the count
35
+ # @return [Integer] the counter value
36
+ #
17
37
  def read_redis_counter(_name, counter)
18
38
  if counter.instance_variable_defined?(:@preloaded_value)
19
39
  raw = counter.instance_variable_get(:@preloaded_value)
@@ -3,15 +3,36 @@
3
3
  class Redis
4
4
  module Objects
5
5
  module Preloadable
6
+ # Redis::Objects types that support MGET (single-value keys).
6
7
  MGET_TYPES = %i[counter value].freeze
7
8
 
9
+ # Holds a batch of records and attribute names, and resolves them
10
+ # in a single Redis round-trip on first access.
11
+ #
12
+ # PreloadContext is created by {RelationExtension#load} or
13
+ # {Preloadable.preload} and attached to each redis-objects instance.
14
+ # Resolution is lazy and idempotent.
15
+ #
16
+ # @example
17
+ # context = PreloadContext.new(records, [:view_count, :tag_ids])
18
+ # context.resolve! # fetches all values in one batch
19
+ # context.resolve! # no-op on subsequent calls
20
+ #
8
21
  class PreloadContext
22
+ # @param records [Array<ActiveRecord::Base>] records to preload
23
+ # @param names [Array<Symbol>] redis-objects attribute names
9
24
  def initialize(records, names)
10
25
  @records = records
11
26
  @names = names
12
27
  @resolved = false
13
28
  end
14
29
 
30
+ # Resolve all preloaded attributes in a single batch.
31
+ #
32
+ # Uses MGET for counter/value types and pipelined commands for
33
+ # list/set/sorted_set/hash_key types. This method is idempotent.
34
+ #
35
+ # @return [void]
15
36
  def resolve!
16
37
  return if @resolved
17
38
 
@@ -3,15 +3,38 @@
3
3
  class Redis
4
4
  module Objects
5
5
  module Preloadable
6
+ # Extends ActiveRecord::Relation with +redis_preload+ scope.
7
+ #
8
+ # This module is automatically mixed into relations via
9
+ # {ModelExtension::ClassMethods#all}.
10
+ #
11
+ # @example
12
+ # Widget.where(active: true).redis_preload(:view_count, :tag_ids).each do |w|
13
+ # w.view_count.value # preloaded
14
+ # end
15
+ #
6
16
  module RelationExtension
17
+ # Declare redis-objects attributes to batch-preload when the relation loads.
18
+ #
19
+ # Can be chained with other ActiveRecord scopes. Preloading is lazy:
20
+ # no Redis calls until the first attribute access.
21
+ #
22
+ # @param names [Array<Symbol>] redis-objects attribute names
23
+ # @return [ActiveRecord::Relation] a new relation with preload metadata
24
+ #
25
+ # @example
26
+ # Widget.order(:id).redis_preload(:view_count).limit(100)
27
+ #
7
28
  def redis_preload(*names)
8
29
  spawn.tap { |r| r.instance_variable_set(:@redis_preload_names, names) }
9
30
  end
10
31
 
32
+ # @return [Array<Symbol>] the redis-objects attribute names to preload
11
33
  def redis_preload_names
12
34
  @redis_preload_names || []
13
35
  end
14
36
 
37
+ # @api private
15
38
  def load
16
39
  result = super
17
40
 
@@ -4,7 +4,10 @@ class Redis
4
4
  module Objects
5
5
  module Preloadable
6
6
  module TypePatches
7
+ # Prepended onto Redis::Counter to support preloaded values.
8
+ # Fetched via MGET. Returns +.to_i+ (0 for nil/missing keys).
7
9
  module Counter
10
+ # @api private
8
11
  def preload!(raw_value)
9
12
  @preloaded_value = raw_value
10
13
  end
@@ -4,7 +4,10 @@ class Redis
4
4
  module Objects
5
5
  module Preloadable
6
6
  module TypePatches
7
+ # Prepended onto Redis::HashKey to support preloaded values.
8
+ # Fetched via HGETALL in a pipeline.
7
9
  module HashKey
10
+ # @api private
8
11
  def preload!(raw_value)
9
12
  @preloaded_value = raw_value || {}
10
13
  end
@@ -4,7 +4,10 @@ class Redis
4
4
  module Objects
5
5
  module Preloadable
6
6
  module TypePatches
7
+ # Prepended onto Redis::List to support preloaded values.
8
+ # Fetched via LRANGE 0 -1 in a pipeline.
7
9
  module List
10
+ # @api private
8
11
  def preload!(raw_value)
9
12
  @preloaded_value = raw_value || []
10
13
  end
@@ -4,7 +4,10 @@ class Redis
4
4
  module Objects
5
5
  module Preloadable
6
6
  module TypePatches
7
+ # Prepended onto Redis::Set to support preloaded values.
8
+ # Fetched via SMEMBERS in a pipeline.
7
9
  module Set
10
+ # @api private
8
11
  def preload!(raw_value)
9
12
  @preloaded_value = raw_value || []
10
13
  end
@@ -4,7 +4,10 @@ class Redis
4
4
  module Objects
5
5
  module Preloadable
6
6
  module TypePatches
7
+ # Prepended onto Redis::SortedSet to support preloaded values.
8
+ # Fetched via ZRANGE 0 -1 WITHSCORES in a pipeline.
7
9
  module SortedSet
10
+ # @api private
8
11
  def preload!(raw_value)
9
12
  @preloaded_value = raw_value || []
10
13
  end
@@ -4,7 +4,10 @@ class Redis
4
4
  module Objects
5
5
  module Preloadable
6
6
  module TypePatches
7
+ # Prepended onto Redis::Value to support preloaded values.
8
+ # Fetched via MGET. Returns raw string or nil.
7
9
  module Value
10
+ # @api private
8
11
  def preload!(raw_value)
9
12
  @preloaded_value = raw_value
10
13
  end
@@ -3,7 +3,7 @@
3
3
  class Redis
4
4
  module Objects
5
5
  module Preloadable
6
- VERSION = "0.1.0"
6
+ VERSION = "0.1.1"
7
7
  end
8
8
  end
9
9
  end
@@ -21,6 +21,36 @@ Redis::Set.prepend(Redis::Objects::Preloadable::TypePatches::Set)
21
21
  Redis::SortedSet.prepend(Redis::Objects::Preloadable::TypePatches::SortedSet)
22
22
  Redis::HashKey.prepend(Redis::Objects::Preloadable::TypePatches::HashKey)
23
23
 
24
+ # Redis::Objects::Preloadable eliminates N+1 Redis calls for redis-objects
25
+ # attributes on ActiveRecord models.
26
+ #
27
+ # It provides two APIs:
28
+ #
29
+ # 1. +redis_preload+ scope on ActiveRecord relations
30
+ # 2. +Redis::Objects::Preloadable.preload+ for arbitrary record arrays
31
+ #
32
+ # == Basic usage
33
+ #
34
+ # class Pack < ApplicationRecord
35
+ # include Redis::Objects
36
+ # include Redis::Objects::Preloadable
37
+ #
38
+ # counter :cache_total_count
39
+ # list :recent_item_ids
40
+ # end
41
+ #
42
+ # # Scope-based (top-level queries)
43
+ # Pack.redis_preload(:cache_total_count, :recent_item_ids).limit(100).each do |pack|
44
+ # pack.cache_total_count.value # preloaded via MGET
45
+ # pack.recent_item_ids.values # preloaded via pipeline
46
+ # end
47
+ #
48
+ # == Association-loaded records
49
+ #
50
+ # users = User.includes(:articles).load
51
+ # articles = users.flat_map(&:articles)
52
+ # Redis::Objects::Preloadable.preload(articles, :view_count)
53
+ #
24
54
  class Redis
25
55
  module Objects
26
56
  module Preloadable
@@ -28,6 +58,23 @@ class Redis
28
58
  base.include(ModelExtension)
29
59
  end
30
60
 
61
+ # Batch-preload Redis::Objects attributes on an array of records.
62
+ #
63
+ # Use this for records loaded outside of +redis_preload+ scope,
64
+ # such as association-loaded records via +includes+.
65
+ #
66
+ # Preloading is lazy: no Redis calls are made until the first
67
+ # attribute access on any of the records.
68
+ #
69
+ # @param records [Array<ActiveRecord::Base>, ActiveRecord::Relation] records to preload
70
+ # @param names [Array<Symbol>] redis-objects attribute names to preload
71
+ # @return [Array<ActiveRecord::Base>] the same records array
72
+ #
73
+ # @example Preload on association-loaded records
74
+ # users = User.includes(:articles).load
75
+ # articles = users.flat_map(&:articles)
76
+ # Redis::Objects::Preloadable.preload(articles, :view_count, :cached_summary)
77
+ #
31
78
  def self.preload(records, *names)
32
79
  records = records.to_a
33
80
  return records if records.empty? || names.empty?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-objects-preloadable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kyohah