Skip to content

Fix store_accessor on MariaDB by declaring JSON attribute type#2769

Open
diogovernier wants to merge 1 commit intobasecamp:mainfrom
diogovernier:fix-mariadb-json-store-accessor
Open

Fix store_accessor on MariaDB by declaring JSON attribute type#2769
diogovernier wants to merge 1 commit intobasecamp:mainfrom
diogovernier:fix-mariadb-json-store-accessor

Conversation

@diogovernier
Copy link
Copy Markdown

Summary

  • Fixes store_accessor crash when running Fizzy on MariaDB by adding explicit attribute :column, :json declarations to Filter::Fields and Event::Particulars
  • MariaDB represents JSON columns as LONGTEXT internally, causing Rails to map them to Type::Text which is not a recognized store type
  • On MySQL this change is a no-op since the columns are already typed as Type::Json

Fixes #2402

The problem

MariaDB implements JSON as an alias for LONGTEXT with a CHECK (json_valid(...)) constraint. When ActiveRecord introspects the column, it sees longtext and maps it to Type::Text. Calling store_accessor then fails because Type::Text is not a recognized store type:

ActiveRecord::ConfigurationError: the column 'fields' has not been configured as a store.
Please make sure the column is declared serializable via 'ActiveRecord.store' or, if your
database supports it, use a structured column type like hstore or json.

Why attribute :column, :json over store :column, coder: JSON

The workaround suggested in the issue (store :fields, coder: JSON) works, but introduces unnecessary complexity on MySQL:

  1. Adds a serialization layer that isn't needed. store wraps the column in Type::Serialized with an IndifferentCoder. On MySQL, where the column is already native JSON, this stacks an extra encoding/decoding layer on top of what the database handles natively.

  2. Changes the accessor type. Type::Json uses StringKeyedHashAccessor (string keys, matching JSON semantics). Type::Serialized switches to IndifferentHashAccessor (HashWithIndifferentAccess) — a subtle behavior change for any code accessing the underlying attribute hash directly.

attribute :column, :json avoids both issues. It simply tells Rails "this column is JSON" regardless of what the database adapter reports. On MySQL it's a no-op; on MariaDB it overrides Type::Text with Type::Json. No extra layers, no behavior changes.

This is the same approach recommended in rails/rails#44997, where the Rails community converged on attribute :column, :json as the correct fix for MariaDB's LONGTEXT-backed JSON columns.

Reproduction

Standalone reproduction script (requires MariaDB on port 3307)

Setup:

docker run -d \
  --name fizzy-mariadb-test \
  -e MARIADB_ROOT_PASSWORD=test \
  -e MARIADB_DATABASE=fizzy_repro \
  -p 3307:3306 \
  mariadb:11

ruby repro_2402.rb

Script (repro_2402.rb):

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"
  gem "activerecord", github: "rails/rails", branch: "main"
  gem "trilogy"
end

require "active_record"
require "trilogy"

# Connect to MariaDB
ActiveRecord::Base.establish_connection(
  adapter:  "trilogy",
  host:     "127.0.0.1",
  port:     3307,
  username: "root",
  password: "test",
  database: "fizzy_repro"
)

# Create a table with a JSON column (MariaDB stores this as LONGTEXT internally)
ActiveRecord::Schema.define do
  create_table :filters, id: :uuid, force: true do |t|
    t.json :fields, null: false, default: -> { "(json_object())" }
  end

  create_table :events, id: :uuid, force: true do |t|
    t.json :particulars, default: -> { "(json_object())" }
  end
end

# Verify MariaDB reports the column as longtext, not json
col = ActiveRecord::Base.connection.columns("filters").find { |c| c.name == "fields" }
puts "\n=== Column metadata ==="
puts "Column: #{col.name}"
puts "SQL type: #{col.sql_type}"
puts "AR type:  #{ActiveRecord::Base.connection.send(:type_map).lookup(col.sql_type).class}"
puts ""

# -------------------------------------------------------
# STEP 1: Reproduce the error (no fix applied)
# -------------------------------------------------------
puts "=== Without fix: bare store_accessor ==="

class FilterBroken < ActiveRecord::Base
  self.table_name = "filters"
  store_accessor :fields, :indexed_by, :sorted_by
end

begin
  f = FilterBroken.new
  f.indexed_by = "all"
  puts "ERROR: should have raised, but got: #{f.indexed_by}"
rescue ActiveRecord::ConfigurationError, NoMethodError => e
  puts "Got expected error: #{e.message}"
end

# -------------------------------------------------------
# STEP 2: Verify the issue's suggested fix works
# -------------------------------------------------------
puts "\n=== With issue's workaround: store :fields, coder: JSON ==="

class FilterStoreWorkaround < ActiveRecord::Base
  self.table_name = "filters"
  before_create { self.id ||= SecureRandom.uuid }
  store :fields, coder: JSON
  store_accessor :fields, :indexed_by, :sorted_by
end

begin
  f = FilterStoreWorkaround.new
  f.indexed_by = "all"
  f.save!
  f.reload
  puts "OK: indexed_by = #{f.indexed_by.inspect}"
  puts "     Accessor type: #{FilterStoreWorkaround.type_for_attribute(:fields).class}"
rescue => e
  puts "Failed: #{e.message}"
end

# -------------------------------------------------------
# STEP 3: Verify our fix works
# -------------------------------------------------------
puts "\n=== With our fix: attribute :fields, :json ==="

class FilterFixed < ActiveRecord::Base
  self.table_name = "filters"
  before_create { self.id ||= SecureRandom.uuid }
  attribute :fields, :json
  store_accessor :fields, :indexed_by, :sorted_by
end

begin
  f = FilterFixed.new
  f.indexed_by = "all"
  f.save!
  f.reload
  puts "OK: indexed_by = #{f.indexed_by.inspect}"
  puts "     Accessor type: #{FilterFixed.type_for_attribute(:fields).class}"
rescue => e
  puts "Failed: #{e.message}"
end

# -------------------------------------------------------
# STEP 4: Compare accessor types across all approaches
# -------------------------------------------------------
puts "\n=== Comparing accessor types ==="
puts "FilterBroken    (no fix):        #{FilterBroken.type_for_attribute(:fields).class}" rescue nil
puts "FilterStoreWorkaround (store):   #{FilterStoreWorkaround.type_for_attribute(:fields).class}"
puts "FilterFixed     (attribute):     #{FilterFixed.type_for_attribute(:fields).class}"

puts "\n=== Done ==="

Expected output:

=== Column metadata ===
Column: fields
SQL type: longtext
AR type:  ActiveRecord::Type::Text

=== Without fix: bare store_accessor ===
Got expected error: the column 'fields' has not been configured as a store. ...

=== With issue's workaround: store :fields, coder: JSON ===
OK: indexed_by = "all"
     Accessor type: ActiveRecord::Type::Serialized

=== With our fix: attribute :fields, :json ===
OK: indexed_by = "all"
     Accessor type: ActiveRecord::Type::Json

=== Comparing accessor types ===
FilterBroken    (no fix):        ActiveRecord::Type::Text
FilterStoreWorkaround (store):   ActiveRecord::Type::Serialized
FilterFixed     (attribute):     ActiveRecord::Type::Json

MariaDB represents JSON columns as LONGTEXT, which causes Rails to map
them to Type::Text instead of Type::Json. Since Type::Text lacks the
accessor method that store_accessor requires, this raises
ActiveRecord::ConfigurationError on MariaDB.

Adding explicit `attribute :column, :json` declarations ensures the
columns are typed as JSON regardless of what the database adapter
reports. On MySQL this is a no-op (already Type::Json); on MariaDB it
overrides Type::Text with Type::Json.

Fixes basecamp#2402
Copilot AI review requested due to automatic review settings March 28, 2026 15:44
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a MariaDB-specific Rails incompatibility where JSON columns are introspected as LONGTEXT (typed as ActiveRecord::Type::Text), causing store_accessor to raise ActiveRecord::ConfigurationError. It fixes this by explicitly declaring the affected columns as JSON attributes in the relevant concerns, preserving native JSON behavior on MySQL while preventing crashes on MariaDB.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Changes:

  • Declare Filter#fields as :json before store_accessor via Filter::Fields.
  • Declare Event#particulars as :json before store_accessor via Event::Particulars.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
app/models/filter/fields.rb Forces fields to be typed as JSON so store_accessor works on MariaDB.
app/models/event/particulars.rb Forces particulars to be typed as JSON so store_accessor works on MariaDB.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

end

included do
attribute :fields, :json
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

Consider adding a regression test that fails when the backing column is treated as text (MariaDB JSON-as-LONGTEXT) to ensure store_accessor :fields continues to work. One approach is a small test model/table with a :text fields column that includes Filter::Fields and asserts reading/writing a store accessor does not raise ActiveRecord::ConfigurationError.

Suggested change
attribute :fields, :json
if respond_to?(:columns_hash) && columns_hash["fields"]&.type == :json
attribute :fields, :json
end

Copilot uses AI. Check for mistakes.
extend ActiveSupport::Concern

included do
attribute :particulars, :json
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

Consider adding a regression test for the MariaDB JSON-as-LONGTEXT scenario to ensure store_accessor :particulars keeps working. For example, define a temporary test table/model with a :text particulars column, include Event::Particulars, and assert setting/reading assignee_ids does not raise ActiveRecord::ConfigurationError.

Copilot uses AI. Check for mistakes.
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.

MariaDB JSON columns cause ActiveRecord::ConfigurationError with store_accessor (filters.fields)

2 participants