diff --git a/Gemfile.lock b/Gemfile.lock index f49a7bd..f8c11c2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - activerecord-pg-extensions (0.7.0) + activerecord-pg-extensions (0.7.1) activerecord (>= 7.2, < 8.2) railties (>= 7.2, < 8.2) diff --git a/Gemfile.rails-7.2.lock b/Gemfile.rails-7.2.lock index 265c598..d32098c 100644 --- a/Gemfile.rails-7.2.lock +++ b/Gemfile.rails-7.2.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - activerecord-pg-extensions (0.7.0) + activerecord-pg-extensions (0.7.1) activerecord (>= 7.2, < 8.2) railties (>= 7.2, < 8.2) diff --git a/Gemfile.rails-8.0.lock b/Gemfile.rails-8.0.lock index b5c49fe..df87c06 100644 --- a/Gemfile.rails-8.0.lock +++ b/Gemfile.rails-8.0.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - activerecord-pg-extensions (0.7.0) + activerecord-pg-extensions (0.7.1) activerecord (>= 7.2, < 8.2) railties (>= 7.2, < 8.2) diff --git a/lib/active_record/pg_extensions/command_recorder.rb b/lib/active_record/pg_extensions/command_recorder.rb new file mode 100644 index 0000000..b3882f8 --- /dev/null +++ b/lib/active_record/pg_extensions/command_recorder.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module ActiveRecord + module PGExtensions + # adds support for reverting migration methods added by this gem + module CommandRecorder + def rename_constraint(table_name, old_name, new_name, **options) + record(:rename_constraint, [table_name, old_name, new_name, options]) + end + + def invert_rename_constraint(args) + table_name, old_name, new_name, options = args + options ||= {} + # flag the trailing hash as keyword arguments so it's replayed as + # `rename_constraint(..., if_exists:)` rather than a positional hash + [:rename_constraint, [table_name, new_name, old_name, Hash.ruby2_keywords_hash(options)]] + end + end + end +end diff --git a/lib/active_record/pg_extensions/postgresql_adapter.rb b/lib/active_record/pg_extensions/postgresql_adapter.rb index 7267232..4eab209 100644 --- a/lib/active_record/pg_extensions/postgresql_adapter.rb +++ b/lib/active_record/pg_extensions/postgresql_adapter.rb @@ -27,6 +27,17 @@ def defer_constraints(*constraints) set_constraints(:immediate, *constraints) end + # renames a constraint on a table + # see https://www.postgresql.org/docs/current/sql-altertable.html + def rename_constraint(table_name, old_name, new_name, if_exists: false) + return if if_exists && !constraint_exists?(table_name, name: old_name) + + execute(<<~SQL.squish) + ALTER TABLE #{quote_table_name(table_name)} + RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)} + SQL + end + # see https://www.postgresql.org/docs/current/sql-altertable.html#SQL-CREATETABLE-REPLICA-IDENTITY def set_replica_identity(table, identity = :default) identity_clause = case identity @@ -285,6 +296,19 @@ def initialize_type_map(map = type_map) super end + def constraint_exists?(table_name, name:) + scope = quoted_scope(name) + table = quoted_scope(table_name) + select_value(<<~SQL, "SCHEMA") == 1 + SELECT 1 FROM pg_constraint + INNER JOIN pg_class ON pg_class.oid = pg_constraint.conrelid + INNER JOIN pg_namespace ON pg_namespace.oid = pg_constraint.connamespace + WHERE pg_constraint.conname = #{scope[:name]} + AND pg_class.relname = #{table[:name]} + AND pg_namespace.nspname = #{scope[:schema]} + SQL + end + def pre_pg10_wal_function_name(func) return func if postgresql_version >= 100_000 diff --git a/lib/active_record/pg_extensions/railtie.rb b/lib/active_record/pg_extensions/railtie.rb index 4bce713..9a5fe09 100644 --- a/lib/active_record/pg_extensions/railtie.rb +++ b/lib/active_record/pg_extensions/railtie.rb @@ -10,9 +10,11 @@ class Railtie < Rails::Railtie ActiveSupport.on_load(:active_record) do require "active_record/pg_extensions/errors" require "active_record/pg_extensions/postgresql_adapter" + require "active_record/pg_extensions/command_recorder" require "active_record/pg_extensions/transaction" ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(PostgreSQLAdapter) + ::ActiveRecord::Migration::CommandRecorder.include(CommandRecorder) ::ActiveRecord::ConnectionAdapters::NullTransaction.prepend(NullTransaction) ::ActiveRecord::ConnectionAdapters::Transaction.prepend(Transaction) # if they've already require 'all', then inject now diff --git a/lib/active_record/pg_extensions/version.rb b/lib/active_record/pg_extensions/version.rb index d336efa..5fe7e6e 100644 --- a/lib/active_record/pg_extensions/version.rb +++ b/lib/active_record/pg_extensions/version.rb @@ -2,6 +2,6 @@ module ActiveRecord module PGExtensions - VERSION = "0.7.0" + VERSION = "0.7.1" end end diff --git a/spec/postgresql_adapter_spec.rb b/spec/postgresql_adapter_spec.rb index 0279e17..84b580e 100644 --- a/spec/postgresql_adapter_spec.rb +++ b/spec/postgresql_adapter_spec.rb @@ -436,4 +436,43 @@ ) end end + + describe "#rename_constraint" do + around do |example| + connection.dont_execute(&example) + end + + it "renames the constraint" do + connection.rename_constraint(:users, :old_chk, :new_chk) + expect(connection.executed_statements).to eq( + ['ALTER TABLE "users" RENAME CONSTRAINT "old_chk" TO "new_chk"'] + ) + end + + context "with if_exists" do + it "skips when the constraint does not exist" do + allow(connection).to receive(:constraint_exists?).and_return(false) + connection.rename_constraint(:users, :old_chk, :new_chk, if_exists: true) + expect(connection.executed_statements).to be_empty + end + + it "renames when the constraint exists" do + allow(connection).to receive(:constraint_exists?).and_return(true) + connection.rename_constraint(:users, :old_chk, :new_chk, if_exists: true) + expect(connection.executed_statements).to eq( + ['ALTER TABLE "users" RENAME CONSTRAINT "old_chk" TO "new_chk"'] + ) + end + end + + it "is reversible" do + recorder = ActiveRecord::Migration::CommandRecorder.new(connection) + recorder.revert do + recorder.rename_constraint(:users, :old_chk, :new_chk, if_exists: true) + end + expect(recorder.commands).to eq( + [[:rename_constraint, [:users, :new_chk, :old_chk, { if_exists: true }]]] + ) + end + end end