# DataSource.java

    /*
    ** Module   : DataSource.java
    ** Abstract : DataSource implementation.
    **
    ** Copyright (c) 2019-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 OM  20190325 Created initial version.
    ** 002 OM  20190611 Added runtime implementation.
    ** 003 OM  20190720 Added NEXT/RESTART-ROWID implementation.
    **     OM  20190721 Added validation of NAME attribute. Fixed case-sensitivity.
    **     CA  20190728 If the DATA-SOURCE is already attached when attempting to attach it, detach it
    **                  from its buffer.
    **     CA  20190812 Changes to allow for mutable buffers; any API which receives a Buffer instance
    **                  and is invoked from converted code must resolve the runtime instance before
    **                  saving the instance.
    ** 004 OM  20201120 Made FILL-WHERE-STRING recompute automatically when conditions change.
    **     OM  20210226 Added sibling navigation support.
    **     OM  20210309 Do not use DmoMeta as key in TableMapper because temp-tables may share the same
    **                  DmoMeta instance.
    **     CA  20210928 DATA-SOURCE:QUERY attribute must return an unknown value if the query is not valid.
    **     CA  20211005 Added deletable support for dynamic data-sources.
    **     CA  20210929 Fixed SAVE-WHERE-STRING when the pairs contain the rowid function.
    **     OM  20211023 Fixed FILL for EXTENT fields.
    **     OM  20220217 Fixed SAVE-WHERE-STRING for ROWID keys.
    **     OM  20220412 Setting the query attribute to unknown is a no-op.
    **     OM  20220801 Recompute the buffer list dynamically.
    **     OM  20220523 Fine-tuned conditions when the error 11892 is raised.
    **     OM  20220524 Allowed qualified field names in ADD-SOURCE-BUFFER parameter.
    **     OM  20220526 Added legacy name for static DATA-SOURCEs. Moved implementation of KEYS attributes
    **                  to BufferImpl.
    **     OM  20220617 Populate the buffer list form the associated query if no buffers are present.
    **     OM  20220620 Added missing features in ATTACH-DATA-SOURCE method and DATA-SOURCE-ROWID attribute.
    **     CA  20221123 Static DATA-SOURCE instances must not be added to SESSION:FIRST-DATA-SOURCE chain.
    **     OM  20221129 FILL-WHERE-STRING is unknown if the buffer is not child an active dataset relation.
    **     SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
    **                  performance.
    **     HC  20230116 Replaced some handle usages with the actual wrapped resources for
    **                  performance.
    **     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
    **     HC  20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
    **                  for performance.
    ** 005 IAS 20230223 Fix after rebase.
    **         20230301 Added support for the RESTART-ROW attribute.
    ** 006 OM  20230315 DataRelation.get***BufferNative() methods return BufferImpl values.
    ** 007 CA  20230523 Performance improvement of the buffer fill operation - when possible (data type match),
    **                  both source and destination buffers), use direct access to read and write the value.
    **                  Extent fields are not supported yet.
    ** 008 OM  20230712 Reworked the DATA-SOURCE-COMPLETE-MAP and the related methods and attributes.
    ** 009 CA  20230724 Further reduce context-local usage.
    **     CA  20230727 FieldReference reads the field's extent directly without the overhead of creating the
    **                  BUFFER-FIELD resource.
    ** 010 CA  20230828 Clear the 'BufferImpl.parentDataSource' when a data-source gets deleted.
    ** 011 OM  20240110 Fixed management of the query attribute.
    ** 012 CA  20240320 Avoid BDTs within internal FWD runtime.  Replaced iterators with Java 'for', where it
    **                  applies.
    ** 013 OM  20240326 Setting the FILL-WHERE-STRING does not trigger a query-prepare.
    ** 014 OM  20240401 Code for collecting the substitution buffers for dataset queries was moved to QueryWrapper
    **                  class, avoiding code duplication.
    ** 015 OM  20240306 Fixed initialization of fields in FieldReference constructor.
    ** 016 AP  20240701 Overloaded addSourceBuffer to receive only a Buffer object. It is used for adding a new
    **                  buffer when a query is for a data source.
    **                  Modified addSourceBufferImpl method to, first, check if the actual resource used by the
    **                  runtime(returned by ref method) exists in the buffers list, as this is also added,
    **                  if successful.
    ** 017 CA  20240918 Removed default methods from interfaces and moved them as concrete implementations, as
    **                  default methods cause the Java metaspace to spike one order of magnitude.
    ** 018 ICP 20250129 Used logical constants to leverage cached instances.
    ** 019 AS  20250404 Take into account the reposition attribute of the relations for a fill operation.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.persist;

    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.util.*;
    import org.apache.commons.lang3.tuple.*;
    import java.util.*;

    /**
     * A data-source object supports the automatic filling of a {@code DataSet} object member table,
     * as well as applying updates back to one or more database tables. There is a distinct
     * data-source object for each member buffer, which allows a single ProDataSet object and a
     * single FILL operation on that object to combine data from multiple databases. A data-source
     * object is defined independently of any {@code DataSet} object.
     */
    @LegacyResource(resource = LegacyResource.DATA_SOURCE)
    public class DataSource
    extends HandleChain
    implements ADMData,
               DataSourceInterface,
               DynamicResource,
               Keyable,
               QueryAssociable
    {
       /** Marker for added buffers which use their ROWID instead of a field-formed unique key. */
       public static final String ROWID_KEY = "rowid";

       /**
        * Flags the dynamically constructed objects (using {@code CREATE} as opposed to statically
        * defined objects, with {@code DEFINE}).
        */
       private final boolean dynamic;

       /** Flag indicating if this resource has been deleted. */
       private boolean deleted = false;

       /**
        * Temporary builder during the DEFINE process. Defines the {@code DataSource} using a chaining
        * set of methods that ends with the registration of the (new) {@code DataSource}.
        */
       private Builder builder = null;

       /** The value of {@code ADM-DATA} ABL attribute. */
       private String admData = null;

       /** The list of buffers of this {@code DataSource}. */
       private final List<BufferImpl> buffers = new LinkedList<>();

       /** The current values of NEXT-ROWID attributes. */
       private final Map<BufferImpl, Long> nextRowids = new HashMap<>();

       /** The current values of RESTART-ROWID attributes. */
       private final Map<BufferImpl, Long> restartRowids = new HashMap<>();

       /** The current values of RESTART-ROW attribute. */
       private long restartRow = 0;

       /**
        * The {@code Buffer} attached to this {@code DataSource}. It must be a {@code DataSet} buffer.
        */
       private BufferImpl datasetBuffer = null;

       /** The handle to the implicit query that defines the buffers for the data-source object. */
       private handle hImplicitQuery = new handle();

       /**
        * The handle to the optional explicit query that defines the buffers for the data-source object. If this
        * is not {@code null} and valid, it will take precedence before {@link #hImplicitQuery}.
        */
       private handle hExplicitQuery = null;

       /**
        * The current value of the {@code PREFER-DATASET} attribute. Default value is {@code false}.
        */
       private boolean preferDataset = false;

       /**
        * The current value of the {@code MERGE-BY-FIELD} attribute. Default value is {@code true}.
        */
       private boolean mergeByField = true;

       /**
        * The current value of {@code FILL-WHERE-STRING} attribute, if set programmatically. The default value
        * is {@code null} / {@code unknown}, but this value cannot be set programmatically, at runtime.
        */
       private String _fillWhereString = null;

       /** The current mapping between the {@link #datasetBuffer} and fields from this object. */
       private List<Pair<String, FieldReference>> buf2dsrc = null;

       /** The reversed {@link #buf2dsrc} map. */
       private Map<String, String> revBuf2src = null;

       /** The values of {@code SAVE-WHERE-STRING} attribute, mapped by their buffers. */
       private final Map<Buffer, String[]> saveWhereStrings = new HashMap<>();

       /**
        * Factory method. Corresponds to {@code CREATE DATASOURCE} statement. Creates a new
        * {@code DataSource} and stores the object in the supplied handle.
        *
        * @param   dsHandle
        *          The destination handle.
        */
       public static void create(handle dsHandle)
       {
          DataSource.create(dsHandle, (character) null);
       }

       /**
        * Factory method. Corresponds to {@code CREATE DATASOURCE} statement. Creates a new
        * {@code DataSource} and stores the object in the supplied handle.
        *
        * @param   dsHandle
        *          The destination handle.
        * @param   widgetPool
        *          The widget pool to created the new {@code DataSource} into.
        */
       public static void create(handle dsHandle, String widgetPool)
       {
          DataSource.create(dsHandle, new character(widgetPool));
       }

       /**
        * Factory method. Corresponds to {@code CREATE DATASOURCE} statement. Creates a new
        * {@code DataSource} and stores the object in the supplied handle.
        *
        * @param   dsHandle
        *          The destination handle.
        * @param   widgetPool
        *          The widget pool to created the new {@code DataSource} into.
        */
       public static void create(handle dsHandle, character widgetPool)
       {
          // validate widget pool
          if (!WidgetPool.validWidgetPool(widgetPool))
          {
             return;
          }

          DataSource ds = new DataSource(false);
          dsHandle.assign(ds);
          ds.addToPool(widgetPool);
          ds.name("");
       }

       /**
        * Starts the definition of a new static {@code DataSource}. This corresponds to the static
        * {@code DEFINE DATA-SOURCE} statement.
        *
        * @return  A {@code Builder} object used to chain the options of the statement.
        */
       @Deprecated
       public static Builder define()
       {
          DataSource ds = new DataSource(true);
          return ds.builder;
       }

       /**
        * Starts the definition of a new static {@code DataSource}. This corresponds to the static
        * {@code DEFINE DATA-SOURCE} statement.
        *
        * @param   legacyName
        *          The legacy name of the DATA-SOURCE.
        *
        * @return  A {@code Builder} object used to chain the options of the statement.
        */
       public static Builder define(String legacyName)
       {
          DataSource ds = new DataSource(true);
          ds.name = legacyName;
          return ds.builder;
       }

       /**
        * The private constructor.
        *
        * @param   staticDef
        *          {@code true} when constructor is called form a static {@code DEFINE DATA-SET}
        *          statement and {@code false} when called from a {@code CREATE DATA-SET} statement.
        */
       private DataSource(boolean staticDef)
       {
          super(!staticDef, !staticDef);
          this.dynamic = !staticDef;

          if (staticDef)
          {
             builder = new Builder();
          }
       }

       /**
        * Check if this resource supports the NAME attribute in read-only mode.
        *
        * @return  {@code false}, allowing the name to be altered.
        */
       @Override
       protected boolean hasNameReadOnly()
       {
          return false;
       }

       /**
        * Allows each resource to declare whether it has some naming constraints.
        *
        * @return  This method always returns {@code false}, requiring that its name should be
        *          validated before assignment.
        *
        * @see  #validateName
        */
       @Override
       protected boolean hasNameConstraints()
       {
          return true;
       }

       /**
        * Returns {@code true} if the buffer is dynamic.
        * <p>
        * This is not emitted by conversion, is used internally by FWD.
        *
        * @return {@code true} if the resource is dynamic, {@code false} otherwise.
        */
       @Override
       public boolean _dynamic()
       {
          return this.dynamic;
       }

       /**
        * Checks  whether the resource is dynamic or static.
        *
        * @return   {@code true} if the resource is dynamic, {@code false} otherwise.
        */
       @Override
       public logical dynamic()
       {
          return logical.of(dynamic);
       }

       /**
        * Reports if this object is valid for use.
        *
        * @return  {@code true} if we are valid (can be used).
        */
       @Override
       public boolean valid()
       {
          return !deleted;
       }

       /**
        * Get the {@code PREV-SIBLING} attribute of this DataSource. Actually always returns {@code null} because
        * this attribute is not supported by DataSources. Only {@code NEXT-SIBLING} is supported.
        *
        * @return  Always {@code null}.
        */
       @Override
       public handle getPrevSibling()
       {
          handle.invalidAttribute("PREV-SIBLING", false, LegacyResource.DATASET);
          return new handle();
       }

       /**
        * Get the {@code NEXT-SIBLING} attribute of this DataSource. Actually always returns the super's
        * {@code getPrevSibling()} because DataSets are queued in reverse order(!).
        * <p>
        * TODO: investigate other handle resource that manifest the same from the point of NEXT/PREV-SIBLING.
        *
        * @return  Always {@code null}.
        */
       @Override
       public handle getNextSibling()
       {
          return super.getPrevSibling();
       }

       /**
        * Get the value of the ADM-DATA attribute.
        *
        * @return See above.
        */
       @Override
       public character getADMData()
       {
          return new character(admData);
       }

       /**
        * Set the value of the ADM-DATA attribute.
        *
        * @param value The new value.
        */
       @Override
       public void setADMData(String value)
       {
          this.admData = value;
       }

       /**
        * Set the value of the ADM-DATA attribute.
        *
        * @param value The new value.
        */
       @Override
       public void setADMData(character value)
       {
          this.admData = value.toJavaType();
       }

       /**
        * Get the query associated with the given resource.
        *
        * @return The associated query or {@code unknown} if none exists.
        */
       @Override
       public handle getQueryAsHandle()
       {
          if (hExplicitQuery != null)
          {
             if (hExplicitQuery._isValid())
             {
                // if [hExplicitQuery] is set and valid, it takes precedence before [hImplicitQuery]
                return hExplicitQuery;
             }
             else
             {
                // self update:  if the explicit query was deleted switch back to implicit query
                hExplicitQuery = null;
             }
          }

          if (!hImplicitQuery._isValid())
          {
             createImplicitQuery();
          }
          return hImplicitQuery;
       }

       /**
        * Set the query associated with the given resource. If an explicit query was previously set and now an
        * unknown handle is use, the implicit handle is restored.
        *
        * @param   qry
        *          The query to associate.
        */
       @Override
       public void setQueryAsHandle(handle qry)
       {
          if (qry.isUnknown())
          {
             hExplicitQuery = null;
             return;
          }

          if (!qry.isType(LegacyResource.QUERY) || !qry._isValid())
          {
             ErrorManager.recordOrShowError(11976);
             // Data-source QUERY attribute requires valid query handle or unknown.
             ErrorManager.recordOrThrowError(3131, "QUERY", "", "DATA-SOURCE");
             // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
             return;
          }

          setQueryImpl(qry);
       }

       /**
        * Set the query associated with the given resource.
        *
        * @param   qry
        *          The query to associate.
        */
       @Override
       public void setQueryAsHandle(QueryWrapper qry)
       {
          setQueryImpl(new handle(qry));
       }

       /**
        * Checks whether the modifications to the data currently in the data source are ignored when
        * saving changes from a {@code DataSet} temp-table buffer to the associated data source using
        * the {@code SAVE-ROW-CHANGES()} method. The default value is {@code FALSE}.
        *
        * @return  the current value of {@code PREFER-DATASET} attribute, as described above.
        */
       @Override
       public logical getPreferDataset()
       {
          return new logical(preferDataset);
       }

       /**
        * Configures whether the modifications to the data currently in the data source are ignored
        * when saving changes from a {@code DataSet} temp-table buffer to the associated data source
        * using the {@code SAVE-ROW-CHANGES()} method.
        * <p>
        * This attribute is synonym to {@code IGNORE-CURRENT-MODIFIED}. The latter one is deprecated,
        * but its name is reported in exception message.
        *
        * @param   pref
        *          the new value of {@code PREFER-DATASET} attribute, as described above.
        */
       @Override
       public void setPreferDataset(logical pref)
       {
          if (pref == null || pref.isUnknown())
          {
             unableToAssignUnknown("IGNORE-CURRENT-MODIFIED", "DATA-SET widget");
             return;
          }

          setPreferDataset(pref.getValue());
       }

       /**
        * Configures whether the modifications to the data currently in the data source are ignored
        * when saving changes from a {@code DataSet} temp-table buffer to the associated data source
        * using the {@code SAVE-ROW-CHANGES()} method.
        *
        * @param   pref
        *          the new value of {@code PREFER-DATASET} attribute, as described above.
        */
       @Override
       public void setPreferDataset(boolean pref)
       {
          preferDataset = pref;
       }

       /**
        * Gets the current {@code WHERE} expression for a data-source query, beginning with the
        * keyword {@code WHERE}, and not including the prior {@code FOR EACH} phrase of an
        * {@code OPEN QUERY} statement.
        *
        * @return  The current value of the {@code FILL-WHERE-STRING} attribute, as described above.
        */
       @Override
       public character getFillWhereString()
       {
          return new character(_fillWhereString != null ? _fillWhereString : computeFillWhereString());
       }

       /**
        * Sets the current {@code WHERE} expression for a data-source query, beginning with the
        * keyword {@code WHERE}, and not including the prior {@code FOR EACH} phrase of an
        * {@code OPEN QUERY} statement.
        *
        * @param   pref
        *          The new value of the {@code FILL-WHERE-STRING} attribute, as described above.
        */
       @Override
       public void setFillWhereString(String pref)
       {
          if (datasetBuffer == null)
          {
             ErrorManager.recordOrThrowError(12786);
             // FILL-WHERE-STRING cannot be assigned unless the DATA-SOURCE is attached to some dataset buffer by a prior ATTACH-DATA-SOURCE. (12786)
             return;
          }

          // NOTE: initially I lived under the impression that IN P4GL, the syntax of the string is NOT checked
          //       (at least at this moment). It turned out that a query is created and prepare for it, so the
          //       syntax is surely checked. Possibly if no buffers are set first?
          this._fillWhereString = pref;

          if (hImplicitQuery != null && hImplicitQuery._isValid())
          {
             QueryWrapper query = (QueryWrapper) hImplicitQuery.getResource();
             if (!query.isExplicit())
             {
                query.delete();
             }
          }
          hImplicitQuery = new handle();
       }

       /**
        * Sets the current {@code WHERE} expression for a data-source query, beginning with the
        * keyword {@code WHERE}, and not including the prior {@code FOR EACH} phrase of an
        * {@code OPEN QUERY} statement.
        *
        * @param   pref
        *          The new value of the {@code FILL-WHERE-STRING} attribute, as described above.
        */
       @Override
       public void setFillWhereString(Text pref)
       {
          if (pref == null || pref.isUnknown())
          {
             unableToAssignUnknown("FILL-WHERE-STRING", "DATA-SOURCE widget");
             return;
          }

          setFillWhereString(pref.getValue());
       }

       /**
        * Obtain the list of key fields of a specified buffer as a comma-separated list.
        *
        * @return  the list of key fields for the requested buffer.
        */
       @Override
       public character keys()
       {
          return keys(1);
       }

       /**
        * Obtain the list of key fields of a specified buffer as a comma-separated list.
        *
        * @param   seqNum
        *          The index of the buffer in the list of buffer for this {@code DataSource}.
        *
        * @return  the list of key fields for the requested buffer.
        */
       @Override
       public character keys(long seqNum)
       {
          if (seqNum < 1 || seqNum > buffers.size())
          {
             return new character();
          }

          BufferImpl buffer = buffers.get((int) (seqNum - 1));
          return buffer.ref().keys();
       }

       /**
        * Obtain the list of key fields of a specified buffer as a comma-separated list.
        *
        * @param   seqNum
        *          The index of the buffer in the list of buffer for this {@code DataSource}.
        *
        * @return  the list of key fields for the requested buffer.
        */
       @Override
       public character keys(NumberType seqNum)
       {
          if (seqNum == null || seqNum.isUnknown())
          {
             return new character();
          }

          return keys(seqNum.longValue());
       }

       /**
        * Checks whether the changes on a field-by-field basis will be merged when saving changes from
        * a {@code DataSet} temp-table buffer to the associated data source using the
        * {@code SAVE-ROW-CHANGES()} method. The default value is {@code TRUE}.
        *
        * @return  {@code true} if the changes are merged on save on a field-by-field basis.
        */
       @Override
       public logical getMergeByField()
       {
          return new logical(mergeByField);
       }

       /**
        * Configures whether the changes on a field-by-field basis will be merged when saving changes
        * from a {@code DataSet} temp-table buffer to the associated data source using the
        * {@code SAVE-ROW-CHANGES()} method.
        *
        * @param   merge
        *          The new value for the {@code MERGE-BY-FIELD} attribute.
        */
       @Override
       public void setMergeByField(boolean merge)
       {
          mergeByField = merge;
       }

       /**
        * Configures whether the changes on a field-by-field basis will be merged when saving changes
        * from a {@code DataSet} temp-table buffer to the associated data source using the
        * {@code SAVE-ROW-CHANGES()} method.
        *
        * @param   merge
        *          The new value for the {@code MERGE-BY-FIELD} attribute.
        */
       @Override
       public void setMergeByField(logical merge)
       {
          if (merge == null || merge.isUnknown())
          {
             unableToAssignUnknown("MERGE-BY-FIELD", "DATA-SOURCE widget");
             return;
          }

          setMergeByField(merge.getValue());
       }

       /**
        * Gets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. The attribute is set after each {@code FILL} operation in a series of
        * {@code FILL} operations to retrieve data source rows in batches.
        *
        * @return  The current value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public rowid getNextRowid()
       {
          return getNextRowid(1L);
       }

       /**
        * Gets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. The attribute is set after each {@code FILL} operation in a series of
        * {@code FILL} operations to retrieve data source rows in batches.
        *
        * @param   buffNo
        *          the sequence number of a buffer in the list of buffers for the data-source object.
        *
        * @return  The current value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public rowid getNextRowid(NumberType buffNo)
       {
          if (buffNo == null || buffNo.isUnknown())
          {
             return new rowid();
          }

          return getNextRowid(buffNo.longValue());
       }

       /**
        * Gets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. The attribute is set after each {@code FILL} operation in a series of
        * {@code FILL} operations to retrieve data source rows in batches.
        *
        * @param   buffNo
        *          the sequence number of a buffer in the list of buffers for the data-source object.
        *
        * @return  The current value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public rowid getNextRowid(long buffNo)
       {
          if (buffNo <= 0 || buffNo > buffers.size())
          {
             return new rowid();
          }

          return new rowid(nextRowids.get(buffers.get((int) buffNo - 1)));
       }

       /**
        * Gets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. The attribute is set after each {@code FILL} operation in a series of
        * {@code FILL} operations to retrieve data source rows in batches.
        *
        * @param   buffName
        *          The name of a buffer in the list of buffers for the data-source object.
        *
        * @return  The current value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public rowid getNextRowid(String buffName)
       {
          for (BufferImpl buffer : buffers)
          {
             if (buffer.doGetName().equals(buffName))
             {
                return new rowid(nextRowids.get(buffer));
             }
          }

          return new rowid();
       }

       /**
        * Gets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. The attribute is set after each {@code FILL} operation in a series of
        * {@code FILL} operations to retrieve data source rows in batches.
        *
        * @param   buffName
        *          The name of a buffer in the list of buffers for the data-source object.
        *
        * @return  The current value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public rowid getNextRowid(character buffName)
       {
          if (buffName == null || buffName.isUnknown())
          {
             return new rowid();
          }

          return getNextRowid(buffName.getValue());
       }

       /**
        * Sets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. Typically, the value of this attribute is assigned to the
        * {@code RESTART-ROWID} attribute before each {@code FILL} operation.
        *
        * @param   row
        *          The new value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public void setNextRowid(rowid row)
       {
          setNextRowid(1, row);
       }

       /**
        * Sets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. Typically, the value of this attribute is assigned to the
        * {@code RESTART-ROWID} attribute before each {@code FILL} operation.
        *
        * @param   buffNo
        *          the sequence number of a buffer in the list of buffers for the data-source object.
        * @param   row
        *          The new value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public void setNextRowid(NumberType buffNo, rowid row)
       {
          if (buffNo != null && !buffNo.isUnknown())
          {
             setNextRowid(buffNo.longValue(), row);
          }
       }

       /**
        * Sets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. Typically, the value of this attribute is assigned to the
        * {@code RESTART-ROWID} attribute before each {@code FILL} operation.
        *
        * @param   buffNo
        *          the sequence number of a buffer in the list of buffers for the data-source object.
        * @param   row
        *          The new value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public void setNextRowid(long buffNo, rowid row)
       {
          if (buffNo > 0 && buffNo <= buffers.size())
          {
             nextRowids.put(buffers.get((int) buffNo - 1), row.getValue());
          }
       }

       /**
        * Sets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. Typically, the value of this attribute is assigned to the
        * {@code RESTART-ROWID} attribute before each {@code FILL} operation.
        *
        * @param   buffName
        *          The name of a buffer in the list of buffers for the data-source object.
        * @param   row
        *          The new value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public void setNextRowid(String buffName, rowid row)
       {
          for (BufferImpl buffer : buffers)
          {
             if (buffer.doGetName().equals(buffName))
             {
                nextRowids.put(buffer, row.getValue());
                return;
             }
          }
       }

       /**
        * Sets the {@code ROWID} of the data source row at which the next {@code FILL} operation
        * should start. Typically, the value of this attribute is assigned to the
        * {@code RESTART-ROWID} attribute before each {@code FILL} operation.
        *
        * @param   buffName
        *          The name of a buffer in the list of buffers for the data-source object.
        * @param   row
        *          The new value of code {@code NEXT-ROWID} as described above.
        */
       @Override
       public void setNextRowid(character buffName, rowid row)
       {
          if (buffName != null && !buffName.isUnknown())
          {
             setNextRowid(buffName.getValue(), row);
          }
       }

       /**
        * Obtain the number of source buffers in this data-source object.
        *
        * @return  the number of source buffers in this data-source object.
        */
       @Override
       public integer getNumSourceBuffers()
       {
          return new integer(_getNumSourceBuffers());
       }

       /**
        * Gets the current position (absolute row number) for the {@code FILL} query.
        *
        * @return  the current value of {@code RESTART-ROW} attribute.
        */
       @Override
       public integer getRestartRow()
       {
          return new integer(restartRow);
       }

       /**
        * Reposition the {@code FILL} query to an absolute row number.
        *
        * @param   newRow
        *          The new row number to reposition the query.
        */
       @Override
       public void setRestartRow(long newRow)
       {
          this.restartRow = newRow;
       }

       /**
        * Reposition the {@code FILL} query to an absolute row number.
        *
        * @param   newRow
        *          The new row number to reposition the query.
        */
       @Override
       public void setRestartRow(NumberType newRow)
       {
          if (newRow == null || newRow.isUnknown())
          {
             ErrorManager.recordOrShowError(4083, "RESTART-ROW", "DATA-SOURCE");
             // **Unable to assign UNKNOWN value to attribute RESTART-ROW on DATA-SOURCE widget. (4083)
             return;
          }
          setRestartRow(newRow.longValue());
       }

       /**
        * Obtain the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start.
        *
        * @return  the current value of {@code RESTART-ROWID} attribute.
        */
       @Override
       public rowid getRestartRowid()
       {
          return getRestartRowid(1L);
       }

       /**
        * Obtain the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start.
        *
        * @param   buffNum
        *          The index of the buffer.
        *
        * @return  the current value of {@code RESTART-ROWID} attribute.
        */
       @Override
       public rowid getRestartRowid(NumberType buffNum)
       {
          if (buffNum == null || buffNum.isUnknown())
          {
             return new rowid();
          }

          return getRestartRowid(buffNum.longValue());
       }

       /**
        * Obtain the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start.
        *
        * @param   buffNum
        *          The index of the buffer.
        *
        * @return  the current value of {@code RESTART-ROWID} attribute.
        */
       @Override
       public rowid getRestartRowid(long buffNum)
       {
          if (buffNum <= 0 || buffNum > buffers.size())
          {
             return new rowid();
          }

          return new rowid(restartRowids.get(buffers.get((int) buffNum - 1)));
       }

       /**
        * Obtain the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start.
        *
        * @param   buffName
        *          The name of the buffer.
        *
        * @return  the current value of {@code RESTART-ROWID} attribute.
        */
       @Override
       public rowid getRestartRowid(Text buffName)
       {
          if (buffName == null || buffName.isUnknown())
          {
             return new rowid();
          }

          return getRestartRowid(buffName.getValue());
       }

       /**
        * Obtain the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start.
        *
        * @param   buffName
        *          The name of the buffer.
        *
        * @return  the current value of {@code RESTART-ROWID} attribute.
        */
       @Override
       public rowid getRestartRowid(String buffName)
       {
          for (BufferImpl buffer : buffers)
          {
             if (buffer.doGetName().equals(buffName))
             {
                return new rowid(restartRowids.get(buffer));
             }
          }

          return new rowid();
       }

       /**
        * Configures the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start. Set this attribute before each {@code FILL} operation in a series of {@code FILL}
        * operations to retrieve data source rows in batches. Typically, this attribute is set by
        * assigning the value of the {@code NEXT-ROWID} attribute, after each {@code FILL} operation.
        *
        * @param   row
        *          The new value for {@code RESTART-ROWID} attribute as described above.
        */
       @Override
       public void setRestartRowid(rowid row)
       {
          setRestartRowid(1L, row);
       }

       /**
        * Configures the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start. Set this attribute before each {@code FILL} operation in a series of {@code FILL}
        * operations to retrieve data source rows in batches. Typically, this attribute is set by
        * assigning the value of the {@code NEXT-ROWID} attribute, after each {@code FILL} operation.
        *
        * @param   buffNum
        *          The index of the buffer.
        * @param   row
        *          The new value for {@code RESTART-ROWID} attribute as described above.
        */
       @Override
       public void setRestartRowid(NumberType buffNum, rowid row)
       {
          if (buffNum != null && !buffNum.isUnknown())
          {
             setRestartRowid(buffNum.longValue(), row);
          }
       }

       /**
        * Configures the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start. Set this attribute before each {@code FILL} operation in a series of {@code FILL}
        * operations to retrieve data source rows in batches. Typically, this attribute is set by
        * assigning the value of the {@code NEXT-ROWID} attribute, after each {@code FILL} operation.
        *
        * @param   buffNum
        *          The index of the buffer.
        * @param   row
        *          The new value for {@code RESTART-ROWID} attribute as described above.
        */
       @Override
       public void setRestartRowid(long buffNum, rowid row)
       {
          if (buffNum > 0 && buffNum <= buffers.size())
          {
             restartRowids.put(buffers.get((int) buffNum - 1), row.getValue());
          }
       }

       /**
        * Configures the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start. Set this attribute before each {@code FILL} operation in a series of {@code FILL}
        * operations to retrieve data source rows in batches. Typically, this attribute is set by
        * assigning the value of the {@code NEXT-ROWID} attribute, after each {@code FILL} operation.
        *
        * @param   buffName
        *          The name of the buffer.
        * @param   row
        *          The new value for {@code RESTART-ROWID} attribute as described above.
        */
       @Override
       public void setRestartRowid(Text buffName, rowid row)
       {
          if (buffName != null && !buffName.isUnknown())
          {
             setRestartRowid(buffName.getValue(), row);
          }
       }

       /**
        * Configures the {@code ROWID} of the data source row at which a {@code FILL} operation will
        * start. Set this attribute before each {@code FILL} operation in a series of {@code FILL}
        * operations to retrieve data source rows in batches. Typically, this attribute is set by
        * assigning the value of the {@code NEXT-ROWID} attribute, after each {@code FILL} operation.
        *
        * @param   buffName
        *          The name of the buffer.
        * @param   row
        *          The new value for {@code RESTART-ROWID} attribute as described above.
        */
       @Override
       public void setRestartRowid(String buffName, rowid row)
       {
          for (BufferImpl buffer : buffers)
          {
             if (buffer.doGetName().equals(buffName))
             {
                restartRowids.put(buffer, row.getValue());
                return;
             }
          }
       }

       /**
        * Obtain the {@code WHERE} clause used to find the database buffer identified by the
        * {@code buffName} in a data-source object.
        *
        * @return  the {@code WHERE} clause used to find the specified buffer.
        */
       @Override
       public character getSaveWhereString()
       {
          return getSaveWhereString(1L);
       }

       /**
        * Obtain the {@code WHERE} clause used to find the database buffer identified by the
        * {@code buffName} in a data-source object.
        *
        * @param   buffNum
        *          The buffer index.
        *
        * @return  the {@code WHERE} clause used to find the specified buffer.
        */
       @Override
       public character getSaveWhereString(NumberType buffNum)
       {
          if (buffNum == null || buffNum.isUnknown() || buffers.isEmpty())
          {
             // no errors this time!
             return new character();
          }

          return getSaveWhereString(buffNum.longValue());
       }

       /**
        * Obtain the {@code WHERE} clause used to find the database buffer identified by the
        * {@code buffName} in a data-source object.
        *
        * @param   buffNum
        *          The buffer index.
        *
        * @return  the {@code WHERE} clause used to find the specified buffer.
        */
       @Override
       public character getSaveWhereString(long buffNum)
       {
          if (buffers.isEmpty())
          {
             return new character();
          }
          if (buffNum < 1 || buffNum > buffers.size())
          {
             return new character();
          }

          BufferImpl buf = buffers.get((int) (buffNum - 1));

          if (datasetBuffer == null)
          {
             String[] ret = saveWhereStrings.get(buf);
             if (ret == null)
             {
                ErrorManager.recordOrShowError(11892, buffers.get(0).doGetName());
                // Attempt to reference dataset buffer of a data-source before ATTACH-DATA-SOURCE for data-source buffer <name>.
                return new character();
             }

             AbstractTempTable bt =
                (AbstractTempTable) datasetBuffer.buffer().getParentTable().getBeforeTableNative();
             return new character(String.join(bt._name(), ret));
          }

          return new character(getSaveWhereStringImpl(buf, true));
       }

       /**
        * Obtain the {@code WHERE} clause used to find the database buffer identified by the
        * {@code buffName} in a data-source object.
        *
        * @param   buffName
        *          The buffer name.
        *
        * @return  the {@code WHERE} clause used to find the specified buffer.
        */
       @Override
       public character getSaveWhereString(Text buffName)
       {
          if (buffName == null || buffName.isUnknown() || buffers.isEmpty())
          {
             // no errors this time!
             return new character();
          }

          return getSaveWhereString(buffName.getValue());
       }

       /**
        * Obtain the {@code WHERE} clause used to find the database buffer identified by the
        * {@code buffName} in a data-source object.
        *
        * @param   buffName
        *          The buffer name.
        *
        * @return  the {@code WHERE} clause used to find the specified buffer.
        */
       @Override
       public character getSaveWhereString(String buffName)
       {
          if (buffers.isEmpty())
          {
             return new character();
          }

          for (BufferImpl buffer : buffers)
          {
             if (buffer.doGetName().equalsIgnoreCase(buffName))
             {
                if (datasetBuffer == null)
                {
                   String[] ret = saveWhereStrings.get(buffer);
                   if (ret == null)
                   {
                      ErrorManager.recordOrShowError(11892, buffers.get(0).doGetName());
                      // Attempt to reference dataset buffer of a data-source before ATTACH-DATA-SOURCE for data-source buffer <name>.
                      return new character();
                   }

                   AbstractTempTable bt =
                      (AbstractTempTable) datasetBuffer.buffer().getParentTable().getBeforeTableNative();
                   return new character(String.join(bt._name(), ret));
                }

                return new character(getSaveWhereStringImpl(buffer, true));
             }
          }

          // no buffer found, no problem
          return new character();
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(Text val)
       {
          if (buffers.isEmpty())
          {
             return;
          }

          if (val == null || val.isUnknown())
          {
             unableToAssignUnknown("SAVE-WHERE-STRING", "DATA-SOURCE widget");
             return;
          }

          setSaveWhereString(1, val.getValue());
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   buffNum
        *          The buffer index.
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(NumberType buffNum, Text val)
       {
          if (buffers.isEmpty())
          {
             return;
          }

          if (val == null || val.isUnknown())
          {
             unableToAssignUnknown("SAVE-WHERE-STRING", "DATA-SOURCE widget");
             return;
          }

          if (buffNum == null || buffNum.isUnknown())
          {
             // no error here
             return;
          }

          setSaveWhereString(buffNum.longValue(), val.getValue());
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   buffNum
        *          The buffer index.
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(long buffNum, Text val)
       {
          if (buffers.isEmpty())
          {
             return;
          }

          if (val == null || val.isUnknown())
          {
             unableToAssignUnknown("SAVE-WHERE-STRING", "DATA-SOURCE widget");
             return;
          }

          setSaveWhereString(buffNum, val.getValue());
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   buffName
        *          The buffer name.
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(Text buffName, Text val)
       {
          if (buffers.isEmpty())
          {
             return;
          }

          if (val == null || val.isUnknown())
          {
             unableToAssignUnknown("SAVE-WHERE-STRING", "DATA-SOURCE widget");
             return;
          }
          if (buffName == null || buffName.isUnknown())
          {
             // no errors here
             return;
          }

          setSaveWhereString(buffName.getValue(), val.getValue());
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   buffName
        *          The buffer name.
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(String buffName, Text val)
       {
          if (buffers.isEmpty())
          {
             return;
          }

          if (val == null || val.isUnknown())
          {
             unableToAssignUnknown("SAVE-WHERE-STRING", "DATA-SOURCE widget");
             return;
          }

          setSaveWhereString(buffName, val.getValue());
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(String val)
       {
          setSaveWhereString(1, val);
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   buffNum
        *          The buffer index.
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(NumberType buffNum, String val)
       {
          if (buffNum == null || buffNum.isUnknown() || buffers.isEmpty())
          {
             return;
          }

          setSaveWhereString(buffNum.longValue(), val);
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   buffNum
        *          The buffer index.
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(long buffNum, String val)
       {
          if (buffers.isEmpty())
          {
             return;
          }

    //      if (datasetBuffer == null)
    //      {
    //         ErrorManager.recordOrShowError(11892, buffers.get(0).doGetName());
    //         // Attempt to reference dataset buffer of a data-source before ATTACH-DATA-SOURCE for data-source buffer <name>.
    //         return;
    //      }

          if (buffNum < 1 || buffNum > buffers.size())
          {
             return;
          }

          saveWhereStrings.put(buffers.get((int) (buffNum - 1)), new String[] { val });
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   buffName
        *          The buffer name.
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(Text buffName, String val)
       {
          if (buffName == null || buffName.isUnknown())
          {
             // no errors here
             return;
          }

          setSaveWhereString(buffName.getValue(), val);
       }

       /**
        * Configures the {@code SAVE-WHERE-STRING} attribute.
        *
        * @param   buffName
        *          The buffer name.
        * @param   val
        *          The new value for {@code SAVE-WHERE-STRING} attribute.
        */
       @Override
       public void setSaveWhereString(String buffName, String val)
       {
    //      if (datasetBuffer == null)
    //      {
    //         ErrorManager.recordOrShowError(11892, buffers.get(0).doGetName());
    //         // Attempt to reference dataset buffer of a data-source before ATTACH-DATA-SOURCE for data-source buffer <name>.
    //         return;
    //      }

          for (BufferImpl buffer : buffers)
          {
             if (buffer.doGetName().equalsIgnoreCase(buffName))
             {
                saveWhereStrings.put(buffer, new String[] { val });
                return;
             }
          }

          // buffer not found by name? no problem.
       }

       /**
        * Checks whether the to ignores modifications to the data currently in the data source when
        * saving changes from a {@code DataSet} temp-table buffer to the associated data source using
        * the {@link Buffer#saveRowChanges} method.
        * <p>
        * This attribute is supported only for backward compatibility. Use the
        * {@link #getPreferDataset} method instead.
        *
        * @return  the current value of {@code IGNORE-CURRENT-MODIFIED} attribute.
        */
       @Override
       public logical isIgnoreCurrentModified()
       {
          return getPreferDataset();
       }

       /**
        * Sets whether the to ignores modifications to the data currently in the data source when
        * saving changes from a {@code DataSet} temp-table buffer to the associated data source using
        * the {@link Buffer#saveRowChanges} method.
        * <p>
        * This attribute is supported only for backward compatibility. Use the
        * {@link #setPreferDataset} method instead.
        *
        * @param   on
        *          The new value of {@code IGNORE-CURRENT-MODIFIED} attribute.
        */
       @Override
       public void setIgnoreCurrentModified(boolean on)
       {
          setPreferDataset(on);
       }

       /**
        * Sets whether the to ignores modifications to the data currently in the data source when
        * saving changes from a {@code DataSet} temp-table buffer to the associated data source using
        * the {@link Buffer#saveRowChanges} method.
        * <p>
        * This attribute is supported only for backward compatibility. Use the
        * {@link #setPreferDataset} method instead.
        *
        * @param   on
        *          The new value of {@code IGNORE-CURRENT-MODIFIED} attribute.
        */
       @Override
       public void setIgnoreCurrentModified(logical on)
       {
          setPreferDataset(on);
       }

       /**
        * Adds a database buffer to a data-source object at run time.
        *
        * @param   buf
        *          The buffer to be added.
        *
        * @return  {@code true} on success.
        */
       public logical addSourceBuffer(Buffer buf)
       {
          return addSourceBufferImpl((BufferImpl) buf, null);
       }

       /**
        * Adds a database buffer to a dynamic data-source object at run time.
        *
        * @param   hBuf
        *          The buffer handle to be added.
        * @param   keyFields
        *          A list of one or more comma-separated database table fields that constitute a
        *          unique key
        *
        * @return  {@code true} on success.
        */
       @Override
       public logical addSourceBuffer(handle hBuf, Text keyFields)
       {
          if (!isAlterable())
          {
             ErrorManager.recordOrShowError(11973);
             // You may not change the buffers for a static data-source.
             return new logical(false);
          }

          if (hBuf.isUnknown() || !hBuf.isType(LegacyResource.BUFFER))
          {
             ErrorManager.recordOrShowError(11974);
             // The first argument to ADD-SOURCE-BUFFER must be a valid buffer handle.
             return new logical(false);
          }

          BufferImpl buffer = (BufferImpl) hBuf.getResource();
          RecordBuffer recBuf = buffer.buffer();
          String[] fields = null;
          if (keyFields == null || keyFields.isUnknown() || keyFields.getValue().isEmpty())
          {
             // search for first unique index and use its field list
             // NOTE: TO 2xCHECK: apparently, only the first index is used if UNIQUE, the subsequent are not
             //       'seen', even if UNIQUE
             int k = 0;
             TableMapper.LegacyIndexInfo index = TableMapper.getLegacyIndexInfo(recBuf, k);
             while (index != null)
             {
                if (index.isUnique())
                {
                   fields = index.getComponents();
                   break;
                }

                index = TableMapper.getLegacyIndexInfo(recBuf, ++k);
             }

             // if no unique index found, use the ROWID_KEY
             if (fields == null)
             {
                fields = new String[]{ROWID_KEY};
             }
          }
          else
          {
             fields = keyFields.getValue().split(",");
          }
          return addSourceBufferImpl(buffer, fields);
       }

       /**
        * Adds a database buffer to a dynamic data-source object at run time.
        *
        * @param   hBuf
        *          The buffer handle to be added.
        * @param   keyFields
        *          A list of one or more comma-separated database table fields that constitute a
        *          unique key
        *
        * @return  {@code true} on success.
        */
       @Override
       public logical addSourceBuffer(handle hBuf, String keyFields)
       {
          if (!isAlterable())
          {
             ErrorManager.recordOrShowError(11973);
             // You may not change the buffers for a static data-source.
             return new logical(false);
          }

          if (hBuf.isUnknown() || !hBuf.isType(LegacyResource.BUFFER))
          {
             ErrorManager.recordOrShowError(11974);
             // The first argument to ADD-SOURCE-BUFFER must be a valid buffer handle.
             return new logical(false);
          }

          String[] fields;
          if (keyFields == null || keyFields.isEmpty())
          {
             fields = new String[] { ROWID_KEY };
          }
          else
          {
             fields = keyFields.split(",");
          }
          return addSourceBufferImpl((BufferImpl) hBuf.getResource(), fields);
       }

       /**
        * Gets the handle to the source buffer in the data-source object at the specified index
        * position.
        *
        * @return  the handle to the specified buffer.
        */
       @Override
       public handle getSourceBuffer()
       {
          return getSourceBuffer(1);
       }

       /**
        * Gets the handle to the source buffer in the data-source object at the specified index
        * position.
        *
        * @return  the specified buffer.
        */
       public BufferImpl getSourceBufferNative()
       {
          return getSourceBufferNative(1);
       }

       /**
        * Gets the handle to the source buffer in the data-source object at the specified index
        * position.
        *
        * @param   bufIndex
        *          The index of the requested buffer.
        *
        * @return  the handle to the specified buffer.
        */
       @Override
       public handle getSourceBuffer(long bufIndex)
       {
          return new handle(getSourceBufferNative(bufIndex));
       }

       /**
        * Gets the handle to the source buffer in the data-source object at the specified index
        * position.
        *
        * @param   bufIndex
        *          The index of the requested buffer.
        *
        * @return  the specified buffer.
        */
       public BufferImpl getSourceBufferNative(long bufIndex)
       {
          if (bufIndex < 1 || bufIndex > buffers.size())
          {
             return null;
          }

          return buffers.get((int) (bufIndex - 1));
       }

       /**
        * Gets the handle to the source buffer in the data-source object at the specified index
        * position.
        *
        * @param   bufIndex
        *          The index of the requested buffer.
        *
        * @return  the handle to the specified buffer.
        */
       @Override
       public handle getSourceBuffer(NumberType bufIndex)
       {
          if (bufIndex == null || bufIndex.isUnknown())
          {
             return new handle();
          }

          return getSourceBuffer(bufIndex.longValue());
       }

       /**
        * Gets the handle to the source buffer attached/associated with this {@code DataSource}.
        *
        * @return  the handle to the specified buffer.
        */
       @Override
       public handle getDatasetBuffer()
       {
          return new handle(datasetBuffer);
       }

       /**
        * Delete the resource.
        *
        * @return   {@code true} if the resource was deleted.
        */
       @Override
       protected boolean resourceDelete()
       {
          if (hImplicitQuery != null && hImplicitQuery._isValid())
          {
             QueryWrapper query = (QueryWrapper) hImplicitQuery.getResource();
             query.delete();
          }

          if (!_dynamic() && !implicitDeletion())
          {
             // no error is shown
             return false;
          }

          this.deleted = true;

          // detach the datasource from all buffers
          for (BufferImpl buf : buffers)
          {
             buf.ref().clearParentDataSource();
          }

          return true;
       }

       /**
        * Get a source buffer by its name. If not found {@code null} is returned.
        *
        * @param   bufName
        *          The name of the desired source buffer.
        *
        * @return  The 0-base index of the buffer or -1 if no source buffer is found with provided
        *          name.
        */
       int getSourceBufferIndex(String bufName)
       {
          for (int i = 0; i < buffers.size(); i++)
          {
             if (buffers.get(i).doGetName().equalsIgnoreCase(bufName))
             {
                return i;
             }
          }

          return -1;
       }

       /**
        * Obtain the number of source buffers in this data-source object.
        *
        * @return  the number of source buffers in this data-source object.
        */
       int _getNumSourceBuffers()
       {
          return buffers.size();
       }

       /**
        * Attaches or detaches the dataset buffer to this source (sets the dataset buffer).
        *
        * @param   buffer
        *          The buffer to be attached. If {@code null} is passed, the currently attached buffer
        *          is detached.
        * @param   buf2dsrc
        *          The mapping between the dataset buffer and the fields of this object (these may
        *          come from different tables).
        */
       void attach(BufferImpl buffer, List<Pair<String, FieldReference>> buf2dsrc)
       {
          this._fillWhereString = null;

          if (buffer == null)
          {
             // this is a DETACH operation
             this.datasetBuffer = null;
             this.buf2dsrc = null;
             this.revBuf2src = null;
             this.saveWhereStrings.clear();
             return;
          }

          buffer = buffer.ref();

          if (this.datasetBuffer != null && datasetBuffer != buffer)
          {
             // detach the old buffer, if the same, let it be
             this.datasetBuffer.detachDataSourceImpl();
          }

          this.datasetBuffer = buffer;
          this.buf2dsrc = buf2dsrc;
       }

       /**
        * Get the list of {@code RESTART-ROWID}-s for this {@code DataSource}. The order is important,
        * it is the order in which the buffers were added at first.
        *
        * @return  the current list of {@code RESTART-ROWID}.
        */
       Long[] getRestartRowids()
       {
          Long[] ret = new Long[buffers.size()];
          for (int i = 0; i < buffers.size(); i++)
          {
             ret[i] = restartRowids.get(buffers.get(i));
          }

          return ret;
       }

       /**
        * Updates the FILL internal data, preparing it for the next batch, if any. Actually does the
        * following things:
        * <ul>
        *    <li>reset the list of {@code RESTART-ROWID}. All values will become unknown.</li>
        *    <li>updates the list of {@code NEXT-ROWID} values with the rowids currently selected
        *       in buffers. They were not processed yet so they are the 'NEXT' rows.</li>
        * </ul>
        * If the {@code FILL} operation failed, the {@code NEXT-ROWID} will be also set to unknown.
        *
        * @param   success
        *          {@code true} if the fill ended with success.
        */
       void updateFillRowidInfo(boolean success)
       {
          restartRowids.clear();

          if (success)
          {
             // TODO: improve performance by using a mutable value (?)
             for (BufferImpl buffer : buffers)
             {
                nextRowids.put(buffer, buffer._rowID());
             }
          }
          else
          {
             nextRowids.clear();
          }
       }

       /**
        * Computes the value of {@code FILL-WHERE-STRING} attribute at this moment. Depending on the active
        * relations from the dataset, this value may change.
        *
        * @return  the value of {@code FILL-WHERE-STRING} attribute.
        */
       private String computeFillWhereString()
       {
          if (datasetBuffer == null)
          {
             return null;
          }

          // check whether the buffer is filled as result as being the child buffer in an active relation
          DataSet dataSet = datasetBuffer._dataSet();
          if (!dataSet._areRelationsActive() ||
              dataSet.getRelations(datasetBuffer, false, true, true, true).isEmpty())
          {
             return null;
          }

          StringBuilder sb = new StringBuilder();
          String chBuff = datasetBuffer.doGetName();
          List<DataRelation> rels = datasetBuffer._dataSet().getRelations(datasetBuffer, false, true, false);

          if (rels.isEmpty())
          {
             // no constraints here
             return null;
          }

          for (DataRelation rel : rels)
          {
             String parBuff = rel.getParentBufferNative()._name();
             if (rel.isParentIdRelation().booleanValue())
             {
                // in PARENT-ID relations, the FILL WHERE is unknown
                return null;
             }

             String[] relFields = rel._getRelationFields().split(",");

             for (int k = 0; k < relFields.length; k += 2)
             {
                sb.append(sb.length() == 0 ? " WHERE " : "  and "); // extra space!
                String pf = relFields[k];
                String cf = relFields[k + 1];
                FieldReference fr = BufferImpl.getMappedField(this.datasetBuffer, cf);
                if (fr == null && cf.toLowerCase().startsWith((datasetBuffer.doGetName() + ".").toLowerCase()))
                {
                   // second chance, when the [cf] is qualified
                   cf = cf.substring(datasetBuffer.doGetName().length() + 1);
                   fr = BufferImpl.getMappedField(this.datasetBuffer, cf);
                }

                if (fr == null)
                {
                   // this is but a guess, usually there is only a single buffer in data source,
                   // but there might be several
                   String guess = buffers.get(0).doGetName();
                   ErrorManager.recordOrShowError(11883, guess, cf, chBuff);
                   // Unable to auto-generate a FILL query predicate for <buffer> if field <fieldname> in <buffer> is not mapped.

                   return null; // not an error, but the fillWhereString remains [null]
                }

                sb.append(fr).append("=");
                if (!pf.toLowerCase().startsWith((parBuff + ".").toLowerCase()))
                {
                   sb.append(parBuff).append(".");
                }
                sb.append(pf);
             }
          }

          // everything is fine
          return sb.toString();
       }

       /**
        * The implementation of assignation of QUERY attribute. Validates and link the query.
        *
        * @param   hQry
        *          The handle to the new query to be assigned.
        */
       private void setQueryImpl(handle hQry)
       {
          QueryWrapper qry = (QueryWrapper) hQry.getResource();

          // once a query is assigned on an empty data-source, all its buffers are copied.
          // if the query is set to null or other one, the buffers are kept.
          int bufCount = qry.numBuffers().intValue();
          if (buffers.isEmpty())
          {
             for (int k = 0; k < bufCount; k++)
             {
                if (!addSourceBufferImpl((BufferImpl) qry.getBufferByIndex(k + 1), null).booleanValue())
                {
                   return;
                }
             }
          }
          else
          {
             if (qry.numBuffers().intValue() != buffers.size())
             {
                ErrorManager.recordOrShowError(11867);
                // Query buffers and data-source buffers must match.
                ErrorManager.recordOrThrowError(3131, "QUERY", "", "DATA-SOURCE");
                // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
                return;
             }

             for (int k = 0; k < bufCount; k++)
             {
                if (buffers.get(k) != qry.getBufferByIndex(k + 1))
                {
                   ErrorManager.recordOrShowError(11867);
                   // Query buffers and data-source buffers must match.
                   ErrorManager.recordOrThrowError(3131, "QUERY", "", "DATA-SOURCE");
                   // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
                   return;
                }
             }
          }

          if (datasetBuffer != null)
          {
             qry.setSubstitutionBuffers(datasetBuffer);
          }

          hExplicitQuery = hQry;
       }

       /**
        * Actual implementation of {@code ADD-SOURCE-BUFFER} method. The {@link #addSourceBuffer}
        * method will call this method after preliminary validating the parameters.
        *
        * @param   buffer
        *          The buffer to add.
        * @param   keyFields
        *          The fields set that for a unique index.
        *
        * @return {@code true} on success.
        */
       private logical addSourceBufferImpl(BufferImpl buffer, String[] keyFields)
       {
          if (buffers.contains(buffer.ref()))
          {
             // this is strange, ADD action is not actually performed, but YES is returned
             return logical.TRUE;
          }

          if (keyFields == null || keyFields.length == 0)
          {
             DmoMeta dmoInfo = buffer.buffer().getDmoInfo();
             P2JIndex pIndex = dmoInfo.getPrimaryIndex(true);
             if (pIndex != null && pIndex.isUnique())
             {
                String[] keyFieldsBuf = new String[20]; // 16 fields per index max theoretical
                int fieldCount = 0;

                ArrayList<P2JIndexComponent> comps = pIndex.components();
                for (int i = 0; i < comps.size(); i++)
                {
                   keyFieldsBuf[fieldCount++] = comps.get(i).getName(P2JIndexComponent.LEGACY);
                }

                keyFields = new String[fieldCount];
                System.arraycopy(keyFieldsBuf, 0, keyFields, 0, fieldCount);
             }
          }

          if (keyFields == null)
          {
             // to avoid NPE if the next test is disabled
             keyFields = new String[0];
          }

    // NOTE: sometimes this is tested, sometimes not. What is the criterion?
    //      if (keyFields == null || keyFields.length == 0)
    //      {
    //         ErrorManager.recordOrShowError(11895, buffer.doGetName());
    //         // Unable to get SAVE-WHERE-STRING because no KEYS phrase given in DATA-SOURCE definition and no unique index found in <buffer-name>.
    //         return new logical(false);
    //      }

          for (int i = 0; i < keyFields.length; i++)
          {
             String keyField = keyFields[i];
             if (ROWID_KEY.equalsIgnoreCase(keyField))
             {
                continue;
             }

             if (!buffer.hasFieldEx(keyField))
             {
                ErrorManager.recordOrShowError(11975, keyField, buffer.doGetName());
                // ADD-SOURCE-BUFFER key name <name> must be a field in buffer <name>.
                return logical.FALSE;
             }

             int dotIdx = keyField.indexOf('.');
             if (dotIdx != -1)
             {
                keyFields[i] = keyField.substring(dotIdx + 1);
             }
          }

          // if everything was fine, save and return [true]
          BufferImpl bufRef = buffer.ref();
          boolean ok = bufRef.keys(this, keyFields);
          if (ok)
          {
             buffers.add(bufRef);
          }
          return logical.of(ok);
       }

       /**
        * Checks whether this object can be altered (added buffers and relations). Normally, the
        * static defined {@code DataSource} s are not alterable, but they can 'altered' by their
        * {@code Builder} while the object is being constructed.
        *
        * @return  {@code true} if new buffers or relations can be added.
        */
       private boolean isAlterable()
       {
          return _dynamic() || builder != null;
       }

       /**
        * Get the current value of {@code SAVE-WHERE-STRING} for a specified {@code Buffer}. If this
        * is the first time the attribute is accessed, its value is (lazily) computed.
        *
        * @param   buf
        *          A {@code buffer} to obtain its save where string.
        * @param   useBefore
        *          Use the before-table name to construct the returned SAVE-WHERE-STRING (used for SAVE
        *          operation), as opposed to the after-table name (used for DATA-SOURCE-ROWID attribute).
        *
        * @return  Get the current value of {@code SAVE-WHERE-STRING} for {@code buf}.
        */
       String getSaveWhereStringImpl(Buffer buf, boolean useBefore)
       {
          BufferImpl bufImpl = (BufferImpl) buf;
          String bufName = bufImpl.doGetName();

          List<String> uIndexComp = bufImpl.keysList();
          if (uIndexComp == null || uIndexComp.isEmpty())
          {
             boolean found = false;
             // check if the buf2dsrc has a "<field>,rowid(<table>)" pair
             for (Pair<String, FieldReference> pair : buf2dsrc)
             {
                FieldReference dsrcField = pair.getValue();
                if (dsrcField.getName().equals(Session.PK))
                {
                   // add these to the index component
                   uIndexComp = new ArrayList<>();
                   uIndexComp.add(Session.PK);
                   found = true;
                }
             }

             if (!found)
             {
                ErrorManager.recordOrShowError(11895, bufName);
                // Unable to get SAVE-WHERE-STRING because no KEYS phrase given in DATA-SOURCE definition and no unique index found in <buffer-name>.
                return null;
             }
          }

          String[] saveWhere = saveWhereStrings.get(buf);
          if (saveWhere != null)
          {
             AbstractTempTable tempTable =
                (AbstractTempTable) TempTableBuilder.asTempTable((Temporary) datasetBuffer);
             TempTable bt = tempTable.getBeforeTableNative();
             if (bt == null)
             {
                if (!tempTable._isTrackingChanges())
                {
                   ErrorManager.recordOrShowError(11893);
                   // SAVE-WHERE-STRING cannot be generated when there is no BEFORE-TABLE--define one or use TRACKING-CHANGES.
                }
                // return [null] / unknown either way
                return null;
             }

             if (useBefore)
             {
                tempTable = (AbstractTempTable) tempTable.getBeforeTableNative();
             }
             else
             {
                datasetBuffer.buffer().getParentTable()._name();
             }

             return String.join(tempTable._name(), saveWhere);
          }

          AbstractTempTable tempTable =
             (AbstractTempTable) TempTableBuilder.asTempTable((Temporary) datasetBuffer);
          TempTable bt = tempTable.getBeforeTableNative();
          if (bt == null)
          {
             if (!tempTable._isTrackingChanges())
             {
                ErrorManager.recordOrShowError(11893);
                // SAVE-WHERE-STRING cannot be generated when there is no BEFORE-TABLE--define one or use TRACKING-CHANGES.
             }
             // return [null] / unknown either way
             return null;
          }

          if (revBuf2src == null)
          {
             if (buf2dsrc == null)
             {
                return null;
             }
             revBuf2src = new CaseInsensitiveHashMap<>();
             for (Pair<String, FieldReference> pair : buf2dsrc)
             {
                revBuf2src.put(pair.getValue().toString(), pair.getKey());
             }
          }

          String btName = bt.name().toStringMessage();
          List<String> tokens = new ArrayList<>(6);
          StringBuilder sb = new StringBuilder();

          if (uIndexComp.size() == 1 && uIndexComp.get(0).equalsIgnoreCase(ROWID_KEY))
          {
             String btField = revBuf2src.get(bufName + ".recid");
             if (btField == null)
             {
                ErrorManager.recordOrShowError(11894, bufName, btName);
                // KEYS phrase for data-source buffer %1 contains ROWID, but no field in %2 was mapped to the rowid during ATTACH-DATA-SOURCE
                return null;
             }

             int k = btField.indexOf('.');
             if (k != -1)
             {
                btField = btField.substring(k + 1);
             }
             sb.append("WHERE ROWID(").append(bufName).append(") = ");
             tokens.add(sb.toString());
             sb = new StringBuilder();
             sb.append(".").append(btField);
          }
          else
          {
             // auto generate the SAVE-WHERE-STRING value on first read access
             for (String comp : uIndexComp)
             {
                String srcFld = bufName + "." + comp;
                String btField = revBuf2src.get(srcFld);
                if (btField == null)
                {
                   ErrorManager.recordOrShowError(11896, bufName, comp, btName);
                   // Unable to generate a save predicate for <table-name> if field <field-name> is not mapped in <temp-table-name>.
                   return null;
                }
                int k = btField.indexOf('.');
                if (k != -1)
                {
                   btField = btField.substring(k + 1);
                }

                sb.append(sb.length() == 0 ? "WHERE " : " and ");
                if (comp.equals(Session.PK))
                {
                   srcFld = "rowid(" + bufName + ")";
                }
                sb.append(srcFld).append(" = ");
                tokens.add(sb.toString());
                sb = new StringBuilder();
                sb.append('.').append(btField);
             }
          }

          tokens.add(sb.toString());
          String[] tokArray = new String[tokens.size()];
          tokens.toArray(tokArray);
          saveWhereStrings.put(buf, tokArray);

          if (useBefore)
          {
             tempTable = (AbstractTempTable) tempTable.getBeforeTableNative();
          }
          else
          {
             datasetBuffer.buffer().getParentTable()._name();
          }

          return String.join(tempTable._name(), tokArray);
       }

       /**
        * Test whether at current moment this data-source has an explicit query configured.
        *
        * @return  {@code true} if a custom query was set for this data-source.
        */
       boolean hasExplicitQuery()
       {
          return hExplicitQuery != null && hExplicitQuery._isValid();
       }

       /**
        * Creates and prepares the implicit query. If a query already exists, does nothing.
        *
        * @return  {@code true} if a new query was created and prepared.
        */
       private boolean createImplicitQuery()
       {
          if (buffers.isEmpty() || hImplicitQuery._isValid())
          {
             return false;
          }
          BufferImpl srcBuffer = buffers.get(0);

          // create an implicit query
          hImplicitQuery = new handle();
          QueryWrapper.createQuery(hImplicitQuery);
          QueryWrapper query = (QueryWrapper) hImplicitQuery.getResource();
          query.setImplicit();

          StringBuilder sb = new StringBuilder("FOR EACH ");
          sb.append(srcBuffer.name().toStringMessage());
          query.addBuffer(srcBuffer);

          String whereFillStr = getFillWhereString().toJavaType();
          if (whereFillStr != null)
          {
             sb.append(" ");
             sb.append(whereFillStr);
          }

          DataSet dataSet = null;
          if (datasetBuffer != null)
          {
             query.setSubstitutionBuffers(datasetBuffer);
             dataSet = (DataSet) datasetBuffer.dataSet().getResource();
          }

          if (!query.prepare(sb.toString()).getValue())
          {
             ErrorManager.recordOrShowError(11981);
             // Failed to auto-prepare an automatic fill query. (11981)
             // NOTE: OE crashes after printing the above message, should FWD do the same?

             if (dataSet != null)
             {
                dataSet.error(true);
             }

             return false;
          }

          return true;
       }

       /**
        * Gets a string representation of the {@code DataSource} object which contains the name, the buffers list
        * and some details on the active queries, inlcluding their {@code FILL-WHERE-STRING} and
        * {@code PREPARE-STRING}. This method is a usual debug tool.
        *
        * @return  a string representation of the {@code DataSource} object.
        */
       @Override
       public String toString()
       {
          StringBuilder sb = new StringBuilder("DataSource{");
          sb.append(datasetBuffer == null ? "no DS buffer" : datasetBuffer.doGetName());

          sb.append(", buffers=");
          buffers.forEach(b -> sb.append(b.doGetName()).append(", "));
          sb.append("} FILL: implicit: ");

          if (!hImplicitQuery._isValid())
          {
             sb.append("none");
          }
          else
          {
             QueryWrapper q = (QueryWrapper) hImplicitQuery.getResource();

             sb.append("'").append(_fillWhereString).append("' / '");
             sb.append(q.prepareString().toStringMessage()).append("'");
          }

          if (hExplicitQuery == null || hExplicitQuery._isValid())
          {
             sb.append(", explicit: none");
          }
          else
          {
             QueryWrapper q = (QueryWrapper) hExplicitQuery.getResource();

             sb.append(", explicit: '").append(q.prepareString().toStringMessage()).append("'");
          }
          sb.append("}");
          return sb.toString();
       }

       /**
        * Builder class that helps construct a {@code DataSource} object statically, that is, using
        * the {@code DEFINE DataSource} statement. It exposes a set of chaining methods that take each
        * option parameters and set them into the {@code DataSource} being constructed. The chaining
        * must finish with {@link #generate()} to obtain the final {@code DataSource} object. Before
        * returning, the {@code generate()} will do the final checks before returning.
        */
       public class Builder
       {
          /**
           * Configures the a buffer for the {@code DataSource}. Corresponds to {@code FOR} option
           * from {@code DEFINE DATASOURCE} statement. The syntax allows to specify none or more
           * fields.
           *
           * @param   buf
           *          The buffer for the {@code DataSource} to be constructed.
           * @param   fields
           *          The set of fields (if any) that that form a unique key.
           *
           * @return  A reference of this object in order to help chaining.
           */
          public Builder addBuffer(Buffer buf, String... fields)
          {
             DataSource.this.addSourceBufferImpl((BufferImpl) buf, fields);
             return this;
          }

          /**
           * Corresponds to {@code FOR} option from {@code DEFINE DATASOURCE} statement. This
           * particular syntax allows to specify the ROWID as the key for record lookup.
           *
           * @param   buf
           *          The buffer for the {@code DataSource} to be constructed.
           * @param   rowid
           *          This boolean will be ignored. It is used only to distinct the overloaded method.
           *
           * @return  A reference of this object in order to help chaining.
           */
          public Builder addBuffer(Buffer buf, boolean rowid)
          {
             DataSource.this.addSourceBufferImpl((BufferImpl) buf, new String[] { ROWID_KEY.toUpperCase() });
             return this;
          }

          /**
           * Sets the query associated with the given resource.
           *
           * @param   query
           *          The query to associate.
           *
           * @return  A reference of this object in order to help chaining.
           */
          public Builder setQuery(QueryWrapper query)
          {
             DataSource.this.setQueryAsHandle(query);
             return this;
          }

          /**
           * Finalizes the build of the {@code DataSource}.
           *
           * @return  The {@code DataSource} that have been constructed.
           */
          public DataSource generate()
          {
             // prevent further options to be set
             builder = null;

             // end chain by returning the final object
             return DataSource.this;
          }
       }

       /**
        * A reference to a field of a buffer. This is the 'lite' version of the {@link FieldReference}
        * in the sense that it puts together the legacy field name to the reference of its buffer.
        * This is a non-mutable object.
        */
       public static class FieldReference
       {
          /** The buffer whose field this object represents. */
          private final BufferImpl buffer;

          /** The legacy name of the field. */
          private final String name;

          /** The EXTENT attribute of this field, if any, otherwise 0. */
          private final int extent;

          /** The EXTENT index of this field, if any, otherwise 0. {@code extent} must also be {@code > 0}. */
          private final int index;

          /** The field's resolved type. */
          private final Class<? extends BaseDataType> type;

          /**
           * The field's resolved offset in the record's datum, for direct access.  This will be used only when
           * the buffer fill determines that the read and write can be done directly in the datum.  Otherwise,
           * this field can be null.
           */
          private final Integer offset;

          /**
           * The constructor initialize both non mutable fields.
           *
           * @param   buffer
           *          The buffer whose field this object represents.
           * @param   name
           *          The legacy name of the field.
           * @param   type
           *          The field's datatype.
           * @param   offset
           *          The field offset in the record datum.
           */
          public FieldReference(BufferImpl buffer, String name, Class<? extends BaseDataType> type, Integer offset)
          {
             this.buffer = buffer;
             this.type = type;
             this.offset = offset;

             if (name.contains("."))
             {
                String buffName = buffer.doGetName().toLowerCase();
                String prefix = buffName + ".";
                if (name.toLowerCase().startsWith(prefix))
                {
                   name = name.substring(prefix.length());
                }
                else
                {
                   throw new RuntimeException(
                         "Invalid buffer (" + buffName + ") for field reference: " + name);
                }
             }

             if ("recid".equalsIgnoreCase(name))
             {
                this.name = name;
                extent = 0;
                index = 0;
             }
             else
             {
                int lb = name.indexOf('[');
                int rb = name.indexOf(']');
                int idx = 0;
                if (lb > 0 && rb > 0)
                {
                   try
                   {
                      idx = Integer.parseInt(name.substring(lb + 1, rb));
                      name = name.substring(0, lb);
                   }
                   catch (NumberFormatException e)
                   {
                      // do nothing, will throw exception later
                   }
                }

                // just resolve the extent, and not the BufferFieldImpl entire resource!
                RecordBuffer recBuf = buffer.buffer();
                Property property = recBuf.getDmoInfo().byLegacyName(name);

                if (property.extent == 0)
                {
                   this.extent = 0;
                   this.index = 0;
                }
                else
                {
                   this.extent = property.extent;
                   this.index = idx;
                }
                this.name = name;
             }
          }

          /**
           * Get the field's {@link #offset} in the record datum.
           *
           * @return   See above.
           */
          public Integer getOffset()
          {
             return offset;
          }

          /**
           * Get the field's data {@link #type}.
           *
           * @return   See above.
           */
          public Class<? extends BaseDataType> getType()
          {
             return type;
          }

          /**
           * Obtains the buffer whose field this object represents.
           *
           * @return  The buffer whose field thei object represents.
           */
          public BufferImpl getBuffer()
          {
             return buffer;
          }

          /**
           * Obtains the legacy name of the field.
           *
           * @return  The legacy name of the field.
           */
          public String getName()
          {
             return name;
          }

          /**
           * Obtains the extent of this field.
           *
           * @return  The EXTENT attribute of this field, if any, otherwise 0.
           */
          public int getExtent()
          {
             return extent;
          }

          /**
           * Obtains the index (1-base) of extent field. If this is not an extent, 0 is returned.
           *
           * @return  the index (1-base) of extent field or 0 if this is not an extent.
           */
          public int getIndex()
          {
             return index;
          }

          /**
          * Gets a string representation of the object which contains the name buffer and the name of
          * the field. This method is a usual debug tool.
          *
          * @return  a string representation of the object.
          */
          @Override
          public String toString()
          {
             StringBuilder sb = new StringBuilder(buffer.doGetName()).append(".").append(name);
             if (index > 0)
             {
                sb.append("[").append(index).append("]");
             }
             return sb.toString();
          }
       }
    }