From a16df289ca0ec68f13fa684b1f0fb96a9ac1e023 Mon Sep 17 00:00:00 2001 From: Ngan Pham Date: Wed, 6 May 2026 12:56:02 -0700 Subject: [PATCH] Skip virtual columns when generating cached fixture INSERTs Models with generated/virtual columns reject INSERTs into those columns ("cannot INSERT into generated column"). The coder was including every column from model.column_names, so any model with a t.virtual column would crash mount. Filter columns by adapter capability + Column#virtual?, mirroring how Rails' build_fixture_sql filters them in connection_adapters/abstract/ database_statements.rb. Adds a computed_widgets table with a stored generated column to the dummy app and a regression spec exercising the round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/fixture_kit/coders/active_record_coder.rb | 12 ++++++++--- spec/dummy/app/models/computed_widget.rb | 4 ++++ spec/integration/virtual_columns_spec.rb | 21 +++++++++++++++++++ spec/support/dummy_rails_helper.rb | 8 +++++++ 4 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 spec/dummy/app/models/computed_widget.rb create mode 100644 spec/integration/virtual_columns_spec.rb diff --git a/lib/fixture_kit/coders/active_record_coder.rb b/lib/fixture_kit/coders/active_record_coder.rb index e42f394..8df921c 100644 --- a/lib/fixture_kit/coders/active_record_coder.rb +++ b/lib/fixture_kit/coders/active_record_coder.rb @@ -60,22 +60,28 @@ def base_table_model(model) def generate_statements(models) models.each_with_object({}) do |model, statements| - columns = model.column_names + columns = insertable_columns(model) + column_names = columns.map(&:name) rows = [] model.unscoped.order(:id).find_each do |record| - row_values = columns.map do |col| + row_values = column_names.map do |col| value = record.read_attribute_before_type_cast(col) model.connection.quote(value) end rows << "(#{row_values.join(", ")})" end - sql = rows.empty? ? nil : build_insert_sql(model.table_name, columns, rows, model.connection) + sql = rows.empty? ? nil : build_insert_sql(model.table_name, column_names, rows, model.connection) statements[model] = sql end end + def insertable_columns(model) + supports_virtual = model.connection.supports_virtual_columns? + model.columns.reject { |c| supports_virtual && c.virtual? } + end + def build_delete_sql(connection, table_name) "DELETE FROM #{connection.quote_table_name(table_name)}" end diff --git a/spec/dummy/app/models/computed_widget.rb b/spec/dummy/app/models/computed_widget.rb new file mode 100644 index 0000000..b09e76b --- /dev/null +++ b/spec/dummy/app/models/computed_widget.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ComputedWidget < ApplicationRecord +end diff --git a/spec/integration/virtual_columns_spec.rb b/spec/integration/virtual_columns_spec.rb new file mode 100644 index 0000000..a28666a --- /dev/null +++ b/spec/integration/virtual_columns_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "spec_helper" + +# Models with generated/virtual columns can't accept INSERTs into those +# columns. The coder must filter them out when building cached statements. +RSpec.describe "Fixture round-trip with virtual columns" do + fixture do + ComputedWidget.create!(name: "alpha", quantity: 1) + ComputedWidget.create!(name: "beta", quantity: 2) + end + + after { ComputedWidget.delete_all } + + it "loads records with their generated column values" do + widgets = ComputedWidget.order(:id).to_a + + expect(widgets.map(&:name)).to eq(["alpha", "beta"]) + expect(widgets.map(&:name_upper)).to eq(["ALPHA", "BETA"]) + end +end diff --git a/spec/support/dummy_rails_helper.rb b/spec/support/dummy_rails_helper.rb index 8c5f7c0..c71015c 100644 --- a/spec/support/dummy_rails_helper.rb +++ b/spec/support/dummy_rails_helper.rb @@ -34,6 +34,7 @@ def setup_databases ActiveRecord::Base.connection.drop_table(:users, if_exists: true) ActiveRecord::Base.connection.drop_table(:vehicles, if_exists: true) ActiveRecord::Base.connection.drop_table(:gadgets, if_exists: true) + ActiveRecord::Base.connection.drop_table(:computed_widgets, if_exists: true) end AnalyticsRecord.connection.disable_referential_integrity do @@ -88,6 +89,13 @@ def setup_databases t.timestamps end + ActiveRecord::Base.connection.create_table :computed_widgets, force: true do |t| + t.string :name, null: false + t.integer :quantity, null: false, default: 0 + t.virtual :name_upper, type: :string, as: "UPPER(name)", stored: true + t.timestamps + end + # Analytics database schema AnalyticsRecord.connection.create_table :activity_logs, force: true do |t| t.integer :external_user_id, null: false