In Rails 8.1 SQLite column order differs between platforms

Thomas von Deyen

If you're upgrading to Rails 8.1 and using SQLite with Ransack, you might encounter mysterious test failures that only happen on CI but never locally.

Here's what we discovered while upgrading AlchemyCMS to Rails 8.1, and how to fix it.

Travolta.jpg

The Symptom

While preparing AlchemyCMS for Rails 8.1 compatibility, our test suite passed locally on macOS but consistently failed on GitHub Actions (using ubuntu-24) for the SQLite + Rails 8.1 combination.

The failures were in search functionality using Ransack:

ActionController::UnpermittedParameters:
found unpermitted parameter: :name_or_hidden_name_or_description_or_location_name_cont

The puzzling part:

The same Rails version, same Ruby version, same SQLite, yet different behavior.

The Investigation

After ruling out Ruby versions, storage adapters, and SQLite versions, we created a Docker container with platform: linux/amd64 to reproduce the CI environment locally. Adding debug logging revealed the issue:

What the client sent:

name_or_hidden_name_or_description_or_location_name_cont

What the server permitted:

description_or_hidden_name_or_name_or_location_name_cont

The field names were alphabetically sorted differently!

The Root Cause

SQLite returns columns from PRAGMA table_info in different orders depending on the platform. AlchemyCMS generates Ransack search field names dynamically by joining column names with _or_:

def searchable_attribute_names
attributes.select { |a| searchable_attribute?(a) }
  .collect { |h| h[:name] }
end

def search_field_name
"#{searchable_attribute_names.join("_or_")}_cont"
end

On macOS, columns came back in one order. On Linux, a different order. This created mismatched field names between what the form submitted and what the controller permitted.

The Fix

Sort the attribute names to ensure consistent, deterministic field names across all platforms:

def searchable_attribute_names
  attributes.select { |a| searchable_attribute?(a) }
    .collect { |h| h[:name] }
    .sort  # Add this!
end

Bonus: Ransack Association Validation

After sorting, we hit a second issue. Ransack validates associations differently based on attribute order in compound predicates:

  • name_or_image_file_blob_filename_cont — works ✓
  • image_file_blob_filename_or_name_cont — fails ✗

When the association-traversing attribute comes first, Ransack requires ransackable_associations on the target model.

We added:

ActiveSupport.on_load(:active_storage_blob) do
  ActiveStorage::Blob.define_singleton_method(:ransackable_associations) do |_auth_object|
  %w[]
end
end

And this fixed it.

Key Takeaways

  • SQLite column order is platform-dependent. Don't assume deterministic ordering
  • Sort dynamic field names. Any code generating identifiers from database metadata should sort for consistency
  • Use Docker Compose with platform: linux/amd64
  • Ransack compound predicates. Attribute order affects validation behavior

Debugging Tips

If you're seeing similar issues:

  • Add logging to see exactly what parameters are being sent vs permitted
  • Use Docker to match your CI environment exactly
  • Check any code that iterates over database columns or attributes

The fix will be included in AlchemyCMS 8.1. If you're building your own Ransack integration with dynamic field names, make sure to sort them!

Happy Hacking!