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
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/middleware/
|
1
|
+
# lib/middleware/database_logger.rb
|
2
2
|
|
3
3
|
require 'concurrent-ruby'
|
4
4
|
|
@@ -29,21 +29,40 @@ require 'concurrent-ruby'
|
|
29
29
|
# often outweigh the slight performance cost when enabled.
|
30
30
|
module DatabaseLogger
|
31
31
|
@logger = nil
|
32
|
-
@commands =
|
32
|
+
@commands = Concurrent::Array.new
|
33
|
+
@max_commands = 10_000
|
34
|
+
@process_start = Time.now.to_f.freeze
|
35
|
+
|
36
|
+
CommandMessage = Data.define(:command, :μs, :timeline) do
|
37
|
+
alias_method :to_a, :deconstruct
|
38
|
+
def inspect
|
39
|
+
cmd, duration, timeline = to_a
|
40
|
+
format('%.6f %4dμs > %s', timeline, duration, cmd)
|
41
|
+
end
|
42
|
+
end
|
33
43
|
|
34
44
|
class << self
|
35
45
|
# Gets/sets the logger instance used by DatabaseLogger.
|
36
46
|
# @return [Logger, nil] The current logger instance or nil if not set.
|
37
47
|
attr_accessor :logger
|
38
48
|
|
49
|
+
# Gets/sets the maximum number of commands to capture.
|
50
|
+
# @return [Integer] The maximum number of commands to capture.
|
51
|
+
attr_accessor :max_commands
|
52
|
+
|
39
53
|
# Gets the captured commands for testing purposes.
|
40
|
-
# @return [Array] Array of command hashes with :command, :duration, :
|
54
|
+
# @return [Array] Array of command hashes with :command, :duration, :timeline
|
41
55
|
attr_reader :commands
|
42
56
|
|
57
|
+
# Gets the timestamp when DatabaseLogger was loaded.
|
58
|
+
# @return [Float] The timestamp when DatabaseLogger was loaded.
|
59
|
+
attr_reader :process_start
|
60
|
+
|
43
61
|
# Clears the captured commands array.
|
44
62
|
# @return [Array] Empty array
|
45
63
|
def clear_commands
|
46
|
-
@commands
|
64
|
+
@commands.clear
|
65
|
+
nil
|
47
66
|
end
|
48
67
|
|
49
68
|
# Captures commands in a block and returns them.
|
@@ -62,8 +81,34 @@ module DatabaseLogger
|
|
62
81
|
def capture_commands
|
63
82
|
clear_commands
|
64
83
|
yield
|
65
|
-
@commands.
|
84
|
+
@commands.to_a
|
85
|
+
end
|
86
|
+
|
87
|
+
# Gets the current count of Database commands executed.
|
88
|
+
# @return [Integer] The number of Database commands executed.
|
89
|
+
def index
|
90
|
+
@commands.size
|
91
|
+
end
|
92
|
+
|
93
|
+
# Thread-safe append with bounded size
|
94
|
+
#
|
95
|
+
# @param message [String] The message to append.
|
96
|
+
# @return [Array] The updated array of commands.
|
97
|
+
def append_command(message)
|
98
|
+
@commands.shift if @commands.size >= @max_commands
|
99
|
+
@commands << message
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns the current time in microseconds.
|
103
|
+
# This is used to measure the duration of Database commands.
|
104
|
+
#
|
105
|
+
# Alias: now_in_microseconds
|
106
|
+
#
|
107
|
+
# @return [Integer] The current time in microseconds.
|
108
|
+
def now_in_μs
|
109
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
66
110
|
end
|
111
|
+
alias now_in_microseconds now_in_μs
|
67
112
|
end
|
68
113
|
|
69
114
|
# Logs the Database command and its execution time.
|
@@ -79,19 +124,65 @@ module DatabaseLogger
|
|
79
124
|
# @note Commands are always captured with minimal overhead for testing purposes.
|
80
125
|
# Logging only occurs when DatabaseLogger.logger is set.
|
81
126
|
def call(command, _config)
|
82
|
-
|
127
|
+
block_start = DatabaseLogger.now_in_μs
|
83
128
|
result = yield
|
84
|
-
|
129
|
+
block_duration = DatabaseLogger.now_in_μs - block_start
|
130
|
+
|
131
|
+
# We intentionally use two different codepaths for getting the
|
132
|
+
# time, although they will almost always be so similar that the
|
133
|
+
# difference is negligible.
|
134
|
+
lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
|
85
135
|
|
86
|
-
|
87
|
-
DatabaseLogger.
|
88
|
-
command: command.dup,
|
89
|
-
duration: duration,
|
90
|
-
timestamp: Time.now,
|
91
|
-
}
|
136
|
+
msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
|
137
|
+
DatabaseLogger.append_command(msgpack)
|
92
138
|
|
93
139
|
# Log if logger is set
|
94
|
-
DatabaseLogger.
|
140
|
+
message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
|
141
|
+
DatabaseLogger.logger&.trace(message)
|
142
|
+
|
143
|
+
result
|
144
|
+
end
|
145
|
+
|
146
|
+
# Handle pipelined commands (including MULTI/EXEC transactions)
|
147
|
+
#
|
148
|
+
# Captures MULTI/EXEC and shows you the full transaction. The WATCH
|
149
|
+
# and EXISTS appear separately because they're executed as individual
|
150
|
+
# commands before the transaction starts.
|
151
|
+
def call_pipelined(commands, _config)
|
152
|
+
block_start = DatabaseLogger.now_in_μs
|
153
|
+
results = yield
|
154
|
+
block_duration = DatabaseLogger.now_in_μs - block_start
|
155
|
+
lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
|
156
|
+
|
157
|
+
# Log the entire pipeline as a single operation
|
158
|
+
cmd_string = commands.map { |cmd| cmd.join(' ') }.join(' | ')
|
159
|
+
msgpack = CommandMessage.new(cmd_string, block_duration, lifetime_duration)
|
160
|
+
DatabaseLogger.append_command(msgpack)
|
161
|
+
|
162
|
+
message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
|
163
|
+
DatabaseLogger.logger&.trace(message)
|
164
|
+
|
165
|
+
results
|
166
|
+
end
|
167
|
+
|
168
|
+
# call_once is used for commands that need dedicated connection handling:
|
169
|
+
#
|
170
|
+
# * Blocking commands (BLPOP, BRPOP, BRPOPLPUSH)
|
171
|
+
# * Pub/sub operations (SUBSCRIBE, PSUBSCRIBE)
|
172
|
+
# * Commands requiring connection affinity
|
173
|
+
# * Explicit non-pooled command execution
|
174
|
+
#
|
175
|
+
def call_once(command, _config)
|
176
|
+
block_start = DatabaseLogger.now_in_μs
|
177
|
+
result = yield
|
178
|
+
block_duration = DatabaseLogger.now_in_μs - block_start
|
179
|
+
lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
|
180
|
+
|
181
|
+
msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
|
182
|
+
DatabaseLogger.append_command(msgpack)
|
183
|
+
|
184
|
+
message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
|
185
|
+
DatabaseLogger.logger&.trace(message)
|
95
186
|
|
96
187
|
result
|
97
188
|
end
|
@@ -188,5 +279,18 @@ module DatabaseCommandCounter
|
|
188
279
|
klass.increment unless klass.skip_command?(command)
|
189
280
|
yield
|
190
281
|
end
|
282
|
+
|
283
|
+
def call_pipelined(commands, _config)
|
284
|
+
# Count all commands in the pipeline (except skipped ones)
|
285
|
+
commands.each do |command|
|
286
|
+
klass.increment unless klass.skip_command?(command)
|
287
|
+
end
|
288
|
+
yield
|
289
|
+
end
|
290
|
+
|
291
|
+
def call_once(command, _config)
|
292
|
+
klass.increment unless klass.skip_command?(command)
|
293
|
+
yield
|
294
|
+
end
|
191
295
|
end
|
192
296
|
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
data/pr_agent.toml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Qodo Merge Configuration
|
2
|
+
# Documentation: https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/
|
3
|
+
|
4
|
+
[config]
|
5
|
+
# Ensure consistent review language across all PRs
|
6
|
+
response_language = "en"
|
7
|
+
|
8
|
+
[rag_arguments]
|
9
|
+
# Enable RAG context enrichment for codebase duplication compliance checks
|
10
|
+
enable_rag = true
|
11
|
+
# Include related repositories for comprehensive context
|
12
|
+
rag_repo_list = ['delano/familia', 'delano/tryouts', 'delano/otto']
|
13
|
+
|
14
|
+
[compliance]
|
15
|
+
# Reference custom compliance checklist for project-specific rules
|
16
|
+
custom_compliance_path = "pr_compliance_checklist.yaml"
|
17
|
+
|
18
|
+
[ignore]
|
19
|
+
# Reduce noise by excluding generated files and build artifacts
|
20
|
+
glob = [
|
21
|
+
"*.lock", # Lock files (Gemfile.lock, etc.)
|
22
|
+
"*.gem", # Built gem files
|
23
|
+
"vendor/**", # Vendored dependencies
|
24
|
+
"tmp/**", # Temporary files
|
25
|
+
"log/**", # Log files
|
26
|
+
"data/**", # Data directories
|
27
|
+
"public/**", # Public assets
|
28
|
+
".yardoc/**", # YARD documentation cache
|
29
|
+
"dump.rdb", # Redis database dumps
|
30
|
+
"appendonlydir/**", # Redis append-only file directory
|
31
|
+
]
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Custom Compliance Checklist for Familia
|
2
|
+
# Documentation: https://qodo-merge-docs.qodo.ai/tools/compliance/
|
3
|
+
|
4
|
+
pr_compliances:
|
5
|
+
- title: "ErrorHandling"
|
6
|
+
compliance_label: true
|
7
|
+
objective: "All external API calls and database operations must have proper error handling"
|
8
|
+
success_criteria: "Try-catch blocks around external calls with appropriate logging or error handling mechanisms"
|
9
|
+
failure_criteria: "External API calls, database operations, or network requests without error handling"
|
10
|
+
|
11
|
+
- title: "TestCoverage"
|
12
|
+
compliance_label: true
|
13
|
+
objective: "New features must include corresponding tests using the Tryouts framework"
|
14
|
+
success_criteria: "Test files present in try/ directory for new functionality following *_try.rb or *.try.rb naming convention"
|
15
|
+
failure_criteria: "New code without test coverage or tests not following Tryouts framework conventions"
|
16
|
+
|
17
|
+
- title: "ChangelogFragment"
|
18
|
+
compliance_label: true
|
19
|
+
objective: "User-facing changes must include a changelog"
|
20
|
+
success_criteria: "New fragment file in changelog.d/ directory following the naming convention and RST format, or updates to CHANGELOG.rst in root directory, or explicit justification for omission"
|
21
|
+
failure_criteria: "User-facing changes without changelog fragment, CHANGELOG.rst updates, or documentation updates"
|
22
|
+
|
23
|
+
- title: "DocumentationUpdates"
|
24
|
+
compliance_label: true
|
25
|
+
objective: "API changes must be reflected in documentation"
|
26
|
+
success_criteria: "YARD documentation comments for new public methods, or updates to docs/ for significant changes"
|
27
|
+
failure_criteria: "New public APIs or significant behavior changes without documentation updates"
|
28
|
+
|
29
|
+
- title: "BackwardCompatibility"
|
30
|
+
compliance_label: true
|
31
|
+
objective: "Changes must maintain backward compatibility or document breaking changes"
|
32
|
+
success_criteria: "No breaking changes to public APIs, or breaking changes clearly documented in migration guides"
|
33
|
+
failure_criteria: "Breaking changes without migration documentation or deprecation warnings"
|
34
|
+
|
35
|
+
- title: "ThreadSafety"
|
36
|
+
compliance_label: true
|
37
|
+
objective: "Code handling shared state must be thread-safe"
|
38
|
+
success_criteria: "Proper synchronization for shared mutable state, or clear documentation of thread-safety assumptions"
|
39
|
+
failure_criteria: "Shared mutable state accessed without synchronization in concurrent contexts"
|
40
|
+
|
41
|
+
- title: "DatabaseKeyNaming"
|
42
|
+
compliance_label: true
|
43
|
+
objective: "Database key generation must follow Familia conventions"
|
44
|
+
success_criteria: "Keys use delim separator, avoid reserved keywords (ttl, db, valkey, redis), and handle empty identifiers"
|
45
|
+
failure_criteria: "Keys using reserved keywords, empty identifiers, or non-standard separators"
|
@@ -4,7 +4,7 @@
|
|
4
4
|
# bug in Tryouts 3.1 that prevents the setup instance vars from
|
5
5
|
# being available to the testcases.
|
6
6
|
|
7
|
-
require_relative '../helpers/test_helpers'
|
7
|
+
require_relative '../support/helpers/test_helpers'
|
8
8
|
|
9
9
|
Familia.debug = false
|
10
10
|
|
@@ -25,22 +25,31 @@ end
|
|
25
25
|
@test_hash.keys
|
26
26
|
#=> ["name", "age", "nested"]
|
27
27
|
|
28
|
-
## After save and refresh, default behavior uses
|
28
|
+
## After save and refresh, default behavior uses string keys
|
29
29
|
@test_obj.refresh!
|
30
30
|
@test_obj.config.keys
|
31
|
-
#=> [
|
31
|
+
#=> ["name", "age", "nested"]
|
32
32
|
|
33
|
-
## Nested hash also has
|
34
|
-
@test_obj.config[
|
35
|
-
#=> [
|
33
|
+
## Nested hash also has string keys
|
34
|
+
@test_obj.config["nested"].keys
|
35
|
+
#=> ["theme"]
|
36
36
|
|
37
37
|
## Get raw JSON from Valkey/Redis
|
38
38
|
@raw_json = @test_obj.hget('config')
|
39
39
|
@raw_json.class
|
40
40
|
#=> String
|
41
41
|
|
42
|
-
## deserialize_value with default symbolize:
|
43
|
-
@
|
42
|
+
## deserialize_value with default symbolize: false returns string keys
|
43
|
+
@string_result_default = @test_obj.deserialize_value(@raw_json)
|
44
|
+
@string_result_default.keys
|
45
|
+
#=> ["name", "age", "nested"]
|
46
|
+
|
47
|
+
## Nested hash in default result also has string keys
|
48
|
+
@string_result_default["nested"].keys
|
49
|
+
#=> ["theme"]
|
50
|
+
|
51
|
+
## deserialize_value with symbolize: true returns symbol keys
|
52
|
+
@symbol_result = @test_obj.deserialize_value(@raw_json, symbolize: true)
|
44
53
|
@symbol_result.keys
|
45
54
|
#=> [:name, :age, :nested]
|
46
55
|
|
@@ -48,21 +57,12 @@ end
|
|
48
57
|
@symbol_result[:nested].keys
|
49
58
|
#=> [:theme]
|
50
59
|
|
51
|
-
##
|
52
|
-
@string_result = @test_obj.deserialize_value(@raw_json, symbolize: false)
|
53
|
-
@string_result.keys
|
54
|
-
#=> ["name", "age", "nested"]
|
55
|
-
|
56
|
-
## Nested hash in string result also has string keys
|
57
|
-
@string_result['nested'].keys
|
58
|
-
#=> ["theme"]
|
59
|
-
|
60
|
-
## Values are preserved correctly in both cases
|
60
|
+
## Values are preserved correctly with symbol keys
|
61
61
|
@symbol_result[:name]
|
62
62
|
#=> "John"
|
63
63
|
|
64
|
-
##
|
65
|
-
@
|
64
|
+
## Values are preserved correctly with string keys
|
65
|
+
@string_result_default['name']
|
66
66
|
#=> "John"
|
67
67
|
|
68
68
|
## Arrays are handled correctly too
|
@@ -71,27 +71,27 @@ end
|
|
71
71
|
@array_json = @test_obj.hget('config')
|
72
72
|
#=> "[{\"item\":\"value\"},\"string\",123]"
|
73
73
|
|
74
|
+
## Array with default (symbolize: false) keeps hash keys as strings
|
75
|
+
@string_array_default = @test_obj.deserialize_value(@array_json)
|
76
|
+
@string_array_default[0].keys
|
77
|
+
#=> ["item"]
|
78
|
+
|
74
79
|
## Array with symbolize: true converts hash keys to symbols
|
75
|
-
@symbol_array = @test_obj.deserialize_value(@array_json)
|
80
|
+
@symbol_array = @test_obj.deserialize_value(@array_json, symbolize: true)
|
76
81
|
@symbol_array[0].keys
|
77
82
|
#=> [:item]
|
78
83
|
|
79
|
-
##
|
80
|
-
@string_array = @test_obj.deserialize_value(@array_json, symbolize: false)
|
81
|
-
@string_array[0].keys
|
82
|
-
#=> ["item"]
|
83
|
-
|
84
|
-
## Non-hash/array values are returned as-is
|
84
|
+
## JSON-encoded string is parsed correctly
|
85
85
|
@test_obj.deserialize_value('"just a string"')
|
86
|
-
#=> "
|
86
|
+
#=> "just a string"
|
87
87
|
|
88
|
-
## Non-
|
88
|
+
## Non-JSON string returns as-is
|
89
89
|
@test_obj.deserialize_value('just a string')
|
90
90
|
#=> "just a string"
|
91
91
|
|
92
|
-
##
|
92
|
+
## JSON number is parsed to Integer
|
93
93
|
@test_obj.deserialize_value('42')
|
94
|
-
#=>
|
94
|
+
#=> 42
|
95
95
|
|
96
96
|
## Invalid JSON returns original string
|
97
97
|
@test_obj.deserialize_value('invalid json')
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/edge_cases/json_serialization_try.rb
|
2
2
|
|
3
|
-
require_relative '../helpers/test_helpers'
|
3
|
+
require_relative '../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -44,7 +44,7 @@ test_obj.simple = 'just a string'
|
|
44
44
|
test_obj.tags = %w[ruby valkey json familia]
|
45
45
|
test_obj.save
|
46
46
|
test_obj.hgetall
|
47
|
-
#=> {"id"=>"json_test_1", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"valkey\",\"json\",\"familia\"]", "simple"=>"just a string"}
|
47
|
+
#=> {"id"=>"\"json_test_1\"", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"valkey\",\"json\",\"familia\"]", "simple"=>"\"just a string\""}
|
48
48
|
|
49
49
|
## Test 4: Hash should be deserialized back to Hash
|
50
50
|
test_obj = JsonTest.new 'any_id_will_do'
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# Edge case tests for deserialize_value with legacy data detection
|
2
|
+
#
|
3
|
+
# Tests the nuanced deserialization that distinguishes between:
|
4
|
+
# - Corrupted JSON (data that looks like JSON but fails to parse)
|
5
|
+
# - Legacy plain strings (data that was never JSON)
|
6
|
+
# - Valid JSON data
|
7
|
+
|
8
|
+
require_relative '../../../lib/familia'
|
9
|
+
require 'logger'
|
10
|
+
require 'stringio'
|
11
|
+
|
12
|
+
# Capture log output for verification
|
13
|
+
@log_output = StringIO.new
|
14
|
+
@original_logger = Familia.instance_variable_get(:@logger)
|
15
|
+
Familia.instance_variable_set(:@logger, Logger.new(@log_output))
|
16
|
+
Familia.instance_variable_get(:@logger).level = Logger::DEBUG
|
17
|
+
|
18
|
+
class TestModel < Familia::Horreum
|
19
|
+
identifier_field :test_id
|
20
|
+
field :test_id
|
21
|
+
field :data
|
22
|
+
end
|
23
|
+
|
24
|
+
@model = TestModel.new(test_id: "test1")
|
25
|
+
|
26
|
+
## Valid JSON number deserializes correctly
|
27
|
+
@result = @model.deserialize_value("123", field_name: :data)
|
28
|
+
@result
|
29
|
+
#=> 123
|
30
|
+
|
31
|
+
## Valid JSON boolean deserializes correctly
|
32
|
+
@result = @model.deserialize_value("true", field_name: :data)
|
33
|
+
@result
|
34
|
+
#=> true
|
35
|
+
|
36
|
+
## Valid JSON string deserializes correctly
|
37
|
+
@result = @model.deserialize_value('"hello"', field_name: :data)
|
38
|
+
@result
|
39
|
+
#=> "hello"
|
40
|
+
|
41
|
+
## Valid JSON array deserializes correctly
|
42
|
+
@result = @model.deserialize_value('[1,2,3]', field_name: :data)
|
43
|
+
@result
|
44
|
+
#=> [1, 2, 3]
|
45
|
+
|
46
|
+
## Valid JSON object deserializes correctly
|
47
|
+
@result = @model.deserialize_value('{"key":"value"}', field_name: :data)
|
48
|
+
@result
|
49
|
+
#=> {"key"=>"value"}
|
50
|
+
|
51
|
+
## Plain string (legacy data) returns as-is
|
52
|
+
@log_output = StringIO.new
|
53
|
+
Familia.instance_variable_set(:@logger, Logger.new(@log_output))
|
54
|
+
Familia.instance_variable_get(:@logger).level = Logger::DEBUG
|
55
|
+
@result = @model.deserialize_value("plain text", field_name: :data)
|
56
|
+
@result
|
57
|
+
#=> "plain text"
|
58
|
+
|
59
|
+
## Legacy data logs at debug level
|
60
|
+
@log_output.rewind
|
61
|
+
@log_content = @log_output.read
|
62
|
+
puts "LOG CONTENT: #{@log_content.inspect}" if ENV['DEBUG']
|
63
|
+
@log_content
|
64
|
+
#=~> /Legacy plain string/
|
65
|
+
|
66
|
+
## Corrupted JSON starting with { logs error
|
67
|
+
@log_output = StringIO.new
|
68
|
+
Familia.instance_variable_set(:@logger, Logger.new(@log_output))
|
69
|
+
Familia.instance_variable_get(:@logger).level = Logger::DEBUG
|
70
|
+
@result = @model.deserialize_value("{broken", field_name: :data)
|
71
|
+
@result
|
72
|
+
#=> "{broken"
|
73
|
+
|
74
|
+
## Corrupted JSON logs at error level
|
75
|
+
@log_output.rewind
|
76
|
+
@log_content = @log_output.read
|
77
|
+
puts "LOG CONTENT: #{@log_content.inspect}" if ENV['DEBUG']
|
78
|
+
@log_content.match?(/Corrupted JSON/)
|
79
|
+
#=> true
|
80
|
+
|
81
|
+
## Corrupted JSON starting with [ logs error
|
82
|
+
@log_output = StringIO.new
|
83
|
+
Familia.instance_variable_set(:@logger, Logger.new(@log_output))
|
84
|
+
Familia.instance_variable_get(:@logger).level = Logger::DEBUG
|
85
|
+
@result = @model.deserialize_value("[1,2,", field_name: :data)
|
86
|
+
@result
|
87
|
+
#=> "[1,2,"
|
88
|
+
|
89
|
+
## Corrupted array logs at error level
|
90
|
+
@log_output.rewind
|
91
|
+
@log_content = @log_output.read
|
92
|
+
@log_content.match?(/Corrupted JSON/)
|
93
|
+
#=> true
|
94
|
+
|
95
|
+
## Corrupted JSON starting with quote logs error
|
96
|
+
@log_output = StringIO.new
|
97
|
+
Familia.instance_variable_set(:@logger, Logger.new(@log_output))
|
98
|
+
Familia.instance_variable_get(:@logger).level = Logger::DEBUG
|
99
|
+
@result = @model.deserialize_value('"unterminated', field_name: :data)
|
100
|
+
@result
|
101
|
+
#=> '"unterminated'
|
102
|
+
|
103
|
+
## Unterminated string logs at error level
|
104
|
+
@log_output.rewind
|
105
|
+
@log_content = @log_output.read
|
106
|
+
@log_content.match?(/Corrupted JSON/)
|
107
|
+
#=> true
|
108
|
+
|
109
|
+
## Corrupted boolean-like value logs error
|
110
|
+
@log_output = StringIO.new
|
111
|
+
Familia.instance_variable_set(:@logger, Logger.new(@log_output))
|
112
|
+
Familia.instance_variable_get(:@logger).level = Logger::DEBUG
|
113
|
+
@result = @model.deserialize_value("true123", field_name: :data)
|
114
|
+
@result
|
115
|
+
#=> "true123"
|
116
|
+
|
117
|
+
## Plain text starting with 'true' is legacy data
|
118
|
+
@log_output.rewind
|
119
|
+
@log_content = @log_output.read
|
120
|
+
@log_content
|
121
|
+
#=~> /Legacy plain string/
|
122
|
+
|
123
|
+
## Field name context appears in error messages
|
124
|
+
@log_output = StringIO.new
|
125
|
+
Familia.instance_variable_set(:@logger, Logger.new(@log_output))
|
126
|
+
Familia.instance_variable_get(:@logger).level = Logger::DEBUG
|
127
|
+
@result = @model.deserialize_value("{broken", field_name: :important_field)
|
128
|
+
@log_output.rewind
|
129
|
+
@log_content = @log_output.read
|
130
|
+
@log_content.match?(/TestModel#important_field/)
|
131
|
+
#=> true
|
132
|
+
|
133
|
+
## dbkey context appears in error messages when available
|
134
|
+
@model.save
|
135
|
+
@log_output = StringIO.new
|
136
|
+
Familia.instance_variable_set(:@logger, Logger.new(@log_output))
|
137
|
+
Familia.instance_variable_get(:@logger).level = Logger::DEBUG
|
138
|
+
@result = @model.deserialize_value("{broken", field_name: :data)
|
139
|
+
@log_output.rewind
|
140
|
+
@log_content = @log_output.read
|
141
|
+
@log_content.match?(/#{Regexp.escape(@model.dbkey)}/)
|
142
|
+
#=> true
|
143
|
+
|
144
|
+
## Empty string returns nil
|
145
|
+
@result = @model.deserialize_value("", field_name: :data)
|
146
|
+
@result
|
147
|
+
#=> nil
|
148
|
+
|
149
|
+
## nil returns nil
|
150
|
+
@result = @model.deserialize_value(nil, field_name: :data)
|
151
|
+
@result
|
152
|
+
#=> nil
|
153
|
+
|
154
|
+
## JSON null deserializes to nil
|
155
|
+
@result = @model.deserialize_value("null", field_name: :data)
|
156
|
+
@result
|
157
|
+
#=> nil
|
158
|
+
|
159
|
+
## Symbolize option works with hash keys
|
160
|
+
@result = @model.deserialize_value('{"name":"test"}', symbolize: true, field_name: :data)
|
161
|
+
@result.keys.first.class
|
162
|
+
#=> Symbol
|
163
|
+
|
164
|
+
## Default keeps string keys
|
165
|
+
@result = @model.deserialize_value('{"name":"test"}', field_name: :data)
|
166
|
+
@result.keys.first.class
|
167
|
+
#=> String
|
168
|
+
|
169
|
+
# Teardown
|
170
|
+
Familia.instance_variable_set(:@logger, @original_logger)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/edge_cases/string_coercion_try.rb
|
2
2
|
|
3
|
-
require_relative '../helpers/test_helpers'
|
3
|
+
require_relative '../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -130,15 +130,15 @@ process_identifier(@customer)
|
|
130
130
|
|
131
131
|
## Cleanup after test, 1
|
132
132
|
@metadata.delete!
|
133
|
-
#=>
|
133
|
+
#=> 1
|
134
134
|
|
135
135
|
## Cleanup after test, 2
|
136
136
|
@customer.delete!
|
137
|
-
#=>
|
137
|
+
#=> 1
|
138
138
|
|
139
139
|
## Cleanup after test, 3
|
140
140
|
@session.delete!
|
141
|
-
#=>
|
141
|
+
#=> 1
|
142
142
|
|
143
143
|
## to_s handles identifier errors gracefully
|
144
144
|
badboi = BadIdentifierTest.new
|
@@ -154,4 +154,4 @@ badboi.to_s # .include?('BadIdentifierTest')
|
|
154
154
|
|
155
155
|
## Delete customer2
|
156
156
|
[@customer2.exists?, @customer2.delete!]
|
157
|
-
#=> [false,
|
157
|
+
#=> [false, 0]
|