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.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/lib/ree_lib/packages/ree_dao/package/ree_dao/dataset_extensions.rb +15 -13
  4. data/lib/ree_lib/packages/ree_dao/spec/ree_dao/db_load_performance_mode_spec.rb +190 -0
  5. data/lib/ree_lib/packages/ree_dto/package/ree_dto/dto/dto_class_methods.rb +14 -0
  6. data/lib/ree_lib/packages/ree_dto/spec/ree_dto/build_from_attrs_spec.rb +48 -0
  7. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/db_load_compiler.rb +145 -0
  8. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/mapper.rb +25 -0
  9. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/mapper_strategy.rb +1 -3
  10. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/abstract_type.rb +9 -0
  11. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/any.rb +4 -0
  12. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/bool.rb +4 -0
  13. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/date.rb +5 -0
  14. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/date_time.rb +4 -0
  15. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/float.rb +4 -0
  16. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/integer.rb +4 -0
  17. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/rational.rb +4 -0
  18. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/string.rb +4 -0
  19. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/time.rb +5 -0
  20. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/wrappers/abstract_wrapper.rb +5 -0
  21. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper.rb +6 -0
  22. data/lib/ree_lib/packages/ree_mapper/spec/ree_mapper/benchmarks/db_load_benchmark_spec.rb +218 -0
  23. data/lib/ree_lib/packages/ree_mapper/spec/ree_mapper/db_load_performance_mode_spec.rb +339 -0
  24. data/lib/ree_lib/version.rb +1 -1
  25. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 237cf0a00005f59af6c0741f79302159d84433b60b63e401baa4ae95ab0dd2ae
4
- data.tar.gz: f9c00515a41e9d78fc2c3ac1785d5a8a21d1f4142461537299e4c1678d4a6b61
3
+ metadata.gz: 9602630839840f13c5b0be8226aea07ddabc2846b22ee90be4d2a2ba64e43aba
4
+ data.tar.gz: 3bb721b4ccecb67d709d850ca7e56357fe52350a96a1b817d33654a2eed6e759
5
5
  SHA512:
6
- metadata.gz: c712530b729aa1e48d53de393ee737b7605c233108940e78f09f4561c36b69c413136880f8b72f73f694d4a6ac5ee7e4010d2d46ff2dcdb30450ad1fd2bb9a56
7
- data.tar.gz: e148d3f589e0c2863a9b1958d903458407c1646f0fca6ca79c5fb0839893c0bc00059b4a80a9c0b45cf6710e4e9b6802f6a94ee9b8958fa485701164cd99a240
6
+ metadata.gz: 8afde36de03a451291b092d3449438de32ccd1f3bd5ea1356d1b487e0a91373e3ce9f7a1bd149913c26953274009244ca343c7e1a508aaab2f219b3391a3f190
7
+ data.tar.gz: b97d9425262c5c99d56c7d23c7069901676594060c40d93c57a9e07a846486bff0cf176c0d39fcff6f77edbdfa239af46261800a7eff74ccb964d2363bc2262d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../ree
3
3
  specs:
4
- ree (1.3.3)
4
+ ree (1.3.5)
5
5
  base64
6
6
  commander
7
7
  logger
@@ -9,7 +9,7 @@ PATH
9
9
  PATH
10
10
  remote: .
11
11
  specs:
12
- ree_lib (1.3.9)
12
+ ree_lib (1.3.10)
13
13
  bigdecimal
14
14
  binding_of_caller
15
15
  i18n
@@ -183,21 +183,23 @@ module ReeDao
183
183
  end
184
184
 
185
185
  def with_mapper(mapper)
186
- clone(
187
- schema_mapper: mapper || opts[:schema_mapper],
188
- ).with_row_proc(
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
- m = mapper || opts[:schema_mapper]
191
-
192
- if m
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)
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::Any < ReeMapper::AbstractType
4
+ def db_load_trusted_check(var)
5
+ "true"
6
+ end
7
+
4
8
  contract(Any => Any)
5
9
  def serialize(value)
6
10
  value
@@ -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
@@ -3,6 +3,10 @@
3
3
  require 'date'
4
4
 
5
5
  class ReeMapper::DateTime < ReeMapper::AbstractType
6
+ def db_load_trusted_check(var)
7
+ "#{var}.instance_of?(::DateTime)"
8
+ end
9
+
6
10
  contract(Any => DateTime).throws(ReeMapper::TypeError)
7
11
  def serialize(value)
8
12
  if value.class == DateTime
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::Float < ReeMapper::AbstractType
4
+ def db_load_trusted_check(var)
5
+ "#{var}.is_a?(::Float)"
6
+ end
7
+
4
8
  contract(Any => Float).throws(ReeMapper::TypeError)
5
9
  def serialize(value)
6
10
  if value.is_a?(Float)
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::Integer < ReeMapper::AbstractType
4
+ def db_load_trusted_check(var)
5
+ "#{var}.is_a?(::Integer)"
6
+ end
7
+
4
8
  contract(Any => Integer).throws(ReeMapper::TypeError)
5
9
  def serialize(value)
6
10
  if value.is_a? Integer
@@ -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)
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::String < ReeMapper::AbstractType
4
+ def db_load_trusted_check(var)
5
+ "#{var}.is_a?(::String)"
6
+ end
7
+
4
8
  contract(Any => String).throws(ReeMapper::TypeError)
5
9
  def serialize(value)
6
10
  if value.is_a? String
@@ -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
@@ -6,6 +6,11 @@ class ReeMapper::AbstractWrapper
6
6
  @subject = field
7
7
  end
8
8
 
9
+ # See ReeMapper::AbstractType#db_load_trusted_check. Wrappers always cast.
10
+ def db_load_trusted_check(var)
11
+ nil
12
+ end
13
+
9
14
  private
10
15
 
11
16
  def truncate(str, limit = 180)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReeLib
4
- VERSION = "1.3.9"
4
+ VERSION = "1.3.10"
5
5
  end
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.9
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