activesupport_cache_database 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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