Skip to content

Commit f0251a7

Browse files
patching cursor engine meta data information grabs for better ordering. Also added StableOrdering() functionality.
1 parent 3c00d6d commit f0251a7

File tree

9 files changed

+79
-8
lines changed

9 files changed

+79
-8
lines changed

Magic.IndexedDb/LinqTranslation/Interfaces/IMagicCursor.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,13 @@ public interface IMagicCursor<T> : IMagicExecute<T> where T : class
3131
/// <param name="predicate"></param>
3232
/// <returns></returns>
3333
IMagicCursorStage<T> OrderByDescending(Expression<Func<T, object>> predicate);
34+
35+
/// <summary>
36+
/// Removes ordering of any indexed columns. Only orders by
37+
/// what you dictate then by the row insert order. This creates
38+
/// a much more predictable and stable ordering. Only needs
39+
/// to be applied once.
40+
/// </summary>
41+
/// <returns></returns>
42+
IMagicCursorStage<T> StableOrdering();
3443
}

Magic.IndexedDb/LinqTranslation/Models/MagicCursor.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Magic.IndexedDb.LinqTranslation.Extensions;
2+
using Magic.IndexedDb.Models;
23
using System.Linq.Expressions;
34
using System.Runtime.CompilerServices;
45

@@ -35,6 +36,15 @@ public IMagicCursorStage<T> OrderBy(Expression<Func<T, object>> predicate)
3536
public IMagicCursorStage<T> OrderByDescending(Expression<Func<T, object>> predicate)
3637
=> new MagicCursorExtension<T>(MagicQuery).OrderByDescending(predicate);
3738

39+
public IMagicCursorStage<T> StableOrdering()
40+
{
41+
var _MagicQuery = new MagicQuery<T>(this.MagicQuery);
42+
StoredMagicQuery smq = new StoredMagicQuery();
43+
smq.additionFunction = MagicQueryFunctions.StableOrdering;
44+
_MagicQuery.StoredMagicQueries.Add(smq);
45+
return new MagicCursorExtension<T>(_MagicQuery);
46+
}
47+
3848
public async IAsyncEnumerable<T> AsAsyncEnumerable(
3949
[EnumeratorCancellation] CancellationToken cancellationToken = default)
4050
{

Magic.IndexedDb/LinqTranslation/Models/MagicQuery.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public MagicQuery(MagicQuery<T> _MagicQuery)
4040
StoredMagicQueries = new List<StoredMagicQuery>(_MagicQuery.StoredMagicQueries); // Deep copy
4141
ResultsUnique = _MagicQuery.ResultsUnique;
4242
Predicates = new List<Expression<Func<T, bool>>>(_MagicQuery.Predicates); // Deep copy
43+
ForceCursorMode = _MagicQuery.ForceCursorMode;
4344
}
4445

4546
public IMagicQueryStaging<T> Where(Expression<Func<T, bool>> predicate)

Magic.IndexedDb/Models/StoredMagicQuery.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal struct MagicQueryFunctions
1010
public const string Reverse = "reverse";
1111
public const string First = "first";
1212
public const string Last = "last";
13+
public const string StableOrdering = "stableOrdering";
1314

1415
}
1516
public class StoredMagicQuery

Magic.IndexedDb/wwwroot/magicLinqToIndexedDb.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export async function* magicQueryYield(db, table, universalSerializedPredicate,
3434
throw new Error("A valid Dexie table instance must be provided.");
3535
}
3636

37+
const stableOrderingRequested = hasStableOrdering(queryAdditions);
38+
if (stableOrderingRequested) {
39+
forceCursor = true;
40+
}
41+
3742
debugLog('universal serialized predicate');
3843
debugLog(universalSerializedPredicate);
3944
const { nestedOrFilterUnclean, isUniversalTrue, isUniversalFalse } = flattenUniversalPredicate(universalSerializedPredicate);
@@ -116,6 +121,10 @@ export async function* magicQueryYield(db, table, universalSerializedPredicate,
116121

117122
}
118123

124+
function hasStableOrdering(queryAdditions) {
125+
return queryAdditions?.some(q => q.additionFunction === QUERY_ADDITIONS.STABLE_ORDERING);
126+
}
127+
119128
async function runIndexedQueries(db, table, universalQueries,
120129
queryAdditions, primaryKeys, yieldedPrimaryKeys) {
121130
if (universalQueries.length === 0) {
@@ -281,6 +290,8 @@ function runIndexedQuery(table, indexedConditions, queryAdditions = []) {
281290
return query.first();
282291
case QUERY_ADDITIONS.LAST:
283292
return query.last();
293+
case QUERY_ADDITIONS.STABLE_ORDERING:
294+
break; // do nothing
284295
default:
285296
throw new Error(`Unsupported query addition: ${addition.additionFunction}`);
286297
}

Magic.IndexedDb/wwwroot/utilities/cursorEngine.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,20 @@ export async function runCursorQuery(db, table, conditions, queryAdditions, yiel
2424
);
2525

2626
if (requiresMetaProcessing) {
27-
// **Metadata Path: Extract primary keys and sorting properties**
28-
let primaryKeyList = await runMetaDataCursorQuery(db, table, structuredPredicateTree, queryAdditions, yieldedPrimaryKeys, compoundKeys);
2927

30-
const indexOrderProps = detectIndexOrderProperties(structuredPredicateTree, table);
28+
let stableOrdering = hasStableOrdering(queryAdditions);
29+
30+
let indexOrderProps = [];
31+
32+
if (!stableOrdering) {
33+
indexOrderProps = detectIndexOrderProperties(structuredPredicateTree, table);
34+
}
35+
else {
36+
debugLog("Stable Ordering detected. Disabling any ordering by indexed queries.");
37+
}
38+
39+
// **Metadata Path: Extract primary keys and sorting properties**
40+
let primaryKeyList = await runMetaDataCursorQuery(db, table, structuredPredicateTree, queryAdditions, yieldedPrimaryKeys, compoundKeys, indexOrderProps);
3141

3242
// **Apply sorting, take, and skip operations**
3343
let finalPrimaryKeys = applyCursorQueryAdditions(primaryKeyList, queryAdditions, compoundKeys, true, indexOrderProps);
@@ -43,6 +53,11 @@ export async function runCursorQuery(db, table, conditions, queryAdditions, yiel
4353
}
4454
}
4555

56+
function hasStableOrdering(queryAdditions) {
57+
return queryAdditions?.some(q => q.additionFunction === QUERY_ADDITIONS.STABLE_ORDERING);
58+
}
59+
60+
4661
function detectIndexOrderProperties(predicateTree, table) {
4762
const indexedProps = new Set();
4863

@@ -183,7 +198,7 @@ function optimizeSingleCondition(condition) {
183198
// Lowercase normalization for string values if not case-sensitive
184199
if (!condition.caseSensitive && typeof condition.value === "string") {
185200
optimized.value = condition.value.toLowerCase();
186-
}
201+
}
187202

188203
optimized.comparisonFunction = getComparisonFunction(condition.operation);
189204
return optimized;
@@ -226,7 +241,7 @@ async function runDirectCursorQuery(db, table, conditions, yieldedPrimaryKeys, c
226241
/**
227242
* Extracts only necessary metadata using a Dexie cursor in a transaction.
228243
*/
229-
async function runMetaDataCursorQuery(db, table, conditions, queryAdditions, yieldedPrimaryKeys, compoundKeys) {
244+
async function runMetaDataCursorQuery(db, table, conditions, queryAdditions, yieldedPrimaryKeys, compoundKeys, detectedIndexOrderProperties = []) {
230245
debugLog("Extracting Metadata for Cursor Query", { conditions, queryAdditions });
231246

232247
let requiredProperties = new Set();
@@ -252,6 +267,12 @@ async function runMetaDataCursorQuery(db, table, conditions, queryAdditions, yie
252267
requiredProperties.add(key);
253268
}
254269

270+
// Include all indexable props that affect ordering
271+
for (const prop of detectedIndexOrderProperties) {
272+
requiredProperties.add(prop);
273+
}
274+
275+
255276
requiredProperties.add("_MagicOrderId");
256277

257278
let estimatedSize = await table.count();
@@ -820,6 +841,9 @@ function applyCursorQueryAdditions(
820841
primaryKeyList = primaryKeyList.length > 0 ? [primaryKeyList[primaryKeyList.length - 1]] : [];
821842
break;
822843

844+
case QUERY_ADDITIONS.STABLE_ORDERING:
845+
break; // skip this
846+
823847
default:
824848
throw new Error(`Unsupported query addition: ${addition.additionFunction}`);
825849
}

Magic.IndexedDb/wwwroot/utilities/linqValidation.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44
import { QUERY_OPERATIONS, QUERY_ADDITIONS, QUERY_COMBINATION_RULES, QUERY_ADDITION_RULES } from "./queryConstants.js";
55
import { debugLog } from "./utilityHelpers.js";
66

7-
export function validateQueryAdditions(queryAdditions, indexCache) {
8-
queryAdditions = queryAdditions || []; // Ensure it's always an array
7+
export function validateQueryAdditions(queryAdditionsPre, indexCache) {
8+
queryAdditionsPre = queryAdditionsPre || []; // Ensure it's always an array
9+
10+
// Filter out non-actionable additions like STABLE_ORDERING
11+
const queryAdditions = queryAdditionsPre.filter(q =>
12+
q.additionFunction !== QUERY_ADDITIONS.STABLE_ORDERING
13+
);
14+
915
let seenAdditions = new Set();
1016
let requiresCursor = false;
1117

@@ -151,6 +157,11 @@ export function isValidQueryAdditions(arr) {
151157
return;
152158
}
153159

160+
// Skip intValue and property validation for STABLE_ORDERING
161+
if (obj.additionFunction === QUERY_ADDITIONS.STABLE_ORDERING) {
162+
return;
163+
}
164+
154165
if (typeof obj.additionFunction !== 'string') {
155166
console.error(`Error at index ${index}: additionFunction must be a string but got:`, obj.additionFunction);
156167
isValid = false;

Magic.IndexedDb/wwwroot/utilities/queryConstants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export const QUERY_ADDITIONS = {
119119
SKIP: "skip",
120120
TAKE: "take",
121121
TAKE_LAST: "takeLast",
122+
STABLE_ORDERING: "stableOrdering",
122123
};
123124

124125
// Query Combination Ruleset (What Can Be Combined in AND `&&`)

TestWasm/Share/PanelCustomTests.razor

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,13 @@
9191
StateHasChanged();
9292

9393

94-
RunTest("Weird IndexedDB order by test",
94+
RunTest("Indexed Column in predicate order by",
9595
await personQuery.Where(x => x.TestInt > 2).Take(2).ToListAsync(),
9696
allPeople.Where(x => x.TestInt > 2).OrderBy(x => x.TestInt).ThenBy(x => x._Id).Take(2));
9797

98+
RunTest("Indexed Column in predicate order by StableOrderBy",
99+
await personQuery.Cursor(x => x.TestInt > 2).StableOrdering().Take(2).ToListAsync(),
100+
allPeople.Where(x => x.TestInt > 2).OrderBy(x => x._Id).Take(2));
98101

99102
RunTest("Date Equal",
100103
await personQuery.Where(x => x.DateOfBirth.Value.Date == new DateTime(2020, 2, 10)).ToListAsync(),

0 commit comments

Comments
 (0)