From dd885b9a65188f0b0ac12d02eeb509297a6859c5 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Tue, 14 Oct 2025 10:40:17 +0200 Subject: [PATCH 01/17] fix(ci): rails reporter priority When using `--fail-fast` option the rails reporter could raise before other reporters records the failure. Hence we place rails last. Signed-off-by: Ulysse Buonomo --- test/cases/helper_cockroachdb.rb | 36 ++++++++++++------- test/support/copy_cat.rb | 2 +- .../exclude_from_transactional_tests.rb | 1 - 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/test/cases/helper_cockroachdb.rb b/test/cases/helper_cockroachdb.rb index 1f453420..7568e178 100644 --- a/test/cases/helper_cockroachdb.rb +++ b/test/cases/helper_cockroachdb.rb @@ -7,7 +7,6 @@ module ExcludeMessage end require "minitest/excludes" -require "minitest/github_action_reporter" # This gives great visibility on schema dump related tests, but # some rails specific messages are then ignored. @@ -152,21 +151,34 @@ def with_postgresql_datetime_type(type) ActiveRecord::TestCase.prepend(SetDatetimeInCockroachDBAdapter) -module Minitest - module GithubActionReporterExt - def gh_link(loc) - return super unless loc.include?("/gems/") +if ENV["GITHUB_ACTIONS"] + require "minitest/github_action_reporter" - path, _, line = loc[%r(/(?:test|spec|lib)/.*)][1..].rpartition(":") + module Minitest + module GithubActionReporterExt + def gh_link(loc) + return super unless loc.include?("/gems/") - rails_version = "v#{ActiveRecord::VERSION::STRING}" - "#{ENV["GITHUB_SERVER_URL"]}/rails/rails/blob/#{rails_version}/activerecord/#{path}#L#{line}" - rescue - warn "Failed to generate link for #{loc}" - super + path, _, line = loc[%r(/(?:test|spec|lib)/.*)][1..].rpartition(":") + + rails_version = "v#{ActiveRecord::VERSION::STRING}" + "#{ENV["GITHUB_SERVER_URL"]}/rails/rails/blob/#{rails_version}/activerecord/#{path}#L#{line}" + rescue + warn "Failed to generate link for #{loc}" + super + end end + GithubActionReporter.prepend(GithubActionReporterExt) end - GithubActionReporter.prepend(GithubActionReporterExt) +end + +# Using '--fail-fast' may cause the rails plugin to raise Interrupt when recording +# a test. This would prevent other plugins from recording it. Hence we make sure +# that rails plugin is loaded last. +Minitest.load_plugins +if Minitest.extensions.include?("rails") + Minitest.extensions.delete("rails") + Minitest.extensions << "rails" end if ENV['TRACE_LIB'] diff --git a/test/support/copy_cat.rb b/test/support/copy_cat.rb index cd9f0c73..30803fc5 100644 --- a/test/support/copy_cat.rb +++ b/test/support/copy_cat.rb @@ -20,7 +20,7 @@ def warn(message, category: nil, **kwargs) # Copy methods from The original rails class to our adapter. # While copying, we can rewrite the source code of the method using - # ast. Use `debug: true` to lead you true that process. + # ast. Use `debug: true` to lead you through that process. def copy_methods(new_klass, old_klass, *methods, debug: false, &block) methods.each do |met| file, _ = old_klass.instance_method(met).source_location diff --git a/test/support/exclude_from_transactional_tests.rb b/test/support/exclude_from_transactional_tests.rb index 76da602a..61775b64 100644 --- a/test/support/exclude_from_transactional_tests.rb +++ b/test/support/exclude_from_transactional_tests.rb @@ -18,7 +18,6 @@ def self.prepended(base) end def before_setup - # binding.irb if self.class.non_transactional_list.include?(@NAME.to_s) @old_use_transactional_tests = self.use_transactional_tests if @old_use_transactional_tests # stay false if false self.use_transactional_tests = !self.class.non_transactional_list.include?(@NAME.to_s) From 1ca4c386be0377a849477c5708a9515790a94a0f Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Tue, 14 Oct 2025 10:59:49 +0200 Subject: [PATCH 02/17] fix(test): propagate setup and teardown --- test/cases/adapter_test.rb | 2 ++ test/cases/fixtures_test.rb | 3 +++ 2 files changed, 5 insertions(+) diff --git a/test/cases/adapter_test.rb b/test/cases/adapter_test.rb index 5d394fef..99535ed1 100644 --- a/test/cases/adapter_test.rb +++ b/test/cases/adapter_test.rb @@ -70,6 +70,7 @@ class AdapterForeignKeyTest < ActiveRecord::TestCase fixtures :fk_test_has_pk def before_setup + super conn = ActiveRecord::Base.lease_connection conn.drop_table :fk_test_has_fk, if_exists: true @@ -87,6 +88,7 @@ def before_setup end def setup + super @connection = ActiveRecord::Base.lease_connection end diff --git a/test/cases/fixtures_test.rb b/test/cases/fixtures_test.rb index 86c44d62..74d90df1 100644 --- a/test/cases/fixtures_test.rb +++ b/test/cases/fixtures_test.rb @@ -288,6 +288,7 @@ class FixturesResetPkSequenceTest < ActiveRecord::TestCase # We'll do this in a before_setup so we get ahead of # ActiveRecord::TestFixtures#before_setup. def before_setup + super Account.lease_connection.drop_table :accounts, if_exists: true Account.lease_connection.exec_query("CREATE SEQUENCE IF NOT EXISTS accounts_id_seq") Account.lease_connection.exec_query(" @@ -336,12 +337,14 @@ def before_setup end def setup + super @instances = [Account.new(credit_limit: 50), Company.new(name: "RoR Consulting"), Course.new(name: "Test")] ActiveRecord::FixtureSet.reset_cache # make sure tables get reinitialized end # Drop the primary key sequences and bring back the original tables. def teardown + super Account.lease_connection.drop_table :accounts, if_exists: true Account.lease_connection.exec_query("DROP SEQUENCE IF EXISTS accounts_id_seq") Account.lease_connection.create_table :accounts, force: true do |t| From 6c973a7c9364da2af8a660b83da76460017173d6 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Wed, 15 Oct 2025 13:40:39 +0200 Subject: [PATCH 03/17] fix(test): deeply dupped flaky test This test was failing due to missing a column in the model. We fix it by resetting the cache after adding the column. Signed-off-by: Ulysse Buonomo --- test/cases/fixtures_test.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/cases/fixtures_test.rb b/test/cases/fixtures_test.rb index 74d90df1..b25be7bd 100644 --- a/test/cases/fixtures_test.rb +++ b/test/cases/fixtures_test.rb @@ -386,6 +386,8 @@ def teardown end recreate_parrots + + ActiveRecord::FixtureSet.reset_cache # make sure tables get reinitialized end # This replaces the same test that's been excluded from From f4be207523c49aadb96c1f87faba1c3a1b93ed58 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Wed, 15 Oct 2025 19:02:28 +0200 Subject: [PATCH 04/17] fix(test): deeply dupped flaky test This test was failing due to missing a column in the model. We fix it by resetting the cache after adding the column. Signed-off-by: Ulysse Buonomo --- test/cases/fixtures_test.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/cases/fixtures_test.rb b/test/cases/fixtures_test.rb index b25be7bd..e2da21c7 100644 --- a/test/cases/fixtures_test.rb +++ b/test/cases/fixtures_test.rb @@ -387,7 +387,9 @@ def teardown recreate_parrots - ActiveRecord::FixtureSet.reset_cache # make sure tables get reinitialized + Account.reset_column_information + Company.reset_column_information + Course.reset_column_information end # This replaces the same test that's been excluded from From 0536e07ac7e0281f03e4e01d28bc0ddb37bc0d27 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Wed, 15 Oct 2025 15:56:38 +0200 Subject: [PATCH 05/17] feat(ci): pre-set seeds for flaky.yml --- .github/workflows/flaky.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flaky.yml b/.github/workflows/flaky.yml index 71d48bc6..af7d21f0 100644 --- a/.github/workflows/flaky.yml +++ b/.github/workflows/flaky.yml @@ -33,15 +33,16 @@ jobs: ruby=$(jq --raw-input --compact-output 'split(" ")' <<<"${{ github.event.inputs.ruby }}") crdb_len=$(wc -w <<<"${{ github.event.inputs.crdb }}") ruby_len=$(wc -w <<<"${{ github.event.inputs.ruby }}") - (( range_count = ${{github.event.inputs.max}} / ( crdb_len * ruby_len ) )) - range=$(jq --compact-output "[range($range_count)]" <<<[]) + (( seeds_count = ${{github.event.inputs.max}} / ( crdb_len * ruby_len ) )) + seeds=$(shuf --input-range=1-65535 --head-count=$seeds_count | jq --slurp --compact-output) + echo $seeds echo "crdb=$crdb" >> $GITHUB_OUTPUT echo "ruby=$ruby" >> $GITHUB_OUTPUT - echo "numbers=$range" >> $GITHUB_OUTPUT + echo "seeds=$seeds" >> $GITHUB_OUTPUT outputs: crdb: ${{ steps.generate-matrix.outputs.crdb }} ruby: ${{ steps.generate-matrix.outputs.ruby }} - numbers: ${{ steps.generate-matrix.outputs.numbers }} + seeds: ${{ steps.generate-matrix.outputs.seeds }} test: runs-on: ubuntu-latest needs: prepare-matrix @@ -50,8 +51,10 @@ jobs: matrix: crdb: ${{ fromJSON(needs.prepare-matrix.outputs.crdb) }} ruby: ${{ fromJSON(needs.prepare-matrix.outputs.ruby) }} - number: ${{ fromJSON(needs.prepare-matrix.outputs.numbers) }} - name: Test (crdb=${{ matrix.crdb }} ruby=${{ matrix.ruby }} number=${{ matrix.number }}) + seed: ${{ fromJSON(needs.prepare-matrix.outputs.seeds) }} + name: Test (crdb=${{ matrix.crdb }} ruby=${{ matrix.ruby }} seed=${{ matrix.seed }}) + env: + SEED: ${{ matrix.seed }} steps: - name: Set Up Actions uses: actions/checkout@v4 From e04b563dd6049b3ae97bca7067e37ba14854e421 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Wed, 15 Oct 2025 17:06:23 +0200 Subject: [PATCH 06/17] feat(ci): bisect flaky tests --- .github/workflows/flaky.yml | 4 ++++ Gemfile | 1 + bin/minitest_bisect | 19 +++++++++++++++++++ test/support/rake_helpers.rb | 8 ++++---- 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100755 bin/minitest_bisect diff --git a/.github/workflows/flaky.yml b/.github/workflows/flaky.yml index af7d21f0..ca686749 100644 --- a/.github/workflows/flaky.yml +++ b/.github/workflows/flaky.yml @@ -59,7 +59,11 @@ jobs: - name: Set Up Actions uses: actions/checkout@v4 - uses: ./.github/actions/test-runner + id: test with: crdb: ${{ matrix.crdb }} ruby: ${{ matrix.ruby }} TESTOPTS: --fail-fast + - name: Bisect failing test + if: ${{ failure() && steps.test.conclusion == 'failure' }} + run: bin/minitest_bisect ${{ matrix.seed }} diff --git a/Gemfile b/Gemfile index 903ce903..59cdbce6 100644 --- a/Gemfile +++ b/Gemfile @@ -54,6 +54,7 @@ group :development, :test do gem "rake" gem "debug" + gem "minitest-bisect", github: "BuonOmo/minitest-bisect", branch: "main" gem "minitest-excludes", "~> 2.0.1" gem "minitest-github_action_reporter", github: "BuonOmo/minitest-github_action_reporter", require: "minitest/github_action_reporter_plugin" gem "ostruct", "~> 0.6" diff --git a/bin/minitest_bisect b/bin/minitest_bisect new file mode 100755 index 00000000..d3fef657 --- /dev/null +++ b/bin/minitest_bisect @@ -0,0 +1,19 @@ +#!/usr/bin/env -S bundle exec ruby + +require 'rake/file_list' +require_relative '../test/support/paths_cockroachdb' +require_relative '../test/support/rake_helpers' + +libs = ARTest::CockroachDB.test_load_paths +test_files = RakeHelpers.test_files + +Dir.chdir(File.dirname __dir__) do + system( + "bundle", + "exec", + "minitest_bisect", + "--seed=#{ARGV[0]}", + "-I" + libs.join(":"), + *test_files + ) +end diff --git a/test/support/rake_helpers.rb b/test/support/rake_helpers.rb index e73c1b8e..6e573a2a 100644 --- a/test/support/rake_helpers.rb +++ b/test/support/rake_helpers.rb @@ -26,12 +26,12 @@ def test_files def all_test_files activerecord_test_files = - FileList["#{ARTest::CockroachDB.root_activerecord}/test/cases/**/*_test.rb"]. + ::Rake::FileList["#{ARTest::CockroachDB.root_activerecord}/test/cases/**/*_test.rb"]. reject { _1.include?("/adapters/") || _1.include?("/encryption/performance") } + - FileList["#{ARTest::CockroachDB.root_activerecord}/test/cases/adapters/postgresql/**/*_test.rb"] + ::Rake::FileList["#{ARTest::CockroachDB.root_activerecord}/test/cases/adapters/postgresql/**/*_test.rb"] - cockroachdb_test_files = FileList['test/cases/**/*_test.rb'] + cockroachdb_test_files = ::Rake::FileList['test/cases/**/*_test.rb'] - FileList[COCKROACHDB_TEST_HELPER] + activerecord_test_files + cockroachdb_test_files + ::Rake::FileList[COCKROACHDB_TEST_HELPER] + activerecord_test_files + cockroachdb_test_files end end From 68b6877ac29495a1b1f02c6be4321563fb736cdf Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Wed, 15 Oct 2025 18:51:34 +0200 Subject: [PATCH 07/17] temp: debug bisect --- .github/workflows/flaky.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/flaky.yml b/.github/workflows/flaky.yml index ca686749..fb67601b 100644 --- a/.github/workflows/flaky.yml +++ b/.github/workflows/flaky.yml @@ -67,3 +67,6 @@ jobs: - name: Bisect failing test if: ${{ failure() && steps.test.conclusion == 'failure' }} run: bin/minitest_bisect ${{ matrix.seed }} + env: + MTB_VERBOSE: 2 + MTB_DEBUG: 1 From 6a526ad4956ea2669dc7c8a544f09c6fa3c32ad7 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Wed, 15 Oct 2025 19:51:17 +0200 Subject: [PATCH 08/17] fix(test): latest schema.rb def --- test/cases/fixtures_test.rb | 84 ++++++++++++++----------------------- 1 file changed, 31 insertions(+), 53 deletions(-) diff --git a/test/cases/fixtures_test.rb b/test/cases/fixtures_test.rb index e2da21c7..5c714242 100644 --- a/test/cases/fixtures_test.rb +++ b/test/cases/fixtures_test.rb @@ -18,6 +18,29 @@ require "models/traffic_light" require "models/treasure" +# Hacky tool that searches for table definition code in schema.rb +# and evals it. Doing this ensure we're always using the latest +# schema. +module TableCreator + def self.create_table_using_test_schema(table_name, connection) + table_name = table_name.to_sym + @schema_file ||= SCHEMA_ROOT + "/schema.rb" + @ast ||= Prism::Translation::Parser.parse_file(@schema_file) + to_search = [@ast] + found = nil + while !to_search.empty? + node = to_search.shift + next unless node.is_a?(Parser::AST::Node) + if node in [:block, [:send, _, :create_table, [:sym, ^table_name], *], *] + break found = node + end + to_search += node.children + end + raise "Schema #{table_name.inspect} not found" unless found + connection.instance_eval(found.location.expression.source) + end +end + module CockroachDB class FixturesTest < ActiveRecord::TestCase include ConnectionHelper @@ -347,43 +370,17 @@ def teardown super Account.lease_connection.drop_table :accounts, if_exists: true Account.lease_connection.exec_query("DROP SEQUENCE IF EXISTS accounts_id_seq") - Account.lease_connection.create_table :accounts, force: true do |t| - t.timestamps null: true - t.references :firm, index: false - t.string :firm_name - t.integer :credit_limit - t.string :status - t.integer "a" * max_identifier_length - end + TableCreator.create_table_using_test_schema(:accounts, Account.lease_connection) Company.lease_connection.drop_table :companies, if_exists: true Company.lease_connection.exec_query("DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE") - Company.lease_connection.create_table :companies, force: true do |t| - t.string :type - t.references :firm, index: false - t.string :firm_name - t.string :name - t.bigint :client_of - t.bigint :rating, default: 1 - t.integer :account_id - t.string :description, default: "" - t.integer :status, default: 0 - t.index [:name, :rating], order: :desc - t.index [:name, :description], length: 10 - t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } - t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)" - t.index [:firm_id], name: "company_nulls_not_distinct", nulls_not_distinct: true - t.index :name, name: "company_name_index", using: :btree - t.index "(CASE WHEN rating > 0 THEN lower(name) END) DESC", name: "company_expression_index" if Company.lease_connection.supports_expression_index? - t.index [:firm_id, :type], name: "company_include_index", include: [:name, :account_id] - end + TableCreator.create_table_using_test_schema(:companies, Company.lease_connection) + # CockroachDB specific schema addition + Company.lease_connection.add_index(:companies, [:firm_id, :type], name: "company_include_index", include: [:name, :account_id]) Course.lease_connection.drop_table :courses, if_exists: true Course.lease_connection.exec_query("DROP SEQUENCE IF EXISTS courses_id_seq") - Course.lease_connection.create_table :courses, force: true do |t| - t.column :name, :string, null: false - t.column :college_id, :integer, index: true - end + TableCreator.create_table_using_test_schema(:courses, Course.lease_connection) recreate_parrots @@ -455,28 +452,9 @@ def recreate_parrots conn.drop_table :parrots_treasures, if_exists: true conn.drop_table :parrots, if_exists: true - conn.create_table :parrots, force: :cascade do |t| - t.string :name - t.integer :breed, default: 0 - t.string :color - t.string :parrot_sti_class - t.integer :killer_id - t.integer :updated_count, :integer, default: 0 - t.datetime :created_at - t.datetime :created_on - t.datetime :updated_at - t.datetime :updated_on - end - - conn.create_table :parrots_pirates, id: false, force: true do |t| - t.references :parrot, foreign_key: true - t.references :pirate, foreign_key: true - end - - conn.create_table :parrots_treasures, id: false, force: true do |t| - t.references :parrot, foreign_key: true - t.references :treasure, foreign_key: true - end + TableCreator.create_table_using_test_schema(:parrots, conn) + TableCreator.create_table_using_test_schema(:parrots_pirates, conn) + TableCreator.create_table_using_test_schema(:parrots_treasures, conn) end end end From e23a0b263ba8b3eaca552b516a4fbb5b4aef63d8 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Thu, 16 Oct 2025 12:28:01 +0200 Subject: [PATCH 09/17] fix(test): update template creator It was using old and invalid SQL statements. I took that opportunity to also change a bit its behaviour: - Remove the related rake task and automatically cache the schema if non-existent. - Cache every databases used in tests. - Ensure that cache name is based on schema files. --- CONTRIBUTING.md | 27 +++---- Rakefile | 19 ----- test/cases/helper_cockroachdb.rb | 78 +++++++++++++-------- test/support/template_creator.rb | 117 ++++++++++++++++++++----------- 4 files changed, 135 insertions(+), 106 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58f8bf71..b7ccd3aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,23 +61,16 @@ Only do it if you know the schema was left in a correct state. ### Run Tests from a Backup -Loading the full test schema every time a test runs can take a while, so for cases where loading the schema sequentially is unimportant, it is possible to use a backup to set up the database. This is significantly faster than the standard method and is provided to run individual tests faster, but should not be used to validate a build. - -First create the template database. - -```bash -bundle exec rake db:create_test_template -``` - -This will create a template database for the current version (ex. `activerecord_test_template611` for version 6.1.1) and create a `BACKUP` in the `nodelocal://self/activerecord-crdb-adapter/#{activerecord_version}` directory. - -To load from the template, use the `COCKROACH_LOAD_FROM_TEMPLATE` flag. - -```bash -COCKROACH_LOAD_FROM_TEMPLATE=1 TEST_FILES="test/cases/adapters/postgresql/ddl_test.rb" bundle exec rake test -``` - -And the `activerecord_unittest` database will use the `RESTORE` command to load the schema from the template database. +Loading the full test schema every time a test runs can take +a while, so for cases where loading the schema sequentially +is unimportant, it is possible to use a backup to set up the +database. This is significantly faster than the standard +method and is provided to run individual tests faster, but +should not be used to validate a build. + +To do so, just set the env variable `COCKROACH_LOAD_FROM_TEMPLATE`. +First run will generate and cache a template, latter runs will use +it. # Improvements diff --git a/Rakefile b/Rakefile index 21437647..f7243da5 100644 --- a/Rakefile +++ b/Rakefile @@ -2,28 +2,9 @@ require "bundler/gem_tasks" require "rake/testtask" require_relative 'test/support/paths_cockroachdb' require_relative 'test/support/rake_helpers' -require_relative 'test/support/template_creator' task default: [:test] -namespace :db do - task "create_test_template" do - ENV['DEBUG_COCKROACHDB_ADAPTER'] = "1" - ENV['COCKROACH_SKIP_LOAD_SCHEMA'] = "1" - - TemplateCreator.connect - require_relative 'test/cases/helper' - - # TODO: look into this more, but for some reason the blob alias - # is not defined while running this task. - ActiveRecord::ConnectionAdapters::CockroachDB::TableDefinition.class_eval do - alias :blob :binary - end - - TemplateCreator.create_test_template - end -end - Rake::TestTask.new do |t| t.libs = ARTest::CockroachDB.test_load_paths t.test_files = RakeHelpers.test_files diff --git a/test/cases/helper_cockroachdb.rb b/test/cases/helper_cockroachdb.rb index 7568e178..d0aa101b 100644 --- a/test/cases/helper_cockroachdb.rb +++ b/test/cases/helper_cockroachdb.rb @@ -22,11 +22,57 @@ module ExcludeMessage require "support/load_schema_helper" module LoadSchemaHelperExt + # Load the CockroachDB specific schema. It replaces ActiveRecord's PostgreSQL + # specific schema. + def load_cockroachdb_specific_schema + # silence verbose schema loading + shh do + load "schema/cockroachdb_specific_schema.rb" + + ActiveRecord::FixtureSet.reset_cache + end + end + + if ENV['COCKROACH_LOAD_FROM_TEMPLATE'].nil? && ENV['COCKROACH_SKIP_LOAD_SCHEMA'].nil? + load_cockroachdb_specific_schema + end + def load_schema - return if ENV['COCKROACH_LOAD_FROM_TEMPLATE'] return if ENV['COCKROACH_SKIP_LOAD_SCHEMA'] + unless ENV['COCKROACH_LOAD_FROM_TEMPLATE'] + print "Loading schema..." + t0 = Time.now + super + puts format(" %.2fs", Time.now - t0) + end + + require 'support/template_creator' + + if TemplateCreator.template_exists? + print "Loading schema from template..." + else + print "Generating and caching template schema..." + end + + t0 = Time.now + + TemplateCreator.load_from_template do + super + load_cockroachdb_specific_schema + end - super + puts format(" %.2fs", Time.now - t0) + + # reconnect to activerecord_unittest + shh { ARTest.connect } + end + + private def shh + original_stdout = $stdout + $stdout = StringIO.new + yield + ensure + $stdout = original_stdout end end LoadSchemaHelper.prepend(LoadSchemaHelperExt) @@ -41,34 +87,6 @@ def load_schema # Allow exclusion of tests by name using #exclude_from_transactional_tests(test_name) ActiveRecord::TestCase.prepend(ExcludeFromTransactionalTests) -# Load the CockroachDB specific schema. It replaces ActiveRecord's PostgreSQL -# specific schema. -def load_cockroachdb_specific_schema - # silence verbose schema loading - original_stdout = $stdout - $stdout = StringIO.new - - load "schema/cockroachdb_specific_schema.rb" - - ActiveRecord::FixtureSet.reset_cache -ensure - $stdout = original_stdout -end - -if ENV['COCKROACH_LOAD_FROM_TEMPLATE'].nil? && ENV['COCKROACH_SKIP_LOAD_SCHEMA'].nil? - load_cockroachdb_specific_schema -elsif ENV['COCKROACH_LOAD_FROM_TEMPLATE'] - require 'support/template_creator' - - p "loading schema from template" - - # load from template - TemplateCreator.restore_from_template - - # reconnect to activerecord_unittest - ARTest.connect -end - require 'timeout' module TestTimeoutHelper diff --git a/test/support/template_creator.rb b/test/support/template_creator.rb index ef946d49..3437202f 100644 --- a/test/support/template_creator.rb +++ b/test/support/template_creator.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require 'active_record' +require "activerecord-cockroachdb-adapter" require_relative 'paths_cockroachdb' +require 'support/config' # ARTest.config +require 'support/connection' # ARTest.connect module TemplateCreator - # extend self - DEFAULT_CONNECTION_HASH = { adapter: 'cockroachdb', database: 'defaultdb', @@ -14,37 +14,60 @@ module TemplateCreator host: 'localhost' }.freeze - BACKUP_DIR = "nodelocal://self/activerecord-crdb-adapter" + BACKUP_DIR = "userfile://defaultdb.public/activerecord-crdb-adapter" module_function - def ar_version - ActiveRecord.version.version.gsub('.','') + def template_version + ar_version = ActiveRecord.version.version.gsub('.','_') + main_schema_digest = Digest::MD5.file(SCHEMA_ROOT + "/schema.rb").hexdigest + crdb_schema_digest = Digest::MD5.file("#{__dir__}/../schema/cockroachdb_specific_schema.rb").hexdigest + "#{ar_version}_#{main_schema_digest}_#{crdb_schema_digest}" end def version_backup_path - BACKUP_DIR + "/#{ar_version}" + BACKUP_DIR + "/#{template_version}" + end + + def template_db_name(db_name) + "#{db_name}__template__#{template_version}" + end + + # NOTE: this verification that databases exist may not be enough if a failure + # occurs during the backup process. However, a developer should see that + # failure happening, and in new envs we always run at least a first time + # the backup process. + def template_exists? + databases.all? { template_db_exists?(_1) } + end + + def connect + ActiveRecord::Base.establish_connection(DEFAULT_CONNECTION_HASH) end - def template_db_name - "activerecord_unittest_template#{ar_version}" + def databases + @databases ||=ARTest.config.dig("connections", "cockroachdb").map { |_, value| value["database"] }.uniq end - def connect(connection_hash=nil) - connection_hash = DEFAULT_CONNECTION_HASH if connection_hash.nil? - ActiveRecord::Base.establish_connection(connection_hash) + def with_template_db_names + old_crdb = ARTest.config["connections"]["cockroachdb"].dup + new_crdb = old_crdb.transform_values { |value| value.merge("database" => template_db_name(value["database"])) } + ARTest.config["connections"]["cockroachdb"] = new_crdb + yield + ensure + ARTest.config["connections"]["cockroachdb"] = old_crdb end - def template_db_exists? - ActiveRecord::Base.lease_connection.select_value("SELECT 1 FROM pg_database WHERE datname='#{template_db_name}'") == 1 + def template_db_exists?(db_name) + ActiveRecord::Base.lease_connection.select_value("SELECT 1 FROM pg_database WHERE datname='#{template_db_name(db_name)}'") == 1 end - def drop_template_db - ActiveRecord::Base.lease_connection.execute("DROP DATABASE #{template_db_name}") + def drop_template_db(db_name) + ActiveRecord::Base.lease_connection.execute("DROP DATABASE #{template_db_name(db_name)} CASCADE") end - def create_template_db - ActiveRecord::Base.lease_connection.execute("CREATE DATABASE #{template_db_name}") + def create_template_db(db_name) + ActiveRecord::Base.lease_connection.execute("CREATE DATABASE #{template_db_name(db_name)}") end def load_schema @@ -53,36 +76,50 @@ def load_schema load 'test/schema/cockroachdb_specific_schema.rb' end - def create_test_template + def create_test_template(&block) connect - raise "#{template_db_name} already exists. If you do not have a backup created, please drop the database and run again." if template_db_exists? - - create_template_db - - # switch connection to template db - conn = DEFAULT_CONNECTION_HASH.dup - conn['database'] = template_db_name - connect(conn) + databases.each do |db_name| + drop_template_db(db_name) if template_db_exists?(db_name) + create_template_db(db_name) + end - load_schema + with_template_db_names do + shh { ARTest.connect } + block.call + end - # create BACKUP to restore from - ActiveRecord::Base.lease_connection.execute("BACKUP DATABASE #{template_db_name} TO '#{version_backup_path}'") + connect + ActiveRecord::Base.lease_connection.execute(<<~SQL) + BACKUP DATABASE #{databases.map { |db| template_db_name(db) }.join(', ')} + INTO '#{version_backup_path}' + SQL end - def restore_from_template + def load_from_template(&block) connect - raise "The TemplateDB does not exist. Run 'rake db:create_test_template' first." unless template_db_exists? - - begin - ActiveRecord::Base.lease_connection.execute("DROP DATABASE activerecord_unittest") - rescue ActiveRecord::StatementInvalid => e - unless e.cause.class == PG::InvalidCatalogName - raise e + create_test_template(&block) unless template_exists? + databases.each do |db_name| + begin + ActiveRecord::Base.lease_connection.execute("DROP DATABASE #{db_name}") + rescue ActiveRecord::StatementInvalid => e + unless e.cause.class == PG::InvalidCatalogName + raise e + end end + ActiveRecord::Base.lease_connection.execute("CREATE DATABASE #{db_name}") + ActiveRecord::Base.lease_connection.execute(<<~SQL) + RESTORE #{template_db_name(db_name)}.* + FROM LATEST IN '#{version_backup_path}' + WITH into_db = '#{db_name}' + SQL end - ActiveRecord::Base.lease_connection.execute("CREATE DATABASE activerecord_unittest") + end - ActiveRecord::Base.lease_connection.execute("RESTORE #{template_db_name}.* FROM '#{version_backup_path}' WITH into_db = 'activerecord_unittest'") + private def shh + original_stdout = $stdout + $stdout = StringIO.new + yield + ensure + $stdout = original_stdout end end From 5ffcbdc7203b13fea4ef4818848e2fe1cc62d52e Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Thu, 16 Oct 2025 12:37:30 +0200 Subject: [PATCH 10/17] feat(ci): fast flaky with template db --- .github/workflows/flaky.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flaky.yml b/.github/workflows/flaky.yml index fb67601b..2e4a4ef3 100644 --- a/.github/workflows/flaky.yml +++ b/.github/workflows/flaky.yml @@ -55,6 +55,7 @@ jobs: name: Test (crdb=${{ matrix.crdb }} ruby=${{ matrix.ruby }} seed=${{ matrix.seed }}) env: SEED: ${{ matrix.seed }} + COCKROACH_LOAD_FROM_TEMPLATE: yes_please steps: - name: Set Up Actions uses: actions/checkout@v4 From 66c447524565837ea6774da3a1d6595ed20f27fd Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Thu, 16 Oct 2025 12:41:34 +0200 Subject: [PATCH 11/17] fixup! fix(test): update template creator --- test/cases/helper_cockroachdb.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/cases/helper_cockroachdb.rb b/test/cases/helper_cockroachdb.rb index d0aa101b..48ce5e0e 100644 --- a/test/cases/helper_cockroachdb.rb +++ b/test/cases/helper_cockroachdb.rb @@ -33,16 +33,13 @@ def load_cockroachdb_specific_schema end end - if ENV['COCKROACH_LOAD_FROM_TEMPLATE'].nil? && ENV['COCKROACH_SKIP_LOAD_SCHEMA'].nil? - load_cockroachdb_specific_schema - end - def load_schema return if ENV['COCKROACH_SKIP_LOAD_SCHEMA'] unless ENV['COCKROACH_LOAD_FROM_TEMPLATE'] print "Loading schema..." t0 = Time.now super + load_cockroachdb_specific_schema puts format(" %.2fs", Time.now - t0) end From 446fdfe7ceba71b764495ed8d50515b94e0ebca5 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Thu, 16 Oct 2025 14:01:24 +0200 Subject: [PATCH 12/17] temp: try minitest_bisect without failing fast --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 59cdbce6..276f253c 100644 --- a/Gemfile +++ b/Gemfile @@ -54,7 +54,7 @@ group :development, :test do gem "rake" gem "debug" - gem "minitest-bisect", github: "BuonOmo/minitest-bisect", branch: "main" + gem "minitest-bisect", github: "BuonOmo/minitest-bisect", branch: "no-fail-fast" gem "minitest-excludes", "~> 2.0.1" gem "minitest-github_action_reporter", github: "BuonOmo/minitest-github_action_reporter", require: "minitest/github_action_reporter_plugin" gem "ostruct", "~> 0.6" From b43f37903f348be302d9b3a19c501a7780147e01 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Thu, 9 Oct 2025 13:18:37 +0200 Subject: [PATCH 13/17] feat: upgrade to Rails 8.1 --- activerecord-cockroachdb-adapter.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord-cockroachdb-adapter.gemspec b/activerecord-cockroachdb-adapter.gemspec index 9492cb0e..542d058b 100644 --- a/activerecord-cockroachdb-adapter.gemspec +++ b/activerecord-cockroachdb-adapter.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |spec| spec.description = "Allows the use of CockroachDB as a backend for ActiveRecord and Rails apps." spec.homepage = "https://github.com/cockroachdb/activerecord-cockroachdb-adapter" - spec.add_dependency "activerecord", "~> 8.0.0" + spec.add_dependency "activerecord", "~> 8.1.0.a" spec.add_dependency "pg", "~> 1.5" spec.add_dependency "rgeo-activerecord", "~> 8.0.0" From f9b1c886b95e58cdd8c5263efa4af18c1e177437 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Thu, 16 Oct 2025 18:15:35 +0200 Subject: [PATCH 14/17] fix: add `cast_type` to `Column.new` --- lib/active_record/connection_adapters/cockroachdb/column.rb | 4 ++-- .../connection_adapters/cockroachdb/schema_statements.rb | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/active_record/connection_adapters/cockroachdb/column.rb b/lib/active_record/connection_adapters/cockroachdb/column.rb index fdaab7c7..eee40652 100644 --- a/lib/active_record/connection_adapters/cockroachdb/column.rb +++ b/lib/active_record/connection_adapters/cockroachdb/column.rb @@ -20,7 +20,7 @@ module CockroachDB class Column < PostgreSQL::Column # most functions taken from activerecord-postgis-adapter spatial_column # https://github.com/rgeo/activerecord-postgis-adapter/blob/master/lib/active_record/connection_adapters/postgis/spatial_column.rb - def initialize(name, default, sql_type_metadata = nil, null = true, + def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, identity: nil, serial: nil, spatial: nil, generated: nil, hidden: nil) @sql_type_metadata = sql_type_metadata @@ -45,7 +45,7 @@ def initialize(name, default, sql_type_metadata = nil, null = true, # @geometric_type = geo_type_from_sql_type(sql_type) build_from_sql_type(sql_type_metadata.sql_type) end - super(name, default, sql_type_metadata, null, default_function, + super(name, cast_type, default, sql_type_metadata, null, default_function, collation: collation, comment: comment, serial: serial, generated: generated, identity: identity) if spatial? && @srid @limit = { srid: @srid, type: to_type_name(geometric_type) } diff --git a/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb b/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb index ba5f469a..a7500781 100644 --- a/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb +++ b/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb @@ -250,6 +250,7 @@ def new_column_from_field(table_name, field, _definition) CockroachDB::Column.new( column_name, + get_oid_type(oid.to_i, fmod.to_i, column_name, type), default_value, type_metadata, !notnull, From e656a24a28b9089e61a6dd14df9d92a13d8e3f20 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Fri, 17 Oct 2025 13:25:04 +0200 Subject: [PATCH 15/17] fix(test): don't alter backtrace from gems --- .../AssociationDeprecationTest/NotifyModeTest.rb | 2 ++ .../RaiseBacktraceModeTest.rb | 2 ++ .../AssociationDeprecationTest/RaiseModeTest.rb | 2 ++ .../WarnBacktraceModeTest.rb | 2 ++ .../AssociationDeprecationTest/WarnModeTest.rb | 2 ++ .../fix_backtrace_cleaner.rb | 10 ++++++++++ 6 files changed, 20 insertions(+) create mode 100644 test/excludes/AssociationDeprecationTest/NotifyModeTest.rb create mode 100644 test/excludes/AssociationDeprecationTest/RaiseBacktraceModeTest.rb create mode 100644 test/excludes/AssociationDeprecationTest/RaiseModeTest.rb create mode 100644 test/excludes/AssociationDeprecationTest/WarnBacktraceModeTest.rb create mode 100644 test/excludes/AssociationDeprecationTest/WarnModeTest.rb create mode 100644 test/excludes/AssociationDeprecationTest/fix_backtrace_cleaner.rb diff --git a/test/excludes/AssociationDeprecationTest/NotifyModeTest.rb b/test/excludes/AssociationDeprecationTest/NotifyModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/NotifyModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/RaiseBacktraceModeTest.rb b/test/excludes/AssociationDeprecationTest/RaiseBacktraceModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/RaiseBacktraceModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/RaiseModeTest.rb b/test/excludes/AssociationDeprecationTest/RaiseModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/RaiseModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/WarnBacktraceModeTest.rb b/test/excludes/AssociationDeprecationTest/WarnBacktraceModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/WarnBacktraceModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/WarnModeTest.rb b/test/excludes/AssociationDeprecationTest/WarnModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/WarnModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/fix_backtrace_cleaner.rb b/test/excludes/AssociationDeprecationTest/fix_backtrace_cleaner.rb new file mode 100644 index 00000000..9ed08029 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/fix_backtrace_cleaner.rb @@ -0,0 +1,10 @@ +module FixBacktraceCleaner + def setup + super + bc = ActiveSupport::BacktraceCleaner.new + bc.remove_silencers! + bc.remove_filters! + bc.add_silencer { !_1.include?(::AssociationDeprecationTest::TestCase::THIS_FILE) } + ActiveRecord::LogSubscriber.backtrace_cleaner = bc + end +end From d4497fb82c46719481f49a7c11b1d7afeb2766c4 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Fri, 17 Oct 2025 15:09:03 +0200 Subject: [PATCH 16/17] fix(oid): freeze spatial oid Rails is moving towards ractor compatibility, and to do so it swapped to frozen database objects. Hence we cannot simply memoize the factories anymore. I've checked the initialization speed for factories, and the memoization was too fast for it to be worth implementing a cache. See https://github.com/rails/rails/commit/d2da81670c0049aeb2b7b062a80dd069ee2e725a See https://github.com/rails/rails/commit/76448a01667f9d477e6884a986276687167dfc83 --- .../cockroachdb/oid/spatial.rb | 17 +++++++---------- .../connection_adapters/cockroachdb_adapter.rb | 2 +- test/cases/adapters/postgresql/postgis_test.rb | 8 +------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb b/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb index eed4ffc7..740b8421 100644 --- a/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb +++ b/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb @@ -28,6 +28,10 @@ class Spatial < Type::Value def initialize(oid, sql_type) @sql_type = sql_type @geo_type, @srid, @has_z, @has_m = self.class.parse_sql_type(sql_type) + @spatial_factory = + RGeo::ActiveRecord::SpatialFactoryStore.instance.factory( + factory_attrs + ) end # sql_type: geometry, geometry(Point), geometry(Point,4326), ... @@ -59,13 +63,6 @@ def self.parse_sql_type(sql_type) [geo_type, srid, has_z, has_m] end - def spatial_factory - @spatial_factory ||= - RGeo::ActiveRecord::SpatialFactoryStore.instance.factory( - factory_attrs - ) - end - def geographic? @sql_type =~ /geography/ end @@ -108,14 +105,14 @@ def parse_wkt(string) end def binary_string?(string) - string[0] == "\x00" || string[0] == "\x01" || string[0, 4] =~ /[0-9a-fA-F]{4}/ + string[0] == "\x00" || string[0] == "\x01" || string[0, 4].match?(/[0-9a-fA-F]{4}/) end def wkt_parser(string) if binary_string?(string) - RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid) + RGeo::WKRep::WKBParser.new(@spatial_factory, support_ewkb: true, default_srid: @srid) else - RGeo::WKRep::WKTParser.new(spatial_factory, support_ewkt: true, default_srid: @srid) + RGeo::WKRep::WKTParser.new(@spatial_factory, support_ewkt: true, default_srid: @srid) end end diff --git a/lib/active_record/connection_adapters/cockroachdb_adapter.rb b/lib/active_record/connection_adapters/cockroachdb_adapter.rb index 3a98cbff..489ba726 100644 --- a/lib/active_record/connection_adapters/cockroachdb_adapter.rb +++ b/lib/active_record/connection_adapters/cockroachdb_adapter.rb @@ -299,7 +299,7 @@ def initialize_type_map(m = type_map) st_polygon ).each do |geo_type| m.register_type(geo_type) do |oid, _, sql_type| - CockroachDB::OID::Spatial.new(oid, sql_type) + CockroachDB::OID::Spatial.new(oid, sql_type).freeze end end diff --git a/test/cases/adapters/postgresql/postgis_test.rb b/test/cases/adapters/postgresql/postgis_test.rb index f3dd811d..561cd10a 100644 --- a/test/cases/adapters/postgresql/postgis_test.rb +++ b/test/cases/adapters/postgresql/postgis_test.rb @@ -185,12 +185,6 @@ def reset_memoized_spatial_factories # necessary to reset the @spatial_factory variable on spatial # OIDs, otherwise the results of early tests will be memoized # since the table is not dropped and recreated between test cases. - ObjectSpace.each_object(spatial_oid) do |oid| - oid.instance_variable_set(:@spatial_factory, nil) - end - end - - def spatial_oid - ActiveRecord::ConnectionAdapters::CockroachDB::OID::Spatial + klass.lease_connection.send(:reload_type_map) end end From 3ccfb4ffc1c81dfa848f1773e52f38ea9a76805c Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Fri, 17 Oct 2025 15:55:24 +0200 Subject: [PATCH 17/17] fix: update run_command execution --- .../connection_adapters/cockroachdb/database_tasks.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/cockroachdb/database_tasks.rb b/lib/active_record/connection_adapters/cockroachdb/database_tasks.rb index b951e4f6..c5abc93e 100644 --- a/lib/active_record/connection_adapters/cockroachdb/database_tasks.rb +++ b/lib/active_record/connection_adapters/cockroachdb/database_tasks.rb @@ -74,7 +74,7 @@ def structure_load(filename, extra_flags=nil) "https://github.com/cockroachdb/activerecord-cockroachdb-adapter/issues/new" end - run_cmd("cockroach", ["sql", "--set", "errexit=false", "--file", filename], "loading") + run_cmd("cockroach", "sql", "--set", "errexit=false", "--file", filename) end private