Blog › ICP guides
Clipper developer on retainer: SEEK index case sensitivity, CA-Clipper 5.2, Harbour, and xBase DBF on monthly retainer
October 8, 2026 · ~14 min read
A Clipper developer was maintaining a legacy inventory management application written in CA-Clipper 5.2 for a small parts distributor. The application included an invoice lookup routine: the user typed an invoice ID into a screen form, pressed Enter, and the program searched the INVOICES.DBF file for a matching record. The developer had built the search using SEEK() against an NTX index file (INVOICES.NTX), created with the expression UPPER(invoice_id). For five consecutive business days, five customers reported that invoice lookups returned “invoice not found” for invoices that were clearly visible when the user browsed INVOICES.DBF directly. The invoices existed in the database — querying the same DBF file manually with BROWSE() showed every record correctly. SEEK() could not find them. The reports came in one per day, and each day the affected invoice had been entered that morning by a different staff member at the front desk, which made the failure pattern look intermittent rather than systematic. No Clipper runtime error was produced. No NTX index error was produced. FOUND() simply returned .F. and the lookup routine branched to its “not found” message path.
The root cause was the mismatch between the index expression and the SEEK value. The NTX index on INVOICES.DBF was created with: INDEX ON UPPER(invoice_id) TO INVOICES. Every invoice ID stored in the index was in uppercase (“INV-2024-001”, “INV-2025-042”). The invoice lookup routine called SEEK(m_invoice_id) — where m_invoice_id was the raw user-entered string from the GET/READ form. If the user typed “Inv-2025-042” or “inv-2025-042” (common for users who did not have CAPS LOCK on), the SEEK value was mixed-case. SEEK performs a binary search against the index expression values stored in the NTX file; the binary search compared “Inv-2025-042” against the uppercase-indexed values and found no match because binary comparison of character strings is case-sensitive and “I” does not equal “i” at the byte level. FOUND() returned .F.. The lookup reported “not found.” The five “missing” invoices were precisely those looked up with mixed-case user input on that day; all five invoices were present and correct in INVOICES.DBF.
The root of the problem is what SEEK means against an expression-based NTX index. In Clipper’s xBase model, an NTX index file stores the evaluated result of the index expression for each record, in sorted order. If the index expression is UPPER(invoice_id), the NTX stores the uppercase form of every invoice_id field value. SEEK performs a binary search comparing its argument directly against these stored index values — it does not apply the index expression to the SEEK argument. The developer must ensure that the SEEK argument matches the form in which data is stored in the index: SEEK(UPPER(m_invoice_id)) to match an UPPER(invoice_id) index, or SEEK(DTOC(m_date)) to match a DTOC(date_field) date-as-string index, or SEEK(STR(m_part_no, 8)) to match a STR(part_number, 8) numeric-as-string index. This is fundamentally distinct from SQL-style indexes on non-expression columns, where the database engine evaluates case-insensitivity automatically and the application developer does not need to mirror the index transformation in the query predicate. In Clipper’s xBase model, the SEEK argument is the application’s responsibility: no engine-level normalization occurs between the SEEK argument and the index-stored value.
The fix was to change SEEK(m_invoice_id) to SEEK(UPPER(m_invoice_id)) in the invoice lookup routine. After the fix, the binary search compared “INV-2025-042” (UPPER-applied SEEK argument) against “INV-2025-042” (UPPER-applied index value) — exact match. FOUND() returned .T. The five unfound invoices were all resolved: unfound invoices: 5 → 0 after the SEEK(UPPER()) fix. The investigation — reading the NTX index expression from the Clipper source (INDEX ON UPPER(invoice_id) TO INVOICES), identifying the SEEK call that did not apply UPPER(), and understanding why SEEK performs an exact binary match against index-stored values rather than re-evaluating the index expression on both sides — took 2 hours. The fix itself was a single-word change in one line of INVOICES.PRG. The 2-hour cost was entirely in investigation: reconstructing which index file was active, reading the INDEX ON expression that created the NTX, tracing the m_invoice_id GET variable through the form input, and confirming that no other SEEK call in the codebase had the same mismatch for other index files.
Clipper, the xBase model, and the NTX index architecture
Nantucket Corporation released CA-Clipper in 1985 as a compiler for dBASE III programs. Where dBASE III interpreted source code at runtime, Clipper compiled the same xBase syntax into a native DOS executable, producing dramatically faster applications. Computer Associates (CA) acquired Nantucket Corporation in 1992. Clipper 5.2 — internally codenamed “Summer ’87” for its original design era, released commercially in 1993 — was the last major version of CA-Clipper. It added improved OOP support via class definitions, enhanced array functions, and better memory management for larger DBF files. After CA-Clipper was discontinued, the xBase community forked the language into several compatible implementations. Harbour is the leading open-source Clipper-compatible compiler maintained today; it compiles Clipper 5.2 source with few modifications and targets Windows, Linux, and macOS. MiniGUI is a widely used Harbour GUI toolkit that provides Windows dialogs, grids, and forms through a Harbour-compatible API. FiveWin is a commercial framework that adds a full Windows GUI layer to Harbour applications, allowing legacy Clipper 5.2 DOS programs to be ported to Windows with minimal source changes. xBase is the collective term for the family of dBASE-descended programming languages and file formats, including dBASE, Clipper, FoxPro, Visual FoxPro, and Harbour.
The DBF file format is the foundational data storage format of the xBase family, originating with dBASE II in the early 1980s. A DBF file is a fixed-length record file: every record occupies exactly the same number of bytes, determined by the sum of the field widths defined in the DBF header. Byte 0 of each record is the deletion flag: a space character (0x20) indicates an active record; an asterisk (*, 0x2A) indicates a deleted record. Deleted records remain physically present in the DBF file until a PACK command rewrites the file without them. SET DELETED ON causes Clipper to skip deleted records in all navigation and SEEK operations; SET DELETED OFF (the default) makes deleted records visible to all operations. The DBF header contains field definitions: each field has a name (up to 10 characters), a type, a width, and for numeric fields, a decimal places count. Field types in CA-Clipper 5.2 are: C (Character — fixed-length, right-padded with spaces to the defined width); N (Numeric — stored as a right-justified string of digits with optional decimal point); D (Date — stored as an 8-byte string in YYYYMMDD format); L (Logical — stored as T or F, accessed as .T. or .F. in Clipper code); and M (Memo — a 10-byte pointer to a variable-length text block in a paired .DBT file). Character fields are always padded to their full defined width, which means SEEK values for character fields must also match that width or be trimmed with TRIM() or RTRIM() as appropriate.
NTX is Clipper’s native index file format. An NTX file is created with INDEX ON <expression> TO <filename> and stores the evaluated result of the index expression for every non-deleted record in the associated DBF, arranged in a B-tree for binary search performance. The index expression can be a field name (INDEX ON invoice_id TO INVOICES), a function call on a field (INDEX ON UPPER(invoice_id) TO INVOICES), a concatenation (INDEX ON cust_id + DTOS(order_date) TO CUSTORD), or any valid Clipper expression that returns a character or numeric value. SEEK(value) performs a binary search against the B-tree of stored expression values; it requires that the supplied value match the type and form of the stored index values. SET ORDER TO n selects which of the currently open index files is the “controlling” index — the one used for SEEK and for the logical navigation order. USE file.dbf INDEX idx1,idx2,idx3 opens a DBF file with up to fifteen NTX index files simultaneously; Clipper updates all open indexes on every REPLACE, APPEND BLANK, and DELETE. In CA-Clipper 5.2, ORDERSET() and DBSETORDER() provide the Clipper 5.x API for selecting the active order among open index files. FOUND() returns .T. if the last SEEK or LOCATE succeeded. RECNO() returns the current record number (1-based). EOF() returns .T. when the record pointer is past the last record. BOF() returns .T. when the record pointer is before the first record.
Key xBase operations in CA-Clipper 5.2 that retainer developers work with regularly include: LOCATE FOR <condition> for a sequential scan with a logical condition (slower than SEEK but does not require an index); CONTINUE to resume the scan after the last LOCATE match; SET FILTER TO <condition> for persistent record filtering that hides non-matching records from all navigation; GOTO <recno> to position by physical record number; SKIP n to move the record pointer forward or backward by n records; DBEDIT() and BROWSE() for interactive tabular display of DBF records; @row,col SAY <value> GET <variable> followed by READ for full-screen form input; REPLACE <field> WITH <value> to update a field in the current record; APPEND BLANK to add a new empty record and position the pointer on it; SELECT <n> followed by USE <file> to open a DBF in a numbered work area (1–255); alias->fieldname notation to access fields in a different work area without changing the current SELECT; DBCLOSEALL() to close all open DBF and index files; MEMOEDIT() for interactive memo field editing; codeblock syntax {|x| expression} with EVAL() for codeblock execution; AEVAL() to iterate an array with a codeblock; ASORT() to sort an array; and ASIZE() to resize an array. The SET DELETED ON/OFF flag interacts with all of these operations: with SET DELETED ON, physically present but deletion-flagged records are invisible to SEEK, LOCATE, SKIP, BROWSE, and all other navigation.
Typical Clipper retainer work and what it looks like in a work log
SEEK case-sensitivity bugs are the most common and most underlogged category of Clipper retainer work. The pattern always follows the same structure: an NTX index is built with a transformation expression — UPPER(field) for case-insensitive character lookups, STR(field, n) to convert numeric fields to fixed-width strings for character index storage, DTOC(date_field) or DTOS(date_field) to convert date fields to strings — and the SEEK call does not apply the matching transformation to its argument. For UPPER-based indexes, the mismatch is usually user input: a GET/READ variable that captures whatever case the user typed. For STR-based numeric indexes, the mismatch is usually a data type issue: the developer SEEKs a raw numeric variable against an index that stored STR(part_number, 8), and the binary comparison of a numeric SEEK value against a character-type index fails silently. For DTOC-based date indexes, the mismatch is locale-dependent: DTOC() returns the date in the locale-defined format (MM/DD/YY or DD/MM/YY depending on SET DATE), and a SEEK argument formatted differently produces no match. In every case, FOUND() returns .F. with no error, no warning, and no diagnostic output. The investigation requires reading both the INDEX ON expression and the SEEK() call in the same source review session. Work log entry: “INVOICES.PRG: INDEX ON UPPER(invoice_id) TO INVOICES; SEEK(m_invoice_id) without UPPER(); binary search found no match for mixed-case user input; unfound invoices: 5 → 0 via SEEK(UPPER(m_invoice_id)); 2h.”
Work area collision bugs are the second most common Clipper retainer pattern. CA-Clipper 5.2 applications frequently open multiple DBF files simultaneously in numbered work areas (SELECT 1 / USE INVOICES, SELECT 2 / USE CUSTMAST, SELECT 3 / USE PARTS). When a subroutine or procedure opens a DBF with USE <file> in a work area that was already in use by a different file, the prior file is silently closed and replaced. If the calling routine still holds a reference to that prior file via an alias (INVOICES->invoice_total), Clipper may return stale data from a cached record position, return data from the newly opened file if it happens to have a field with the same name, or produce an “Alias not found” runtime error if the alias has been cleared entirely. In large Clipper applications with many subroutines and procedures, work area assignments are often implicit (SELECT 0 to “find the next available work area” is sometimes used, but the assignment is not always tracked), and a code change in one procedure that adds a USE statement can silently displace a file opened by the caller. The symptom is wrong data appearing in one part of the screen with no reproducible pattern, because the collision only happens when both code paths execute in the same session. Work log entry: “RPT_AGING.PRG: SELECT 2 / USE CUSTMAST opened over an existing INVOICES alias in SELECT 2 opened by calling routine ORDER_RPT.PRG; CUSTMAST->balance field read where INVOICES->invoice_total was expected; wrong balance column on aging report: 12 rows → 0 after assigning RPT_AGING.PRG to SELECT 4 and updating alias references; 3h.”
Deletion flag bugs are the third common Clipper retainer pattern. In CA-Clipper 5.2, the DELETE command marks a record with the deletion flag (byte 0 = asterisk) but does not physically remove it from the DBF file. The record remains physically present and is visible to all navigation when SET DELETED OFF (the default). PACK is the command that physically rewrites the DBF file without the deleted records, compacting the file and reassigning record numbers. Bugs emerge in two common scenarios. First: a batch routine runs with SET DELETED OFF and uses SEEK to find records, then calls DELETE to mark them, then calls PACK at the end to compact the file. If the PACK is called while a SEEK loop is still iterating over record positions, the PACK rewrites the file and invalidates every cached record number — records that were at RECNO() 5, 10, 15 before PACK may now be at RECNO() 4, 8, 12, and the loop positions to wrong records for the remaining iterations, producing double-updates or missed updates. Second: a report program runs with SET DELETED OFF and counts or sums records that include deletion-flagged rows from a prior incomplete PACK, producing totals that include logically deleted data that end users believe has been removed. The correct pattern is to complete all DELETE marks before calling PACK, and to restructure SEEK loops that depend on stable record positions so they collect all target RECNOs into an array before modifying any records. Work log entry: “BATCH_INV.PRG: PACK called inside SEEK/DELETE loop; PACK rewrote file mid-loop; RECNO() references invalidated; 6 inventory records double-updated (deducted twice from on-hand qty); SET DELETED ON added at batch start; PACK moved to after full DELETE pass; double-updated records: 6 → 0; 2.5h.”
Track Clipper developer retainer hours without the status emails
When a 2-hour investigation traces 5 unfound invoices to a SEEK argument not matching the NTX index expression — UPPER(invoice_id) in the index, plain m_invoice_id in the SEEK call — the work log must name the index file, the index expression, the SEEK argument, and the unfound count before and after. HourTab gives your Clipper retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the index expression mismatch and the fix. No client login. No status emails. CSV in, URL out.
How HourTab tracks Clipper retainer hours
Clipper SEEK case-sensitivity bugs are invisible in single-user desktop testing by the same mechanism that makes them dangerous in production. When a developer creates test data for the invoice lookup routine, they type invoice IDs directly into the DBF via APPEND BLANK / REPLACE, typically using uppercase strings (“INV-2025-001”, “INV-2025-002”) because they know the index is built on UPPER(invoice_id). When they test the lookup form, they type the same uppercase IDs into the GET field. SEEK(m_invoice_id) compares “INV-2025-001” (user-typed uppercase) against “INV-2025-001” (index-stored uppercase) and returns .T.. Every test case passes. The developer has no reason to suspect that the SEEK argument needs UPPER() applied, because the test data and test input were both naturally uppercase. The failure only occurs in production when an actual front-desk user — without CAPS LOCK enabled, typing quickly — enters “Inv-2025-042” or “inv-2025-042” into the form. Clipper produces no runtime error, no NTX diagnostic, and no log entry. FOUND() returns .F. silently. The only indication that anything is wrong is the user reporting that the invoice “can’t be found” even though they just entered it an hour ago.
The work log needs to name the mechanism: which program file (INVOICES.PRG), the active index file (INVOICES.NTX), the INDEX ON expression that built it (UPPER(invoice_id)), the SEEK argument as written (SEEK(m_invoice_id)), the mismatch analysis (UPPER not applied to SEEK argument; binary comparison against uppercase index values fails for any mixed-case input), and the before/after count (5 unfound invoices → 0 after SEEK(UPPER(m_invoice_id))). A log entry that says “fixed invoice lookup bug, 2h” is not auditable. A log entry that names the NTX file, the index expression, the unmatched SEEK argument, and the 5 resolved lookups is auditable and defensible. HourTab gives Clipper developers a public retainer-hours URL they send to clients — parts distributors, small manufacturers, and retail operations running CA-Clipper 5.2 inventory and invoicing applications built in the late 1980s and 1990s, maintained today either on DOS under emulation or ported to Harbour on Windows. Comparative context: Clipper SEEK index expression mismatch bugs have structural overlap with adjacent legacy database issues on other xBase platforms. dBASE retainers cover the same DBF file format and deletion-flag model — the same SET DELETED ON/OFF behavior, the same PACK-rewrites-file risk during active loops, and the same character field space-padding that affects SEEK argument width matching. Visual FoxPro retainers cover xBase variable scope bugs in VFP 9 — a language in the same xBase family where CDX index expressions and SEEK() behavior are structurally identical to Clipper’s NTX model, and where the same UPPER()/STR()/DTOC() expression-matching discipline applies to every SEEK call.
FAQ: Clipper developer retainers
What does a Clipper developer on retainer typically do?
A Clipper developer on monthly retainer covers SEEK and index auditing (reviewing all INDEX ON expressions in CA-Clipper 5.2 or Harbour source files and confirming that every SEEK call applies the same transformation to its argument as the index expression applies to the field — UPPER() for character indexes built on UPPER(field), STR() for numeric keys, DTOC() or DTOS() for date keys); work area management (reviewing USE / SELECT work area assignments to confirm that alias references in multi-area programs point to open, correctly populated work areas); DBF and NTX backup and recovery (auditing DBF file integrity, diagnosing memo field .DBT linkage corruption, and guiding PACK and ZAP operations); memo field issues (diagnosing MEMOEDIT() display problems, memo field truncation, and .DBT pointer mismatches after incomplete PACK operations); and Harbour migration assessment (reviewing CA-Clipper 5.2 source for compatibility with Harbour and identifying which library calls or third-party components require replacement for a Harbour/MiniGUI or Harbour/FiveWin port).
What Clipper SEEK work is most commonly underlogged?
SEEK case-sensitivity mismatch bugs are the most systematically underlogged Clipper retainer work. The pattern: a developer creates an NTX index with INDEX ON UPPER(invoice_id) TO INVOICES, storing all index values in uppercase, but writes the SEEK call as SEEK(m_invoice_id) without applying UPPER() to the GET/READ variable. In single-user desktop testing, the developer types uppercase IDs directly, all SEEKs succeed, and FOUND() returns .T. on every test case. In production, users without CAPS LOCK type mixed-case IDs and SEEK performs a binary search that finds no match against the uppercase index values. FOUND() returns .F. with no runtime error, no NTX warning, and no diagnostic output. The same pattern applies to numeric fields (STR(part_number, 8) index requires SEEK(STR(m_part_no, 8))) and date fields (DTOC(order_date) index requires SEEK(DTOC(m_order_date))). The fix is always to apply to the SEEK argument the identical transformation that the index expression applies to the field value.
What are typical Clipper developer retainer rates?
Entry-level Clipper developers with experience in basic CA-Clipper 5.2 or Harbour syntax, DBF/NTX file operations, and standard GET/READ form input typically bill at $60 to $110 per hour. Mid-level Clipper programmers with experience in multi-work-area programs, complex NTX index management, SET FILTER TO optimization, memo field handling, and Harbour/MiniGUI or FiveWin GUI migration typically bill at $90 to $160 per hour. Senior Clipper developers with deep knowledge of Clipper internals (NTX B-tree structure, DBF header layout, codeblock execution with EVAL(), AEVAL()/ASORT() array processing, and Harbour migration project management) typically bill at $130 to $240 per hour. Monthly retainer ranges: $1,500 to $2,800 per month for advisory engagements (12 to 20 hours per month); $2,000 to $4,500 per month for active maintenance. Clipper retainer rates reflect a severely constrained talent pool; CA-Clipper 5.2 and Harbour expertise is among the rarest legacy application skills available.
What should a Clipper developer retainer agreement include?
A Clipper developer retainer agreement should specify: Clipper version (CA-Clipper 5.2 for DOS versus Harbour for Windows or Linux — Harbour is source-compatible with Clipper 5.2 but has significant differences in memory management, multi-threading, and GUI support); target operating system (DOS under DOSBox or a dedicated DOS machine versus Windows via FiveWin or MiniGUI); NTX versus CDX index format compatibility (CA-Clipper 5.2 uses NTX; FoxPro uses CDX; Harbour supports both via DBFNTX and DBFCDX drivers — if the retainer involves migration or interoperability with Visual FoxPro, the index format and SEEK behavior differences must be scoped); DBF character encoding (CP437 for English-only DOS applications versus CP850 for Western European characters — international character sets sort and compare differently under UPPER() across code pages, producing SEEK mismatches for accented characters); and Harbour migration scope (whether the retainer covers a full Harbour port including GUI layer replacement with MiniGUI or FiveWin, or only CA-Clipper 5.2 maintenance on the existing DOS platform).
How should Clipper developer retainer hours be logged?
Log each Clipper retainer session with the specific index and SEEK details. For SEEK case-sensitivity mismatch bugs: program name (INVOICES.PRG), index file (INVOICES.NTX), INDEX ON expression (INDEX ON UPPER(invoice_id) TO INVOICES), SEEK argument as written (SEEK(m_invoice_id)), mismatch type (UPPER not applied to SEEK argument), unfound count before fix (5 invoices unfound), unfound count after fix (0 via SEEK(UPPER(m_invoice_id))), hours (2h). For work area collision bugs: program name, work area number, alias involved, error type (Alias not found / wrong data from stale open file), fix (explicit DBCLOSEALL() or USE file ALIAS in correct area), hours. For deletion flag bugs: program name, SET DELETED state (ON or OFF), operation that exposed the bug (PACK inside SEEK loop invalidated cached RECNO()), records affected, fix (restructure loop to collect RECNOs before PACK, or PACK separately after all DELETEs), hours.