# 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();
          }
       }
    }