typed_eav 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +207 -0
  3. data/README.md +271 -61
  4. data/app/models/typed_eav/field/base.rb +90 -33
  5. data/app/models/typed_eav/field/currency.rb +43 -0
  6. data/app/models/typed_eav/field/file.rb +1 -1
  7. data/app/models/typed_eav/field/image.rb +1 -1
  8. data/app/models/typed_eav/field/reference.rb +11 -0
  9. data/app/models/typed_eav/option.rb +0 -8
  10. data/app/models/typed_eav/section.rb +11 -5
  11. data/app/models/typed_eav/value.rb +95 -32
  12. data/db/migrate/20260430000000_add_parent_scope_to_typed_eav_partitions.rb +1 -1
  13. data/db/migrate/20260712000000_enforce_parent_scope_invariant.rb +54 -0
  14. data/db/migrate/20260816000000_use_partial_covering_scalar_indexes.rb +198 -0
  15. data/lib/generators/typed_eav/scaffold/templates/controllers/typed_eav_controller.rb +1 -9
  16. data/lib/typed_eav/bulk_read.rb +41 -9
  17. data/lib/typed_eav/bulk_upsert.rb +141 -0
  18. data/lib/typed_eav/bulk_write.rb +65 -39
  19. data/lib/typed_eav/config.rb +19 -21
  20. data/lib/typed_eav/csv_mapper.rb +1 -1
  21. data/lib/typed_eav/engine.rb +16 -27
  22. data/lib/typed_eav/entity_query.rb +42 -8
  23. data/lib/typed_eav/event_dispatcher.rb +21 -30
  24. data/lib/typed_eav/field/typed_storage.rb +48 -0
  25. data/lib/typed_eav/field_deletion.rb +75 -0
  26. data/lib/typed_eav/filter_query.rb +9 -7
  27. data/lib/typed_eav/has_typed_eav/instance_methods.rb +11 -8
  28. data/lib/typed_eav/partition.rb +8 -3
  29. data/lib/typed_eav/query_builder.rb +17 -27
  30. data/lib/typed_eav/registry.rb +7 -8
  31. data/lib/typed_eav/schema_portability/import_index.rb +58 -0
  32. data/lib/typed_eav/schema_portability.rb +22 -25
  33. data/lib/typed_eav/version.rb +1 -1
  34. data/lib/typed_eav/versioning/subscriber.rb +25 -29
  35. data/lib/typed_eav/versioning.rb +59 -41
  36. data/lib/typed_eav.rb +2 -0
  37. metadata +19 -5
@@ -1,23 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypedEAV
4
- # Phase 04 versioning namespace. Houses the Subscriber that writes
5
- # TypedEAV::ValueVersion rows in response to Value lifecycle events
6
- # dispatched by EventDispatcher.
4
+ # Transactional versioning namespace. Houses the Subscriber that writes
5
+ # TypedEAV::ValueVersion rows from Value lifecycle callbacks.
7
6
  #
8
7
  # ## Architecture
9
8
  #
10
9
  # - TypedEAV::Versioning::Subscriber.call(value, change_type, context)
11
- # is conditionally registered with
12
- # EventDispatcher.register_internal_value_change at engine boot via
13
- # `TypedEAV::Versioning.register_if_enabled`, which is invoked from
14
- # the `config.after_initialize` block in lib/typed_eav/engine.rb.
15
- # When TypedEAV.config.versioning is false (default), the helper
16
- # returns early — no callable is added to the dispatcher chain.
17
- # When true, the subscriber registers and runs FIRST in the value-
18
- # change subscriber chain (slot 0 by `after_initialize` block
19
- # declaration order — Phase 07 will declare its matview block LATER
20
- # in the same engine to keep matview at slot ≥ 1).
10
+ # is conditionally installed on Value's transactional callbacks at engine
11
+ # boot via `TypedEAV::Versioning.register_if_enabled`. The version row
12
+ # therefore shares the source transaction.
21
13
  #
22
14
  # - The subscriber is gated by TWO checks at call time (both must
23
15
  # pass for a version row to be written):
@@ -30,10 +22,9 @@ module TypedEAV
30
22
  # the callable — when false, the subscriber is never registered in
31
23
  # the first place.
32
24
  #
33
- # - Errors raised by Subscriber.call PROPAGATE per the EventDispatcher
34
- # internal-vs-user error policy (03-CONTEXT.md §User-callback error
35
- # policy). Versioning corruption must be loud — silent failure
36
- # leaves the audit log inconsistent with the live row.
25
+ # - Errors raised by Subscriber.call propagate. Versioning corruption must
26
+ # be loud silent failure leaves the audit log inconsistent with the live
27
+ # row.
37
28
  #
38
29
  # ## Public API surface
39
30
  #
@@ -52,43 +43,70 @@ module TypedEAV
52
43
  # this namespace shell — it does NOT recursively autoload nested
53
44
  # constants. Without the explicit declaration below, the engine's
54
45
  # config.after_initialize block (which references
55
- # `TypedEAV::Versioning::Subscriber.method(:call)`) raises
46
+ # `TypedEAV::Versioning::Subscriber`) raises
56
47
  # `NameError: uninitialized constant TypedEAV::Versioning::Subscriber`
57
48
  # at boot time, breaking every host that enables versioning.
58
49
  autoload :Subscriber, "typed_eav/versioning/subscriber"
59
50
 
60
- # Conditionally register the Subscriber with EventDispatcher's internal
61
- # value-change subscriber chain. Called by the engine's
62
- # `config.after_initialize` block (lib/typed_eav/engine.rb).
51
+ CALLBACKS = {
52
+ create: %i[_write_version_create after].freeze,
53
+ update: %i[_write_version_update after].freeze,
54
+ destroy: %i[_write_version_destroy before].freeze,
55
+ }.freeze
56
+
57
+ # Conditionally install the Subscriber on Value's transactional callbacks.
58
+ # Called by the engine's `config.after_initialize` block.
63
59
  #
64
60
  # Extracted into a class method (not inlined inside the after_initialize
65
- # block) for testability: specs can call this against a freshly-cleared
66
- # `EventDispatcher.value_change_internals` to exercise both branches
67
- # (versioning on/off) in-process, without booting the engine. The
68
- # slot-0 regression spec (plan 04-03 P03) and the zero-overhead
69
- # verification spec (this plan, subscriber_spec engine-boot block) both
70
- # rely on this seam.
61
+ # block) for testability: specs can call this seam in-process without
62
+ # booting a second Rails application.
71
63
  #
72
- # Idempotent — safe to call multiple times. Calling twice with
73
- # versioning on results in exactly ONE entry in
74
- # `EventDispatcher.value_change_internals`. The idempotency check uses
75
- # `Array#include?` against `Subscriber.method(:call)`; `Method#==`
76
- # compares receiver+name (semantic equality), so two fresh
77
- # `Subscriber.method(:call)` instances compare equal even though they
78
- # are different Method objects. The engine block runs this exactly
79
- # once per boot in production; the idempotency guard protects future
80
- # code paths that might re-invoke for any reason.
64
+ # Idempotent — safe to call multiple times. Pool validation happens before
65
+ # any callback is installed, so a multi-database misconfiguration fails
66
+ # closed without partial activation.
81
67
  #
82
68
  # When `TypedEAV.config.versioning` is false (default), this method is
83
- # a no-op: zero callable in `value_change_internals`, zero per-write
84
- # dispatch cost. That is the locked CONTEXT line 17 contract.
69
+ # a no-op: no callback is installed and the disabled path adds no
70
+ # per-write predicate or dispatcher work.
85
71
  def self.register_if_enabled
86
72
  return unless TypedEAV.config.versioning
87
73
 
88
- method_ref = TypedEAV::Versioning::Subscriber.method(:call)
89
- return if TypedEAV::EventDispatcher.value_change_internals.include?(method_ref)
74
+ value_pool = TypedEAV::Value.connection_pool
75
+ version_pool = TypedEAV::ValueVersion.connection_pool
76
+ unless value_pool.equal?(version_pool)
77
+ raise ArgumentError, "TypedEAV versioning requires Value and ValueVersion to share a connection pool"
78
+ end
79
+
80
+ callback_chains = callback_chains_for_installation
81
+
82
+ callback_chains.each do |event, (filter, kind, chain)|
83
+ next if chain.any? { |callback| callback.filter == filter && callback.kind == kind }
84
+
85
+ TypedEAV::Value.set_callback(event, kind, filter, prepend: true)
86
+ end
87
+ end
88
+
89
+ def self.callback_chains_for_installation
90
+ CALLBACKS.to_h do |event, (filter, kind)|
91
+ chain = TypedEAV::Value.send(:get_callbacks, event).to_a
92
+ wrong_kind = chain.find { |callback| callback.filter == filter && callback.kind != kind }
93
+ if wrong_kind
94
+ raise ArgumentError,
95
+ "TypedEAV versioning callback #{filter.inspect} on #{event} has kind " \
96
+ "#{wrong_kind.kind.inspect}; expected #{kind.inspect}"
97
+ end
98
+
99
+ [event, [filter, kind, chain]]
100
+ end
101
+ end
102
+ private_class_method :callback_chains_for_installation
90
103
 
91
- TypedEAV::EventDispatcher.register_internal_value_change(method_ref)
104
+ def self.atomic_callbacks_installed?
105
+ CALLBACKS.all? do |event, (filter, kind)|
106
+ TypedEAV::Value.send(:get_callbacks, event).to_a.any? do |callback|
107
+ callback.filter == filter && callback.kind == kind
108
+ end
109
+ end
92
110
  end
93
111
  end
94
112
  end
data/lib/typed_eav.rb CHANGED
@@ -18,6 +18,7 @@ module TypedEAV
18
18
  autoload :FilterQuery
19
19
  autoload :BulkRead
20
20
  autoload :BulkWrite
21
+ autoload :BulkUpsert
21
22
  autoload :Partition
22
23
  autoload :QueryBuilder
23
24
  autoload :SchemaPortability
@@ -27,6 +28,7 @@ module TypedEAV
27
28
  autoload :ValueVersion
28
29
  autoload :Versioned
29
30
  autoload :Versioning
31
+ autoload :FieldDeletion
30
32
 
31
33
  # Raised when a model declared `has_typed_eav scope_method: ...` but no
32
34
  # scope can be resolved at query time and `config.require_scope` is truthy.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typed_eav
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dchuk
@@ -15,14 +15,20 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '7.1'
18
+ version: '7.2'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '8.2'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
26
  - - ">="
24
27
  - !ruby/object:Gem::Version
25
- version: '7.1'
28
+ version: '7.2'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '8.2'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: csv
28
34
  requirement: !ruby/object:Gem::Requirement
@@ -87,6 +93,8 @@ files:
87
93
  - db/migrate/20260505000000_create_typed_eav_value_versions.rb
88
94
  - db/migrate/20260506000001_add_version_group_id_to_typed_eav_value_versions.rb
89
95
  - db/migrate/20260507000000_add_label_to_typed_eav_fields.rb
96
+ - db/migrate/20260712000000_enforce_parent_scope_invariant.rb
97
+ - db/migrate/20260816000000_use_partial_covering_scalar_indexes.rb
90
98
  - lib/generators/typed_eav/install/install_generator.rb
91
99
  - lib/generators/typed_eav/scaffold/scaffold_generator.rb
92
100
  - lib/generators/typed_eav/scaffold/templates/config/initializers/typed_eav.rb
@@ -138,6 +146,7 @@ files:
138
146
  - lib/generators/typed_eav/scaffold/templates/views/typed_eav/values/inputs/_url.html.erb
139
147
  - lib/typed_eav.rb
140
148
  - lib/typed_eav/bulk_read.rb
149
+ - lib/typed_eav/bulk_upsert.rb
141
150
  - lib/typed_eav/bulk_write.rb
142
151
  - lib/typed_eav/config.rb
143
152
  - lib/typed_eav/csv_mapper.rb
@@ -145,6 +154,7 @@ files:
145
154
  - lib/typed_eav/entity_query.rb
146
155
  - lib/typed_eav/event_dispatcher.rb
147
156
  - lib/typed_eav/field/typed_storage.rb
157
+ - lib/typed_eav/field_deletion.rb
148
158
  - lib/typed_eav/filter_query.rb
149
159
  - lib/typed_eav/has_typed_eav.rb
150
160
  - lib/typed_eav/has_typed_eav/instance_methods.rb
@@ -152,6 +162,7 @@ files:
152
162
  - lib/typed_eav/query_builder.rb
153
163
  - lib/typed_eav/registry.rb
154
164
  - lib/typed_eav/schema_portability.rb
165
+ - lib/typed_eav/schema_portability/import_index.rb
155
166
  - lib/typed_eav/scope_tuple.rb
156
167
  - lib/typed_eav/version.rb
157
168
  - lib/typed_eav/versioned.rb
@@ -174,14 +185,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
174
185
  requirements:
175
186
  - - ">="
176
187
  - !ruby/object:Gem::Version
177
- version: '3.1'
188
+ version: '3.3'
189
+ - - "<"
190
+ - !ruby/object:Gem::Version
191
+ version: '4.1'
178
192
  required_rubygems_version: !ruby/object:Gem::Requirement
179
193
  requirements:
180
194
  - - ">="
181
195
  - !ruby/object:Gem::Version
182
196
  version: '0'
183
197
  requirements: []
184
- rubygems_version: 3.7.1
198
+ rubygems_version: 3.6.9
185
199
  specification_version: 4
186
200
  summary: Typed custom fields for ActiveRecord models
187
201
  test_files: []