Skip to content

fix(store): stop legacy path migration from evicting live files (#717) - #784

Open
danmackinlay wants to merge 1 commit into
tobi:mainfrom
danmackinlay:fix/legacy-slug-shadowing
Open

fix(store): stop legacy path migration from evicting live files (#717)#784
danmackinlay wants to merge 1 commit into
tobi:mainfrom
danmackinlay:fix/legacy-slug-shadowing

Conversation

@danmackinlay

@danmackinlay danmackinlay commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #717 — the parts of it that are still reproducible on main (v2.6.3).

Index-time slugging is already gone (070147d stores literal filesystem paths), but two lossy-path bugs survived it, and both still cost snake_case corpora documents today.

1. The legacy-path migration evicts live files

findOrMigrateLegacyDocument() reconstructs the path a pre-2.6 index would have stored (handelize("2026_06_16.md")"2026-06-16.md") and adopts the row it finds. That inverse is lossy — a b.md, a_b.md and a-b.md all handalize to a-b.md — and the lookup never checked whether the row it matched belongs to a file that still exists. So it renames a live file's row onto the new document, and the live file disappears from the index.

On main today:

mkdir -p /tmp/qmd-717 && cd /tmp/qmd-717
echo hyphen     > 2026-06-16.md
echo underscore > 2026_06_16.md
qmd collection add /tmp/qmd-717 --name t717
qmd ls t717
# qmd://t717/2026_06_16.md      <- only one row; 2026-06-16.md is gone

The incremental form is worse, because it removes a document that was already indexed and searchable: index 2026-06-16.md alone, then add 2026_06_16.md and run qmd update — the hyphenated file is dropped. This is the OP's "Only one file is shown", and @manpreetmaso's Logseq journals (2026_06_16.md) are the worst case since every filename in that corpus has the collision.

Both legacy lookups now skip a candidate row whose path is still owned by a file in the current scan, so only genuinely stale rows are adopted. Legitimate migration of a pre-2.6 index is unaffected (covered by the existing migration test, plus a new one where the legacy slug collides with a live file).

handelize() is deliberately left alone. It is no longer a display or index function — its only caller is this migration, where its job is to reproduce the old path format byte for byte. Making it preserve _ would mean pre-2.6 snake_case indexes stop matching and stop migrating in place — precisely the users this issue is about. Added a doc comment saying so, since "why does this still slugify underscores?" is an obvious next question.

2. _ in a path is a SQL LIKE wildcard

Same report, different mechanism, and it returns the wrong document rather than none:

qmd get 2026_06_16.md    # printed the contents of 2026-06-16.md
qmd ls notes/2026_06     # listed notes/2026-06-… too

qmd get, qmd multi-get and qmd ls <prefix> interpolate the user's path straight into a LIKE pattern, where _ matches any character and % matches anything. Added escapeLikePattern() + an ESCAPE clause at the four call sites. I found this because the regression test for (1) failed on it.

Tests

test/path-fidelity.test.ts gains a "separator collisions (#717)" block: fresh index keeps all four of 2026-06-16.md / 2026_06_16.md / my file.md / my-file.md; adding an underscored twin to an indexed collection does not evict the hyphenated file (and each path still resolves to its own content); _ is matched literally by multi-get and ls; and a legacy row still migrates when a live file shares its slug. Plus a unit test for escapeLikePattern().

Full suite passes (952 passed / 7 skipped). Two pre-existing failures on my machine are unrelated to this change and fail identically on a clean main: a batch of cli.test.ts cases that assert stderr === "" and pick up Node's DEP0205 warning under this Node version, and mcp.test.ts > MCP HTTP Transport (SqliteError: no such column: T.name). Both go away with NODE_OPTIONS=--no-deprecation / are environmental.

Not addressed here

--full-path still falls back to the qmd:// URI when a result can't be resolved on disk, and drops the docid while doing it (get/multi-get keep it). With literal paths stored, that fallback now only fires for files that genuinely no longer exist, so it reads as a stale-index signal rather than a normalization bug — happy to follow up separately if you want it to warn or keep the docid.

…#717)

Documents have been stored under their literal filesystem path since
070147d, but `findOrMigrateLegacyDocument()` still reconstructs the slug a
pre-2.6 index would have stored and adopts the row it finds. That inverse is
lossy — "a b.md", "a_b.md" and "a-b.md" all handalize to "a-b.md" — and the
lookup did not check whether the row it matched belonged to a file that still
exists. Indexing "2026_06_16.md" alongside "2026-06-16.md" therefore renamed
the hyphenated file's row onto the underscored file and dropped the former
from the index: a fresh `collection add` of both silently indexed one, and
`qmd update` after adding an underscored twin removed the already-indexed
hyphenated file. snake_case corpora (Logseq journals, dated notes) hit this
constantly.

Both legacy lookups now skip any row whose path is still owned by a file in
the current scan, so only genuinely stale rows are adopted. `handelize()` is
deliberately unchanged: it exists solely to reproduce the pre-2.6 path format,
so preserving `_` there would strand exactly the snake_case indexes this
issue is about on the old format. Documented that on the function.

Also fixes a second wrong-document bug from the same report: `_` and `%` are
SQL LIKE wildcards, and the suffix/prefix lookups behind `qmd get`,
`qmd multi-get` and `qmd ls <prefix>` interpolated user paths raw. `qmd get
2026_06_16.md` could return the contents of a sibling "2026-06-16.md". Added
`escapeLikePattern()` and an ESCAPE clause at those four call sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PowderAddicts

PowderAddicts commented Jul 24, 2026 via email

Copy link
Copy Markdown

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.

Lossy path normalization breaks QMD_EDITOR_URI for files with spaces

2 participants