MetadataManager.java

/*
** Module   : MetadataManager.java
** Abstract : Manages databases which contain metadata information
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20131001 Created initial version.
** 002 ECF 20131028 Moved to meta sub-package.
** 003 ECF 20140315 Fixed path from which remote port is read in directory.
** 004 ECF 20141204 Use schema name rather than physical database name to load metaschema data.
**                  Do not initialize the metadata manager (and server) if no metadata is in use.
** 005 OM  20151130 Adjusted import for H2Helper.
** 006 ECF 20160225 Changes required by new PropertyHelper implementation.
** 007 OM  20160722 Added support for _file._template. Use caching for fast access.
** 008 OM  20160712 Added setupIdentityManager() method for creating p2j_id_generator_sequence.
** 009 OM  20181207 Populated DatabaseFeature metadata table.
** 010 IAS 20200518 Populating _File, _Field, _Index, and _Index-Field metadata tables.
** 011 IAS 20200604 Added support for *-SA and CAN-* fields in _File/_Field tables.
** 012 IAS 20200617 No fatal error if p2j.cfg.xml is not found; mandatory meta tables support
** 013 ECF 20200906 New ORM implementation.
** 014 IAS 20200622 Re-worked metadata configuration in p2j.cfg.xml.
**     IAS 20200624 Added _Field._View-As field population.
**     CA  20200706 Flush the session before doing the commit, to release the DMOs from Hibernate.
**     IAS 20200809 Added _Field.(_Charset, _Collation, _Decimals, _Desc, _dtype, _Fetch-Type, _Fld-case,
**                  _Sys_field, _Width)  fields population.
**     GES 20200810 Removed Apache dependency.
**     IAS 20200819 Added more mandatory tables.
**     OM  20200924 Index components carry multiple information to avoid map lookups for them.
**     CA  20200924 Replaced Method.invoke with ReflectASM.
**     CA  20200927 Use IdentityHashMap instead of plain map when the key is a Class.
**     OM  20201001 Improved DMO manipulation performance by caching slow Property annotation access.
**     CA  20201003 Use an identity HashSet where possible.
**     OM  20201012 Force use locally cached meta information instead of map lookup.
**     IAS 20201210 Added support for word tables.
**     IAS 20210409 Added option to disable SSL for the remote-meta mode.
**     IAS 20210419 Population of the _Area, _Filelist, and _Tenant metatables.
**     IAS 20220525 Added metadata reset. 
**     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.
**     IAS 20220509 Add _User table to the FILE_NUM and FILE_NUM_BY_TBL_NAME maps.
**     OM  20220516 A bit of code cleanup.
**     ECF 20220630 Fixed hard-coded references to converted metadata column/property names which have
**                  changed.
**     TJD 20220504 Upgrade do Java 11 minor changes
**     SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
**                  performance.
** 015 OM  20230411 Fixed support for DUMP-NAME table attribute.
** 016 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 017 OM  20240229 Fixed the meta field type for datetime-tz.
** 018 ES  20240229 Get rid of parsing of the *.meta.xml files for population of the MetaDatabase,
**                  and made use of DmoMeta, Property, P2JIndex and annotations.
**     OM  20240311 Bug fixes. Simplified code. Maintenance.
**     OM  20240314 Added support for dynamic permanent tables in addTableToFile().
** 019 OM  20240402 Improved management for mutable permanent tables.
** 020 OM  20240422 Improved logging for tables from mutable databases.
** 021 OM  20240521 Forced a short-lived local transaction in removeDynamicTable() if none is active.
** 022 TJD 20240123 Java 17 compatibility updates
** 023 RAA 20240807 Redirected tenant database to core database in getFileNum.
** 024 OM  20240815 TenantManager.getDatabase() returns a Database object of same type as the argument.
**     OM  20240909 Basic implementation of populateTenant() method.
** 025 OM  20241001 Improved _tenant metadata support.
** 026 OM  20240708 Simplified log for creation of table meta information.
**     OM  20241021 Added support for initializing extent fields/properties in meta tables.
** 027 OM  20241031 Marked _sec-authentication-system and _sec-authentication-domain tables as REGULAR.
** 028 ICP 20250123 Used integer, logical and character constants to leverage caches instances.
** 029 DDF 20250219 Added filterTables() to replace mandatoryTables().
**     DDF 20250221 Corrected the filterTables() condition.
** 030 OM  20250128 Added synchronization of _Tenant table with landlord database via TenantTableUpdater.
** 031 OM  20250310 Added support for READ/ONLY metadata tables.
** 032 OM  20250331 Make sure the [byName] map's keys are in lowercase. Avoid possible duplication of default
**                  tenant record in _tenant meta table.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist.meta;

import java.io.*;
import java.lang.reflect.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.orm.Property;
import com.goldencode.p2j.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.persist.annotation.Trigger;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.orm.DmoMeta.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.CentralLogger;

/**
 * This class creates, prepares, and populates the embedded database(s) used to serve metadata
 * information to converted business logic and to the persistence framework. All methods are
 * static; the class is never instantiated. Most methods are only called during server startup.
 * The {@link #isMetaDMO(Class)} method, however, is meant to be used by callers who need to
 * determine whether a particular DMO interface is metadata-related.
 */
public final class MetadataManager
{
   /** Well-known name of metadata schema */
   public static final String META_SCHEMA = "_meta";
   
   /** Well-known prefix of metadata DMOs */
   public static final String META_PREFIX = "Meta";
   
   /** Well-known name of DDL script to create tables */
   public static final String DDL_TABLE = "meta_table_ddl.sql";
   
   /** Well-known name of DDL script to create indexes */
   public static final String DDL_INDEX = "meta_index_ddl.sql";
   
   /** Constant used to identify the {@code _File} meta table. Used a key in {@code helpers}. */
   public static final String _FILE = "File";
   
   /** Constant used to identify the {@code _Db} meta table. Used a key in {@code helpers}. */
   public static final String _DB = "Db";
   
   /** Constant used to identify the {@code _Lock} meta table. Used a key in {@code helpers}. */
   public static final String _LOCK = "Lock";
   
   /** Constant used to identify the {@code _MyConnection} meta table. Used a key in {@code helpers}. */
   public static final String _MYCONNECTION = "Myconnection";
   
   /** Constant used to identify the {@code _Connect} meta table. Used a key in {@code helpers}. */
   public static final String _CONNECT = "Connect";
   
   /** Constant used to identify the {@code _Trans} meta table. Used a key in {@code helpers}. */
   public static final String _TRANS = "Trans";
   
   /** Constant used to identify the {@code _Index} meta table. Used a key in {@code helpers}. */
   public static final String _INDEX = "Index";
   
   /** Constant used to identify the {@code _Index} meta table. Used a key in {@code helpers}. */
   public static final String _INDEX_FIELD = "IndexField";
   
   /** Constant used to identify the {@code _Field} meta table. Used a key in {@code helpers}. */
   public static final String _FIELD = "Field";
   
   /** Constant used to identify the {@code _UserTableStat} meta table. Used a key in {@code helpers}. */
   public static final String _USERTABLESTAT = "Usertablestat";
   
   /** Logger */
   private static final CentralLogger log = CentralLogger.get(MetadataManager.class);
   
   /** Do we log WARNING-level events ? */
   private static final boolean LOG_WARNING = log.isLoggable(Level.WARNING);
   
   /** Do we log INFO-level events ? */
   private static final boolean LOG_INFO = log.isLoggable(Level.INFO);
   
   /** The recid of the database in {@code _Db} table. */
   private static final int DB_RECID = 4032;
   
   /** Map of data type names to associated _Field._dtype value */
   private static final Map<String, Integer> DTYPE = Collections.unmodifiableMap(
         new HashMap<String, Integer>() 
         {{
            put("character", 1);
            put("date", 2);
            put("logical", 3);
            put("integer", 4);
            put("decimal", 5);
            put("recid", 7);
            put("raw", 8);
            put("blob", 18);
            put("clob", 19);
            put("short", 22);
            put("float", 24);
            put("double", 25);
            put("fixchar", 31);
            put("bigint", 32);
            put("time", 33);
            put("timestamp", 34);
            put("datetime", 34);
            put("datetime-tz", 40);
            put("int64", 41);
         }}
   );

   /** Map of data type names to associated _Field._fetch-type value */
   private static final Map<String, String> FETCH_TYPE = Collections.unmodifiableMap(
         new HashMap<String, String>() 
         {{
            put("character", "varchar");
            put("date", "date");
            put("logical", "bit");
            put("integer", "integer");
            put("decimal", "numetic");
            put("recid", "integer");
            put("raw", "varbinary");
            put("blob", "varbinary");
            put("clob", "varchar");
            put("short", "smallint");
            put("float", "float");
            put("double", "real");
            put("fixchar", "character");
            put("bigint", "bigint");
            put("time", "time");
            put("timestamp", "timestamp");
            put("datetime", "timestamp");
            put("datetime-tz", "timestamp_timezone");
            put("int64", "bigint");
         }}
   );

   /** Character field types */
   private static final Set<String> CHAR_TYPES = Collections.unmodifiableSet(
         new HashSet<String>()
         {{
           add("character");
           add("fixchar");
           add("clob");
         }}
   );
   
   /** Map of short meta DMO names to helper objects */
   private static final Map<String, Helper> helpers = new HashMap<>();
   
   /** Map that stores the default (logical) database for a tenant database.  */
   private static final Map<Database, Database> coreDatabases = new HashMap<>();
   
   /** Set of DMO interfaces which are metadata DMOs */
   private static final Set<Class<? extends DataModelObject>> dmoMetaTypes =
         Collections.newSetFromMap(new IdentityHashMap<>());
   
   /** Base directory for DMO resources in jar file */
   private static final String baseDir = DmoMetadataManager.getDmoBasePackage().replace('.', '/');
   
   /** Stores the status of the metadata server. */
   private static boolean inUse = false;
   
   /**
    * Cached the template records ({@code _file._template} values) for fast access. The keys are
    * the DMO implementation classes of the tables from permanent database. The values are the
    * {@link rowid} values as {@link Long} data.
    */
   private static final Map<Class<? extends Record>, Long> templateRecords = new IdentityHashMap<>();
   
   /** _File table ID generator */
   private static final ConcurrentMap<Database, ConcurrentMap<String, AtomicLong>> TABLE_IDS =
         new ConcurrentHashMap<>();
   
   /** _File._File_num values for the primary databases */
   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<>();
   
   /** The system tables indexed by their legacy name. The keys are in lowercase. */
   private static final Map<String, SystemTable> byName = new HashMap<>();
   
   /**
    * Private constructor; this class is not instantiated.
    */
   private MetadataManager()
   {
   }
   
   /**
    * Reset in-memory metadata.
    * 
    * @param primaryDB
    *        primary database
    * @param metaDB
    *        meta database
    */
   public static void reset(Database primaryDB, Database metaDB)
   {
      TABLE_IDS.remove(metaDB);
      FILE_NUM.remove(primaryDB);
   }
   
   /**
    * Indicate whether the given DMO interface represents a metaschema table.
    * 
    * @param   dmoIface
    *          DMO interface to test.
    * 
    * @return  {@code true} if {@code dmoIface} represents a metaschema table, else {@code false}.
    */
   public static boolean isMetaDMO(Class<? extends DataModelObject> dmoIface)
   {
      return dmoMetaTypes.contains(dmoIface);
   }
   
   /**
    * Obtain a {@code SystemTable} by its legacy name.
    * 
    * @param   legacyName
    *          The legacy name.
    *
    * @return  The metadata {@code SystemTable} with specified legacy name, if one exists, 
    *          otherwise {@code null}.
    */
   public static SystemTable byLegacyName(String legacyName)
   {
      return byName.get(legacyName.toLowerCase());
   }
   
   /**
    * Get _File._File_num value for the table
    *  
    * @param db
    *        primary db
    * @param dmoImpl
    *        Table DMO class
    * @return _File._File_num value for the table
    */
   public static integer getFileNum(Database db, Class<?> dmoImpl)
   {
      return FILE_NUM.computeIfAbsent(db, d -> new ConcurrentHashMap<>()).get(dmoImpl);
   }
   
   /**
    * Get _File._File_num value for the table
    *
    * @param   db
    *          The primary database.
    * @param   tableName
    *          The converted table name.
    *
    * @return  _File._File_num value for the table
    */
   public static Integer getFileNum(Database db, String tableName)
   {
      if (TenantConfig.isEnabled())
      {
         // redirect the tenant database to the core one
         Database coreDb = coreDatabases.get(db);
         if (coreDb == null)
         {
            String ldb = ConnectionManager.get().getLDBName(db);
            if (ldb == null)
            {
               ldb = db.getName();
            }
            coreDb = TenantManager.getDatabase(TenantManager.DEFAULT_TENANT_NAME, ldb, true, db.getType());
            coreDatabases.put(db, coreDb);
         }
         
         db = coreDb;
      }
      
      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.
    * 
    * @return  <code>true</code> if the metadata manager was initialized; <code>false</code> if
    *          there are no metadata classes found.
    * 
    * @throws  PersistenceException
    *          if there is any error initializing the class or starting the server.
    */
   @SuppressWarnings("unchecked")
   public static boolean initialize()
   throws PersistenceException
   {
      boolean active;
      try
      {
         active = Configuration.getSchemaConfig().isMetadataActive();
      }
      catch (Exception e)
      {
         // TODO: read active metadata and meta files from directory 
         active = false;
      }
      
      if (!active)
      {
         if (LOG_INFO)
         {
            log.log(Level.INFO, "Metadata server was not started; no metadata in use");
         }
         return false;
      }
      
      Set<String> legacyTables = SystemTable.filterTables(SystemTable.MANDATORY);
      SchemaConfig.Metadata cfgMetadata = Configuration.getSchemaConfig().getMetadata();
      if (cfgMetadata != null)
      {
         legacyTables.addAll(cfgMetadata.getTables());
      }
      for (String aTable : legacyTables)
      {
         SystemTable systemTable = byLegacyName(aTable);
         if (systemTable == null)
         {
            // TODO: log a warning ?
            continue;
         }
         if (systemTable.isRegular())
         {
            // the regular tables are merged in each database, there is no such DMO in meta database
            continue;
         }
         
         String shortName = getShortName(aTable);
         String dmoIfaceName = 
               DmoMetadataManager.getDmoBasePackage() + "." + META_SCHEMA + "." + META_PREFIX + shortName; 
         
         Class<? extends DataModelObject> dmoIface;
         Class<? extends Record> dmoImpl;
         try
         {
            dmoIface = (Class<? extends DataModelObject>) Class.forName(dmoIfaceName);
            DmoMeta dmoMeta = DmoMetadataManager.registerDmo(dmoIface);
            dmoImpl = dmoMeta.getImplementationClass();
         }
         catch (Exception e)
         {
            // catch all error cases; just skip the table, but let a message in log
            if (LOG_WARNING)
            {
               log.log(Level.WARNING, "Failed to load _meta DMO: " + dmoIfaceName, e);
            }
            continue;
         }
         
         helpers.put(shortName, new Helper(dmoIface, dmoImpl));
         dmoMetaTypes.add(dmoIface);
      }
      
      if (LOG_INFO)
      {
         log.log(Level.INFO, "Metadata server modules: " + helpers.keySet());
      }
      
      // start the server for mixed mode connections
      DirectoryService ds = DirectoryService.getInstance();
      if (!ds.bind())
      {
         throw new PersistenceException("Directory bind failed");
      }
      try
      {
         if (Utils.getDirectoryNodeBoolean(ds, "persistence/remote-meta/enabled", false, false))
         {
            int port = Utils.getDirectoryNodeInt(ds,
                                                 "persistence/remote-meta/port",
                                                 DatabaseManager.MIXED_MODE_SSL_PORT,
                                                 false);
            String portString = String.valueOf(port);
            if (LOG_INFO)
            {
               log.log(Level.INFO,
                       "Starting secure, mixed mode, H2 metaschema server on port " +
                       portString + "...");
            }
            if (Utils.getDirectoryNodeBoolean(ds, "persistence/remote-meta/ssl", true, false))
            {
               H2Helper.startMixedModeServer("-tcpSSL", "-tcpPort", portString, "-tcpAllowOthers");
            }
            else
            {
               H2Helper.startMixedModeServer("-tcpPort", portString, "-tcpAllowOthers");
            }
            if (LOG_INFO)
            {
               log.log(Level.INFO, "Successfully started secure mixed mode H2 server");
            }
         }
      }
      catch (PersistenceException exc)
      {
         log.log(Level.SEVERE, "Error starting mixed mode, remote metaschema server", exc);
         
         throw exc;
      }
      finally
      {
         ds.unbind();
      }
      
      inUse = true;
      return true;
   }
   
   /**
    * Checks the status of one of the tables of the metadata subsystem. Note that this method
    * returns {@code true} from the moment the {@link #initialize()} has finished with success.
    * At that moment the whole metadata system is not yet ready as the metadata tables are not yet
    * populated. 
    * <p>
    * Use this method to check if the project was not configured with this feature or if metadata
    * initialization failed, and quickly leave the calling method in these cases. If this method
    * returns {@code false} it means that:
    * <ul>
    *    <li>metadata was completely disabled from server configuration; or</li>  
    *    <li>the specified table was not added to metadata block in static config file; or</li>  
    *    <li>the DMO for the specified _meta table was not converted, not found in classpath
    *          or failed to load and register as a standard DMO interface.</li>
    * </ul>
    *  
    * @param   meta
    *          The short name (see {@code _<name>} constants in this class) of a metadata table.
    * 
    * @return  {@code true} if the {@code MetadataManager} was successfully initialized, queries
    *          on internal data are possible and the specified {@code meta} table is active in the
    *          current configuration.
    *          Returns {@code false} if the initialization failed, the metadata is not "in use" or
    *          the specified table does not appear in the {@code metadata} block of the current
    *          config file.
    */
   public static boolean inUse(String meta)
   {
      return MetadataManager.inUse && helpers.containsKey(meta);
   }
   
   /**
    * Checks the status of the metadata subsystem. Note that this method returns {@code true} from
    * the moment the {@link #initialize()} has finished with success. At that moment the whole
    * metadata system is not yet ready as the metadata tables are not yet populated. 
    * <p>
    * Use this method to check if the project was not configured with this feature or if metadata
    * initialization failed, and quickly leave the calling method in these cases.
    *  
    * @return  {@code true} if the {@code MetadataManager} was successfully initialized and
    *          queries on internal data are possible and {@code false} if the initialization
    *          failed and the metadata is not "in use".
    */
   public static boolean inUse()
   {
      return MetadataManager.inUse;
   }
   
   /**
    * Create and load the embedded metadata database for the associated primary database.
    * start up a secure database server to allow remote P2J servers to access this metadata.
    * 
    * @param   database
    *          Database information object.
    * 
    * @throws  PersistenceException
    *          if there is any error creating or loading the database.
    */
   public static void prepareDatabase(Database database)
   throws PersistenceException
   {
      if (!Configuration.getSchemaConfig().isMetadataActive())
      {
         return;
      }
      
      try (Session session = new Session(database))
      {
         Connection conn = session.getConnection();
         
         // define schema
         executeDDLScript(database, conn, DDL_TABLE);
         
         // apply indexes
         executeDDLScript(database, conn, DDL_INDEX);
      }
      catch (PersistenceException exc)
      {
         DBUtils.handleException(database, exc);
         
         throw exc;
      }
   }
   
   /**
    * Set up sequence identity mManager for this database by creating the fresh instance of 
    * {@code p2j_id_generator_sequence}. Because it is repopulated at each startup the start value
    * of the sequence is not crucial; the value is hardcoded to 10000. 
    * 
    * @param   database
    *          The database to set up the identity manager for.
    * @param   dialect
    *          The dialect for the database. It is used to build the DDL for the sequence create
    *          statement.
    *          
    * @throws  PersistenceException
    *          if something goes wrong with database connection. 
    */
   public static void setupIdentityManager(Database database, Dialect dialect)
   throws PersistenceException
   {
      try (Session session = new Session(database))
      {
         String seq_ddl = dialect.getCreateSequenceString(Persistence.ID_GEN_SEQUENCE, 10000);
         Session.createSQLQuery(seq_ddl).executeUpdate(session);
      }
   }
   
   /**
    * Populate the specified, embedded database with metaschema information read from DMO interface
    * annotations.
    * 
    * @param   metaDb
    *          Metaschema database to populate.
    * 
    * @throws  PersistenceException
    *          if there is any problem reading the DMO annotations, instantiating DMO instances, or
    *          inserting records into the database.
    */
   public static void populateDatabase(Database metaDb)
   throws PersistenceException
   {
      if (!Configuration.getSchemaConfig().isMetadataActive())
      {
         if (LOG_INFO)
         {
            log.log(Level.INFO, "Metadata server was not started; no metadata in use");
         }
         return;
      }
      
      if (!inUse(_FILE))
      {
         log.log(Level.WARNING,
                 _FILE + " metadata table not present. Metaschema support for '" +
                 metaDb.getName() + "' database disabled.");
         return;
      }
      
      try
      {
         Persistence p = PersistenceFactory.getInstance(metaDb);
         p.beginTransaction(Persistence.META_CTX);
         
         SystemTable.populateAll(metaDb, p);
         p.commit(Persistence.META_CTX);
         p.executeSQL("analyze", Persistence.META_CTX);
      }
      catch (Exception exc)
      {
         throw new PersistenceException("Error populating metadata database " + metaDb, exc);
      }
      
      if (LOG_INFO)
      {
         log.log(Level.INFO, "Metaschema database " + metaDb + " ready");
         log.log(Level.INFO, "Caching " + templateRecords.size() + " template record ids");
      }
   }
   
   /**
    * Get the word index data.
    *
    * @param   dbName
    *          Database name.
    * @param   tableName
    *          Master table name.
    * @param   fieldName
    *          Field name.
    * 
    * @return  The word index data.
    */
   public static WordIndexData getWordTableName(String dbName, String tableName, String fieldName)
   {
      Map<String, Map<String, WordIndexData>> tables = WORD_TABLES.get(dbName);
      if (tables == null)
      {
         return null;
      }
      Map<String, WordIndexData> fields = tables.get(tableName);
      if (fields == null)
      {
         return null;
      }
      return fields.get(fieldName);
   }
   
   /**
    * Adds the details of a dynamic permanent table to meta tables.
    * 
    * @param   primaryDb
    *          The primary database where the table belongs to.
    * @param   dmoName
    *          The DMO of the dynamic table.
    */
   public static void addDynamicTableToFile(Database primaryDb, String dmoName)
   {
      Helper fileHelper = getHelper(SystemTable._File.shortName);
      if (fileHelper == null)
      {
         return;
      }
      
      Database metaDB = new Database(primaryDb.getName(), Database.Type.META, true);
      Persistence p = PersistenceFactory.getInstance(metaDB);
      addTableToFile(primaryDb, metaDB, dmoName, fileHelper, p);
   }
   
   /**
    * Removes a registered dynamic permanent table from this manager.
    * 
    * @param   primaryDb
    *          The primary database
    * @param   dmoMeta
    *          The meta information of the table to be removed.
    */
   public static void removeDynamicTable(Database primaryDb, DmoMeta dmoMeta)
   {
      // drop WORD indices, if any
      Map<String, WordIndexData> wt = dmoMeta.getWordTables();
      if (!wt.isEmpty())
      {
         Map<String, Map<String, WordIndexData>> schemaMap = WORD_TABLES.get(dmoMeta.schema /*primaryDb ?*/);
         if (schemaMap != null)
         {
            schemaMap.remove(dmoMeta.sqlTable);
         }
      }
      
      integer fileNum1 = null;
      Integer fileNum2 = null;
      Long templateFileNum = templateRecords.remove(dmoMeta.getImplementationClass());
      ConcurrentMap<Class<?>, integer> fileNumMap = FILE_NUM.get(primaryDb);
      if (fileNumMap != null)
      {
         fileNum1 = fileNumMap.remove(dmoMeta.getImplementationClass());
      }
      ConcurrentMap<String, Integer> fileByNameMap = FILE_NUM_BY_TBL_NAME.get(primaryDb);
      if (fileByNameMap != null)
      {
         fileNum2 = fileByNameMap.remove(dmoMeta.sqlTable);
      }
      
      if (fileNum2 == null)
      {
         return; // cannot advance
      }
      
      // remove from _File meta table 
      Helper fileHelper = getHelper(SystemTable._File.shortName);
      if (fileHelper == null)
      {
         return;
      }
      
      Database metaDB = new Database(primaryDb.getName(), Database.Type.META, true);
      Persistence p = PersistenceFactory.getInstance(metaDB);
      Session session = p.getSession(Persistence.META_CTX);
      Connection conn = null;
      boolean inTx = false;
      
      // this is the 'orthodox' method which is rather resource intensive
      //    Record file = p.getSession().get(fileHelper.dmoImpl, (long) fileNum2);
      //    p.getSession().delete(file);
      //    do the same for fields, indices and index components 
      
      try
      {
         conn = session.getConnection();
         inTx = session.beginTransaction();
         
         // extract the list of indices of this table ...
         PreparedStatement stmt = conn.prepareStatement(
               "select " + Session.PK + " from meta_index where file_recid=?");
         stmt.setLong(1, (long) fileNum2);
         ResultSet rs = stmt.executeQuery();
         while (rs.next())
         {
            // ... and drop their index components
            deleteRecord(
                  conn, rs.getInt(1), "delete from meta_index_field where index_recid=?", "index components");
         }
         rs.close();
         stmt.close();
         
         // then, delete them from _Index
         deleteRecord(conn, (long) fileNum2, "delete from meta_index where file_recid=?", "indices");
         
         // next, delete from _Field
         deleteRecord(conn, (long) fileNum2, "delete from meta_field where file_recid=?", "fields");
         
         // finally, delete from _File
         deleteRecord(conn, (long) fileNum2, "delete from meta_file where " + Session.PK + "=?", "tables");
         
         if (inTx)
         {
            session.commit();
         }
         
         if (LOG_INFO)
         {
            log.log(Level.INFO, 
                    "Deleted meta information on table " + dmoMeta.legacyTable + " (" + fileNum2 + ").");
         }
      }
      catch (PersistenceException | SQLException e)
      {
         if (inTx)
         {
            try
            {
               session.rollback();
            }
            catch (PersistenceException pe)
            {
               log.log(Level.SEVERE, "Failed to rollback changes in " + conn, pe);
            }
         }
         if (LOG_WARNING)
         {
            log.log(Level.WARNING,
                    "Failed to clean meta information on dropped table '" + dmoMeta.legacyTable + "'.",
                    e);
         }
      }
   }
   
   /**
    * Executes a specific delete statement and optionally log the result using the logger's configuration.
    * 
    * @param   conn
    *          The {@code Connection} to database.
    * @param   sql
    *          The SQL statement to be executed.
    * @param   p
    *          The unique long parameter which identifies the records to be deleted.
    * @param   what
    *          Used only when logging is enabled, additional information on what is deleted.
    *
    * @throws  SQLException
    *          If the statement failed to be executed.
    */
   private static void deleteRecord(Connection conn, long p, String sql, String what)
   throws SQLException
   {
      PreparedStatement stmt = conn.prepareStatement(sql);
      stmt.setLong(1, p);
      if (LOG_INFO)
      {
         log.log(Level.INFO, "Deleted " + stmt.executeUpdate() + " " + what + ".");
      }
      else
      {
         stmt.executeUpdate();
      }
      stmt.close();
   }
   
   /**
    * Populates the {@code _File} meta table with initial values. 
    * 
    * @param   database
    *          The database structure whose features need to be added to {@code _Database-Feature}
    *          table.
    * @param   p
    *          The {@code Persistence} that manages the database.
    * 
    */
   private static void populateFileTable(Database database, Persistence p)
   {
      Helper fileHelper = getHelper(SystemTable._File.shortName);
      if (fileHelper == null)
      {
         return;
      }
      
      helpers.values().forEach(h -> addMetaTableToFile(database, fileHelper, h.dmoIface, h.dmoImpl, p));
      Database master = new Database(database.getName());
      DatabaseManager.getDatabaseDMOs(master).forEach(n -> addTableToFile(master, database, n, fileHelper, p));
   }
   
   /**
    * Add record for a table from the primary database to the _File table.
    * 
    * @param   primary
    *          primary database
    * @param   meta
    *          meta database
    * @param   dmoName
    *          table DMO interface
    * @param   fileHelper
    *          _File table helper
    * @param   p
    *          Persistence for the meta database
    */
   private static void addTableToFile(Database primary,
                                      Database meta,
                                      String dmoName,
                                      Helper fileHelper,
                                      Persistence p)
   {
      boolean isDynamic = false;
      DmoMeta dmoMeta;
      Class<? extends DataModelObject> dmoIface;
      try
      {
         dmoIface = (Class<? extends DataModelObject>) Class.forName(dmoName);
         dmoMeta = DmoMetadataManager.registerDmo(dmoIface);
      } 
      catch (ClassNotFoundException | NullPointerException | IllegalArgumentException e)
      {
         try
         {
            // 2nd chance: in case of the dynamic DMO interfaces (which were loaded with AsmClassLoader)
            // then the default classloader won't recognize them, but they are already in memory and the
            // DmoMetadataManager will provide not only the interface but the DMO info, as well 
            dmoMeta = DmoMetadataManager.getDmoInfo(dmoName);
            dmoIface = dmoMeta.getAnnotatedInterface();
            isDynamic = true;
         }
         catch (Exception ex)
         {
            if (log.isLoggable(Level.FINE))
            {
               log.log(Level.FINE, "Failed to load " + dmoName, e);
            }
            else if (LOG_WARNING)
            {
               log.log(Level.WARNING,
                       "Failed to load " + dmoName + " (set debug level to FINE for stacktrace)");
            }
            return;
         }
      }
      
      registerWordTables(dmoMeta);
      
      Class<? extends Record> dmoImpl = dmoMeta.getImplementationClass();
      
      // be sure the _User meta table is handled appropriately
      if (addMetaTableToFile(meta, fileHelper, dmoIface, dmoImpl, p))
      {
         return; // if it was processed stop here 
      }
      
      long fileNum = nextId(meta, SystemTable._File.fileName);
      integer pk = new integer();
      Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children =
            childData(meta, dmoIface, (int)fileNum, pk);
      setMetaTableRecord(p, fileHelper,
            new FileTableRecordBuilder()
                  .put(Session.PK, fileNum)
                  .put("fileNumber", integer.of(fileNum))
                  .put("template", integer.of(-fileNum))
                  .put("tblType", character.of("T"))
                  .build(dmoIface, pk, children)
      );
      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);
      
      if ((isDynamic && LOG_INFO) || log.isLoggable(Level.FINE))
      {
         log.log(Level.INFO,
                 "Created meta information on table " + dmoMeta.legacyTable + " (" + fileNum + ").");
      }
   }
   
   /**
    * Register word tables for a master table.
    * 
    * @param   dmoMeta
    *          DmoMeta for master table 
    */
   private static void registerWordTables( DmoMeta dmoMeta)
   {
      Map<String, WordIndexData> wt = dmoMeta.getWordTables();
      if (!wt.isEmpty())
      {
         WORD_TABLES.computeIfAbsent(dmoMeta.schema, db -> new HashMap<>())
                    .put(dmoMeta.sqlTable, wt);
      }
   }
   
   /**
    * Add record for a table from the meta database to the _File table.
    * 
    * @param   metaDb
    *          meta database
    * @param   fileHelper
    *          _File table helper
    * @param   dmoIface
    *          table DMO interface
    * @param   dmoImpl
    *          table DMO
    * @param   p
    *          Persistence for the meta database.
    *
    * @return  <code>true</code> if successful
    */
   private static boolean addMetaTableToFile(Database metaDb,
                                             Helper fileHelper,
                                             Class<? extends DataModelObject> dmoIface,
                                             Class<? extends Record> dmoImpl,
                                             Persistence p)
   {
      Table tableAnn = dmoIface.getAnnotation(Table.class);
      if (tableAnn == null                                               ||
          !tableAnn.isHidden()                                           ||
          dmoIface.getSimpleName().equals("MetaUser")                    ||
          dmoIface.getSimpleName().equals("MetaSecAuthenticationDomain") ||
          dmoIface.getSimpleName().equals("MetaSecAuthenticationSystem"))
      {
         // not a hidden (meta) table
         return false;
      }
      
      String ifaceName = dmoIface.getSimpleName();
      SystemTable systemTable = SystemTable.forIface(ifaceName);
      if (systemTable == null)
      {
         // not a system table
         return false;
      }
      
      integer pk = new integer();
      Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children = childData(
            metaDb, dmoIface, systemTable.fileNum, pk);
      long id = nextId(metaDb, SystemTable._File.fileName);
      
      setMetaTableRecord(p, fileHelper,
            new FileTableRecordBuilder()
                  .put(Session.PK, id)
                  .put("fileNumber", new integer(systemTable.fileNum))
                  .put("template", new integer(-id))
                  .put("tblType", new character("S"))
                  .build(dmoIface, pk, children)
      );
      
      saveChildren(p, children);
      if (systemTable.isRegular())
      {
         templateRecords.put(dmoImpl, -id);
      }
      
      return true;
   }
   
   /**
    * Save child metadata (_Field, _Index, _Index-Field, ... ) of the table.
    * 
    * @param   p
    *          Persistence for the meta database
    * @param   children
    *          child metadata
    */
   private static void saveChildren(Persistence p,
                                    Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children)
   {
      children.forEach((key, value) ->
      {
         Helper h = getHelper(key.shortName);
         if (h == null)
         {
            return;
         }
         
         value.forEach(m -> setMetaTableRecord(p, h, m));
      });
   }
   
   /**
    * Populates the {@code _Sequence} meta table with initial values. These are specific
    * to FWD implementation, specific to backing database server or may be set manually in a
    * configuration file.
    * 
    * @param   metaDb
    *          The database structure whose features need to be added to {@code _Database-Feature}
    *          table.
    * @param   p
    *          The {@code Persistence} that manages the database.
    */
   private static void populateSequence(Database metaDb, Persistence p)
   {
      Helper seqHelper = getHelper(SystemTable._Sequence.shortName);
      if (seqHelper == null)
      {
         return;
      }
      
      String seqClass = DmoMetadataManager.getDmoBasePackage() + "." + metaDb.getName() + "._Sequences";
      Class<?> seqEnum = null;
      try
      {
          seqEnum = Class.forName(seqClass);
      } 
      catch (ClassNotFoundException e)
      {
         if (LOG_INFO)
         {
            log.log(Level.INFO, "No sequences were detected for " + metaDb.getName() + " database.");
         }
         return;
      }
      
      if (!Enum.class.isAssignableFrom(seqEnum))
      {
         if (LOG_WARNING)
         {
            log.log(Level.WARNING, "Invalid _Sequences class for " + metaDb.getName() + " database.");
         }
         return;
      }
      Method m;
      try
      {
         m = seqEnum.getDeclaredMethod("values");
         Object[] seqs = (Object[])Utils.invoke(m, null, new Object[0]); 
         for(Object o: seqs ) 
         {
            Field fs = seqEnum.getDeclaredField(o.toString());
            Sequence seq = fs.getAnnotation(Sequence.class);
            if (seq == null)
            {
               // TODO: log warning
               continue;
            }
            long nextId = nextId(metaDb, SystemTable._Sequence.fileName);
            setMetaTableRecord(p, seqHelper,
                  new MapBuilder<>().
                     put(Session.PK, nextId).
                     put("seqName", new character(seq.legacy())).
                     put("seqInit", new int64(seq.start())).
                     put("seqIncr", new int64(seq.increment())).
                     put("seqMin", new int64(seq.minValue())).
                     put("seqMax",new int64(seq.maxValue())).
                     put("cycleOk",new logical(seq.cycle())).
                     put("dbRecid",new recid(nextId)).
                  build()
            );
         }
      } catch (NoSuchMethodException | SecurityException | 
               InvocationTargetException | IllegalAccessException | NoSuchFieldException e)
      {
         if (LOG_WARNING)
         {
            log.log(Level.WARNING, "Failed to populate _Sequence table", e);
         }
      }
      
   }
   
   /**
    * Populates the {@code _Database-Feature} meta table with initial values. These are specific
    * to FWD implementation, specific to backing database server or may be set manually in a
    * configuration file.
    * 
    * @param   database
    *          The database structure whose features need to be added to {@code _Database-Feature}
    *          table.
    * @param   p
    *          The {@code Persistence} that manages the database.
    */
   private static void populateDatabaseFeatures(Database database, Persistence p)
   {
      Helper h = getHelper("DatabaseFeature");
      if (h == null)
      {
         return; // _DatabaseFeature metatable is not converted/active
      }
      
      setDatabaseFeature(p, h, 1, "OpenEdge Replication", false, false);
      setDatabaseFeature(p, h, 2, "Failover Clusters", false, false);
      setDatabaseFeature(p, h, 3, "DataXtend Remote Edition", false, false);
      setDatabaseFeature(p, h, 4, "Reserved", false, false);
      setDatabaseFeature(p, h, 5, "Large Files", false, false);
      setDatabaseFeature(p, h, 6, "Database Auditing", false, false);
      setDatabaseFeature(p, h, 7, "JTA", false, false);
      setDatabaseFeature(p, h, 8, "After Image Mangement/Archiver", false, false);
      setDatabaseFeature(p, h, 9, "64 Bit DBKEYS", true, true);
      
      // database is a META database, we need information about its MASTER
      Dialect dialect = DatabaseManager.getDialect(new Database(database.getName(), Database.Type.PRIMARY));
      int indexLimit = dialect.getIndexLengthLimit();
      setDatabaseFeature(p, h, 10, "Large Keys", indexLimit == 0 || indexLimit > 200, true);
      
      setDatabaseFeature(p, h, 11, "64 Bit Sequences", true, true);
      setDatabaseFeature(p, h, 12, "DataXtend Integration Edition", false, false);
      setDatabaseFeature(p, h, 13, "Encryption", false, false);
      setDatabaseFeature(p, h, 14, "Multi-tenancy", false, false);
      setDatabaseFeature(p, h, 15, "Concurrent JTA and Replication", false, false);
      setDatabaseFeature(p, h, 16, "Reserved", false, false);
      setDatabaseFeature(p, h, 17, "MT Index Rebuild", false, false);
      setDatabaseFeature(p, h, 18, "MT Data Move", false, false);
      setDatabaseFeature(p, h, 19, "Roll Forward Restricted Mode", false, false);
      setDatabaseFeature(p, h, 20, "TP Index Rebuild", false, false);
      setDatabaseFeature(p, h, 21, "Table Partitioning", false, false);
      setDatabaseFeature(p, h, 22, "Read-only Partitions", false, false);
      setDatabaseFeature(p, h, 23, "New VST Tables", true, true);
      setDatabaseFeature(p, h, 24, "Reserved", false, false);
      setDatabaseFeature(p, h, 25, "Partition Copy", false, false);
      setDatabaseFeature(p, h, 26, "Authentication Gateway", false, false);
      
      // TODO: to be completed with missing values.
      //       to be updated with correct values
   }
   
   /**
    * Creates and saves a record in {@code MetaDb} table with the given recId.
    *
    * @param   database
    *          The database to be processed. 
    * @param   p
    *          The persistence context to be used when saving.
    */
   private static void createDbMetaRecord(Database database, Persistence p)
   {
      Helper h = getHelper(_DB);
      if (h == null)
      {
         return;
      }
      
      setMetaTableRecord(p, h,
            new MapBuilder<>()
                  .put(Session.PK, (long) DB_RECID)
                  .put("dbName",  new character("_meta"))
                  .put("dbType",  new character(""))
                  .put("dbComm",  new character(""))
                  .put("dbSlave", new logical(false))
                  .put("dbLocal", new logical(false))
                  .build()
      );
   }
   
   /**
    * Populates the {@code _Area} meta table with initial values. 
    * This is a "mock" since _Area data has no meaningful counterparts in FWD 
    * 
    * @param   database
    *          The database structure whose features need to be added to {@code _Area}
    *          table.
    * @param   p
    *          The {@code Persistence} that manages the database.
    */
   private static void populateArea(Database database, Persistence p)
   {
      Helper h = getHelper("Area");
      if (h == null)
      {
         return; // _Area metatable is not converted/active
      }
      setArea(p, h, 1, 1, "Control Area", 6, 0, 0, 1, 4096, 5, 1);
      setArea(p, h, 1, 3, "Primary Recovery Area", 3, 0, 0, 1, 8192, 0, 1);
      setArea(p, h, 1, 6, "Schema Area", 6, 0, 0, 1, 4096, 5, 1);
      setArea(p, h, 7, 7, "Data", 6, 0, 0, 64, 4096, 6, 1);
      setArea(p, h, 7, 8, "Index", 6, 0, 0, 8, 4096, 5, 1);
      setArea(p, h, 1, 9, "After Image Area 1", 7, 0, 0, 1, 8192, 0, 1);
   }
   
   /**
    * Populates the {@code _Filelist} meta table with initial values. 
    * This is a "mock" since _Filelist data has no meaningful counterparts in FWD 
    * 
    * @param   database
    *          The database structure whose features need to be added to {@code _Area}
    *          table.
    * @param   p
    *          The {@code Persistence} that manages the database.
    */
   private static void populateFilelist(Database database, Persistence p)
   {
      Helper h = getHelper("Filelist");
      if (h == null)
      {
         return; // _Filelist metatable is not converted/active
      }
      addFilelist(p, h, 1, 32, 256, 6, 4096, "BOTHIO", ".4filelist.db");
      addFilelist(p, h, 2, 2176, 512, 272, 8192, "UNBUFIO", ".4filelist.b1");
      addFilelist(p, h, 3, 146944, 256, 36704, 4096, "BOTHIO", ".4filelist.d1");
   }
   
   /**
    * Populates the {@code _Startup} meta table with an initial record. 
    * 
    * @param   database
    *          The database.
    * @param   p
    *          The {@code Persistence} that manages the database.
    */
   private static void populateStartupTable(Database database, Persistence p)
   {
      Helper h = getHelper("Startup");
      if (h == null)
      {
         return; // _Startup metatable is not converted/active
      }
      
      setMetaTableRecord(p, h,
          new MapBuilder<>()
                .put(Session.PK,         1L) 
                .put("startupLockTable", new integer("8192"))
                .put("startupMaxUsers",  new integer("64"))
                .build()
      );
   }
   
   /**
    * Populates the {@code _Trans} meta table with an initial record.
    * 
    * @param   database
    *          The database.
    * @param   p
    *          The {@code Persistence} that manages the database.
    */
   private static void populateTransTable(Database database, Persistence p)
   {
      Helper h = getHelper(_TRANS);
      if (h == null)
      {
         return; // _Startup metatable is not converted/active
      }
      
      setMetaTableRecord(p, h, new MapBuilder<>().put(Session.PK, 1L).build());
   }
   
   /**
    * Populates the {@code _Tenant} meta table with initial values. 
    * This is a "mock" since _Tenant data has no meaningful counterparts in FWD 
    * 
    * @param   database
    *          The database structure whose features need to be added to {@code _Area}
    *          table.
    * @param   p
    *          The {@code Persistence} that manages the database.
    */
   private static void populateTenant(Database database, Persistence p)
   {
      Helper h = getHelper("Tenant");
      if (h == null)
      {
         return; // _Tenant metatable is not converted/active
      }
      
      try {
         // set the Default tenant for all databases is always available
         long tenIdPk = 0;
         setMetaTableRecord(p, h, 
               new MapBuilder<>()
                     .put(Session.PK,                tenIdPk)
                     .put("tenantId",                new integer(TenantManager.DEFAULT_TENANT_ID))
                     .put("tenantName",              new character(TenantManager.DEFAULT_TENANT_NAME))
                     .put("tenantExtId",             new character(""))
                     .put("tenantDescription",       new character("Default Tenant"))
                     .put("tenantAttributes", 0,     logical.TRUE)
                     .put("tenantType",              new integer(TenantManager.TYPE_DEFAULT))
                     .put("tenantSequenceBlock",     new recid(0))
                     .put("tenantDataAreaDefault",   new integer(0))
                     .put("tenantIndexAreaDefault",  new integer(0))
                     .put("tenantLobAreaDefault",    new integer(0))
                     .put("tenantAllocationDefault", new character("None"))
                     .build());
         
         ArrayList<TenantManager.Tenant> tenants = TenantManager.listTenants(new ArrayList<>());
         if (tenants == null)
         {
            return; // no other tenant defined locally
         }
         
         for (int i = 0; i < tenants.size(); i++)
         {
            TenantManager.Tenant tenant = tenants.get(i);
            if (tenant.id() == TenantManager.DEFAULT_TENANT_ID)
            {
               continue; // already saved above
            }
            
            for (Map.Entry<String, TenantDatabaseDescriptor> it : tenant.getDatabaseCredentials().entrySet())
            {
               if (it.getValue().getLdbName().equalsIgnoreCase(database.getName()))
               {
                  ++tenIdPk;
                  setMetaTableRecord(p, h,
                      new MapBuilder<>()
                            .put(Session.PK,                tenIdPk)
                            .put("tenantId",                new integer(tenant.id()))
                            .put("tenantName",              new character(tenant.getTenantName()))
                            .put("tenantExtId",             new character(tenant.getId().toString())) /*UUID*/
                            .put("tenantDescription",       new character(tenant.getTenantDescription()))
                            .put("tenantAttributes", 0,     logical.of(tenant.getStatus()))
                            .put("tenantType",              new integer(tenant.getType()))
                            .put("tenantSequenceBlock",     new recid(tenIdPk + 0x1000_0000))
                            .put("tenantDataAreaDefault",   new integer(tenIdPk * 100 + 0))
                            .put("tenantIndexAreaDefault",  new integer(tenIdPk * 100 + 1))
                            .put("tenantLobAreaDefault",    new integer(tenIdPk * 100 + 2))
                            .put("tenantAllocationDefault", new character("Immediate"))
                            .build());
               }
            }
         }
      }
      finally
      {
         TenantManager.registerMetaListener(new TenantTableUpdater(p, database));
      }
   }
   
   /**
    * Sets a single database feature. Only the id, name and active/enabled status are set, the
    * reserved fields are ignored.
    * 
    * @param   p
    *          Persistence instance for the table
    * @param   h
    *          The helper to be used.
    * @param   featureId
    *          The id of the feature to be set.
    * @param   featureName
    *          The name of the feature to be set.
    * @param   isActive
    *          Whether the feature is active at startup.
    * @param   isEnabled
    *          Whether the feature is enabled at startup.
    * 
    */
   private static void setDatabaseFeature(Persistence p,
                                          Helper h,
                                          int featureId,
                                          String featureName,
                                          boolean isActive,
                                          boolean isEnabled)
   {
      setMetaTableRecord(p, h,
            new MapBuilder<>().
               put(Session.PK,        (long) featureId).
               put("dbfeatureId",     new int64(featureId)).
               put("dbfeatureName",   new character(featureName)).
               put("dbfeatureActive", new character(isActive ? "1" : "0")).
               put("dbfeatureEnabled",new character(isEnabled ? "1" : "0")).
            build()
      );
   }

   /**
    * Sets a single _Area metatable data. _Area-misc field is not populated 
    * 
    * @param   p
    *          Persistence instance for the table
    * @param   h
    *          The helper to be used.
    * @param   areaVersion
    *          area version
    * @param   areaNumber
    *          area number
    * @param   areaName
    *          area name
    * @param   areaType
    *          area type
    * @param   areaBlock
    *          area block
    * @param   areaAttrib
    *          area attributes
    * @param   areaClustersize
    *          area cluster size
    * @param   areaBlocksize
    *          area block size
    * @param   areaRecbits
    *          area recbits (rows per block = 2 ^ recbits)
    * @param   areaExtents
    *          area extents
    */
   private static void setArea(Persistence p,
                               Helper h,
                               int areaVersion,
                               int areaNumber,
                               String areaName,
                               int areaType,
                               int areaBlock,
                               int areaAttrib,
                               int areaClustersize,
                               int areaBlocksize,
                               int areaRecbits,
                               int areaExtents)
   {
      setMetaTableRecord(p, h,
            new MapBuilder<>().
               put(Session.PK,          (long) areaNumber).
               put("areaVersion",       new int64(areaVersion)).
               put("areaNumber",        new int64(areaNumber)).
               put("areaName",          new character(areaName)).
               put("areaType",          new int64(areaType)).
               put("areaBlock",         new int64(areaBlock)).
               put("areaAttrib",        new int64(areaAttrib)).
               put("areaClustersize",   new int64(areaClustersize)).
               put("areaBlocksize",     new int64(areaBlocksize)).
               put("areaRecbits",       new int64(areaRecbits)).
               put("areaExtents",       new int64(areaExtents)).
            build()
      );
   }
   
   /**
    * Sets a single _Filelist metatable data. _FileList-Misc field is not populated 
    * 
    * @param   p
    *          Persistence instance for the table
    * @param   h
    *          The helper to be used.
    * @param   fileListId
    *          fileList id  
    * @param   fileListSize
    *          fileList size
    * @param   fileListExtend
    *          fileList extend
    * @param   fileListLogicalSz
    *          fileList logical size
    * @param   fileListBlkSize
    *          fileList block size
    * @param   fileListOpenmode
    *          fileList open mode
    * @param   fileListName
    *          fileList name
    */
   private static void addFilelist(Persistence p,
                                   Helper h,
                                   int fileListId,
                                   int fileListSize,
                                   int fileListExtend,
                                   int fileListLogicalSz,
                                   int fileListBlkSize,
                                   String fileListOpenmode,
                                   String fileListName)
   {
      setMetaTableRecord(p, h,
            new MapBuilder<>().
               put(Session.PK,           (long) fileListId).
               put("fileListId",         new int64(fileListId)).
               put("fileListSize",       new int64(fileListSize)).
               put("fileListExtend",     new int64(fileListExtend)).
               put("fileListLogicalSz",  new int64(fileListLogicalSz)).
               put("fileListBlkSize",    new int64(fileListBlkSize)).
               put("fileListOpenmode",   new character(fileListOpenmode)).
               put("fileListName",       new character(fileListName)).
            build()
      );
   }
   
   /**
    * Created a new meta-table record using the helper constructor, populates it with the values passed in the
    * map and save it to (meta) database.
    * 
    * @param   p
    *          Persistence instance for the table
    * @param   h
    *          The helper to be used.
    * @param   values
    *          A map containing the values for specific properties. A property is specified by a {@code Tuple}
    *          of ({@code property} / {@code index}). The {@code index} should be {@code null} for scalar
    *          fields. 
    *
    * @throws  IllegalAccessException 
    * @throws  InstantiationException 
    * @throws  InvocationTargetException 
    * @throws  IllegalArgumentException 
    * @throws  PersistenceException 
    */
   private static void setMetaTableRecord(Persistence p, Helper h, Map<Tuple<String, Integer>, Object> values) 
   {
      try
      {
         Record dmo = h.dmoImpl.getDeclaredConstructor().newInstance();
         dmo.initialize(null, true);
         
         for (Map.Entry<Tuple<String, Integer>, Object> e: values.entrySet())
         {
            Tuple<String, Integer> prop = e.getKey();
            h.setValue(dmo, prop.getKey(), prop.getValue(), e.getValue());
         }
         p.save(dmo, dmo.primaryKey());
      } 
      catch (ReflectiveOperationException | PersistenceException e)
      {
         throw new RuntimeException("Failed to set record for " + h.dmoIface.getName(), e);
      }
   }

   /**
    * Return the rowid of the template record for a permanent table.
    * 
    * @param   table
    *          The permanent table to lookup.
    * 
    * @return  The value of the {@link rowid} of the template record. If metadata is not enabled
    *          for this project or {@code _file} metatable is not configured or {@code table} is
    *          an invalid name {@code null} is returned.
    */
   public static Long getTemplateRowid(Class<?> table)
   {
      return MetadataManager.inUse() ? templateRecords.get(table) : null;
   }
   
   /**
    * Gets the metadata helper for a specified table. In case such helper is not found, the event is logged
    * with WARNING level and {@code null} is returned.
    * 
    * @param   type
    *          The name of meta file.
    * 
    * @return  The helper corresponding to {@code type}, or {@code null} if no support such table was set up.
    */
   private static Helper getHelper(String type)
   {
      Helper h = helpers.get(type);
      if (h == null)
      {
         if (LOG_WARNING)
         {
            log.log(Level.WARNING,
                    "Could not obtain the helper class for calling DMO setter methods. _" + 
                    type + " metadata not available.");
         }
      }
      return h;
   }
   
   /**
    * Execute a script containing DDL SQL statements.
    * 
    * @param   database
    *          Target database.
    * @param   connection
    *          Connection to target database.
    * @param   script
    *          Short name of script file, which is expected to be found in the application jar
    *          file. The following path is implied:
    *          <pre>
    *          {base DMO package}._meta.{script name}
    *          </pre>
    * 
    * @throws  PersistenceException
    *          if there is any error connecting to the target database or executing the DDL
    *          statements.
    */
   private static void executeDDLScript(Database database, Connection connection, String script)
   throws PersistenceException
   {
      String path = String.format("%s/%s/%s", baseDir, META_SCHEMA, script);
      BufferedReader br = null;
      
      try
      {
         InputStream in = ClassLoader.getSystemResourceAsStream(path);
         if (in == null)
         {
            throw new MissingDataException("Could not load DDL script " + path);
         }
         
         br = new BufferedReader(new InputStreamReader(in));
         H2Helper.runScript(connection, br);
      }
      catch (Exception exc)
      {
         throw new PersistenceException("Error executing DDL for " + database, exc);
      }
      finally
      {
         if (br != null)
         {
            try
            {
               br.close();
            }
            catch (IOException exc)
            {
            }
         }
      }
   }

   /**
    * Calculate short DMO interface name for a table.
    * 
    * @param   tableName
    *          The table name.
    *
    * @return  short DMO interface name
    */
   private static String getShortName(String tableName)
   {
      StringBuilder sb = new StringBuilder();
      int k = tableName.indexOf("-");
      int rest = 1; // drop the first _ char at 0 
      while (k != -1)
      {
         sb.append(Character.toUpperCase(tableName.charAt(rest)));
         sb.append(tableName, rest + 1, k);
         rest = k + 1;
         k = tableName.indexOf("-", k + 1);
      }
      sb.append(Character.toUpperCase(tableName.charAt(rest)));
      sb.append(tableName.substring(rest + 1));
      return sb.toString();
   }
   
   /**
    * Helper class for calling DMO setter methods when populating a metaschema record.
    */
   private static class Helper
   {
      /** Well-known name of primary key setter method */
      private static final String SET_ID = "primaryKey";
      
      /** Primary key setter method */
      private static final Method idSetter;
      
      /** DMO implementation class */
      private final Class<? extends Record> dmoImpl;
      
      /** DMO interface class */
      private final Class<? extends DataModelObject> dmoIface;
      
      /** Map of DMO property setters (excluding primary key setter) */
      private final Map<String, Method> setters;
      
      static
      {
         try
         {
            idSetter = BaseRecord.class.getDeclaredMethod(SET_ID, Long.class);
         }
         catch (NoSuchMethodException | SecurityException e)
         {
            throw new RuntimeException(e);
         }
      }
      
      /**
       * Constructor.
       * 
       * @param   dmoIface
       *          DMO interface.
       * @param   dmoImpl
       *          DMO implementation class.
       */
      Helper(Class<? extends DataModelObject> dmoIface, Class<? extends Record> dmoImpl)
      {
         this.dmoImpl = dmoImpl;
         this.dmoIface = dmoIface;
         this.setters = PropertyHelper.pojoSettersByProperty(dmoIface);
      }
      
      /**
       * Set a property value into a DMO instance.
       * 
       * @param   dmo
       *          Target DMO instance.
       * @param   property
       *          Name of property to be set.
       * @param   index
       *          Optional string representation of index (for extent fields only). Should be the
       *          {@code null} if property does not represent an extent field.
       * @param   value
       *          Value of the appropriate data type (i.e., data wrapper instance) for the target
       *          property.
       * 
       * @throws  PersistenceException
       *          if <code>index</code> cannot be parsed into an <code>Integer</code>.
       * @throws  IllegalAccessException
       *          if the target setter method cannot be accessed.
       * @throws  IllegalArgumentException
       *          if <code>value</code> is the wrong type.
       * @throws  InvocationTargetException
       *          if the underlying setter method throws an exception.
       */
      void setValue(Object dmo, String property, Integer index, Object value)
      throws PersistenceException,
             IllegalAccessException,
             IllegalArgumentException,
             InvocationTargetException
      {
         Method m = getSetter(property);
         
         if (index == null)
         {
            Utils.invoke(m, dmo, value);
         }
         else
         {
            Utils.invoke(m, dmo, index, value);
         }
      }
      
      /**
       * Retrieve the DMO property setter method for the specified property name.
       * 
       * @param   property
       *          Name of a DMO property (case-sensitive).
       * 
       * @return  Matching setter method.
       * 
       * @throws  PersistenceException
       *          if a setter method cannot be found for the specified property.
       */
      private Method getSetter(String property)
      throws PersistenceException
      {
         Method m = setters.get(property);
         if (m == null && Session.PK.equals(property))
         {
            m = idSetter;
         }
         
         if (m == null)
         {
            String msg = String.format("No setter method found for %s#%s", dmoIface.getName(), property);
            throw new PersistenceException(msg);
         }
         
         return m;
      }
   }
   
   /**
    * Return record id for table in the meta database.
    *   
    * @param metaDb
    *        meta database
    * @param table
    *        meta table
    * @return record id for table in the meta database
    */
   private static long nextId(Database metaDb, String table)
   {
      return TABLE_IDS.computeIfAbsent(metaDb, db -> new ConcurrentHashMap<>()).
            computeIfAbsent(table.toLowerCase(), t -> new AtomicLong()).incrementAndGet();
   }
   
   /**
    * Collect child metadata for a record in the _File meta table.
    * 
    * @param   metaDb
    *          meta database
    * @param   dmoIface
    *          table DMO interface
    * @param   _file_num
    *          _File._file_num field value
    * @param   pk
    *          table primary key id in the _Index table holder
    * 
    * @return child metadata for a record in the _File meta table.
    */
   private static Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> childData(
             Database metaDb,
             Class<? extends DataModelObject> dmoIface,
             int _file_num,
             integer pk)
   {
      Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children = new EnumMap<>(SystemTable.class);
      List<Map<Tuple<String, Integer>, Object>> fields = new LinkedList<>();
      List<Map<Tuple<String, Integer>, Object>> indexes = new LinkedList<>();
      List<Map<Tuple<String, Integer>, Object>> indexFields = new LinkedList<>();
      List<Map<Tuple<String, Integer>, Object>> fileTriggers = new LinkedList<>();
      List<Map<Tuple<String, Integer>, Object>> fieldTriggers = new LinkedList<>();
      Map<String, integer> fieldRecidByName = new LinkedHashMap<>();
      Map<String, integer> fieldRposByName = new LinkedHashMap<>();
      children.put(SystemTable._Field, fields);
      children.put(SystemTable._Index, indexes);
      children.put(SystemTable._Index_Field, indexFields);
      children.put(SystemTable._File_Trig, fileTriggers);
      children.put(SystemTable._Field_Trig, fieldTriggers);
      
      DmoMeta meta = DmoMetadataManager.getDmoInfo(dmoIface);
      Iterator<Property> properties = meta.getFields(false);
      
      while (properties.hasNext())
      {
         Property prop = properties.next();
         Class<?> ftype = prop._fwdType;
         if (prop.index > 1)
         {
            continue; // skip duplicates caused by custom extents
         }
         fields.add(fieldData(metaDb, _file_num, prop, ftype, fieldRecidByName, fieldRposByName));
      }
      Iterator<P2JIndex> indices = meta.getDatabaseIndexes();
      indexData(metaDb, dmoIface.getSimpleName(), _file_num, indices, pk, indexes, indexFields, fieldRecidByName);
      
      Triggers legacyTriggers = dmoIface.getAnnotation(Triggers.class);
      Trigger[] triggers = legacyTriggers == null ? new Trigger[0] : legacyTriggers.value();
      for (Trigger trigger: triggers)
      {
         String field = trigger.field();
         if (!field.isEmpty())
         {
            integer fieldRecId = fieldRecidByName.get(field);
            integer fieldRpos = fieldRposByName.get(field);
            if (fieldRecId == null || fieldRpos == null)
            {
               if (LOG_WARNING)
               {
                  log.log(Level.WARNING,
                          "Unknown field [" + field + "] in the trigger definition for [" + 
                          dmoIface.getName() + "]");
               }
               continue;
            }
            fieldTriggers.add(
                  new MapBuilder<>().
                  put(Session.PK, nextId(metaDb, SystemTable._Field_Trig.fileName)).
                  put("fieldRecid", fieldRecId).
                  put("fileRecid", integer.of(_file_num)).
                  put("fieldRpos", fieldRpos).
                  put("event", new character(trigger.event())).
                  put("procName", new character(trigger.procedure())).
                  put("override", logical.of(trigger.overridable())).
                  build()
            );
         }
         else
         {
            fileTriggers.add(
                  new MapBuilder<>().
                  put(Session.PK, nextId(metaDb, SystemTable._File_Trig.fileName)).
                  put("fileRecid", integer.of(_file_num)).
                  put("event", new character(trigger.event())).
                  put("procName", new character(trigger.procedure())).
                  put("override", logical.of(trigger.overridable())).
                  build()
            );
         }
      }
      
      return children;
   }

   /**
    * Collect index metadata for a record in the _File meta table.
    * @param   metaDb
    *          meta database
    * @param   tblDmoName
    *          table DMO
    * @param   _file_num
    *          _File._file_num field value
    * @param   indices
    *          P2JIndex iterator 
    * @param   pk
    *          table primary key id in the _Index table holder
    * @param   indexes
    *          table indexes metadata
    * @param   indexFields
    *          table index fields metadata
    * @param   fieldRecidByName
    *          Map of property names to their recid.
    */
   private static void indexData(Database metaDb,
                                 String tblDmoName,
                                 int _file_num,
                                 Iterator<P2JIndex> indices,
                                 integer pk,
                                 List<Map<Tuple<String, Integer>, Object>> indexes,
                                 List<Map<Tuple<String, Integer>, Object>> indexFields,
                                 Map<String, integer> fieldRecidByName)
   {
      while (indices.hasNext())
      {
         P2JIndex idx = indices.next();
         long id = nextId(metaDb, SystemTable._Index.fileName);
         if (idx.isPrimary())
         {
            pk.assign(integer.of(id));
         }
         indexes.add(new MapBuilder<>().
               put(Session.PK, id).
               put("fileRecid", integer.of(_file_num)).
               put("indexName", new character(idx.getName())).
               put("unique", logical.of(idx.isUnique())).
               build()
         );
         int seqNo = 1;
         ArrayList<P2JIndexComponent> comps = idx.components();
         for (int i = 0; i < comps.size(); i++)
         {
            P2JIndexComponent fld = comps.get(i);
            String compName = fld.getPropertyName();
            integer fieldRecid = fieldRecidByName.get(compName);
            
            if (fieldRecid == null)
            {
               log.warning("Unknown index field [" + compName + "] for table [" + tblDmoName + "]");
               continue;
            }
            indexFields.add(new MapBuilder<>().
                  put(Session.PK, nextId(metaDb, SystemTable._Index_Field.fileName)).
                  put("indexRecid", integer.of(id)).
                  put("fieldRecid", fieldRecidByName.getOrDefault(compName, integer.UNKNOWN)).
                  put("indexSeq", integer.of(seqNo++)).
                  build()
            );
            
         }
      }
   }

   /**
    * Collect _Field table data for a DMO field.
    * 
    * @param   metaDb
    *          meta database
    * @param   _file_num
    *          _File._file_num field value
    * @param   property
    *          {@code Property} annotation of the field.
    * @param   fieldRecidByName
    *          Fields' record id by property name map.
    * @param   fieldRposByName
    *          Fields' (r)pos id by property name map.
    *
    * @return _Field table data for a DMO field.
    */
   private static Map<Tuple<String, Integer>, Object> fieldData(Database metaDb,
                                                                int _file_num,
                                                                Property property,
                                                                Class<?> ftype,
                                                                Map<String, integer> fieldRecidByName,
                                                                Map<String, integer> fieldRposByName)
   {
      long id = nextId(metaDb, SystemTable._Field.fileName);
      fieldRecidByName.put(property.name, new integer(id));
      fieldRposByName.put(property.name, integer.of(property.order));
      int  wd = property.width;
      integer width = wd >= 0 ? integer.of(wd) : integer.UNKNOWN;
      boolean caseSensitive = property.caseSensitive;
      String typeName = ftype == datetimetz.class ? "datetime-tz" : ftype.getSimpleName();
      boolean charField = CHAR_TYPES.contains(typeName);
      String charset = charField ?  "iso8859-1" : null;
      String collation =  charField ? (caseSensitive ? "basic_S" : "basic_I") : null;
      return new MapBuilder<>().
            put(Session.PK, id).
            put("fileRecid", integer.of(_file_num)).
            put("fieldName", new character(property.legacy)).
            put("dataType", character.of(typeName)).
            put("mandatory", logical.of(property.mandatory)).
            put("label", new character(property.label)).
            put("labelSa", new character(property.label_sa)).
            put("colLabel", new character(property.columnLabel)).
            put("colLabelSa", new character(property.columnLabel_sa)).
            put("format", character.of(property.format)).
            put("formatSa", character.of(property.format_sa)).
            put("fieldRpos", integer.of(property.position)).
            put("fieldPhyspos", integer.of(property.order)). // TODO: order/position?
            put("order", integer.of(property.order)).
            put("help", new character(property.help)).
            put("helpSa", new character(property.help_sa)).
            put("valexp", new character(property.validateExpression)).
            put("valmsg", new character(property.validateMessage)).
            put("valmsgSa", new character(property.valmsg_sa)).
            put("initial", property.initialNull ?  character.UNKNOWN : new character(property.initial)).
            put("initialSa", new character(property.initial_sa)).
            put("canRead", new character(property.canRead)).
            put("canWrite", new character(property.canWrite)).
            put("extent", integer.of(property.extent)).
            put("viewAs", character.of(property.viewAs)).
            put("desc", new character(property.desc)).
            put("width", integer.of(width)).
            put("decimals", integer.of(property.scale)).
            put("dtype", integer.of(DTYPE.get(typeName))).
            put("fetchType", character.of(FETCH_TYPE.get(typeName))).
            put("fldCase" , logical.of(caseSensitive)).
            put("sysField" , logical.of(false)).
            put("charset", character.of(charset)).
            put("collation", character.of(collation)).
            build();
   }
   
   /** System table categories */
   public enum Category
   {
      /** Virtual system table */
      VST,
      /** Constraints */
      DS_SCHEMA,
      /** Physical layout */
      PHY_ST,
      /** Authentication */
      AUTH,
      /** Logging */
      LOG,
      /** Audit */
      AUDIT,
      /** SQL statistics */
      SQL_STAT,
      /** SQL catalog */
      SQL_CAT,
      /** SQL permissions */
      SQL_AUTH,
      /** SQL stored procedures */
      SQL_SP,
      /** SQL triggers */
      SQL_TR,
      /** DB schema */
      SCHEMA,
      /** SQL views */
      SQL89,
   }
   
   /** System tables' flags */
   private interface TableFlags
   {
      /** Flag indicating that the table is a part of the main db schema, not a meta database */
      int REGULAR   = 1 << 0;
      
      /** Flag indicating that the table should always present */
      int MANDATORY = 1 << 1;
      
      /** Flag indicating that the table is read-only from 4GL code. */
      int READONLY = 1 << 2;
   }
   
   /** System tables (as of Progress 11.6.3) */
   public enum SystemTable
   implements TableFlags
   {
      /** Provides detailed information about registered objects of a legacy database. */
      _DbServiceManagerObjects(-16436, "_DbServiceManagerObjects", Category.VST),
      /** Provides detailed information about the Replication plugin manager communication area. */
      _DbServiceManager(-16435, "_DbServiceManager", Category.VST),
      /** Provides dynamic information about active OpenEdge Replication agents. */
      _Repl_AgentActivity(-16434, "_Repl-AgentActivity", Category.VST),
      /** Provides dynamic information about active replication agents this replication server controls. */
      _Repl_AgentControlActivity(-16433, "_Repl-AgentControlActivity", Category.VST), 
      /** */
      _DbParams(-16432, "_DbParams", Category.VST),
      /** */
      _UserIndexStat(-16431, "_UserIndexStat", Category.VST), 
      /** */
      _UserTableStat(-16430, "_UserTableStat", Category.VST, MANDATORY),
      /** */
      _AreaThreshold(-16429, "_AreaThreshold", Category.VST), 
      /** */
      _Code_Feature(-16428, "_Code-Feature", Category.VST),
      /** */
      _Database_Feature(-16427, "_Database-Feature", Category.VST, MANDATORY, 
            MetadataManager::populateDatabaseFeatures),
      /** Provides detailed information about a replication agent. */
      _Repl_Agent(-16426, "_Repl-Agent", Category.VST),
      /** Provides detailed information about replication agents this replication server controls. */
      _Repl_AgentControl(-16425, "_Repl-AgentControl", Category.VST),
      /** Provides detailed information about the OpenEdge Replication server. */
      _Repl_Server(-16424, "_Repl-Server", Category.VST),
      /** */
      _AreaStatus(-16423, "_AreaStatus", Category.VST), 
      /** */
      _MyConnection(-16422, "_MyConnection", Category.VST , MANDATORY),
      /** */
      _UserStatus(-16421, "_UserStatus", Category.VST), 
      /** */
      _StatBase(-16420, "_StatBase", Category.VST), 
      /** */
      _TxeLock(-16419, "_TxeLock", Category.VST),
      /** */
      _Resrc(-16418, "_Resrc", Category.VST), 
      /** */
      _Latch(-16417, "_Latch", Category.VST), 
      /** */
      _IndexStat(-16416, "_IndexStat", Category.VST),
      /** */
      _TableStat(-16415, "_TableStat", Category.VST), 
      /** */
      _License(-16414, "_License", Category.VST), 
      /** */
      _Trans(-16413, "_Trans", Category.VST, MetadataManager::populateTransTable), 
      /** */
      _Lock(-16412, "_Lock", Category.VST, MANDATORY),
      /** */
      _Checkpoint(-16411, "_Checkpoint", Category.VST), 
      /** */
      _LockReq(-16410, "_LockReq", Category.VST), 
      /** */
      _UserIO(-16409, "_UserIO", Category.VST),
      /** */
      _Block(-16408, "_Block", Category.VST), 
      /** */
      _ActOther(-16407, "_ActOther", Category.VST), 
      /** */
      _ActRecord(-16406, "_ActRecord", Category.VST),
      /** */
      _ActIndex(-16405, "_ActIndex", Category.VST), 
      /** */
      _ActSpace(-16404, "_ActSpace", Category.VST), 
      /** */
      _ActIOFile(-16403, "_ActIOFile", Category.VST),
      /** */
      _ActIOType(-16402, "_ActIOType", Category.VST), 
      /** */
      _ActLock(-16401, "_ActLock", Category.VST), 
      /** */
      _ActAILog(-16400, "_ActAILog", Category.VST),
      /** */
      _ActBILog(-16399, "_ActBILog", Category.VST), 
      /** */
      _ActPWs(-16398, "_ActPWs", Category.VST), 
      /** */
      _ActBuffer(-16397, "_ActBuffer", Category.VST),
      /** */
      _ActServer(-16396, "_ActServer", Category.VST), 
      /** */
      _ActSummary(-16395, "_ActSummary", Category.VST), 
      /** */
      _UserLock(-16394, "_UserLock", Category.VST),
      /** */
      _Filelist(-16393, "_Filelist", Category.VST, MANDATORY, MetadataManager::populateFilelist), 
      /** */
      _Startup(-16392, "_Startup", Category.VST, MANDATORY, MetadataManager::populateStartupTable), 
      /** */
      _Servers(-16391, "_Servers", Category.VST),
      /** */
      _Segments(-16390, "_Segments", Category.VST), 
      /** */
      _Logging(-16389, "_Logging", Category.VST), 
      /** */
      _BuffStatus(-16388, "_BuffStatus", Category.VST),
      /** */
      _DbStatus(-16387, "_DbStatus", Category.VST), 
      /** */
      _MstrBlk(-16386, "_MstrBlk", Category.VST), 
      /** */
      _Connect(-16385, "_Connect", Category.VST, MANDATORY),
      /** */
      _Constraint_Keys(-361, "_Constraint-Keys", Category.DS_SCHEMA),
      /** */
      _Constraint(-360, "_Constraint", Category.DS_SCHEMA),
      /** */
      _Partition_Set_Detail(-351, "_Partition-Set-Detail", Category.PHY_ST),
      /** */
      _Partition_Set(-350, "_Partition-Set", Category.PHY_ST), 
      /** */
      _Tenant(-340, "_Tenant", Category.AUTH, MANDATORY, MetadataManager::populateTenant),
      /** */
      _KeyEvent(-208, "_KeyEvent", Category.LOG),
      /** */
      _sec_authentication_domain(-207, "_sec-authentication-domain", Category.AUTH, REGULAR | READONLY),
      /** */
      _sec_authentication_system(-206, "_sec-authentication-system", Category.AUTH, REGULAR | READONLY),
      /** */
      _sec_granted_role_condition(-205, "_sec-granted-role-condition", Category.AUTH),
      /** */
      _sec_granted_role(-204, "_sec-granted-role", Category.AUTH), 
      /** */
      _sec_role(-203, "_sec-role", Category.AUTH),
      /** */
      _Db_Option(-202, "_Db-Option", Category.AUTH), 
      /** */
      _Db_Detail(-201, "_Db-Detail", Category.AUDIT),
      /** */
      _Client_Session(-200, "_Client-Session", Category.AUDIT), 
      /** */
      _Sysautostats(-127, "_Sysautostats", Category.SQL_STAT),
      /** */
      _SQL_Properties(-126, "_SQL_Properties", Category.SQL_CAT), 
      /** */
      _Sysschemas(-125, "_Sysschemas", Category.SQL_CAT),
      /** */
      _Sysroles(-124, "_Sysroles", Category.SQL_CAT),
      /** */
      _Syschkconstr_Name_Map(-123, "_Syschkconstr_Name_Map", Category.SQL_CAT),
      /** */
      _Sysviews_Name_Map(-122, "_Sysviews_Name_Map", Category.SQL_CAT),
      /** */
      _Sysseqauth(-121, "_Sysseqauth", Category.SQL_AUTH), 
      /** */
      _Syststzstat(-120, "_Syststzstat", Category.SQL_STAT),
      /** */
      _Sysnvarcharstat(-119, "_Sysnvarcharstat", Category.SQL_STAT),
      /** */
      _Sysncharstat(-118, "_Sysncharstat", Category.SQL_STAT),
      /** */
      _Sysbigintstat(-117, "_Sysbigintstat", Category.SQL_STAT),
      /** */
      _Systinyintstat(-116, "_Systinyintstat", Category.SQL_STAT), 
      /** */
      _Systsstat(-115, "_Systsstat", Category.SQL_STAT),
      /** */
      _Sysrealstat(-114, "_Sysrealstat", Category.SQL_STAT), 
      /** */
      _Syscharstat(-113, "_Syscharstat", Category.SQL_STAT),
      /** */
      _Systimestat(-112, "_Systimestat", Category.SQL_STAT), 
      /** */
      _Sysfloatstat(-110, "_Sysfloatstat", Category.SQL_STAT),
      /** */
      _Sysdatestat(-109, "_Sysdatestat", Category.SQL_STAT), 
      /** */
      _Sysnumstat(-108, "_Sysnumstat", Category.SQL_STAT),
      /** */
      _Sysintstat(-107, "_Sysintstat", Category.SQL_STAT), 
      /** */
      _Syssmintstat(-106, "_Syssmintstat", Category.SQL_STAT),
      /** */
      _Sysvarcharstat(-105, "_Sysvarcharstat", Category.SQL_STAT), 
      /** */
      _Syscolstat(-104, "_Syscolstat", Category.SQL_STAT),
      /** */
      _Sysproctext(-103, "_Sysproctext", Category.SQL_SP), 
      /** */
      _Sysprocbin(-102, "_Sysprocbin", Category.SQL_SP),
      /** */
      _Systrigcols(-99, "_Systrigcols", Category.SQL_TR), 
      /** */
      _Systrigger(-98, "_Systrigger", Category.SQL_TR),
      /** */
      _Sysproccolumns(-97, "_Sysproccolumns", Category.SQL_SP), 
      /** */
      _Sysprocedures(-96, "_Sysprocedures", Category.SQL_SP),
      /** */
      _Sysidxstat(-94, "_Sysidxstat", Category.SQL_STAT), 
      /** */
      _Systblstat(-93, "_Systblstat", Category.SQL_STAT),
      /** */
      _Syscalctable(-92, "_Syscalctable", Category.SQL_CAT), 
      /** */
      _Sysdatatypes(-91, "_Sysdatatypes", Category.SQL_CAT),
      /** */
      _Syschkcolusage(-90, "_Syschkcolusage", Category.SQL_CAT),
      /** */
      _Systblconstrs(-89, "_Systblconstrs", Category.SQL_CAT), 
      /** */
      _Syschkconstrs(-88, "_Syschkconstrs", Category.SQL_CAT),
      /** */
      _Sysrefconstrs(-87, "_Sysrefconstrs", Category.SQL_CAT),
      /** */
      _Syskeycolusage(-86, "_Syskeycolusage", Category.SQL_CAT),
      /** */
      _Syssynonyms(-84, "_Syssynonyms", Category.SQL_CAT),
      /** */
      _Syscolauth(-83, "_Syscolauth", Category.SQL_AUTH), 
      /** */
      _Systabauth(-82, "_Systabauth", Category.SQL_AUTH),
      /** */
      _Sysdbauth(-81, "_Sysdbauth", Category.SQL_AUTH), 
      /** */
      _Sysviews(-80, "_Sysviews", Category.SQL_CAT),
      /** */
      _Word_rule(-79, "_Word-rule", Category.SCHEMA), 
      /** */
      _AreaExtent(-72, "_AreaExtent", Category.PHY_ST),
      /** */
      _Area(-71, "_Area", Category.PHY_ST, MANDATORY, MetadataManager::populateArea), 
      /** */
      _StorageObject(-70, "_StorageObject", Category.PHY_ST),
      /** */
      _Collation(-69, "_Collation", Category.SCHEMA), 
      /** */
      _Codepage(-68, "_Codepage", Category.SCHEMA),
      /** */
      _Sequence(-67, "_Sequence", Category.SCHEMA, MANDATORY, MetadataManager::populateSequence), 
      /** */
      _Field_Trig(-66, "_Field-Trig", Category.SCHEMA, MANDATORY),
      /** */
      _File_Trig(-65, "_File-Trig", Category.SCHEMA, MANDATORY), 
      /** */
      _View_Ref(-32, "_View-Ref", Category.SQL89),
      /** */
      _View_Col(-31, "_View-Col", Category.SQL89), 
      /** */
      _View(-30, "_View", Category.SQL89), 
      /** */
      _Db(-6, "_Db", Category.SCHEMA, MANDATORY, MetadataManager::createDbMetaRecord),
      /** */
      _User(-5, "_User", Category.AUTH, REGULAR | MANDATORY), 
      /** */
      _Index_Field(-4, "_Index-Field", Category.SCHEMA, MANDATORY),
      /** */
      _Index(-3, "_Index", Category.SCHEMA, MANDATORY), 
      /** */
      _Field(-2, "_Field", Category.SCHEMA, MANDATORY), 
      /** */
      _File(-1, "_File", Category.SCHEMA, MANDATORY, MetadataManager::populateFileTable);
      
      /** Table id */
      public final int fileNum;
      
      /** Table name */
      public final String fileName;
      
      /** Table converted name */
      public final String shortName;
      
      /** DMO interface simple name == FWD table name*/
      public final String ifaceName;
      
      /** Table category */
      public final Category category;
      
      /** table flags */
      public final int flags;
      
      /** A bi-function which populates the table on startup */
      public final BiConsumer<Database, Persistence> init;
      
      /**
       * Constructor.
       * 
       * @param   fileNum
       *          table id
       * @param   fileName
       *          table name
       * @param   category
       *          table category
       * @param   flags
       *          table flags
       * @param   init
       *          table initializer
       */
      SystemTable(int fileNum,
                  String fileName,
                  Category category,
                  int flags,
                  BiConsumer<Database, Persistence> init)
      {
         this.fileNum = fileNum;
         this.fileName = fileName;
         this.shortName = getShortName(fileName);
         this.ifaceName = META_PREFIX + shortName;
         this.category = category;
         this.flags = flags;
         this.init = init == null || !LOG_INFO ? init : (d, p) -> 
            {
               long start = System.nanoTime();
               log.log(Level.INFO, String.format("Populating [%s]", fileName));
               init.accept(d, p);
               log.log(Level.INFO, String.format("Populated [%s] in %d ms",  
                              fileName,  TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)
                           )
               );
            };
         MetadataManager.byName.put(fileName.toLowerCase(), this);
      }
      
      /**
       * Constructor.
       * 
       * @param   fileNum
       *          table id
       * @param   fileName
       *          table name
       * @param   category
       *          table category
       */
      private SystemTable(int fileNum, String fileName, Category category)
      {
         this(fileNum, fileName, category, 0, null);
      }
      
      /**
       * Constructor
       * 
       * @param   fileNum
       *          table id
       * @param   fileName
       *          table name
       * @param   category
       *          table category
       * @param   flags
       *          table flags
       */
      private SystemTable(int fileNum, String fileName, Category category, int flags)
      {
         this(fileNum, fileName, category, flags, null);
      }
      
      /**
       * Constructor
       * 
       * @param   fileNum
       *          table id
       * @param   fileName
       *          table name
       * @param   category
       *          table category
       * @param   populate
       *          table initializer
       */
      private SystemTable(int fileNum,
                          String fileName,
                          Category category,
                          BiConsumer<Database,
                          Persistence> populate)
      {
         this(fileNum, fileName, category, 0, populate);
      }
      
      /**
       * Populate all metatables with provided initializer.
       * 
       * @param   metaDb
       *          meta database
       * @param   p
       *          Persistence instance for the meta database
       */
      public static void populateAll(Database metaDb, Persistence p)
      {
         Arrays.stream(values())
               .map(t -> t.init)
               .filter(Objects::nonNull)
               .forEach(init -> init.accept(metaDb, p));
      }
      
      /**
       * Get system table by DMO interface short name
       * 
       * @param   iface
       *          table DMO interface short name
       * 
       * @return  System table with a given DMO interface short name.
       */
      public static SystemTable forIface(String iface)
      {
         return Arrays.stream(values())
                      .filter(t -> t.ifaceName.equalsIgnoreCase(iface))
                      .findFirst()
                      .orElse(null);
      }
      
      /**
       * Get a set of tables names that match the provided filter.
       * 
       * @param   flag
       *          The flag used to filter the tables. The filter can be any flag from
       *          {@link TableFlags} and is applied to {@link SystemTable#values()}.
       *          
       * @return  Set of table names that match the provided filter.
       */
      public static Set<String> filterTables(int flag)
      {
         Set<String> filteredTables = new HashSet<>();
         SystemTable[] tables = SystemTable.values();
         for (int i = 0; i < tables.length; i++)
         {
            SystemTable t = tables[i];
            if ((t.flags & flag) == flag)
            {
               filteredTables.add(t.fileName.trim().toLowerCase());
            }
         }
         return filteredTables;
      }
      
      /**
       * Check if the table is in the master database
       * 
       * @return  <code>true</code> if the table is in the master database.
       */
      public boolean isRegular() 
      {
         return (this.flags & REGULAR) != 0;
      }
      
      /**
       * Check if the table is mandatory
       * 
       * @return  <code>true</code> if the table is mandatory.
       */
      public boolean isMandatory()
      {
         return (this.flags & MANDATORY) != 0;
      }
      
      /**
       * Check if the table is read-only.
       * 
       * @return  {@code true} if the table is mandatory.
       */
      public boolean isReadOnly()
      {
         return (this.flags & READONLY) != 0;
      }
   }
   
   /**
    * {@code Map<String, Object>} fluent builder.
    * 
    * @param <T> builder extension class
    */
   private static class MapBuilder<T extends MapBuilder<T>>
   {
      /** Map of properties to be built. */
      private final Map<Tuple<String, Integer>, Object> m = new LinkedHashMap<>();
      
      /**
       * Return reference to the actual class (required for the fluent operations' extension)
       * 
       * @return reference to the actual class
       */
      public T self()
      {
         return (T) this;
      }
      
      /**
       * Add key/value pair to the map. This is the method used for scalar properties.
       * 
       * @param   key
       *          key
       * @param   value
       *          value
       * 
       * @return  The builder instance.
       */
      public T put(String key, Object value)
      {
         m.put(new Tuple<>(key, null), value);
         return self();
      }
      
      /**
       * Add key/index/value tuple for mapping. This method is to be used for extent properties.
       * 
       * @param   key
       *          The extent property to be populated.
       * @param   idx
       *          The index within the extent property to be populated.
       * @param   value
       *          The value for the property.
       * 
       * @return  The builder instance.
       */
      public T put(String key, Integer idx, Object value)
      {
         m.put(new Tuple<>(key, idx), value);
         return self();
      }
      
      /**
       * Add all key/value pairs from the provided map
       * 
       * @param   map
       *          Map to be added.
       *
       * @return  The builder instance.
       */
      public T putAll(Map<Tuple<String, Integer>, Object> map)
      {
         m.putAll(map);
         return self();
      }
      
      /**
       * Build the immutable map.
       * 
       * @return the immutable map
       */
      public Map<Tuple<String, Integer>, Object> build()
      {
         return Collections.unmodifiableMap(m);
      }
   }
   
   /**
    * _File table record field/value map builder.
    */
   private static class FileTableRecordBuilder extends MapBuilder<FileTableRecordBuilder>
   {
      /**
       * Return reference to the actual class (required for the fluent operations' extension)
       * 
       * @return  Reference to the actual class.
       */
      @Override
      public FileTableRecordBuilder self()
      {
         return this;
      }
      
      /**
       * Build the immutable map for the _File table record.
       * 
       * @param   dmoIface
       *          _File table DMO interface.
       * @param   pk
       *          The _prime-index field value holder.
       * @param   children
       *          Records' data for the child tables.
       *
       * @return  The immutable map for the _File table record.
       */
      public Map<Tuple<String, Integer>, Object> build(
            Class<? extends DataModelObject> dmoIface,
            integer pk,
            Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children)
      {
         Table table = dmoIface.getAnnotation(Table.class);
         int numfld = children.get(SystemTable._Field).size();
         
         return put("dbRecid", integer.of(DB_RECID)).
                put("fileName", new character(table.legacy())).
                put("owner", character.of("PUB")).
                put("userMisc", new character(table.name())).
                put("dumpName", new character(table.dumpName())).
                put("frozen", logical.of(table.isFrozen())).
                put("hidden", logical.of(table.isHidden())).
                put("category", new character(table.category())).
                put("numfld", integer.of(numfld)).
                put("primeIndex", pk).
                put("desc", new character(table.description())).
                put("fileLabel", new character(table.label())).
                put("fileLabelSa", new character(table.label_sa())).
                put("valexp", new character(table.valexp())).
                put("valmsg", new character(table.valmsg())).
                put("valmsgSa", new character(table.valmsg_sa())).
                put("canRead", character.of(table.canRead())).
                put("canWrite", character.of(table.canWrite())).
                put("canCreate", character.of(table.canCreate())).
                put("canDelete", character.of(table.canDelete())).
                put("canDump", character.of(table.canDump())).
                put("canLoad", character.of(table.canLoad())).
                build();
      }
   }
}