Skip to content

Corax static index returns incorrect results when paging a filtered query sorted by ISO date strings #23570

Description

@wagich

Description

A filtered, sorted, and paged query against a Corax static index can return incorrect documents when textual sort values collide on Corax's compact prefix key. The query results then depend on index ingestion order rather than the complete sort values.

The ISO strings reflect a real use case: RavenDB.Client.NodaTime serializes NodaTime.LocalDate in yyyy-MM-dd form. These values are then treated as a string and therefore use textual Sequence sorting which fails for these types of values.
DateTime, native DateOnly, Instant, and LocalDateTime use RavenDB's temporal indexing paths and sort correctly.

Reproduced with:

  • RavenDB 6.2.17, build 62092
  • Current release/v6.2: 6.2.20-custom-62

Full reproduction:

https://github.com/wagich/RavenDbCoraxSortedPagingRepro

Reduced reproduction

using Raven.Client.Documents.Indexes;
using Raven.TestDriver;
using Xunit;

public class CoraxSortedPagingTest : RavenTestDriver
{
    private sealed class Item
    {
        public string Id { get; set; } = default!;
        public string Category { get; set; } = default!;
        public string Date { get; set; } = default!;
        public string Name { get; set; } = default!;
    }

    private sealed class Items_ByCategory : AbstractIndexCreationTask<Item>
    {
        public Items_ByCategory()
        {
            Map = items => from item in items
                           select new { item.Category, item.Date };
        }
    }

    [Fact]
    public async Task Should_page_after_sorting_complete_terms()
    {
        using var store = GetDocumentStore();
        await new Items_ByCategory().ExecuteAsync(store);

        using (var session = store.OpenAsyncSession())
        {
            await session.StoreAsync(new Item { Id = "items/A", Name = "A", Category = "News", Date = "2026-09-01" });
            await session.StoreAsync(new Item { Id = "items/B", Name = "B", Category = "News", Date = "2026-08-01" });
            await session.StoreAsync(new Item { Id = "items/C", Name = "C", Category = "News", Date = "2026-07-01" });
            await session.StoreAsync(new Item { Id = "items/D", Name = "D", Category = "News", Date = "2026-06-01" });
            await session.SaveChangesAsync();
        }

        WaitForIndexing(store);

        using var querySession = store.OpenAsyncSession();
        var page = await querySession.Query<Item, Items_ByCategory>()
            .Where(item => item.Category == "News")
            .OrderByDescending(item => item.Date)
            .Take(2)
            .ToListAsync();

        Assert.Equal(new[] { "A", "B" }, page.Select(item => item.Name));
    }
}

Expected: A,B
Actual: C,D

Without Take(2), the query correctly returns A,B,C,D.

Root cause

In SortingMatch<TInner>.EntryComparerByTerm.SortByTerms, Corax:

  1. Builds an approximate key from at most six bytes of each compact encoded term.
  2. Sorts those approximate keys.
  3. Truncates the results to _take.
  4. Resolves equal prefixes using the complete terms.

Consequently, candidates tied on the approximate key can be discarded before the complete-term comparison determines their correct order.

Moving MaybeBreakTies(...) before the _take truncation made all reproduction tests pass against 6.2.20-custom-62. A narrower correction could resolve the tie group crossing the cutoff before truncating.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions