resplite 1.4.12 → 1.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resplite",
3
- "version": "1.4.12",
3
+ "version": "1.4.14",
4
4
  "description": "A RESP2 server with practical Redis compatibility, backed by SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -154,7 +154,11 @@ export function addDocument(db, idx, docId, score, replace, fields) {
154
154
  let ftsRowid;
155
155
  if (existing) {
156
156
  if (!replace) throw new Error('ERR document exists');
157
- ftsRowid = existing.fts_rowid;
157
+ // FTS5 contentless bug: INSERT OR REPLACE doesn't remove old tokens.
158
+ // Solution: assign a new fts_rowid to avoid token pollution.
159
+ const maxRow = db.prepare(`SELECT COALESCE(MAX(fts_rowid), 0) AS m FROM ${docmapT}`).get();
160
+ ftsRowid = maxRow.m + 1;
161
+ db.prepare(`UPDATE ${docmapT} SET fts_rowid = ? WHERE doc_id = ?`).run(ftsRowid, docId);
158
162
  } else {
159
163
  const maxRow = db.prepare(`SELECT COALESCE(MAX(fts_rowid), 0) AS m FROM ${docmapT}`).get();
160
164
  ftsRowid = maxRow.m + 1;
@@ -172,12 +176,12 @@ export function addDocument(db, idx, docId, score, replace, fields) {
172
176
  ).run(docId, score, fieldsJson, now, now);
173
177
  }
174
178
 
175
- // FTS5 contentless: cannot DELETE; use INSERT OR REPLACE to overwrite by rowid when replacing.
179
+ // FTS5 contentless: insert with new rowid (old rowid becomes orphaned and won't match via docmap join).
176
180
  const ftsColumns = ['rowid', ...fieldNames.sort()];
177
181
  const ftsValues = [ftsRowid, ...fieldNames.sort().map((f) => fields[f] ?? '')];
178
182
  const placeholders = ftsValues.map(() => '?').join(', ');
179
183
  const colList = ftsColumns.join(', ');
180
- db.prepare(`INSERT OR REPLACE INTO ${ftsT}(${colList}) VALUES (${placeholders})`).run(...ftsValues);
184
+ db.prepare(`INSERT INTO ${ftsT}(${colList}) VALUES (${placeholders})`).run(...ftsValues);
181
185
  });
182
186
 
183
187
  run();
@@ -196,6 +196,40 @@ describe('Search integration', () => {
196
196
  const err = v?.error ?? v;
197
197
  assert.ok(String(err).includes('Unknown index name'));
198
198
  });
199
+
200
+ it('FT.SEARCH prefix query should not match unrelated terms', async () => {
201
+ // Create a fresh index for this test
202
+ await sendCommand(port, argv('FT.CREATE', 'bugtest', 'SCHEMA', 'payload', 'TEXT'));
203
+
204
+ // Add document with "gorge" first
205
+ await sendCommand(port, argv('FT.ADD', 'bugtest', 'DOC1', '1', 'REPLACE', 'FIELDS', 'payload', 'gorge'));
206
+
207
+ // Verify it matches gorge*
208
+ const gorge1 = await sendCommand(port, argv('FT.SEARCH', 'bugtest', 'gorge*', 'NOCONTENT', 'LIMIT', '0', '10'));
209
+ const g1 = tryParseValue(gorge1, 0).value;
210
+ assert.equal(g1[0], 1, 'gorge* should match gorge');
211
+ assert.equal(g1[1].toString?.('utf8') ?? g1[1], 'DOC1');
212
+
213
+ // Replace with "martan"
214
+ await sendCommand(port, argv('FT.ADD', 'bugtest', 'DOC1', '1', 'REPLACE', 'FIELDS', 'payload', 'martan'));
215
+
216
+ // Verify FT.GET shows only martan
217
+ const get = await sendCommand(port, argv('FT.GET', 'bugtest', 'DOC1'));
218
+ const getArr = tryParseValue(get, 0).value;
219
+ assert.equal(getArr[0].toString?.('utf8') ?? getArr[0], 'payload');
220
+ assert.equal(getArr[1].toString?.('utf8') ?? getArr[1], 'martan');
221
+
222
+ // martan* should match
223
+ const martan = await sendCommand(port, argv('FT.SEARCH', 'bugtest', 'martan*', 'NOCONTENT', 'LIMIT', '0', '10'));
224
+ const m = tryParseValue(martan, 0).value;
225
+ assert.equal(m[0], 1, 'martan* should match martan');
226
+ assert.equal(m[1].toString?.('utf8') ?? m[1], 'DOC1');
227
+
228
+ // gorge* should NOT match martan
229
+ const gorge2 = await sendCommand(port, argv('FT.SEARCH', 'bugtest', 'gorge*', 'NOCONTENT', 'LIMIT', '0', '10'));
230
+ const g2 = tryParseValue(gorge2, 0).value;
231
+ assert.equal(g2[0], 0, 'gorge* should NOT match martan - this is the bug');
232
+ });
199
233
  });
200
234
 
201
235
  describe('Search persistence', () => {