From 806b23cd6d1343188919bdd30ca39b3fc795ae50 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 29 Jul 2026 15:45:34 -0700 Subject: [PATCH 1/2] fix: preserve '=' inside --filter values Previously `apply_filter_value` called `value.split("=")`, so a filter like `Notes=x=y` silently truncated to name=Notes, value=x. Split on the first '=' only (maxsplit=1) so multi-'=' values round-trip intact, and raise a clear error when the input isn't in name=value form. Verified end-to-end against a real workbook: `Product Name=x=y Config Kit` now filters correctly instead of returning empty results. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../datasources_and_workbooks_command.py | 12 ++++++++++-- .../test_datasources_and_workbooks_command.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index ae577bfa..62145ebf 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -103,9 +103,17 @@ def apply_encoded_filter_value(logger, request_options: RequestOptionsType, valu @staticmethod def apply_filter_value(logger, request_options: RequestOptionsType, value: str) -> None: logger.debug("handling filter param {}".format(value)) - data_filter = value.split("=") + # Split on the first '=' only so that filter values containing '=' are + # preserved intact (e.g. Notes=x=y should filter Notes to the value "x=y"). + parts = value.split("=", maxsplit=1) + if len(parts) != 2: + Errors.exit_with_error( + logger, + message="Filter clause '{}' must be in name=value form".format(value), + ) + name, filter_value = parts # we should export the _DataExportOptions class from tsc - request_options.vf(data_filter[0], data_filter[1]) # type: ignore + request_options.vf(name, filter_value) # type: ignore # this is called from within from_url_params, for each param value # expects either ImageRequestOptions or PDFRequestOptions diff --git a/tests/commands/test_datasources_and_workbooks_command.py b/tests/commands/test_datasources_and_workbooks_command.py index 8fa60dde..d41ef428 100644 --- a/tests/commands/test_datasources_and_workbooks_command.py +++ b/tests/commands/test_datasources_and_workbooks_command.py @@ -40,6 +40,25 @@ def test_apply_encoded_filters_from_url_params(self): DatasourcesAndWorkbooks.apply_values_from_url_params(mock_logger, request_options, query_params) assert request_options.view_filters == expected + def test_apply_filter_value_with_equals_in_value(self): + # A filter value containing '=' should survive parsing intact. + # Old behavior: value.split("=") truncated at the second '=', + # so "Notes=x=y" incorrectly became name="Notes", value="x". + request_options = tsc.PDFRequestOptions() + DatasourcesAndWorkbooks.apply_filter_value(mock_logger, request_options, "Notes=x=y") + assert request_options.view_filters == [("Notes", "x=y")] + + def test_apply_filter_value_with_multiple_equals_in_value(self): + request_options = tsc.PDFRequestOptions() + DatasourcesAndWorkbooks.apply_filter_value(mock_logger, request_options, "Config=k1=v1=extra") + assert request_options.view_filters == [("Config", "k1=v1=extra")] + + def test_apply_filter_value_with_trailing_equals(self): + # Empty string after the '=' should produce an empty-string value. + request_options = tsc.PDFRequestOptions() + DatasourcesAndWorkbooks.apply_filter_value(mock_logger, request_options, "Name=") + assert request_options.view_filters == [("Name", "")] + def test_apply_options_from_url_params(self): query_params = "?:iid=5&:refresh=yes&:size=600,700" request_options = tsc.PDFRequestOptions() From 439b06751d726049285b751b9050d9823ece74c8 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 29 Jul 2026 16:16:50 -0700 Subject: [PATCH 2/2] test: add filter_test_data.csv fixture for e2e filter tests Dataset with product names containing '=', '&', ',', backslashes, and metacharacter samples for exercising --filter parsing edge cases against a real server. Also includes columns whose names contain '&', '#', and '\' so future tests can exercise special characters on both sides of the name=value pair. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/assets/filter_test_data.csv | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/assets/filter_test_data.csv diff --git a/tests/assets/filter_test_data.csv b/tests/assets/filter_test_data.csv new file mode 100644 index 00000000..6034d83a --- /dev/null +++ b/tests/assets/filter_test_data.csv @@ -0,0 +1,33 @@ +Product Name,Category,Region,Sales,Sales & Cost,Order#,Path\File +Widget Plain,Standard,West,100,10,W001,C:\a +AT&T 841000 Phone,Electronics,East,200,20,W002,C:\b +Salt & Pepper Shaker,Kitchenware,West,150,15,W003,C:\c +K&N Air Filter,Automotive,South,175,17,W004,C:\d +x=y Config Kit,Software,North,250,25,W005,C:\e +Config=default Bundle,Software,East,300,30,W006,C:\f +Formula=E=mc2 Poster,Books,West,50,5,W007,C:\g +"Rock, Paper, Scissors Game",Toys,South,80,8,W008,C:\h +"Comma, Separated, Product",Standard,North,90,9,W009,C:\i +AT&T x=y Combo,Electronics,East,400,40,W010,C:\j +Region=West Reference Book,Books,West,60,6,W011,C:\k +Zurich Special,Standard,East,120,12,W012,C:\l +Zürich Special,Standard,East,130,13,W013,C:\m +Empty Value Test,Standard,West,0,0,W014,C:\n +"Path C:\temp\file",Software,North,500,50,W015,"C:\temp\file" +"Escape\, comma",Software,East,510,51,W016,C:\o +"Trail\",Software,West,520,52,W017,C:\p +META /slash/,Test,North,100,10,W018,C:\q +META /percent%/,Test,North,101,10,W019,C:\r +META /plus+/,Test,North,102,10,W020,C:\s +META /colon:/,Test,North,103,10,W021,C:\t +META /semi;/,Test,North,104,10,W022,C:\u +META /brace{}/,Test,North,105,10,W023,C:\v +META /bracket[]/,Test,North,106,10,W024,C:\w +META /paren()/,Test,North,107,10,W025,C:\x +META /question?/,Test,North,108,10,W026,C:\y +META /hash#/,Test,North,109,10,W027,C:\z +META /star*/,Test,North,110,10,W028,C:\aa +META /at@/,Test,North,111,10,W029,C:\bb +"META /quote""/",Test,North,112,10,W030,C:\cc +META /apos'/,Test,North,113,10,W031,C:\dd +"META /less<>greater/",Test,North,114,10,W032,C:\ee