Skip to content

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

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

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

Conversation

@jacalata

@jacalata jacalata commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Motivation

Two related client-side filter-parser bugs, both caught while porting
Classic scripts:

  • = inside a filter value was silently truncated (Notes=x=y became
    Notes=x). apply_filter_value split on every =; fix is
    split("=", maxsplit=1).
  • & inside a URL-syntax filter value used to hard-error under the new
    strict path introduced by the above fix (?Product%20Name=AT&T%20841000%20Phone
    -- a real Classic script pattern -- got split on & into a bogus
    T%20841000%20Phone fragment). Classic silently skipped such
    fragments. On the URL path we now rejoin fragments where the fragment
    after & has no = and isn't an options key, warning loudly to
    suggest %26 encoding.

Behavior change

For users:

Input Classic tabcmd 2 (before) tabcmd 2 (this PR)
Product%20Name=Widget%20Plain (baseline) 1 match 1 match 1 match
Product Name=x=y Config Kit (via --filter) 1 match 0 (silent truncation) 1 match
Product%20Name=AT&T%20841000%20Phone (URL syntax) 0 (silent skip) ERROR 1 match (rejoined, WARNING logged)

Note that spaces in URL query strings must be percent-encoded as %20;
literal spaces fail at tabcmd's URL parser (cannot include spaces)
before reaching this code path. The URL-path rejoin is stricter than
Classic's silent skip but avoids Classic's own footgun of silently
returning wrong data when the truncated prefix happens to match rows.

Adds tests/assets/filter_test_data.csv with product names containing
=, &, ,, backslashes, unicode, and quotes -- staged for future e2e
filter tests, not consumed by tests in this PR yet.

Test plan

  • pytest tests/commands/test_datasources_and_workbooks_command.py
    — 34 passed
  • pytest tests/ — 346 passed, 2 skipped
  • End-to-end: tabcmd export --csv --filter "Product Name=x=y Config Kit" returns the expected row against a live server
  • End-to-end (2026-08-10, main-windows.tsi.lan): tabcmd get "views/escapeyvalues/Sheet1.csv?Product%20Name=AT&T%20841000%20Phone"
    returns the AT&T 841000 Phone row; WARNING logged suggesting %26
    encoding

🤖 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.py1691818 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%
TOTAL288545884% 

@jacalata
jacalata requested a lite review from Copilot August 6, 2026 21:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds more robust parsing for filter clauses (especially when values contain = or URL fragments are malformed) and expands test coverage and fixtures for these edge cases.

Changes:

  • Update filter parsing to split on the first = only and support strict vs non-strict behavior.
  • Make URL-embedded filter parsing non-strict to match tabcmd Classic’s “skip invalid fragments” behavior.
  • Add tests (and a CSV fixture) covering = in values, empty values, and &-split URL fragments.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
tests/commands/test_datasources_and_workbooks_command.py Adds regression tests for = inside filter values and non-strict URL fragment handling.
tests/assets/filter_test_data.csv Adds fixture data with special characters used in filter parsing scenarios.
tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py Updates filter parsing logic and introduces strict mode to control behavior for malformed clauses.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

# fragment. Match tabcmd Classic's silent-skip behavior so drop-in
# migration of scripts that contain literal '&' in filter values
# (which the parser splits on) doesn't hard-fail.
logger.warning("Skipping unparseable filter clause from URL: %r", value)
jacalata and others added 2 commits August 7, 2026 00:00
Black 22 (pinned in pyproject.toml) reformats these two files;
the CI 'Check formatting with black' step was failing.
The prior fix for `?Product Name=AT&T 841000 Phone` matched Classic by
skipping the "T 841000 Phone" fragment, but it also applied the truncated
first fragment (`Product Name=AT`) as a real filter. On any dataset with
a matching "AT" row that produces silent wrong data with only a WARNING
log the user might miss.

Detect the pattern: if a fragment after '&' has no '=' and isn't a
tabcmd options key (":..."), assume it's a continuation of the previous
value and rejoin. Warns loudly that the URL should encode '&' as '%26'
so the ambiguity is visible.

Tests cover the AT&T case, multiple '&' in a value, options mixed with
filters, and multiple legitimate filters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants