6985bf4850
使用者在搜尋框打 `%` 或 `_`,搜出來一堆跟他打的字無關的東西。
病根:pattern 一直是 `'%' + 使用者輸入 + '%'` 直接內插,而 SQLite 的 LIKE 有
兩個萬用字元(`%` 任意長度、`_` 任意一字)且**沒有預設跳脫字元**——不寫
ESCAPE 就沒有任何辦法表示「字面上的 %」。所以他打的符號被當成 pattern 語法:
`100%` → `%100%%` → 「100」後面接什麼都算 ⇒ 撈回一堆不相干的
`owner_id` → `%owner_id%` → `_` 匹配任一字元 ⇒ ownerXid 也中
單打 `%`/`_` → `%%%`/`%_%` → 整個庫都回來
舊病,不是 08-10 斷詞(#84)引進的:pattern 從來就是這樣拼的。之前關鍵字搜尋
幾乎恆為 0 命中,這個洞被那個洞蓋住;斷詞讓搜尋真的會回東西之後才浮出來。
斷詞那段一個字都沒動。
修法:pattern 產生點全部收斂到兩支 helper——
· escapeLikeLiteral():跳脫 `%` `_` `\` 三個字元
· CONTENT_LIKE 常數:每個 `content LIKE ?` 一律帶 `ESCAPE '\'`
為什麼跳脫字元本身(`\`)也要處理:宣告 ESCAPE 之後 `\` 就變成 pattern 裡有
意義的字元,而且它會把後面那個字吃掉、且不報錯——打 `C:\` 會變成去找 `C:%`
(真正的 `C:\` 反而漏掉);只跳脫 %/_ 而漏掉 `\`,打 `C:\_temp` 時 `\\` 先被
讀成字面 `\`、後面的 `_` 變回萬用字元 ⇒ 撈到 `C:\Xtemp`。三個是一組的。
`[` `]` `?` `*` 不需要跳脫(那是 GLOB/別的方言,LIKE 不吃),多跳只會白白吃掉
pattern 的 byte 預算。
連帶:跳脫會變長(`%`→`\%`),所以 50 bytes 上限改用「跳脫後」的長度算
(likeBytes),否則打 48 個 `%` 會產生 98 bytes 的 pattern ⇒ 退回 2026-08-03
修掉的那個 HTTP 500。不含這三個字元的查詢 likeBytes ≡ utf8Len ⇒ 既有查詢逐字不變。
search-long-query.test.ts 兩條「逐字比對謂詞字串」的斷言跟著新字串更新——改的是
比對用的常數、不是放寬檢查(仍逐字相等比對),pattern 本身一個字都沒變。
沒有引進第三種搜尋機制,沒有動資料層(三表不變),沒有動斷詞。