corebasic 1.0.212 → 1.0.214

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.
@@ -0,0 +1,227 @@
1
+ function flatten_id_values(target_key, values) {
2
+ let flattened = [];
3
+ flatten_value(target_key, values, flattened, false);
4
+ return flattened;
5
+ }
6
+ function flatten_value(target_key, value, flattened, ancestor_has_id) {
7
+ const is_array = Array.isArray(value);
8
+ const is_object = value !== null && !is_array && typeof value === "object";
9
+ const is_regular = typeof value === "boolean" || typeof value === "number" || typeof value === "string";
10
+ const map = value;
11
+ const arr = value;
12
+ if (is_regular) {
13
+ flattened.push(value);
14
+ }
15
+ else if (is_array) {
16
+ for (let v of arr) {
17
+ flatten_value(target_key, v, flattened, ancestor_has_id);
18
+ }
19
+ }
20
+ else if (is_object) {
21
+ // {$nin}, {$not: {$in: []}} & $nor means Exclude. Indexes ensures everything else is excluded. $nin & $in with same id can be excluded but smells bad query thus punishable and so must incur the cost of fetching.
22
+ // Ensure _id is processed before entering the loop, otherwise a query { _id: 1, $or: [{_id: 2}, {}] } will skip _id: 1
23
+ if (!ancestor_has_id && Object.hasOwn(map, target_key)) {
24
+ flatten_value(target_key, map[target_key], flattened, true); // ancestor_has_id: true safely skips {_id: {_id: 1}} situation
25
+ }
26
+ for (let key in map) {
27
+ let val = map[key];
28
+ if (key === "$eq") {
29
+ flatten_value(target_key, val, flattened, ancestor_has_id);
30
+ }
31
+ else if (key == "$gt" || key === "$gte" || key === "$lt" || key === "$lte") {
32
+ flattened.push({ [key]: val });
33
+ }
34
+ else if (key == "$in") {
35
+ // Only select $in, $eq, or (direct value which is done above in the match value section)
36
+ let arr = val;
37
+ if (Array.isArray(arr)) {
38
+ for (let v of arr) {
39
+ flatten_value(target_key, v, flattened, ancestor_has_id);
40
+ }
41
+ }
42
+ }
43
+ else if (key === "$and" && Array.isArray(val)) {
44
+ flatten_value(target_key, val, flattened, ancestor_has_id); // recursively process all values
45
+ }
46
+ else if (key === "$or" && Array.isArray(val)) {
47
+ // Store prior count.
48
+ let checkpoint = flattened.length;
49
+ // Every branch must produce 1 or more candidate _id.
50
+ // Recursively, inner $or rollback/capture correctly affects outer $or rollback/capture. So works correctly.
51
+ let arr = val;
52
+ if (Array.isArray(arr)) {
53
+ for (let branch of arr) {
54
+ let before = flattened.length;
55
+ flatten_value(target_key, branch, flattened, ancestor_has_id);
56
+ if (flattened.length === before) {
57
+ // branch produced no candidate _ids. Rollback to checkpoint.
58
+ flattened.length = checkpoint;
59
+ return;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ export function entries(target_key, query, binary_slice) {
68
+ let result = flatten_id_values(target_key, query); // extract all valid _id
69
+ // let cows = result.map(v => extract_direct_id(v, binary_slice))
70
+ // // cows.sort_by(|a, b| a.as_ref().cmp(b.as_ref())); // sort for improved forward processing because kv engine keys are already sorted.
71
+ // cows.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref())); // sort for improved forward processing because kv engine keys are already sorted.
72
+ //
73
+ // // unique_bytes(cows)
74
+ // unique_sorted_bytes(cows) // Leverage already sorted so only need comparisons with the previous item.
75
+ const objects = result.filter(item => typeof item === "object");
76
+ const values = result.filter(item => typeof item !== "object");
77
+ let unique = [...new Set(values)];
78
+ return unique.concat(objects);
79
+ }
80
+ // NOTE:
81
+ // reduceRanges() intentionally computes a conservative (wider) approximation.
82
+ // It is used only for suffix policy planning, where false positives are acceptable
83
+ // (extra suffix scans) but false negatives are not.
84
+ export function reduceRanges(values) {
85
+ let gt = null;
86
+ let gte = null;
87
+ let lt = null;
88
+ let lte = null;
89
+ const other = [];
90
+ for (const value of values) {
91
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
92
+ other.push(value);
93
+ continue;
94
+ }
95
+ if ("$gt" in value) {
96
+ if (gt === null || value.$gt < gt)
97
+ gt = value.$gt;
98
+ }
99
+ else if ("$gte" in value) {
100
+ if (gte === null || value.$gte < gte)
101
+ gte = value.$gte;
102
+ }
103
+ else if ("$lt" in value) {
104
+ if (lt === null || value.$lt > lt)
105
+ lt = value.$lt;
106
+ }
107
+ else if ("$lte" in value) {
108
+ if (lte === null || value.$lte > lte)
109
+ lte = value.$lte;
110
+ }
111
+ else {
112
+ other.push(value);
113
+ }
114
+ }
115
+ // Resolve lower bound.
116
+ if (gt !== null || gte !== null) {
117
+ if (gt === null) {
118
+ other.push({ $gte: gte });
119
+ }
120
+ else if (gte === null) {
121
+ other.push({ $gt: gt });
122
+ }
123
+ else if (gt < gte) {
124
+ other.push({ $gt: gt });
125
+ }
126
+ else if (gte < gt) {
127
+ other.push({ $gte: gte });
128
+ }
129
+ else {
130
+ // Same value: inclusive is wider.
131
+ other.push({ $gte: gt });
132
+ }
133
+ }
134
+ // Resolve upper bound.
135
+ if (lt !== null || lte !== null) {
136
+ if (lt === null) {
137
+ other.push({ $lte: lte });
138
+ }
139
+ else if (lte === null) {
140
+ other.push({ $lt: lt });
141
+ }
142
+ else if (lt > lte) {
143
+ other.push({ $lt: lt });
144
+ }
145
+ else if (lte > lt) {
146
+ other.push({ $lte: lte });
147
+ }
148
+ else {
149
+ // Same value: inclusive is wider.
150
+ other.push({ $lte: lt });
151
+ }
152
+ }
153
+ return other;
154
+ }
155
+ // reduceRanges() resolves conflicting bounds before extractBounds() runs.
156
+ // extractBounds() receives at most one lower bound and atmost one upper bound. reduceRanges() ensures this
157
+ // The for loop in extractBounds() may look slightly misleading because it looks like it is designed to resolve conflicts: but reduceRanges() has already done the conflict resolution.
158
+ // NOTE CRITICAL: extractBounds() MUST ONLY BE RUN ON THE OUTPUT OF reduceRanges()
159
+ export function extractBounds(values) {
160
+ let lower = null;
161
+ let upper = null;
162
+ let lowerOp = null;
163
+ let upperOp = null;
164
+ const remaining = [];
165
+ for (const value of values) {
166
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
167
+ remaining.push(value);
168
+ continue;
169
+ }
170
+ if ("$gt" in value) {
171
+ lower = value.$gt;
172
+ lowerOp = "$gt";
173
+ }
174
+ else if ("$gte" in value) {
175
+ lower = value.$gte;
176
+ lowerOp = "$gte";
177
+ }
178
+ else if ("$lt" in value) {
179
+ upper = value.$lt;
180
+ upperOp = "$lt";
181
+ }
182
+ else if ("$lte" in value) {
183
+ upper = value.$lte;
184
+ upperOp = "$lte";
185
+ }
186
+ else {
187
+ remaining.push(value);
188
+ }
189
+ }
190
+ return {
191
+ values: remaining,
192
+ from: lower,
193
+ to: upper,
194
+ fromOp: lowerOp,
195
+ toOp: upperOp,
196
+ };
197
+ }
198
+ // Converts exclusive bounds ($gt/$lt) into inclusive bounds
199
+ export function getNormalizedBounds(bounds_t, type) {
200
+ let bounds = { ...bounds_t };
201
+ if (type === "number") {
202
+ if (bounds.fromOp === "$gt")
203
+ bounds.from++;
204
+ if (bounds.toOp === "$lt")
205
+ bounds.to--;
206
+ }
207
+ else if (type === "date") {
208
+ if (bounds.fromOp === "$gt")
209
+ bounds.from = new Date(bounds.from.getTime() + 1);
210
+ if (bounds.toOp === "$lt")
211
+ bounds.to = new Date(bounds.to.getTime() - 1);
212
+ }
213
+ return bounds;
214
+ }
215
+ // console.log('=============')
216
+ // let a = entries("date", {
217
+ // $or: [
218
+ // {date: {$gt: 10, $lt: 20}},
219
+ // {date: {$gt: 3, $lt: 90}},
220
+ // {date: {$gt: 15, $lt: 25}},
221
+ // {date: 5},
222
+ // // {},
223
+ // {date: 5},
224
+ // ]
225
+ // })
226
+ // a = reduce_ranges(a)
227
+ // console.log(a)
@@ -0,0 +1,113 @@
1
+ import { entries, reduceRanges, extractBounds } from './index.js';
2
+ import { suffix } from './suffix.js';
3
+ export function overlaps(a, b) {
4
+ const aFrom = a.from ?? -Infinity;
5
+ const aTo = a.to ?? Infinity;
6
+ const bFrom = b.from ?? -Infinity;
7
+ const bTo = b.to ?? Infinity;
8
+ const left = Math.max(aFrom, bFrom);
9
+ const right = Math.min(aTo, bTo);
10
+ if (left < right)
11
+ return true;
12
+ if (left > right)
13
+ return false;
14
+ // left === right, touching at one point.
15
+ const leftClosed = (left === aFrom ? a.fromOp === "$gte" : a.toOp === "$lte") &&
16
+ (left === bFrom ? b.fromOp === "$gte" : b.toOp === "$lte");
17
+ return leftClosed;
18
+ // return Math.max(aFrom, bFrom) <= Math.min(aTo, bTo);
19
+ }
20
+ export function contains(bounds, value) {
21
+ const from = bounds.from ?? -Infinity;
22
+ const to = bounds.to ?? Infinity;
23
+ if (value < from)
24
+ return false;
25
+ if (value > to)
26
+ return false;
27
+ if (value === from && bounds.fromOp === "$gt")
28
+ return false;
29
+ if (value === to && bounds.toOp === "$lt")
30
+ return false;
31
+ return true;
32
+ // return value >= from && value <= to;
33
+ }
34
+ export function intersects(w_values, w_bounds, q_values, q_bounds) {
35
+ // value ↔ value
36
+ for (const value of w_values) {
37
+ if (q_values.includes(value))
38
+ return true;
39
+ }
40
+ // value(when) ↔ range(query)
41
+ if (q_bounds) {
42
+ for (const value of w_values) {
43
+ if (contains(q_bounds, value))
44
+ return true;
45
+ }
46
+ }
47
+ // value(query) ↔ range(when)
48
+ if (w_bounds) {
49
+ for (const value of q_values) {
50
+ if (contains(w_bounds, value))
51
+ return true;
52
+ }
53
+ }
54
+ // range ↔ range
55
+ if (w_bounds && q_bounds && overlaps(w_bounds, q_bounds))
56
+ return true;
57
+ return false;
58
+ }
59
+ // NOTE: Matching is based on intersection, not equality. A policy matches if, for every key in `when`, there exists at least one value that satisfies both the `when` constraint and the query constraint.
60
+ export function matchPolicy(query, when, g_obj) {
61
+ let obj = g_obj ?? {};
62
+ for (let key in when) {
63
+ if (key === "$and" || key === "$or") {
64
+ for (const element of when[key]) {
65
+ matchPolicy(query, element, obj);
66
+ }
67
+ }
68
+ else if (key === "$ne" || key === "$nin" || key === "$not" || (when[key] && typeof when[key] === "object" && ("$ne" in when[key] || "$nin" in when[key] || "$not" in when[key]))) {
69
+ throw new Error(`Error: Policy 'when' does not support negative constraints $ne, $nin and $not in Dip`);
70
+ // NOTE:
71
+ // Negative constraints are invalid for policy selection. Cost is additional suffix scans. But we disallow it so that it is immediately communicated
72
+ // There's no suffix you can eliminate from just knowing a negative. The safest thing is exactly to not let it influence policy selection.
73
+ }
74
+ else {
75
+ obj[key] = obj[key] ?? [];
76
+ obj[key].push(...entries(key, when));
77
+ }
78
+ }
79
+ // Only compare once, on the outermost call.
80
+ if (g_obj)
81
+ return;
82
+ for (let key in obj) {
83
+ obj[key] = [...new Set(obj[key])];
84
+ let w_values = obj[key];
85
+ let w_ranges = w_values.filter(item => typeof item === "object");
86
+ w_values = w_values.filter(item => typeof item !== "object");
87
+ const w_bounds = w_ranges.length ? extractBounds(reduceRanges(w_ranges)) : null;
88
+ let q_values = entries(key, query);
89
+ let q_ranges = q_values.filter(item => typeof item === "object");
90
+ q_values = q_values.filter(item => typeof item !== "object");
91
+ const q_bounds = q_ranges.length ? extractBounds(reduceRanges(q_ranges)) : null;
92
+ if (!intersects(w_values, w_bounds, q_values, q_bounds))
93
+ return false;
94
+ }
95
+ return true;
96
+ }
97
+ export async function applySuffixPolicy(COLLECTIONS_JSON, collections, query, insertMode, arg) {
98
+ collections = Array.isArray(collections) ? collections : [collections];
99
+ let suffixes = [];
100
+ for (let collection of collections) {
101
+ const collection_policy = COLLECTIONS_JSON[collection]?.policy ?? [];
102
+ const suffixPolicies = typeof collection_policy === "object" && !Array.isArray(collection_policy) ? [collection_policy] : COLLECTIONS_JSON[collection]?.policy ?? [];
103
+ for (const policy of suffixPolicies) {
104
+ if (matchPolicy(query, policy.when ?? {})) {
105
+ // generate suffixes
106
+ const array = await suffix(query, policy.suffix ?? [], insertMode, arg);
107
+ suffixes.push(...array);
108
+ }
109
+ }
110
+ suffixes = [...new Set(suffixes)];
111
+ }
112
+ return suffixes;
113
+ }
@@ -0,0 +1,279 @@
1
+ // Lightweight Query Matcher (JavaScript)
2
+ // ========= Utility Functions =========
3
+ function toF64(value) {
4
+ if (value === null || value === undefined)
5
+ return 0;
6
+ if (typeof value === 'number')
7
+ return value;
8
+ if (typeof value === 'string') {
9
+ const parsed = parseFloat(value);
10
+ return isNaN(parsed) ? 0 : parsed;
11
+ }
12
+ if (typeof value === 'boolean')
13
+ return value ? 1 : 0;
14
+ return 0;
15
+ }
16
+ function getValueAt(obj, key) {
17
+ if (obj === null || obj === undefined)
18
+ return null;
19
+ if (typeof obj !== 'object')
20
+ return null;
21
+ return obj[key] !== undefined ? obj[key] : null;
22
+ }
23
+ // ========= Comparison Primitives =========
24
+ function eqQuery(v1, v2) {
25
+ // Deep equality for objects/arrays, strict for primitives
26
+ if (v1 === v2)
27
+ return true;
28
+ if (v1 === null || v2 === null)
29
+ return v1 === v2;
30
+ if (typeof v1 !== typeof v2) {
31
+ // Try numeric comparison
32
+ if (typeof v1 === 'number' || typeof v2 === 'number') {
33
+ return toF64(v1) === toF64(v2);
34
+ }
35
+ return false;
36
+ }
37
+ if (typeof v1 === 'object') {
38
+ if (Array.isArray(v1) !== Array.isArray(v2))
39
+ return false;
40
+ if (Array.isArray(v1)) {
41
+ if (v1.length !== v2.length)
42
+ return false;
43
+ for (let i = 0; i < v1.length; i++) {
44
+ if (!eqQuery(v1[i], v2[i]))
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ const keys1 = Object.keys(v1);
50
+ const keys2 = Object.keys(v2);
51
+ if (keys1.length !== keys2.length)
52
+ return false;
53
+ for (const key of keys1) {
54
+ if (!keys2.includes(key))
55
+ return false;
56
+ if (!eqQuery(v1[key], v2[key]))
57
+ return false;
58
+ }
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ function gtQuery(v1, v2) {
64
+ return toF64(v1) > toF64(v2);
65
+ }
66
+ function gteQuery(v1, v2) {
67
+ return toF64(v1) >= toF64(v2);
68
+ }
69
+ function ltQuery(v1, v2) {
70
+ return toF64(v1) < toF64(v2);
71
+ }
72
+ function lteQuery(v1, v2) {
73
+ return toF64(v1) <= toF64(v2);
74
+ }
75
+ function existsQuery(v1, v2) {
76
+ const exists = v1 !== null && v1 !== undefined;
77
+ const condition = typeof v2 === 'boolean' ? v2 : true;
78
+ return condition === exists;
79
+ }
80
+ function beginQuery(input, arg) {
81
+ if (typeof input === 'string' && typeof arg === 'string') {
82
+ return input.startsWith(arg);
83
+ }
84
+ return false;
85
+ }
86
+ function endQuery(input, arg) {
87
+ if (typeof input === 'string' && typeof arg === 'string') {
88
+ return input.endsWith(arg);
89
+ }
90
+ return false;
91
+ }
92
+ function containsQuery(input, arg, caseSensitive = true) {
93
+ if (typeof input !== 'string')
94
+ return false;
95
+ if (Array.isArray(arg)) {
96
+ if (arg.length === 0)
97
+ return false;
98
+ let allMatch = true;
99
+ for (const v of arg) {
100
+ let needle = typeof v === 'string' ? v : String(v);
101
+ let haystack = input;
102
+ if (!caseSensitive) {
103
+ needle = needle.toLowerCase();
104
+ haystack = haystack.toLowerCase();
105
+ }
106
+ if (!haystack.includes(needle)) {
107
+ allMatch = false;
108
+ break;
109
+ }
110
+ }
111
+ return allMatch;
112
+ }
113
+ if (typeof arg === 'string') {
114
+ let needle = arg;
115
+ let haystack = input;
116
+ if (!caseSensitive) {
117
+ needle = needle.toLowerCase();
118
+ haystack = haystack.toLowerCase();
119
+ }
120
+ return haystack.includes(needle);
121
+ }
122
+ return false;
123
+ }
124
+ // ========= Query Commands =========
125
+ function inQuery(input, args) {
126
+ if (Array.isArray(args)) {
127
+ for (const item of args) {
128
+ if (eqQuery(input, item))
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+ if (args !== null && typeof args === 'object')
134
+ return false;
135
+ return eqQuery(input, args);
136
+ }
137
+ function elemMatchQuery(input, query) {
138
+ if (!Array.isArray(input))
139
+ return false;
140
+ const term = query.$ || '$';
141
+ const captureLimit = query.$captureLimit || 0;
142
+ let countMatched = 0;
143
+ for (let index = 0; index < input.length; index++) {
144
+ if (andQuery(input[index], query)) {
145
+ if (captureLimit > 0 && countMatched >= captureLimit)
146
+ break;
147
+ countMatched++;
148
+ }
149
+ }
150
+ return countMatched > 0;
151
+ }
152
+ function lengthQuery(input, arg) {
153
+ let len;
154
+ if (Array.isArray(input)) {
155
+ len = input.length;
156
+ }
157
+ else if (typeof input === 'string') {
158
+ len = input.length;
159
+ }
160
+ else {
161
+ return false;
162
+ }
163
+ if (arg !== null && typeof arg === 'object' && !Array.isArray(arg)) {
164
+ return andQuery(len, arg);
165
+ }
166
+ if (typeof arg === 'number') {
167
+ return len === arg;
168
+ }
169
+ return false;
170
+ }
171
+ function icaseQuery(input, arg) {
172
+ if (typeof input !== 'string')
173
+ return false;
174
+ const lowerInput = input.toLowerCase();
175
+ if (arg !== null && typeof arg === 'object' && !Array.isArray(arg)) {
176
+ return andQuery(lowerInput, arg);
177
+ }
178
+ if (typeof arg === 'string') {
179
+ return eqQuery(lowerInput, arg.toLowerCase());
180
+ }
181
+ return false;
182
+ }
183
+ function selectCommand(key, input, value) {
184
+ switch (key) {
185
+ // Comparison
186
+ case '$':
187
+ return true;
188
+ case '$eq':
189
+ return eqQuery(input, value);
190
+ case '$ne':
191
+ return !eqQuery(input, value);
192
+ case '$gt':
193
+ return gtQuery(input, value);
194
+ case '$gte':
195
+ return gteQuery(input, value);
196
+ case '$lt':
197
+ return ltQuery(input, value);
198
+ case '$lte':
199
+ return lteQuery(input, value);
200
+ case '$in':
201
+ return inQuery(input, value);
202
+ case '$nin':
203
+ return !inQuery(input, value);
204
+ // Logical
205
+ case '$not':
206
+ return !andQuery(input, value);
207
+ case '$exists':
208
+ return existsQuery(input, value);
209
+ case '$or':
210
+ return orQuery(input, value);
211
+ case '$nor':
212
+ return !orQuery(input, value);
213
+ case '$and':
214
+ if (!Array.isArray(value))
215
+ return false;
216
+ for (const item of value) {
217
+ if (!andQuery(input, item))
218
+ return false;
219
+ }
220
+ return true;
221
+ // Array
222
+ case '$elemMatch':
223
+ return elemMatchQuery(input, value);
224
+ case '$length':
225
+ return lengthQuery(input, value);
226
+ // String matching
227
+ case '$icase':
228
+ return icaseQuery(input, value);
229
+ case '$begin':
230
+ return beginQuery(input, value);
231
+ case '$end':
232
+ return endQuery(input, value);
233
+ case '$contains':
234
+ return containsQuery(input, value);
235
+ default:
236
+ if (key.startsWith('$'))
237
+ return true; // Unknown $keys pass through
238
+ return false;
239
+ }
240
+ }
241
+ function orQuery(input, value) {
242
+ if (!Array.isArray(value))
243
+ return false;
244
+ for (const item of value) {
245
+ if (andQuery(input, item))
246
+ return true;
247
+ }
248
+ return false;
249
+ }
250
+ function andQuery(input, qvalue) {
251
+ if (qvalue === null || qvalue === undefined) {
252
+ return eqQuery(input, qvalue);
253
+ }
254
+ if (typeof qvalue === 'object' && !Array.isArray(qvalue)) {
255
+ for (const [key, value] of Object.entries(qvalue)) {
256
+ if (key.startsWith('$')) {
257
+ if (!selectCommand(key, input, value)) {
258
+ return false;
259
+ }
260
+ }
261
+ else {
262
+ const v = (typeof value === 'object' && value !== null && !Array.isArray(value))
263
+ ? andQuery(getValueAt(input, key), value)
264
+ : eqQuery(getValueAt(input, key), value);
265
+ if (!v)
266
+ return false;
267
+ }
268
+ }
269
+ return true;
270
+ }
271
+ if (Array.isArray(qvalue)) {
272
+ return false;
273
+ }
274
+ return eqQuery(input, qvalue);
275
+ }
276
+ // ========= Main Entry Point =========
277
+ export function search(data, query) {
278
+ return andQuery(data, query);
279
+ }