From f185f6348d6a6ed692c7b1c4f848b9624124027a Mon Sep 17 00:00:00 2001 From: Teo Ljungberg Date: Sun, 8 Mar 2026 09:26:44 +0100 Subject: [PATCH] Add Function#signature for PostgreSQL function identity Fetch argument types via pg_get_function_identity_arguments in the schema dump query and expose a signature method that returns the full PostgreSQL identity (e.g. "add(integer, integer)"). Use signature for equality and ordering so overloaded functions are correctly distinguished. --- lib/fx/adapters/postgres/functions.rb | 3 +- lib/fx/function.rb | 16 ++++- spec/fx/function_spec.rb | 96 ++++++++++++++++++++++++--- 3 files changed, 103 insertions(+), 12 deletions(-) diff --git a/lib/fx/adapters/postgres/functions.rb b/lib/fx/adapters/postgres/functions.rb index cf5d336f..639b7b88 100644 --- a/lib/fx/adapters/postgres/functions.rb +++ b/lib/fx/adapters/postgres/functions.rb @@ -12,7 +12,8 @@ class Functions FUNCTIONS_WITH_DEFINITIONS_QUERY = <<~SQL.freeze SELECT pp.proname AS name, - pg_get_functiondef(pp.oid) AS definition + pg_get_functiondef(pp.oid) AS definition, + pg_get_function_identity_arguments(pp.oid) AS arguments FROM pg_proc pp JOIN pg_namespace pn ON pn.oid = pp.pronamespace diff --git a/lib/fx/function.rb b/lib/fx/function.rb index 7f497ec9..a87ec542 100644 --- a/lib/fx/function.rb +++ b/lib/fx/function.rb @@ -4,15 +4,27 @@ class Function include Comparable attr_reader :name, :definition - delegate :<=>, to: :name def initialize(row) @name = row.fetch("name") @definition = row.fetch("definition") + @arguments = row.fetch("arguments", nil) + end + + def <=>(other) + signature <=> other.signature end def ==(other) - name == other.name && definition == other.definition + signature == other.signature && definition == other.definition + end + + def signature + if @arguments.nil? + name + else + "#{name}(#{@arguments})" + end end def to_schema diff --git a/spec/fx/function_spec.rb b/spec/fx/function_spec.rb index 6784b2ec..5174777c 100644 --- a/spec/fx/function_spec.rb +++ b/spec/fx/function_spec.rb @@ -2,44 +2,121 @@ RSpec.describe Fx::Function do describe "#<=>" do - it "delegates to `name`" do + it "delegates to `signature`" do function_a = Fx::Function.new( "name" => "name_a", - "definition" => "some definition" + "definition" => "some definition", + "arguments" => "" ) function_b = Fx::Function.new( "name" => "name_b", - "definition" => "some definition" + "definition" => "some definition", + "arguments" => "" ) function_c = Fx::Function.new( "name" => "name_c", - "definition" => "some definition" + "definition" => "some definition", + "arguments" => "" ) expect(function_b).to be_between(function_a, function_c) end + + it "orders overloads by signature" do + add_int = Fx::Function.new( + "name" => "add", + "definition" => "some definition", + "arguments" => "integer, integer" + ) + add_text = Fx::Function.new( + "name" => "add", + "definition" => "some definition", + "arguments" => "text, text" + ) + + expect(add_int <=> add_text).to be < 0 + end end describe "#==" do - it "compares `name` and `definition`" do + it "compares `signature` and `definition`" do function_a = Fx::Function.new( "name" => "name_a", - "definition" => "some definition" + "definition" => "some definition", + "arguments" => "" ) function_b = Fx::Function.new( "name" => "name_b", - "definition" => "some other definition" + "definition" => "some other definition", + "arguments" => "" ) expect(function_a).not_to eq(function_b) end + + it "distinguishes overloads with the same name" do + add_int = Fx::Function.new( + "name" => "add", + "definition" => "CREATE FUNCTION add(integer)", + "arguments" => "integer" + ) + add_text = Fx::Function.new( + "name" => "add", + "definition" => "CREATE FUNCTION add(text)", + "arguments" => "text" + ) + + expect(add_int).not_to eq(add_text) + end + end + + describe "#signature" do + it "returns name with arguments when arguments are present" do + function = Fx::Function.new( + "name" => "inc", + "definition" => "some definition", + "arguments" => "integer" + ) + + expect(function.signature).to eq("inc(integer)") + end + + it "returns name with multiple arguments" do + function = Fx::Function.new( + "name" => "add", + "definition" => "some definition", + "arguments" => "integer, integer" + ) + + expect(function.signature).to eq("add(integer, integer)") + end + + it "returns name with empty parens for no-arg functions" do + function = Fx::Function.new( + "name" => "now_utc", + "definition" => "some definition", + "arguments" => "" + ) + + expect(function.signature).to eq("now_utc()") + end + + it "returns just the name when arguments key is missing" do + function = Fx::Function.new( + "name" => "now_utc", + "definition" => "some definition" + ) + + expect(function.signature).to eq("now_utc") + end end describe "#to_schema" do it "returns a schema compatible version of the function" do function = Fx::Function.new( "name" => "uppercase_users_name", - "definition" => "CREATE OR REPLACE TRIGGER uppercase_users_name ..." + "definition" => "CREATE OR REPLACE TRIGGER uppercase_users_name ...", + "arguments" => "" ) expect(function.to_schema).to eq(<<-EOS) @@ -52,7 +129,8 @@ it "maintains backslashes" do function = Fx::Function.new( "name" => "regex", - "definition" => "CREATE OR REPLACE FUNCTION regex \\1" + "definition" => "CREATE OR REPLACE FUNCTION regex \\1", + "arguments" => "" ) expect(function.to_schema).to eq(<<-'EOS')