/** * Copyright (C) 2009-2013 FoundationDB, LLC * * 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 should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.rowdata; import com.foundationdb.ais.model.AkibanInformationSchema; import com.foundationdb.ais.model.Table; import com.foundationdb.server.AkServerUtil; import com.foundationdb.server.rowdata.encoding.EncodingException; import com.foundationdb.server.spatial.Spatial; import com.foundationdb.server.types.TInstance; import com.foundationdb.server.types.value.UnderlyingType; import com.foundationdb.util.AkibanAppender; import com.geophile.z.spatialobject.jts.JTSSpatialObject; /** * Represent one or more rows of table data. The backing store is a byte array * supplied in the constructor. The {@link #RowData(byte[], int, int)} * constructor allows use of a partially filled reusable buffer. * * This class provides methods for both interpreting and constructing row data * structures in the byte array. After a call to {@link #reset(int, int)} the * index will point to the first field of the first row represented in the * buffer. * * <pre> * +0: record length (Z) * +4: signature bytes, e.g., 'AB' * +6: field count (short) * +8: rowDefId (int) * +12: null-map (1 bit per schema-defined column) * +M: fixed-length field * +N: fixed-length field ... * +Q: variable-length field * +R: variable-length field ... * +Z-6: signature bytes: e.g., 'BA' * +Z-4: record length (Z) * </pre> * * @author peter */ public class RowData { public final static int O_LENGTH_A = 0; public final static int O_SIGNATURE_A = 4; public final static int O_FIELD_COUNT = 6; public final static int O_ROW_DEF_ID = 8; public final static int O_NULL_MAP = 12; public final static int O_SIGNATURE_B = -6; public final static int O_LENGTH_B = -4; public final static int MINIMUM_RECORD_LENGTH = 18; // Arbitrary sanity bound on maximum size public final static int MAXIMUM_RECORD_LENGTH = 8 * 1024 * 1024; public final static char SIGNATURE_A = (char) ('A' + ('B' << 8)); public final static char SIGNATURE_B = (char) ('B' + ('A' << 8)); public final static int ENVELOPE_SIZE = 12; public final static int LEFT_ENVELOPE_SIZE = 6; public final static int RIGHT_ENVELOPE_SIZE = 6; public final static int CREATE_ROW_INITIAL_SIZE = 500; private byte[] bytes; private int bufferStart; private int bufferEnd; private int rowStart; private int rowEnd; public RowData() { } public RowData(final byte[] bytes) { this.bytes = bytes; reset(0, bytes.length); } public RowData(final byte[] bytes, final int offset, final int length) { this.bytes = bytes; reset(offset, length); } public void reset(final int offset, final int length) { this.bufferStart = offset; this.bufferEnd = offset + length; rowStart = rowEnd = bufferStart; } public void reset(final byte[] bytes) { this.bytes = bytes; reset(0, bytes.length); } public void reset(final byte[] bytes, final int offset, final int length) { this.bytes = bytes; reset(offset, length); } /** * Interpret the length and signature fixed fields of the row at the * specified offset. This method sets the {@link #rowStart} and {@link #rowEnd} * fields so that subsequent calls to interpret fields are supported. * * @param offset * byte offset to record start within the buffer */ public boolean prepareRow(final int offset) throws CorruptRowDataException { if (offset == bufferEnd) { return false; } if (offset < 0 || offset + MINIMUM_RECORD_LENGTH > bufferEnd) { throw new CorruptRowDataException("Invalid offset: " + offset); } else { validateRow(offset); rowStart = offset; rowEnd = offset + AkServerUtil.getInt(bytes, O_LENGTH_A + offset); return true; } } public void validateRow(final int offset) throws CorruptRowDataException { if (offset < 0 || offset + MINIMUM_RECORD_LENGTH > bufferEnd) { throw new CorruptRowDataException("Invalid offset: " + offset); } else { final int recordLength = AkServerUtil.getInt(bytes, O_LENGTH_A + offset); if (recordLength < 0 || recordLength + offset > bufferEnd) { throw new CorruptRowDataException("Invalid record length: " + recordLength + " at offset: " + offset); } if (AkServerUtil.getUShort(bytes, O_SIGNATURE_A + offset) != SIGNATURE_A) { throw new CorruptRowDataException( "Invalid signature at offset: " + offset); } final int trailingLength = AkServerUtil.getInt(bytes, offset + recordLength + O_LENGTH_B); if (trailingLength != recordLength) { throw new CorruptRowDataException( "Invalid trailing record length " + trailingLength + " in record at offset: " + offset); } if (AkServerUtil.getUShort(bytes, offset + recordLength + O_SIGNATURE_B) != SIGNATURE_B) { throw new CorruptRowDataException( "Invalid signature at offset: " + offset); } } } public int getBufferStart() { return bufferStart; } public int getBufferLength() { return bufferEnd - bufferStart; } public int getBufferEnd() { return bufferEnd; } public int getRowStart() { return rowStart; } public int getRowStartData() { return rowStart + O_NULL_MAP + (getFieldCount() + 7) / 8; } public int getRowEnd() { return rowEnd; } public int getRowSize() { return rowEnd - rowStart; } public int getInnerStart() { return rowStart + O_FIELD_COUNT; } public int getInnerSize() { return rowEnd - rowStart + O_SIGNATURE_B - O_FIELD_COUNT; } public int getFieldCount() { return AkServerUtil.getUShort(bytes, rowStart + O_FIELD_COUNT); } public int getRowDefId() { return AkServerUtil.getInt(bytes, rowStart + O_ROW_DEF_ID); } public byte[] getBytes() { return bytes; } public int getColumnMapByte(final int offset) { return bytes[offset + rowStart + O_NULL_MAP] & 0xFF; } public boolean isNull(final int fieldIndex) { if (fieldIndex < 0 || fieldIndex >= getFieldCount()) { throw new IllegalArgumentException("No such field " + fieldIndex + " in " + this); } else { return (getColumnMapByte(fieldIndex / 8) & (1 << (fieldIndex % 8))) != 0; } } public long getIntegerValue(final int offset, final int width) { checkOffsetAndWidth(offset, width); return AkServerUtil.getSignedIntegerByWidth(bytes, offset, width); } public long getUnsignedIntegerValue(final int offset, final int width) { checkOffsetAndWidth(offset, width); return AkServerUtil.getUnsignedIntegerByWidth(bytes, offset, width); } public String getStringValue(final int offset, final int width, final FieldDef fieldDef) { if (offset == 0 && width == 0) { return null; } checkOffsetAndWidth(offset, width); return AkServerUtil.decodeMySQLString(bytes, offset, width, fieldDef); } private void checkOffsetAndWidth(int offset, int width) { if (offset < rowStart || offset + width >= rowEnd) { throw new IllegalArgumentException(String.format("Bad location: {offset=%d width=%d start=%d end=%d}", offset, width, rowStart, rowEnd)); } } public RowData copy() { byte[] copyBytes = new byte[rowEnd - rowStart]; System.arraycopy(bytes, rowStart, copyBytes, 0, rowEnd - rowStart); RowData copy = new RowData(copyBytes); copy.prepareRow(0); return copy; } public void createRow(final RowDef rowDef, final Object[] values, boolean growBuffer) { if (growBuffer && !(bufferStart == 0 && bufferEnd == bytes.length)) { // This RowData is embedded in a larger buffer. Can't grow it safely. throw new CannotGrowBufferException(); } RuntimeException exception = null; do { try { exception = null; createRow(rowDef, values); } catch (ArrayIndexOutOfBoundsException e) { exception = e; } catch (EncodingException e) { if (e.getCause() instanceof ArrayIndexOutOfBoundsException) { exception = e; } else { throw e; } } if (exception != null && growBuffer) { int newSize = bytes.length == 0 ? CREATE_ROW_INITIAL_SIZE : bytes.length * 2; reset(new byte[newSize]); } } while (growBuffer && exception != null); if (exception != null) { throw exception; } } public void createRow(final RowDef rowDef, final Object[] values) { if (values.length > rowDef.getFieldCount()) { throw new IllegalArgumentException("Too many values."); } // Serialize spatial objects Object[] valuesWithSpatialObjectsSerialized = new Object[values.length]; for (int i = 0; i < values.length; i++) { if (values[i] instanceof JTSSpatialObject) { UnderlyingType underlyingType = TInstance.underlyingType(rowDef.getFieldDef(i).column().getType()); switch (underlyingType) { case BYTES: valuesWithSpatialObjectsSerialized[i] = Spatial.serializeWKB((JTSSpatialObject) values[i]); break; case STRING: valuesWithSpatialObjectsSerialized[i] = Spatial.serializeWKT((JTSSpatialObject) values[i]); break; default: assert false : rowDef.getFieldDef(i); } } else { valuesWithSpatialObjectsSerialized[i] = values[i]; } } // RowDataBuilder builder = new RowDataBuilder(rowDef, this); builder.startAllocations(); for (int i=0; i < values.length; ++i) { FieldDef fieldDef = rowDef.getFieldDef(i); builder.allocate(fieldDef, valuesWithSpatialObjectsSerialized[i]); } builder.startPuts(); for (Object object : valuesWithSpatialObjectsSerialized) { builder.putObject(object); } rowEnd = builder.finalOffset(); } public void updateNonNullLong(FieldDef fieldDef, long rowId) { // Offset is in low 32 bits of fieldLocation return value int offset = (int) fieldDef.getRowDef().fieldLocation(this, fieldDef.getFieldIndex()); AkServerUtil.putLong(bytes, offset, rowId); } @Override public String toString() { return toString(RowDefBuilder.LATEST_FOR_DEBUGGING); } public String toString(AkibanInformationSchema ais) { if (ais == null) { return toStringWithoutRowDef("No AIS"); } int rowDefID = getRowDefId(); Table table = ais.getTable(rowDefID); if(table == null) { return toStringWithoutRowDef("Unknown RowDefID(" + rowDefID + ")"); } return toString(table.rowDef()); } public String toString(final RowDef rowDef) { if (rowDef == null) { return toStringWithoutRowDef("No RowDef provided"); } final AkibanAppender sb = AkibanAppender.of(new StringBuilder()); RowDataValueSource source = new RowDataValueSource(); try { sb.append(rowDef.table().getName().getTableName()); for (int i = 0; i < getFieldCount(); i++) { final FieldDef fieldDef = rowDef.getFieldDef(i); sb.append(i == 0 ? '(' : ','); final long location = fieldDef.getRowDef().fieldLocation( this, fieldDef.getFieldIndex()); if (location == 0) { // sb.append("null"); } else { source.bind(fieldDef, this); fieldDef.column().getType().format(source, sb); } } sb.append(')'); } catch (Exception e) { int size = Math.min(getRowSize(), 64); if (size > 0 && rowStart >= 0) { sb.append(AkServerUtil.dump(bytes, rowStart, size)); } return sb.toString(); } return sb.toString(); } /** Returns a hex-dump of the backing buffer. */ public String toStringWithoutRowDef(String missingRowDefExplanation) { final AkibanAppender sb = AkibanAppender.of(new StringBuilder()); try { sb.append("RowData["); sb.append(missingRowDefExplanation); sb.append("]?(rowDefId="); sb.append(getRowDefId()); sb.append(": "); AkServerUtil.hex(sb, bytes, rowStart, rowEnd - rowStart); } catch (Exception e) { int size = Math.min(getRowSize(), 64); if (size > 0 && rowStart >= 0) { sb.append(AkServerUtil.dump(bytes, rowStart, size)); } return sb.toString(); } return sb.toString(); } }