Over the last two rounds of the series Building a customer app in Uniface 10, the app grew by about 60 service operations: pipelines, owners, contact rhythms, text templates, CSV import and export, statistics, notices, settings snapshots. Most of it was routine. These six things were not - each one cost a failed test or a wrong result before I understood it.
None of them is in the obvious places of the documentation, and none of them is specific to this app. If you write ProcScript against SQLite, you will probably meet some of them.
Tested with Rocket Uniface 10.4.03 Community Edition (10.4.03.021) and the SQLite version of its SLE driver (3.50.2), October 2026.
1. One variable as input and output
The follow-up service creates the next follow-up of a repeating task and moves its due date to the next working day. The helper entry for that has an input and an output parameter:
entry NEXT_WORKDAY_ISO
params
string pIso : IN
string pNext : OUT
endparams
...
pNext = ""
call REAL_DATE(pIso, vDay)
if (vDay = "" | vDay != pIso)
return 0
endif
The first version called it with the same variable on both sides, because "replace the date with the next working day" felt natural:
call NEXT_WORKDAY_ISO(vNext, vNext)
The result was empty. The entry starts by clearing its output - a habit that makes every early return safe - and with one variable on both sides, that also cleared the input. As far as I can tell, the parameters of a call behave like references to the caller's variable here. A separate variable fixed it:
call NEXT_WORKDAY_ISO(vNext, vWork)
vNext = vWork
The same project has call SQL_LIT(vText, vText) in several places, and that one works - because SQL_LIT copies its input into a local variable before it writes the output. Whether such a call works depends on the order inside the entry, which is a bad thing to depend on. Use separate variables for IN and OUT.
2. Blanks at the end don't survive the round trip
A test for the e-mail normalization inserted a customer with the address " Ann@Example.COM " - blanks on both sides on purpose. The dry run must not change it, so the test read it back and compared:
call GET_TEXT("SELECT EMAIL FROM CUSTOMER WHERE CUSTOMER_ID = 999876", vValue)
call CHECK("...", (... & vValue = " Ann@Example.COM "), ...)
The check failed. The value that came back was " Ann@Example.COM" - the leading blank was still there, the trailing one was gone. I did not dig into whether it gets lost on the way in or on the way out; for the test it does not matter. The fixed check looks for what the dry run must preserve, the upper-case letters:
call CHECK("EMAILS the dry run counts customers and contacts", (vStatus = 0 & vCount = 2 & $scan(vValue, "Ann@Example.COM") > 0), ...)
If trailing blanks matter to you - for example because you want to prove that a clean-up removes them - check them in SQL, where they are still there: SELECT COUNT(*) FROM CUSTOMER WHERE EMAIL <> TRIM(EMAIL). Don't compare trailing blanks after reading a value into ProcScript.
3. Two timestamps in a row are often the same
The "recently opened customers" list keeps the last ten customers per user, ordered by the time they were opened. Opening two customers directly after each other - which a test does - gave both the same OPENED_AT, down to the millisecond. The Windows system clock that SQLite's 'now' reads typically advances in steps of about 15 milliseconds, so "later" is not guaranteed to be "greater".
The fix makes the database guarantee the order: the new timestamp is the current time, or one millisecond after the newest entry of that user, whichever is greater:
vSql = "INSERT OR REPLACE INTO APP_RECENT (USER_NAME, CUSTOMER_ID, OPENED_AT) SELECT '%%(vUser)', %%(pCustomerId), CASE WHEN m IS NULL OR n > m THEN n ELSE strftime('%Y-%m-%d %H:%M:%f', m, '+0.001 seconds') END FROM (SELECT strftime('%Y-%m-%d %H:%M:%f', 'now', 'localtime') AS n, (SELECT MAX(OPENED_AT) FROM APP_RECENT WHERE USER_NAME = '%%(vUser)') AS m)"
sql vSql, "CUSTOMERS"
And every query that sorts by the timestamp gets a second sort key (ORDER BY OPENED_AT DESC, CUSTOMER_ID DESC), so that even equal values have a fixed order. Never let the order of events depend on the clock alone.
4. lfiledump with "UTF-8" writes a BOM
The CSV export writes its file with lfiledump vText, pFileName, "UTF-8". The first bytes of the file are EF BB BF - a byte order mark. Excel likes that for CSV. Many other consumers don't: a JSON parser may refuse it, and a checksum file with a BOM in front of the hash is no longer in the format sha256sum -c expects.
There is a switch for it:
lfiledump/nobom pText, pFileName, "UTF-8"
The JSON export and the checksum files use /nobom, the CSV exports keep the BOM. Decide per file type, not once for the project.
5. Hashing a file the way sha256sum does
The data export writes a checksum file next to each export, so that a copy can be verified later - also without Uniface. That only works if the hash is computed over the bytes of the file, not over a decoded text. Loading the file as raw data does that:
entry FILE_HASH
params
string pFileName : IN
string pHash : OUT
string pError : OUT
endparams
variables
raw vData
endvariables
pHash = ""
pError = ""
if ($lfileexists(pFileName) != 1)
pError = $concat("The file ", pFileName, " does not exist.")
return -1
endif
fileload/raw pFileName, vData
if ($status < 0)
pError = $concat("The file ", pFileName, " could not be read (error ", $procerror, ").")
return -1
endif
pHash = $lowercase($encode("HEX", $encode("SHA256", vData)))
return 0
end
The checksum file then gets one line in the usual format - hash, two blanks, file name - written with /nobom. I checked the result against sha256sum with a test file that contains umlauts, which is where a text-based hash would differ: both gave the same value. $lowercase is there because sha256sum prints lower case. For hashes, load /raw into a raw variable.
6. One SQL statement can end the process
This one gets its own article, because finding it took the longest: a SELECT CASE WHEN ... GLOB ... CAST(...) without a FROM, sent through the sql statement, ended the test process without an error status and without a log line. Simpler SELECTs without a FROM - SELECT date('now', 'localtime') and the like - work fine and are used all over the app. I moved the validation into ProcScript and haven't looked back.
The longer story, including how to find a crash when the log loses its last lines, is in The Uniface test that "hung" had died. Keep validation in ProcScript and use SQL for data.
Takeaways
Use separate variables for IN and OUT, even when it looks redundant.
Check whitespace in SQL, not after reading the value into ProcScript.
Give every timestamp-ordered list a second sort key and, where order matters, make the database guarantee it.
Choose the BOM per file type. lfiledump adds one for "UTF-8" unless you say /nobom.
Hash bytes, not text: fileload/raw into a raw variable, then $encode("SHA256", ...).
When a statement can take the process down, don't be clever in SQL.
Top comments (0)