frozen_record 0.18.0 → 0.19.0

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: 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