resplite 1.4.16 → 1.4.18
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
|
@@ -72,6 +72,10 @@ CREATE TABLE IF NOT EXISTS search_indices (
|
|
|
72
72
|
created_at INTEGER NOT NULL,
|
|
73
73
|
updated_at INTEGER NOT NULL
|
|
74
74
|
);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS search_rowid_allocator (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
78
|
+
);
|
|
75
79
|
`;
|
|
76
80
|
|
|
77
81
|
/** Type enum: 1=string, 2=hash, 3=set, 4=list, 5=zset */
|
|
@@ -68,6 +68,17 @@ function deleteFtsRow(db, ftsTableName, ftsRowid, fieldNames, fields) {
|
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Allocate a monotonic FTS rowid that is never reused, even after deletes.
|
|
73
|
+
* This prevents legacy stale tokens from being remapped to a new document.
|
|
74
|
+
* @param {import('better-sqlite3').Database} db
|
|
75
|
+
* @returns {number}
|
|
76
|
+
*/
|
|
77
|
+
function allocateFtsRowid(db) {
|
|
78
|
+
const info = db.prepare('INSERT INTO search_rowid_allocator DEFAULT VALUES').run();
|
|
79
|
+
return Number(info.lastInsertRowid);
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
/**
|
|
72
83
|
* Build canonical schema JSON (fields sorted by name). D.12.2
|
|
73
84
|
* @param {{ name: string, type: string }[]} fields
|
|
@@ -194,15 +205,16 @@ export function addDocument(db, idx, docId, score, replace, fields) {
|
|
|
194
205
|
let ftsRowid;
|
|
195
206
|
if (existing) {
|
|
196
207
|
if (!replace) throw new Error('ERR document exists');
|
|
197
|
-
|
|
208
|
+
const previousRowid = existing.fts_rowid;
|
|
209
|
+
ftsRowid = allocateFtsRowid(db);
|
|
210
|
+
db.prepare(`UPDATE ${docmapT} SET fts_rowid = ? WHERE doc_id = ?`).run(ftsRowid, docId);
|
|
198
211
|
const oldDoc = db.prepare(`SELECT fields_json FROM ${docsT} WHERE doc_id = ?`).get(docId);
|
|
199
212
|
if (oldDoc?.fields_json) {
|
|
200
213
|
const oldFields = JSON.parse(oldDoc.fields_json);
|
|
201
|
-
deleteFtsRow(db, ftsT,
|
|
214
|
+
deleteFtsRow(db, ftsT, previousRowid, fieldNames, oldFields);
|
|
202
215
|
}
|
|
203
216
|
} else {
|
|
204
|
-
|
|
205
|
-
ftsRowid = maxRow.m + 1;
|
|
217
|
+
ftsRowid = allocateFtsRowid(db);
|
|
206
218
|
db.prepare(`INSERT INTO ${docmapT}(doc_id, fts_rowid) VALUES (?, ?)`).run(docId, ftsRowid);
|
|
207
219
|
}
|
|
208
220
|
|
package/test/unit/search.test.js
CHANGED
|
@@ -88,6 +88,31 @@ describe('Search layer', () => {
|
|
|
88
88
|
assert.equal(deleteDocument(db, 'names', 'nonexistent'), 0);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
it('delete + re-add never remaps legacy stale tokens', () => {
|
|
92
|
+
createIndex(db, 'legacy_stale', [{ name: 'payload', type: 'TEXT' }]);
|
|
93
|
+
addDocument(db, 'legacy_stale', 'doc1', 1, true, { payload: 'gorrion' });
|
|
94
|
+
|
|
95
|
+
const mapped = db
|
|
96
|
+
.prepare('SELECT fts_rowid FROM search_docmap__legacy_stale WHERE doc_id = ?')
|
|
97
|
+
.get('doc1');
|
|
98
|
+
const oldRowid = mapped.fts_rowid;
|
|
99
|
+
|
|
100
|
+
// Simulate a legacy-polluted index row where stale term postings exist for the same rowid.
|
|
101
|
+
db.prepare('INSERT INTO search_fts__legacy_stale(rowid, payload) VALUES (?, ?)').run(oldRowid, 'bicho');
|
|
102
|
+
|
|
103
|
+
assert.equal(search(db, 'legacy_stale', 'bicho*', { noContent: true }).total, 1);
|
|
104
|
+
assert.equal(deleteDocument(db, 'legacy_stale', 'doc1'), 1);
|
|
105
|
+
|
|
106
|
+
addDocument(db, 'legacy_stale', 'doc1', 1, true, { payload: 'gorrion' });
|
|
107
|
+
const remapped = db
|
|
108
|
+
.prepare('SELECT fts_rowid FROM search_docmap__legacy_stale WHERE doc_id = ?')
|
|
109
|
+
.get('doc1');
|
|
110
|
+
assert.notEqual(remapped.fts_rowid, oldRowid);
|
|
111
|
+
|
|
112
|
+
assert.equal(search(db, 'legacy_stale', 'gorrion*', { noContent: true }).total, 1);
|
|
113
|
+
assert.equal(search(db, 'legacy_stale', 'bicho*', { noContent: true }).total, 0);
|
|
114
|
+
});
|
|
115
|
+
|
|
91
116
|
it('search with NOCONTENT returns total and doc ids', () => {
|
|
92
117
|
const r = search(db, 'names', 'hello', { noContent: true, offset: 0, count: 10 });
|
|
93
118
|
assert.equal(typeof r.total, 'number');
|