activesupport_cache_database 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '029071110552b3cd5a259762e7b3a38b57c67cb30e19daca6d33b90c79963b38'
4
+ data.tar.gz: 96686b4c427b8eac4cb7a5c0474e8b2d5b867bf06654379594628620fbf7e27e
5
+ SHA512:
6
+ metadata.gz: 02a282d23cf3b0c48022ffa23585e876d9c8a4fd64da959f9eedc1cbb86248e2463755dfb0de9f7b8f85ffa8b50461354a97809d37246a69b66d9bbe2345b288
7
+ data.tar.gz: 93b25ebd6d14d93cc3cc794fb785a94d61c25b10c56e8df08b33e13543b536ea6c93c9bd05b617eed793a7c6fcc5568195bdee85093ba30158e2760043ae764b
data/.editorconfig ADDED
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .rubocop-*
2
+ pkg/
3
+ spec/*.sqlite*
data/.rubocop.yml ADDED
@@ -0,0 +1,7 @@
1
+ inherit_from:
2
+ - https://gitlab.com/bsm/misc/raw/master/rubocop/default.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: "2.5"
6
+ Security/MarshalLoad:
7
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.6
4
+ - 2.5
5
+ before_install:
6
+ - gem install bundler
7
+ - mysql -e 'CREATE DATABASE ci_test;'
8
+ - psql -c 'CREATE DATABASE ci_test;' -U postgres
9
+ services:
10
+ - postgresql
11
+ - mysql
12
+ env:
13
+ - DATABASE_URL=sqlite3:///tmp/ci_test.sqlite3
14
+ - DATABASE_URL=mysql2://travis@127.0.0.1/ci_test
15
+ - DATABASE_URL=postgresql://127.0.0.1/ci_test
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,78 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ activesupport_cache_database (0.1.0)
5
+ activerecord (>= 5.0)
6
+ activesupport (>= 5.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (6.0.0)
12
+ activesupport (= 6.0.0)
13
+ activerecord (6.0.0)
14
+ activemodel (= 6.0.0)
15
+ activesupport (= 6.0.0)
16
+ activesupport (6.0.0)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 0.7, < 2)
19
+ minitest (~> 5.1)
20
+ tzinfo (~> 1.1)
21
+ zeitwerk (~> 2.1, >= 2.1.8)
22
+ ast (2.4.0)
23
+ concurrent-ruby (1.1.5)
24
+ diff-lcs (1.3)
25
+ i18n (1.6.0)
26
+ concurrent-ruby (~> 1.0)
27
+ jaro_winkler (1.5.3)
28
+ minitest (5.11.3)
29
+ mysql2 (0.5.2)
30
+ parallel (1.17.0)
31
+ parser (2.6.4.0)
32
+ ast (~> 2.4.0)
33
+ pg (1.1.4)
34
+ rainbow (3.0.0)
35
+ rake (12.3.3)
36
+ rspec (3.8.0)
37
+ rspec-core (~> 3.8.0)
38
+ rspec-expectations (~> 3.8.0)
39
+ rspec-mocks (~> 3.8.0)
40
+ rspec-core (3.8.2)
41
+ rspec-support (~> 3.8.0)
42
+ rspec-expectations (3.8.4)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.8.0)
45
+ rspec-mocks (3.8.1)
46
+ diff-lcs (>= 1.2.0, < 2.0)
47
+ rspec-support (~> 3.8.0)
48
+ rspec-support (3.8.2)
49
+ rubocop (0.74.0)
50
+ jaro_winkler (~> 1.5.1)
51
+ parallel (~> 1.10)
52
+ parser (>= 2.6)
53
+ rainbow (>= 2.2.2, < 4.0)
54
+ ruby-progressbar (~> 1.7)
55
+ unicode-display_width (>= 1.4.0, < 1.7)
56
+ ruby-progressbar (1.10.1)
57
+ sqlite3 (1.4.1)
58
+ thread_safe (0.3.6)
59
+ tzinfo (1.2.5)
60
+ thread_safe (~> 0.1)
61
+ unicode-display_width (1.6.0)
62
+ zeitwerk (2.1.10)
63
+
64
+ PLATFORMS
65
+ ruby
66
+
67
+ DEPENDENCIES
68
+ activesupport_cache_database!
69
+ bundler
70
+ mysql2
71
+ pg
72
+ rake
73
+ rspec
74
+ rubocop
75
+ sqlite3
76
+
77
+ BUNDLED WITH
78
+ 2.0.2
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2019 Black Square Media Ltd
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # ActiveSupport::Cache::DatabaseStore
2
+
3
+ [![Build Status](https://travis-ci.org/bsm/activesupport-cache-database.png?branch=master)](https://travis-ci.org/bsm/activesupport-cache-database)
4
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
5
+
6
+ ActiveSupport::Cache::Store implementation backed by a database via ActiveRecord.
7
+
8
+ Tested with:
9
+
10
+ - PostgreSQL
11
+ - SQlite3
12
+ - MySQL/MariaDB
13
+
14
+ ## Usage
15
+
16
+ Include the data migration:
17
+
18
+ ```ruby
19
+ # db/migrate/20190908102030_create_activesupport_cache_entries.rb
20
+ require 'active_support/cache/database_store/migration'
21
+
22
+ class CreateActivesupportCacheEntries < ActiveRecord::Migration[5.2]
23
+ def up
24
+ ActiveSupport::Cache::DatabaseStore::Migration.migrate(:up)
25
+ end
26
+
27
+ def down
28
+ ActiveSupport::Cache::DatabaseStore::Migration.migrate(:down)
29
+ end
30
+ end
31
+ ```
32
+
33
+ Open and use the new cache instance:
34
+
35
+ ```ruby
36
+ cache = ActiveSupport::Cache::DatabaseStore.new namespace: 'my-scope'
37
+ value = cache.fetch('some-key') { 'default' }
38
+ ```
39
+
40
+ To use as a Rails cache store (not recommended!), simply use a new instance.
41
+
42
+ ```ruby
43
+ config.cache_store = ActiveSupport::Cache::DatabaseStore.new
44
+ ```
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'rubocop/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ RuboCop::RakeTask.new(:rubocop)
8
+
9
+ task default: %i[spec rubocop]
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'activesupport_cache_database'
3
+ s.version = '0.1.0'
4
+ s.authors = ['Black Square Media Ltd']
5
+ s.email = ['info@blacksquaremedia.com']
6
+ s.summary = %(ActiveSupport::Cache::Store implementation backed by ActiveRecord.)
7
+ s.description = %(Use your DB as a cache store)
8
+ s.homepage = 'https://github.com/bsm/record-cache-store'
9
+ s.license = 'Apache-2.0'
10
+
11
+ s.files = `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^spec/}) }
12
+ s.test_files = `git ls-files -z -- spec/*`.split("\x0")
13
+ s.require_paths = ['lib']
14
+ s.required_ruby_version = '>= 2.5'
15
+
16
+ s.add_dependency 'activerecord', '>= 5.0'
17
+ s.add_dependency 'activesupport', '>= 5.0'
18
+
19
+ s.add_development_dependency 'bundler'
20
+ s.add_development_dependency 'mysql2'
21
+ s.add_development_dependency 'pg'
22
+ s.add_development_dependency 'rake'
23
+ s.add_development_dependency 'rspec'
24
+ s.add_development_dependency 'rubocop'
25
+ s.add_development_dependency 'sqlite3'
26
+ end
@@ -0,0 +1,111 @@
1
+ require 'active_support/cache'
2
+ require 'active_record'
3
+
4
+ module ActiveSupport
5
+ module Cache
6
+ # A cache store implementation which stores everything in the database, using ActiveRecord as the backend.
7
+ #
8
+ # DatabaseStore implements the Strategy::LocalCache strategy which implements
9
+ # an in-memory cache inside of a block.
10
+ class DatabaseStore < Store
11
+ prepend Strategy::LocalCache
12
+
13
+ autoload :Model, 'active_support/cache/database_store/model'
14
+ autoload :Migration, 'active_support/cache/database_store/migration'
15
+
16
+ # param [Hash] options options
17
+ # option options [Class] :model model class. Default: ActiveSupport::Cache::DatabaseStore::Model
18
+ def initialize(options=nil)
19
+ @model = (options || {}).delete(:model) || Model
20
+ super(options)
21
+ end
22
+
23
+ # Preemptively iterates through all stored keys and removes the ones which have expired.
24
+ def cleanup(options=nil)
25
+ options = merged_options(options)
26
+ scope = @model.expired
27
+ if (namespace = options[:namespace])
28
+ scope = scope.namespaced(namespace)
29
+ end
30
+ scope.delete_all
31
+ end
32
+
33
+ # Clears the entire cache. Be careful with this method.
34
+ def clear(options=nil)
35
+ options = merged_options(options)
36
+ if (namespace = options[:namespace])
37
+ @model.namespaced(namespace).delete_all
38
+ else
39
+ @model.truncate!
40
+ end
41
+ true
42
+ end
43
+
44
+ # Calculates the number of entries in the cache.
45
+ def count(options=nil)
46
+ options = merged_options(options)
47
+ scope = @model.all
48
+ if (namespace = options[:namespace])
49
+ scope = scope.namespaced(namespace)
50
+ end
51
+ scope = scope.fresh unless options[:all]
52
+ scope.count
53
+ end
54
+
55
+ private
56
+
57
+ def normalize_key(name, options=nil)
58
+ key = super.to_s
59
+ raise ArgumentError, 'Namespaced key exceeds the length limit' if key && key.bytesize > 255
60
+
61
+ key
62
+ end
63
+
64
+ def read_entry(key, _options=nil)
65
+ from_record @model.where(key: key).first
66
+ end
67
+
68
+ def write_entry(key, entry, _options=nil)
69
+ record = @model.where(key: key).first_or_initialize
70
+ expires_at = Time.zone.at(entry.expires_at) if entry.expires_at
71
+ record.update! value: Marshal.dump(entry.value), version: entry.version.presence, expires_at: expires_at
72
+ end
73
+
74
+ def delete_entry(key, _options=nil)
75
+ @model.where(key: key).destroy_all
76
+ end
77
+
78
+ def read_multi_entries(names, options)
79
+ keyed = {}
80
+ names.each do |name|
81
+ version = normalize_version(name, options)
82
+ keyed[normalize_key(name, options)] = { name: name, version: version }
83
+ end
84
+
85
+ results = {}
86
+ @model.where(key: keyed.keys).find_each do |rec|
87
+ name, version = keyed[rec.key].values_at(:name, :version)
88
+ entry = from_record(rec)
89
+ next if entry.nil?
90
+
91
+ if entry.expired?
92
+ delete_entry(rec.key, options)
93
+ elsif entry.mismatched?(version)
94
+ # Skip mismatched versions
95
+ else
96
+ results[name] = entry.value
97
+ end
98
+ end
99
+ results
100
+ end
101
+
102
+ def from_record(record)
103
+ return unless record
104
+
105
+ entry = Entry.new Marshal.load(record.value), version: record.version
106
+ entry.expires_at = record.expires_at
107
+ entry
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_support/cache/database_store'
2
+
3
+ module ActiveSupport
4
+ module Cache
5
+ class DatabaseStore < Store
6
+ class Migration < ::ActiveRecord::Migration[5.2]
7
+ def change
8
+ create_table :activesupport_cache_entries, primary_key: 'key', id: :binary, limit: 255 do |t|
9
+ t.binary :value, null: false
10
+ t.string :version, index: true
11
+ t.timestamp :created_at, null: false, index: true
12
+ t.timestamp :expires_at, index: true
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_support/cache/database_store'
2
+
3
+ module ActiveSupport
4
+ module Cache
5
+ class DatabaseStore < Store
6
+ class Model < ActiveRecord::Base
7
+ self.table_name = 'activesupport_cache_entries'
8
+
9
+ def self.truncate!
10
+ connection.truncate(table_name)
11
+ end
12
+
13
+ scope :fresh, -> { where(arel_table[:expires_at].gt(Time.zone.now)) }
14
+ scope :expired, -> { where(arel_table[:expires_at].lteq(Time.zone.now)) }
15
+
16
+ def self.namespaced(namespace)
17
+ prefix = "#{namespace}:"
18
+ clause = ::Arel::Nodes::NamedFunction.new('SUBSTR', [arel_table[:key], 1, prefix.bytesize])
19
+ where clause.eq(prefix)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1 @@
1
+ require 'active_support/cache/database_store'
@@ -0,0 +1,328 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ActiveSupport::Cache::DatabaseStore do
4
+ subject do
5
+ described_class.new expires_in: 60
6
+ end
7
+
8
+ it 'should read and write strings' do
9
+ expect(subject.write('foo', 'bar')).to be_truthy
10
+ expect(subject.read('foo')).to eq('bar')
11
+ end
12
+
13
+ it 'should read and write hash' do
14
+ expect(subject.write('foo', a: 'b')).to be_truthy
15
+ expect(subject.read('foo')).to eq(a: 'b')
16
+ end
17
+
18
+ it 'should read and write integer' do
19
+ expect(subject.write('foo', 1)).to be_truthy
20
+ expect(subject.read('foo')).to eq(1)
21
+ end
22
+
23
+ it 'should read and write nil' do
24
+ expect(subject.write('foo', nil)).to be_truthy
25
+ expect(subject.read('foo')).to eq(nil)
26
+ end
27
+
28
+ it 'should read and write false' do
29
+ expect(subject.write('foo', false)).to be_truthy
30
+ expect(subject.read('foo')).to eq(false)
31
+ end
32
+
33
+ it 'should overwrite' do
34
+ expect(subject.write('foo', 'bar')).to be_truthy
35
+ expect(subject.write('foo', 'baz')).to be_truthy
36
+ expect(subject.read('foo')).to eq('baz')
37
+ end
38
+
39
+ it 'should support exist?' do
40
+ subject.write('foo', 'bar')
41
+ expect(subject.exist?('foo')).to be_truthy
42
+ expect(subject.exist?('bar')).to be_falsey
43
+ end
44
+
45
+ it 'should support nil exist?' do
46
+ subject.write('foo', nil)
47
+ expect(subject.exist?('foo')).to be_truthy
48
+ end
49
+
50
+ it 'should support delete' do
51
+ subject.write('foo', 'bar')
52
+ expect(subject.exist?('foo')).to be_truthy
53
+ expect(subject.delete('foo')).to be_truthy
54
+ expect(subject.exist?('foo')).to be_falsey
55
+ end
56
+
57
+ it 'should support expires_in' do
58
+ time = Time.local(2008, 4, 24)
59
+ allow(Time).to receive(:now).and_return(time)
60
+
61
+ subject.write('foo', 'bar')
62
+ expect(subject.read('foo')).to eq('bar')
63
+
64
+ allow(Time).to receive(:now).and_return(time + 30)
65
+ expect(subject.read('foo')).to eq('bar')
66
+
67
+ allow(Time).to receive(:now).and_return(time + 61)
68
+ expect(subject.read('foo')).to be_nil
69
+ end
70
+
71
+ it 'should support long keys' do
72
+ key = 'x' * 255
73
+ expect(subject.write(key, 'bar')).to be_truthy
74
+ expect(subject.read(key)).to eq('bar')
75
+ expect(subject.fetch(key)).to eq('bar')
76
+ expect(subject.read(key[0..-2])).to be_nil
77
+ expect(subject.read_multi(key)).to eq(key => 'bar')
78
+ expect(subject.delete(key)).to be_truthy
79
+
80
+ expect { subject.write("#{key}x", 'bar') }.to raise_error(ArgumentError, /exceeds the length limit/)
81
+ expect { subject.read("#{key}x") }.to raise_error(ArgumentError, /exceeds the length limit/)
82
+ end
83
+
84
+ describe '#cleanup' do
85
+ it 'should delete expired' do
86
+ time = Time.now
87
+ subject.write('foo', 'bar', expires_in: 10)
88
+ subject.write('fud', 'biz', expires_in: 20)
89
+
90
+ allow(Time).to receive(:now).and_return(time + 9)
91
+ expect(subject.cleanup).to eq(0)
92
+
93
+ allow(Time).to receive(:now).and_return(time + 19)
94
+ expect(subject.cleanup).to eq(1)
95
+ expect(subject.read('foo')).to be_nil
96
+ expect(subject.read('fud')).to eq('biz')
97
+ end
98
+
99
+ it 'should support namespace' do
100
+ time = Time.now
101
+ subject.write('foo', 'bar', expires_in: 10, namespace: 'x')
102
+ subject.write('foo', 'biz', expires_in: 10, namespace: 'y')
103
+
104
+ allow(Time).to receive(:now).and_return(time + 11)
105
+ expect(subject.count).to eq(0)
106
+ expect(subject.count(all: true)).to eq(2)
107
+
108
+ expect(subject.cleanup(namespace: 'x')).to eq(1)
109
+ expect(subject.count).to eq(0)
110
+ expect(subject.count(all: true)).to eq(1)
111
+ end
112
+ end
113
+
114
+ describe '#clear' do
115
+ it 'should remove all entries' do
116
+ subject.write('foo', 'bar')
117
+ subject.write('fud', 'biz')
118
+ expect(subject.clear).to be_truthy
119
+ expect(subject.read('foo')).to be_nil
120
+ expect(subject.read('fud')).to be_nil
121
+ end
122
+
123
+ it 'should support namespace' do
124
+ subject.write('foo', 'bar', namespace: 'x')
125
+ subject.write('foo', 'biz', namespace: 'y')
126
+ expect(subject.count).to eq(2)
127
+
128
+ expect(subject.clear(namespace: 'x')).to be_truthy
129
+ expect(subject.count).to eq(1)
130
+ end
131
+ end
132
+
133
+ describe '#fetch' do
134
+ it 'should support cache hit' do
135
+ subject.write('foo', 'bar')
136
+ expect(subject).not_to receive(:write)
137
+
138
+ expect(subject.fetch('foo') { 'baz' }).to eq('bar')
139
+ end
140
+
141
+ it 'should support cache miss' do
142
+ expect(subject).to receive(:write).with('foo', 'baz', instance_of(Hash))
143
+ expect(subject.fetch('foo') { 'baz' }).to eq('baz')
144
+ end
145
+
146
+ it 'should pass key to block on cache miss' do
147
+ cache_miss = false
148
+ expect(subject.fetch('foo') {|key| cache_miss = true; key.length }).to eq(3)
149
+ expect(cache_miss).to be_truthy
150
+
151
+ cache_miss = false
152
+ expect(subject.fetch('foo') {|key| cache_miss = true; key.length }).to eq(3)
153
+ expect(cache_miss).to be_falsey
154
+ end
155
+
156
+ it 'should support forced cache miss' do
157
+ subject.write('foo', 'bar')
158
+ expect(subject).not_to receive(:read)
159
+
160
+ expect(subject.fetch('foo', force: true) { 'baz' }).to eq('baz')
161
+ end
162
+
163
+ it 'should support nil values' do
164
+ subject.write('foo', nil)
165
+ expect(subject).not_to receive(:write)
166
+
167
+ expect(subject.fetch('foo') { 'baz' }).to be_nil
168
+ end
169
+
170
+ it 'should support skip_nil option' do
171
+ expect(subject).not_to receive(:write)
172
+ expect(subject.fetch('foo', skip_nil: true) { nil }).to be_nil
173
+ expect(subject.exist?('foo')).to be_falsey
174
+ end
175
+
176
+ it 'should support forced cache miss with block' do
177
+ subject.write('foo', 'bar')
178
+ expect(subject.fetch('foo', force: true) { 'baz' }).to eq('baz')
179
+ end
180
+
181
+ it 'should support forced cache miss without block' do
182
+ subject.write('foo', 'bar')
183
+ expect { subject.fetch('foo', force: true) }.to raise_error(ArgumentError)
184
+ expect(subject.read('foo')).to eq('bar')
185
+ end
186
+ end
187
+
188
+ describe '#read_multi' do
189
+ it 'should support read_multi' do
190
+ subject.write('foo', 'bar')
191
+ subject.write('fu', 'baz')
192
+ subject.write('fud', 'biz')
193
+ expect(subject.read_multi('foo', 'fu')).to eq('foo' => 'bar', 'fu' => 'baz')
194
+ end
195
+
196
+ it 'should support expires' do
197
+ time = Time.now
198
+ subject.write('foo', 'bar', expires_in: 10)
199
+ subject.write('fu', 'baz')
200
+ subject.write('fud', 'biz')
201
+
202
+ allow(Time).to receive(:now).and_return(time + 11)
203
+ expect(subject.read_multi('foo', 'fu')).to eq('fu' => 'baz')
204
+ end
205
+ end
206
+
207
+ describe '#fetch_multi' do
208
+ it 'should support fetch_multi' do
209
+ subject.write('foo', 'bar')
210
+ subject.write('fud', 'biz')
211
+ values = subject.fetch_multi('foo', 'fu', 'fud') {|v| v * 2 }
212
+
213
+ expect(values).to eq('foo' => 'bar', 'fu' => 'fufu', 'fud' => 'biz')
214
+ expect(subject.read('fu')).to eq('fufu')
215
+ end
216
+
217
+ it 'should support without expires_in' do
218
+ subject.write('foo', 'bar')
219
+ subject.write('fud', 'biz')
220
+ values = subject.fetch_multi('foo', 'fu', 'fud', expires_in: nil) {|v| v * 2 }
221
+
222
+ expect(values).to eq('foo' => 'bar', 'fu' => 'fufu', 'fud' => 'biz')
223
+ expect(subject.read('fu')).to eq('fufu')
224
+ end
225
+
226
+ it 'should support with objects' do
227
+ cache_struct = Struct.new(:cache_key, :title)
228
+ foo = cache_struct.new('foo', 'FOO!')
229
+ bar = cache_struct.new('bar')
230
+
231
+ subject.write('bar', 'BAM!')
232
+ values = subject.fetch_multi(foo, bar, &:title)
233
+ expect(values).to eq(foo => 'FOO!', bar => 'BAM!')
234
+ end
235
+
236
+ it 'should support ordered names' do
237
+ subject.write('bam', 'BAM')
238
+ values = subject.fetch_multi('foo', 'bar', 'bam', &:upcase)
239
+ expect(values.keys).to eq(%w[foo bar bam])
240
+ end
241
+
242
+ it 'should raise without block' do
243
+ expect { subject.fetch_multi('foo') }.to raise_error(ArgumentError)
244
+ end
245
+ end
246
+
247
+ describe 'cache key' do
248
+ it 'should support cache keys' do
249
+ obj = Object.new
250
+ def obj.cache_key
251
+ :foo
252
+ end
253
+ subject.write(obj, 'bar')
254
+ expect(subject.read('foo')).to eq('bar')
255
+ end
256
+
257
+ it 'should support to_param keys' do
258
+ obj = Object.new
259
+ def obj.to_param
260
+ :foo
261
+ end
262
+ subject.write(obj, 'bar')
263
+ expect(subject.read('foo')).to eq('bar')
264
+ end
265
+
266
+ it 'should support unversioned keys' do
267
+ obj = Object.new
268
+ def obj.cache_key
269
+ :foo
270
+ end
271
+
272
+ def obj.cache_key_with_version
273
+ 'foo-v1'
274
+ end
275
+ subject.write(obj, 'bar')
276
+ expect(subject.read('foo')).to eq('bar')
277
+ end
278
+
279
+ it 'should support array keys' do
280
+ subject.write([:fu, 'foo'], 'bar')
281
+ expect(subject.read('fu/foo')).to eq('bar')
282
+ end
283
+
284
+ it 'should support hash keys' do
285
+ subject.write({ foo: 1, fu: 2 }, 'bar')
286
+ expect(subject.read('foo=1/fu=2')).to eq('bar')
287
+ end
288
+
289
+ it 'should be case sensitive' do
290
+ subject.write('foo', 'bar')
291
+ expect(subject.read('FOO')).to be_nil
292
+ end
293
+ end
294
+
295
+ describe 'with version' do
296
+ it 'should support fetch/read' do
297
+ subject.fetch('foo', version: 1) { 'bar' }
298
+ expect(subject.read('foo', version: 1)).to eq('bar')
299
+ expect(subject.read('foo', version: 2)).to be_nil
300
+ end
301
+
302
+ it 'should support write/read' do
303
+ subject.write('foo', 'bar', version: 1)
304
+ expect(subject.read('foo', version: 1)).to eq('bar')
305
+ expect(subject.read('foo', version: 2)).to be_nil
306
+ end
307
+
308
+ it 'should support exists' do
309
+ subject.write('foo', 'bar', version: 1)
310
+ expect(subject.exist?('foo', version: 1)).to be_truthy
311
+ expect(subject.exist?('foo', version: 2)).to be_falsey
312
+ end
313
+
314
+ it 'should cache/version keys' do
315
+ m1v1 = ModelWithKeyAndVersion.new('model/1', 1)
316
+ m1v2 = ModelWithKeyAndVersion.new('model/1', 2)
317
+
318
+ subject.write(m1v1, 'bar')
319
+ expect(subject.read(m1v1)).to eq('bar')
320
+ expect(subject.read(m1v2)).to be_nil
321
+ end
322
+
323
+ it 'should normalise' do
324
+ subject.write('foo', 'bar', version: 1)
325
+ expect(subject.read('foo', version: '1')).to eq('bar')
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,30 @@
1
+ ENV['RACK_ENV'] ||= 'test'
2
+
3
+ require 'rspec'
4
+ require 'fileutils'
5
+ require 'activesupport_cache_database'
6
+
7
+ Time.zone_default = Time.find_zone!('UTC')
8
+
9
+ database_url = ENV.fetch('DATABASE_URL') do
10
+ path = File.expand_path('./test.sqlite3', __dir__)
11
+ FileUtils.rm_f(path)
12
+ "sqlite3://#{path}"
13
+ end
14
+ ActiveRecord::Base.configurations = { 'test' => { 'url' => database_url, 'pool' => 20 } }
15
+
16
+ ActiveRecord::Base.establish_connection :test
17
+ ActiveRecord::Base.connection.instance_eval do
18
+ drop_table 'activesupport_cache_entries', if_exists: true
19
+ end
20
+ ActiveRecord::Migration.suppress_messages do
21
+ ActiveSupport::Cache::DatabaseStore::Migration.migrate(:up)
22
+ end
23
+
24
+ RSpec.configure do |c|
25
+ c.after :each do
26
+ ActiveSupport::Cache::DatabaseStore::Model.truncate!
27
+ end
28
+ end
29
+
30
+ ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version)
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activesupport_cache_database
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Black Square Media Ltd
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mysql2
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'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pg
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: sqlite3
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Use your DB as a cache store
140
+ email:
141
+ - info@blacksquaremedia.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".editorconfig"
147
+ - ".gitignore"
148
+ - ".rubocop.yml"
149
+ - ".travis.yml"
150
+ - Gemfile
151
+ - Gemfile.lock
152
+ - LICENSE
153
+ - README.md
154
+ - Rakefile
155
+ - activesupport_cache_database.gemspec
156
+ - lib/active_support/cache/database_store.rb
157
+ - lib/active_support/cache/database_store/migration.rb
158
+ - lib/active_support/cache/database_store/model.rb
159
+ - lib/activesupport_cache_database.rb
160
+ - spec/active_support/cache/database_store_spec.rb
161
+ - spec/spec_helper.rb
162
+ homepage: https://github.com/bsm/record-cache-store
163
+ licenses:
164
+ - Apache-2.0
165
+ metadata: {}
166
+ post_install_message:
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '2.5'
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubygems_version: 3.0.3
182
+ signing_key:
183
+ specification_version: 4
184
+ summary: ActiveSupport::Cache::Store implementation backed by ActiveRecord.
185
+ test_files:
186
+ - spec/active_support/cache/database_store_spec.rb
187
+ - spec/spec_helper.rb