Skip to content

Serve descending ORDER BY from ascending index tags#300

Merged
chrisrichards merged 1 commit into
mainfrom
feature/descending-order-index
Jul 6, 2026
Merged

Serve descending ORDER BY from ascending index tags#300
chrisrichards merged 1 commit into
mainfrom
feature/descending-order-index

Conversation

@chrisrichards

Copy link
Copy Markdown
Member

Closes #294.

Summary

ORDER BY col DESC is now index-served whenever the column has a usable ascending tag — previously any descending order meant a full scan plus in-memory sort:

select * from setup.dbf order by KEY_NAME desc                  → index order scan (descending) on tag 'KEY_NAME'
select CALL_ID from calls.dbf where CALL_ID >= 5 order by CALL_ID desc → index range scan on tag 'CALL_ID'   (no in-memory sort)
select CALL_ID from calls.dbf order by CALL_ID desc limit 3     → 16, 15, 14

How

The planner reverses the ascending tag's entry list — and then re-stabilizes each run of duplicate keys by record index, because a stable descending sort keeps tied rows in their original ascending row order, while a naive reversal would flip them. The duplicate-heavy CONTACT_ID tag (runs of five equal keys) pins this: the reversed index order must match the forced-scan stable sort byte-for-byte, and does. Works for pure order scans, for WHERE candidates on the same column (seeks and ranges), with TOP/LIMIT applied after the order, and through DbfQuery<T>.OrderByDescending. Multi-key orderings and untagged columns still sort in memory, as before.

Deliberate remainder

Tags that are themselves DESCENDING stay excluded from planning. No fixture anywhere contains one, and their on-disk key semantics are unverified (VFP may store complemented key bytes keeping the tree byte-ascending, or store a genuinely reversed tree) — searching them on a guess could silently miss rows, which is the one failure mode this planner never accepts. A follow-up issue tracks that remainder with the verification plan; contributing a VFP file with INDEX ON … TAG … DESCENDING would unblock it immediately.

Test plan

8 new tests in QueryDescendingOrderTests, all differential against forced scans: descending order scans (character and integer tags), the stable-duplicate-run case, descending order satisfied after equality seeks and ranges, LIMIT after descending order (exact sequence asserted), untagged-column fallback, and both builder paths. The phase-6 test that pinned DESC to a full scan is repurposed to a genuinely unsatisfiable multi-key ordering. Full suite: 440 passed, 1 pre-existing skip, zero warnings on both TFMs.

🤖 Generated with Claude Code

Closes #294.

A single-key ORDER BY ... DESC no longer forces an in-memory sort
when the column has a usable ascending tag: the planner reverses the
index entry list, and re-sorts each run of duplicate keys by record
index afterwards, because a stable descending sort keeps ties in
their original ascending row order. This applies to pure order
scans, to WHERE candidates on the same column (equality seeks and
ranges), to TOP/LIMIT after the order, and to the Query<T> builder's
OrderByDescending.

Tags that are themselves DESCENDING remain excluded from planning:
no fixture contains one and their on-disk key semantics (complement
encoding versus reversed tree order) are unverified, so searching
them could silently miss rows. That remainder is tracked separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

@chrisrichards chrisrichards merged commit df15938 into main Jul 6, 2026
3 checks passed
@chrisrichards chrisrichards deleted the feature/descending-order-index branch July 6, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support descending CDX index tags

1 participant