familia 2.0.0.pre18 → 2.0.0.pre19
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/CHANGELOG.rst +58 -6
- data/CLAUDE.md +34 -9
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +39 -0
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +3 -2
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/database_commands.rb +7 -4
- data/lib/familia/data_type/serialization.rb +4 -0
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +39 -38
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +115 -43
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +129 -10
- data/lib/familia/horreum/definition.rb +2 -1
- data/lib/familia/horreum/management.rb +21 -15
- data/lib/familia/horreum/persistence.rb +190 -66
- data/lib/familia/horreum/serialization.rb +3 -0
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +31 -12
- data/lib/familia/logging.rb +2 -5
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/middleware/database_logger.rb +76 -5
- data/try/edge_cases/string_coercion_try.rb +4 -4
- data/try/features/expiration/expiration_try.rb +1 -1
- data/try/features/relationships/indexing_try.rb +28 -4
- data/try/features/relationships/relationships_api_changes_try.rb +4 -4
- data/try/integration/connection/fiber_context_preservation_try.rb +3 -3
- data/try/integration/connection/operation_mode_guards_try.rb +1 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +12 -12
- data/try/integration/create_method_try.rb +22 -22
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/integration/models/customer_safe_dump_try.rb +5 -1
- data/try/integration/models/familia_object_try.rb +1 -1
- data/try/integration/persistence_operations_try.rb +162 -10
- data/try/unit/data_types/boolean_try.rb +1 -1
- data/try/unit/data_types/string_try.rb +1 -1
- data/try/unit/horreum/auto_indexing_on_save_try.rb +32 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/unit/horreum/base_try.rb +1 -1
- data/try/unit/horreum/class_methods_try.rb +2 -2
- data/try/unit/horreum/initialization_try.rb +1 -1
- data/try/unit/horreum/relations_try.rb +4 -4
- data/try/unit/horreum/serialization_try.rb +2 -2
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- metadata +14 -2
@@ -49,11 +49,11 @@ module Familia
|
|
49
49
|
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
50
50
|
# @param field [Symbol] The field to index
|
51
51
|
# @param index_name [Symbol] Name of the index
|
52
|
-
# @param within [Class, Symbol, nil]
|
52
|
+
# @param within [Class, Symbol, nil] Scope class for instance-scoped index
|
53
53
|
# @param query [Boolean] Whether to generate query methods
|
54
54
|
def setup(indexed_class:, field:, index_name:, within:, query:)
|
55
55
|
# Normalize parameters and determine scope type
|
56
|
-
|
56
|
+
scope_class, scope_type = if within
|
57
57
|
k = Familia.resolve_class(within)
|
58
58
|
[k, :instance]
|
59
59
|
else
|
@@ -63,7 +63,8 @@ module Familia
|
|
63
63
|
# Store metadata for this indexing relationship
|
64
64
|
indexed_class.indexing_relationships << IndexingRelationship.new(
|
65
65
|
field: field,
|
66
|
-
|
66
|
+
scope_class: scope_class,
|
67
|
+
within: within,
|
67
68
|
index_name: index_name,
|
68
69
|
query: query,
|
69
70
|
cardinality: :unique,
|
@@ -73,10 +74,10 @@ module Familia
|
|
73
74
|
case scope_type
|
74
75
|
when :instance
|
75
76
|
# Instance-scoped index (within: Company)
|
76
|
-
if query &&
|
77
|
-
generate_query_methods_destination(indexed_class, field,
|
77
|
+
if query && scope_class.is_a?(Class)
|
78
|
+
generate_query_methods_destination(indexed_class, field, scope_class, index_name)
|
78
79
|
end
|
79
|
-
generate_mutation_methods_self(indexed_class, field,
|
80
|
+
generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
|
80
81
|
when :class
|
81
82
|
# Class-level index (no within:)
|
82
83
|
indexed_class.send(:ensure_index_field, indexed_class, index_name, :class_hashkey)
|
@@ -85,7 +86,8 @@ module Familia
|
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
88
|
-
# Generates query methods ON THE
|
89
|
+
# Generates query methods ON THE SCOPE CLASS (Company when within: Company)
|
90
|
+
#
|
89
91
|
# - company.find_by_badge_number(badge) - find by field value
|
90
92
|
# - company.find_all_by_badge_number([badges]) - batch lookup
|
91
93
|
# - company.badge_index - DataType accessor
|
@@ -93,17 +95,17 @@ module Familia
|
|
93
95
|
#
|
94
96
|
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
95
97
|
# @param field [Symbol] The field to index (e.g., :badge_number)
|
96
|
-
# @param
|
98
|
+
# @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
|
97
99
|
# @param index_name [Symbol] Name of the index (e.g., :badge_index)
|
98
|
-
def generate_query_methods_destination(indexed_class, field,
|
99
|
-
# Resolve
|
100
|
-
|
100
|
+
def generate_query_methods_destination(indexed_class, field, scope_class, index_name)
|
101
|
+
# Resolve scope class using Familia pattern
|
102
|
+
actual_scope_class = Familia.resolve_class(scope_class)
|
101
103
|
|
102
104
|
# Ensure the index field is declared (creates accessor that returns DataType)
|
103
|
-
|
105
|
+
actual_scope_class.send(:ensure_index_field, actual_scope_class, index_name, :hashkey)
|
104
106
|
|
105
107
|
# Generate instance query method (e.g., company.find_by_badge_number)
|
106
|
-
|
108
|
+
actual_scope_class.class_eval do
|
107
109
|
define_method(:"find_by_#{field}") do |provided_value|
|
108
110
|
# Use declared field accessor instead of manual instantiation
|
109
111
|
index_hash = send(index_name)
|
@@ -121,7 +123,10 @@ module Familia
|
|
121
123
|
|
122
124
|
# Generate bulk query method (e.g., company.find_all_by_badge_number)
|
123
125
|
define_method(:"find_all_by_#{field}") do |provided_ids|
|
124
|
-
|
126
|
+
# Convert to array and filter nil inputs before querying Redis.
|
127
|
+
# This prevents wasteful lookups for empty string keys (nil.to_s → "").
|
128
|
+
# Output may contain fewer elements than input (standard ORM behavior).
|
129
|
+
provided_ids = Array(provided_ids).compact
|
125
130
|
return [] if provided_ids.empty?
|
126
131
|
|
127
132
|
# Use declared field accessor instead of manual instantiation
|
@@ -129,7 +134,8 @@ module Familia
|
|
129
134
|
|
130
135
|
# Get all identifiers from the hash
|
131
136
|
record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
|
132
|
-
|
137
|
+
|
138
|
+
# Filter out nil values (non-existent records) and instantiate objects
|
133
139
|
record_ids.compact.map { |record_id|
|
134
140
|
indexed_class.find_by_identifier(record_id)
|
135
141
|
}
|
@@ -153,63 +159,102 @@ module Familia
|
|
153
159
|
end
|
154
160
|
end
|
155
161
|
|
156
|
-
# Generates mutation methods ON THE INDEXED CLASS (Employee)
|
157
|
-
#
|
158
|
-
# -
|
162
|
+
# Generates mutation methods ON THE INDEXED CLASS (Employee)
|
163
|
+
#
|
164
|
+
# Instance methods for scope-scoped unique index operations:
|
165
|
+
# - employee.add_to_company_badge_index(company) - automatically validates uniqueness
|
159
166
|
# - employee.remove_from_company_badge_index(company)
|
160
167
|
# - employee.update_in_company_badge_index(company, old_badge)
|
168
|
+
# - employee.guard_unique_company_badge_index!(company) - manual validation
|
161
169
|
#
|
162
170
|
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
163
171
|
# @param field [Symbol] The field to index (e.g., :badge_number)
|
164
|
-
# @param
|
172
|
+
# @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
|
165
173
|
# @param index_name [Symbol] Name of the index (e.g., :badge_index)
|
166
|
-
def generate_mutation_methods_self(indexed_class, field,
|
167
|
-
|
174
|
+
def generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
|
175
|
+
scope_class_config = scope_class.config_name
|
168
176
|
indexed_class.class_eval do
|
169
|
-
method_name = :"add_to_#{
|
177
|
+
method_name = :"add_to_#{scope_class_config}_#{index_name}"
|
170
178
|
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
171
179
|
|
172
|
-
define_method(method_name) do |
|
173
|
-
return unless
|
180
|
+
define_method(method_name) do |scope_instance|
|
181
|
+
return unless scope_instance
|
174
182
|
|
175
183
|
field_value = send(field)
|
176
184
|
return unless field_value
|
177
185
|
|
178
|
-
#
|
179
|
-
|
186
|
+
# Automatically validate uniqueness before adding to index.
|
187
|
+
# Skip validation inside transactions since guard methods require read
|
188
|
+
# operations not available in MULTI/EXEC blocks.
|
189
|
+
unless Fiber[:familia_transaction]
|
190
|
+
guard_method = :"guard_unique_#{scope_class_config}_#{index_name}!"
|
191
|
+
send(guard_method, scope_instance) if respond_to?(guard_method)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Use declared field accessor on scope instance
|
195
|
+
index_hash = scope_instance.send(index_name)
|
180
196
|
|
181
|
-
#
|
197
|
+
# Set the value (guard already validated uniqueness)
|
182
198
|
index_hash[field_value.to_s] = identifier
|
183
199
|
end
|
184
200
|
|
185
|
-
|
201
|
+
# Add a guard method to enforce unique constraint on this instance-scoped index
|
202
|
+
#
|
203
|
+
# @param scope_instance [Object] The scope instance providing uniqueness context (e.g., a Company)
|
204
|
+
# @raise [Familia::RecordExistsError] if a record with the same field value
|
205
|
+
# exists in the scope's index. Values are compared as strings.
|
206
|
+
# @return [void]
|
207
|
+
#
|
208
|
+
# @example
|
209
|
+
# employee.guard_unique_company_badge_index!(company)
|
210
|
+
#
|
211
|
+
method_name = :"guard_unique_#{scope_class_config}_#{index_name}!"
|
186
212
|
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
187
213
|
|
188
|
-
define_method(method_name) do |
|
189
|
-
return unless
|
214
|
+
define_method(method_name) do |scope_instance|
|
215
|
+
return unless scope_instance
|
190
216
|
|
191
217
|
field_value = send(field)
|
192
218
|
return unless field_value
|
193
219
|
|
194
|
-
# Use declared field accessor on
|
195
|
-
index_hash =
|
220
|
+
# Use declared field accessor on scope instance
|
221
|
+
index_hash = scope_instance.send(index_name)
|
222
|
+
existing_id = index_hash.get(field_value.to_s)
|
223
|
+
|
224
|
+
if existing_id && existing_id != identifier
|
225
|
+
raise Familia::RecordExistsError,
|
226
|
+
"#{self.class} exists in #{scope_instance.class} with #{field}=#{field_value}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
method_name = :"remove_from_#{scope_class_config}_#{index_name}"
|
231
|
+
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
232
|
+
|
233
|
+
define_method(method_name) do |scope_instance|
|
234
|
+
return unless scope_instance
|
235
|
+
|
236
|
+
field_value = send(field)
|
237
|
+
return unless field_value
|
238
|
+
|
239
|
+
# Use declared field accessor on scope instance
|
240
|
+
index_hash = scope_instance.send(index_name)
|
196
241
|
|
197
242
|
# Remove using HashKey DataType method
|
198
243
|
index_hash.remove(field_value.to_s)
|
199
244
|
end
|
200
245
|
|
201
|
-
method_name = :"update_in_#{
|
246
|
+
method_name = :"update_in_#{scope_class_config}_#{index_name}"
|
202
247
|
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
203
248
|
|
204
|
-
define_method(method_name) do |
|
205
|
-
return unless
|
249
|
+
define_method(method_name) do |scope_instance, old_field_value = nil|
|
250
|
+
return unless scope_instance
|
206
251
|
|
207
252
|
new_field_value = send(field)
|
208
253
|
|
209
254
|
# Use Familia's transaction method for atomicity with DataType abstraction
|
210
|
-
|
211
|
-
# Use declared field accessor on
|
212
|
-
index_hash =
|
255
|
+
scope_instance.transaction do |_tx|
|
256
|
+
# Use declared field accessor on scope instance
|
257
|
+
index_hash = scope_instance.send(index_name)
|
213
258
|
|
214
259
|
# Remove old value if provided
|
215
260
|
index_hash.remove(old_field_value.to_s) if old_field_value
|
@@ -228,10 +273,12 @@ module Familia
|
|
228
273
|
# - Employee.email_index
|
229
274
|
# - Employee.rebuild_email_index
|
230
275
|
def generate_query_methods_class(field, index_name, indexed_class)
|
276
|
+
# Generate class-level single record method
|
231
277
|
indexed_class.define_singleton_method(:"find_by_#{field}") do |provided_id|
|
232
|
-
index_hash = send(index_name) #
|
278
|
+
index_hash = send(index_name) # access the class-level hashkey DataType
|
233
279
|
|
234
|
-
# Get the identifier from the
|
280
|
+
# Get the identifier from the db hashkey using .get method.
|
281
|
+
#
|
235
282
|
# We use .get instead of [] because it's part of the standard interface
|
236
283
|
# common across all DataType classes (List, UnsortedSet, SortedSet, HashKey).
|
237
284
|
# While unique indexes always use HashKey, using .get maintains consistency
|
@@ -245,12 +292,18 @@ module Familia
|
|
245
292
|
|
246
293
|
# Generate class-level bulk query method
|
247
294
|
indexed_class.define_singleton_method(:"find_all_by_#{field}") do |provided_ids|
|
248
|
-
|
295
|
+
# Convert to array and filter nil inputs before querying Redis.
|
296
|
+
# This prevents wasteful lookups for empty string keys (nil.to_s → "").
|
297
|
+
# Output may contain fewer elements than input (standard ORM behavior).
|
298
|
+
provided_ids = Array(provided_ids).compact
|
249
299
|
return [] if provided_ids.empty?
|
250
300
|
|
251
|
-
index_hash = send(index_name) #
|
301
|
+
index_hash = send(index_name) # access the class-level hashkey DataType
|
302
|
+
|
303
|
+
# Get multiple identifiers from the db hashkey using .values_at
|
252
304
|
record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
|
253
|
-
|
305
|
+
|
306
|
+
# Filter out nil values (non-existent records) and instantiate objects
|
254
307
|
record_ids.compact.map { |record_id|
|
255
308
|
indexed_class.find_by_identifier(record_id)
|
256
309
|
}
|
@@ -285,9 +338,28 @@ module Familia
|
|
285
338
|
|
286
339
|
return unless field_value
|
287
340
|
|
341
|
+
# Just set the value - uniqueness should be validated before save
|
288
342
|
index_hash[field_value.to_s] = identifier
|
289
343
|
end
|
290
344
|
|
345
|
+
# Add a guard method to enforce unique constraint on this specific index
|
346
|
+
#
|
347
|
+
# @raise [Familia::RecordExistsError] if a record with the same
|
348
|
+
# field value exists. Values are compared as strings.
|
349
|
+
#
|
350
|
+
# @return [void]
|
351
|
+
define_method(:"guard_unique_#{index_name}!") do
|
352
|
+
field_value = send(field)
|
353
|
+
return unless field_value
|
354
|
+
|
355
|
+
index_hash = self.class.send(index_name)
|
356
|
+
existing_id = index_hash.get(field_value.to_s)
|
357
|
+
|
358
|
+
if existing_id && existing_id != identifier
|
359
|
+
raise Familia::RecordExistsError, "#{self.class} exists #{field}=#{field_value}"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
291
363
|
define_method(:"remove_from_class_#{index_name}") do
|
292
364
|
index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
|
293
365
|
field_value = send(field)
|
@@ -50,7 +50,7 @@ module Familia
|
|
50
50
|
# Terminology:
|
51
51
|
# - unique_index: 1:1 field-to-object mapping (HashKey)
|
52
52
|
# - multi_index: 1:many field-to-objects mapping (UnsortedSet, no scores)
|
53
|
-
# - within:
|
53
|
+
# - within: scope class providing uniqueness boundary for instance-scoped indexes
|
54
54
|
# - query: whether to generate find_by_* methods (default: true)
|
55
55
|
#
|
56
56
|
# Key Patterns:
|
@@ -89,7 +89,7 @@ module Familia
|
|
89
89
|
#
|
90
90
|
# @param field [Symbol] The field to index on
|
91
91
|
# @param index_name [Symbol] Name of the index
|
92
|
-
# @param within [Class, Symbol] The
|
92
|
+
# @param within [Class, Symbol] The scope class providing uniqueness context
|
93
93
|
# @param query [Boolean] Whether to generate query methods
|
94
94
|
#
|
95
95
|
# @example Instance-scoped multi-value indexing
|
@@ -109,7 +109,7 @@ module Familia
|
|
109
109
|
#
|
110
110
|
# @param field [Symbol] The field to index on
|
111
111
|
# @param index_name [Symbol] Name of the index hash
|
112
|
-
# @param within [Class, Symbol] Optional
|
112
|
+
# @param within [Class, Symbol] Optional scope class for instance-scoped unique index
|
113
113
|
# @param query [Boolean] Whether to generate query methods
|
114
114
|
#
|
115
115
|
# @example Class-level unique index
|
@@ -136,70 +136,68 @@ module Familia
|
|
136
136
|
|
137
137
|
# Ensure proper DataType field is declared for index
|
138
138
|
# Similar to ensure_collection_field in participation system
|
139
|
-
def ensure_index_field(
|
140
|
-
return if
|
139
|
+
def ensure_index_field(scope_class, index_name, field_type)
|
140
|
+
return if scope_class.method_defined?(index_name) || scope_class.respond_to?(index_name)
|
141
141
|
|
142
|
-
|
142
|
+
scope_class.send(field_type, index_name)
|
143
143
|
end
|
144
144
|
end
|
145
145
|
|
146
146
|
# Instance methods for indexed objects
|
147
147
|
module ModelInstanceMethods
|
148
|
-
# Update all indexes for a given
|
149
|
-
# For class-level indexes (
|
150
|
-
# For
|
151
|
-
def update_all_indexes(old_values = {},
|
148
|
+
# Update all indexes for a given scope context
|
149
|
+
# For class-level indexes (unique_index without within:), scope_context should be nil
|
150
|
+
# For instance-scoped indexes (with within:), scope_context should be the scope instance
|
151
|
+
def update_all_indexes(old_values = {}, scope_context = nil)
|
152
152
|
return unless self.class.respond_to?(:indexing_relationships)
|
153
153
|
|
154
154
|
self.class.indexing_relationships.each do |config|
|
155
155
|
field = config.field
|
156
156
|
index_name = config.index_name
|
157
|
-
target_class = config.target_class
|
158
157
|
old_field_value = old_values[field]
|
159
158
|
|
160
159
|
# Determine which update method to call
|
161
|
-
if
|
160
|
+
if config.within.nil?
|
162
161
|
# Class-level index (unique_index without within:)
|
163
162
|
send("update_in_class_#{index_name}", old_field_value)
|
164
163
|
else
|
165
|
-
#
|
166
|
-
next unless
|
164
|
+
# Instance-scoped index (unique_index or multi_index with within:) - requires scope context
|
165
|
+
next unless scope_context
|
167
166
|
|
168
167
|
# Use config_name for method naming
|
169
|
-
|
170
|
-
send("update_in_#{
|
168
|
+
scope_class_config = Familia.resolve_class(config.scope_class).config_name
|
169
|
+
send("update_in_#{scope_class_config}_#{index_name}", scope_context, old_field_value)
|
171
170
|
end
|
172
171
|
end
|
173
172
|
end
|
174
173
|
|
175
|
-
# Remove from all indexes for a given
|
176
|
-
# For class-level indexes (
|
177
|
-
# For
|
178
|
-
def remove_from_all_indexes(
|
174
|
+
# Remove from all indexes for a given scope context
|
175
|
+
# For class-level indexes (unique_index without within:), scope_context should be nil
|
176
|
+
# For instance-scoped indexes (with within:), scope_context should be the scope instance
|
177
|
+
def remove_from_all_indexes(scope_context = nil)
|
179
178
|
return unless self.class.respond_to?(:indexing_relationships)
|
180
179
|
|
181
180
|
self.class.indexing_relationships.each do |config|
|
182
181
|
index_name = config.index_name
|
183
|
-
target_class = config.target_class
|
184
182
|
|
185
183
|
# Determine which remove method to call
|
186
|
-
if
|
184
|
+
if config.within.nil?
|
187
185
|
# Class-level index (unique_index without within:)
|
188
186
|
send("remove_from_class_#{index_name}")
|
189
187
|
else
|
190
|
-
#
|
191
|
-
next unless
|
188
|
+
# Instance-scoped index (unique_index or multi_index with within:) - requires scope context
|
189
|
+
next unless scope_context
|
192
190
|
|
193
191
|
# Use config_name for method naming
|
194
|
-
|
195
|
-
send("remove_from_#{
|
192
|
+
scope_class_config = Familia.resolve_class(config.scope_class).config_name
|
193
|
+
send("remove_from_#{scope_class_config}_#{index_name}", scope_context)
|
196
194
|
end
|
197
195
|
end
|
198
196
|
end
|
199
197
|
|
200
198
|
# Get all indexes this object appears in
|
201
|
-
# Note: For
|
202
|
-
# since
|
199
|
+
# Note: For instance-scoped indexes, this only shows class-level indexes
|
200
|
+
# since instance-scoped indexes require a specific scope instance
|
203
201
|
#
|
204
202
|
# @return [Array<Hash>] Array of index information
|
205
203
|
def current_indexings
|
@@ -210,19 +208,18 @@ module Familia
|
|
210
208
|
self.class.indexing_relationships.each do |config|
|
211
209
|
field = config.field
|
212
210
|
index_name = config.index_name
|
213
|
-
target_class = config.target_class
|
214
211
|
cardinality = config.cardinality
|
215
212
|
field_value = send(field)
|
216
213
|
|
217
214
|
next unless field_value
|
218
215
|
|
219
|
-
if
|
216
|
+
if config.within.nil?
|
220
217
|
# Class-level index (unique_index without within:) - check hash key using DataType
|
221
218
|
index_hash = self.class.send(index_name)
|
222
219
|
next unless index_hash.key?(field_value.to_s)
|
223
220
|
|
224
221
|
memberships << {
|
225
|
-
|
222
|
+
scope_class: 'class',
|
226
223
|
index_name: index_name,
|
227
224
|
field: field,
|
228
225
|
field_value: field_value,
|
@@ -231,17 +228,17 @@ module Familia
|
|
231
228
|
type: 'unique_index',
|
232
229
|
}
|
233
230
|
else
|
234
|
-
# Instance-scoped index (unique_index or multi_index with within:) - cannot check without
|
235
|
-
# This would require scanning all possible
|
231
|
+
# Instance-scoped index (unique_index or multi_index with within:) - cannot check without scope instance
|
232
|
+
# This would require scanning all possible scope instances
|
236
233
|
memberships << {
|
237
|
-
|
234
|
+
scope_class: config.scope_class_config_name,
|
238
235
|
index_name: index_name,
|
239
236
|
field: field,
|
240
237
|
field_value: field_value,
|
241
|
-
index_key: '
|
238
|
+
index_key: 'scope_dependent',
|
242
239
|
cardinality: cardinality,
|
243
240
|
type: cardinality == :unique ? 'unique_index' : 'multi_index',
|
244
|
-
note: 'Requires
|
241
|
+
note: 'Requires scope instance for verification',
|
245
242
|
}
|
246
243
|
end
|
247
244
|
end
|
@@ -249,9 +246,9 @@ module Familia
|
|
249
246
|
memberships
|
250
247
|
end
|
251
248
|
|
252
|
-
# Check if this object is indexed in a specific
|
249
|
+
# Check if this object is indexed in a specific scope
|
253
250
|
# For class-level indexes, checks the hash key
|
254
|
-
# For
|
251
|
+
# For instance-scoped indexes, returns false (requires scope instance)
|
255
252
|
def indexed_in?(index_name)
|
256
253
|
return false unless self.class.respond_to?(:indexing_relationships)
|
257
254
|
|
@@ -262,14 +259,12 @@ module Familia
|
|
262
259
|
field_value = send(field)
|
263
260
|
return false unless field_value
|
264
261
|
|
265
|
-
|
266
|
-
|
267
|
-
if target_class == self.class
|
262
|
+
if config.within.nil?
|
268
263
|
# Class-level index (class_indexed_by) - check hash key using DataType
|
269
264
|
index_hash = self.class.send(index_name)
|
270
265
|
index_hash.key?(field_value.to_s)
|
271
266
|
else
|
272
|
-
#
|
267
|
+
# Instance-scoped index (with within:) - cannot verify without scope instance
|
273
268
|
false
|
274
269
|
end
|
275
270
|
end
|
@@ -14,20 +14,30 @@ module Familia
|
|
14
14
|
# Similar to ParticipationRelationship but for attribute-based lookups
|
15
15
|
# rather than collection membership.
|
16
16
|
#
|
17
|
+
# Terminology:
|
18
|
+
# - `scope_class`: The class that provides the uniqueness boundary for
|
19
|
+
# instance-scoped indexes. For example, in `unique_index :badge_number,
|
20
|
+
# :badge_index, within: Company`, the Company is the scope class.
|
21
|
+
# - `within`: Preserves the original DSL parameter to explicitly distinguish
|
22
|
+
# class-level indexes (within: nil) from instance-scoped indexes (within:
|
23
|
+
# SomeClass). This avoids brittle class comparisons and prevents issues
|
24
|
+
# with inheritance scenarios.
|
25
|
+
#
|
17
26
|
IndexingRelationship = Data.define(
|
18
27
|
:field, # Symbol - field being indexed (e.g., :email, :department)
|
19
28
|
:index_name, # Symbol - name of the index (e.g., :email_index, :dept_index)
|
20
|
-
:
|
29
|
+
:scope_class, # Class/Symbol - scope class for instance-scoped indexes (within:)
|
30
|
+
:within, # Class/Symbol/nil - within: parameter (nil for class-level, Class for instance-scoped)
|
21
31
|
:cardinality, # Symbol - :unique (1:1) or :multi (1:many)
|
22
32
|
:query # Boolean - whether to generate query methods
|
23
33
|
) do
|
24
34
|
#
|
25
|
-
# Get the normalized config name for the
|
35
|
+
# Get the normalized config name for the scope class
|
26
36
|
#
|
27
37
|
# @return [String] The config name (e.g., "user", "company", "test_company")
|
28
38
|
#
|
29
|
-
def
|
30
|
-
|
39
|
+
def scope_class_config_name
|
40
|
+
scope_class.config_name
|
31
41
|
end
|
32
42
|
end
|
33
43
|
end
|
data/lib/familia/field_type.rb
CHANGED
@@ -143,7 +143,8 @@ module Familia
|
|
143
143
|
val = args.first
|
144
144
|
|
145
145
|
# If no value provided, return current stored value
|
146
|
-
|
146
|
+
# Handle Redis::Future objects during transactions
|
147
|
+
return hget(field_name) if val.nil? || val.is_a?(Redis::Future)
|
147
148
|
|
148
149
|
begin
|
149
150
|
# Trace the operation if debugging is enabled
|
@@ -6,40 +6,16 @@ module Familia
|
|
6
6
|
# Provides connection handling, transactions, and URI normalization for both
|
7
7
|
# class-level operations (e.g., Customer.dbclient) and instance-level operations
|
8
8
|
# (e.g., customer.dbclient)
|
9
|
+
#
|
10
|
+
# Includes shared connection behavior from Familia::Connection::Behavior, providing:
|
11
|
+
# - URI normalization (normalize_uri)
|
12
|
+
# - Connection creation (create_dbclient)
|
13
|
+
# - Transaction method signatures
|
14
|
+
# - Pipeline method signatures
|
9
15
|
module Connection
|
10
|
-
|
11
|
-
|
12
|
-
# Normalizes various URI formats to a consistent URI object
|
13
|
-
# Considers the class/instance logical_database when uri is nil or Integer
|
14
|
-
def normalize_uri(uri)
|
15
|
-
case uri
|
16
|
-
when Integer
|
17
|
-
new_uri = Familia.uri.dup
|
18
|
-
new_uri.db = uri
|
19
|
-
new_uri
|
20
|
-
when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
|
21
|
-
URI.parse(uri)
|
22
|
-
when URI
|
23
|
-
uri
|
24
|
-
when nil
|
25
|
-
# Use logical_database if available, otherwise fall back to Familia.uri
|
26
|
-
if respond_to?(:logical_database) && logical_database
|
27
|
-
new_uri = Familia.uri.dup
|
28
|
-
new_uri.db = logical_database
|
29
|
-
new_uri
|
30
|
-
else
|
31
|
-
Familia.uri
|
32
|
-
end
|
33
|
-
else
|
34
|
-
raise ArgumentError, "Invalid URI type: #{uri.class.name}"
|
35
|
-
end
|
36
|
-
end
|
16
|
+
include Familia::Connection::Behavior
|
37
17
|
|
38
|
-
|
39
|
-
def create_dbclient(uri = nil)
|
40
|
-
parsed_uri = normalize_uri(uri)
|
41
|
-
Familia.create_dbclient(parsed_uri)
|
42
|
-
end
|
18
|
+
attr_reader :uri
|
43
19
|
|
44
20
|
# Returns the Database connection for the class using Chain of Responsibility pattern.
|
45
21
|
#
|
@@ -277,9 +253,9 @@ module Familia
|
|
277
253
|
@provider_connection_handler ||= Familia::Connection::ProviderConnectionHandler.new
|
278
254
|
|
279
255
|
# Determine the appropriate class context
|
280
|
-
# When called from instance: self is instance,
|
281
|
-
# When called from class:
|
282
|
-
klass =
|
256
|
+
# When called from instance: self is instance, use the model class connection
|
257
|
+
# When called from class: we'll use our own connection
|
258
|
+
klass = is_a?(Class) ? self : self.class
|
283
259
|
|
284
260
|
# Always check class first for @dbclient since instance-level connections were removed
|
285
261
|
@cached_connection_handler ||= Familia::Connection::CachedConnectionHandler.new(klass)
|