frozen_record 0.22.2 → 0.25.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46d80cfe87901ec547852256bff7eed06ac990a7bb9705b93fa13464354c5af5
4
- data.tar.gz: 63f48c770a8e86d867382f0333f1c5d6d48b64ece3b849717fd840945f0781ff
3
+ metadata.gz: d2be87eaecead5b4f1d4c5e941d48fcd9be163c56f2c9b988ec4d6ff7ea84757
4
+ data.tar.gz: 0a4a51c609251f3a46ba49e16f85b1d27ce5e52581fb70bd605c8585b1f62fa8
5
5
  SHA512:
6
- metadata.gz: 0a5c7c217742022ff5d23532cc54c04f040ae83e9e05c191783a49db5012c2c95f2815a85a8743e00bd467bbff9fb451d94ba5a75cdd80bde71dee66ccb2a256
7
- data.tar.gz: 587dc2bcd6cec261cd53c9be8cfe696b7f4944d1c55b5cea55945b7826112eed2829a323c617c906dfcd039d72578e4b4ba5cb790a1ba1131277c6076dafcafc
6
+ metadata.gz: 4326747e566bdb03b700dd5bb8164b550f8d339991754cebeb2a5414f8858a4bb19c074ac2231d4e978be881ed0ff87611c107c69bbc2a21ba498b7d3eb563eb
7
+ data.tar.gz: f73715f137336bada8a8aad2a3be9d6b4cf87f4de9dae6c2b77463201f7a3e510ae6c7491d9957db9c7d2b6df8280576e7e896eb8bf28b661bff02d32d4634f5
@@ -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,22 @@
1
+ # Unreleased
2
+
3
+ # v0.25.0
4
+
5
+ - Disable max_records_scan checks when loading records.
6
+ - Add `FrozenRecord::Base.with_max_record_scan` for more easily allowing larger amount in specific tests.
7
+
8
+ # v0.24.1
9
+
10
+ - Fix index selection not applying some restrictions.
11
+
12
+ # v0.24.0 (yanked)
13
+
14
+ - Improve index selection and combinaison. Should significantly help with performance in some cases.
15
+ - Implement `max_records_scan` to reject slow queries.
16
+ - Only load `Railtie` integration if `Rails::Railtie` is defined
17
+ - Allow granular fixture unloading
18
+ - Fix a bug affecting older bootsnap versions
19
+
20
+ # v0.23.0
21
+
22
+ 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
@@ -190,7 +197,7 @@ require 'frozen_record/test_helper'
190
197
 
191
198
  class CountryTest < ActiveSupport::TestCase
192
199
  setup do
193
- test_fixtures_base_path = Rails.root.join(%w(test support fixtures))
200
+ test_fixtures_base_path = Rails.root.join('test/support/fixtures')
194
201
  FrozenRecord::TestHelper.load_fixture(Country, test_fixtures_base_path)
195
202
  end
196
203
 
@@ -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,8 @@ require 'active_support/descendants_tracker'
4
4
  require 'frozen_record/backends'
5
5
 
6
6
  module FrozenRecord
7
+ SlowQuery = Class.new(StandardError)
8
+
7
9
  class Base
8
10
  extend ActiveSupport::DescendantsTracker
9
11
  extend ActiveModel::Naming
@@ -15,6 +17,7 @@ module FrozenRecord
15
17
 
16
18
  class_attribute :base_path, :primary_key, :backend, :auto_reloading, :default_attributes, instance_accessor: false
17
19
  class_attribute :index_definitions, instance_accessor: false
20
+ class_attribute :max_records_scan, instance_accessor: false
18
21
  self.index_definitions = {}.freeze
19
22
 
20
23
  self.primary_key = 'id'
@@ -42,6 +45,14 @@ module FrozenRecord
42
45
  end
43
46
 
44
47
  class << self
48
+ def with_max_records_scan(value)
49
+ previous_max_records_scan = max_records_scan
50
+ self.max_records_scan = value
51
+ yield
52
+ ensure
53
+ self.max_records_scan = previous_max_records_scan
54
+ end
55
+
45
56
  alias_method :set_default_attributes, :default_attributes=
46
57
  private :set_default_attributes
47
58
  def default_attributes=(default_attributes)
@@ -145,7 +156,7 @@ module FrozenRecord
145
156
  end
146
157
  @attributes = list_attributes(records).freeze
147
158
  define_attribute_methods(@attributes.to_a)
148
- records = records.map { |r| load(r) }.freeze
159
+ records = with_max_records_scan(nil) { records.map { |r| load(r) }.freeze }
149
160
  index_definitions.values.each { |index| index.build(records) }
150
161
  records
151
162
  end
@@ -23,5 +23,5 @@ module FrozenRecord
23
23
  end
24
24
  end
25
25
 
26
- self.deprecated_yaml_erb_backend = true
26
+ self.deprecated_yaml_erb_backend = false
27
27
  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 @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.22.2'
4
+ VERSION = '0.25.0'
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.22.2
4
+ version: 0.25.0
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-07-06 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.3.0.dev
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