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

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: 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