From 170e5afe69907adc2452e917c4e4342b51512f36 Mon Sep 17 00:00:00 2001 From: Sang Date: Mon, 28 Sep 2026 20:17:16 +0700 Subject: [PATCH] Narrow Generator#schema_columns' rescue to ActiveRecordError Generator#schema_columns caught StandardError, so a broken custom type adapter or any other real bug in model.table_exists?/.columns was swallowed and permittable:generate silently emitted a degraded, column-less draft instead of raising. ColumnGuard.schema_reachable? already scopes its equivalent rescue to ActiveRecord::ActiveRecordError specifically so a real bug keeps surfacing; schema_columns now matches it, including the same defined?-guard so the gem stays loadable in a host without activerecord. Co-Authored-By: Claude Sonnet 5 --- lib/permittable/generator.rb | 13 ++++++++++++- spec/generator_spec.rb | 12 ++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/permittable/generator.rb b/lib/permittable/generator.rb index ec28371..c7a407c 100644 --- a/lib/permittable/generator.rb +++ b/lib/permittable/generator.rb @@ -400,6 +400,15 @@ def columns_for(model) # top (draft_column) must not share this rescue: an error there would # otherwise discard EVERY column — the draft silently falls back to a # scan alone, or to nothing — for what is one column's problem. + # + # The rescue is scoped to ActiveRecord::ActiveRecordError, same as + # ColumnGuard.schema_reachable? and for the same reason: a genuinely + # unreachable schema (no database yet, table not migrated) degrades to + # no columns, but a real bug — a broken custom type adapter, a NameError + # from a typo — must keep surfacing instead of quietly emitting an empty + # draft. The defined? guard keeps this gem loadable without + # activerecord, same as schema_reachable? (a host without it duck-types + # `model:` and cannot raise an ActiveRecordError in the first place). def schema_columns(model) return nil unless model.respond_to?(:columns) return nil unless model.table_exists? @@ -408,7 +417,9 @@ def schema_columns(model) # into its column names; a nil primary key becomes []. skipped = SKIPPED_COLUMNS + Array(model.primary_key).map(&:to_s) model.columns.reject { |c| skipped.include?(c.name) } - rescue StandardError + rescue StandardError => e + raise unless defined?(ActiveRecord::ActiveRecordError) && e.is_a?(ActiveRecord::ActiveRecordError) + nil end diff --git a/spec/generator_spec.rb b/spec/generator_spec.rb index c2f41e4..9e40aad 100644 --- a/spec/generator_spec.rb +++ b/spec/generator_spec.rb @@ -588,6 +588,18 @@ def deserialize(_value) = raise("a type deserialize ran while drafting") allow(GenUnreachable).to receive(:columns).and_raise(ActiveRecord::StatementInvalid, "no such table") expect(described_class.draft(model: GenUnreachable)).to be_nil end + + it "lets a real bug from schema access propagate instead of drafting an empty contract" do + # Same rescue scope as ColumnGuard.schema_reachable?: only + # ActiveRecord::ActiveRecordError is swallowed (a genuinely + # unreachable schema). Anything else — a broken custom type + # adapter, a real app bug — must surface loudly rather than + # quietly falling back to "no columns". + stub_const("GenMisconfigured", Class.new(TestModel) { self.table_name = "gen_vehicles" }) + allow(GenMisconfigured).to receive(:columns).and_raise(NoMethodError, "undefined method `type' for nil") + expect { described_class.draft(model: GenMisconfigured) } + .to raise_error(NoMethodError, /undefined method `type'/) + end end context "with STI and optimistic locking" do