familia 2.0.0.pre17 → 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 +118 -6
- data/CLAUDE.md +43 -11
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +52 -0
- data/bin/irb +1 -1
- 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/core-field-system.md +48 -26
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/middleware.rb +58 -4
- 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 +2 -1
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
- data/lib/familia/data_type/serialization.rb +9 -5
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- 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 +45 -44
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +130 -11
- data/lib/familia/horreum/definition.rb +8 -38
- data/lib/familia/horreum/management.rb +38 -27
- data/lib/familia/horreum/persistence.rb +191 -67
- data/lib/familia/horreum/serialization.rb +94 -73
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +41 -18
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +268 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +5 -5
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +2 -2
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +34 -5
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +5 -5
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +23 -23
- data/try/integration/cross_component_try.rb +1 -1
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +2 -2
- data/try/{core → integration}/persistence_operations_try.rb +163 -11
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +2 -2
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/{horreum → unit/horreum}/base_try.rb +4 -4
- data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +147 -126
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -5,7 +5,7 @@
|
|
5
5
|
# Tests automatic index population when Familia::Horreum objects are saved
|
6
6
|
#
|
7
7
|
|
8
|
-
require_relative '
|
8
|
+
require_relative '../../support/helpers/test_helpers'
|
9
9
|
|
10
10
|
# Test classes for auto-indexing functionality
|
11
11
|
class ::AutoIndexUser < Familia::Horreum
|
@@ -43,6 +43,19 @@ class ::AutoIndexEmployee < Familia::Horreum
|
|
43
43
|
multi_index :department, :dept_index, within: AutoIndexCompany
|
44
44
|
end
|
45
45
|
|
46
|
+
class ::AutoIndexWithTransient < Familia::Horreum
|
47
|
+
feature :transient_fields
|
48
|
+
feature :relationships
|
49
|
+
|
50
|
+
identifier_field :id
|
51
|
+
field :id
|
52
|
+
field :email
|
53
|
+
transient_field :temp_value
|
54
|
+
|
55
|
+
unique_index :email, :email_index
|
56
|
+
end
|
57
|
+
|
58
|
+
|
46
59
|
# Setup
|
47
60
|
@user_id = "user_#{rand(1000000)}"
|
48
61
|
@user = AutoIndexUser.new(user_id: @user_id, email: 'test@example.com', username: 'testuser', department: 'engineering')
|
@@ -125,9 +138,9 @@ AutoIndexUser.email_index.has_key?('')
|
|
125
138
|
AutoIndexUser.email_index.has_key?('')
|
126
139
|
#=> true
|
127
140
|
|
128
|
-
## Auto-indexing works with create method
|
141
|
+
## Auto-indexing works with create! method
|
129
142
|
@user2_id = "user_#{rand(1000000)}"
|
130
|
-
@user2 = AutoIndexUser.create(user_id: @user2_id, email: 'create@example.com', username: 'createuser', department: 'marketing')
|
143
|
+
@user2 = AutoIndexUser.create!(user_id: @user2_id, email: 'create@example.com', username: 'createuser', department: 'marketing')
|
131
144
|
AutoIndexUser.find_by_email('create@example.com')&.user_id
|
132
145
|
#=> @user2_id
|
133
146
|
|
@@ -152,22 +165,11 @@ old_email = @user2.email
|
|
152
165
|
# =============================================
|
153
166
|
|
154
167
|
## Auto-indexing works with transient fields
|
155
|
-
class ::AutoIndexWithTransient < Familia::Horreum
|
156
|
-
feature :transient_fields
|
157
|
-
feature :relationships
|
158
|
-
|
159
|
-
identifier_field :id
|
160
|
-
field :id
|
161
|
-
field :email
|
162
|
-
transient_field :temp_value
|
163
|
-
|
164
|
-
unique_index :email, :email_index
|
165
|
-
end
|
166
|
-
|
167
168
|
@transient_id = "trans_#{rand(1000000)}"
|
168
|
-
@transient_obj = AutoIndexWithTransient.new(id: @transient_id, email:
|
169
|
+
@transient_obj = AutoIndexWithTransient.new(id: @transient_id, email: "transient_#{rand(1000000)}@example.com", temp_value: 'ignored')
|
169
170
|
@transient_obj.save
|
170
|
-
|
171
|
+
@transient_email = @transient_obj.email
|
172
|
+
AutoIndexWithTransient.find_by_email(@transient_email)&.id
|
171
173
|
#=> @transient_id
|
172
174
|
|
173
175
|
## Auto-indexing works regardless of other features
|
@@ -175,6 +177,20 @@ AutoIndexWithTransient.find_by_email('transient@example.com')&.id
|
|
175
177
|
@transient_obj.class.respond_to?(:indexing_relationships)
|
176
178
|
#=> true
|
177
179
|
|
180
|
+
## Guard validation prevents duplicate transient field email
|
181
|
+
@transient_dup = AutoIndexWithTransient.new(id: "trans_dup_#{rand(1000000)}", email: @transient_email, temp_value: 'duplicate')
|
182
|
+
begin
|
183
|
+
@transient_dup.save
|
184
|
+
rescue Familia::RecordExistsError => ex
|
185
|
+
Familia.ld ex.backtrace.join("\n")
|
186
|
+
ex.message
|
187
|
+
end
|
188
|
+
#=:> String
|
189
|
+
#=~> /Key already exists/
|
190
|
+
#=~> /AutoIndexWithTransient exists /
|
191
|
+
#=~> /email=#{@transient_email}/
|
192
|
+
#=/=> @transient_email.nil?
|
193
|
+
|
178
194
|
# =============================================
|
179
195
|
# 5. Performance and Behavior Verification
|
180
196
|
# =============================================
|
@@ -0,0 +1,253 @@
|
|
1
|
+
# try/unit/horreum/automatic_index_validation_try.rb
|
2
|
+
|
3
|
+
#
|
4
|
+
# Automatic index validation tests
|
5
|
+
# Tests that unique index validation happens automatically when adding to indexes
|
6
|
+
#
|
7
|
+
|
8
|
+
require_relative '../../support/helpers/test_helpers'
|
9
|
+
|
10
|
+
# Test classes for automatic validation
|
11
|
+
class ::AutoValidCompany < Familia::Horreum
|
12
|
+
feature :relationships
|
13
|
+
|
14
|
+
identifier_field :company_id
|
15
|
+
field :company_id
|
16
|
+
field :name
|
17
|
+
end
|
18
|
+
|
19
|
+
class ::AutoValidEmployee < Familia::Horreum
|
20
|
+
feature :relationships
|
21
|
+
|
22
|
+
identifier_field :emp_id
|
23
|
+
field :emp_id
|
24
|
+
field :badge_number
|
25
|
+
field :email
|
26
|
+
|
27
|
+
# Instance-scoped unique index (should auto-validate in add_to_* methods)
|
28
|
+
unique_index :badge_number, :badge_index, within: AutoValidCompany
|
29
|
+
|
30
|
+
# Class-level unique index (auto-validates in save)
|
31
|
+
unique_index :email, :email_index
|
32
|
+
end
|
33
|
+
|
34
|
+
class ::AutoValidUser < Familia::Horreum
|
35
|
+
feature :relationships
|
36
|
+
|
37
|
+
identifier_field :user_id
|
38
|
+
field :user_id
|
39
|
+
field :email
|
40
|
+
|
41
|
+
unique_index :email, :email_index
|
42
|
+
end
|
43
|
+
|
44
|
+
# Setup
|
45
|
+
@company_id = "comp_#{rand(1000000)}"
|
46
|
+
@company = AutoValidCompany.new(company_id: @company_id, name: 'Test Corp')
|
47
|
+
@company.save
|
48
|
+
|
49
|
+
@emp1_id = "emp_#{rand(1000000)}"
|
50
|
+
@emp2_id = "emp_#{rand(1000000)}"
|
51
|
+
|
52
|
+
# =============================================
|
53
|
+
# 1. Automatic Validation in add_to_* Methods
|
54
|
+
# =============================================
|
55
|
+
|
56
|
+
## First employee can add badge to company index
|
57
|
+
@emp1 = AutoValidEmployee.new(emp_id: @emp1_id, badge_number: 'BADGE123', email: 'emp1@example.com')
|
58
|
+
@emp1.save # Save first to establish class-level email index
|
59
|
+
@emp1.add_to_auto_valid_company_badge_index(@company)
|
60
|
+
@company.badge_index.has_key?('BADGE123')
|
61
|
+
#=> true
|
62
|
+
|
63
|
+
## Duplicate badge is automatically rejected without manual guard call
|
64
|
+
@emp2 = AutoValidEmployee.new(emp_id: @emp2_id, badge_number: 'BADGE123', email: 'emp2@example.com')
|
65
|
+
begin
|
66
|
+
@emp2.add_to_auto_valid_company_badge_index(@company)
|
67
|
+
false
|
68
|
+
rescue Familia::RecordExistsError => e
|
69
|
+
e.message.include?('AutoValidEmployee exists in AutoValidCompany with badge_number=BADGE123')
|
70
|
+
end
|
71
|
+
#=> true
|
72
|
+
|
73
|
+
## Badge was not added after validation failure
|
74
|
+
@company.badge_index.get('BADGE123')
|
75
|
+
#=> @emp1_id
|
76
|
+
|
77
|
+
## Different badge number works fine
|
78
|
+
@emp2.badge_number = 'BADGE456'
|
79
|
+
@emp2.add_to_auto_valid_company_badge_index(@company)
|
80
|
+
@company.badge_index.has_key?('BADGE456')
|
81
|
+
#=> true
|
82
|
+
|
83
|
+
## Same employee can re-add (idempotent)
|
84
|
+
@emp1.add_to_auto_valid_company_badge_index(@company)
|
85
|
+
@company.badge_index.get('BADGE123')
|
86
|
+
#=> @emp1_id
|
87
|
+
|
88
|
+
## Different company allows same badge (scoped uniqueness)
|
89
|
+
@company2_id = "comp_#{rand(1000000)}"
|
90
|
+
@company2 = AutoValidCompany.new(company_id: @company2_id, name: 'Other Corp')
|
91
|
+
@company2.save
|
92
|
+
@emp3_id = "emp_#{rand(1000000)}"
|
93
|
+
@emp3 = AutoValidEmployee.new(emp_id: @emp3_id, badge_number: 'BADGE123', email: 'emp3@example.com')
|
94
|
+
@emp3.add_to_auto_valid_company_badge_index(@company2)
|
95
|
+
@company2.badge_index.has_key?('BADGE123')
|
96
|
+
#=> true
|
97
|
+
|
98
|
+
## Nil badge_number handled gracefully (no validation or addition)
|
99
|
+
@emp_nil = AutoValidEmployee.new(emp_id: "emp_nil_#{rand(1000000)}", badge_number: nil, email: 'empnil@example.com')
|
100
|
+
@emp_nil.add_to_auto_valid_company_badge_index(@company)
|
101
|
+
#=> nil
|
102
|
+
|
103
|
+
## Nil parent handled gracefully (no validation or addition)
|
104
|
+
@emp4 = AutoValidEmployee.new(emp_id: "emp4_#{rand(1000000)}", badge_number: 'BADGE789', email: 'emp4@example.com')
|
105
|
+
@emp4.add_to_auto_valid_company_badge_index(nil)
|
106
|
+
#=> nil
|
107
|
+
|
108
|
+
# =============================================
|
109
|
+
# 2. Transaction Detection in save()
|
110
|
+
# =============================================
|
111
|
+
|
112
|
+
## Normal save works outside transaction
|
113
|
+
@user1_id = "user_#{rand(1000000)}"
|
114
|
+
@user1 = AutoValidUser.new(user_id: @user1_id, email: 'user1@example.com')
|
115
|
+
@user1.save
|
116
|
+
#=> true
|
117
|
+
|
118
|
+
## save() raises error when called within transaction
|
119
|
+
@user2_id = "user_#{rand(1000000)}"
|
120
|
+
begin
|
121
|
+
AutoValidUser.transaction do
|
122
|
+
@user2 = AutoValidUser.new(user_id: @user2_id, email: 'user2@example.com')
|
123
|
+
@user2.save
|
124
|
+
end
|
125
|
+
false
|
126
|
+
rescue Familia::OperationModeError => e
|
127
|
+
e.message.include?('Cannot call save within a transaction')
|
128
|
+
end
|
129
|
+
#=> true
|
130
|
+
|
131
|
+
## Object was not saved due to transaction error
|
132
|
+
AutoValidUser.find_by_email('user2@example.com')
|
133
|
+
#=> nil
|
134
|
+
|
135
|
+
## Transaction with explicit field updates works (bypass save)
|
136
|
+
@user3_id = "user_#{rand(1000000)}"
|
137
|
+
@user3 = AutoValidUser.new(user_id: @user3_id, email: 'user3@example.com')
|
138
|
+
AutoValidUser.transaction do |_tx|
|
139
|
+
@user3.hmset(@user3.to_h_for_storage)
|
140
|
+
end
|
141
|
+
@user3.exists?
|
142
|
+
#=> true
|
143
|
+
|
144
|
+
## save() works after transaction completes
|
145
|
+
@user4_id = "user_#{rand(1000000)}"
|
146
|
+
AutoValidUser.transaction do
|
147
|
+
# Do something else in transaction
|
148
|
+
end
|
149
|
+
@user4 = AutoValidUser.new(user_id: @user4_id, email: 'user4@example.com')
|
150
|
+
@user4.save
|
151
|
+
#=> true
|
152
|
+
|
153
|
+
# =============================================
|
154
|
+
# 3. Combined Automatic Validation Scenarios
|
155
|
+
# =============================================
|
156
|
+
|
157
|
+
## Employee with duplicate class-level email caught in save
|
158
|
+
@emp5_id = "emp_#{rand(1000000)}"
|
159
|
+
@emp5 = AutoValidEmployee.new(emp_id: @emp5_id, badge_number: 'BADGE999', email: 'emp1@example.com')
|
160
|
+
begin
|
161
|
+
@emp5.save
|
162
|
+
false
|
163
|
+
rescue Familia::RecordExistsError => e
|
164
|
+
e.message.include?('AutoValidEmployee exists email=emp1@example.com')
|
165
|
+
end
|
166
|
+
#=> true
|
167
|
+
|
168
|
+
## Employee can save with unique email
|
169
|
+
@emp1.save
|
170
|
+
AutoValidEmployee.find_by_email('emp1@example.com')&.emp_id
|
171
|
+
#=> @emp1_id
|
172
|
+
|
173
|
+
## After save, duplicate instance-scoped index still caught automatically
|
174
|
+
@emp6_id = "emp_#{rand(1000000)}"
|
175
|
+
@emp6 = AutoValidEmployee.new(emp_id: @emp6_id, badge_number: 'BADGE123', email: 'emp6@example.com')
|
176
|
+
@emp6.save # Class-level index is fine
|
177
|
+
begin
|
178
|
+
@emp6.add_to_auto_valid_company_badge_index(@company) # Instance-scoped duplicate
|
179
|
+
false
|
180
|
+
rescue Familia::RecordExistsError => e
|
181
|
+
e.message.include?('badge_number=BADGE123')
|
182
|
+
end
|
183
|
+
#=> true
|
184
|
+
|
185
|
+
# =============================================
|
186
|
+
# 4. Error Message Quality
|
187
|
+
# =============================================
|
188
|
+
|
189
|
+
## Instance-scoped validation error includes both class names
|
190
|
+
begin
|
191
|
+
@emp2.badge_number = 'BADGE123' # Reset to duplicate
|
192
|
+
@emp2.add_to_auto_valid_company_badge_index(@company)
|
193
|
+
rescue Familia::RecordExistsError => e
|
194
|
+
[e.message.include?('AutoValidEmployee'), e.message.include?('AutoValidCompany')]
|
195
|
+
end
|
196
|
+
#=> [true, true]
|
197
|
+
|
198
|
+
## Instance-scoped validation error includes field name and value
|
199
|
+
begin
|
200
|
+
@emp2.add_to_auto_valid_company_badge_index(@company)
|
201
|
+
rescue Familia::RecordExistsError => e
|
202
|
+
[e.message.include?('badge_number'), e.message.include?('BADGE123')]
|
203
|
+
end
|
204
|
+
#=> [true, true]
|
205
|
+
|
206
|
+
## Error type is RecordExistsError
|
207
|
+
begin
|
208
|
+
@emp2.add_to_auto_valid_company_badge_index(@company)
|
209
|
+
rescue => e
|
210
|
+
e.class
|
211
|
+
end
|
212
|
+
#=> Familia::RecordExistsError
|
213
|
+
|
214
|
+
# =============================================
|
215
|
+
# 5. Performance - No Double Validation
|
216
|
+
# =============================================
|
217
|
+
|
218
|
+
## Manual guard call before add_to_* is redundant but harmless
|
219
|
+
@emp7_id = "emp_#{rand(1000000)}"
|
220
|
+
@emp7 = AutoValidEmployee.new(emp_id: @emp7_id, badge_number: 'BADGE777', email: 'emp7@example.com')
|
221
|
+
@emp7.guard_unique_auto_valid_company_badge_index!(@company)
|
222
|
+
@emp7.add_to_auto_valid_company_badge_index(@company)
|
223
|
+
@company.badge_index.has_key?('BADGE777')
|
224
|
+
#=> true
|
225
|
+
|
226
|
+
## Manual guard call detects duplicate
|
227
|
+
@emp8_id = "emp_#{rand(1000000)}"
|
228
|
+
@emp8 = AutoValidEmployee.new(emp_id: @emp8_id, badge_number: 'BADGE777', email: 'emp8@example.com')
|
229
|
+
begin
|
230
|
+
@emp8.guard_unique_auto_valid_company_badge_index!(@company) # Should fail - duplicate badge
|
231
|
+
false
|
232
|
+
rescue Familia::RecordExistsError
|
233
|
+
true
|
234
|
+
end
|
235
|
+
#=> true
|
236
|
+
|
237
|
+
# Teardown - clean up test objects
|
238
|
+
[@emp1, @emp2, @emp3, @emp_nil, @emp4, @emp5, @emp6, @emp7, @emp8].compact.each do |obj|
|
239
|
+
obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
|
240
|
+
end
|
241
|
+
|
242
|
+
[@user1, @user3, @user4].compact.each do |obj|
|
243
|
+
obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
|
244
|
+
end
|
245
|
+
|
246
|
+
[@company, @company2].each do |obj|
|
247
|
+
obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
|
248
|
+
end
|
249
|
+
|
250
|
+
# Clean up class-level indexes
|
251
|
+
[AutoValidEmployee.email_index, AutoValidUser.email_index].each do |index|
|
252
|
+
index.delete! if index.respond_to?(:delete!) && index.respond_to?(:exists?) && index.exists?
|
253
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/horreum/base_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -41,7 +41,7 @@ Familia.debug = false
|
|
41
41
|
|
42
42
|
## Remove the key
|
43
43
|
@hashkey.delete!
|
44
|
-
#=>
|
44
|
+
#=> 1
|
45
45
|
|
46
46
|
## Horreum objects can update and save their fields (1 of 2)
|
47
47
|
@customer.name = 'John Doe'
|
@@ -203,11 +203,11 @@ end
|
|
203
203
|
@aliased.display_size! 100
|
204
204
|
#=> true
|
205
205
|
|
206
|
-
## Aliased field refresh works correctly
|
206
|
+
## Aliased field refresh works correctly (type preserved)
|
207
207
|
@aliased.width = 50 # unsaved change
|
208
208
|
@aliased.refresh!
|
209
209
|
@aliased.width
|
210
|
-
#=>
|
210
|
+
#=> 100
|
211
211
|
|
212
212
|
## Fast method with custom name
|
213
213
|
class CustomFastMethodTest < Familia::Horreum
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# Test Horreum class methods
|
4
4
|
|
5
|
-
require_relative '
|
5
|
+
require_relative '../../support/helpers/test_helpers'
|
6
6
|
|
7
7
|
TestUser = Class.new(Familia::Horreum) do
|
8
8
|
identifier_field :email
|
@@ -16,9 +16,9 @@ module AnotherModuleName
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
## create factory method with existence checking
|
19
|
+
## create! factory method with existence checking
|
20
20
|
TestUser
|
21
|
-
#==> _.respond_to?(:create)
|
21
|
+
#==> _.respond_to?(:create!)
|
22
22
|
#==> _.respond_to?(:exists?)
|
23
23
|
|
24
24
|
## multiget method is available
|
@@ -9,7 +9,7 @@
|
|
9
9
|
# This addresses the bug where destroy! only deleted the main object key
|
10
10
|
# but left related field keys in the database.
|
11
11
|
|
12
|
-
require_relative '
|
12
|
+
require_relative '../../support/helpers/test_helpers'
|
13
13
|
|
14
14
|
MANY_FIELD_MULTIPLIER = 10
|
15
15
|
|
@@ -1,44 +1,53 @@
|
|
1
1
|
# try/horreum/field_categories_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
7
|
-
# Define test class with various field
|
7
|
+
# Define test class with various field types
|
8
8
|
class FieldCategoryTest < Familia::Horreum
|
9
|
+
feature :encrypted_fields
|
10
|
+
feature :transient_fields
|
11
|
+
|
9
12
|
identifier_field :id
|
10
13
|
field :id
|
11
|
-
field :name #
|
12
|
-
|
13
|
-
|
14
|
-
field :description
|
15
|
-
field :settings
|
14
|
+
field :name # regular field
|
15
|
+
encrypted_field :email # encrypted field
|
16
|
+
transient_field :tryouts_cache_data # transient field
|
17
|
+
field :description # regular persistent field
|
18
|
+
field :settings # regular field
|
16
19
|
end
|
17
20
|
|
18
21
|
# Test class with multiple transient fields
|
19
22
|
class MultiTransientTest < Familia::Horreum
|
23
|
+
feature :transient_fields
|
24
|
+
|
20
25
|
identifier_field :id
|
21
26
|
field :id
|
22
27
|
field :permanent_data
|
23
|
-
|
24
|
-
|
25
|
-
|
28
|
+
transient_field :temp1
|
29
|
+
transient_field :temp2
|
30
|
+
transient_field :temp3
|
26
31
|
end
|
27
32
|
|
28
|
-
# Field
|
33
|
+
# Field types work with field aliasing
|
29
34
|
class AliasedCategoryTest < Familia::Horreum
|
35
|
+
feature :transient_fields
|
36
|
+
|
30
37
|
identifier_field :id
|
31
38
|
field :id
|
32
|
-
|
33
|
-
field :internal_perm, as: :perm
|
39
|
+
transient_field :internal_temp, as: :temp
|
40
|
+
field :internal_perm, as: :perm
|
34
41
|
end
|
35
42
|
|
36
43
|
# Test edge case with all transient fields
|
37
44
|
class AllTransientTest < Familia::Horreum
|
45
|
+
feature :transient_fields
|
46
|
+
|
38
47
|
identifier_field :id
|
39
48
|
field :id
|
40
|
-
|
41
|
-
|
49
|
+
transient_field :temp1
|
50
|
+
transient_field :temp2
|
42
51
|
end
|
43
52
|
|
44
53
|
## Field types are stored correctly
|
@@ -58,11 +67,11 @@ FieldCategoryTest.field_types[:email].category
|
|
58
67
|
FieldCategoryTest.field_types[:tryouts_cache_data].category
|
59
68
|
#=> :transient
|
60
69
|
|
61
|
-
##
|
70
|
+
## Regular fields have :field category
|
62
71
|
FieldCategoryTest.field_types[:description].category
|
63
|
-
#=> :
|
72
|
+
#=> :field
|
64
73
|
|
65
|
-
##
|
74
|
+
## Regular fields default to :field category
|
66
75
|
FieldCategoryTest.field_types[:settings].category
|
67
76
|
#=> :field
|
68
77
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/horreum/initialization_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -97,11 +97,11 @@ Familia.debug = false
|
|
97
97
|
@complex.save
|
98
98
|
@complex.refresh!
|
99
99
|
[@complex.custid, @complex.name, @complex.role, @complex.verified]
|
100
|
-
#=> ["complex@test.com", "Complex User", "admin",
|
100
|
+
#=> ["complex@test.com", "Complex User", "admin", true]
|
101
101
|
|
102
102
|
## Clean up saved test objects
|
103
103
|
[@customer6, @complex].map(&:delete!)
|
104
|
-
#=> [
|
104
|
+
#=> [1, 1]
|
105
105
|
|
106
106
|
## "Cleaning up" test objects that were never saved returns true regardless
|
107
107
|
## b/c it takes place in a transaction and it's the transaction's success
|