/**
 * SQLDecimal satisfies the DataValueDescriptor interfaces (i.e., OrderableDataType). It implements
 * a numeric/decimal column, e.g. for * storing a column value; it can be specified when constructed
 * to not allow nulls. Nullability cannot be changed after construction, as it affects the storage
 * size and mechanism.
 *
 * <p>Because OrderableDataType is a subtype of DataType, SQLDecimal can play a role in either a
 * DataType/Row or a OrderableDataType/Row, interchangeably.
 *
 * <p>We assume the store has a flag for nullness of the value, and simply return a 0-length array
 * for the stored form when the value is null.
 */
public final class SQLDecimal extends NumberDataType implements VariableSizeDataValue {
  /**
   * object state. Note that scale and precision are always determined dynamically from value when
   * it is not null.
   *
   * <p>The field value can be null without the data value being null. In this case the value is
   * stored in rawData and rawScale. This is to allow the minimal amount of work to read a
   * SQLDecimal from disk. Creating the BigDecimal is expensive as it requires allocating three
   * objects, the last two are a waste in the case the row does not qualify or the row will be
   * written out by the sorter before being returned to the application.
   *
   * <p>This means that this field must be accessed for read indirectly through the getBigDecimal()
   * method, and when setting it the rawData field must be set to null.
   */
  private BigDecimal value;

  /** See comments for value */
  private byte[] rawData;

  /** See comments for value */
  private int rawScale;

  private static final int BASE_MEMORY_USAGE = ClassSize.estimateBaseFromCatalog(SQLDecimal.class);
  private static final int BIG_DECIMAL_MEMORY_USAGE =
      ClassSize.estimateBaseFromCatalog(BigDecimal.class);

  public int estimateMemoryUsage() {
    int sz = BASE_MEMORY_USAGE;
    if (null != value) sz += BIG_DECIMAL_MEMORY_USAGE + (value.unscaledValue().bitLength() + 8) / 8;
    if (null != rawData) sz += rawData.length;
    return sz;
  }

  ////////////////////////////////////////////////////////////////////
  //
  // CLASS INTERFACE
  //
  ////////////////////////////////////////////////////////////////////
  /** no-arg constructor, required by Formattable */
  public SQLDecimal() {}

  public SQLDecimal(BigDecimal val) {
    value = val;
  }

  public SQLDecimal(BigDecimal val, int nprecision, int scale) throws StandardException {

    value = val;
    if ((value != null) && (scale >= 0)) {
      value = value.setScale(scale, BigDecimal.ROUND_DOWN);
    }
  }

  public SQLDecimal(String val) {
    value = new BigDecimal(val);
  }

  /*
   * DataValueDescriptor interface
   * (mostly implemented in DataType)
   *
   */

  /** @exception StandardException thrown on failure to convert */
  public int getInt() throws StandardException {
    if (isNull()) return 0;

    try {
      long lv = getLong();

      if ((lv >= Integer.MIN_VALUE) && (lv <= Integer.MAX_VALUE)) return (int) lv;

    } catch (StandardException se) {
    }

    throw StandardException.newException(SQLState.LANG_OUTSIDE_RANGE_FOR_DATATYPE, "INTEGER");
  }

  /** @exception StandardException thrown on failure to convert */
  public byte getByte() throws StandardException {
    if (isNull()) return (byte) 0;

    try {
      long lv = getLong();

      if ((lv >= Byte.MIN_VALUE) && (lv <= Byte.MAX_VALUE)) return (byte) lv;

    } catch (StandardException se) {
    }

    throw StandardException.newException(SQLState.LANG_OUTSIDE_RANGE_FOR_DATATYPE, "TINYINT");
  }

  /** @exception StandardException thrown on failure to convert */
  public short getShort() throws StandardException {
    if (isNull()) return (short) 0;

    try {
      long lv = getLong();

      if ((lv >= Short.MIN_VALUE) && (lv <= Short.MAX_VALUE)) return (short) lv;

    } catch (StandardException se) {
    }

    throw StandardException.newException(SQLState.LANG_OUTSIDE_RANGE_FOR_DATATYPE, "SMALLINT");
  }

  /** @exception StandardException thrown on failure to convert */
  public long getLong() throws StandardException {
    BigDecimal localValue = getBigDecimal();
    if (localValue == null) return (long) 0;

    // Valid range for long is
    //   greater than Long.MIN_VALUE - 1
    // *and*
    //   less than Long.MAX_VALUE + 1
    //
    // This ensures that DECIMAL values with an integral value
    // equal to the Long.MIN/MAX_VALUE round correctly to those values.
    // e.g. 9223372036854775807.1  converts to 9223372036854775807
    // this matches DB2 UDB behaviour

    if ((localValue.compareTo(MINLONG_MINUS_ONE) == 1)
        && (localValue.compareTo(MAXLONG_PLUS_ONE) == -1)) {

      return localValue.longValue();
    }

    throw StandardException.newException(SQLState.LANG_OUTSIDE_RANGE_FOR_DATATYPE, "BIGINT");
  }

  /** @exception StandardException thrown on failure to convert */
  public float getFloat() throws StandardException {
    BigDecimal localValue = getBigDecimal();
    if (localValue == null) return (float) 0;

    // If the BigDecimal is out of range for the float
    // then positive or negative infinity is returned.
    float value = NumberDataType.normalizeREAL(localValue.floatValue());

    return value;
  }

  /**
   * If we have a value that is greater than the maximum double, exception is thrown. Otherwise, ok.
   * If the value is less than can be represented by a double, ti will get set to the smallest
   * double value.
   *
   * @exception StandardException thrown on failure to convert
   */
  public double getDouble() throws StandardException {
    BigDecimal localValue = getBigDecimal();
    if (localValue == null) return (double) 0;

    // If the BigDecimal is out of range for double
    // then positive or negative infinity is returned.
    double value = NumberDataType.normalizeDOUBLE(localValue.doubleValue());
    return value;
  }

  private BigDecimal getBigDecimal() {
    if ((value == null) && (rawData != null)) {
      value = new BigDecimal(new BigInteger(rawData), rawScale);
    }

    return value;
  }

  /**
   * DECIMAL implementation. Convert to a BigDecimal using getObject which will return a BigDecimal
   */
  public int typeToBigDecimal() {
    return java.sql.Types.DECIMAL;
  }

  // 0 or null is false, all else is true
  public boolean getBoolean() {

    BigDecimal localValue = getBigDecimal();
    if (localValue == null) return false;

    return localValue.compareTo(ZERO) != 0;
  }

  public String getString() {
    BigDecimal localValue = getBigDecimal();
    if (localValue == null) return null;
    else if (toPlainString == null) return localValue.toString();
    else {
      // use reflection so we can still compile using JDK1.4
      // if we are prepared to require 1.5 to compile then this can be a direct call
      try {
        return (String) toPlainString.invoke(localValue, null);
      } catch (IllegalAccessException e) {
        // can't happen based on the JDK spec
        throw new IllegalAccessError("toPlainString");
      } catch (InvocationTargetException e) {
        Throwable t = e.getTargetException();
        if (t instanceof RuntimeException) {
          throw (RuntimeException) t;
        } else if (t instanceof Error) {
          throw (Error) t;
        } else {
          // can't happen
          throw new IncompatibleClassChangeError("toPlainString");
        }
      }
    }
  }

  private static final Method toPlainString;
  private static final Method bdPrecision;

  static {
    Method m;
    try {
      m = BigDecimal.class.getMethod("toPlainString", null);
    } catch (NoSuchMethodException e) {
      m = null;
    }
    toPlainString = m;
    try {
      m = BigDecimal.class.getMethod("precision", null);
    } catch (NoSuchMethodException e) {
      m = null;
    }
    bdPrecision = m;
  }

  public Object getObject() {
    /*
     ** BigDecimal is immutable
     */
    return getBigDecimal();
  }

  /**
   * Set the value from a correctly typed BigDecimal object.
   *
   * @throws StandardException
   */
  void setObject(Object theValue) throws StandardException {
    setValue((BigDecimal) theValue);
  }

  protected void setFrom(DataValueDescriptor theValue) throws StandardException {

    setCoreValue(SQLDecimal.getBigDecimal(theValue));
  }

  public int getLength() {
    return getDecimalValuePrecision();
  }

  // this is for DataType's error generator
  public String getTypeName() {
    return TypeId.DECIMAL_NAME;
  }

  /*
   * Storable interface, implies Externalizable, TypedFormat
   */

  /**
   * Return my format identifier.
   *
   * @see org.apache.derby.iapi.services.io.TypedFormat#getTypeFormatId
   */
  public int getTypeFormatId() {
    return StoredFormatIds.SQL_DECIMAL_ID;
  }

  /*
   * see if the decimal value is null.
   */
  /** @see Storable#isNull */
  public boolean isNull() {
    return (value == null) && (rawData == null);
  }

  /**
   * Distill the BigDecimal to a byte array and write out:
   *
   * <UL>
   *   <LI>scale (zero or positive) as a byte
   *   <LI>length of byte array as a byte
   *   <LI>the byte array
   * </UL>
   */
  public void writeExternal(ObjectOutput out) throws IOException {
    // never called when value is null
    if (SanityManager.DEBUG) SanityManager.ASSERT(!isNull());

    int scale;
    byte[] byteArray;

    if (value != null) {
      scale = value.scale();

      // J2SE 5.0 introduced negative scale value for BigDecimals.
      // In previouse Java releases a negative scale was not allowed
      // (threw an exception on setScale and the constructor that took
      // a scale).
      //
      // Thus the Derby format for DECIMAL implictly assumed a
      // positive or zero scale value, and thus now must explicitly
      // be positive. This is to allow databases created under J2SE 5.0
      // to continue to be supported under JDK 1.3/JDK 1.4, ie. to continue
      // the platform independence, independent of OS/cpu and JVM.
      //
      // If the scale is negative set the scale to be zero, this results
      // in an unchanged value with a new scale. A BigDecimal with a
      // negative scale by definition is a whole number.
      // e.g. 1000 can be represented by:
      //    a BigDecimal with scale -3 (unscaled value of 1)
      // or a BigDecimal with scale 0 (unscaled value of 1000)

      if (scale < 0) {
        scale = 0;
        value = value.setScale(0);
      }

      BigInteger bi = value.unscaledValue();
      byteArray = bi.toByteArray();
    } else {
      scale = rawScale;
      byteArray = rawData;
    }

    if (SanityManager.DEBUG) {
      if (scale < 0)
        SanityManager.THROWASSERT(
            "DECIMAL scale at writeExternal is negative " + scale + " value " + toString());
    }

    out.writeByte(scale);
    out.writeByte(byteArray.length);
    out.write(byteArray);
  }

  /**
   * Note the use of rawData: we reuse the array if the incoming array is the same length or smaller
   * than the array length.
   *
   * @see java.io.Externalizable#readExternal
   */
  public void readExternal(ObjectInput in) throws IOException {
    // clear the previous value to ensure that the
    // rawData value will be used
    value = null;

    rawScale = in.readUnsignedByte();
    int size = in.readUnsignedByte();

    /*
    ** Allocate a new array if the data to read
    ** is larger than the existing array, or if
    ** we don't have an array yet.

          Need to use readFully below and NOT just read because read does not
          guarantee getting size bytes back, whereas readFully does (unless EOF).
          */
    if ((rawData == null) || size != rawData.length) {
      rawData = new byte[size];
    }
    in.readFully(rawData);
  }

  /** @see Storable#restoreToNull */
  public void restoreToNull() {
    value = null;
    rawData = null;
  }

  /** @exception StandardException Thrown on error */
  protected int typeCompare(DataValueDescriptor arg) throws StandardException {
    BigDecimal otherValue = SQLDecimal.getBigDecimal(arg);
    return getBigDecimal().compareTo(otherValue);
  }

  /*
   * DataValueDescriptor interface
   */

  /** @see DataValueDescriptor#cloneValue */
  public DataValueDescriptor cloneValue(boolean forceMaterialization) {
    return new SQLDecimal(getBigDecimal());
  }

  /** @see DataValueDescriptor#getNewNull */
  public DataValueDescriptor getNewNull() {
    return new SQLDecimal();
  }

  /**
   * @see DataValueDescriptor#setValueFromResultSet
   * @exception SQLException Thrown on error
   */
  public void setValueFromResultSet(ResultSet resultSet, int colNumber, boolean isNullable)
      throws SQLException {
    value = resultSet.getBigDecimal(colNumber);
    rawData = null;
  }
  /**
   * Set the value into a PreparedStatement.
   *
   * @exception SQLException Error setting value in PreparedStatement
   */
  public final void setInto(PreparedStatement ps, int position) throws SQLException {

    if (isNull()) {
      ps.setNull(position, java.sql.Types.DECIMAL);
      return;
    }

    ps.setBigDecimal(position, getBigDecimal());
  }

  /**
   * <B> WARNING </B> there is no checking to make sure that theValue doesn't exceed the
   * precision/scale of the current SQLDecimal. It is just assumed that the SQLDecimal is supposed
   * to take the precision/scale of the BigDecimalized String.
   *
   * @exception StandardException throws NumberFormatException when the String format is not
   *     recognized.
   */
  public void setValue(String theValue) throws StandardException {
    rawData = null;

    if (theValue == null) {
      value = null;
    } else {
      try {
        theValue = theValue.trim();
        value = new BigDecimal(theValue);
        rawData = null;
      } catch (NumberFormatException nfe) {
        throw invalidFormat();
      }
    }
  }

  /**
   * @see NumberDataValue#setValue
   * @exception StandardException Thrown on error
   */
  public void setValue(double theValue) throws StandardException {
    setCoreValue(NumberDataType.normalizeDOUBLE(theValue));
  }

  /** @see NumberDataValue#setValue */
  public void setValue(float theValue) throws StandardException {
    setCoreValue((double) NumberDataType.normalizeREAL(theValue));
  }

  /** @see NumberDataValue#setValue */
  public void setValue(long theValue) {
    value = BigDecimal.valueOf(theValue);
    rawData = null;
  }

  /** @see NumberDataValue#setValue */
  public void setValue(int theValue) {
    setValue((long) theValue);
  }

  /**
   * Only to be called when the application sets a value using BigDecimal through setBigDecimal
   * calls.
   */
  public void setBigDecimal(Number theValue) throws StandardException {
    setCoreValue((BigDecimal) theValue);
  }

  /**
   * Called when setting a DECIMAL value internally or from through a procedure or function. Handles
   * long in addition to BigDecimal to handle identity being stored as a long but returned as a
   * DECIMAL.
   */
  public void setValue(Number theValue) throws StandardException {
    if (SanityManager.ASSERT) {
      if (theValue != null
          && !(theValue instanceof java.math.BigDecimal)
          && !(theValue instanceof java.lang.Long))
        SanityManager.THROWASSERT("SQLDecimal.setValue(Number) passed a " + theValue.getClass());
    }

    if (theValue instanceof BigDecimal || theValue == null) setCoreValue((BigDecimal) theValue);
    else setValue(theValue.longValue());
  }

  /** @see NumberDataValue#setValue */
  public void setValue(boolean theValue) {
    setCoreValue(theValue ? ONE : ZERO);
  }

  /*
   * DataValueDescriptor interface
   */

  /** @see DataValueDescriptor#typePrecedence */
  public int typePrecedence() {
    return TypeId.DECIMAL_PRECEDENCE;
  }
  // END DataValueDescriptor interface

  private void setCoreValue(BigDecimal theValue) {
    value = theValue;
    rawData = null;
  }

  private void setCoreValue(double theValue) {
    value = new BigDecimal(Double.toString(theValue));
    rawData = null;
  }

  /**
   * Normalization method - this method may be called when putting a value into a SQLDecimal, for
   * example, when inserting into a SQLDecimal column. See NormalizeResultSet in execution.
   *
   * <p>Note that truncation is allowed on the decimal portion of a numeric only.
   *
   * @param desiredType The type to normalize the source column to
   * @param source The value to normalize
   * @throws StandardException Thrown for null into non-nullable column, and for truncation error
   */
  public void normalize(DataTypeDescriptor desiredType, DataValueDescriptor source)
      throws StandardException {
    int desiredScale = desiredType.getScale();
    int desiredPrecision = desiredType.getPrecision();

    setFrom(source);
    setWidth(desiredPrecision, desiredScale, true);
  }

  /*
   ** SQL Operators
   */

  /**
   * This method implements the + operator for DECIMAL.
   *
   * @param addend1 One of the addends
   * @param addend2 The other addend
   * @param result The result of a previous call to this method, null if not called yet
   * @return A SQLDecimal containing the result of the addition
   * @exception StandardException Thrown on error
   */
  public NumberDataValue plus(
      NumberDataValue addend1, NumberDataValue addend2, NumberDataValue result)
      throws StandardException {
    if (result == null) {
      result = new SQLDecimal();
    }

    if (addend1.isNull() || addend2.isNull()) {
      result.setToNull();
      return result;
    }

    result.setBigDecimal(SQLDecimal.getBigDecimal(addend1).add(SQLDecimal.getBigDecimal(addend2)));
    return result;
  }

  /**
   * This method implements the - operator for "decimal - decimal".
   *
   * @param left The value to be subtracted from
   * @param right The value to be subtracted
   * @param result The result of a previous call to this method, null if not called yet
   * @return A SQLDecimal containing the result of the subtraction
   * @exception StandardException Thrown on error
   */
  public NumberDataValue minus(NumberDataValue left, NumberDataValue right, NumberDataValue result)
      throws StandardException {
    if (result == null) {
      result = new SQLDecimal();
    }

    if (left.isNull() || right.isNull()) {
      result.setToNull();
      return result;
    }

    result.setBigDecimal(SQLDecimal.getBigDecimal(left).subtract(SQLDecimal.getBigDecimal(right)));
    return result;
  }

  /**
   * This method implements the * operator for "double * double".
   *
   * @param left The first value to be multiplied
   * @param right The second value to be multiplied
   * @param result The result of a previous call to this method, null if not called yet
   * @return A SQLDecimal containing the result of the multiplication
   * @exception StandardException Thrown on error
   */
  public NumberDataValue times(NumberDataValue left, NumberDataValue right, NumberDataValue result)
      throws StandardException {
    if (result == null) {
      result = new SQLDecimal();
    }

    if (left.isNull() || right.isNull()) {
      result.setToNull();
      return result;
    }

    result.setBigDecimal(SQLDecimal.getBigDecimal(left).multiply(SQLDecimal.getBigDecimal(right)));
    return result;
  }

  /**
   * This method implements the / operator for BigDecimal/BigDecimal
   *
   * @param dividend The numerator
   * @param divisor The denominator
   * @param result The result of a previous call to this method, null if not called yet
   * @return A SQLDecimal containing the result of the division
   * @exception StandardException Thrown on error
   */
  public NumberDataValue divide(
      NumberDataValue dividend, NumberDataValue divisor, NumberDataValue result)
      throws StandardException {
    return divide(dividend, divisor, result, -1);
  }

  /**
   * This method implements the / operator for BigDecimal/BigDecimal
   *
   * @param dividend The numerator
   * @param divisor The denominator
   * @param result The result of a previous call to this method, null if not called yet
   * @param scale The result scale, if < 0, calculate the scale according to the actual values'
   *     sizes
   * @return A SQLDecimal containing the result of the division
   * @exception StandardException Thrown on error
   */
  public NumberDataValue divide(
      NumberDataValue dividend, NumberDataValue divisor, NumberDataValue result, int scale)
      throws StandardException {
    if (result == null) {
      result = new SQLDecimal();
    }

    if (dividend.isNull() || divisor.isNull()) {
      result.setToNull();
      return result;
    }

    BigDecimal divisorBigDecimal = SQLDecimal.getBigDecimal(divisor);

    if (divisorBigDecimal.compareTo(ZERO) == 0) {
      throw StandardException.newException(SQLState.LANG_DIVIDE_BY_ZERO);
    }
    BigDecimal dividendBigDecimal = SQLDecimal.getBigDecimal(dividend);

    /*
     ** Set the result scale to be either the passed in scale, whcih was
     ** calculated at bind time to be max(ls+rp-rs+1, 4), where ls,rp,rs
     ** are static data types' sizes, which are predictable and stable
     ** (for the whole result set column, eg.); otherwise dynamically
     ** calculates the scale according to actual values.  Beetle 3901
     */
    result.setBigDecimal(
        dividendBigDecimal.divide(
            divisorBigDecimal,
            scale > -1
                ? scale
                : Math.max(
                    (dividendBigDecimal.scale() + SQLDecimal.getWholeDigits(divisorBigDecimal) + 1),
                    NumberDataValue.MIN_DECIMAL_DIVIDE_SCALE),
            BigDecimal.ROUND_DOWN));

    return result;
  }

  /**
   * This method implements the unary minus operator for double.
   *
   * @param result The result of a previous call to this method, null if not called yet
   * @return A SQLDecimal containing the result of the division
   * @exception StandardException Thrown on error
   */
  public NumberDataValue minus(NumberDataValue result) throws StandardException {
    if (result == null) {
      result = new SQLDecimal();
    }

    if (this.isNull()) {
      result.setToNull();
      return result;
    }

    result.setBigDecimal(getBigDecimal().negate());
    return result;
  }

  /**
   * This method implements the isNegative method.
   *
   * @return A boolean. If this.value is negative, return true. For positive values or null, return
   *     false.
   */
  protected boolean isNegative() {
    return !isNull() && (getBigDecimal().compareTo(ZERO) == -1);
  }

  /*
   * String display of value
   */
  public String toString() {
    if (isNull()) return "NULL";
    else return getString();
  }

  /*
   * Hash code
   */
  public int hashCode() {
    long longVal;
    BigDecimal localValue = getBigDecimal();

    double doubleVal = (localValue != null) ? localValue.doubleValue() : 0;

    if (Double.isInfinite(doubleVal)) {
      /*
       ** This loses the fractional part, but it probably doesn't
       ** matter for numbers that are big enough to overflow a double -
       ** it's probably rare for numbers this big to be different only in
       ** their fractional parts.
       */
      longVal = localValue.longValue();
    } else {
      longVal = (long) doubleVal;
      if (longVal != doubleVal) {
        longVal = Double.doubleToLongBits(doubleVal);
      }
    }

    return (int) (longVal ^ (longVal >> 32));
  }

  ///////////////////////////////////////////////////////////////////////////////
  //
  // VariableSizeDataValue interface
  //
  ///////////////////////////////////////////////////////////////////////////////

  /**
   * Set the precision/scale of the to the desired values. Used when CASTing. Ideally we'd recycle
   * normalize(), but the use is different.
   *
   * @param desiredPrecision the desired precision -- IGNORE_PREICISION if it is to be ignored.
   * @param desiredScale the desired scale
   * @param errorOnTrunc throw error on truncation (ignored -- always thrown if we truncate the
   *     non-decimal part of the value)
   * @exception StandardException Thrown on non-zero truncation if errorOnTrunc is true
   */
  public void setWidth(int desiredPrecision, int desiredScale, boolean errorOnTrunc)
      throws StandardException {
    if (isNull()) return;

    if (desiredPrecision != IGNORE_PRECISION
        && ((desiredPrecision - desiredScale) < SQLDecimal.getWholeDigits(getBigDecimal()))) {
      throw StandardException.newException(
          SQLState.LANG_OUTSIDE_RANGE_FOR_DATATYPE,
          ("DECIMAL/NUMERIC(" + desiredPrecision + "," + desiredScale + ")"));
    }
    value = value.setScale(desiredScale, BigDecimal.ROUND_DOWN);
    rawData = null;
  }

  /**
   * Return the SQL scale of this value, number of digits after the decimal point, or zero for a
   * whole number. This does not match the return from BigDecimal.scale() since in J2SE 5.0 onwards
   * that can return negative scales.
   */
  public int getDecimalValuePrecision() {
    if (isNull()) return 0;

    BigDecimal localValue = getBigDecimal();

    return SQLDecimal.getWholeDigits(localValue) + getDecimalValueScale();
  }

  /**
   * Return the SQL scale of this value, number of digits after the decimal point, or zero for a
   * whole number. This does not match the return from BigDecimal.scale() since in J2SE 5.0 onwards
   * that can return negative scales.
   */
  public int getDecimalValueScale() {
    if (isNull()) return 0;

    if (value == null) return rawScale;

    int scale = value.scale();
    if (scale >= 0) return scale;

    // BigDecimal scale is negative, so number must have no fractional
    // part as its value is the unscaled value * 10^-scale
    return 0;
  }

  /**
   * Get a BigDecimal representing the value of a DataValueDescriptor
   *
   * @param value Non-null value to be converted
   * @return BigDecimal value
   * @throws StandardException Invalid conversion or out of range.
   */
  public static BigDecimal getBigDecimal(DataValueDescriptor value) throws StandardException {
    if (SanityManager.DEBUG) {
      if (value.isNull())
        SanityManager.THROWASSERT("NULL value passed to SQLDecimal.getBigDecimal");
    }

    switch (value.typeToBigDecimal()) {
      case Types.DECIMAL:
        return (BigDecimal) value.getObject();
      case Types.CHAR:
        try {
          return new BigDecimal(value.getString().trim());
        } catch (NumberFormatException nfe) {
          throw StandardException.newException(
              SQLState.LANG_FORMAT_EXCEPTION, "java.math.BigDecimal");
        }
      case Types.BIGINT:
        return BigDecimal.valueOf(value.getLong());
      default:
        if (SanityManager.DEBUG)
          SanityManager.THROWASSERT(
              "invalid return from "
                  + value.getClass()
                  + ".typeToBigDecimal() "
                  + value.typeToBigDecimal());
        return null;
    }
  }

  /**
   * Calculate the number of digits to the left of the decimal point of the passed in value.
   *
   * @param decimalValue Value to get whole digits from, never null.
   * @return number of whole digits.
   */
  private static int getWholeDigits(BigDecimal decimalValue) {
    /** if ONE > abs(value) then the number of whole digits is 0 */
    decimalValue = decimalValue.abs();
    if (ONE.compareTo(decimalValue) == 1) {
      return 0;
    }

    if (bdPrecision != null) {
      // use reflection so we can still compile using JDK1.4
      // if we are prepared to require 1.5 to compile then this can be a
      // direct call
      try {
        // precision is the number of digits in the unscaled value,
        // subtracting the scale (positive or negative) will give the
        // number of whole digits.
        int precision = ((Integer) bdPrecision.invoke(decimalValue, null)).intValue();
        return precision - decimalValue.scale();
      } catch (IllegalAccessException e) {
        // can't happen based on the JDK spec
        throw new IllegalAccessError("precision");
      } catch (InvocationTargetException e) {
        Throwable t = e.getTargetException();
        if (t instanceof RuntimeException) {
          throw (RuntimeException) t;
        } else if (t instanceof Error) {
          throw (Error) t;
        } else {
          // can't happen
          throw new IncompatibleClassChangeError("precision");
        }
      }
    }

    String s = decimalValue.toString();
    return (decimalValue.scale() == 0) ? s.length() : s.indexOf('.');
  }
}
Example #2
0
/**
 * Implements an instance of a B-Tree secondary index conglomerate. A B2I object has two roles.
 *
 * <ol>
 *   <li>The B2I object is stored on disk, and holds the store specific information needed to
 *       access/describe the conglomerate. This includes information such as the format ids of the
 *       columns, the conglomerate id of the base table, the location of row location column.
 *   <li>Access to all the interfaces start by making a call off the Conglomerate interface. So for
 *       instance to get a scan on the conglomerate method {@link #openScan openScan} should be
 *       called.
 * </ol>
 */
public class B2I extends BTree {
  private static final String PROPERTY_BASECONGLOMID = "baseConglomerateId";
  private static final String PROPERTY_ROWLOCCOLUMN = "rowLocationColumn";

  static final int FORMAT_NUMBER = StoredFormatIds.ACCESS_B2I_V5_ID;

  /*
   ** Fields of B2I.
   */

  /**
   * The id of the conglomerate which contains the base table. Row locations inserted into this
   * secondary index are assumed to refer to that conglomerate. Used to obtain table/row locks on
   * the base table rows which the index rows point at.
   */
  long baseConglomerateId;

  /**
   * The column id (zero-based integer index) of the column which holds the row location to the base
   * conglomerate. The default value of RowLocationColumn is the last key column. Used to obtain
   * table/row locks on the base table rows with the index rows point at. Currently,
   * RowLocationColumn must be the last key column.
   */
  int rowLocationColumn;

  private static final int BASE_MEMORY_USAGE = ClassSize.estimateBaseFromCatalog(B2I.class);

  public int estimateMemoryUsage() {
    return BASE_MEMORY_USAGE;
  }

  /**
   * ************************************************************************ Constructors for This
   * class: *************************************************************************
   */

  /**
   * ************************************************************************ Protected locking
   * implmentations of abtract BTree routines: getBtreeLockingPolicy lockTable
   * *************************************************************************
   */

  /**
   * Create a new btree locking policy from scratch.
   *
   * @exception StandardException Standard exception policy.
   */
  protected BTreeLockingPolicy getBtreeLockingPolicy(
      Transaction rawtran,
      int lock_level,
      int mode,
      int isolation_level,
      ConglomerateController base_cc,
      OpenBTree open_btree)
      throws StandardException {
    BTreeLockingPolicy ret_locking_policy = null;

    if (SanityManager.DEBUG) {
      SanityManager.ASSERT(
          (isolation_level == TransactionController.ISOLATION_SERIALIZABLE)
              || (isolation_level == TransactionController.ISOLATION_REPEATABLE_READ)
              || (isolation_level == TransactionController.ISOLATION_READ_COMMITTED_NOHOLDLOCK)
              || (isolation_level == TransactionController.ISOLATION_READ_COMMITTED)
              || (isolation_level == TransactionController.ISOLATION_READ_UNCOMMITTED),
          "bad isolation_level = " + isolation_level);
    }

    if (lock_level == TransactionController.MODE_TABLE) {
      ret_locking_policy =
          new B2ITableLocking3(
              rawtran,
              lock_level,
              rawtran.newLockingPolicy(LockingPolicy.MODE_CONTAINER, isolation_level, true),
              base_cc,
              open_btree);
    } else if (lock_level == TransactionController.MODE_RECORD) {
      if (isolation_level == TransactionController.ISOLATION_SERIALIZABLE) {
        ret_locking_policy =
            new B2IRowLocking3(
                rawtran,
                lock_level,
                rawtran.newLockingPolicy(LockingPolicy.MODE_RECORD, isolation_level, true),
                base_cc,
                open_btree);
      } else if ((isolation_level == TransactionController.ISOLATION_REPEATABLE_READ)) {
        ret_locking_policy =
            new B2IRowLockingRR(
                rawtran,
                lock_level,
                rawtran.newLockingPolicy(LockingPolicy.MODE_RECORD, isolation_level, true),
                base_cc,
                open_btree);
      } else if ((isolation_level == TransactionController.ISOLATION_READ_COMMITTED)
          || (isolation_level == TransactionController.ISOLATION_READ_COMMITTED_NOHOLDLOCK)) {
        ret_locking_policy =
            new B2IRowLocking2(
                rawtran,
                lock_level,
                rawtran.newLockingPolicy(LockingPolicy.MODE_RECORD, isolation_level, true),
                base_cc,
                open_btree);
      } else if (isolation_level == TransactionController.ISOLATION_READ_UNCOMMITTED) {
        ret_locking_policy =
            new B2IRowLocking1(
                rawtran,
                lock_level,
                rawtran.newLockingPolicy(LockingPolicy.MODE_RECORD, isolation_level, true),
                base_cc,
                open_btree);
      }
    }

    if (SanityManager.DEBUG) {
      SanityManager.ASSERT(ret_locking_policy != null, "ret_locking_policy == null");
    }

    return (ret_locking_policy);
  }

  /**
   * Lock the base table.
   *
   * <p>Assumes that segment of the base container is the same as the segment of the btree segment.
   *
   * <p>RESOLVE - we really want to get the lock without opening the container. raw store will be
   * providing this.
   *
   * @param xact_manager Transaction to associate the lock with.
   * @exception StandardException Standard exception policy.
   */
  public final ConglomerateController lockTable(
      TransactionManager xact_manager, int open_mode, int lock_level, int isolation_level)
      throws StandardException {
    open_mode |= TransactionController.OPENMODE_FOR_LOCK_ONLY;

    // open the base conglomerate - just to get the table lock.
    ConglomerateController cc =
        xact_manager.openConglomerate(
            this.baseConglomerateId, false, open_mode, lock_level, isolation_level);

    return (cc);
  }

  /**
   * ************************************************************************ Private methods of
   * B2I, arranged alphabetically.
   * *************************************************************************
   */
  private void traverseRight() {
    // RESOLVE - Do I have to do this???????????????

    if (SanityManager.DEBUG) SanityManager.THROWASSERT("not implemented.");
  }

  /*
   ** Methods of B2I.
   */

  /**
   * Create an empty secondary index b-tree, using the generic b-tree to do the generic part of the
   * creation process.
   *
   * <p>This routine opens the newly created container, adds a single page, and makes this page the
   * root by inserting a LeafControlRow onto this page at slot 0 and marking in that control row
   * that the page is a root page.
   *
   * <p>The following properties are specific to the b-tree secondary index:
   *
   * <UL>
   *   <LI>"baseConglomerateId" (integer). The conglomerate id of the base conglomerate is never
   *       actually accessed by the b-tree secondary index implementation, it only serves as a
   *       namespace for row locks. This property is required.
   *   <LI>"rowLocationColumn" (integer). The zero-based index into the row which the b-tree
   *       secondary index will assume holds a @see RowLocation of the base row in the base
   *       conglomerate. This value will be used for acquiring locks. In this implementation
   *       RowLocationColumn must be the last key column. This property is required.
   * </UL>
   *
   * A secondary index i (a, b) on table t (a, b, c) would have rows which looked like (a, b,
   * row_location). baseConglomerateId is set to the conglomerate id of t. rowLocationColumns is set
   * to 2. allowsDuplicates would be set to false, @see BTree#create. To create a unique secondary
   * index set uniquenessColumns to 2, this means that the btree code will compare the key values
   * but not the row id when determing uniqueness. To create a nonunique secondary index set
   * uniquenessColumns to 3, this would mean that the uniqueness test would include the row location
   * and since all row locations will be unique all rows inserted into the index will be
   * differentiated (at least) by row location.
   *
   * @see BTree#create
   * @exception StandardException Standard exception policy.
   */
  public void create(
      TransactionManager xact_manager,
      int segmentId,
      long input_conglomid,
      DataValueDescriptor[] template,
      ColumnOrdering[] columnOrder,
      int[] collationIds,
      Properties properties,
      int temporaryFlag)
      throws StandardException {
    String property_value = null;
    Transaction rawtran = xact_manager.getRawStoreXact();

    if (properties == null) {
      throw (StandardException.newException(
          SQLState.BTREE_PROPERTY_NOT_FOUND, PROPERTY_BASECONGLOMID));
    }

    // Get baseConglomerateId //
    property_value = properties.getProperty(PROPERTY_BASECONGLOMID);
    if (property_value == null) {
      throw (StandardException.newException(
          SQLState.BTREE_PROPERTY_NOT_FOUND, PROPERTY_BASECONGLOMID));
    }

    if (SanityManager.DEBUG) {
      if (property_value == null)
        SanityManager.THROWASSERT(PROPERTY_BASECONGLOMID + "property not passed to B2I.create()");
    }

    baseConglomerateId = Long.parseLong(property_value);

    // Get rowLocationColumn //
    property_value = properties.getProperty(PROPERTY_ROWLOCCOLUMN);

    if (SanityManager.DEBUG) {
      if (property_value == null)
        SanityManager.THROWASSERT(PROPERTY_ROWLOCCOLUMN + "property not passed to B2I.create()");
    }

    if (property_value == null) {
      throw (StandardException.newException(
          SQLState.BTREE_PROPERTY_NOT_FOUND, PROPERTY_BASECONGLOMID));
    }

    rowLocationColumn = Integer.parseInt(property_value);

    // Currently the row location column must be the last column (makes)
    // comparing the columns in the index easier.
    if (SanityManager.DEBUG) {
      SanityManager.ASSERT(
          rowLocationColumn == template.length - 1,
          "rowLocationColumn is not the last column in the index");
      SanityManager.ASSERT(template[rowLocationColumn] instanceof RowLocation);

      // There must be at least one key column
      if (rowLocationColumn < 1)
        SanityManager.THROWASSERT(
            "rowLocationColumn (" + rowLocationColumn + ") expected to be >= 1");
    }

    /* convert the sorting order information into a boolean array map.
     * If the sorting order for the columns is not provided, we
     * assign the default as Ascending Order.
     * array length is equal to template length, because column order
     * length changes whether it is unique or is non unique. store assumes
     * template length arrays. So, we make template length array and make
     * the last column as ascending instead of having lot of execeptions
     * code.
     */

    ascDescInfo = new boolean[template.length];
    for (int i = 0; i < ascDescInfo.length; i++) {
      if (columnOrder != null && i < columnOrder.length)
        ascDescInfo[i] = columnOrder[i].getIsAscending();
      else ascDescInfo[i] = true; // default values - ascending order
    }

    // get collation ids from input collation ids, store it in the
    // conglom state.
    collation_ids = ConglomerateUtil.createCollationIds(template.length, collationIds);
    hasCollatedTypes = hasCollatedColumns(collation_ids);

    // Do the generic part of creating the b-tree.
    super.create(
        rawtran,
        segmentId,
        input_conglomid,
        template,
        properties,
        getTypeFormatId(),
        temporaryFlag);

    // open the base conglomerate - to get the lock
    ConglomerateController base_cc =
        xact_manager.openConglomerate(
            baseConglomerateId,
            false,
            TransactionController.OPENMODE_FOR_LOCK_ONLY,
            TransactionController.MODE_TABLE,
            TransactionController.ISOLATION_SERIALIZABLE);

    OpenBTree open_btree = new OpenBTree();

    BTreeLockingPolicy b2i_locking_policy =
        new B2ITableLocking3(
            rawtran,
            TransactionController.MODE_TABLE,
            rawtran.newLockingPolicy(
                LockingPolicy.MODE_CONTAINER, TransactionController.ISOLATION_SERIALIZABLE, true),
            base_cc,
            open_btree);

    // The following call will "open" the new btree.  Create is
    // an interesting case.  What we really want is read only table lock
    // on the base conglomerate and update locks on the index.  For now
    // just get the update lock on the base table, this is done by the
    // lockTable() call made by base class.

    open_btree.init(
        (TransactionManager) xact_manager, // current user xact
        (TransactionManager) xact_manager, // current xact
        (ContainerHandle) null, // have init open the container.
        rawtran,
        false,
        (ContainerHandle.MODE_FORUPDATE),
        TransactionController.MODE_TABLE,
        b2i_locking_policy, // get table level lock.
        this,
        (LogicalUndo) null, // no logical undo necessary, as
        // initEmptyBtree()
        // work will be done single user and
        // rows will not move.
        (DynamicCompiledOpenConglomInfo) null);

    // Open the newly created container, and insert the first control row.
    LeafControlRow.initEmptyBtree(open_btree);

    open_btree.close();

    base_cc.close();
  }

  /*
   ** Methods of Conglomerate
   */

  /**
   * Retrieve the maximum value row in an ordered conglomerate.
   *
   * <p>Returns true and fetches the rightmost row of an ordered conglomerate into "fetchRow" if
   * there is at least one row in the conglomerate. If there are no rows in the conglomerate it
   * returns false.
   *
   * <p>Non-ordered conglomerates will not implement this interface, calls will generate a
   * StandardException.
   *
   * <p>RESOLVE - this interface is temporary, long term equivalent (and more) functionality will be
   * provided by the openBackwardScan() interface.
   *
   * @param xact_manager The TransactionController under which this operation takes place.
   * @param conglomId The identifier of the conglomerate to open the scan for.
   * @param open_mode Specifiy flags to control opening of table. OPENMODE_FORUPDATE - if set open
   *     the table for update otherwise open table shared.
   * @param lock_level One of (MODE_TABLE, MODE_RECORD, or MODE_NONE).
   * @param isolation_level The isolation level to lock the conglomerate at. One of
   *     (ISOLATION_READ_COMMITTED or ISOLATION_SERIALIZABLE).
   * @param scanColumnList A description of which columns to return from every fetch in the scan.
   *     template, and scanColumnList work together to describe the row to be returned by the scan -
   *     see RowUtil for description of how these three parameters work together to describe a
   *     "row".
   * @param fetchRow The row to retrieve the maximum value into.
   * @return boolean indicating if a row was found and retrieved or not.
   * @exception StandardException Standard exception policy.
   */
  public boolean fetchMaxOnBTree(
      TransactionManager xact_manager,
      Transaction rawtran,
      long conglomId,
      int open_mode,
      int lock_level,
      LockingPolicy locking_policy,
      int isolation_level,
      FormatableBitSet scanColumnList,
      DataValueDescriptor[] fetchRow)
      throws StandardException {
    boolean row_exists;

    // row level locking implementation.

    // RESOLVE (revisit implementation after all the Xena rowlocking
    // changes have been made).  Can probably come up with single
    // path implementation.

    // Create a new b-tree secondary index scan.
    B2IMaxScan b2is = new B2IMaxScan();

    // Initialize it.
    b2is.init(
        xact_manager,
        rawtran,
        open_mode,
        lock_level,
        locking_policy,
        isolation_level,
        true /* get locks on base table as part of open */,
        scanColumnList,
        this,
        new B2IUndo());

    row_exists = b2is.fetchMax(fetchRow);

    b2is.close();

    return (row_exists);
  }

  /**
   * Bulk Load a B-tree secondary index.
   *
   * @see Conglomerate#load
   * @exception StandardException Standard Derby Error policy. raise
   *     SQLState.STORE_CONGLOMERATE_DUPLICATE_KEY_EXCEPTION if a duplicate key is detected in the
   *     load.
   */
  public long load(
      TransactionManager xact_manager, boolean createConglom, RowLocationRetRowSource rowSource)
      throws StandardException {
    long num_rows_loaded = 0;
    B2IController b2ic = new B2IController();

    try {
      int open_mode = TransactionController.OPENMODE_FORUPDATE;

      if (createConglom) {
        open_mode |= (ContainerHandle.MODE_UNLOGGED | ContainerHandle.MODE_CREATE_UNLOGGED);
      }

      // Do the actual open of the container in the super class.
      b2ic.init(
          xact_manager, // current transaction
          xact_manager.getRawStoreXact(), // current raw store xact
          false, // Not holdable
          open_mode,
          TransactionController.MODE_TABLE,
          xact_manager
              .getRawStoreXact()
              .newLockingPolicy(
                  LockingPolicy.MODE_CONTAINER, TransactionController.ISOLATION_SERIALIZABLE, true),
          true,
          this,
          new B2IUndo(),
          (B2IStaticCompiledInfo) null,
          (DynamicCompiledOpenConglomInfo) null);

      num_rows_loaded = b2ic.load(xact_manager, createConglom, rowSource);

    } finally {
      b2ic.close();
    }

    return (num_rows_loaded);
  }

  /**
   * Open a b-tree controller.
   *
   * @see Conglomerate#open
   * @exception StandardException Standard exception policy.
   */
  public ConglomerateController open(
      TransactionManager xact_manager,
      Transaction rawtran,
      boolean hold,
      int open_mode,
      int lock_level,
      LockingPolicy locking_policy,
      StaticCompiledOpenConglomInfo static_info,
      DynamicCompiledOpenConglomInfo dynamic_info)
      throws StandardException {
    // Create a new b-tree secondary index controller.
    B2IController b2ic = new B2IController();

    // Do the actual open of the container in the super class.
    b2ic.init(
        xact_manager, // current transaction
        rawtran, // current raw store transaction
        hold, // holdability
        open_mode,
        lock_level,
        locking_policy,
        true,
        this,
        new B2IUndo(),
        (B2IStaticCompiledInfo) static_info,
        dynamic_info);

    // Return it to the caller.
    return b2ic;
  }

  /**
   * Open a b-tree secondary index scan controller.
   *
   * @see Conglomerate#openScan
   * @see BTree#openScan
   * @exception StandardException Standard exception policy.
   */
  public ScanManager openScan(
      TransactionManager xact_manager,
      Transaction rawtran,
      boolean hold,
      int open_mode,
      int lock_level,
      LockingPolicy locking_policy,
      int isolation_level,
      FormatableBitSet scanColumnList,
      DataValueDescriptor[] startKeyValue,
      int startSearchOperator,
      Qualifier qualifier[][],
      DataValueDescriptor[] stopKeyValue,
      int stopSearchOperator,
      StaticCompiledOpenConglomInfo static_info,
      DynamicCompiledOpenConglomInfo dynamic_info)
      throws StandardException {
    // Create a new b-tree secondary index scan.
    B2IForwardScan b2is = new B2IForwardScan();

    // Initialize it.
    b2is.init(
        xact_manager,
        rawtran,
        hold,
        open_mode,
        lock_level,
        locking_policy,
        isolation_level,
        true /* get locks on base table as part of open */,
        scanColumnList,
        startKeyValue,
        startSearchOperator,
        qualifier,
        stopKeyValue,
        stopSearchOperator,
        this,
        new B2IUndo(),
        (B2IStaticCompiledInfo) static_info,
        dynamic_info);

    // Return it to the caller.
    return b2is;
  }

  /**
   * Open a b-tree compress scan.
   *
   * <p>B2I does not support a compress scan.
   *
   * <p>
   *
   * @see Conglomerate#defragmentConglomerate
   * @exception StandardException Standard exception policy.
   */
  public ScanManager defragmentConglomerate(
      TransactionManager xact_manager,
      Transaction rawtran,
      boolean hold,
      int open_mode,
      int lock_level,
      LockingPolicy locking_policy,
      int isolation_level)
      throws StandardException {
    throw StandardException.newException(SQLState.BTREE_UNIMPLEMENTED_FEATURE);
  }

  public void purgeConglomerate(TransactionManager xact_manager, Transaction rawtran)
      throws StandardException {
    // currently on work to do in btree's for purge rows, purging
    // happens best when split is about to happen.
    return;
  }

  public void compressConglomerate(TransactionManager xact_manager, Transaction rawtran)
      throws StandardException {
    B2IController b2ic = new B2IController();

    try {
      int open_mode = TransactionController.OPENMODE_FORUPDATE;

      // Do the actual open of the container in the super class.
      b2ic.init(
          xact_manager, // current transaction
          xact_manager.getRawStoreXact(), // current raw store xact
          false, // Not holdable
          open_mode,
          TransactionController.MODE_TABLE,
          xact_manager
              .getRawStoreXact()
              .newLockingPolicy(
                  LockingPolicy.MODE_CONTAINER, TransactionController.ISOLATION_SERIALIZABLE, true),
          true,
          this,
          new B2IUndo(),
          (B2IStaticCompiledInfo) null,
          (DynamicCompiledOpenConglomInfo) null);

      b2ic.getContainer().compressContainer();

    } finally {
      b2ic.close();
    }

    return;
  }

  /**
   * Return an open StoreCostController for the conglomerate.
   *
   * <p>Return an open StoreCostController which can be used to ask about the estimated row counts
   * and costs of ScanController and ConglomerateController operations, on the given conglomerate.
   *
   * <p>
   *
   * @param xact_manager The TransactionController under which this operation takes place.
   * @param rawtran raw transaction context in which scan is managed.
   * @return The open StoreCostController.
   * @exception StandardException Standard exception policy.
   * @see StoreCostController
   */
  public StoreCostController openStoreCost(TransactionManager xact_manager, Transaction rawtran)
      throws StandardException {
    B2ICostController b2icost = new B2ICostController();

    b2icost.init(xact_manager, this, rawtran);

    return (b2icost);
  }

  /**
   * Drop this b-tree secondary index.
   *
   * @see Conglomerate#drop
   * @see BTree#drop
   * @exception StandardException Standard exception policy.
   */
  public void drop(TransactionManager xact_manager) throws StandardException {
    // HACK to get around problem where index is dropped after the base
    // table.
    ConglomerateController base_cc = null;

    /* Get X table lock to make sure no thread is accessing index */
    base_cc =
        lockTable(
            xact_manager,
            TransactionController.OPENMODE_FORUPDATE,
            TransactionController.MODE_TABLE,
            TransactionController.ISOLATION_REPEATABLE_READ);

    xact_manager.getRawStoreXact().dropContainer(id);

    if (base_cc != null) base_cc.close();
  }

  /**
   * Return static information about the conglomerate to be included in a a compiled plan.
   *
   * <p>The static info would be valid until any ddl was executed on the conglomid, and would be up
   * to the caller to throw away when that happened. This ties in with what language already does
   * for other invalidation of static info. The type of info in this would be containerid and array
   * of format id's from which templates can be created. The info in this object is read only and
   * can be shared among as many threads as necessary.
   *
   * <p>
   *
   * @return The static compiled information.
   * @param conglomId The identifier of the conglomerate to open.
   * @exception StandardException Standard exception policy.
   */
  public StaticCompiledOpenConglomInfo getStaticCompiledConglomInfo(
      TransactionController xact_manager, long conglomId) throws StandardException {
    return (new B2IStaticCompiledInfo(xact_manager, this));
  }

  /*
   ** Methods of Storable (via Conglomerate via BTree).
   ** This class is responsible for re/storing its
   ** own state and calling its superclass to store its'.
   */

  /*
   * Storable interface, implies Externalizable, TypedFormat
   */

  /**
   * Return my format identifier.
   *
   * @see org.apache.derby.iapi.services.io.TypedFormat#getTypeFormatId
   */
  public int getTypeFormatId() {
    return StoredFormatIds.ACCESS_B2I_V5_ID;
  }

  /**
   * Store the stored representation of the column value in the stream.
   *
   * <p>For more detailed description of the ACCESS_B2I_V3_ID format see documentation at top of
   * file.
   *
   * @see java.io.Externalizable#writeExternal
   */
  public void writeExternal_v10_2(ObjectOutput out) throws IOException {
    super.writeExternal(out);
    out.writeLong(baseConglomerateId);
    out.writeInt(rowLocationColumn);

    // write the columns ascend/descend information as bits
    FormatableBitSet ascDescBits = new FormatableBitSet(ascDescInfo.length);

    for (int i = 0; i < ascDescInfo.length; i++) {
      if (ascDescInfo[i]) ascDescBits.set(i);
    }
    ascDescBits.writeExternal(out);
  }

  /**
   * Store the stored representation of the column value in the stream.
   *
   * <p>For more detailed description of the ACCESS_B2I_V3_ID and ACCESS_B2I_V4_ID formats see
   * documentation at top of file.
   *
   * @see java.io.Externalizable#writeExternal
   */
  public void writeExternal_v10_3(ObjectOutput out) throws IOException {
    // First part of ACCESS_B2I_V4_ID format is the ACCESS_B2I_V3_ID format.
    writeExternal_v10_2(out);
    if (conglom_format_id == StoredFormatIds.ACCESS_B2I_V4_ID
        || conglom_format_id == StoredFormatIds.ACCESS_B2I_V5_ID) {
      // Now append sparse array of collation ids
      ConglomerateUtil.writeCollationIdArray(collation_ids, out);
    }
  }

  /**
   * Store the stored representation of the column value in the stream.
   *
   * <p>For more detailed description of the ACCESS_B2I_V3_ID and ACCESS_B2I_V5_ID formats see
   * documentation at top of file.
   *
   * @see java.io.Externalizable#writeExternal
   */
  public void writeExternal(ObjectOutput out) throws IOException {
    writeExternal_v10_3(out);
    if (conglom_format_id == StoredFormatIds.ACCESS_B2I_V5_ID)
      out.writeBoolean(isUniqueWithDuplicateNulls());
  }

  /**
   * Restore the in-memory representation from the stream.
   *
   * <p>
   *
   * @exception ClassNotFoundException Thrown if the stored representation is serialized and a class
   *     named in the stream could not be found.
   * @see java.io.Externalizable#readExternal
   */
  private final void localReadExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    super.readExternal(in);
    baseConglomerateId = in.readLong();
    rowLocationColumn = in.readInt();

    // read the column sort order info
    FormatableBitSet ascDescBits = new FormatableBitSet();
    ascDescBits.readExternal(in);
    ascDescInfo = new boolean[ascDescBits.getLength()];
    for (int i = 0; i < ascDescBits.getLength(); i++) ascDescInfo[i] = ascDescBits.isSet(i);

    // In memory maintain a collation id per column in the template.
    collation_ids = new int[format_ids.length];
    if (SanityManager.DEBUG) {
      SanityManager.ASSERT(!hasCollatedTypes);
    }

    // initialize all the entries to COLLATION_TYPE_UCS_BASIC,
    // and then reset as necessary.  For version ACCESS_B2I_V3_ID,
    // this is the default and no resetting is necessary.
    for (int i = 0; i < format_ids.length; i++)
      collation_ids[i] = StringDataValue.COLLATION_TYPE_UCS_BASIC;

    // initialize the unique with null setting to false, to be reset
    // below when read from disk.  For version ACCESS_B2I_V3_ID and
    // ACCESS_B2I_V4_ID, this is the default and no resetting is necessary.
    setUniqueWithDuplicateNulls(false);

    if (conglom_format_id == StoredFormatIds.ACCESS_B2I_V4_ID
        || conglom_format_id == StoredFormatIds.ACCESS_B2I_V5_ID) {
      // current format id, read collation info from disk
      if (SanityManager.DEBUG) {
        // length must include row location column and at least
        // one other field.
        SanityManager.ASSERT(collation_ids.length >= 2, "length = " + collation_ids.length);
      }

      hasCollatedTypes = ConglomerateUtil.readCollationIdArray(collation_ids, in);
    } else if (conglom_format_id != StoredFormatIds.ACCESS_B2I_V3_ID) {
      // Currently only V3, V4 and V5 should be possible in a Derby DB.
      // Actual work for V3 is handled by default code above, so no
      // special work is necessary.

      if (SanityManager.DEBUG) {
        SanityManager.THROWASSERT("Unexpected format id: " + conglom_format_id);
      }
    }
    if (conglom_format_id == StoredFormatIds.ACCESS_B2I_V5_ID) {
      setUniqueWithDuplicateNulls(in.readBoolean());
    }
  }

  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    localReadExternal(in);
  }

  public void readExternalFromArray(ArrayInputStream in)
      throws IOException, ClassNotFoundException {
    localReadExternal(in);
  }
}