/** * 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.server.AkServerUtil; import com.foundationdb.server.rowdata.encoding.EncodingException; import com.foundationdb.server.types.TExecutionContext; import com.foundationdb.server.types.TInstance; import com.foundationdb.server.types.mcompat.mtypes.MBinary; import com.foundationdb.server.types.mcompat.mtypes.MString; import com.foundationdb.server.types.value.*; import com.foundationdb.server.types.value.UnderlyingType; import com.foundationdb.util.ByteSource; public final class RowDataBuilder { public void startAllocations() { state.require(State.NEWLY_CONSTRUCTED); // header byte[] bytes = rowData.getBytes(); AkServerUtil.putShort(bytes, fixedWidthSectionOffset + RowData.O_SIGNATURE_A, RowData.SIGNATURE_A); AkServerUtil.putInt(bytes, fixedWidthSectionOffset + RowData.O_ROW_DEF_ID, rowDef.getRowDefId()); AkServerUtil.putShort(bytes, fixedWidthSectionOffset + RowData.O_FIELD_COUNT, rowDef.getFieldCount()); fixedWidthSectionOffset = fixedWidthSectionOffset + RowData.O_NULL_MAP; nullMapOffset = fixedWidthSectionOffset; // TODO unloop this? for (int index = 0; index < rowDef.getFieldCount(); index += 8) { rowData.getBytes()[fixedWidthSectionOffset++] = 0; } fieldIndex = 0; assert fixedWidthSectionOffset == rowData.getRowStartData() : fixedWidthSectionOffset + " != " + rowData.getRowStartData(); state = State.ALLOCATING; } public void allocate(FieldDef fieldDef, Object object) { checkWithinRange(State.ALLOCATING); if (fieldDef != rowDef.getFieldDef(fieldIndex)) { throw new IllegalStateException(fieldDef + " but expected " + rowDef.getFieldDef(fieldIndex)); } if (fieldDef.getRowDef() != rowDef) { throw new IllegalStateException("expected RowDef " + rowDef + " but found " + fieldDef.getRowDef()); } final int fieldFixedWidth; if (fieldDef.isFixedSize()) { fieldFixedWidth = (object == null) ? 0 : fieldDef.getEncoding().widthFromObject(fieldDef, object); } else { int fieldMax = fieldDef.getMaxStorageSize(); vmax += fieldMax; if (object == null) { fieldFixedWidth = 0; } else { final int varWidth; try { varWidth = fieldDef.getEncoding().widthFromObject(fieldDef, object); vlength += varWidth; } catch (Exception e) { throw EncodingException.dueTo(e); } if (varWidth > fieldMax) { throw new EncodingException( String.format("Value for field %s has size %s, exceeding maximum allowed: %s", fieldDef.column(), varWidth, fieldMax)); } fieldFixedWidth = AkServerUtil.varWidth(vmax); byte[] bytes = rowData.getBytes(); switch (fieldFixedWidth) { case 0: break; case 1: AkServerUtil.putByte(bytes, fixedWidthSectionOffset, (byte) vlength); break; case 2: AkServerUtil.putShort(bytes, fixedWidthSectionOffset, (char) vlength); break; case 3: AkServerUtil.putMediumInt(bytes, fixedWidthSectionOffset, vlength); break; case 4: AkServerUtil.putInt(bytes, fixedWidthSectionOffset, vlength); break; default: throw new UnsupportedOperationException("bad width-of-width: " + fieldFixedWidth); } } } fieldWidths[fieldIndex] = fieldFixedWidth; fixedWidthSectionOffset += fieldFixedWidth; ++fieldIndex; } public void startPuts() { state.require(State.ALLOCATING); vlength = 0; nullRemainingAllocations(); variableWidthSectionOffset = fixedWidthSectionOffset; // rewind to the start of the fixed length portion for (int fieldWidth : fieldWidths) { fixedWidthSectionOffset -= fieldWidth; } fieldIndex = 0; state = State.PUTTING; } public void putObject(Object o) { FieldDef fieldDef = rowDef.getFieldDef(fieldIndex); adapter.objectToSource(o, fieldDef); convert(null, adapter, fieldDef); } private <S,T extends RowDataTarget> void convert(S source, ValueAdapter<S,T> valueAdapter, FieldDef fieldDef) { state.require(State.PUTTING); byte[] bytes = rowData.getBytes(); int currFixedWidth = fieldWidths[fieldIndex]; if (source == null) source = valueAdapter.source(); T target = valueAdapter.target(); if (valueAdapter.isNull(source)) { if (currFixedWidth != 0) { throw new IllegalStateException("expected source to give null: " + source); } target.bind(fieldDef, bytes, nullMapOffset); target.putNull(); if (target.lastEncodedLength() != 0) { throw new IllegalStateException("putting a null should have encoded 0 bytes"); } } else if (fieldDef.isFixedSize()) { target.bind(fieldDef, bytes, fixedWidthSectionOffset); valueAdapter.convert(fieldDef); if (target.lastEncodedLength() != currFixedWidth) { throw new IllegalStateException("expected to write " + currFixedWidth + " fixed-width byte(s), but wrote " + target.lastEncodedLength()); } } else { target.bind(fieldDef, bytes, variableWidthSectionOffset); valueAdapter.convert(fieldDef); int varWidthExpected = readVarWidth(bytes, currFixedWidth); // the stored value (retrieved by readVarWidth) is actually the *cumulative* length; we want just // this field's length. So, we'll subtract from this cumulative value the previously-maintained sum of the // previous variable-length fields, and use that for our comparison. Once that's done, we'll add this // field's length to that cumulative sum. varWidthExpected -= vlength; if (target.lastEncodedLength() != varWidthExpected) { throw new IllegalStateException("expected to write " + varWidthExpected + " variable-width byte(s), but wrote " + target.lastEncodedLength() + " (vlength=" + vlength + "). FieldDef=" + fieldDef + ", value = <" + source + '>'); } vlength += varWidthExpected; variableWidthSectionOffset += varWidthExpected; } fixedWidthSectionOffset += currFixedWidth; ++fieldIndex; } public int finalOffset() { state.require(State.PUTTING); nullRemainingPuts(adapter); // footer byte[] bytes = rowData.getBytes(); AkServerUtil.putShort(bytes, variableWidthSectionOffset, RowData.SIGNATURE_B); variableWidthSectionOffset += 6; int length = variableWidthSectionOffset - rowData.getRowStart(); AkServerUtil.putInt(bytes, rowData.getRowStart() + RowData.O_LENGTH_A, length); AkServerUtil.putInt(bytes, variableWidthSectionOffset + RowData.O_LENGTH_B, length); state = State.DONE; return variableWidthSectionOffset; } public RowDataBuilder(RowDef rowDef, RowData rowData) { // TODO argument validations this.rowDef = rowDef; this.rowData = rowData; fixedWidthSectionOffset = rowData.getRowStart(); state = State.NEWLY_CONSTRUCTED; this.fieldWidths = new int[rowDef.getFieldCount()]; this.adapter = new NewValueAdapter(); } private final ValueAdapter<?,?> adapter; private final RowDef rowDef; private final RowData rowData; private final int[] fieldWidths; private State state; private int fixedWidthSectionOffset; private int fieldIndex; private int nullMapOffset = -1; private int vmax; private int vlength; private int variableWidthSectionOffset; private void checkWithinRange(State requiredState) { state.require(requiredState); assert fieldIndex >= 0; if (fieldIndex >= rowDef.getFieldCount()) { throw new IllegalArgumentException("went past last field index of " + rowDef.getFieldCount()); } } private static abstract class ValueAdapter<S,T extends RowDataTarget> { public abstract void doConvert(S source, T target, FieldDef fieldDef); public abstract void objectToSource(Object object, FieldDef fieldDef); protected abstract S nullSource(FieldDef fieldDef); public abstract boolean isNull(S source); public void convert(FieldDef fieldDef) { try { doConvert(source, target, fieldDef); } catch (ArrayIndexOutOfBoundsException e) { throw EncodingException.dueTo(e); // assumed to be during writing to the RowData's byte[] } } public S source() { return source; } public T target() { return target; } protected ValueAdapter(S source, T target) { this.source = source; this.target = target; } private S source; private T target; } private static class NewValueAdapter extends ValueAdapter<ValueSource,RowDataValueTarget> { @Override public void doConvert(ValueSource source, RowDataValueTarget target, FieldDef fieldDef) { TInstance type = target.targetType(); if (stringInput != null) { // turn the string input into a ValueSource, then give it to TClass.fromObject. // Strings being inserted to binary types are a special, weird case. if (type.typeClass() instanceof MBinary) { target.putStringBytes(stringInput); return; } if (stringCache == null) stringCache = new Value(MString.VARCHAR.instance(Integer.MAX_VALUE, true)); stringCache.putString(stringInput, null); TExecutionContext context = new TExecutionContext(null, type, null); type.typeClass().fromObject(context, stringCache, value); } type.writeCanonical(value, target); } @Override public void objectToSource(Object object, FieldDef fieldDef) { TInstance underlying = underlying(fieldDef); value.underlying(underlying); stringInput = null; if (object == null) { ValueTargets.copyFrom(nullSource(fieldDef), value); } else if (object instanceof String) { // This is the common case, so let's test for it first if (TInstance.underlyingType(underlying) == UnderlyingType.STRING) value.putString((String)object, null); else stringInput = (String)object; } else { switch (TInstance.underlyingType(underlying)) { case INT_8: case INT_16: case UINT_16: case INT_32: case INT_64: if (object instanceof Number) ValueSources.valueFromLong(((Number) object).longValue(), value); break; case FLOAT: if (object instanceof Number) value.putFloat(((Number)object).floatValue()); break; case DOUBLE: if (object instanceof Number) value.putDouble(((Number)object).doubleValue()); break; case BYTES: if (object instanceof byte[]) value.putBytes((byte[])object); else if (object instanceof ByteSource) value.putBytes(((ByteSource)object).toByteSubarray()); break; case STRING: value.putString(object.toString(), null); break; case BOOL: if (object instanceof Boolean) value.putBool((Boolean)object); break; } if (!value.hasAnyValue()) { // last ditch effort! This is mostly to play nice with the loosey-goosy typing from types2. ValueCacher cacher = fieldDef.column().getType().typeClass().cacher(); if (cacher != null) object = cacher.sanitize(object); value.putObject(object); } } } @Override public ValueSource nullSource(FieldDef fieldDef) { return ValueSources.getNullSource(underlying(fieldDef)); } @Override public boolean isNull(ValueSource source) { return (stringInput == null) && source.isNull(); } private TInstance underlying(FieldDef fieldDef) { return fieldDef.column().getType(); } public NewValueAdapter() { this( new Value(), new RowDataValueTarget()); } public NewValueAdapter(Value value, RowDataValueTarget target) { super(value, target); this.value = value; } private String stringInput; private Value value; private Value stringCache; } private void nullRemainingAllocations() { int fieldsCount = rowDef.getFieldCount(); while ( fieldIndex < fieldsCount) { allocate(rowDef.getFieldDef(fieldIndex), null); } } private <S> void nullRemainingPuts(ValueAdapter<S,?> adapter) { int fieldsCount = rowDef.getFieldCount(); while ( fieldIndex < fieldsCount) { FieldDef fieldDef = rowDef.getFieldDef(fieldIndex); S source = adapter.nullSource(fieldDef); convert(source, adapter, fieldDef); } } private int readVarWidth(byte[] bytes, int widthWidth) { switch (widthWidth) { case 1: return AkServerUtil.getUByte(bytes, fixedWidthSectionOffset); case 2: return AkServerUtil.getUShort(bytes, fixedWidthSectionOffset); case 3: return AkServerUtil.getUMediumInt(bytes, fixedWidthSectionOffset); case 4: return AkServerUtil.getInt(bytes, fixedWidthSectionOffset); default: throw new UnsupportedOperationException("bad width-of-width: " + widthWidth); } } private enum State { NEWLY_CONSTRUCTED, ALLOCATING, PUTTING, DONE ; void require(State requiredState) { if (this != requiredState) { throw new IllegalStateException("required state " + requiredState + " but am currently " + name()); } } } }