Compared with Protocol Buffers, PlainBuffer delivers better performance for serialization and resolution of small objects. Therefore, PlainBuffer is used to format data in Tablestore.
This document is intended for developers implementing a custom Tablestore SDK or debugging raw data transmission at the protocol level. Application developers using an existing SDK do not need to understand PlainBuffer internals.
Format definition
A PlainBuffer message is a header followed by one or more rows. Each row contains a primary key section and an attribute column section — both can be optional depending on the operation (see grammar below). Tags act as field delimiters — each tag tells the parser what comes next, so the parser can advance through the byte stream without ambiguity.
plainbuffer = tag_header row1 [row2] [row3]
row = ( pk [attr] | [pk] attr | pk attr ) [tag_delete_marker] row_checksum;
pk = tag_pk cell_1 [cell_2] [cell_3]
attr = tag_attr cell1 [cell_2] [cell_3]
cell = tag_cell cell_name [cell_value] [cell_op] [cell_ts] cell_checksum
cell_name = tag_cell_name formated_value
cell_value = tag_cell_value formated_value
cell_op = tag_cell_op cell_op_value
cell_ts = tag_cell_ts cell_ts_value
row_checksum = tag_row_checksum row_crc8
cell_checksum = tag_cell_checksum row_crc8
formated_value = value_type value_len value_data
value_type = int8
value_len = int32
cell_op_value = delete_all_version | delete_one_version
cell_ts_value = int64
delete_all_version = 0x01 (1byte)
delete_one_version = 0x03 (1byte)
Tag values
Most tags are a single byte that marks the start of a field; tag_header is 4 bytes. The parser reads the tag, determines what field follows, reads the appropriate number of bytes, and advances to the next tag.
tag_header = 0x75 (4byte)
tag_pk = 0x01 (1byte)
tag_attr = 0x02 (1byte)
tag_cell = 0x03 (1byte)
tag_cell_name = 0x04 (1byte)
tag_cell_value = 0x05 (1byte)
tag_cell_op = 0x06 (1byte)
tag_cell_ts = 0x07 (1byte)
tag_delete_marker = 0x08 (1byte)
tag_row_checksum = 0x09 (1byte)
tag_cell_checksum = 0x0A (1byte)
ValueType values
Valid values for value_type in formated_value:
VT_INTEGER = 0x0
VT_DOUBLE = 0x1
VT_BOOLEAN = 0x2
VT_STRING = 0x3
VT_NULL = 0x6
VT_BLOB = 0x7
VT_INF_MIN = 0x9
VT_INF_MAX = 0xa
VT_AUTO_INCREMENT = 0xb
Checksum calculation
Checksums use CRC8. The calculation follows this logic:
Each cell's name, value, type, and timestamp contribute to that cell's checksum.
Each row's delete marker contributes a single byte:
0x1if the row has a delete marker,0x0if it does not.The row checksum is computed by running CRC8 over the individual cell checksums — not over the raw cell data.
Java implementation:
The following code is extracted from $tablestore-4.2.1-sources/com/alicloud/openservices/tablestore/core/protocol/PlainBufferCrc8.java. For more information, see Installation.
public static byte getChecksum(byte crc, PlainBufferCell cell) throws IOException {
if (cell.hasCellName()) {
crc = crc8(crc, cell.getNameRawData());
}
if (cell.hasCellValue()) {
if (cell.isPk()) {
crc = cell.getPkCellValue().getChecksum(crc);
} else {
crc = cell.getCellValue().getChecksum(crc);
}
}
if (cell.hasCellTimestamp()) {
crc = crc8(crc, cell.getCellTimestamp());
}
if (cell.hasCellType()) {
crc = crc8(crc, cell.getCellType());
}
return crc;
}
public static byte getChecksum(byte crc, PlainBufferRow row) throws IOException {
for (PlainBufferCell cell : row.getPrimaryKey()) {
crc = crc8(crc, cell.getChecksum());
}
for (PlainBufferCell cell : row.getCells()) {
crc = crc8(crc, cell.getChecksum());
}
byte del = 0;
if (row.hasDeleteMarker()) {
del = (byte)0x1;
}
crc = crc8(crc, del);
return crc;
}
Example
The following row has two primary key columns and four attribute columns.
-
Primary key columns:
[pk1:string:iampk]
[pk2:integer:100]
-
Attribute columns:
[column1:string:bad:1001]
[column2:integer:128:1002]
[column3:double:34.2:1003]
[column4:del_all_versions]
Encoding:
<The start position for headers>[0x75]
<The start position for primary key columns>[0x1]
<Cell1>[0x3][0x4][0x3][3][pk1][0x5][0x3][5][iampk][_cell_checksum]
<Cell2>[0x3][0x4][0x3][3][pk2][0x5][0x0][8][100][_cell_checksum]
<The start position for attribute columns>[0x2]
<Cell1>[0x3][0x4][0x3][7][column1][0x5][0x3][3][bad][0x7][1001][_cell_checksum]
<Cell2>[0x3][0x4][0x3][7][column2][0x5][0x0][8][128][0x7][1002][_cell_checksum]
<Cell3>[0x3][0x4][0x3][7][column3][0x5][0x1][8][34.2][0x7][1003][_cell_checksum]
<Cell4>[0x3][0x4][0x3][7][column4][0x6][1][_cell_checksum]
[_row_check_sum]
Reading the first primary key cell byte by byte:
|
Bytes |
Tag/field |
Meaning |
|
|
|
Start of a cell |
|
|
|
The cell name follows |
|
|
value_type |
|
|
|
value_len |
Name is 3 bytes long |
|
|
value_data |
Cell name: |
|
|
|
The cell value follows |
|
|
value_type |
|
|
|
value_len |
Value is 5 bytes long |
|
|
value_data |
Cell value: |
|
|
|
Checksum over name, value, type, and timestamp |
The second primary key cell follows the same structure, except the value type is 0x0 (VT_INTEGER) and value_len is 8 (a 64-bit integer).
Attribute cells add a timestamp field (0x7 = tag_cell_ts) after the value. column4 has no value — the 0x6 tag (tag_cell_op) with value 1 marks it as a delete-all-versions operation.