From 4979496735b31c7498acf453e5a9767a50053949 Mon Sep 17 00:00:00 2001 From: Tatamis <80774326+Tatamis@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:45:17 +0300 Subject: [PATCH 1/2] Fix FunctionMetadata.fields() crash when arg_names is None Fixes #1204. fields() guarded against a missing arg_modes, but zipped arg_names/arg_types/arg_modes unconditionally otherwise. A function with a truthy arg_modes but no arg_names (e.g. an unnamed variadic parameter, as in the reported hstore/variadic example) hit 'NoneType' object is not iterable instead of being handled. Fall back to None placeholders for arg_names/arg_types the same way args() already falls back for arg_modes a few lines up. --- pgcli/packages/parseutils/meta.py | 9 ++++++--- tests/parseutils/test_function_metadata.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/pgcli/packages/parseutils/meta.py b/pgcli/packages/parseutils/meta.py index df41cf4ee..465bbfc49 100644 --- a/pgcli/packages/parseutils/meta.py +++ b/pgcli/packages/parseutils/meta.py @@ -157,8 +157,11 @@ def fields(self): # E.g. 'SELECT unnest FROM unnest(...);' return [ColumnMetadata(self.func_name, self.return_type, [])] + # arg_modes being truthy doesn't guarantee arg_names/arg_types are + # populated too (e.g. an unnamed variadic parameter), so fall back + # to None placeholders the same way args() falls back for modes. + names = self.arg_names or [None] * len(self.arg_modes) + types = self.arg_types or [None] * len(self.arg_modes) return [ - ColumnMetadata(name, typ, []) - for name, typ, mode in zip(self.arg_names, self.arg_types, self.arg_modes) - if mode in ("o", "b", "t") + ColumnMetadata(name, typ, []) for name, typ, mode in zip(names, types, self.arg_modes) if mode in ("o", "b", "t") ] # OUT, INOUT, TABLE diff --git a/tests/parseutils/test_function_metadata.py b/tests/parseutils/test_function_metadata.py index c4000ab1c..42a6823da 100644 --- a/tests/parseutils/test_function_metadata.py +++ b/tests/parseutils/test_function_metadata.py @@ -11,3 +11,19 @@ def test_function_metadata_eq(): assert not (f1 == f3) assert hash(f1) == hash(f2) assert hash(f1) != hash(f3) + + +def test_function_metadata_fields_with_variadic_and_no_arg_names(): + # Regression test: arg_modes being truthy doesn't guarantee arg_names is + # populated (e.g. an unnamed variadic parameter). fields() used to crash + # with "'NoneType' object is not iterable" instead of returning []. + f = FunctionMetadata("s", "labels", None, ["text[]"], ["v"], "hstore", False, False, False, False, None) + assert f.fields() == [] + + +def test_function_metadata_fields_table_mode_with_no_arg_names(): + f = FunctionMetadata("s", "f", None, ["int4", "text"], ["t", "t"], "record", False, False, True, False, None) + fields = f.fields() + assert len(fields) == 2 + assert [field.datatype for field in fields] == ["int4", "text"] + assert [field.name for field in fields] == [None, None] From 2224ee062b4299fa58f332f0fb25612079ac48a2 Mon Sep 17 00:00:00 2001 From: Tatamis <80774326+Tatamis@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:08:25 +0300 Subject: [PATCH 2/2] Use the function name when fields() has no named output columns DiegoDAF pointed out that the earlier change only moved the crash: for a TABLE(...) function declared without argument names, fields() now returned ColumnMetadata entries with name=None, and generate_alias() then failed iterating that None. When arg_modes has no usable output column (unnamed TABLE columns, or only a variadic parameter), fall back to the function name as the column name, like the branch above already does for functions without output parameters. The variadic case now completes 'labels' instead of offering nothing, and the TABLE case no longer reaches generate_alias() with None. --- pgcli/packages/parseutils/meta.py | 17 ++++++++++------- tests/parseutils/test_function_metadata.py | 19 +++++++++++++------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/pgcli/packages/parseutils/meta.py b/pgcli/packages/parseutils/meta.py index 465bbfc49..73decbff4 100644 --- a/pgcli/packages/parseutils/meta.py +++ b/pgcli/packages/parseutils/meta.py @@ -158,10 +158,13 @@ def fields(self): return [ColumnMetadata(self.func_name, self.return_type, [])] # arg_modes being truthy doesn't guarantee arg_names/arg_types are - # populated too (e.g. an unnamed variadic parameter), so fall back - # to None placeholders the same way args() falls back for modes. - names = self.arg_names or [None] * len(self.arg_modes) - types = self.arg_types or [None] * len(self.arg_modes) - return [ - ColumnMetadata(name, typ, []) for name, typ, mode in zip(names, types, self.arg_modes) if mode in ("o", "b", "t") - ] # OUT, INOUT, TABLE + # populated too (e.g. an unnamed variadic parameter or a TABLE(...) + # return without argument names). + fields = [ + ColumnMetadata(name, typ, []) + for name, typ, mode in zip(self.arg_names or [], self.arg_types or [], self.arg_modes) + if mode in ("o", "b", "t") # OUT, INOUT, TABLE + ] + # Without any usable output column, fall back to the function name, + # as for functions declared without output parameters. + return fields or [ColumnMetadata(self.func_name, self.return_type, [])] diff --git a/tests/parseutils/test_function_metadata.py b/tests/parseutils/test_function_metadata.py index 42a6823da..938e2a5ea 100644 --- a/tests/parseutils/test_function_metadata.py +++ b/tests/parseutils/test_function_metadata.py @@ -1,4 +1,5 @@ -from pgcli.packages.parseutils.meta import FunctionMetadata +from pgcli.packages.parseutils.meta import ColumnMetadata, FunctionMetadata +from pgcli.pgcompleter import generate_alias def test_function_metadata_eq(): @@ -16,14 +17,20 @@ def test_function_metadata_eq(): def test_function_metadata_fields_with_variadic_and_no_arg_names(): # Regression test: arg_modes being truthy doesn't guarantee arg_names is # populated (e.g. an unnamed variadic parameter). fields() used to crash - # with "'NoneType' object is not iterable" instead of returning []. + # with "'NoneType' object is not iterable". f = FunctionMetadata("s", "labels", None, ["text[]"], ["v"], "hstore", False, False, False, False, None) - assert f.fields() == [] + assert f.fields() == [ColumnMetadata("labels", "hstore", [])] def test_function_metadata_fields_table_mode_with_no_arg_names(): + # Without argument names there is no output column name to offer, so the + # function name is used and generate_alias() gets a real string. f = FunctionMetadata("s", "f", None, ["int4", "text"], ["t", "t"], "record", False, False, True, False, None) fields = f.fields() - assert len(fields) == 2 - assert [field.datatype for field in fields] == ["int4", "text"] - assert [field.name for field in fields] == [None, None] + assert fields == [ColumnMetadata("f", "record", [])] + assert all(generate_alias(field.name) for field in fields) + + +def test_function_metadata_fields_table_mode_with_arg_names(): + f = FunctionMetadata("s", "f", ["a", "b"], ["int4", "text"], ["t", "t"], "record", False, False, True, False, None) + assert f.fields() == [ColumnMetadata("a", "int4", []), ColumnMetadata("b", "text", [])]