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