From ef8c56337059e44851e26ca0ff40ab5ad64a0532 Mon Sep 17 00:00:00 2001 From: Gannon McGibbon Date: Tue, 18 Nov 2025 18:14:21 -0600 Subject: [PATCH] Make explain accept format hash MySQL uses FORMAT=JSON whereas Postgres uses FORMAT JSON. We should be able to accept a hash that maps to either formats as options. --- activerecord/CHANGELOG.md | 7 +++++++ .../connection_adapters/mysql/database_statements.rb | 4 ++++ .../connection_adapters/postgresql/database_statements.rb | 4 ++++ .../adapters/abstract_mysql_adapter/mysql_explain_test.rb | 6 ++++++ .../test/cases/adapters/postgresql/explain_test.rb | 6 ++++++ 5 files changed, 27 insertions(+) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index b975d6c14f1cf..ff583f59fe53f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,10 @@ +* Allow either explain format syntax for EXPLAIN queries. + + MySQL uses FORMAT=JSON whereas Postgres uses FORMAT JSON. We should be + able to accept both formats as options. + + *Gannon McGibbon* + * On MySQL parallel test database table reset to use `DELETE` instead of `TRUNCATE`. Truncating on MySQL is very slow even on empty or nearly empty tables. diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index c1f8050207d12..604d08d5d63b2 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -36,6 +36,10 @@ def explain(arel, binds = [], options = []) def build_explain_clause(options = []) return "EXPLAIN" if options.empty? + options = options.flat_map do |option| + option.is_a?(Hash) ? option.to_a.map { |nested| nested.join("=") } : option + end + explain_clause = "EXPLAIN #{options.join(" ").upcase}" if analyze_without_explain? && explain_clause.include?("ANALYZE") diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 2bbde99945426..ad2ae3c90be7e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -96,6 +96,10 @@ def high_precision_current_timestamp def build_explain_clause(options = []) return "EXPLAIN" if options.empty? + options = options.flat_map do |option| + option.is_a?(Hash) ? option.to_a.map { |nested| nested.join(" ") } : option + end + "EXPLAIN (#{options.join(", ").upcase})" end diff --git a/activerecord/test/cases/adapters/abstract_mysql_adapter/mysql_explain_test.rb b/activerecord/test/cases/adapters/abstract_mysql_adapter/mysql_explain_test.rb index 101c5657132d3..c1d75dd22dc7b 100644 --- a/activerecord/test/cases/adapters/abstract_mysql_adapter/mysql_explain_test.rb +++ b/activerecord/test/cases/adapters/abstract_mysql_adapter/mysql_explain_test.rb @@ -37,6 +37,12 @@ def test_explain_options_with_eager_loading assert_match %(#{expected_analyze_clause} SELECT `posts`.* FROM `posts` WHERE `posts`.`author_id` = 1), explain end + def test_explain_format_option + explain = Author.all.explain(format: :json).inspect + + assert_match(/\{.*\}/m, explain) + end + private def explain_option supports_analyze? || supports_explain_analyze? ? :analyze : :extended diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index be58c545b280c..119c9196e6feb 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -38,4 +38,10 @@ def test_explain_options_with_eager_loading assert_match %r(EXPLAIN \(ANALYZE\) SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %r(EXPLAIN \(ANALYZE\) SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\$1 \[\["author_id", 1\]\]|1)), explain end + + def test_explain_format_option + explain = Author.all.explain(format: :json).inspect + + assert_match(/\{.*\}/m, explain) + end end