diff --git a/RULES.md b/RULES.md index c92db25..281848f 100644 --- a/RULES.md +++ b/RULES.md @@ -123,7 +123,7 @@ skipped unless `--execute` is passed. | `VGI313` | warning | argument-description-states-type | An argument description should not restate the data type (it's a separate field). | | | `VGI314` | warning | function-restates-argument-docs | A function's description shouldn't re-document its arguments (they'd drift). | | | `VGI315` | warning | argument-type-consistent | An argument name should map to one SQL type across all functions (no type drift). | | -| `VGI316` | warning | array-argument-could-be-table | A function with a single multi-dimensional-array argument should take a table input. | | +| `VGI316` | warning | array-argument-could-be-table | A table function with a single multi-dimensional-array argument should take a table input. | | | `VGI317` | error | constrained-argument-not-discoverable | An argument whose description enumerates allowed values or a numeric range should declare machine-readable constraints (choices / ge / le) so agents discover valid inputs. Needs a vgi extension exposing vgi_function_arguments(); silent on older ones. | | | `VGI318` | error | default-violates-constraint | An argument's declared default must satisfy its own constraints — be a member of choices, inside the numeric range, and match the pattern. Needs a vgi extension exposing vgi_function_arguments(); silent on older ones. | | | `VGI319` | warning | invalid-constraint | A declared constraint must be well-formed: the pattern must be a valid regex and the numeric range must be non-empty. Needs a vgi extension exposing vgi_function_arguments(). | | diff --git a/src/vgi_lint_check/rules/functions.py b/src/vgi_lint_check/rules/functions.py index e4d7d3c..71df586 100644 --- a/src/vgi_lint_check/rules/functions.py +++ b/src/vgi_lint_check/rules/functions.py @@ -798,16 +798,18 @@ class ArrayArgumentCouldBeTable(Rule): name = "array-argument-could-be-table" category = FUNC default_severity = Severity.WARNING - targets = ( - ObjectKind.SCALAR_FUNCTION, - ObjectKind.AGGREGATE, - ObjectKind.MACRO, - ObjectKind.TABLE_FUNCTION, + # Table functions only. The remedy ("expose a table/relation input so callers + # pass a subquery FROM …") is only structurally possible for a table function: + # a scalar or aggregate function returns one value per row and cannot take a + # relation as an argument, and DuckDB macros take scalar expressions, not + # relations — flagging any of them is a false positive with no achievable fix. + targets = (ObjectKind.TABLE_FUNCTION,) + summary = ( + "A table function with a single multi-dimensional-array argument should take a table input." ) - summary = "A function with a single multi-dimensional-array argument should take a table input." def check(self, ctx: RuleContext) -> Iterator[Finding]: - for f in ctx.catalog.iter_all_functions(): + for f in _iter_table_functions(ctx): array_args = [ a for a in f.arguments diff --git a/tests/unit/test_strict_rules.py b/tests/unit/test_strict_rules.py index e6d6b0a..8aac135 100644 --- a/tests/unit/test_strict_rules.py +++ b/tests/unit/test_strict_rules.py @@ -711,12 +711,12 @@ def test_vgi152_agent_test_tasks_present_nudge(): def test_vgi316_single_array_arg_suggests_table(): - def codes_for(*args): - fn = F.func("main", "solve", description="d", arguments=list(args)) + def codes_for(*args, ftype="table"): + fn = F.func("main", "solve", ftype=ftype, description="d", arguments=list(args)) s = F.schema("main", comment="c", tags=_TAGS, functions=[fn]) return {f.code for f in run(select_rules(Config()), RuleContext(F.catalog(s), Config()))} - # a single 2-D array argument (a matrix / list of rows) -> fires + # a TABLE FUNCTION with a single 2-D array argument (a matrix / list of rows) -> fires assert "VGI316" in codes_for(F.arg("distance", "BIGINT[][]", "the distance matrix")) # a list-of-struct (typed rows) -> fires assert "VGI316" in codes_for(F.arg("rows", "STRUCT(a INTEGER, b INTEGER)[]", "rows")) @@ -726,6 +726,15 @@ def codes_for(*args): ) # a plain 1-D array (a vector) -> not flagged assert "VGI316" not in codes_for(F.arg("v", "BIGINT[]", "a vector of weights")) + # a SCALAR function with the same matrix arg -> NOT flagged: a scalar returns one + # value per row and has no table-function form, so the remedy is impossible. + assert "VGI316" not in codes_for( + F.arg("distance", "BIGINT[][]", "the distance matrix"), ftype="scalar" + ) + # likewise an AGGREGATE with a matrix arg -> not flagged. + assert "VGI316" not in codes_for( + F.arg("distance", "BIGINT[][]", "the distance matrix"), ftype="aggregate" + ) def test_vgi145_view_wraps_table_function():