Blog › ICP guides
PowerBuilder developer on retainer: DataWindow buffer model, PowerScript, Sybase PowerBuilder, and SAP PowerBuilder on monthly retainer
October 7, 2026 · ~14 min read
A PowerBuilder developer was maintaining an enterprise client-server inventory management application written in PowerScript for a manufacturing company. The application used a DataWindow control — dw_main on window w_inventory_list — to display and edit inventory records. Users could switch between viewing all inventory items and viewing only items below reorder threshold by clicking filter buttons. Each filter button called dw_main.SetSQLSelect(new_sql) to swap the DataWindow’s retrieval SQL for the new filter condition, then called dw_main.Retrieve() to fetch the matching records. Users could also delete selected rows using a Delete button that called dw_main.DeleteRow(dw_main.GetRow()) and then dw_main.Update() to commit the deletion. After a user navigated from the “all items” view to the “below reorder” view and back, the Delete button on a subsequent filter switch was deleting 3 records that the user had not selected for deletion.
The root cause was the DataWindow delete buffer. When a user selected a row in the “all items” view and deleted it via DeleteRow(), PowerBuilder moved that row from the DataWindow’s primary buffer to its delete buffer and called Update(), which sent the DELETE SQL to the database. The deletion succeeded. However, the row remained in the delete buffer after Update() completed — PowerBuilder does not automatically clear the delete buffer after a successful Update(). When the user then clicked a filter button, the code called SetSQLSelect(new_sql) followed by Retrieve(). SetSQLSelect modifies the retrieval SQL but does not call Reset() — it does not clear any of the DataWindow’s four buffers (primary, filter, delete, or original). Retrieve() repopulates the primary buffer with the new result set, but it also does not clear the delete buffer. The delete buffer retained the 3 rows from the prior DeleteRow() operations. When Update() was called later (by a subsequent delete operation or by a Save button), PowerBuilder submitted DELETE statements for all rows currently in the delete buffer — including the 3 stale rows that had already been deleted from the database in the prior session. The database executed those DELETE statements against records that were currently visible in the primary buffer (new rows fetched by the latest Retrieve), resulting in 3 wrong record deletions.
The fix was to call dw_main.Reset() before SetSQLSelect(new_sql) in every filter button click event handler. Reset() clears all four DataWindow buffers — primary, filter, delete, and original — returning the DataWindow to an empty state as if it had just been created. After Reset(), the delete buffer contains no rows. The subsequent SetSQLSelect + Retrieve sequence fetches only the new result set into the primary buffer, and any subsequent Update() call has no delete-buffer rows to submit. Wrong record deletions: 3 → 0 after adding Reset() before every SetSQLSelect call. The investigation — identifying the 3 incorrectly deleted records, tracing the DELETE statements to the delete buffer via SQL Server Profiler, understanding the DataWindow buffer lifecycle, confirming that ResetUpdate() (which the developer had initially tried) does not clear the delete buffer, and adding Reset() to all filter button handlers — took 3.5 hours.
PowerBuilder, DataWindow architecture, and the four-buffer model
PowerSoft Corporation developed PowerBuilder in 1992 as a rapid application development platform for client-server database applications. SAP acquired Sybase (which had acquired PowerSoft) in 2010 and continues to develop PowerBuilder; the current releases are PowerBuilder 2019 R3 and PowerBuilder 2022. The platform consists of the PowerScript language (an event-driven, object-oriented language with reference types, window inheritance, and a message-passing event model), the DataWindow object (a declarative data-aware control that handles its own SQL generation, retrieval, row buffering, and database synchronization), the PowerBuilder IDE (with visual painters for windows, DataWindows, menus, and user objects), and Appeon for Mobile and Appeon Web for deploying PowerScript applications to mobile and web targets without a full rewrite. PowerBuilder applications are built from PBL library files (PowerBuilder Library) containing compiled PowerBuilder objects. The PBK project file specifies which PBL files belong to the application and their search order. Runtime deployment uses PBD files (PowerBuilder Dynamic Library) or the full PBL set. PowerBuilder communicates with relational databases through a Transaction object — most commonly SQLCA (SQL Communications Area), the global default Transaction object — configured with database-specific DBMS, ServerName, DBName, LogID, LogPass, and DBParm properties. DataWindows use the Transaction object at Retrieve and Update time; embedded SQL in PowerScript uses it for all direct SQL operations.
The DataWindow is the most powerful and most complex PowerBuilder construct. A DataWindow object has a presentation style (tabular, freeform, grid, label, N-up, crosstab, OLE 2.0) and a data source (SQL SELECT, Quick Select, external, stored procedure, or web service). At runtime, a DataWindow control placed on a window is bound to a DataWindow object and a Transaction object. The DataWindow maintains four internal row buffers, and understanding all four is essential for correct PowerBuilder retainer work. The primary buffer holds all rows currently visible in the DataWindow control — rows returned by the most recent Retrieve() call that have not been filtered out by SetFilter(). The filter buffer holds rows that were in the primary buffer but were moved to the filter buffer by a SetFilter() call that did not match them; they are hidden from display but not deleted from the DataWindow. The delete buffer holds rows that were in the primary buffer but were moved to the delete buffer by DeleteRow(); they are hidden from display and will be submitted as DELETE statements the next time Update() is called. The original buffer holds the original database-retrieved values for rows currently in the primary buffer; it is used by Update() to generate the WHERE clause for UPDATE statements (optimistic concurrency: the generated UPDATE includes WHERE conditions matching original values to detect concurrent modifications). Reset() clears all four buffers simultaneously. RowsDiscard() clears the primary and filter buffers but not the delete buffer. ResetUpdate() clears row status flags in the primary buffer (resetting DataModified! and NewModified! rows to NotModified!) but does not clear the delete buffer or remove any rows from it.
The Update() method iterates all four row types to generate the SQL batch that is submitted to the database. For each row in the primary buffer with status DataModified! or NewModified!, it generates an UPDATE or INSERT. For each row in the delete buffer, it generates a DELETE. The critical property is that Update() processes the delete buffer regardless of what the primary buffer currently contains — there is no consistency check between the delete buffer rows and the rows currently in the primary buffer. If the delete buffer was populated by a DeleteRow() on a prior Retrieve result set, and the primary buffer now contains a completely different result set (after SetSQLSelect + Retrieve), Update() will still submit DELETE statements for the old delete buffer rows. Those DELETE statements may or may not affect the same rows that are currently visible — in the scenario described above, the primary keys in the delete buffer matched rows that had been re-fetched by the new Retrieve (because the user had undone the deletion at the database level, or the records had been inserted again by another user or batch process), causing unexpected deletions of currently visible records.
Row status tracking via GetItemStatus() and SetItemStatus() is the mechanism PowerScript uses to manually control which rows Update() acts on. GetItemStatus(row, 0, primary!) returns one of four dwItemStatus values: NotModified! (row is unchanged since Retrieve; Update() will not generate SQL for it); DataModified! (one or more columns were changed via SetItem() or user edit; Update() will generate UPDATE); New! (row was added by InsertRow() but no columns have been set; Update() will generate INSERT); NewModified! (row was added by InsertRow() and one or more columns were set; Update() will generate INSERT). Calling SetItemStatus(row, 0, primary!, NotModified!) prevents Update() from generating SQL for a row even if columns have been changed — it is used to suppress specific rows from the update batch, but it does not remove the row from the delete buffer if the row was deleted. PowerScript developers who confuse ResetUpdate() with Reset() often discover this distinction only after a production incident: ResetUpdate() resets the primary-buffer row status flags (preventing re-submission of already-saved changes on a second Update() call) but leaves the delete buffer intact for the next Update() call.
PowerBuilder developer retainer rates span a wide range by experience level. Entry-level PowerBuilder developers typically bill at $70–$125 per hour. Mid-level PowerBuilder programmers with DataWindow performance knowledge, PowerScript inheritance, and SQLCA configuration experience typically bill at $105–$185 per hour. Senior PowerBuilder developers with deep DataWindow buffer model expertise, Appeon migration experience, and enterprise client-server modernization capability typically bill at $150–$275 per hour. Monthly retainer engagements range from $1,800–$3,200/mo for advisory (15–22 hours) to $2,500–$5,800/mo for active maintenance.
Typical PowerBuilder retainer work and what it looks like in a work log
The DataWindow delete-buffer stale-row bug is the most invisible category of PowerBuilder retainer work. The opening scenario is representative: a SetSQLSelect + Retrieve sequence that does not call Reset() first. In test environments, the test sequence typically starts with a freshly opened window, so the DataWindow begins with empty buffers. Test scripts exercise the delete button on the visible rows, verify the deletion, and close the window. This tests the DeleteRow() + Update() sequence correctly but never exercises the path where SetSQLSelect is called while delete-buffer rows already exist. The production user navigates between filter views repeatedly in a single session, accumulating delete-buffer rows across each filter switch. The symptom — wrong deletions — appears only after a specific sequence of filter navigations and delete operations that the test suite did not cover. Work log entry: “w_inventory_list / dw_main: SetSQLSelect(new_sql) called without Reset(); delete buffer retained 3 rows from prior DeleteRow sequence; Update() sent DELETE for those 3 rows during subsequent filter switch; wrong deletions: 3 → 0 via Reset() before SetSQLSelect in all filter button Clicked handlers; SQL Server Profiler trace confirmed DELETE statements; 3.5h.”
The RetrieveAsNeeded lazy-load buffer confusion is the second most common PowerBuilder retainer pattern. A DataWindow configured with RetrieveAsNeeded = ‘Yes’ fetches rows in pages (default 20 rows per page) as the user scrolls. The primary buffer is populated incrementally: only the rows on the current visible page and previously scrolled pages are in the primary buffer; rows not yet scrolled to are not retrieved. When Update() is called, it acts only on the rows currently in the primary buffer — rows in pages not yet retrieved by RetrieveAsNeeded are not in any buffer and are not included in the update batch. A developer who calls Update() assuming that the entire result set has been retrieved (as it would be without RetrieveAsNeeded) may see partial updates: the first 20 rows saved, the remaining rows in the same logical group not saved, with no error returned by Update(). The fix is either to call Retrieve() without RetrieveAsNeeded for DataWindows that require full-set updates, or to scroll programmatically to the last row before Update() to ensure all pages are loaded. Work log entry: “dw_orders / RetrieveAsNeeded enabled; Update() called after user reviewed first page; 47 of 63 order status changes not saved (rows on pages 4–4 not yet retrieved); fix: disabled RetrieveAsNeeded for this DataWindow, full Retrieve() before Save button enabled; unsaved rows: 47 → 0; 2h.”
Inheritance event chain gaps are the third common PowerBuilder retainer pattern. PowerBuilder window and user object inheritance allows a descendant window to inherit PowerScript event handlers from an ancestor. Each inherited event handler has an Extend! or Override! setting in the PowerBuilder IDE. With Extend!, the descendant’s handler code runs after the ancestor’s handler code. With Override!, only the descendant’s handler code runs — the ancestor’s code is silently bypassed. A common retainer scenario: a developer creates a descendant window and adds a RowFocusChanged event handler with Override! to customize row-focus behavior. The ancestor’s RowFocusChanged handler contained validation logic that updated a status bar and enabled or disabled Save and Delete buttons based on the selected row’s editability. With Override!, that ancestor validation code no longer runs in the descendant, so the status bar is never updated and the Save button is always enabled regardless of whether the selected row is editable. Three data entry errors occurred because users could click Save on read-only rows. Fix: changed RowFocusChanged from Override! to Extend! and moved the descendant’s customization to run after the ancestor’s validation. Work log entry: “w_order_entry_detail inherits w_order_entry; RowFocusChanged Override! bypassed ancestor validation (status bar + button state); 3 save attempts on read-only rows; changed to Extend!; wrong saves: 3 → 0; 1.5h.”
Track PowerBuilder developer retainer hours without the status emails
When a 3.5-hour investigation traces 3 wrong record deletions to a stale DataWindow delete buffer — Reset() was missing before SetSQLSelect, leaving prior DeleteRow rows to be submitted as unexpected DELETE statements by the next Update() call — the work log must name the window, the DataWindow control, the buffer sequence, and the wrong-deletion count before and after the fix. HourTab gives your PowerBuilder retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the DataWindow control, the buffer state sequence, and the fix. No client login. No status emails. CSV in, URL out.
See HourTab pricing →How HourTab tracks PowerBuilder retainer hours
PowerBuilder DataWindow retainer work is invisible by the same mechanism that makes the delete-buffer bug dangerous: in development and test environments, DataWindows are typically created fresh per test run, so the delete buffer always starts empty. The developer verified that DeleteRow() + Update() correctly deleted the selected row; that the filter button correctly switched the DataWindow to a new result set via SetSQLSelect + Retrieve(); and that Update() correctly saved changes made to the new result set. Each of these individual behaviors tested correctly in isolation. The compound failure — delete buffer accumulation across filter switches in a single session — only appeared in production when users worked through repeated filter navigations without closing the window. There is no DataWindow exception, no PowerScript runtime error, and no application-visible error code from Update() (it returns 1 for success even when it executes unintended DELETE statements). The developer must reconstruct the buffer state sequence from the PowerScript event chain, the SQL trace from the database server, and knowledge of the four-buffer model to identify that Reset() was the missing call.
The work log needs to name the mechanism: which window and DataWindow control, the event sequence (filter button Clicked handler: SetSQLSelect without prior Reset()), the buffer state at the time of the wrong Update() (delete buffer: 3 rows from prior session), the DELETE statements confirmed via SQL Server Profiler, and the wrong-deletion count before and after (3 → 0). A log entry that says “fixed DataWindow delete bug, 3.5h” is not auditable. A log entry that names the window, the DataWindow control, the missing Reset() call, the four-buffer model behavior, the SQL trace confirmation, and the 3 wrong deletions that were prevented is auditable and defensible. HourTab gives PowerBuilder developers a public retainer-hours URL they send to clients — manufacturing companies, insurance carriers, and financial institutions running enterprise client-server applications built in Sybase PowerBuilder in the 1990s and 2000s, maintained today on SAP PowerBuilder 2019 R3 and 2022. Comparative context: PowerBuilder DataWindow buffer bugs have structural overlap with adjacent client-server data-binding platforms. ColdFusion retainers cover SESSION scope race conditions where concurrent requests produce last-write-wins overwrites of shared session state — both are shared-state bugs where the developer assumes a fresh state that persists from a prior interaction. Progress OpenEdge retainers cover implicit transaction scope bugs where ABL’s per-statement auto-commit leaves record groups in partially committed states after batch interruptions — similarly invisible in testing and only visible in production under specific interaction sequences.
FAQ: PowerBuilder developer retainers
What does a PowerBuilder developer on retainer typically do?
A PowerBuilder developer on monthly retainer covers DataWindow buffer audit (reviewing all SetSQLSelect and Retrieve sequences to confirm Reset() is called before modifying the DataWindow retrieval SQL — without Reset(), the delete buffer from a prior Retrieve is not cleared and Update() will send stale DELETE statements to the database); RetrieveAsNeeded and lazy-load patterns (confirming that pages fetched via RetrieveAsNeeded have their buffers correctly populated before Update() is called); GetItemStatus row flag analysis (auditing usage of GetItemStatus to confirm that DataModified!, New!, NewModified!, and NotModified! row statuses are correctly interpreted before calling SetItemStatus to reset flags); DataWindow nested report and crosstab patterns (reviewing nested DataWindows used as detail bands in composite reports for buffer isolation and refresh timing); PowerScript window ancestry and event override chains (confirming that Extend! and Override! event routing in inherited windows and user objects does not silently prevent base class event code from executing); and Appeon mobile/web migration compatibility (reviewing PowerScript features that do not translate to Appeon JavaScript rendering, including OLE objects, GetFocus/LoseFocus painting anomalies, and timer-driven DataWindow refreshes).
What PowerBuilder DataWindow work is most commonly underlogged?
Delete-buffer stale-row bugs are the most systematically underlogged PowerBuilder retainer work. The pattern: developer calls SetSQLSelect(new_sql) to rebuild a DataWindow’s retrieval query, then calls Retrieve() to fetch the new result set; the DataWindow’s delete buffer from a prior DeleteRow() sequence is not cleared by SetSQLSelect or by Retrieve(); when Update() is called later, PowerBuilder submits DELETE statements for all rows in the delete buffer, including stale rows from a prior result set; records that should not have been deleted are deleted; the symptom is invisible in testing because test sequences start with a fresh DataWindow with no prior delete buffer state. The investigation produces no error message (Update() returns 1 for success even when it executes unexpected deletes), no DataWindow exception, and no SQL log visible without enabling database-side SQL tracing. The developer must reconstruct the buffer state sequence from the PowerScript event chain to identify that Reset() was the missing call before SetSQLSelect.
What are typical PowerBuilder developer retainer rates?
Entry-level PowerBuilder developers with experience in basic PowerScript, DataWindow design (tabular, freeform, grid presentations), and standard window and menu design typically bill at $70 to $125 per hour. Mid-level PowerBuilder programmers with experience in DataWindow performance optimization (RetrieveAsNeeded, SetFilter, SetSort for client-side filtering and sorting), PowerScript inheritance and user object libraries, and SQLCA Transaction object configuration typically bill at $105 to $185 per hour. Senior PowerBuilder developers with deep knowledge of the DataWindow buffer model, PowerBuilder 2019 R3 and 2022 feature set, Appeon mobile and web migration, and enterprise client-server modernization typically bill at $150 to $275 per hour. Monthly retainer ranges: $1,800 to $3,200 per month for advisory engagements (15 to 22 hours per month); $2,500 to $5,800 per month for active maintenance. PowerBuilder retainer rates are moderate for the legacy market; the talent pool is small and shrinking.
What should a PowerBuilder developer retainer agreement include?
A PowerBuilder developer retainer agreement should specify: PowerBuilder version (2019 R3, 2022, or legacy 12.x and 11.x; significant differences in Appeon compatibility, DataWindow JSON support, and REST client capabilities); DataWindow scope (whether the retainer covers full DataWindow SQL and buffer state review, or only PowerScript event handling); database backend (SQL Server, Oracle, Sybase ASE, or Informix via ODBC/OLE DB); Appeon migration scope (whether the retainer includes reviewing PowerScript compatibility for Appeon mobile or web deployment); and source control situation (whether the PBL library files are under version control — legacy PowerBuilder projects often lack source control, and the retainer should specify whether the developer is responsible for establishing source control or only for maintaining the existing binary PBL files).
How should PowerBuilder developer retainer hours be logged?
Log each PowerBuilder retainer session with the relevant DataWindow and PowerScript specifics. For delete-buffer stale-row bugs: window name (w_inventory_list), DataWindow control name (dw_main), sequence (SetSQLSelect called without prior Reset(); delete buffer retained 3 rows from prior DeleteRow sequence; Update() sent DELETE for those 3 rows; wrong deletions: 3; fix: Reset() called before SetSQLSelect; wrong deletions: 3 → 0), SQL trace confirmation (SQL Server Profiler), hours (3.5h). For RetrieveAsNeeded partial update: DataWindow name, lazy-load page size, rows in primary buffer vs total result set, unsaved rows, fix (disabled RetrieveAsNeeded or added programmatic scroll-to-last before Save), hours (2h). For inheritance event chain gaps: window name, event name, Extend! vs Override! setting, ancestor code missed, user-visible symptom, fix, hours (1.5h).