TypeManager.java

/*
** Module   : TypeManager.java
** Abstract : Provides converter objects for marshaling between native/BDT to SQL values.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM  20191101 Created initial version. Basic BDT support in both direction.
**     CA  20200622 Small performance improvement - use IdentityHashMap if the key is java.lang.Class.
** 002 CA  20201214 Fixed setByteArrayParameter when the value is null - the type is VARBINARY.
**     CA  20210305 The object and handle fields have a Long representation at the SQL table field.
**     OM  20220813 Attempt to patch prepareUrl(). Probably needs to be dialect aware.
**     OM  20220927 Made prepareUrl() dialect-aware (syntax + specific ports).
**     OM  20221117 Some properties (datetime-tz) may take a different number of positional parameters when
**                  used in insert/update or where predicate.
** 003 CA  20240331 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime, so logical
**                  type checks must include sub-classes, too.
** 004 CA  20241107 Added character constant support.
** 005 ICP 20250123 Added support for integer and int64 constants.
*/

/*
** 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.orm.types;

import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import java.io.*;
import java.math.*;
import java.sql.*;
import java.sql.Date;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.regex.*;

/**
 * This manager provides objects that handle two-way conversion between SQL values to BDT.
 */
public class TypeManager
{
   /**
    * The registry with all known converters. Since they implement the both the {@code Converter}
    * and {@code ParameterSetter}, they will act in both direction, converting pieces of data
    * coming from SQL to BDT values and vice-versa.
    */
   private static final Map<Class<? extends BaseDataType>, DataHandler> typeHelpers = new IdentityHashMap<>();
   
   /**
    * The registry with all known parameter type handlers. The {@code ParameterSetter}
    * implementations allow the converted value to be set as parameter to a provided
    * {@code PreparedStatement}. 
    */
   private static final Map<Class<?>, ParameterSetter> paramSetters = new IdentityHashMap<>();
   
   /** Dialect-specific functions to create a {@link Blob} instance. */
   private static final Map<String, BiFunction<Connection, byte[], Blob>> blobCreators = new ConcurrentHashMap<>();
   
   /** Dialect-specific functions to create a {@link Clob} instance. */
   private static final Map<String, BiFunction<Connection, String, Clob>> clobCreators = new ConcurrentHashMap<>();
   
   static
   {
      Int64Type int64Type = new Int64Type();
      DateType dateType = new DateType(int64Type);
      
      // FWD data types
      typeHelpers.put(blob.class, new BlobType());
      typeHelpers.put(character.class, new CharacterType());
      typeHelpers.put(clob.class, new ClobType());
      typeHelpers.put(comhandle.class, new ComhandleType());
      typeHelpers.put(datetimetz.class, new DatetimetzType(dateType, int64Type));
      typeHelpers.put(datetime.class, new DatetimeType(dateType, int64Type));
      typeHelpers.put(date.class, dateType);
      typeHelpers.put(decimal.class, new DecimalType());
      typeHelpers.put(handle.class, new HandleType(int64Type));
      typeHelpers.put(int64.class, int64Type);
      typeHelpers.put(integer.class, new IntegerType(int64Type));
      typeHelpers.put(logical.class, new LogicalType());
      typeHelpers.put(object.class, new ObjectType(int64Type));
      typeHelpers.put(raw.class, new RawType());
      typeHelpers.put(recid.class, new RecidType(int64Type));
      typeHelpers.put(rowid.class, new RowidType(int64Type));
      // BDT constants
      typeHelpers.put(logical.logicalConstant.class, new LogicalType());
      typeHelpers.put(character.characterConstant.class, new CharacterType());
      typeHelpers.put(integer.integerConstant.class, new IntegerType(int64Type));
      typeHelpers.put(int64.int64Constant.class, int64Type);
      // dynamic FWD classes
      typeHelpers.put(Text.class, new CharacterType());
      typeHelpers.put(NumberType.class, new NumericType());
      
      // setters for BDT parameters in queries
      paramSetters.put(blob.class, TypeManager::setFwdBlobParameter);
      paramSetters.put(character.class, TypeManager::setFwdCharacterParameter);
      paramSetters.put(clob.class, TypeManager::setFwdClobParameter);
      paramSetters.put(comhandle.class, TypeManager::setFwdComhandleParameter);
      paramSetters.put(datetimetz.class, TypeManager::setFwdDatetimetzParameter);
      paramSetters.put(datetime.class, TypeManager::setFwdDatetimeParameter);
      paramSetters.put(date.class, TypeManager::setFwdDateParameter);
      paramSetters.put(decimal.class, TypeManager::setFwdDecimalParameter);
      paramSetters.put(handle.class, TypeManager::setFwdHandleParameter);
      paramSetters.put(int64.class, TypeManager::setFwdInt64Parameter);
      paramSetters.put(integer.class, TypeManager::setFwdIntegerParameter);
      paramSetters.put(logical.class, TypeManager::setFwdLogicalParameter);
      paramSetters.put(object.class, TypeManager::setFwdObjectParameter);
      paramSetters.put(raw.class, TypeManager::setFwdRawParameter);
      paramSetters.put(recid.class, TypeManager::setFwdRecidParameter);
      paramSetters.put(rowid.class, TypeManager::setFwdRowidParameter);
      // BDT constants
      paramSetters.put(logical.logicalConstant.class, TypeManager::setFwdLogicalParameter);
      paramSetters.put(character.characterConstant.class, TypeManager::setFwdCharacterParameter);
      paramSetters.put(integer.integerConstant.class, TypeManager::setFwdIntegerParameter);
      paramSetters.put(int64.int64Constant.class, TypeManager::setFwdInt64Parameter);

      // add Java native data types:
      paramSetters.put(Blob.class, TypeManager::setBlobParameter);
      paramSetters.put(Clob.class, TypeManager::setClobParameter);
      paramSetters.put(Integer.class, TypeManager::setIntegerParameter);
      paramSetters.put(Long.class, TypeManager::setLongParameter);
      paramSetters.put(String.class, TypeManager::setStringParameter);
      paramSetters.put(Boolean.class, TypeManager::setBooleanParameter);
      paramSetters.put(Double.class, TypeManager::setDoubleParameter);
      paramSetters.put(Float.class, TypeManager::setFloatParameter);
      
      // and other support types:
      paramSetters.put(byte[].class, TypeManager::setByteArrayParameter);
      paramSetters.put(BigDecimal.class, TypeManager::setBigDecimalParameter);
      paramSetters.put(java.util.Date.class, TypeManager::setUtilDateParameter);
      paramSetters.put(java.sql.Timestamp.class, TypeManager::setTimestampParameter);
      paramSetters.put(java.sql.Date.class, TypeManager::setSqlDateParameter);
      paramSetters.put(java.time.OffsetDateTime.class, TypeManager::setOffsetDateTimeParameter);
   }
   
   /**
    * Gets a parameter handler for a specific type. The returned {@link ParameterSetter} expects a
    * {@link BaseDataType} of {@code type} type to be passed as the 3rd argument to its 
    * {@code setParameter} when the method is invoked.  
    * 
    * @param   type
    *          The type of the argument that needs to be set in a {@code PreparedStatement}.
    * 
    * @return  An object which can set a parameter of a specified type in a query.
    */
   public static ParameterSetter getTypeHandler(Class<?> type)
   {
      return paramSetters.get(type);
   }
   
   /**
    * Gets a converter for a specific target {@code BaseDataType}.
    * 
    * @param   type
    *          The class to which the conversion needs to be performed.
    * 
    * @return  A {@code Converter} that will handle conversion to the requested type.
    */
   public static DataHandler getDataHandler(Class<? extends BaseDataType> type)
   {
      return typeHelpers.get(type);
   }
   
   /**
    * Add a dialect-specific function to create a {@link Blob} instance.
    * 
    * @param    url
    *           The JDBC URL for the database connection.
    * @param    blobCreator
    *           The dialect-specific function to create a {@link Blob} instance.
    */
   public static void addBlobCreator(String url, BiFunction<Connection, byte[], Blob> blobCreator)
   {
      blobCreators.putIfAbsent(prepareUrl(url), blobCreator);
   }
   
   /**
    * Add a dialect-specific function to create a {@link Clob} instance.
    * 
    * @param    url
    *           The JDBC URL for the database connection.
    * @param    clobCreator
    *           The dialect-specific function to create a {@link Clob} instance.
    */
   public static void addClobCreator(String url, BiFunction<Connection, String, Clob> clobCreator)
   {
      clobCreators.putIfAbsent(prepareUrl(url), clobCreator);
   }
   
   /**
    * Sets a value assumed of type {@code Integer} to a parameter in a {@code PreparedStatement}.
    * 
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code Integer}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setIntegerParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.INTEGER);
      }
      else
      {
         stmt.setInt(index, (Integer) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code Long} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code Long}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setLongParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.BIGINT);
      }
      else
      {
         stmt.setLong(index, (Long) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code String} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code String}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setStringParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.VARCHAR);
      }
      else
      {
         stmt.setString(index, (String) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code Boolean} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code Boolean}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setBooleanParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.BOOLEAN);
      }
      else
      {
         stmt.setBoolean(index, (Boolean) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code Double} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code Double}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setDoubleParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.DOUBLE);
      }
      else
      {
         stmt.setDouble(index, (Double) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code Float} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code Float}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setFloatParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.FLOAT);
      }
      else
      {
         stmt.setFloat(index, (Float) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code Blob} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code Blob}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setBlobParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.BLOB);
      }
      else
      {
         Connection conn = stmt.getConnection();
         String url = prepareUrl(conn.getMetaData().getURL());
         BiFunction<Connection, byte[], Blob> blobCreator = blobCreators.get(url);
         Blob blob = blobCreator.apply(conn, (byte[]) val);
         stmt.setBlob(index, blob);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code Clob} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code Clob}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setClobParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.CLOB);
      }
      else
      {
         Connection conn = stmt.getConnection();
         String url = prepareUrl(conn.getMetaData().getURL());
         BiFunction<Connection, String, Clob> clobCreator = clobCreators.get(url);
         Clob blob = clobCreator.apply(conn, (String) val);
         stmt.setClob(index, blob);
      }
      
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code java.util.Date} to a parameter in a
    * {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code java.util.Date}.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setDateParameter(PreparedStatement stmt, int index, Object val)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.DATE);
      }
      else
      {
         stmt.setDate(index, new java.sql.Date(((java.util.Date) val).getTime()));
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code BigDecimal} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code BigDecimal}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setBigDecimalParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.DECIMAL);
      }
      else
      {
         stmt.setBigDecimal(index, (BigDecimal) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code byte} array to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code byte} array.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setByteArrayParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.VARBINARY);
      }
      else
      {
         stmt.setBytes(index, (byte[]) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code java.util.Date} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code java.util.Date}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setUtilDateParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.DATE);
      }
      else
      {
         stmt.setDate(index, new java.sql.Date(((java.util.Date) val).getTime()));
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code Timestamp} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code Timestamp}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setTimestampParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.TIMESTAMP);
      }
      else
      {
         stmt.setTimestamp(index, (Timestamp) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code java.sql.Date} to a parameter in a {@code PreparedStatement}.
    *
    * @param   stmt
    *          The {@code PreparedStatement} to receive the parameter.
    * @param   index
    *          The position of the parameter in the statement.
    * @param   val
    *          The value to be set, assumed to be of type {@code java.sql.Date}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered.
    */
   static int setSqlDateParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.DATE);
      }
      else
      {
         stmt.setDate(index, (java.sql.Date) val);
      }
      return 1;
   }
   
   /**
    * Sets a value assumed of type {@code OffsetDateTime} to a parameter in a {@code PreparedStatement}.
    * <p> 
    * <strong>Note:</strong><br>
    * This implementation of {@code DataHandler} is the only one that fills in two parameters in a
    * prepared statement.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameters.
    * @param   val
    *          The value of the parameter. Must be a {@code OffsetDateTime}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate.
    *
    * @return  2 if the parameter is used to update / insert a new record (the number of position used by a
    *          datetimetz value) or 1 if the parameter is used in where predicate (all supported dialects
    *          handle time-zoned instants natively, without the need of offset).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   static int setOffsetDateTimeParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val == null)
      {
         stmt.setNull(index, Types.TIMESTAMP_WITH_TIMEZONE);
         if (forUpdate)
         {
            stmt.setNull(index + 1, Types.INTEGER);
         }
      }
      else
      {
         stmt.setObject(index, val, Types.TIMESTAMP_WITH_TIMEZONE);
         if (forUpdate)
         {
            stmt.setObject(index + 1,
                           ((OffsetDateTime) val).getOffset().getTotalSeconds() / 60,
                           Types.INTEGER);
         }
      }
      return forUpdate ? 2 : 1;
   }
   
   /**
    * Extracts the data from a {@code raw} object and use it to initialize a parameter of type
    * raw in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code raw}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdRawParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         raw asBDT = (raw) val;
         if (!asBDT.isUnknown())
         {
            stmt.setBytes(index, asBDT.getByteArray());
            return 1;
         }
      }
   
      stmt.setNull(index, Types.VARBINARY);
      return 1;
   }
   
   /**
    * Extracts the data from an {@code object} object and use it to initialize a parameter of type
    * object in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code object}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdObjectParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         object<? extends _BaseObject_> asBDT = (object<? extends _BaseObject_>) val;
         if (!asBDT.isUnknown())
         {
            stmt.setLong(index, object.resourceId(asBDT));
            return 1;
         }
      }
      
      stmt.setNull(index, Types.BIGINT);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code logical} object and use it to initialize a parameter of type
    * logical in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code logical}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdLogicalParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         logical asBDT = (logical) val;
         if (!asBDT.isUnknown())
         {
            stmt.setBoolean(index, asBDT.toJavaType());
            return 1;
         }
      }
      
      stmt.setNull(index, Types.BOOLEAN);
      return 1;
   }
   
   /**
    * Extracts the data from an {@code integer} object and use it to initialize a parameter of type
    * integer in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code integer}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdIntegerParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         integer asBDT = (integer) val;
         if (!asBDT.isUnknown())
         {
            stmt.setInt(index, asBDT.intValue());
            return 1;
         }
      }
      
      stmt.setNull(index, Types.INTEGER);
      return 1;
   }
   
   /**
    * Extracts the data from an {@code int64} object and use it to initialize a parameter of type
    * int64 in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code int64}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdInt64Parameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         int64 asBDT = (int64) val;
         if (!asBDT.isUnknown())
         {
            stmt.setLong(index, asBDT.longValue());
            return 1;
         }
      }
      
      stmt.setNull(index, Types.BIGINT);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code handle} object and use it to initialize a parameter of type
    * handle in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code handle}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdHandleParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         handle asBDT = (handle) val;
         if (!asBDT.isUnknown())
         {
            stmt.setLong(index, asBDT.getResourceId());
            return 1;
         }
      }
      
      stmt.setNull(index, Types.BIGINT);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code decimal} object and use it to initialize a parameter of type
    * decimal in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code decimal}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdDecimalParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         decimal asBDT = (decimal) val;
         if (!asBDT.isUnknown())
         {
            stmt.setBigDecimal(index, asBDT.toBigDecimal());
            return 1;
         }
      }
      
      stmt.setNull(index, Types.DECIMAL);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code date} object and use it to initialize a parameter of type
    * date in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code date}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdDateParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         date asBDT = (date) val;
         if (!asBDT.isUnknown())
         {
            stmt.setDate(index, new Date(asBDT.dateValue(null).getTime()));
            return 1;
         }
      }
      
      stmt.setNull(index, Types.DATE);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code datetime} object and use it to initialize a parameter of
    * type datetime in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code datetime}.
    * @param   forUpd
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdDatetimeParameter(PreparedStatement stmt, int index, Object val, boolean forUpd)
   throws SQLException
   {
      if (val != null)
      {
         datetime asBDT = (datetime) val;
         if (!asBDT.isUnknown())
         {
            stmt.setTimestamp(index, new Timestamp(asBDT.dateValue().getTime()));
            return 1;
         }
      }
      
      stmt.setNull(index, Types.TIMESTAMP);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code datetimetz} object and use it to initialize a parameter of
    * type datetimetz in a SQL prepared query.
    * <p>
    * <strong>Note:</strong><br>
    * This implementation of {@code DataHandler} is the only one that fills in two parameters in a
    * prepared statement.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameters.
    * @param   val
    *          The value of the parameter. Must be a {@code datetimetz}.
    * @param   forUpd
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate.
    *
    * @return  2 (the number of position used by a datetimetz value) if case of an insert/update and 1
    *          otherwise (all supported SQL databases handle correctly the time-zoned instants natively).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdDatetimetzParameter(PreparedStatement stmt, int index, Object val, boolean forUpd)
   throws SQLException
   {
      if (val != null)
      {
         datetimetz asBDT = (datetimetz) val;
         if (!asBDT.isUnknown())
         {
            stmt.setObject(index, asBDT.offsetDateTimeValue(), Types.TIMESTAMP_WITH_TIMEZONE);
            if (forUpd)
            {
               stmt.setObject(index + 1, asBDT.getTimeZoneOffset().intValue(), Types.INTEGER);
            }
            return forUpd ? 2 : 1;
         }
      }
      
      stmt.setNull(index, Types.TIMESTAMP_WITH_TIMEZONE);
      if (forUpd)
      {
         stmt.setNull(index + 1, Types.INTEGER);
      }
      return forUpd ? 2 : 1;
   }
   
   /**
    * Extracts the data from a {@code comhandle} object and use it to initialize a parameter of
    * type comhandle in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code comhandle}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdComhandleParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         comhandle asBDT = (comhandle) val;
         if (!asBDT.isUnknown())
         {
            stmt.setString(index, asBDT.toStringMessage());
            return 1;
         }
      }
      
      stmt.setNull(index, Types.VARCHAR);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code clob} object and use it to initialize a parameter of type
    * clob in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code clob}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdClobParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         clob asBDT = (clob) val;
         if (!asBDT.isUnknown())
         {
            stmt.setClob(index, new StringReader(asBDT.getValue()));
            return 1;
         }
      }
      
      stmt.setNull(index, Types.CLOB);
      return 1;
   }
   
   /**
    * Extracts the data from a {@link character} object and use it to initialize a parameter of
    * type char in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code character}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdCharacterParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         character asBDT = (character) val;
         if (!asBDT.isUnknown())
         {
            stmt.setString(index, asBDT.toJavaType());
            return 1;
         }
      }
      
      stmt.setNull(index, Types.VARCHAR);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code blob} object and use it to initialize a parameter of type
    * blob in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code blob}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdBlobParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         blob asBDT = (blob) val;
         if (!asBDT.isUnknown())
         {
            stmt.setBlob(index, new ByteArrayInputStream(asBDT.getByteArray()));
            return 1;
         }
      }
      
      stmt.setNull(index, Types.BLOB);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code recid} object and use it to initialize a parameter of type
    * recid in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code recid}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdRecidParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         recid asBDT = (recid) val;
         if (!asBDT.isUnknown())
         {
            stmt.setInt(index, asBDT.intValue());
            return 1;
         }
      }
      
      stmt.setNull(index, Types.INTEGER);
      return 1;
   }
   
   /**
    * Extracts the data from a {@code rowid} object and use it to initialize a parameter of type
    * rowid in a SQL prepared query.
    *
    * @param   stmt
    *          The prepared statement whose parameter is to be set.
    * @param   index
    *          The index/position of the parameter.
    * @param   val
    *          The value of the parameter. Must be a {@code rowid}.
    * @param   forUpdate
    *          {@code true} if the parameter in the statement is used to update a field and {@code false} if
    *          it is used as part of the where predicate. Ignored here.
    *
    * @return  1 (the number of position used by this value).
    *
    * @throws  SQLException
    *          when an error is encountered while setting the value.
    */
   private static int setFwdRowidParameter(PreparedStatement stmt, int index, Object val, boolean forUpdate)
   throws SQLException
   {
      if (val != null)
      {
         rowid asBDT = (rowid) val;
         if (!asBDT.isUnknown())
         {
            stmt.setLong(index, asBDT.getValue());
            return 1;
         }
      }
      
      stmt.setNull(index, Types.BIGINT);
      return 1;
   }
   
   /**
    * Strip the JDBC URL's options.
    * <p> 
    * Note: This proved not to be a reliable method to identify a database. The URL may optionally contain the
    *       host, port, user, and password. While a connection to same database can be done using various
    *       user/password combinations, the optional port may cause connection to a different database.
    * 
    * @param   url
    *          The JDBC URL.
    *
    * @return  The URL without any options.
    */
   private static String prepareUrl(String url)
   {
      // the H2 url does not feature a port number and has the optional parameters separated by ;
      if (url.startsWith("jdbc:h2:"))
      {
         // TODO: the path for the database may not be normalized
         int idx = url.indexOf(';');
         return (idx >= 0) ? url.substring(0, idx) : url;
      }
      
      // the other dialects use ? to separate the optional parameters and may contain a port number. If not,
      // use the default dialect-specific, in a normalized manner
      int idx = url.indexOf('?');
      String noOptionsUrl = (idx >= 0) ? url.substring(0, idx) : url;
      
      Matcher matcher = Pattern.compile("(\\w*):(\\w*):(//)?(\\w*)(:\\d*)?(/)?(\\w*)").matcher(noOptionsUrl);
      if (matcher.find())
      {
         String port = matcher.group(5);
         if (port == null)
         {
            // compute the dialect-specific default port
            port = url.startsWith("jdbc:mariadb:") || url.startsWith("jdbc:mysql:") ? ":3306" :
                   url.startsWith("jdbc:postgresql:") ? ":5432" : 
                   ":1433" /*mssql*/;
         }
         return matcher.group(1) + ":" +           // "jdbc"
                matcher.group(2) + ":" +           // dialect
                matcher.group(4) + port +  ":" +   // host:port
                matcher.group(7);                  // database name
      }
      
      return noOptionsUrl;
   }
}