Skip to content

Load the record class when Active Record loads - #44

Merged
cardmagic merged 2 commits into
mainfrom
fix/defer-active-record-load
Aug 18, 2026
Merged

Load the record class when Active Record loads#44
cardmagic merged 2 commits into
mainfrom
fix/defer-active-record-load

Conversation

@cardmagic

Copy link
Copy Markdown
Owner

Fixes #38.

Cause

lib/solid_objects/engine.rb required app/models/solid_objects/record.rb at
the top of the file, and SolidObjects::Record is a subclass of
ActiveRecord::Base. Bundler.require runs that before the host application
boots, so ActiveRecord::Base loaded during railtie setup instead of on first
use.

That moved every deferred on_load(:active_record) hook in the host
application forward. The Active Record railtie registers its encryption
configuration inside on_load(:active_record_encryption). The constant was
already loaded, so that hook ran at once, before config/initializers. An
application that assigns its encryption keys in an initializer got
Encryption.configure with nils, and the later assignment was discarded.

Change

  • The engine requires the record class from an ActiveSupport.on_load(:active_record)
    hook, so nothing touches ActiveRecord::Base until the application does.
  • The record class applies the configured connects_to in its class body.
    The initializer could no longer call configure_connection: Zeitwerk can
    load record.rb first, and the superclass expression there is what loads
    ActiveRecord::Base, so the hook would run while SolidObjects::Record was
    still undefined. Reading the configuration in the class body also survives a
    development reload, which the initializer did not.

Tests

test/integration/active_record_load_timing_test.rb is new. One test asks a
fresh process whether require "solid_objects" leaves the :active_record
load hooks deferred. The other boots the dummy application, which now assigns
encryption keys in config/initializers, and reads the key back.

test/integration/engine_test.rb gains a test that boots the dummy
application with connects_to configured in an initializer and asserts the
record class owns its own connection pool. That test passes on main, so it
records the behaviour the class-body change had to keep.

Observed failures before the fix:

ActiveRecordLoadTimingTest#test_requiring_the_gem_leaves_the_active_record_load_hooks_deferred
the gem loads ActiveRecord::Base at require time, so host application hooks run too early.
Expected: "deferred"
  Actual: "immediate"

ActiveRecordLoadTimingTest#test_an_application_initializer_configures_Active_Record_encryption
active_record/encryption/config.rb:44: Missing Active Record encryption credential:
active_record_encryption.primary_key (ActiveRecord::Encryption::Errors::Configuration)

With the engine fix in place but configure_connection removed from the record
class body:

EngineTest#test_connects_the_record_class_to_the_configured_database
--- expected
+++ actual
-"ActiveRecord::Base SolidObjects::Record"
+"ActiveRecord::Base"

Validation

bundle exec rake (tests, Standard Ruby, RuboCop, RBS, Steep, Brakeman):
535 runs, 1768 assertions, 0 failures, 0 errors, 15 skips. The skip count
matches main.

The engine required app/models/solid_objects/record.rb at the top of the
file, so `require "solid_objects"` defined a subclass of
ActiveRecord::Base. Bundler.require runs that before the host
application boots, which loaded ActiveRecord::Base during railtie setup
instead of on first use.

That moved every deferred on_load(:active_record) hook in the host
application forward. The Active Record railtie registers its encryption
configuration inside on_load(:active_record_encryption), and the
constant was already loaded, so the hook ran at once, before
config/initializers. An application that assigns its encryption keys in
an initializer got Encryption.configure with nils, and every encrypted
attribute then raised a missing credential error far from the cause.

The engine now requires the record class from an
on_load(:active_record) hook, so nothing touches ActiveRecord::Base
until the application does.

The record class also configures its own connection now. The initializer
could no longer call configure_connection: Zeitwerk can load
record.rb first, and the superclass expression there is what loads
ActiveRecord::Base, so the hook would run while SolidObjects::Record was
still undefined. Reading the configuration in the class body also
survives a development reload, which the initializer did not.

Fixes #38
@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown

Greptile Summary

The PR defers loading SolidObjects::Record until Active Record loads, preserving host initializer timing while moving connection setup into the record class so it is reapplied when that class loads.

  • Replaces the engine’s eager model require with an ActiveSupport.on_load(:active_record) hook.
  • Applies configured connects_to settings from the record class body.
  • Adds subprocess integration coverage for deferred loading, encryption initialization, and connection-pool ownership.
  • Updates generated signatures and the changelog.

Confidence Score: 5/5

The PR appears safe to merge with no actionable correctness, security, or repository-rule violations identified.

The new hook preserves Active Record’s deferred initialization, the class-body connection setup handles both hook-driven and autoload-driven class definition, and the packaged gem retains the required model path.

Important Files Changed

Filename Overview
lib/solid_objects/engine.rb Defers loading the record model through an Active Record load hook, preventing premature execution of host load hooks.
app/models/solid_objects/record.rb Applies optional connection configuration when the record class is defined, covering both deferred and Zeitwerk-driven loading.
test/integration/active_record_load_timing_test.rb Adds fresh-process regression coverage for deferred Active Record loading and initializer-provided encryption credentials.
test/integration/engine_test.rb Verifies that initializer-provided connection configuration gives the record class its own connection pool.

Sequence Diagram

sequenceDiagram
    participant Host as Host application
    participant Engine as SolidObjects::Engine
    participant Init as config/initializers
    participant AR as Active Record
    participant Record as SolidObjects::Record

    Host->>Engine: Load Solid Objects engine
    Engine->>AR: Register on_load(:active_record)
    Host->>Init: Run application initializers
    Init->>Host: Set encryption keys and connects_to
    Host->>AR: Load ActiveRecord::Base
    AR->>Engine: Run active_record load hook
    Engine->>Record: Require record.rb
    Record->>Record: configure_connection
Loading

Reviews (1): Last reviewed commit: "Load the record class when Active Record..." | Re-trigger Greptile

Bumps the version constant, dates the changelog section for the deferred
Active Record load, and refreshes the lockfile.
@cardmagic
cardmagic merged commit fc3a8f8 into main Aug 18, 2026
28 checks passed
@cardmagic
cardmagic deleted the fix/defer-active-record-load branch August 18, 2026 14:12
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.

Engine loads ActiveRecord::Base at require time, breaking host apps' initializer-configured Active Record encryption

1 participant