frozen_record 0.18.0 → 0.19.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: a47352d34eb964f32bf8d253cffe682abc221f80db5782ed7dce6aee76967661
4
- data.tar.gz: ea62e1c1bcc0cf927a2614f2fb4cfc1acbb6780cdfec273a44dbbe1bd14a55e2
3
+ metadata.gz: e1006ddbdc9f21010a819e6085800b05fc549c9b513c75431b329f20d46e055a
4
+ data.tar.gz: b3f65c04430375bf7fe2be7ecd233515e9a8b4a1dc7ebc29a430fa4934fad8be
5
5
  SHA512:
6
- metadata.gz: 33f76d7869d6a1e8a9cae954b074fa93cb89ffa3fcee9bc170d3e23b5ee2fcb39164a7e9de0b0f1570588bd7919d88b288bb35bc9c965cb32b77e3e9c6b5928a
7
- data.tar.gz: e38977cfbdb6136aed4042060e130bc260f6e33c8db9c876f05f832d65037ba504fe7119a5b338c62e9a17391b2d1472ab2e2de5df5ae2c90b72772bd0599cd8
6
+ metadata.gz: db63b827feb659e1228812947551db15ef777b33c71b38d3d9c77aa304d148b2e23eaaa6075e3dda90bd759abd9774635030bd01947b6d7b61fa01a53114601c
7
+ data.tar.gz: a11f92b0a5ff35033c348b69735821969f33a93fdca861f58b3dd4c5a58263da643ce6efe6c9f7f473e754615e91c94712dce7901f7d49b2893bd3a808b9bc7b
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  vendor
19
+ .byebug_history
data/README.md CHANGED
@@ -140,6 +140,22 @@ Country.european.republics.part_of_nato.order(id: :desc)
140
140
  - average
141
141
 
142
142
 
143
+ ## Indexing
144
+
145
+ Querying is implemented as a simple linear search (`O(n)`). However if you are using Frozen Record with larger datasets, or are querying
146
+ a collection repetedly, you can define indices for faster access.
147
+
148
+ ```ruby
149
+ class Country < FrozenRecord::Base
150
+ add_index :name, unique: true
151
+ add_index :continent
152
+ end
153
+ ```
154
+
155
+ Composite index keys are not supported.
156
+
157
+ The primary key isn't indexed by default.
158
+
143
159
  ## Configuration
144
160
 
145
161
  ### Reloading
@@ -7,6 +7,7 @@ require 'active_model'
7
7
 
8
8
  require 'frozen_record/version'
9
9
  require 'frozen_record/scope'
10
+ require 'frozen_record/index'
10
11
  require 'frozen_record/base'
11
12
  require 'frozen_record/compact'
12
13
  require 'frozen_record/deduplication'
@@ -21,6 +21,7 @@ module FrozenRecord
21
21
  FALSY_VALUES = [false, nil, 0, -''].to_set
22
22
 
23
23
  class_attribute :base_path, :primary_key, :backend, :auto_reloading, :default_attributes, instance_accessor: false
24
+ class_attribute :index_definitions, instance_accessor: false, default: {}.freeze
24
25
 
25
26
  self.primary_key = 'id'
26
27
 
@@ -103,6 +104,11 @@ module FrozenRecord
103
104
  end
104
105
  end
105
106
 
107
+ def add_index(attribute, unique: false)
108
+ index = unique ? UniqueIndex.new(self, attribute) : Index.new(self, attribute)
109
+ self.index_definitions = index_definitions.merge(index.attribute => index).freeze
110
+ end
111
+
106
112
  def memsize(object = self, seen = Set.new.compare_by_identity)
107
113
  return 0 unless seen.add?(object)
108
114
 
@@ -143,15 +149,14 @@ module FrozenRecord
143
149
  records = Deduplication.deep_deduplicate!(records)
144
150
  @attributes = list_attributes(records).freeze
145
151
  define_attribute_methods(@attributes.to_a)
146
- records.map { |r| load(r) }.freeze
152
+ records = records.map { |r| load(r) }.freeze
153
+ index_definitions.values.each { |index| index.build(records) }
154
+ records
147
155
  end
148
156
  end
149
157
 
150
158
  def scope(name, body)
151
- unless body.respond_to?(:call)
152
- raise ArgumentError, "The scope body needs to be callable."
153
- end
154
- singleton_class.send(:define_method, name) { |*args| body.call(*args) }
159
+ singleton_class.send(:define_method, name, &body)
155
160
  end
156
161
 
157
162
  alias_method :load, :new
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FrozenRecord
4
+ class Index
5
+ EMPTY_ARRAY = [].freeze
6
+ private_constant :EMPTY_ARRAY
7
+
8
+ AttributeNonUnique = Class.new(StandardError)
9
+
10
+ attr_reader :attribute, :model
11
+
12
+ def initialize(model, attribute, unique: false)
13
+ @model = model
14
+ @attribute = -attribute.to_s
15
+ @index = nil
16
+ end
17
+
18
+ def unique?
19
+ false
20
+ end
21
+
22
+ def query(value)
23
+ @index.fetch(value, EMPTY_ARRAY)
24
+ end
25
+
26
+ def reset
27
+ @index = nil
28
+ end
29
+
30
+ def build(records)
31
+ @index = records.each_with_object({}) do |record, index|
32
+ entry = (index[record[attribute]] ||= [])
33
+ entry << record
34
+ end
35
+ @index.values.each(&:freeze)
36
+ @index.freeze
37
+ end
38
+ end
39
+
40
+ class UniqueIndex < Index
41
+ def unique?
42
+ true
43
+ end
44
+
45
+ def query(value)
46
+ record = @index[value]
47
+ record ? [record] : EMPTY_ARRAY
48
+ end
49
+
50
+ def build(records)
51
+ @index = records.to_h { |r| [r[attribute], r] }
52
+ if @index.size != records.size
53
+ raise AttributeNonUnique, "#{model}##{attribute.inspect} is not unique."
54
+ end
55
+ @index.freeze
56
+ end
57
+ end
58
+ end
@@ -175,9 +175,20 @@ module FrozenRecord
175
175
  def select_records(records)
176
176
  return records if @where_values.empty? && @where_not_values.empty?
177
177
 
178
+ indices = @klass.index_definitions
179
+ indexed_where_values, unindexed_where_values = @where_values.partition { |criteria| indices.key?(criteria.first) }
180
+
181
+ unless indexed_where_values.empty?
182
+ attribute, value = indexed_where_values.shift
183
+ records = indices[attribute].query(value)
184
+ indexed_where_values.each do |(attribute, value)|
185
+ records &= indices[attribute].query(value)
186
+ end
187
+ end
188
+
178
189
  records.select do |record|
179
- @where_values.all? { |attr, value| compare_value(record[attr], value) } &&
180
- @where_not_values.all? { |attr, value| !compare_value(record[attr], value) }
190
+ unindexed_where_values.all? { |attr, value| compare_value(record[attr], value) } &&
191
+ !@where_not_values.any? { |attr, value| compare_value(record[attr], value) }
181
192
  end
182
193
  end
183
194
 
@@ -226,12 +237,12 @@ module FrozenRecord
226
237
  end
227
238
 
228
239
  def where!(criterias)
229
- @where_values += criterias.to_a
240
+ @where_values += criterias.map { |k, v| [k.to_s, v] }
230
241
  self
231
242
  end
232
243
 
233
244
  def where_not!(criterias)
234
- @where_not_values += criterias.to_a
245
+ @where_not_values += criterias.map { |k, v| [k.to_s, v] }
235
246
  self
236
247
  end
237
248
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FrozenRecord
4
- VERSION = '0.18.0'
4
+ VERSION = '0.19.0'
5
5
  end
@@ -83,7 +83,7 @@ RSpec.shared_examples 'main' do
83
83
  describe '.scope' do
84
84
 
85
85
  it 'defines a scope method' do
86
- country_model.scope :north_american, -> { country_model.where(continent: 'North America') }
86
+ country_model.scope :north_american, -> { where(continent: 'North America') }
87
87
  expect(country_model).to respond_to(:north_american)
88
88
  expect(country_model.north_american.first.name).to be == 'Canada'
89
89
  end
@@ -203,6 +203,10 @@ describe 'querying' do
203
203
  expect(countries.length).to be == 2
204
204
  end
205
205
 
206
+ it 'can combine indices' do
207
+ countries = Country.where(name: 'France', continent: 'Europe')
208
+ expect(countries.length).to be == 1
209
+ end
206
210
  end
207
211
 
208
212
  describe '.where.not' do
@@ -428,8 +432,8 @@ describe 'querying' do
428
432
  it 'returns true when the same scope has be rechained' do
429
433
  scope_a = Country.nato.republics.nato.republics
430
434
  scope_b = Country.republics.nato
431
- expect(scope_a.instance_variable_get(:@where_values)).to be == [[:nato, true ], [:king, nil ], [:nato, true], [:king, nil]]
432
- expect(scope_b.instance_variable_get(:@where_values)).to be == [[:king, nil ], [:nato, true]]
435
+ expect(scope_a.instance_variable_get(:@where_values)).to be == [['nato', true], ['king', nil], ['nato', true], ['king', nil]]
436
+ expect(scope_b.instance_variable_get(:@where_values)).to be == [['king', nil], ['nato', true]]
433
437
  expect(scope_a).to be == scope_b
434
438
  end
435
439
  end
@@ -1,6 +1,9 @@
1
1
  class Country < FrozenRecord::Base
2
2
  self.default_attributes = { contemporary: true, available: true }
3
3
 
4
+ add_index :name, unique: true
5
+ add_index :continent
6
+
4
7
  def self.republics
5
8
  where(king: nil)
6
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: frozen_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
@@ -119,6 +119,7 @@ files:
119
119
  - lib/frozen_record/base.rb
120
120
  - lib/frozen_record/compact.rb
121
121
  - lib/frozen_record/deduplication.rb
122
+ - lib/frozen_record/index.rb
122
123
  - lib/frozen_record/railtie.rb
123
124
  - lib/frozen_record/scope.rb
124
125
  - lib/frozen_record/test_helper.rb