Skip to content

fix: preserve '=' inside --filter values - #442

Open
jacalata wants to merge 3 commits into
developmentfrom
jac/filter-equals-in-value
Open

fix: preserve '=' inside --filter values#442
jacalata wants to merge 3 commits into
developmentfrom
jac/filter-equals-in-value

Conversation

@jacalata

@jacalata jacalata commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Two related client-side filter-parser fixes, plus test data.

= inside a filter value was silently truncated. apply_filter_value split on every =, so --filter "Notes=x=y" became name=Notes, value=x. Values that legitimately contain = (config strings, formulas, part numbers, etc.) got dropped. Fix: split("=", maxsplit=1) and raise a clear error when the input isn't in name=value form.

& inside a URL-syntax filter value used to hard-error under the strict path introduced above. tabcmd export "view?Product Name=AT&T 841000 Phone" — a tabcmd Classic script pattern — got split on & into a bogus T 841000 Phone fragment, which the new strict apply_filter_value refused. Classic silently skipped such fragments; verified end-to-end against a live server that this is a real regression on drop-in Classic script migration. Fix: apply_filter_value now takes strict: bool = True. --filter callers keep strict validation; the URL-syntax code path (apply_encoded_filter_value) passes strict=False, which logs a WARNING and skips the fragment.

Test fixturetests/assets/filter_test_data.csv with product names, columns and values containing =, &, ,, backslashes, and various metacharacters, for e2e filter-parsing tests.

Verification

Against a live Tableau Server workbook (Product Name column, escapeyvalues/Sheet1):

Input Classic tabcmd 2 (before) tabcmd 2 (this PR)
Product Name=Widget Plain (baseline) 1 match 1 match 1 match
Product Name=x=y Config Kit (= in value) 1 match 0 (silent truncation) 1 match
Product Name=AT&T 841000 Phone (& in value, URL syntax) 0 (silent skip) ERROR 0 (silent skip, WARNING logged)
Product Name=x=y Config Kit (via --filter) n/a 0 (silent truncation) 1 match

Test plan

  • pytest tests/commands/test_datasources_and_workbooks_command.py — 31 passed
  • pytest tests/ --ignore=tests/e2e — 343 passed, 2 skipped
  • End-to-end tabcmd export --csv --filter "Product Name=x=y Config Kit" returns the expected row
  • End-to-end tabcmd export "escapeyvalues/Sheet1?Product Name=AT&T 841000 Phone" (Classic URL syntax) no longer errors; matches Classic's silent-skip behavior

🤖 Generated with Claude Code

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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@jacalata
jacalata changed the base branch from main to development July 30, 2026 21:57
…rity)

The earlier tightening in this PR made apply_filter_value raise a clean
error when a clause isn't in name=value form. That's the right behavior
for the --filter flag, but URL-syntax exports inherited from tabcmd
Classic scripts often contain literal '&' inside filter values (e.g.
`?Product Name=AT&T 841000 Phone`), which get split into fragments the
parser can't understand.

Classic silently skipped such fragments. Verified end-to-end against a
Tableau Server today: with the strict path, tabcmd 2 errored out
("Filter clause 'T 841000 Phone' must be in name=value form") where
Classic silently continued and let the server return whatever the well-
formed portion matched.

Add strict: bool = True to apply_filter_value. --filter callers stay
strict; apply_encoded_filter_value (the URL path) passes strict=False,
which logs a WARNING and skips the fragment instead of exiting.

Adds three tests: strict path still exits, non-strict path skips
silently, and end-to-end apply_values_from_url_params tolerates a value
with '&'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
tabcmd
   __main__.py121212 0%
   _version.py111111 0%
   tabcmd.py151515 0%
   version.py955 44%
tabcmd/commands
   commands.py101010 0%
   constants.py771818 77%
   server.py1351818 87%
tabcmd/commands/auth
   session.py3945050 87%
tabcmd/commands/datasources_and_workbooks
   datasources_and_workbooks_command.py1631818 89%
   datasources_workbooks_views_url_parser.py14255 96%
   delete_command.py601616 73%
   export_command.py1202525 79%
   get_url_command.py1274747 63%
   publish_command.py1232828 77%
   runschedule_command.py2177 67%
tabcmd/commands/extracts
   create_extracts_command.py4288 81%
   decrypt_extracts_command.py2722 93%
   delete_extracts_command.py3766 84%
   encrypt_extracts_command.py2722 93%
   extracts.py2022 90%
   reencrypt_extracts_command.py2722 93%
   refresh_extracts_command.py481010 79%
tabcmd/commands/group
   create_group_command.py2955 83%
   delete_group_command.py2722 93%
tabcmd/commands/project
   create_project_command.py4688 83%
   delete_project_command.py3544 89%
   publish_samples_command.py3044 87%
tabcmd/commands/site
   create_site_command.py3455 85%
   delete_site_command.py2722 93%
   edit_site_command.py3822 95%
   list_command.py771212 84%
   list_sites_command.py2922 93%
tabcmd/commands/user
   add_users_command.py2955 83%
   create_site_users.py581111 81%
   create_users_command.py5999 85%
   delete_site_users_command.py4355 88%
   user_data.py2223131 86%
tabcmd/execution
   _version.py222 0%
   global_options.py12588 94%
   localize.py661111 83%
   logger_config.py6066 90%
   tabcmd_controller.py4277 83%
TOTAL287945884% 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant