fix(store): stop legacy path migration from evicting live files (#717) - #784
Open
danmackinlay wants to merge 1 commit into
Open
fix(store): stop legacy path migration from evicting live files (#717)#784danmackinlay wants to merge 1 commit into
danmackinlay wants to merge 1 commit into
Conversation
…#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>
|
Dear tobi/qmd,
We would like to acknowledge that we have received your request and a ticket has been created.
A support representative will be reviewing your request and will send you a personal response.(usually within 24-48 hours).
Thank you for your patience.
…On Fri, Jul 24 2026, at 08:55 PM, tobi/qmd ***@***.***> wrote:
Fixes #717 — the parts of it that are still reproducible on (#717) main (v2.6.3).
Index-time slugging is already gone ( stores literal filesystem paths), but two lossy-path bugs survived it, and both still cost snake_case corpora documents today. (070147d) 070147d
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 ( (https://github.com/manpreetmaso) 2026_06_16.md) are the worst case since every filename in that corpus has the collision shape.
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). 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 handelize() is deliberately left alone. _ 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 qmd ls notes/2026_06 # printed the contents of 2026-06-16.md # 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 (#717) 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. You can view, comment on, or merge this pull request online at: #784 (#784) Commit Summary a73f4b9 fix(store): stop legacy path migration from evicting live files (#717) (a73f4b9) File Changes
( 5 files) (https://github.com/tobi/qmd/pull/784/files) M CHANGELOG.md
(19) (https://github.com/tobi/qmd/pull/784/files#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed) M src/cli/qmd.ts
(15) (https://github.com/tobi/qmd/pull/784/files#diff-e7252cdb14bd10e7feaedc038d3506947354fe9b644cd034e37a7f39c580859f) M src/store.ts
(68) (https://github.com/tobi/qmd/pull/784/files#diff-2717c7fb31c2070dd96f07394997809bc9be7f3fff03ab638db696cc480f39b6) M test/path-fidelity.test.ts
(155) (https://github.com/tobi/qmd/pull/784/files#diff-b64a9cad6d330085e7e182984b7a40d03d35cc68b02260c5b3c1cfa3b2f5a324) M test/store.helpers.unit.test.ts
(10) (https://github.com/tobi/qmd/pull/784/files#diff-ee2e697b3b8fd5a3ffc8512c1b66a3a95181f458fcca811a807feee58e058046) Patch Links: https://github.com/tobi/qmd/pull/784.patch (https://github.com/tobi/qmd/pull/784.patch) https://github.com/tobi/qmd/pull/784.diff (https://github.com/tobi/qmd/pull/784.diff)
—
Reply to this email directly, view it on GitHub, or (#784?email_source=notifications&email_token=BRF3HG6JOEME34NMFQFCONT5GPENTA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DCMRZGIZTAMRZGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVRTG633UMVZF6Y3MNFRWW) unsubscribe. (https://github.com/notifications/unsubscribe-auth/BRF3HG6327KTBV55PCL2CBL5GPENTAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMJSGM3DKMZQGE5US43TOVSTWNBZG4YTSNZSGUZTRILWAI)
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and (https://github.com/notifications/mobile/ios/BRF3HG77PLJE3TMPNYKIGJL5GPENTA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DCMRZGIZTAMRZGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVJTG633UMVZF62LPOM) Android. Download it today! (https://github.com/notifications/mobile/android/BRF3HG3GIUX4Y56T2PHQPLL5GPENTA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DCMRZGIZTAMRZGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVZTG633UMVZF6YLOMRZG62LE)
You are receiving this because you are subscribed to this thread. Message ID: <tobi/qmd/pull/784 @ github . com> [
{
***@***.***": "http://schema.org",
***@***.***": "EmailMessage",
"potentialAction": {
***@***.***": "ViewAction",
"target": "#784?email_source=notifications\u0026email_token=BRF3HG3R22OFDOCG5WUIQ5D5GPENTA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DCMRZGIZTAMRZGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVNTW2YLJNRPWG3DJMNVQ",
"url": "#784?email_source=notifications\u0026email_token=BRF3HG3R22OFDOCG5WUIQ5D5GPENTA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DCMRZGIZTAMRZGSTHEZLBONXW5KTTOVRHGY3SNFRGKZFFMV3GK3TUVNTW2YLJNRPWG3DJMNVQ",
"name": "View Pull Request"
},
"description": "View this Pull Request on GitHub",
"publisher": {
***@***.***": "Organization",
"name": "GitHub",
"url": "https://github.com"
}
}
]
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdanda-b.mdall handalize toa-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
maintoday:The incremental form is worse, because it removes a document that was already indexed and searchable: index
2026-06-16.mdalone, then add2026_06_16.mdand runqmd 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 SQLLIKEwildcardSame report, different mechanism, and it returns the wrong document rather than none:
qmd get,qmd multi-getandqmd ls <prefix>interpolate the user's path straight into aLIKEpattern, where_matches any character and%matches anything. AddedescapeLikePattern()+ anESCAPEclause at the four call sites. I found this because the regression test for (1) failed on it.Tests
test/path-fidelity.test.tsgains a "separator collisions (#717)" block: fresh index keeps all four of2026-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 bymulti-getandls; and a legacy row still migrates when a live file shares its slug. Plus a unit test forescapeLikePattern().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 ofcli.test.tscases that assertstderr === ""and pick up Node'sDEP0205warning under this Node version, andmcp.test.ts > MCP HTTP Transport(SqliteError: no such column: T.name). Both go away withNODE_OPTIONS=--no-deprecation/ are environmental.Not addressed here
--full-pathstill falls back to theqmd://URI when a result can't be resolved on disk, and drops the docid while doing it (get/multi-getkeep 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.