Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2020 QuestDB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
package io.questdb.cutlass.pgwire;
import io.questdb.MessageBus;
import io.questdb.Telemetry;
import io.questdb.cairo.*;
import io.questdb.cairo.pool.WriterSource;
import io.questdb.cairo.security.AllowAllCairoSecurityContext;
import io.questdb.cairo.sql.*;
import io.questdb.cutlass.text.TextLoader;
import io.questdb.cutlass.text.types.TypeManager;
import io.questdb.griffin.*;
import io.questdb.griffin.engine.functions.bind.BindVariableServiceImpl;
import io.questdb.log.Log;
import io.questdb.log.LogFactory;
import io.questdb.network.*;
import io.questdb.std.*;
import io.questdb.std.datetime.DateLocale;
import io.questdb.std.datetime.microtime.TimestampFormatUtils;
import io.questdb.std.str.*;
import org.jetbrains.annotations.Nullable;
import static io.questdb.cutlass.pgwire.PGOids.*;
import static io.questdb.std.datetime.millitime.DateFormatUtils.PG_DATE_MILLI_TIME_Z_FORMAT;
import static io.questdb.std.datetime.millitime.DateFormatUtils.PG_DATE_Z_FORMAT;
public class PGConnectionContext implements IOContext, Mutable, WriterSource {
public static final String TAG_SET = "SET";
public static final String TAG_BEGIN = "BEGIN";
public static final String TAG_COMMIT = "COMMIT";
public static final String TAG_ROLLBACK = "ROLLBACK";
public static final String TAG_SELECT = "SELECT";
public static final String TAG_OK = "OK";
public static final String TAG_COPY = "COPY";
public static final String TAG_INSERT = "INSERT";
public static final char STATUS_IN_TRANSACTION = 'T';
public static final char STATUS_IN_ERROR = 'E';
public static final char STATUS_IDLE = 'I';
private static final int INT_BYTES_X = Numbers.bswap(Integer.BYTES);
private static final int INT_NULL_X = Numbers.bswap(-1);
private static final int SYNC_PARSE = 1;
private static final int SYNC_DESCRIBE = 2;
private static final int SYNC_BIND = 3;
private static final byte MESSAGE_TYPE_ERROR_RESPONSE = 'E';
private static final int INIT_SSL_REQUEST = 80877103;
private static final int INIT_STARTUP_MESSAGE = 196608;
private static final int INIT_CANCEL_REQUEST = 80877102;
private static final byte MESSAGE_TYPE_COMMAND_COMPLETE = 'C';
private static final byte MESSAGE_TYPE_EMPTY_QUERY = 'I';
private static final byte MESSAGE_TYPE_DATA_ROW = 'D';
private static final byte MESSAGE_TYPE_READY_FOR_QUERY = 'Z';
private final static Log LOG = LogFactory.getLog(PGConnectionContext.class);
private static final int PREFIXED_MESSAGE_HEADER_LEN = 5;
private static final byte MESSAGE_TYPE_LOGIN_RESPONSE = 'R';
private static final byte MESSAGE_TYPE_PARAMETER_STATUS = 'S';
private static final byte MESSAGE_TYPE_ROW_DESCRIPTION = 'T';
private static final byte MESSAGE_TYPE_PARAMETER_DESCRIPTION = 't';
private static final byte MESSAGE_TYPE_PARSE_COMPLETE = '1';
private static final byte MESSAGE_TYPE_BIND_COMPLETE = '2';
private static final byte MESSAGE_TYPE_CLOSE_COMPLETE = '3';
private static final byte MESSAGE_TYPE_NO_DATA = 'n';
private static final byte MESSAGE_TYPE_COPY_IN_RESPONSE = 'G';
private static final int NO_TRANSACTION = 0;
private static final int IN_TRANSACTION = 1;
private static final int COMMIT_TRANSACTION = 2;
private static final int ERROR_TRANSACTION = 3;
private static final int ROLLING_BACK_TRANSACTION = 4;
private final long recvBuffer;
private final long sendBuffer;
private final int recvBufferSize;
private final CharacterStore characterStore;
private final BindVariableService bindVariableService;
private final long sendBufferLimit;
private final int sendBufferSize;
private final ResponseAsciiSink responseAsciiSink = new ResponseAsciiSink();
private final DirectByteCharSequence dbcs = new DirectByteCharSequence();
private final int maxBlobSizeOnQuery;
private final NetworkFacade nf;
private final boolean dumpNetworkTraffic;
private final int idleSendCountBeforeGivingUp;
private final int idleRecvCountBeforeGivingUp;
private final String serverVersion;
private final PGAuthenticator authenticator;
private final SqlExecutionContextImpl sqlExecutionContext;
private final Path path = new Path();
private final IntList bindVariableTypes = new IntList();
private final IntList selectColumnTypes = new IntList();
private final WeakObjectPool namedStatementWrapperPool;
private final WeakAutoClosableObjectPool typesAndInsertPool;
private final DateLocale locale;
private final CharSequenceObjHashMap pendingWriters;
private final DirectCharSink utf8Sink;
private final TypeManager typeManager;
private final AssociativeCache typesAndInsertCache;
private final CharSequenceObjHashMap namedStatementMap;
private final IntList syncActions = new IntList(4);
private final CairoEngine engine;
private IntList activeSelectColumnTypes;
private int parsePhaseBindVariableCount;
private long sendBufferPtr;
private boolean requireInitialMessage = false;
private long recvBufferWriteOffset = 0;
private long recvBufferReadOffset = 0;
private int bufferRemainingOffset = 0;
private int bufferRemainingSize = 0;
private RecordCursor currentCursor = null;
private RecordCursorFactory currentFactory = null;
// these references are held by context only for a period of processing single request
// in PF world this request can span multiple messages, but still, only for one request
// the rationale is to be able to return "selectAndTypes" instance to thread-local
// cache, which is "typesAndSelectCache". We typically do this after query results are
// served to client or query errored out due to network issues
private TypesAndSelect typesAndSelect = null;
private TypesAndInsert typesAndInsert = null;
private long fd;
private CharSequence queryText;
private CharSequence queryTag;
private CharSequence username;
private boolean authenticationRequired = true;
private IODispatcher dispatcher;
private Rnd rnd;
private long rowCount;
private boolean isEmptyQuery;
private int transactionState = NO_TRANSACTION;
private NamedStatementWrapper wrapper;
private AssociativeCache typesAndSelectCache;
private WeakAutoClosableObjectPool typesAndSelectPool;
// this is a reference to types either from the context or named statement, where it is provided
private IntList activeBindVariableTypes;
private boolean sendParameterDescription;
private PGResumeProcessor resumeProcessor;
private final PGResumeProcessor resumeCursorRef = this::resumeCursor;
public PGConnectionContext(
CairoEngine engine,
PGWireConfiguration configuration,
@Nullable MessageBus messageBus,
int workerCount
) {
this.engine = engine;
this.utf8Sink = new DirectCharSink(engine.getConfiguration().getTextConfiguration().getUtf8SinkSize());
this.typeManager = new TypeManager(engine.getConfiguration().getTextConfiguration(), utf8Sink);
this.nf = configuration.getNetworkFacade();
this.bindVariableService = new BindVariableServiceImpl(engine.getConfiguration());
this.recvBufferSize = Numbers.ceilPow2(configuration.getRecvBufferSize());
this.recvBuffer = Unsafe.malloc(this.recvBufferSize);
this.sendBufferSize = Numbers.ceilPow2(configuration.getSendBufferSize());
this.sendBuffer = Unsafe.malloc(this.sendBufferSize);
this.sendBufferPtr = sendBuffer;
this.sendBufferLimit = sendBuffer + sendBufferSize;
this.characterStore = new CharacterStore(
configuration.getCharacterStoreCapacity(),
configuration.getCharacterStorePoolCapacity()
);
this.maxBlobSizeOnQuery = configuration.getMaxBlobSizeOnQuery();
this.dumpNetworkTraffic = configuration.getDumpNetworkTraffic();
this.idleSendCountBeforeGivingUp = configuration.getIdleSendCountBeforeGivingUp();
this.idleRecvCountBeforeGivingUp = configuration.getIdleRecvCountBeforeGivingUp();
this.serverVersion = configuration.getServerVersion();
this.authenticator = new PGBasicAuthenticator(configuration.getDefaultUsername(), configuration.getDefaultPassword());
this.locale = configuration.getDefaultDateLocale();
this.sqlExecutionContext = new SqlExecutionContextImpl(engine, workerCount, messageBus);
this.sqlExecutionContext.setRandom(this.rnd = configuration.getRandom());
this.namedStatementWrapperPool = new WeakObjectPool<>(NamedStatementWrapper::new, configuration.getNamesStatementPoolCapacity()); // 16
this.typesAndInsertPool = new WeakAutoClosableObjectPool<>(TypesAndInsert::new, configuration.getInsertPoolCapacity()); // 32
this.typesAndInsertCache = new AssociativeCache<>(
configuration.getInsertCacheBlockCount(), // 8
configuration.getInsertCacheRowCount()
);
this.namedStatementMap = new CharSequenceObjHashMap<>(configuration.getNamedStatementCacheCapacity());
this.pendingWriters = new CharSequenceObjHashMap<>(configuration.getPendingWritersCacheSize());
}
public static int getInt(long address, long msgLimit, CharSequence errorMessage) throws BadProtocolException {
if (address + Integer.BYTES <= msgLimit) {
return getIntUnsafe(address);
}
LOG.error().$(errorMessage).$();
throw BadProtocolException.INSTANCE;
}
public static long getLongUnsafe(long address) {
return Numbers.bswap(Unsafe.getUnsafe().getLong(address));
}
public static short getShort(long address, long msgLimit, CharSequence errorMessage) throws BadProtocolException {
if (address + Short.BYTES <= msgLimit) {
return getShortUnsafe(address);
}
LOG.error().$(errorMessage).$();
throw BadProtocolException.INSTANCE;
}
public static long getStringLength(
long x,
long limit,
CharSequence errorMessage
) throws BadProtocolException {
long len = Unsafe.getUnsafe().getByte(x) == 0 ? x : getStringLengthTedious(x, limit);
if (len > -1) {
return len;
}
// we did not find 0 within message limit
LOG.error().$(errorMessage).$();
throw BadProtocolException.INSTANCE;
}
public static long getStringLengthTedious(long x, long limit) {
// calculate length
for (long i = x; i < limit; i++) {
if (Unsafe.getUnsafe().getByte(i) == 0) {
return i;
}
}
return -1;
}
public static void putInt(long address, int value) {
Unsafe.getUnsafe().putInt(address, Numbers.bswap(value));
}
public static void putLong(long address, long value) {
Unsafe.getUnsafe().putLong(address, Numbers.bswap(value));
}
public static void putShort(long address, short value) {
Unsafe.getUnsafe().putShort(address, Numbers.bswap(value));
}
@Override
public void clear() {
sendBufferPtr = sendBuffer;
requireInitialMessage = true;
bufferRemainingOffset = 0;
bufferRemainingSize = 0;
responseAsciiSink.reset();
prepareForNewQuery();
authenticationRequired = true;
username = null;
typeManager.clear();
clearWriters();
clearRecvBuffer();
typesAndInsertCache.clear();
namedStatementMap.clear();
bindVariableService.clear();
bindVariableTypes.clear();
}
public void clearWriters() {
for (int i = 0, n = pendingWriters.size(); i < n; i++) {
Misc.free(pendingWriters.valueQuick(i));
}
pendingWriters.clear();
}
@Override
public void close() {
clear();
this.fd = -1;
sqlExecutionContext.with(AllowAllCairoSecurityContext.INSTANCE, null, null, -1, null);
Unsafe.free(sendBuffer, sendBufferSize);
Unsafe.free(recvBuffer, recvBufferSize);
Misc.free(typesAndSelectCache);
Misc.free(path);
Misc.free(utf8Sink);
}
@Override
public long getFd() {
return fd;
}
@Override
public boolean invalid() {
return fd == -1;
}
@Override
public IODispatcher getDispatcher() {
return dispatcher;
}
@Override
public TableWriter getWriter(CairoSecurityContext context, CharSequence name) {
final int index = pendingWriters.keyIndex(name);
if (index < 0) {
return pendingWriters.valueAt(index);
}
return engine.getWriter(context, name);
}
public void handleClientOperation(
@Transient SqlCompiler compiler,
@Transient AssociativeCache selectAndTypesCache,
@Transient WeakAutoClosableObjectPool selectAndTypesPool,
int operation
) throws PeerDisconnectedException, PeerIsSlowToReadException, PeerIsSlowToWriteException, BadProtocolException {
this.typesAndSelectCache = selectAndTypesCache;
this.typesAndSelectPool = selectAndTypesPool;
try {
if (bufferRemainingSize > 0) {
doSend(bufferRemainingOffset, bufferRemainingSize);
if (resumeProcessor != null) {
resumeProcessor.resume();
}
}
boolean keepReceiving = true;
OUTER:
do {
if (operation == IOOperation.READ) {
if (recv() == 0) {
keepReceiving = false;
}
}
// we do not pre-compute length because 'parse' will mutate 'recvBufferReadOffset'
if (keepReceiving) {
do {
// Parse will update the value of recvBufferOffset upon completion of
// logical block. We cannot count on return value because 'parse' may try to
// respond to client and fail with exception. When it does fail we would have
// to retry 'send' but not parse the same input again
long readOffsetBeforeParse = recvBufferReadOffset;
parse(
recvBuffer + recvBufferReadOffset,
(int) (recvBufferWriteOffset - recvBufferReadOffset),
compiler
);
// nothing changed?
if (readOffsetBeforeParse == recvBufferReadOffset) {
// shift to start
if (readOffsetBeforeParse > 0) {
shiftReceiveBuffer(readOffsetBeforeParse);
}
continue OUTER;
}
} while (recvBufferReadOffset < recvBufferWriteOffset);
clearRecvBuffer();
}
} while (keepReceiving&&operation==IOOperation.READ);
} catch (SqlException e) {
reportError(e.getPosition(), e.getFlyweightMessage());
} catch (CairoException e) {
reportError(-1, e.getFlyweightMessage());
}
}
public PGConnectionContext of(long clientFd, IODispatcher dispatcher) {
this.fd = clientFd;
sqlExecutionContext.with(clientFd);
this.dispatcher = dispatcher;
clear();
return this;
}
public void setBooleanBindVariable(int index, int valueLen) throws SqlException {
if (valueLen != 4 && valueLen != 5) {
throw SqlException.$(0, "bad value for BOOLEAN parameter [index=").put(index).put(", valueLen=").put(valueLen).put(']');
}
bindVariableService.setBoolean(index, valueLen == 4);
}
public void setCharBindVariable(int index, long address, int valueLen) throws BadProtocolException, SqlException {
CharacterStoreEntry e = characterStore.newEntry();
if (Chars.utf8Decode(address, address + valueLen, e)) {
bindVariableService.setChar(index, characterStore.toImmutable().charAt(0));
} else {
LOG.error().$("invalid char UTF8 bytes [index=").$(index).$(']').$();
throw BadProtocolException.INSTANCE;
}
}
public void setDateBindVariable(int index, long address, int valueLen) throws SqlException {
dbcs.of(address, address + valueLen);
try {
bindVariableService.setDate(index, PG_DATE_Z_FORMAT.parse(dbcs, locale));
} catch (NumericException ex) {
throw SqlException.$(0, "bad parameter value [index=").put(index).put(", value=").put(dbcs).put(']');
}
}
public void setDoubleBindVariable(int index, long address, int valueLen) throws BadProtocolException, SqlException {
ensureValueLength(index, Double.BYTES, valueLen);
bindVariableService.setDouble(index, Double.longBitsToDouble(getLongUnsafe(address)));
}
public void setFloatBindVariable(int index, long address, int valueLen) throws BadProtocolException, SqlException {
ensureValueLength(index, Float.BYTES, valueLen);
bindVariableService.setFloat(index, Float.intBitsToFloat(getIntUnsafe(address)));
}
public void setIntBindVariable(int index, long address, int valueLen) throws BadProtocolException, SqlException {
ensureValueLength(index, Integer.BYTES, valueLen);
bindVariableService.setInt(index, getIntUnsafe(address));
}
public void setLongBindVariable(int index, long address, int valueLen) throws BadProtocolException, SqlException {
ensureValueLength(index, Long.BYTES, valueLen);
bindVariableService.setLong(index, getLongUnsafe(address));
}
public void setShortBindVariable(int index, long address, int valueLen) throws BadProtocolException, SqlException {
ensureValueLength(index, Short.BYTES, valueLen);
bindVariableService.setShort(index, getShortUnsafe(address));
}
public void setStrBindVariable(int index, long address, int valueLen) throws BadProtocolException, SqlException {
CharacterStoreEntry e = characterStore.newEntry();
if (Chars.utf8Decode(address, address + valueLen, e)) {
bindVariableService.setStr(index, characterStore.toImmutable());
} else {
LOG.error().$("invalid str UTF8 bytes [index=").$(index).$(']').$();
throw BadProtocolException.INSTANCE;
}
}
public void setTimestampBindVariable(int index, long address, int valueLen) throws BadProtocolException, SqlException {
ensureValueLength(index, Long.BYTES, valueLen);
bindVariableService.setTimestamp(index, getLongUnsafe(address) + Numbers.JULIAN_EPOCH_OFFSET_USEC);
}
private static int getIntUnsafe(long address) {
return Numbers.bswap(Unsafe.getUnsafe().getInt(address));
}
private static short getShortUnsafe(long address) {
return Numbers.bswap(Unsafe.getUnsafe().getShort(address));
}
private static void ensureValueLength(int index, int required, int actual) throws BadProtocolException {
if (required == actual) {
return;
}
LOG.error()
.$("bad parameter value length [required=").$(required)
.$(", actual=").$(actual)
.$(", index=").$(index)
.I$();
throw BadProtocolException.INSTANCE;
}
private static void prepareParams(PGConnectionContext.ResponseAsciiSink sink, String name, String value) {
sink.put(MESSAGE_TYPE_PARAMETER_STATUS);
final long addr = sink.skip();
sink.encodeUtf8Z(name);
sink.encodeUtf8Z(value);
sink.putLen(addr);
}
private static void bindParameterFormats(
long lo,
long msgLimit,
short parameterFormatCount,
IntList bindVariableTypes
) throws BadProtocolException {
if (lo + Short.BYTES * parameterFormatCount <= msgLimit) {
LOG.debug().$("processing bind formats [count=").$(parameterFormatCount).$(']').$();
for (int i = 0; i < parameterFormatCount; i++) {
final short code = getShortUnsafe(lo + i * Short.BYTES);
bindVariableTypes.setQuick(i, toParamBinaryType(code, bindVariableTypes.getQuick(i)));
}
} else {
LOG.error().$("invalid format code count [value=").$(parameterFormatCount).$(']').$();
throw BadProtocolException.INSTANCE;
}
}
private static void setupBindVariables(long lo, IntList bindVariableTypes, int count) {
bindVariableTypes.setPos(count);
for (int i = 0; i < count; i++) {
bindVariableTypes.setQuick(i, Unsafe.getUnsafe().getInt(lo + i * 4L));
}
}
private static void bindSingleFormatForAll(long lo, long msgLimit, IntList activeBindVariableTypes) throws BadProtocolException {
short code = getShort(lo, msgLimit, "could not read parameter formats");
for (int i = 0, n = activeBindVariableTypes.size(); i < n; i++) {
activeBindVariableTypes.setQuick(i, toParamBinaryType(code, activeBindVariableTypes.getQuick(i)));
}
}
private void appendBinColumn(Record record, int i) throws SqlException {
BinarySequence sequence = record.getBin(i);
if (sequence == null) {
responseAsciiSink.setNullValue();
} else {
// if length is above max we will error out the result set
long blobSize = sequence.length();
if (blobSize < maxBlobSizeOnQuery) {
responseAsciiSink.put(sequence);
} else {
throw SqlException.position(0)
.put("blob is too large [blobSize=").put(blobSize)
.put(", max=").put(maxBlobSizeOnQuery)
.put(", columnIndex=").put(i)
.put(']');
}
}
}
private void appendBooleanColumn(Record record, int columnIndex) {
responseAsciiSink.putNetworkInt(Byte.BYTES);
responseAsciiSink.put(record.getBool(columnIndex) ? 't' : 'f');
}
private void appendByteColumn(Record record, int columnIndex) {
long a = responseAsciiSink.skip();
responseAsciiSink.put((int) record.getByte(columnIndex));
responseAsciiSink.putLenEx(a);
}
private void appendByteColumnBin(Record record, int columnIndex) {
final byte value = record.getByte(columnIndex);
responseAsciiSink.putNetworkInt(Short.BYTES);
responseAsciiSink.putNetworkShort(value);
}
private void appendCharColumn(Record record, int columnIndex) {
long a = responseAsciiSink.skip();
responseAsciiSink.putUtf8(record.getChar(columnIndex));
responseAsciiSink.putLenEx(a);
}
private void appendDateColumn(Record record, int columnIndex) {
final long longValue = record.getDate(columnIndex);
if (longValue != Numbers.LONG_NaN) {
final long a = responseAsciiSink.skip();
PG_DATE_MILLI_TIME_Z_FORMAT.format(longValue, null, null, responseAsciiSink);
responseAsciiSink.putLenEx(a);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendDateColumnBin(Record record, int columnIndex) {
final long longValue = record.getLong(columnIndex);
if (longValue != Numbers.LONG_NaN) {
responseAsciiSink.putNetworkInt(Long.BYTES);
// PG epoch starts at 2000 rather than 1970
responseAsciiSink.putNetworkLong(longValue * 1000 - Numbers.JULIAN_EPOCH_OFFSET_USEC);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendDoubleColumn(Record record, int columnIndex) {
final double doubleValue = record.getDouble(columnIndex);
if (doubleValue == doubleValue) {
final long a = responseAsciiSink.skip();
responseAsciiSink.put(doubleValue);
responseAsciiSink.putLenEx(a);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendDoubleColumnBin(Record record, int columnIndex) {
final double value = record.getDouble(columnIndex);
if (value == value) {
responseAsciiSink.putNetworkInt(Double.BYTES);
responseAsciiSink.putNetworkDouble(value);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendFloatColumn(Record record, int columnIndex) {
final float floatValue = record.getFloat(columnIndex);
if (floatValue == floatValue) {
final long a = responseAsciiSink.skip();
responseAsciiSink.put(floatValue, 3);
responseAsciiSink.putLenEx(a);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendFloatColumnBin(Record record, int columnIndex) {
final float value = record.getFloat(columnIndex);
if (value == value) {
responseAsciiSink.putNetworkInt(Float.BYTES);
responseAsciiSink.putNetworkFloat(value);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendIntCol(Record record, int i) {
final int intValue = record.getInt(i);
if (intValue != Numbers.INT_NaN) {
final long a = responseAsciiSink.skip();
responseAsciiSink.put(intValue);
responseAsciiSink.putLenEx(a);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendIntColumnBin(Record record, int columnIndex) {
final int value = record.getInt(columnIndex);
if (value != Numbers.INT_NaN) {
responseAsciiSink.ensureCapacity(8);
responseAsciiSink.putIntUnsafe(0, INT_BYTES_X);
responseAsciiSink.putIntUnsafe(4, Numbers.bswap(value));
responseAsciiSink.bump(8);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendLong256Column(Record record, int columnIndex) {
final Long256 long256Value = record.getLong256A(columnIndex);
final long a = responseAsciiSink.skip();
Numbers.appendLong256(long256Value.getLong0(), long256Value.getLong1(), long256Value.getLong2(), long256Value.getLong3(), responseAsciiSink);
responseAsciiSink.putLenEx(a);
}
private void appendLongColumn(Record record, int columnIndex) {
final long longValue = record.getLong(columnIndex);
if (longValue != Numbers.LONG_NaN) {
final long a = responseAsciiSink.skip();
responseAsciiSink.put(longValue);
responseAsciiSink.putLenEx(a);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendLongColumnBin(Record record, int columnIndex) {
final long longValue = record.getLong(columnIndex);
if (longValue != Numbers.LONG_NaN) {
responseAsciiSink.putNetworkInt(Long.BYTES);
responseAsciiSink.putNetworkLong(longValue);
} else {
responseAsciiSink.setNullValue();
}
}
private void appendRecord(Record record, int columnCount) throws SqlException {
responseAsciiSink.put(MESSAGE_TYPE_DATA_ROW); // data
final long offset = responseAsciiSink.skip();
responseAsciiSink.putNetworkShort((short) columnCount);
for (int i = 0; i < columnCount; i++) {
switch (activeSelectColumnTypes.getQuick(i)) {
case BINARY_TYPE_INT:
appendIntColumnBin(record, i);
break;
case ColumnType.INT:
appendIntCol(record, i);
break;
case ColumnType.STRING:
case BINARY_TYPE_STRING:
appendStrColumn(record, i);
break;
case ColumnType.SYMBOL:
case BINARY_TYPE_SYMBOL:
appendSymbolColumn(record, i);
break;
case BINARY_TYPE_LONG:
appendLongColumnBin(record, i);
break;
case ColumnType.LONG:
appendLongColumn(record, i);
break;
case ColumnType.SHORT:
appendShortColumn(record, i);
break;
case BINARY_TYPE_DOUBLE:
appendDoubleColumnBin(record, i);
break;
case ColumnType.DOUBLE:
appendDoubleColumn(record, i);
break;
case BINARY_TYPE_FLOAT:
appendFloatColumnBin(record, i);
break;
case BINARY_TYPE_SHORT:
appendShortColumnBin(record, i);
break;
case BINARY_TYPE_DATE:
appendDateColumnBin(record, i);
break;
case BINARY_TYPE_TIMESTAMP:
appendTimestampColumnBin(record, i);
break;
case BINARY_TYPE_BYTE:
appendByteColumnBin(record, i);
break;
case ColumnType.FLOAT:
appendFloatColumn(record, i);
break;
case ColumnType.TIMESTAMP:
appendTimestampColumn(record, i);
break;
case ColumnType.DATE:
appendDateColumn(record, i);
break;
case ColumnType.BOOLEAN:
case BINARY_TYPE_BOOLEAN:
appendBooleanColumn(record, i);
break;
case ColumnType.BYTE:
appendByteColumn(record, i);
break;
case ColumnType.BINARY:
case BINARY_TYPE_BINARY:
appendBinColumn(record, i);
break;
case ColumnType.CHAR:
case BINARY_TYPE_CHAR:
appendCharColumn(record, i);
break;
case ColumnType.LONG256:
case BINARY_TYPE_LONG256:
appendLong256Column(record, i);
break;
default:
assert false;
}
}
responseAsciiSink.putLen(offset);
}
private void appendShortColumn(Record record, int columnIndex) {
final long a = responseAsciiSink.skip();
responseAsciiSink.put(record.getShort(columnIndex));
responseAsciiSink.putLenEx(a);
}
private void appendShortColumnBin(Record record, int columnIndex) {
final short value = record.getShort(columnIndex);
responseAsciiSink.putNetworkInt(Short.BYTES);
responseAsciiSink.putNetworkShort(value);
}
private void appendStrColumn(Record record, int columnIndex) {
final CharSequence strValue = record.getStr(columnIndex);
if (strValue == null) {
responseAsciiSink.setNullValue();
} else {
final long a = responseAsciiSink.skip();
responseAsciiSink.encodeUtf8(strValue);
responseAsciiSink.putLenEx(a);
}
}
private void appendSymbolColumn(Record record, int columnIndex) {
final CharSequence strValue = record.getSym(columnIndex);
if (strValue == null) {
responseAsciiSink.setNullValue();
} else {
final long a = responseAsciiSink.skip();
responseAsciiSink.encodeUtf8(strValue);
responseAsciiSink.putLenEx(a);
}
}
private void appendTimestampColumn(Record record, int i) {
long a;
long longValue = record.getTimestamp(i);
if (longValue == Numbers.LONG_NaN) {
responseAsciiSink.setNullValue();
} else {
a = responseAsciiSink.skip();
TimestampFormatUtils.PG_TIMESTAMP_FORMAT.format(longValue, null, null, responseAsciiSink);
responseAsciiSink.putLenEx(a);
}
}
private void appendTimestampColumnBin(Record record, int columnIndex) {
final long longValue = record.getLong(columnIndex);
if (longValue == Numbers.LONG_NaN) {
responseAsciiSink.setNullValue();
} else {
responseAsciiSink.putNetworkInt(Long.BYTES);
// PG epoch starts at 2000 rather than 1970
responseAsciiSink.putNetworkLong(longValue - Numbers.JULIAN_EPOCH_OFFSET_USEC);
}
}
private void assertTrue(boolean check, String message) throws BadProtocolException {
if (check) {
return;
}
// we did not find 0 within message limit
LOG.error().$(message).$();
throw BadProtocolException.INSTANCE;
}
private long bindValuesAsStrings(long lo, long msgLimit, short parameterValueCount) throws BadProtocolException, SqlException {
for (int j = 0; j < parameterValueCount; j++) {
final int valueLen = getInt(lo, msgLimit, "malformed bind variable");
lo += Integer.BYTES;
if (valueLen != -1 && lo + valueLen <= msgLimit) {
setStrBindVariable(j, lo, valueLen);
lo += valueLen;
} else if (valueLen != -1) {
LOG.error()
.$("value length is outside of buffer [parameterIndex=").$(j)
.$(", valueLen=").$(valueLen)
.$(", messageRemaining=").$(msgLimit - lo)
.$(']').$();
throw BadProtocolException.INSTANCE;
}
}
return lo;
}
private long bindValuesUsingSetters(
long lo,
long msgLimit,
short parameterValueCount
) throws BadProtocolException, SqlException {
for (int j = 0; j < parameterValueCount; j++) {
final int valueLen = getInt(lo, msgLimit, "malformed bind variable");
lo += Integer.BYTES;
if (valueLen == -1) {
// this is null we have already defaulted parameters to
continue;
}
if (lo + valueLen <= msgLimit) {
switch (activeBindVariableTypes.getQuick(j)) {
case X_B_PG_INT4:
setIntBindVariable(j, lo, valueLen);
break;
case X_B_PG_INT8:
setLongBindVariable(j, lo, valueLen);
break;
case X_B_PG_TIMESTAMP:
setTimestampBindVariable(j, lo, valueLen);
break;
case X_B_PG_INT2:
setShortBindVariable(j, lo, valueLen);
break;
case X_B_PG_FLOAT8:
setDoubleBindVariable(j, lo, valueLen);
break;
case X_B_PG_FLOAT4:
setFloatBindVariable(j, lo, valueLen);
break;
case X_B_PG_CHAR:
setCharBindVariable(j, lo, valueLen);
break;
case X_B_PG_DATE:
setDateBindVariable(j, lo, valueLen);
break;
case X_B_PG_BOOL:
setBooleanBindVariable(j, valueLen);
break;
default:
setStrBindVariable(j, lo, valueLen);
break;
}
lo += valueLen;
} else {
LOG.error()
.$("value length is outside of buffer [parameterIndex=").$(j)
.$(", valueLen=").$(valueLen)
.$(", messageRemaining=").$(msgLimit - lo)
.$(']').$();
throw BadProtocolException.INSTANCE;
}
}
return lo;
}
private void buildSelectColumnTypes() {
final RecordMetadata m = typesAndSelect.getFactory().getMetadata();
final int columnCount = m.getColumnCount();
activeSelectColumnTypes.setPos(columnCount);
for (int i = 0; i < columnCount; i++) {
activeSelectColumnTypes.setQuick(i, m.getColumnType(i));
}
}
void clearRecvBuffer() {
recvBufferWriteOffset = 0;
recvBufferReadOffset = 0;
}
private boolean compileQuery(@Transient SqlCompiler compiler)
throws SqlException, PeerDisconnectedException, PeerIsSlowToReadException {
if (queryText != null && queryText.length() > 0) {
// try insert, peek because this is our private cache
// and we do not want to remove statement from it
typesAndInsert = typesAndInsertCache.peek(queryText);
// not found or not insert, try select
// poll this cache because it is shared and we do not want
// select factory to be used by another thread concurrently
if (typesAndInsert != null) {
typesAndInsert.defineBindVariables(bindVariableService);
queryTag = TAG_INSERT;
return false;
}
typesAndSelect = typesAndSelectCache.poll(queryText);
if (typesAndSelect != null) {
// cache hit, define bind variables
bindVariableService.clear();
typesAndSelect.defineBindVariables(bindVariableService);
queryTag = TAG_SELECT;
return false;
}
// not cached - compile to see what it is
final CompiledQuery cc = compiler.compile(queryText, sqlExecutionContext);
sqlExecutionContext.storeTelemetry(cc.getType(), Telemetry.ORIGIN_POSTGRES);
switch (cc.getType()) {
case CompiledQuery.SELECT:
typesAndSelect = typesAndSelectPool.pop();
typesAndSelect.of(cc.getRecordCursorFactory(), bindVariableService);
queryTag = TAG_SELECT;
LOG.debug().$("cache select [sql=").$(queryText).$(", thread=").$(Thread.currentThread().getId()).$(']').$();
break;
case CompiledQuery.INSERT:
queryTag = TAG_INSERT;
typesAndInsert = typesAndInsertPool.pop();
typesAndInsert.of(cc.getInsertStatement(), bindVariableService);
if (bindVariableService.getIndexedVariableCount() > 0) {
LOG.debug().$("cache insert [sql=").$(queryText).$(", thread=").$(Thread.currentThread().getId()).$(']').$();
// we can add insert to cache right away because it is local to the connection
typesAndInsertCache.put(queryText, typesAndInsert);
}
break;
case CompiledQuery.COPY_LOCAL:
// uncached
queryTag = TAG_COPY;
sendCopyInResponse(compiler.getEngine(), cc.getTextLoader());
break;
case CompiledQuery.SET:
configureContextForSet();
break;
default:
// DDL SQL
queryTag = TAG_OK;
break;
}
} else {
isEmptyQuery = true;
}
return true;
}
private void configureContextForSet() {
if (SqlKeywords.isBegin(queryText)) {
queryTag = TAG_BEGIN;
transactionState = IN_TRANSACTION;
} else if (SqlKeywords.isCommit(queryText)) {
queryTag = TAG_COMMIT;
if (transactionState != ERROR_TRANSACTION) {
transactionState = COMMIT_TRANSACTION;
}
} else if (SqlKeywords.isRollback(queryText)) {
queryTag = TAG_ROLLBACK;
transactionState = ROLLING_BACK_TRANSACTION;
} else {
queryTag = TAG_SET;
}
}
private void configureContextFromNamedStatement(long lo, long hi, @Nullable @Transient SqlCompiler compiler)
throws BadProtocolException, SqlException, PeerDisconnectedException, PeerIsSlowToReadException {
final CharSequence statementName = getStatementName(lo, hi);
this.sendParameterDescription = statementName != null;
if (wrapper != null) {
LOG.debug().$("reusing existing wrapper").$();
return;
}
// make sure there is no current wrapper is set, so that we don't assign values
// from the wrapper back to context on the first pass where named statement is setup
if (statementName != null) {
LOG.debug().$("named statement [name=").$(statementName).$(']').$();
wrapper = namedStatementMap.get(statementName);
if (wrapper != null) {
setupVariableSettersFromWrapper(wrapper, compiler);
} else {
// todo: when we have nothing for prepared statement name we need to produce an error
LOG.error().$("statement does not exist [name=").$(statementName).$(']').$();
throw BadProtocolException.INSTANCE;
}
}
}
private void configurePreparedStatement(CharSequence statementName) throws BadProtocolException {
// this is a PARSE message asking us to setup named SQL
// we need to keep SQL text in case our SQL cache expires
// as well as PG types of the bind variables, which we will need to configure setters
int index = namedStatementMap.keyIndex(statementName);
if (index > -1) {
wrapper = namedStatementWrapperPool.pop();
wrapper.queryText = Chars.toString(queryText);
wrapper.tag = queryTag;
namedStatementMap.putAt(index, Chars.toString(statementName), wrapper);
this.activeBindVariableTypes = wrapper.bindVariableTypes;
this.activeSelectColumnTypes = wrapper.selectColumnTypes;
} else {
LOG.error().$("duplicate statement [name=").$(statementName).$(']').$();
throw BadProtocolException.INSTANCE;
}
}
private void doAuthentication(long msgLo, long msgLimit) throws BadProtocolException, PeerDisconnectedException, PeerIsSlowToReadException, SqlException {
final CairoSecurityContext cairoSecurityContext = authenticator.authenticate(username, msgLo, msgLimit);
if (cairoSecurityContext != null) {
sqlExecutionContext.with(cairoSecurityContext, bindVariableService, rnd, this.fd, null);
authenticationRequired = false;
prepareLoginOk();
sendAndReset();
}
}
int doReceive(int remaining) {
final long data = recvBuffer + recvBufferWriteOffset;
final int n = nf.recv(getFd(), data, remaining);
dumpBuffer('>', data, n);
return n;
}
void doSend(int offset, int size) throws PeerDisconnectedException, PeerIsSlowToReadException {
final int n = nf.send(getFd(), sendBuffer + offset, size);
dumpBuffer('<', sendBuffer + offset, n);
if (n < 0) {
throw PeerDisconnectedException.INSTANCE;
}
if (n < size) {
doSendWithRetries(offset + n, size - n);
}
sendBufferPtr = sendBuffer;
bufferRemainingSize = 0;
bufferRemainingOffset = 0;
}
private void doSendWithRetries(int bufferOffset, int bufferSize) throws PeerDisconnectedException, PeerIsSlowToReadException {
int offset = bufferOffset;
int remaining = bufferSize;
int idleSendCount = 0;
while (remaining > 0 && idleSendCount < idleSendCountBeforeGivingUp) {
int m = nf.send(
getFd(),
sendBuffer + offset,
remaining
);
if (m < 0) {
throw PeerDisconnectedException.INSTANCE;
}
dumpBuffer('<', sendBuffer + offset, m);
if (m > 0) {
remaining -= m;
offset += m;
} else {
idleSendCount++;
}
}
if (remaining > 0) {
bufferRemainingOffset = offset;
bufferRemainingSize = remaining;
throw PeerIsSlowToReadException.INSTANCE;
}
}
private void dumpBuffer(char direction, long buffer, int len) {
if (dumpNetworkTraffic && len > 0) {
StdoutSink.INSTANCE.put(direction);
Net.dump(buffer, len);
}
}
private void executeInsert() {
final TableWriter w;
try {
switch (transactionState) {
case IN_TRANSACTION:
final InsertMethod m = typesAndInsert.getInsert().createMethod(sqlExecutionContext, this);
try {
rowCount = m.execute();
w = m.popWriter();
pendingWriters.put(w.getTableName(), w);
} catch (Throwable e) {
Misc.free(m);
throw e;
}
break;
case ERROR_TRANSACTION:
// when transaction is in error state, skip execution
break;
default:
// in any other case we will commit in place
try (final InsertMethod m2 = typesAndInsert.getInsert().createMethod(sqlExecutionContext, this)) {
rowCount = m2.execute();
m2.commit();
}
break;
}
prepareCommandComplete(true);
} catch (Throwable e) {
if (transactionState == IN_TRANSACTION) {
transactionState = ERROR_TRANSACTION;
}
throw e;
}
}
private void executeTag() {
LOG.debug().$("executing [tag=").$(queryTag).$(']').$();
if (queryTag != null && TAG_OK != queryTag) { //do not run this for OK tag (i.e.: create table)
executeTag0();
}
}
private void executeTag0() {
switch (transactionState) {
case COMMIT_TRANSACTION:
try {
for (int i = 0, n = pendingWriters.size(); i < n; i++) {
final TableWriter m = pendingWriters.valueQuick(i);
m.commit();
Misc.free(m);
}
} finally {
pendingWriters.clear();
transactionState = NO_TRANSACTION;
}
break;
case ROLLING_BACK_TRANSACTION:
try {
for (int i = 0, n = pendingWriters.size(); i < n; i++) {
final TableWriter m = pendingWriters.valueQuick(i);
m.rollback();
Misc.free(m);
}
} finally {
pendingWriters.clear();
transactionState = NO_TRANSACTION;
}
break;
default:
break;
}
}
private CharSequence getStatement0(long lo, long hi) throws BadProtocolException {
CharacterStoreEntry e = characterStore.newEntry();
if (Chars.utf8Decode(lo, hi, e)) {
return characterStore.toImmutable();
} else {
LOG.error().$("invalid UTF8 bytes in statement name").$();
throw BadProtocolException.INSTANCE;
}
}
@Nullable
private CharSequence getStatementName(long lo, long hi) throws BadProtocolException {
if (hi - lo > 0) {
return getStatement0(lo, hi);
}
return null;
}
/**
* returns address of where parsing stopped. If there are remaining bytes left
* int the buffer they need to be passed again in parse function along with
* any additional bytes received
*/
private void parse(long address, int len, @Transient SqlCompiler compiler)
throws PeerDisconnectedException, PeerIsSlowToReadException, BadProtocolException, SqlException {
if (requireInitialMessage) {
processInitialMessage(address, len);
return;
}
// this is a type-prefixed message
// we will wait until we receive the entire header
if (len < PREFIXED_MESSAGE_HEADER_LEN) {
// we need to be able to read header and length
return;
}
final byte type = Unsafe.getUnsafe().getByte(address);
final int msgLen = getIntUnsafe(address + 1);
LOG.debug()
.$("received msg [type=").$((char) type)
.$(", len=").$(msgLen)
.$(']').$();
if (msgLen < 1) {
LOG.error().$("invalid message length [type=").$(type).$(", msgLen=").$(msgLen).$(']').$();
throw BadProtocolException.INSTANCE;
}
// msgLen does not take into account type byte
if (msgLen > len - 1) {
// When this happens we need to shift our receive buffer left
// to fit this message. Outer function will do that if we
// just exit.
LOG.debug()
.$("not enough data in buffer [expected=").$(msgLen)
.$(", have=").$(len)
.$(", recvBufferWriteOffset=").$(recvBufferWriteOffset)
.$(", recvBufferReadOffset=").$(recvBufferReadOffset)
.$(']').$();
return;
}
// we have enough to read entire message
recvBufferReadOffset += msgLen + 1;
final long msgLimit = address + msgLen + 1;
final long msgLo = address + PREFIXED_MESSAGE_HEADER_LEN; // 8 is offset where name value pairs begin
if (authenticationRequired) {
doAuthentication(msgLo, msgLimit);
return;
}
switch (type) {
case 'P':
processParse(
address,
msgLo,
msgLimit,
compiler
);
break;
case 'X':
// 'Terminate'
throw PeerDisconnectedException.INSTANCE;
case 'C':
// close
processClose(msgLo, msgLimit);
break;
case 'B': // bind
processBind(msgLo, msgLimit, compiler);
break;
case 'E': // execute
processExec();
break;
case 'S': // sync
processSyncActions();
prepareReadyForQuery();
prepareForNewQuery();
// fall thru
case 'H': // flush
sendAndReset();
break;
case 'D': // describe
processDescribe(msgLo, msgLimit, compiler);
break;
case 'Q':
processQuery(msgLo, msgLimit, compiler);
break;
case 'd':
System.out.println("data " + msgLen);
// msgLen includes 4 bytes of self
break;
default:
LOG.error().$("unknown message [type=").$(type).$(']').$();
throw BadProtocolException.INSTANCE;
}
}
private void parseQueryText(long lo, long hi, @Transient SqlCompiler compiler)
throws BadProtocolException, PeerDisconnectedException, PeerIsSlowToReadException, SqlException {
CharacterStoreEntry e = characterStore.newEntry();
if (Chars.utf8Decode(lo, hi, e)) {
queryText = characterStore.toImmutable();
LOG.info().$("parse [q=").utf8(queryText).$(']').$();
compileQuery(compiler);
return;
}
LOG.error().$("invalid UTF8 bytes in parse query").$();
throw BadProtocolException.INSTANCE;
}
private void prepareBindComplete() {
responseAsciiSink.put(MESSAGE_TYPE_BIND_COMPLETE);
responseAsciiSink.putIntDirect(INT_BYTES_X);
}
private void prepareCloseComplete() {
responseAsciiSink.put(MESSAGE_TYPE_CLOSE_COMPLETE);
responseAsciiSink.putIntDirect(INT_BYTES_X);
}
void prepareCommandComplete(boolean addRowCount) {
if (isEmptyQuery) {
LOG.debug().$("empty").$();
responseAsciiSink.put(MESSAGE_TYPE_EMPTY_QUERY);
responseAsciiSink.putIntDirect(INT_BYTES_X);
} else {
responseAsciiSink.put(MESSAGE_TYPE_COMMAND_COMPLETE);
long addr = responseAsciiSink.skip();
if (addRowCount) {
if (queryTag == TAG_INSERT) {
LOG.debug().$("insert [rowCount=").$(rowCount).$(']').$();
responseAsciiSink.encodeUtf8(queryTag).put(" 0 ").put(rowCount).put((char) 0);
} else {
LOG.debug().$("other [rowCount=").$(rowCount).$(']').$();
responseAsciiSink.encodeUtf8(queryTag).put(' ').put(rowCount).put((char) 0);
}
} else {
LOG.debug().$("now row count").$();
responseAsciiSink.encodeUtf8(queryTag).put((char) 0);
}
responseAsciiSink.putLen(addr);
}
}
private void prepareDescribeResponse() {
// only send parameter description when we have named statement
if (sendParameterDescription) {
prepareParameterDescription();
}
if (typesAndSelect != null) {
try {
prepareRowDescription();
} catch (NoSpaceLeftInResponseBufferException ignored) {
LOG.error().$("not enough space in buffer for row description [buffer=").$(sendBufferSize).I$();
responseAsciiSink.reset();
throw CairoException.instance(0).put("server configuration error: not enough space in send buffer for row description");
}
} else {
prepareNoDataMessage();
}
}
private void prepareError(int position, CharSequence message) {
responseAsciiSink.put(MESSAGE_TYPE_ERROR_RESPONSE);
long addr = responseAsciiSink.skip();
responseAsciiSink.put('C');
responseAsciiSink.encodeUtf8Z("00000");
responseAsciiSink.put('M');
responseAsciiSink.encodeUtf8Z(message);
responseAsciiSink.put('S');
responseAsciiSink.encodeUtf8Z("ERROR");
if (position > -1) {
responseAsciiSink.put('P').put(position + 1).put((char) 0);
}
responseAsciiSink.put((char) 0);
responseAsciiSink.putLen(addr);
LOG.error().$("error [pos=").$(position).$(", msg=`").$(message).$("`]").$();
}
private void prepareForNewQuery() {
LOG.debug().$("prepare for new query").$();
isEmptyQuery = false;
characterStore.clear();
bindVariableService.clear();
currentCursor = Misc.free(currentCursor);
typesAndInsert = null;
rowCount = 0;
queryTag = TAG_OK;
queryText = null;
wrapper = null;
syncActions.clear();
sendParameterDescription = false;
}
private void prepareLoginOk() {
responseAsciiSink.put(MESSAGE_TYPE_LOGIN_RESPONSE);
responseAsciiSink.putNetworkInt(Integer.BYTES * 2); // length of this message
responseAsciiSink.putIntDirect(0); // response code
prepareParams(responseAsciiSink, "TimeZone", "GMT");
prepareParams(responseAsciiSink, "application_name", "QuestDB");
prepareParams(responseAsciiSink, "server_version", serverVersion);
prepareParams(responseAsciiSink, "integer_datetimes", "on");
prepareParams(responseAsciiSink, "client_encoding", "UTF8");
prepareReadyForQuery();
}
private void prepareLoginResponse() {
responseAsciiSink.put(MESSAGE_TYPE_LOGIN_RESPONSE);
responseAsciiSink.putNetworkInt(Integer.BYTES * 2);
responseAsciiSink.putNetworkInt(3);
}
private void prepareNoDataMessage() {
responseAsciiSink.put(MESSAGE_TYPE_NO_DATA);
responseAsciiSink.putIntDirect(INT_BYTES_X);
}
private void prepareParameterDescription() {
responseAsciiSink.put(MESSAGE_TYPE_PARAMETER_DESCRIPTION);
final long l = responseAsciiSink.skip();
final int n = bindVariableService.getIndexedVariableCount();
responseAsciiSink.putNetworkShort((short) n);
if (n > 0) {
for (int i = 0; i < n; i++) {
responseAsciiSink.putIntDirect(toParamType(activeBindVariableTypes.getQuick(i)));
}
}
responseAsciiSink.putLen(l);
}
private void prepareParseComplete() {
responseAsciiSink.put(MESSAGE_TYPE_PARSE_COMPLETE);
responseAsciiSink.putIntDirect(INT_BYTES_X);
}
void prepareReadyForQuery() {
responseAsciiSink.put(MESSAGE_TYPE_READY_FOR_QUERY);
responseAsciiSink.putNetworkInt(Integer.BYTES + Byte.BYTES);
switch (transactionState) {
case IN_TRANSACTION:
responseAsciiSink.put(STATUS_IN_TRANSACTION);
break;
case ERROR_TRANSACTION:
responseAsciiSink.put(STATUS_IN_ERROR);
break;
default:
responseAsciiSink.put(STATUS_IDLE);
break;
}
}
private void prepareRowDescription() {
final RecordMetadata metadata = typesAndSelect.getFactory().getMetadata();
ResponseAsciiSink sink = responseAsciiSink;
sink.put(MESSAGE_TYPE_ROW_DESCRIPTION);
final long addr = sink.skip();
final int n = activeSelectColumnTypes.size();
sink.putNetworkShort((short) n);
for (int i = 0; i < n; i++) {
final int typeFlag = activeSelectColumnTypes.getQuick(i);
final int columnType = toColumnType(typeFlag);
sink.encodeUtf8Z(metadata.getColumnName(i));
sink.putIntDirect(0); //tableOid ?
sink.putNetworkShort((short) (i + 1)); //column number, starting from 1
sink.putNetworkInt(TYPE_OIDS.get(columnType)); // type
if (columnType < ColumnType.STRING) {
// type size
// todo: cache small endian type sizes and do not check if type is valid - its coming from metadata, must be always valid
sink.putNetworkShort((short) ColumnType.sizeOf(columnType));
} else {
// type size
sink.putNetworkShort((short) -1);
}
//type modifier
sink.putIntDirect(INT_NULL_X);
// this is special behaviour for binary fields to prevent binary data being hex encoded on the wire
// format code
sink.putNetworkShort(typeFlag == ColumnType.BINARY ? 1 : getColumnBinaryFlag(typeFlag)); // format code
}
sink.putLen(addr);
}
private void prepareSslResponse() {
responseAsciiSink.put('N');
}
private void processBind(long lo, long msgLimit, @Transient SqlCompiler compiler)
throws BadProtocolException, SqlException, PeerDisconnectedException, PeerIsSlowToReadException {
long hi;
short parameterFormatCount;
short parameterValueCount;
LOG.debug().$("bind").$();
// portal name
hi = getStringLength(lo, msgLimit, "bad portal name length [msgType='B']");
// named statement
lo = hi + 1;
hi = getStringLength(lo, msgLimit, "bad prepared statement name length [msgType='B']");
configureContextFromNamedStatement(lo, hi, compiler);
//parameter format count
lo = hi + 1;
parameterFormatCount = getShort(lo, msgLimit, "could not read parameter format code count");
lo += Short.BYTES;
if (parameterFormatCount > 0) {
if (parameterFormatCount == 1) {
// same format applies to all parameters
bindSingleFormatForAll(lo, msgLimit, activeBindVariableTypes);
} else if (parameterFormatCount == parsePhaseBindVariableCount) {
bindParameterFormats(lo, msgLimit, parameterFormatCount, activeBindVariableTypes);
}
}
// parameter value count
lo += parameterFormatCount * Short.BYTES;
parameterValueCount = getShort(lo, msgLimit, "could not read parameter value count");
LOG.debug().$("binding [parameterValueCount=").$(parameterValueCount).$(", thread=").$(Thread.currentThread().getId()).$(']').$();
//we now have all parameter counts, validate them
validateParameterCounts(parameterFormatCount, parameterValueCount, parsePhaseBindVariableCount);
lo += Short.BYTES;
if (parameterValueCount > 0) {
if (this.parsePhaseBindVariableCount == parameterValueCount) {
lo = bindValuesUsingSetters(lo, msgLimit, parameterValueCount);
} else {
lo = bindValuesAsStrings(lo, msgLimit, parameterValueCount);
}
}
if (typesAndSelect != null) {
short columnFormatCodeCount = getShort(lo, msgLimit, "could not read result set column format codes");
if (columnFormatCodeCount > 0) {
final RecordMetadata m = typesAndSelect.getFactory().getMetadata();
final int columnCount = m.getColumnCount();
// apply format codes to the cursor column types
// but check if there is message is consistent
final long spaceNeeded = lo + (columnFormatCodeCount + 1) * Short.BYTES;
if (spaceNeeded <= msgLimit) {
if (columnFormatCodeCount == columnCount) {
// good to go
for (int i = 0; i < columnCount; i++) {
lo += Short.BYTES;
activeSelectColumnTypes.setQuick(i, toColumnBinaryType(getShortUnsafe(lo), m.getColumnType(i)));
}
} else if (columnFormatCodeCount == 1) {
final short code = getShortUnsafe(lo);
for (int i = 0; i < columnCount; i++) {
activeSelectColumnTypes.setQuick(i, toColumnBinaryType(code, m.getColumnType(i)));
}
} else {
LOG.error()
.$("could not process column format codes [fmtCount=").$(columnFormatCodeCount)
.$(", columnCount=").$(columnCount)
.$(']').$();
throw BadProtocolException.INSTANCE;
}
} else {
LOG.error()
.$("could not process column format codes [bufSpaceNeeded=").$(spaceNeeded)
.$(", bufSpaceAvail=").$(msgLimit)
.$(']').$();
throw BadProtocolException.INSTANCE;
}
}
}
syncActions.add(SYNC_BIND);
}
private void processClose(long lo, long msgLimit) throws BadProtocolException {
final byte type = Unsafe.getUnsafe().getByte(lo);
switch (type) {
case 'S':
lo = lo + 1;
final long hi = getStringLength(lo, msgLimit, "bad prepared statement name length");
final CharSequence statementName = getStatementName(lo, hi);
if (statementName != null) {
final int index = namedStatementMap.keyIndex(statementName);
if (index < 0) {
namedStatementWrapperPool.push(namedStatementMap.valueAt(index));
namedStatementMap.removeAt(index);
} else {
LOG.error().$("invalid statement name [value=").$(statementName).$(']').$();
throw BadProtocolException.INSTANCE;
}
}
break;
case 'P':
LOG.info().$("close message for portal - ignoring").$();
break;
default:
LOG.error().$("invalid type for close message [type=").$(type).$(']').$();
throw BadProtocolException.INSTANCE;
}
prepareCloseComplete();
}
private void processDescribe(long lo, long msgLimit, @Transient SqlCompiler compiler)
throws SqlException, BadProtocolException, PeerDisconnectedException, PeerIsSlowToReadException {
LOG.debug().$("describe").$();
configureContextFromNamedStatement(
lo + 1,
getStringLength(lo + 1, msgLimit, "bad portal name length [msgType='D']"),
compiler
);
// initialize activeBindVariableTypes from bind variable service
final int n = bindVariableService.getIndexedVariableCount();
if (sendParameterDescription && n > 0 && activeBindVariableTypes.size() == 0) {
activeBindVariableTypes.setPos(n);
for (int i = 0; i < n; i++) {
activeBindVariableTypes.setQuick(i, Numbers.bswap(TYPE_OIDS.getQuick(bindVariableService.getFunction(i).getType())));
}
}
syncActions.add(SYNC_DESCRIBE);
}
private void processExec() throws PeerDisconnectedException, PeerIsSlowToReadException, SqlException {
LOG.debug().$("execute").$();
processSyncActions();
processExecute();
wrapper = null;
}
private void processExecute() throws PeerDisconnectedException, PeerIsSlowToReadException, SqlException {
if (typesAndSelect != null) {
LOG.debug().$("executing query").$();
setupFactoryAndCursor();
// cache random if it was replaced
this.rnd = sqlExecutionContext.getRandom();
sendCursor();
} else if (typesAndInsert != null) {
LOG.debug().$("executing insert").$();
executeInsert();
} else { //this must be a OK/SET/COMMIT/ROLLBACK or empty query
executeTag();
prepareCommandComplete(false);
}
}
private void processInitialMessage(long address, int len) throws PeerDisconnectedException, PeerIsSlowToReadException, BadProtocolException {
int msgLen;
long msgLimit;// expect startup request
if (len < Long.BYTES) {
return;
}
// there is data for length
// this is quite specific to message type :(
msgLen = getIntUnsafe(address); // postgresql includes length bytes in length of message
// do we have the rest of the message?
if (msgLen > len) {
// we have length - get the rest when ready
return;
}
// enough to read login request
recvBufferReadOffset += msgLen;
// consume message
// process protocol
int protocol = getIntUnsafe(address + Integer.BYTES);
switch (protocol) {
case INIT_SSL_REQUEST:
// SSLRequest
prepareSslResponse();
sendAndReset();
return;
case INIT_STARTUP_MESSAGE:
// StartupMessage
// extract properties
requireInitialMessage = false;
msgLimit = address + msgLen;
long lo = address + Long.BYTES;
// there is an extra byte at the end and it has to be 0
LOG.info()
.$("protocol [major=").$(protocol >> 16)
.$(", minor=").$((short) protocol)
.$(']').$();
while (lo < msgLimit - 1) {
final long nameLo = lo;
final long nameHi = getStringLength(lo, msgLimit, "malformed property name");
lo = nameHi + 1;
final long valueLo = lo;
final long valueHi = getStringLength(valueLo, msgLimit, "malformed property value");
// store user
dbcs.of(nameLo, nameHi);
if (Chars.equals(dbcs, "user")) {
CharacterStoreEntry e = characterStore.newEntry();
e.put(dbcs.of(valueLo, valueHi));
this.username = e.toImmutable();
}
LOG.info().$("property [name=").$(dbcs.of(nameLo, nameHi)).$(", value=").$(dbcs.of(valueLo, valueHi)).$(']').$();
}
characterStore.clear();
assertTrue(this.username != null, "user is not specified");
prepareLoginResponse();
sendAndReset();
break;
case INIT_CANCEL_REQUEST:
//todo - 1. do not disconnect
// 2. should cancel running query only if PID and secret provided are the same as the ones provided upon logon
// 3. send back error message (e) for the cancelled running query
LOG.info().$("cancel request").$();
throw PeerDisconnectedException.INSTANCE;
default:
LOG.error().$("unknown init message [protocol=").$(protocol).$(']').$();
throw BadProtocolException.INSTANCE;
}
}
private void processParse(long address, long lo, long msgLimit, @Transient SqlCompiler compiler)
throws BadProtocolException, SqlException, PeerDisconnectedException, PeerIsSlowToReadException {
// 'Parse'
//message length
long hi = getStringLength(lo, msgLimit, "bad prepared statement name length");
// When we encounter statement name in the "parse" message
// we need to ensure the wrapper is properly setup to deal with
// "describe", "bind" message sequence that will follow next.
// In that all parameter types that we need to infer will have to be added to the
// "bindVariableTypes" list.
// Perhaps this is a good idea to make named statement writer a part of the context
final CharSequence statementName = getStatementName(lo, hi);
//query text
lo = hi + 1;
hi = getStringLength(lo, msgLimit, "bad query text length");
parseQueryText(lo, hi, compiler);
//parameter type count
lo = hi + 1;
this.parsePhaseBindVariableCount = getShort(lo, msgLimit, "could not read parameter type count");
if (statementName != null) {
LOG.info().$("prepare [name=").$(statementName).$(']').$();
configurePreparedStatement(statementName);
} else {
this.activeBindVariableTypes = bindVariableTypes;
this.activeSelectColumnTypes = selectColumnTypes;
}
//process parameter types
if (this.parsePhaseBindVariableCount > 0) {
if (lo + Short.BYTES + this.parsePhaseBindVariableCount * 4L > msgLimit) {
LOG.error()
.$("could not read parameters [parameterCount=").$(this.parsePhaseBindVariableCount)
.$(", offset=").$(lo - address)
.$(", remaining=").$(msgLimit - lo)
.$(']').$();
throw BadProtocolException.INSTANCE;
}
LOG.debug().$("params [count=").$(this.parsePhaseBindVariableCount).$(']').$();
setupBindVariables(lo + Short.BYTES, activeBindVariableTypes, this.parsePhaseBindVariableCount);
} else if (this.parsePhaseBindVariableCount < 0) {
LOG.error()
.$("invalid parameter count [parameterCount=").$(this.parsePhaseBindVariableCount)
.$(", offset=").$(lo - address)
.$(']').$();
throw BadProtocolException.INSTANCE;
}
if (typesAndSelect != null) {
buildSelectColumnTypes();
}
syncActions.add(SYNC_PARSE);
}
private void processQuery(long lo, long limit, @Transient SqlCompiler compiler)
throws BadProtocolException, SqlException, PeerDisconnectedException, PeerIsSlowToReadException {
// simple query, typically a script, which we don't yet support
prepareForNewQuery();
parseQueryText(lo, limit - 1, compiler);
if (typesAndSelect != null) {
activeSelectColumnTypes = selectColumnTypes;
buildSelectColumnTypes();
assert queryText != null;
queryTag = TAG_SELECT;
setupFactoryAndCursor();
prepareRowDescription();
sendCursor();
} else if (typesAndInsert != null) {
executeInsert();
} else {
executeTag();
prepareCommandComplete(false);
}
prepareReadyForQuery();
sendAndReset();
}
private void processSyncActions() {
try {
for (int i = 0, n = syncActions.size(); i < n; i++) {
switch (syncActions.getQuick(i)) {
case SYNC_PARSE:
prepareParseComplete();
break;
case SYNC_DESCRIBE:
prepareDescribeResponse();
break;
case SYNC_BIND:
prepareBindComplete();
break;
}
}
} finally {
syncActions.clear();
}
}
int recv() throws PeerDisconnectedException, PeerIsSlowToWriteException, BadProtocolException {
final int remaining = (int) (recvBufferSize - recvBufferWriteOffset);
assertTrue(remaining > 0, "undersized receive buffer or someone is abusing protocol");
int n = doReceive(remaining);
LOG.debug().$("recv [n=").$(n).$(']').$();
if (n < 0) {
throw PeerDisconnectedException.INSTANCE;
}
if (n == 0) {
int retriesRemaining = idleRecvCountBeforeGivingUp;
while (retriesRemaining > 0) {
n = doReceive(remaining);
if (n == 0) {
retriesRemaining--;
continue;
}
if (n < 0) {
LOG.info().$("disconnect [code=").$(n).$(']').$();
throw PeerDisconnectedException.INSTANCE;
}
break;
}
if (retriesRemaining == 0) {
throw PeerIsSlowToWriteException.INSTANCE;
}
}
recvBufferWriteOffset += n;
return n;
}
private void reportError(int position, CharSequence flyweightMessage) throws PeerDisconnectedException, PeerIsSlowToReadException {
prepareError(position, flyweightMessage);
prepareReadyForQuery();
sendAndReset();
clearRecvBuffer();
}
private void resumeCursor() throws SqlException, PeerDisconnectedException, PeerIsSlowToReadException {
final Record record = currentCursor.getRecord();
final int columnCount = currentFactory.getMetadata().getColumnCount();
responseAsciiSink.bookmark();
appendSingleRecord(record, columnCount);
sendCursor0(record, columnCount);
}
private void sendAndReset() throws PeerDisconnectedException, PeerIsSlowToReadException {
doSend(0, (int) (sendBufferPtr - sendBuffer));
responseAsciiSink.reset();
}
private void sendCopyInResponse(CairoEngine engine, TextLoader textLoader) throws PeerDisconnectedException, PeerIsSlowToReadException {
if (TableUtils.TABLE_EXISTS == engine.getStatus(
sqlExecutionContext.getCairoSecurityContext(),
path,
textLoader.getTableName()
)) {
responseAsciiSink.put(MESSAGE_TYPE_COPY_IN_RESPONSE);
long addr = responseAsciiSink.skip();
responseAsciiSink.put((byte) 0); // TEXT (1=BINARY, which we do not support yet)
try (TableWriter writer = engine.getWriter(sqlExecutionContext.getCairoSecurityContext(), textLoader.getTableName())) {
RecordMetadata metadata = writer.getMetadata();
responseAsciiSink.putNetworkShort((short) metadata.getColumnCount());
for (int i = 0, n = metadata.getColumnCount(); i < n; i++) {
responseAsciiSink.putNetworkShort((short) TYPE_OIDS.get(metadata.getColumnType(i)));
}
}
responseAsciiSink.putLen(addr);
} else {
final SqlException e = SqlException.$(0, "table '").put(textLoader.getTableName()).put("' does not exist");
prepareError(e.getPosition(), e.getFlyweightMessage());
prepareReadyForQuery();
}
sendAndReset();
}
private void sendCursor() throws PeerDisconnectedException, PeerIsSlowToReadException, SqlException {
// the assumption for now is that any will fit into response buffer. This of course precludes us from
// streaming large BLOBs, but, and its a big one, PostgreSQL protocol for DataRow does not allow for
// streaming anyway. On top of that Java PostgreSQL driver downloads data row fully. This simplifies our
// approach for general queries. For streaming protocol we will code something else. PostgreSQL Java driver is
// slow anyway.
rowCount = 0;
final Record record = currentCursor.getRecord();
final RecordMetadata metadata = currentFactory.getMetadata();
final int columnCount = metadata.getColumnCount();
resumeProcessor = resumeCursorRef;
sendCursor0(record, columnCount);
}
private void sendCursor0(Record record, int columnCount) throws PeerDisconnectedException, PeerIsSlowToReadException, SqlException {
try {
while (currentCursor.hasNext()) {
// create checkpoint to which we can undo the buffer in case
// current DataRow will does not fit fully.
responseAsciiSink.bookmark();
try {
try {
appendRecord(record, columnCount);
rowCount++;
} catch (NoSpaceLeftInResponseBufferException e) {
responseAsciiSink.resetToBookmark();
sendAndReset();
appendSingleRecord(record, columnCount);
}
} catch (SqlException e) {
responseAsciiSink.resetToBookmark();
throw e;
}
}
resumeProcessor = null;
currentCursor = Misc.free(currentCursor);
// do not free factory, it will be cached
currentFactory = null;
} finally {
// we we resumed the cursor send the typeAndSelect will be null
// we do not want to overwrite cache entries and potentially
// leak memory
if (typesAndSelect != null) {
typesAndSelectCache.put(queryText, typesAndSelect);
// clear selectAndTypes so that context doesn't accidentally
// free the factory when context finishes abnormally
this.typesAndSelect = null;
}
}
prepareCommandComplete(true);
}
private void appendSingleRecord(Record record, int columnCount) throws SqlException {
try {
appendRecord(record, columnCount);
} catch (NoSpaceLeftInResponseBufferException e1) {
// oopsie, buffer is too small for single record
LOG.error().$("not enough space in buffer for row data [buffer=").$(sendBufferSize).I$();
responseAsciiSink.reset();
throw CairoException.instance(0).put("server configuration error: not enough space in send buffer for row data");
}
}
private void setupFactoryAndCursor() {
currentFactory = typesAndSelect.getFactory();
try {
currentCursor = currentFactory.getCursor(sqlExecutionContext);
} catch (Throwable e) {
currentFactory = Misc.free(currentFactory);
throw e;
}
}
private void setupVariableSettersFromWrapper(
@Transient NamedStatementWrapper wrapper,
@Nullable @Transient SqlCompiler compiler
) throws SqlException, PeerDisconnectedException, PeerIsSlowToReadException {
queryText = wrapper.queryText;
LOG.debug().$("wrapper query [q=`").$(wrapper.queryText).$("`]").$();
this.activeBindVariableTypes = wrapper.bindVariableTypes;
this.parsePhaseBindVariableCount = wrapper.bindVariableTypes.size();
this.activeSelectColumnTypes = wrapper.selectColumnTypes;
if (compileQuery(compiler) && typesAndSelect != null) {
buildSelectColumnTypes();
}
}
private void shiftReceiveBuffer(long readOffsetBeforeParse) {
final long len = recvBufferWriteOffset - readOffsetBeforeParse;
LOG.debug()
.$("shift [offset=").$(readOffsetBeforeParse)
.$(", len=").$(len)
.$(']').$();
Vect.memcpy(
recvBuffer + readOffsetBeforeParse,
recvBuffer,
len
);
recvBufferWriteOffset = len;
recvBufferReadOffset = 0;
}
private void validateParameterCounts(short parameterFormatCount, short parameterValueCount, int parameterTypeCount) throws BadProtocolException {
if (parameterValueCount > 0) {
if (parameterValueCount < parameterTypeCount) {
LOG.error().$("parameter type count must be less or equals to number of parameters values").$();
throw BadProtocolException.INSTANCE;
}
if (parameterFormatCount > 1 && parameterFormatCount != parameterValueCount) {
LOG.error().$("parameter format count and parameter value count must match").$();
throw BadProtocolException.INSTANCE;
}
}
}
@FunctionalInterface
private interface PGResumeProcessor {
void resume() throws PeerIsSlowToReadException, SqlException, PeerDisconnectedException;
}
public static class NamedStatementWrapper implements Mutable {
public final IntList bindVariableTypes = new IntList();
public final IntList selectColumnTypes = new IntList();
public CharSequence queryText = null;
public CharSequence tag;
public void clear() {
tag = null;
queryText = null;
bindVariableTypes.clear();
selectColumnTypes.clear();
}
}
class ResponseAsciiSink extends AbstractCharSink {
private long bookmarkPtr = -1;
public void bookmark() {
this.bookmarkPtr = sendBufferPtr;
}
public void bump(int size) {
sendBufferPtr += size;
}
@Override
public CharSink put(CharSequence cs) {
// this method is only called by date format utility to print timezone name
final int len;
if (cs != null && (len = cs.length()) > 0) {
ensureCapacity(len);
for (int i = 0; i < len; i++) {
Unsafe.getUnsafe().putByte(sendBufferPtr + i, (byte) cs.charAt(i));
}
sendBufferPtr += len;
}
return this;
}
@Override
public CharSink put(char c) {
ensureCapacity(Byte.BYTES);
Unsafe.getUnsafe().putByte(sendBufferPtr++, (byte) c);
return this;
}
@Override
public CharSink put(char[] chars, int start, int len) {
ensureCapacity(len);
Chars.asciiCopyTo(chars, start, len, sendBufferPtr);
sendBufferPtr += len;
return this;
}
public CharSink put(byte b) {
ensureCapacity(Byte.BYTES);
Unsafe.getUnsafe().putByte(sendBufferPtr++, b);
return this;
}
public void put(BinarySequence sequence) {
final long len = sequence.length();
if (len > maxBlobSizeOnQuery) {
setNullValue();
} else {
ensureCapacity((int) (len + Integer.BYTES));
// when we reach here the "long" length would have to fit in response buffer
// if it was larger than integers it would never fit into integer-bound response buffer
putInt(sendBufferPtr, (int) len);
sendBufferPtr += Integer.BYTES;
for (long x = 0; x < len; x++) {
Unsafe.getUnsafe().putByte(sendBufferPtr + x, sequence.byteAt(x));
}
sendBufferPtr += len;
}
}
public void putIntDirect(int value) {
ensureCapacity(Integer.BYTES);
putIntUnsafe(0, value);
sendBufferPtr += Integer.BYTES;
}
public void putIntUnsafe(long offset, int value) {
Unsafe.getUnsafe().putInt(sendBufferPtr + offset, value);
}
public void putLen(long start) {
putInt(start, (int) (sendBufferPtr - start));
}
public void putLenEx(long start) {
putInt(start, (int) (sendBufferPtr - start - Integer.BYTES));
}
public void putNetworkDouble(double value) {
ensureCapacity(Double.BYTES);
Unsafe.getUnsafe().putDouble(sendBufferPtr, Double.longBitsToDouble(Numbers.bswap(Double.doubleToLongBits(value))));
sendBufferPtr += Double.BYTES;
}
public void putNetworkFloat(float value) {
ensureCapacity(Float.BYTES);
Unsafe.getUnsafe().putFloat(sendBufferPtr, Float.intBitsToFloat(Numbers.bswap(Float.floatToIntBits(value))));
sendBufferPtr += Float.BYTES;
}
public void putNetworkInt(int value) {
ensureCapacity(Integer.BYTES);
putInt(sendBufferPtr, value);
sendBufferPtr += Integer.BYTES;
}
public void putNetworkLong(long value) {
ensureCapacity(Long.BYTES);
putLong(sendBufferPtr, value);
sendBufferPtr += Long.BYTES;
}
public void putNetworkShort(short value) {
ensureCapacity(Short.BYTES);
putShort(sendBufferPtr, value);
sendBufferPtr += Short.BYTES;
}
public void resetToBookmark() {
assert bookmarkPtr != -1;
sendBufferPtr = bookmarkPtr;
bookmarkPtr = -1;
}
void encodeUtf8Z(CharSequence value) {
encodeUtf8(value);
ensureCapacity(Byte.BYTES);
Unsafe.getUnsafe().putByte(sendBufferPtr++, (byte) 0);
}
private void ensureCapacity(int size) {
if (sendBufferPtr + size < sendBufferLimit) {
return;
}
throw NoSpaceLeftInResponseBufferException.INSTANCE;
}
void reset() {
sendBufferPtr = sendBuffer;
}
void setNullValue() {
putIntDirect(INT_NULL_X);
}
long skip() {
ensureCapacity(Integer.BYTES);
long checkpoint = sendBufferPtr;
sendBufferPtr += Integer.BYTES;
return checkpoint;
}
}
}