frozen_record 0.23.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +4 -12
- data/CHANGELOG.md +26 -0
- data/README.md +10 -3
- data/lib/frozen_record/backends/empty.yml +1 -0
- data/lib/frozen_record/backends/yaml.rb +1 -1
- data/lib/frozen_record/base.rb +24 -1
- data/lib/frozen_record/scope.rb +23 -4
- data/lib/frozen_record/test_helper.rb +19 -9
- data/lib/frozen_record/version.rb +1 -1
- data/lib/frozen_record.rb +1 -1
- data/spec/fixtures/continents.yml.erb +9 -0
- data/spec/fixtures/prices.yml.erb +13 -0
- data/spec/fixtures/test_helper/continents.yml.erb +3 -0
- data/spec/scope_spec.rb +26 -0
- data/spec/support/continent.rb +2 -0
- data/spec/support/price.rb +13 -0
- data/spec/test_helper_spec.rb +16 -0
- metadata +15 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c4f3cb4248d83050752933b1a2c48d0bf0914a73e695932650a03cae9feffa0
|
4
|
+
data.tar.gz: fb8517287957255269ffec7ab7bac5740c6d7e45b4f1c3c0b321ce4fc5badf4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d66248c13f74ccaa779db89359467e54c1ee18979683fcd42e62485ac5657345115985329f07be02f0ffe6f347334646e26bb920df1b1efad2d3ef55ab24d74
|
7
|
+
data.tar.gz: f395e256aca6a133fe684f3e28a94cbdca4f70ea6a2c565a63f30bb4f52144f62529a6b20c24d806e97a4ba0bb11a5ca0e2402d521c700f13bc5b1bb49331578
|
data/.github/workflows/main.yml
CHANGED
@@ -6,26 +6,18 @@ jobs:
|
|
6
6
|
build:
|
7
7
|
runs-on: ubuntu-latest
|
8
8
|
strategy:
|
9
|
+
fail-fast: false
|
9
10
|
matrix:
|
10
|
-
ruby: [ '2.5', '2.6', '2.7', '3.0' ]
|
11
|
+
ruby: [ '2.5', '2.6', '2.7', '3.0' , '3.1']
|
11
12
|
minimal: [ false, true ]
|
12
13
|
name: Ruby ${{ matrix.ruby }} tests, minimal=${{ matrix.minimal }}
|
13
14
|
steps:
|
14
15
|
- uses: actions/checkout@v2
|
15
|
-
- name:
|
16
|
+
- name: Set up Ruby
|
16
17
|
uses: ruby/setup-ruby@v1
|
17
18
|
with:
|
18
19
|
ruby-version: ${{ matrix.ruby }}
|
19
|
-
|
20
|
-
with:
|
21
|
-
path: vendor/bundle
|
22
|
-
key: ${{ runner.os }}-${{ matrix.ruby }}-gems-${{ hashFiles('Gemfile', 'frozen_record.gemspec') }}
|
23
|
-
restore-keys: |
|
24
|
-
${{ runner.os }}-${{ matrix.ruby }}-gems-
|
25
|
-
- name: Bundle install
|
26
|
-
run: |
|
27
|
-
gem install bundler
|
28
|
-
bundle install --jobs 4 --retry 3 --path=vendor/bundle
|
20
|
+
bundler-cache: true
|
29
21
|
- name: Run tests
|
30
22
|
env:
|
31
23
|
MINIMAL: ${{ matrix.minimal }}
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Unreleased
|
2
|
+
|
3
|
+
# v0.25.1
|
4
|
+
|
5
|
+
- More reliable way to disable max_records_scan checks when loading records.
|
6
|
+
|
7
|
+
# v0.25.0
|
8
|
+
|
9
|
+
- Disable max_records_scan checks when loading records.
|
10
|
+
- Add `FrozenRecord::Base.with_max_record_scan` for more easily allowing larger amount in specific tests.
|
11
|
+
|
12
|
+
# v0.24.1
|
13
|
+
|
14
|
+
- Fix index selection not applying some restrictions.
|
15
|
+
|
16
|
+
# v0.24.0 (yanked)
|
17
|
+
|
18
|
+
- Improve index selection and combinaison. Should significantly help with performance in some cases.
|
19
|
+
- Implement `max_records_scan` to reject slow queries.
|
20
|
+
- Only load `Railtie` integration if `Rails::Railtie` is defined
|
21
|
+
- Allow granular fixture unloading
|
22
|
+
- Fix a bug affecting older bootsnap versions
|
23
|
+
|
24
|
+
# v0.23.0
|
25
|
+
|
26
|
+
NO DATA
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[](http://travis-ci.org/byroot/frozen_record)
|
4
4
|
[](http://badge.fury.io/rb/frozen_record)
|
5
5
|
|
6
|
-
|
6
|
+
Activec Record-like interface for **read only** access to static data files of reasonable size.
|
7
7
|
|
8
8
|
## Installation
|
9
9
|
|
@@ -21,7 +21,7 @@ Or install it yourself as:
|
|
21
21
|
|
22
22
|
## Models definition
|
23
23
|
|
24
|
-
Just like with
|
24
|
+
Just like with Active Record, your models need to inherits from `FrozenRecord::Base`:
|
25
25
|
|
26
26
|
```ruby
|
27
27
|
class Country < FrozenRecord::Base
|
@@ -72,7 +72,7 @@ end
|
|
72
72
|
|
73
73
|
## Query interface
|
74
74
|
|
75
|
-
FrozenRecord aim to replicate only modern
|
75
|
+
FrozenRecord aim to replicate only modern Active Record querying interface, and only the non "string typed" ones.
|
76
76
|
|
77
77
|
e.g
|
78
78
|
```ruby
|
@@ -154,6 +154,13 @@ Composite index keys are not supported.
|
|
154
154
|
|
155
155
|
The primary key isn't indexed by default.
|
156
156
|
|
157
|
+
## Limitations
|
158
|
+
|
159
|
+
Frozen Record is not meant to operate or large unindexed datasets.
|
160
|
+
|
161
|
+
To ensure that it doesn't happen by accident, you can set `FrozenRecord::Base.max_records_scan = 500` (or whatever limit makes sense to you), in your development and test environments.
|
162
|
+
This setting will cause Frozen Record to raise an error if it has to scan more than `max_records_scan` records. This property can also be set on a per model basis.
|
163
|
+
|
157
164
|
## Configuration
|
158
165
|
|
159
166
|
### Reloading
|
@@ -0,0 +1 @@
|
|
1
|
+
true
|
data/lib/frozen_record/base.rb
CHANGED
@@ -4,6 +4,20 @@ require 'active_support/descendants_tracker'
|
|
4
4
|
require 'frozen_record/backends'
|
5
5
|
|
6
6
|
module FrozenRecord
|
7
|
+
SlowQuery = Class.new(StandardError)
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :enforce_max_records_scan
|
11
|
+
|
12
|
+
def ignore_max_records_scan
|
13
|
+
previous = enforce_max_records_scan
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
self.enforce_max_records_scan = previous
|
17
|
+
end
|
18
|
+
end
|
19
|
+
@enforce_max_records_scan = true
|
20
|
+
|
7
21
|
class Base
|
8
22
|
extend ActiveSupport::DescendantsTracker
|
9
23
|
extend ActiveModel::Naming
|
@@ -15,6 +29,7 @@ module FrozenRecord
|
|
15
29
|
|
16
30
|
class_attribute :base_path, :primary_key, :backend, :auto_reloading, :default_attributes, instance_accessor: false
|
17
31
|
class_attribute :index_definitions, instance_accessor: false
|
32
|
+
class_attribute :max_records_scan, instance_accessor: false
|
18
33
|
self.index_definitions = {}.freeze
|
19
34
|
|
20
35
|
self.primary_key = 'id'
|
@@ -42,6 +57,14 @@ module FrozenRecord
|
|
42
57
|
end
|
43
58
|
|
44
59
|
class << self
|
60
|
+
def with_max_records_scan(value)
|
61
|
+
previous_max_records_scan = max_records_scan
|
62
|
+
self.max_records_scan = value
|
63
|
+
yield
|
64
|
+
ensure
|
65
|
+
self.max_records_scan = previous_max_records_scan
|
66
|
+
end
|
67
|
+
|
45
68
|
alias_method :set_default_attributes, :default_attributes=
|
46
69
|
private :set_default_attributes
|
47
70
|
def default_attributes=(default_attributes)
|
@@ -145,7 +168,7 @@ module FrozenRecord
|
|
145
168
|
end
|
146
169
|
@attributes = list_attributes(records).freeze
|
147
170
|
define_attribute_methods(@attributes.to_a)
|
148
|
-
records = records.map { |r| load(r) }.freeze
|
171
|
+
records = FrozenRecord.ignore_max_records_scan { records.map { |r| load(r) }.freeze }
|
149
172
|
index_definitions.values.each { |index| index.build(records) }
|
150
173
|
records
|
151
174
|
end
|
data/lib/frozen_record/scope.rb
CHANGED
@@ -173,6 +173,8 @@ module FrozenRecord
|
|
173
173
|
sort_records(select_records(@klass.load_records))
|
174
174
|
end
|
175
175
|
|
176
|
+
ARRAY_INTERSECTION = Array.method_defined?(:intersection)
|
177
|
+
|
176
178
|
def select_records(records)
|
177
179
|
return records if @where_values.empty? && @where_not_values.empty?
|
178
180
|
|
@@ -180,13 +182,30 @@ module FrozenRecord
|
|
180
182
|
indexed_where_values, unindexed_where_values = @where_values.partition { |criteria| indices.key?(criteria.first) }
|
181
183
|
|
182
184
|
unless indexed_where_values.empty?
|
183
|
-
attribute, value
|
184
|
-
|
185
|
-
|
186
|
-
|
185
|
+
usable_indexes = indexed_where_values.map { |(attribute, value)| [attribute, value, indices[attribute].query(value)] }
|
186
|
+
usable_indexes.sort_by! { |r| r[2].size }
|
187
|
+
records = usable_indexes.shift.last
|
188
|
+
|
189
|
+
# If the index is 5 times bigger that the current set of records it's not worth doing an array intersection.
|
190
|
+
# The value is somewhat arbitrary and could be adjusted.
|
191
|
+
useless_indexes, usable_indexes = usable_indexes.partition { |_, _, indexed_records| indexed_records.size > records.size * 5 }
|
192
|
+
unindexed_where_values += useless_indexes.map { |a| a.first(2) }
|
193
|
+
|
194
|
+
unless usable_indexes.empty?
|
195
|
+
if ARRAY_INTERSECTION
|
196
|
+
records = records.intersection(*usable_indexes.map(&:last))
|
197
|
+
else
|
198
|
+
usable_indexes.each do |_, _, indexed_records|
|
199
|
+
records &= indexed_records
|
200
|
+
end
|
201
|
+
end
|
187
202
|
end
|
188
203
|
end
|
189
204
|
|
205
|
+
if FrozenRecord.enforce_max_records_scan && @klass.max_records_scan && records.size > @klass.max_records_scan
|
206
|
+
raise SlowQuery, "Scanning #{records.size} records is too slow, the allowed maximum is #{@klass.max_records_scan}. Try to find a better index or consider an alternative storage"
|
207
|
+
end
|
208
|
+
|
190
209
|
records.select do |record|
|
191
210
|
unindexed_where_values.all? { |attr, matcher| matcher.match?(record[attr]) } &&
|
192
211
|
!@where_not_values.any? { |attr, matcher| matcher.match?(record[attr]) }
|
@@ -8,9 +8,7 @@ module FrozenRecord
|
|
8
8
|
def load_fixture(model_class, alternate_base_path)
|
9
9
|
@cache ||= {}
|
10
10
|
|
11
|
-
|
12
|
-
raise ArgumentError, "Model class (#{model_class}) does not inherit from #{FrozenRecord::Base}"
|
13
|
-
end
|
11
|
+
ensure_model_class_is_frozenrecord(model_class)
|
14
12
|
|
15
13
|
return if @cache.key?(model_class)
|
16
14
|
|
@@ -20,20 +18,32 @@ module FrozenRecord
|
|
20
18
|
model_class.load_records(force: true)
|
21
19
|
end
|
22
20
|
|
23
|
-
def
|
21
|
+
def unload_fixture(model_class)
|
24
22
|
return unless defined?(@cache) && @cache
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
ensure_model_class_is_frozenrecord(model_class)
|
25
|
+
|
26
|
+
return unless @cache.key?(model_class)
|
27
|
+
|
28
|
+
old_base_path = @cache[model_class]
|
29
|
+
model_class.base_path = old_base_path
|
30
|
+
model_class.load_records(force: true)
|
30
31
|
|
31
|
-
@cache
|
32
|
+
@cache.delete(model_class)
|
33
|
+
end
|
34
|
+
|
35
|
+
def unload_fixtures
|
36
|
+
return unless defined?(@cache) && @cache
|
37
|
+
|
38
|
+
@cache.keys.each { |model_class| unload_fixture(model_class) }
|
32
39
|
end
|
33
40
|
|
34
41
|
private
|
35
42
|
|
36
43
|
def ensure_model_class_is_frozenrecord(model_class)
|
44
|
+
unless model_class < FrozenRecord::Base
|
45
|
+
raise ArgumentError, "Model class (#{model_class}) does not inherit from #{FrozenRecord::Base}"
|
46
|
+
end
|
37
47
|
end
|
38
48
|
end
|
39
49
|
end
|
data/lib/frozen_record.rb
CHANGED
@@ -0,0 +1,13 @@
|
|
1
|
+
<% 100.times.map { |i| "plan_#{i}"}.each do |plan_name| %>
|
2
|
+
<% ["base", "online", "retail"].each do |type| %>
|
3
|
+
<% [nil, "USD", "EUR", "GBP", "JPY"].each do |currency| %>
|
4
|
+
<% ["monthly", "annual", "biennial", "triennial"].each do |period| %>
|
5
|
+
- plan_name: <%= plan_name %>
|
6
|
+
type: <%= type %>
|
7
|
+
period: <%= period %>
|
8
|
+
currency: <%= currency %>
|
9
|
+
amount: 10.0
|
10
|
+
<% end %>
|
11
|
+
<% end %>
|
12
|
+
<% end %>
|
13
|
+
<% end %>
|
data/spec/scope_spec.rb
CHANGED
@@ -474,4 +474,30 @@ describe 'querying' do
|
|
474
474
|
|
475
475
|
end
|
476
476
|
|
477
|
+
context 'when max_records_scan is set' do
|
478
|
+
|
479
|
+
it 'raises on slow queries' do
|
480
|
+
expect {
|
481
|
+
FrozenRecord::Base.with_max_records_scan(1) do
|
482
|
+
Country.where(king: "Louis").to_a
|
483
|
+
end
|
484
|
+
}.to raise_error(FrozenRecord::SlowQuery)
|
485
|
+
end
|
486
|
+
|
487
|
+
it 'is accurate' do
|
488
|
+
FrozenRecord::Base.with_max_records_scan(60) do
|
489
|
+
expect(Price.count).to be == 6_000
|
490
|
+
|
491
|
+
Price.where(plan_name: "plan_24", currency: [nil, "EUR"], period: "monthly", type: "base").each do |price|
|
492
|
+
expect(price.plan_name).to be == "plan_24"
|
493
|
+
unless price.currency.nil?
|
494
|
+
expect(price.currency).to be == "EUR"
|
495
|
+
end
|
496
|
+
expect(price.period).to be == "monthly"
|
497
|
+
expect(price.type).to be == "base"
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
end
|
477
503
|
end
|
data/spec/test_helper_spec.rb
CHANGED
@@ -38,19 +38,35 @@ describe 'test fixture loading' do
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
describe '.unload_fixture' do
|
42
|
+
it 'restores the default fixtures for the specified model class' do
|
43
|
+
test_fixtures_base_path = File.join(File.dirname(__FILE__), 'fixtures', 'test_helper')
|
44
|
+
|
45
|
+
FrozenRecord::TestHelper.load_fixture(Continent, test_fixtures_base_path)
|
46
|
+
FrozenRecord::TestHelper.load_fixture(Country, test_fixtures_base_path)
|
47
|
+
FrozenRecord::TestHelper.unload_fixture(Country)
|
48
|
+
|
49
|
+
expect(Continent.count).to be == 1
|
50
|
+
expect(Country.count).to be == 3
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
41
54
|
describe '.unload_fixtures' do
|
42
55
|
it 'restores the default fixtures' do
|
43
56
|
test_fixtures_base_path = File.join(File.dirname(__FILE__), 'fixtures', 'test_helper')
|
44
57
|
|
58
|
+
FrozenRecord::TestHelper.load_fixture(Continent, test_fixtures_base_path)
|
45
59
|
FrozenRecord::TestHelper.load_fixture(Country, test_fixtures_base_path)
|
46
60
|
FrozenRecord::TestHelper.unload_fixtures
|
47
61
|
|
62
|
+
expect(Continent.count).to be == 3
|
48
63
|
expect(Country.count).to be == 3
|
49
64
|
end
|
50
65
|
|
51
66
|
it 'does has no effect if no alternate fixtures were loaded' do
|
52
67
|
FrozenRecord::TestHelper.unload_fixtures
|
53
68
|
|
69
|
+
expect(Continent.count).to be == 3
|
54
70
|
expect(Country.count).to be == 3
|
55
71
|
end
|
56
72
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: frozen_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.25.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-06-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -76,6 +76,7 @@ files:
|
|
76
76
|
- ".github/workflows/main.yml"
|
77
77
|
- ".gitignore"
|
78
78
|
- ".rspec"
|
79
|
+
- CHANGELOG.md
|
79
80
|
- Gemfile
|
80
81
|
- LICENSE.txt
|
81
82
|
- README.md
|
@@ -88,6 +89,7 @@ files:
|
|
88
89
|
- lib/frozen_record.rb
|
89
90
|
- lib/frozen_record/backends.rb
|
90
91
|
- lib/frozen_record/backends/empty.json
|
92
|
+
- lib/frozen_record/backends/empty.yml
|
91
93
|
- lib/frozen_record/backends/json.rb
|
92
94
|
- lib/frozen_record/backends/yaml.rb
|
93
95
|
- lib/frozen_record/base.rb
|
@@ -101,7 +103,10 @@ files:
|
|
101
103
|
- lib/frozen_record/version.rb
|
102
104
|
- spec/fixtures/animals.json
|
103
105
|
- spec/fixtures/cars.yml
|
106
|
+
- spec/fixtures/continents.yml.erb
|
104
107
|
- spec/fixtures/countries.yml.erb
|
108
|
+
- spec/fixtures/prices.yml.erb
|
109
|
+
- spec/fixtures/test_helper/continents.yml.erb
|
105
110
|
- spec/fixtures/test_helper/countries.yml.erb
|
106
111
|
- spec/frozen_record_spec.rb
|
107
112
|
- spec/scope_spec.rb
|
@@ -109,7 +114,9 @@ files:
|
|
109
114
|
- spec/support/abstract_model.rb
|
110
115
|
- spec/support/animal.rb
|
111
116
|
- spec/support/car.rb
|
117
|
+
- spec/support/continent.rb
|
112
118
|
- spec/support/country.rb
|
119
|
+
- spec/support/price.rb
|
113
120
|
- spec/test_helper_spec.rb
|
114
121
|
homepage: https://github.com/byroot/frozen_record
|
115
122
|
licenses:
|
@@ -130,14 +137,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
137
|
- !ruby/object:Gem::Version
|
131
138
|
version: '0'
|
132
139
|
requirements: []
|
133
|
-
rubygems_version: 3.
|
140
|
+
rubygems_version: 3.3.7
|
134
141
|
signing_key:
|
135
142
|
specification_version: 4
|
136
143
|
summary: ActiveRecord like interface to read only access and query static YAML files
|
137
144
|
test_files:
|
138
145
|
- spec/fixtures/animals.json
|
139
146
|
- spec/fixtures/cars.yml
|
147
|
+
- spec/fixtures/continents.yml.erb
|
140
148
|
- spec/fixtures/countries.yml.erb
|
149
|
+
- spec/fixtures/prices.yml.erb
|
150
|
+
- spec/fixtures/test_helper/continents.yml.erb
|
141
151
|
- spec/fixtures/test_helper/countries.yml.erb
|
142
152
|
- spec/frozen_record_spec.rb
|
143
153
|
- spec/scope_spec.rb
|
@@ -145,5 +155,7 @@ test_files:
|
|
145
155
|
- spec/support/abstract_model.rb
|
146
156
|
- spec/support/animal.rb
|
147
157
|
- spec/support/car.rb
|
158
|
+
- spec/support/continent.rb
|
148
159
|
- spec/support/country.rb
|
160
|
+
- spec/support/price.rb
|
149
161
|
- spec/test_helper_spec.rb
|