# SchemaDictionary.java

    /*
    ** Module   : SchemaDictionary.java
    ** Abstract : schema namespace dictionary
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20041216   @19178 Created initial version.
    ** 002 ECF 20050103   @19191 Changed configuration name from 'schema' to
    **                           'schema-dict'.
    ** 003 ECF 20050103   @19193 Modified isXXXX methods to return false in
    **                           the event of an ambiguous search, rather than
    **                           throw an exception.
    ** 004 ECF 20050118   @19337 Implemented updated support for creates and
    **                           deletes of new databases, tables, and fields,
    **                           and for promotion of a table's search
    **                           precedence to a higher scope. New and changed
    **                           public methods:
    **                           - addDatabaseEntry
    **                           - removeDatabaseEntry
    **                           - addTableEntry
    **                           - removeTableEntry
    **                           - addFieldEntry
    **                           - removeFieldEntry
    **                           - addFieldEntries
    **                           - promoteTable
    **                           - dump (no-arg)
    **                           - dump (takes PrintStream)
    **                           Updated test harness to include a dump ("U")
    **                           command to create a debug dump file in the
    **                           current directory (schema.dmp).
    ** 005 ECF 20050128   @19458 Overloaded promoteTable method. The new
    **                           version accepts a flag to force a refresh of
    **                           the promoted table in the top scope  if it
    **                           already exists there.
    ** 006 ECF 20050203   @19564 Fixed a problem in addEntries to ensure the
    **                           "from" node found is not the same as the "to"
    **                           node specified. This defect was causing temp
    **                           tables created with an unqualified name which
    **                           matched a schema table name, to fail to add
    **                           field name nodes to the temp table's scope.
    ** 007 ECF 20050203   @19565 Modified search algorithm to find schema
    **                           entries. This change was made to allow us to
    **                           mimic the actual symbol resolution performed
    *                            by Progress. Progress looks first for an
    **                           exact match in all scopes. Only if this fails
    **                           are all scopes searched again for an
    **                           abbreviated match. Added a parameter to the
    **                           findNode method of inner class Scope to
    **                           specify whether search must find an exact
    **                           match or whether an abbreviated match is
    **                           permitted.
    ** 008 ECF 20050203   @19567 Modified isDatabase, isTable, isField methods
    **                           to eat exceptions caused by malformed entity
    **                           names (IllegalArgumentException thrown by
    **                           EntityName class upon construction).
    ** 009 ECF 20050207   @19626 Changes to entity promotion algorithm to
    **                           remove certain cases of ambiguity left behind
    **                           after #007. The promoteTable methods were
    **                           refactored to use the more generic
    **                           promoteEntry method. Promotion now involves
    **                           copying NameNode's from lower to higher
    **                           scopes by reference rather than by value.
    **                           The promoteTable(String, boolean) method is
    **                           no longer necessary and has been removed.
    ** 010 ECF 20050207   @19641 Made findEntry(String, int) package private
    **                           to permit NamespaceLoader to call it for a
    **                           database name lookup when loading schema data
    **                           and metadata.
    ** 011 ECF 20050209   @19732 Added special handling for record entries,
    **                           since different record types (table, buffer,
    **                           temp table, and work table) each are treated
    **                           very differently in terms of scopes to which
    **                           they may be added, and whether references to
    **                           them in Progress source code may be
    **                           abbreviated. Also, partially reverted search
    **                           algorithm to pre-007 behavior based on
    **                           additional testing against live code.
    ** 012 ECF 20050209   @19741 Refined the add and search rules for buffer
    **                           record entries. Buffers can only be present
    **                           in two scope levels in Progress: that of an
    **                           external procedure, or that of an internal
    **                           procedure or function. The table add- and
    **                           find-related methods now implement this
    **                           requirement. Modified scope lookup as well.
    **                           Changed scopeMap to use weak references
    **                           within a WeakHashMap to allow stale mappings
    **                           to be garbage collected. Removed all "remove"
    **                           methods for individual entities, as it was
    **                           decided this functionality was not needed,
    **                           and its implementation would otherwise have
    **                           complicated this change.
    ** 013 ECF 20050210   @19743 Added database alias support. Added the
    **                           createAlias method and a constructor which
    **                           does not load default schemas. Modified
    **                           Scope.findNode to substitute database name
    **                           for alias during a lookup. Modified
    **                           getQualifiedName implementation to correctly
    **                           qualify entity names which use aliases.
    ** 014 ECF 20050210   @19753 Fixed regression in addEntries(String,
    **                           NameNode, int). A temp-table with the same
    **                           name as a real table was trying to copy its
    **                           own fields to itself.
    ** 015 ECF 20050210   @19754 Handle special case when copying entries from
    **                           one parent node to another: if a buffer is
    **                           created from an unqualified name which is
    **                           common between a temp-table and a real table,
    **                           the buffer must alias the real table and not
    **                           the temp-table. In our implementation, this
    **                           translates to copying the set of field
    **                           entries from the real table's namespace
    **                           (rather than the temp-table's namespace) into
    **                           the buffer's scope's global table namespace.
    ** 016 ECF 20050211   @19775 Yet another rewrite of the search algorithm.
    **                           The complex findTableEntry method added in
    **                           #011 (@19732) has been removed in favor of a
    **                           simpler approach implemented at the Scope
    **                           level. This is made possible by changes to
    **                           Scope (it is now aware of its context within
    **                           the stack), and promoteEntry, which now
    **                           copies only the leaf name nodes (representing
    **                           fields) to the highest scope. Previously,
    **                           parent nodes were copied as well, which
    **                           caused lookup collisions at the table
    **                           qualifier level.
    ** 017 ECF 20050211   @19780 Fixed defect in promoteEntry. Code was not
    **                           looking up a source record correctly when it
    **                           was a buffer in a function.
    ** 018 ECF 20050214   @19783 Modified scope processing to expect four
    **                           standard scope levels. From outermost to
    **                           innermost:
    **                           0. SCHEMA_GLOBAL_SCOPE
    **                              Assigned to schema data loaded from
    **                              persistence; currently, new records of
    **                              type table are not added by the parser,
    **                              but if they were, they would be added to
    **                              this level.
    **                           1. USER_GLOBAL_SCOPE
    **                              Assigned to temp-table and work-table
    **                              data.
    **                           2. EXT_PROC_SCOPE
    **                              Associated with an external procedure's
    **                              default scope. Buffers created anywhere in
    **                              an external procedure are represented
    **                              here.
    **                           3. INT_PROC_SCOPE
    **                              Associated with the scope of an internal
    **                              procedure or function. Buffers created
    **                              anywhere in an internal procedure or a
    **                              function are represented here.
    ** 019 ECF 20050214   @19785 Modified addEntry to add special handling for
    **                           buffer fields. A field entry is now only
    **                           added to the global namespace of a scope if
    **                           its parent is not a buffer record. This fixes
    **                           an edge case where an unqualified field
    **                           lookup is ambiguous because a buffer field of
    **                           the same name was present. The buffer fields
    **                           can thus only be found during a lookup if the
    **                           enclosing buffer previously has been
    **                           promoted.
    ** 020 ECF 20050216   @19837 Added support for qualified buffer name
    **                           lookups. This involved creating a fake name
    **                           node (a shadow) to represent the buffer's
    **                           parent database, and storing it in the
    **                           global database namespace of the buffer's
    **                           scope.
    ** 021 ECF 20050217   @19840 Fixed defect in addEntries. First search for
    **                           "from" node was only looking in bottom two
    **                           scopes. Needs to look in all scopes. This was
    **                           preventing the definition of a buffer based
    **                           upon another buffer.
    ** 022 ECF 20050308   @20247 Modified getQualifiedName to correctly handle
    **                           temp- and work-table names. These do not have
    **                           database qualifiers, which broke the previous
    **                           code in NameNode.toAliasedName.
    ** 023 ECF 20050322   @20600 Added functionality to load and expose more
    **                           schema data. The hierarchy of Progress schema
    **                           objects (e.g., ProgressSchema, etc.) is now
    **                           used internally to manage table indices and
    **                           table and field options. Added methods to
    **                           store, access, and persist these new objects.
    **                           The temp-table database is now persisted for
    **                           each source file.
    ** 024 ECF 20050413   @20713 Replaced ProgressEntity-based internal data
    **                           representation with an AST-based mechanism,
    **                           based on the output of a new SchemaParser.
    **                           This approach is much more compatible with
    **                           the Progress parser and symbol resolver. This
    **                           entailed a rewrite of significant portions of
    **                           this class. Also enabled sharing a single
    **                           instance of this class across multiple source
    **                           code parsing runs, which significantly cuts
    **                           down the time required for all but the first
    **                           run in a series.
    ** 025 ECF 20050415   @20846 Refactored constructors to accommodate
    **                           loading a primary database by name. Fixed
    **                           buffer alias defect which caused improper
    **                           name to be returned by getQualifiedName.
    ** 026 ECF 20050502   @20988 Fix for loading an AST. Logic was altering an
    **                           AST during a tree walk, causing an infinite
    **                           loop inside antlr.BaseAST. Changed to use a
    **                           copy of the AST instead of the original.
    ** 027 ECF 20050603   @21403 Fixed a defect in the addIndex method. This
    **                           method was creating 2 layers of INDEX AST
    **                           nodes by grafting an AST node to the wrong
    **                           parent.
    ** 028 ECF 20050623   @21605 Added getIndex method variant. Convenience
    **                           version to find index based on table name and
    **                           index name.
    ** 029 ECF 20050817   @22153 Added methods getTableRecordType and
    **                           getFieldRecordType. They accept the name of
    **                           a table or field, respectively, and report
    **                           back the token type of the associated record.
    **                           For buffers, the type of the backing record
    **                           is returned instead of BUFFER.
    ** 030 ECF 20050922   @22272 Added getBufferForTable, getBufferForField.
    **                           These methods get the record (buffer) name
    **                           for a raw record or field name, respectively,
    **                           as found in Progress source code. The name
    **                           returned is always unabbreviated and is
    **                           qualified by database only if the input was
    **                           also so qualified.
    ** 031 ECF 20050928   @22896 Added getLogicalDatabaseForXXX() methods.
    **                           These are used by the parser to retrieve the
    **                           logical database name, given a table or field
    **                           name.
    ** 032 ECF 20050930   @22921 Modified getQualifiedName method. Substitutes
    **                           backing table name for buffer name in all
    **                           cases, not just when requesting full table
    **                           name.
    ** 033 ECF 20051005   @22977 Fixed NPE in getLogicalDatabaseForXXX()
    **                           methods. If the name passed in represented a
    **                           temp- or work-table (or a field contained
    **                           within such a table), the database name node
    **                           would always be null. Dereferencing the node
    **                           to get its name was causing the error.
    ** 034 ECF 20060202   @24254 Assign a standard database name to the P2Os
    **                           persisted for temp/work-table schema ASTs.
    **                           This name determines the package in which DMO
    **                           classes are created. It must match the temp
    **                           table database used to support temp tables at
    **                           runtime.
    ** 035 ECF 20060226   @24817 Added support for field-level validation
    **                           conversion. Added methods to retrieve field
    **                           properties. Modified getRecordName method to
    **                           aggressively prepend a database qualifier.
    ** 036 ECF 20060407   @25424 Always lowercase database qualifier before
    **                           returning record name from getRecordName().
    ** 037 ECF 20060407   @25425 Update to resetState(). Remove all aliases.
    ** 038 ECF 20060407   @25426 Fixed defect in copying field nodes. Child
    **                           AST nodes were not being copied correctly.
    ** 039 ECF 20060426   @25737 Enhanced support for adding a field like an
    **                           existing field. If the source field is an
    **                           extent, it is possible to make a like field
    **                           which is based upon an individual element of
    **                           the source field.
    ** 040 ECF 20060427   @25822 Further improvements to LIKE field support.
    **                           Extended changes in #039 (@25737) to be more
    **                           generic.
    ** 041 GES 20060912   @29486 Split copyProperties into field and table
    **                           versions.  The field version overrides props
    **                           in the target AST if they already exist (it
    **                           previously just added to the props). Special
    **                           processing for case sensitivity was also
    **                           added.
    ** 042 ECF 20061020   @30587 Fixed LIKE processing for temp/work-table
    **                           fields. If defined LIKE a permanent table's
    **                           field, a temp/work-table field is annotated
    **                           as verbose. This allows verbose conversion of
    **                           field names downstream.
    ** 043 ECF 20061025   @30651 Refined fix for LIKE processing for temp/work
    **                           table fields. Fields of a temp table which is
    **                           like another temp table which is like a
    **                           permanent table were not properly being
    **                           annotated as verbose. The verbose annotation
    **                           is now inherited properly, regardless of the
    **                           number of levels of indirection.
    ** 044 ECF 20061126   @31400 New annotation for temp/work-tables defined
    **                           LIKE other tables. The 'model' annotation
    **                           records the schema name of the table after
    **                           which the table was modelled. This will be
    **                           the original table, in the event multiple
    **                           generations of LIKE phrases separate a temp
    **                           table from its original template. This is
    **                           used downstream to associate the appropriate
    **                           table hints, if any, with a temp table.
    ** 045 ECF 20070221   @32211 Fixed defect when defining a buffer for a
    **                           temp-table with the same name. The algorithm
    **                           implemented in addEntries() to recover from
    **                           a same-name condition only handled the case
    **                           where the source table was a permanent table,
    **                           not a temp/work-table.
    ** 046 GES 20070329   @32653 Moved a method into the uast package to make
    **                           it a common utility routine.
    ** 047 GES 20070330   @32697 Force permanent databases (and contents)
    **                           to be loaded into the schema global scope.
    **                           This is needed to ensure that the lookup of
    **                           abbreviated names can be handled since
    **                           abbreviations are only processed for
    **                           namespaces in the schema global scope.
    ** 048 GES 20070402   @32705 Added database load option to the test
    **                           harness. Made scope copy constructor make
    **                           a deeper copy to ensure that modifications
    **                           are dropped between runs.
    ** 049 GES 20070404   @32791 Since internal procedures, user-defined
    **                           functions and triggers can be nested, there
    **                           cannot be a hard coded scope to treat as the
    **                           internal procedure scope. In particular, all
    **                           3 types can be nested inside inner blocks,
    **                           triggers can be nested inside all other
    **                           types and triggers can even be multiply
    **                           nested. This removes the INT_PROC_SCOPE and
    **                           replaces it with a calculation of the nearest
    **                           scope that is marked as internal.
    ** 050 GES 20070406   @32829 Added safety code to allow a temp-table like
    **                           another temp-table (when there are no
    **                           properties in the source table).
    ** 051 ECF 20070410   @32922 Modified getAllFieldProperties() to return a
    **                           Map instead of a List. This makes it easier
    **                           to correlate multiple sets of field-level
    **                           properties from the same table to the proper
    **                           fields.
    ** 052 GES 20070418   @33114 Added support for COM-HANDLE type.
    ** 053 GES 20070629   @34340 Removed terse flag from AST persistence so
    **                           that we retail line/column info in our ASTs.
    **                           Added the "srcfile" annotation to the
    **                           database node in the AST so that the original
    **                           .df filename can easily be found later.
    **                           Reworked APIs and logic for temp-table
    **                           creation to use ASTs from the source file
    **                           to set the line and column numbers in the
    **                           generated AST nodes.  This allows them to be
    **                           cross-referenced back to the defining source
    **                           code.
    ** 054 GES 20070723   @34642 Force those field and index definitions that
    **                           are copied from another table to have the
    **                           same line and column numbers as the temp
    **                           table definition node. Force the line/col
    **                           numbers for more deeply nested nodes in other
    **                           "like" situations.
    ** 055 GES 20070907   @35015 Support for 10.1B data types.
    ** 056 ECF 20070911   @35052 Fixed addEntries(). Was not considering table
    **                           type of WORK_TABLE.
    ** 057 GES 20070911   @35142 Refactored into an instance approach instead
    **                           of the singleton approach previously used.
    **                           This handles the majority of the data as
    **                           read-only and shared across all instances.
    **                           A given instance of this class is still NOT
    **                           thread-safe, however different instances can
    **                           now be simultaneously used, each on a
    **                           different thread, without problems.
    ** 058 GES 20070926   @35258 Once a table is promoted, don't ever promote
    **                           it again. Propagate namespace contents from
    **                           a removed scope into its enclosing scope.
    ** 059 GES 20071106   @35894 Propagation may cause ambiguity problems
    **                           when buffer nodes are created in nested
    **                           (internal) scopes and then the same buffer
    **                           name is created again in the external scope.
    **                           In this case, nodes with the same name are
    **                           removed from a scope before a buffer node
    **                           is added. Added safety code to avoid NPE
    **                           in copyIndex().
    ** 060 GES 20071204   @36190 Fixed typo that always forced promotion.
    **                           Disable propagation of buffer nodes across
    **                           internal scope boundaries. Also disabled
    **                           propagation of any other nodes that are
    **                           "bound" to buffers that are explicitly
    **                           defined inside an internal scope. Together
    **                           these actions make the propagation behavior
    **                           closer to the 4GL.
    ** 061 GES 20071207   @36285 Promotion is temporarily reset inside
    **                           internal scopes and then restored when the
    **                           internal scope pops. This means that things
    **                           that have already been promoted at the
    **                           external procedure level will promote again
    **                           in an internal procedure. Weak scopes that
    **                           have buffers associated (such that the
    **                           buffer is a weak reference) will force any
    **                           references to that buffer inside that weak
    **                           scope to not be propagated.
    ** 062 ECF 20081126   @40696 Keep track of explicit database qualifiers
    **                           for promoted buffers. These may be needed for
    **                           unqualified references to the same buffers in
    **                           the current or a more deeply nested scope.
    **                           This prevents the instantiation of separate
    **                           buffers for such unqualified references.
    ** 063 ECF 20081216   @40936 Exposed some private constants. Changed these
    **                           to package private access.
    ** 064 GES 20090421   @41832 Matched utility class package change.
    ** 065 GES 20090429   @42052 Match package and class name changes.
    ** 066 GES 20090515   @42221 Moved to AstManager from AstRegistry/AstPersister.
    ** 067 GES 20090518   @42401 Moved filename normalization here from brainwash().
    ** 068 ECF 20130225          Fixed addEntries, which was not dealing with some buffer-related
    **                           edge cases properly.
    ** 069 ECF 20130302          Fixed bug during initial loading. Duplicate field and table names
    **                           added to the schema global scope were replacing nodes whose
    **                           unqualified names were the same. This replacement is only supposed to
    **                           happen during promotion, in case the same table is promoted multiple
    **                           times, not during the initial load, when all fields must be added to
    **                           the schema global scope.
    ** 070 CA  20130304          Fixed H069, to include temp-tables too. Fixed promoteEntry, to
    **                           include all the record types when checking for a weak reference.
    **                           The promoted tracking must be done by each scope, and not just by the
    **                           the scope for top level blocks.
    ** 071 CA  20130307          Fixed abbreviated field name disambiguation - the tables which are
    **                           weak reference in the current scope have precedence over others.
    ** 072 CA  20130307          Fixed honoring of VALIDATE clause for temp-table fields defined using
    **                           the LIKE clause and tables defined using the LIKE clause.
    ** 073 CA  20130524          When temp-tables are defined, their fields need to be added proper
    **                           ORDER clause, to reflect their actual position at the temp-table
    **                           definition.
    ** 074 CA  20131024          Inferred generics.  Allow each context to have their own instance.
    ** 075 CA  20140114          Save the legacy table name in an annotation, for later use.
    ** 076 ECF 20141008          Remove MANDATORY and KW_FLD_TRG nodes from the target temp-table
    **                           field or variable defined using LIKE.
    ** 077 ECF 20150103          When defining a temp-table like an existing table, and there exist
    **                           both a temp-table and a persistent table with the same name as the
    **                           source/from table, use the persistent table in preference to the
    **                           temp-table.
    ** 078 ECF 20150104          Implemented support for implicit DICTDB alias. Ambiguous matches on
    **                           table and field names without a database qualifier are now retried
    **                           using this alias as a database qualifier.
    ** 079 VMN 20150215          Propagate the schemaname of the original source of the field
    **                           defined with LIKE clause.
    ** 080 OM  20150824          Copied TEMP-TABLE specific annotations to new data ASTs.
    ** 081 EVL 20160224          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 082 ECF 20160313          Refactored some methods to allow table data copies across schema
    **                           dictionaries. Added API to remove all name nodes from dictionary,
    **                           except for a subset provided by the caller. Added getFieldInfo and
    **                           performed related refactoring.
    ** 083 ECF 20170303          Honor FOR TEMP-TABLE clause in DEFINE BUFFER/PARAMETER statements.
    ** 084 ECF 20170826          Added support for various field options.
    ** 085 CA  20180404          CAN-FIND allows field abbreviations to be resolved even if ambiguous:
    **                           it will return the first field which matches the prefix.
    ** 086 CA  20180410          Unqualified fields referred in BY sort order clauses will look up
    **                           with precedence the current table: if BY clauses are just fields and
    **                           they match an index in the current table, then that will be used.
    **     CA  20180510          Added temp-table options: LABEL, XML-NODE-NAME, NAMESPACE-URI,
    **                           NAMESPACE-PREFIX and SERIALIZE-NAME.
    **     CA  20180526          USE-INDEX prim-idx for LIKE clause can be overridden by an explicit
    **                           PRIMARY index.
    ** 087 CA  20180621          Track temp-table fields defined as LIKE via a 'like' annotation with
    **                           the source field name.  This is required to properly parse any
    **                           VALEXP.
    ** 088 ECF 20181116          Improved heap utilization by caching all database ASTs loaded, even
    **                           if they are not loaded by default.
    ** 089 CA  20181213          Fixed DEF BUFFER FOR [TEMP-TABLE] - TEMP-TABLE forces a temp-table,
    **                           and without it a persistent table must have precedence over a temp
    **                           table.
    ** 090 ECF 20190620          EmptyIterator API change.
    ** 091 CA  20190720          Load implicit default databases - these databases are for non-default
    **                           schemas in p2j.cfg.xml, so that they will not be loaded during normal
    **                           conversion unless a hint is specified.  We emulate this in the
    **                           directory, in the 'default-databases' section, where the databases as
    **                           specified for each Java package prefix.
    **     OM  20190723          Preserve camel-casing of historical names for fields, indexes and
    **                           tables.
    **     CA  20190817          Load implicit default databases - these databases are for non-default
    **                           schemas in p2j.cfg.xml, so that they will not be loaded during normal
    **                           conversion unless a hint is specified.  We emulate this in the
    **                           directory, in the 'default-databases' section, where the databases
    **                           as specified for each Java package prefix.
    **     ECF 20190827          Fixed runtime SchemaDictionary initialization in the event
    **                           "default-databases" is configured.
    **     OM  20190829          Manufactured BEFORE-TABLE specific HIDDEN fields and index.
    ** 092 VVT 20190829          WORK-TABLE must never have indices (see #4103 and #3379 for the
    **                           explanation).
    ** 093 EVL 20190928          Fix for NPE in finalizeTableDefine for incoming null ast.
    ** 094 GES 20190518          Add disambiguation if only one match is strongly scoped to the
    **                           current scope.
    **     ECF 20190718          Fixed table promotion. The field nodes were being promoted, but not
    **                           the table node itself.
    **     ECF 20191013          Allow only one buffer of a given name to be defined at a time. This
    **                           already was enforced naturally in normal mode, but it was possible to
    **                           create duplicate buffer definitions for the same buffer name in OO
    **                           pre-scan mode. Only the latest definition is preserved.
    ** 095 ECF 20200301          Added APIs needed by SymbolResolver, which expose a table's NameNode
    **                           and which allow more specific NameNode lookup algorithms for an
    **                           unqualified field name if its table's private namespace is known.
    **     GES 20200302          Changed getFieldInfo() to findFieldInfo() and return null if there
    **                           is no match found.
    ** 096 CA  20200514          Added support for SOAP web services (getAllFields(tableName), to build the WSDL
    **                           type for table or dataset request/response messages).
    ** 097 IAS 20200601          Added support for the tables/fields string attributes (*-SA properties).
    ** 098 CA  20200714          The reserved properties for a before-table must have their hard-coded field ID
    **                           as set at the ReservedProperty and also must be added before any other fields, at
    **                           the table definition.
    ** 099 ECF 20200825          Once a buffer is promoted for a block to which it is strongly scoped, do not
    **                           permit it to be resolved outside that block.
    **     GES 20200826          Disabled the strong scope removal.
    **     ECF 20200910          When promoting a defined buffer to a block to which it is strongly scoped, do
    **                           not allow its promoted node or its fields' promoted nodes to propagate out of
    **                           that strong scope.
    ** 100 CA  20200924          Fixed a leak in 'databases' map for dynamic conversion.
    **                           Added a cache of Scope instances - this is used for dynamic query conversion, to
    **                           not re-compute the temp-table scope each time the same list of temp-table buffers
    **                           is used.
    **     OM  20201001          Improved DMO manipulation performance by caching slow Property annotation access.
    **     CA  20201011          Scope.promoted changed to an identity hash set.
    **     CA  20201015          Replaced java.util.Stack with a non-synchronized custom implementation.
    **     AIL 20210309          Clean local schema dictionary instances.
    **     OM  20210908          Made TEMP_TABLE_DB public. Added META_DB public constant. Marked imported .dict
    **                           files with dedicated flag.
    **     VVT 20210917          Javadoc fixed, missing @Override annotations added.
    **     HC  20211001          Implementation of i18n support.
    **     OM  20211122          Added hints-based support for 'unloading' schema. A bit of code maintenance.
    **     OM  20211020          Added _DATASOURCE_ROWID property.
    **     ECF 20220321          Resolve ambiguity for unqualified, metadata tables or fields, in a multi-database
    **                           configuration by searching only within the last schema loaded. Format cleanup.
    **     ECF 20220325          Support a mode where an unqualified field lookup will be found in the default
    **                           buffer of a table, rather than in any explicitly defined buffer for that table.
    **     ECF 20220405          Addressed flaw in previous fix to resolve ambiguity for unqualified metadata
    **                           table/field references in multi-database configurations.
    **     OM  20220727          FieldId and PropertyId are different for denormalized extent fields.
    **     TJD 20220504          Java 11 compatibility minor changes
    **     CA  20220501          Each profile has the same structure as the main 'global' config.  Allow multiple
    **                           profiles to be ran at once, with the conversion switching the state between
    **                           profiles, when a resource (like a file or namespace) is being processed.  Only
    **                           front phase is supported at this time.
    **     GES 20210208          Added table-level signature calculation (saved as an annotation). Added
    **                           findTableInfo() to provide a set of related data about a matched record. This
    **                           saves multiple calls each having to resolve the same name node lookup.
    **     CA  20220531          During parsing, the temp-tables are gathered from the entire hierarchy of a class
    **                           and loaded in the SchemaDictionary.  But these must not be persisted to the
    **                           current class' .dict file!
    **     CA  20220707          'findTableInfo' must return the database alias (if used), and not the database
    **                           schema name for that alias.
    **     CA  20220727          Improved memory management for parsing; cleanup is done in two phases:
    **                           1. after each legacy class file has finished parsing, the SchemaDictionary will
    **                              keep only protected temp-tables (all private tables are removed, as these
    **                              can't be reached from sub-classes; all permanent tables are removed, as each
    **                              class has its own reference to the permanent tables).  Also, the class def
    **                              instance will reduce its own used memory, which is not required when parsing
    **                              sub-classes.
    **                           2. after parsing of the entire file set is finished, any SchemaDictionary or
    **                              other ASTs referenced by the ClassDefinition are released.
    **     CA  20220728          Fixed CA/20220727 - 'ClassDefinition.schemaDict' must preserve the global
    **                           namespace, to allow buffer definitions for permanent tables to be resolved from
    **                           sub-classes.
    **     TJD 20230220          Implement isExactMatchField to allow checking exact field names
    **     CA  20230110          Let 'getImmediateChild' work with a set, if an array is used for the types.
    ** 101 CA  20230321          A TEMP-TABLE's signature must include index information (this includes the index
    **                           name, options, field position and type).
    ** 102 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 103 OM  20230115          Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
    **                           versions, based on node types rather on string paths.
    ** 104 CA  20230619          In ECF/20220321, the ambiguity for unqualified, metadata tables or fields, in a
    **                           multi-database configuration, must use the first connected database (configured
    **                           as first in p2j.cfg.xml namespace).  This is required because the 4GL compiler
    **                           fixes the database for such unqualified meta tables at compile time, when
    **                           generating the .r file, and this can not be changed by runtime.  In previous fix,
    **                           CA may have compiled the .r file from the Procedure Editor instead of command
    **                           line.
    ** 105 CA  20230722          Check for empty schemata in 'localInstance'.
    ** 106 OM  20240318          Special handling of tables in mutable permanent databases.
    ** 107 OM  20240410          The the information on mutable databases are mergeFromAst() into global scope.
    ** 108 SP  20240729          If the field is unqualified and promoted names are more than 1, don't limit
    **                           the lookup to a specific table, use the normal node search algorithm instead
    ** 109 RNC 20241030          Added null check in getTableProperty(String, int).
    ** 110 CA  20241104          'getFirstDatabase' must check 'bannedSchemas'.
    */

    /*
    ** 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.lang.ref.*;
    import java.util.*;
    import java.util.logging.*;
    import com.goldencode.ast.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.convert.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.orm.ReservedProperty;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.uast.*;
    import com.goldencode.util.*;
    import com.goldencode.util.Stack;
    import com.goldencode.p2j.util.*;

    /**
     * A dictionary used by {@link com.goldencode.p2j.uast.SymbolResolver} to
     * perform lookups of database entity names. Defines an API to load one or
     * more Progress schemas into the primary lookup scope, to add arbitrary
     * database, table, and field name hierarchies into additional lookup scopes,
     * and to remove scopes in the reverse order of that in which they were added.
     * Allows lookups of abbreviated table and field names, and of qualified and
     * unqualified (see {@link Namespace namespace class description}) database,
     * table, and field names.
     * <p>
     * Using qualified or unqualified names (abbreviated as well for tables and
     * fields), callers can:
     * <ul>
     * <li>determine the existence of databases, tables, and fields;
     * <li>query table types and field data types; and
     * <li>convert unqualified table and field names to their fully qualified,
     *     unabbreviated representations.
     * </ul>
     * All lookups are performed from innermost (most recently added) to outermost
     * (least recently added) scope. Any match immediately terminates the lookup
     * and remaining, unsearched scopes are ignored.
     * <p>
     * A set of default database schemas, as defined by the schema configuration,
     * is loaded into the dictionary upon construction. If a caller wishes to
     * add additional schema information dynamically, a new scope should first
     * be created with the {@link #addScope} method. This ensures the dynamically
     * added schema information is easily removed when it goes out of scope, with
     * a subsequent call to {@link #removeScope}.
     * <p>
     * The most recently added scope may be removed in its entirety via the
     * {@link #removeScope} method. The outermost scopes -- the schema global and
     * user global scopes -- are protected from removal. Individual entries may
     * not be removed from the schema global scope. Namespaces for each
     * non-protected scope are maintained by the {@link #promoteTable} method
     * which adds name nodes to the namespaces of the top-most scope. This changes
     * the lookup processing, making some normally ambiguous names unambiguous
     * within the current scope's context. As scopes are removed, the namespace
     * contents of the removed scope are propagated into the corresponding
     * namespace of the enclosing scope. This duplicates Progress' behavior such
     * that subsequent code in the enclosing block can be disambiguated too.
     * <p>
     * This dictionary manages requests to store and retrieve other schema
     * information as well, such as field and table options and table indices.
     * This data is stored internally as a set of ASTs rooted at the database
     * level.
     * <p>
     * <strong>Known issues/limitations:</strong>
     * <p>
     * <ul>
     *    <li> Buffer definitions in 4GL do not have the effect of promotion
     *         but in this class there is no distinction. It is suspected that
     *         any "no reference" (in buffer scoping terms) would also cause no
     *         promotion. If this is the case, this is also an issue here. The
     *         current approach to limit this problem (which has been successful
     *         to date) has been to disable propagation of buffer nodes when
     *         internal scopes are popped. This behavior may itself be flawed in
     *         any scenario where the buffer would actually propagate in the 4GL.
     *    <li> Currently, all known cases of name resolution are handled with
     *         an implementation that uses a concept of promotion and propagation.
     *         It is possible that in Progress, the implementation is based on
     *         buffer scoping. Since buffer scoping is not handled at parsing
     *         time, if requirements are found that cause buffer scoping to be
     *         a requirement in order to properly resolve all names, then a major
     *         rewrite will be needed to both move buffer scoping here and to
     *         use it properly.
     *    <li> This class is not thread-safe. A fundamental assumption is that
     *         this class is provided solely to service the UAST symbol resolver,
     *         which is driven in a single thread by the Progress parser.
     * </ul>
     */
    public final class SchemaDictionary
    implements SchemaParserTokenTypes
    {
       /** Logger. */
       private static final ConversionStatus LOG = ConversionStatus.get(SchemaDictionary.class);

       /** Indicates that a token type cannot be determined */
       public static final int UNKNOWN = -1;

       /** Temporary table database name which is persisted in ASTs */
       public static final String TEMP_TABLE_DB = "_temp";

       /** Meta database name which is persisted in ASTs. */
       public static final String META_DB = "_meta";

       /** Annotation name for a mutable database. The value is not important (always 'true') but its presence. */
       public static final String MUTABLE_ANNOTATION = "mutable";

       /** Reserved name of temp-table parent database */
       static final String TEMP_DB = "@temp_db";

       /** Reserved name of work-table parent database */
       static final String WORK_DB = "@work_db";

       /** Implicit DICTDB alias */
       private static final String DICTDB = "dictdb";

       /** Index of schema global scope in stack */
       private static final int SCHEMA_GLOBAL_SCOPE = 0;

       /** Index of user global scope in stack */
       private static final int USER_GLOBAL_SCOPE = SCHEMA_GLOBAL_SCOPE + 1;

       /** Index of external procedure scope */
       private static final int EXT_PROC_SCOPE = SCHEMA_GLOBAL_SCOPE + 2;

       /** Token types which represent permitted field properties for copy purposes */
       private static final int[] FIELD_PROPS =
       {
          // these token types are used in a binary search, so they must be in ascending order by
          // token number
          KW_COL_L_SA,   KW_HELP_SA,    KW_SQL_WID,    KW_VALEXP,     KW_VALMSG,     KW_VALMG_SA,
          KW_CASE_SEN,   KW_COL_LAB,    KW_DECIMALS,   KW_FORMAT,     KW_HELP,       KW_LABEL,
          KW_NOT,        KW_VIEW_AS,
          KW_COL_CP,     KW_DESCR,      KW_EXTENT,     KW_INIT,       KW_LENGTH,     KW_MAND,
          KW_ORDER,      KW_POS,        KW_SERIALZH,   KW_SERIALZN,
          KW_XML_DTYP,   KW_XML_NNAM,   KW_XML_NTYP,
       };

       /** Token types which represent permitted table properties for copy purposes */
       private static final int[] TABLE_PROPS =
       {
          // these token types are used in a binary search, so they must be in ascending order by
          // token number
          KW_LABEL_SA, KW_VALMSG, KW_VALMG_SA, KW_LABEL, KW_NAMESP_P, KW_NAMESP_U, KW_SERIALZN, KW_XML_NNAM
       };

       // Sort field properties token type array into ascending numeric order,
       // so that it can be used in a binary search algorithm later.
       static
       {
          Arrays.sort(FIELD_PROPS);
          Arrays.sort(TABLE_PROPS);
       }

       /** List of namespaces by entity type. */
       private static final int[] NS_LIST = new int[]
       {
          EntityName.DATABASE,
          EntityName.TABLE,
          EntityName.FIELD
       };

       /** List of all non-metadata field types */
       private static final Set<Integer> FIELD_TYPE_LIST = new HashSet<>();

       // Populate FIELD_TYPE_LIST.
       static
       {
          for (int i = BEGIN_FIELDTYPES + 1; i < END_FIELDTYPES; i++)
          {
             FIELD_TYPE_LIST.add(i);
          }
       }

       /** Context-local SchemaDictionary instance. */
       private static final ContextLocal<SchemaDictionary> localDict = new ContextLocal<SchemaDictionary>()
       {
          @Override
          protected SchemaDictionary initialValue()
          {
             try
             {
                return new SchemaDictionary((Set<String>) null);
             }
             catch (SchemaException e)
             {
                throw new RuntimeException(e);
             }
          }
       };

       /** Context-local SchemaDictionary instance of non-default databases. */
       private static ContextLocal<Map<String, SchemaDictionary>> nonDefDict =
       new ContextLocal<Map<String, SchemaDictionary>>()
       {
          @Override
          protected Map<String, SchemaDictionary> initialValue()
          {
             return new HashMap<>();
          }
       };

       /** Original unmodified schema global scope. */
       private static Scope pristineScope = null;

       /** Original unmodified map of database ASTs. */
       private static Map<String, Aast> pristineDbs = null;

       /** Original unmodified map of aliases to database names. */
       private static Map<String, String> pristineAliases = null;

       /** Original unmodified schema global scope for non-default cases. */
       private static Map<String, Scope> nonDefPristineScope = new HashMap<>();

       /** Original unmodified map of database ASTs for non-default cases. */
       private static Map<String, Map<String, Aast>> nonDefPristineDbs = new HashMap<>();

       /** Original unmodified map of aliases to database names for non-default cases. */
       private static Map<String, Map<String, String>> nonDefPristineAliases = new HashMap<>();

       /** Database ASTs, mapped by database name */
       private Map<String, Aast> databases = new LinkedHashMap<>();

       /** Map of database aliases to database names */
       private Map<String, String> aliases = new HashMap<>();

       /** Object which loads namespace data from persistent storage */
       private SchemaLoader loader = null;

       /** Stack of namespace scopes for contextual changes to dictionary */
       private final Stack<Scope> scopes = new Stack<>();

       /**
        * The set of banned schemas. The {@code Namespace} will not recognize element from these schemas even is
        * they are loaded.
        */
       private final Set<String> bannedSchemas;

       /** Number of protected scopes (i.e., those which cannot be removed) */
       private int protectedScopeCount = 0;

       /** When finding an unqualified field name node, prefer a default buffer to an explicitly defined buffer */
       private boolean preferDefaultBuffer = false;

       /**
        * Map of weak references to scopes, indexed by name nodes. This can hold
        * stale data as scope removal does not clear this map. Make sure the
        * retrieved scope is still in use.
        */
       private final Map<NameNode, WeakReference<Scope>> scopeMap = new WeakHashMap<>();

       /** Map of promoted buffers to explicit database qualifiers */
       private final ScopedDictionary<NameNode, String> qualifiers = new ScopedDictionary<>();

       /** Table which defines state being copied in a temp-table "like" clause */
       private Aast modelTable = null;

       /**
        * A flag indicating that all fields should be marked as preferred. Set before a record phrase
        * parse.
        */
       private boolean markPreferred = false;

       /**
        * A flag indicating that all fields should be associated with a strong buffer scope. Set
        * before a record phrase parse.
        */
       private boolean markStrongScope = false;

       /**
        * Flag which forces an unique field match when an abbreviation is used. If this is off, it
        * will return the first matching field.  Used by CAN-FIND statements.
        */
       private boolean uniqueFieldMatch = true;

       /**
        * A lazy-populated map of longest unique table indexes (as in, no other index in this list
        * is a prefix for another index).
        */
       private final Map<Aast, Map<Aast, List<Aast>>> tableIndexes = new HashMap<>();

       /** Cache of temp-table added scopes. */
       private final Map<String, Scope> cachedScopes = new HashMap<>();

       /**
        * Create a schema dictionary based upon a single database schema. This
        * is necessary during schema parsing when resolving inline Progress code
        * for various schema constructs, such as view-as phrases and validation
        * expressions. To parse this code, the Progress parser requires a symbol
        * resolver which can resolve schema entity names. Thus, this constructor
        * is called with an early version of the target database schema AST,
        * such that the symbol resolver has enough information to resolve the
        * table and field references within the inlined code.
        *
        * @param   root
        *          The root node of an AST representing a database schema.
        */
       SchemaDictionary(Aast root)
       {
          initialize(root);
          bannedSchemas = null;
       }

       /**
        * Create a schema dictionary based upon a single database schema. Uses
        * the {@link SchemaLoader} to load the schema named <code>database</code>.
        *
        * @param   database
        *          Name of logical database whose schema should be loaded.
        *
        * @throws  SchemaException
        *          if any error occurs reading configuration information.
        */
       SchemaDictionary(String database)
       throws SchemaException
       {
          // Create loader.
          loader = new SchemaLoader();

          bannedSchemas = null;

          // Initialize using AST for target database.
          Aast root = loader.loadSchema(database);
          initialize(root);
       }

       /**
        * Get the context-local {@link SchemaDictionary} instance, configured to work with specified
        * set of schemata.
        * <p>
        * Note that the first time this is invoked for a particular combination of schemata, the
        * dictionary must be loaded with the corresponding ASTs, which can be slow. Subsequent
        * requests with the same combination will get a cached instance.
        * <p>
        * TODO: share instances in a thread-safe way across contexts for better scalability.
        *
        * @param   schemata
        *          An alphabetically sorted set of schema names to be loaded into the dictionary.
        *          The set is alphabetical in order to create a consistent key into a cache of
        *          dictionaries, since configuring a new dictionary for every request would be
        *          extremely time-consuming.
        *
        * @return  See above.
        */
       public static SchemaDictionary localInstance(SortedSet<String> schemata)
       {
          SchemaDictionary res;
          if (schemata != null && !schemata.isEmpty())
          {
             String key = schemata.toString();
             SchemaDictionary dict = nonDefDict.get().get(key);

             if (dict == null)
             {
                try
                {
                   dict = new SchemaDictionary(key, schemata, null);
                }
                catch (SchemaException e)
                {
                   throw new RuntimeException(e);
                }

                nonDefDict.get().put(key, dict);
             }

             res = dict;
          }
          else
          {
             res = localDict.get();
          }

          // refresh these, otherwise temp-tables will get 'leaked'
          res.clear();

          return res;
       }

       /**
        * A constructor which reads schema configuration data and loads default namespace schema information into
        * the outermost (global) lookup scope.
        * <p>
        * Loading the default schema information is quite time-consuming, so the first time an instance is
        * created, the loaded default data is backed up to a pristine copy. This copy is duplicated on subsequent
        * runs to ensure a clean slate without the extra load time. This is especially useful since this data is
        * never edited, so it can be (and is) shared among multiple simultaneously used instances of this class.
        *
        * @param   bannedSchemas
        *          The set of schemas which must be 'unloaded'. The {@code Namespace} will not recognize elements
        *          from these schemas even is they are loaded.
        *
        * @throws  SchemaException
        *          if any error occurs reading configuration information.
        */
       public SchemaDictionary(Set<String> bannedSchemas)
       throws SchemaException
       {
          this(null, null, bannedSchemas);
       }

       /**
        * Default constructor which reads schema configuration data and loads default namespace schema information
        * into the outermost (global) lookup scope.
        * <p>
        * Loading the default schema information is quite time-consuming, so the first time an instance is
        * created, the loaded default data is backed up to a pristine copy. This copy is duplicated on subsequent
        * runs to ensure a clean slate without the extra load time. This is especially useful since this data is
        * never edited, so it can be (and is) shared among multiple simultaneously used instances of this class.
        *
        * @param   key
        *          The key for the non-default set.
        * @param   nonDefaults
        *          The set of non-default schemas, to be loaded, beside the default ones.
        * @param   bannedSchemas
        *          The set of schemas which must be 'unloaded'. The {@code Namespace} will not recognize elements
        *          from these schemas even is they are loaded.
        *
        * @throws  SchemaException
        *          if any error occurs reading configuration information.
        */
       private SchemaDictionary(String key, Set<String> nonDefaults, Set<String> bannedSchemas)
       throws SchemaException
       {
          // Create loader.
          loader = new SchemaLoader();
          this.bannedSchemas = bannedSchemas;

          // Protect the initialization and use of static data.
          synchronized (SchemaDictionary.class)
          {
             Scope pristineScope = null;
             Map<String, Aast> pristineDbs = null;
             Map<String, String> pristineAliases = null;

             if (nonDefaults == null)
             {
                Configuration cfg = Configuration.getInstance();
                if (cfg.isMultiProfile())
                {
                   Object o = cfg.getProfileState(SchemaDictionary.class.getName());
                   Object[] state = (Object[]) o;

                   pristineScope = state == null ? null : (Scope) state[0];
                   pristineDbs = state == null ? null : (Map<String, Aast>) state[1];
                   pristineAliases = state == null ? null : (Map<String, String>) state[2];
                }
                else
                {
                   pristineScope = SchemaDictionary.pristineScope;
                   pristineDbs = SchemaDictionary.pristineDbs;
                   pristineAliases = SchemaDictionary.pristineAliases;
                }
             }
             else
             {
                pristineScope = SchemaDictionary.nonDefPristineScope.get(key);
                pristineDbs = SchemaDictionary.nonDefPristineDbs.get(key);
                pristineAliases = SchemaDictionary.nonDefPristineAliases.get(key);
             }

             if (pristineScope == null)
             {
                // First time through.

                // Create schema global scope.
                addScope(false);

                // Load default schemas into global scope.
                loader.loadDefaults(this);

                if (nonDefaults != null)
                {
                   for (String db : nonDefaults)
                   {
                      Aast ast = loader.loadSchema(db);
                      loadFromAst(ast, false);
                   }
                }

                // Backup the schema global scope.
                pristineScope = new Scope(scopes.get(SCHEMA_GLOBAL_SCOPE));

                // Backup the database map.
                pristineDbs = new HashMap<>(databases);

                // Backup the alias map.
                pristineAliases = new HashMap<>(aliases);

                if (nonDefaults == null)
                {
                   if (Configuration.getInstance().isMultiProfile())
                   {
                      Object[] state = new Object[3];

                      state[0] = pristineScope;
                      state[1] = pristineDbs;
                      state[2] = pristineAliases;
                      Configuration.getInstance().setProfileState(SchemaDictionary.class.getName(), state);
                   }
                   else
                   {
                      SchemaDictionary.pristineScope = pristineScope;
                      SchemaDictionary.pristineDbs = pristineDbs;
                      SchemaDictionary.pristineAliases = pristineAliases;
                   }
                }
                else
                {
                   SchemaDictionary.nonDefPristineScope.put(key, pristineScope);
                   SchemaDictionary.nonDefPristineDbs.put(key, pristineDbs);
                   SchemaDictionary.nonDefPristineAliases.put(key, pristineAliases);
                }
             }
             else
             {
                // Subsequent time through. Reset to pristine state.

                // Restore the global scope and add it back.
                scopes.push(new Scope(pristineScope));

                // Restore the default databases map.
                databases = new LinkedHashMap<>();
                Iterator<String> iter = Configuration.getSchemaConfig().defaultDatabases();
                while (iter.hasNext())
                {
                   String database = iter.next();
                   Aast ast = pristineDbs.get(database);
                   if (ast != null)
                   {
                      databases.put(database, ast);
                   }
                }

                if (nonDefaults != null)
                {
                   for (String database : nonDefaults)
                   {
                      Aast ast = pristineDbs.get(database);
                      if (ast != null)
                      {
                         databases.put(database, ast);
                      }
                   }
                }

                // Restore the aliases map.
                aliases = new HashMap<>(pristineAliases);
             }
          }

          // Create user global scope.
          addScope();

          // Protect schema and user global scopes, such that they cannot be removed.
          protectedScopeCount = scopes.size();

          // Add database ASTs for temp and work tables.
          newDatabase(TEMP_DB);
          newDatabase(WORK_DB);
       }

       /**
        * Check if there is a cached {@link Scope}, which can be used to replace the top of the {@link #scopes}
        * stack.
        *
        * @param    key
        *           The key in the {@link #cachedScopes} map.
        *
        * @return   <code>true</code> if a cached scope was found and re-used as the top of {@link #scopes}.
        */
       public boolean reuseCachedScope(String key)
       {
          if (key.isEmpty())
          {
             return true;
          }

          Scope scope = cachedScopes.get(key);
          if (scope != null)
          {
             scopes.pop();
             scopes.push(scope);

             return true;
          }

          return false;
       }

       /**
        * Cache the top of the {@link #scopes} stack.
        *
        * @param    key
        *           The key to use in the {@link #cachedScopes}.
        */
       public void cacheScope(String key)
       {
          Scope scope = scopes.peek();

          cachedScopes.put(key, scope);
       }

       /**
        * Load a database schema which is identified in the schema configuration
        * by the specified database name.
        *
        * @param   database
        *          Name which identifies in the configuration the namespace data
        *          to be loaded.
        *
        * @throws  SchemaException
        *          if the schema namespace information cannot be loaded for any
        *          reason, including an invalid database name.
        */
       public void loadSchema(String database)
       throws SchemaException
       {
          loadSchema(database, true);
       }

       /**
        * Load a database schema which is identified in the schema configuration
        * by the specified database name.
        *
        * @param   database
        *          Name which identifies in the configuration the namespace data
        *          to be loaded.
        * @param   global
        *          <code>true</code> to add entry to global scope, else
        *          <code>false</code> to add entry to top scope. Only considered
        *          if <code>parent</code> is not specified; otherwise, entry is
        *          added to scope in which <code>parent</code> resides.
        *
        * @throws  SchemaException
        *          if the schema namespace information cannot be loaded for any
        *          reason, including an invalid database name.
        */
       public void loadSchema(String database, boolean global)
       throws SchemaException
       {
          // try to load AST (which can be large) from cache first
          Aast ast = pristineDbs.get(database);
          if (ast == null)
          {
             synchronized (SchemaDictionary.class)
             {
                ast = pristineDbs.get(database);
                if (ast == null)
                {
                   // load AST from schema loader and cache it
                   ast = loader.loadSchema(database);
                   pristineDbs.put(database, ast);
                }
             }
          }
          loadFromAst(ast, true, global);
       }

       /**
        * Create an alias for a previously loaded database. Any schema name
        * lookups which use the alias as a qualifier will map to the specified
        * database instead. Chained aliases (i.e., an alias for another alias)
        * are disallowed; however, reseting an existing alias to a new underlying
        * database name is allowed.
        *
        * @param   alias
        *          Alias to use for the database qualifier portion of a schema
        *          entity name.
        * @param   database
        *          Logical name of the database for which the alias is being
        *          created. May not be <code>null</code>.
        *
        * @return  <code>true</code> if the alias was created; <code>false</code>
        *          if <code>database</code> could not be found in the dictionary,
        *          or if the caller tried to create an alias for another alias.
        */
       public boolean createAlias(String alias, String database)
       {
          alias = alias.toLowerCase();
          database = database.toLowerCase();

          // Disallow a chained alias or an an alias to a non-existent database
          // name. This test works because logical database names must be aliases
          // to themselves (mapping is created when a database entry is added).
          if (!database.equals(aliases.get(database)))
          {
             return false;
          }

          // Map alias to logical database name.
          aliases.put(alias, database);

          return true;
       }

       /**
        * Add a dictionary entry for the specified database and return a
        * {@link NameNode} object representing the database name, which can be
        * used as a parent node for subsequent table entry additions.
        *
        * @param   name
        *          Database name to be added.
        *
        * @return  Name node representing database name.
        */
       public NameNode addDatabaseEntry(String name)
       {
          // Always use lower case names.
          name = name.toLowerCase();

          // Add a self-referencing alias for this database name.
          aliases.put(name, name);

          // Create AST and name node.
          Aast ast = newDatabase(name);
          NameNode node = addEntry(null, ast, EntityName.DATABASE, true);

          return node;
       }

       /**
        * Add a dictionary entry for the specified table and return a
        * {@link NameNode} object representing the table name, which can be
        * used as a parent node for subsequent field entry additions.
        *
        * @param   parent
        *          Database name node which represents this table's parent entity.
        *          May be <code>null</code>.
        * @param   name
        *          Table name to be added.
        * @param   tokenType
        *          Type of table to be added (e.g., normal schema-defined table,
        *          buffer, temp table, work table).
        * @param   globalScope
        *          <code>true</code> to add entry to global scope, else
        *          <code>false</code> to add entry to top scope.
        * @param   tnode
        *          The node that triggered the table creation.
        *
        * @return  Name node representing table name.
        */
       public NameNode addTableEntry(NameNode parent, String name, int tokenType, boolean globalScope, Aast tnode)
       {
          String legacyName = name;

          // Always use lower case names.
          name = name.toLowerCase();

          // Create an AST. If the new table is a BUFFER, this AST is just a shim
          // whose parent will later be set to the backing, "real" table's AST.
          Aast ast = new ProgressAst();
          ast.setType(tokenType);
          ast.setText(name);
          ast.setLine(tnode.getLine());
          ast.setColumn(tnode.getColumn());
          ast.putAnnotation("legacy_name", legacyName);
          ast.putAnnotation("historical", legacyName);
          if (tnode.isAnnotation("tt_shared"))
          {
             ast.putAnnotation("tt_shared", (Boolean) tnode.getAnnotation("tt_shared"));
             if (tnode.isAnnotation("tt_global"))
             {
                ast.putAnnotation("tt_global", (Boolean) tnode.getAnnotation("tt_global"));
             }
             if (tnode.isAnnotation("tt_new"))
             {
                ast.putAnnotation("tt_new", (Boolean) tnode.getAnnotation("tt_new"));
             }
          }

          // Create name node.
          NameNode node = addEntry(parent, ast, EntityName.TABLE, globalScope);

          // Add AST to the larger tree, unless new table is a BUFFER, in which
          // case its backing AST is already part of the larger tree.
          Aast database = null;
          switch (tokenType)
          {
             case TABLE:
                database = parent.getAst();
                break;
             case TEMP_TABLE:
                database = databases.get(TEMP_DB);
                break;
             case WORK_TABLE:
                database = databases.get(WORK_DB);
                break;
             default:
                return node;
          }

          database.addChild(ast);
          ast.setParent(database);

          return node;
       }

       /**
        * Add a dictionary entry for the specified field and return a
        * {@link NameNode} object representing the field name.
        *
        * @param   parent
        *          Table name node which represents this field's parent entity.
        * @param   fnode
        *          The node that triggered the field creation.
        * @param   name
        *          Field name to be added.
        * @param   type
        *          Data type of field to be added (e.g., integer, character, raw,
        *          etc.).
        * @param   globalScope
        *          <code>true</code> to add entry to global scope, else
        *          <code>false</code> to add entry to top scope.
        *
        * @return  Name node representing field name.
        */
       public NameNode addFieldEntry(NameNode parent, Aast fnode, String name, int type, boolean globalScope)
       {
          Aast ast = newField(parent, fnode, name, type);

          NameNode node = addEntry(parent, ast, EntityName.FIELD, globalScope);

          return node;
       }

       /**
        * Add a dictionary entry for a field  which is <em>like</em> an existing
        * field, and return a {@link NameNode} object representing the field name.
        * This is used when processing temp-tables and work-tables.
        *
        * @param   parent
        *          Table name node which represents this field's parent entity,
        *          a temp-table or work-table. It should not be <code>null</code>.
        * @param   fnode
        *          The node that triggered the field creation.
        * @param   name
        *          Field name to be added.
        * @param   source
        *          Name node for the existing field which provides the basis of the
        *          definition of the new field. Can be qualified or unqualified,
        *          abbreviated or unabbreviated, but must be unambiguous.
        * @param   extent
        *          One-based index of the element (for an extent field) after
        *          which the new field entry is to be modelled. This is only
        *          relevant for the initializer, so that we copy the correct
        *          initial value in the case of an initializer list for an extent
        *          field.  Will be <code>0</code> if the "like" field is not an
        *          extent field, or if the entire extent field is the target of
        *          the like phrase, rather than only a specific element.
        *
        * @return  Name node representing new field name.
        *
        * @throws  SchemaException
        *          if there is an error copying the field's AST.
        */
       public NameNode addFieldEntry(NameNode parent, Aast fnode, String name, NameNode source, int extent)
       throws SchemaException
       {
          Aast srcAst = source.getAst();
          Aast copy = Variable.copyOptions(srcAst, extent);

          // propagate the schemaname of the original source of the field definition

          String likeAnnotation = srcAst.isAnnotation("like") ?
                (String) srcAst.getAnnotation("like") :
                source.toQualifiedName();
          copy.putAnnotation("like", likeAnnotation);
          copy.putAnnotation("historical", name);

          // Graft a copy of the source AST onto the table's AST, but rename it.
          NameNode node = null;
          try
          {
             name = name.toLowerCase();
             Aast table = getTable(parent);
             copy.setText(name);
             copy.setLine(fnode.getLine());
             copy.setColumn(fnode.getColumn());

             // force all children to have the same line/col numbers as the dup
             // node, rather than the line/col nums from the copied (from) table
             forceLineColumnNums(copy);

             table.graft(copy);

             // Add a new field name node based on the copied AST.
             node = addEntry(parent, copy, EntityName.FIELD, true);
          }
          catch (AstException exc)
          {
             throw new SchemaException("Error copying field AST: " + name, exc);
          }

          return node;
       }

       /**
        * Add a dictionary entry for a field  which is <em>like</em> the field
        * described by the AST specified by the <code>like</code> parameter.
        * Return a {@link NameNode} object representing the field name. A copy
        * of <code>like</code> is made, irrelevant properties are removed from
        * the copy, and the resulting AST is grafted onto the schema AST under
        * the table AST stored in <code>parent</code>.
        *
        * @param   parent
        *          Table name node which represents this field's parent entity.
        *          It should not be <code>null</code>.
        * @param   fnode
        *          The node that triggered the field creation.
        * @param   name
        *          Name of field to be added.
        * @param   type
        *          Token type of field to be added.
        * @param   like
        *          An AST which describes the properties of the new field. This
        *          AST might not be a field (in fact, it likely represents a
        *          variable). As such, it may contain additional properties
        *          which we do not care about for our field definition. These
        *          are filtered out when a copy of the AST is made and grafted
        *          into the appropriate schema AST.
        *
        * @return  Name node representing new field name.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>like</code> is an ambiguous field reference.
        * @throws  SchemaException
        *          if an error occurs grafting a copy of <code>like</code> onto
        *          the schema AST.
        */
       public NameNode addFieldEntry(NameNode parent, Aast fnode, String name, int type, Aast like)
       throws SchemaException
       {
          NameNode node = null;

          try
          {
             // Graft a copy of the source AST onto the table's AST, but rename it
             // and re-type it.
             name = name.toLowerCase();
             Aast table = getTable(parent);
             Aast copy = like.graftCopyTo(table, FIELD_PROPS);
             copy.setText(name);
             copy.setType(type);
             copy.setLine(fnode.getLine());
             copy.setColumn(fnode.getColumn());

             // Add a new field name node based on the copied AST.
             node = addEntry(parent, copy, EntityName.FIELD, true);
          }
          catch (AstException exc)
          {
             throw new SchemaException(
                "Error copying source AST to field: " + like.getText(), exc);
          }

          return node;
       }

       /**
        * Create a new table index under table <code>parent</code>, which is a
        * copy of the specified index <code>like</code>. Normalize the index
        * field ASTs to look like those provided by the schema directly.
        *
        * @param   parent
        *          Name node of table with which this index is associated.
        * @param   name
        *          Name of the index.
        * @param   like
        *          The original index AST to be copied.
        *
        * @return  Object which represents the new table index.
        *
        * @throws  SchemaException
        *          if an error occurs grafting a copy of <code>like</code> onto
        *          the schema AST.
        */
       public Aast addIndex(NameNode parent, String name, Aast like)
       throws SchemaException
       {
          Aast table = getTable(parent);

          // Copy index AST.
          Aast ast;
          try
          {
             ast = like.graftCopyTo(table, null);
          }
          catch (AstException exc)
          {
             throw new SchemaException("Error copying index", exc);
          }

          // Each INDEX_FIELD AST has some sort of a FIELD_* AST child, which is
          // duplicative with the information stored in the (temp/work)-table
          // itself. Remove these, but first copy the text up to the INDEX_FIELD
          // AST, since it is not present there.

          List<Aast> remove = new ArrayList<>();
          Aast indexField = ast.getImmediateChild(INDEX_FIELD, null);
          while (indexField != null)
          {
             Aast dup = (Aast) indexField.getFirstChild();
             if (dup != null)
             {
                remove.add(dup);
                indexField.setText(dup.getText());
             }
             indexField = ast.getImmediateChild(INDEX_FIELD, indexField);
          }

          Iterator<Aast> iter = remove.iterator();
          while (iter.hasNext())
          {
             Aast next = iter.next();
             next.remove();
          }

          return ast;
       }

       /**
        * Create a new table index field under index <code>parent</code>.
        *
        * @param   parent
        *          Index object with which this index field is associated.
        * @param   inode
        *          The node that triggered the index creation.
        * @param   name
        *          Name of the index field.
        *
        * @return  Object which represents the new table index field.
        */
       public Aast addIndexField(Aast parent, Aast inode, String name)
       {
          Aast field = new ProgressAst();
          field.setType(INDEX_FIELD);
          field.setText(name.toLowerCase());
          field.setLine(inode.getLine());
          field.setColumn(inode.getColumn());
          parent.addChild(field);
          field.setParent(parent);

          return field;
       }

       /**
        * Remove all traces of all temp-tables and buffers from the schema dictionary, except for
        * those with the specified names. The given names are expected to resolve to table entities
        * in the user global scope or in a higher scope. Permanent tables are also removed.
        * <p>
        * For the entities not in the exception list, all name nodes and children name nodes will be
        * removed from all their respective global and private namespaces, and all associated ASTs
        * will be pruned from their respective schema ASTs.
        *
        * @param   preserve
        *          Names of those tables to preserve.
        */
       public void removeAllExcept(String[] preserve)
       {
          // collect the name nodes associated with the names of the tables to preserve
          Set<NameNode> except = new HashSet<>();
          for (String name : preserve)
          {
             try
             {
                NameNode node = findEntry(name, EntityName.TABLE);
                except.add(node);
             }
             catch (AmbiguousSchemaNameException exc)
             {
                LOG.log(Level.SEVERE, "", exc);
             }
          }

          // walk each scope from top-most to and including the user global scope, and purge all
          // table information not associated with the exception list
          for (int i = scopes.size() - 1; i >= USER_GLOBAL_SCOPE; i--)
          {
             Scope scope = scopes.get(i);
             scope.removeAllNodes(except, EntityName.TABLE);
          }
       }

       /**
        * Copy the index with the given name from the current model, source
        * table to the specified destination table. Optionally set the index
        * as a primary index.
        * <p>
        * The current model source table is set at the beginning of
        * <code>like</code> clause processing for a temp-table and is nulled out
        * when {@link #finalizeTableDefine} is invoked.
        *
        * @param   destTable
        *          Destination temp-table to which the index will be copied.
        * @param   fnode
        *          The node that triggered the field creation.
        * @param   name
        *          Name of the index to be copied from the model table.
        * @param   primary
        *          <code>true</code> if the index is the primary index for the
        *          target temp-table. Note: the primary property is only set
        *          for the index if this argument is <code>true</code>.
        *
        * @throws  SchemaException
        *          if no index named <code>name</code> is found in the source
        *          table or if an error occurs grafting a copy of the source
        *          index onto the destination temp-table AST.
        */
       public void copyIndex(NameNode destTable, Aast fnode, String name, boolean primary)
       throws SchemaException
       {
          // Index AST which is the source template for the graft.
          name = name.toLowerCase();
          Aast source = getChildAst(modelTable, INDEX, name);
          if (source == null)
          {
             throw new SchemaException(
                "Index not found in table " + modelTable.getText() + ":  " +
                name);
          }

          // Temp-table AST which is the destination for the graft.
          Aast parent = destTable.getAst();

          // Detect the primary index, if any, of the destination table.
          Aast destPrimary = null;
          Aast next = parent.getImmediateChild(INDEX, null);
          while (next != null && destPrimary == null)
          {
             destPrimary = next.getImmediateChild(KW_PRIMARY, null);
             next = parent.getImmediateChild(INDEX, next);
          }

          // Graft a copy of the source onto the destination temp-table.
          Aast copy = null;
          try
          {
             copy = source.graftCopyTo(parent, null);
             copy.setLine(fnode.getLine());
             copy.setColumn(fnode.getColumn());

             // force all children to have the same line/col numbers as the dup
             // node, rather than the line/col nums from the copied (from) index
             forceLineColumnNums(copy);
          }
          catch (AstException exc)
          {
             throw new SchemaException("Error copying index: " + name, exc);
          }

          // Detect whether the copied index is defined as the primary index
          // for its table.
          Aast copyProps = copy.getImmediateChild(PROPERTIES, null);

          Aast copyPrimary = null;

          if (copyProps != null)
          {
             copyPrimary = copyProps.getImmediateChild(KW_PRIMARY, null);
          }

          // Primary index processing. Only one index can be the primary index
          // for a table.
          if (primary)
          {
             // This index is defined in the source code as primary, which
             // trumps all other indices. If another index is already defined
             // as primary, remove that designation.
             if (destPrimary != null)
             {
                destPrimary.remove();
             }

             // Set the copied index as primary, if it is not already so set.
             if (copyPrimary == null)
             {
                copyPrimary = new ProgressAst();
                copyPrimary.setType(KW_PRIMARY);
                copyPrimary.setText("primary");
                copyPrimary.setParent(copyProps);
                copyProps.addChild(copyPrimary);
             }
          }
          else if (copyPrimary != null && destPrimary != null)
          {
             // No AS PRIMARY clause was present, but if the copied index is
             // defined as primary, we need to make sure no other index has
             // already been defined for this temp-table as the primary index.
             // If there is one, it must override this one as the primary.
             copyPrimary.remove();
          }
       }

       /**
        * Finalize the definition of a temp-table. This method is invoked by
        * the Progress parser (via its symbol resolver) to notify the schema
        * dictionary that the current temp-table definition "transaction" has
        * completed. A "model" source table is set at the beginning of
        * <code>like</code> clause processing for a temp-table. If the given
        * temp-table at this point contains no indices, we copy the index
        * definitions described by the model table. Otherwise, we assume that
        * this default behavior has been overridden by individual indices
        * explicitly copied or defined in the Progress source code.
        *
        * @param   destTable
        *          Name node of the temp-table being defined.
        * @param   ast
        *          The created table AST.
        *
        * @throws  SchemaException
        *          Wrapping an AstException if one is detected during processing.
        *
        * @see     #addEntries
        * @see     #copyIndex
        * @see     #modelTable
        */
       public void finalizeTableDefine(NameNode destTable, Aast ast)
       throws SchemaException
       {
          Aast table = getTable(destTable);

          if (ast != null)
          {
             copyTableProperties(ast, table);
          }

          // calculate and save the table signature
          calculateSignature(table);

          if (modelTable == null)
          {
             // Temp-table has been defined without using a "like" clause.
             return;
          }

          // If the destination is a temp-table does not yet have any indices
          // defined, copy all indices from the current model table.
          // Note: work-tables never have indices.
          if (ast != null && ast.getType() == KW_TEMP_TAB)
          {
             try
             {
                if (table.isAnnotation("after-table"))
                {
                   // the BEFORE-TABLEs have only one index, primary named rowState with a single field
                   // __row-state__
                   ProgressAst index = new ProgressAst();
                   index.setText(Buffer.ROW_STATE_IDX);
                   index.setType(INDEX);
                   ProgressAst indexField = new ProgressAst();
                   indexField.setText(Buffer.__ROW_STATE__);
                   indexField.setType(INDEX_FIELD);
                   index.graft(indexField);
                   table.graft(index);
                }

                Iterator<Aast> iter = indices(table);
                if (!iter.hasNext())
                {
                   iter = indices(modelTable);
                   while (iter.hasNext())
                   {
                      Aast next = iter.next();
                      next.graftCopyTo(table, null);
                   }
                }

                Aast lastPrimaryIndex = null;
                // keep only the last primary index; multiple primary indexes can happen if both
                // USE-INDEX pk-idx and INDEX IS PRIMARY is defined
                while (iter.hasNext())
                {
                   Aast next = iter.next();
                   if (next.downPath(PROPERTIES, KW_PRIMARY))
                   {
                      if (lastPrimaryIndex != null)
                      {
                         lastPrimaryIndex.remove();
                      }

                      lastPrimaryIndex = next;
                   }
                }
             }
             catch (AstException exc)
             {
                throw new SchemaException("Error copying index", exc);
             }
          }

          // Reset the modelTable variable for further temp-table definition
          // "transactions".
          modelTable = null;
       }

       /**
        * Get the table object associated with the given table name.
        *
        * @param   name
        *          Name of the target table. Can be qualified or unqualified,
        *          abbreviated or unabbreviated, but must be unambiguous.
        *
        * @return  The table object associated with <code>name</code>.
        *
        * @throws  SchemaException
        *          if no table is found with the given name.
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is an ambiguous table reference.
        */
       public Aast getTable(String name)
       throws SchemaException
       {
          NameNode node = findEntry(name, EntityName.TABLE);
          if (node == null)
          {
             throw new SchemaException("No table matching name: " + name);
          }

          return getTable(node);
       }

       /**
        * Get the AST associated with the given table name node. If the given
        * node represents a buffer, the AST of the record which backs the buffer
        * is returned instead.
        *
        * @param   node
        *          Name node of the target table.
        *
        * @return  Table AST.
        */
       public Aast getTable(NameNode node)
       {
          Aast ast = node.getAst();

          // If table node represents a buffer, we have to get the AST
          // associated with the backing table.
          if (ast.getType() == BUFFER)
          {
             ast = ast.getParent();
          }

          return ast;
       }

       /**
        * Get the field AST associated with the given field name.
        *
        * @param   name
        *          Name of the target field. Can be qualified or unqualified,
        *          abbreviated or unabbreviated, but must be unambiguous.
        *
        * @return  The field AST associated with <code>name</code>.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is an ambiguous field reference.
        */
       public Aast getField(String name)
       throws AmbiguousSchemaNameException
       {
          NameNode node = findEntry(name, EntityName.FIELD);

          return node.getAst();
       }

       /**
        * Determines if the given node is contained in this dictionary.
        *
        * @param    node
        *           The namenode to check.
        *
        * @return   <code>true</code> if the given node can be found in this dictionary.
        */
       public boolean containsNode(NameNode node)
       {
          return retrieveNode(node) != null;
       }

       /**
        * Get the Progress parser token type of a table, temp-table, work-table,
        * or buffer. If <code>name</code> represents a buffer, the type of the
        * backing record is returned. That is, a type of <code>BUFFER</code> is
        * never returned, only the type of the record which backs the buffer.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   name
        *          Case insensitive record name; may be qualified or not. If
        *          qualified, only the table portion of the name may be
        *          abbreviated.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  One of the Progress parser token types <code>TABLE</code>,
        *          <code>TEMP_TABLE</code>, or <code>WORK_TABLE</code>, or
        *          <code>UNKNOWN</code> if <code>name</code> cannot be found.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is an ambiguous identifier for the
        *          target record.
        */
       public TableInfo findTableInfo(String name, boolean forceTemp, boolean forcePersistent)
       throws AmbiguousSchemaNameException
       {
          // TODO: The recordType lookup used scopes.size() - 1 instead of nearestEnclosingInternal(), is this
          //       an issue?  It seems like it should have been consistent with the other lookups, so this is
          //       being made consistent here.
          int l1 = computeLevel(forceTemp, forcePersistent, true, nearestEnclosingInternal());
          int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);

          // Look up record name node.
          NameNode node = findEntry(name, EntityName.TABLE, l1, l2, false);

          TableInfo info = null;

          if (node != null)
          {
             NameNode dbNode  = node.getParent();

             // don't just use getAst() here, we need the AST for the backing table if this is a buffer
             // (this will work for both the simple case and the buffer case)
             Aast   ast   = getTable(node);
             String sig   = ast != null ? (String) ast.getAnnotation("db_signature") : null;
             String sname = getQualifiedName(node);
             String bname = getRecordName(node, EntityName.TABLE, name);
             String dname = getLogicalDatabaseForTable(name, forceTemp, forcePersistent);
             int    ttype = getTable(node).getType();

             info = new TableInfo(sig, sname, bname, dname, ttype);
          }

          return info;
       }

       /**
        * Get an object which exposes schema-related information about a field which is referenced in
        * source code.
        *
        * @param   tableNode
        *          Name node asssociated with the field reference. If not {@code null}, only the
        *          private namespace of the given {@code tableNode} is searched (for an exact match)
        *          of {@code fieldName}. If {@code null}, a lenient search (i.e., abbreviations
        *          permitted) is performed across all scopes in the schema dictionary.
        * @param   fieldName
        *          Unaltered field name, as referenced in source code.
        * @param   preferDefaultBuffer
        *          {@code true} to indicate unqualified fields should prefer the default buffer over an
        *          explicitly defined buffer match.
        *
        * @return  Field schema information if found or {@code null} if nothing is found.
        *
        * @throws  AmbiguousSchemaNameException
        *          if {@code tableNode} is {@code null} and a search for {@code fieldName} finds more
        *          than one result.
        * @throws  SchemaException
        *          If there is some other problem in the lookup.
        */
       public FieldInfo findFieldInfo(NameNode tableNode, String fieldName, boolean preferDefaultBuffer)
       throws AmbiguousSchemaNameException,
              SchemaException
       {
          NameNode fieldNode = null;

          try
          {
             // temporarily use the provided preferDefaultBuffer setting; this works as long as this method
             // cannot recursively call itself, which it does not at the time this is being written
             this.preferDefaultBuffer = preferDefaultBuffer;
             boolean unqualified = false;
             if (Configuration.isRuntimeConfig())
             {
                // if the field is unqualified and promoted names are more than 1, don't limit the lookup to
                // a specific table, use the normal node search algorithm instead
                unqualified = !fieldName.contains(".") && scopes.get(scopes.size() - 1).promoted.size() > 1;
             }
             fieldNode = findFieldNode(unqualified ? null : tableNode, fieldName);
          }
          finally
          {
             // set the preferDefaultBuffer instance variable back to its default after the lookup
             this.preferDefaultBuffer = false;
          }

          if (fieldNode == null)
          {
             return null;
          }

          // get AST
          Aast ast = fieldNode.getAst();

          // get qualified field name
          String qualifiedName = getQualifiedName(fieldNode);

          // get buffer name
          String bufferName = getRecordName(fieldNode, EntityName.FIELD, fieldName);

          // get database name associated with buffer
          EntityName eName = new EntityName(EntityName.TABLE, bufferName);
          String dbName = eName.getDatabase();
          if (dbName == null)
          {
             NameNode bufNode = findEntry(bufferName,
                                          EntityName.TABLE,
                                          nearestEnclosingInternal(),
                                          SCHEMA_GLOBAL_SCOPE,
                                          false);

             NameNode dbNode = bufNode.getParent();

             dbName = (dbNode != null ? dbNode.getName() : "");
          }

          // get record type
          int ttype = UNKNOWN;
          NameNode parent = fieldNode.getParent();
          if (parent != null)
          {
             // getTable() method automatically resolves a buffer reference to its backing table
             ttype = getTable(parent).getType();
          }

          FieldInfo info = new FieldInfo(ast, qualifiedName, bufferName, dbName, ttype);

          return info;
       }

       /**
        * Get a copy of the AST which represents field <code>name</code>, with
        * possible modifications.  Specifically, if <code>extent</code> is
        * greater than 0 and the source field is an extent field, the copy will
        * represent a copy of a single element of the original extent field.
        * The effect of this is to remove the KW_EXTENT child node, and to use
        * only the initializer (if any) which corresponds with the element at
        * index <code>extent</code>.
        *
        * @param   name
        *          Name of the existing field which provides the basis of the
        *          definition of the copy. Can be qualified or unqualified,
        *          abbreviated or unabbreviated, but must be unambiguous.
        * @param   extent
        *          One-based index of the element (for an extent field) after
        *          which the new field entry is to be modelled. This is only
        *          relevant for the initializer, so that we copy the correct
        *          initial value in the case of an initializer list for an extent
        *          field.  Will be <code>0</code> if the "like" field is not an
        *          extent field, or if the entire extent field is the target of
        *          the like phrase, rather than only a specific element.
        *
        * @return  Copy of the AST corresponding with the given name, with the
        *          possible modifications described above.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is an ambiguous field reference.
        */
       public Aast getFieldCopy(String name, int extent)
       throws AmbiguousSchemaNameException
       {
          return Variable.copyOptions(getField(name), extent);
       }

       /**
        * Get a specific index AST for a table.
        *
        * @param   table
        *          Node of the target table. Can be qualified or unqualified,
        *          abbreviated or unabbreviated, but must be unambiguous.
        * @param   name
        *          Name of the target index defined for the target table.
        *
        * @return  Index AST or <code>null</code> if not found.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is an ambiguous field reference.
        */
       public Aast getIndex(String table, String name)
       throws AmbiguousSchemaNameException
       {
          NameNode node = findEntry(table, EntityName.TABLE);

          return getIndex(node, name);
       }

       /**
        * Get a specific index AST for a table.
        *
        * @param   tableNode
        *          Name node of the target table.
        * @param   name
        *          Name of the target index defined for the target table. May be
        *          abbreviated.
        *
        * @return  Index AST or <code>null</code> if not found.
        */
       public Aast getIndex(NameNode tableNode, String name)
       {
          Aast index = null;

          Aast table = getTable(tableNode);
          if (table != null)
          {
             // Try for an exact match first.
             Aast tmp = null;
             while (index == null)
             {
                tmp = table.getImmediateChild(INDEX, tmp);
                if (tmp == null)
                {
                   break;
                }

                if (tmp.getText().toLowerCase().equals(name))
                {
                   index = tmp;
                }
             }

             // Try for an abbreviated match if necessary. First match wins.
             tmp = null;
             while (index == null)
             {
                tmp = table.getImmediateChild(INDEX, tmp);
                if (tmp == null)
                {
                   break;
                }

                if (tmp.getText().toLowerCase().startsWith(name))
                {
                   index = tmp;
                }
             }
          }

          return index;
       }

       /**
        * Get the longest unique indexes for this table.  First, the {@link #tableIndexes} is checked;
        * if there is an entry, that is returned.  Otherwise, all longest unique indexes (with no
        * other index being a prefix for another index) for this table are collected.
        *
        * @param     table
        *            The table for which the indexes are required.
        *
        * @return    See above.
        */
       public Map<Aast, List<Aast>> getUniqueIndexes(Aast table)
       {
          Map<Aast, List<Aast>> indexes = tableIndexes.get(table);

          if (indexes != null)
          {
             return indexes;
          }

          indexes = new HashMap<>();
          tableIndexes.put(table, indexes);

          Iterator<Aast> iter = indices(table);
          while (iter.hasNext())
          {
             Aast next = iter.next();

             boolean unique = false;
             Aast props = next.getImmediateChild(PROPERTIES, null);
             while (!unique && props != null)
             {
                unique = (props.getImmediateChild(KW_UNIQUE, null) != null);
                props = next.getImmediateChild(PROPERTIES, props);
             }
             if (!unique)
             {
                continue;
             }

             List<Aast> fields = new ArrayList<>();
             Aast field = null;
             while ((field = next.getImmediateChild(INDEX_FIELD, field)) != null)
             {
                fields.add(field);
             }

             indexes.put(next, fields);
          }

          // keep only the indexes which don't have another index as a prefix
          l1: while (true)
          {
             Iterator<Aast> iter1 = indexes.keySet().iterator();
             while (iter1.hasNext())
             {
                Aast idx1 = iter1.next();
                List<Aast> fields1 = indexes.get(idx1);

                Iterator<Aast> iter2 = indexes.keySet().iterator();
                while (iter2.hasNext())
                {
                   Aast idx2 = iter2.next();
                   if (idx2 == idx1)
                   {
                      continue;
                   }

                   List<Aast> fields2 = indexes.get(idx2);

                   if (fields1.size() == fields2.size())
                   {
                      // not a possible prefix
                      continue;
                   }

                   boolean match = true;
                   for (int i = 0; i < Math.min(fields1.size(), fields2.size()); i++)
                   {
                      Aast fld1 = fields1.get(i);
                      Aast fld2 = fields2.get(i);

                      if (!fld1.getText().equalsIgnoreCase(fld2.getText()))
                      {
                         match = false;
                         break;
                      }
                   }
                   if (match)
                   {
                      if (fields1.size() > fields2.size())
                      {
                         iter2.remove();
                      }
                      else
                      {
                         iter1.remove();
                      }
                      continue l1;
                   }
                }
             }

             // we've reached here means no other change has been made
             break;
          }

          return indexes;
       }

       /**
        * Get an iterator on all of the table ASTs defined for a given database.
        * The tables are iterated in the order in which they were added to the
        * dictionary. All types are included in the iteration: TABLE, TEMP_TABLE,
        * WORK-TABLE, and BUFFER. Metadata tables are included in the iteration.
        *
        * @param   databaseName
        *          Name of the target database.
        *
        * @return  An iterator of tables for the specified database.
        */
       public Iterator<Aast> tables(String databaseName)
       {
          final Aast database = databases.get(databaseName);

          // Return a safe iterator if the database was not found.
          if (database == null)
          {
             return EmptyIterator.get();
          }

          // Define an iterator that walks the table children of the database.
          return new Iterator<Aast>()
          {
             private Aast next = advance();

             @Override
             public boolean hasNext()
             {
                return (next != null);
             }

             @Override
             public Aast next()
             {
                if (next == null)
                {
                   throw new NoSuchElementException();
                }

                Aast ast = next;
                next = advance();

                return ast;
             }

             @Override
             public void remove()
             {
                throw new UnsupportedOperationException();
             }

             private Aast advance()
             {
                Aast test = null;
                Aast ast = null;

                if (next == null)
                {
                   test = (Aast) database.getFirstChild();
                }
                else
                {
                   test = (Aast) next.getNextSibling();
                }

                while (ast == null && test != null)
                {
                   switch (test.getType())
                   {
                      case TABLE:
                      case BUFFER:
                      case TEMP_TABLE:
                      case WORK_TABLE:
                         ast = test;
                         break;
                      default:
                         ast = null;
                         break;
                   }

                   test = (Aast) test.getNextSibling();
                }

                return ast;
             }
          };
       }

       /**
        * Get an iterator on all of the field ASTs defined for a given table.
        * The fields are iterated in the order in which they were added to the
        * dictionary. All field types (including metatypes) are included in the
        * iteration. Fields of metadata tables are included in the iteration.
        * By convention, these fields' names seem to begin with a leading
        * underscore, while normal field names typically do not.
        *
        * @param   table
        *          Target table AST.
        *
        * @return  An iterator of fields for the specified table.
        */
       public Iterator<Aast> fields(final Aast table)
       {
          // Return a safe iterator if the table is null.
          if (table == null)
          {
             return EmptyIterator.get();
          }

          // Define an iterator that walks the field children of the table.
          return new Iterator<Aast>()
          {
             private Aast next = advance();

             @Override
             public boolean hasNext()
             {
                return (next != null);
             }

             @Override
             public Aast next()
             {
                if (next == null)
                {
                   throw new NoSuchElementException();
                }

                Aast ast = next;
                next = advance();

                return ast;
             }

             @Override
             public void remove()
             {
                throw new UnsupportedOperationException();
             }

             private Aast advance()
             {
                Aast test = null;
                Aast ast = null;

                if (next == null)
                {
                   test = (Aast) table.getFirstChild();
                }
                else
                {
                   test = (Aast) next.getNextSibling();
                }

                while (ast == null && test != null)
                {
                   int type = test.getType();

                   if ((type > BEGIN_FIELDTYPES && type < END_FIELDTYPES) ||
                       (type > BEGIN_METATYPES  && type < END_METATYPES))
                   {
                      ast = test;
                   }

                   test = (Aast) test.getNextSibling();
                }

                return ast;
             }
          };
       }

       /**
        * Get the index ASTs associated with a database table. The indices are
        * iterated in the order in which they were added to the dictionary.
        *
        * @param   table
        *          Target table AST.
        *
        * @return  An iterator of indices for the specified table.
        */
       public Iterator<Aast> indices(final Aast table)
       {
          // Return a safe iterator if the table was null.
          if (table == null)
          {
             return EmptyIterator.get();
          }

          // Define an iterator that walks the index children of the table.
          return new Iterator<Aast>()
          {
             private Aast next = advance();

             @Override
             public boolean hasNext()
             {
                return (next != null);
             }

             @Override
             public Aast next()
             {
                if (next == null)
                {
                   throw new NoSuchElementException();
                }

                Aast ast = next;
                next = advance();

                return ast;
             }

             @Override
             public void remove()
             {
                throw new UnsupportedOperationException();
             }

             private Aast advance()
             {
                Aast test = null;
                Aast ast = null;

                if (next == null)
                {
                   test = (Aast) table.getFirstChild();
                }
                else
                {
                   test = (Aast) next.getNextSibling();
                }

                while (ast == null && test != null)
                {
                   if (test.getType() == INDEX)
                   {
                      ast = test;
                   }

                   test = (Aast) test.getNextSibling();
                }

                return ast;
             }
          };
       }

       /**
        * Promotes <code>table</code> and all of its fields from the global scope
        * to the top-most scope, if not already present there. Creates the parent
        * database node in the same scope if not already present. Name nodes
        * that already exist in the current scope will no be re-added but any
        * missing nodes will be added.
        *
        * @param   table
        *          Name of table to be promoted. Must be an unambiguous reference
        *          within the global scope.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>table</code> matches more than one entry in the
        *          global scope of the dictionary.
        * @throws  SchemaException
        *          if <code>table</code> does not exist in the global scope or
        *          if there is no scope above the global scope to which this
        *          table may be promoted.
        */
       public void promoteTable(String table)
       throws SchemaException
       {
          promoteTable(table, true, false, true);
       }

       /**
        * Promotes <code>table</code> and all of its fields from the global scope
        * to the top-most scope, if not already present there. Creates the parent
        * database node in the same scope if not already present. If the named
        * table has ever been promoted in the current instance, it will not be
        * promoted again, unless the <code>force</code> flag is on.
        *
        * @param   table
        *          Name of table to be promoted. Must be an unambiguous reference
        *          within the global scope.
        * @param   force
        *          Override the default promotion behavior and force a refresh
        *          of the promoted table even if it was already promoted
        *          previously.
        * @param   noProp
        *          <code>true</code> to force any promoted nodes to have their
        *          no propagate flag set. This disables propagation of these
        *          nodes across the current scope boundary.
        * @param   self
        *          <code>true</code> to promote the node directly associated with
        *          <code>name</code>; <code>false</code> to promote only its
        *          descendants.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>table</code> matches more than one entry in the
        *          global scope of the dictionary.
        * @throws  SchemaException
        *          if <code>table</code> does not exist in the global scope or
        *          if there is no scope above the global scope to which this
        *          table may be promoted.
        */
       public void promoteTable(String table, boolean force, boolean noProp, boolean self)
       throws SchemaException
       {
          promoteEntry(table, self, EntityName.TABLE, force, noProp);
       }

       /**
        * Post-process the given field, in terms of honoring the VALIDATE clause, if present. This
        * will:
        * <ul>
        *    <li>
        *       If the VALIDATE clause is not present, it will remove the VALEXP and VALMSG children
        *    </li>
        *    <li>
        *       If the VALIDATE clause is present, it will remove all extent and LBRACKET information
        *       from the VALEXP nodes. Also, any reference of the source field in the VALEXP will
        *       be rewritten to reference the new field.
        *    </li>
        * </ul>
        *
        * @param   field
        *          The field which needs to be processed. This is a field defined using a
        *          <code>LIKE table.sourceField</code> clause, with or without the VALIDATE clause.
        * @param   hasValidate
        *          When <code>true</code>, it indicates the VALIDATE clause was present and the
        *          VALEXP node will need to be processed.
        * @param   sourceSchemaName
        *          The schema name of the source field
        *
        * @throws  AmbiguousSchemaNameException
        *          If field matches more than one entry in the dictionary.
        */
       public void processLikeField(Aast field, boolean hasValidate, String sourceSchemaName)
       throws AmbiguousSchemaNameException
       {
          if (!hasValidate)
          {
             // if fields do not have the VALIDATE clause specified, then the VALMSG and VALEXP
             // nodes need to be removed
             if (field.downPath(VALEXP))
             {
                field.getImmediateChild(VALEXP, null).remove();
             }
             if (field.downPath(VALMSG))
             {
                field.getImmediateChild(VALMSG, null).remove();
             }
          }

          // target temp-table field (or variable?) can't be mandatory, so we remove this node
          if (field.downPath(MANDATORY))
          {
             field.getImmediateChild(MANDATORY, null).remove();
          }

          // target temp-table field (or variable?) can't have triggers, so we remove this node
          if (field.downPath(KW_FLD_TRG))
          {
             field.getImmediateChild(KW_FLD_TRG, null).remove();
          }

          Aast valexp = field.getImmediateChild(VALEXP, null);
          if (valexp != null)
          {
             Iterator<Aast> iter = valexp.iterator();
             Set<Aast> remove = new HashSet<>();
             while (iter.hasNext())
             {
                Aast child = iter.next();
                if (child.getType() > BEGIN_FIELDTYPES &&
                    child.getType() < END_FIELDTYPES   &&
                    sourceSchemaName.equals(child.getAnnotation("schemaname")))
                {
                   child.removeAnnotation("extent");

                   // get rid of any LBRACKET, as this is no longer needed
                   if (child.downPath(LBRACKET))
                   {
                      remove.add(child.getImmediateChild(LBRACKET, null));
                   }

                   // in this case, rewrite this to reference the new field, not the source field
                   String name     = field.getText();
                   String fullname = getFullFieldName(name);
                   String bufname  = getBufferForField(name);
                   String dbname   = getLogicalDatabaseForTable(bufname);

                   if (fullname != null)
                   {
                      child.putAnnotation("schemaname", fullname);
                   }

                   if (bufname != null)
                   {
                      child.putAnnotation("bufname", bufname);
                   }

                   if (dbname != null)
                   {
                      child.putAnnotation("dbname", dbname);
                   }

                   child.putAnnotation("name", name);
                   child.setText(name);
                }
             }

             // remove the collected nodes
             for (Aast ast : remove)
             {
                ast.remove();
             }
          }
       }

       /**
        * Given a table name representing table whose structure is to be copied (e.g., for a LIKE
        * clause), find the name node associated with that table. Prefer persistent tables to
        * temp-tables. Ensure that the node returned is a table node, not a buffer node, and ensure
        * that the source (i.e., "from") node is not the same as the target (i.e., "to") node, which
        * can happen for an unqualified lookup.
        *
        * @param   fromTable
        *          Name of the table whose node is to be found.
        * @param   toNode
        *          Copy target node.
        * @param   skipGlobal
        *          if (@code true}, do not look in the global schema scope to find {@code fromTable}.
        *
        * @return  Name node as described above.
        *
        * @throws  AmbiguousSchemaNameException
        *          if the lookup results in ambiguity within a scope.
        */
       public NameNode findTableFromNode(String fromTable, NameNode toNode, boolean skipGlobal)
       throws AmbiguousSchemaNameException
       {
          int type = EntityName.TABLE;

          // Try to find the "from" node in any scope.
          String fromName = fromTable;

          // look first for a persistent table in the schema global scope; a persistent table takes
          // precedence over a temp-table with the same name, unless this behavior is explicitly
          // overridden (e.g. DEFINE BUFFER x FOR TEMP-TABLE y)
          NameNode fromNode = null;
          if (!skipGlobal)
          {
             try
             {
                fromNode = findEntry(fromTable, type, SCHEMA_GLOBAL_SCOPE, SCHEMA_GLOBAL_SCOPE, false);
             }
             catch (AmbiguousSchemaNameException exc)
             {
                // if an unambiguous match for a persistent table could not be found, allow fromNode
                // to remain null
             }
          }

          // if not found there, look for a temp-table in a higher scope
          if (fromNode == null)
          {
             fromNode = findEntry(fromTable, type, scopes.size() - 1, USER_GLOBAL_SCOPE, false);
          }

          // Make sure the node we found is not our "to" node. This can happen if the "to" node is
          // named the same as the "from" node, and was not qualified to differentiate it when it was
          // added. Also ensure that the "from" node is not a buffer;  we need the backing table.
          int i = scopes.size() - 2;
          while ((fromNode == toNode || (fromNode != null && fromNode.getType() == BUFFER)) &&
                 i >= SCHEMA_GLOBAL_SCOPE)
          {
             boolean decrement = true;

             if (fromNode != toNode && fromNode.getType() == BUFFER)
             {
                String tableName = fromNode.getAst().getParent().getText();
                if (!tableName.equals(fromName))
                {
                   fromName = tableName;
                   decrement = false;
                }
             }

             fromNode = findEntry(fromName, type, i, SCHEMA_GLOBAL_SCOPE, true);

             if (decrement)
             {
                i--;
             }
          }

          return fromNode;
       }

       /**
        * Copies all of the field entries currently stored in {@code fromNode} into the specified
        * target table node.
        *
        * @param   fromTable
        *          Name of table from which field entries are to be copied. Must
        *          be an unambiguous reference within the global scope.
        * @param   fromNode
        *          Name node which represents the table from which the field entries
        *          will be copied.
        * @param   toNode
        *          Name node which represents the table to which the field entries
        *          will be copied.
        * @param   forceTemp
        *          if (@code true}, do not look in the global schema scope to find a "real" table to
        *          replace the temp/work-table {@code fromNode} previously resolved.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>table</code> matches more than one entry in the
        *          global scope of the dictionary.
        * @throws  SchemaException
        *          if <code>fromTable</code> does not exist in the global scope.
        */
       public void addFieldEntries(String fromTable, NameNode fromNode, NameNode toNode, boolean forceTemp)
       throws SchemaException
       {
          addEntries(fromTable, fromNode, toNode, EntityName.TABLE, forceTemp);

          Iterator<Aast> fromIter = fields(fromNode.getAst());
          Iterator<Aast> toIter = fields(toNode.getAst());
          List<Aast> fromFields = new ArrayList<>();
          List<Aast> toFields = new ArrayList<>();
          while (toIter.hasNext() && fromIter.hasNext())
          {
             Aast fromField = fromIter.next();
             Aast toField = toIter.next();
             fromFields.add(fromField);
             toFields.add(toField);
          }

          Comparator<Aast> orderSort = (n1, n2) ->
          {
             if (n1.isAnnotation("order"))
             {
                return (int) ((long) n1.getAnnotation("order") - (long) n2.getAnnotation("order"));
             }
             else if (n1.downPath(ORDER))
             {
                Aast o1 = (Aast) n1.getImmediateChild(ORDER, null).getFirstChild();
                Aast o2 = (Aast) n2.getImmediateChild(ORDER, null).getFirstChild();
                return Integer.parseInt(o1.getText()) - Integer.parseInt(o2.getText());
             }
             else if (n1.isAnnotation("position"))
             {
                return (int) ((long) n1.getAnnotation("position") - (long) n2.getAnnotation("position"));
             }
             else if (n1.downPath(POSITION))
             {
                Aast o1 = (Aast) n1.getImmediateChild(POSITION, null).getFirstChild();
                Aast o2 = (Aast) n2.getImmediateChild(POSITION, null).getFirstChild();
                return Integer.parseInt(o1.getText()) - Integer.parseInt(o2.getText());
             }

             return n1.getIndexPos() - n2.getIndexPos();
          };
          Collections.sort(fromFields, orderSort);
          Collections.sort(toFields, orderSort);

          fromIter = fromFields.iterator();
          toIter = toFields.iterator();
          while (toIter.hasNext() && fromIter.hasNext())
          {
             Aast fromField = fromIter.next();
             Aast toField = toIter.next();
             String like =  fromField.isAnnotation("like")
                               ? (String) fromField.getAnnotation("like")
                               : fromNode.toQualifiedName() + "." + fromField.getText();
             toField.putAnnotation("like", like);
          }

          // force all children to have the same line/col numbers as the table
          // node, rather than the line/col nums from the copied (from) table
          forceLineColumnNums(toNode.getAst());
       }

       /**
        * Indicate whether the specified name represents a valid database in the
        * dictionary.
        *
        * @param   database
        *          Case insensitive database name; may not be abbreviated.
        *
        * @return  <code>true</code> if an entry for this database is found in
        *          the dictionary, else <code>false</code>.
        */
       public boolean isDatabase(String database)
       {
          boolean result = false;

          try
          {
             NameNode node = findEntry(database,
                                       EntityName.DATABASE,
                                       USER_GLOBAL_SCOPE,
                                       SCHEMA_GLOBAL_SCOPE,
                                       true);
             result = (node != null);
          }
          catch (IllegalArgumentException exc)
          {
             // Indicates that entity name was malformed.
             // TODO: log error.
          }
          catch (AmbiguousSchemaNameException exc)
          {
             // TODO: log error.
          }

          return result;
       }

       /**
        * Indicate whether the specified name represents a valid table in the dictionary.
        *
        * @param   table
        *          Case insensitive table name; may be qualified or not. If
        *          qualified, only the table portion of the name may be
        *          abbreviated.
        *
        * @return  <code>true</code> if an entry for this table is found in
        *          the dictionary, else <code>false</code>.
        */
       public boolean isTable(String table)
       {
          return isTable(table, false, false);
       }

       /**
        * Indicate whether the specified name represents a valid table in the dictionary.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   table
        *          Case insensitive table name; may be qualified or not. If
        *          qualified, only the table portion of the name may be
        *          abbreviated.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  <code>true</code> if an entry for this table is found in
        *          the dictionary, else <code>false</code>.
        */
       public boolean isTable(String table, boolean forceTemp, boolean forcePersistent)
       {
          boolean result = false;

          try
          {
             int l1 = computeLevel(forceTemp, forcePersistent, true, scopes.size() - 1);
             int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
             NameNode node = findEntry(table, EntityName.TABLE, l1, l2, false);
             result = (node != null);
          }
          catch (IllegalArgumentException exc)
          {
             // Indicates that entity name was malformed.
             // TODO: log error.
          }
          catch (AmbiguousSchemaNameException exc)
          {
             // TODO: log error.
          }

          return result;
       }

       /**
        * Indicate whether the specified exact name represents a valid field in the
        * dictionary, exact name match required.
        *
        * @param   field
        *          Case insensitive potential field name; may be qualified or not. No abbreviations allowed.
        *
        * @return  <code>true</code> if an entry for this field is found in
        *          the dictionary, else <code>false</code>.
        */
       public boolean isExactMatchField(String field)
       {
          boolean result = false;

          try
          {
             result = (findEntry(field, EntityName.FIELD, scopes.size() - 1, SCHEMA_GLOBAL_SCOPE, true) != null);
          }
          catch (IllegalArgumentException exc)
          {
             // Indicates that entity name was malformed.
             // TODO: log error.
          }
          catch (AmbiguousSchemaNameException exc)
          {
             // TODO: log error.
          }

          return result;
       }

       /**
        * Indicate whether the specified name represents a valid field in the
        * dictionary.
        *
        * @param   field
        *          Case insensitive field name; may be qualified or not. If
        *          qualified, only the table and/or field portion of the name may
        *          be abbreviated.
        *
        * @return  <code>true</code> if an entry for this field is found in
        *          the dictionary, else <code>false</code>.
        */
       public boolean isField(String field)
       {
          boolean result = false;

          try
          {
             result = (findEntry(field, EntityName.FIELD) != null);
          }
          catch (IllegalArgumentException exc)
          {
             // Indicates that entity name was malformed.
             // TODO: log error.
          }
          catch (AmbiguousSchemaNameException exc)
          {
             // TODO: log error.
          }

          return result;
       }

       /**
        * Perform a search for the specified table name and if found, return the
        * table's fully qualified, unabbreviated name.
        *
        * @param   table
        *          Case insensitive table name; may be qualified or not. If
        *          qualified, only the table portion of the name may be
        *          abbreviated.
        *
        * @return  The table's fully qualified, unabbreviated name, or
        *          <code>null</code> if the lookup produced no match.
        *
        * @throws  IllegalArgumentException
        *          if <code>table</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>table</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          table entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>table</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getFullTableName(String table)
       throws AmbiguousSchemaNameException
       {
          return getFullTableName(table, false, false);
       }

       /**
        * Perform a search for the specified table name and if found, return the
        * table's fully qualified, unabbreviated name.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   table
        *          Case insensitive table name; may be qualified or not. If
        *          qualified, only the table portion of the name may be
        *          abbreviated.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  The table's fully qualified, unabbreviated name, or
        *          <code>null</code> if the lookup produced no match.
        *
        * @throws  IllegalArgumentException
        *          if <code>table</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>table</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          table entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>table</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getFullTableName(String table, boolean forceTemp, boolean forcePersistent)
       throws AmbiguousSchemaNameException
       {
          int l1 = computeLevel(forceTemp, forcePersistent, true, nearestEnclosingInternal());
          int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);

          return getQualifiedName(table, EntityName.TABLE, l1, l2);
       }

       /**
        * Perform a search for the specified field name and if found, return the
        * field's fully qualified, unabbreviated name.
        *
        * @param   field
        *          Case insensitive field name; may be qualified or not. If
        *          qualified, only the table and/or field portion of the name may
        *          be abbreviated.
        *
        * @return  The field's fully qualified, unabbreviated name, or
        *          <code>null</code> if the lookup produced no match.
        *
        * @throws  IllegalArgumentException
        *          if <code>field</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>field</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          field entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>field</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getFullFieldName(String field)
       throws AmbiguousSchemaNameException
       {
          return getQualifiedName(field, EntityName.FIELD, scopes.size() - 1, SCHEMA_GLOBAL_SCOPE);
       }

       /**
        * Get the logical database name for the database associated with the
        * specified table. If <code>table</code> includes a database qualifier,
        * this is returned. Otherwise, the logical database name is determined
        * by a lookup.
        *
        * @param   table
        *          A possibly qualified, possibly abbreviated name of a table,
        *          buffer, temp-table, or work-table.
        *
        * @return  The logical name of the database associated with the buffer
        *          represented by <code>table</code>. An empty string is returned
        *          if <code>table</code> represents a temp- or work-table.
        *
        * @throws  IllegalArgumentException
        *          if <code>table</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>table</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          table entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>table</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getLogicalDatabaseForTable(String table)
       throws AmbiguousSchemaNameException
       {
          return getLogicalDatabaseForTable(table, false, false);
       }

       /**
        * Get the logical database name for the database associated with the
        * specified table. If <code>table</code> includes a database qualifier,
        * this is returned. Otherwise, the logical database name is determined
        * by a lookup.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   table
        *          A possibly qualified, possibly abbreviated name of a table,
        *          buffer, temp-table, or work-table.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  The logical name of the database associated with the buffer
        *          represented by <code>table</code>. An empty string is returned
        *          if <code>table</code> represents a temp- or work-table.
        *
        * @throws  IllegalArgumentException
        *          if <code>table</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>table</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          table entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>table</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getLogicalDatabaseForTable(String table, boolean forceTemp, boolean forcePersistent)
       throws AmbiguousSchemaNameException
       {
          EntityName eName = new EntityName(EntityName.TABLE, table);
          String dbName = eName.getDatabase();
          if (dbName != null)
          {
             return dbName;
          }

          int l1 = computeLevel(forceTemp, forcePersistent, true, nearestEnclosingInternal());
          int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
          NameNode node = findEntry(table, EntityName.TABLE, l1, l2, false);
          NameNode dbNode = node.getParent();

          return (dbNode != null ? dbNode.getName() : "");
       }

       /**
        * Get the logical database name for the database associated with the
        * specified field. If <code>field</code> includes a database qualifier,
        * this is returned. Otherwise, the logical database name is determined
        * by a lookup.
        *
        * @param   field
        *          A possibly qualified, possibly abbreviated name of a field.
        *
        * @return  The logical name of the database associated with the buffer
        *          represented by <code>field</code>'s parent. An empty string is
        *          returned if <code>field</code> is contained within a temp- or
        *          work-table.
        *
        * @throws  IllegalArgumentException
        *          if <code>field</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>field</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          field entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>field</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getLogicalDatabaseForField(String field)
       throws AmbiguousSchemaNameException
       {
          EntityName eName = new EntityName(EntityName.FIELD, field);
          String dbName = eName.getDatabase();
          if (dbName != null)
          {
             return dbName;
          }

          NameNode node = findEntry(field, EntityName.FIELD, scopes.size() - 1, SCHEMA_GLOBAL_SCOPE, false);
          NameNode dbNode = node.getParent().getParent();

          return (dbNode != null ? dbNode.getName() : "");
       }

       /**
        * Get the unabbreviated buffer name associated with the possibly
        * abbreviated name <code>table</code>.  If a database alias is provided,
        * it is passed through to the result unaltered.  Otherwise, the buffer's
        * default database qualifier is prepended.
        *
        * @param   table
        *          A possibly qualified, possibly abbreviated name of a table,
        *          buffer, temp-table, or work-table.
        *
        * @return  The unabbreviated form of <code>table</code>, using the same
        *          database qualifier, if any.
        *
        * @throws  IllegalArgumentException
        *          if <code>table</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>table</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          table entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>table</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getBufferForTable(String table)
       throws AmbiguousSchemaNameException
       {
          return getBufferForTable(table, false, false);
       }

       /**
        * Get the unabbreviated buffer name associated with the possibly
        * abbreviated name <code>table</code>.  If a database alias is provided,
        * it is passed through to the result unaltered.  Otherwise, the buffer's
        * default database qualifier is prepended.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   table
        *          A possibly qualified, possibly abbreviated name of a table,
        *          buffer, temp-table, or work-table.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  The unabbreviated form of <code>table</code>, using the same
        *          database qualifier, if any.
        *
        * @throws  IllegalArgumentException
        *          if <code>table</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>table</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          table entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>table</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getBufferForTable(String table, boolean forceTemp, boolean forcePersistent)
       throws AmbiguousSchemaNameException
       {
          int l1 = computeLevel(forceTemp, forcePersistent, true, nearestEnclosingInternal());
          int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
          NameNode node = findEntry(table, EntityName.TABLE, l1, l2, false);

          return getRecordName(node, EntityName.TABLE, table);
       }

       /**
        * Get the unabbreviated buffer name associated with the possibly
        * abbreviated name <code>field</code>.  If a database alias is provided,
        * it is passed through to the result unaltered.  Otherwise, the buffer's
        * default database qualifier is prepended.
        *
        * @param   field
        *          A possibly qualified, possibly abbreviated name of a field.
        *
        * @return  The unabbreviated form of <code>field</code>, using the same
        *          database qualifier, if any, with the field portion discarded.
        *
        * @throws  IllegalArgumentException
        *          if <code>field</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>field</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          field entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>field</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getBufferForField(String field)
       throws AmbiguousSchemaNameException
       {
          return getBufferForField(field, false, false);
       }

       /**
        * Get the unabbreviated buffer name associated with the possibly
        * abbreviated name <code>field</code>.  If a database alias is provided,
        * it is passed through to the result unaltered.  Otherwise, the buffer's
        * default database qualifier is prepended.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   field
        *          A possibly qualified, possibly abbreviated name of a field.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  The unabbreviated form of <code>field</code>, using the same
        *          database qualifier, if any, with the field portion discarded.
        *
        * @throws  IllegalArgumentException
        *          if <code>field</code> is malformed.
        * @throws  AmbiguousSchemaNameException
        *          if <code>field</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          field entry was mistakenly added to the dictionary with the
        *          same name, or (b) <code>field</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public String getBufferForField(String field, boolean forceTemp, boolean forcePersistent)
       throws AmbiguousSchemaNameException
       {
          int l1 = computeLevel(forceTemp, forcePersistent, true, scopes.size() - 1);
          int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
          NameNode node = findEntry(field, EntityName.FIELD, l1, l2, false);

          return getRecordName(node, EntityName.FIELD, field);
       }

       /**
        * Perform a search for the specified table name and if found, return the {@link NameNode}
        * associated with it.
        * <p>
        * If both forceTemp and forcePersistent are {@code false}, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   name
        *          Case insensitive table name; may be qualified or not. If qualified, only the table
        *          portion of the name may be abbreviated.
        * @param   forceTemp
        *          If {@code true}, force a temp-table lookup.
        * @param   forcePersistent
        *          If {@code true}>, force a persistent table lookup.
        *
        * @return  The {@code NameNode} associated with the given table name, or {@code null} if the
        *          lookuup produced no match.
        *
        * @throws  AmbiguousSchemaNameException
        *          if {@code table} matches more than one entry in the dictionary. This would
        *          indicate that either (a) more than one table entry mistakenly was added to the
        *          dictionary with the same name, or (b) {@code table} represents an ambiguous
        *          abbreviation that matches more than one entry.
        * @throws  SchemaException
        *          if no lookup scopes exist.
        */
       public NameNode findTableNode(String name, boolean forceTemp, boolean forcePersistent)
       throws SchemaException
       {
          int l1 = computeLevel(forceTemp, forcePersistent, true, scopes.size() - 1);
          int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);

          NameNode node = findEntry(name, EntityName.TABLE, l1, l2, false);

          return node;
       }

       /**
        * Perform a search for the specified table name and if found, return the table's type (i.e.,
        * schema table, temp table, work table, buffer).
        * <p>
        * If both forceTemp and forcePersistent are {@code false}, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   name
        *          Case insensitive table name; may be qualified or not. If qualified, only the table
        *          portion of the name may be abbreviated.
        * @param   forceTemp
        *          If {@code true}, force a temp-table lookup.
        * @param   forcePersistent
        *          If {@code true}>, force a persistent table lookup.
        *
        * @return  The table's type will be {@link #UNKNOWN} if the lookup produced no match.
        *
        * @throws  AmbiguousSchemaNameException
        *          if {@code table} matches more than one entry in the dictionary. This would
        *          indicate that either (a) more than one table entry mistakenly was added to the
        *          dictionary with the same name, or (b) {@code table} represents an ambiguous
        *          abbreviation that matches more than one entry.
        * @throws  SchemaException
        *          if no lookup scopes exist.
        */
       public int getTableType(String name, boolean forceTemp, boolean forcePersistent)
       throws SchemaException
       {
          NameNode node = findTableNode(name, forceTemp, forcePersistent);

          return (node == null ? UNKNOWN : node.getType());
       }

       /**
        * Get the Progress parser token type of a table, temp-table, work-table,
        * or buffer. If <code>name</code> represents a buffer, the type of the
        * backing record is returned. That is, a type of <code>BUFFER</code> is
        * never returned, only the type of the record which backs the buffer.
        *
        * @param   name
        *          Case insensitive record name; may be qualified or not. If
        *          qualified, only the table portion of the name may be
        *          abbreviated.
        *
        * @return  One of the Progress parser token types <code>TABLE</code>,
        *          <code>TEMP_TABLE</code>, or <code>WORK_TABLE</code>, or
        *          <code>UNKNOWN</code> if <code>name</code> cannot be found.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is an ambiguous identifier for the
        *          target record.
        */
       public int getTableRecordType(String name)
       throws AmbiguousSchemaNameException
       {
          return getTableRecordType(name, false, false);
       }

       /**
        * Get the Progress parser token type of a table, temp-table, work-table,
        * or buffer. If <code>name</code> represents a buffer, the type of the
        * backing record is returned. That is, a type of <code>BUFFER</code> is
        * never returned, only the type of the record which backs the buffer.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   name
        *          Case insensitive record name; may be qualified or not. If
        *          qualified, only the table portion of the name may be
        *          abbreviated.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  One of the Progress parser token types <code>TABLE</code>,
        *          <code>TEMP_TABLE</code>, or <code>WORK_TABLE</code>, or
        *          <code>UNKNOWN</code> if <code>name</code> cannot be found.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is an ambiguous identifier for the
        *          target record.
        */
       public int getTableRecordType(String name, boolean forceTemp, boolean forcePersistent)
       throws AmbiguousSchemaNameException
       {
          int l1 = computeLevel(forceTemp, forcePersistent, true, scopes.size() - 1);
          int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
          // Look up record name node.
          NameNode node = findEntry(name, EntityName.TABLE, l1, l2, false);

          int ttype = UNKNOWN;
          if (node != null)
          {
             // Get record type; getTable() method automatically resolves a
             // buffer reference to its backing table.
             ttype = getTable(node).getType();
          }

          return ttype;
       }

       /**
        * Get the Progress parser token type of a table, temp-table, work-table,
        * or buffer which contains the field represented by <code>name</code>. If
        * the record containing field <code>name</code> is a buffer, the type of
        * the backing record is returned. That is, a type of <code>BUFFER</code>
        * is never returned, only the type of the record which backs the buffer.
        *
        * @param   name
        *          Case insensitive field name; may be qualified or not. If
        *          qualified, only the table and field portions of the name may
        *          be abbreviated.
        *
        * @return  One of the Progress parser token types <code>TABLE</code>,
        *          <code>TEMP_TABLE</code>, or <code>WORK_TABLE</code>, or
        *          <code>UNKNOWN</code> if <code>name</code> cannot be found.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is an ambiguous identifier for the
        *          target field.
        */
       public int getFieldRecordType(String name)
       throws AmbiguousSchemaNameException
       {
          // Look up field name node.
          NameNode node = findEntry(name, EntityName.FIELD);

          int ttype = UNKNOWN;
          if (node != null)
          {
             // Get field's parent name node (a table, temp-table, work-table,
             // or buffer).
             NameNode parent = node.getParent();
             if (parent != null)
             {
                // Get record type; getTable() method automatically resolves a
                // buffer reference to its backing table.
                ttype = getTable(parent).getType();
             }
          }

          return ttype;
       }

       /**
        * Perform a search for the specified field name and if found, return the field's data type.
        *
        * @param   tableNode
        *          An optional, private name node for a table, within which to limit the lookup.
        *          If {@code null}, the normal node search algorithm is used.
        * @param   fieldName
        *          Case insensitive field name; may be qualified or not. If qualified, only the table
        *          and/or field portion of the name may be abbreviated.
        *
        * @return  The field's data type; will be type {@code UNKNOWN} if the lookup produced no
        *          match.
        *
        * @throws  AmbiguousSchemaNameException
        *          if {@code field} matches more than one entry in the dictionary. This would
        *          indicate that either (a) more than one field entry mistakenly was added to the
        *          dictionary with the same name, or (b) {@code field} represents an ambiguous
        *          abbreviation that matches more than one entry.
        * @throws  SchemaException
        *          if no lookup scopes exist.
        */
       public int getFieldDataType(NameNode tableNode, String fieldName)
       throws SchemaException
       {
          NameNode fieldNode = findFieldNode(tableNode, fieldName);

          return (fieldNode == null ? UNKNOWN : fieldNode.getType());
       }

       /**
        * Push a new scope onto the lookup stack. When performing a lookup,
        * scopes are searched in LIFO (last in first out) order. The scope will
        * be marked as one that is not an internal procedure, user-defined
        * function or trigger.
        */
       public void addScope()
       {
          addScope(false);
       }

       /**
        * Push a new scope onto the lookup stack. When performing a lookup,
        * scopes are searched in LIFO (last in first out) order.
        *
        * @param    mark
        *           <code>true</code> to mark the associated scope as one which
        *           is an internal procedure, user-defined function or trigger.
        *           <code>false</code> for any other case including external
        *           procedures and inner blocks (<code>DO, REPEAT or FOR</code>).
        */
       public void addScope(boolean mark)
       {
          scopes.push(new Scope(scopes.size(), mark));

          qualifiers.addScope(null);
       }

       /**
        * Pop the most recently added scope off the lookup stack. Does not permit
        * popping of protected scopes. No propagation of namespace contents will
        * occur.
        *
        * @throws  SchemaException
        *          if the stack is already empty.
        */
       public void removeScope()
       throws SchemaException
       {
          removeScope(false);
       }

       /**
        * Pop the most recently added scope off the lookup stack. Does not permit
        * popping of protected scopes.
        *
        * @param    propagate
        *           <code>true</code> to force the propagation of namespace
        *           contents from the removed scope into the scope that is now
        *           on the top of the stack (the enclosing scope).
        *
        * @throws   SchemaException
        *           if the stack is already empty.
        */
       public void removeScope(boolean propagate)
       throws SchemaException
       {
          if (scopes.size() == protectedScopeCount)
          {
             throw new SchemaException("No scope available to remove");
          }

          // No need to catch EmptyStackException; the above check has handled
          // that...
          Scope oldScope = scopes.pop();

          if (propagate)
          {
             // Copy the contents from the old to the new.
             propagate(oldScope, scopes.peek());
          }

          qualifiers.deleteScope();
       }

       /**
        * Persist schema dictionary information which is specific to the source
        * file currently being parsed.  For now, only temp-table and work-table
        * schema data is preserved;  buffer schema data is transient, and the
        * regular schema is persisted separately and shared across all source
        * files.
        *
        * @param   srcFile
        *          Name of the 4GL source code file containing the schema
        *          definitions being persisted.
        * @param   schemaFile
        *          Name of schema persistence file.
        *
        * @throws  AstException
        *          if any error occurs writing persistence file.
        */
       public void persist(String srcFile, String schemaFile)
       throws AstException
       {
          List<Aast> list = new ArrayList<>();

          // Check for temp tables to save.
          Iterator<Aast> iter = tables(TEMP_DB);
          if (iter.hasNext())
          {
             list.add(databases.get(TEMP_DB));
          }

          // Check for work tables to save.
          iter = tables(WORK_DB);
          if (iter.hasNext())
          {
             list.add(databases.get(WORK_DB));
          }

          if (list.isEmpty())
          {
             // No tables to save.
             return;
          }

          String normal = Configuration.normalizeFilename(schemaFile);

          // Collect all tables under one, unified, database AST. Save the
          // original 4GL source filename in the root node.
          Aast db = new ProgressAst();
          db.setType(DATABASE);
          db.setText(TEMP_TABLE_DB);
          db.putAnnotation("srcfile", srcFile);
          db.brainwash(normal);
          AstManager.get().setFlag(db.getId(), AstGenerator.FLAG_DICT);

          // Graft all temp/work tables to the new database AST. IDs are fixed
          // up as part of the graft.
          iter = list.iterator();
          while (iter.hasNext())
          {
             Aast nextDb = iter.next();
             Aast nextChild = (Aast) nextDb.getFirstChild();
             while (nextChild != null)
             {
                if (nextChild.getFilename() == null)
                {
                   // only tables which were added in this parse, avoid the tables loaded from super-classes.
                   switch (nextChild.getType())
                   {
                      case TEMP_TABLE:
                      case WORK_TABLE:
                         nextChild.graftCopyTo(db, null);
                         break;
                   }
                }

                nextChild = (Aast) nextChild.getNextSibling();
             }
          }

          if (db.getNumImmediateChildren() != 0)
          {
             AstManager.get().saveTree(db, schemaFile, false);
          }
       }

       /**
        * Debug method to dump the contents of each scope currently defined in
        * the dictionary. Scopes are printed from innermost to outermost, the same
        * order in which they are searched. Only the global namespace contents of
        * each scope are dumped.
        *
        * @param   stream
        *          Destination stream.
        */
       public void dump(PrintStream stream)
       {
          // Dump aliases.
          stream.println("ALIASES:");
          stream.println();
          stream.println("   " + aliases);
          stream.println();

          // Dump scopes.
          int size = scopes.size();
          ListIterator<Scope> iter = scopes.listIterator(size);
          for (int i = size - 1; iter.hasPrevious(); i--)
          {
             stream.println("Scope " + i + ":");
             stream.println();
             iter.previous().dump(stream);
          }
       }

       /**
        * Load a persisted schema AST from the specified file. This is intended
        * primarily to load temp- and work-tables into an instance of the
        * dictionary which already has a primary schema loaded.
        *
        * @param   schemaFile
        *          The name of a persisted schema file which is associated with
        *          a Progress source code file.
        *
        * @throws  SchemaException
        *          if any error occurs reading the schema data from persistence.
        */
       public void loadFromPersistence(String schemaFile)
       throws SchemaException
       {
          try
          {
             loadFromAst(AstManager.get().loadTree(schemaFile), false);
          }
          catch (AstException exc)
          {
             throw new SchemaException("Error reading schema data from " + schemaFile, exc);
          }
       }

       /**
        * Retrieve the AST corresponding with a table property.
        *
        * @param   name
        *          Name of the target table. Can be qualified or unqualified,
        *          abbreviated or unabbreviated, but must be unambiguous.
        * @param   tokenType
        *          Token type of the property to retrieve.
        *
        * @return  AST representing the requested property, or <code>null</code>
        *          if the property was not found.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is ambiguous as an identifier for the
        *          target table.
        * @throws  SchemaException
        *          if a table with the name <code>name</code> cannot be found.
        */
       public Aast getTableProperty(String name, int tokenType)
       throws SchemaException
       {
          // Table properties exist under an artificial PROPERTIES node, which
          // is a child of the table.
          Aast props = getTable(name).getImmediateChild(PROPERTIES, null);
          if (props == null)
          {
             return null;
          }
          return props.getImmediateChild(tokenType, null);
       }

       /**
        * Get all the AST fields for this table.
        *
        * @param   name
        *          The name of the table.
        *
        * @return  See above.
        *
        * @throws  SchemaException
        *          If no table is found with the given name.
        */
       public List<Aast> getAllFields(String name)
       throws SchemaException
       {
          List<Aast> fields = new ArrayList<>();

          Aast table = getTable(name);
          Aast field = table.getImmediateChild(FIELD_TYPE_LIST, null);
          while (field != null)
          {
             fields.add(field);
             // Get next field.
             field = table.getImmediateChild(FIELD_TYPE_LIST, field);
          }

          return fields;
       }

       /**
        * Given a table name, retrieve a map of the ASTs corresponding with all
        * field properties of a particular type.  This method returns field-level
        * properties only, even if <code>tokenType</code> might represent a
        * table-level property as well.  To retrieve a table-level property, use
        * {@link #getTableProperty}.
        *
        * @param   name
        *          Name of the <i>table</i> containing the target fields. Can be
        *          qualified or unqualified, abbreviated or unabbreviated, but
        *          must be unambiguous.
        * @param   tokenType
        *          Token type of the property to retrieve.
        *
        * @return  Map containing the ASTs representing the requested properties,
        *          or an empty map if the property was not found in any field in
        *          the designated table.  The map's keys are the AST instances
        *          for the fields (i.e., the parent ASTs of the property ASTs).
        *          Never returns <code>null</code>.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is ambiguous as an identifier for the
        *          target field.
        * @throws  SchemaException
        *          if a field with the name <code>name</code> cannot be found.
        */
       public Map<Aast, Aast> getAllFieldProperties(String name, int tokenType)
       throws SchemaException
       {
          Map<Aast, Aast> props = new IdentityHashMap<>();
          Aast table = getTable(name);
          Aast field = table.getImmediateChild(FIELD_TYPE_LIST, null);
          while (field != null)
          {
             Aast prop = field.getImmediateChild(tokenType, null);
             if (prop != null)
             {
                props.put(field, prop);
             }

             // Get next field.
             field = table.getImmediateChild(FIELD_TYPE_LIST, field);
          }

          return props;
       }

       /**
        * Retrieve the AST corresponding with a field property.
        *
        * @param   name
        *          Name of the target field. Can be qualified or unqualified,
        *          abbreviated or unabbreviated, but must be unambiguous.
        * @param   tokenType
        *          Token type of the property to retrieve.
        *
        * @return  AST representing the requested property, or <code>null</code>
        *          if the property was not found.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is ambiguous as an identifier for the
        *          target field.
        * @throws  SchemaException
        *          if a field with the name <code>name</code> cannot be found.
        */
       public Aast getFieldProperty(String name, int tokenType)
       throws SchemaException
       {
          Aast field = getField(name);

          if (field == null)
          {
             throw new SchemaException("Field '" + name + "' not found");
          }

          return field.getImmediateChild(tokenType, null);
       }

       /**
        * Copy a subset of properties from AST <code>like</code> into the field
        * AST represented by <code>name</code>. Only those properties which are
        * relevant to schema fields are copied; all others are discarded.
        *
        * @param   name
        *          Name of the target field. Can be qualified or unqualified,
        *          abbreviated or unabbreviated, but must be unambiguous.
        * @param   like
        *          AST containing the field properties to be copied.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> is ambiguous as an identifier for the
        *          target field.
        * @throws  SchemaException
        *          if a field with the name <code>name</code> cannot be found.
        */
       public void setFieldProperties(String name, Aast like)
       throws SchemaException
       {
          NameNode node = findEntry(name, EntityName.FIELD);
          if (node == null)
          {
             throw new SchemaException("Field cannot be found: " + name);
          }

          copyFieldProperties(like, node.getAst());

          // link the source and fabricated field nodes to allow to link field references to the original
          // source field definition nodes, which happens later during parsing
          // TODO: There is a similar approach using tempIdx to implement deferred refid support, consider
          //  merging this implementation with tempIdx.
          UUID uuid = UUID.randomUUID();
          long defidLow = uuid.getLeastSignificantBits();
          long defidHigh = uuid.getMostSignificantBits();
          like.putAnnotation("fdefid-low", defidLow);
          like.putAnnotation("fdefid-high", defidHigh);
          node.getAst().putAnnotation("fdefid-low", defidLow);
          node.getAst().putAnnotation("fdefid-high", defidHigh);
       }

       /**
        * Copy the specified property into the <code>PROPERTIES</code> child AST
        * of <code>table</code>. It is assumed that <code>table</code> does not
        * already contain a property of the same type as <code>property</code>,
        * as it is expected that this method is invoked only by the Progress
        * parser as it builds up a temp- or work-table definition from valid
        * Progress source code.
        *
        * @param   table
        *          AST representing the target table.
        * @param   property
        *          AST representing the property to be copied.
        *
        * @throws  SchemaException
        *          if any error occurs copying the property.
        */
       public void setTableProperty(Aast table, Aast property)
       throws SchemaException
       {
          Aast props = table.getImmediateChild(PROPERTIES, null);
          if (props == null)
          {
             props = makePropertiesAst(table);
          }

          try
          {
             property.graftCopyTo(props, null);
          }
          catch (Exception exc)
          {
             throw new SchemaException("Error setting table property", exc);
          }
       }

       /**
        * Disable or enable unique field matches when abbreviations are used - if ambiguous, it will
        * return the first found field which matches the prefix.  Used by CAN-FIND statements.
        *
        * @param    unique
        *           When <code>true</code>, it forces an unique match; otherwise, first field is
        *           return, when abbreviations are used.
        *
        * @return   The previous flag state.
        */
       public boolean setUniqueFieldMatch(boolean unique)
       {
          boolean old = uniqueFieldMatch;

          uniqueFieldMatch = unique;

          return old;
       }

       /**
        * Set the {@link #markPreferred} flag, so all info for the tables which are weak references
        * will be collected in the {@link Scope#preferredNodes} set for the current scope.
        *
        * @param    preferred
        *           The new value of the flag.
        *
        * @return   The old value of the {@link #markPreferred} field.
        */
       public boolean markPreferred(boolean preferred)
       {
          boolean old = this.markPreferred;
          this.markPreferred = preferred;

          return old;
       }

       /**
        * Set the strong scope flag for the current scope, so all info for the tables which are
        * strong references will be stored for special disambiguation.
        *
        * @param    strong
        *           The new value of the flag.
        */
       public void markStrongScope(boolean strong)
       {
          this.markStrongScope = strong;
       }

       /**
        * Load the schema dictionary from an AST of schema information ultimately
        * imported from a Progress export file. For each database, table, and
        * field, create a companion name node entry to allow namespace lookups.
        *
        * @param   root
        *          Root of the AST representing a database. This is added to the
        *          map of databases managed by the dictionary. Its nodes are
        *          walked to create the name node mirror hierarchy.
        * @param   dictdb
        *          <code>true</code> to assign the implicit DICTDB alias to this database,
        *          else <code>false</code>.
        */
       public void loadFromAst(Aast root, boolean dictdb)
       {
          loadFromAst(root, dictdb, true);
       }

       /**
        * Load the schema dictionary from an AST of schema information ultimately
        * imported from a Progress export file. For each database, table, and
        * field, create a companion name node entry to allow namespace lookups.
        *
        * @param   root
        *          Root of the AST representing a database. This is added to the
        *          map of databases managed by the dictionary. Its nodes are
        *          walked to create the name node mirror hierarchy.
        * @param   dictdb
        *          <code>true</code> to assign the implicit DICTDB alias to this database,
        *          else <code>false</code>.
        * @param   global
        *          <code>true</code> to add entry to global scope, else
        *          <code>false</code> to add entry to top scope. Only considered
        *          if <code>parent</code> is not specified; otherwise, entry is
        *          added to scope in which <code>parent</code> resides.
        */
       public void loadFromAst(Aast root, boolean dictdb, boolean global)
       {
          databases.put(root.getText(), root);

          NameNode lastDb = null;
          NameNode lastTable = null;

          Iterator<Aast> iter = root.iterator();
          while (iter.hasNext())
          {
             Aast next = iter.next();
             switch (next.getType())
             {
                case DATABASE:
                   if (next.getParent() != null)
                   {
                      break;
                   }
                   String name = next.getText().toLowerCase();
                   if (dictdb)
                   {
                      aliases.put(DICTDB, name);
                   }
                   aliases.put(name, name);
                   lastDb = addEntry(null, next, EntityName.DATABASE, global);
                   break;

                case TEMP_TABLE:
                   if (next.getParent().getType() == DATABASE)
                   {
                      Aast database = databases.get(TEMP_DB);
                      Aast copy = next.duplicate(null);
                      database.addChild(copy);
                      copy.setParent(database);
                      lastTable = addEntry(null, copy, EntityName.TABLE, global);
                   }
                   break;

                case WORK_TABLE:
                   if (next.getParent().getType() == DATABASE)
                   {
                      Aast database = databases.get(WORK_DB);
                      Aast copy = next.duplicate(null);
                      database.addChild(copy);
                      copy.setParent(database);
                      lastTable = addEntry(null, copy, EntityName.TABLE, global);
                   }
                   break;

                case TABLE:
                   if (next.getParent().getType() == DATABASE)
                   {
                      lastTable = addEntry(lastDb, next, EntityName.TABLE, global);
                   }
                   break;

                case FIELD_BLOB:
                case FIELD_CHAR:
                case FIELD_CLASS:
                case FIELD_CLOB:
                case FIELD_COM_HANDLE:
                case FIELD_DATE:
                case FIELD_DATETIME:
                case FIELD_DATETIME_TZ:
                case FIELD_DEC:
                case FIELD_HANDLE:
                case FIELD_INT:
                case FIELD_INT64:
                case FIELD_LOGICAL:
                case FIELD_RAW:
                case FIELD_RECID:
                case FIELD_ROWID:
                case FIELD_BIGINT:
                case FIELD_BYTE:
                case FIELD_DOUBLE:
                case FIELD_FIXCHAR:
                case FIELD_FLOAT:
                case FIELD_SHORT:
                case FIELD_TIMESTAMP:
                   switch (next.getParent().getType())
                   {
                      case TABLE:
                      case TEMP_TABLE:
                      case WORK_TABLE:
                         addEntry(lastTable, next, EntityName.FIELD, global);
                         break;
                   }
                   break;
             }
          }
       }

       /**
        * Merge the schema dictionary from an AST into this dictionary. This should be used for addition of nodes
        * of mutable tables into a mutable database. For each database, table, and field, if the node does not
        * exist, one is created otherwise the existing companion name node entry will allow namespace lookups.
        *
        * @param   root
        *          Root of the AST representing a database. If missing, this is added to the map of databases
        *          managed by the dictionary. Its nodes are walked to create the name node mirror hierarchy.
        */
       public void mergeFromAst(Aast root)
       {
          databases.putIfAbsent(root.getText(), root);

          NameNode dbNode = null;
          NameNode crtTable = null;

          Iterator<Aast> iter = root.iterator();
          while (iter.hasNext())
          {
             Aast next = iter.next();
             switch (next.getType())
             {
                case DATABASE:
                   if (next.getParent() != null || dbNode != null)
                   {
                      break; // validation
                   }
                   String name = next.getText().toLowerCase();
                   aliases.putIfAbsent(name, name);

                   try
                   {
                      dbNode = findEntry(next.getText(), EntityName.DATABASE, 0/*GLOBAL*/, 0/*GLOBAL*/, true);
                   }
                   catch (AmbiguousSchemaNameException e)
                   {
                      // silent. This should really not happen. If it does, we will make it worse, next
                   }

                   if (dbNode == null)
                   {
                      dbNode = addEntry(null, next, EntityName.DATABASE, true);
                   }
                   break;

                case TABLE:
                   if (next.getParent().getType() != DATABASE || dbNode == null)
                   {
                      break; // validation
                   }

                   crtTable = null;
                   try
                   {
                      crtTable = dbNode.getNamespace().find(next.getText(), true, null);
                   }
                   catch (AmbiguousSchemaNameException e)
                   {
                      // silent. This should really not happen. If it does, we will make it worse, next
                   }

                   if (crtTable == null)
                   {
                      crtTable = addEntry(dbNode, next, EntityName.TABLE, true);
                   }
                   break;

                case FIELD_BLOB:
                case FIELD_CHAR:
                case FIELD_CLASS:
                case FIELD_CLOB:
                case FIELD_COM_HANDLE:
                case FIELD_DATE:
                case FIELD_DATETIME:
                case FIELD_DATETIME_TZ:
                case FIELD_DEC:
                case FIELD_HANDLE:
                case FIELD_INT:
                case FIELD_INT64:
                case FIELD_LOGICAL:
                case FIELD_RAW:
                case FIELD_RECID:
                case FIELD_ROWID:
                case FIELD_BIGINT:
                case FIELD_BYTE:
                case FIELD_DOUBLE:
                case FIELD_FIXCHAR:
                case FIELD_FLOAT:
                case FIELD_SHORT:
                case FIELD_TIMESTAMP:
                   if (next.getParent().getType() != TABLE || crtTable == null)
                   {
                      break; // validation
                   }

                   NameNode fieldNode = null;
                   try
                   {
                      fieldNode = crtTable.getNamespace().find(next.getText(), true, null);
                   }
                   catch (AmbiguousSchemaNameException e)
                   {
                      // silent. This should really not happen. If it does, we will make it worse, next
                   }

                   if (fieldNode == null)
                   {
                      addEntry(crtTable, next, EntityName.FIELD, true);
                   }
                   break;
             }
          }
       }

       /**
        * Look up the name node for the field associated with the specified name. Searches all scopes
        * from innermost to outermost. The search is halted as soon as a match is found, or when
        * there are no more scopes left to search.
        *
        * @param   name
        *          Case insensitive database, table, or field name; may be
        *          qualified or not. If qualified, only the table and/or field
        *          portion of the name may be abbreviated.
        *
        * @return see above
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> matches more than one field entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          entry was mistakenly added to the dictionary with the same
        *          entity name, or (b) <code>name</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       public NameNode findFieldNode(String name)
       throws AmbiguousSchemaNameException
       {
          return findEntry(name, EntityName.FIELD);
       }

       /**
        * Compose an entity's fully qualified, unabbreviated name.  For nodes
        * which refer to buffers (vs. tables, temp-tables, or work-tables), this
        * returns a name which includes the fully qualified, unabbreviated name
        * of the table <strong>which backs</strong> the buffer, not the name of
        * the buffer itself.
        *
        * @param   node
        *          Name node of the entity which will determine the most specific
        *          part of the fully qualified name.  Must not be
        *          <code>null</code>.
        *
        * @return  The entity's fully qualified, unabbreviated name.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>node</code> matches more than one entry in the
        *          dictionary. This can only occur if <code>node</code> refers
        *          to a buffer, since a lookup only occurs in this case.
        */
       String getQualifiedName(NameNode node)
       throws AmbiguousSchemaNameException
       {
          node = getTableNodeIfBuffer(node);
          StringBuilder buf = new StringBuilder(node.getName());
          while ((node = node.getParent()) != null)
          {
             buf.insert(0, '.');
             node = getTableNodeIfBuffer(node);
             buf.insert(0, node.getName());
          }

          return buf.toString();
       }

       /**
        * Compose a record name from the given name node, entity type, and raw
        * name.  The resulting name will have the same database qualifier (always
        * lowercased) as <code>name</code>, or will prepend the database qualifier
        * (if any) in the event <code>name</code> contains none.
        *
        * @param   node
        *          Name node for a record or field entity.
        * @param   type
        *          <code>EntityName</code> constant indicating whether
        *          <code>name</code> represents a field or record level entity.
        * @param   name
        *          Raw name for the schema entity, possibly abbreviated, possibly
        *          qualified, as drawn directly from Progress source code.
        *
        * @return  The unabbreviated form of <code>name</code>, using the same
        *          database qualifier, if any. If <code>name</code> represents
        *          a database field, the field portion of the name is discarded.
        */
       String getRecordName(NameNode node, int type, String name)
       {
          if (node == null)
          {
             return null;
          }

          EntityName eName = new EntityName(type, name);
          NameNode table = (type == EntityName.FIELD ? node.getParent() : node);
          StringBuilder buf = new StringBuilder();

          String database = eName.getDatabase();
          if (database == null)
          {
             // If buffer found represents a promoted buffer with an explicit
             // database qualifier, use that qualifier in preference to the
             // default...
             database = qualifiers.lookup(table);

             // ...otherwise, use the default database name defined in the schema.
             if (database == null)
             {
                NameNode dbNode = table.getParent();
                if (dbNode != null)
                {
                   database = dbNode.getName();
                }
             }
          }

          if (database != null)
          {
             buf.append(database.toLowerCase()).append('.');
          }

          buf.append(table.getName());

          return buf.toString();
       }

       /**
        * Initialize the schema dictionary by adding a global scope, loading the
        * schema information in <code>root</code> into it, then adding a second
        * scope into which user global data can be loaded (secondary databases,
        * temp-tables, work-tables, etc.).
        *
        * @param   root
        *          AST of primary database to load.
        */
       private void initialize(Aast root)
       {
          // Create schema global scope.
          addScope();

          // Walk database AST to set internal state.
          loadFromAst(root, true);

          // Create user global scope.
          addScope();

          // Protect schema and user global scopes, such that they cannot be removed.
          protectedScopeCount = scopes.size();
       }

       /**
        * Force all the line and column numbers (for non-artificial nodes in the
        * tree) to be the same as the root node which is provided here.
        *
        * @param    root
        *           Parent node of the sub-tree which is to be processed.
        */
       private void forceLineColumnNums(Aast root)
       {
          int line = root.getLine();
          int col  = root.getColumn();

          Iterator<Aast> iter = root.iterator();

          while (iter.hasNext())
          {
             Aast child = iter.next();

             // only change those nodes that aren't artificial (those which
             // already have a line/col number)
             if (child.getLine() != 0 && child.getColumn() != 0)
             {
                child.setLine(line);
                child.setColumn(col);
             }
          }
       }

       /**
        * Look up the name node associated with the specified name and entity
        * type. Searches all scopes from innermost to outermost. The search is
        * halted as soon as a match is found, or when there are no more scopes
        * left to search.
        *
        * @param   name
        *          Case insensitive database, table, or field name; may be
        *          qualified or not. If qualified, only the table and/or field
        *          portion of the name may be abbreviated.
        * @param   type
        *          <code>EntityName</code> constant indicating whether name
        *          represents a <code>DATABASE</code>, <code>TABLE</code>, or
        *          <code>FIELD</code>.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          entry was mistakenly added to the dictionary with the same
        *          entity name, or (b) <code>name</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       private NameNode findEntry(String name, int type)
       throws AmbiguousSchemaNameException
       {
          return findEntry(name, type, scopes.size() - 1, SCHEMA_GLOBAL_SCOPE, false);
       }

       /**
        * Perform a search for the specified field name and if found, return the field's name node.
        *
        * @param   tableNode
        *          An optional, private name node for a table, within which to limit the lookup.
        *          If {@code null}, the normal node search algorithm is used.
        * @param   field
        *          Case insensitive field name; may be qualified or not. If qualified, only the table
        *          and/or field portion of the name may be abbreviated.
        *
        * @return  The field's name node, or {@code null} if the lookup produced no match.
        *
        * @throws  AmbiguousSchemaNameException
        *          if {@code field} matches more than one entry in the dictionary. This would
        *          indicate that either (a) more than one field entry mistakenly was added to the
        *          dictionary with the same name, or (b) {@code field} represents an ambiguous
        *          abbreviation that matches more than one entry.
        * @throws  SchemaException
        *          if no lookup scopes exist.
        */
       private NameNode findFieldNode(NameNode tableNode, String field)
       throws SchemaException
       {
          NameNode fieldNode = null;

          if (tableNode != null)
          {
             Namespace ns = tableNode.getNamespace();
             fieldNode = ns.find(field, true, bannedSchemas);
          }

          if (fieldNode == null)
          {
             fieldNode = findEntry(field, EntityName.FIELD);
          }

          return fieldNode;
       }

       /**
        * Iterate through the direct child nodes of <code>parent</code> and
        * retrieve the first child whose token type matches <code>tokenType</code>
        * and whose text matches <code>name</code>. If <code>name</code> is
        * <code>null</code>, the first child node which matches the given token
        * type is returned.
        *
        * @param   parent
        *          AST whose immediate children are checked.
        * @param   tokenType
        *          Token type of target child node.
        * @param   name
        *          Text of target child node, or <code>null</code> to match the
        *          first child of the appropriate token type.
        *
        * @return  The first child matching the criteria described above, or
        *          <code>null</code> if no match is found.
        */
       private Aast getChildAst(Aast parent, int tokenType, String name)
       {
          Aast ast = null;
          Aast next = (Aast) parent.getFirstChild();

          // Iterate over children.
          while (next != null && ast == null)
          {
             if (next.getType() == tokenType &&
                 (name == null || next.getText().equals(name)))
             {
                ast = next;
             }

             next = (Aast) next.getNextSibling();
          }

          return ast;
       }

       /**
        * Add a new database AST to the map of databases.
        *
        * @param   name
        *          Database name.
        *
        * @return  Database object created.
        */
       private Aast newDatabase(String name)
       {
          Aast ast = new ProgressAst();
          ast.setText(name);
          ast.setType(DATABASE);
          databases.put(name, ast);

          return ast;
       }

       /**
        * Create a field AST and add it as a child of the AST associated with the
        * <code>parent</code> name node.
        *
        * @param   parent
        *          Table name node which represents this field's parent entity.
        * @param   fnode
        *          The node that triggered the field creation.
        * @param   name
        *          Name of field.
        * @param   tokenType
        *          Data type of field (e.g., integer, character, raw, etc.).
        *
        * @return  Field object created.
        */
       private Aast newField(NameNode parent, Aast fnode, String name, int tokenType)
       {
          ProgressAst ast = new ProgressAst();
          ast.setType(tokenType);
          ast.setText(name.toLowerCase());
          ast.setLine(fnode.getLine());
          ast.setColumn(fnode.getColumn());
          ast.putAnnotation("historical", name);

          Aast table = getTable(parent);
          table.addChild(ast);
          ast.setParent(table);

          return ast;
       }

       /**
        * Look up the name node associated with the specified name and entity type.
        * Searches scopes in reverse order from {@code startScope} to {@code endScope}.
        *
        * @param   name
        *          Case insensitive database, table, or field name; may be
        *          qualified or not. If qualified, only the table and/or field
        *          portion of the name may be abbreviated.
        * @param   type
        *          {@code EntityName} constant indicating whether name represents a
        *          {@code DATABASE}, {@code TABLE}, or {@code FIELD}.
        * @param   startScope
        *          Index of scope at which search begins. Must be greater than or
        *          equal to {@code endScope}.
        * @param   endScope
        *          Index of scope at which search ends. Must be greater than or
        *          equal to zero.
        * @param   exact
        *          {@code true} to require an exact match; {@code false} to allow an exact
        *          or an abbreviated name to match.
        *
        * @return  Name node if found, else {@code null}.
        *
        * @throws  IllegalArgumentException
        *          if the scope index range provided is invalid.
        * @throws  AmbiguousSchemaNameException
        *          if {@code name} matches more than one entry in the dictionary. This would
        *          indicate that either (a) more than one entry was mistakenly added to the
        *          dictionary with the same entity name, or (b) {@code name} represents an
        *          ambiguous abbreviation that matches more than one entry.
        */
       private NameNode findEntry(String name, int type, int startScope, int endScope, boolean exact)
       throws AmbiguousSchemaNameException
       {
          // Validate scope range.
          if (startScope >= scopes.size() || startScope < endScope || endScope < 0)
          {
             throw new IllegalArgumentException(
                "Invalid scope search range: " + startScope + " to " + endScope);
          }

          NameNode node = null;
          EntityName entityName = new EntityName(type, name.toLowerCase());

          if (preferDefaultBuffer && type == EntityName.FIELD && entityName.getTable() == null)
          {
             // if we are searching for a field node for an unqualified field name, and we are set to prefer the
             // default buffer over any explicitly defined buffer, limit the search to USER_GLOBAL_SCOPE and
             // lower scopes
             startScope = USER_GLOBAL_SCOPE;
          }

          // Walk through all scopes from higher to lower and try to match the name in each one in turn.
          // The loop ends as soon as we have a match (or if Scope.findNode throws AmbiguousSchemaNameException).
          try
          {
             for (int i = startScope; node == null && i >= endScope; i--)
             {
                Scope scope = scopes.get(i);
                node = scope.findNode(entityName, exact);
             }
          }
          catch (AmbiguousSchemaNameException exc)
          {
             // if we had an ambiguous match on a metadata table or field with no database qualifier,
             // prepend the implicit qualifier of the last connected database to the name and try again;
             // otherwise, rethrow the exception
             // We only do this disambiguation for metadata references; this is a side effect of the way we
             // merge metadata schema into each primary database schema
             if (entityName.getDatabase() == null && name.startsWith("_"))
             {
                String qualified = null;
                String firstDatabase = getFirstDatabase();

                switch (type)
                {
                   case EntityName.TABLE:
                      qualified = String.format("%s.%s", firstDatabase, name);
                      break;
                   case EntityName.FIELD:
                      if (entityName.getTable() == null)
                      {
                         // check whether the ambiguity is at the table level, or only at the database level
                         String foundTable = null;
                         for (NameNode next : exc.getMatches())
                         {
                            // extract the table name from each match; if they are not all the same, the
                            // ambiguity is at the table level and we rethrow the exception
                            String table = next.getParent().getName();
                            if (foundTable == null)
                            {
                               foundTable = table;
                            }
                            else if (!table.equals(foundTable))
                            {
                               // this should not happen if the metadata schema is valid, but for sanity...
                               throw exc;
                            }
                         }
                         qualified = String.format("%s.%s.%s", firstDatabase, foundTable, name);
                      }
                      else
                      {
                         qualified = String.format("%s.%s", firstDatabase, name);
                      }
                      break;
                   default:
                      throw exc;
                }

                if (qualified != null)
                {
                   return findEntry(qualified, type, startScope, endScope, exact);
                }
             }

             throw exc;
          }

          return node;
       }

       /**
        * Get the database qualifier for the first "connected" (i.e. loaded/configured) database, which is not
        * a temp/work table database.
        * <p>
        * N.B. This is not the most efficient method, as it iterates all the entries in a linked hash map to get
        * to the first one. However, it is anticipated that this method only will be used when resolving an
        * exceptional condition, so it is not performance sensitive. If this changes, refactoring should be
        * considered.
        *
        * @return  The database qualifier for the first connected database.
        */
       private String getFirstDatabase()
       {
          String firstDatabase = null;

          for (String next : databases.keySet())
          {
             if ((bannedSchemas == null || !bannedSchemas.contains(next)) &&
                 !next.startsWith("@") &&
                 !next.equals("_temp"))
             {
                firstDatabase = next;
                break;
             }
          }

          return firstDatabase;
       }

       /**
        * Searches scopes from most nested to least nested to find a
        * <code>BUFFER</code> node that matches the given table name. If no
        * match is found, the search stops at the nearest enclosing internal
        * scope. If no internal scope is or encloses the current scope, then
        * this search immediately fails.
        *
        * @param   name
        *          Case insensitive table name; may be qualified or not. The
        *          table portion of the name may be abbreviated.
        *
        * @return  <code>true</code> if there is a matching buffer scoped to an
        *          enclosing internal block.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          entry was mistakenly added to the dictionary with the same
        *          entity name, or (b) <code>name</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       private boolean findInternalBuffer(String name)
       throws AmbiguousSchemaNameException
       {
          int start = scopes.size() - 1;
          int end   = nearestEnclosingInternal();

          Scope last = scopes.get(end);

          if (!last.isInternal())
          {
             // There is no internal scope so there is no internal buffer to find.
             return false;
          }

          NameNode node = null;
          EntityName entityName = new EntityName(EntityName.TABLE, name.toLowerCase());

          // Walk through all scopes from higher to lower and try to match the
          // name in each one in turn. The loop ends as soon as we have a match
          // which is a BUFFER node (or if Scope.findNode throws an
          // AmbiguousSchemaNameException).
          for (int i = start; i >= end; i--)
          {
             Scope scope = scopes.get(i);
             node = scope.findNode(entityName, false);

             if (node != null && node.getType() == BUFFER)
             {
                return true;
             }
          }

          // We didn't find a BUFFER node enclosed within an internal scope.
          return false;
       }

       /**
        * Searches scopes from most nested to least nested to find a matching
        * <code>BUFFER</code> node that is associated with a weak scope.
        *
        * @param   possible
        *          Node to test.
        *
        * @return  <code>true</code> if there is a matching buffer scoped to an
        *          enclosing weak block.
        */
       private boolean findWeakScopedBuffer(NameNode possible)
       throws AmbiguousSchemaNameException
       {
          NameNode weak = null;

          for (int i = scopes.size() - 1; i > EXT_PROC_SCOPE; i--)
          {
             Scope next = scopes.get(i);

             weak = next.getWeakReference();

             if (weak != null && weak.compareTo(possible) == 0)
             {
                return true;
             }
          }

          // We didn't find a matching BUFFER node enclosed within a weak scope.
          return false;
       }

       /**
        * Add a name node entry to a specific scope. The target scope is
        * determined by
        * <ol>
        * <li>the scope in which <code>parent</code> resides; or if this parameter
        *     is <code>null</code></li>
        * <li>the scope determined by <code>globalScope</code>:  the user global
        *     scope if <code>true</code>, else the innermost (most recently added)
        *     scope.</li>
        * </ol>
        * Within that scope, the new name node entry is added both to the
        * appropriate global namespace and to the private namespace of its parent
        * node, if any. The map of nodes to scopes is updated to reflect the
        * addition.
        *
        * @param   parent
        *          New entry's parent name node. If new entry represents a
        *          database, this should be <code>null</code>; if a table, this
        *          should be a database node or <code>null</code> (if added by
        *          parser); if a field, this should be a table node or
        *          <code>null</code> (if added by parser).
        * @param   ast
        *          AST which backs the new name node and contains property
        *          information specific to the schema entity represented by this
        *          name node.
        * @param   entityType
        *          <code>EntityName</code> constant indicating whether the new
        *          entry is a <code>DATABASE</code>, <code>TABLE</code>, or
        *          <code>FIELD</code>.
        * @param   globalScope
        *          <code>true</code> to add entry to global scope, else
        *          <code>false</code> to add entry to top scope. Only considered
        *          if <code>parent</code> is not specified; otherwise, entry is
        *          added to scope in which <code>parent</code> resides.
        *
        * @return  Name node which was added as a dictionary entry.
        */
       private NameNode addEntry(NameNode parent, Aast ast, int entityType, boolean globalScope)
       {
          // NameNode constructor automatically adds node to parent's namespace.
          NameNode node = new NameNode(parent, ast);

          // Also add node to global namespace of the target scope, but only if
          // the parent is not a buffer, as this may interfere with unqualified
          // lookups of identically named fields which are children of other
          // record types in the same scope. The buffer's fields will be added
          // to the global namespace of the proper scope once the buffer is
          // promoted.
          if (parent == null                  ||
              entityType != EntityName.FIELD  ||
              parent.getType() != BUFFER)
          {
             int nodeType = ast.getType();
             if (nodeType == TEMP_TABLE || nodeType == WORK_TABLE)
             {
                // new temp-table, init the lastOrder annotation
                ast.putAnnotation("lastOrder", 0l);
             }
             else if (nodeType >= BEGIN_FIELDTYPES && nodeType <= END_FIELDTYPES)
             {
                Aast pAst = parent != null ? parent.getAst() : ast.getParent();
                int pType = pAst.getType();
                if (pType == TEMP_TABLE || pType == WORK_TABLE)
                {
                   // for temp-table fields, explicitly adjust their order
                   long lastOrder = (Long) pAst.getAnnotation("lastOrder");
                   Aast oAst = null;
                   if (ast.downPath(ORDER))
                   {
                      oAst = ast.getImmediateChild(ORDER, null);
                   }
                   else
                   {
                      oAst = new ProgressAst();
                      oAst.setType(ORDER);
                      oAst.setText("ORDER");
                      ast.graft(oAst);

                      Aast nAst = new ProgressAst();
                      nAst.setType(NUM_LITERAL);
                      oAst.graft(nAst);
                   }
                   oAst.getFirstChild().setText(Long.toString(lastOrder));
                   lastOrder += 10;
                   pAst.putAnnotation("lastOrder", lastOrder);
                }
             }

             Scope scope = getScope(parent, ast, globalScope);

             // Buffers that are created here must not be propagated across
             // internal scope boundaries. Force the exclude flag on in these
             // cases.
             scope.addOrReplaceNode(node, entityType, (node.getType() == BUFFER), globalScope);

             // Update the scope map so we can quickly find the scope associated
             // with the new name node. The scope object has a strong reference
             // tie to key node, so wrap it in a WeakReference.
             scopeMap.put(node, new WeakReference<>(scope));
          }

          return node;
       }

       /**
        * Copies all of the child entries currently stored in {@code fromNode} into the specified
        * target table node, and with the exception of buffers, into the global namespace of the
        * scope containing the parent entity.
        * <p>
        * Record buffer types require some special handling here in the following cases:
        * <ul>
        * <li>If the destination node is a buffer and the source node is a
        *     temp-table, we have to make sure that the temp-table name node is
        *     not masking a real table of the same name. According to the Progress
        *     documentation, a buffer in this case will actually refer to the
        *     real table rather than the temp-table.
        * <li>If the destination node is a buffer and the source node is a real
        *     table, we have to create a fake database node in the buffer's scope
        *     to represent that buffer's parent database. The fake database node
        *     (termed a "shadow") does not contain any children except buffer
        *     nodes. Since shadows reside in a higher scope than the "real"
        *     database nodes created at the schema level global scope, qualified
        *     references to buffers can be resolved. Qualified references to real
        *     tables can still be resolved (unless they are masked by identically
        *     named buffers) because they will not be matched at the higher scope,
        *     and the search will continue down to the schema global scope.
        * </ul>
        *
        * @param   from
        *          Name of entity from which child entries are to be copied.
        * @param   fromNode
        *          Actual name node which references the data structures to be copied.
        * @param   toNode
        *          Name node which represents the parent entity to which the child entries will be
        *          copied.
        * @param   type
        *          Type of the parent entity to which child entries are to be
        *          added (using {@link EntityName} constants).
        * @param   forceTemp
        *          if (@code true}, do not look in the global schema scope to find a "real" table to
        *          replace the temp/work-table {@code fromNode} previously resolved.
        *
        * @throws  AmbiguousSchemaNameException
        *          if a database lookup produces more than one match.
        * @throws  SchemaException
        *          if there is any error copying fields.
        */
       private void addEntries(String from, NameNode fromNode, NameNode toNode, int type, boolean forceTemp)
       throws AmbiguousSchemaNameException,
              SchemaException
       {
          switch (toNode.getType())
          {
             // SPECIAL HANDLING FOR BUFFERS
             case BUFFER:
             {
                int fromType = fromNode.getType();
                if (fromType == TEMP_TABLE || fromType == WORK_TABLE)
                {
                   // forceTemp indicates we know the from table should be a temp/work table; only
                   // search in the schema global scope if set to false
                   if (!forceTemp)
                   {
                      // If fromNode is a temp table and toNode is a buffer, make
                      // sure fromNode is not masking a real table of the same name.
                      try
                      {
                         NameNode realTable = findEntry(from,
                                                        type,
                                                        SCHEMA_GLOBAL_SCOPE,
                                                        SCHEMA_GLOBAL_SCOPE,
                                                        true);
                         if (realTable != null)
                         {
                            fromNode = realTable;
                         }
                      }
                      catch (AmbiguousSchemaNameException exc)
                      {
                         // This answers our question: there is no real table by this name.
                      }
                   }
                }
                else if (fromType == TABLE)
                {
                   // If fromNode is a "real" table, create a fake copy (a
                   // shadow) of its parent database node and store it in the
                   // same scope as the toNode buffer. This will allow qualified
                   // name lookups of this buffer to work. Note, however, that
                   // a shadow database node must only refer to buffers as its
                   // children and must be shared across all buffers in this
                   // scope, since multiples would create ambiguity at the
                   // database qualifier level.

                   // First get the scope from the scope map.
                   Scope scope = retrieveNode(toNode);

                   // Try to find an existing shadow by the name of the
                   // fromNode's parent.
                   int parentType = EntityName.DATABASE;
                   String parentName = fromNode.getParent().getName();
                   EntityName dbName = new EntityName(parentType, parentName);
                   NameNode shadow = scope.findNode(dbName, true);

                   // Create one if it did not already exist in this scope.
                   if (shadow == null)
                   {
                      Aast dbAst = databases.get(parentName);
                      shadow = new NameNode(null, dbAst);

                      // these nodes should not propagate since they are only
                      // associated with buffers
                      scope.addNode(shadow, parentType, true);

                      scopeMap.put(shadow, new WeakReference<>(scope));
                   }

                   // special handling for OO pre-scan mode: in this mode, we do not have the normal
                   // scoping behavior, so it is possible to define multiple buffers for the same
                   // backing table; we must disallow duplicates to avoid ambiguity downstream, and
                   // we preserve only the most recent association
                   Namespace shadowNS = shadow.getNamespace();
                   String toName = toNode.getName();
                   NameNode old = shadowNS.find(toName, true, bannedSchemas);
                   if (old != null)
                   {
                      shadowNS.remove(old);
                      old.getAst().setParent(null);
                   }

                   // assign the shadow database node as the parent of the buffer
                   toNode.setParent(shadow);
                }

                // Map buffer name to underlying table. We do this by setting the
                // buffer's AST's parent to the backing table's AST, but we DO
                // NOT set the buffer's AST as a child of the backing table's
                // AST. This one-way relationship is exploited in other places
                // to associate the backing table's properties with the buffer.
                toNode.getAst().setParent(fromNode.getAst());

                break;
             }

             // If the target represents a temp-table or work-table, we need to
             // store the from table as the model table for possible follow-up
             // index definitions and table and field options.
             case TEMP_TABLE:
             case WORK_TABLE:
             {
                modelTable = getTable(fromNode);

                break;
             }
          }

          addEntries(fromNode, toNode, type);
       }

       /**
        * Copy properties from a source AST to a destination AST. The set of
        * properties (essentially child AST nodes) copied may optionally be
        * filtered by token type. It is assumed that <code>source</code> and
        * <code>destination</code> are compatible types, such that the general
        * AST structure is similar between them.
        *
        * @param   source
        *          AST which is the source of the copy.
        * @param   destination
        *          AST which is the destination of the copy.
        * @param   filter
        *          An array of token types in ascending numeric order, which
        *          represents the set of tokens which are to be included in the
        *          copy. Any property with a token type not in this set is
        *          excluded from the copy. If this parameter is <code>null</code>,
        *          all properties are copied.
        *
        * @throws  SchemaException
        *          if any error occurs during the copy.
        */
       private void copyTableProperties(Aast  source, Aast  destination, int[] filter)
       throws SchemaException
       {
          Aast srcProps = source.getImmediateChild(PROPERTIES, null);
          Aast dstProps = destination.getImmediateChild(PROPERTIES, null);

          if (srcProps == null)
          {
             // nothing to copy, we are done
             return;
          }

          if (dstProps == null)
          {
             dstProps = makePropertiesAst(destination);
          }

          try
          {
             Aast next = (Aast) srcProps.getFirstChild();
             while (next != null)
             {
                if (filter != null &&
                    Arrays.binarySearch(filter, next.getType()) >= 0)
                {
                   next.graftCopyTo(dstProps, null);
                }

                next = (Aast) next.getNextSibling();
             }
          }
          catch (AstException exc)
          {
             throw new SchemaException("Error copying table properties.", exc);
          }
       }

       /**
        * Copy FIELD properties from a source AST to a destination AST. The set
        * of properties (essentially child AST nodes) copied will be limited to
        * those that are valid in a field (filtered by token type). It is assumed
        * that <code>source</code> and <code>destination</code> are compatible
        * types, such that the general AST structure is similar between them.
        * <p>
        * If the <code>source</code> AST contains a type that already exists in
        * the <code>destination</code>, the <code>source</code> AST will take
        * precedence (the subtree of that same type in the destination will be
        * overwritten).
        *
        * @param   source
        *          AST which is the source of the copy.
        * @param   destination
        *          AST which is the destination of the copy.
        *
        * @throws  SchemaException
        *          if any error occurs during the copy.
        */
       private void copyFieldProperties(Aast source, Aast destination)
       throws SchemaException
       {
          try
          {
             Aast next = (Aast) source.getFirstChild();

             while (next != null)
             {
                int type = next.getType();

                if (Arrays.binarySearch(FIELD_PROPS, type) >= 0)
                {
                   boolean copy = true;

                   // special processing for disabling case-sensitive
                   if (type == KW_NOT)
                   {
                      // remove case-sensitivity if it exists, don't copy
                      type = KW_CASE_SEN;
                      copy = false;
                   }

                   // check if the type exists in the destination
                   Aast target = destination.getImmediateChild(type, null);

                   if (target != null)
                   {
                      // remove the target since the source AST should take
                      // precedence
                      target.remove();
                   }

                   // add to the end
                   if (copy)
                      next.graftCopyTo(destination, null);
                }

                next = (Aast) next.getNextSibling();
             }
          }
          catch (AstException exc)
          {
             throw new SchemaException("Error copying field properties.", exc);
          }
       }

       /**
        * Calculate and save the signature for the table.  Each field is represented by "datatype" or
        * "datatype[extent]" format.  The table level signature is all of the field signatures separated
        * by "|" delimiter with the number and order of the fields matching the order in the AST.
        *
        * @param    table
        *           The table AST substree.
        */
       private void calculateSignature(Aast table)
       {
          String signature = "";
          String idxSignature = "";
          String idxName = "";
          boolean isWord = false;
          boolean isUnique = false;
          boolean isPrimary = false;
          boolean isInactive = false;

          Map<String, Integer> fields = new HashMap<>();
          Map<String, Integer> fieldTypes = new HashMap<>();
          int idx = 0;

          Iterator<Aast> iter = table.iterator();

          // iterate through all fields
          while (iter.hasNext())
          {
             Aast next = iter.next();
             int type = next.getType();

             // if this is a field, calculate the field's portion of the signature
             if (type >= BEGIN_FIELDTYPES && type <= END_FIELDTYPES ||
                 type >= BEGIN_METATYPES  && type <= END_METATYPES)
             {
                Aast ext = next.getImmediateChild(KW_EXTENT, null);

                String extent = null;

                // read the extent value if any
                if (ext != null)
                {
                   if (ext.getNumImmediateChildren() == 0)
                   {
                      String err = String.format("Indeterminate EXTENT in a temp-table definition for %s",
                                                 next.dumpTree());

                      // indeterminate extent does not make sense for a temp-table, should not happen
                      throw new RuntimeException(err);
                   }
                   else
                   {
                      // the only child is a NUM_LITERAL
                      Aast num = (Aast) ext.getFirstChild();
                      extent = num.getText();
                   }
                }

                // create the field portion of the signature
                String fsig = SchemaParser.fieldSignature(type, extent);

                // add it in to the table signature
                signature += (signature.length() == 0) ? fsig : "|" + fsig;

                String hist = (String) next.getAnnotation("historical");
                if (hist == null)
                {
                   hist = next.getText();
                }
                String fname = hist.toLowerCase();
                fields.put(fname, idx++);
                fieldTypes.put(fname, type);
             }
             else if (type == KW_WORD_IDX)
             {
                isWord = true;
             }
             else if (type == KW_UNIQUE)
             {
                isUnique = true;
             }
             else if (type == KW_PRIMARY)
             {
                isPrimary = true;
             }
             else if (type == KW_INACTIVE)
             {
                isInactive = true;
             }
             else if (type == INDEX)
             {
                String idxProps = "";
                if (isWord)
                {
                   idxProps += "W";
                }
                if (isUnique)
                {
                   idxProps += "U";
                }
                if (isPrimary)
                {
                   idxProps += "P";
                }
                if (isInactive)
                {
                   idxProps += "I";
                }

                signature = signature + (idxSignature.length() > 0 ? "%" : "") + idxProps + idxSignature;

                // reset the index signature
                idxSignature = "";
                isWord = false;
                isUnique = false;
                isPrimary = false;
                isInactive = false;
                idxName = next.getText().toLowerCase();
             }
             else if (type == INDEX_FIELD)
             {
                String fname = next.getText().toLowerCase();
                int ftype = fieldTypes.get(fname);
                String fsig = SchemaParser.fieldSignature(ftype, null); // TODO: extent?
                int fidx = fields.get(fname);
                idxSignature = (idxSignature.length() == 0 ? "^" + idxName : idxSignature) + "^" + fidx + "|" + fsig;
             }

             next = (Aast) next.getNextSibling();
          }

          // save the result
          if (signature.isEmpty())
          {
             // there should always be at least 1 field
             throw new RuntimeException(String.format("Invalid temp-table, no fields!\n%s", table.dumpTree()));
          }
          else
          {
             if (idxSignature.length() > 0)
             {
                String idxProps = "";
                if (isWord)
                {
                   idxProps += "W";
                }
                if (isUnique)
                {
                   idxProps += "U";
                }
                if (isPrimary)
                {
                   idxProps += "P";
                }
                if (isInactive)
                {
                   idxProps += "I";
                }
                idxSignature = idxProps + idxSignature;
             }

             table.putAnnotation("db_signature",
                                 signature + (idxSignature.length() > 0 ? "%" : "") + idxSignature);
          }
       }

       /**
        * Copy TABLE properties from a source AST to a destination AST. The set
        * of properties (essentially child AST nodes) copied will be limited to
        * those that are valid in a table (filtered by token type). It is assumed
        * that <code>source</code> and <code>destination</code> are compatible
        * types, such that the general AST structure is similar between them.
        * <p>
        * If the <code>source</code> AST contains a type that already exists in
        * the <code>destination</code>, the <code>source</code> AST will take
        * precedence (the subtree of that same type in the destination will be
        * overwritten).
        *
        * @param   source
        *          AST which is the source of the copy.
        * @param   destination
        *          AST which is the destination of the copy.
        *
        * @throws  SchemaException
        *          if any error occurs during the copy.
        */
       private void copyTableProperties(Aast source, Aast destination)
       throws SchemaException
       {
          try
          {
             Aast next = (Aast) source.getFirstChild();

             while (next != null)
             {
                int type = next.getType();

                if (Arrays.binarySearch(TABLE_PROPS, type) >= 0)
                {
                   // make edits in our local schema dictionary
                   setTableProperty(destination, next.duplicateFresh());
                }

                next = (Aast) next.getNextSibling();
             }
          }
          catch (AstException exc)
          {
             throw new SchemaException("Error copying table properties.", exc);
          }
       }

       /**
        * Create an AST of type <code>PROPERTIES</code> and insert it as the
        * first child of <code>parent</code>.
        *
        * @param   parent
        *          AST node of which the new properties AST will become the
        *          first child.
        *
        * @return  The new properties AST.
        */
       private Aast makePropertiesAst(Aast parent)
       {
          Aast props = new ProgressAst();
          props.setType(PROPERTIES);
          props.setText("properties");
          props.setParent(parent);
          props.setNextSibling(parent.getFirstChild());
          parent.setFirstChild(props);

          return props;
       }

       /**
        * Copies all of the child entries currently stored in the global scope
        * under the parent entity identified by <code>fromNode</code> into the
        * specified target table node.
        *
        * @param   fromNode
        *          Name node of entity from which child entries are to be copied.
        * @param   toNode
        *          Name node which represents the parent entity to which the child
        *          entries will be copied.
        * @param   type
        *          Type of the parent entity to which child entries are to be
        *          added (using {@link EntityName} constants).
        *
        * @throws  SchemaException
        *          if there is any error copying a field or properties.
        */
       private void addEntries(NameNode fromNode, NameNode toNode, int type)
       throws SchemaException
       {
          if (fromNode == null)
          {
             return;
          }

          // If we are copying to a temp-table or work-table, we must create
          // the required AST objects and copy table and field properties.
          if (modelTable != null)
          {
             Aast source = null;
             Aast target = null;

             switch (type)
             {
                case EntityName.TABLE:
                   source = modelTable;
                   target = getTable(toNode);
                   copyTableProperties(source, target, null);
                   String modelName = (String) source.getAnnotation("model");
                   if (modelName == null)
                   {
                      modelName = modelTable.getParent().getText() + "." + modelTable.getText();
                   }
                   target.putAnnotation("model", modelName);
                   break;

                case EntityName.FIELD:
                   source = fromNode.getAst();
                   target = source.duplicateFresh();
                   try
                   {
                      // Graft the AST field clone to the parent table.
                      toNode.getParent().getAst().graft(target);

                      // Update the NameNode-->AST linkage.
                      toNode.setAst(target);
                   }
                   catch (AstException exc)
                   {
                      throw new SchemaException("Error grafting field to table", exc);
                   }
                   // Store the source field's fully qualified name in the target
                   // field, so we can query the converted name later.
                   int fromType = modelTable.getType();
                   boolean verbose =
                      (fromType != TEMP_TABLE && fromType != WORK_TABLE)   ||
                      (source.isAnnotation("verbose") &&
                      (Boolean) source.getAnnotation("verbose"));
                   target.putAnnotation("verbose", verbose ? Boolean.TRUE : Boolean.FALSE);
                   break;
             }
          }

          if (EntityName.hasChildType(type))
          {
             // Add copies of all children of "from" node as new entries beneath "to" node.
             boolean tempTable = (toNode.getType() == TEMP_TABLE || toNode.getType() == WORK_TABLE);

             // manufacture BEFORE-TABLE specific HIDDEN fields
             if (tempTable && toNode.getAst().isAnnotation("after-table"))
             {
                manufactureHiddenBeforeTableField(ReservedProperty._ERRORFLAG, FIELD_INT, toNode, true);
                manufactureHiddenBeforeTableField(ReservedProperty._ORIGINROWID, FIELD_ROWID, toNode, false);
                manufactureHiddenBeforeTableField(ReservedProperty._DATASOURCEROWID, FIELD_ROWID, toNode, false);
                manufactureHiddenBeforeTableField(ReservedProperty._ERRORSTRING, FIELD_CHAR, toNode, true);
                manufactureHiddenBeforeTableField(ReservedProperty._PEERROWID, FIELD_ROWID, toNode, false);
                manufactureHiddenBeforeTableField(ReservedProperty._ROWSTATE, FIELD_INT, toNode, true);
             }

             Iterator<NameNode> iter = fromNode.getNamespace().nodes(false, tempTable);
             int childType = EntityName.getChildType(type);
             while (iter.hasNext())
             {
                NameNode orig = iter.next();
                Aast ast = orig.getAst().duplicate(null);

                // Add new child entry itself.
                NameNode copy = addEntry(toNode, ast, childType, true);

                // save this ast, we will need info from it
                Aast backup = copy.getAst();

                // Recursively add all grandchildren entries.
                addEntries(orig, copy, childType);

                if (tempTable)
                {
                   // copy the ORDER node from backup AST to copy.getAst()
                   Aast cAst = copy.getAst();
                   if (cAst.downPath(ORDER))
                   {
                      cAst.getImmediateChild(ORDER, null).remove();
                   }
                   Aast oAst = backup.getImmediateChild(ORDER, null);
                   oAst.graftCopyTo(cAst, null);
                }
             }
          }
       }

       /**
        * Manufactures a hidden BEFORE-TABLE field and add it to a {@code NameNode} as a
        * EntityName.FIELD.
        *
        * @param   prop
        *          The reserved property.
        * @param   fieldType
        *          The progress type of the node.
        * @param   toNode
        *          The target table to add.
        * @param   unknown
        *          If {@code true} the default value is declared to be unknown for this field.
        */
       private void manufactureHiddenBeforeTableField(ReservedProperty prop,
                                                      int fieldType,
                                                      NameNode toNode,
                                                      boolean unknown)
       {
          Aast ast = new ProgressAst();
          ast.setType(fieldType);
          ast.setText(prop.legacy);
          ast.putAnnotation("historical", prop.legacy);
          ast.putAnnotation("__fieldId", (long) prop.propId);

          if (unknown)
          {
             Aast initAst = new ProgressAst();
             initAst.setText("INIT");
             initAst.setType(KW_INIT);
             Aast unkAst = new ProgressAst();
             unkAst.setText("?");
             unkAst.setType(UNKNOWN_VAL);
             unkAst.putAnnotation("is-literal", true);

             ast.graft(initAst);
             initAst.graft(unkAst);
          }

          addEntry(toNode, ast, EntityName.FIELD, true);
          toNode.getAst().graft(ast);
       }

       /**
        * Promotes the youngest descendants of <code>name</code> (field level
        * entities) from the global scope to the top-most scope.
        * <p>
        * The concept of "promotion" means that the reference to the youngest
        * descendants of a {@link NameNode} are copied from a lower scope to a
        * higher scope, so that they enjoy precedence over name nodes in lower
        * scopes when performing a name lookup. This is how ambiguity is removed
        * when the source code being processed by the Progress parser refers to a
        * schema entity by an unqualified or abbreviated (or both) name, which
        * would otherwise be ambiguous, if its representative name node had not
        * been promoted.
        * <p>
        * If <code>name</code>'s youngest descendants are already present in the
        * top scope, they are not added again. No attempt is made to remove
        * descendants which no longer exist (which <b>did</b> exist during a
        * previous promotion of an ancestor), because this would represent an
        * invalid condition.
        *
        * @param   name
        *          Name of entity whose descendants are to be promoted. Must be an
        *          unambiguous reference within one of the "named" scopes (i.e.,
        *          schema global, user global, external procedure, internal
        *          procedure/function).
        * @param   self
        *          <code>true</code> to promote the node directly associated with
        *          <code>name</code>; <code>false</code> to promote only its
        *          descendants.
        * @param   type
        *          Type of entity to be promoted (using {@link EntityName}
        *          constants).
        * @param   force
        *          Override the default promotion behavior and force a refresh
        *          of the promoted table even if it was already promoted
        *          previously.
        * @param   noProp
        *          <code>true</code> to force any promoted nodes to have their
        *          no propagate flag set. This disables propagation of these
        *          nodes across the current scope boundary. This is only set
        *          <code>true</code> for weak scopes.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> matches more than one entry in the
        *          global scope of the dictionary.
        * @throws  SchemaException
        *          if <code>name</code> does not exist in the global scope or
        *          if there is no scope above the global scope to which this
        *          name may be promoted.
        */
       private void promoteEntry(String name, boolean self, int type, boolean force, boolean noProp)
       throws AmbiguousSchemaNameException,
              SchemaException
       {
          // Get index of local (top) scope.
          int index = scopes.size() - 1;

          // Verify there is a scope to which entry can be promoted.
          if (index <= USER_GLOBAL_SCOPE)
          {
             throw new SchemaException("Add a scope before promoting entry " + name);
          }

          // Find name node of entity to be promoted. If not there, this is an error.
          NameNode fromNode = findEntry(name, type);
          if (fromNode == null)
          {
             throw new SchemaException("Entry not found: " + name);
          }

          boolean noPropagate = false;

          // records associated with a weak scope are not propagated.
          int fromType = fromNode.getType();
          if (noProp && fromType >= BEGIN_RECORDTYPES && fromType <= END_RECORDTYPES)
          {
             scopes.get(scopes.size() - 1).setWeakReference(fromNode);

             noPropagate = true;
          }

          // Don't ever promote the same node twice since it will be copied
          // up the stack of scopes during scope removal (see propagate()).
          // Except if the force flag is specified, in which case we will try
          // to re-promote. This is used during temp-table definition (et al)
          // to allow multiple promotions within the same scope (each one
          // refreshing the namespaces with more data) and thus allowing
          // disambiguation as needed.
          Scope scope = scopes.get(index);
          if (!scope.promoted.contains(fromNode) || force)
          {
             if (markStrongScope && fromNode.getType() == BUFFER)
             {
                // we are promoting a defined buffer (and all its fields) into a strong scope; these nodes
                // can only be accessed within the strong scope and thus must not be propagated back into the
                // enclosing scope
                noPropagate = true;
             }

             if (!noPropagate && type == EntityName.TABLE)
             {
                // Disable propagation if this is a record reference to an
                // explicitly defined buffer that is scoped to an internal scope.
                noPropagate = (findInternalBuffer(name) || findWeakScopedBuffer(fromNode));
             }

             if (self)
             {
                // promote "self" node
                promoteNode(fromNode, type, scope, noPropagate);
             }

             // promote descendants
             promoteYoungest(fromNode, type, scope, noPropagate);

             scope.promoted.add(fromNode);

             if (type == EntityName.TABLE)
             {
                // If this promoted buffer uses an explicit database qualifier,
                // remember it, in case we come across an unqualified reference to
                // the same buffer in this or a more deeply nested scope.
                EntityName eName = new EntityName(type, name);
                String database = eName.getDatabase();
                if (database != null)
                {
                   qualifiers.addEntry(false, fromNode, database);
                }
             }
          }
       }

       /**
        * Promote the youngest descendants of <code>node</code> (by reference)
        * to <code>scope</code>. This adds these nodes to the appropriate global
        * namespaces of <code>scope</code>. If the target scope already contains
        * the node, do not add it again. This method is recursive.
        *
        * @param   node
        *          Name node whose youngest descendent nodes are to be promoted.
        * @param   type
        *          Type of <code>node</code> (using {@link EntityName} constants.
        * @param   scope
        *          Target scope into which references to descendants of
        *          <code>node</code> are copied.
        * @param   noPropagate
        *          <code>true</code> to force any promoted nodes to have their
        *          no propagate flag set. This disables propagation of these
        *          nodes across an internal scope boundary.
        *
        * @see     #promoteEntry
        */
       private void promoteYoungest(NameNode node, int type, Scope scope, boolean noPropagate)
       {
          if (EntityName.hasChildType(type))
          {
             // Promote children recursively.
             int childType = EntityName.getChildType(type);
             Iterator<NameNode> iter = node.getNamespace().nodes();
             while (iter.hasNext())
             {
                NameNode child = iter.next();
                promoteYoungest(child, childType, scope, noPropagate);
             }
          }
          else
          {
             // We have reached a leaf node; add it to scope and scope map.
             promoteNode(node, type, scope, noPropagate);
          }
       }

       /**
        * Promote a node into the given scope. If the target scope already contains the node, do not
        * add it again.
        *
        * @param   node
        *          Name node to be promoted.
        * @param   type
        *          Type of {@code node} (using {@link EntityName} constants.
        * @param   scope
        *          Target scope into which references to descendants of {@code node} are copied.
        * @param   noPropagate
        *          {@code true} to force promoted node to have its no propagate flag set. This
        *          disables propagation of this node across an internal scope boundary.
        *
        * @see     #promoteEntry
        */
       private void promoteNode(NameNode node, int type, Scope scope, boolean noPropagate)
       {
          if (!scope.containsNode(node, type))
          {
             scope.addNode(node, type, noPropagate);
             scopeMap.put(node, new WeakReference<>(scope));
          }
       }

       /**
        * Propagate all namespace contents from the old scope into the
        * corresponding namespaces in the new scope. This also maintains the
        * scope map (node to scope lookup) mechanism. This propagation mechanism
        * duplicates behavior in Progress where tables promoted in a more deeply
        * nested block can disambiguate following code in enclosing blocks.
        * <p>
        * In the case where the <code>oldScope</code> is an internal scope, then
        * any nodes in its namespaces that have been marked as excludable will
        * not be propagated. This matches a 4GL behavior where promoted nodes
        * enclosed in internal scopes WHICH ALSO are using a buffer that is
        * scoped inside that internal scope, are not propagated.
        *
        * @param    oldScope
        *           Scope providing the source of the namespace contents.
        * @param    newScope
        *           Scope providing the target for the namespace contents.
        */
       private void propagate(Scope oldScope, Scope newScope)
       {
          boolean exclude = (oldScope.isInternal() || oldScope.isWeak() || oldScope.isStrong());

          for (int i = 0; i < NS_LIST.length; i++)
          {
             Namespace oldns = oldScope.getNamespace(NS_LIST[i]);
             Namespace newns = newScope.getNamespace(NS_LIST[i]);

             // obtain an iterator, but if we are transitioning away from an internal, weak, or strong scope,
             // we must mask off any nodes that have been marked for exclusion
             Iterator<NameNode> nodes = oldns.nodes(exclude);

             while (nodes.hasNext())
             {
                NameNode nn = nodes.next();

                if (!newns.contains(nn))
                {
                   newns.add(nn);
                   scopeMap.put(nn, new WeakReference<>(newScope));
                }
             }
          }
       }

       /**
        * Perform a search for the specified entity name and if found, return the
        * entity's fully qualified, unabbreviated name. For names which refer to
        * buffers (vs. tables, temp-tables, or work-tables), this returns a name
        * which includes the fully qualified, unabbreviated name of the table
        * <strong>which backs</strong> the buffer, not the name of the buffer
        * itself.
        *
        * @param   name
        *          Case insensitive entity name; may be qualified or not. If
        *          qualified, only the table and/or field portion of the name may
        *          be abbreviated.
        * @param   type
        *          <code>EntityName</code> constant indicating whether name
        *          represents a <code>DATABASE</code>, <code>TABLE</code>, or
        *          <code>FIELD</code>.
        *
        * @return  The entity's fully qualified, unabbreviated name, or
        *          <code>null</code> if the lookup produced no match.
        *
        * @throws  IllegalArgumentException
        *          if <code>name</code> is malformed. An entity name is malformed
        *          if it contains more than two dot delimiters.
        * @throws  AmbiguousSchemaNameException
        *          if <code>name</code> matches more than one entry in the
        *          dictionary. This would indicate that either (a) more than one
        *          entry was mistakenly added to the dictionary with the same
        *          entity name, or (b) <code>name</code> represents an ambiguous
        *          abbreviation that matches more than one entry.
        */
       private String getQualifiedName(String name, int type, int startScope, int endScope)
       throws AmbiguousSchemaNameException
       {
          NameNode node = findEntry(name, type, startScope, endScope, false);

          if (node != null)
          {
             return getQualifiedName(node);
          }

          return null;
       }

       /**
        * Special handling for buffer name nodes, which are really aliases to a
        * backing table definition.
        * <p>
        * Given a name node, detect if the node represents a buffer, and if so,
        * return the node associated with the table which backs that buffer.
        * Otherwise, simply return the reference to the node which was passed in.
        *
        * @param   node
        *          A name node which may or may not refer to a buffer.
        *
        * @return  <code>node</code>, or if <code>node</code> represents a
        *          buffer, the table node which backs the buffer.
        *
        * @throws  AmbiguousSchemaNameException
        *          if <code>node</code> matches more than one entry in the
        *          dictionary. This can only occur if <code>node</code> refers
        *          to a buffer, since a lookup only occurs in this case.
        */
       private NameNode getTableNodeIfBuffer(NameNode node)
       throws AmbiguousSchemaNameException
       {
          if (node.getType() == BUFFER)
          {
             Aast tt = getTable(node);
             String tname = tt.getText();
             if (tt.getParent() != null &&
                 !(tt.getParent().getText().equals(TEMP_DB) ||
                   tt.getParent().getText().equals(WORK_DB)))
             {
                tname = tt.getParent().getText() + "." + tname;
             }
             NameNode tableNode = findEntry(tname, EntityName.TABLE);
             if (tableNode != null)
             {
                return tableNode;
             }
          }

          return node;
       }

       /**
        * Returns the topmost internal scope index or the lesser of the
        * <code>EXT_PROC_SCOPE</code> and the last index position in the case
        * that no scope is marked as internal.
        *
        * @return   The internal scope index.
        */
       private int nearestEnclosingInternal()
       {
          for (int i = scopes.size() - 1; i > EXT_PROC_SCOPE; i--)
          {
             Scope next = scopes.get(i);

             if (next.isInternal())
             {
                return i;
             }
          }

          // no internal scope
          return Math.min(EXT_PROC_SCOPE, scopes.size() - 1);
       }

       /**
        * Get the scope in which <code>node</code> resides. If <code>node</code>
        * is <code>null</code>, get the scope indicated by the <code>global</code>
        * flag: the user global scope if <code>true</code>, else the most recently
        * added (innermost or top) scope in the lookup stack.
        *
        * @param   node
        *          Name node for which we want to find the enclosing scope. We
        *          look within the global namespace of each scope to find this,
        *          beginning with the most deeply nested (top) scope, and working
        *          our way back to the outermost (bottom) scope. This is usually
        *          the parent of the name node associated with the <code>ast</code>.
        * @param   ast
        *          The AST defining the text and token type of the resource being found.
        * @param   global
        *          <code>true</code> to get user global scope; <code>false</code>
        *          to get top (innermost) scope. Only considered if
        *          <code>node</code> argument is <code>null</code> or if no scope
        *          was found containing <code>node</code>.
        *
        * @return  Target scope.
        */
       private Scope getScope(NameNode node, Aast ast, boolean global)
       {
          Scope scope = retrieveNode(node);

          // If that didn't work, use the global argument and possibly the data
          // type (used for records and databases but not for fields).
          if (scope == null)
          {
             int tokenType = ast.getType();
             int last = scopes.size() - 1;
             int index = (global ? Math.min(USER_GLOBAL_SCOPE, last) : last);

             // Force records and databases to be placed in the correct scope.
             switch (tokenType)
             {
                // The special database for temp-tables must be located in the
                // user global scope so that all temp-tables will also be in that
                // scope.  Permanent databases (and their contents) must be
                // located in the schema global scope.
                case DATABASE:
                   index = (TEMP_TABLE_DB.equals(ast.getText()))
                           ? USER_GLOBAL_SCOPE
                           : SCHEMA_GLOBAL_SCOPE;
                   break;
                case TABLE:
                   index = SCHEMA_GLOBAL_SCOPE;
                   break;
                case TEMP_TABLE:
                case WORK_TABLE:
                   index = USER_GLOBAL_SCOPE;
                   break;
                case BUFFER:
                   index = (global ? EXT_PROC_SCOPE : nearestEnclosingInternal());
                   break;
             }

             scope = scopes.get(index);
          }

          return scope;
       }

       /**
        * Finds the scope of a node based on the {@code scopeMap} structure. This also
        * makes sure that the information provided is not stale (due to an eventual scope
        * removal).
        *
        * @param   node
        *          The node whose scope should be retrieved.
        *
        * @return  The scope in which the provided node can be found.
        */
       private Scope retrieveNode(NameNode node)
       {
          // The scope map contains a weak reference to the last scope to which
          // this node was added, if any. It's OK if node is null; we'll just get
          // null back from the scope map.
          Scope scope = null;
          WeakReference<Scope> ref = scopeMap.get(node);

          if (ref != null)
          {
             scope = ref.get();

             // Entry in scope map may be stale if scope was popped. In this
             // case remove it and move on.
             if (!scopes.contains(scope))
             {
                scope = null;
                scopeMap.remove(node);
             }
          }

          return scope;
       }

       /**
        * Remove all scopes except {@code SCHEMA_GLOBAL_SCOPE} and add back an empty {@code USER_GLOBAL_SCOPE}.
        */
       private void clear()
       {
          protectedScopeCount = 1;
          while (scopes.size() - 1 > SCHEMA_GLOBAL_SCOPE)
          {
             try
             {
                removeScope();
             }
             catch (SchemaException e)
             {
                // this shouldn't happen as we explicitly set the protected scope count
             }
          }

          addScope();

          // Protect schema and user global scopes, such that they cannot be removed.
          protectedScopeCount = scopes.size();

          // Add database ASTs for temp and work tables.
          newDatabase(TEMP_DB);
          newDatabase(WORK_DB);
       }

       /**
        * Compute the start or ending scoping level, considering if there is a temp-table or
        * persistent table preference.
        * <p>
        * In case of a persistent table lookup, the scope is limited to {@link #SCHEMA_GLOBAL_SCOPE}.
        * <p>
        * For a temp-table lookup, the scope starts with <code>defLvl</code> and ends with
        * {@link #USER_GLOBAL_SCOPE}.
        *
        * @param    forceTemp
        *           If <code>true</code>, force a temp-table lookup.
        * @param    forcePersistent
        *           If <code>true</code>, force a persistent table lookup.
        * @param    first
        *           Flag indicating the starting level (nearest) is computed.
        * @param    defLvl
        *           The default level to use if no preference over temp-table or persistent table.
        *
        * @return   The computed level.
        */
       private static int computeLevel(boolean forceTemp, boolean forcePersistent, boolean first, int defLvl)
       {
          if (forcePersistent)
          {
             return SCHEMA_GLOBAL_SCOPE;
          }
          else if (forceTemp)
          {
             return first ? defLvl : USER_GLOBAL_SCOPE;
          }
          else
          {
             return defLvl;
          }
       }

       /**
        * Test driver for this class. Currently, it only tests a single scope.
        * Loops continually, prompting the user for an entity type and name to
        * look up, and prints the results of the lookup. Can also be used to
        * dump schema dictionary contents to a file in the current directory
        * named <code>schema.dmp</code>.
        *
        * @param   args
        *          Not used.
        */
       public static void main(String[] args)
       {
          try
          {
             SchemaDictionary dictionary = new SchemaDictionary((Set<String>) null);
             BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
             while (true)
             {
                System.out.println("---------------------------------");
                System.out.println("Enter command (case insensitive):");
                System.out.println("  D for database search");
                System.out.println("  T for table search");
                System.out.println("  F for field search");
                System.out.println("  A to create a database alias");
                System.out.println("  L to load a database");
                System.out.println("  U to dump dictionary to ./schema.dmp");
                System.out.println();
                System.out.println("  Q to quit");
                System.out.print("Command: ");
                String command = in.readLine();
                System.out.println();
                if ("Q".equalsIgnoreCase(command))
                {
                   break;
                }
                else if ("U".equalsIgnoreCase(command))
                {
                   FileOutputStream fout = null;
                   PrintStream ps = null;
                   try
                   {
                      File file = new File("schema.dmp");
                      fout = new FileOutputStream(file);
                      ps = new PrintStream(fout);
                      dictionary.dump(ps);
                      System.out.println(file.getAbsolutePath() + " dump file created");
                   }
                   finally
                   {
                      if (ps != null)
                      {
                         ps.close();
                      }
                      else if (fout != null)
                      {
                         fout.close();
                      }
                   }

                   continue;
                }
                else if ("A".equalsIgnoreCase(command))
                {
                   System.out.print("Enter alias name            : ");
                   String alias = in.readLine();
                   System.out.print("Enter database to be aliased: ");
                   String database = in.readLine();
                   System.out.println();
                   boolean result = dictionary.createAlias(alias, database);
                   if (result)
                   {
                      System.out.println("Created alias '" + alias +
                                         "' for database '" + database + "'");
                   }
                   else
                   {
                      System.out.println("Database '" + database + "' not found");
                   }
                   System.out.println();

                   continue;
                }
                else if ("L".equalsIgnoreCase(command))
                {
                   System.out.print("Enter database name         : ");
                   String db = in.readLine();
                   System.out.println();
                   try
                   {
                      dictionary.loadSchema(db);
                      System.out.println("Database '" + db + "' loaded.");
                   }

                   catch (SchemaException se)
                   {
                      LOG.log(Level.FINE, "Unable to load database '" + db + "'.", se);
                   }
                   System.out.println();

                   continue;
                }

                System.out.print("Search Name    : ");
                String name = in.readLine();
                System.out.println();
                try
                {
                   if ("D".equalsIgnoreCase(command))
                   {
                      boolean exists = dictionary.isDatabase(name);
                      System.out.println("Database exists: " + exists);
                   }
                   else if ("T".equalsIgnoreCase(command))
                   {
                      boolean exists = dictionary.isTable(name);
                      System.out.println("Table exists   : " + exists);
                      String fullName = dictionary.getFullTableName(name);
                      if (exists)
                      {
                         System.out.println("Full table name: " + fullName);
                         Aast ast = dictionary.getTable(name);
                         String typeText = ast.getSymbolicTokenType();
                         System.out.println("Table type     : " + typeText);
                      }
                   }
                   else if ("F".equalsIgnoreCase(command))
                   {
                      boolean exists = dictionary.isField(name);
                      System.out.println("Field exists   : " + exists);
                      String fullName = dictionary.getFullFieldName(name);
                      if (exists)
                      {
                         System.out.println("Full field name: " + fullName);
                         Aast ast = dictionary.getField(name);
                         String typeText = ast.getSymbolicTokenType();
                         System.out.println("Field data type: " + typeText);
                      }
                   }
                   else
                   {
                      System.out.println("Invalid command");
                   }
                }
                catch (AmbiguousSchemaNameException exc)
                {
                   System.out.println(exc);
                }

                System.out.println();
             }
          }
          catch (Exception exc)
          {
             LOG.log(Level.SEVERE, "", exc);
          }
       }

       /**
        * Encapsulates several namespaces into a specific lookup scope. Schema
        * dictionary entries are organized by scope, and namespace lookups are
        * performed from narrowest (innermost or most recently added) to widest
        * (outermost or least recently added) scope.
        * <p>
        * Each scope contains the following three global namespaces:
        * <ul>
        * <li>all connected databases;</li>
        * <li>all tables across all databases; and</li>
        * <li>all fields across all tables in all databases.</li>
        * </ul>
        * See the description of the {@link #findNode} method for search
        * implementation details.
        */
       private class Scope
       {
          /** Stores the list of name nodes that have been promoted */
          private Set<NameNode> promoted = Collections.newSetFromMap(new IdentityHashMap<>());

          /** Unique identifier for this scope. */
          private int id = -1;

          /** Marks this scope as an internal procedure, function or trigger. */
          private boolean internal = false;

          /** A weak reference that is associated with this scope. */
          private NameNode weak = null;

          /** Global namespaces indexed by entity type (database, table, field) */
          private Map<Integer, Namespace> globals = new LinkedHashMap<>();

          /**
           * Set of preferred nodes. These nodes are the fields belonging to the table(s) which are
           * a weak reference in the current scope.
           */
          private Set<NameNode> preferredNodes = new HashSet<>();

          /** Set of nodes associated with a strong scope at this scope level. */
          private Set<NameNode> strongNodes = null;

          /**
           * Default constructor which initializes the database, table, and field
           * level global namespaces.
           *
           * @param   id
           *          Unique identifier for this scope.
           * @param   internal
           *          {@code true} to mark this scope as one which is an internal procedure, user-defined function
           *          or trigger. {@code false} for any other case including external procedures and inner blocks.
           */
          Scope(int id, boolean internal)
          {
             this.id = id;
             this.internal = internal;

             // All databases in this scope.
             globals.put(EntityName.DATABASE, new Namespace(false));

             // All tables in this scope across all databases.
             globals.put(EntityName.TABLE, new Namespace(true));

             // All fields in this scope across all tables in all databases.
             globals.put(EntityName.FIELD, new Namespace(true));
          }

          /**
           * Copy constructor which makes a new instance with the same data
           * as the given instance. Note that subsequent modifications to one
           * instance will not affect the other.
           *
           * @param   other
           *          The instance from which to copy.
           */
          Scope(Scope other)
          {
             this.id = other.id;
             this.internal = other.internal;

             // perform a deep copy of the contents of the map
             this.globals = new LinkedHashMap<>();

             Iterator<Map.Entry<Integer, Namespace>> iter = other.globals.entrySet().iterator();

             while (iter.hasNext())
             {
                Map.Entry<Integer, Namespace> next = iter.next();
                Integer key = next.getKey();
                Namespace val = new Namespace(next.getValue());
                this.globals.put(key, val);
             }
          }

          /**
           * Find the name node, if any, corresponding with the database named or
           * aliased by <code>name</code>. <code>name</code> is assumed to be an
           * alias for a logical database name. A lookup is done in the aliases
           * map maintained at the <code>SchemaDictionary</code> level. If the
           * lookup returns a name, the alias is replaced with this name. Logical
           * database names are artificially aliases to themselves, so this
           * substitution is always safe.
           *
           * @param   namespace
           *          Namespace in which to search for this database name node.
           * @param   name
           *          Logical name or alias for a database. May not be
           *          abbreviated.
           *
           * @return  The matching name node, or <code>null</code> if not found.
           *
           * @throws  AmbiguousSchemaNameException
           *          if <code>name</code> matches more than one entry in the
           *          dictionary.
           */
          protected NameNode findDatabaseNode(Namespace namespace, String name)
          throws AmbiguousSchemaNameException
          {
             name = name.toLowerCase();

             // Look up the logical database name based on the alias.
             // Logical names are aliased to themselves, so this is safe.
             String database = aliases.get(name);

             if (database != null)
             {
                name = database;
             }

             // Database names are never abbreviated.
             return namespace.find(name, true, bannedSchemas);
          }

          /**
           * Find a matching node for a table-like record entity. Record types
           * include tables, buffers, temp tables, and work tables. The following
           * rules and assumptions apply to the lookup:
           * <ul>
           * <li>Buffer, temp-table, and work-table names may never be
           *     abbreviated. Their name nodes always exist above the schema
           *     global scope, in either the user global scope, or in the internal
           *     procedure/function level scope (buffers only).
           * <li>Table name nodes exist only in the schema global scope. They are
           *     never promoted above this scope (only their child nodes are, for
           *     the sake of unqualified lookups).
           * <li>Table names may be abbreviated generally, but this may be
           *     overridden by the caller if a test for an exact match only is
           *     required.
           * <li>If we are in any scope other than those mentioned above,
           *     <code>null</code> is always returned, since no record entries
           *     will be present.
           * </ul>
           *
           * @param   namespace
           *          Namespace in which to search for this field name node.
           * @param   name
           *          A record name which may be abbreviated if it represents a
           *          regular table, but may not be abbreviated if it represents
           *          a buffer, temp table, or work table.
           * @param   exact
           *          Overrides the abbreviation rule for a regular table. If
           *          <code>false</code>, a regular table name may be abbreviated,
           *          as normal; if <code>true</code>, it can not.
           *
           * @return  The matching name node, or <code>null</code> if not found.
           *
           * @throws  AmbiguousSchemaNameException
           *          if <code>name</code> matches more than one entry in the
           *          dictionary.
           */
          protected NameNode findRecordNode(Namespace namespace, String name, boolean exact)
          throws AmbiguousSchemaNameException
          {
             if (id != SCHEMA_GLOBAL_SCOPE)
             {
                exact = true;
             }

             return namespace.find(name, exact, bannedSchemas);
          }

          /**
           * Find the name node, if any, for the field associated with
           * <code>name</code>.
           *
           * @param   namespace
           *          Namespace in which to search for this field name node.
           * @param   name
           *          Name of a field. May be abbreviated.
           * @param   exact
           *          Exact name matching required (no abbrevations if true)
           * @param   preferred
           *          A set of priority nodes used to disambiguate abbreviated fields. This set is
           *          populated with the fields for the tables which have weak references.
           * @param   strong
           *          A set of priority nodes used to disambiguate in strong scopes. May be null.
           *
           * @return  The matching name node, or <code>null</code> if not found.
           *
           * @throws  AmbiguousSchemaNameException
           *          if <code>name</code> matches more than one entry in the
           *          dictionary.
           */
          protected NameNode findFieldNode(Namespace namespace,
                                           String name,
                                           boolean exact,
                                           Set<NameNode> preferred,
                                           Set<NameNode> strong)
          throws AmbiguousSchemaNameException
          {
             return namespace.find(name, exact, uniqueFieldMatch, preferred, strong, bannedSchemas);
          }

          /**
           * Reports if this instance is marked as an internal procedure,
           * user-defined function or trigger.
           *
           * @return   <code>true</code> if this is an internal scope.
           */
          boolean isInternal()
          {
             return internal;
          }

          /**
           * Reports if this instance is marked as a weak scope.
           *
           * @return   {@code true} if this is a weak scope.
           */
          boolean isWeak()
          {
             return (weak != null);
          }

          /**
           * Reports if this instance is marked as a strong scope.
           *
           * @return  {@code true} if this is a strong scope.
           */
          boolean isStrong()
          {
             return (strongNodes != null && !strongNodes.isEmpty());
          }

          /**
           * Obtains the buffer that associated with a weak reference, which
           * is a block defined by a <code>FOR</code> statement,
           * <code>DO PRESELECT</code> statement or a
           * <code>REPEAT PRESELECT</code> statement.
           *
           * @return   Buffer node.
           */
          NameNode getWeakReference()
          {
             return weak;
          }

          /**
           * Sets a node as the weak reference associated with a block created
           * created by a <code>FOR</code> statement, <code>DO PRESELECT</code>
           * statement or a <code>REPEAT PRESELECT</code> statement.
           *
           * @param    weak
           *           Buffer node.
           */
          void setWeakReference(NameNode weak)
          {
             this.weak = weak;
          }

          /**
           * Indicate whether the global namespace indicated by <code>type</code>
           * within this scope contains a reference to <code>node</code>.
           *
           * @param   node
           *          Name node to check.
           * @param   type
           *          <code>EntityName</code> constant indicating whether
           *          <code>node</code> represents a <code>DATABASE</code>,
           *          <code>TABLE</code>, or <code>FIELD</code>.
           *
           * @return  <code>true</code> if the node already exists in this scope,
           *          else <code>false</code>.
           */
          boolean containsNode(NameNode node, int type)
          {
             return getNamespace(type).contains(node);
          }

          /**
           * Add a name node to the appropriate global namespace, determined by
           * <code>type</code>.
           *
           * @param   node
           *          Node to be added to a global namespace of this scope.
           * @param   type
           *          <code>EntityName</code> constant indicating whether
           *          <code>node</code> represents a <code>DATABASE</code>,
           *          <code>TABLE</code>, or <code>FIELD</code>.
           * @param   exclude
           *          <code>true</code> to mark this node as one that can be
           *          optionally excluded from an iterator.
           */
          void addNode(NameNode node, int type, boolean exclude)
          {
             if (markPreferred)
             {
                // collect all nodes which need preferred matching
                this.preferredNodes.add(node);
             }

             if (markStrongScope)
             {
                if (strongNodes == null)
                {
                   strongNodes = new HashSet<>();
                }

                // collect all nodes which are strongly scoped to this scope
                this.strongNodes.add(node);
             }

             getNamespace(type).add(node, exclude);
          }

          /**
           * Add a new name node or replace any existing name node (of the same
           * name) in the appropriate global namespace, determined by
           * <code>type</code>.
           *
           * @param   node
           *          Node to be added to or replaced in a global namespace of
           *          this scope.
           * @param   type
           *          <code>EntityName</code> constant indicating whether
           *          <code>node</code> represents a <code>DATABASE</code>,
           *          <code>TABLE</code>, or <code>FIELD</code>.
           * @param   exclude
           *          <code>true</code> to mark this node as one that can be
           *          optionally excluded from an iterator.
           * @param   qualifiedFind
           *          <code>true</code> to match only on a qualified node name before replacing an
           *          existing node;  else just use the unqualified name.
           */
          void addOrReplaceNode(NameNode node, int type, boolean exclude, boolean qualifiedFind)
          {
             Namespace ns  = getNamespace(type);
             NameNode  old = null;
             String name = (qualifiedFind ? node.toQualifiedName() : node.getName());

             try
             {
                old = ns.find(name, true, bannedSchemas);
             }
             catch (AmbiguousSchemaNameException ambig)
             {
                // if we are already screwed up, so be it, we'll make it worse below
             }

             if (old != null)
             {
                ns.remove(old);
             }

             ns.add(node, exclude);
          }

          /**
           * Remove all name nodes of the given type from all namespaces in which they reside, and
           * purge their descendants from all namespaces and the corresponding AST branches from
           * their ASTs.
           *
           * @param   except
           *          Set of name nodes to keep.
           * @param   type
           *          Entity type of the nodes to be removed.
           */
          void removeAllNodes(Set<NameNode> except, int type)
          {
             // remove parent nodes from the global namespace of their type
             Namespace ns = globals.get(type);
             List<NameNode> removed = ns.removeAll(except);

             // clean up all other references from the removed nodes
             noLooseEnds(removed, getChildEntityType(type), false);
          }

          /**
           * Find the name node entry, if any, associated with the specified
           * entity name. The search is performed as follows:
           * <ol>
           * <li>iterate through the constituent parts of the name;</li>
           * <li>search the global namespace associated with that name part;</li>
           * <li>if a node is found, remember it and get its namespace to narrow
           *     the search for the next iteration of the loop ("namespace
           *     reduction").</li>
           * </ol>
           * The loop terminates if any namespace lookup fails to return a match,
           * and <code>null</code> is returned. When all parts of the name have
           * been looked up in the appropriate namespace, the search is complete,
           * and the last found name node is returned.
           * <p>
           * The lookup logic for each name part follows different rules,
           * depending upon whether that part represents a database qualifier,
           * record qualifier, or field qualifier. This function is delegated to
           * the <code>findXXXXNode</code> methods listed below.
           *
           * @param   name
           *          Entity name for which to search.
           * @param   exact
           *          <code>true</code> to require an exact match;
           *          <code>false</code> to allow an exact or abbreviated name to
           *          match.
           *
           * @return  Name node which matches the specified entity name, or
           *          <code>null</code> if <code>name</code> could not be
           *          matched against any entry in this scope.
           *
           * @throws  AmbiguousSchemaNameException
           *          if <code>name</code> matches more than one entry in this
           *          scope, based upon the lookup rules applied.
           *
           * @see     #findDatabaseNode
           * @see     #findRecordNode
           * @see     #findFieldNode
           */
          NameNode findNode(EntityName name, boolean exact)
          throws AmbiguousSchemaNameException
          {
             NameNode node = null;
             Namespace namespace = null;

             // Walk through the components of the entity name and try to match
             // a name node with each qualifier, and finally with the most
             // specific part of the name. The iteration will be in the correct
             // order because globals is a LinkedHashMap.
             Iterator<Integer> iter = globals.keySet().iterator();
             while (iter.hasNext())
             {
                // Determine what part of the entity name we've got.
                int nextType = iter.next();

                // Get the name, if any, of that entity. It may represent a
                // qualifier or the actual target entity.
                String part = name.getPart(nextType);
                if (part == null)
                {
                   continue;
                }

                // If we haven't got a namespace yet (from a previous iteration
                // of the loop), get the global one for the current name part.
                if (namespace == null)
                {
                   namespace = getNamespace(nextType);
                }

                // Different portions of the name require different search logic.
                switch (nextType)
                {
                   case EntityName.DATABASE:
                      node = findDatabaseNode(namespace, part);
                      break;
                   case EntityName.TABLE:
                      node = findRecordNode(namespace, part, exact);
                      break;
                   case EntityName.FIELD:
                      node = findFieldNode(namespace, part, exact, preferredNodes, strongNodes);
                      break;
                }

                // If lookup has failed, abort the loop.
                if (node == null)
                {
                   break;
                }

                // Get the found node's private namespace for the next iteration
                // of the loop, if any (if not, we already have our target node).
                namespace = node.getNamespace();
             }

             return node;
          }

          /**
           * Get the global namespace associated with the specified
           * <code>EntityName</code> type.
           *
           * @param   type
           *          <code>EntityName</code> constant indicating whether
           *          to return the global <code>DATABASE</code>,
           *          <code>TABLE</code>, or <code>FIELD</code> namespace.
           *
           * @return  The global namespace for the specified entity type.
           */
          Namespace getNamespace(int type)
          {
             Namespace namespace = globals.get(type);
             if (namespace == null)
             {
                // Note: represents a compile time error only (invalid entity
                // type constant); no need for callers to catch this.
                throw new IllegalArgumentException("Invalid schema entity type: " + type);
             }

             return namespace;
          }

          /**
           * Debug method to dump the contents of all global namespaces managed
           * by this scope object to a stream. Private namespaces of each name
           * node are not written.
           *
           * @param   stream
           *          Destination stream.
           */
          void dump(PrintStream stream)
          {
             final String[] titles =
             {
                "   Database entries:",
                "   Table entries:",
                "   Field entries:",
             };

             Iterator<Integer> iter = globals.keySet().iterator();
             for (int i = 0; iter.hasNext(); i++)
             {
                stream.println(titles[i]);
                dumpNamespace(stream, globals.get(iter.next()));
                stream.println();
             }
          }

          /**
           * Given an {@code EntityName} part type, get the type of its child part, if any.
           *
           * @param   type
           *          {@code EntityName} part type constant.
           *
           * @return  Child entity name part type (e.g., {@code TABLE}, {@code FIELD}), or -1 if {@code type}
           *          is neither {@code EntityName.DATABASE} nor {@code EntityName.TABLE}.
           */
          private int getChildEntityType(int type)
          {
             switch (type)
             {
                case EntityName.DATABASE:
                   return EntityName.TABLE;
                case EntityName.TABLE:
                   return EntityName.FIELD;
                default:
                   return -1;
             }
          }

          /**
           * Given a list of name nodes that have been removed from their global namespace, sever all
           * of their connections with private namespaces and remove their associated ASTs from the
           * larger tree. Recursively do the same for their children.
           *
           * @param   nodes
           *          Nodes to be purged.
           * @param   type
           *          {@code EntityName} type of the nodes to be purged.
           * @param   global
           *          {@code true} to remove the nodes from their respective global namespace.
           */
          private void noLooseEnds(List<NameNode> nodes, int type, boolean global)
          {
             // iterate through the removed nodes and clean up all loose ends
             for (NameNode node : nodes)
             {
                noLooseEnds(node, type, global);
             }
          }

          /**
           * Given a name node that has been removed from its global namespace, sever all of its
           * connections with its private namespace and remove its associated AST from the larger tree.
           * Recursively do the same for its children.
           *
           * @param   node
           *          Node to be purged.
           * @param   type
           *          {@code EntityName} type of the node to be purged.
           * @param   global
           *          {@code true} to remove the node from its respective global namespace.
           */
          private void noLooseEnds(NameNode node, int type, boolean global)
          {
             // remove the node from its global namespace
             if (global && type != -1)
             {
                Namespace ns = getNamespace(type);
                ns.remove(node);
             }

             // remove the node from its parent's private namespace
             node.setParent(null);

             // remove the node's associated AST from its parent AST
             Aast ast = node.getAst();
             ast.remove();

             // remove all of the node's children from the node's private namespace
             Namespace ns = node.getNamespace();
             Namespace globalns = type == -1 ? null : globals.get(type);
             List<NameNode> removed = ns.removeAll(Collections.emptySet());
             List<NameNode> globalRemoved = globalns == null ? null : globalns.removeAll(Collections.emptySet());

             int childType = getChildEntityType(type);
             // recursively do the purge for all removed nodes
             if (!removed.isEmpty())
             {
                noLooseEnds(removed, childType, true);
             }
             if (globalRemoved != null && !globalRemoved.isEmpty())
             {
                noLooseEnds(globalRemoved, childType, true);
             }
          }

          /**
           * Debug method to dump the contents of the specified namespace to a
           * stream. The fully qualified name and type information for each node
           * is written.
           *
           * @param   stream
           *          Destination stream.
           * @param   namespace
           *          Target namespace.
           */
          private void dumpNamespace(PrintStream stream, Namespace namespace)
          {
             Iterator<NameNode> iter = namespace.nodes();
             while (iter.hasNext())
             {
                NameNode node = iter.next();

                // Get data type.
                String dataType = node.getAst().getSymbolicTokenType();

                // Get parent type, if any.
                NameNode parent = node.getParent();
                String parentType = null;
                if (parent != null)
                {
                   parentType = parent.getAst().getSymbolicTokenType();
                }

                // Compose descriptive string for next entry.
                StringBuilder buf = new StringBuilder("      ");
                buf.append(node).append(" (");

                if (parentType != null)
                {
                   buf.append(parentType).append(':');
                }

                buf.append(dataType).append(")");

                stream.println(buf);
             }
          }
       }
    }