The SQL Your PowerBuilder App Emits Isn't in the Source Code. It's in the Status Flags
by DeeDee Walsh, on Jul 6, 2026 6:11:45 PM
Two users open the same customer record at 4:58 on a Friday. Both change the credit limit. In the PowerBuilder application that's run the business for eighteen years, the second save comes back having affected zero rows, the app catches it, and the user gets a "this record was changed by another user" message. They reload, they re-key, the right number lands.
In the modernized application, the one that compiled clean, passed its tests, and demoed beautifully, the second save silently wins. No message. No conflict. The first user's change is simply gone, and nobody knows until the account goes over limit three weeks later and someone pulls the audit log.
Nothing broke. The screens are identical. The code was translated faithfully. And the application now has a different data-integrity contract than the one it replaced.
This is the failure mode that lives underneath the DataWindow, and it's the one most migration efforts never even see because the behavior that diverged was never written in the source code you were translating.
The DataWindow doesn't display data. It runs a state machine.
Engineers who've never worked in PowerBuilder tend to picture the DataWindow as an advanced data grid: it binds to a query, shows rows, lets users edit them. That mental model is wrong enough to be dangerous.
A DataWindow is a runtime state machine that tracks the status of every row and every column, across multiple buffers, and generates the SQL to reconcile that state with the database when you call Update(). The grid is the visible 10%. The state machine is the part that determines what actually happens to your data and it's nearly invisible in the PowerScript source.
Start with the buffers. A DataWindow doesn't hold one set of rows; it holds several. The Primary! buffer holds the rows currently in play. The Filter! buffer holds rows that a filter has pushed out of view but that still exist in the object. The Delete! buffer holds rows the user "deleted", removed from the screen, but retained specifically so that Update() can generate the DELETE statements for them later. A row that's gone from the display is not gone from the DataWindow. It's sitting in the Delete! buffer waiting to become DML.
Then the status flags. Every row carries a status, retrievable with GetItemStatus() and so does every column within it:
NotModified!— untouched since retrieval.DataModified!— an existing row (or column) that's been changed.New!— a row inserted programmatically (viaInsertRow()) that hasn't received data yet.NewModified!— a new row that now has data.
These are way more than cosmetic. When you call Update(), the DataWindow walks this state and compiles SQL from it: NewModified! rows become INSERTs, DataModified! rows become UPDATEs, and everything in the Delete! buffer becomes DELETEs. Rows still marked NotModified! generate nothing. The status flags are the program. The DataWindow is, functionally, a compiler whose source language is interaction history and whose target language is DML.
The WHERE clause is where your concurrency contract lives
Here's the part that decided the credit-limit story above, and it's the setting most teams don't know they depend on. When the DataWindow generates an UPDATE or DELETE, it has to build a WHERE clause to find the row. How it builds that clause is governed by the "Where Clause for Update/Delete" property, and it has three modes:
- Key Columns — the WHERE matches on the primary key only. Last write wins. No conflict detection.
- Key and Updatable Columns — the WHERE matches on the key plus the original values of all updatable columns.
- Key and Modified Columns — the WHERE matches on the key plus the original values of the columns this user actually changed.
Those last two are optimistic concurrency control, implemented entirely inside the DataWindow. When the app was built with "Key and Modified Columns," the generated UPDATE says, in effect, change this row to the new values, but only if the columns I'm touching still hold the values they held when I retrieved them. If another user got there first, the WHERE matches nothing, the database reports zero rows affected, and well-written PowerBuilder code checks that count and raises the conflict. That check is why the legacy app protected the first user's change.
To generate that WHERE clause, the DataWindow has to retain the original value of every modified item alongside its current value which it does, retrievable with the original-value argument on GetItemString() and friends. Two versions of every changed field, kept specifically so the update can be conditional on the world not having moved underneath it.
Now translate that naively. A modern ORM's default save matches on the primary key and issues a straight UPDATE, "Key Columns" semantics, last-write-wins. If your migration reproduced the screen and the query but defaulted the persistence layer to key-only updates, you didn't port a bug. You silently deleted a concurrency-control mechanism the business has depended on for two decades, and you will not see it in any single-user test, any demo, or any diff. It only appears when two users collide in production, which, on a system that's run the business for eighteen years, is not an edge case. It's Tuesday.
Why AI translation walks straight past this
This is a clean illustration of the 70% wall, and of why it's a wall rather than a slope.
An AI agent translating PowerScript to C# reads the source. And the source (the visible code) contains almost none of this. The buffer model isn't in the PowerScript; it's in the DataWindow engine. The status-flag transitions aren't written down; they're the emergent result of the runtime reacting to InsertRow(), DeleteRow(), SetItem(), user keystrokes, and AcceptText(). The WHERE-clause mode isn't code at all. It's a property set in the DataWindow painter, stored in the object definition, invisible to anyone reading the event scripts. The concurrency contract that decided the credit-limit outcome exists nowhere in the text an agent is translating.
So the agent does exactly what it's built to do: it faithfully translates the code it can see, and it produces a modernized application that reads the same rows and writes the same columns. What it can't do is reconstruct a behavioral contract that was never expressed in the source because the DataWindow's whole design was to keep that contract out of your code. That was the productivity win in 1995. It's the migration trap in 2026.
And it compounds. SetItemStatus() lets enterprise code manipulate the flags deliberately, forcing a NotModified! row to DataModified! to push an update through, or dropping a NewModified! row to NotModified! to skip it. The resetflag argument on Update() lets a developer update without clearing status, so the same changes can participate in a multi-DataWindow transaction against a shared transaction object, committed or rolled back as a unit. These are real patterns in real master-detail code, and every one of them is a place where the emitted SQL depends on runtime state and explicit flag surgery that a source-level translation cannot infer.
Faithful migration means reproducing the state machine, not the screen
The discipline that closes this gap is the one we keep coming back to: you don't validate a migration by looking at it. You validate it by making the old system and the new system do the same thing and proving they agree at the layer where disagreement actually costs you.
For the DataWindow, that layer is the DML. You need to confirm that for a given sequence of interactions: insert these rows, modify these columns, delete that one, then save, does the modernized application emit the same INSERT/UPDATE/DELETE statements, with the same WHERE clauses, in the same order, that the legacy DataWindow emitted? That's a characterization test written against behavior, not code: capture the actual SQL the legacy application produces for a battery of interaction sequences, then run the same sequences against the modernized app and diff the emitted DML. Divergences in the WHERE clause, a missing original-value predicate, a key-only match where the legacy app used key-and-modified surface immediately, at the persistence boundary, before they reach production and become a forensic exercise.
This is the core of how VELO approaches PowerBuilder modernization, and why the process starts with an Assessment rather than a translation. Before committing to a fixed-price engagement, VELO analyzes the actual codebase and, critically, the DataWindow object definitions and update properties, not just the event scripts and reports what it can faithfully modernize. The Quality agent's job is behavioral equivalence at exactly this level: not "does it compile," but "does it emit the same DML and enforce the same concurrency contract as the system it's replacing." The state machine is treated as a thing to be preserved, because it is the thing that was actually running your business.
The question to ask any PowerBuilder migration vendor
Every vendor in this market can translate PowerScript to C# now. That's table stakes. So the question that separates them is the one that exposes whether they understand what a DataWindow actually is:
When you modernize a DataWindow, how do you reproduce its update behavior: the buffer and status model, the WHERE-clause mode, the original-value retention that our optimistic concurrency depends on and how do you prove the modernized application emits the same SQL as the one it's replacing?
If the answer is about faithfully translating the code, you've found the 70% wall, and the other 30% is a silent overwrite waiting for two users and a Friday afternoon. If the answer is a behavioral-equivalence discipline that operates at the DML layer, you're talking to someone who knows that the DataWindow was never a grid. It was a state machine with a SQL compiler attached and preserving it is the whole job.
Want to see what your DataWindows actually do before you modernize them? Start with a VELO Assessment. We analyze your PowerBuilder codebase — object definitions, update properties, and all — and show you exactly what percentage VELO can modernize, grounded in your real code, at no risk.
Note this is the technical deep dive to The Failure Mode of AI PowerBuilder Modernization.



