=== modified file 'src/com/goldencode/p2j/persist/DatabaseManager.java'
--- src/com/goldencode/p2j/persist/DatabaseManager.java	2021-12-28 15:24:58 +0000
+++ src/com/goldencode/p2j/persist/DatabaseManager.java	2022-03-24 18:34:39 +0000
@@ -2,7 +2,7 @@
 ** Module   : DatabaseManager.java
 ** Abstract : Registers and configures databases and manages session factories
 **
-** Copyright (c) 2004-2021, Golden Code Development Corporation.
+** Copyright (c) 2004-2022, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
 ** 001 ECF 20050901   @22473 Created initial version. Extracted and
@@ -261,6 +261,7 @@
 **                           implemented - unknown is not an acceptable returned value for this function.
 **                           Replaced the UnimplementedFeature.missing from tenant-related APIs with a TODO 
 **                           comment.
+**     IAS 20220324          Re-worked LockTableUpdater.
 */
 
 /*
@@ -1238,7 +1239,7 @@
          
          if (lockTableUpdaters != null)
          {
-            LockTableUpdater ltu = new LockTableUpdater(metaDB);
+            LockTableUpdater ltu = new LockTableUpdater(primaryDB, metaDB);
             lockTableUpdaters.put(primaryDB, ltu);
          }
       }
@@ -1534,7 +1535,7 @@
     *          if lock metadata is used by the current application, but no lock table updater is
     *          found for the given database.
     */
-   static LockTableUpdater getLockTableUpdater(Database database)
+   public static LockTableUpdater getLockTableUpdater(Database database)
    {
       if (lockTableUpdaters == null)
       {

=== modified file 'src/com/goldencode/p2j/persist/Persistence.java'
--- src/com/goldencode/p2j/persist/Persistence.java	2022-03-21 13:37:30 +0000
+++ src/com/goldencode/p2j/persist/Persistence.java	2022-03-24 18:35:30 +0000
@@ -564,6 +564,7 @@
 **     ECF 20210927          Minor cleanup.
 **     IAS 20211223          Special processing of errors raised by UDFs. 
 **     IAS 20220221          Call a correct version of the getFWDVersion (Java or SQL) 
+**     IAS 20220324          Re-worked LockTableUpdater.
 */
 
 /*
@@ -1114,20 +1115,6 @@
    }
    
    /**
-    * Force the lock table updater to process all record lock events which are pending at the time
-    * this method is invoked. Additional events may be added by other threads while this is
-    * happening, but any events pending at the start of this method will be processed before this
-    * method returns.
-    */
-   public void flushMetaLockEvents()
-   {
-      if (lockTableUpdater != null)
-      {
-         lockTableUpdater.flushQueue();
-      }
-   }
-   
-   /**
     * Get the identity manager associated with this <code>Persistence</code>
     * instance.  For temp tables, this will be <code>null</code>, since for
     * temp tables unique primary keys generated using {@link

=== modified file 'src/com/goldencode/p2j/persist/RecordBuffer.java'
--- src/com/goldencode/p2j/persist/RecordBuffer.java	2022-03-01 21:50:08 +0000
+++ src/com/goldencode/p2j/persist/RecordBuffer.java	2022-03-24 18:38:39 +0000
@@ -1198,6 +1198,7 @@
 **     OM  20220228          Added byOuterQuery flag for marking situations when the buffer is empty because
 **                           it is set by an OUTER-JOIN table.
 **     OM  20220301          Merged [unknownMode] and [byOuterQuery] flags.
+**     IAS 20220324          Re-worked LockTableUpdater.
 */
 
 /*
@@ -6695,11 +6696,7 @@
          ErrorManager.recordOrShowError(14378, msg, true);
       }
       
-      if (legacyName.equalsIgnoreCase("_Lock"))
-      {
-         persistence.flushMetaLockEvents();
-      } 
-      else if (legacyName.equalsIgnoreCase("_Trans"))
+      if (legacyName.equalsIgnoreCase("_Trans"))
       {
          // flush the transaction id to persistence if required
          TransactionManager.flushTransMetaData(getPersistence());

=== modified file 'src/com/goldencode/p2j/persist/meta/LockTableUpdater.java'
--- src/com/goldencode/p2j/persist/meta/LockTableUpdater.java	2020-10-14 21:06:19 +0000
+++ src/com/goldencode/p2j/persist/meta/LockTableUpdater.java	2022-03-24 18:58:01 +0000
@@ -2,7 +2,7 @@
 ** Module   : LockTableUpdater.java
 ** Abstract : Manages metadata lock records for a particular, primary database
 **
-** Copyright (c) 2013-2020, Golden Code Development Corporation.
+** Copyright (c) 2013-2022, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
 ** 001 ECF 20131101 Created initial version.
@@ -23,6 +23,7 @@
 ** 014 ECF 20200906 New ORM implementation.
 ** 015 CA  20200924 Replaced Method.invoke with ReflectASM.
 **     OM  20201012 Force use locally cached meta information instead of map lookup.
+**     IAS 20220324 Performance optimization in the UserTableStatUpdater style.
 */
 
 /*
@@ -82,6 +83,9 @@
 
 import java.lang.reflect.*;
 import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+import java.util.concurrent.locks.*;
 import java.util.logging.*;
 
 import com.goldencode.p2j.persist.*;
@@ -168,36 +172,25 @@
    
    /**
     * Constructor.
-    * 
+    * @param   primary 
+    *          Primary database
     * @param   metaDatabase
     *          Metadata database.
     */
-   public LockTableUpdater(Database metaDatabase)
-   {
-      this.worker = new UpdateWorker(PersistenceFactory.getInstance(metaDatabase));
-   }
-   
-   /**
-    * This method is called within a user context to force any pending lock events to be
-    * processed. It guarantees that any events that are pending in the queue at the time this
-    * method is invoked will have been processed by the time this method returns. However,
-    * other events may be added by other user contexts in the meantime, and these are not
-    * guaranteed to have been processed.
-    */
-   public void flushQueue()
-   {
-      worker.forceFlush();
-   }
-   
-   /**
-    * Stop processing record lock events for the associated, primary database. This will terminate
-    * the update worker and its processing thread.
-    */
-   public void terminate()
-   {
-      worker.terminate();
-   }
-   
+   public LockTableUpdater(Database primary, Database metaDatabase)
+   {
+      this.worker = new UpdateWorker(
+                        PersistenceFactory.getInstance(metaDatabase),
+                        primary);
+   }
+   
+   /**
+    * Persist locks in the meta database
+    */
+   public void persist()
+   {
+      worker.persist();
+   }
    /**
     * Method which is called by a {@link LockManager} in response to a lock event. Does some
     * preliminary processing and sanity checking, then queues the event for further processing on
@@ -244,7 +237,6 @@
     * thread processes the events.
     */
    private static class UpdateWorker
-   implements Runnable
    {
       /** Metadata lock table name */
       private static final String LOCK_TABLE = "meta_lock";
@@ -304,9 +296,21 @@
          dmoClass = implementingClass;
       }
       
-      /** Event queue to which many threads can write and from which only one thread reads */
-      private final List<RecordLockEvent> eventQueue = new LinkedList<>();
+      /** Locks' map */
+      private final ConcurrentMap<LockRecId, MinimalLockImpl> locks = new ConcurrentSkipListMap<>();
+
+      /** Map version */
+      private final AtomicLong mapVersion = new AtomicLong();
+
+      /** Meta table vesrion */
+      private final AtomicLong databaseVersion = new AtomicLong(0);
+
+      /** Locks' map guard */
+      private final ReentrantReadWriteLock guard = new ReentrantReadWriteLock();
       
+      /** database name */
+      private final Database primary;
+
       /** Persistence helper object for metadata database */
       private final Persistence persistence;
       
@@ -316,32 +320,11 @@
       /** Invocation handler which maps proxy method calls to underlying DMO methods */
       private final Handler handler = new Handler();
       
-      /** Update worker thread */
-      private final AssociatedThread workThread;
-      
-      /** Cache of converted table names to file numbers */
-      private final Map<String, Integer> fileNums = new HashMap<>();
-      
-      /** Termination flag */
-      private boolean done = false;
-      
-      /** Lock needed to read from queue; must be obtained first if write lock is needed also */
-      private final Object readLock = new Object();
-      
-      /** Lock needed to write to queue; must be obtained second if read lock is needed also */
-      private final Object writeLock = new Object();
-      
-      /** Unique and sequential ID of the last event added to the queue */
-      private long lastAddedID = -1L;
-      
-      /** Unique and sequential ID of the last event read from the queue and processed */
-      private long lastProcessedID = -1L;
-      
       /** Next available primary key for a new lock metadata record */
-      private long nextPrimaryKey = 1L;
+      private AtomicLong nextPrimaryKey = new AtomicLong(1L);
       
       /** Dummy empty class to be used as the super class for proxy. */
-      public static class Empty {}
+      public static final class Empty {}
       
       /**
        * Constructor.
@@ -349,59 +332,45 @@
        * @param   persistence
        *          Persistence helper object for metadata database.
        */
-      UpdateWorker(Persistence persistence)
+      UpdateWorker(Persistence persistence, Database primary)
       {
          super();
          
          this.persistence = persistence;
+         this.primary = primary;
          this.proxy = ProxyFactory.getProxy(Empty.class, new Class<?>[] { MinimalLock.class }, handler);
          
-         workThread = new AssociatedThread(this);
-         workThread.setDaemon(true);
-         workThread.setName("Lock metadata update thread");
-         workThread.start();
       }
       
       /**
-       * This method executes in the worker thread, reading record lock events from the queue when
-       * they become available, and updating the metadata database accordingly.
+       * Persist locks in the meta database
        */
-      @Override
-      public void run()
+      public void persist()
       {
-         while (true)
-         {
-            // check if we should terminate event processing
-            synchronized (writeLock)
-            {
-               if (done)
-               {
-                  break;
-               }
-            }
-            
-            // process any pending events
-            processPendingEvents();
-            
-            // wait for new events to be added to the queue if it is still empty
-            synchronized (writeLock)
-            {
-               while (!done && eventQueue.isEmpty())
-               {
-                  try
-                  {
-                     writeLock.wait();
-                  }
-                  catch (InterruptedException exc)
-                  {
-                     // may be spurious, ignore;  if interrupted due to termination, we will
-                     // detect this in the termination check above
-                  }
-               }
-            }
-         }
+         guard.writeLock().lock();
+         try
+         {
+            if (databaseVersion.get() >= mapVersion.get())
+            {
+               return;
+            }
+            for(Iterator<Map.Entry<LockRecId, MinimalLockImpl>> it = locks.entrySet().iterator();
+                it.hasNext(); )
+            {
+               Map.Entry<LockRecId, MinimalLockImpl> e = it.next();
+               if (e.getValue().persist())
+               {
+                  it.remove();
+               }
+            }
+            databaseVersion.set(mapVersion.get());
+         }
+         finally
+         {
+            guard.writeLock().unlock();
+         }
+         
       }
-      
       /**
        * Add a record lock event to the queue, locking out other write and read operations until
        * the queue is updated. Notify a the worker thread when the new event has been added.
@@ -411,238 +380,29 @@
        */
       void addEvent(RecordLockEvent event)
       {
-         synchronized (writeLock)
-         {
-            eventQueue.add(event);
-            // DBG: System.out.println("-> " + event);
-            lastAddedID = event.getEventID();
-            writeLock.notify();
-         }
-      }
-      
-      /**
-       * This method is called within a user context to force any pending lock events to be
-       * processed. It guarantees that any events that are pending in the queue at the time this
-       * method is invoked will have been processed by the time this method returns. However,
-       * other events may be added by other user contexts in the meantime, and these are not
-       * guaranteed to have been processed.
-       */
-      void forceFlush()
-      {
-         long markerID = -1L;
-         long lastProcessed = -1L;
-         
-         synchronized (writeLock)
-         {
-            markerID = lastAddedID;
-            lastProcessed = lastProcessedID;
-         }
-         
-         synchronized (readLock)
-         {
-            while (lastProcessed < markerID)
-            {
-               try
-               {
-                  writeLock.notify();
-                  readLock.wait();
-               }
-               catch (InterruptedException exc)
-               {
-                  if (done)
-                  {
-                     break;
-                  }
-               }
-               
-               synchronized (writeLock)
-               {
-                  lastProcessed = lastProcessedID;
-               }
-            }
-         }
-      }
-      
-      /**
-       * Set a flag to terminate the queue, removing any pending events which have not yet been
-       * processed. The worker thread is interrupted.
-       * <p>
-       * This method should invoked when the primary database associated with the enclosing lock
-       * table updater is deregistered, as part of cleaning up the associated metadata database.
-       */
-      void terminate()
-      {
-         synchronized (writeLock)
-         {
-            done = true;
-            eventQueue.clear();
-            workThread.interrupt();
-         }
-      }
-      
-      /**
-       * Process all pending record lock events in the queue. Events are read and removed from the
-       * queue synchronously, such that no new events may be added during the read/remove
-       * operation. However, they are then processed asynchronously, so new events may be added
-       * while lock metadata is being updated.
-       */
-      private void processPendingEvents()
-      {
-         RecordLockEvent[] pending = null;
-         
-         synchronized (readLock)
-         {
-            // collect all pending events (if any)
-            synchronized (writeLock)
-            {
-               int size = eventQueue.size();
-               if (size > 0)
-               {
-                  pending = new RecordLockEvent[size];
-                  Iterator<RecordLockEvent> iter = eventQueue.iterator();
-                  for (int i = 0; iter.hasNext(); i++)
-                  {
-                     pending[i] = iter.next();
-                     iter.remove();
-                  }
-               }
-            }
-            
-            // process pending events (if any); note that new events can be added to the queue
-            // while we are doing this
-            if (pending != null)
-            {
-               int len = pending.length;
-               for (int i = 0; i < len; i++)
-               {
-                  processEvent(pending[i]);
-               }
-               
-               synchronized (writeLock)
-               {
-                  lastProcessedID = pending[len - 1].getEventID();
-               }
-            }
-            
-            // give any threads waiting in forceFlush a chance to proceed
-            readLock.notifyAll();
-         }
-      }
-      
-      /**
-       * Process an individual record lock event, updating the metadata lock table as appropriate.
-       * If the lock event represents a newly acquired lock, create a new lock record, otherwise
-       * find the existing lock record associated with the event, which must represent a change of
-       * the state of an existing lock. If that change of state represents a lock release, delete
-       * the associated lock record from the lock table.
-       * 
-       * @param   event
-       *          Record lock event describing a new lock or a change in state of an existing
-       *          lock.
-       */
-      private void processEvent(RecordLockEvent event)
-      {
-         Record dmo = null;
-         boolean commit = false;
-         
+         guard.readLock().lock();
          try
          {
-            // DBG: System.out.println("<- " + event);
-            commit = persistence.beginTransaction();
-            
-            LockType oldType = event.getOldType();
-            LockType newType = event.getNewType();
-            
-            if (LockType.NONE.equals(oldType))
-            {
-               dmo = createRecord(event);
-            }
-            else
-            {
-               dmo = findRecord(event);
-            }
-            
-            if (LockType.NONE.equals(newType))
-            {
-               persistence.delete(dmo);
-            }
-            else
-            {
-               String code = newType.isExclusive() ? "X" : "S"; // TODO:  get this right
-               if (oldType.isShare())
-               {
-                  code += " U"; // an upgrade
-               }
-               // From ABL online manual: values of flags field specify:
-               //    S: a share lock,
-               //    X: an exclusive lock,
-               //    U: a lock upgraded from share to exclusive,
-               //    L: a lock in limbo,
-               //    Q: a queued lock,
-               //    K: a lock kept across transaction end boundary,
-               //    J: a lock is part of a JTA transaction,
-               //    C: a lock is in create mode for JTA,
-               //    E: a lock wait timeout has expired on this queued lock.
-               proxy.setLockFlags(new character(code));
-               
-               boolean isTable = (event.getIdentifier() instanceof TableIdentifier);
-               proxy.setLockType(new character(isTable ? "TAB" : "REC"));
-               
-               persistence.save(dmo, dmo.primaryKey());
-            }
-            
-            persistence.flush();
-            
-            if (commit)
-            {
-               commit = false;   // to avoid double-rollback in catch block if commit fails
-               persistence.commit();
-            }
-            
-            if (log.isLoggable(Level.FINEST))
-            {
-               log.log(Level.FINEST, event.toString());
-            }
-         }
-         catch (Exception exc)
-         {
-            try
-            {
-               if (commit)
-               {
-                  persistence.rollback();
-               }
-            }
-            catch (PersistenceException pe)
-            {
-               // error already logged in finally block
-            }
-            finally
-            {
-               if (log.isLoggable(Level.SEVERE))
-               {
-                  String msg = String.format("Error updating _lock table for %s (%s)",
-                                             persistence.getDatabase().toString(),
-                                             event.toString());
-                  log.log(Level.SEVERE, msg, exc);
-               }
-            }
+            LockRecId lrId = new LockRecId(event);
+            MinimalLockImpl ml = locks.computeIfAbsent(lrId, k -> new MinimalLockImpl(event));
+            if (!ml.version.compareAndSet(-1, 0)) // not a new lock, should be updated
+            {
+               ml.guard.writeLock().lock();
+               try
+               {
+                  ml.update(event);
+                  ml.version.incrementAndGet();
+               }
+               finally
+               {
+                  ml.guard.writeLock().unlock();
+               }
+            }
+            mapVersion.incrementAndGet();
          }
          finally
          {
-            if (dmo != null)
-            {
-               RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
-                                                                           dmo.primaryKey());
-               try
-               {
-                  persistence.lock(LockType.NONE, metaIdent, true);
-               }
-               catch (LockUnavailableException exc)
-               {
-                  // won't be thrown from lock release
-               }
-            }
+            guard.readLock().unlock();
          }
       }
       
@@ -655,8 +415,7 @@
        * 
        * @return  See above.
        */
-      private long getLockRecordID(RecordLockEvent event)
-      throws PersistenceException
+      public long getLockRecordID(RecordLockEvent event) 
       {
          Long recordID = event.getIdentifier().getRecordID();
          long recID = (recordID != null) ? (Long) recordID : -getFileNum(event);
@@ -665,115 +424,6 @@
       }
       
       /**
-       * Find the metadata lock record associated with the user ID and lock record ID specified by
-       * the given record lock event.
-       * 
-       * @param   event
-       *          Record lock event.
-       * 
-       * @return  Metadata lock record, if found.
-       * 
-       * @throws  PersistenceException
-       *          if a lock record cannot be found for the given event, or if there is an error
-       *          executing the query.
-       */
-      private Record findRecord(RecordLockEvent event)
-      throws PersistenceException
-      {
-         long lockRec = getLockRecordID(event);
-         int lockUsr = event.getUserid().intValue();
-         Object[] args = new Object[] { lockRec, lockUsr };
-         
-         Long id = persistence.getSingleSQLResult(SQL_LOCK, args);
-         Record dmo = null;
-         
-         if (id != null)
-         {
-            RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(), id);
-            persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
-            dmo = persistence.quickLoad(metaIdent);
-         }
-         
-         if (dmo == null)
-         {
-            throw new PersistenceException("No lock record found for lock event:  " + event);
-         }
-         
-         handler.setDelegate(dmo);
-         
-         return dmo;
-      }
-      
-      /**
-       * Create a metadata lock record for the given record lock event.
-       * 
-       * @param   event
-       *          Event which specifies information to be stored in the lock record.
-       * 
-       * @return  New lock record.
-       * 
-       * @throws  PersistenceException
-       *          if there is any error querying related metadata, or if there is a reflection
-       *          error instantiating the DMO.
-       */
-      private Record createRecord(RecordLockEvent event)
-      throws PersistenceException
-      {
-         Record dmo = null;
-         
-         try
-         {
-            // create new DMO instance and back the proxy with it
-            dmo = dmoClass.newInstance();
-            dmo.initialize(null, true);
-            handler.setDelegate(dmo);
-            
-            // assign primary key
-            Long key = nextPrimaryKey++;
-            dmo.primaryKey(key);
-            
-            // get file number for table
-            Integer fileNum = getFileNum(event);
-            
-            // lock meta record for writing
-            RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(), dmo.primaryKey());
-            persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
-            
-            // set user name and user id
-            String subject = event.getOsUserName();
-            if (subject == null)
-            {
-               subject = event.getLocker().getSubject();
-            }
-            character userName = new character(subject);
-            proxy.setLockName(userName);
-            proxy.setLockUsr(event.getUserid());
-            
-            // assign other properties that will not change with new events
-            proxy.setLockId(new int64(key));    // re-use primary key
-            proxy.setLockTable(new integer(fileNum));
-            proxy.setLockRecid(new int64(getLockRecordID(event)));
-            
-            // In ProgressOE, the locking table is divided into chains anchored in a hash table.
-            // These chains are provided for fast lookup of record locks by Rec-id.
-            // FWD OTOH, relies on underlying database for recid/rowid lookup so the chain is at
-            // least irrelevant. In order to have a substitute and keep things simple, we will
-            // assume that each table has a single chain and its value is computed as an fixed
-            // offset from the table ID.
-            proxy.setLockChain(new integer(fileNum + CHAIN_OFFSET));
-         }
-         catch (InstantiationException | IllegalAccessException exc)
-         {
-            String msg = String.format("Error creating meta lock record for event %s",
-                                       event.toString());
-            
-            throw new PersistenceException(msg, exc);
-         }
-         
-         return dmo;
-      }
-      
-      /**
        * Get the file number associated with a lock event.
        * 
        * @param   event
@@ -782,35 +432,26 @@
        * @return  File number for the table associated with the event.
        */
       private Integer getFileNum(RecordLockEvent event)
-      throws PersistenceException
       {
          RecordIdentifier<String> ident = event.getIdentifier();
          String table = ident.getTable();
-         Integer fileNum = fileNums.get(table);
-         if (fileNum == null)
+         Integer fileNum;
+         if (MetadataManager.inUse(MetadataManager._FILE))
          {
-            if (MetadataManager.inUse(MetadataManager._FILE))
-            {
-               // converted table name is stored in MetaFile.userMisc
-               Object[] args = new Object[] { table };
-               fileNum = persistence.getSingleSQLResult(SQL_FILENUM, args);
-               
-               if (fileNum == null)
-               {
-                  String msg = "Table '" + table + "' not found in metadata [" + event.toString() + "]";
-                  throw new PersistenceException(msg);
-               }
-            }
-            else
-            {
-               // the _File metadata is not available
-               fileNum = 0;
-            }
+            // converted table name is stored in MetaFile.userMisc
+            fileNum = MetadataManager.getFileNum(primary, table);
             
-            // cache this to avoid future query by userMisc, which is not indexed
-            fileNums.put(table, fileNum);
-         }
-         
+            if (fileNum == null)
+            {
+               String msg = "Table '" + table + "' not found in metadata [" + event.toString() + "]";
+               throw new IllegalStateException(msg);
+            }
+         }
+         else
+         {
+            // the _File metadata is not available
+            fileNum = 0;
+         }
          return fileNum;
       }
       
@@ -855,6 +496,311 @@
             this.delegate = delegate;
          }
       }
+
+      /** In-memory holder for the _Lock VST data*/ 
+      private class MinimalLockImpl
+      {
+         /** Lock guard */
+         public final ReentrantReadWriteLock guard = new ReentrantReadWriteLock();
+         /** Lock version */
+         public final AtomicLong version = new AtomicLong(-1);
+         /** Lock version last persisted */
+         public final AtomicLong persisted = new AtomicLong(-1);
+         
+         /** Primary key */
+         public final long key;
+         
+         /** Lock Id */
+         public final int64 lockId;
+         
+         /** User */
+         public final integer lockUsr;
+         
+         /** Lock name */
+         public final character lockName;
+         
+         /** Table fileNum */ 
+         public final integer lockTable;
+         
+         /** Record Id */
+         public final int64 lockRecid;
+         
+         /** Chain */
+         public final integer lockChain;
+
+         /** Old lock type */
+         private LockType oldType;
+         
+         /** New lock type */
+         private LockType newType;
+         
+         /** Lock type string */
+         private character lockType;
+         
+         /** Lock flags */
+         private character lockFlags;
+         
+         /** Event string representation (for logging) */
+         private String eventString;
+         
+         /**
+          * Constructor
+          * 
+          * @param event
+          *        Lock event
+          */
+         public MinimalLockImpl(RecordLockEvent event)
+         {
+            key = nextPrimaryKey.incrementAndGet();
+
+            // set user name and user id
+            String subject = event.getOsUserName();
+            if (subject == null)
+            {
+               subject = event.getLocker().getSubject();
+            }
+            character userName = new character(subject);
+            lockName = userName;
+            lockUsr = event.getUserid();
+            
+            // get file number for table
+            Integer fileNum = getFileNum(event);
+
+            // assign other properties that will not change with new events
+            lockId = new int64(key);    // re-use primary key
+            lockTable = new integer(fileNum);
+            lockRecid = new int64(getLockRecordID(event));
+            
+            // In ProgressOE, the locking table is divided into chains anchored in a hash table.
+            // These chains are provided for fast lookup of record locks by Rec-id.
+            // FWD OTOH, relies on underlying database for recid/rowid lookup so the chain is at
+            // least irrelevant. In order to have a substitute and keep things simple, we will
+            // assume that each table has a single chain and its value is computed as an fixed
+            // offset from the table ID.
+            lockChain = new integer(fileNum + CHAIN_OFFSET);
+            
+            update(event);
+         }
+         
+         /** 
+          * Update holder with event data
+          * @param event 
+          *        lock event
+          *  
+          */ 
+         public void update(RecordLockEvent event)
+         {
+            this.oldType = event.getOldType();
+            this.newType = event.getNewType();
+            String code = newType.isExclusive() ? "X" : "S"; // TODO:  get this right
+            if (oldType.isShare())
+            {
+               code += " U"; // an upgrade
+            }
+            // From ABL online manual: values of flags field specify:
+            //    S: a share lock,
+            //    X: an exclusive lock,
+            //    U: a lock upgraded from share to exclusive,
+            //    L: a lock in limbo,
+            //    Q: a queued lock,
+            //    K: a lock kept across transaction end boundary,
+            //    J: a lock is part of a JTA transaction,
+            //    C: a lock is in create mode for JTA,
+            //    E: a lock wait timeout has expired on this queued lock.
+            this.lockFlags = new character(code);
+            
+            boolean isTable = (event.getIdentifier() instanceof TableIdentifier);
+            this.lockType = new character(isTable ? "TAB" : "REC");
+            this.eventString = event.toString();
+         }
+         
+         /** Create record in the _Lock VST */
+         private Record createRecord() throws PersistenceException
+         {
+            Record dmo = null;
+
+            try
+            {
+               // create new DMO instance and back the proxy with it
+               dmo = dmoClass.newInstance();
+               dmo.initialize(null, true);
+               handler.setDelegate(dmo);
+
+               // assign primary key
+               dmo.primaryKey(key);
+               // lock meta record for writing
+               RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
+                        dmo.primaryKey());
+               persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
+               proxy.setLockName(lockName);
+               proxy.setLockUsr(lockUsr);
+               proxy.setLockId(lockId); // re-use primary key
+               proxy.setLockTable(lockTable);
+               proxy.setLockRecid(lockRecid);
+               proxy.setLockChain(lockChain);
+            }
+            catch (InstantiationException | IllegalAccessException exc)
+            {
+               String msg = String.format("Error creating meta lock record for event %s",
+                        eventString);
+
+               throw new PersistenceException(msg, exc);
+            }
+            return dmo;
+         }
+         
+
+         /** Find record in the _Lock VST */
+         private Record findRecord() throws PersistenceException
+         {
+            Object[] args = new Object[] { lockRecid, lockUsr };
+
+            Long id = persistence.getSingleSQLResult(SQL_LOCK, args);
+            Record dmo = null;
+
+            if (id != null)
+            {
+               RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
+                        id);
+               persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
+               dmo = persistence.quickLoad(metaIdent);
+            }
+
+            if (dmo == null)
+            {
+               throw new PersistenceException(
+                        "No lock record found for lock event:  " + eventString);
+            }
+
+            handler.setDelegate(dmo);
+
+            return dmo;
+         }
+
+         /** 
+          * Persist lock data in the _Lock VST 
+          * @return <code>true</true> if the record is deleted
+          */
+         public boolean persist()
+         {
+            
+            boolean deleted = false;
+            if (persisted.get() >= version.get())
+            {
+               return deleted;
+            }
+            Record dmo = null;
+            boolean commit = false;
+            
+            try
+            {
+               commit = persistence.beginTransaction();
+               
+               if (LockType.NONE.equals(oldType))
+               {
+                  dmo = createRecord();
+               }
+               else
+               {
+                  dmo = findRecord();
+                  if (LockType.NONE.equals(newType))
+                  {
+                     persistence.delete(dmo);
+                     deleted = true;
+                  }
+                  else
+                  {
+                     proxy.setLockFlags(lockFlags);
+                     proxy.setLockType(lockType);
+                     persistence.save(dmo, dmo.primaryKey());
+                  }
+               }
+
+               persistence.flush();
+               
+               if (commit)
+               {
+                  commit = false;   // to avoid double-rollback in catch block if commit fails
+                  persistence.commit();
+               }
+               persisted.set(version.get());
+            }
+            catch (Exception exc)
+            {
+               try
+               {
+                  if (commit)
+                  {
+                     persistence.rollback();
+                  }
+               }
+               catch (PersistenceException pe)
+               {
+                  // error already logged in finally block
+               }
+               finally
+               {
+                  if (log.isLoggable(Level.SEVERE))
+                  {
+                     String msg = String.format("Error updating _lock table for %s (%s)",
+                                                persistence.getDatabase().toString(),
+                                                eventString);
+                     log.log(Level.SEVERE, msg, exc);
+                  }
+               }
+            }
+            finally
+            {
+               if (dmo != null)
+               {
+                  RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
+                                                                              dmo.primaryKey());
+                  try
+                  {
+                     persistence.lock(LockType.NONE, metaIdent, true);
+                  }
+                  catch (LockUnavailableException exc)
+                  {
+                     // won't be thrown from lock release
+                  }
+               }
+            }
+            return deleted;
+         }
+      }
+
+      /** Lock record id */
+      private class LockRecId
+      implements Comparable<LockRecId>
+      {
+         public final long lockRec;
+         public final int lockUsr;
+         
+         /**
+          * Constructor
+          * @param event
+          *        Lock event
+          */
+         public LockRecId(RecordLockEvent event) 
+         {
+            this.lockRec = getLockRecordID(event);
+            this.lockUsr = event.getUserid().intValue();
+         }
+
+         /**
+          * Compare with ther record id
+          * @return -1 if other record id is greater, 1 if other record is less, 0 if ids are equal
+          */
+         @Override
+         public int compareTo(LockRecId o)
+         {
+            return this.lockRec < o.lockRec ? -1 : 
+                   this.lockRec > o.lockRec ?  1 : 
+                      this.lockUsr < o.lockUsr ? -1 :
+                      this.lockUsr > o.lockUsr ?  1 : 0;
+         }
+         
+      }
    }
    
    /**
@@ -986,4 +932,5 @@
        */
       public void setLockFlags(Text lockFlags);
    }
+   
 }

=== modified file 'src/com/goldencode/p2j/persist/meta/MetadataManager.java'
--- src/com/goldencode/p2j/persist/meta/MetadataManager.java	2022-03-01 23:56:49 +0000
+++ src/com/goldencode/p2j/persist/meta/MetadataManager.java	2022-03-24 18:40:04 +0000
@@ -39,7 +39,8 @@
 **     CA  20210709 Set the _File.desc from the Table.description() annotation.
 **     ECF 20210910 Code cleanup.
 **     OM  20220301 Correctly set unknown value as initial value of the field.
-*/
+**     IAS 20220324 Added FILE_NUM_BY_TBL_NAME map.
+**/
 
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -301,6 +302,10 @@
    private static final ConcurrentMap<Database, ConcurrentMap<Class<?>, integer>> FILE_NUM =
          new ConcurrentHashMap<>();
    
+   /** _File._File_num values for the primary databases by table name */
+   private static final ConcurrentMap<Database, ConcurrentMap<String, Integer>> FILE_NUM_BY_TBL_NAME =
+         new ConcurrentHashMap<>();
+
    /** Word tables' names for the primary databases */
    private static final ConcurrentMap<String, Map<String, Map<String, WordIndexData>>> WORD_TABLES =
          new ConcurrentHashMap<>();
@@ -353,6 +358,20 @@
    }
    
    /**
+    * Get _File._File_num value for the table
+    *  
+    * @param db
+    *        primary db
+    * @param tableName
+    *        Table nbame
+    * @return _File._File_num value for the table
+    */
+   public static Integer getFileNum(Database db, String tableName)
+   {
+      return FILE_NUM_BY_TBL_NAME.computeIfAbsent(db, d -> new ConcurrentHashMap<>()).get(tableName);
+   }
+
+   /**
     * Initialize this class and start up a secure database server to allow remote P2J servers to
     * access metadata.
     * 
@@ -821,6 +840,8 @@
       templateRecords.put(dmoImpl, -fileNum);
       FILE_NUM.computeIfAbsent(primary, d -> new ConcurrentHashMap<>()).
             putIfAbsent(dmoImpl, new integer(fileNum));
+      FILE_NUM_BY_TBL_NAME.computeIfAbsent(primary, d -> new ConcurrentHashMap<>()).
+            putIfAbsent(dmoIface.getAnnotation(Table.class).name(), (int)fileNum);
       saveChildren(p, children);
    }
    

=== modified file 'src/com/goldencode/p2j/persist/orm/FqlToSqlConverter.java'
--- src/com/goldencode/p2j/persist/orm/FqlToSqlConverter.java	2022-02-12 01:08:04 +0000
+++ src/com/goldencode/p2j/persist/orm/FqlToSqlConverter.java	2022-03-24 18:37:44 +0000
@@ -30,6 +30,7 @@
 **     IAS 20210905 Re-working re-writing queries with the values of the mutable SESSION attributes.
 **     OM  20210908 Used SchemaDictionary.TEMP_TABLE_DB instead of hardcoded constant.
 **     OM  20220212 Use ReservedProperty static method for testing reserved properties names.
+**     IAS 20220324 Re-worked LockTableUpdater.
 */
 
 /*
@@ -203,6 +204,9 @@
    /** Flag indicating that the query reads from _UserTableStat VST */
    private boolean userTableStatRead = false;
 
+   /** Flag indicating that the query reads from _Lock VST */
+   private boolean lockTableRead = false;
+
    /** Re-write data for CONTAINS operators */
    private final List<ContainsRewriteData> containsRewriteData = new ArrayList<>(); 
    
@@ -334,6 +338,16 @@
    }
 
    /**
+    * Check if the query reads _Lock VST. 
+    * 
+    * @return <code>true</code> if the query reads _Lock VST.
+    */
+   public boolean isLockTableRead()
+   {
+      return lockTableRead;
+   }
+
+   /**
     * Re-write CONTAINS using word tables and CTE
     * 
     * @param   crwd
@@ -2881,6 +2895,10 @@
                {
                   userTableStatRead = true;
                }
+               if (dmoInfo.isVST(SystemTable._Lock))
+               {
+                  lockTableRead = true;
+               }
                if (alias == null || alias.getType() == ALIAS)
                {
                   String aliasName;

=== modified file 'src/com/goldencode/p2j/persist/orm/Query.java'
--- src/com/goldencode/p2j/persist/orm/Query.java	2022-03-04 19:39:40 +0000
+++ src/com/goldencode/p2j/persist/orm/Query.java	2022-03-24 18:38:09 +0000
@@ -18,6 +18,7 @@
 **     IAS 20211223 Make getDialect(Session session) package private.
 **     CA  20220304 Throw a IllegalStateException if the firstResult or maxResults parameter can't be set for
 **                  the query.
+**     IAS 20220324 Re-worked LockTableUpdater.
 */
 
 /*
@@ -77,6 +78,8 @@
 
 import java.util.*;
 import java.util.function.*;
+
+
 import com.goldencode.p2j.util.*;
 import com.goldencode.p2j.persist.*;
 import com.goldencode.p2j.persist.dialect.*;
@@ -102,6 +105,9 @@
    /** Flag indicating that the query reads from _UserTableStat VST */
    private boolean userTableStatRead = false;
 
+   /** Flag indicating that the query reads from _Lock VST */
+   private boolean lockTableRead = false;
+
    /**
     * The maximum number of results accepted by the (SELECT) query. Used with {@code firstResult}
     * to configure result pagination.
@@ -333,6 +339,7 @@
          paramCount = fql2sql.getLastConversionParamCount();
          wasRewritten = fql2sql.isQueryWasRewritten();
          userTableStatRead = fql2sql.isUserTableStatRead();
+         lockTableRead = fql2sql.isLockTableRead();
          if (!wasRewritten)
          {
             cache.accept(this);
@@ -375,6 +382,15 @@
       {
          UserTableStatUpdater.persist(db);
       }
+      if (lockTableRead)
+      {
+         LockTableUpdater ltu = DatabaseManager.getLockTableUpdater(db);
+         if (ltu != null)
+         {
+            ltu.persist();
+         }
+         UserTableStatUpdater.persist(db);
+      }
       return sqlQuery.expandPlaceholders();
    }
    

