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

@jacalatajacalata 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):

InputClassictabcmd 2 (before)tabcmd 2 (this PR)
Product Name=Widget Plain (baseline)1 match1 match1 match
Product Name=x=y Config Kit (= in value)1 match0 (silent truncation)1 match
Product Name=AT&T 841000 Phone (& in value, URL syntax)0 (silent skip)ERROR0 (silent skip, WARNING logged)
Product Name=x=y Config Kit (via --filter)n/a0 (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 developmentJuly 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-actionsBot commented Aug 4, 2026

Copy link
Copy Markdown

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

CopilotAI 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.

FileDescription
tests/commands/test_datasources_and_workbooks_command.pyAdds regression tests for = inside filter values and non-strict URL fragment handling.
tests/assets/filter_test_data.csvAdds fixture data with special characters used in filter parsing scenarios.
tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.pyUpdates 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)
jacalataand 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 freeto 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

@jacalata