Repository navigation
0.8.1: readable one-time token in transactional tests, strong_migrations-safe generators - #79
Merged
Merged
Conversation
Since 0.6.0, TokenSession refuses a retrieval inside any open transaction, because a rollback could undo the token_shown_at claim after the token was shown. Rails' transactional tests run each test inside one transaction. So every test that created a key and read its token got nil, and the dashboard's token page redirected. When Rails.env is test and the only open transaction is an outer, non-joinable one, the claim now runs in it. Rails' transactional tests, rspec-rails and DatabaseCleaner's :transaction strategy open that transaction. The first retrieval returns the token. A second one, or one from a copy of the session, does not. A transaction opened on top of it is still refused, as in production. Outside the test environment nothing changes. The new tests cover the helper and the dashboard under use_transactional_tests = true, and the claim under a real commit, read from a second connection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 0.7.0 add_created_by migration called add_reference with the polymorphic index. That builds the index inside the migration's DDL transaction, which blocks writes to api_keys, so strong_migrations refuses the migration on PostgreSQL. The migration now runs with disable_ddl_transaction!. It adds the columns without an index, then builds index_api_keys_on_created_by with algorithm: :concurrently on PostgreSQL and PostGIS. MySQL and SQLite refuse that option, so there the index is built as usual. Each step checks what exists, so a run that stopped half way can run again. The database check now loads strong_migrations. It starts from an install made before 0.6.0 and runs the four upgrade migrations through MigrationContext, as db:migrate does, so a missing disable_ddl_transaction! fails too. add_key_activity, add_device_authorizations and add_idempotency_keys already pass on PostgreSQL and MySQL and stay unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record the two fixes in the CHANGELOG and the README: the one-time token in transactional tests, and the strong_migrations-safe add_created_by migration. The README also says how to run the database check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gem still ships add_key_types and add_restrictions, and strong_migrations refused both on PostgreSQL, as it did add_created_by. An app that upgrades from an old version must get migrations that a strong_migrations app accepts. add_key_types now runs without a DDL transaction, checks each step, and builds its indexes concurrently on PostgreSQL. strong_migrations calls the four-column index a best-practice problem on every database. The install has the same index, so the migration keeps it inside safety_assured when the app has strong_migrations. Its down removes the indexes before the columns, so no index is left cut down. add_restrictions now adds the check constraint with validate: false on PostgreSQL, then validates it, which does not block reads and writes. On MySQL the migration never ran, because MySQL refuses a literal default on a JSON column. It now uses the expression JSON_OBJECT(), which also gives existing keys an empty object. strong_migrations has no safe form of a check constraint on an existing MySQL table, so it still refuses this migration on MySQL, and the check shows that. add_authentication_index now also recognises PostGIS. The database check runs all seven upgrade migrations as db:migrate does, from an install made before 0.3.0 that has a key, on PostgreSQL, MySQL and SQLite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four generated migrations failed rubocop-rails-omakase, the style a Rails app lints db/migrate with: arrays without inner spaces and a case whose end was not aligned. They now use %i[] arrays, two methods for the key types, and case on its own line under the assignment. A new test generates all eight migrations and runs RuboCop with the omakase config on them. rubocop-rails-omakase joins the test bundle. The Rails 8.0 appraisal keeps json below 3, because Active Support 8.0 passes an option that json 3 removed. After a failed concurrent build, PostgreSQL keeps an invalid index, and a second run reported success. add_created_by, add_key_types and add_authentication_index now remove an invalid index and build it again. The database check shows this on PostgreSQL. ApiKeys.test_environment? is now the one test-environment check, for the token handoff and for idempotency_in_test_transactions. The README and the code document the three cases of the token exemption, and a test pins the depth-1 joinable: false case. The README and the add_restrictions migration say what to do on MySQL with strong_migrations. The CHANGELOG says why the token fix needs no switch, and dates 0.8.1 2026-10-26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brakeman reads test/databases/check.rb with the engine, and it flagged the probe's CREATE UNIQUE INDEX, which interpolated the index name and columns into SQL. add_index with unique: true and algorithm: :concurrently builds the same failing index through Active Record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On MySQL, strong_migrations refuses add_restrictions at add_column, for the callable JSON_OBJECT() default, before it reaches the check constraint. So the advice to wrap only the constraint call did not help, and a maintenance window alone does not get past strong_migrations. The README, the CHANGELOG and the migration comment now say: check the size of api_keys, wrap the whole body of up in safety_assured, and run it when the table can be locked for the copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two fixes. Both were found when an app on the fleet upgraded from 0.5 to 0.8.0.
1. The one-time token in transactional tests
Cause. Since 0.6.0,
ApiKeys::TokenSessionrefuses a retrieval inside any open transaction on theApiKeyconnection (claim_could_roll_back?askstransaction_open?), because a rollback could undo thetoken_shown_atclaim after the token was shown. Rails' transactional tests run each test inside one transaction. So in every such test a retrieval got:inside_transactionand nil, and the dashboard's token page redirected. Apps had to rewrite their tests or stub a private method.Fix. The transaction that a transactional test opens around each test does not count. When
Rails.envis test, and the only open transaction is an outer, non-joinable one, the claim runs in it. Rails' transactional tests, rspec-rails and DatabaseCleaner's:transactionstrategy open that transaction, and a system test's server thread shares it. Then::already_shownor:missing);Outside the test environment nothing changes.
2. Every upgrade generator and strong_migrations
strong_migrations refused three upgrade migrations on PostgreSQL. Those three now run with
disable_ddl_transaction!, and each step checks what exists, so a run that stopped half way can run again.add_created_by(0.7.0). Cause:add_reference ... polymorphic: truebuilt the index inside the migration's DDL transaction, which blocks writes toapi_keys. Fix: add the columns withindex: false, then buildindex_api_keys_on_created_bywithalgorithm: :concurrentlyon PostgreSQL and PostGIS. MySQL and SQLite refuse that option, so there the index is built as usual.add_key_types(0.3.0). Cause: three indexes built inside the transaction, and a four-column index that strong_migrations calls a best-practice problem on every database. Fix: concurrent indexes on PostgreSQL. The four-column index is the same one the install makes, so the migration keeps it insidesafety_assuredwhen the app has strong_migrations.changebecameup/down;downremoves the indexes before the columns, so no index is left cut down.add_restrictions(0.5.0). Cause: the check constraint was validated under a lock that blocks reads and writes. Fix: on PostgreSQL, add it withvalidate: false, thenvalidate_check_constraint. On MySQL the migration never ran, because MySQL refuses a literal default on a JSON column. Now the default there is the expressionJSON_OBJECT(), which also gives existing keys{}. strong_migrations has no safe form of a check constraint on an existing MySQL table, so it still refuses this one on MySQL; the check shows that refusal.add_authentication_index(0.4.0) was already concurrent. It now also recognises PostGIS.add_key_activity,add_device_authorizationsandadd_idempotency_keysalready passed. They are unchanged.Review
An adversarial review of 79d6a8a gave FIX-FIRST: 1 major and 5 minor findings. All six are fixed in 2389a2b. 8033d5c builds the probe's broken index with
add_index, because Brakeman flagged the raw SQL.Layout/SpaceInsideArrayLiteralBrackets, 2Layout/EndAlignment) in the install,add_restrictions,add_device_authorizationsandadd_idempotency_keys. Fix:%i[]arrays, the key types as two methods (primary_key_type,foreign_key_type), andcaseon its own line underexpression =. New test:test/generators/generated_migrations_style_test.rbgenerates all eight migrations and runs RuboCop with the omakase config on them. rubocop-rails-omakase is in the test bundle. RuboCop allows json 3, which Active Support 8.0 cannot use, so the Rails 8.0 appraisal keepsjson < 3.connection.pinned, because that would drop DatabaseCleaner:transaction. The README and the method comment now list the three cases: a depth-1transaction(joinable: false)that the app opens in a test without its own transaction is exempt too; an environment with another name (ci) is not exempt; fixtures plus DatabaseCleaner:transaction(depth 2) is not exempt. New tests pin the depth-1 case andci.ApiKeys.test_environment?is used by the token handoff and byidempotency_in_test_transactions. The CHANGELOG says why the token fix needs no switch: apps whose tests broke on 0.6.0 must not need new configuration.add_created_by,add_key_typesandadd_authentication_indexnow treat an index withrespond_to?(:valid?) && !valid?as absent: they remove it concurrently and build it again. New PostgreSQL probe intest/databases/check.rb: a unique concurrent build over two equal rows leaves an invalid index, and the next run makes it valid. With the templates of 79d6a8a the probe fails.add_restrictionsmigration say: check the size ofapi_keys, then wrapadd_restrictions_object_constraintinsafety_assured, or run the migration in a maintenance window.A second review pass of 8033d5c resolved items 1 to 4 and 6. On item 5 it found that on MySQL strong_migrations refuses
add_restrictionsalready atadd_column, for the callableJSON_OBJECT()default, before the check constraint. 0f501b5 corrects the advice in the README, the CHANGELOG and the migration comment: check the size ofapi_keys, wrap the whole body ofupinsafety_assured, and run it when the table can be locked for the copy. A MySQL probe confirms it: the generated migration is refused with strong_migrations; with the body ofupwrapped, it runs, an existing key gets{}, and the constraint exists.Tests
test/helpers/token_session_transactional_test.rb: the handoff underuse_transactional_tests = true(once, then nothing; a transaction on top is refused; refused outside the test environment) and under real commits (a second connection sees the claim).test/controllers/keys_controller_test.rb: the dashboard in a transactional test. Create a key, the token page shows it once, then a reload and a copied session get "already shown".test/databases/check.rbnow has strong_migrations. It starts from an install made before 0.3.0 that has a key, and runs all seven upgrade migrations asrails db:migrateruns them (MigrationContext, sodisable_ddl_transaction!counts), then down and up again, and again after a run that stopped half way. A canary test shows that strong_migrations really checks there. strong_migrations does not support SQLite, so on SQLite the check only runs the migrations.test/trusted_key_check_test.rb,test/generators/add_key_types_generator_test.rb,test/generators/add_restrictions_generator_test.rb: the generated content, the options for each adapter, and a run on SQLite.test/generators/generated_migrations_style_test.rb: all eight generated migrations pass rubocop-rails-omakase.Evidence
bundle exec rake test: 790 tests, 0 failures, 0 errors, on Rails 8.1, 8.0 and 7.2, two random seeds each (774 before this change). The style test also passes on Ruby 3.3 and 4.0.test/databases/check.rb, all seven upgrades: 10 runs, 0 failures on PostgreSQL 18 (bigint and uuid keys) and MySQL 8.4, on Rails 8.1.4 and 7.2.4; on SQLite (Rails 8.1.4) 0 failures, with the PostgreSQL-only tests skipped.StrongMigrations::UnsafeMigration:add_created_by("Adding an index non-concurrently locks the table"),add_key_types("Adding an index non-concurrently blocks writes"),add_restrictions("Adding a check constraint key blocks reads and writes while every row is checked");add_created_bywithoutdisable_ddl_transaction!fails withPG::ActiveSqlTransaction("CREATE INDEX CONCURRENTLY cannot run inside a transaction block");TokenSession, the three transactional helper tests and the dashboard test fail (the token page redirects).馃 Generated with Claude Code