Skip to content

fix(engine): seek_key must honor SET DELETED ON#4

Open
Admnwk wants to merge 2 commits into
FiveTechSoft:mainfrom
Admnwk:fix/seek-honor-set-deleted
Open

fix(engine): seek_key must honor SET DELETED ON#4
Admnwk wants to merge 2 commits into
FiveTechSoft:mainfrom
Admnwk:fix/seek-honor-set-deleted

Conversation

@Admnwk

@Admnwk Admnwk commented Jun 18, 2026

Copy link
Copy Markdown

Hi Antonio — as discussed, here is the seek/SET DELETED ON fix.

Problem

With SET DELETED ON, dbSeek of a key whose only matching record is
deleted returned Found() == .T. (it landed on the deleted row).
dbGoTop / dbGoBottom / dbSkip and the index walk already skipped
deleted rows correctly — only seek did not.

Root cause

Table::goto_top / goto_bottom / skip consult
openads::abi::show_deleted(), but Table::seek_key returned the raw
B+tree match without checking it.

Fix

After the B+tree positions, when deleted rows are hidden, skip forward
over deleted records and re-derive exact from the row actually landed
on (Clipper/DBFCDX semantics: if every matching row is deleted, Found()
is .F. and the cursor sits on the next live record / Eof).

Single, additive change in src/engine/table.cpp::seek_key; no behavior
change when SET DELETED OFF (the default).

Testing

Developed and proven against a vendored OpenADS build driven by a Harbour
RDD CRUD harness over a legacy DBF/CDX dataset: with the fix, after
DELETE + SET DELETED ON, dbSeek on the deleted key returns .F.
and the cursor lands on the next live record; index walk count is
unchanged. The patch applies cleanly to current main (the seek_key
landing point and all primitives used — load_record_, is_deleted(),
idx->next()/current_key()/key_length(), show_deleted(),
State::Limbo/Eof — are unchanged). Happy to add a focused smoke test if
you'd like it in this PR.

Co-Authored-By: Claude Opus 4.8 (1M context)

dbSeek of a key whose only matching record is deleted returned Found()==.T.
(it landed on the deleted row). goto_top/goto_bottom/skip already consult
show_deleted(); seek_key returned the raw B+tree match without checking it.

After the B+tree positions, when deleted rows are hidden, skip forward over
deleted records and re-derive `exact` from the row actually landed on
(Clipper/DBFCDX semantics: if every matching row is deleted, Found() is .F.
and the cursor sits on the next live record / Eof).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request updates Table::seek_key in src/engine/table.cpp to correctly honor the SET DELETED ON setting by skipping over deleted records when performing a key seek. The review feedback highlights a potential bug where walking to the last matching key (such as in descending indexes) can still land on a deleted record, and suggests walking backward to find the last active matching record. Additionally, the feedback recommends simplifying the string padding and comparison logic using standard library functions like std::string::resize and operator== to improve readability.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/engine/table.cpp
ck.append(del_key.size() - ck.size(), ' ');
if (ck.size() > del_key.size()) ck.resize(del_key.size());
exact = (std::memcmp(ck.data(), del_key.data(), del_key.size()) == 0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

When SET DELETED ON is active, if the seek lands on an exact match and walk_to_last is true (which is always the case for descending indexes or when last is true), the code walks forward to the last matching key without checking if those records are deleted. If the last matching record is deleted, the seek will land on it and report it as found, violating SET DELETED ON.

To fix this, after the walk_to_last loop finishes and loads the record, if SET DELETED ON is active and the loaded record is deleted, we should walk backward using idx->prev() until we find a non-deleted record. Since we started at a known active matching record, we are guaranteed to find one.

Here is how the walk_to_last block (lines 1083-1109) should be updated:

    if (walk_to_last && exact) {
        std::string padded_key = key;
        padded_key.resize(idx->key_length(), ' ');
        std::uint32_t last_recno = r.value().recno;
        while (true) {
            auto step = idx->next();
            if (!step || !step.value().positioned) break;
            std::string ck = idx->current_key();
            ck.resize(padded_key.size(), ' ');
            if (ck != padded_key) {
                (void)idx->prev();
                break;
            }
            last_recno = step.value().recno;
        }
        auto load = load_record_(last_recno);
        if (!load) return load.error();
        if (!openads::abi::show_deleted()) {
            while (is_deleted()) {
                auto step = idx->prev();
                if (!step || !step.value().positioned) break;
                if (auto ld = load_record_(step.value().recno); !ld) return ld.error();
            }
        }
        last_seek_found_ = true;
        return true;
    }

Comment thread src/engine/table.cpp
Comment on lines +1049 to +1073
if (!openads::abi::show_deleted()) {
std::string del_key = key;
if (del_key.size() < idx->key_length())
del_key.append(idx->key_length() - del_key.size(), ' ');
if (del_key.size() > idx->key_length())
del_key.resize(idx->key_length());
while (r.value().positioned) {
if (auto ld = load_record_(r.value().recno); !ld) return ld.error();
if (!is_deleted()) break;
r = idx->next();
if (!r) return r.error();
}
if (!r.value().positioned) {
state_ = (driver_->record_count() == 0) ? State::Limbo
: State::Eof;
recno_ = 0;
last_seek_found_ = false;
return false;
}
std::string ck = idx->current_key();
if (ck.size() < del_key.size())
ck.append(del_key.size() - ck.size(), ' ');
if (ck.size() > del_key.size()) ck.resize(del_key.size());
exact = (std::memcmp(ck.data(), del_key.data(), del_key.size()) == 0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The manual padding and resizing of del_key and ck can be simplified using std::string::resize(size, fill_char). This reduces boilerplate and makes the code much more readable. Additionally, we can use operator== for string comparison instead of std::memcmp.

    if (!openads::abi::show_deleted()) {
        std::string del_key = key;
        del_key.resize(idx->key_length(), ' ');
        while (r.value().positioned) {
            if (auto ld = load_record_(r.value().recno); !ld) return ld.error();
            if (!is_deleted()) break;
            r = idx->next();
            if (!r) return r.error();
        }
        if (!r.value().positioned) {
            state_ = (driver_->record_count() == 0) ? State::Limbo
                                                    : State::Eof;
            recno_ = 0;
            last_seek_found_ = false;
            return false;
        }
        std::string ck = idx->current_key();
        ck.resize(del_key.size(), ' ');
        exact = (ck == del_key);
    }

@Admnwk

Admnwk commented Jun 18, 2026

Copy link
Copy Markdown
Author

Added a focused, self-contained smoke for this fix. It needs no fixtures — it DBCREATEs its own generic table and runs entirely through the OpenADS engine (rddadsopenace64), so it is fully reproducible with synthetic data only.

What it exercises — table CLI(COD, NOME), structural index on COD, rows K001..K005:

  1. DELETE the row for key K003. DBF semantics: this only flags the row — RecCount() stays 5 (a PACK would be needed to remove it physically).
  2. SET DELETED ONdbSeek("K003") must report Found() == .F. and the cursor must land on the next live row (K004).
  3. A live key (K002) still seeks fine; the index walk counts 4 live rows; RecCount() is still 5.
  4. Control — SET DELETED OFF (the default): the deleted row is visible again, so dbSeek("K003") is Found() == .T. and the walk counts all 5. No behavior change when the setting is off.

Output (against a build that includes this patch):

=== OpenADS smoke: seek_key honors SET DELETED ON ===
engine = openace64.dll (next to exe)   data = DAT_SEEKDEL

inserted RecCount = 5

  [PASS] seek deleted key K003 -> Found() == .F.
  [PASS]   cursor lands on next live row (K004)
  [PASS] seek live key K002 -> Found() == .T.
  [PASS] index walk (DELETED ON) counts 4 live rows
  [PASS] physical RecCount unchanged (5) - DELETE only flags
  [PASS] DELETED OFF: deleted key K003 still Found() == .T.
  [PASS]   index walk (DELETED OFF) counts all 5 rows

=== RESULT: 7 passed, 0 failed ===

The driver is a small Harbour/RDD program. Happy to translate it into a doctest case under tests/ if you'd prefer it in the suite's native C++ form.

Gemini review: after walking to the last equal-key entry, skip backward over deleted rows when SET DELETED ON is active. Simplify key padding with resize/operator==.

Co-Authored-By: Grok Build (colab) <noreply@x.ai>
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.

2 participants