frozen_record 0.17.0 → 0.19.3
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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +0 -1
- data/Gemfile +2 -0
- data/README.md +17 -1
- data/benchmark/attribute-read +22 -0
- data/benchmark/memory-usage +10 -0
- data/benchmark/setup.rb +6 -0
- data/frozen_record.gemspec +2 -0
- data/lib/frozen_record.rb +2 -0
- data/lib/frozen_record/base.rb +27 -5
- data/lib/frozen_record/compact.rb +78 -0
- data/lib/frozen_record/index.rb +77 -0
- data/lib/frozen_record/scope.rb +15 -4
- data/lib/frozen_record/version.rb +1 -1
- data/spec/frozen_record_spec.rb +74 -46
- data/spec/scope_spec.rb +14 -2
- data/spec/support/animal.rb +9 -0
- data/spec/support/car.rb +10 -0
- data/spec/support/country.rb +12 -0
- metadata +13 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9976300538ac81378c16689a47f03994e79b65a6cbbf755fa986528d4324533
|
4
|
+
data.tar.gz: 5e9b6c08770cd5186025041b0c6244eff2360c5b096a532802d953527abd3d1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a00f10f827cd7e046d1f12433e913c39b9442dca95986cb395f81d28a685f3e7f4625814de1aeaf31654f4d2b8decab71938fe9999ccf5cb8c2fe3d3fbebe6ca
|
7
|
+
data.tar.gz: 070fa00130a5577cdb7441c14cb76d76f7290245f60b7b927081d169b4e86871f936716c6bfecb9859113df71791611f57546139ab80dcf1f4078a79e1e279a0
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -60,7 +60,7 @@ A custom backend must implement the methods `filename` and `load` as follows:
|
|
60
60
|
|
61
61
|
```ruby
|
62
62
|
module MyCustomBackend
|
63
|
-
extend
|
63
|
+
extend self
|
64
64
|
|
65
65
|
def filename(model_name)
|
66
66
|
# Returns the file name as a String
|
@@ -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
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative 'setup'
|
5
|
+
|
6
|
+
regular = Country.first
|
7
|
+
compact = Compact::Country.first
|
8
|
+
|
9
|
+
|
10
|
+
puts "=== record.attribute ==="
|
11
|
+
Benchmark.ips do |x|
|
12
|
+
x.report('regular') { regular.name }
|
13
|
+
x.report('compact') { compact.name }
|
14
|
+
x.compare!
|
15
|
+
end
|
16
|
+
|
17
|
+
puts "=== record[:attribute] ==="
|
18
|
+
Benchmark.ips do |x|
|
19
|
+
x.report('regular') { regular[:name] }
|
20
|
+
x.report('compact') { compact[:name] }
|
21
|
+
x.compare!
|
22
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative 'setup'
|
5
|
+
|
6
|
+
puts "regular: #{Country.memsize} bytes"
|
7
|
+
puts "compact: #{Compact::Country.memsize} bytes"
|
8
|
+
|
9
|
+
diff = (Compact::Country.memsize - Country.memsize).to_f / Country.memsize
|
10
|
+
puts "diff: #{(diff * 100).round(2)}%"
|
data/benchmark/setup.rb
ADDED
data/frozen_record.gemspec
CHANGED
@@ -17,6 +17,8 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ['lib']
|
19
19
|
|
20
|
+
spec.required_ruby_version = '>= 2.5'
|
21
|
+
|
20
22
|
spec.add_runtime_dependency 'activemodel'
|
21
23
|
spec.add_development_dependency 'bundler'
|
22
24
|
spec.add_development_dependency 'rake'
|
data/lib/frozen_record.rb
CHANGED
data/lib/frozen_record/base.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'set'
|
4
4
|
require 'active_support/descendants_tracker'
|
5
5
|
require 'frozen_record/backends'
|
6
|
+
require 'objspace'
|
6
7
|
|
7
8
|
module FrozenRecord
|
8
9
|
class Base
|
@@ -20,6 +21,8 @@ module FrozenRecord
|
|
20
21
|
FALSY_VALUES = [false, nil, 0, -''].to_set
|
21
22
|
|
22
23
|
class_attribute :base_path, :primary_key, :backend, :auto_reloading, :default_attributes, instance_accessor: false
|
24
|
+
class_attribute :index_definitions, instance_accessor: false
|
25
|
+
self.index_definitions = {}.freeze
|
23
26
|
|
24
27
|
self.primary_key = 'id'
|
25
28
|
|
@@ -102,6 +105,26 @@ module FrozenRecord
|
|
102
105
|
end
|
103
106
|
end
|
104
107
|
|
108
|
+
def add_index(attribute, unique: false)
|
109
|
+
index = unique ? UniqueIndex.new(self, attribute) : Index.new(self, attribute)
|
110
|
+
self.index_definitions = index_definitions.merge(index.attribute => index).freeze
|
111
|
+
end
|
112
|
+
|
113
|
+
def memsize(object = self, seen = Set.new.compare_by_identity)
|
114
|
+
return 0 unless seen.add?(object)
|
115
|
+
|
116
|
+
size = ObjectSpace.memsize_of(object)
|
117
|
+
object.instance_variables.each { |v| size += memsize(object.instance_variable_get(v), seen) }
|
118
|
+
|
119
|
+
case object
|
120
|
+
when Hash
|
121
|
+
object.each { |k, v| size += memsize(k, seen) + memsize(v, seen) }
|
122
|
+
when Array
|
123
|
+
object.each { |i| size += memsize(i, seen) }
|
124
|
+
end
|
125
|
+
size
|
126
|
+
end
|
127
|
+
|
105
128
|
def respond_to_missing?(name, *)
|
106
129
|
if name.to_s =~ FIND_BY_PATTERN
|
107
130
|
load_records # ensure attribute methods are defined
|
@@ -127,15 +150,14 @@ module FrozenRecord
|
|
127
150
|
records = Deduplication.deep_deduplicate!(records)
|
128
151
|
@attributes = list_attributes(records).freeze
|
129
152
|
define_attribute_methods(@attributes.to_a)
|
130
|
-
records.map { |r| load(r) }.freeze
|
153
|
+
records = records.map { |r| load(r) }.freeze
|
154
|
+
index_definitions.values.each { |index| index.build(records) }
|
155
|
+
records
|
131
156
|
end
|
132
157
|
end
|
133
158
|
|
134
159
|
def scope(name, body)
|
135
|
-
|
136
|
-
raise ArgumentError, "The scope body needs to be callable."
|
137
|
-
end
|
138
|
-
singleton_class.send(:define_method, name) { |*args| body.call(*args) }
|
160
|
+
singleton_class.send(:define_method, name, &body)
|
139
161
|
end
|
140
162
|
|
141
163
|
alias_method :load, :new
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FrozenRecord
|
4
|
+
module Compact
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def load_records(force: false)
|
9
|
+
if force || (auto_reloading && file_changed?)
|
10
|
+
@records = nil
|
11
|
+
undefine_attribute_methods
|
12
|
+
end
|
13
|
+
|
14
|
+
@records ||= begin
|
15
|
+
records = backend.load(file_path)
|
16
|
+
records.each { |r| assign_defaults!(r) }
|
17
|
+
records = Deduplication.deep_deduplicate!(records)
|
18
|
+
@attributes = list_attributes(records).freeze
|
19
|
+
build_attributes_cache
|
20
|
+
define_attribute_methods(@attributes.to_a)
|
21
|
+
index_definitions.values.each { |index| index.build(records) }
|
22
|
+
records.map { |r| load(r) }.freeze
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if ActiveModel.gem_version >= Gem::Version.new('6.1.0.alpha')
|
27
|
+
def define_method_attribute(attr, owner:)
|
28
|
+
owner << "attr_reader #{attr.inspect}"
|
29
|
+
end
|
30
|
+
else
|
31
|
+
def define_method_attribute(attr)
|
32
|
+
generated_attribute_methods.attr_reader(attr)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :_attributes_cache
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def build_attributes_cache
|
41
|
+
@_attributes_cache = @attributes.each_with_object({}) do |attr, cache|
|
42
|
+
var = :"@#{attr}"
|
43
|
+
cache[attr.to_s] = var
|
44
|
+
cache[attr.to_sym] = var
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(attrs = {})
|
50
|
+
self.attributes = attrs
|
51
|
+
end
|
52
|
+
|
53
|
+
def attributes
|
54
|
+
self.class.attributes.each_with_object({}) do |attr, hash|
|
55
|
+
hash[attr] = self[attr]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def [](attr)
|
60
|
+
if var = self.class._attributes_cache[attr]
|
61
|
+
instance_variable_get(var)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def attributes=(attributes)
|
68
|
+
self.class.attributes.each do |attr|
|
69
|
+
instance_variable_set(self.class._attributes_cache[attr], Deduplication.deep_deduplicate!(attributes[attr]))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def attribute?(attribute_name)
|
74
|
+
val = self[attribute_name]
|
75
|
+
Base::FALSY_VALUES.exclude?(val) && val.present?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,77 @@
|
|
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
|
+
case value
|
24
|
+
when Array, Range
|
25
|
+
lookup_multi(value)
|
26
|
+
else
|
27
|
+
lookup(value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def lookup_multi(values)
|
32
|
+
values.flat_map { |v| lookup(v) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def lookup(value)
|
36
|
+
@index.fetch(value, EMPTY_ARRAY)
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset
|
40
|
+
@index = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def build(records)
|
44
|
+
@index = records.each_with_object({}) do |record, index|
|
45
|
+
entry = (index[record[attribute]] ||= [])
|
46
|
+
entry << record
|
47
|
+
end
|
48
|
+
@index.values.each(&:freeze)
|
49
|
+
@index.freeze
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class UniqueIndex < Index
|
54
|
+
def unique?
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
def lookup_multi(values)
|
59
|
+
results = @index.values_at(*values)
|
60
|
+
results.compact!
|
61
|
+
results
|
62
|
+
end
|
63
|
+
|
64
|
+
def lookup(value)
|
65
|
+
record = @index[value]
|
66
|
+
record ? [record] : EMPTY_ARRAY
|
67
|
+
end
|
68
|
+
|
69
|
+
def build(records)
|
70
|
+
@index = records.each_with_object({}) { |r, index| index[r[attribute]] = r }
|
71
|
+
if @index.size != records.size
|
72
|
+
raise AttributeNonUnique, "#{model}##{attribute.inspect} is not unique."
|
73
|
+
end
|
74
|
+
@index.freeze
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/frozen_record/scope.rb
CHANGED
@@ -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
|
-
|
180
|
-
|
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.
|
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.
|
245
|
+
@where_not_values += criterias.map { |k, v| [k.to_s, v] }
|
235
246
|
self
|
236
247
|
end
|
237
248
|
|
data/spec/frozen_record_spec.rb
CHANGED
@@ -1,32 +1,21 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
describe '.base_path' do
|
6
|
-
|
7
|
-
it 'raise a RuntimeError on first query attempt if not set' do
|
8
|
-
allow(Country).to receive_message_chain(:base_path).and_return(nil)
|
9
|
-
expect {
|
10
|
-
Country.file_path
|
11
|
-
}.to raise_error(ArgumentError)
|
12
|
-
end
|
13
|
-
|
14
|
-
end
|
3
|
+
RSpec.shared_examples 'main' do
|
15
4
|
|
16
5
|
describe '.primary_key' do
|
17
6
|
|
18
7
|
around do |example|
|
19
|
-
previous_primary_key =
|
8
|
+
previous_primary_key = country_model.primary_key
|
20
9
|
begin
|
21
10
|
example.run
|
22
11
|
ensure
|
23
|
-
|
12
|
+
country_model.primary_key = previous_primary_key
|
24
13
|
end
|
25
14
|
end
|
26
15
|
|
27
16
|
it 'is coerced to string' do
|
28
|
-
|
29
|
-
expect(
|
17
|
+
country_model.primary_key = :foobar
|
18
|
+
expect(country_model.primary_key).to be == 'foobar'
|
30
19
|
end
|
31
20
|
|
32
21
|
end
|
@@ -36,24 +25,24 @@ describe FrozenRecord::Base do
|
|
36
25
|
context 'when enabled' do
|
37
26
|
|
38
27
|
around do |example|
|
39
|
-
previous_auto_reloading =
|
40
|
-
|
28
|
+
previous_auto_reloading = country_model.auto_reloading
|
29
|
+
country_model.auto_reloading = true
|
41
30
|
begin
|
42
31
|
example.run
|
43
32
|
ensure
|
44
|
-
|
33
|
+
country_model.auto_reloading = previous_auto_reloading
|
45
34
|
end
|
46
35
|
end
|
47
36
|
|
48
37
|
it 'reloads the records if the file mtime changed' do
|
49
|
-
mtime = File.mtime(
|
38
|
+
mtime = File.mtime(country_model.file_path)
|
50
39
|
expect {
|
51
|
-
File.utime(mtime + 1, mtime + 1,
|
52
|
-
}.to change {
|
40
|
+
File.utime(mtime + 1, mtime + 1, country_model.file_path)
|
41
|
+
}.to change { country_model.first.object_id }
|
53
42
|
end
|
54
43
|
|
55
44
|
it 'does not reload if the file has not changed' do
|
56
|
-
expect(
|
45
|
+
expect(country_model.first.object_id).to be == country_model.first.object_id
|
57
46
|
end
|
58
47
|
|
59
48
|
end
|
@@ -61,10 +50,10 @@ describe FrozenRecord::Base do
|
|
61
50
|
context 'when disabled' do
|
62
51
|
|
63
52
|
it 'does not reloads the records if the file mtime changed' do
|
64
|
-
mtime = File.mtime(
|
53
|
+
mtime = File.mtime(country_model.file_path)
|
65
54
|
expect {
|
66
|
-
File.utime(mtime + 1, mtime + 1,
|
67
|
-
}.to_not change {
|
55
|
+
File.utime(mtime + 1, mtime + 1, country_model.file_path)
|
56
|
+
}.to_not change { country_model.first.object_id }
|
68
57
|
end
|
69
58
|
|
70
59
|
end
|
@@ -74,40 +63,52 @@ describe FrozenRecord::Base do
|
|
74
63
|
describe '.default_attributes' do
|
75
64
|
|
76
65
|
it 'define the attribute' do
|
77
|
-
expect(
|
66
|
+
expect(country_model.new).to respond_to :contemporary
|
78
67
|
end
|
79
68
|
|
80
69
|
it 'sets the value as default' do
|
81
|
-
expect(
|
70
|
+
expect(country_model.find_by(name: 'Austria').contemporary).to be == true
|
82
71
|
end
|
83
72
|
|
84
73
|
it 'gives precedence to the data file' do
|
85
|
-
expect(
|
74
|
+
expect(country_model.find_by(name: 'Austria').available).to be == false
|
86
75
|
end
|
87
76
|
|
88
77
|
it 'is also set in the initializer' do
|
89
|
-
expect(
|
78
|
+
expect(country_model.new.contemporary).to be == true
|
90
79
|
end
|
80
|
+
|
91
81
|
end
|
92
82
|
|
93
83
|
describe '.scope' do
|
84
|
+
|
94
85
|
it 'defines a scope method' do
|
86
|
+
country_model.scope :north_american, -> { where(continent: 'North America') }
|
87
|
+
expect(country_model).to respond_to(:north_american)
|
88
|
+
expect(country_model.north_american.first.name).to be == 'Canada'
|
89
|
+
end
|
95
90
|
|
96
|
-
|
97
|
-
|
98
|
-
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '.memsize' do
|
94
|
+
|
95
|
+
it 'retuns the records memory footprint' do
|
96
|
+
# Memory footprint is very dependent on the Ruby implementation and version
|
97
|
+
expect(country_model.memsize).to be > 0
|
98
|
+
expect(car_model.memsize).to be > 0
|
99
99
|
end
|
100
|
+
|
100
101
|
end
|
101
102
|
|
102
103
|
describe '#load_records' do
|
103
104
|
|
104
105
|
it 'processes erb by default' do
|
105
|
-
country =
|
106
|
+
country = country_model.first
|
106
107
|
expect(country.capital).to be == 'Ottawa'
|
107
108
|
end
|
108
109
|
|
109
110
|
it 'loads records with a custom backend' do
|
110
|
-
animal =
|
111
|
+
animal = animal_model.first
|
111
112
|
expect(animal.name).to be == 'cat'
|
112
113
|
end
|
113
114
|
|
@@ -116,22 +117,22 @@ describe FrozenRecord::Base do
|
|
116
117
|
describe '#==' do
|
117
118
|
|
118
119
|
it 'returns true if both instances are from the same class and have the same id' do
|
119
|
-
country =
|
120
|
+
country = country_model.first
|
120
121
|
second_country = country.dup
|
121
122
|
|
122
123
|
expect(country).to be == second_country
|
123
124
|
end
|
124
125
|
|
125
126
|
it 'returns false if both instances are not from the same class' do
|
126
|
-
country =
|
127
|
-
car =
|
127
|
+
country = country_model.first
|
128
|
+
car = car_model.new(id: country.id)
|
128
129
|
|
129
130
|
expect(country).to_not be == car
|
130
131
|
end
|
131
132
|
|
132
133
|
it 'returns false if both instances do not have the same id' do
|
133
|
-
country =
|
134
|
-
second_country =
|
134
|
+
country = country_model.first
|
135
|
+
second_country = country_model.last
|
135
136
|
|
136
137
|
expect(country).to_not be == second_country
|
137
138
|
end
|
@@ -141,7 +142,7 @@ describe FrozenRecord::Base do
|
|
141
142
|
describe '#attributes' do
|
142
143
|
|
143
144
|
it 'returns a Hash of the record attributes' do
|
144
|
-
attributes =
|
145
|
+
attributes = country_model.first.attributes
|
145
146
|
expect(attributes).to be == {
|
146
147
|
'id' => 1,
|
147
148
|
'name' => 'Canada',
|
@@ -162,9 +163,9 @@ describe FrozenRecord::Base do
|
|
162
163
|
|
163
164
|
describe '`attribute`?' do
|
164
165
|
|
165
|
-
let(:blank) {
|
166
|
+
let(:blank) { country_model.new(id: 0, name: '', nato: false, king: nil) }
|
166
167
|
|
167
|
-
let(:present) {
|
168
|
+
let(:present) { country_model.new(id: 42, name: 'Groland', nato: true, king: Object.new) }
|
168
169
|
|
169
170
|
it 'considers `0` as missing' do
|
170
171
|
expect(blank.id?).to be false
|
@@ -203,7 +204,7 @@ describe FrozenRecord::Base do
|
|
203
204
|
describe '#present?' do
|
204
205
|
|
205
206
|
it 'returns true' do
|
206
|
-
expect(
|
207
|
+
expect(country_model.first).to be_present
|
207
208
|
end
|
208
209
|
|
209
210
|
end
|
@@ -211,12 +212,39 @@ describe FrozenRecord::Base do
|
|
211
212
|
describe '#count' do
|
212
213
|
|
213
214
|
it 'can count objects with no records' do
|
214
|
-
expect(
|
215
|
+
expect(car_model.count).to be 0
|
215
216
|
end
|
216
217
|
|
217
218
|
it 'can count objects with records' do
|
218
|
-
expect(
|
219
|
+
expect(country_model.count).to be 3
|
219
220
|
end
|
220
221
|
|
221
222
|
end
|
222
223
|
end
|
224
|
+
|
225
|
+
describe FrozenRecord::Base do
|
226
|
+
let(:country_model) { Country }
|
227
|
+
let(:car_model) { Car }
|
228
|
+
let(:animal_model) { Animal }
|
229
|
+
|
230
|
+
it_behaves_like 'main'
|
231
|
+
|
232
|
+
describe '.base_path' do
|
233
|
+
|
234
|
+
it 'raise a RuntimeError on first query attempt if not set' do
|
235
|
+
allow(country_model).to receive_message_chain(:base_path).and_return(nil)
|
236
|
+
expect {
|
237
|
+
country_model.file_path
|
238
|
+
}.to raise_error(ArgumentError)
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
describe FrozenRecord::Compact do
|
245
|
+
let(:country_model) { Compact::Country }
|
246
|
+
let(:car_model) { Compact::Car }
|
247
|
+
let(:animal_model) { Compact::Animal }
|
248
|
+
|
249
|
+
it_behaves_like 'main'
|
250
|
+
end
|
data/spec/scope_spec.rb
CHANGED
@@ -203,6 +203,18 @@ 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
|
210
|
+
|
211
|
+
it 'can use indices with inclusion query' do
|
212
|
+
countries = Country.where(continent: ['Europe', 'North America'])
|
213
|
+
expect(countries.length).to be == 3
|
214
|
+
|
215
|
+
countries = Country.where(name: ['France', 'Canada'])
|
216
|
+
expect(countries.length).to be == 2
|
217
|
+
end
|
206
218
|
end
|
207
219
|
|
208
220
|
describe '.where.not' do
|
@@ -428,8 +440,8 @@ describe 'querying' do
|
|
428
440
|
it 'returns true when the same scope has be rechained' do
|
429
441
|
scope_a = Country.nato.republics.nato.republics
|
430
442
|
scope_b = Country.republics.nato
|
431
|
-
expect(scope_a.instance_variable_get(:@where_values)).to be == [[
|
432
|
-
expect(scope_b.instance_variable_get(:@where_values)).to be == [[
|
443
|
+
expect(scope_a.instance_variable_get(:@where_values)).to be == [['nato', true], ['king', nil], ['nato', true], ['king', nil]]
|
444
|
+
expect(scope_b.instance_variable_get(:@where_values)).to be == [['king', nil], ['nato', true]]
|
433
445
|
expect(scope_a).to be == scope_b
|
434
446
|
end
|
435
447
|
end
|
data/spec/support/animal.rb
CHANGED
data/spec/support/car.rb
CHANGED
data/spec/support/country.rb
CHANGED
@@ -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
|
@@ -13,3 +16,12 @@ class Country < FrozenRecord::Base
|
|
13
16
|
name.reverse
|
14
17
|
end
|
15
18
|
end
|
19
|
+
|
20
|
+
module Compact
|
21
|
+
class Country < ::Country
|
22
|
+
include FrozenRecord::Compact
|
23
|
+
def self.file_path
|
24
|
+
superclass.file_path
|
25
|
+
end
|
26
|
+
end
|
27
|
+
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.
|
4
|
+
version: 0.19.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -94,7 +94,7 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
-
description:
|
97
|
+
description:
|
98
98
|
email:
|
99
99
|
- jean.boussier@gmail.com
|
100
100
|
executables: []
|
@@ -108,13 +108,18 @@ files:
|
|
108
108
|
- LICENSE.txt
|
109
109
|
- README.md
|
110
110
|
- Rakefile
|
111
|
+
- benchmark/attribute-read
|
112
|
+
- benchmark/memory-usage
|
113
|
+
- benchmark/setup.rb
|
111
114
|
- frozen_record.gemspec
|
112
115
|
- lib/frozen_record.rb
|
113
116
|
- lib/frozen_record/backends.rb
|
114
117
|
- lib/frozen_record/backends/json.rb
|
115
118
|
- lib/frozen_record/backends/yaml.rb
|
116
119
|
- lib/frozen_record/base.rb
|
120
|
+
- lib/frozen_record/compact.rb
|
117
121
|
- lib/frozen_record/deduplication.rb
|
122
|
+
- lib/frozen_record/index.rb
|
118
123
|
- lib/frozen_record/railtie.rb
|
119
124
|
- lib/frozen_record/scope.rb
|
120
125
|
- lib/frozen_record/test_helper.rb
|
@@ -136,7 +141,7 @@ homepage: https://github.com/byroot/frozen_record
|
|
136
141
|
licenses:
|
137
142
|
- MIT
|
138
143
|
metadata: {}
|
139
|
-
post_install_message:
|
144
|
+
post_install_message:
|
140
145
|
rdoc_options: []
|
141
146
|
require_paths:
|
142
147
|
- lib
|
@@ -144,15 +149,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
144
149
|
requirements:
|
145
150
|
- - ">="
|
146
151
|
- !ruby/object:Gem::Version
|
147
|
-
version: '
|
152
|
+
version: '2.5'
|
148
153
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
154
|
requirements:
|
150
155
|
- - ">="
|
151
156
|
- !ruby/object:Gem::Version
|
152
157
|
version: '0'
|
153
158
|
requirements: []
|
154
|
-
rubygems_version: 3.
|
155
|
-
signing_key:
|
159
|
+
rubygems_version: 3.1.2
|
160
|
+
signing_key:
|
156
161
|
specification_version: 4
|
157
162
|
summary: ActiveRecord like interface to read only access and query static YAML files
|
158
163
|
test_files:
|