Skip to content

Bug fixes#24

Open
bdurand wants to merge 1 commit into
mainfrom
detailed-review
Open

Bug fixes#24
bdurand wants to merge 1 commit into
mainfrom
detailed-review

Conversation

@bdurand

@bdurand bdurand commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Fixed

  • sync_table_data! with delete_missing: true now raises an ArgumentError instead 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).
  • Generated predicate methods (e.g. 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 returned false whenever the types differed (guaranteed for CSV data files, where all values are strings).
  • Single table inheritance subclasses no longer raise NoMethodError from instance_names, instance_keys, protected_instance?, and other class methods. Subclasses now share the support table state defined on their base class.
  • Named instance helper methods are now redefined when a later data file overrides an attribute or key value, so the helpers always return the merged values that are synced to the database. Previously they permanently returned the values from the first file that defined the named instance.
  • named_instance_attribute_helpers can now be called again with an attribute that was already registered without raising an ArgumentError.
  • YAML data files can now use anchors/aliases and date/time values. Previously these raised Psych::AliasesNotEnabled or Psych::DisallowedClass errors.
  • protected_instance? no longer returns stale results when data files are added after the protected keys were first computed.
  • Fixed broken cycle detection in the autosave association check during syncs that could cause infinite recursion on cyclic autosave associations.
  • sync_table_data! now retries once on ActiveRecord::RecordNotUnique errors caused by concurrent syncs inserting the same rows from another process.
  • sync_table_data! now returns an empty array instead of nil when the table does not exist.
  • named_instance now raises a clear ActiveRecord::RecordNotFound error for undefined named instances instead of querying the database for a nil key (which could silently return a row with a NULL key value).
  • Memoized class-level state is now consistently synchronized with the class mutex to avoid races on non-MRI Ruby implementations.
  • Setting config.support_table.auto_sync = false before the gem is loaded is no longer overwritten back to true by the Railtie.
  • The documentation tasks no longer corrupt model source files that contain duplicated generated YARD doc blocks (e.g. from a bad merge); the regex that finds the generated block is no longer greedy.
  • Data file names containing extra dots no longer break the class name detection used by SupportTableData.sync_all! to eager load models.
  • Error messages for invalid named instance definitions now include the model class name instead of repeating the instance name.

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown

Greptile Summary

This is a comprehensive bug-fix release (v1.6.2) addressing 15 distinct issues across the SupportTableData concern, covering correctness, thread safety, STI support, YAML parsing, and documentation tooling.

  • Core sync logic: sync_table_data! gains an empty-data safeguard (raises ArgumentError instead of wiping the table), a single-retry on ActiveRecord::RecordNotUnique, and a corrected return type ([] instead of nil) when the table doesn't exist.
  • STI and thread safety: Private reader methods (support_table_mutex, support_table_data_files, etc.) now fall back to the superclass chain so STI subclasses share their base class's state, and memoized state is consistently guarded by the mutex to avoid races on non-MRI Ruby.
  • Method helpers and predicates: Named-instance helper methods are now redefined when a later data file overrides a value, predicate methods now cast the raw file value to the column type before comparing, and re-registering an already-registered attribute helper no longer raises ArgumentError.

Confidence Score: 4/5

This 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 seen << self (the class) with seen << record (the instance) — correctly resolves what was effectively broken cycle tracking. The remaining observations are stylistic choices and one low-probability divergence scenario for STI subclasses calling named_instance_attribute_helpers, neither of which affects the happy path.

lib/support_table_data.rb warrants a second look around the attribute-helper tracking logic for STI subclasses and the Psych::VERSION.to_f version comparison, though neither represents a blocking concern for the supported use cases.

Important Files Changed

Filename Overview
lib/support_table_data.rb Core concern file with the most changes: STI fallback readers, mutex-guarded memoization, autosave cycle-detection fix, predicate type-casting, retry logic, empty-data safeguard, and named_instance guard — all logically sound.
lib/support_table_data/railtie.rb Corrects the auto_sync initialization from `
lib/support_table_data/documentation/source_file.rb YARD regex changed from greedy .* to non-greedy .*? to prevent swallowing user code between duplicated generated blocks.
lib/support_table_data/tasks/utils.rb STI subclass skip logic now uses instance_variable_defined?(:@support_table_data_files) instead of rescuing NoMethodError — cleaner and correct.
spec/support_table_data_spec.rb Adds comprehensive tests for all fixed behaviors: empty-data safeguard, retry, predicate casting, YAML aliases/dates, attribute helper redefinition, STI sharing, and stale-cache invalidation.
spec/models/size.rb New test model supporting YAML alias/date tests and attribute helper override tests.

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
Loading
%%{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
Loading

Reviews (1): Last reviewed commit: "Bug fixes" | Re-trigger Greptile

Comment thread lib/support_table_data.rb
Comment on lines 434 to 438
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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!

Comment thread lib/support_table_data.rb
Comment on lines +459 to +464
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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").

Comment thread lib/support_table_data.rb
Comment on lines +388 to 396
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant