From a2ead53dd288dd33aed93f43843fbc307b0d75ae 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/3] 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 33cd256ee1e2466b0c4dec72507c8511ec767422 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/3] 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", [])] From 45fb35fe74bd2a3e39033b200494f87a433d4ef5 Mon Sep 17 00:00:00 2001 From: cristianchiriac <80774326+cristianchiriac@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:23:07 +0000 Subject: [PATCH 3/3] Add changelog entry and author for the fields() fix Claude-Session: https://claude.ai/code/session_01Ce8Hr56xHurqiV5wEkVmxj --- AUTHORS | 1 + changelog.rst | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AUTHORS b/AUTHORS index 45019555e..60f052aa1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -157,6 +157,7 @@ Contributors: * Chris (ChrisJr404) * Pieter Ouwerkerk (pouwerkerk) * VXNCXNX + * Cristian Chiriac (cristianchiriac) * jackwalkerlabs (Codex-assisted) diff --git a/changelog.rst b/changelog.rst index 5a9bc17f1..ba4f8690f 100644 --- a/changelog.rst +++ b/changelog.rst @@ -12,6 +12,11 @@ Bug fixes: ---------- * Fix the development setup instructions to activate the ``.venv`` directory created by ``uv venv``. +* Fix completion crashing with ``TypeError: 'NoneType' object is not iterable`` + for set-returning functions whose parameters or ``TABLE(...)`` output + columns have no names (e.g. an unnamed ``VARIADIC`` parameter). Such + functions now offer the function name as their column, like functions + without output parameters. 4.7.1 (2026-09-20) ==================