frozen_record 0.23.0 → 0.24.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f377e3f70ea220a100dda1e18d3d01cb4a09958a13581283d20df91a1f369a89
4
- data.tar.gz: 136d29c3521aea08c5688a59bf56edfcc231707915c9e67fa9d8e05ad59e3266
3
+ metadata.gz: b99597156ea028c8d0cbd66024b901d55a5e26679030266367caa8ec81353946
4
+ data.tar.gz: 12e4756a7a49d32010298b4320f447d9b18afd76efe969a1f3a32e7d890592de
5
5
  SHA512:
6
- metadata.gz: 4eda9868623fa545ca344ffae2e5849b1350458a55f764924acea2891cc4a7dadb36d7344d13928c34e4d0416950f70f9075cf63130e132aea5fbdb0b54fb105
7
- data.tar.gz: 481858eac4b1a34f2247aa4173afa145bb535ce07c1e54594287b0749fff0a79e538133fa51eb84b3ffac8e40eaf82af703e7a9e63131938cfe87d4e2fab1e80
6
+ metadata.gz: fcb9c7aa9c820dfa50b431d36bef98d39488be55fa034edaed7aefde063a919134af0d945e3233ca5a6efbc3c3798245a5ea5741b5ae1f4d8dad1b0189b1bf7e
7
+ data.tar.gz: df184ce79d274a9fe8a6fcb70c9548bb18f17c1e8da143c88d2ab9f2aedec746314a21ee2e591e191fd248f8d5e332dfdcc379f316d09fcb070a5284724f43ad
@@ -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,17 @@
1
+ # Unreleased
2
+
3
+ # v0.24.1
4
+
5
+ - Fix index selection not applying some restrictions.
6
+
7
+ # v0.24.0 (yanked)
8
+
9
+ - Improve index selection and combinaison. Should significantly help with performance in some cases.
10
+ - Implement `max_records_scan` to reject slow queries.
11
+ - Only load `Railtie` integration if `Rails::Railtie` is defined
12
+ - Allow granular fixture unloading
13
+ - Fix a bug affecting older bootsnap versions
14
+
15
+ # v0.23.0
16
+
17
+ 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,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'
@@ -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.23.0'
4
+ VERSION = '0.24.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,3 @@
1
+ ---
2
+ - id: 1
3
+ name: Some continent
data/spec/scope_spec.rb CHANGED
@@ -474,4 +474,19 @@ describe 'querying' do
474
474
 
475
475
  end
476
476
 
477
+ context 'when max_records_scan is set' do
478
+
479
+ around :each do |example|
480
+ FrozenRecord::Base.max_records_scan = 1
481
+ example.run
482
+ FrozenRecord::Base.max_records_scan = nil
483
+ end
484
+
485
+ it 'raises on slow queries' do
486
+ expect {
487
+ Country.where(king: "Louis").to_a
488
+ }.to raise_error(FrozenRecord::SlowQuery)
489
+ end
490
+
491
+ end
477
492
  end
@@ -0,0 +1,2 @@
1
+ class Continent < FrozenRecord::Base
2
+ 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.24.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-10 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,9 @@ 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/test_helper/continents.yml.erb
105
109
  - spec/fixtures/test_helper/countries.yml.erb
106
110
  - spec/frozen_record_spec.rb
107
111
  - spec/scope_spec.rb
@@ -109,6 +113,7 @@ files:
109
113
  - spec/support/abstract_model.rb
110
114
  - spec/support/animal.rb
111
115
  - spec/support/car.rb
116
+ - spec/support/continent.rb
112
117
  - spec/support/country.rb
113
118
  - spec/test_helper_spec.rb
114
119
  homepage: https://github.com/byroot/frozen_record
@@ -130,14 +135,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
135
  - !ruby/object:Gem::Version
131
136
  version: '0'
132
137
  requirements: []
133
- rubygems_version: 3.2.22
138
+ rubygems_version: 3.3.7
134
139
  signing_key:
135
140
  specification_version: 4
136
141
  summary: ActiveRecord like interface to read only access and query static YAML files
137
142
  test_files:
138
143
  - spec/fixtures/animals.json
139
144
  - spec/fixtures/cars.yml
145
+ - spec/fixtures/continents.yml.erb
140
146
  - spec/fixtures/countries.yml.erb
147
+ - spec/fixtures/test_helper/continents.yml.erb
141
148
  - spec/fixtures/test_helper/countries.yml.erb
142
149
  - spec/frozen_record_spec.rb
143
150
  - spec/scope_spec.rb
@@ -145,5 +152,6 @@ test_files:
145
152
  - spec/support/abstract_model.rb
146
153
  - spec/support/animal.rb
147
154
  - spec/support/car.rb
155
+ - spec/support/continent.rb
148
156
  - spec/support/country.rb
149
157
  - spec/test_helper_spec.rb