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