ree_lib 1.3.9 → 1.3.10
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/Gemfile.lock +2 -2
- data/lib/ree_lib/packages/ree_dao/package/ree_dao/dataset_extensions.rb +15 -13
- data/lib/ree_lib/packages/ree_dao/spec/ree_dao/db_load_performance_mode_spec.rb +190 -0
- data/lib/ree_lib/packages/ree_dto/package/ree_dto/dto/dto_class_methods.rb +14 -0
- data/lib/ree_lib/packages/ree_dto/spec/ree_dto/build_from_attrs_spec.rb +48 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/db_load_compiler.rb +145 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/mapper.rb +25 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/mapper_strategy.rb +1 -3
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/abstract_type.rb +9 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/any.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/bool.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/date.rb +5 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/date_time.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/float.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/integer.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/rational.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/string.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/time.rb +5 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/wrappers/abstract_wrapper.rb +5 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper.rb +6 -0
- data/lib/ree_lib/packages/ree_mapper/spec/ree_mapper/benchmarks/db_load_benchmark_spec.rb +218 -0
- data/lib/ree_lib/packages/ree_mapper/spec/ree_mapper/db_load_performance_mode_spec.rb +339 -0
- data/lib/ree_lib/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9602630839840f13c5b0be8226aea07ddabc2846b22ee90be4d2a2ba64e43aba
|
|
4
|
+
data.tar.gz: 3bb721b4ccecb67d709d850ca7e56357fe52350a96a1b817d33654a2eed6e759
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8afde36de03a451291b092d3449438de32ccd1f3bd5ea1356d1b487e0a91373e3ce9f7a1bd149913c26953274009244ca343c7e1a508aaab2f219b3391a3f190
|
|
7
|
+
data.tar.gz: b97d9425262c5c99d56c7d23c7069901676594060c40d93c57a9e07a846486bff0cf176c0d39fcff6f77edbdfa239af46261800a7eff74ccb964d2363bc2262d
|
data/Gemfile.lock
CHANGED
|
@@ -183,21 +183,23 @@ module ReeDao
|
|
|
183
183
|
end
|
|
184
184
|
|
|
185
185
|
def with_mapper(mapper)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
m = mapper || opts[:schema_mapper]
|
|
187
|
+
|
|
188
|
+
row_proc = if m.nil?
|
|
189
|
+
Proc.new { |hash| hash }
|
|
190
|
+
elsif (dto = m.find_strategy(:db_load)&.dto) && dto.include?(ReeDto::DSL)
|
|
191
|
+
# ree_dto entities skip set_persistence_state anyway; avoid the
|
|
192
|
+
# per-row ancestor walk entirely
|
|
193
|
+
Proc.new { |hash| m.db_load(hash) }
|
|
194
|
+
else
|
|
189
195
|
Proc.new { |hash|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
entity = m.db_load(hash)
|
|
194
|
-
self.set_persistence_state(entity, hash)
|
|
195
|
-
entity
|
|
196
|
-
else
|
|
197
|
-
hash
|
|
198
|
-
end
|
|
196
|
+
entity = m.db_load(hash)
|
|
197
|
+
self.set_persistence_state(entity, hash)
|
|
198
|
+
entity
|
|
199
199
|
}
|
|
200
|
-
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
clone(schema_mapper: m).with_row_proc(row_proc)
|
|
201
203
|
end
|
|
202
204
|
|
|
203
205
|
private
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
package_require('ree_dao')
|
|
4
|
+
package_require('ree_dto/entity_dsl')
|
|
5
|
+
package_require('ree_dto/dsl')
|
|
6
|
+
|
|
7
|
+
RSpec.describe "db_load performance mode (dao integration)" do
|
|
8
|
+
link :build_sqlite_connection, from: :ree_dao
|
|
9
|
+
|
|
10
|
+
after do
|
|
11
|
+
Ree.disable_irb_mode
|
|
12
|
+
Ree.disable_db_load_performance_mode
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
before :all do
|
|
16
|
+
connection = build_sqlite_connection({database: 'sqlite_db'})
|
|
17
|
+
|
|
18
|
+
if connection.table_exists?(:perf_mode_users)
|
|
19
|
+
connection.drop_table(:perf_mode_users)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
connection.create_table :perf_mode_users do
|
|
23
|
+
primary_key :id
|
|
24
|
+
|
|
25
|
+
column :name, 'varchar(256)'
|
|
26
|
+
column :age, :integer
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
connection.disconnect
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Ree.enable_irb_mode
|
|
33
|
+
|
|
34
|
+
module ReeDaoPerfModeTest
|
|
35
|
+
include Ree::PackageDSL
|
|
36
|
+
|
|
37
|
+
package do
|
|
38
|
+
depends_on :ree_dao
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ReeDaoPerfModeTest::Db
|
|
43
|
+
include Ree::BeanDSL
|
|
44
|
+
|
|
45
|
+
bean :db do
|
|
46
|
+
singleton
|
|
47
|
+
factory :build
|
|
48
|
+
|
|
49
|
+
link :build_sqlite_connection, from: :ree_dao
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build
|
|
53
|
+
build_sqlite_connection({database: 'sqlite_db'})
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class ReeDaoPerfModeTest::User
|
|
58
|
+
include ReeDto::EntityDSL
|
|
59
|
+
|
|
60
|
+
properties(
|
|
61
|
+
id: Nilor[Integer],
|
|
62
|
+
name: String,
|
|
63
|
+
age: Integer,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
attr_accessor :name
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class ReeDaoPerfModeTest::UserDto
|
|
70
|
+
include ReeDto::DSL
|
|
71
|
+
|
|
72
|
+
build_dto do
|
|
73
|
+
field :id, Nilor[Integer]
|
|
74
|
+
field :name, String
|
|
75
|
+
field :age, Integer
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# mappers are built at class definition time, so the flag state around each
|
|
80
|
+
# class body decides which db_load implementation the dao gets
|
|
81
|
+
Ree.enable_db_load_performance_mode
|
|
82
|
+
|
|
83
|
+
class ReeDaoPerfModeTest::CompiledUsersDao
|
|
84
|
+
include ReeDao::DSL
|
|
85
|
+
|
|
86
|
+
dao :compiled_users_dao do
|
|
87
|
+
link :db
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
table :perf_mode_users
|
|
91
|
+
|
|
92
|
+
schema ReeDaoPerfModeTest::User do
|
|
93
|
+
integer :id, null: true
|
|
94
|
+
string :name
|
|
95
|
+
integer :age
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class ReeDaoPerfModeTest::CompiledDtoDao
|
|
100
|
+
include ReeDao::DSL
|
|
101
|
+
|
|
102
|
+
dao :compiled_dto_dao do
|
|
103
|
+
link :db
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
table :perf_mode_users
|
|
107
|
+
|
|
108
|
+
schema ReeDaoPerfModeTest::UserDto do
|
|
109
|
+
integer :id, null: true
|
|
110
|
+
string :name
|
|
111
|
+
integer :age
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
Ree.disable_db_load_performance_mode
|
|
116
|
+
|
|
117
|
+
class ReeDaoPerfModeTest::GenericUsersDao
|
|
118
|
+
include ReeDao::DSL
|
|
119
|
+
|
|
120
|
+
dao :generic_users_dao do
|
|
121
|
+
link :db
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
table :perf_mode_users
|
|
125
|
+
|
|
126
|
+
schema ReeDaoPerfModeTest::User do
|
|
127
|
+
integer :id, null: true
|
|
128
|
+
string :name
|
|
129
|
+
integer :age
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
let(:compiled_dao) { ReeDaoPerfModeTest::CompiledUsersDao.new }
|
|
134
|
+
let(:compiled_dto_dao) { ReeDaoPerfModeTest::CompiledDtoDao.new }
|
|
135
|
+
let(:generic_dao) { ReeDaoPerfModeTest::GenericUsersDao.new }
|
|
136
|
+
|
|
137
|
+
it "compiles the schema mapper of daos defined with the mode on" do
|
|
138
|
+
compiled_mapper = ReeDaoPerfModeTest::CompiledUsersDao.instance_variable_get(:@schema_mapper)
|
|
139
|
+
generic_mapper = ReeDaoPerfModeTest::GenericUsersDao.instance_variable_get(:@schema_mapper)
|
|
140
|
+
|
|
141
|
+
expect(compiled_mapper.class.method_defined?(:__generic_db_load, false)).to be true
|
|
142
|
+
expect(generic_mapper.class.method_defined?(:__generic_db_load, false)).to be false
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "reads identical entities through compiled and generic daos" do
|
|
146
|
+
generic_dao.delete_all
|
|
147
|
+
|
|
148
|
+
user = ReeDaoPerfModeTest::User.new(name: 'John', age: 30)
|
|
149
|
+
generic_dao.put(user)
|
|
150
|
+
|
|
151
|
+
from_generic = generic_dao.find(user.id)
|
|
152
|
+
from_compiled = compiled_dao.find(user.id)
|
|
153
|
+
|
|
154
|
+
expect(from_compiled.id).to eq(from_generic.id)
|
|
155
|
+
expect(from_compiled.name).to eq(from_generic.name)
|
|
156
|
+
expect(from_compiled.age).to eq(from_generic.age)
|
|
157
|
+
|
|
158
|
+
expect(compiled_dao.all.map(&:id)).to eq(generic_dao.all.map(&:id))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "keeps the update round-trip working for entity_dsl entities" do
|
|
162
|
+
generic_dao.delete_all
|
|
163
|
+
|
|
164
|
+
user = ReeDaoPerfModeTest::User.new(name: 'John', age: 30)
|
|
165
|
+
compiled_dao.put(user)
|
|
166
|
+
|
|
167
|
+
loaded = compiled_dao.find(user.id)
|
|
168
|
+
loaded.name = 'Doe'
|
|
169
|
+
compiled_dao.update(loaded)
|
|
170
|
+
|
|
171
|
+
expect(generic_dao.find(user.id).name).to eq('Doe')
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "loads and updates ree_dto entities through the compiled dao" do
|
|
175
|
+
compiled_dto_dao.delete_all
|
|
176
|
+
|
|
177
|
+
dto = ReeDaoPerfModeTest::UserDto.build(id: nil, name: 'Jane', age: 25)
|
|
178
|
+
compiled_dto_dao.put(dto)
|
|
179
|
+
|
|
180
|
+
loaded = compiled_dto_dao.find(dto.id)
|
|
181
|
+
expect(loaded).to be_a(ReeDaoPerfModeTest::UserDto)
|
|
182
|
+
expect(loaded.name).to eq('Jane')
|
|
183
|
+
|
|
184
|
+
loaded.name = 'Janet'
|
|
185
|
+
expect(loaded.changed_fields).to eq([:name])
|
|
186
|
+
compiled_dto_dao.update(loaded)
|
|
187
|
+
|
|
188
|
+
expect(compiled_dto_dao.find(dto.id).name).to eq('Janet')
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -32,6 +32,20 @@ module ReeDto::DtoClassMethods
|
|
|
32
32
|
dto_obj
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Fast-path constructor used by compiled db_load: takes ownership of attrs
|
|
36
|
+
# (it becomes @_attrs without copying), so callers must not reuse the hash.
|
|
37
|
+
# Reads @fields_with_default directly: the contract-wrapped reader would
|
|
38
|
+
# revalidate the array on every row.
|
|
39
|
+
def build_from_attrs(attrs)
|
|
40
|
+
(@fields_with_default || []).each do |field|
|
|
41
|
+
attrs[field.name] = field.default unless attrs.key?(field.name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
dto_obj = allocate
|
|
45
|
+
dto_obj.instance_variable_set(:@_attrs, attrs)
|
|
46
|
+
dto_obj
|
|
47
|
+
end
|
|
48
|
+
|
|
35
49
|
private
|
|
36
50
|
|
|
37
51
|
def set_fields(v)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
package_require("ree_dto/dsl")
|
|
3
|
+
|
|
4
|
+
RSpec.describe "ReeDto build_from_attrs" do
|
|
5
|
+
class ReeDto::BuildFromAttrsDto
|
|
6
|
+
include ReeDto::DSL
|
|
7
|
+
|
|
8
|
+
build_dto do
|
|
9
|
+
field :id, Integer
|
|
10
|
+
field :name, Nilor[String]
|
|
11
|
+
field :with_default, Nilor[Integer], default: 7
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "takes ownership of the attrs hash without copying" do
|
|
16
|
+
attrs = { id: 1, name: "n" }
|
|
17
|
+
dto = ReeDto::BuildFromAttrsDto.build_from_attrs(attrs)
|
|
18
|
+
|
|
19
|
+
expect(dto.attrs).to equal(attrs)
|
|
20
|
+
expect(dto.id).to eq(1)
|
|
21
|
+
expect(dto.name).to eq("n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "fills defaults only for absent keys" do
|
|
25
|
+
dto = ReeDto::BuildFromAttrsDto.build_from_attrs({ id: 1 })
|
|
26
|
+
expect(dto.with_default).to eq(7)
|
|
27
|
+
|
|
28
|
+
dto = ReeDto::BuildFromAttrsDto.build_from_attrs({ id: 1, with_default: nil })
|
|
29
|
+
expect(dto.get_value(:with_default)).to eq(nil)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "matches build for identical inputs" do
|
|
33
|
+
built = ReeDto::BuildFromAttrsDto.build({ id: 1, name: "n" })
|
|
34
|
+
fast = ReeDto::BuildFromAttrsDto.build_from_attrs({ id: 1, name: "n" })
|
|
35
|
+
|
|
36
|
+
expect(fast.attrs).to eq(built.attrs)
|
|
37
|
+
expect(fast).to eq(built)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "produces a fully functional dto" do
|
|
41
|
+
dto = ReeDto::BuildFromAttrsDto.build_from_attrs({ id: 1 })
|
|
42
|
+
|
|
43
|
+
dto.name = "x"
|
|
44
|
+
expect(dto.changed_fields).to eq([:name])
|
|
45
|
+
expect(dto.has_value?(:name)).to be true
|
|
46
|
+
expect(dto.to_h).to eq({ id: 1, name: "x", with_default: 7 })
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Compiles a specialized :db_load for composite mappers when
|
|
4
|
+
# Ree.db_load_performance_mode? is on at mapper build time.
|
|
5
|
+
#
|
|
6
|
+
# The compiled method handles the hot call shape `db_load(hash)` with no
|
|
7
|
+
# role/filters as straight-line per-field code: values of primitive types
|
|
8
|
+
# skip casting behind a cheap type guard (see AbstractType#db_load_trusted_check),
|
|
9
|
+
# non-trusted types (custom, enums, pg wrappers, nested mappers) are cast as
|
|
10
|
+
# usual, and dto construction goes through a single attrs hash instead of
|
|
11
|
+
# build-then-copy. Every other call shape falls back to the untouched generic
|
|
12
|
+
# method, kept as __generic_db_load.
|
|
13
|
+
#
|
|
14
|
+
# Codegen targets the mapper's anonymous class (each Mapper.build creates
|
|
15
|
+
# one), never the instance: registered mappers are frozen.
|
|
16
|
+
class ReeMapper::DbLoadCompiler
|
|
17
|
+
class << self
|
|
18
|
+
def compile(mapper)
|
|
19
|
+
return if mapper.type # leaf mappers keep their generic delegation
|
|
20
|
+
|
|
21
|
+
strategy = mapper.find_strategy(:db_load)
|
|
22
|
+
return unless strategy
|
|
23
|
+
|
|
24
|
+
case (output = strategy.output)
|
|
25
|
+
when ReeMapper::ReeDtoOutput
|
|
26
|
+
builder = "acc = {}"
|
|
27
|
+
finisher = "DB_LOAD_DTO.build_from_attrs(acc)"
|
|
28
|
+
when ReeMapper::HashOutput
|
|
29
|
+
return unless strategy.dto == ::Hash # OpenStruct keeps the generic path
|
|
30
|
+
builder = "acc = {}"
|
|
31
|
+
finisher = "acc"
|
|
32
|
+
when ReeMapper::ObjectOutput
|
|
33
|
+
builder = "acc = DB_LOAD_DTO.allocate"
|
|
34
|
+
finisher = "acc"
|
|
35
|
+
else
|
|
36
|
+
return # StructOutput and unknown outputs keep the generic path
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
klass = mapper.class
|
|
40
|
+
return if klass.method_defined?(:__generic_db_load, false) # already compiled
|
|
41
|
+
|
|
42
|
+
fields = mapper.fields.values
|
|
43
|
+
|
|
44
|
+
set_const(klass, :DB_LOAD_FIELDS, fields.freeze)
|
|
45
|
+
set_const(klass, :DB_LOAD_DTO, strategy.dto)
|
|
46
|
+
set_const(
|
|
47
|
+
klass,
|
|
48
|
+
:DB_LOAD_NESTED_FILTERS,
|
|
49
|
+
fields.map { _1.fields_filter ? [_1.fields_filter].freeze : nil }.freeze
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
body = fields.each_with_index.filter_map { |field, idx|
|
|
53
|
+
# the generic loop skips role-scoped fields when role is nil; calls
|
|
54
|
+
# with an actual role go through the generic fallback
|
|
55
|
+
next unless field.roles.empty?
|
|
56
|
+
field_code(field, idx, output, strategy.always_optional)
|
|
57
|
+
}.join("\n")
|
|
58
|
+
|
|
59
|
+
klass.send(:alias_method, :__generic_db_load, :db_load)
|
|
60
|
+
|
|
61
|
+
klass.class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
|
62
|
+
def db_load(obj, role: nil, only: nil, except: nil, fields_filters: nil)
|
|
63
|
+
if role || only || except || fields_filters || !obj.is_a?(::Hash)
|
|
64
|
+
return __generic_db_load(obj, role: role, only: only, except: except, fields_filters: fields_filters)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#{builder}
|
|
68
|
+
#{body}
|
|
69
|
+
#{finisher}
|
|
70
|
+
end
|
|
71
|
+
RUBY
|
|
72
|
+
|
|
73
|
+
# keep the generated source inspectable for debugging
|
|
74
|
+
klass.instance_variable_set(:@compiled_db_load_source, body)
|
|
75
|
+
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def field_code(field, idx, output, always_optional)
|
|
82
|
+
cast_expr = "__dbl_cast(DB_LOAD_FIELDS[#{idx}], v, DB_LOAD_NESTED_FILTERS[#{idx}])"
|
|
83
|
+
guard = field.type.db_load_trusted_check("v")
|
|
84
|
+
|
|
85
|
+
value_expr = if guard == "true"
|
|
86
|
+
# identity type (any): nil also passes through unchanged, as in the
|
|
87
|
+
# generic loop where Any#db_load(nil) returns nil
|
|
88
|
+
"v"
|
|
89
|
+
elsif guard
|
|
90
|
+
if field.null
|
|
91
|
+
"v.nil? ? nil : (#{guard} ? v : #{cast_expr})"
|
|
92
|
+
else
|
|
93
|
+
"#{guard} ? v : #{cast_expr}"
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
if field.null
|
|
97
|
+
"v.nil? ? nil : #{cast_expr}"
|
|
98
|
+
else
|
|
99
|
+
cast_expr
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sym_key = field.from.inspect
|
|
104
|
+
str_key = field.from_as_str.inspect
|
|
105
|
+
|
|
106
|
+
missing_code = if field.optional || always_optional
|
|
107
|
+
assign(field, output, "DB_LOAD_FIELDS[#{idx}].default") if field.has_default?
|
|
108
|
+
else
|
|
109
|
+
<<~RUBY.chomp
|
|
110
|
+
raise ReeMapper::TypeError.new(
|
|
111
|
+
"is missing (required field)", DB_LOAD_FIELDS[#{idx}].location, [#{str_key}]
|
|
112
|
+
)
|
|
113
|
+
RUBY
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
code = +<<~RUBY
|
|
117
|
+
if obj.key?(#{sym_key})
|
|
118
|
+
v = obj[#{sym_key}]
|
|
119
|
+
#{assign(field, output, value_expr)}
|
|
120
|
+
elsif obj.key?(#{str_key})
|
|
121
|
+
v = obj[#{str_key}]
|
|
122
|
+
#{assign(field, output, value_expr)}
|
|
123
|
+
RUBY
|
|
124
|
+
|
|
125
|
+
if missing_code
|
|
126
|
+
code << "else\n #{missing_code}\n"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
code << "end\n"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def assign(field, output, value_expr)
|
|
133
|
+
if output.is_a?(ReeMapper::ObjectOutput)
|
|
134
|
+
"acc.instance_variable_set(#{field.name_as_instance_var_name.inspect}, #{value_expr})"
|
|
135
|
+
else
|
|
136
|
+
"acc[#{field.name.inspect}] = #{value_expr}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def set_const(klass, name, value)
|
|
141
|
+
klass.send(:remove_const, name) if klass.const_defined?(name, false)
|
|
142
|
+
klass.const_set(name, value)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -139,9 +139,34 @@ class ReeMapper::Mapper
|
|
|
139
139
|
def prepare_dto
|
|
140
140
|
raise ReeMapper::ArgumentError, "mapper should contain at least one field" if fields.empty?
|
|
141
141
|
strategies.each { _1.prepare_dto(fields.keys) }
|
|
142
|
+
|
|
143
|
+
if ReeMapper.db_load_performance_mode? && find_strategy(:db_load)
|
|
144
|
+
ReeMapper::DbLoadCompiler.compile(self)
|
|
145
|
+
end
|
|
146
|
+
|
|
142
147
|
nil
|
|
143
148
|
end
|
|
144
149
|
|
|
150
|
+
# DbLoadCompiler helpers below are deliberately contract-free: they run
|
|
151
|
+
# per field per row on the hot path.
|
|
152
|
+
|
|
153
|
+
# Guard expression for the wrapped type, or nil when this mapper's db_load
|
|
154
|
+
# must always be called (composite mappers, wrappers, custom types)
|
|
155
|
+
def db_load_trusted_check(var)
|
|
156
|
+
return nil if @type.nil?
|
|
157
|
+
@type.respond_to?(:db_load_trusted_check) ? @type.db_load_trusted_check(var) : nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Slow path of compiled db_load: full cast with the same error decoration
|
|
161
|
+
# as the generic per-field loop
|
|
162
|
+
def __dbl_cast(field, value, nested_filters)
|
|
163
|
+
field.type.db_load(value, role: nil, fields_filters: nested_filters)
|
|
164
|
+
rescue ReeMapper::ErrorWithLocation => e
|
|
165
|
+
e.prepend_field_name(field.name_as_str)
|
|
166
|
+
e.location ||= field.location
|
|
167
|
+
raise e
|
|
168
|
+
end
|
|
169
|
+
|
|
145
170
|
contract(Symbol => Nilor[ReeMapper::MapperStrategy])
|
|
146
171
|
def find_strategy(strategy_method)
|
|
147
172
|
strategies.detect { _1.method == strategy_method }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ReeMapper::MapperStrategy
|
|
4
|
-
attr_reader :method, :always_optional
|
|
4
|
+
attr_reader :method, :always_optional, :output
|
|
5
5
|
|
|
6
6
|
contract(Symbol, Class, Bool => Any)
|
|
7
7
|
def initialize(method:, dto:, always_optional:)
|
|
@@ -61,8 +61,6 @@ class ReeMapper::MapperStrategy
|
|
|
61
61
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
|
-
attr_reader :output
|
|
65
|
-
|
|
66
64
|
def build_output(dto)
|
|
67
65
|
if dto == Hash || (defined?(OpenStruct) && dto == OpenStruct)
|
|
68
66
|
ReeMapper::HashOutput.new(dto)
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
class ReeMapper::AbstractType
|
|
2
|
+
# Ruby expression (as source string) checking that `var` already holds a
|
|
3
|
+
# correctly-typed DB value, so db_load may return it as-is. Types that
|
|
4
|
+
# cannot be skipped (custom types, enums, pg wrappers) return nil. The
|
|
5
|
+
# expression must accept exactly the values for which this type's db_load
|
|
6
|
+
# is an identity, or db_load semantics change in performance mode.
|
|
7
|
+
def db_load_trusted_check(var)
|
|
8
|
+
nil
|
|
9
|
+
end
|
|
10
|
+
|
|
2
11
|
private
|
|
3
12
|
|
|
4
13
|
def truncate(str, limit = 180)
|
|
@@ -4,6 +4,10 @@ class ReeMapper::Bool < ReeMapper::AbstractType
|
|
|
4
4
|
TRUE_CAST_VALUES = ['1', 'true', 'on', 1, true].freeze
|
|
5
5
|
FALSE_CAST_VALUES = ['0', 'false', 'off', 0, false].freeze
|
|
6
6
|
|
|
7
|
+
def db_load_trusted_check(var)
|
|
8
|
+
"(true.equal?(#{var}) || false.equal?(#{var}))"
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
contract(Any => Bool).throws(ReeMapper::TypeError)
|
|
8
12
|
def serialize(value)
|
|
9
13
|
if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
require 'date'
|
|
4
4
|
|
|
5
5
|
class ReeMapper::Date < ReeMapper::AbstractType
|
|
6
|
+
# instance_of?, not is_a?: DateTime < Date, and cast converts DateTime
|
|
7
|
+
def db_load_trusted_check(var)
|
|
8
|
+
"#{var}.instance_of?(::Date)"
|
|
9
|
+
end
|
|
10
|
+
|
|
6
11
|
contract(Any => Date).throws(ReeMapper::TypeError)
|
|
7
12
|
def serialize(value)
|
|
8
13
|
if value.class == Date
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class ReeMapper::Rational < ReeMapper::AbstractType
|
|
4
|
+
def db_load_trusted_check(var)
|
|
5
|
+
"#{var}.is_a?(::Rational)"
|
|
6
|
+
end
|
|
7
|
+
|
|
4
8
|
contract(Any => Rational).throws(ReeMapper::TypeError)
|
|
5
9
|
def serialize(value)
|
|
6
10
|
if value.is_a?(Rational)
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
require 'time'
|
|
4
4
|
|
|
5
5
|
class ReeMapper::Time < ReeMapper::AbstractType
|
|
6
|
+
# instance_of?: cast accepts subclasses (e.g. Sequel::SQLTime) only via parse paths
|
|
7
|
+
def db_load_trusted_check(var)
|
|
8
|
+
"#{var}.instance_of?(::Time)"
|
|
9
|
+
end
|
|
10
|
+
|
|
6
11
|
contract(Any => Time).throws(ReeMapper::TypeError)
|
|
7
12
|
def serialize(value)
|
|
8
13
|
if value.class == Time
|
|
@@ -47,12 +47,18 @@ module ReeMapper
|
|
|
47
47
|
|
|
48
48
|
require_relative 'ree_mapper/mapper_strategy'
|
|
49
49
|
require_relative 'ree_mapper/mapper'
|
|
50
|
+
require_relative 'ree_mapper/db_load_compiler'
|
|
50
51
|
require_relative 'ree_mapper/mapper_factory_proxy'
|
|
51
52
|
require_relative 'ree_mapper/mapper_factory'
|
|
52
53
|
|
|
53
54
|
require_relative 'ree_mapper/default_factory'
|
|
54
55
|
require_relative 'ree_mapper/dsl'
|
|
55
56
|
|
|
57
|
+
# Defensive read: an older ree gem without the flag means the mode is off
|
|
58
|
+
def self.db_load_performance_mode?
|
|
59
|
+
Ree.respond_to?(:db_load_performance_mode?) && Ree.db_load_performance_mode?
|
|
60
|
+
end
|
|
61
|
+
|
|
56
62
|
def self.get_mapper_factory(mod)
|
|
57
63
|
if !mod.is_a?(Module)
|
|
58
64
|
raise Ree::Error.new("module should be provided", :invalid_dsl_usage)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'benchmark'
|
|
3
|
+
|
|
4
|
+
package_require "ree_mapper"
|
|
5
|
+
package_require "ree_dto/entity_dsl"
|
|
6
|
+
|
|
7
|
+
# Run with:
|
|
8
|
+
# REE_BENCHMARK=1 bundle exec rspec ... db_load_benchmark_spec.rb
|
|
9
|
+
# Contracts state is fixed at method definition time, so measure both setups
|
|
10
|
+
# in separate processes: a plain run (contracts on) and NO_CONTRACTS=1.
|
|
11
|
+
RSpec.public_send(ENV["REE_BENCHMARK"] ? :describe : :xdescribe, 'db_load performance mode benchmark') do
|
|
12
|
+
link :build_mapper_factory, from: :ree_mapper
|
|
13
|
+
link :build_mapper_strategy, from: :ree_mapper
|
|
14
|
+
|
|
15
|
+
after { Ree.disable_db_load_performance_mode }
|
|
16
|
+
|
|
17
|
+
ROWS_COUNT = 10_000
|
|
18
|
+
REPS = 10
|
|
19
|
+
|
|
20
|
+
class DbLoadBenchDto
|
|
21
|
+
include ReeDto::DSL
|
|
22
|
+
|
|
23
|
+
build_dto do
|
|
24
|
+
field :id, Integer
|
|
25
|
+
field :num2, Integer
|
|
26
|
+
field :num3, Integer
|
|
27
|
+
field :num4, Integer
|
|
28
|
+
field :num5, Integer
|
|
29
|
+
field :name, String
|
|
30
|
+
field :s2, String
|
|
31
|
+
field :s3, String
|
|
32
|
+
field :s4, String
|
|
33
|
+
field :s5, String
|
|
34
|
+
field :active, Bool
|
|
35
|
+
field :flag2, Bool
|
|
36
|
+
field :score, Float
|
|
37
|
+
field :score2, Float
|
|
38
|
+
field :born_on, Date
|
|
39
|
+
field :born2, Date
|
|
40
|
+
field :seen_at, Time
|
|
41
|
+
field :seen2, Time
|
|
42
|
+
field :registered_at, DateTime
|
|
43
|
+
field :payload, Any
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class DbLoadBenchEntity
|
|
48
|
+
include ReeDto::EntityDSL
|
|
49
|
+
|
|
50
|
+
properties(
|
|
51
|
+
id: Nilor[Integer], num2: Nilor[Integer], num3: Nilor[Integer],
|
|
52
|
+
num4: Nilor[Integer], num5: Nilor[Integer],
|
|
53
|
+
name: Nilor[String], s2: Nilor[String], s3: Nilor[String],
|
|
54
|
+
s4: Nilor[String], s5: Nilor[String],
|
|
55
|
+
active: Nilor[Bool], flag2: Nilor[Bool],
|
|
56
|
+
score: Nilor[Float], score2: Nilor[Float],
|
|
57
|
+
born_on: Nilor[Date], born2: Nilor[Date],
|
|
58
|
+
seen_at: Nilor[Time], seen2: Nilor[Time],
|
|
59
|
+
registered_at: Nilor[DateTime], payload: Any,
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
wide_blk = Proc.new do
|
|
64
|
+
integer :id
|
|
65
|
+
integer :num2
|
|
66
|
+
integer :num3
|
|
67
|
+
integer :num4
|
|
68
|
+
integer :num5
|
|
69
|
+
string :name
|
|
70
|
+
string :s2
|
|
71
|
+
string :s3
|
|
72
|
+
string :s4
|
|
73
|
+
string :s5
|
|
74
|
+
bool :active
|
|
75
|
+
bool :flag2
|
|
76
|
+
float :score
|
|
77
|
+
float :score2
|
|
78
|
+
date :born_on
|
|
79
|
+
date :born2
|
|
80
|
+
time :seen_at
|
|
81
|
+
time :seen2
|
|
82
|
+
date_time :registered_at
|
|
83
|
+
any :payload
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
mixed_blk = Proc.new do
|
|
87
|
+
integer :id
|
|
88
|
+
string :name
|
|
89
|
+
bool :active
|
|
90
|
+
date :born_on
|
|
91
|
+
hash :meta, null: true do
|
|
92
|
+
date :day
|
|
93
|
+
integer :num
|
|
94
|
+
end
|
|
95
|
+
array :nums, integer, null: true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_mapper_with_mode(mode_on, dto, blk)
|
|
99
|
+
mode_on ? Ree.enable_db_load_performance_mode : Ree.disable_db_load_performance_mode
|
|
100
|
+
|
|
101
|
+
build_mapper_factory(
|
|
102
|
+
strategies: [
|
|
103
|
+
build_mapper_strategy(method: :db_load, dto: dto, always_optional: true)
|
|
104
|
+
]
|
|
105
|
+
).call.use(:db_load, dto: dto, &blk)
|
|
106
|
+
ensure
|
|
107
|
+
Ree.disable_db_load_performance_mode
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def wide_rows
|
|
111
|
+
Array.new(ROWS_COUNT) do |i|
|
|
112
|
+
{
|
|
113
|
+
id: i, num2: i, num3: i, num4: i, num5: i,
|
|
114
|
+
name: "name-#{i}", s2: "a", s3: "b", s4: "c", s5: "d",
|
|
115
|
+
active: true, flag2: false,
|
|
116
|
+
score: 1.5, score2: 2.5,
|
|
117
|
+
born_on: Date.new(2020, 1, 1), born2: Date.new(2021, 2, 2),
|
|
118
|
+
seen_at: Time.at(i).utc, seen2: Time.at(i * 2).utc,
|
|
119
|
+
registered_at: DateTime.new(2020, 1, 1, 12, 0, 0),
|
|
120
|
+
payload: { x: i },
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def mixed_rows
|
|
126
|
+
Array.new(ROWS_COUNT) do |i|
|
|
127
|
+
{
|
|
128
|
+
id: i, name: "name-#{i}", active: true, born_on: Date.new(2020, 1, 1),
|
|
129
|
+
meta: { day: "2020-01-01", num: i }, # string date: the JSONB guard-fallback path
|
|
130
|
+
nums: [1, 2, 3],
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def report_alloc(label, rows, mapper)
|
|
136
|
+
GC.start
|
|
137
|
+
allocated_before = GC.stat(:total_allocated_objects)
|
|
138
|
+
gc_before = GC.count
|
|
139
|
+
rows.each { mapper.db_load(_1) }
|
|
140
|
+
allocated = GC.stat(:total_allocated_objects) - allocated_before
|
|
141
|
+
gc_runs = GC.count - gc_before
|
|
142
|
+
puts format("%-28s %10d objects, %3d GC runs per %d rows", label, allocated, gc_runs, rows.size)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'wide primitive row: generic vs compiled' do
|
|
146
|
+
rows = wide_rows
|
|
147
|
+
|
|
148
|
+
pairs = {
|
|
149
|
+
"hash dto" => [
|
|
150
|
+
build_mapper_with_mode(false, Hash, wide_blk),
|
|
151
|
+
build_mapper_with_mode(true, Hash, wide_blk),
|
|
152
|
+
],
|
|
153
|
+
"ree_dto" => [
|
|
154
|
+
build_mapper_with_mode(false, DbLoadBenchDto, wide_blk),
|
|
155
|
+
build_mapper_with_mode(true, DbLoadBenchDto, wide_blk),
|
|
156
|
+
],
|
|
157
|
+
"entity_dsl" => [
|
|
158
|
+
build_mapper_with_mode(false, DbLoadBenchEntity, wide_blk),
|
|
159
|
+
build_mapper_with_mode(true, DbLoadBenchEntity, wide_blk),
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
puts "\ncontracts: #{Ree::Contracts.no_contracts? ? 'OFF (NO_CONTRACTS)' : 'ON'}"
|
|
164
|
+
|
|
165
|
+
Benchmark.bmbm do |x|
|
|
166
|
+
pairs.each do |name, (generic, compiled)|
|
|
167
|
+
x.report("generic #{name}") { REPS.times { rows.each { generic.db_load(_1) } } }
|
|
168
|
+
x.report("compiled #{name}") { REPS.times { rows.each { compiled.db_load(_1) } } }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
puts "\nallocations:"
|
|
173
|
+
pairs.each do |name, (generic, compiled)|
|
|
174
|
+
report_alloc("generic #{name}", rows, generic)
|
|
175
|
+
report_alloc("compiled #{name}", rows, compiled)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'mixed row with nested hash and array: generic vs compiled' do
|
|
180
|
+
rows = mixed_rows
|
|
181
|
+
generic = build_mapper_with_mode(false, DbLoadBenchDto, mixed_blk)
|
|
182
|
+
compiled = build_mapper_with_mode(true, DbLoadBenchDto, mixed_blk)
|
|
183
|
+
|
|
184
|
+
Benchmark.bmbm do |x|
|
|
185
|
+
x.report("generic mixed") { REPS.times { rows.each { generic.db_load(_1) } } }
|
|
186
|
+
x.report("compiled mixed") { REPS.times { rows.each { compiled.db_load(_1) } } }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
puts "\nallocations:"
|
|
190
|
+
report_alloc("generic mixed", rows, generic)
|
|
191
|
+
report_alloc("compiled mixed", rows, compiled)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Results (2026-08-16, Ruby 4.0.1, 10_000 rows x 10 reps, real seconds):
|
|
196
|
+
#
|
|
197
|
+
# contracts: ON
|
|
198
|
+
# generic compiled speedup
|
|
199
|
+
# wide (20 fields) hash 60.53s 0.41s ~149x
|
|
200
|
+
# wide ree_dto 74.76s 0.43s ~174x
|
|
201
|
+
# wide entity_dsl 63.78s 0.36s ~175x
|
|
202
|
+
# mixed (jsonb-like) 37.23s 5.43s ~6.9x
|
|
203
|
+
#
|
|
204
|
+
# allocations per 10_000 rows:
|
|
205
|
+
# wide hash 15_820_000 -> 10_000 wide ree_dto 18_350_000 -> 20_000
|
|
206
|
+
# wide entity_dsl 15_820_000 -> 10_000 mixed 8_930_000 -> 1_190_000
|
|
207
|
+
# GC runs: 40-46 -> 0 (wide), 72 -> 9 (mixed)
|
|
208
|
+
#
|
|
209
|
+
# contracts: OFF (NO_CONTRACTS=1)
|
|
210
|
+
# generic compiled speedup
|
|
211
|
+
# wide (20 fields) hash 2.54s 0.42s ~6.0x
|
|
212
|
+
# wide ree_dto 2.77s 0.43s ~6.4x
|
|
213
|
+
# wide entity_dsl 2.43s 0.39s ~6.2x
|
|
214
|
+
# mixed (jsonb-like) 2.52s 1.21s ~2.1x
|
|
215
|
+
#
|
|
216
|
+
# allocations per 10_000 rows:
|
|
217
|
+
# wide hash 220_000 -> 10_000 wide ree_dto 260_000 -> 20_000
|
|
218
|
+
# wide entity_dsl 220_000 -> 10_000 mixed 360_000 -> 200_000
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
package_require "ree_mapper"
|
|
3
|
+
package_require "ree_dto/entity_dsl"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "db_load performance mode" do
|
|
6
|
+
link :build_mapper_factory, from: :ree_mapper
|
|
7
|
+
link :build_mapper_strategy, from: :ree_mapper
|
|
8
|
+
|
|
9
|
+
after do
|
|
10
|
+
Ree.disable_db_load_performance_mode
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class PerfModeDto
|
|
14
|
+
include ReeDto::DSL
|
|
15
|
+
|
|
16
|
+
build_dto do
|
|
17
|
+
field :id, Integer
|
|
18
|
+
field :name, String
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class PerfModeEntity
|
|
23
|
+
include ReeDto::EntityDSL
|
|
24
|
+
|
|
25
|
+
properties(
|
|
26
|
+
id: Nilor[Integer],
|
|
27
|
+
name: Nilor[String],
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_factory(dto:, always_optional: true)
|
|
32
|
+
build_mapper_factory(
|
|
33
|
+
strategies: [
|
|
34
|
+
build_mapper_strategy(method: :db_load, dto: dto, always_optional: always_optional)
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# builds the same mapper twice: with the mode off (generic path) and on
|
|
40
|
+
# (compiled path), so examples can assert identical behavior
|
|
41
|
+
def build_pair(dto: Hash, always_optional: true, register_as: nil, &blk)
|
|
42
|
+
Ree.disable_db_load_performance_mode
|
|
43
|
+
generic = build_factory(dto: dto, always_optional: always_optional)
|
|
44
|
+
.call(register_as: register_as).use(:db_load, dto: dto, &blk)
|
|
45
|
+
|
|
46
|
+
Ree.enable_db_load_performance_mode
|
|
47
|
+
compiled = build_factory(dto: dto, always_optional: always_optional)
|
|
48
|
+
.call(register_as: register_as).use(:db_load, dto: dto, &blk)
|
|
49
|
+
Ree.disable_db_load_performance_mode
|
|
50
|
+
|
|
51
|
+
[generic, compiled]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def expect_same_result(generic, compiled, input, **opts)
|
|
55
|
+
generic_result = begin; generic.db_load(input, **opts); rescue => e; e; end
|
|
56
|
+
compiled_result = begin; compiled.db_load(input, **opts); rescue => e; e; end
|
|
57
|
+
|
|
58
|
+
if generic_result.is_a?(Exception)
|
|
59
|
+
expect(compiled_result.class).to eq(generic_result.class)
|
|
60
|
+
expect(compiled_result.message).to eq(generic_result.message)
|
|
61
|
+
else
|
|
62
|
+
expect(compiled_result).to eq(generic_result)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
compiled_result
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
wide_blk = Proc.new do
|
|
69
|
+
integer :id
|
|
70
|
+
string :name
|
|
71
|
+
bool :active
|
|
72
|
+
float :score, null: true
|
|
73
|
+
date :born_on, null: true
|
|
74
|
+
time :seen_at, null: true
|
|
75
|
+
date_time :registered_at, null: true
|
|
76
|
+
rational :ratio, null: true
|
|
77
|
+
any :payload, null: true
|
|
78
|
+
integer :renamed, from: :src
|
|
79
|
+
string? :note, default: "-"
|
|
80
|
+
integer? :maybe
|
|
81
|
+
integer :admin_count, role: :admin
|
|
82
|
+
hash :meta, null: true do
|
|
83
|
+
date :day
|
|
84
|
+
integer :num
|
|
85
|
+
end
|
|
86
|
+
array :nums, integer, null: true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def wide_row
|
|
90
|
+
{
|
|
91
|
+
id: 1,
|
|
92
|
+
name: "n",
|
|
93
|
+
active: true,
|
|
94
|
+
score: 1.5,
|
|
95
|
+
born_on: Date.new(2020, 1, 1),
|
|
96
|
+
seen_at: Time.at(0).utc,
|
|
97
|
+
registered_at: DateTime.new(2020, 1, 1, 12, 0, 0),
|
|
98
|
+
ratio: Rational(1, 2),
|
|
99
|
+
payload: { x: 1 },
|
|
100
|
+
src: 5,
|
|
101
|
+
note: "hi",
|
|
102
|
+
maybe: 2,
|
|
103
|
+
meta: { day: "2020-01-01", num: 3 },
|
|
104
|
+
nums: [1, 2],
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "compiles db_load when the mode is on and keeps the generic fallback" do
|
|
109
|
+
_generic, compiled = build_pair(&wide_blk)
|
|
110
|
+
|
|
111
|
+
expect(compiled.class.method_defined?(:__generic_db_load, false)).to be true
|
|
112
|
+
expect(compiled.class.const_defined?(:DB_LOAD_FIELDS, false)).to be true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "does not compile when the mode is off" do
|
|
116
|
+
generic, _compiled = build_pair(&wide_blk)
|
|
117
|
+
|
|
118
|
+
expect(generic.class.method_defined?(:__generic_db_load, false)).to be false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
context "parity with the generic path" do
|
|
122
|
+
it "loads a full row identically" do
|
|
123
|
+
generic, compiled = build_pair(&wide_blk)
|
|
124
|
+
result = expect_same_result(generic, compiled, wide_row)
|
|
125
|
+
|
|
126
|
+
expect(result[:id]).to eq(1)
|
|
127
|
+
expect(result[:renamed]).to eq(5)
|
|
128
|
+
expect(result[:meta][:day]).to eq(Date.new(2020, 1, 1)) # JSONB-style string is still cast
|
|
129
|
+
expect(result[:nums]).to eq([1, 2])
|
|
130
|
+
expect(result.key?(:admin_count)).to be false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "still coerces values that fail the type guard" do
|
|
134
|
+
generic, compiled = build_pair(&wide_blk)
|
|
135
|
+
result = expect_same_result(generic, compiled, wide_row.merge(id: "5", score: 2))
|
|
136
|
+
|
|
137
|
+
expect(result[:id]).to eq(5)
|
|
138
|
+
expect(result[:score]).to eq(2.0)
|
|
139
|
+
expect(result[:score]).to be_a(Float)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "raises the same error for nil in a null: false field" do
|
|
143
|
+
generic, compiled = build_pair(&wide_blk)
|
|
144
|
+
error = expect_same_result(generic, compiled, wide_row.merge(name: nil))
|
|
145
|
+
|
|
146
|
+
expect(error).to be_a(ReeMapper::TypeError)
|
|
147
|
+
expect(error.message).to include("`name`")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "raises the same error for garbage that fails coercion" do
|
|
151
|
+
generic, compiled = build_pair(&wide_blk)
|
|
152
|
+
error = expect_same_result(generic, compiled, wide_row.merge(id: "abc"))
|
|
153
|
+
|
|
154
|
+
expect(error).to be_a(ReeMapper::CoercionError)
|
|
155
|
+
expect(error.message).to include("`id`")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "raises the same error from a nested mapper with the field path" do
|
|
159
|
+
generic, compiled = build_pair(&wide_blk)
|
|
160
|
+
error = expect_same_result(generic, compiled, wide_row.merge(meta: { day: "not a date", num: 1 }))
|
|
161
|
+
|
|
162
|
+
expect(error).to be_a(ReeMapper::CoercionError)
|
|
163
|
+
expect(error.message).to include("`meta[day]`")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "assigns nil without casting for null: true fields" do
|
|
167
|
+
generic, compiled = build_pair(&wide_blk)
|
|
168
|
+
result = expect_same_result(
|
|
169
|
+
generic, compiled,
|
|
170
|
+
wide_row.merge(score: nil, born_on: nil, meta: nil, nums: nil, payload: nil)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
expect(result[:score]).to be_nil
|
|
174
|
+
expect(result[:meta]).to be_nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "applies defaults and skips missing optional fields" do
|
|
178
|
+
generic, compiled = build_pair(&wide_blk)
|
|
179
|
+
row = wide_row
|
|
180
|
+
row.delete(:note)
|
|
181
|
+
row.delete(:maybe)
|
|
182
|
+
result = expect_same_result(generic, compiled, row)
|
|
183
|
+
|
|
184
|
+
expect(result[:note]).to eq("-")
|
|
185
|
+
expect(result.key?(:maybe)).to be false
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "skips missing fields when the strategy is always_optional" do
|
|
189
|
+
generic, compiled = build_pair(&wide_blk)
|
|
190
|
+
result = expect_same_result(generic, compiled, { id: 1 })
|
|
191
|
+
|
|
192
|
+
expect(result).to eq({ id: 1, note: "-" })
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "raises for missing required fields when the strategy is not always_optional" do
|
|
196
|
+
generic, compiled = build_pair(always_optional: false) do
|
|
197
|
+
integer :id
|
|
198
|
+
string? :name
|
|
199
|
+
end
|
|
200
|
+
error = expect_same_result(generic, compiled, { name: "n" })
|
|
201
|
+
|
|
202
|
+
expect(error).to be_a(ReeMapper::TypeError)
|
|
203
|
+
expect(error.message).to include("`id` is missing (required field)")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "supports string-keyed input" do
|
|
207
|
+
generic, compiled = build_pair(&wide_blk)
|
|
208
|
+
result = expect_same_result(generic, compiled, { "id" => 1, "name" => "n" })
|
|
209
|
+
|
|
210
|
+
expect(result[:id]).to eq(1)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "falls back to the generic path for non-Hash input" do
|
|
214
|
+
generic, compiled = build_pair(&wide_blk)
|
|
215
|
+
input = OpenStruct.new(id: 1, name: "n")
|
|
216
|
+
result = expect_same_result(generic, compiled, input)
|
|
217
|
+
|
|
218
|
+
expect(result[:id]).to eq(1)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "falls back to the generic path for role calls" do
|
|
222
|
+
generic, compiled = build_pair(&wide_blk)
|
|
223
|
+
result = expect_same_result(generic, compiled, wide_row.merge(admin_count: 7), role: :admin)
|
|
224
|
+
|
|
225
|
+
expect(result[:admin_count]).to eq(7)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "falls back to the generic path for only/except calls" do
|
|
229
|
+
generic, compiled = build_pair(&wide_blk)
|
|
230
|
+
result = expect_same_result(generic, compiled, wide_row, only: [:id, :name])
|
|
231
|
+
|
|
232
|
+
expect(result.keys).to contain_exactly(:id, :name)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "respects field-level only filters on nested mappers" do
|
|
236
|
+
generic, compiled = build_pair do
|
|
237
|
+
integer :id
|
|
238
|
+
hash :meta, null: true, only: [:num] do
|
|
239
|
+
date :day, null: true
|
|
240
|
+
integer :num
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
result = expect_same_result(generic, compiled, { id: 1, meta: { day: "2020-01-01", num: 3 } })
|
|
244
|
+
|
|
245
|
+
expect(result[:meta]).to eq({ num: 3 })
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
context "dto outputs" do
|
|
250
|
+
it "builds ree_dto entities" do
|
|
251
|
+
generic, compiled = build_pair(dto: PerfModeDto) do
|
|
252
|
+
integer :id
|
|
253
|
+
string :name
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
generic_dto = generic.db_load({ id: 1, name: "n" })
|
|
257
|
+
compiled_dto = compiled.db_load({ id: 1, name: "n" })
|
|
258
|
+
|
|
259
|
+
expect(compiled_dto).to be_a(PerfModeDto)
|
|
260
|
+
expect(compiled_dto.id).to eq(generic_dto.id)
|
|
261
|
+
expect(compiled_dto.name).to eq(generic_dto.name)
|
|
262
|
+
expect(compiled_dto.attrs).to eq(generic_dto.attrs)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "builds entity_dsl objects via instance variables" do
|
|
266
|
+
generic, compiled = build_pair(dto: PerfModeEntity) do
|
|
267
|
+
integer :id
|
|
268
|
+
string :name
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
generic_entity = generic.db_load({ id: 1, name: "n" })
|
|
272
|
+
compiled_entity = compiled.db_load({ id: 1, name: "n" })
|
|
273
|
+
|
|
274
|
+
expect(compiled_entity).to be_a(PerfModeEntity)
|
|
275
|
+
expect(compiled_entity.id).to eq(generic_entity.id)
|
|
276
|
+
expect(compiled_entity.name).to eq(generic_entity.name)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it "keeps the generic path for struct dto" do
|
|
280
|
+
Ree.enable_db_load_performance_mode
|
|
281
|
+
mapper = build_factory(dto: Struct).call.use(:db_load, dto: Struct) do
|
|
282
|
+
integer :id
|
|
283
|
+
end
|
|
284
|
+
Ree.disable_db_load_performance_mode
|
|
285
|
+
|
|
286
|
+
expect(mapper.class.method_defined?(:__generic_db_load, false)).to be false
|
|
287
|
+
expect(mapper.db_load({ id: 1 }).id).to eq(1)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
context "registered (frozen) mappers" do
|
|
292
|
+
it "compiles mappers registered via register_as" do
|
|
293
|
+
generic, compiled = build_pair(register_as: :perf_mode_frozen, &wide_blk)
|
|
294
|
+
|
|
295
|
+
expect(compiled.frozen?).to be true
|
|
296
|
+
expect(compiled.class.method_defined?(:__generic_db_load, false)).to be true
|
|
297
|
+
expect_same_result(generic, compiled, wide_row)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
describe "guard/cast identity invariant" do
|
|
302
|
+
{
|
|
303
|
+
ReeMapper::Integer => [1, -5, 10**20],
|
|
304
|
+
ReeMapper::String => ["s", ""],
|
|
305
|
+
ReeMapper::Bool => [true, false],
|
|
306
|
+
ReeMapper::Float => [1.5, -0.0],
|
|
307
|
+
ReeMapper::Date => [Date.new(2020, 1, 1)],
|
|
308
|
+
ReeMapper::Time => [Time.at(0).utc],
|
|
309
|
+
ReeMapper::DateTime => [DateTime.new(2020, 1, 1)],
|
|
310
|
+
ReeMapper::Rational => [Rational(1, 2)],
|
|
311
|
+
ReeMapper::Any => [1, "s", nil, [1], { a: 1 }],
|
|
312
|
+
}.each do |type_class, values|
|
|
313
|
+
it "#{type_class}: guard implies db_load is identity" do
|
|
314
|
+
type = type_class.new
|
|
315
|
+
check = type.db_load_trusted_check("v")
|
|
316
|
+
|
|
317
|
+
values.each do |v|
|
|
318
|
+
expect(eval(check)).to be(true), "guard rejected #{v.inspect} for #{type_class}"
|
|
319
|
+
expect(type.db_load(v)).to equal(v)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it "guards reject values that db_load would convert" do
|
|
325
|
+
{
|
|
326
|
+
ReeMapper::Integer.new => "5",
|
|
327
|
+
ReeMapper::Float.new => 2,
|
|
328
|
+
ReeMapper::Date.new => DateTime.new(2020, 1, 1),
|
|
329
|
+
ReeMapper::Time.new => DateTime.new(2020, 1, 1),
|
|
330
|
+
ReeMapper::DateTime.new => Time.at(0).utc,
|
|
331
|
+
ReeMapper::Rational.new => "1/2",
|
|
332
|
+
ReeMapper::Bool.new => 1,
|
|
333
|
+
}.each do |type, v|
|
|
334
|
+
check = type.db_load_trusted_check("v")
|
|
335
|
+
expect(eval(check)).to be(false), "guard accepted #{v.inspect} for #{type.class}"
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
data/lib/ree_lib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ree_lib
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ruslan Gatiyatov
|
|
@@ -303,6 +303,7 @@ files:
|
|
|
303
303
|
- lib/ree_lib/packages/ree_dao/package/ree_dao/thread_parents.rb
|
|
304
304
|
- lib/ree_lib/packages/ree_dao/package/ree_dao/wrappers/pg_array.rb
|
|
305
305
|
- lib/ree_lib/packages/ree_dao/package/ree_dao/wrappers/pg_jsonb.rb
|
|
306
|
+
- lib/ree_lib/packages/ree_dao/spec/ree_dao/db_load_performance_mode_spec.rb
|
|
306
307
|
- lib/ree_lib/packages/ree_dao/spec/ree_dao/functions/build_dto_spec.rb
|
|
307
308
|
- lib/ree_lib/packages/ree_dao/spec/ree_dao/functions/build_pg_connection_spec.rb
|
|
308
309
|
- lib/ree_lib/packages/ree_dao/spec/ree_dao/functions/build_sqlite_connection/max_connections_spec.rb
|
|
@@ -624,6 +625,7 @@ files:
|
|
|
624
625
|
- lib/ree_lib/packages/ree_dto/package/ree_dto/entity_dsl.rb
|
|
625
626
|
- lib/ree_lib/packages/ree_dto/package/ree_dto/functions/build_dto.rb
|
|
626
627
|
- lib/ree_lib/packages/ree_dto/package/ree_dto/functions/build_dto_collection_class.rb
|
|
628
|
+
- lib/ree_lib/packages/ree_dto/spec/ree_dto/build_from_attrs_spec.rb
|
|
627
629
|
- lib/ree_lib/packages/ree_dto/spec/ree_dto/dsl_spec.rb
|
|
628
630
|
- lib/ree_lib/packages/ree_dto/spec/ree_dto/entity_dsl_spec.rb
|
|
629
631
|
- lib/ree_lib/packages/ree_dto/spec/spec_helper.rb
|
|
@@ -763,6 +765,7 @@ files:
|
|
|
763
765
|
- lib/ree_lib/packages/ree_mapper/.rspec
|
|
764
766
|
- lib/ree_lib/packages/ree_mapper/bin/console
|
|
765
767
|
- lib/ree_lib/packages/ree_mapper/package/ree_mapper.rb
|
|
768
|
+
- lib/ree_lib/packages/ree_mapper/package/ree_mapper/db_load_compiler.rb
|
|
766
769
|
- lib/ree_lib/packages/ree_mapper/package/ree_mapper/default_factory.rb
|
|
767
770
|
- lib/ree_lib/packages/ree_mapper/package/ree_mapper/dsl.rb
|
|
768
771
|
- lib/ree_lib/packages/ree_mapper/package/ree_mapper/errors/argument_error.rb
|
|
@@ -797,7 +800,9 @@ files:
|
|
|
797
800
|
- lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/time.rb
|
|
798
801
|
- lib/ree_lib/packages/ree_mapper/package/ree_mapper/wrappers/abstract_wrapper.rb
|
|
799
802
|
- lib/ree_lib/packages/ree_mapper/package/ree_mapper/wrappers/array.rb
|
|
803
|
+
- lib/ree_lib/packages/ree_mapper/spec/ree_mapper/benchmarks/db_load_benchmark_spec.rb
|
|
800
804
|
- lib/ree_lib/packages/ree_mapper/spec/ree_mapper/benchmarks/mapper_benchmark_spec.rb
|
|
805
|
+
- lib/ree_lib/packages/ree_mapper/spec/ree_mapper/db_load_performance_mode_spec.rb
|
|
801
806
|
- lib/ree_lib/packages/ree_mapper/spec/ree_mapper/dsl/dsl_spec.rb
|
|
802
807
|
- lib/ree_lib/packages/ree_mapper/spec/ree_mapper/functions/build_mapper_factory_spec.rb
|
|
803
808
|
- lib/ree_lib/packages/ree_mapper/spec/ree_mapper/functions/build_mapper_strategy_spec.rb
|