Skip to content

Commit ce57a44

Browse files
significantly re-optimized cursor predicate layer
1 parent 3ce7e1b commit ce57a44

File tree

3 files changed

+313
-218
lines changed

3 files changed

+313
-218
lines changed

Magic.IndexedDb/wwwroot/utilities/cursorEngine.js

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,33 @@ async function processCursorRecords(db, table, predicateTree, yieldedPrimaryKeys
5353
const now = Date.now();
5454
let shouldLogWarning = !lastCursorWarningTime || now - lastCursorWarningTime > 10 * 60 * 1000;
5555

56-
// Dynamically extract all required properties from the predicate tree
5756
const requiredPropertiesFiltered = new Set();
58-
collectPropertiesFromTree(predicateTree, requiredPropertiesFiltered);
57+
58+
// Only collect properties if we actually have a filter tree with children
59+
const hasConditions =
60+
predicateTree &&
61+
(
62+
predicateTree.nodeType === "condition" ||
63+
(predicateTree.children && predicateTree.children.length > 0)
64+
);
65+
66+
if (hasConditions) {
67+
collectPropertiesFromTree(predicateTree, requiredPropertiesFiltered);
68+
}
5969

6070
await db.transaction('r', table, async () => {
6171
await table.orderBy(compoundKeys[0]).each((record) => {
62-
// Skip if required property is missing
63-
for (const prop of requiredPropertiesFiltered) {
64-
if (record[prop] === undefined) {
65-
if (shouldLogWarning) {
66-
console.warn(`[IndexedDB Cursor Warning] Skipping record due to missing property: ${prop}`);
67-
lastCursorWarningTime = now;
68-
shouldLogWarning = false;
72+
// Still apply property checks *only* if we have any to check
73+
if (requiredPropertiesFiltered.size > 0) {
74+
for (const prop of requiredPropertiesFiltered) {
75+
if (record[prop] === undefined) {
76+
if (shouldLogWarning) {
77+
console.warn(`[IndexedDB Cursor Warning] Skipping record due to missing property: ${prop}`);
78+
lastCursorWarningTime = now;
79+
shouldLogWarning = false;
80+
}
81+
return;
6982
}
70-
return;
7183
}
7284
}
7385

@@ -76,48 +88,77 @@ async function processCursorRecords(db, table, predicateTree, yieldedPrimaryKeys
7688
return;
7789
}
7890

79-
// Evaluate the structured predicate tree
80-
if (!evaluatePredicateTree(predicateTree, record)) return;
91+
// Only evaluate if we actually have predicate logic
92+
if (hasConditions && !evaluatePredicateTree(predicateTree, record))
93+
return;
8194

8295
recordHandler(record, recordKey);
8396
});
8497
});
8598
}
8699

100+
87101
function collectPropertiesFromTree(node, propertySet) {
88102
if (node.nodeType === "condition") {
89103
propertySet.add(node.condition.property);
90104
return;
91105
}
92-
for (const child of node.children) {
106+
for (const child of node.children ?? []) {
93107
collectPropertiesFromTree(child, propertySet);
94108
}
95109
}
96110

97111
function evaluatePredicateTree(node, record) {
98112
if (node.nodeType === "condition") {
99-
const condition = optimizeSingleCondition(node.condition);
100-
return applyCondition(record, condition);
113+
if (!node.optimized) {
114+
node.optimized = optimizeSingleCondition(node.condition);
115+
}
116+
return applyCondition(record, node.optimized);
101117
}
102118

103-
const results = node.children.map(child => evaluatePredicateTree(child, record));
119+
const results = (node.children ?? []).map(child => evaluatePredicateTree(child, record));
104120
return node.operator === "And"
105121
? results.every(r => r)
106122
: results.some(r => r);
107123
}
108124

109125
function optimizeSingleCondition(condition) {
110-
let optimized = { ...condition };
126+
if (condition.value === -Infinity || condition.value === Infinity) {
127+
return condition; // Already a no-op filter, skip transformation
128+
}
111129

130+
const optimized = { ...condition };
131+
132+
// Lowercase normalization for string values if not case-sensitive
112133
if (!condition.caseSensitive && typeof condition.value === "string") {
113134
optimized.value = condition.value.toLowerCase();
114135
}
115136

137+
// Edge case: GETDAY sanity check
138+
if (
139+
condition.operation === QUERY_OPERATIONS.GETDAY &&
140+
typeof condition.value !== "number"
141+
) {
142+
console.warn(`[IndexedDB Warning] GETDAY expects a numeric day (1–31). Got:`, condition.value);
143+
}
144+
145+
// Generic numeric validation for date-based derived operations
146+
if (
147+
[QUERY_OPERATIONS.GET_DAY_OF_WEEK, QUERY_OPERATIONS.GET_DAY_OF_YEAR, QUERY_OPERATIONS.GETDAY]
148+
.includes(condition.operation)
149+
) {
150+
if (typeof condition.value !== "number") {
151+
console.warn(`[IndexedDB Warning] ${condition.operation} expects a numeric value. Got:`, condition.value);
152+
}
153+
}
154+
116155
optimized.comparisonFunction = getComparisonFunction(condition.operation);
117156
return optimized;
118157
}
119158

120159

160+
161+
121162
/**
122163
* Directly retrieves records that match the conditions without metadata processing.
123164
*/
@@ -158,12 +199,14 @@ async function runMetaDataCursorQuery(db, table, conditions, queryAdditions, yie
158199
let requiredProperties = new Set();
159200
let magicOrder = 0;
160201

161-
for (const andGroup of conditions) {
162-
for (const condition of andGroup) {
163-
if (condition.property) requiredProperties.add(condition.property);
164-
}
202+
if (conditions?.nodeType === "logical" && !conditions.children) {
203+
// No conditions — grab everything
204+
debugLog("Detected no-op predicate. All records will be evaluated.");
205+
} else {
206+
collectPropertiesFromTree(conditions, requiredProperties);
165207
}
166208

209+
167210
for (const addition of queryAdditions) {
168211
if ((addition.additionFunction === QUERY_ADDITIONS.ORDER_BY
169212
|| addition.additionFunction === QUERY_ADDITIONS.ORDER_BY_DESCENDING) &&
@@ -209,33 +252,6 @@ async function runMetaDataCursorQuery(db, table, conditions, queryAdditions, yie
209252
return primaryKeyList.slice(0, resultIndex);
210253
}
211254

212-
213-
function optimizeConditions(conditions) {
214-
return conditions.map(condition => {
215-
let optimizedCondition = { ...condition };
216-
217-
if (!condition.caseSensitive && typeof condition.value === "string") {
218-
optimizedCondition.value = condition.value.toLowerCase();
219-
}
220-
221-
// Edge case: GETDAY sanity check
222-
if (condition.operation === QUERY_OPERATIONS.GETDAY &&
223-
typeof condition.value !== "number") {
224-
console.warn(`[IndexedDB Warning] GETDAY expects a numeric day (1–31). Got:`, condition.value);
225-
}
226-
227-
if ([QUERY_OPERATIONS.GET_DAY_OF_WEEK, QUERY_OPERATIONS.GET_DAY_OF_YEAR, QUERY_OPERATIONS.GETDAY].includes(condition.operation)) {
228-
if (typeof condition.value !== "number") {
229-
console.warn(`[IndexedDB Warning] ${condition.operation} expects a numeric value. Got:`, condition.value);
230-
}
231-
}
232-
233-
optimizedCondition.comparisonFunction = getComparisonFunction(condition.operation);
234-
return optimizedCondition;
235-
});
236-
}
237-
238-
239255
function getComparisonFunction(operation) {
240256
const operations = {
241257
[QUERY_OPERATIONS.EQUAL]: (recordValue, queryValue) => recordValue === queryValue,
Lines changed: 119 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,131 @@
11
export function rebuildCursorConditionsToPredicateTree(flattened) {
2-
// Step 1: Normalize to OR of ANDs
3-
const orRoot = {
4-
nodeType: "logical",
5-
operator: "Or",
6-
children: flattened.map(andArray => ({
2+
const orGroups = [];
3+
4+
for (const andSet of flattened) {
5+
const filteredConditions = andSet.filter(cond => !isDummyCondition(cond));
6+
if (filteredConditions.length === 0) continue; // Skip this group if all conditions were dummy
7+
8+
const andGroup = {
79
nodeType: "logical",
810
operator: "And",
9-
children: andArray.map(cond => ({
11+
children: filteredConditions.map(condition => ({
1012
nodeType: "condition",
1113
condition: {
12-
property: cond.property,
13-
operation: cond.operation,
14-
value: cond.value,
15-
isString: cond.isString ?? false,
16-
caseSensitive: cond.caseSensitive ?? false
14+
property: condition.property,
15+
operation: condition.operation,
16+
value: condition.value,
17+
isString: condition.isString ?? false,
18+
caseSensitive: condition.caseSensitive ?? false
1719
}
1820
}))
19-
}))
20-
};
21+
};
2122

22-
// Step 2: Simplify redundant ANDs if possible
23-
const simplified = fixpointSimplify(orRoot);
23+
orGroups.push(andGroup);
24+
}
25+
26+
// Handle the case where ALL conditions were dummy: this means "match everything"
27+
if (orGroups.length === 0) {
28+
return fixpointSimplify({
29+
nodeType: "logical",
30+
operator: "Or",
31+
children: []
32+
});
33+
}
2434

25-
return simplified;
35+
const collapsed = collapseOrGroups(orGroups);
36+
37+
return fixpointSimplify({
38+
nodeType: "logical",
39+
operator: "Or",
40+
children: collapsed
41+
});
2642
}
2743

44+
function collapseOrGroups(orChildren) {
45+
const buckets = new Map();
46+
const results = [];
47+
48+
for (const node of orChildren) {
49+
if (node.operator !== "And") {
50+
results.push(node);
51+
continue;
52+
}
2853

29-
function extractDynamicIntentGroups(branches) {
30-
const conditionMap = new Map();
54+
const simpleKeys = [];
55+
const complexNodes = [];
3156

32-
for (const group of branches) {
33-
for (const conditionNode of group) {
34-
const { property, operation, value } = conditionNode.condition;
35-
const key = `${property}||${operation}`;
36-
if (!conditionMap.has(key)) conditionMap.set(key, new Set());
37-
conditionMap.get(key).add(JSON.stringify(conditionNode.condition));
57+
for (const child of node.children) {
58+
if (child.nodeType === "condition") {
59+
const key = `${child.condition.property}||${child.condition.operation}`;
60+
simpleKeys.push({ key, node: child });
61+
} else {
62+
complexNodes.push(child);
63+
}
3864
}
39-
}
4065

41-
// Build OR groups per property+operation
42-
const groupedByProperty = new Map();
66+
const groupKey = simpleKeys.map(s => s.key).sort().join("&&");
4367

44-
for (const [key, valueSet] of conditionMap.entries()) {
45-
const [property, op] = key.split("||");
46-
if (!groupedByProperty.has(property)) groupedByProperty.set(property, []);
47-
groupedByProperty.get(property).push({
48-
operation: op,
49-
values: [...valueSet].map(json => ({
50-
nodeType: "condition",
51-
condition: JSON.parse(json)
52-
}))
53-
});
68+
if (!buckets.has(groupKey)) {
69+
buckets.set(groupKey, []);
70+
}
71+
72+
buckets.get(groupKey).push(node);
5473
}
5574

56-
const resultGroups = [];
57-
for (const [property, ops] of groupedByProperty.entries()) {
58-
const propertyGroup = {
75+
// Build optimized OR sets per bucket
76+
for (const group of buckets.values()) {
77+
if (group.length === 1) {
78+
results.push(group[0]);
79+
continue;
80+
}
81+
82+
// Find which conditions are shared across all ANDs
83+
const conditionMatrix = group.map(g => g.children);
84+
const transposed = transpose(conditionMatrix);
85+
86+
const collapsedAnd = {
5987
nodeType: "logical",
60-
operator: "Or",
61-
children: ops.flatMap(o => o.values)
88+
operator: "And",
89+
children: []
6290
};
63-
resultGroups.push(propertyGroup);
91+
92+
for (const col of transposed) {
93+
// If all columns refer to the same property/operator but different values
94+
const base = col[0];
95+
const allSamePO = col.every(c =>
96+
c.nodeType === "condition" &&
97+
c.condition.property === base.condition.property &&
98+
c.condition.operation === base.condition.operation
99+
);
100+
101+
if (allSamePO) {
102+
// Build OR group of all values
103+
const orGroup = {
104+
nodeType: "logical",
105+
operator: "Or",
106+
children: col
107+
};
108+
collapsedAnd.children.push(orGroup);
109+
} else {
110+
// They differ structurally, preserve individually
111+
collapsedAnd.children.push(...col);
112+
}
113+
}
114+
115+
results.push(collapsedAnd);
64116
}
65117

66-
return resultGroups;
118+
return results;
119+
}
120+
121+
122+
function transpose(matrix) {
123+
if (!matrix.length) return [];
124+
const len = Math.max(...matrix.map(row => row.length));
125+
const transposed = Array.from({ length: len }, (_, i) =>
126+
matrix.map(row => row[i]).filter(Boolean)
127+
);
128+
return transposed;
67129
}
68130

69131
function fixpointSimplify(node) {
@@ -89,6 +151,7 @@ function simplify(node) {
89151
flattened.push(child);
90152
}
91153
}
154+
92155
node.children = flattened;
93156

94157
const seen = new Set();
@@ -104,4 +167,15 @@ function simplify(node) {
104167
}
105168

106169
return node;
107-
}
170+
}
171+
172+
173+
function isDummyCondition(cond) {
174+
if (typeof cond.value === "number") {
175+
if ((cond.value === Infinity || cond.value === -Infinity) &&
176+
["GreaterThanOrEqual", "LessThanOrEqual", "GreaterThan", "LessThan"].includes(cond.operation)) {
177+
return true;
178+
}
179+
}
180+
return false;
181+
}

0 commit comments

Comments
 (0)