From 889cb589b6b63db133b68eabf4536f81ac28197b Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sun, 1 Mar 2026 08:52:36 +0100 Subject: [PATCH 01/10] Auto-detect function argument types when dropping or updating Functions with parameters previously could not be dropped or updated because PostgreSQL requires the argument types in DROP FUNCTION for functions that accept arguments. The adapter now looks up argument types from pg_proc automatically, removing the need for manual intervention. For overloaded functions (same name, different signatures), an explicit `arguments:` option is available on drop_function and update_function to disambiguate. When multiple overloads exist and no arguments are given, an AmbiguousFunctionError is raised with guidance. Closes #7 --- lib/fx.rb | 6 ++ lib/fx/adapters/postgres.rb | 66 ++++++++++++-- lib/fx/statements.rb | 18 +++- .../acceptance/user_manages_functions_spec.rb | 89 +++++++++++++++++++ spec/acceptance_helper.rb | 6 ++ spec/fx/adapters/postgres_spec.rb | 52 ++++++++++- spec/fx/command_recorder_spec.rb | 32 +++++++ spec/fx/statements_spec.rb | 19 ++++ 8 files changed, 278 insertions(+), 10 deletions(-) diff --git a/lib/fx.rb b/lib/fx.rb index 797a315..d70cc91 100644 --- a/lib/fx.rb +++ b/lib/fx.rb @@ -14,6 +14,12 @@ # F(x) adds methods `ActiveRecord::Migration` to create and manage database # triggers and functions in Rails applications. module Fx + class Error < StandardError; end + + # Raised when dropping an overloaded function without specifying which + # overload to target. Pass the `arguments:` option to disambiguate. + class AmbiguousFunctionError < Error; end + # Hooks Fx into Rails. # # Enables fx migration methods, migration reversability, and `schema.rb` diff --git a/lib/fx/adapters/postgres.rb b/lib/fx/adapters/postgres.rb index 1329f5d..771cca0 100644 --- a/lib/fx/adapters/postgres.rb +++ b/lib/fx/adapters/postgres.rb @@ -91,10 +91,12 @@ def create_trigger(sql_definition) # # @param name [String, Symbol] The name of the function. # @param sql_definition [String] The SQL schema for the function. + # @param arguments [String] Optional function argument types for + # identifying overloaded functions (e.g. "integer, text"). # # @return [void] - def update_function(name, sql_definition) - drop_function(name) + def update_function(name, sql_definition, arguments: nil) + drop_function(name, arguments: arguments) create_function(sql_definition) end @@ -121,11 +123,22 @@ def update_trigger(name, on:, sql_definition:) # This is typically called in a migration via # {Fx::Statements::Function#drop_function}. # - # @param name [String, Symbol] The name of the function to drop + # @param name [String, Symbol] The name of the function to drop. + # @param arguments [String] Optional function argument types for + # identifying overloaded functions (e.g. "integer, text"). When not + # provided, the argument types are looked up automatically from + # pg_proc. If multiple overloads exist, an {Fx::AmbiguousFunctionError} + # is raised. # # @return [void] - def drop_function(name) - execute("DROP FUNCTION #{name};") + def drop_function(name, arguments: nil) + arguments ||= function_arguments_for(name) + + if arguments + execute("DROP FUNCTION #{name}(#{arguments});") + else + execute("DROP FUNCTION #{name};") + end end # Drops the trigger from the database @@ -141,12 +154,55 @@ def drop_trigger(name, on:) execute("DROP TRIGGER #{name} ON #{on};") end + # The SQL query used to look up a function's argument types from pg_proc. + FUNCTION_ARGUMENTS_QUERY = <<~SQL.freeze + SELECT pg_get_function_identity_arguments(pp.oid) AS arguments + FROM pg_proc pp + JOIN pg_namespace pn ON pn.oid = pp.pronamespace + WHERE pp.proname = %{function_name} + AND pp.prokind = 'f' + AND %{schema_condition} + SQL + private_constant :FUNCTION_ARGUMENTS_QUERY + private attr_reader :connectable delegate :execute, to: :connection + def function_arguments_for(name) + name_str = name.to_s + + if name_str.include?(".") + schema, function_name = name_str.split(".", 2) + schema_condition = "pn.nspname = #{connection.quote(schema)}" + else + function_name = name_str + schema_condition = "pn.nspname = ANY(current_schemas(false))" + end + + rows = connection.execute( + FUNCTION_ARGUMENTS_QUERY % { + function_name: connection.quote(function_name), + schema_condition: schema_condition + } + ).to_a + + case rows.length + when 0 + nil + when 1 + rows.first["arguments"] + else + signatures = rows.map { |r| "#{name_str}(#{r["arguments"]})" } + raise Fx::AmbiguousFunctionError, <<~MSG.chomp + Multiple definitions for function "#{name_str}": #{signatures.join(", ")}. + Specify which to drop: drop_function :#{name_str}, arguments: "" + MSG + end + end + def connection Fx::Adapters::Postgres::Connection.new(connectable.connection) end diff --git a/lib/fx/statements.rb b/lib/fx/statements.rb index bcb2332..93f5b13 100644 --- a/lib/fx/statements.rb +++ b/lib/fx/statements.rb @@ -10,6 +10,10 @@ module Statements # @param sql_definition [String] The SQL query for the function schema. # If both `sql_definition` and `version` are provided, # `sql_definition` takes precedence. + # @param arguments [String] Function argument types (e.g. "integer, text"). + # Not used during creation itself, but preserved by the command recorder + # so that a rollback of this migration can pass the correct signature to + # {#drop_function}. Only needed for overloaded functions. # @return [void] The database response from executing the create statement. # # @example Create from `db/functions/uppercase_users_name_v02.sql` @@ -39,13 +43,16 @@ def create_function(name, version: 1, sql_definition: nil, revert_to_version: ni # @param revert_to_version [Integer] Used to reverse the `drop_function` # command on `rake db:rollback`. The provided version will be passed as # the `version` argument to {#create_function}. + # @param arguments [String] Function argument types for identifying + # overloaded functions (e.g. "integer, text"). When omitted, the + # adapter auto-detects the signature from the database. # @return [void] The database response from executing the drop statement. # # @example Drop a function, rolling back to version 2 on rollback # drop_function(:uppercase_users_name, revert_to_version: 2) # - def drop_function(name, revert_to_version: nil) - Fx.database.drop_function(name) + def drop_function(name, revert_to_version: nil, arguments: nil) + Fx.database.drop_function(name, **{arguments: arguments}.compact) end # Update a database function. @@ -57,6 +64,9 @@ def drop_function(name, revert_to_version: nil) # @param sql_definition [String] The SQL query for the function schema. # If both `sql_definition` and `version` are provided, # `sql_definition` takes precedence. + # @param arguments [String] Function argument types for identifying + # overloaded functions (e.g. "integer, text"). When omitted, the + # adapter auto-detects the signature from the database. # @return [void] The database response from executing the create statement. # # @example Update function to a given version @@ -77,12 +87,12 @@ def drop_function(name, revert_to_version: nil) # $$ LANGUAGE plpgsql; # SQL # - def update_function(name, version: nil, sql_definition: nil, revert_to_version: nil) + def update_function(name, version: nil, sql_definition: nil, revert_to_version: nil, arguments: nil) validate_version_or_sql_definition_present!(version, sql_definition) sql_definition = resolve_sql_definition(sql_definition, name, version, :function) - Fx.database.update_function(name, sql_definition) + Fx.database.update_function(name, sql_definition, **{arguments: arguments}.compact) end # Create a new database trigger. diff --git a/spec/acceptance/user_manages_functions_spec.rb b/spec/acceptance/user_manages_functions_spec.rb index 8612915..9698ed3 100644 --- a/spec/acceptance/user_manages_functions_spec.rb +++ b/spec/acceptance/user_manages_functions_spec.rb @@ -54,4 +54,93 @@ successfully "rails destroy fx:function adder" successfully "rake db:migrate" end + + it "handles updating functions with arguments" do + successfully "rails generate fx:function multiply" + write_function_definition "multiply_v01", <<~SQL + CREATE FUNCTION multiply(x int, y int) + RETURNS int AS $$ + BEGIN + RETURN x * y; + END; + $$ LANGUAGE plpgsql; + SQL + successfully "rake db:migrate" + + result = execute("SELECT * FROM multiply(3, 4) AS result") + result["result"] = result["result"].to_i + expect(result).to eq("result" => 12) + + successfully "rails generate fx:function multiply" + write_function_definition "multiply_v02", <<~SQL + CREATE FUNCTION multiply(x int, y int) + RETURNS int AS $$ + BEGIN + RETURN x * y * 2; + END; + $$ LANGUAGE plpgsql; + SQL + successfully "rake db:migrate" + + result = execute("SELECT * FROM multiply(3, 4) AS result") + result["result"] = result["result"].to_i + expect(result).to eq("result" => 24) + + successfully "rake db:rollback" + + result = execute("SELECT * FROM multiply(3, 4) AS result") + result["result"] = result["result"].to_i + expect(result).to eq("result" => 12) + + successfully "rake db:rollback" + + expect { execute("SELECT * FROM multiply(3, 4) AS result") } + .to raise_error(ActiveRecord::StatementInvalid) + end + + it "handles dropping overloaded functions with explicit arguments" do + successfully "rails generate fx:function inc" + write_function_definition "inc_v01", <<~SQL + CREATE FUNCTION inc(x int) + RETURNS int AS $$ + BEGIN RETURN x + 1; END; + $$ LANGUAGE plpgsql; + SQL + successfully "rake db:migrate" + + execute <<~SQL + CREATE FUNCTION inc(x int, step int) + RETURNS int AS $$ BEGIN RETURN x + step; END; $$ LANGUAGE plpgsql; + SQL + + result = execute("SELECT inc(5) AS result") + result["result"] = result["result"].to_i + expect(result).to eq("result" => 6) + + result = execute("SELECT inc(5, 10) AS result") + result["result"] = result["result"].to_i + expect(result).to eq("result" => 15) + + write_migration "drop_inc_one_arg", <<~RUBY + class DropIncOneArg < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}] + def change + drop_function :inc, arguments: "integer", revert_to_version: 1 + end + end + RUBY + successfully "rake db:migrate" + + expect { execute("SELECT inc(5) AS result") } + .to raise_error(ActiveRecord::StatementInvalid) + + result = execute("SELECT inc(5, 10) AS result") + result["result"] = result["result"].to_i + expect(result).to eq("result" => 15) + + successfully "rake db:rollback" + + result = execute("SELECT inc(5) AS result") + result["result"] = result["result"].to_i + expect(result).to eq("result" => 6) + end end diff --git a/spec/acceptance_helper.rb b/spec/acceptance_helper.rb index 6245362..7bd57c1 100644 --- a/spec/acceptance_helper.rb +++ b/spec/acceptance_helper.rb @@ -61,6 +61,12 @@ def verify_identical_definitions(def_a, def_b) successfully "cmp #{def_a} #{def_b}" end + def write_migration(name, contents) + Dir.mkdir("db/migrate") unless Dir.exist?("db/migrate") + timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S") + File.write("db/migrate/#{timestamp}_#{name}.rb", contents) + end + def execute(command) ActiveRecord::Base.connection.execute(command).first end diff --git a/spec/fx/adapters/postgres_spec.rb b/spec/fx/adapters/postgres_spec.rb index 980d1f3..aaf189e 100644 --- a/spec/fx/adapters/postgres_spec.rb +++ b/spec/fx/adapters/postgres_spec.rb @@ -53,7 +53,7 @@ describe "#drop_function" do context "when the function has arguments" do - it "successfully drops a function with the entire function signature" do + it "successfully drops a function by looking up its signature" do adapter = Fx::Adapters::Postgres.new adapter.create_function( <<~SQL @@ -91,6 +91,56 @@ expect(adapter.functions.map(&:name)).not_to include("test") end end + + context "when the function is overloaded" do + it "raises AmbiguousFunctionError" do + adapter = Fx::Adapters::Postgres.new + adapter.create_function( + <<~SQL + CREATE FUNCTION foo(x int) + RETURNS int AS $$ + BEGIN RETURN x; END; + $$ LANGUAGE plpgsql; + SQL + ) + adapter.create_function( + <<~SQL + CREATE FUNCTION foo(x int, y int) + RETURNS int AS $$ + BEGIN RETURN x + y; END; + $$ LANGUAGE plpgsql; + SQL + ) + + expect { adapter.drop_function(:foo) } + .to raise_error(Fx::AmbiguousFunctionError, /Multiple definitions/) + end + + it "drops the correct overload when arguments are specified" do + adapter = Fx::Adapters::Postgres.new + adapter.create_function( + <<~SQL + CREATE FUNCTION foo(x int) + RETURNS int AS $$ + BEGIN RETURN x; END; + $$ LANGUAGE plpgsql; + SQL + ) + adapter.create_function( + <<~SQL + CREATE FUNCTION foo(x int, y int) + RETURNS int AS $$ + BEGIN RETURN x + y; END; + $$ LANGUAGE plpgsql; + SQL + ) + + adapter.drop_function(:foo, arguments: "int, int") + + remaining = adapter.functions.select { |f| f.name == "foo" } + expect(remaining.length).to eq(1) + end + end end describe "#functions" do diff --git a/spec/fx/command_recorder_spec.rb b/spec/fx/command_recorder_spec.rb index a51dd29..5091d2c 100644 --- a/spec/fx/command_recorder_spec.rb +++ b/spec/fx/command_recorder_spec.rb @@ -17,6 +17,18 @@ expect(recorder.commands).to eq([[:drop_function, [:test]]]) end + + it "reverts to drop_function preserving arguments" do + recorder = ActiveRecord::Migration::CommandRecorder.new + + recorder.revert do + recorder.create_function :test, arguments: "integer, text" + end + + expect(recorder.commands).to eq( + [[:drop_function, [:test, {arguments: "integer, text"}]]] + ) + end end describe "#drop_function" do @@ -38,6 +50,16 @@ expect(recorder.commands).to eq([[:create_function, revert_args]]) end + it "reverts to create_function preserving arguments" do + recorder = ActiveRecord::Migration::CommandRecorder.new + args = [:test, {revert_to_version: 3, arguments: "integer"}] + revert_args = [:test, {arguments: "integer", version: 3}] + + recorder.revert { recorder.drop_function(*args) } + + expect(recorder.commands).to eq([[:create_function, revert_args]]) + end + it "raises when reverting without revert_to_version set" do recorder = ActiveRecord::Migration::CommandRecorder.new args = [:test, {another_argument: 1}] @@ -68,6 +90,16 @@ expect(recorder.commands).to eq([[:update_function, revert_args]]) end + it "reverts to update_function preserving arguments" do + recorder = ActiveRecord::Migration::CommandRecorder.new + args = [:test, {version: 2, revert_to_version: 1, arguments: "integer"}] + revert_args = [:test, {arguments: "integer", version: 1}] + + recorder.revert { recorder.update_function(*args) } + + expect(recorder.commands).to eq([[:update_function, revert_args]]) + end + it "raises when reverting without revert_to_version set" do recorder = ActiveRecord::Migration::CommandRecorder.new args = [:test, {version: 42, another_argument: 1}] diff --git a/spec/fx/statements_spec.rb b/spec/fx/statements_spec.rb index f893568..6578903 100644 --- a/spec/fx/statements_spec.rb +++ b/spec/fx/statements_spec.rb @@ -48,6 +48,15 @@ expect(database).to have_received(:drop_function).with(:test) end + + it "passes arguments through to the adapter" do + database = stubbed_database + + connection.drop_function(:test, arguments: "integer, text") + + expect(database).to have_received(:drop_function) + .with(:test, arguments: "integer, text") + end end describe "#update_function" do @@ -72,6 +81,16 @@ .with(:test, "a definition") end + it "passes arguments through to the adapter" do + database = stubbed_database + definition = stubbed_definition + + connection.update_function(:test, version: 3, arguments: "integer") + + expect(database).to have_received(:update_function) + .with(:test, definition.to_sql, arguments: "integer") + end + it "raises an error if not supplied a version" do expect do connection.update_function( From 1b80c7a4d73e4de83e9118974c42fe0d1db2e7c6 Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sun, 1 Mar 2026 08:59:13 +0100 Subject: [PATCH 02/10] Add CHANGELOG entry for function argument auto-detection --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd65c6..b44fd33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ changelog, see the [commits] for each version via the version links. - Add `Function#signature` for PostgreSQL function identity (#207) - Add PostgreSQL versioning policy, officially supporting PostgreSQL 14-18 (#194) - Refactor Statements module to use explicit keyword arguments instead of `**options` hash (#186) +- Auto-detect function argument types from `pg_proc` when dropping or updating + functions, fixing support for functions with parameters (#7) +- Add `arguments:` option to `drop_function` and `update_function` for + targeting specific overloads of functions that share a name - Internal refactorings / improvements - Add scheduled EOL check for Ruby, Rails, and PostgreSQL (#205) - Add GitHub release creation to release task (#209) From aad90dd0d29b42f790cbfe2477b5d838c994357a Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sat, 7 Mar 2026 11:52:02 +0100 Subject: [PATCH 03/10] Move constant --- lib/fx/adapters/postgres.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/fx/adapters/postgres.rb b/lib/fx/adapters/postgres.rb index 771cca0..e45d9c1 100644 --- a/lib/fx/adapters/postgres.rb +++ b/lib/fx/adapters/postgres.rb @@ -154,6 +154,8 @@ def drop_trigger(name, on:) execute("DROP TRIGGER #{name} ON #{on};") end + private + # The SQL query used to look up a function's argument types from pg_proc. FUNCTION_ARGUMENTS_QUERY = <<~SQL.freeze SELECT pg_get_function_identity_arguments(pp.oid) AS arguments @@ -165,8 +167,6 @@ def drop_trigger(name, on:) SQL private_constant :FUNCTION_ARGUMENTS_QUERY - private - attr_reader :connectable delegate :execute, to: :connection From 0702daea33ab32baa864dfaabe91ab18ddc4ac6f Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sat, 7 Mar 2026 23:27:45 +0100 Subject: [PATCH 04/10] Fix edge cases in function argument auto-detection - Use .presence on pg_get_function_identity_arguments result so no-arg functions preserve the original DROP FUNCTION name form instead of generating unnecessary empty parens - Handle quoted identifiers in schema-qualified names (e.g. "My_Schema"."My_Func") by parsing with a regex instead of split - Document that the arguments: option is Postgres-specific and that custom adapters must accept the keyword to use it --- lib/fx/adapters/postgres.rb | 13 ++++++++----- lib/fx/statements.rb | 6 ++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/fx/adapters/postgres.rb b/lib/fx/adapters/postgres.rb index e45d9c1..436d235 100644 --- a/lib/fx/adapters/postgres.rb +++ b/lib/fx/adapters/postgres.rb @@ -92,7 +92,8 @@ def create_trigger(sql_definition) # @param name [String, Symbol] The name of the function. # @param sql_definition [String] The SQL schema for the function. # @param arguments [String] Optional function argument types for - # identifying overloaded functions (e.g. "integer, text"). + # identifying overloaded functions (e.g. "integer, text"). This + # option is specific to the Postgres adapter. # # @return [void] def update_function(name, sql_definition, arguments: nil) @@ -128,7 +129,8 @@ def update_trigger(name, on:, sql_definition:) # identifying overloaded functions (e.g. "integer, text"). When not # provided, the argument types are looked up automatically from # pg_proc. If multiple overloads exist, an {Fx::AmbiguousFunctionError} - # is raised. + # is raised. This option is specific to the Postgres adapter; custom + # adapters that do not accept it will raise an ArgumentError. # # @return [void] def drop_function(name, arguments: nil) @@ -174,8 +176,9 @@ def drop_trigger(name, on:) def function_arguments_for(name) name_str = name.to_s - if name_str.include?(".") - schema, function_name = name_str.split(".", 2) + if (match = name_str.match(/\A"?([^"]+)"?\."?([^"]+)"?\z/)) + schema = match[1] + function_name = match[2] schema_condition = "pn.nspname = #{connection.quote(schema)}" else function_name = name_str @@ -193,7 +196,7 @@ def function_arguments_for(name) when 0 nil when 1 - rows.first["arguments"] + rows.first["arguments"].presence else signatures = rows.map { |r| "#{name_str}(#{r["arguments"]})" } raise Fx::AmbiguousFunctionError, <<~MSG.chomp diff --git a/lib/fx/statements.rb b/lib/fx/statements.rb index 93f5b13..ce53730 100644 --- a/lib/fx/statements.rb +++ b/lib/fx/statements.rb @@ -45,7 +45,8 @@ def create_function(name, version: 1, sql_definition: nil, revert_to_version: ni # the `version` argument to {#create_function}. # @param arguments [String] Function argument types for identifying # overloaded functions (e.g. "integer, text"). When omitted, the - # adapter auto-detects the signature from the database. + # Postgres adapter auto-detects the signature from the database. + # Custom adapters must accept this keyword to use it. # @return [void] The database response from executing the drop statement. # # @example Drop a function, rolling back to version 2 on rollback @@ -66,7 +67,8 @@ def drop_function(name, revert_to_version: nil, arguments: nil) # `sql_definition` takes precedence. # @param arguments [String] Function argument types for identifying # overloaded functions (e.g. "integer, text"). When omitted, the - # adapter auto-detects the signature from the database. + # Postgres adapter auto-detects the signature from the database. + # Custom adapters must accept this keyword to use it. # @return [void] The database response from executing the create statement. # # @example Update function to a given version From b764bd18577cdff99eaa338623ca8df934c0f08b Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sun, 8 Mar 2026 09:34:29 +0100 Subject: [PATCH 05/10] Use Function#signature in adapter drop logic Replace manual signature string construction with Function#signature in drop_function and the ambiguous function error message. --- lib/fx/adapters/postgres.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/fx/adapters/postgres.rb b/lib/fx/adapters/postgres.rb index 436d235..3b39e66 100644 --- a/lib/fx/adapters/postgres.rb +++ b/lib/fx/adapters/postgres.rb @@ -136,11 +136,12 @@ def update_trigger(name, on:, sql_definition:) def drop_function(name, arguments: nil) arguments ||= function_arguments_for(name) - if arguments - execute("DROP FUNCTION #{name}(#{arguments});") - else - execute("DROP FUNCTION #{name};") - end + function = Fx::Function.new( + "name" => name.to_s, + "definition" => "", + "arguments" => arguments + ) + execute("DROP FUNCTION #{function.signature};") end # Drops the trigger from the database @@ -198,7 +199,9 @@ def function_arguments_for(name) when 1 rows.first["arguments"].presence else - signatures = rows.map { |r| "#{name_str}(#{r["arguments"]})" } + signatures = rows.map { |r| + Fx::Function.new("name" => name_str, "definition" => "", "arguments" => r["arguments"]).signature + } raise Fx::AmbiguousFunctionError, <<~MSG.chomp Multiple definitions for function "#{name_str}": #{signatures.join(", ")}. Specify which to drop: drop_function :#{name_str}, arguments: "" From a775881d6f2cbf3f694feb85fef4e1d510c3a6c2 Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sun, 8 Mar 2026 09:35:42 +0100 Subject: [PATCH 06/10] Formatting --- lib/fx/adapters/postgres.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/fx/adapters/postgres.rb b/lib/fx/adapters/postgres.rb index 3b39e66..843950c 100644 --- a/lib/fx/adapters/postgres.rb +++ b/lib/fx/adapters/postgres.rb @@ -199,8 +199,12 @@ def function_arguments_for(name) when 1 rows.first["arguments"].presence else - signatures = rows.map { |r| - Fx::Function.new("name" => name_str, "definition" => "", "arguments" => r["arguments"]).signature + signatures = rows.map { |row| + Fx::Function.new( + "name" => name_str, + "definition" => "", + "arguments" => row["arguments"] + ).signature } raise Fx::AmbiguousFunctionError, <<~MSG.chomp Multiple definitions for function "#{name_str}": #{signatures.join(", ")}. From edb5d03d60e88f9a306a1935d4fc595247f5b25f Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sun, 8 Mar 2026 09:38:49 +0100 Subject: [PATCH 07/10] Reuse functions query for argument lookup Replace the separate FUNCTION_ARGUMENTS_QUERY with a lookup against the existing functions method, which already fetches arguments via pg_get_function_identity_arguments. --- lib/fx/adapters/postgres.rb | 59 ++++++++++--------------------------- 1 file changed, 15 insertions(+), 44 deletions(-) diff --git a/lib/fx/adapters/postgres.rb b/lib/fx/adapters/postgres.rb index 843950c..2f5c1d6 100644 --- a/lib/fx/adapters/postgres.rb +++ b/lib/fx/adapters/postgres.rb @@ -134,13 +134,16 @@ def update_trigger(name, on:, sql_definition:) # # @return [void] def drop_function(name, arguments: nil) - arguments ||= function_arguments_for(name) + function = if arguments + Fx::Function.new( + "name" => name.to_s, + "definition" => "", + "arguments" => arguments + ) + else + find_function(name) + end - function = Fx::Function.new( - "name" => name.to_s, - "definition" => "", - "arguments" => arguments - ) execute("DROP FUNCTION #{function.signature};") end @@ -159,53 +162,21 @@ def drop_trigger(name, on:) private - # The SQL query used to look up a function's argument types from pg_proc. - FUNCTION_ARGUMENTS_QUERY = <<~SQL.freeze - SELECT pg_get_function_identity_arguments(pp.oid) AS arguments - FROM pg_proc pp - JOIN pg_namespace pn ON pn.oid = pp.pronamespace - WHERE pp.proname = %{function_name} - AND pp.prokind = 'f' - AND %{schema_condition} - SQL - private_constant :FUNCTION_ARGUMENTS_QUERY - attr_reader :connectable delegate :execute, to: :connection - def function_arguments_for(name) + def find_function(name) name_str = name.to_s + matches = functions.select { |f| f.name == name_str } - if (match = name_str.match(/\A"?([^"]+)"?\."?([^"]+)"?\z/)) - schema = match[1] - function_name = match[2] - schema_condition = "pn.nspname = #{connection.quote(schema)}" - else - function_name = name_str - schema_condition = "pn.nspname = ANY(current_schemas(false))" - end - - rows = connection.execute( - FUNCTION_ARGUMENTS_QUERY % { - function_name: connection.quote(function_name), - schema_condition: schema_condition - } - ).to_a - - case rows.length + case matches.length when 0 - nil + Fx::Function.new("name" => name_str, "definition" => "") when 1 - rows.first["arguments"].presence + matches.first else - signatures = rows.map { |row| - Fx::Function.new( - "name" => name_str, - "definition" => "", - "arguments" => row["arguments"] - ).signature - } + signatures = matches.map(&:signature) raise Fx::AmbiguousFunctionError, <<~MSG.chomp Multiple definitions for function "#{name_str}": #{signatures.join(", ")}. Specify which to drop: drop_function :#{name_str}, arguments: "" From 98a8bfce1acbc10c7f79de989bad4826458402b4 Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sun, 8 Mar 2026 09:40:05 +0100 Subject: [PATCH 08/10] Style --- lib/fx/adapters/postgres.rb | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/fx/adapters/postgres.rb b/lib/fx/adapters/postgres.rb index 2f5c1d6..fc2c558 100644 --- a/lib/fx/adapters/postgres.rb +++ b/lib/fx/adapters/postgres.rb @@ -134,15 +134,16 @@ def update_trigger(name, on:, sql_definition:) # # @return [void] def drop_function(name, arguments: nil) - function = if arguments - Fx::Function.new( - "name" => name.to_s, - "definition" => "", - "arguments" => arguments - ) - else - find_function(name) - end + function = + if arguments + Fx::Function.new( + "name" => name.to_s, + "definition" => "", + "arguments" => arguments + ) + else + find_function(name) + end execute("DROP FUNCTION #{function.signature};") end @@ -168,9 +169,9 @@ def drop_trigger(name, on:) def find_function(name) name_str = name.to_s - matches = functions.select { |f| f.name == name_str } + matches = functions.select { |function| function.name == name_str } - case matches.length + case matches.size when 0 Fx::Function.new("name" => name_str, "definition" => "") when 1 From 169469ac58e5663588496d42a89b4973cc7c696a Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sun, 8 Mar 2026 09:46:46 +0100 Subject: [PATCH 09/10] Accept arguments keyword in create_function The command recorder passes arguments through when reverting drop_function, so create_function must accept it even though it is unused during creation. --- lib/fx/statements.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fx/statements.rb b/lib/fx/statements.rb index ce53730..bbfc79b 100644 --- a/lib/fx/statements.rb +++ b/lib/fx/statements.rb @@ -30,7 +30,7 @@ module Statements # $$ LANGUAGE plpgsql; # SQL # - def create_function(name, version: 1, sql_definition: nil, revert_to_version: nil) + def create_function(name, version: 1, sql_definition: nil, revert_to_version: nil, arguments: nil) validate_version_or_sql_definition_present!(version, sql_definition) sql_definition = resolve_sql_definition(sql_definition, name, version, :function) From 7bc3c5230df22300293a48960b396b8c8782d917 Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sun, 8 Mar 2026 09:55:38 +0100 Subject: [PATCH 10/10] Add CHANGELOG note for custom adapter interface change --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b44fd33..8bff1c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ changelog, see the [commits] for each version via the version links. functions, fixing support for functions with parameters (#7) - Add `arguments:` option to `drop_function` and `update_function` for targeting specific overloads of functions that share a name +- Custom adapters that override `drop_function` or `update_function` must + now accept an `arguments:` keyword argument - Internal refactorings / improvements - Add scheduled EOL check for Ruby, Rails, and PostgreSQL (#205) - Add GitHub release creation to release task (#209)