# P2OAccessWorker.java

    /*
    ** Module   : P2OAccessWorker.java
    ** Abstract : Pattern worker which provides lookup access to information in P2O ASTs
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ---------------------------------- Description ----------------------------------
    ** 001 ECF 20050921   @22771 Created initial version. Lazily loads P2O
    **                           ASTs and enables lookups of Java names for
    **                           legacy Progress table and field names.
    **                           This first implementation supports fixed
    **                           tables only (not temp- or work-tables).
    ** 002 ECF 20051004   @22933 Added javaPackage user function. Retrieves
    **                           java package name of DMO associated with a
    **                           legacy table name.
    ** 003 ECF 20051005   @22985 Shorten P2O filename. Replace ".schema" with
    **                           ".p2o" instead of appending ".p2o" extension.
    ** 004 ECF 20051013   @23035 Refactored to abstract out P2O AST loading
    **                           and initialization. These tasks are now
    **                           delegated to P2OLookup.
    ** 005 ECF 20051017   @23053 Added javaPropertyName user function. Returns
    **                           name of DMO property associated with a legacy
    **                           database field name.
    ** 006 ECF 20051024   @23096 Added boolean 'qualified' parameter to
    **                           javaPropertyName user function. Optionally
    **                           qualifies property name with a buffer name to
    **                           enable use within HQL where and order by
    **                           clauses, where qualification is necessary.
    ** 007 ECF 20051102   @23182 Added legacyFieldNames user function.
    **                           Retrieves set of legacy field names for a
    **                           Progress table, in display order.
    ** 008 ECF 20051121   @23422 Added isValidatable user function. Indicates
    **                           whether a schema field must invoke validation
    **                           processing before it may be saved to its DMO.
    **                           Delegates work to P2OLookup.
    ** 009 ECF 20060202   @24253 Added support for temp/work-table schema
    **                           data lookups. Implemented visitAst method to
    **                           load current, temporary schema data into
    **                           P2OLookup's state each time a new source file
    **                           is visited during conversion.
    ** 010 ECF 20060408   @25430 Added isDMOInterface() method. Indicates
    **                           whether a given class name collides with any
    **                           known DMO interface name. Temp table DMOs not
    **                           currently handled.
    ** 011 ECF 20061026   @30796 Added isForeignReference() method. Indicates
    **                           whether a given table is referenced by the
    **                           foreign key of any other table.
    ** 012 GES 20090424   @41952 Import change.
    ** 013 GES 20090518   @42397 Import change.
    ** 014 OM  20131030          Added support for checking if a schema contains a certain dmo iface.
    ** 015 VMN 20140328          Added support for denormalized properties.
    ** 016 VMN 20150128          Fixed denormalized field assignments.
    ** 017 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 018 ECF 20180305          Added isReadOnly API to check for a read-only table.
    ** 019 ECF 20180909          Added support for unused database resource reports.
    ** 020 CA  20190508          Buffers must be looked in the class hierarchy, not only in the
    **                           current class (thus we need to recursively load .p2o files).
    ** 021 CA  20200122          Javadoc fixes.
    ** 022 CA  20200412          Added incremental conversion support.
    ** 023 CA  20201008          Added APIs to obtain an iterator over the indexes or properties of a table.
    **     OM  20201231          Avoid java name conflicts.
    **     OM  20210711          In case of buffer parameters, use the super .Buf interface name to allow calls
    **                           with 'sibling' tables. Access that with the new javaInterfaceName() method.
    **     IAS 20211928          Added isNullable methods
    **     CA  20211214          The AST manager plugin must be context-local, for runtime conversion, as the
    **                           InMemoryRegistryPlugin is not thread-safe.
    **                           All worker's state must be context-local, for runtime conversion to work in
    **                           multi-context mode, as the pattern workers are singletons.
    **     CA  20230110          Let 'getImmediateChild' work with a set, if an array is used for the types.
    ** 024 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 025 RAA 20240821          Added support for reporting indeterminate sorting queries.
    **     RAA 20240821          Modified query string initialization in writeIndeterminateSortQueryContent.
    **     RAA 20240826          Added isIndeterminateSortQuery and checkFieldMatching functions.
    **     RAA 20240904          Added risk factor to indeterminate sorting queries.
    **     RAA 20240905          Refactored QueryIndexInfo to not store the given AST.
    **     RAA 20240906          Added writeHeaderToIndeterminateSortFile() method and cleared the query list.
    **     RAA 20240910          checkFieldMatching now uses the legacy name criteria instead of the order by.
    ** 026 OM  20241128          Multi-tenant conversion support: added getTenantType() method.
    ** 027 CA  20250319          This worker can be used before the 'qualified_oo_name' is set in annotation
    **                           (like in schema conversion phase), so ensure we can load the ClassDefinition.
    */

    /*
    ** 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.schema;

    import java.io.*;
    import java.util.*;
    import java.util.logging.*;

    import com.goldencode.ast.*;
    import com.goldencode.p2j.convert.*;
    import com.goldencode.util.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.pattern.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.uast.*;

    /**
     * A pattern worker that provides services to support the retrieval of
     * information from previously created, persisted, P2O ASTs.
     * <p>
     * Currently, this worker supports lookups of converted Java schema names,
     * based upon legacy, Progress schema names as input. Specifically, given a
     * fully qualified, Progress database table name, the Java name for the
     * corresponding Data Model Object (DMO) class can be retrieved. The Java
     * name of the implicit (default) buffer for that table is also available.
     * Given a fully qualified, Progress database field name, the Java name for
     * the corresponding DMO getter and setter methods can be retrieved.
     */
    public final class P2OAccessWorker
    extends AbstractPatternWorker
    implements DataModelTokenTypes
    {
       /** Logger. */
       private static final ConversionStatus LOG = ConversionStatus.get(P2OAccessWorker.class);

       /** Table token types */
       private static final Collection<Integer> types = new HashSet<>();

       static
       {
          types.add(ProgressParserTokenTypes.TABLE);
          types.add(ProgressParserTokenTypes.TEMP_TABLE);
          types.add(ProgressParserTokenTypes.WORK_TABLE);
          types.add(ProgressParserTokenTypes.BUFFER);
       }

       /** Context local work area. */
       private static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
       {
          protected WorkArea initialValue()
          {
             return new WorkArea();
          }
       };

       /**
        * Default constructor which initializes libraries.
        */
       public P2OAccessWorker()
       {
          super();

          setLibrary(new Library());
       }

       /**
        * Extract the target table (or temp/work-table or buffer) name from the given AST, which is
        * expected to be of type {@code RECORD_PHRASE} or {@code EMBEDDED_SQL}.
        *
        * @param   ast
        *          Record phrase or embedded SQL AST.
        *
        * @return  Target table name, qualified by database schema name.
        *
        * @throws  SchemaException
        *          if an unexpected AST node is passed to this method.
        */
       private static String getTableName(Aast ast)
       throws SchemaException
       {
          Aast child = null;
          switch (ast.getType())
          {
             case ProgressParserTokenTypes.RECORD_PHRASE:
                child = ast.getImmediateChild(types, null);
                break;

             case ProgressParserTokenTypes.EMBEDDED_SQL:
                child = ast.getChildAt(0); // kw_select or delete_from
                if (child.getType() == ProgressParserTokenTypes.KW_SELECT)
                {
                   // table node is one generation further down in this case, under an explicit
                   // kw_from node
                   child = child.getImmediateChild(ProgressParserTokenTypes.KW_FROM, null);
                }
                child = child.getImmediateChild(types, null);
                break;

             default:
                throw new SchemaException("Unexpected token: " + ast.toString());
          }

          return  (String) child.getAnnotation("schemaname");
       }

       /**
        * This method is called each time a new AST is visited, just before the
        * pattern engine processes that AST.  We exploit this hook by loading the
        * current temp/work-table schema (P2O) AST into the <code>P2OLookup</code>
        * class' state.  This enables us to resolve references to temp and work
        * tables, which are local to the currently loaded source file AST.
        *
        * @param   ast
        *          The root node of the source AST about to be processed by the
        *          pattern engine.
        */
       public void visitAst(Aast ast)
       {
          WorkArea wa = context.get();

          ClassDefinition clsDef = SymbolResolver.resolveClassDefinition(ast);
          String fileName = ast.getFilename();
          P2OLookup.loadTemporarySchema(fileName, clsDef);
          wa.suspiciousQueries.clear();
          wa.indeterminateSortQueries.clear();
       }

       /**
        * Resolve <code>constant</code> to a literal which can be compiled into
        * expressions.
        *
        * @param   constant
        *          Constant indicating a data model token name.
        *
        * @return  Token type associated with the token name.
        */
       public Object resolveConstant(String constant)
       {
          return DataModelAst.resolveConstant(constant);
       }

       /**
        * Helper to lookup Java names for tables and fields, given their
        * historical, Progress names.
        */
       public class Library
       {
          /**
           * Obtains an iterator to the current record's list of indexes.
           *
           * @param    schemaname
           *           The fully qualified schema name of the record being processed.
           *
           * @return   The list of indexes.
           */
          public Iterator<P2JIndex> indexes(String schemaname)
          {
             Iterator<P2JIndex> iter = null;

             try
             {
                iter = P2OLookup.indexes(schemaname);
             }

             catch (SchemaException se)
             {
                LOG.log(Level.SEVERE, "", se);
             }

             return iter;
          }

          /**
           * Obtains an iterator to the current record's list of indexes.
           *
           * @param    schemaname
           *           The fully qualified schema name of the record being processed.
           *
           * @return   The list of indexes.
           */
          public Iterator<Aast> properties(String schemaname)
          {
             Iterator<Aast> iter = null;

             try
             {
                iter = P2OLookup.properties(schemaname);
             }

             catch (SchemaException se)
             {
                LOG.log(Level.SEVERE, "", se);
             }

             return iter;
          }

          /**
           * Look up the package of a Java DMO interface, given its legacy name.
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a table.
           *
           * @return  Package name of the DMO interface.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence.
           */
          public String javaPackage(String historical)
          throws SchemaException
          {
             return P2OLookup.javaPackage(historical);
          }

          /**
           * Look up the name of a Java DMO interface, given its legacy name.
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a table.
           *
           * @return  Unqualified, Java name of the interface which defines the
           *          DMO API.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if the table portion of <code>historical</code> cannot be
           *          resolved to a corresponding Java name.
           */
          public String javaClassName(String historical)
          throws SchemaException
          {
             return P2OLookup.javaName(historical, EntityName.TABLE);
          }

          /**
           * Obtain the multi-tenant table type. The values are:
           *    0 = single-tenant (not store here),
           *    1 = multi-tenant,
           *    2 = multi-tenant-with-default.
           *
           * @param   historical
           *          The table name.
           *
           * @return  The table multi-tenant type, as described above.
           *
           * @throws  SchemaException
           *          In case of error accessing internal data.
           */
          public int getTenantType(String historical)
          throws SchemaException
          {
             return P2OLookup.getTenantType(historical);
          }

          /**
           * In the case of temp-tables, look up the name of a Java DMO super interface, given its legacy name.
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a temp-table.
           *
           * @return  Unqualified, Java name of the interface which defines the DMO API.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if the table portion of {@code historical} cannot be resolved to a corresponding Java name.
           */
          public String javaInterfaceName(String historical)
          throws SchemaException
          {
             return P2OLookup.javaInterfaceName(historical);
          }

          /**
           * Look up the name of a Java DMO interface, given its legacy name,
           * and return the name of the default, implicit buffer associated with
           * this interface. This is simply the name of the appropriate DMO
           * interface, with the first letter lowercased.
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a table.
           *
           * @return  Short DMO interface name with first letter decapitalized.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if the table portion of <code>historical</code> cannot be
           *          resolved to a corresponding Java name.
           */
          public String javaBufferName(String historical)
          throws SchemaException
          {
             return StringHelper.changeCase(javaClassName(historical), false);
          }

          /**
           * Look up the name of the name of the DMO property which represents
           * the specified, legacy database field.
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a database field.
           * @param   qualified
           *          <code>true</code> if property name should be qualified with
           *          a buffer name;  <code>false</code> if it should remain
           *          unqualified.
           *
           * @return  Converted Java property name.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if the field portion of <code>historical</code> cannot be
           *          resolved to a corresponding Java name.
           */
          public String javaPropertyName(String historical, boolean qualified)
          throws SchemaException
          {
             return P2OLookup.javaPropertyName(historical, qualified, null);
          }

          /**
           * Look up the name of the name of the DMO property which represents
           * the specified, legacy database field.
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a database field.
           * @param   qualified
           *          <code>true</code> if property name should be qualified with
           *          a buffer name;  <code>false</code> if it should remain
           *          unqualified.
           * @param   index
           *          Zero-based index of custom property name for denormalized field with extent,
           *          otherwise null.
           *
           * @return  Converted Java property name.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if the field portion of <code>historical</code> cannot be
           *          resolved to a corresponding Java name.
           */
          public String javaPropertyName(String historical, boolean qualified, Long index)
          throws SchemaException
          {
             return P2OLookup.javaPropertyName(historical, qualified, index);
          }

          /**
           * Retrieve the full set of fully qualified, legacy field names for a
           * legacy Progress table.  The set is guaranteed to iterate in the
           * default display order defined for the original, Progress schema for
           * that table.
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a table.
           * @param   includeHidden
           *          If {@code true} the hidden fields are also included.
           *
           * @return  Set of fully qualified, legacy field names.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if <code>historical</code> does not represent a known table.
           */
          public Set<String> legacyFieldNames(String historical, boolean includeHidden)
          throws SchemaException
          {
             return P2OLookup.legacyFieldNames(historical, includeHidden);
          }

          /**
           * Look up the name of the accessor (getter) method associated with the
           * DMO property which represents the specified, legacy database field.
           * The method name is in the form of a typical Java-bean getter:
           * <pre>
           *    get{PropertyName}
           * </pre>
           * or
           * <pre>
           *    is{PropertyName}
           * </pre>
           * if <i>PropertyName</i> represents a <code>logical</code> data type.
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a database field.
           * @param   isLogical
           *          <code>true</code> if <code>historical</code> represents a
           *          <code>logical</code> field, else <code>false</code>.
           *
           * @return  Java-bean style getter method name.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if the field portion of <code>historical</code> cannot be
           *          resolved to a corresponding Java name.
           */
          public String javaGetterName(String historical, boolean isLogical)
          throws SchemaException
          {
             String prefix = isLogical ? "is" : "get";

             return P2OLookup.beanMethodName(prefix, historical);
          }

          /**
           * Look up the name of the accessor (getter) method associated with the
           * DMO property which represents the specified, legacy database field.
           * The method name is in the form of a typical Java-bean getter:
           * <pre>
           *    get{PropertyName}
           * </pre>
           * or
           * <pre>
           *    is{PropertyName}
           * </pre>
           * if <i>PropertyName</i> represents a <code>logical</code> data type.
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a database field.
           * @param   index
           *          Zero-based index of custom property name for denormalized field with extent,
           *          otherwise null.
           * @param   isLogical
           *          <code>true</code> if <code>historical</code> represents a
           *          <code>logical</code> field, else <code>false</code>.
           *
           * @return  Java-bean style getter method name.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if the field portion of <code>historical</code> cannot be
           *          resolved to a corresponding Java name.
           */
          public String javaGetterName(String historical, Long index, boolean isLogical)
          throws SchemaException
          {
             String prefix = isLogical ? "is" : "get";
             if ( index != null)
             {
                index --;
             }
             return P2OLookup.beanMethodName(prefix, historical, index);
          }

          /**
           * Look up the name of the mutator (setter) method associated with the
           * DMO property which represents the specified, legacy database field.
           * The method name is in the form of a typical Java-bean setter:
           * <pre>
           *    set{PropertyName}
           * </pre>
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a database field.
           *
           * @return  Java-bean style setter method name.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if the field portion of <code>historical</code> cannot be
           *          resolved to a corresponding Java name.
           */
          public String javaSetterName(String historical)
          throws SchemaException
          {
             return P2OLookup.beanMethodName("set", historical);
          }

          /**
           * Look up the name of the mutator (setter) method associated with the
           * DMO property which represents the specified, legacy database field.
           * The method name is in the form of a typical Java-bean setter:
           * <pre>
           *    set{PropertyName}
           * </pre>
           *
           * @param   historical
           *          Fully qualified, legacy, Progress schema name for a
           *          database field.
           * @param   index
           *          Zero-based index of custom property name for denormalized field with extent,
           *          otherwise null.
           *
           * @return  Java-bean style setter method name.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence;
           *          if the field portion of <code>historical</code> cannot be
           *          resolved to a corresponding Java name.
           */
          public String javaSetterName(String historical, Long index)
          throws SchemaException
          {
             if ( index != null)
             {
                index --;
             }
             return P2OLookup.beanMethodName("set", historical, index);
          }

          /**
           * Check if given historical fully qualified property is denormalized.
           *
           * @param    historical
           *           Historical fully qualified property name.
           *
           * @return  <code>true</code> if given property is denormalized.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence.
           */
          public boolean isDenormalizedProperty(String historical)
          throws SchemaException
          {
             return P2OLookup.isDenormalizedProperty(historical);
          }

          /**
           * Indicate whether a DMO property can be validated at runtime, given
           * its legacy schema name.  Possible forms of validation include:
           * <ul>
           *   <li>mandatory (non-nullable) constraint;
           *   <li>participation in a unique constraint;
           *   <li>explicit validation method (converted from a Progress validation expression).
           * </ul>
           *
           * @param   historical
           *          Fully qualified, legacy Progress field name.
           *
           * @return  <code>true</code> if the field should be validated at
           *          runtime, else <code>false</code>.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence.
           */
          public boolean isValidatable(String historical)
          throws SchemaException
          {
             return P2OLookup.isValidatable(historical);
          }

          /**
           * Indicate whether a DMO property is nullable.
           *
           * @param   historical
           *          Fully qualified, legacy Progress field name.
           *
           * @return  <code>true</code> if the field is nullable, else <code>false</code>.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence.
           */
          public boolean isNullable(String historical)
          throws SchemaException
          {
             return P2OLookup.isNullable(historical);
          }

          /**
           * Indicate whether a DMO class represents a read-only table, given its schema name.
           *
           * @param   historical
           *          Fully qualified, legacy Progress table name.
           *
           * @return  {@code true} if the table is read-only, else {@code false}.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence.
           */
          public boolean isReadOnly(String historical)
          throws SchemaException
          {
             return P2OLookup.isReadOnly(historical);
          }

          /**
           * Report whether the given class name is the unqualified name of any
           * DMO interface, in any schema tracked by the current P2J
           * configuration.
           *
           * @param   name
           *          An unqualified class name.
           *
           * @return  <code>true</code> if <code>name</code> collides with any
           *          DMO interface in any schema, else <code>false</code>.
           *
           * @throws  SchemaException
           *          if there is an error loading schema names from configuration.
           */
          public boolean isDMOInterface(String name)
          throws SchemaException
          {
             return P2OLookup.isDMOInterface(name);
          }

          /**
           * Report whether the given class name is the unqualified name of a DMO interface, in the
           * <code>schema</code> tracked by the current P2J configuration.
           *
           * @param   name
           *          An unqualified class name.
           * @param   schema
           *          The name of the schema to look into.
           *
           * @return  <code>true</code> if <code>name</code> is present as java name of a table from
           *          the specified <code>schema</code> and <code>false</code> if <code>schema</code>
           *          does not exist or it does not contain such interface.
           *
           * @throws  SchemaException
           *          if there is an error loading schema names from configuration.
           */
          public boolean isDMOInterface(String name, String schema)
          throws SchemaException
          {
             return P2OLookup.isDMOInterface(name, schema);
          }

          /**
           * Report whether the given, historical table will be mapped in the
           * converted application to a database table which represents a foreign
           * reference of another table's foreign key.
           *
           * @param   historical
           *          Fully qualified, legacy Progress table name.
           *
           * @return  <code>true</code> if the given table represents the foreign
           *          end of a one-to-one or a one-to-many relation between two
           *          tables.
           *
           * @throws  SchemaException
           *          if there is an error loading schema names from configuration.
           */
          public boolean isForeignReference(String historical)
          throws SchemaException
          {
             return P2OLookup.isForeignReference(historical);
          }

          /**
           * The given index in the given table has been referenced in business logic. Remove it from
           * the mutable index resource map.
           *
           * @param   ast
           *          Record phrase or embedded SQL AST node.
           * @param   index
           *          Legacy name of index which was referenced.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence.
           */
          public void indexReferenced(Aast ast, String index)
          throws SchemaException
          {
             if (Configuration.isRuntimeConfig())
             {
                return;
             }

             String table = getTableName(ast);
             P2OLookup.indexReferenced(table, index);
          }

          /**
           * The given field has been referenced in business logic. Remove it from the mutable field
           * resource map.
           *
           * @param   field
           *          Legacy name (normalized schemaname) of field which was referenced.
           *
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence.
           */
          public void fieldReferenced(String field)
          throws SchemaException
          {
             if (Configuration.isRuntimeConfig())
             {
                return;
             }

             P2OLookup.fieldReferenced(field);
          }

          /**
           * Delete previous database resource report files, if any.
           */
          public void deleteDatabaseResourceReports()
          {
             if (Configuration.isRuntimeConfig())
             {
                return;
             }

             P2OLookup.deleteDatabaseResourceReports();
          }

          /**
           * Write the header for the indeterminate sorting queries report.
           */
          public void writeHeaderToIndeterminateSortFile()
          throws IOException
          {
             if (Configuration.isRuntimeConfig())
             {
                return;
             }

             P2OLookup.writeHeaderToIndeterminateSortFile();
          }

          /**
           * Emit content (if any) for the current source file into the unused index report.
           *
           * @param   temp
           *          {@code true} if content relates to temp-tables; {@code false} for permanent
           *          tables.
           * @throws  IOException
           *          if there is an error writing the report.
           */
          public void unusedIndexReport(boolean temp)
          throws IOException
          {
             if (Configuration.isRuntimeConfig())
             {
                return;
             }

             P2OLookup.unusedIndexReport(temp);
          }

          /**
           * Emit content (if any) for the current source file into the unused field report.
           *
           * @param   temp
           *          {@code true} if content relates to temp-tables; {@code false} for permanent
           *          tables.
           * @throws  IOException
           *          if there is an error writing the report.
           */
          public void unusedFieldReport(boolean temp)
          throws IOException
          {
             if (Configuration.isRuntimeConfig())
             {
                return;
             }

             P2OLookup.unusedFieldReport(temp);
          }

          /**
           * Store information about queries with suspicious index selection.
           *
           * @param    ast
           *           Record phrase or embedded SQL AST.
           * @param    comment
           *           The comment to store.
           */
          public void unindexedQuery(Aast ast, String comment)
          throws SchemaException
          {
             if (Configuration.isRuntimeConfig())
             {
                return;
             }

             context.get().suspiciousQueries.add(new QueryIndexInfo(ast, comment, null, 0));
          }

          /**
           * Emit content (if any) for the current source file into the unindexed query report.
           *
           * @throws  IOException
           *          if there is an error writing the report.
           * @throws  SchemaException
           *          if there is any error loading P2J schema configuration data;
           *          if there is any error loading the AST from persistence.
           */
          public void unindexedQueryReport()
          throws IOException,
                 SchemaException
          {
             WorkArea wa = context.get();

             if (Configuration.isRuntimeConfig() || wa.suspiciousQueries.isEmpty())
             {
                return;
             }

             String sep = System.lineSeparator();

             FileWriter fw = new FileWriter(P2OLookup.UNINDEXED_QUERIES_FILE, true);

             try (BufferedWriter bw = new BufferedWriter(fw))
             {
                boolean writeFilename = true;
                for (QueryIndexInfo qii : wa.suspiciousQueries)
                {
                   qii.writeContent(bw, writeFilename);
                   writeFilename = false;
                }

                bw.write(sep);
             }
          }

          /**
           * Check if the chosen legacy name criteria leads to an indeterminate sorting situation.
           * In order to do this, check if the criteria set contains the fields of at least one unique index.
           *
           * @param   ast
           *          Record phrase or embedded SQL AST.
           * @param   legacyNames
           *          A set with the legacy name criteria.
           *
           * @return  {@code 0} if the query has a deterministic sort,
           *          {@code 1} if the query is nearly deterministic (i.e. there is an unique index that has
           *          overlapping fields with the legacy name criteria, but the index fields are not all
           *          mandatory),
           *          {@code 2} if the query has an indeterminate sort (i.e. no unique index overlaps with the
           *          legacy name criteria).
           */
          public int isIndeterminateSortQuery(Aast ast, Set<String> legacyNames)
          throws SchemaException
          {
             if (Configuration.isRuntimeConfig())
             {
                return 0;
             }

             String tableName = getTableName(ast);
             Iterator<P2JIndex> indexes = P2OLookup.indexes(tableName);
             if (!indexes.hasNext())
             {
                return 0;
             }

             boolean foundNonMandatoryOverlappingUniqueIndex = false;
             while (indexes.hasNext())
             {
                P2JIndex index = indexes.next();
                if (index.isUnique() && checkFieldMatching(index, legacyNames))
                {
                   if (areIndexFieldsMandatory(index))
                   {
                      return 0;
                   }
                   else
                   {
                      foundNonMandatoryOverlappingUniqueIndex = true;
                   }
                }
             }

             return foundNonMandatoryOverlappingUniqueIndex ? 1 : 2;
          }

          /**
           * Store information about queries with an indeterminate sorting order.
           *
           * @param   ast
           *          Record phrase or embedded SQL AST.
           * @param   indexName
           *          The name of the chosen index.
           * @param   risk
           *          The risk factor associated with the query.
           */
          public void indeterminateSortQuery(Aast ast, String indexName, int risk)
          throws SchemaException
          {
             if (Configuration.isRuntimeConfig())
             {
                return;
             }

             context.get().indeterminateSortQueries.add(new QueryIndexInfo(ast, null, indexName, risk));
          }

          /**
           * Generate a report in a CSV file about the queries with an indeterminate sorting order.
           */
          public void indeterminateSortQueryReport()
          throws IOException,
                 SchemaException
          {
             WorkArea wa = context.get();

             if (Configuration.isRuntimeConfig() || wa.indeterminateSortQueries.isEmpty())
             {
                return;
             }

             FileWriter fw = new FileWriter(P2OLookup.INDETERMINATE_SORT_QUERIES_FILE, true);
             try (BufferedWriter bw = new BufferedWriter(fw))
             {
                for (QueryIndexInfo qii : wa.indeterminateSortQueries)
                {
                   qii.writeIndeterminateSortQueryContent(bw);
                }

                bw.close();
             }
          }

          /**
           * Check if the fields of the given index are part of the legacy name criteria.
           *
           * @param   index
           *          The index for which to checks its fields.
           * @param   legacyNames
           *          A set with the legacy name criteria.
           *
           * @return  {@code true} if the fields of the index are part of the legacy name criteria set,
           *          {@code false} otherwise.
           */
          private boolean checkFieldMatching(P2JIndex index, Set<String> legacyNames)
          {
             Iterator<P2JIndexComponent> components = index.componentsIter();
             while (components.hasNext())
             {
                String legacyName = components.next().getNormalizedName();
                if (legacyName == null || !legacyNames.contains(legacyName))
                {
                   return false;
                }
             }

             return true;
          }

          /**
           * Check if all the fields from an index are mandatory. A field is mandatory if it is marked as
           * non-nullable.
           *
           * @param   index
           *          The index for which to check the fields.
           *
           * @return  {@code true} if all the fields are mandatory, {@code false} otherwise.
           */
          private boolean areIndexFieldsMandatory(P2JIndex index)
          throws SchemaException
          {
             Iterator<P2JIndexComponent> components = index.componentsIter();
             while (components.hasNext())
             {
                String legacyName = components.next().getLegacyName();
                if (isNullable(legacyName))
                {
                   return false;
                }
             }

             return true;
          }
       }

       /**
        * Helper class which keeps track of queries with suspicious index selection, and which
        * writes content to the unindexed query report.
        */
       private static class QueryIndexInfo
       {
          /** The source file in which the query was found. */
          private final String sourceFile;

          /** The table against which the query is executed. */
          private final String tableName;

          /** Suspicious index selection comment. */
          private final String comment;

          /** The name of the index. */
          private final String indexName;

          /** The risk factor of the query. */
          private final int risk;

          /** The start and end lines of the query from the source file. */
          private final int bounds[];

          /**
           * Constructor.
           *
           * @param   ast
           *          Record phrase or embedded SQL AST.
           * @param   comment
           *          Suspicious index selection comment.
           * @param   indexName
           *          The name of the index.
           * @param   risk
           *          The risk factor of the query.
           */
          QueryIndexInfo(Aast ast, String comment, String indexName, int risk)
          throws SchemaException
          {
             this.sourceFile = ast.getFilename();
             this.tableName = getTableName(ast);
             this.comment = comment;
             this.indexName = indexName;
             this.risk = risk;
             this.bounds = computeStartAndEndLine(ast);
          }

          /**
           * Write content about this query's index selection to the report, including a code
           * snippet of the query.
           * <p>
           * TODO: make the source file reading more efficient; if there are multiple queries from
           * the same file, we re-read a lot of lines unnecessarily.
           *
           * @param   bw
           *          Report writer.
           * @param   writeFilename
           *          Flag that denotes if the filename should be written.
           *
           * @throws  IOException
           *          if there is an error reading from the source file or writing to the report
           *          file.
           * @throws  SchemaException
           *          if there is an error inspecting the AST for information.
           */
          void writeContent(Writer bw, boolean writeFilename)
          throws IOException,
                 SchemaException
          {
             String cacheFile = sourceFile + ".cache";
             String sep = System.lineSeparator();
             String dsep = sep + sep;
             String pad = "   ";
             String dpad = pad + pad;

             if (writeFilename)
             {
                bw.write(cacheFile + ':' + dsep);
             }

             // emit section heading with table name and index selection comment
             bw.write(pad + '(' + tableName + " - " + comment + ')' + dsep);

             // read the relevant source lines from the cache file and emit them into the report
             try (BufferedReader br = new BufferedReader(new FileReader(cacheFile)))
             {
                for (int i = 1; i <= bounds[1]; i++)
                {
                   String line = br.readLine();
                   if (i >= bounds[0])
                   {
                      String fmtLine = String.format("%05d     %s", i, line);
                      bw.write(dpad + fmtLine + sep);
                   }
                }
             }

             bw.write(sep);
          }

          /**
           * Write content about a query that has an indeterminate sorting order. This includes writing the
           * source file name, the source file line number, the query, the table that is accessed and the index
           * that has been chosen.
           *
           * @param   bw
           *          Report writer.
           */
          void writeIndeterminateSortQueryContent(Writer bw)
          throws IOException,
                 SchemaException
          {
             String cacheFile = sourceFile + ".cache";
             String sep = System.lineSeparator();

             try (BufferedReader br = new BufferedReader(new FileReader(cacheFile)))
             {
                String query = new String();
                int startingLine = -1;
                for (int i = 1; i <= bounds[1]; i++)
                {
                   String line = br.readLine();
                   if (line == null)
                   {
                      continue;
                   }

                   if (i >= bounds[0])
                   {
                      query += line.trim() + ' ';
                      if (startingLine == -1)
                      {
                         startingLine = i;
                      }
                   }
                }

                String fmtQuery = "\"" + query.replaceAll("\"", "\"\"") + "\"";
                String fmtRisk;
                switch (risk)
                {
                   case 1:
                      fmtRisk = "Low";
                      break;
                   case 2:
                      fmtRisk = "Moderate";
                      break;
                   default:
                      fmtRisk = "None";
                }

                String fmtLine = String.format("%s, %05d, %s, %s, \"%s\", %s",
                                               cacheFile,
                                               startingLine,
                                               tableName,
                                               indexName,
                                               fmtRisk,
                                               fmtQuery);
                bw.write(fmtLine + sep);
             }
          }

          /**
           * Compute the start and end line of the source file.
           *
           * @param   ast
           *          The referenced record phrase or embedded SQL AST.
           *
           * @return  An array where the first element is the start line, and the second element is the end line.
           */
          private int[] computeStartAndEndLine(Aast ast)
          {
             int start;
             Aast next = ast;

             switch (ast.getType())
             {
                case ProgressParserTokenTypes.RECORD_PHRASE:
                   // record_phrase has no line number; walk up the tree to find nearest enclosing
                   // node with a line number
                   do
                   {
                      start = next.getLine();
                      next = next.getParent();
                   } while (start == 0 && next != null);
                   break;

                case ProgressParserTokenTypes.EMBEDDED_SQL:
                   // embedded_sql has no line number, but its first child does
                   start = ast.getChildAt(0).getLine();
                   break;

                default:
                   // we shouldn't encounter any other token types at this point
                   start = 0;
                   break;
             }

             if (start == 0)
             {
                // TODO: raise error?
                return new int[] {0, 0};
             }

             // determine the appropriate source file end line
             int end = start;
             Iterator<Aast> iter = ast.iterator();
             while (iter.hasNext())
             {
                next = iter.next();
                end = Math.max(end, next.getLine());
             }

             return new int[] {start, end};
          }
       }

       /**
        * Context local work area.
        */
       private static class WorkArea
       {
          /** List of objects describing potentially suspicious query index selection */
          private final List<QueryIndexInfo> suspiciousQueries = new ArrayList<>();

          /** List of queries that produce an indeterminate order of the records when executed. */
          private final List<QueryIndexInfo> indeterminateSortQueries = new ArrayList<>();
       }
    }