Skip to content

Commit f3c6f84

Browse files
cardmagicclaude
andcommitted
fix: name every migrated column in the doctor
`EXPECTED_COLUMNS` omitted `instances.state_revision`, so the doctor passed on a schema that stops a worker. The list had drifted the same way four other times, and the earlier fix named one column rather than the rule. A test now applies the first migration to one scratch database and every migration to another, and fails when the doctor does not name a column that the later migrations add. A new table is skipped, because the missing-table check already reports it. The test failed with: Expected ["instances.state_revision", "messages.operation", "effects.success_operation", "effects.failure_operation", "dead_letters.operation"] to be empty. The TypeScript doctor needs no equivalent. It compares the recorded migration versions against `SCHEMA_VERSIONS`, which reports any migration that did not run. The gem cannot do that, because a host application copies these migrations under its own timestamps, so the column list is the only signal Ruby has. Validation: bundle exec rake (784 runs, 0 failures) and the full suite on PostgreSQL 16 (784 runs, 0 failures). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a902aa5 commit f3c6f84

2 files changed

Lines changed: 61 additions & 5 deletions

File tree

‎lib/solid_objects/doctor.rb‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,25 @@ def to_s
6565
instances: %w[
6666
id actor_type actor_id state state_version next_message_sequence
6767
activation_owner_id activation_token activation_expires_at
68-
activation_generation completed_idempotency_keys
68+
activation_generation state_revision completed_idempotency_keys
6969
],
7070
messages: %w[
71-
id instance_id delivery_mode arguments sequence attempt_count request_id
72-
result error rejection completed_at rejected_at
71+
id instance_id operation delivery_mode arguments sequence attempt_count
72+
request_id result error rejection completed_at rejected_at
7373
],
7474
ready_messages: %w[id message_id instance_id sequence available_at],
7575
claimed_messages: %w[
7676
id message_id instance_id process_id activation_token
7777
activation_generation claimed_at
7878
],
7979
reminders: %w[id instance_id operation next_run_at status],
80-
effects: %w[id message_id instance_id effect_id status available_at],
80+
effects: %w[
81+
id message_id instance_id effect_id status available_at
82+
success_operation failure_operation
83+
],
8184
effect_recoveries: %w[effect_id instance_id recovery_operation status_operation recovery_timeout retired_at],
8285
broadcasts: %w[id message_id instance_id broadcast_id status available_at],
83-
dead_letters: %w[id message_id instance_id actor_type actor_id attempts],
86+
dead_letters: %w[id message_id instance_id actor_type actor_id operation attempts],
8487
administration_events: %w[id action kind subject_id actor occurred_at],
8588
redrives: %w[id kind filters status active_scope moved move_limit started_at finished_at]
8689
}.freeze

‎test/integration/doctor_test.rb‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
require "database_test_helper"
44
require "rake"
55
require "solid_objects/doctor"
6+
require "solid_objects/schema_bootstrap"
7+
require "tmpdir"
8+
9+
class DoctorScratchSchema < ActiveRecord::Base
10+
self.abstract_class = true
11+
end
612

713
class DoctorTest < ActiveSupport::TestCase
814
test "verifies a workerless synchronous installation" do
@@ -132,6 +138,18 @@ class DoctorTest < ActiveSupport::TestCase
132138
assert_match(/completed_idempotency_keys/, report.check(:schema).message)
133139
end
134140

141+
test "names a column from every migration that follows the first" do
142+
added = columns_later_migrations_add
143+
144+
refute_empty added, "the schema has no migration after the first to verify"
145+
unnamed = added.flat_map do |table, columns|
146+
listed = SolidObjects::Doctor::EXPECTED_COLUMNS.fetch(table, [])
147+
(columns - listed).map { |column| "#{table}.#{column}" }
148+
end
149+
150+
assert_empty unnamed, "the doctor cannot report these half-applied migrations"
151+
end
152+
135153
test "reports live runtime roles" do
136154
now = SolidObjects.database_adapter.database_now
137155
SolidObjects::Process.create!(
@@ -166,6 +184,41 @@ class DoctorTest < ActiveSupport::TestCase
166184

167185
private
168186

187+
# @rbs () -> Hash[Symbol, Array[String]]
188+
def columns_later_migrations_add
189+
Dir.mktmpdir do |directory|
190+
migrations = SolidObjects::SchemaBootstrap.migrations
191+
first = schema_columns(directory, "first", migrations.first(1))
192+
whole = schema_columns(directory, "whole", migrations)
193+
first.each_with_object({}) do |(table, columns), added|
194+
later = whole.fetch(table) - columns
195+
added[table] = later unless later.empty?
196+
end
197+
end
198+
end
199+
200+
# @rbs (String, String, Array[Class]) -> Hash[Symbol, Array[String]]
201+
def schema_columns(directory, name, migrations)
202+
DoctorScratchSchema.establish_connection(
203+
adapter: "sqlite3",
204+
database: File.join(directory, "#{name}.sqlite3")
205+
)
206+
connection = DoctorScratchSchema.connection
207+
migrations.each do |migration_class|
208+
migration = migration_class.new
209+
migration.define_singleton_method(:connection) { connection }
210+
migration.migrate(:up)
211+
end
212+
SolidObjects::Doctor::EXPECTED_COLUMNS.keys.each_with_object({}) do |table, columns|
213+
name = SolidObjects.table_name(table)
214+
next unless connection.data_sources.include?(name)
215+
216+
columns[table] = connection.columns(name).map(&:name)
217+
end
218+
ensure
219+
DoctorScratchSchema.remove_connection
220+
end
221+
169222
def hold_sqlite_write_lock
170223
locked = Queue.new
171224
release = Queue.new

0 commit comments

Comments
 (0)