jexidb 2.1.8 → 2.1.9
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/dist/Database.cjs +286 -0
- package/package.json +1 -1
- package/src/Database.mjs +50 -0
- package/src/managers/IndexManager.mjs +266 -0
package/dist/Database.cjs
CHANGED
|
@@ -2154,6 +2154,247 @@ class IndexManager {
|
|
|
2154
2154
|
return candidateLines.size > 0;
|
|
2155
2155
|
}
|
|
2156
2156
|
|
|
2157
|
+
/**
|
|
2158
|
+
* Evaluate many index-only existence checks while sharing the same field data.
|
|
2159
|
+
* Returns a map keyed by the criteria id so callers can reuse a single scan per field.
|
|
2160
|
+
*
|
|
2161
|
+
* @param {string} fieldName - Indexed field name
|
|
2162
|
+
* @param {Array<Object>} criteriaArray - Array of { id, terms, options }
|
|
2163
|
+
* @param {Object} opts - { limit, allowPartial } (limit only applies when allowPartial is true)
|
|
2164
|
+
* @returns {Object<string, boolean>} - Map of criteria id to boolean existence
|
|
2165
|
+
*/
|
|
2166
|
+
multiExists(fieldName, criteriaArray, opts = {}) {
|
|
2167
|
+
const results = {};
|
|
2168
|
+
if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
|
|
2169
|
+
return results;
|
|
2170
|
+
}
|
|
2171
|
+
const prepared = [];
|
|
2172
|
+
for (let i = 0; i < criteriaArray.length; i++) {
|
|
2173
|
+
const entry = criteriaArray[i] || {};
|
|
2174
|
+
const key = entry.id !== undefined && entry.id !== null ? String(entry.id) : `criteria-${i}`;
|
|
2175
|
+
results[key] = false;
|
|
2176
|
+
prepared.push({
|
|
2177
|
+
id: key,
|
|
2178
|
+
terms: entry.terms,
|
|
2179
|
+
options: entry.options || {}
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
2183
|
+
return results;
|
|
2184
|
+
}
|
|
2185
|
+
const fieldIndex = this.index?.data?.[fieldName];
|
|
2186
|
+
if (!fieldIndex || typeof fieldIndex !== 'object') {
|
|
2187
|
+
return results;
|
|
2188
|
+
}
|
|
2189
|
+
const termManager = this.database?.termManager;
|
|
2190
|
+
const isTermMappingField = Boolean(termManager && termManager.termMappingFields && termManager.termMappingFields.includes(fieldName));
|
|
2191
|
+
const normalizedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : Infinity;
|
|
2192
|
+
const allowPartial = Boolean(opts.allowPartial);
|
|
2193
|
+
const positiveTarget = allowPartial ? Math.max(1, normalizedLimit) : Infinity;
|
|
2194
|
+
const termCache = new Map();
|
|
2195
|
+
let caseInsensitiveLookup = null;
|
|
2196
|
+
let termMappingLookup = null;
|
|
2197
|
+
const normalizeValues = value => {
|
|
2198
|
+
if (value === null || value === undefined) {
|
|
2199
|
+
return [];
|
|
2200
|
+
}
|
|
2201
|
+
if (Array.isArray(value)) {
|
|
2202
|
+
return value.slice();
|
|
2203
|
+
}
|
|
2204
|
+
return [value];
|
|
2205
|
+
};
|
|
2206
|
+
const buildCaseInsensitiveLookup = () => {
|
|
2207
|
+
const map = new Map();
|
|
2208
|
+
for (const key in fieldIndex) {
|
|
2209
|
+
const lowerKey = key.toLowerCase();
|
|
2210
|
+
if (!map.has(lowerKey)) {
|
|
2211
|
+
map.set(lowerKey, key);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return map;
|
|
2215
|
+
};
|
|
2216
|
+
const buildTermMappingLookup = () => {
|
|
2217
|
+
const map = new Map();
|
|
2218
|
+
const termToId = termManager?.termToId;
|
|
2219
|
+
if (termToId) {
|
|
2220
|
+
for (const [termStr, id] of termToId.entries()) {
|
|
2221
|
+
const lower = termStr.toLowerCase();
|
|
2222
|
+
if (!map.has(lower)) {
|
|
2223
|
+
map.set(lower, String(id));
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return map;
|
|
2228
|
+
};
|
|
2229
|
+
const resolveTermKey = (value, caseInsensitive) => {
|
|
2230
|
+
if (value === null || value === undefined) {
|
|
2231
|
+
return null;
|
|
2232
|
+
}
|
|
2233
|
+
if (isTermMappingField) {
|
|
2234
|
+
if (typeof value === 'string') {
|
|
2235
|
+
if (caseInsensitive) {
|
|
2236
|
+
if (!termMappingLookup) {
|
|
2237
|
+
termMappingLookup = buildTermMappingLookup();
|
|
2238
|
+
}
|
|
2239
|
+
return termMappingLookup.get(value.toLowerCase()) ?? null;
|
|
2240
|
+
}
|
|
2241
|
+
const termId = termManager?.getTermIdWithoutIncrement(String(value));
|
|
2242
|
+
if (termId === undefined || termId === null) {
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
return String(termId);
|
|
2246
|
+
}
|
|
2247
|
+
return String(value);
|
|
2248
|
+
}
|
|
2249
|
+
if (caseInsensitive && typeof value === 'string') {
|
|
2250
|
+
if (!caseInsensitiveLookup) {
|
|
2251
|
+
caseInsensitiveLookup = buildCaseInsensitiveLookup();
|
|
2252
|
+
}
|
|
2253
|
+
return caseInsensitiveLookup.get(value.toLowerCase()) ?? null;
|
|
2254
|
+
}
|
|
2255
|
+
return String(value);
|
|
2256
|
+
};
|
|
2257
|
+
const ensureTermEntry = termKey => {
|
|
2258
|
+
if (!termCache.has(termKey)) {
|
|
2259
|
+
const data = fieldIndex[termKey];
|
|
2260
|
+
const entry = {
|
|
2261
|
+
data,
|
|
2262
|
+
hasData: Boolean(data && (data.set && data.set.size > 0 || data.ranges && data.ranges.length > 0)),
|
|
2263
|
+
lineArray: null
|
|
2264
|
+
};
|
|
2265
|
+
termCache.set(termKey, entry);
|
|
2266
|
+
}
|
|
2267
|
+
return termCache.get(termKey);
|
|
2268
|
+
};
|
|
2269
|
+
const getLineArray = entry => {
|
|
2270
|
+
if (!entry.hasData || !entry.data) return [];
|
|
2271
|
+
if (!entry.lineArray) {
|
|
2272
|
+
entry.lineArray = this._getAllLineNumbers(entry.data);
|
|
2273
|
+
}
|
|
2274
|
+
return entry.lineArray;
|
|
2275
|
+
};
|
|
2276
|
+
const applyExcludes = (lineSet, excludeKeys) => {
|
|
2277
|
+
if (excludeKeys.length === 0) {
|
|
2278
|
+
return lineSet.size > 0;
|
|
2279
|
+
}
|
|
2280
|
+
for (const excludeKey of excludeKeys) {
|
|
2281
|
+
const excludeEntry = ensureTermEntry(excludeKey);
|
|
2282
|
+
if (!excludeEntry.hasData) continue;
|
|
2283
|
+
const excludeLines = getLineArray(excludeEntry);
|
|
2284
|
+
for (const line of excludeLines) {
|
|
2285
|
+
lineSet.delete(line);
|
|
2286
|
+
}
|
|
2287
|
+
if (lineSet.size === 0) {
|
|
2288
|
+
return false;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return lineSet.size > 0;
|
|
2292
|
+
};
|
|
2293
|
+
let successes = 0;
|
|
2294
|
+
for (const criterion of prepared) {
|
|
2295
|
+
if (successes >= positiveTarget) {
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
const {
|
|
2299
|
+
id,
|
|
2300
|
+
terms,
|
|
2301
|
+
options
|
|
2302
|
+
} = criterion;
|
|
2303
|
+
const {
|
|
2304
|
+
$all = false,
|
|
2305
|
+
caseInsensitive = false,
|
|
2306
|
+
excludes = []
|
|
2307
|
+
} = options;
|
|
2308
|
+
const normalizedTerms = normalizeValues(terms);
|
|
2309
|
+
if (normalizedTerms.length === 0) {
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
const normalizedExcludes = normalizeValues(excludes);
|
|
2313
|
+
const resolvedKeys = [];
|
|
2314
|
+
const seenKeys = new Set();
|
|
2315
|
+
let missingRequiredTerm = false;
|
|
2316
|
+
for (const term of normalizedTerms) {
|
|
2317
|
+
const termKey = resolveTermKey(term, caseInsensitive);
|
|
2318
|
+
if (termKey === null) {
|
|
2319
|
+
if ($all) {
|
|
2320
|
+
missingRequiredTerm = true;
|
|
2321
|
+
break;
|
|
2322
|
+
}
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (!seenKeys.has(termKey)) {
|
|
2326
|
+
seenKeys.add(termKey);
|
|
2327
|
+
resolvedKeys.push(termKey);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if ($all && missingRequiredTerm) {
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
if (resolvedKeys.length === 0) {
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
const excludeKeySet = [];
|
|
2337
|
+
const seenExcludeKeys = new Set();
|
|
2338
|
+
for (const excludeTerm of normalizedExcludes) {
|
|
2339
|
+
const excludeKey = resolveTermKey(excludeTerm, caseInsensitive);
|
|
2340
|
+
if (excludeKey && !seenExcludeKeys.has(excludeKey)) {
|
|
2341
|
+
seenExcludeKeys.add(excludeKey);
|
|
2342
|
+
excludeKeySet.push(excludeKey);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
let match = false;
|
|
2346
|
+
if ($all) {
|
|
2347
|
+
let intersection = null;
|
|
2348
|
+
let failed = false;
|
|
2349
|
+
for (const termKey of resolvedKeys) {
|
|
2350
|
+
const entry = ensureTermEntry(termKey);
|
|
2351
|
+
if (!entry.hasData) {
|
|
2352
|
+
failed = true;
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2355
|
+
const lines = getLineArray(entry);
|
|
2356
|
+
if (lines.length === 0) {
|
|
2357
|
+
failed = true;
|
|
2358
|
+
break;
|
|
2359
|
+
}
|
|
2360
|
+
const currentSet = new Set(lines);
|
|
2361
|
+
if (intersection === null) {
|
|
2362
|
+
intersection = currentSet;
|
|
2363
|
+
} else {
|
|
2364
|
+
intersection = new Set([...intersection].filter(value => currentSet.has(value)));
|
|
2365
|
+
if (intersection.size === 0) {
|
|
2366
|
+
failed = true;
|
|
2367
|
+
break;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (!failed && intersection && intersection.size > 0) {
|
|
2372
|
+
match = applyExcludes(intersection, excludeKeySet);
|
|
2373
|
+
}
|
|
2374
|
+
} else if (excludeKeySet.length === 0) {
|
|
2375
|
+
match = resolvedKeys.some(termKey => ensureTermEntry(termKey).hasData);
|
|
2376
|
+
} else {
|
|
2377
|
+
const candidates = new Set();
|
|
2378
|
+
for (const termKey of resolvedKeys) {
|
|
2379
|
+
const entry = ensureTermEntry(termKey);
|
|
2380
|
+
if (!entry.hasData) continue;
|
|
2381
|
+
const lines = getLineArray(entry);
|
|
2382
|
+
for (const line of lines) {
|
|
2383
|
+
candidates.add(line);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
if (candidates.size > 0) {
|
|
2387
|
+
match = applyExcludes(candidates, excludeKeySet);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
results[id] = Boolean(match);
|
|
2391
|
+
if (match) {
|
|
2392
|
+
successes += 1;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
return results;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2157
2398
|
// Ultra-fast load with minimal conversions
|
|
2158
2399
|
load(index) {
|
|
2159
2400
|
// CRITICAL FIX: Check if index is already loaded by looking for actual data, not just empty field structures
|
|
@@ -11656,6 +11897,51 @@ class Database extends events.EventEmitter {
|
|
|
11656
11897
|
}
|
|
11657
11898
|
}
|
|
11658
11899
|
|
|
11900
|
+
/**
|
|
11901
|
+
* Run batched index-only existence checks using a single connection per field.
|
|
11902
|
+
* Each entry must specify { field, terms, options?, id? } so the result map stays stable.
|
|
11903
|
+
*
|
|
11904
|
+
* @param {Array<Object>} criteriaArray
|
|
11905
|
+
* @param {Object} opts
|
|
11906
|
+
* @returns {Promise<Object<string, boolean>>}
|
|
11907
|
+
*/
|
|
11908
|
+
async multiExists(criteriaArray, opts = {}) {
|
|
11909
|
+
this._validateInitialization('multiExists');
|
|
11910
|
+
if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
|
|
11911
|
+
return {};
|
|
11912
|
+
}
|
|
11913
|
+
if (!this.indexManager) {
|
|
11914
|
+
return {};
|
|
11915
|
+
}
|
|
11916
|
+
const results = {};
|
|
11917
|
+
const perField = new Map();
|
|
11918
|
+
for (let i = 0; i < criteriaArray.length; i++) {
|
|
11919
|
+
const raw = criteriaArray[i] || {};
|
|
11920
|
+
const fieldName = raw.field || raw.fieldName || raw.fieldname;
|
|
11921
|
+
const terms = raw.terms ?? raw.value;
|
|
11922
|
+
const id = raw.id !== undefined && raw.id !== null ? String(raw.id) : `criteria-${i}`;
|
|
11923
|
+
const options = raw.options;
|
|
11924
|
+
results[id] = false;
|
|
11925
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
11926
|
+
continue;
|
|
11927
|
+
}
|
|
11928
|
+
const bucket = perField.get(fieldName) || [];
|
|
11929
|
+
bucket.push({
|
|
11930
|
+
id,
|
|
11931
|
+
terms,
|
|
11932
|
+
options
|
|
11933
|
+
});
|
|
11934
|
+
perField.set(fieldName, bucket);
|
|
11935
|
+
}
|
|
11936
|
+
for (const [field, entries] of perField) {
|
|
11937
|
+
const fieldResults = this.indexManager.multiExists(field, entries, opts);
|
|
11938
|
+
for (const [id, value] of Object.entries(fieldResults)) {
|
|
11939
|
+
results[id] = value;
|
|
11940
|
+
}
|
|
11941
|
+
}
|
|
11942
|
+
return results;
|
|
11943
|
+
}
|
|
11944
|
+
|
|
11659
11945
|
/**
|
|
11660
11946
|
* Check if any records exist using full query criteria
|
|
11661
11947
|
* Uses index intersection when possible for maximum performance
|
package/package.json
CHANGED
package/src/Database.mjs
CHANGED
|
@@ -3516,6 +3516,56 @@ class Database extends EventEmitter {
|
|
|
3516
3516
|
}
|
|
3517
3517
|
}
|
|
3518
3518
|
|
|
3519
|
+
/**
|
|
3520
|
+
* Run batched index-only existence checks using a single connection per field.
|
|
3521
|
+
* Each entry must specify { field, terms, options?, id? } so the result map stays stable.
|
|
3522
|
+
*
|
|
3523
|
+
* @param {Array<Object>} criteriaArray
|
|
3524
|
+
* @param {Object} opts
|
|
3525
|
+
* @returns {Promise<Object<string, boolean>>}
|
|
3526
|
+
*/
|
|
3527
|
+
async multiExists(criteriaArray, opts = {}) {
|
|
3528
|
+
this._validateInitialization('multiExists')
|
|
3529
|
+
|
|
3530
|
+
if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
|
|
3531
|
+
return {}
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
if (!this.indexManager) {
|
|
3535
|
+
return {}
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
const results = {}
|
|
3539
|
+
const perField = new Map()
|
|
3540
|
+
|
|
3541
|
+
for (let i = 0; i < criteriaArray.length; i++) {
|
|
3542
|
+
const raw = criteriaArray[i] || {}
|
|
3543
|
+
const fieldName = raw.field || raw.fieldName || raw.fieldname
|
|
3544
|
+
const terms = raw.terms ?? raw.value
|
|
3545
|
+
const id = raw.id !== undefined && raw.id !== null ? String(raw.id) : `criteria-${i}`
|
|
3546
|
+
const options = raw.options
|
|
3547
|
+
|
|
3548
|
+
results[id] = false
|
|
3549
|
+
|
|
3550
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
3551
|
+
continue
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
const bucket = perField.get(fieldName) || []
|
|
3555
|
+
bucket.push({ id, terms, options })
|
|
3556
|
+
perField.set(fieldName, bucket)
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
for (const [field, entries] of perField) {
|
|
3560
|
+
const fieldResults = this.indexManager.multiExists(field, entries, opts)
|
|
3561
|
+
for (const [id, value] of Object.entries(fieldResults)) {
|
|
3562
|
+
results[id] = value
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
return results
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3519
3569
|
/**
|
|
3520
3570
|
* Check if any records exist using full query criteria
|
|
3521
3571
|
* Uses index intersection when possible for maximum performance
|
|
@@ -1812,6 +1812,272 @@ export default class IndexManager {
|
|
|
1812
1812
|
|
|
1813
1813
|
return candidateLines.size > 0;
|
|
1814
1814
|
}
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* Evaluate many index-only existence checks while sharing the same field data.
|
|
1818
|
+
* Returns a map keyed by the criteria id so callers can reuse a single scan per field.
|
|
1819
|
+
*
|
|
1820
|
+
* @param {string} fieldName - Indexed field name
|
|
1821
|
+
* @param {Array<Object>} criteriaArray - Array of { id, terms, options }
|
|
1822
|
+
* @param {Object} opts - { limit, allowPartial } (limit only applies when allowPartial is true)
|
|
1823
|
+
* @returns {Object<string, boolean>} - Map of criteria id to boolean existence
|
|
1824
|
+
*/
|
|
1825
|
+
multiExists(fieldName, criteriaArray, opts = {}) {
|
|
1826
|
+
const results = {}
|
|
1827
|
+
if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
|
|
1828
|
+
return results
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const prepared = []
|
|
1832
|
+
for (let i = 0; i < criteriaArray.length; i++) {
|
|
1833
|
+
const entry = criteriaArray[i] || {}
|
|
1834
|
+
const key = entry.id !== undefined && entry.id !== null ? String(entry.id) : `criteria-${i}`
|
|
1835
|
+
results[key] = false
|
|
1836
|
+
prepared.push({
|
|
1837
|
+
id: key,
|
|
1838
|
+
terms: entry.terms,
|
|
1839
|
+
options: entry.options || {}
|
|
1840
|
+
})
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
1844
|
+
return results
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
const fieldIndex = this.index?.data?.[fieldName]
|
|
1848
|
+
if (!fieldIndex || typeof fieldIndex !== 'object') {
|
|
1849
|
+
return results
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
const termManager = this.database?.termManager
|
|
1853
|
+
const isTermMappingField = Boolean(termManager && termManager.termMappingFields && termManager.termMappingFields.includes(fieldName))
|
|
1854
|
+
|
|
1855
|
+
const normalizedLimit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : Infinity
|
|
1856
|
+
const allowPartial = Boolean(opts.allowPartial)
|
|
1857
|
+
const positiveTarget = allowPartial ? Math.max(1, normalizedLimit) : Infinity
|
|
1858
|
+
|
|
1859
|
+
const termCache = new Map()
|
|
1860
|
+
let caseInsensitiveLookup = null
|
|
1861
|
+
let termMappingLookup = null
|
|
1862
|
+
|
|
1863
|
+
const normalizeValues = (value) => {
|
|
1864
|
+
if (value === null || value === undefined) {
|
|
1865
|
+
return []
|
|
1866
|
+
}
|
|
1867
|
+
if (Array.isArray(value)) {
|
|
1868
|
+
return value.slice()
|
|
1869
|
+
}
|
|
1870
|
+
return [value]
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const buildCaseInsensitiveLookup = () => {
|
|
1874
|
+
const map = new Map()
|
|
1875
|
+
for (const key in fieldIndex) {
|
|
1876
|
+
const lowerKey = key.toLowerCase()
|
|
1877
|
+
if (!map.has(lowerKey)) {
|
|
1878
|
+
map.set(lowerKey, key)
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return map
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
const buildTermMappingLookup = () => {
|
|
1885
|
+
const map = new Map()
|
|
1886
|
+
const termToId = termManager?.termToId
|
|
1887
|
+
if (termToId) {
|
|
1888
|
+
for (const [termStr, id] of termToId.entries()) {
|
|
1889
|
+
const lower = termStr.toLowerCase()
|
|
1890
|
+
if (!map.has(lower)) {
|
|
1891
|
+
map.set(lower, String(id))
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return map
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const resolveTermKey = (value, caseInsensitive) => {
|
|
1899
|
+
if (value === null || value === undefined) {
|
|
1900
|
+
return null
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
if (isTermMappingField) {
|
|
1904
|
+
if (typeof value === 'string') {
|
|
1905
|
+
if (caseInsensitive) {
|
|
1906
|
+
if (!termMappingLookup) {
|
|
1907
|
+
termMappingLookup = buildTermMappingLookup()
|
|
1908
|
+
}
|
|
1909
|
+
return termMappingLookup.get(value.toLowerCase()) ?? null
|
|
1910
|
+
}
|
|
1911
|
+
const termId = termManager?.getTermIdWithoutIncrement(String(value))
|
|
1912
|
+
if (termId === undefined || termId === null) {
|
|
1913
|
+
return null
|
|
1914
|
+
}
|
|
1915
|
+
return String(termId)
|
|
1916
|
+
}
|
|
1917
|
+
return String(value)
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (caseInsensitive && typeof value === 'string') {
|
|
1921
|
+
if (!caseInsensitiveLookup) {
|
|
1922
|
+
caseInsensitiveLookup = buildCaseInsensitiveLookup()
|
|
1923
|
+
}
|
|
1924
|
+
return caseInsensitiveLookup.get(value.toLowerCase()) ?? null
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
return String(value)
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const ensureTermEntry = (termKey) => {
|
|
1931
|
+
if (!termCache.has(termKey)) {
|
|
1932
|
+
const data = fieldIndex[termKey]
|
|
1933
|
+
const entry = {
|
|
1934
|
+
data,
|
|
1935
|
+
hasData: Boolean(data && ((data.set && data.set.size > 0) || (data.ranges && data.ranges.length > 0))),
|
|
1936
|
+
lineArray: null
|
|
1937
|
+
}
|
|
1938
|
+
termCache.set(termKey, entry)
|
|
1939
|
+
}
|
|
1940
|
+
return termCache.get(termKey)
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
const getLineArray = (entry) => {
|
|
1944
|
+
if (!entry.hasData || !entry.data) return []
|
|
1945
|
+
if (!entry.lineArray) {
|
|
1946
|
+
entry.lineArray = this._getAllLineNumbers(entry.data)
|
|
1947
|
+
}
|
|
1948
|
+
return entry.lineArray
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
const applyExcludes = (lineSet, excludeKeys) => {
|
|
1952
|
+
if (excludeKeys.length === 0) {
|
|
1953
|
+
return lineSet.size > 0
|
|
1954
|
+
}
|
|
1955
|
+
for (const excludeKey of excludeKeys) {
|
|
1956
|
+
const excludeEntry = ensureTermEntry(excludeKey)
|
|
1957
|
+
if (!excludeEntry.hasData) continue
|
|
1958
|
+
const excludeLines = getLineArray(excludeEntry)
|
|
1959
|
+
for (const line of excludeLines) {
|
|
1960
|
+
lineSet.delete(line)
|
|
1961
|
+
}
|
|
1962
|
+
if (lineSet.size === 0) {
|
|
1963
|
+
return false
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return lineSet.size > 0
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
let successes = 0
|
|
1970
|
+
|
|
1971
|
+
for (const criterion of prepared) {
|
|
1972
|
+
if (successes >= positiveTarget) {
|
|
1973
|
+
break
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
const { id, terms, options } = criterion
|
|
1977
|
+
const { $all = false, caseInsensitive = false, excludes = [] } = options
|
|
1978
|
+
|
|
1979
|
+
const normalizedTerms = normalizeValues(terms)
|
|
1980
|
+
if (normalizedTerms.length === 0) {
|
|
1981
|
+
continue
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
const normalizedExcludes = normalizeValues(excludes)
|
|
1985
|
+
|
|
1986
|
+
const resolvedKeys = []
|
|
1987
|
+
const seenKeys = new Set()
|
|
1988
|
+
let missingRequiredTerm = false
|
|
1989
|
+
|
|
1990
|
+
for (const term of normalizedTerms) {
|
|
1991
|
+
const termKey = resolveTermKey(term, caseInsensitive)
|
|
1992
|
+
if (termKey === null) {
|
|
1993
|
+
if ($all) {
|
|
1994
|
+
missingRequiredTerm = true
|
|
1995
|
+
break
|
|
1996
|
+
}
|
|
1997
|
+
continue
|
|
1998
|
+
}
|
|
1999
|
+
if (!seenKeys.has(termKey)) {
|
|
2000
|
+
seenKeys.add(termKey)
|
|
2001
|
+
resolvedKeys.push(termKey)
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
if ($all && missingRequiredTerm) {
|
|
2006
|
+
continue
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (resolvedKeys.length === 0) {
|
|
2010
|
+
continue
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const excludeKeySet = []
|
|
2014
|
+
const seenExcludeKeys = new Set()
|
|
2015
|
+
for (const excludeTerm of normalizedExcludes) {
|
|
2016
|
+
const excludeKey = resolveTermKey(excludeTerm, caseInsensitive)
|
|
2017
|
+
if (excludeKey && !seenExcludeKeys.has(excludeKey)) {
|
|
2018
|
+
seenExcludeKeys.add(excludeKey)
|
|
2019
|
+
excludeKeySet.push(excludeKey)
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
let match = false
|
|
2024
|
+
|
|
2025
|
+
if ($all) {
|
|
2026
|
+
let intersection = null
|
|
2027
|
+
let failed = false
|
|
2028
|
+
|
|
2029
|
+
for (const termKey of resolvedKeys) {
|
|
2030
|
+
const entry = ensureTermEntry(termKey)
|
|
2031
|
+
if (!entry.hasData) {
|
|
2032
|
+
failed = true
|
|
2033
|
+
break
|
|
2034
|
+
}
|
|
2035
|
+
const lines = getLineArray(entry)
|
|
2036
|
+
if (lines.length === 0) {
|
|
2037
|
+
failed = true
|
|
2038
|
+
break
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const currentSet = new Set(lines)
|
|
2042
|
+
if (intersection === null) {
|
|
2043
|
+
intersection = currentSet
|
|
2044
|
+
} else {
|
|
2045
|
+
intersection = new Set([...intersection].filter(value => currentSet.has(value)))
|
|
2046
|
+
if (intersection.size === 0) {
|
|
2047
|
+
failed = true
|
|
2048
|
+
break
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
if (!failed && intersection && intersection.size > 0) {
|
|
2054
|
+
match = applyExcludes(intersection, excludeKeySet)
|
|
2055
|
+
}
|
|
2056
|
+
} else if (excludeKeySet.length === 0) {
|
|
2057
|
+
match = resolvedKeys.some(termKey => ensureTermEntry(termKey).hasData)
|
|
2058
|
+
} else {
|
|
2059
|
+
const candidates = new Set()
|
|
2060
|
+
for (const termKey of resolvedKeys) {
|
|
2061
|
+
const entry = ensureTermEntry(termKey)
|
|
2062
|
+
if (!entry.hasData) continue
|
|
2063
|
+
const lines = getLineArray(entry)
|
|
2064
|
+
for (const line of lines) {
|
|
2065
|
+
candidates.add(line)
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
if (candidates.size > 0) {
|
|
2069
|
+
match = applyExcludes(candidates, excludeKeySet)
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
results[id] = Boolean(match)
|
|
2074
|
+
if (match) {
|
|
2075
|
+
successes += 1
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
return results
|
|
2080
|
+
}
|
|
1815
2081
|
|
|
1816
2082
|
// Ultra-fast load with minimal conversions
|
|
1817
2083
|
load(index) {
|