Blog › ICP guides
Paradox developer on retainer: LOCKRECORD lock hold, Borland Paradox, Corel Paradox, ObjectPAL on monthly retainer
October 8, 2026 · ~15 min read
A Paradox developer was maintaining a legacy customer account management application built in Corel Paradox 7 for a regional insurance broker. The application ran on a shared Windows network with eight workstations accessing the same Paradox tables in a shared network directory. A credit-limit update form allowed back-office staff to open a customer record, review the current credit balance and credit limit, and optionally adjust the limit field. The developer had implemented the form using an ObjectPAL pushButton method on a “Save Changes” button: the method called LOCKRECORD to acquire an exclusive record lock before beginning the update, read the current balance and credit-limit fields, applied a business-rule validation (the new limit must be at least 10 percent above the current balance), and on the passing branch called POSTRECORD to write the change and release the lock. The developer had tested the method against a set of test records where all entries passed the validation. In production, two accounts were flagged that failed the validation check — the new limit submitted was below the required minimum. The method showed a MSGBOX alert telling the operator to enter a higher limit, then called table.close() to cancel the edit and return the form to its default state. Both those accounts were then unavailable to all other workstations for the remainder of the day: any other operator who tried to open either account encountered “Record locked by another user” and could not edit the record. Concurrent record-lock blocks: 2 → 0 after adding UNLOCKRECORD before the table.close() call on the validation-failure branch.
The root cause was a missing UNLOCKRECORD call on the failure branch of the pushButton method. In Paradox (versions 4.5 for Windows through Corel Paradox 9), LOCKRECORD acquires a process-level exclusive write lock on the current record in a table. The lock is held until one of three things happens: UNLOCKRECORD explicitly releases it; POSTRECORD writes the record buffer and releases the lock; or the Paradox session ends (closing the Paradox application on the workstation releases all locks held by that session). Calling table.close(), endEdit(), doDefault, or navigating to a different record with MOVETO does not release a lock acquired by LOCKRECORD — the lock is strictly process-level and must be released explicitly. In the credit-limit form, the developer had correctly called POSTRECORD on the success path (which writes the change and releases the lock). On the failure path, the developer called table.close() under the assumption that closing the edit context would release the lock — but Paradox does not do this. The two locked records remained locked for the rest of the workstation’s Paradox session, blocking all other operators who tried to access those accounts. The fix was to add UNLOCKRECORD immediately before table.close() on the failure branch, and also before any other early-exit code path in the method (including the RETURN in the parameter-validation block at the top of the method, which could also exit before POSTRECORD was reached).
The Paradox multi-user coordination model depends on two mechanisms: process-level record locks (managed by the Paradox application on each workstation) and the Paradox Network Control File (the .NET file in the shared network directory). Every workstation running Paradox in a shared network directory must be able to read and write the .NET file; Paradox uses it to register which workstations are active, to coordinate record-level lock arbitration across processes, and to track the “lock count” for each table. When a workstation calls LOCKRECORD, Paradox registers the lock in the .NET file and in the workstation’s local Paradox process memory. Other workstations polling for lock availability consult the .NET file to determine whether a record is currently locked by another session. If a workstation crashes or exits Paradox abnormally (power failure, process kill), the locks registered in the .NET file by that workstation remain in the file because the cleanup code that would have called UNLOCKRECORD (or terminated the session gracefully) never ran. This leaves “stale” lock entries in the .NET file that block all other workstations indefinitely — a common Paradox production incident that requires all Paradox sessions to close, the .NET file to be deleted from the shared directory (Paradox recreates it automatically when the first workstation reconnects), and all workstations to reopen Paradox. A Paradox developer on retainer regularly performs this .NET file recovery procedure and documents the workstation shutdown discipline required to minimize crash-induced stale locks.
Paradox distinguishes between LOCKRECORD (an exclusive write lock on a single record, preventing all other sessions from calling LOCKRECORD on the same record) and LOCK TABLE (a table-level write lock that prevents all other sessions from opening the table in write mode). A third lock type is the SHARE LOCK (implicit when a workstation opens a table in read-only mode), which prevents another session from acquiring a full write lock on the table but allows multiple read-only sessions to coexist. The typical ObjectPAL update pattern acquires a LOCKRECORD rather than a LOCK TABLE because record-level locking allows multiple operators to edit different records in the same table simultaneously — a table-level lock would serialize all edits across the entire table, blocking all other workstations from any write operation for the duration of every single edit. In the credit-limit form scenario, using LOCK TABLE instead of LOCKRECORD would have made the problem worse: two table-level locks (one per failed validation) would have blocked all other workstations from opening the entire customer table, not just two specific records. The correct design is LOCKRECORD for fine-grained exclusive access, with a guaranteed UNLOCKRECORD on every exit path.
Paradox, the ObjectPAL event model, and the .NET file
Ansa Software developed the original Paradox in 1985 for DOS as a relational database with a forms-and-query interface notable for its “query by example” (QBE) feature: users could query tables by typing example values directly into a query grid, without writing SQL. Paradox for DOS used PAL (Paradox Application Language), a procedural scripting language for automating forms, queries, and batch processing. Borland acquired Ansa and Paradox in 1987 and continued DOS development through Paradox 4.5 for DOS, which became a widely-used small-business database platform. Borland released Paradox for Windows 4.5 in the early 1990s, introducing ObjectPAL — a new event-driven object-oriented scripting language replacing PAL for Windows development. ObjectPAL adopted a message-passing event model in which every visual object on a form (buttons, table views, fields, form controls) had a hierarchy of event methods: open, close, mouseClick, pushButton, action, canDepart, arrive, and others. Borland sold Paradox to Corel Corporation in 1996 as part of the Corel WordPerfect Office suite; Corel released Paradox 7, 8, and 9 (the final Windows version). Corel Paradox 9 was bundled with WordPerfect Office X3 and later versions. Corel ended active development of Paradox for Windows in the 2010s; the final publicly available Windows-compatible version is Paradox 9 (with some compatibility workarounds for Windows 10).
The Paradox file format system stores each table as a set of files in the same directory: a .DB file (the table data — fixed-width record slots in a proprietary format, not DBF-compatible); a .PX primary index file (a B-tree index on the table’s primary key fields, required for LOCKRECORD to work — a table without a primary key uses a memo-style sequential lock instead); one or more .X## secondary index files (B-tree indexes on non-primary key fields, numbered sequentially as .X01, .X02, etc.); optional .MB memo data files (for Memo and Formatted Memo field types, which store variable-length text outside the fixed-width record structure); optional .VAL validity check files (defining field-level constraints such as minimum/maximum values, lookup tables, required fields, and picture patterns); and the .NET Network Control File in the shared directory root (a single file shared by all workstations accessing the same network directory, containing the workstation-lock registry used by all Paradox multi-user coordination). The Paradox primary key is defined during table creation in the Table Structure dialog (or via PAL/ObjectPAL): primary key fields are the first N fields defined as key fields (marked with an asterisk in the Table Structure grid). Primary key values must be unique; Paradox enforces uniqueness via the .PX B-tree at POSTRECORD / POST time. The .PX file is required for LOCKRECORD to function: Paradox stores the record lock registration as an entry in the .PX B-tree, tagged with the locking workstation’s identity from the .NET file. A table without a primary key cannot use LOCKRECORD — the developer must use LOCK TABLE for write isolation on keyless tables.
The ObjectPAL event model in Paradox for Windows introduces several subtle lock-management failure modes beyond the straightforward LOCKRECORD-without-UNLOCKRECORD pattern. The canDepart event method fires before Paradox moves focus away from a TableView or record (when the user presses Tab, clicks a different record, or navigates via keyboard); if an ObjectPAL developer overrides canDepart to perform field-level validation and returns FALSE (to prevent departure), a LOCKRECORD called earlier in the same editing sequence remains held because the record departure was prevented. The action event method fires for a wide range of user interface actions (data entry, deletion, navigation); a developer who intercepts action(DataPostRecord) to add custom post-record logic must be careful to call UNLOCKRECORD if the custom logic decides to abort the post, because the DataPostRecord action that would have called the built-in POSTRECORD (which releases the lock) is being intercepted. A developer who calls doDefault in an action override will trigger the default Paradox behavior for the action (including default lock release in DataPostRecord), but a developer who calls RETURN FALSE to suppress the default behavior must manage the lock explicitly. The pushButton method on a Save button is the most common place where developers add custom business logic around record edits, and the most common place where lock releases are missed on the failure and error branches.
PAL (Paradox Application Language), the DOS-era scripting language, uses a different but structurally similar locking model. PAL scripts use LOCK tablename WRITE to acquire a write lock (table-level in PAL, not record-level) and UNLOCK tablename to release it. PAL also has LOCKRECORD in certain contexts, with the same release requirement as ObjectPAL. Legacy applications that were developed originally in Paradox for DOS with PAL and then partially ported to Paradox for Windows may contain mixed PAL and ObjectPAL code; in those applications, lock management patterns from both languages coexist, and a retainer developer must audit both PAL script files (typically .SC source files and compiled .SC2 files) and ObjectPAL methods (embedded in Paradox form files, .FSL source and .FDL design files) for lock-hold bugs.
Typical Paradox retainer work and what it looks like in a work log
LOCKRECORD without UNLOCKRECORD on error branches is the canonical Paradox invisible production lock. The pattern is consistent: an ObjectPAL pushButton method (or another method that initiates a record edit) calls LOCKRECORD near the start of the method body to acquire exclusive write access before reading and potentially modifying a record. The developer tests the method against records where the business-rule validation always passes; on the passing branch, POSTRECORD is called, which writes the record and releases the lock. In production, a subset of records hits the failure branch — the validation condition is false, the method shows a MSGBOX and calls table.close() or simply exits the method without calling either UNLOCKRECORD or POSTRECORD. The LOCKRECORD lock on those records remains held for the duration of the locking workstation’s Paradox session. Any other workstation that tries to LOCKRECORD the same record receives “Record locked by another user” and cannot proceed. The original operator sees no error; they saw the MSGBOX, clicked OK, and resumed normal work, unaware that two records are now locked to all other operators for the rest of the day. The developer examining the incident will find that the locking workstation shows no error in any log, because Paradox does not generate a log of unreleased locks. Work log: “CUSTOMER.FSL / pushButton: LOCKRECORD acquired; validation failure (new_limit < balance * 1.1); table.close() called without UNLOCKRECORD; 2 customer records locked until session end; fix: UNLOCKRECORD added before table.close() on failure branch AND before top-level parameter RETURN guard; locked records: 2 → 0; 1.5h.”
Paradox .NET file corruption after workstation crash is the second most common Paradox retainer pattern. When a workstation exits Paradox abnormally — power failure, process kill from Task Manager, hard reboot — the cleanup code that calls UNLOCKRECORD for all active locks and that deregisters the workstation from the .NET file never runs. The .NET file in the shared network directory retains the stale lock entries and the stale workstation registration for the crashed session. Other workstations that subsequently try to LOCKRECORD any of the records that were locked by the crashed session receive “Record locked by another user” indefinitely — the crashed workstation will never come back to release the locks, and Paradox has no lock-timeout mechanism. The recovery procedure requires all currently running Paradox sessions to close Paradox cleanly (so that no legitimate lock entries remain in the .NET file), then an administrator to delete the .NET file from the shared network directory (Paradox recreates it automatically when the first workstation reconnects), then all workstations to reopen Paradox and reconnect to the shared directory. If any workstation fails to close Paradox before the .NET file is deleted, that workstation’s existing session loses its .NET coordination and subsequent lock operations on that workstation may fail or produce unpredictable results. Retainer work log: “\\server\paradox-data\.NET: stale entries for WS-ACCTG-04 after power failure; all sessions closed; .NET deleted and recreated; stale lock messages: 8 → 0; recovery procedure documented in network-admin-runbook.txt; 2h.”
Primary index (.PX) corruption and LOCATE / FINDREC failures is the third common Paradox retainer pattern. After a .NET file corruption recovery, an abnormal exit during a POSTRECORD that was mid-index-update, or a network-level write interruption during the .PX B-tree update, the .PX file may contain partially-written index entries: some B-tree nodes have been updated with the new record’s primary key, but the root or parent nodes still point to the pre-update structure. The symptom is a LOCATE or FINDREC call in ObjectPAL or PAL that returns a wrong record (a record with a different primary key than requested), returns “record not found” for a primary key value that visibly exists in the .DB data file, or causes a Paradox error code 91 (“Key violation on primary index”) when attempting to insert a record whose primary key does not conflict with any existing data record. The fix is to perform a Paradox table Restructure (open the table in the Restructure dialog, make no changes, and save — Paradox automatically rebuilds the .PX primary index from the .DB data file during a Restructure save). For secondary index corruption (in .X## files), the same Restructure procedure rebuilds all secondary indexes as well. Work log: “CUSTOMER.PX: B-tree inconsistency after abnormal exit; LOCATE [Customer_ID] = “C00437” returned wrong record; Paradox Restructure (no field changes) rebuilt .PX from .DB; LOCATE now returns correct record; record count verified against backup: 4,812 → 4,812 (no data loss); 1.5h.”
Track Paradox developer retainer hours without the status emails
When a 1.5-hour investigation traces 2 day-long record-lock blocks to a missing UNLOCKRECORD before the table.close() call on a validation-failure branch — Paradox’s LOCKRECORD acquires a process-level exclusive lock that persists until UNLOCKRECORD or POSTRECORD is explicitly called — the work log must name the form, the method, the lock-acquisition line, the exit path that bypassed the release, and the concurrent-block count before and after. HourTab gives your Paradox retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the ObjectPAL method and the lock semantics. No client login. No status emails. CSV in, URL out.
How HourTab tracks Paradox retainer hours
Paradox LOCKRECORD-without-UNLOCKRECORD bugs are invisible by the same mechanism that makes them operationally severe: the LOCKRECORD call returns successfully with no error or warning; the ObjectPAL method that acquired the lock shows the user a MSGBOX and exits normally; the Paradox session on the locking workstation continues operating without issue. Nothing in the Paradox runtime output, in the Windows application event log, or in any Paradox error file indicates that a process-level record lock was acquired and not released. The symptom only appears on a different workstation: an operator trying to open an account record for editing sees “Record locked by another user” and cannot proceed. The locking workstation shows no error; the lock-holding Paradox session is likely now idle or the operator is working on other tasks. Without a structured work log that records each LOCKRECORD acquisition and its corresponding release (or the absence of a release on a specific code path), reconstructing the incident requires reading every ObjectPAL method in every form that touches the locked table — a labor-intensive process in a large Paradox application with dozens of forms.
The work log needs to name the form (CUSTOMER.FSL), the method (pushButton on the “Save Changes” button), the lock acquisition (LOCKRECORD at line N), the code path that bypassed the release (the validation_failed branch that called MSGBOX and table.close() without UNLOCKRECORD), the number of records locked (2 customer accounts), the concurrent-block count before and after (2 → 0), and whether UNLOCKRECORD or POSTRECORD was used as the release mechanism on the fix path. A log entry that says “fixed locking issue in customer form, 1.5h” is not auditable. A log entry that names the CUSTOMER.FSL / pushButton method, the new_limit < balance * 1.1 failure branch, the 2 locked accounts, and the UNLOCKRECORD-before-table.close() fix is auditable and defensible to the insurance broker client who lost access to two accounts for a full workday. HourTab gives Paradox developers a public retainer-hours URL they send to clients — regional brokers, manufacturers, law firms, and county government offices that built Paradox 7/8/9 applications in the late 1990s and early 2000s for customer account management, case tracking, inventory control, and contract management, maintained today by the original developer or a successor retainer consultant.
Comparative context: Paradox LOCKRECORD lock-hold bugs have structural parallels with record-lock hold patterns in other desktop database platforms. dBASE retainers cover the FLOCK() / RLOCK() / UNLOCK() pattern in dBASE IV and dBASE Plus where file and record locks acquired for batch updates are not released on error branches or LOOP continuations. FoxPro retainers cover the FLOCK() / RLOCK() / UNLOCK() pattern in Visual FoxPro where similar file-server-level locks can be held invisibly across a workday if application code does not explicitly release them on every exit path. Advantage Database Server retainers cover the AdsTable.Edit / AdsTable.Cancel pattern in Delphi applications where the Edit call acquires an implicit exclusive record lock that must be explicitly cancelled on every non-post exit path.
FAQ: Paradox developer retainers
What does a Paradox developer on retainer typically do?
A Paradox developer on monthly retainer covers LOCKRECORD lock-hold audits for all ObjectPAL methods that use the LOCKRECORD / UNLOCKRECORD / POSTRECORD pattern (reviewing every LOCKRECORD call in the application to confirm that every error branch, validation-failure branch, early-RETURN branch, and canDepart FALSE branch calls UNLOCKRECORD before leaving the lock scope); Paradox .NET file diagnostics and recovery after workstation crashes; ObjectPAL event-model debugging including action, canDepart, and pushButton method lock interactions; Paradox table Restructure and primary/secondary index repair for .PX and .X## file corruption; and Paradox-to-modern-stack migration assessment (evaluating path from Corel Paradox 7/8/9 tables and ObjectPAL to Microsoft Access, SQL Server, PostgreSQL, or modern web frameworks).
What Paradox LOCKRECORD work is most commonly underlogged?
ObjectPAL methods that call LOCKRECORD, perform field-level edits, and then exit through a validation-failure or error branch without calling UNLOCKRECORD are the most systematically underlogged Paradox retainer work. The failure mode: developer calls LOCKRECORD, validates the proposed change, calls MSGBOX on failure, calls table.close() to cancel the edit — under the assumption that closing the table releases the lock. Paradox does not release LOCKRECORD locks on table.close(). The lock persists for the session lifetime. No error is raised in the locking session. The locked record is invisible to all other workstations until the locking user closes Paradox. The developer must know the rule independently: every LOCKRECORD that does not lead to POSTRECORD must be followed by UNLOCKRECORD on every possible code path, including all conditional branches, error branches, and early-return guards.
What are typical Paradox developer retainer rates?
Entry-level Paradox developers with experience in basic ObjectPAL event programming, Paradox table management, LOCKRECORD / UNLOCKRECORD record handling, and standard form and report development typically bill at $55 to $100 per hour. Mid-level Paradox programmers with experience in complex ObjectPAL inheritance hierarchies, Paradox .NET file multi-user coordination, PAL script automation, and Paradox-to-SQL migration planning typically bill at $85 to $150 per hour. Senior Paradox developers with deep knowledge of Paradox file internals (.DB / .PX / .X## structures, .NET file coordination, ObjectPAL OOP event hierarchy), migration execution experience (Paradox 5/7/9 to Access, SQL Server, or modern web), and production incident forensics expertise typically bill at $125 to $225 per hour. Monthly retainer ranges: $1,300 to $2,400 per month for advisory engagements (10 to 18 hours per month); $1,800 to $3,800 per month for active maintenance including ObjectPAL bug fixes and migration work.
What should a Paradox developer retainer agreement include?
A Paradox developer retainer agreement should specify: Paradox version (Paradox for DOS 3.5/4.5 with PAL vs Paradox for Windows 4.5/5.0 vs Corel Paradox 7/8/9 with ObjectPAL — each version has significant differences in scripting language, form model, and multi-user coordination behavior); whether the application uses ObjectPAL (event-driven, Windows), PAL (procedural, DOS), or a mixture of both; network infrastructure scope (whether the retainer covers .NET file corruption recovery and coordination with the network administrator for shared directory permissions); Paradox-to-modern-stack migration assessment (migration path options from Corel Paradox to Access, SQL Server, PostgreSQL, or modern web frameworks); and table Restructure and index maintenance scope (whether retainer includes running Paradox Restructure operations, rebuilding .PX and .X## index files, and managing table validity checks).
How should Paradox developer retainer hours be logged?
Log each Paradox retainer session with the ObjectPAL method name and lock-management outcome. For LOCKRECORD lock-hold bugs: form and method name (CUSTOMER.FSL / pushButton), the lock-acquisition call (LOCKRECORD), the exit path that bypassed UNLOCKRECORD (validation-failure branch calling table.close()), the number of records locked (2 customer accounts), the concurrent-block count before and after fix (2 → 0), the fix (UNLOCKRECORD added before table.close() on failure branch), and hours (1.5h). For .NET file corruption: shared directory path, symptom (“Record locked by another user” on all records), fix (delete .NET after all sessions closed, recreated on reconnect), workstation shutdown procedure documented, hours. For .PX index corruption: table name, symptom (LOCATE returned wrong record), fix (Paradox Restructure rebuilt .PX from .DB), record-count verification, hours.