Automation
A Query That Matches Nothing Is a Silent Bug
An automation run reported success and changed nothing — its XPath matched zero nodes because the file had a default namespace it didn't account for. Selectors that find nothing don't error, they no-op, so any automation that edits via a selector has to assert it actually found the target.
- Automation
- Debugging
- XML
- Reliability
A config-editing playbook ran clean — green the whole way down, no errors — and changed absolutely nothing. The task that was supposed to update a value was gated on “did we find exactly one match?”, the match count came back zero, so the update quietly skipped. The run reported success because a count of zero isn’t an error. That’s the trap at the heart of a whole class of bugs: a selector that matches nothing doesn’t fail, it no-ops, and a no-op wearing a green checkmark is one of the harder bugs to even notice.
The specific gotcha: a default namespace
The mechanism in my case is worth knowing because it’s a classic. The XML being
edited declared a default namespace on its root element — every element in the
document belonged to that namespace, even though nothing carried a visible prefix.
The playbook’s XPath used plain, unprefixed names like /table/entry/property. In
XPath 1.0, an unprefixed name matches only elements in no namespace — so against a
document where everything is in a namespace, it matched zero nodes. Not an error.
Just zero.
The fix was to make the XPath namespace-aware (bind a prefix to the namespace, or match by local name regardless of namespace). But the namespace detail isn’t really the lesson. The lesson is what the zero match did: nothing, silently, while reporting success.
Zero matches isn’t an error. It’s an answer — the wrong one — delivered with a straight face.
Selectors fail open, and that’s the danger
Most selector languages share this behavior: ask for something that isn’t there and you get an empty result, not an exception. It’s by design, and usually convenient. But when a selector drives an action — “find this node and change it,” “find these rows and update them” — failing open means the action silently doesn’t happen:
- An XPath or CSS selector that matches nothing updates nothing.
- A
jqfilter with a slightly wrong path returnsnulland your script carries on. - A SQL
UPDATE ... WHEREwhose predicate matches no rows reports success and zero rows affected. - A
find -namewith the wrong pattern lists nothing and your loop body never runs.
In every case the tool did exactly what you asked. You just asked for something that matches nothing, and “nothing” is a perfectly valid result it’s happy to return.
Assert the match, don’t assume it
The defense is simple and worth making a habit: when a selector drives a change, assert that it found what you expected before you trust the result. If you’re about to edit “the one entry named X,” check that you matched exactly one. If a migration should touch a few hundred rows, check the affected count is in range, not just that the statement ran. The gate that silently skipped on zero in my case should instead have been loud — “expected to match a node here and found none” is a failure, not a clean skip.
Concretely:
- Count matches and fail (or warn loudly) on zero when zero is unexpected.
- Prefer “changed N, expected ~N” assertions over “command exited 0.”
- On a second idempotent run, a correct edit reports already done; a broken selector keeps reporting skipped/zero — that difference is diagnostic.
”It skipped” is data, if you’re looking
The tell was there the whole time: every edit task reported skipped, uniformly, including ones that should obviously have matched. A skip is easy to read as “no work needed,” but a uniform skip across cases that should differ is a fingerprint of “the selector matches nothing, ever.” The information was in the run output; it just looked like success. Reading skips and zero-counts as signals — not noise — is what turns this from a bug you find in production into one you catch on the first run.
The principle
Any time automation locates its target by a query — XPath, CSS, jq, a SQL
predicate, a glob — that query is a place the whole operation can quietly become a
no-op. The command succeeds, nothing changes, and everyone moves on. So make
“did I actually match the thing?” an explicit, checked precondition, not an
assumption. It’s the same family as
a job running not being a job working and why
scaffolding that passes its checks isn’t a finished feature —
green is not the same as done. If you’ve been burned by an edit that succeeded at
doing nothing, I’d like to hear it.