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