Tag: Forms

Current Record Visual Attribute problem

If you have a multi-record block, and you use the handy Current Record Visual Attribute (CRVA) to set, say, a different background colour on all the items in the currently selected record, you may have run into this problem.

If you want to conditionally switch the visual attribute for certain items, at the item instance (i.e. record) level, this causes the CRVA to be overwritten, and the current record won’t be consistently highlighted. To get around this problem, a bit more code is required.

For example, let’s say you have a multi-record block called EMP, and it has two items that are sometimes gray, sometimes white – SALARY and ROLE. You have defined the following visual attributes:
EDITABLE_ITEM – white background
READONLY_ITEM – gray background
CURRENT_RECORD – blue background

Package Specification

PACKAGE EMP_BLOCK IS
PROCEDURE highlight_current_record;
PROCEDURE when_clear_block;
PROCEDURE when_remove_record;
END EMP_BLOCK;

Package Body

PACKAGE EMP_BLOCK IS

record_highlighted INTEGER;

PROCEDURE highlight_current_record IS
  rec INTEGER := GET_BLOCK_PROPERTY('EMP', CURRENT_RECORD);
  PROCEDURE set_visattr
    (itemn IN VARCHAR2
    ,rec IN NUMBER
    ,visattr IN VARCHAR2) IS
  BEGIN
    IF visattr IS NULL THEN
      -- (we could, if needed, make this more intelligent about
      -- detecting whether the record is NEW/INSERT/CHANGED,
      -- and examine the INSERT_ALLOWED/UPDATE_ALLOWED
      -- properties accordingly)
      IF GET_ITEM_INSTANCE_PROPERTY
        (itemn
        ,record_highlighted
        ,UPDATE_ALLOWED) = 'TRUE' THEN
        set_visattr(itemn, record_highlighted, 'EDITABLE_ITEM');
      ELSE
        set_visattr(itemn, record_highlighted, 'READONLY_ITEM');
      END IF;
    ELSE
      SET_ITEM_INSTANCE_PROPERTY
        (itemn
        ,rec
        ,VISUAL_ATTRIBUTE
        ,visattr);
    END IF;
  END set_visattr;
BEGIN
  -- Note: if record_highlighted is null, then no record
  -- is currently highlighted
  IF rec != record_highlighted THEN
    --un-highlight the record that was highlighted
    set_visattr('EMP.SALARY', record_highlighted);
    set_visattr('EMP.ROLE', record_highlighted);
  END IF;
  --highlight the newly selected record
  set_visattr('EMP.SALARY', rec, 'CURRENT_RECORD');
  set_visattr('EMP.ROLE', rec, 'CURRENT_RECORD');
  record_highlighted := rec;
END highlight_current_record;

PROCEDURE when_clear_block IS
BEGIN
  record_highlighted := NULL;
END when_clear_block;

PROCEDURE when_remove_record IS
BEGIN
  IF record_highlighted = :SYSTEM.TRIGGER_RECORD THEN
    record_highlighted := NULL;
  END IF;
END when_remove_record;

END EMP_BLOCK;

Block-level triggers on EMP:

when-new-record-instance

EMP_BLOCK.highlight_current_record;

when-clear-block

EMP_BLOCK.when_clear_block;

when-remove-record

EMP_BLOCK.when_remove_record;

Also, whenever your code modifies the UPDATE_ALLOWED property on SALARY or ROLE, it must then call EMP_BLOCK.highlight_current_record again.

Forms Library: PKG_FORM

This is just a post to document a standard library that I like to create in Oracle Forms, if equivalents are not already available. Suggestions or critiques are welcome.

“No warranty is express or implied about the suitability of this code for any purpose.”

Specification

PACKAGE PKG_FORM IS

PROCEDURE centre_window (windown IN VARCHAR2);
PROCEDURE check_record_is_saved;
PROCEDURE commit;
FUNCTION current_record (blockn IN VARCHAR2) RETURN INTEGER;
FUNCTION current_record_status (blockn IN VARCHAR2) RETURN VARCHAR2;
PROCEDURE delay_action (timern IN VARCHAR2);
PROCEDURE set_current_record_status
  (blockn IN VARCHAR2
  ,istatus IN NUMBER);
PROCEDURE set_mdi_window_title (clock IN BOOLEAN := TRUE);

-- gets the date/time from the app server
FUNCTION system_datetime RETURN DATE;

END PKG_FORM;

Package Body

PACKAGE PKG_FORM IS

cAPP_TITLE CONSTANT VARCHAR2(100) := '[insert app title here]';

PROCEDURE centre_window (windown IN VARCHAR2) IS
  cMAIN_WINDOW CONSTANT VARCHAR2(100) := '[insert main window name here]';
  x NUMBER;
  y NUMBER;
  main_win WINDOW := FIND_WINDOW(cMAIN_WINDOW);
  window_id WINDOW := FIND_WINDOW(windown);
BEGIN
  x := GET_WINDOW_PROPERTY(main_win,X_POS)
     + (GET_WINDOW_PROPERTY(main_win,WIDTH )
        - GET_WINDOW_PROPERTY(window_id,WIDTH )) / 2;
  y := GET_WINDOW_PROPERTY(main_win,Y_POS)
     + (GET_WINDOW_PROPERTY(main_win,HEIGHT)
        - GET_WINDOW_PROPERTY(window_id,HEIGHT)) / 2;
  SHOW_WINDOW(window_id, x, y);
END centre_window;

PROCEDURE check_record_is_saved IS
BEGIN
  IF :SYSTEM.FORM_STATUS = 'CHANGED' THEN
    PKG_MESSAGE.error('Please save or cancel your changes first.');
  END IF;
END check_record_is_saved;

PROCEDURE commit IS
  msglevel VARCHAR2(2) := :SYSTEM.MESSAGE_LEVEL;
BEGIN
  msg('PKG_FORM.commit');
  -- set message level to avoid FRM-40401 "No changes to save."
  :SYSTEM.MESSAGE_LEVEL := '5';
  COMMIT_FORM;
  :SYSTEM.MESSAGE_LEVEL := msglevel;
  IF :SYSTEM.FORM_STATUS != 'QUERY' THEN
    PKG_MESSAGE.error('Unable to save changes.');
  END IF;
END commit;

FUNCTION current_record (blockn IN VARCHAR2) RETURN INTEGER IS
BEGIN
  RETURN GET_BLOCK_PROPERTY(blockn, CURRENT_RECORD);
END current_record;

FUNCTION current_record_status (blockn IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
  RETURN GET_RECORD_PROPERTY(
    GET_BLOCK_PROPERTY(blockn, CURRENT_RECORD),
    blockn,
    STATUS);
END current_record_status;

PROCEDURE delay_action (timern IN VARCHAR2) IS
-- requires a suitable WHEN-TIMER-EXPIRED form-level trigger
  timer_id TIMER;
BEGIN
  IF ID_NULL(FIND_TIMER(timern)) THEN
    msg('CREATE_TIMER('||timern||')');
    timer_id := CREATE_TIMER(timern,1,NO_REPEAT);
  END IF;
END delay_action;

PROCEDURE set_current_record_status
  (blockn IN VARCHAR2
  ,istatus IN NUMBER) IS
BEGIN
  SET_RECORD_PROPERTY(
     GET_BLOCK_PROPERTY(blockn, CURRENT_RECORD),
     blockn,
     STATUS,
     istatus);
END set_current_record_status;

PROCEDURE set_mdi_window_title (clock IN BOOLEAN := TRUE) IS
  --scaling factor is the number of milliseconds in a second
  cSCALING_FACTOR CONSTANT NUMBER := 1000;
  --don't update the time more than once every 10 seconds
  cMIN_UPDATE_FREQUENCY CONSTANT NUMBER := 10;
  date_time DATE;
  timer_id TIMER;
  seconds PLS_INTEGER;
  ms PLS_INTEGER;
BEGIN
  IF clock THEN
    date_time := system_datetime;
    SET_WINDOW_PROPERTY(FORMS_MDI_WINDOW, TITLE,
	        cAPP_TITLE || ' - '
	        || TO_CHAR(date_time,'Dy FMDD Mon YYYY HHFM:MIpm'));
    IF ID_NULL(FIND_TIMER('CLOCK')) THEN
      --get the seconds portion of the current time
      seconds := (date_time - TRUNC(date_time,'MI')) * 86400;
      --update the time at the end of the minute (or thereabouts)
      ms := GREATEST((61 - seconds) * cSCALING_FACTOR
	         ,cMIN_UPDATE_FREQUENCY * cSCALING_FACTOR);
      timer_id := CREATE_TIMER('CLOCK', ms, NO_REPEAT);
    END IF;
  ELSE
    SET_WINDOW_PROPERTY(FORMS_MDI_WINDOW, TITLE, cAPP_TITLE);
  END IF;
END set_mdi_window_title;

FUNCTION system_datetime RETURN DATE IS
-- gets the date/time from the app server
-- should perform better than calling SYSDATE all the time
-- WARNING: this assumes that the builtin date format includes the time component!
BEGIN
  RETURN TO_DATE(:SYSTEM.EFFECTIVE_DATE,
    GET_APPLICATION_PROPERTY(BUILTIN_DATE_FORMAT));
END system_datetime;

END PKG_FORM;

Example form-level trigger: when-timer-expired

DECLARE
  timern VARCHAR2(100) := GET_APPLICATION_PROPERTY(TIMER_NAME);
BEGIN
  msg('when-timer-expired ' || timern);
  CASE timern
    WHEN 'CLOCK' THEN
      PKG_FORM.set_mdi_window_title;
    WHEN 'PKGNAME_XYZ' THEN
      PKGNAME.xyz;
    -- ... etc. ...
  ELSE
    PKG_MESSAGE.error('Unexpected timer name: ' || timern, 'System Error');
  END CASE;
END;

Forms Library: PKG_ITEM

This is just a post to document a standard library that I like to create in Oracle Forms, if equivalents are not already available. Suggestions or critiques are welcome.

“No warranty is express or implied about the suitability of this code for any purpose.”

Specification
The main point of this package is to (a) easily make form items more proactive regarding business requirements, e.g. by making it simple to make items conditionally editable, readonly or mandatory; and (b) mitigate the pain of diagnosing bugs with the way items work (by logging when most of these procedures are called).

PACKAGE PKG_ITEM IS

-- set item to value, without changing item state if possible
PROCEDURE assign (itemn IN VARCHAR2, value IN VARCHAR2);

FUNCTION current_record (itemn IN VARCHAR2) RETURN INTEGER;
PROCEDURE disable_button (itemn IN VARCHAR2);
PROCEDURE enable_button (itemn IN VARCHAR2);
PROCEDURE go_next (itemn IN VARCHAR2);
PROCEDURE go_prev (itemn IN VARCHAR2);
PROCEDURE hide (itemn IN VARCHAR2);
FUNCTION is_navigable (itemn IN VARCHAR2) RETURN BOOLEAN;
PROCEDURE set_editable
  (itemn    IN VARCHAR2
  ,rec      IN INTEGER  := NULL
  ,editable IN BOOLEAN  := TRUE
  ,set_null IN BOOLEAN  := FALSE);
PROCEDURE set_readonly (itemn IN VARCHAR2, rec IN INTEGER := NULL);
PROCEDURE set_required (itemn IN VARCHAR2, rec IN INTEGER := NULL);
PROCEDURE set_valid (itemn IN VARCHAR2);
PROCEDURE set_visattr (itemn IN VARCHAR2, currec IN BOOLEAN);
PROCEDURE show (itemn IN VARCHAR2);

END PKG_ITEM;

Package Body

PACKAGE PKG_ITEM IS

cVISATTR_NORMAL CONSTANT VARCHAR2(100) := 'NORMAL_ITEM';
cVISATTR_CURRENT CONSTANT VARCHAR2(100) := 'CURRENT_RECORD';
cVISATTR_DISPLAY CONSTANT VARCHAR2(100) := 'DISPLAY_ITEM';
cVISATTR_REQUIRED CONSTANT VARCHAR2(100) := 'REQUIRED_ITEM';

PROCEDURE assign (itemn IN VARCHAR2, value IN VARCHAR2) IS
-- set item to value, without changing item state if possible
BEGIN
  IF NAME_IN(itemn) != value
    OR (NAME_IN(itemn) IS NULL AND value IS NOT NULL)
    OR (NAME_IN(itemn) IS NOT NULL AND value IS NULL) THEN
    PKG_DEBUG.msg(itemn || ':=' || value);
    COPY(value, itemn);
  END IF;
END assign;

FUNCTION current_record (itemn IN VARCHAR2) RETURN INTEGER IS
--gets the record number for the block for the given item
BEGIN
  RETURN GET_BLOCK_PROPERTY
    (SUBSTR(itemn,1,INSTR(itemn,'.')-1)
    ,CURRENT_RECORD);
END current_record;

PROCEDURE disable_button (itemn IN VARCHAR2) IS
BEGIN
  msg('PKG_ITEM.disable_button('||itemn||')');
  IF :SYSTEM.CURSOR_ITEM = UPPER(itemn) THEN
    DO_KEY('NEXT_ITEM');
    check_package_failure;
  END IF;
  SET_ITEM_PROPERTY(itemn, ENABLED, PROPERTY_FALSE);
END disable_button;

PROCEDURE enable_button (itemn IN VARCHAR2) IS
  item_id ITEM := FIND_ITEM(itemn);
BEGIN
  msg('PKG_ITEM.enable_button('||itemn||')');
  SET_ITEM_PROPERTY(item_id, ENABLED, PROPERTY_TRUE);
  SET_ITEM_PROPERTY(item_id, NAVIGABLE, PROPERTY_TRUE);
END enable_button;

PROCEDURE go_next (itemn IN VARCHAR2) IS
--This is used when doing a GO_ITEM in order to implement a NEXT_ITEM.
--This will issue a NEXT_ITEM if the item we've just gone to
--is not currently navigable on the current record.
BEGIN
  msg('PKG_ITEM.go_next('||itemn||')');
  GO_ITEM(itemn);
  check_package_failure;
  IF GET_ITEM_INSTANCE_PROPERTY(:SYSTEM.CURSOR_ITEM
	,GET_BLOCK_PROPERTY(:SYSTEM.CURSOR_BLOCK, CURRENT_RECORD)
	,NAVIGABLE) = 'FALSE' THEN
    NEXT_ITEM;
    check_package_failure;
  END IF;
END go_next_if_not_navigable;

PROCEDURE go_prev (itemn IN VARCHAR2) IS
--This is used when doing a GO_ITEM in order to implement a PREVIOUS_ITEM.
--This will issue a PREVIOUS_ITEM if the item we've just gone to
--is not currently navigable on the current record.
BEGIN
  msg('PKG_ITEM.go_prev('||itemn||')');
  GO_ITEM(itemn);
  check_package_failure;
  IF GET_ITEM_INSTANCE_PROPERTY(:SYSTEM.CURSOR_ITEM
	,GET_BLOCK_PROPERTY(:SYSTEM.CURSOR_BLOCK, CURRENT_RECORD)
	,NAVIGABLE) = 'FALSE' THEN
    PREVIOUS_ITEM;
    check_package_failure;
  END IF;
END go_prev_if_not_navigable;

PROCEDURE hide (itemn IN VARCHAR2) IS
BEGIN
  msg('PKG_ITEM.hide('||itemn||')');
  IF :SYSTEM.CURSOR_ITEM = UPPER(itemn) THEN
    NEXT_ITEM;
    check_package_failure;
  END IF;
  SET_ITEM_PROPERTY(itemn, VISIBLE, PROPERTY_FALSE);
END hide;

FUNCTION is_navigable (itemn IN VARCHAR2) RETURN BOOLEAN IS
BEGIN
  RETURN GET_ITEM_PROPERTY(itemn, NAVIGABLE) = 'TRUE';
END is_navigable;

PROCEDURE set_editable
  (itemn    IN VARCHAR2
  ,rec      IN INTEGER  := NULL
  ,editable IN BOOLEAN  := TRUE
  ,set_null IN BOOLEAN  := FALSE) IS
  item_id ITEM;
BEGIN
  msg('PKG_ITEM.set_editable('||itemn||','||rec||')');
  IF editable THEN
    item_id := FIND_ITEM(itemn);
    IF rec IS NULL THEN
      IF :SYSTEM.CURSOR_ITEM != UPPER(itemn) THEN
        SET_ITEM_PROPERTY(item_id, VISIBLE, PROPERTY_TRUE);
        SET_ITEM_PROPERTY(item_id, ENABLED, PROPERTY_TRUE);
      END IF;
      SET_ITEM_PROPERTY(item_id, NAVIGABLE, PROPERTY_TRUE);
      SET_ITEM_PROPERTY(item_id, INSERT_ALLOWED, PROPERTY_TRUE);
      SET_ITEM_PROPERTY(item_id, UPDATE_ALLOWED, PROPERTY_TRUE);
      SET_ITEM_PROPERTY(item_id, VISUAL_ATTRIBUTE, cVISATTR_NORMAL);
    ELSE
      SET_ITEM_INSTANCE_PROPERTY(item_id, rec, NAVIGABLE, PROPERTY_TRUE);
      SET_ITEM_INSTANCE_PROPERTY(item_id, rec, INSERT_ALLOWED, PROPERTY_TRUE);
      SET_ITEM_INSTANCE_PROPERTY(item_id, rec, UPDATE_ALLOWED, PROPERTY_TRUE);
      SET_ITEM_INSTANCE_PROPERTY(item_id, rec, VISUAL_ATTRIBUTE, cVISATTR_NORMAL);
    END IF;
  ELSE
    set_readonly(itemn, rec);
    --clear any data the user may have entered into the item (current record only)
    IF set_null AND NAME_IN(itemn) IS NOT NULL THEN
      msg(itemn||':=NULL');
      COPY(NULL, itemn);
    END IF;
  END IF;
END set_editable;

PROCEDURE set_readonly (itemn IN VARCHAR2, rec IN INTEGER := NULL) IS
  item_id ITEM := FIND_ITEM(itemn);
BEGIN
  msg('PKG_ITEM.set_readonly('||itemn||','||rec||')');
  IF rec IS NULL THEN
    SET_ITEM_PROPERTY(item_id, NAVIGABLE, PROPERTY_FALSE);
    SET_ITEM_PROPERTY(item_id, INSERT_ALLOWED, PROPERTY_FALSE);
    SET_ITEM_PROPERTY(item_id, UPDATE_ALLOWED, PROPERTY_FALSE);
    SET_ITEM_PROPERTY(item_id, VISUAL_ATTRIBUTE, cVISATTR_DISPLAY);
  ELSE
    SET_ITEM_INSTANCE_PROPERTY(item_id, rec, NAVIGABLE, PROPERTY_FALSE);
    SET_ITEM_INSTANCE_PROPERTY(item_id, rec, INSERT_ALLOWED, PROPERTY_FALSE);
    SET_ITEM_INSTANCE_PROPERTY(item_id, rec, UPDATE_ALLOWED, PROPERTY_FALSE);
    SET_ITEM_INSTANCE_PROPERTY(item_id, rec, VISUAL_ATTRIBUTE, cVISATTR_DISPLAY);
  END IF;
END set_readonly;

PROCEDURE set_required (itemn IN VARCHAR2, rec IN INTEGER := NULL) IS
BEGIN
  msg('PKG_ITEM.set_required('||itemn||','||rec||')');
  IF rec IS NULL THEN
    SET_ITEM_PROPERTY(itemn, REQUIRED, PROPERTY_TRUE);
    SET_ITEM_PROPERTY(itemn, VISUAL_ATTRIBUTE, cVISATTR_REQUIRED);
  ELSE
    SET_ITEM_INSTANCE_PROPERTY(itemn, rec, REQUIRED, PROPERTY_TRUE);
    SET_ITEM_INSTANCE_PROPERTY(itemn, rec, VISUAL_ATTRIBUTE, cVISATTR_REQUIRED);
  END IF;
END set_required;

PROCEDURE set_valid (itemn IN VARCHAR2) IS
BEGIN
  msg('PKG_ITEM.set_valid('||itemn||')');
  SET_ITEM_PROPERTY(itemn, ITEM_IS_VALID, PROPERTY_TRUE);
END set_valid;

PROCEDURE set_visattr (itemn IN VARCHAR2, currec IN BOOLEAN) IS
  rec INTEGER;
BEGIN
  msg('PKG_ITEM.set_visattr('||itemn||')');
  rec := item_current_record(itemn);
  IF currec THEN
    SET_ITEM_INSTANCE_PROPERTY(itemn, rec, VISUAL_ATTRIBUTE, cVISATTR_CURRENT);
  ELSE
    SET_ITEM_INSTANCE_PROPERTY(itemn, rec, VISUAL_ATTRIBUTE, cVISATTR_NORMAL);
  END IF;
END set_current_item_visattr;

PROCEDURE show (itemn IN VARCHAR2) IS
BEGIN
  msg('PKG_ITEM.show('||itemn||')');
  SET_ITEM_PROPERTY(itemn, VISIBLE, PROPERTY_TRUE);
END show;

END PKG_ITEM;

Example usage:

PKG_ITEM.set_editable('EMP.SALARY'
  ,rec      => PKG_FORM.current_record('MYBLOCK')
  ,editable => (:EMP.ROLE != 'CEO')
  );

The above example makes the SALARY item readonly if the employee’s role is ‘CEO’ – you would call this from the post-query trigger on the block, so on rows where the role is not ‘CEO’, SALARY will be editable.

Setting the visual attributes at the row level like this on multi-row blocks, however, has one side-effect. If you have a Current Record Visual Attribute (CRVA) set on the block (or the form), this code will overwrite that, so the CRVA will not be effective for these items. There is a workaround (to be published).

Bias in Testing

I’ve been trying a number of strategies to improve the performance of a very complex form (Oracle Forms 6i) currently in development. We’ve already done a fair amount of work making the code as efficient as possible, while still being reasonably maintainable, so there don’t seem to be any more low-hanging fruit we can pick off easily.

One aspect of the performance is the form startup time, which is on the order of 9-17 seconds. The kinds of changes I’m testing don’t make a very visible change to this time, so to work out whether the changes are worthwhile I have to be a bit more scientific about performance measurement. That means I have to use statistics (Disclaimer: I am not a statistician, so take what I say with a grain of salt and do your own research!). I test the form with the original code and with the modified code to see whether a particular code change makes any difference to the performance.

I start the form up, wait for it to load, then run our debug tool which tells me the times (to the nearest second) when the form started and when control was passed to the user. I then record these times in a spreadsheet, then close the form and try again two or three times, recording each result. Then I take the average and write that down. I’ll then change back to the original (unimproved) code, run the whole test again, then compare the average results. Sometimes the average is better, worse or the same as the original.

This is what I was doing today – I’d made a change which I thought might knock off a second or two off the startup time, and so I ran my 2-phase timing test:

Improved code timings: 19s, 8s, 7s, 7s
Original code timings: 9s, 8s, 8s

What can I conclude from these data? What I did conclude was that the first timing (19s) was an outlier – it was the first time I’d run the form today so it probably had to load various caches and possibly even perform hard-parses for some of the queries. The original code had an average startup time of 8.3 seconds, and the improved code loaded in 7.3 seconds on average (discounting the outlier). My improvement saved 1 second!

Or did it? These tests were performed between 8:16 and 8:27 on a Tuesday morning. I later thought of a slightly different way of doing the code improvement, implemented it and ran the whole test again:

Improved(#2) code timings: 11s, 9s, 12s, 9s
Original code timings: 11s, 12s, 11s, 7s, 8s

The averages have risen to 10.3 and 9.8 seconds, and the “improved” code actually looks worse here! This second battery of tests were performed between 9:13 and 9:22 on the same morning. You’ll also notice that I ran the test more often this time. Why? Well, I couldn’t believe that the improved code slowed the form to an average 10.6 seconds, so ran it a fourth time; and due to my increasing suspicion that these timings were more random than I’d realised, I ran the final test five times. In the end I decided the slower times must be because more and more developers have arrived by 9am and so the app server is busier, throwing my test results into confusion.

One thing I’ll confess is that I’ve had to resist a temptation all along to only include timings that seem “reasonable” – e.g. if the form took 19 seconds to load I’d immediately suspect some other process on the server had slowed my session down, and so I’d be tempted to not include it in the results spreadsheet. I knew enough about statistics to know that doing this would run the risk of introducing confirmation bias into the results, so I diligently recorded every result I observed.

After considering the apparent failure of my attempt to prove my code change made a discernable difference to the performance of the code, I started thinking a bit more about what was going on as I ran the tests and recorded the results. I realised that a more subtle form of confirmation bias has crept into my results because I didn’t decide firsthand how many tests I would run. I simply ran the test three or our times, depending on whether I was in a hurry, and if it didn’t seem to be coming up with the numbers I was expecting, I’d keep running it until it did!

Writing it out like that, it seems blatently obvious, but when you’re in the middle of running these tests and recording results it’s very easy to slip up.

What did I learn from all this? Before running any tests, write down exactly how many tests will be run, and at what times of the day. In other words, try to eliminate irrelevant variables such as concurrent activity on the server by spreading the tests randomly throughout the day, and try to avoid confirmation bias by predicting ahead of time how many tests will be needed to reduce the impact of outliers on the average result.

FRM-40654 “Record has been updated by another user”

There are several reasons you might get this error in Oracle Forms, e.g. another user has modified the record before you saved it (as the error message suggests), or a table trigger has modified the record and your form’s DML Returning Value is set to No.

Another cause to chalk up is what one of my colleagues got today. He made a form with a simple table-based block, he would insert a record and save successfully, then try to modify it and consistently got FRM-40654 when he tried to save. No other user was trying to update the row, and there were no triggers on the table.

The answer? The table is an index-organised table, but the block on the form had Key Mode set to Automatic. I suspect the form is comparing the ROWID of the before-and-after change, but because the table is index-organised the ROWID is not necessarily constant. Whatever the reason, changing the Key Mode to Updateable or Non-Updateable solves the problem.