familia 2.0.0.pre23 → 2.0.0.pre25

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.
@@ -29,6 +29,48 @@ module Familia
29
29
 
30
30
  using Familia::Refinements::StylizeWords
31
31
 
32
+ # Maximum recommended length for field values used in index keys.
33
+ # Longer values are allowed but will trigger a warning.
34
+ MAX_FIELD_VALUE_LENGTH = 256
35
+
36
+ # Validates a field value for use in index key construction.
37
+ # This is primarily for data quality and debugging clarity, not security.
38
+ #
39
+ # Security note: Redis SCAN patterns use the namespace prefix (e.g.,
40
+ # "customer:role_index:") which is derived from class/index metadata,
41
+ # not user input. Glob characters in stored field values are treated
42
+ # as literal characters in key names, not as pattern wildcards.
43
+ #
44
+ # @param field_value [Object] The field value to validate
45
+ # @param context [String] Description for warning messages
46
+ # @return [String, nil] The validated string value, or nil if invalid
47
+ def validate_field_value(field_value, context: 'index')
48
+ return nil if field_value.nil?
49
+
50
+ str_value = field_value.to_s
51
+ return nil if str_value.strip.empty?
52
+
53
+ # Warn on values containing Redis glob pattern characters
54
+ # These are legal but can be confusing when debugging key patterns
55
+ if str_value.match?(/[*?\[\]]/)
56
+ Familia.warn "[#{context}] Field value contains glob pattern characters: #{str_value.inspect}. " \
57
+ 'These are stored as literal characters but may be confusing during debugging.'
58
+ end
59
+
60
+ # Warn on control characters (except common whitespace)
61
+ if str_value.match?(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/)
62
+ Familia.warn "[#{context}] Field value contains control characters: #{str_value.inspect}"
63
+ end
64
+
65
+ # Warn on excessively long values
66
+ if str_value.length > MAX_FIELD_VALUE_LENGTH
67
+ Familia.warn "[#{context}] Field value exceeds #{MAX_FIELD_VALUE_LENGTH} characters " \
68
+ "(#{str_value.length} chars): #{str_value[0..50]}..."
69
+ end
70
+
71
+ str_value
72
+ end
73
+
32
74
  # Main setup method that orchestrates multi-value index creation
33
75
  #
34
76
  # @param indexed_class [Class] The class being indexed (e.g., Employee)
@@ -37,32 +79,36 @@ module Familia
37
79
  # @param within [Class, Symbol] Scope class for instance-scoped index (required)
38
80
  # @param query [Boolean] Whether to generate query methods
39
81
  def setup(indexed_class:, field:, index_name:, within:, query:)
40
- # Multi-index always requires a scope context
41
- scope_class = within
42
- resolved_class = Familia.resolve_class(scope_class)
82
+ # Determine scope type: class-level or instance-scoped
83
+ scope_class, scope_type = if within == :class
84
+ [indexed_class, :class]
85
+ else
86
+ k = Familia.resolve_class(within)
87
+ [k, :instance]
88
+ end
43
89
 
44
90
  # Store metadata for this indexing relationship
45
91
  indexed_class.indexing_relationships << IndexingRelationship.new(
46
92
  field: field,
47
93
  scope_class: scope_class,
48
- within: within,
94
+ within: within, # Preserve original (:class or actual class)
49
95
  index_name: index_name,
50
96
  query: query,
51
97
  cardinality: :multi,
52
98
  )
53
99
 
54
- # Always generate the factory method - required by mutation methods
55
- if scope_class.is_a?(Class)
56
- generate_factory_method(resolved_class, index_name)
57
- end
58
-
59
- # Generate query methods on the scope class (optional)
60
- if query && scope_class.is_a?(Class)
61
- generate_query_methods_destination(indexed_class, field, resolved_class, index_name)
100
+ case scope_type
101
+ when :instance
102
+ # Instance-scoped multi-index (existing behavior)
103
+ generate_factory_method(scope_class, index_name)
104
+ generate_query_methods_destination(indexed_class, field, scope_class, index_name) if query
105
+ generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
106
+ when :class
107
+ # Class-level multi-index (new behavior)
108
+ generate_factory_method_class(indexed_class, index_name)
109
+ generate_query_methods_class(indexed_class, field, index_name) if query
110
+ generate_mutation_methods_class(indexed_class, field, index_name)
62
111
  end
63
-
64
- # Generate mutation methods on the indexed class
65
- generate_mutation_methods_self(indexed_class, field, resolved_class, index_name)
66
112
  end
67
113
 
68
114
  # Generates the factory method ON THE SCOPE CLASS (Company when within: Company):
@@ -325,6 +371,226 @@ module Familia
325
371
  end
326
372
  end
327
373
  end
374
+
375
+ # =========================================================================
376
+ # CLASS-LEVEL MULTI-INDEX GENERATORS
377
+ # =========================================================================
378
+ #
379
+ # When within: :class is used, these generators create class-level methods
380
+ # instead of instance-scoped methods.
381
+ #
382
+ # Example:
383
+ # multi_index :role, :role_index # within: :class is default
384
+ #
385
+ # Generates on Customer (class methods):
386
+ # - Customer.role_index_for('admin') -> UnsortedSet factory
387
+ # - Customer.find_all_by_role('admin') -> [Customer, ...]
388
+ # - Customer.sample_from_role('admin', 3) -> random sample
389
+ # - Customer.rebuild_role_index -> rebuild index
390
+ #
391
+ # Generates on Customer (instance methods, auto-called on save):
392
+ # - customer.add_to_class_role_index
393
+ # - customer.remove_from_class_role_index
394
+ # - customer.update_in_class_role_index(old_value)
395
+
396
+ # Generates class-level factory method:
397
+ # - Customer.role_index_for(field_value) -> UnsortedSet
398
+ #
399
+ # The factory validates field values for data quality. Glob pattern
400
+ # characters (*, ?, [, ]) in field values are allowed but trigger
401
+ # warnings since they can be confusing during debugging.
402
+ #
403
+ # @param indexed_class [Class] The class being indexed (e.g., Customer)
404
+ # @param index_name [Symbol] Name of the index (e.g., :role_index)
405
+ def generate_factory_method_class(indexed_class, index_name)
406
+ # Capture index_name for use in validation context
407
+ idx_name = index_name
408
+ indexed_class.define_singleton_method(:"#{index_name}_for") do |field_value|
409
+ # Validate field value and use the validated string for consistent key format.
410
+ # Validation returns nil for nil/empty values, string otherwise.
411
+ # We allow nil through (creates a "null" index key) but use the validated
412
+ # string to ensure consistent type handling in key construction.
413
+ validated = MultiIndexGenerators.validate_field_value(field_value, context: "#{name}.#{idx_name}")
414
+ index_key = Familia.join(index_name, validated)
415
+ Familia::UnsortedSet.new(index_key, parent: self)
416
+ end
417
+ end
418
+
419
+ # Generates class-level query methods:
420
+ # - Customer.find_all_by_role(value) -> [Customer, ...]
421
+ # - Customer.sample_from_role(value, count) -> random sample
422
+ # - Customer.rebuild_role_index -> rebuild index
423
+ #
424
+ # @param indexed_class [Class] The class being indexed (e.g., Customer)
425
+ # @param field [Symbol] The field to index (e.g., :role)
426
+ # @param index_name [Symbol] Name of the index (e.g., :role_index)
427
+ def generate_query_methods_class(indexed_class, field, index_name)
428
+ # find_all_by_role(value)
429
+ # Uses load_multi for efficient batch loading (avoids N+1 queries)
430
+ indexed_class.define_singleton_method(:"find_all_by_#{field}") do |field_value|
431
+ index_set = send("#{index_name}_for", field_value)
432
+ identifiers = index_set.members
433
+ load_multi(identifiers).compact
434
+ end
435
+
436
+ # sample_from_role(value, count)
437
+ # Uses load_multi for efficient batch loading (avoids N+1 queries)
438
+ indexed_class.define_singleton_method(:"sample_from_#{field}") do |field_value, count = 1|
439
+ return [] if field_value.nil? || field_value.to_s.strip.empty?
440
+
441
+ index_set = send("#{index_name}_for", field_value)
442
+ identifiers = index_set.sample(count)
443
+ load_multi(identifiers).compact
444
+ end
445
+
446
+ # rebuild_role_index(batch_size:, &progress)
447
+ # For class-level indexes, we iterate all instances of the class
448
+ indexed_class.define_singleton_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
449
+ # PHASE 1: Discover all field values and collect objects
450
+ progress_block&.call(phase: :discovering, current: 0, total: 0)
451
+
452
+ # Use class-level instances collection if available
453
+ unless respond_to?(:instances) && instances.respond_to?(:members)
454
+ Familia.warn "[Rebuild] Cannot rebuild class-level multi-index #{index_name}: " \
455
+ "no instances collection found. " \
456
+ "Ensure #{name} has class_sorted_set :instances or similar."
457
+ return 0 # Return 0 for consistency - always return integer count
458
+ end
459
+
460
+ field_values = Set.new
461
+ cached_objects = []
462
+ processed = 0
463
+ total_count = instances.size
464
+
465
+ progress_block&.call(phase: :loading, current: 0, total: total_count)
466
+
467
+ instances.members.each_slice(batch_size) do |identifiers|
468
+ objects = load_multi(identifiers).compact
469
+ cached_objects.concat(objects)
470
+
471
+ objects.each do |obj|
472
+ value = obj.send(field)
473
+ field_values << value.to_s if value && !value.to_s.strip.empty?
474
+ end
475
+
476
+ processed += identifiers.size
477
+ progress_block&.call(phase: :loading, current: processed, total: total_count)
478
+ end
479
+
480
+ # PHASE 2: Clear existing index sets using SCAN
481
+ progress_block&.call(phase: :clearing, current: 0, total: field_values.size)
482
+
483
+ # Get pattern for all index keys: "customer:role_index:*"
484
+ #
485
+ # Security note: The SCAN pattern is safe because:
486
+ # 1. The namespace prefix (e.g., "customer:role_index:") is derived from
487
+ # class metadata and index name, not user input
488
+ # 2. The "*" wildcard only matches keys within this namespace
489
+ # 3. Glob characters stored IN field values (e.g., a role named "admin*")
490
+ # are literal characters in the key name, not SCAN wildcards
491
+ # 4. SCAN cannot match keys outside the namespace prefix
492
+ sample_index = send(:"#{index_name}_for", "*")
493
+ index_pattern = sample_index.dbkey
494
+
495
+ cleared_count = 0
496
+ dbclient.scan_each(match: index_pattern) do |key|
497
+ dbclient.del(key)
498
+ cleared_count += 1
499
+ progress_block&.call(phase: :clearing, current: cleared_count, total: field_values.size, key: key)
500
+ end
501
+
502
+ # PHASE 3: Rebuild from cached objects
503
+ progress_block&.call(phase: :rebuilding, current: 0, total: cached_objects.size)
504
+
505
+ processed = 0
506
+ cached_objects.each_slice(batch_size) do |objects|
507
+ dbclient.multi do |conn|
508
+ objects.each do |obj|
509
+ field_value = obj.send(field)
510
+ next unless field_value && !field_value.to_s.strip.empty?
511
+
512
+ index_set = send("#{index_name}_for", field_value)
513
+ # Use JsonSerializer for consistent serialization with update method
514
+ serialized_id = Familia::JsonSerializer.dump(obj.identifier)
515
+ conn.sadd(index_set.dbkey, serialized_id)
516
+ end
517
+ end
518
+
519
+ processed += objects.size
520
+ progress_block&.call(phase: :rebuilding, current: processed, total: cached_objects.size)
521
+ end
522
+
523
+ Familia.info "[Rebuild] Class-level multi-index #{index_name} rebuilt: " \
524
+ "#{field_values.size} field values, #{processed} objects"
525
+
526
+ processed
527
+ end
528
+ end
529
+
530
+ # Generates instance mutation methods for class-level indexes:
531
+ # - customer.add_to_class_role_index
532
+ # - customer.remove_from_class_role_index
533
+ # - customer.update_in_class_role_index(old_value)
534
+ #
535
+ # These are auto-called on save/destroy when auto-indexing is enabled.
536
+ #
537
+ # @param indexed_class [Class] The class being indexed (e.g., Customer)
538
+ # @param field [Symbol] The field to index (e.g., :role)
539
+ # @param index_name [Symbol] Name of the index (e.g., :role_index)
540
+ def generate_mutation_methods_class(indexed_class, field, index_name)
541
+ indexed_class.class_eval do
542
+ method_name = :"add_to_class_#{index_name}"
543
+ Familia.debug("[MultiIndexGenerators] #{name} class method #{method_name}")
544
+
545
+ define_method(method_name) do
546
+ field_value = send(field)
547
+ return unless field_value && !field_value.to_s.strip.empty?
548
+
549
+ index_set = self.class.send("#{index_name}_for", field_value)
550
+ index_set.add(identifier)
551
+ end
552
+
553
+ method_name = :"remove_from_class_#{index_name}"
554
+ Familia.debug("[MultiIndexGenerators] #{name} class method #{method_name}")
555
+
556
+ define_method(method_name) do
557
+ field_value = send(field)
558
+ return unless field_value && !field_value.to_s.strip.empty?
559
+
560
+ index_set = self.class.send("#{index_name}_for", field_value)
561
+ index_set.remove(identifier)
562
+ end
563
+
564
+ method_name = :"update_in_class_#{index_name}"
565
+ Familia.debug("[MultiIndexGenerators] #{name} class method #{method_name}")
566
+
567
+ define_method(method_name) do |old_field_value|
568
+ return unless old_field_value
569
+
570
+ new_field_value = send(field)
571
+ return if old_field_value == new_field_value
572
+
573
+ # Get the index sets for old and new values
574
+ old_set = self.class.send("#{index_name}_for", old_field_value)
575
+
576
+ # Use DataType's serialize_value for consistency with add/remove methods.
577
+ # This ensures the same serialization path is used across all index operations.
578
+ serialized_id = old_set.serialize_value(identifier)
579
+
580
+ # Use transaction for atomic remove + add to prevent data inconsistency
581
+ transaction do |conn|
582
+ # Remove from old index
583
+ conn.srem(old_set.dbkey, serialized_id)
584
+
585
+ # Add to new index if present
586
+ if new_field_value && !new_field_value.to_s.strip.empty?
587
+ new_set = self.class.send("#{index_name}_for", new_field_value)
588
+ conn.sadd(new_set.dbkey, serialized_id)
589
+ end
590
+ end
591
+ end
592
+ end
593
+ end
328
594
  end
329
595
  end
330
596
  end
@@ -98,7 +98,7 @@ module Familia
98
98
  # @example Instance-scoped multi-value indexing
99
99
  # multi_index :department, :dept_index, within: Company
100
100
  #
101
- def multi_index(field, index_name, within:, query: true)
101
+ def multi_index(field, index_name, within: :class, query: true)
102
102
  MultiIndexGenerators.setup(
103
103
  indexed_class: self,
104
104
  field: field,
@@ -159,12 +159,14 @@ module Familia
159
159
  index_name = config.index_name
160
160
  old_field_value = old_values[field]
161
161
 
162
- # Determine which update method to call
163
- if config.within.nil?
164
- # Class-level index (unique_index without within:)
162
+ # Determine which update method to call based on scope type
163
+ # Class-level: within is nil (unique_index default) or :class (multi_index default)
164
+ # Instance-scoped: within is a specific class
165
+ if config.within.nil? || config.within == :class
166
+ # Class-level index (auto-indexed on save)
165
167
  send("update_in_class_#{index_name}", old_field_value)
166
168
  else
167
- # Instance-scoped index (unique_index or multi_index with within:) - requires scope context
169
+ # Instance-scoped index - requires explicit scope context
168
170
  next unless scope_context
169
171
 
170
172
  # Use config_name for method naming
@@ -183,12 +185,14 @@ module Familia
183
185
  self.class.indexing_relationships.each do |config|
184
186
  index_name = config.index_name
185
187
 
186
- # Determine which remove method to call
187
- if config.within.nil?
188
- # Class-level index (unique_index without within:)
188
+ # Determine which remove method to call based on scope type
189
+ # Class-level: within is nil (unique_index default) or :class (multi_index default)
190
+ # Instance-scoped: within is a specific class
191
+ if config.within.nil? || config.within == :class
192
+ # Class-level index (auto-indexed on save)
189
193
  send("remove_from_class_#{index_name}")
190
194
  else
191
- # Instance-scoped index (unique_index or multi_index with within:) - requires scope context
195
+ # Instance-scoped index - requires explicit scope context
192
196
  next unless scope_context
193
197
 
194
198
  # Use config_name for method naming
@@ -216,20 +220,38 @@ module Familia
216
220
 
217
221
  next unless field_value
218
222
 
219
- if config.within.nil?
220
- # Class-level index (unique_index without within:) - check hash key using DataType
221
- index_hash = self.class.send(index_name)
222
- next unless index_hash.key?(field_value.to_s)
223
+ # Class-level indexes have within: nil (unique_index) or within: :class (multi_index)
224
+ # Instance-scoped indexes have within: SomeClass (a specific class)
225
+ if config.within.nil? || config.within == :class
226
+ if cardinality == :unique
227
+ # Class-level unique index - check hash key using DataType
228
+ index_hash = self.class.send(index_name)
229
+ next unless index_hash.key?(field_value.to_s)
223
230
 
224
- memberships << {
225
- scope_class: 'class',
226
- index_name: index_name,
227
- field: field,
228
- field_value: field_value,
229
- index_key: index_hash.dbkey,
230
- cardinality: cardinality,
231
- type: 'unique_index',
232
- }
231
+ memberships << {
232
+ scope_class: 'class',
233
+ index_name: index_name,
234
+ field: field,
235
+ field_value: field_value,
236
+ index_key: index_hash.dbkey,
237
+ cardinality: cardinality,
238
+ type: 'unique_index',
239
+ }
240
+ else
241
+ # Class-level multi index - check set membership using factory method
242
+ index_set = self.class.send("#{index_name}_for", field_value)
243
+ next unless index_set.member?(identifier)
244
+
245
+ memberships << {
246
+ scope_class: 'class',
247
+ index_name: index_name,
248
+ field: field,
249
+ field_value: field_value,
250
+ index_key: index_set.dbkey,
251
+ cardinality: cardinality,
252
+ type: 'multi_index',
253
+ }
254
+ end
233
255
  else
234
256
  # Instance-scoped index (unique_index or multi_index with within:) - cannot check without scope instance
235
257
  # This would require scanning all possible scope instances
@@ -250,7 +272,7 @@ module Familia
250
272
  end
251
273
 
252
274
  # Check if this object is indexed in a specific scope
253
- # For class-level indexes, checks the hash key
275
+ # For class-level indexes, checks the hash key (unique) or set membership (multi)
254
276
  # For instance-scoped indexes, returns false (requires scope instance)
255
277
  def indexed_in?(index_name)
256
278
  return false unless self.class.respond_to?(:indexing_relationships)
@@ -262,10 +284,18 @@ module Familia
262
284
  field_value = send(field)
263
285
  return false unless field_value
264
286
 
265
- if config.within.nil?
266
- # Class-level index (class_indexed_by) - check hash key using DataType
267
- index_hash = self.class.send(index_name)
268
- index_hash.key?(field_value.to_s)
287
+ # Class-level indexes have within: nil (unique_index) or within: :class (multi_index)
288
+ # Instance-scoped indexes have within: SomeClass (a specific class)
289
+ if config.within.nil? || config.within == :class
290
+ if config.cardinality == :unique
291
+ # Class-level unique index - check hash key using DataType
292
+ index_hash = self.class.send(index_name)
293
+ index_hash.key?(field_value.to_s)
294
+ else
295
+ # Class-level multi index - check set membership using factory method
296
+ index_set = self.class.send("#{index_name}_for", field_value)
297
+ index_set.member?(identifier)
298
+ end
269
299
  else
270
300
  # Instance-scoped index (with within:) - cannot verify without scope instance
271
301
  false
@@ -46,9 +46,6 @@ module Familia
46
46
 
47
47
  using Familia::Refinements::StylizeWords
48
48
 
49
- @dump_method = :to_json
50
- @load_method = :from_json
51
-
52
49
  def self.included(base)
53
50
  Familia.trace(:LOADED, self, base) if Familia.debug?
54
51
  base.extend ModelClassMethods
@@ -156,7 +156,10 @@ module Familia
156
156
  # doesn't, we return nil. If it does, we proceed to load the object.
157
157
  # Otherwise, hgetall will return an empty hash, which will be passed to
158
158
  # the constructor, which will then be annoying to debug.
159
- return unless does_exist
159
+ unless does_exist
160
+ cleanup_stale_instance_entry(objkey)
161
+ return nil
162
+ end
160
163
  else
161
164
  # Optimized mode: Skip existence check
162
165
  Familia.debug "[find_by_key] #{self} from key #{objkey} (check_exists: false)"
@@ -166,12 +169,42 @@ module Familia
166
169
  obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
167
170
  Familia.trace :FIND_BY_DBKEY_INSPECT, nil, "#{objkey}: #{obj.inspect}"
168
171
 
169
- # If we skipped existence check and got empty hash, key doesn't exist
170
- return nil if !check_exists && obj.empty?
172
+ # Always check for empty hash to handle race conditions where the key
173
+ # expires between EXISTS check and HGETALL (when check_exists: true),
174
+ # or simply doesn't exist (when check_exists: false).
175
+ if obj.empty?
176
+ cleanup_stale_instance_entry(objkey)
177
+ return nil
178
+ end
171
179
 
172
180
  # Create instance and deserialize fields using shared helper method
173
181
  instantiate_from_hash(obj)
174
182
  end
183
+
184
+ # Removes a stale entry from the instances sorted set.
185
+ # Called when find_by_dbkey detects that an object no longer exists
186
+ # (either EXISTS returned false, or HGETALL returned empty hash).
187
+ #
188
+ # This provides lazy cleanup of phantom instance entries that can
189
+ # accumulate when objects expire via TTL without explicit destroy!
190
+ #
191
+ # @param objkey [String] The full database key (prefix:identifier:suffix)
192
+ # @return [void]
193
+ # @api private
194
+ def cleanup_stale_instance_entry(objkey)
195
+ return unless respond_to?(:instances)
196
+
197
+ # Key format is prefix:identifier:suffix, so identifier is at index 1
198
+ parts = Familia.split(objkey)
199
+ return unless parts.length >= 2
200
+
201
+ identifier = parts[1]
202
+ return if identifier.nil? || identifier.empty?
203
+
204
+ instances.remove(identifier)
205
+ Familia.debug "[find_by_dbkey] Removed stale instance entry: #{identifier}"
206
+ end
207
+ private :cleanup_stale_instance_entry
175
208
  alias find_by_key find_by_dbkey
176
209
 
177
210
  # Retrieves and instantiates an object from Database using its identifier.
@@ -589,7 +589,10 @@ module Familia
589
589
  # Skip instance-scoped indexes (require scope context)
590
590
  # Instance-scoped indexes must be manually populated because they need
591
591
  # the scope instance reference (e.g., employee.add_to_company_badge_index(company))
592
- if rel.within
592
+ #
593
+ # Class-level indexes have within: nil (unique_index) or within: :class (multi_index)
594
+ # Instance-scoped indexes have within: SomeClass (a specific class)
595
+ if rel.within && rel.within != :class
593
596
  Familia.debug <<~LOG_MESSAGE
594
597
  [auto_update_class_indexes] Skipping #{rel.index_name} (requires scope context)
595
598
  LOG_MESSAGE
@@ -10,10 +10,10 @@ module Familia
10
10
  #
11
11
  class Horreum
12
12
  # Settings - Instance-level configuration methods for Horreum models
13
- # Provides per-instance settings like logical_database, dump_method, load_method, suffix
13
+ # Provides per-instance settings like logical_database, suffix
14
14
  #
15
15
  module Settings
16
- attr_writer :dump_method, :load_method, :suffix
16
+ attr_writer :suffix
17
17
 
18
18
  def opts
19
19
  @opts ||= {}
@@ -40,14 +40,6 @@ module Familia
40
40
  def suffix
41
41
  @suffix || self.class.suffix
42
42
  end
43
-
44
- def dump_method
45
- @dump_method || self.class.dump_method
46
- end
47
-
48
- def load_method
49
- @load_method || self.class.load_method
50
- end
51
43
  end
52
44
  end
53
45
  end
@@ -80,13 +80,12 @@ module Familia
80
80
  # Class-Level Attributes:
81
81
  # * @parent - Parent object reference for nested relationships
82
82
  # * @dbclient - Database connection override for this class
83
- # * @dump_method/@load_method - Serialization method configuration
84
83
  # * @has_related_fields - Flag indicating if DataType relationships are defined
85
84
  #
86
85
  class << self
87
86
  attr_accessor :parent
88
87
  # TODO: Where are we calling dbclient= from now with connection pool?
89
- attr_writer :dbclient, :dump_method, :load_method
88
+ attr_writer :dbclient
90
89
  attr_reader :has_related_fields
91
90
 
92
91
  # Extends ClassMethods to subclasses and tracks Familia members
@@ -4,5 +4,5 @@
4
4
 
5
5
  module Familia
6
6
  # Version information for the Familia
7
- VERSION = '2.0.0.pre23'.freeze unless defined?(Familia::VERSION)
7
+ VERSION = '2.0.0.pre25' unless defined?(Familia::VERSION)
8
8
  end