diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2baea8b..4367266 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,68 @@ jobs: - name: Run tests run: bundle exec rspec + test_db: + runs-on: ubuntu-latest + name: test (${{ matrix.appraisal }}, ${{ matrix.database }}) + + strategy: + fail-fast: false + matrix: + ruby: ["4.0"] + appraisal: ["rails_8.1"] + database: ["postgresql", "mysql2"] + + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + mysql: + image: mysql:8 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: >- + --health-cmd "mysqladmin ping -h localhost" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + BUNDLE_GEMFILE: gemfiles/${{ matrix.appraisal }}.gemfile + FIXTURE_KIT_INTEGRATION_FRAMEWORK: rspec + FIXTURE_KIT_DB: ${{ matrix.database }} + FIXTURE_KIT_DB_HOST: ${{ matrix.database == 'mysql2' && '127.0.0.1' || 'localhost' }} + FIXTURE_KIT_DB_USERNAME: ${{ matrix.database == 'mysql2' && 'root' || 'postgres' }} + FIXTURE_KIT_DB_PASSWORD: ${{ matrix.database == 'mysql2' && 'root' || 'postgres' }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby ${{ matrix.ruby }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Install dummy app dependencies + working-directory: spec/dummy + env: + BUNDLE_GEMFILE: "" + run: bundle install + + - name: Run tests + run: bundle exec rspec + lint: runs-on: ubuntu-latest diff --git a/Appraisals b/Appraisals index 0067581..e07b3fb 100644 --- a/Appraisals +++ b/Appraisals @@ -2,10 +2,26 @@ appraise "rails-8.0" do gem "activesupport", "~> 8.0.0" gem "activerecord", "~> 8.0.0" gem "railties", "~> 8.0.0" + + group :postgres do + gem "pg" + end + + group :mysql do + gem "mysql2" + end end appraise "rails-8.1" do gem "activesupport", "~> 8.1.0" gem "activerecord", "~> 8.1.0" gem "railties", "~> 8.1.0" + + group :postgres do + gem "pg" + end + + group :mysql do + gem "mysql2" + end end diff --git a/Gemfile b/Gemfile index be173b2..87f6250 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,11 @@ source "https://rubygems.org" gemspec + +group :postgres do + gem "pg" +end + +group :mysql do + gem "mysql2" +end diff --git a/gemfiles/rails_8.0.gemfile b/gemfiles/rails_8.0.gemfile index 621a692..40fd720 100644 --- a/gemfiles/rails_8.0.gemfile +++ b/gemfiles/rails_8.0.gemfile @@ -6,4 +6,12 @@ gem "activesupport", "~> 8.0.0" gem "activerecord", "~> 8.0.0" gem "railties", "~> 8.0.0" +group :postgres do + gem "pg" +end + +group :mysql do + gem "mysql2" +end + gemspec path: "../" diff --git a/gemfiles/rails_8.0.gemfile.lock b/gemfiles/rails_8.0.gemfile.lock index 1d5f4a1..b4c26a4 100644 --- a/gemfiles/rails_8.0.gemfile.lock +++ b/gemfiles/rails_8.0.gemfile.lock @@ -74,6 +74,8 @@ GEM nokogiri (>= 1.12.0) minitest (6.0.1) prism (~> 1.5) + mysql2 (0.5.7) + bigdecimal nokogiri (1.19.1-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.1-aarch64-linux-musl) @@ -90,6 +92,13 @@ GEM racc (~> 1.4) nokogiri (1.19.1-x86_64-linux-musl) racc (~> 1.4) + pg (1.6.3) + pg (1.6.3-aarch64-linux) + pg (1.6.3-aarch64-linux-musl) + pg (1.6.3-arm64-darwin) + pg (1.6.3-x86_64-darwin) + pg (1.6.3-x86_64-linux) + pg (1.6.3-x86_64-linux-musl) pp (0.6.3) prettyprint prettyprint (0.2.0) @@ -181,6 +190,8 @@ DEPENDENCIES appraisal fixture_kit! irb + mysql2 + pg railties (~> 8.0.0) rake rspec-rails @@ -212,6 +223,7 @@ CHECKSUMS logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 loofah (2.25.0) sha256=df5ed7ac3bac6a4ec802df3877ee5cc86d027299f8952e6243b3dac446b060e6 minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb + mysql2 (0.5.7) sha256=ba09ede515a0ae8a7192040a1b778c0fb0f025fa5877e9be895cd325fa5e9d7b nokogiri (1.19.1-aarch64-linux-gnu) sha256=cfdb0eafd9a554a88f12ebcc688d2b9005f9fce42b00b970e3dc199587b27f32 nokogiri (1.19.1-aarch64-linux-musl) sha256=1e2150ab43c3b373aba76cd1190af7b9e92103564063e48c474f7600923620b5 nokogiri (1.19.1-arm-linux-gnu) sha256=0a39ed59abe3bf279fab9dd4c6db6fe8af01af0608f6e1f08b8ffa4e5d407fa3 @@ -220,6 +232,13 @@ CHECKSUMS nokogiri (1.19.1-x86_64-darwin) sha256=7093896778cc03efb74b85f915a775862730e887f2e58d6921e3fa3d981e68bf nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a nokogiri (1.19.1-x86_64-linux-musl) sha256=4267f38ad4fc7e52a2e7ee28ed494e8f9d8eb4f4b3320901d55981c7b995fc23 + pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99 + pg (1.6.3-aarch64-linux) sha256=0698ad563e02383c27510b76bf7d4cd2de19cd1d16a5013f375dd473e4be72ea + pg (1.6.3-aarch64-linux-musl) sha256=06a75f4ea04b05140146f2a10550b8e0d9f006a79cdaf8b5b130cde40e3ecc2c + pg (1.6.3-arm64-darwin) sha256=7240330b572e6355d7c75a7de535edb5dfcbd6295d9c7777df4d9dddfb8c0e5f + pg (1.6.3-x86_64-darwin) sha256=ee2e04a17c0627225054ffeb43e31a95be9d7e93abda2737ea3ce4a62f2729d6 + pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d + pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009 pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 diff --git a/gemfiles/rails_8.1.gemfile b/gemfiles/rails_8.1.gemfile index b394a09..4cb9159 100644 --- a/gemfiles/rails_8.1.gemfile +++ b/gemfiles/rails_8.1.gemfile @@ -6,4 +6,12 @@ gem "activesupport", "~> 8.1.0" gem "activerecord", "~> 8.1.0" gem "railties", "~> 8.1.0" +group :postgres do + gem "pg" +end + +group :mysql do + gem "mysql2" +end + gemspec path: "../" diff --git a/gemfiles/rails_8.1.gemfile.lock b/gemfiles/rails_8.1.gemfile.lock index d6d959c..5b684f2 100644 --- a/gemfiles/rails_8.1.gemfile.lock +++ b/gemfiles/rails_8.1.gemfile.lock @@ -75,6 +75,8 @@ GEM nokogiri (>= 1.12.0) minitest (6.0.1) prism (~> 1.5) + mysql2 (0.5.7) + bigdecimal nokogiri (1.19.1-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.1-aarch64-linux-musl) @@ -91,6 +93,13 @@ GEM racc (~> 1.4) nokogiri (1.19.1-x86_64-linux-musl) racc (~> 1.4) + pg (1.6.3) + pg (1.6.3-aarch64-linux) + pg (1.6.3-aarch64-linux-musl) + pg (1.6.3-arm64-darwin) + pg (1.6.3-x86_64-darwin) + pg (1.6.3-x86_64-linux) + pg (1.6.3-x86_64-linux-musl) pp (0.6.3) prettyprint prettyprint (0.2.0) @@ -182,6 +191,8 @@ DEPENDENCIES appraisal fixture_kit! irb + mysql2 + pg railties (~> 8.1.0) rake rspec-rails @@ -214,6 +225,7 @@ CHECKSUMS logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 loofah (2.25.0) sha256=df5ed7ac3bac6a4ec802df3877ee5cc86d027299f8952e6243b3dac446b060e6 minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb + mysql2 (0.5.7) sha256=ba09ede515a0ae8a7192040a1b778c0fb0f025fa5877e9be895cd325fa5e9d7b nokogiri (1.19.1-aarch64-linux-gnu) sha256=cfdb0eafd9a554a88f12ebcc688d2b9005f9fce42b00b970e3dc199587b27f32 nokogiri (1.19.1-aarch64-linux-musl) sha256=1e2150ab43c3b373aba76cd1190af7b9e92103564063e48c474f7600923620b5 nokogiri (1.19.1-arm-linux-gnu) sha256=0a39ed59abe3bf279fab9dd4c6db6fe8af01af0608f6e1f08b8ffa4e5d407fa3 @@ -222,6 +234,13 @@ CHECKSUMS nokogiri (1.19.1-x86_64-darwin) sha256=7093896778cc03efb74b85f915a775862730e887f2e58d6921e3fa3d981e68bf nokogiri (1.19.1-x86_64-linux-gnu) sha256=1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a nokogiri (1.19.1-x86_64-linux-musl) sha256=4267f38ad4fc7e52a2e7ee28ed494e8f9d8eb4f4b3320901d55981c7b995fc23 + pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99 + pg (1.6.3-aarch64-linux) sha256=0698ad563e02383c27510b76bf7d4cd2de19cd1d16a5013f375dd473e4be72ea + pg (1.6.3-aarch64-linux-musl) sha256=06a75f4ea04b05140146f2a10550b8e0d9f006a79cdaf8b5b130cde40e3ecc2c + pg (1.6.3-arm64-darwin) sha256=7240330b572e6355d7c75a7de535edb5dfcbd6295d9c7777df4d9dddfb8c0e5f + pg (1.6.3-x86_64-darwin) sha256=ee2e04a17c0627225054ffeb43e31a95be9d7e93abda2737ea3ce4a62f2729d6 + pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d + pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009 pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 diff --git a/spec/dummy/Gemfile b/spec/dummy/Gemfile index 85a9cf8..3688cf9 100644 --- a/spec/dummy/Gemfile +++ b/spec/dummy/Gemfile @@ -3,3 +3,11 @@ source "https://rubygems.org" gemspec path: "../.." + +group :postgres do + gem "pg" +end + +group :mysql do + gem "mysql2" +end diff --git a/spec/dummy/Gemfile.lock b/spec/dummy/Gemfile.lock index 82b2f45..8206416 100644 --- a/spec/dummy/Gemfile.lock +++ b/spec/dummy/Gemfile.lock @@ -76,6 +76,8 @@ GEM minitest (6.0.2) drb (~> 2.0) prism (~> 1.5) + mysql2 (0.5.7) + bigdecimal nokogiri (1.19.2-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.2-aarch64-linux-musl) @@ -92,6 +94,13 @@ GEM racc (~> 1.4) nokogiri (1.19.2-x86_64-linux-musl) racc (~> 1.4) + pg (1.6.3) + pg (1.6.3-aarch64-linux) + pg (1.6.3-aarch64-linux-musl) + pg (1.6.3-arm64-darwin) + pg (1.6.3-x86_64-darwin) + pg (1.6.3-x86_64-linux) + pg (1.6.3-x86_64-linux-musl) pp (0.6.3) prettyprint prettyprint (0.2.0) @@ -181,6 +190,8 @@ DEPENDENCIES appraisal fixture_kit! irb + mysql2 + pg railties rake rspec-rails @@ -213,6 +224,7 @@ CHECKSUMS logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 minitest (6.0.2) sha256=db6e57956f6ecc6134683b4c87467d6dd792323c7f0eea7b93f66bd284adbc3d + mysql2 (0.5.7) sha256=ba09ede515a0ae8a7192040a1b778c0fb0f025fa5877e9be895cd325fa5e9d7b nokogiri (1.19.2-aarch64-linux-gnu) sha256=c34d5c8208025587554608e98fd88ab125b29c80f9352b821964e9a5d5cfbd19 nokogiri (1.19.2-aarch64-linux-musl) sha256=7f6b4b0202d507326841a4f790294bf75098aef50c7173443812e3ac5cb06515 nokogiri (1.19.2-arm-linux-gnu) sha256=b7fa1139016f3dc850bda1260988f0d749934a939d04ef2da13bec060d7d5081 @@ -221,6 +233,13 @@ CHECKSUMS nokogiri (1.19.2-x86_64-darwin) sha256=7d9af11fda72dfaa2961d8c4d5380ca0b51bc389dc5f8d4b859b9644f195e7a4 nokogiri (1.19.2-x86_64-linux-gnu) sha256=fa8feca882b73e871a9845f3817a72e9734c8e974bdc4fbad6e4bc6e8076b94f nokogiri (1.19.2-x86_64-linux-musl) sha256=93128448e61a9383a30baef041bf1f5817e22f297a1d400521e90294445069a8 + pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99 + pg (1.6.3-aarch64-linux) sha256=0698ad563e02383c27510b76bf7d4cd2de19cd1d16a5013f375dd473e4be72ea + pg (1.6.3-aarch64-linux-musl) sha256=06a75f4ea04b05140146f2a10550b8e0d9f006a79cdaf8b5b130cde40e3ecc2c + pg (1.6.3-arm64-darwin) sha256=7240330b572e6355d7c75a7de535edb5dfcbd6295d9c7777df4d9dddfb8c0e5f + pg (1.6.3-x86_64-darwin) sha256=ee2e04a17c0627225054ffeb43e31a95be9d7e93abda2737ea3ce4a62f2729d6 + pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d + pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009 pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 diff --git a/spec/dummy/config/database.yml b/spec/dummy/config/database.yml index 86e56f2..938774e 100644 --- a/spec/dummy/config/database.yml +++ b/spec/dummy/config/database.yml @@ -1,11 +1,61 @@ +<% +adapter = ENV.fetch("FIXTURE_KIT_DB", "sqlite3") +host = ENV.fetch("FIXTURE_KIT_DB_HOST") { adapter == "mysql2" ? "127.0.0.1" : "localhost" } +username = ENV.fetch("FIXTURE_KIT_DB_USERNAME") do + case adapter + when "postgresql" then ENV.fetch("USER", "postgres") + when "mysql2" then "root" + end +end +password = ENV["FIXTURE_KIT_DB_PASSWORD"] +%> + test: primary: + <% case adapter + when "postgresql" %> + adapter: postgresql + database: fixture_kit_test_primary + host: <%= host %> + username: <%= username %> + password: <%= password %> + encoding: unicode + pool: 5 + <% when "mysql2" %> + adapter: mysql2 + database: fixture_kit_test_primary + host: <%= host %> + username: <%= username %> + password: <%= password %> + encoding: utf8mb4 + pool: 5 + <% else %> adapter: sqlite3 database: tmp/primary_test.sqlite3 pool: 5 timeout: 5000 + <% end %> analytics: + <% case adapter + when "postgresql" %> + adapter: postgresql + database: fixture_kit_test_analytics + host: <%= host %> + username: <%= username %> + password: <%= password %> + encoding: unicode + pool: 5 + <% when "mysql2" %> + adapter: mysql2 + database: fixture_kit_test_analytics + host: <%= host %> + username: <%= username %> + password: <%= password %> + encoding: utf8mb4 + pool: 5 + <% else %> adapter: sqlite3 database: tmp/analytics_test.sqlite3 pool: 5 timeout: 5000 + <% end %> diff --git a/spec/support/dummy_rails_helper.rb b/spec/support/dummy_rails_helper.rb index c71015c..088d6d4 100644 --- a/spec/support/dummy_rails_helper.rb +++ b/spec/support/dummy_rails_helper.rb @@ -25,21 +25,68 @@ def clear_fixture_cache(fixture_name = nil) end end +class TestDatabaseBootstrap < ActiveRecord::Base + self.abstract_class = true +end + +# Ensure non-sqlite test databases exist before connecting (sqlite files auto-create). +# Uses a separate connection class so the main ActiveRecord::Base connection +# is established cleanly later from its own configuration. +def ensure_test_databases_exist! + return if ENV.fetch("FIXTURE_KIT_DB", "sqlite3") == "sqlite3" + + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + create_database_if_missing(db_config) + end +end + +def create_database_if_missing(db_config) + config = db_config.configuration_hash.dup + database = config[:database] + + case config[:adapter] + when "postgresql" + config = config.merge(database: "postgres") + when "mysql2" + config = config.except(:database) + else + return + end + + bootstrap = TestDatabaseBootstrap + bootstrap.remove_connection if bootstrap.connected? + bootstrap.establish_connection(config) + quoted = bootstrap.connection.quote_column_name(database) + + case db_config.adapter + when "postgresql" + bootstrap.connection.execute("CREATE DATABASE #{quoted}") + when "mysql2" + bootstrap.connection.execute("CREATE DATABASE IF NOT EXISTS #{quoted}") + end +rescue ActiveRecord::StatementInvalid => e + raise unless e.message.include?("already exists") +ensure + TestDatabaseBootstrap.remove_connection if TestDatabaseBootstrap.connected? +end + # Create schema for both databases def setup_databases + ensure_test_databases_exist! + ActiveRecord::Base.connection.disable_referential_integrity do - ActiveRecord::Base.connection.drop_table(:comments, if_exists: true) - ActiveRecord::Base.connection.drop_table(:tasks, if_exists: true) - ActiveRecord::Base.connection.drop_table(:projects, if_exists: true) - 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) + ActiveRecord::Base.connection.drop_table(:comments, if_exists: true, force: :cascade) + ActiveRecord::Base.connection.drop_table(:tasks, if_exists: true, force: :cascade) + ActiveRecord::Base.connection.drop_table(:projects, if_exists: true, force: :cascade) + ActiveRecord::Base.connection.drop_table(:users, if_exists: true, force: :cascade) + ActiveRecord::Base.connection.drop_table(:vehicles, if_exists: true, force: :cascade) + ActiveRecord::Base.connection.drop_table(:gadgets, if_exists: true, force: :cascade) + ActiveRecord::Base.connection.drop_table(:computed_widgets, if_exists: true, force: :cascade) end AnalyticsRecord.connection.disable_referential_integrity do - AnalyticsRecord.connection.drop_table(:activity_logs, if_exists: true) - AnalyticsRecord.connection.drop_table(:time_entries, if_exists: true) + AnalyticsRecord.connection.drop_table(:activity_logs, if_exists: true, force: :cascade) + AnalyticsRecord.connection.drop_table(:time_entries, if_exists: true, force: :cascade) end # Primary database schema diff --git a/spec/unit/coders/active_record_coder_spec.rb b/spec/unit/coders/active_record_coder_spec.rb index f7619c4..ba969a8 100644 --- a/spec/unit/coders/active_record_coder_spec.rb +++ b/spec/unit/coders/active_record_coder_spec.rb @@ -20,14 +20,16 @@ def exercise_user_write_operations(suffix) { name: "Bulk Insert 2 #{suffix}", email: "bulk-insert-2-#{suffix}@example.com" } ]) + upsert_options = upsert_all_options(User) + User.upsert_all([ { id: user.id, name: "Upsert #{suffix}", email: "create-#{suffix}@example.com" } - ], unique_by: :id) + ], **upsert_options) User.upsert_all([ { id: user.id, name: "Bulk Upsert Existing #{suffix}", email: "create-#{suffix}@example.com" }, { id: user.id + 10_000_000, name: "Bulk Upsert New #{suffix}", email: "bulk-upsert-#{suffix}@example.com" } - ], unique_by: :id) + ], **upsert_options) doomed = User.create!(name: "Delete #{suffix}", email: "delete-#{suffix}@example.com") User.where(id: doomed.id).delete_all @@ -36,6 +38,13 @@ def exercise_user_write_operations(suffix) destroyed.destroy! end + # MySQL's upsert uses ON DUPLICATE KEY UPDATE and rejects :unique_by; + # Postgres requires it to target the conflict. + def upsert_all_options(model) + return {} if model.connection.adapter_name.to_s.downcase.start_with?("mysql", "trilogy") + { unique_by: :id } + end + describe "#generate" do it "captures user model writes for all supported write operation types" do suffix = SecureRandom.hex(6)