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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f377e3f70ea220a100dda1e18d3d01cb4a09958a13581283d20df91a1f369a89
4
- data.tar.gz: 136d29c3521aea08c5688a59bf56edfcc231707915c9e67fa9d8e05ad59e3266
3
+ metadata.gz: 2c4f3cb4248d83050752933b1a2c48d0bf0914a73e695932650a03cae9feffa0
4
+ data.tar.gz: fb8517287957255269ffec7ab7bac5740c6d7e45b4f1c3c0b321ce4fc5badf4f
5
5
  SHA512:
6
- metadata.gz: 4eda9868623fa545ca344ffae2e5849b1350458a55f764924acea2891cc4a7dadb36d7344d13928c34e4d0416950f70f9075cf63130e132aea5fbdb0b54fb105
7
- data.tar.gz: 481858eac4b1a34f2247aa4173afa145bb535ce07c1e54594287b0749fff0a79e538133fa51eb84b3ffac8e40eaf82af703e7a9e63131938cfe87d4e2fab1e80
6
+ metadata.gz: 1d66248c13f74ccaa779db89359467e54c1ee18979683fcd42e62485ac5657345115985329f07be02f0ffe6f347334646e26bb920df1b1efad2d3ef55ab24d74
7
+ data.tar.gz: f395e256aca6a133fe684f3e28a94cbdca4f70ea6a2c565a63f30bb4f52144f62529a6b20c24d806e97a4ba0bb11a5ca0e2402d521c700f13bc5b1bb49331578
@@ -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: Setup Ruby
16
+ - name: Set up Ruby
16
17
  uses: ruby/setup-ruby@v1
17
18
  with:
18
19
  ruby-version: ${{ matrix.ruby }}
19
- - uses: actions/cache@v2
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
  [![Build Status](https://secure.travis-ci.org/byroot/frozen_record.svg)](http://travis-ci.org/byroot/frozen_record)
4
4
  [![Gem Version](https://badge.fury.io/rb/frozen_record.svg)](http://badge.fury.io/rb/frozen_record)
5
5
 
6
- ActiveRecord-like interface for **read only** access to static data files.
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 ActiveRecord, your models need to inherits from `FrozenRecord::Base`:
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 ActiveRecord querying interface, and only the non "string typed" ones.
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
@@ -49,7 +49,7 @@ module FrozenRecord
49
49
  private
50
50
 
51
51
  supports_freeze = begin
52
- YAML.load_file(File.expand_path('../empty.json', __FILE__), freeze: true)
52
+ YAML.load_file(File.expand_path('../empty.yml', __FILE__), freeze: true)
53
53
  rescue ArgumentError
54
54
  false
55
55
  end
@@ -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
@@ -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 = indexed_where_values.shift
184
- records = indices[attribute].query(value)
185
- indexed_where_values.each do |(attribute, value)|
186
- records &= indices[attribute].query(value)
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
- unless model_class < FrozenRecord::Base
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 unload_fixtures
21
+ def unload_fixture(model_class)
24
22
  return unless defined?(@cache) && @cache
25
23
 
26
- @cache.each do |model_class, old_base_path|
27
- model_class.base_path = old_base_path
28
- model_class.load_records(force: true)
29
- end
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 = nil
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FrozenRecord
4
- VERSION = '0.23.0'
4
+ VERSION = '0.25.1'
5
5
  end
data/lib/frozen_record.rb CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  require 'frozen_record/minimal'
4
4
  require 'frozen_record/serialization'
5
- require 'frozen_record/railtie' if defined?(Rails)
5
+ require 'frozen_record/railtie' if defined?(Rails::Railtie)
@@ -0,0 +1,9 @@
1
+ ---
2
+ - id: 1
3
+ name: Africa
4
+
5
+ - id: 2
6
+ name: Antarctica
7
+
8
+ - id: 3
9
+ name: Asia
@@ -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 %>
@@ -0,0 +1,3 @@
1
+ ---
2
+ - id: 1
3
+ name: Some continent
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
@@ -0,0 +1,2 @@
1
+ class Continent < FrozenRecord::Base
2
+ end
@@ -0,0 +1,13 @@
1
+ class Price < FrozenRecord::Base
2
+ add_index :plan_name
3
+ add_index :currency
4
+ end
5
+
6
+ module Compact
7
+ class Price < ::Price
8
+ include FrozenRecord::Compact
9
+ def self.file_path
10
+ superclass.file_path
11
+ end
12
+ end
13
+ end
@@ -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.23.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: 2021-10-22 00:00:00.000000000 Z
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.2.22
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