Bug fixes#24
Conversation
Greptile SummaryThis is a comprehensive bug-fix release (v1.6.2) addressing 15 distinct issues across the
Confidence Score: 4/5This PR is safe to merge; all changes are targeted bug fixes with matching test coverage, and no regressions were identified in the core sync, STI sharing, or thread-safety paths. The changes are well-scoped and well-tested, covering 15 distinct fixes without introducing new architectural complexity. The most impactful change — the autosave cycle-detection fix that replaced lib/support_table_data.rb warrants a second look around the attribute-helper tracking logic for STI subclasses and the Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A([sync_table_data!]) --> B{table_exists?}
B -- No --> C[return empty array]
B -- Yes --> D[Build canonical_data from data files]
D --> E{delete_missing AND canonical_data empty AND rows exist?}
E -- Yes --> F[raise ArgumentError - guard against table wipe]
E -- No --> G[Query existing records where key IN canonical_data.keys]
G --> H[Begin transaction]
H --> I[Update existing records from canonical_data]
I --> J[Create new records for remaining canonical_data entries]
J --> K{delete_missing?}
K -- Yes --> L[destroy_all records not in synced_ids]
K -- No --> M[return changes array]
L --> M
H -- RecordInvalid --> N[raise ValidationError]
H -- RecordNotUnique --> O{retried?}
O -- Yes --> P[re-raise error]
O -- No --> Q[retried = true]
Q --> D
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A([sync_table_data!]) --> B{table_exists?}
B -- No --> C[return empty array]
B -- Yes --> D[Build canonical_data from data files]
D --> E{delete_missing AND canonical_data empty AND rows exist?}
E -- Yes --> F[raise ArgumentError - guard against table wipe]
E -- No --> G[Query existing records where key IN canonical_data.keys]
G --> H[Begin transaction]
H --> I[Update existing records from canonical_data]
I --> J[Create new records for remaining canonical_data entries]
J --> K{delete_missing?}
K -- Yes --> L[destroy_all records not in synced_ids]
K -- No --> M[return changes array]
L --> M
H -- RecordInvalid --> N[raise ValidationError]
H -- RecordNotUnique --> O{retried?}
O -- Yes --> P[re-raise error]
O -- No --> Q[retried = true]
Q --> D
Reviews (1): Last reviewed commit: "Bug fixes" | Re-trigger Greptile |
| class_eval <<~RUBY, __FILE__, __LINE__ + 1 | ||
| def #{method_name} | ||
| #{attribute_name} == #{attribute_value.inspect} | ||
| #{attribute_name} == self.class.type_for_attribute(#{attribute_name.inspect}).cast(#{attribute_value.inspect}) | ||
| end | ||
| RUBY |
There was a problem hiding this comment.
The predicate method body calls
type_for_attribute(...).cast(...) on every single invocation to cast a value that is a compile-time constant (the raw value from the data file). While not incorrect, this repeatedly looks up and invokes the type caster for the same literal input. The cast result could instead be pre-computed at method-definition time and inlined as a constant in the generated Ruby, avoiding the runtime overhead on every predicate call. This matters most for predicate-heavy hot paths.
| class_eval <<~RUBY, __FILE__, __LINE__ + 1 | |
| def #{method_name} | |
| #{attribute_name} == #{attribute_value.inspect} | |
| #{attribute_name} == self.class.type_for_attribute(#{attribute_name.inspect}).cast(#{attribute_value.inspect}) | |
| end | |
| RUBY | |
| cast_value = type_for_attribute(attribute_name).cast(attribute_value) | |
| class_eval <<~RUBY, __FILE__, __LINE__ + 1 | |
| def #{method_name} | |
| #{attribute_name} == #{cast_value.inspect} | |
| end | |
| RUBY |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| data = if Psych::VERSION.to_f >= 3.1 | ||
| YAML.safe_load(file_data, permitted_classes: [Date, Time], aliases: true) | ||
| else | ||
| # Positional arguments for Psych < 3.1 (Ruby 2.5). | ||
| YAML.safe_load(file_data, [Date, Time], [], true) | ||
| end |
There was a problem hiding this comment.
Fragile version comparison via
to_f
Psych::VERSION.to_f truncates everything after the first decimal point, so "3.1.0" and "3.10.0" both produce 3.1. In practice Psych hasn't released a 3.10.x, but the comparison is inherently imprecise. Consider using Gem::Version for a semantically correct comparison: Gem::Version.new(Psych::VERSION) >= Gem::Version.new("3.1").
| support_table_attribute_helpers_map.each do |attribute_name, defined_methods| | ||
| attribute_method_name = "#{method_name}_#{attribute_name}" | ||
| next if defined_methods.include?(attribute_method_name) | ||
|
|
||
| define_support_table_instance_attribute_helper(attribute_method_name, attributes[attribute_name]) | ||
| defined_methods << attribute_method_name | ||
| if defined_methods.include?(attribute_method_name) | ||
| define_support_table_instance_attribute_helper(attribute_method_name, attributes[attribute_name], redefine: true) | ||
| else | ||
| define_support_table_instance_attribute_helper(attribute_method_name, attributes[attribute_name]) | ||
| defined_methods << attribute_method_name | ||
| end | ||
| end |
There was a problem hiding this comment.
Attribute-helper tracking array mutated inside mutex but shared with STI superclass
support_table_attribute_helpers_map returns the parent's @support_table_attribute_helpers hash for STI subclasses. The defined_methods << attribute_method_name mutation therefore writes directly into the parent class's tracking array while holding the parent's mutex — correct for the common case. However, if define_support_table_named_instance_methods is ever invoked on an STI subclass (e.g. via a subclass calling named_instance_attribute_helpers), the write to @support_table_attribute_helpers on line 195 creates a new, independent copy on the subclass, while defined_methods from the previous support_table_attribute_helpers_map call still points into the parent's array. After that point the subclass's map and the parent's map diverge. This is unlikely in practice since STI subclasses should not register their own attribute helpers, but a clarifying comment would help.
Fixed
sync_table_data!withdelete_missing: truenow raises anArgumentErrorinstead of deleting every row in the table when the data files contain no rows (for example, when a data file was accidentally emptied or truncated).record.active?) now cast the data file value to the attribute type before comparing. Previously the raw file value was compared to the cast database attribute, so predicates silently returnedfalsewhenever the types differed (guaranteed for CSV data files, where all values are strings).NoMethodErrorfrominstance_names,instance_keys,protected_instance?, and other class methods. Subclasses now share the support table state defined on their base class.named_instance_attribute_helperscan now be called again with an attribute that was already registered without raising anArgumentError.Psych::AliasesNotEnabledorPsych::DisallowedClasserrors.protected_instance?no longer returns stale results when data files are added after the protected keys were first computed.sync_table_data!now retries once onActiveRecord::RecordNotUniqueerrors caused by concurrent syncs inserting the same rows from another process.sync_table_data!now returns an empty array instead ofnilwhen the table does not exist.named_instancenow raises a clearActiveRecord::RecordNotFounderror for undefined named instances instead of querying the database for anilkey (which could silently return a row with aNULLkey value).config.support_table.auto_sync = falsebefore the gem is loaded is no longer overwritten back totrueby the Railtie.SupportTableData.sync_all!to eager load models.