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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jexidb",
3
- "version": "2.1.8",
3
+ "version": "2.1.9",
4
4
  "type": "module",
5
5
  "description": "JexiDB is a pure JS NPM library for managing data on disk efficiently, without the need for a server.",
6
6
  "main": "./dist/Database.cjs",
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) {