Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/convex-helpers/server/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,4 +813,45 @@ describe("stream", () => {
]);
});
});

test("pagination with eq(undefined) on compound index", async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes without the proposed changes, so I think we need another test that previously failed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// Regression test for bug where eq(field, undefined) on a compound index
// would cause "Cannot use gte on field 'undefined'" error during pagination
const testSchema = defineSchema({
items: defineTable({
softDeletedAt: v.optional(v.number()),
updatedAt: v.number(),
}).index("by_active_updated", ["softDeletedAt", "updatedAt"]),
});
const t = convexTest(testSchema, modules);
await t.run(async (ctx) => {
// Insert some active (non-deleted) items
await ctx.db.insert("items", { updatedAt: 1 });
await ctx.db.insert("items", { updatedAt: 2 });
await ctx.db.insert("items", { updatedAt: 3 });
// Insert some soft-deleted items
await ctx.db.insert("items", { softDeletedAt: 100, updatedAt: 4 });

const query = stream(ctx.db, testSchema)
.query("items")
.withIndex("by_active_updated", (q) => q.eq("softDeletedAt", undefined))
.order("desc");

// This should not throw "Cannot use gte on field 'undefined'"
const result = await query.paginate({ numItems: 2, cursor: null });
expect(result.page.length).toBe(2);
expect(result.page[0]!.updatedAt).toBe(3);
expect(result.page[1]!.updatedAt).toBe(2);
expect(result.isDone).toBe(false);

// Continue pagination
const page2 = await query.paginate({
numItems: 2,
cursor: result.continueCursor,
});
expect(page2.page.length).toBe(1);
expect(page2.page[0]!.updatedAt).toBe(1);
expect(page2.isDone).toBe(true);
});
Comment on lines +817 to +855

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix Prettier failure in the new regression test.

CI reports a Prettier formatting failure for this file; please run Prettier (or your formatter) to normalize the indentation in this new test block.

🤖 Prompt for AI Agents
In `@packages/convex-helpers/server/stream.test.ts` around lines 817 - 856,
Prettier formatting failed for the new test "pagination with eq(undefined) on
compound index"; run your project's Prettier (or formatter) on
packages/convex-helpers/server/stream.test.ts and reformat the new test block so
indentation and spacing match the repo style (normalize indentation around the
test definition and the block using defineSchema, defineTable, convexTest,
stream, and query.paginate); then re-run lint/CI to ensure the file passes
formatting checks before reopening the PR.

});
});
9 changes: 6 additions & 3 deletions packages/convex-helpers/server/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ function splitRange(
) => {
const range = commonPrefix.slice();
let i = 0;
for (; i < key.length - 1; i++) {
for (; i < key.length - 1 && i < indexFields.length; i++) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would key be longer than indexFields? That seems like a bug - unless they indexFields don't include _creationTime, _id, which would be important to respect here. If we want to add more safety let's assert that it's the case, vs. not constraining the range as much as expected

range.push(["eq", indexFields[i]!, key[i]!]);
}
if (i < key.length) {
if (i < key.length && i < indexFields.length) {
range.push([boundType, indexFields[i]!, key[i]!]);
}
return range;
Expand All @@ -111,12 +111,15 @@ function splitRange(
middleRange = makeCompare(startBoundType, startBound);
} else if (startBound.length === 0) {
middleRange = makeCompare(endBoundType, endBound);
} else {
} else if (indexFields.length > 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, not sure how this could happen, so would rather assert - not sure how we would get here

const startValue = startBound[0]!;
const endValue = endBound[0]!;
middleRange = commonPrefix.slice();
middleRange.push([startBoundType, indexFields[0]!, startValue]);
middleRange.push([endBoundType, indexFields[0]!, endValue]);
} else {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love to have some unit tests just on splitRange to validate that it's doing what it's supposed to for the four variants we expect here -

// All fields are in the common prefix, use it as the middle range
middleRange = commonPrefix.slice();
}
const ranges = [...startRanges, middleRange, ...endRanges];
if (order === "desc") {
Expand Down
Loading