familia 2.0.0.pre5 → 2.0.0.pre6

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +8 -5
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +4 -3
  5. data/docs/wiki/API-Reference.md +95 -18
  6. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  7. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  8. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  9. data/docs/wiki/Feature-System-Guide.md +600 -0
  10. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  11. data/docs/wiki/Field-System-Guide.md +784 -0
  12. data/docs/wiki/Home.md +72 -15
  13. data/docs/wiki/Implementation-Guide.md +126 -33
  14. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  15. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  16. data/docs/wiki/Security-Model.md +65 -25
  17. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  18. data/lib/familia/base.rb +1 -1
  19. data/lib/familia/data_type/types/counter.rb +38 -0
  20. data/lib/familia/data_type/types/hashkey.rb +18 -0
  21. data/lib/familia/data_type/types/lock.rb +43 -0
  22. data/lib/familia/data_type/types/string.rb +9 -2
  23. data/lib/familia/data_type.rb +2 -2
  24. data/lib/familia/encryption/encrypted_data.rb +137 -0
  25. data/lib/familia/encryption/manager.rb +21 -4
  26. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  27. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  28. data/lib/familia/encryption.rb +1 -1
  29. data/lib/familia/errors.rb +17 -3
  30. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  31. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  32. data/lib/familia/features/expiration.rb +1 -1
  33. data/lib/familia/features/quantization.rb +1 -1
  34. data/lib/familia/features/safe_dump.rb +1 -1
  35. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  36. data/lib/familia/features/transient_fields.rb +1 -1
  37. data/lib/familia/field_type.rb +5 -2
  38. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  39. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  40. data/lib/familia/horreum/core/serialization.rb +535 -0
  41. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  42. data/lib/familia/horreum/core.rb +21 -0
  43. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  44. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +44 -28
  45. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  46. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  47. data/lib/familia/horreum.rb +17 -17
  48. data/lib/familia/version.rb +1 -1
  49. data/lib/familia.rb +1 -1
  50. data/try/core/create_method_try.rb +240 -0
  51. data/try/core/database_consistency_try.rb +299 -0
  52. data/try/core/errors_try.rb +25 -4
  53. data/try/core/familia_try.rb +1 -1
  54. data/try/core/persistence_operations_try.rb +297 -0
  55. data/try/data_types/counter_try.rb +93 -0
  56. data/try/data_types/lock_try.rb +133 -0
  57. data/try/debugging/debug_aad_process.rb +82 -0
  58. data/try/debugging/debug_concealed_internal.rb +59 -0
  59. data/try/debugging/debug_concealed_reveal.rb +61 -0
  60. data/try/debugging/debug_context_aad.rb +68 -0
  61. data/try/debugging/debug_context_simple.rb +80 -0
  62. data/try/debugging/debug_cross_context.rb +62 -0
  63. data/try/debugging/debug_database_load.rb +64 -0
  64. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  65. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  66. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  67. data/try/debugging/debug_field_decrypt.rb +74 -0
  68. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  69. data/try/debugging/debug_load_path.rb +66 -0
  70. data/try/debugging/debug_method_definition.rb +46 -0
  71. data/try/debugging/debug_method_resolution.rb +41 -0
  72. data/try/debugging/debug_minimal.rb +24 -0
  73. data/try/debugging/debug_provider.rb +68 -0
  74. data/try/debugging/debug_secure_behavior.rb +73 -0
  75. data/try/debugging/debug_string_class.rb +46 -0
  76. data/try/debugging/debug_test.rb +46 -0
  77. data/try/debugging/debug_test_design.rb +80 -0
  78. data/try/encryption/encryption_core_try.rb +3 -3
  79. data/try/features/encrypted_fields_core_try.rb +19 -11
  80. data/try/features/encrypted_fields_integration_try.rb +66 -70
  81. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  82. data/try/features/encrypted_fields_security_try.rb +151 -144
  83. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  84. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  85. data/try/features/encryption_fields/context_isolation_try.rb +29 -8
  86. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  87. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  88. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  89. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  90. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  91. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  92. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  93. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  94. data/try/features/feature_dependencies_try.rb +3 -3
  95. data/try/features/transient_fields_core_try.rb +1 -1
  96. data/try/features/transient_fields_integration_try.rb +1 -1
  97. data/try/helpers/test_helpers.rb +25 -0
  98. data/try/horreum/enhanced_conflict_handling_try.rb +1 -1
  99. data/try/horreum/initialization_try.rb +1 -1
  100. data/try/horreum/relations_try.rb +1 -1
  101. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  102. data/try/horreum/serialization_try.rb +39 -4
  103. data/try/models/customer_safe_dump_try.rb +1 -1
  104. data/try/models/customer_try.rb +1 -1
  105. metadata +51 -10
  106. data/TEST_COVERAGE.md +0 -40
  107. data/lib/familia/horreum/serialization.rb +0 -473
@@ -1,29 +1,38 @@
1
- # lib/familia/horreum/definition_methods.rb
1
+ # lib/familia/horreum/subclass/definition.rb
2
2
 
3
3
  require_relative 'related_fields_management'
4
+ require_relative '../shared/settings'
4
5
 
5
6
  module Familia
6
- VALID_STRATEGIES = %i[raise skip warn overwrite].freeze
7
+ VALID_STRATEGIES = %i[raise skip ignore warn overwrite].freeze
7
8
 
8
9
  # Familia::Horreum
9
10
  #
10
11
  class Horreum
11
12
  # Class-level instance variables
12
13
  # These are set up as nil initially and populated later
13
- @dbclient = nil # TODO
14
- @identifier_field = nil
15
- @default_expiration = nil
14
+ #
15
+ # Connection and database settings
16
+ @dbclient = nil
16
17
  @logical_database = nil
17
18
  @uri = nil
18
- @suffix = nil
19
+
20
+ # Database Key generation settings
19
21
  @prefix = nil
20
- @fields = nil # []
21
- @class_related_fields = nil # {}
22
- @related_fields = nil # {}
22
+ @identifier_field = nil
23
+ @suffix = nil
24
+
25
+ # Fields and relationships
26
+ @fields = nil
27
+ @class_related_fields = nil
28
+ @related_fields = nil
29
+ @default_expiration = nil
30
+
31
+ # Serialization settings
23
32
  @dump_method = nil
24
33
  @load_method = nil
25
34
 
26
- # DefinitionMethods: Provides class-level functionality for Horreum
35
+ # DefinitionMethods: Provides class-level functionality for Horreum subclasses
27
36
  #
28
37
  # This module is extended into classes that include Familia::Horreum,
29
38
  # providing methods for Database operations and object management.
@@ -35,7 +44,7 @@ module Familia
35
44
  #
36
45
  module DefinitionMethods
37
46
  include Familia::Settings
38
- include Familia::Horreum::RelatedFieldsManagement
47
+ include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
39
48
 
40
49
  # Sets or retrieves the unique identifier field for the class.
41
50
  #
@@ -86,12 +95,12 @@ module Familia
86
95
  # - Others, depending on features available
87
96
  #
88
97
  def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, category: nil)
89
- # Use field type system internally for consistency
90
- require_relative '../field_type'
98
+ # Use field type system for consistency
99
+ require_relative '../../field_type'
91
100
 
92
101
  # Create appropriate field type based on category
93
102
  field_type = if category == :transient
94
- require_relative '../features/transient_fields/transient_field_type'
103
+ require_relative '../../features/transient_fields/transient_field_type'
95
104
  TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
96
105
  else
97
106
  # For regular fields and other categories, create custom field type with category override
@@ -232,7 +241,7 @@ module Familia
232
241
  # @param options [Hash] Field options
233
242
  #
234
243
  def transient_field(name, **)
235
- require_relative '../features/transient_fields/transient_field_type'
244
+ require_relative '../../features/transient_fields/transient_field_type'
236
245
  field_type = TransientFieldType.new(name, **, fast_method: false)
237
246
  register_field_type(field_type)
238
247
  end
@@ -261,7 +270,7 @@ module Familia
261
270
  WARNING
262
271
  when :raise
263
272
  raise ArgumentError, "Method >>> #{method_name} <<< already defined for #{self}"
264
- when :skip
273
+ when :skip, :ignore
265
274
  # Do nothing, skip silently
266
275
  end
267
276
  end
@@ -279,19 +288,26 @@ module Familia
279
288
  end
280
289
  end
281
290
 
282
- # Defines a fast attribute method with a bang (!) suffix for a given
283
- # attribute name. Fast attribute methods are used to immediately read or
284
- # write attribute values from/to the database. Calling a fast attribute
285
- # method has no effect on any of the object's other attributes and does
286
- # not trigger a call to update the object's expiration time.
291
+ # Fast attribute accessor method for immediate DB persistence.
292
+ #
293
+ # @param field_name [Symbol, String] the name of the horreum model attribute
294
+ # @param method_name [Symbol, String] the name of the regular accessor method
295
+ # @param fast_method_name [Symbol, String] the name of the fast method (must end with '!')
296
+ # @param on_conflict [Symbol] conflict resolution strategy for method name conflicts
287
297
  #
288
- # @param [Symbol, String] name the name of the attribute for which the
289
- # fast method is defined.
290
- # @return [Object] the current value of the attribute when called without
291
- # arguments.
292
- # @raise [ArgumentError] if more than one argument is provided.
293
- # @raise [RuntimeError] if an exception occurs during the execution of the
294
- # method.
298
+ # @return [void]
299
+ #
300
+ # @raise [ArgumentError] if fast_method_name doesn't end with '!'
301
+ #
302
+ # @note Generated method behavior:
303
+ # - Without args: Retrieves current value from Redis
304
+ # - With value: Sets and immediately persists to Redis
305
+ # - Returns boolean indicating success for writes
306
+ # - Bypasses object-level caching and expiration updates
307
+ #
308
+ # @example
309
+ # # Creates a method like: username!(value = nil)
310
+ # define_fast_writer_method(:username, :username, :username!, :raise)
295
311
  #
296
312
  def define_fast_writer_method(field_name, method_name, fast_method_name, on_conflict)
297
313
  raise ArgumentError, 'Must end with !' unless fast_method_name.to_s.end_with?('!')
@@ -1,4 +1,4 @@
1
- # lib/familia/horreum/management_methods.rb
1
+ # lib/familia/horreum/subclass/management.rb
2
2
 
3
3
  require_relative 'related_fields_management'
4
4
 
@@ -15,7 +15,7 @@ module Familia
15
15
  # * Provides utility methods for working with Database objects
16
16
  #
17
17
  module ManagementMethods
18
- include Familia::Horreum::RelatedFieldsManagement
18
+ include Familia::Horreum::RelatedFieldsManagement # Provides DataType query methods
19
19
 
20
20
  # Creates and persists a new instance of the class.
21
21
  #
@@ -55,9 +55,7 @@ module Familia
55
55
  #
56
56
  def create(*, **)
57
57
  fobj = new(*, **)
58
- raise Familia::Problem, "#{self} already exists: #{fobj.dbkey}" if fobj.exists?
59
-
60
- fobj.save
58
+ fobj.save_if_not_exists
61
59
  fobj
62
60
  end
63
61
 
@@ -172,8 +170,8 @@ module Familia
172
170
  # User.exists?(123) # Returns true if user:123:object exists in Redis
173
171
  #
174
172
  def exists?(identifier, suffix = nil)
173
+ raise NoIdentifier, "Empty identifier" if identifier.to_s.empty?
175
174
  suffix ||= self.suffix
176
- return false if identifier.to_s.empty?
177
175
 
178
176
  objkey = dbkey identifier, suffix
179
177
 
@@ -234,9 +232,11 @@ module Familia
234
232
  # distinction b/c passing in an explicitly nil is how DataType objects
235
233
  # at the class level are created without the global default 'object'
236
234
  # suffix. See DataType#dbkey "parent_class?" for more details.
235
+ #
237
236
  def dbkey(identifier, suffix = self.suffix)
238
- # Familia.ld "[.dbkey] #{identifier} for #{self} (suffix:#{suffix})"
239
- raise NoIdentifier, self if identifier.to_s.empty?
237
+ if identifier.to_s.empty?
238
+ raise NoIdentifier, "#{self} requires non-empty identifier, got: #{identifier.inspect}"
239
+ end
240
240
 
241
241
  identifier &&= identifier.to_s
242
242
  Familia.dbkey(prefix, identifier, suffix)
@@ -255,6 +255,7 @@ module Familia
255
255
  # Returns the number of dbkeys matching the given filter pattern
256
256
  # @param filter [String] dbkey pattern to match (default: '*')
257
257
  # @return [Integer] Number of matching keys
258
+ #
258
259
  def matching_keys_count(filter = '*')
259
260
  dbclient.keys(dbkey(filter)).compact.size
260
261
  end
@@ -15,7 +15,7 @@ module Familia
15
15
  #
16
16
  # Usage:
17
17
  # Include this module in classes that need DataType management
18
- # Call setup_relations_accessors to initialize the feature
18
+ # Call setup_related_fields_accessors to initialize the feature
19
19
  #
20
20
  module RelatedFieldsManagement
21
21
  # A practical flag to indicate that a Horreum member has relations,
@@ -23,17 +23,22 @@ module Familia
23
23
  @has_relations = nil
24
24
 
25
25
  def self.included(base)
26
- base.extend(ClassMethods)
27
- base.setup_relations_accessors
26
+ base.extend(RelatedFieldsAccessors)
27
+ base.setup_related_fields_accessors
28
28
  end
29
29
 
30
- module ClassMethods
30
+ module RelatedFieldsAccessors
31
31
  # Sets up all DataType related methods
32
- # This method is the core of the metaprogramming logic
32
+ # This method generates the following for each registered DataType:
33
33
  #
34
- def setup_relations_accessors
34
+ # Instance methods: set(), list(), hashkey(), sorted_set(), etc.
35
+ # Query methods: set?(), list?(), hashkey?(), sorted_set?(), etc.
36
+ # Collection methods: sets(), lists(), hashkeys(), sorted_sets(), etc.
37
+ # Class methods: class_set(), class_list(), etc.
38
+ #
39
+ def setup_related_fields_accessors
35
40
  Familia::DataType.registered_types.each_pair do |kind, klass|
36
- Familia.ld "[registered_types] #{kind} => #{klass}"
41
+ Familia.trace :registered_types, kind, klass, caller(1..1) if Familia.debug?
37
42
 
38
43
  # Dynamically define instance-level relation methods
39
44
  #
@@ -86,11 +91,11 @@ module Familia
86
91
  end
87
92
  end
88
93
  end
89
- # End of ClassMethods module
94
+ # End of RelatedFieldsAccessors module
90
95
 
91
96
  # Creates an instance-level relation
92
97
  def attach_instance_related_field(name, klass, opts)
93
- Familia.ld "[#{self}##{name}] Attaching instance-level #{klass} #{opts}"
98
+ Familia.trace :attach_instance, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
94
99
  raise ArgumentError, "Name is blank (#{klass})" if name.to_s.empty?
95
100
 
96
101
  name = name.to_s.to_sym
@@ -115,7 +120,7 @@ module Familia
115
120
 
116
121
  # Creates a class-level relation
117
122
  def attach_class_related_field(name, klass, opts)
118
- Familia.ld "[#{self}.#{name}] Attaching class-level #{klass} #{opts}"
123
+ Familia.trace :attach_class_related_field, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
119
124
  raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
120
125
 
121
126
  name = name.to_s.to_sym
@@ -1,5 +1,10 @@
1
1
  # lib/familia/horreum.rb
2
2
 
3
+ require_relative 'horreum/subclass/definition'
4
+ require_relative 'horreum/subclass/management'
5
+ require_relative 'horreum/shared/settings'
6
+ require_relative 'horreum/core'
7
+
3
8
  module Familia
4
9
  #
5
10
  # Horreum: A module for managing Redis-based object storage and relationships
@@ -23,6 +28,8 @@ module Familia
23
28
  #
24
29
  class Horreum
25
30
  include Familia::Base
31
+ include Familia::Horreum::Core
32
+ include Familia::Horreum::Settings
26
33
 
27
34
  # Singleton Class Context
28
35
  #
@@ -62,13 +69,14 @@ module Familia
62
69
  # Extends ClassMethods to subclasses and tracks Familia members
63
70
  def inherited(member)
64
71
  Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
65
- member.extend(DefinitionMethods)
66
- member.extend(ManagementMethods)
67
- member.extend(Connection)
68
- member.extend(Features)
69
72
 
70
- # Tracks all the classes/modules that include Familia. It's
71
- # 10pm, do you know where you Familia members are?
73
+ # Class-level functionality extensions:
74
+ member.extend(Familia::Horreum::DefinitionMethods) # field(), identifier_field(), dbkey()
75
+ member.extend(Familia::Horreum::ManagementMethods) # create(), find(), destroy!()
76
+ member.extend(Familia::Horreum::Connection) # dbclient, connection management
77
+ member.extend(Familia::Features) # feature() method for optional modules
78
+
79
+ # Track all classes that inherit from Horreum
72
80
  Familia.members << member
73
81
  super
74
82
  end
@@ -84,7 +92,7 @@ module Familia
84
92
  # Session.new({sessid: "abc123", custid: "user456"}) # legacy hash (robust)
85
93
  #
86
94
  def initialize(*args, **kwargs)
87
- Familia.ld "[Horreum] Initializing #{self.class}"
95
+ Familia.trace :INITIALIZE, dbclient, "Initializing #{self.class}", caller(1..1) if Familia.debug?
88
96
  initialize_relatives
89
97
 
90
98
  # No longer auto-create a key field - the identifier method will
@@ -123,7 +131,7 @@ module Familia
123
131
  elsif args.any?
124
132
  initialize_with_positional_args(*args)
125
133
  else
126
- Familia.ld "[Horreum] #{self.class} initialized with no arguments"
134
+ Familia.trace :INITIALIZE, dbclient, "#{self.class} initialized with no arguments", caller(1..1) if Familia.debug?
127
135
  # Default values are intentionally NOT set here to:
128
136
  # - Maintain Database memory efficiency (only store non-nil values)
129
137
  # - Avoid conflicts with nil-skipping serialization logic
@@ -158,7 +166,7 @@ module Familia
158
166
  self.class.related_fields.each_pair do |name, data_type_definition|
159
167
  klass = data_type_definition.klass
160
168
  opts = data_type_definition.opts
161
- Familia.ld "[#{self.class}] initialize_relatives #{name} => #{klass} #{opts.keys}"
169
+ Familia.trace :INITIALIZE_RELATIVES, dbclient, "#{name} => #{klass} #{opts.keys}", caller(1..1) if Familia.debug?
162
170
 
163
171
  # As a subclass of Familia::Horreum, we add ourselves as the parent
164
172
  # automatically. This is what determines the dbkey for DataType
@@ -310,11 +318,3 @@ module Familia
310
318
  end
311
319
  end
312
320
  end
313
-
314
- require_relative 'horreum/definition_methods'
315
- require_relative 'horreum/management_methods'
316
- require_relative 'horreum/database_commands'
317
- require_relative 'horreum/connection'
318
- require_relative 'horreum/serialization'
319
- require_relative 'horreum/settings'
320
- require_relative 'horreum/utils'
@@ -3,6 +3,6 @@
3
3
  module Familia
4
4
  # Version information for the Familia
5
5
  unless defined?(Familia::VERSION)
6
- VERSION = '2.0.0.pre5'
6
+ VERSION = '2.0.0.pre6'
7
7
  end
8
8
  end
data/lib/familia.rb CHANGED
@@ -73,9 +73,9 @@ module Familia
73
73
  require_relative 'familia/utils'
74
74
 
75
75
  extend SecureIdentifier
76
- extend Logging
77
76
  extend Connection
78
77
  extend Settings
78
+ extend Logging
79
79
  extend Utils
80
80
  end
81
81
 
@@ -0,0 +1,240 @@
1
+ # try/core/create_method_try.rb
2
+ #
3
+ # Comprehensive test coverage for the create method
4
+ # Tests the correct exception type and error message handling
5
+
6
+ require_relative '../helpers/test_helpers'
7
+
8
+ # Test class for create method behavior
9
+ class CreateTestModel < Familia::Horreum
10
+ identifier_field :id
11
+ field :id
12
+ field :name
13
+ field :value
14
+ end
15
+
16
+ # Clean up any existing test data
17
+ cleanup_keys = []
18
+ begin
19
+ existing_test_keys = Familia.dbclient.keys('createtestmodel:*')
20
+ cleanup_keys.concat(existing_test_keys)
21
+ Familia.dbclient.del(*existing_test_keys) if existing_test_keys.any?
22
+ rescue => e
23
+ # Ignore cleanup errors
24
+ end
25
+
26
+ @test_id_counter = 0
27
+ def next_test_id
28
+ @test_id_counter += 1
29
+ "create-test-#{Time.now.to_i}-#{@test_id_counter}"
30
+ end
31
+
32
+ # =============================================
33
+ # 1. Basic create method functionality
34
+ # =============================================
35
+
36
+ ## create method successfully creates new object
37
+ @test_id = next_test_id
38
+ @created_obj = CreateTestModel.create(id: @test_id, name: 'Created Object', value: 'test_value')
39
+ [@created_obj.class, @created_obj.exists?, @created_obj.name]
40
+ #=> [CreateTestModel, true, 'Created Object']
41
+
42
+ ## create method returns the created object
43
+ @created_obj.is_a?(CreateTestModel)
44
+ #=> true
45
+
46
+ ## create method persists object fields
47
+ @created_obj.refresh!
48
+ [@created_obj.name, @created_obj.value]
49
+ #=> ['Created Object', 'test_value']
50
+
51
+ # =============================================
52
+ # 2. Duplicate creation error handling
53
+ # =============================================
54
+
55
+ ## create method raises RecordExistsError for duplicate
56
+ begin
57
+ CreateTestModel.create(id: @test_id, name: 'Duplicate Attempt')
58
+ false # Should not reach here
59
+ rescue => e
60
+ e.class
61
+ end
62
+ #=> Familia::RecordExistsError
63
+
64
+ ## RecordExistsError includes the dbkey in the message
65
+ begin
66
+ CreateTestModel.create(id: @test_id, name: 'Another Duplicate')
67
+ false # Should not reach here
68
+ rescue Familia::RecordExistsError => e
69
+ expected_dbkey = "createtestmodel:#{@test_id}:object"
70
+ e.message.include?(expected_dbkey)
71
+ end
72
+ #=> true
73
+
74
+ ## RecordExistsError message follows consistent format
75
+ begin
76
+ CreateTestModel.create(id: @test_id, name: 'Yet Another Duplicate')
77
+ false # Should not reach here
78
+ rescue Familia::RecordExistsError => e
79
+ e.message.start_with?('Key already exists:')
80
+ end
81
+ #=> true
82
+
83
+ ## RecordExistsError exposes key property for programmatic access
84
+ @final_test_id = next_test_id
85
+ CreateTestModel.create(id: @final_test_id, name: 'Setup for Key Test')
86
+
87
+ begin
88
+ CreateTestModel.create(id: @final_test_id, name: 'Key Test Duplicate')
89
+ false # Should not reach here
90
+ rescue Familia::RecordExistsError => e
91
+ # Key should be accessible and contain the identifier
92
+ [e.respond_to?(:key), e.key.include?(@final_test_id)]
93
+ end
94
+ #=> [true, true]
95
+
96
+ # =============================================
97
+ # 3. Edge cases and error conditions
98
+ # =============================================
99
+
100
+ ## create with empty identifier raises NoIdentifier error
101
+ CreateTestModel.create(id: '')
102
+ #=!> Familia::NoIdentifier
103
+
104
+ ## create with nil identifier raises NoIdentifier error
105
+ CreateTestModel.create(id: nil)
106
+ #=!> Familia::NoIdentifier
107
+
108
+ ## create with only some fields set
109
+ @partial_id = next_test_id
110
+ @partial_obj = CreateTestModel.create(id: @partial_id, name: 'Partial Object')
111
+ [@partial_obj.exists?, @partial_obj.name, @partial_obj.value]
112
+ #=> [true, 'Partial Object', nil]
113
+
114
+ ## create with no additional fields (only identifier)
115
+ @minimal_id = next_test_id
116
+ @minimal_obj = CreateTestModel.create(id: @minimal_id)
117
+ [@minimal_obj.exists?, @minimal_obj.id]
118
+ #=> [true, @minimal_id]
119
+
120
+ # =============================================
121
+ # 4. Concurrency and transaction behavior
122
+ # =============================================
123
+
124
+ ## create is atomic - no partial state on failure
125
+ @concurrent_id = next_test_id
126
+ @first_obj = CreateTestModel.create(id: @concurrent_id, name: 'First')
127
+
128
+ # Verify first object exists
129
+ first_exists = @first_obj.exists?
130
+
131
+ # Attempt to create duplicate should not affect existing object
132
+ begin
133
+ CreateTestModel.create(id: @concurrent_id, name: 'Concurrent Attempt')
134
+ false # Should not reach here
135
+ rescue Familia::RecordExistsError
136
+ # Original object should be unchanged
137
+ @first_obj.refresh!
138
+ @first_obj.name == 'First'
139
+ end
140
+ #=> true
141
+
142
+ ## create failure doesn't leave partial data
143
+ before_failed_create = Familia.dbclient.keys("createtestmodel:#{@concurrent_id}:*").length
144
+ begin
145
+ CreateTestModel.create(id: @concurrent_id, name: 'Should Fail')
146
+ rescue Familia::RecordExistsError
147
+ # Should not create any additional keys
148
+ after_failed_create = Familia.dbclient.keys("createtestmodel:#{@concurrent_id}:*").length
149
+ after_failed_create == before_failed_create
150
+ end
151
+ #=> true
152
+
153
+ # =============================================
154
+ # 5. Consistency with save_if_not_exists
155
+ # =============================================
156
+
157
+ ## Both create and save_if_not_exists raise same error type for duplicates
158
+ @consistency_id = next_test_id
159
+ @consistency_obj = CreateTestModel.create(id: @consistency_id, name: 'Consistency Test')
160
+
161
+ # Test create raises RecordExistsError
162
+ create_error_class = begin
163
+ CreateTestModel.create(id: @consistency_id, name: 'Create Duplicate')
164
+ nil
165
+ rescue => e
166
+ e.class
167
+ end
168
+
169
+ # Test save_if_not_exists raises RecordExistsError
170
+ sine_error_class = begin
171
+ duplicate_obj = CreateTestModel.new(id: @consistency_id, name: 'SINE Duplicate')
172
+ duplicate_obj.save_if_not_exists
173
+ nil
174
+ rescue => e
175
+ e.class
176
+ end
177
+
178
+ [create_error_class, sine_error_class]
179
+ #=> [Familia::RecordExistsError, Familia::RecordExistsError]
180
+
181
+ ## Both methods have similar error message patterns
182
+ @error_comparison_id = next_test_id
183
+ CreateTestModel.create(id: @error_comparison_id, name: 'Error Comparison')
184
+
185
+ create_error_msg = begin
186
+ CreateTestModel.create(id: @error_comparison_id, name: 'Create Error')
187
+ nil
188
+ rescue => e
189
+ e.message
190
+ end
191
+
192
+ sine_error_msg = begin
193
+ CreateTestModel.new(id: @error_comparison_id, name: 'SINE Error').save_if_not_exists
194
+ nil
195
+ rescue => e
196
+ e.message
197
+ end
198
+
199
+ # Both should reference the same key concept
200
+ [create_error_msg.include?('already exists'), sine_error_msg.include?('already exists')]
201
+ #=> [true, true]
202
+
203
+ # =============================================
204
+ # 6. Integration with different field types
205
+ # =============================================
206
+
207
+ ## create works with complex field values
208
+ @complex_id = next_test_id
209
+ @complex_obj = CreateTestModel.create(
210
+ id: @complex_id,
211
+ name: 'Complex Object',
212
+ value: { nested: 'data', array: [1, 2, 3] }
213
+ )
214
+ [@complex_obj.exists?, @complex_obj.value[:nested]]
215
+ #=> [true, 'data']
216
+
217
+ # =============================================
218
+ # 7. Class vs instance method consistency
219
+ # =============================================
220
+
221
+ ## Class.create and instance.save_if_not_exists have consistent existence checking
222
+ @consistency_check_id = next_test_id
223
+
224
+ # Create via class method
225
+ @class_created = CreateTestModel.create(id: @consistency_check_id, name: 'Class Created')
226
+
227
+ # Both class and instance methods should see the object as existing
228
+ class_sees_exists = CreateTestModel.exists?(@consistency_check_id)
229
+ instance_sees_exists = @class_created.exists?
230
+
231
+ [class_sees_exists, instance_sees_exists]
232
+ #=> [true, true]
233
+
234
+ # =============================================
235
+ # Cleanup
236
+ # =============================================
237
+
238
+ # Clean up all test data
239
+ test_keys = Familia.dbclient.keys('createtestmodel:*')
240
+ Familia.dbclient.del(*test_keys) if test_keys.any?