redis-memo 0.0.0.beta.2 → 0.0.0.beta.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfc639839800aa1290622d733d9fd23619de8ccd63f2129e4570eb41a295884b
4
- data.tar.gz: 26e2d86776244ba5cb0a8cebbc18e468ccf737888de9990c3a2bb9fc931bd4d5
3
+ metadata.gz: fd8d075c12b20782f27bdfe2488cba1c0f31f5fb537241a9cedcc72f484588b9
4
+ data.tar.gz: 0eef678b83746557ba84aca013e660ecc31a028042cf052606673179e185d61e
5
5
  SHA512:
6
- metadata.gz: 3c6950a588fbf448d4b88e126ee9b32753ef314225d3aa5f69430d7e386a9a1085b517a08f4f6de2725810f35b493c20e2af62729f547891bcc78aaf4ce73ef2
7
- data.tar.gz: 1e380d3ff6d457a6c93dcdebcbf9be072e919b2f5ff25377f169287ed8ac6c379f9a8db29a494d9174cff91e815e680201970ece43baba69b627678b628a3d77
6
+ metadata.gz: a8f573eb77832f3dfb0600ab607c8d60090969082c86a9403a98b784899931da60d66f1b761bf863fd4007492ff47dee2d7880ec2f10cf5143fce33bc791684b
7
+ data.tar.gz: 4f9e44c13d2b4918f69f143f78eb4d80ad306b43abb650c03c481d727a88938f70c3ef89854e5c67afe92cc656df82ff44df84bb07878904bcdfbb2b86c15275
data/lib/redis-memo.rb CHANGED
@@ -1 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Automatically require the main file of the redis-memo gem when adding it to
4
+ # the Gemfile
1
5
  require_relative 'redis_memo'
data/lib/redis_memo.rb CHANGED
@@ -1,18 +1,53 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'active_support/all'
3
4
  require 'digest'
4
5
  require 'json'
6
+ require 'securerandom'
5
7
 
6
8
  module RedisMemo
7
9
  require 'redis_memo/memoize_method'
8
10
  require 'redis_memo/memoize_query'
9
11
 
12
+ # A process-level +RedisMemo::Options+ instance that stores the global
13
+ # options. This object can be modified by +RedisMemo.configure+.
14
+ #
15
+ # +memoize_method+ allows users to provide method-level configuration.
16
+ # When no callsite-level configuration specified we will use the values in
17
+ # +DefaultOptions+ as the default value.
10
18
  DefaultOptions = RedisMemo::Options.new
11
19
 
20
+ # @todo Move thread keys to +RedisMemo::ThreadKey+
21
+ THREAD_KEY_WITHOUT_MEMO = :__redis_memo_without_memo__
22
+
23
+ # Configure global-level default options. Those options will be used unless
24
+ # some options specified at +memoize_method+ callsite level. See
25
+ # +RedisMemo::Options+ for all the possible options.
26
+ #
27
+ # @yieldparam [RedisMemo::Options] default_options
28
+ # +RedisMemo::DefaultOptions+
29
+ # @return [void]
12
30
  def self.configure(&blk)
13
31
  blk.call(DefaultOptions)
14
32
  end
15
33
 
34
+ # Batch Redis calls triggered by +memoize_method+ to minimize the round trips
35
+ # to Redis.
36
+ # - Batches cannot be nested
37
+ # - When a batch is still open (while still in the +RedisMemo.batch+ block)
38
+ # the return value of any memoized method is a +RedisMemo::Future+ instead of
39
+ # the actual method value
40
+ # - The actual method values are returned as a list, in the same order as
41
+ # invoking, after exiting the block
42
+ #
43
+ # @example
44
+ # results = RedisMemo.batch do
45
+ # 5.times { |i| memoized_calculation(i) }
46
+ # nil # Not the return value of the block
47
+ # end
48
+ # results.size == 5 # true
49
+ #
50
+ # See +RedisMemo::Batch+ for more information.
16
51
  def self.batch(&blk)
17
52
  RedisMemo::Batch.open
18
53
  blk.call
@@ -21,10 +56,17 @@ module RedisMemo
21
56
  RedisMemo::Batch.close
22
57
  end
23
58
 
59
+ # @todo Move this method out of the top namespace
24
60
  def self.checksum(serialized)
25
61
  Digest::SHA1.base64digest(serialized)
26
62
  end
27
63
 
64
+ # @todo Move this method out of the top namespace
65
+ def self.uuid
66
+ SecureRandom.uuid
67
+ end
68
+
69
+ # @todo Move this method out of the top namespace
28
70
  def self.deep_sort_hash(orig_hash)
29
71
  {}.tap do |new_hash|
30
72
  orig_hash.sort.each do |k, v|
@@ -33,12 +75,17 @@ module RedisMemo
33
75
  end
34
76
  end
35
77
 
36
- THREAD_KEY_WITHOUT_MEMO = :__redis_memo_without_memo__
37
-
78
+ # Whether the current execution context has been configured to skip
79
+ # memoization and use the uncached code path.
80
+ #
81
+ # @return [Boolean]
38
82
  def self.without_memo?
39
83
  Thread.current[THREAD_KEY_WITHOUT_MEMO] == true
40
84
  end
41
85
 
86
+ # Configure the wrapped code in the block to skip memoization.
87
+ #
88
+ # @yield [] no_args The block of code to skip memoization.
42
89
  def self.without_memo
43
90
  prev_value = Thread.current[THREAD_KEY_WITHOUT_MEMO]
44
91
  Thread.current[THREAD_KEY_WITHOUT_MEMO] = true
@@ -47,6 +94,7 @@ module RedisMemo
47
94
  Thread.current[THREAD_KEY_WITHOUT_MEMO] = prev_value
48
95
  end
49
96
 
97
+ # @todo Move errors to a separate file errors.rb
50
98
  class ArgumentError < ::ArgumentError; end
51
99
  class RuntimeError < ::RuntimeError; end
52
100
  end
@@ -66,7 +66,12 @@ class RedisMemo::AfterCommit
66
66
  @@pending_memo_versions.each do |key, version|
67
67
  invalidation_queue =
68
68
  RedisMemo::Memoizable::Invalidation.class_variable_get(:@@invalidation_queue)
69
- invalidation_queue << [key, version, @@previous_memo_versions[key]]
69
+
70
+ invalidation_queue << RedisMemo::Memoizable::Invalidation::Task.new(
71
+ key,
72
+ version,
73
+ @@previous_memo_versions[key],
74
+ )
70
75
  end
71
76
 
72
77
  RedisMemo::Memoizable::Invalidation.drain_invalidation_queue
@@ -5,8 +5,9 @@ require_relative 'redis'
5
5
  class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
6
6
  class Rescuable < Exception; end
7
7
 
8
- THREAD_KEY_LOCAL_CACHE = :__redis_memo_cache_local_cache__
9
- THREAD_KEY_RAISE_ERROR = :__redis_memo_cache_raise_error__
8
+ THREAD_KEY_LOCAL_CACHE = :__redis_memo_cache_local_cache__
9
+ THREAD_KEY_LOCAL_DEPENDENCY_CACHE = :__redis_memo_local_cache_dependency_cache__
10
+ THREAD_KEY_RAISE_ERROR = :__redis_memo_cache_raise_error__
10
11
 
11
12
  @@redis = nil
12
13
  @@redis_store = nil
@@ -44,12 +45,18 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
44
45
  Thread.current[THREAD_KEY_LOCAL_CACHE]
45
46
  end
46
47
 
48
+ def self.local_dependency_cache
49
+ Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE]
50
+ end
51
+
47
52
  class << self
48
53
  def with_local_cache(&blk)
49
54
  Thread.current[THREAD_KEY_LOCAL_CACHE] = {}
55
+ Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE] = {}
50
56
  blk.call
51
57
  ensure
52
58
  Thread.current[THREAD_KEY_LOCAL_CACHE] = nil
59
+ Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE] = nil
53
60
  end
54
61
 
55
62
  # RedisCacheStore doesn't read from the local cache before reading from redis
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require 'securerandom'
3
2
 
4
3
  class RedisMemo::Memoizable
5
4
  require_relative 'memoizable/dependency'
@@ -52,7 +51,7 @@ class RedisMemo::Memoizable
52
51
  cache_key = instance.cache_key
53
52
  RedisMemo::Memoizable::Invalidation.bump_version_later(
54
53
  cache_key,
55
- SecureRandom.uuid,
54
+ RedisMemo.uuid,
56
55
  )
57
56
  end
58
57
 
@@ -96,7 +95,7 @@ class RedisMemo::Memoizable
96
95
  # cached result.
97
96
  need_to_bump_versions = true
98
97
 
99
- new_version = SecureRandom.uuid
98
+ new_version = RedisMemo.uuid
100
99
  RedisMemo::Memoizable::Invalidation.bump_version_later(
101
100
  key,
102
101
  new_version,
@@ -25,6 +25,9 @@ class RedisMemo::Memoizable::Dependency
25
25
  # Extract dependencies from the current memoizable and recurse
26
26
  instance_exec(&memo.depends_on)
27
27
  end
28
+ when ActiveRecord::Relation
29
+ extracted = extract_dependencies_for_relation(dependency)
30
+ nodes.merge!(extracted.nodes)
28
31
  when UsingActiveRecord
29
32
  [
30
33
  dependency.redis_memo_class_memoizable,
@@ -40,6 +43,21 @@ class RedisMemo::Memoizable::Dependency
40
43
  end
41
44
  end
42
45
 
46
+ def extract_dependencies_for_relation(relation)
47
+ # Extract the dependent memos of an Arel without calling exec_query to actually execute the query
48
+ RedisMemo::MemoizeQuery::CachedSelect.with_new_query_context do
49
+ connection = ActiveRecord::Base.connection
50
+ query, binds, _ = connection.send(:to_sql_and_binds, relation.arel)
51
+ RedisMemo::MemoizeQuery::CachedSelect.current_query = relation.arel
52
+ is_query_cached = RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(query)
53
+ raise(
54
+ RedisMemo::ArgumentError,
55
+ "Invalid Arel dependency. Query is not enabled for RedisMemo caching."
56
+ ) unless is_query_cached
57
+ extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
58
+ end
59
+ end
60
+
43
61
  class UsingActiveRecord
44
62
  def self.===(dependency)
45
63
  RedisMemo::MemoizeQuery.using_active_record?(dependency)
@@ -3,12 +3,29 @@ require_relative '../after_commit'
3
3
  require_relative '../cache'
4
4
 
5
5
  module RedisMemo::Memoizable::Invalidation
6
- # This is a thread safe data structure
7
- #
8
- # Handle transient network errors during cache invalidation
9
- # Each item in the queue is a tuple:
10
- #
11
- # [key, version (a UUID), previous_version (a UUID)]
6
+ class Task
7
+ attr_reader :key
8
+ attr_reader :version
9
+ attr_reader :previous_version
10
+
11
+ def initialize(key, version, previous_version)
12
+ @key = key
13
+ @version = version
14
+ @previous_version = previous_version
15
+ @created_at = current_timestamp
16
+ end
17
+
18
+ def current_timestamp
19
+ Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
20
+ end
21
+
22
+ def duration
23
+ current_timestamp - @created_at
24
+ end
25
+ end
26
+
27
+ # This is a thread safe data structure to handle transient network errors
28
+ # during cache invalidation
12
29
  #
13
30
  # When an invalidation call arrives at Redis, we only bump to the specified
14
31
  # version (so the cached results using that version will become visible) if
@@ -28,8 +45,6 @@ module RedisMemo::Memoizable::Invalidation
28
45
  @@invalidation_queue = Queue.new
29
46
 
30
47
  def self.bump_version_later(key, version, previous_version: nil)
31
- RedisMemo::DefaultOptions.logger&.info("[received] Bump memo key #{key}")
32
-
33
48
  if RedisMemo::AfterCommit.in_transaction?
34
49
  previous_version ||= RedisMemo::AfterCommit.pending_memo_versions[key]
35
50
  end
@@ -52,7 +67,7 @@ module RedisMemo::Memoizable::Invalidation
52
67
  previous_version: previous_version,
53
68
  )
54
69
  else
55
- @@invalidation_queue << [key, version, previous_version]
70
+ @@invalidation_queue << Task.new(key, version, previous_version)
56
71
  end
57
72
  end
58
73
 
@@ -89,31 +104,31 @@ module RedisMemo::Memoizable::Invalidation
89
104
  return redis.call('set', key, new_version, unpack(px))
90
105
  LUA
91
106
 
92
- def self.bump_version(cache_key, version, previous_version:)
93
- RedisMemo::Tracer.trace('redis_memo.memoizable.bump_version', nil) do
107
+ def self.bump_version(task)
108
+ RedisMemo::Tracer.trace('redis_memo.memoizable.bump_version', task.key) do
94
109
  ttl = RedisMemo::DefaultOptions.expires_in
95
110
  ttl = (ttl * 1000.0).to_i if ttl
96
111
  RedisMemo::Cache.redis.eval(
97
112
  LUA_BUMP_VERSION,
98
- keys: [cache_key],
99
- argv: [previous_version, version, SecureRandom.uuid, ttl],
113
+ keys: [task.key],
114
+ argv: [task.previous_version, task.version, RedisMemo.uuid, ttl],
100
115
  )
116
+ RedisMemo::Tracer.set_tag(enqueue_to_finish: task.duration)
101
117
  end
102
- RedisMemo::DefaultOptions.logger&.info("[performed] Bump memo key #{cache_key}")
103
118
  end
104
119
 
105
120
  def self.drain_invalidation_queue_now
106
121
  retry_queue = []
107
122
  until @@invalidation_queue.empty?
108
- tuple = @@invalidation_queue.pop
123
+ task = @@invalidation_queue.pop
109
124
  begin
110
- bump_version(tuple[0], tuple[1], previous_version: tuple[2])
125
+ bump_version(task)
111
126
  rescue SignalException, Redis::BaseConnectionError
112
- retry_queue << tuple
127
+ retry_queue << task
113
128
  end
114
129
  end
115
130
  ensure
116
- retry_queue.each { |t| @@invalidation_queue << t }
131
+ retry_queue.each { |task| @@invalidation_queue << task }
117
132
  end
118
133
 
119
134
  at_exit do
@@ -52,7 +52,7 @@ module RedisMemo::MemoizeMethod
52
52
  "#{method_name} is not a memoized method"
53
53
  )
54
54
  end
55
- RedisMemo::MemoizeMethod.extract_dependencies(self, *method_args, &method_depends_on)
55
+ RedisMemo::MemoizeMethod.get_or_extract_dependencies(self, *method_args, &method_depends_on)
56
56
  end
57
57
  end
58
58
 
@@ -71,11 +71,21 @@ module RedisMemo::MemoizeMethod
71
71
  dependency
72
72
  end
73
73
 
74
+ def self.get_or_extract_dependencies(ref, *method_args, &depends_on)
75
+ if RedisMemo::Cache.local_dependency_cache
76
+ RedisMemo::Cache.local_dependency_cache[ref] ||= {}
77
+ RedisMemo::Cache.local_dependency_cache[ref][depends_on] ||= {}
78
+ RedisMemo::Cache.local_dependency_cache[ref][depends_on][method_args] ||= extract_dependencies(ref, *method_args, &depends_on)
79
+ else
80
+ extract_dependencies(ref, *method_args, &depends_on)
81
+ end
82
+ end
83
+
74
84
  def self.method_cache_keys(future_contexts)
75
85
  memos = Array.new(future_contexts.size)
76
86
  future_contexts.each_with_index do |(ref, _, method_args, depends_on), i|
77
87
  if depends_on
78
- dependency = extract_dependencies(ref, *method_args, &depends_on)
88
+ dependency = get_or_extract_dependencies(ref, *method_args, &depends_on)
79
89
  memos[i] = dependency.memos
80
90
  end
81
91
  end
@@ -103,6 +103,10 @@ if defined?(ActiveRecord)
103
103
  end
104
104
 
105
105
  def self.invalidate(record)
106
+ RedisMemo::Memoizable.invalidate(to_memos(record))
107
+ end
108
+
109
+ def self.to_memos(record)
106
110
  # Invalidate memos with current values
107
111
  memos_to_invalidate = memoized_columns(record.class).map do |columns|
108
112
  props = {}
@@ -137,7 +141,7 @@ if defined?(ActiveRecord)
137
141
  end
138
142
  end
139
143
 
140
- RedisMemo::Memoizable.invalidate(memos_to_invalidate)
144
+ memos_to_invalidate
141
145
  end
142
146
  end
143
147
  end
@@ -187,6 +187,19 @@ class RedisMemo::MemoizeQuery::CachedSelect
187
187
  Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = nil
188
188
  end
189
189
 
190
+ def self.with_new_query_context
191
+ prev_arel = Thread.current[THREAD_KEY_AREL]
192
+ prev_substitutes = Thread.current[THREAD_KEY_SUBSTITUTES]
193
+ prev_bind_params = Thread.current[THREAD_KEY_AREL_BIND_PARAMS]
194
+ RedisMemo::MemoizeQuery::CachedSelect.reset_current_query
195
+
196
+ yield
197
+ ensure
198
+ Thread.current[THREAD_KEY_AREL] = prev_arel
199
+ Thread.current[THREAD_KEY_SUBSTITUTES] = prev_substitutes
200
+ Thread.current[THREAD_KEY_AREL_BIND_PARAMS] = prev_bind_params
201
+ end
202
+
190
203
  private
191
204
 
192
205
  # A pre-order Depth First Search
@@ -33,7 +33,6 @@ class RedisMemo::MemoizeQuery::Invalidation
33
33
  # Methods that won't trigger model callbacks
34
34
  # https://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks
35
35
  %i(
36
- import import!
37
36
  decrement_counter
38
37
  delete_all delete_by
39
38
  increment_counter
@@ -43,7 +42,7 @@ class RedisMemo::MemoizeQuery::Invalidation
43
42
  upsert upsert_all
44
43
  ).each do |method_name|
45
44
  # Example: Model.update_all
46
- rewrite_bulk_update_method(
45
+ rewrite_default_method(
47
46
  model_class,
48
47
  model_class,
49
48
  method_name,
@@ -51,7 +50,7 @@ class RedisMemo::MemoizeQuery::Invalidation
51
50
  )
52
51
 
53
52
  # Example: Model.where(...).update_all
54
- rewrite_bulk_update_method(
53
+ rewrite_default_method(
55
54
  model_class,
56
55
  model_class.const_get(:ActiveRecord_Relation),
57
56
  method_name,
@@ -59,20 +58,28 @@ class RedisMemo::MemoizeQuery::Invalidation
59
58
  )
60
59
  end
61
60
 
61
+ %i(
62
+ import import!
63
+ ).each do |method_name|
64
+ rewrite_import_method(
65
+ model_class,
66
+ method_name,
67
+ )
68
+ end
69
+
62
70
  model_class.class_variable_set(var_name, true)
63
71
  end
64
72
 
65
73
  private
66
74
 
67
75
  #
68
- # There’s no good way to perform fine-grind cache invalidation when operations
69
- # are bulk update operations such as import, update_all, and destroy_all:
70
- # Performing fine-grind cache invalidation would require the applications to
71
- # fetch additional data from the database, which might lead to performance
72
- # degradation. Thus we simply invalidate all existing cached records after each
73
- # bulk_updates.
76
+ # There’s no good way to perform fine-grind cache invalidation when
77
+ # operations are bulk update operations such as update_all, and delete_all
78
+ # witout fetching additional data from the database, which might lead to
79
+ # performance degradation. Thus, by default, we simply invalidate all
80
+ # existing cached records after each bulk_updates.
74
81
  #
75
- def self.rewrite_bulk_update_method(model_class, klass, method_name, class_method:)
82
+ def self.rewrite_default_method(model_class, klass, method_name, class_method:)
76
83
  methods = class_method ? :methods : :instance_methods
77
84
  return unless klass.send(methods).include?(method_name)
78
85
 
@@ -87,4 +94,79 @@ class RedisMemo::MemoizeQuery::Invalidation
87
94
  end
88
95
  end
89
96
  end
97
+
98
+ def self.rewrite_import_method(model_class, method_name)
99
+ # This optimization to avoid over-invalidation only works on postgres
100
+ unless ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
101
+ rewrite_default_method(model_class, model_class, method_name, class_method: true)
102
+ return
103
+ end
104
+
105
+ model_class.singleton_class.class_eval do
106
+ alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
107
+
108
+ # For the args format, see
109
+ # https://github.com/zdennis/activerecord-import/blob/master/lib/activerecord-import/import.rb#L128
110
+ define_method method_name do |*args, &blk|
111
+ options = args.last.is_a?(Hash) ? args.last : {}
112
+ records = args[args.last.is_a?(Hash) ? -2 : -1]
113
+ unique_by = options[:on_duplicate_key_update]
114
+ if unique_by.is_a?(Hash)
115
+ unique_by = unique_by[:columns]
116
+ end
117
+
118
+ if records.last.is_a?(Hash)
119
+ records.map! { |hash| model_class.new(hash) }
120
+ end
121
+
122
+ # Invalidate the records before and after the import to resolve
123
+ # - default values filled by the database
124
+ # - updates on conflict conditions
125
+ records_to_invalidate =
126
+ if unique_by
127
+ RedisMemo::MemoizeQuery::Invalidation.send(
128
+ :select_by_uniq_index,
129
+ records,
130
+ unique_by,
131
+ )
132
+ else
133
+ []
134
+ end
135
+
136
+ result = send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
137
+
138
+ records_to_invalidate += RedisMemo.without_memo do
139
+ # Not all databases support "RETURNING", which is useful when
140
+ # invaldating records after bulk creation
141
+ model_class.where(model_class.primary_key => result.ids).to_a
142
+ end
143
+
144
+ memos_to_invalidate = records_to_invalidate.map do |record|
145
+ RedisMemo::MemoizeQuery.to_memos(record)
146
+ end
147
+ RedisMemo::Memoizable.invalidate(memos_to_invalidate.flatten)
148
+
149
+ result
150
+ end
151
+ end
152
+ end
153
+
154
+ def self.select_by_uniq_index(records, unique_by)
155
+ model_class = records.first.class
156
+ or_chain = nil
157
+
158
+ records.each do |record|
159
+ conditions = {}
160
+ unique_by.each do |column|
161
+ conditions[column] = record.send(column)
162
+ end
163
+ if or_chain
164
+ or_chain = or_chain.or(model_class.where(conditions))
165
+ else
166
+ or_chain = model_class.where(conditions)
167
+ end
168
+ end
169
+
170
+ RedisMemo.without_memo { or_chain.to_a }
171
+ end
90
172
  end
@@ -11,13 +11,15 @@ class RedisMemo::Tracer
11
11
  end
12
12
  end
13
13
 
14
- def self.set_tag(cache_hit:)
14
+ def self.set_tag(**tags)
15
15
  tracer = RedisMemo::DefaultOptions.tracer
16
16
  return if tracer.nil? || !tracer.respond_to?(:active_span)
17
17
 
18
18
  active_span = tracer.active_span
19
19
  return if !active_span.respond_to?(:set_tag)
20
20
 
21
- active_span.set_tag('cache_hit', cache_hit)
21
+ tags.each do |name, value|
22
+ active_span.set_tag(name, value)
23
+ end
22
24
  end
23
25
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-memo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0.beta.2
4
+ version: 0.0.0.beta.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chan Zuckerberg Initiative
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord-import
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: codecov
57
71
  requirement: !ruby/object:Gem::Requirement