/* This file is part of VoltDB.
* Copyright (C) 2008-2010 VoltDB L.L.C.
*
* VoltDB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VoltDB 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import org.voltdb.types.TimestampType;
import org.voltdb.types.VoltDecimalHelper;
/**
* <h3>Summary</h3>
*
* <p>Represents the interface to a row in a VoltTable result set.</p>
*
* <h3>Accessing Row Fields</h3>
*
* <p>Row fields are acessed via methods of the form
* <code>get</code><i><Type></i><code>(</code><i>col_index</i><code>|</code><i>col_name</i><code>)</code>.
* Note: it is more performant to access rows by column index than by column name.</p>
*
* <h3>Advancing and Iterating through a Table</h3>
*
* <p>VoltTableRow represents both a row in a table and an iterator for all the
* rows in the table. For a given table, each VoltTableRow instance has a position
* value which represents the index represented in the table. To increment the
* position, call {@link #advanceRow()}. To reset the position, call
* {@link #resetRowPosition()}, which moves the position before the first row.
* Note that before you can access fields after a call to resetRowPosition,
* advanceRow must be called to move to the first row.</p>
*
* <h3>Example</h3>
*
* <code>
* VoltTableRow row = table.fetchRow(5);<br/>
* System.out.println(row.getString("foo");<br/>
* row.resetRowPosition();<br/>
* while (row.advanceRow()) {<br/>
* System.out.println(row.getLong(7));<br/>
* }
* </code>
*/
public abstract class VoltTableRow {
/**
* Size in bytes of the maximum length for a VoltDB tuple.
* This is inclusive of the 4-byte row length prefix.
*
* 2 megs to allow a max size string/blob + length prefix + some other stuff
*/
public static final int MAX_TUPLE_LENGTH = 2097152;
public static final String MAX_TUPLE_LENGTH_STR = String.valueOf(MAX_TUPLE_LENGTH / 1024) + "k";
static final int ROW_HEADER_SIZE = Integer.SIZE/8;
static final int ROW_COUNT_SIZE = Integer.SIZE/8;
static final int STRING_LEN_SIZE = Integer.SIZE/8;
static final int INVALID_ROW_INDEX = -1;
/** Stores the row data (and possibly much more) */
protected ByteBuffer m_buffer;
/** Was the last value retrieved null? */
protected boolean m_wasNull = false;
/** Where in the buffer is the start of the active row's data */
protected int m_position = -1;
/** Offsets of each column in the buffer */
protected int[] m_offsets;
/** Have the offsets been calculated */
protected boolean m_hasCalculatedOffsets = false;
protected int m_activeRowIndex = INVALID_ROW_INDEX;
protected VoltTableRow() {}
/**
* Return the {@link VoltType type} of the column with the specified index.
* @param columnIndex Index of the column
* @return {@link VoltType VoltType} of the column
*/
protected abstract VoltType getColumnType(int columnIndex);
/**
* Return the index of the column with the specified index.
* @param columnName Name of the column
* @return Index of the column
*/
protected abstract int getColumnIndex(String columnName);
protected abstract boolean hasColumn(String columnName);
/**
* Returns the number of columns in the table schema
* @return Number of columns in the table schema
*/
protected abstract int getColumnCount();
/**
* Returns the number of rows.
* @return Number of rows in the table
*/
protected abstract int getRowCount();
protected abstract int getRowStart();
public abstract int getRowSize();
/**
* Clone a row. The new instance returned will have an independent
* position from the original instance.
* @return A deep copy of the current <tt>VoltTableRow</tt> instance.
*/
public abstract VoltTableRow cloneRow();
private final void ensureCalculatedOffsets() {
if (m_hasCalculatedOffsets == true)
return;
m_offsets[0] = m_position;
for (int i = 1, cnt = getColumnCount(); i < cnt; i++) {
final VoltType type = getColumnType(i - 1);
// handle variable length types specially
if (type == VoltType.STRING) {
final int strlen = m_buffer.getInt(m_offsets[i - 1]);
if (strlen == VoltTable.NULL_STRING_INDICATOR)
m_offsets[i] = m_offsets[i - 1] + STRING_LEN_SIZE;
else if (strlen < 0)
throw new RuntimeException("Invalid object length for column: " + i);
else
m_offsets[i] = m_offsets[i - 1] + strlen + STRING_LEN_SIZE;
}
else {
m_offsets[i] = m_offsets[i - 1] + type.getLengthInBytesForFixedTypes();
}
}
m_hasCalculatedOffsets = true;
}
protected final int getOffset(int index) {
ensureCalculatedOffsets();
assert(index >= 0);
assert(index < m_offsets.length);
return m_offsets[index];
}
/**
* Sets the active position indicator so that the next call
* to {@link #advanceRow()} will make the first record active.
* This never needs to be called if the table is only going to
* be scanned once. After this call {@link #getActiveRowIndex()}
* will return -1 until {@link #advanceRow()} is called.
*/
public void resetRowPosition() {
m_activeRowIndex = INVALID_ROW_INDEX;
}
/**
* Get the position in the table of this row instance, starting
* at zero for the first row.
* @return The index of the active row or -1 if none.
*/
public int getActiveRowIndex() {
return m_activeRowIndex;
}
/**
* Makes the next row active so calls to getXXX() will return
* values from the current record. At initialization time, the
* active row index is -1, which is invalid. If advanced past the
* end of the resultset, {@link #resetRowPosition()} must be
* called to re-iterate through the rows.
* @return True if a valid row became active. False otherwise.
*/
public boolean advanceRow() {
return advanceToRow(m_activeRowIndex + 1);
}
/**
* Advance to a specific row so calls to getXXX() will return values from
* the current record. At initialization time, the active row index is -1,
* which is invalid. If advanced past the end of the resultset,
* {@link #resetRowPosition()} must be called to re-iterate through the
* rows.
*
* @param rowIndex The row to jump to.
* @return True if a valid row became active. False otherwise.
*/
public boolean advanceToRow(int rowIndex) {
m_activeRowIndex = rowIndex;
if (m_activeRowIndex >= getRowCount())
return false;
m_hasCalculatedOffsets = false;
if (m_offsets == null)
m_offsets = new int[getColumnCount()];
if (m_activeRowIndex == 0)
m_position = getRowStart() + ROW_COUNT_SIZE + ROW_HEADER_SIZE;
else {
int rowlength = m_buffer.getInt(m_position - ROW_HEADER_SIZE);
if (rowlength <= 0) {
throw new RuntimeException("Invalid row length.");
}
m_position += rowlength + ROW_HEADER_SIZE;
if (m_position >= m_buffer.limit())
throw new RuntimeException("Row length exceeds table boundary.");
}
return true;
}
public Object get(int columnIndex) {
return (this.get(columnIndex, this.getColumnType(columnIndex)));
}
/**
* Retrieve a value from the row by specifying the column index and the {@link VoltType type}.
* This method is slower then linking directly against the type specific getter. Prefer the
* type specific getter methods where viable. Looking at the return value is not a reliable
* way to check if the value is <tt>null</tt>. Use {@link #wasNull()} instead.
* @param columnIndex Index of the column
* @param type {@link VoltType} of the value
* @return The value or <tt>null</tt> if the value is not set.
* @see #wasNull()
*/
public Object get(int columnIndex, VoltType type) {
Object ret = null;
switch (type) {
case TINYINT:
ret = Byte.valueOf((byte)getLong(columnIndex));
break;
case SMALLINT:
ret = Short.valueOf((short)getLong(columnIndex));
break;
case INTEGER:
ret = Integer.valueOf((int)getLong(columnIndex));
break;
case BIGINT:
ret = getLong(columnIndex);
break;
case FLOAT:
ret = getDouble(columnIndex);
break;
case STRING:
ret = getString(columnIndex);
break;
case TIMESTAMP:
ret = getTimestampAsTimestamp(columnIndex);
break;
case DECIMAL:
ret = getDecimalAsBigDecimal(columnIndex);
break;
case BOOLEAN:
ret = getBoolean(columnIndex);
break;
default:
throw new IllegalArgumentException("Invalid type '" + type + "'");
}
return ret;
}
/**
* Retrieve a value from the row by specifying the column name and the
* {@link VoltType type}. This method is slower then linking directly
* against the type specific getter. Prefer the type specific getter methods
* where viable. Looking at the return value is not a reliable way to check
* if the value is <tt>null</tt>. Use {@link #wasNull()} instead.
*
* @param columnName
* Name of the column
* @param type
* {@link VoltType} of the value
* @return The value or <tt>null</tt> if the value is not set.
* @see #wasNull()
*/
public Object get(String columnName, VoltType type) {
Object ret = null;
switch (type) {
case TINYINT:
ret = Byte.valueOf((byte) getLong(columnName));
break;
case SMALLINT:
ret = Short.valueOf((short) getLong(columnName));
break;
case INTEGER:
ret = Integer.valueOf((int) getLong(columnName));
break;
case BIGINT:
ret = getLong(columnName);
break;
case FLOAT:
ret = getDouble(columnName);
break;
case STRING:
ret = getString(columnName);
break;
case TIMESTAMP:
ret = getTimestampAsTimestamp(columnName);
break;
case DECIMAL:
ret = getDecimalAsBigDecimal(columnName);
break;
case BOOLEAN:
ret = getBoolean(columnName);
break;
default:
throw new IllegalArgumentException("Invalid type '" + type + "'");
}
return ret;
}
public boolean getBoolean(int columnIndex) {
return (this.getLong(columnIndex) != 0);
}
public boolean getBoolean(String columnName) {
return (this.getLong(columnName) != 0);
}
/**
* Retrieve the <tt>long</tt> value stored in the column specified by index.
* Looking at the return value is not a reliable way to check if the value
* is <tt>null</tt>. Use {@link #wasNull()} instead.
*
* @param columnIndex
* Index of the column
* @return <tt>long</tt> value stored in the specified column
* @see #wasNull()
*/
public long getLong(int columnIndex) {
if ((columnIndex >= getColumnCount()) || (columnIndex < 0)) {
throw new IndexOutOfBoundsException("Column index " + columnIndex + " is type greater than the number of columns");
}
final VoltType type = getColumnType(columnIndex);
if (m_activeRowIndex == INVALID_ROW_INDEX)
throw new RuntimeException("VoltTableRow.advanceRow() must be called to advance to the first row before any access.");
switch (type) {
case TINYINT:
case BOOLEAN:
final byte value1 = m_buffer.get(getOffset(columnIndex));
m_wasNull = (value1 == VoltType.NULL_TINYINT);
return value1;
case SMALLINT:
final short value2 = m_buffer.getShort(getOffset(columnIndex));
m_wasNull = (value2 == VoltType.NULL_SMALLINT);
return value2;
case INTEGER:
final int value3 = m_buffer.getInt(getOffset(columnIndex));
m_wasNull = (value3 == VoltType.NULL_INTEGER);
return value3;
case BIGINT:
final long value4 = m_buffer.getLong(getOffset(columnIndex));
m_wasNull = (value4 == VoltType.NULL_BIGINT);
return value4;
default:
throw new IllegalArgumentException("getLong() called on non-integral column.");
}
}
/**
* Retrieve the <tt>long</tt> value stored in the column
* specified by name. Avoid retrieving via this method as it is slower than specifying the
* column by index. Use {@link #getLong(int)} instead.
* Looking at the return value is not a reliable way to check if the value
* is <tt>null</tt>. Use {@link #wasNull()} instead.
* @param columnName Name of the column
* @return <tt>long</tt> value stored in the specified column
* @see #wasNull()
* @see #getLong(int)
*/
public long getLong(String columnName) {
final int colIndex = getColumnIndex(columnName);
return getLong(colIndex);
}
/**
* Returns whether last retrieved value was <tt>null</tt>.
* Some special values that are NOT Java's NULL represents <tt>null</tt> the SQL notion of <tt>null</tt>
* in our system.
* @return <tt>true</tt> if the value was <tt>null</tt>, false otherwise.
*/
public boolean wasNull() {
return m_wasNull;
}
/**
* Retrieve the <tt>double</tt> value stored in the column specified by index.
* Looking at the return value is not a reliable way to check if the value
* is <tt>null</tt>. Use {@link #wasNull()} instead.
* @param columnIndex Index of the column
* @return <tt>double</tt> value stored in the specified column
* @see #wasNull()
*/
public double getDouble(int columnIndex) {
validateColumnType(columnIndex, VoltType.FLOAT);
final double value = m_buffer.getDouble(getOffset(columnIndex));
m_wasNull = (value <= VoltType.NULL_FLOAT); // see value.h
return value;
}
/**
* Retrieve the <tt>double</tt> value stored in the column
* specified by name. Avoid retrieving via this method as it is slower than specifying the
* column by index. Use {@link #getDouble(int)} instead.
* Looking at the return value is not a reliable way to check if the value
* is <tt>null</tt>. Use {@link #wasNull()} instead.
* @param columnName Name of the column
* @return <tt>double</tt> value stored in the specified column
* @see #wasNull()
* @see #getDouble(int)
*/
public double getDouble(String columnName) {
final int colIndex = getColumnIndex(columnName);
return getDouble(colIndex);
}
/**
* Retrieve the {@link java.lang.String String} value stored in the column specified by index.
* Looking at the return value is not a reliable way to check if the value
* is <tt>null</tt>. Use {@link #wasNull()} instead. If at all possible you should use {@link #getStringAsBytes(int)} instead
* and avoid the overhead of constructing the {@link java.lang.String String} object.
* @param columnIndex Index of the column
* @return {@link java.lang.String String} value stored in the specified column
* @see #wasNull()
*/
public String getString(int columnIndex) {
validateColumnType(columnIndex, VoltType.STRING);
String retval = readString(getOffset(columnIndex), VoltTable.ROWDATA_ENCODING);
m_wasNull = (retval == null);
return retval;
}
/**
* Retrieve the {@link java.lang.String String} value stored in the column
* specified by name. Avoid retrieving via this method as it is slower than specifying the
* column by index. Use {@link #getString(int)} instead.
* Looking at the return value is not a reliable way to check if the value
* is <tt>null</tt>. Use {@link #wasNull()} instead. If at all possible you should use {@link #getStringAsBytes(int)} instead
* and avoid the overhead of constructing the {@link java.lang.String String} object.
* @param columnName Name of the column
* @return {@link java.lang.String String} value stored in the specified column
* @see #wasNull()
* @see #getString(int)
*/
public String getString(String columnName) {
final int colIndex = getColumnIndex(columnName);
return getString(colIndex);
}
/**
* Retrieve the <tt>string</tt> value stored in the column specified by index as
* an array of bytes. Assume UTF-8 encoding for all string values in VoltDB.
* Looking at the return value is not a reliable way to check if the value
* is <tt>null</tt>. Use {@link #wasNull()} instead.
* @param columnIndex Index of the column
* @return <tt>string</tt> value stored in the specified column as a <tt>byte[]</tt>
* @see #wasNull()
*/
public byte[] getStringAsBytes(int columnIndex) {
validateColumnType(columnIndex, VoltType.STRING);
int pos = m_buffer.position();
m_buffer.position(getOffset(columnIndex));
int len = m_buffer.getInt();
if (len == VoltTable.NULL_STRING_INDICATOR) {
m_wasNull = true;
m_buffer.position(pos);
return null;
}
m_wasNull = false;
byte[] data = new byte[len];
m_buffer.get(data);
m_buffer.position(pos);
return data;
}
/**
* Retrieve the <tt>string</tt> value stored in the column
* specified by name as an array of bytes. Assume UTF-8 encoding for all
* string values in VoltDB.Avoid retrieving via this method as it is slower
* than specifying the column by index. Use {@link #getStringAsBytes(int)} instead.
* Looking at the return value is not a reliable way to check if the value
* is <tt>null</tt>. Use {@link #wasNull()} instead.
* @param columnName Name of the column
* @return <tt>string</tt> value stored in the specified column as a <tt>byte[]</tt>
* @see #wasNull()
* @see #getStringAsBytes(int)
*/
public byte[] getStringAsBytes(String columnName) {
final int colIndex = getColumnIndex(columnName);
return getStringAsBytes(colIndex);
}
/**
* Retrieve the <tt>long</tt> timestamp stored in the column specified by index.
* Note that VoltDB uses GMT universally within its process space. Date objects sent over
* the wire from clients may seem to be different times because of this, but it is just
* a time zone offset. Timestamps represent microseconds from epoch.
* @param columnIndex Index of the column
* @return <tt>long</tt> timestamp value stored in the specified column
* @see #wasNull()
* @see #getTimestampAsTimestamp(int)
*/
public long getTimestampAsLong(int columnIndex) {
validateColumnType(columnIndex, VoltType.TIMESTAMP);
final long value = m_buffer.getLong(getOffset(columnIndex));
m_wasNull = (value == Long.MIN_VALUE); // see value.h
return value;
}
/**
* Retrieve the <tt>long</tt> timestamp value stored in the column
* specified by name. Note that VoltDB uses GMT universally within its
* process space. Date objects sent over the wire from clients may seem
* to be different times because of this, but it is just a time zone offset.
* Avoid retrieving via this method as it is slower than specifying the
* column by index. Use {@link #getTimestampAsLong(int)} instead.
* @param columnName Name of the column
* @return <tt>long</tt> timestamp value stored in the specified column
* @see #wasNull()
* @see #getTimestampAsLong(int)
* @see #getTimestampAsTimestamp(String)
*/
public long getTimestampAsLong(String columnName) {
final int colIndex = getColumnIndex(columnName);
return getTimestampAsLong(colIndex);
}
/**
* Retrieve the {@link org.voltdb.types.TimestampType TimestampType}
* value stored in the column specified by index.
* Note that VoltDB uses GMT universally within its process space. Date objects sent over
* the wire from clients may seem to be different times because of this, but it is just
* a time zone offset.
* @param columnIndex Index of the column
* @return {@link org.voltdb.types.TimestampType TimestampType}
* value stored in the specified column
* @see #wasNull()
*/
public TimestampType getTimestampAsTimestamp(int columnIndex) {
final long timestamp = getTimestampAsLong(columnIndex);
if (m_wasNull) return null;
return new TimestampType(timestamp);
}
/**
* Retrieve the {@link java.util.Date Date} value stored in the column
* specified by name. Note that VoltDB uses GMT universally within its
* process space. Date objects sent over the wire from clients may seem
* to be different times because of this, but it is just a time zone offset.
* Avoid retrieving via this method as it is slower than specifying the
* column by index. Use {@link #getTimestampAsTimestamp(int)} instead.
* @param columnName Name of the column
* @return {@link org.voltdb.types.TimestampType TimestampType}
* value stored in the specified column
* @see #wasNull()
* @see #getTimestampAsTimestamp(int)
*/
public TimestampType getTimestampAsTimestamp(String columnName) {
final int colIndex = getColumnIndex(columnName);
return getTimestampAsTimestamp(colIndex);
}
/**
* Retrieve the BigDecimal value stored in the column
* specified by the index. All DECIMAL types have a fixed
* scale when represented as BigDecimals.
* @param columnIndex Index of the column
* @return BigDecimal representation.
* @see #wasNull()
*/
public BigDecimal getDecimalAsBigDecimal(int columnIndex) {
validateColumnType(columnIndex, VoltType.DECIMAL);
final int position = m_buffer.position();
m_buffer.position(getOffset(columnIndex));
final BigDecimal bd = VoltDecimalHelper.deserializeBigDecimal(m_buffer);
m_buffer.position(position);
m_wasNull = bd == null ? true : false;
return bd;
}
/**
* Retrieve the BigDecimal value stored in the column
* specified by columnName. All DECIMAL types have a
* fixed scale when represented as BigDecimals.
* @param columnName Name of the column
* @return BigDecimal representation.
* @see #wasNull()
* @see #getDecimalAsBigDecimal(int)
*/
public BigDecimal getDecimalAsBigDecimal(String columnName) {
int colIndex = getColumnIndex(columnName);
return getDecimalAsBigDecimal(colIndex);
}
/** Validates that type and columnIndex match and are valid. */
protected final void validateColumnType(int columnIndex, VoltType... types) {
if (m_position < 0)
throw new RuntimeException("VoltTableRow is in an invalid state. Consider calling advanceRow().");
if ((columnIndex >= getColumnCount()) || (columnIndex < 0)) {
throw new IndexOutOfBoundsException("Column index " + columnIndex + " is type greater than the number of columns");
}
final VoltType columnType = getColumnType(columnIndex);
for (VoltType type : types)
if (columnType == type)
return;
throw new IllegalArgumentException("Column index " + columnIndex + " is type " + columnType);
}
/** Reads a string from a buffer with a specific encoding. */
protected final String readString(int position, String encoding) {
final int len = m_buffer.getInt(position);
//System.out.println(len);
// check for null string
if (len == VoltTable.NULL_STRING_INDICATOR)
return null;
if (len < 0) {
throw new RuntimeException("Invalid object length.");
}
// this is a bit slower than directly getting the array (see below)
// but that caused bugs
byte[] stringData = new byte[len];
int oldPos = m_buffer.position();
m_buffer.position(position + STRING_LEN_SIZE);
m_buffer.get(stringData);
m_buffer.position(oldPos);
String retval = null;
try {
retval = new String(stringData, encoding);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retval;
}
}