WL#8132: JSON datatype and binary storage format
Status: Complete
The goal of this WL is to develop a binary format that allows the server to efficiently store, retrieve and search JSON data.
F1: Provide an interface that can be used by the WL#7909 functions to transform their Json_dom data structures into binary form. F2: Provide an interface that can be used by the WL#7909 functions to access the binary representation of a JSON document. F3: Enhance CREATE TABLE and ALTER TABLE so that they can create JSON columns. For example: CREATE TABLE T(json_col1 JSON); ALTER TABLE T ADD COLUMN json_col2 JSON; F4: Extend Field to allow insert to and select from JSON typed fields. E.g: INSERT INTO T(json_col1) VALUES('[1]'); SELECT * FROM T; // Should return '[1]'
On a high level, we will store the contents of the JSON document in three sections: 1) A table of pointers to all the keys and values, in the order in which the keys and values are stored. Each pointer contains information about where the data associated with the key or the value is located, as well as type information about the key or value pointed to. 2) All the keys. The keys are sorted, so that lookup can use binary search to locate the key quickly. 3) All the values, in the same order as their corresponding keys. If the document is an array, it has two sections only: the dictionary and the values. If the document is a scalar, it has a single section which contains the scalar value. In JSON objects, the keys are sorted on length, and keys with the same length are sorted lexicographically (on the Unicode code points, regardless of collation), in ascending order. (By sorting on length first, it is possible to do binary search without even looking at the actual contents of most of the keys.) In JSON arrays, the values are stored in the index order. Having a table of pointers to the keys and values at the beginning of the binary representation, makes it possible to look up a member in the middle of the document directly without scanning past all the members that precede it. The entries in the table must have fixed size, so that the position of a given entry can be calculated easily (pos = offset + entry_num * size_of_entry). Because we want to allow in-place updates of JSON values in the future, and we want to do that without changing the format specification, our format will store both the offset and the length of variable-length values (strings, blobs, ...). This is redundant information for now, because the length of a value can be calculated as the difference between the offset of the value and the offset of the value that follows it. However, by storing the length redundantly, we can later go and change the value of a length field to make a value shorter, without having to move all the values that come after it. This would leave a hole between the values. One can also imagine that one could proactively add holes between values when serializing a JSON document, so that one can also grow variable-length values without having to move other values to make room for the new value. For now, though, the serialization will store documents compactly with no holes in them. To compensate for the extra space needed by the redundant length information, we will make the format allow offset and length fields to come in two different variants: 2 bytes for documents smaller than 64KB, and 4 bytes to support larger documents. In JSON documents nested within other JSON documents, all offsets are relative to the beginning of the nested document, so that the nested document can be moved around freely without having to update the offsets. Every nested document is self-contained, and is a valid binary JSON document on its own. -------- doc ::= type value type ::= 0x00 | // small JSON object 0x01 | // large JSON object 0x02 | // small JSON array 0x03 | // large JSON array 0x04 | // literal (true/false/null) 0x05 | // int16 0x06 | // uint16 0x07 | // int32 0x08 | // uint32 0x09 | // int64 0x0a | // uint64 0x0b | // double 0x0c | // utf8mb4 string 0x0f // custom data (any MySQL data type) value ::= object | array | literal | number | string | custom-data object ::= element-count size key-entry* value-entry* key* value* array ::= element-count size value-entry* value* // number of members in object or number of elements in array element-count ::= uint16 | // if used in small JSON object/array uint32 // if used in large JSON object/array // number of bytes in the binary representation of the object or array size ::= uint16 | // if used in small JSON object/array uint32 // if used in large JSON object/array key-entry ::= key-offset key-length key-offset ::= uint16 | // if used in small JSON object uint32 // if used in large JSON object key-length ::= uint16 // key length must be less than 64KB value-entry ::= type offset-or-inlined-value // This field holds either the offset to where the value is stored, // or the value itself if it is small enough to be inlined (that is, // if it is a JSON literal or a small enough [u]int). offset-or-inlined-value ::= uint16 | // if used in small JSON object/array uint32 // if used in large JSON object/array key ::= utf8mb4-data literal ::= 0x00 | // JSON null literal 0x01 | // JSON true literal 0x02 | // JSON false literal number ::= .... // usual format for [u]int(16|32|64), double string ::= data-length utf8mb4-data custom-data ::= custom-type data-length binary-data custom-type ::= uint8 // type identifier that matches the // internal enum_field_types enum data-length ::= uint8* // If the high bit of a byte is 1, the length // field is continued in the next byte, // otherwise it is the last byte of the length // field. So we need 1 byte to represent // lengths up to 127, 2 bytes to represent // lengths up to 16383, and so on... -------- LIMITATIONS: The total size of a binary document must be less than 4GB, because offset and length fields are limited to four bytes. This is also the maximum size of a LONGBLOB column, so documents larger than this could not be stored in the database even if the format allowed them. Every key of a JSON object must be smaller than 64KB, because the key entries have only two bytes for representing the key length. -------- In order to be able to store JSON values in this binary format, a new MySQL data type called JSON will be added. This involves: - Making the parser recognize "JSON" as type name. - Adding a subclass of Field that will be used when storing values of the new type in a table. This class will be called Field_json and extend the existing Field_blob class. - Adding a new field type MYSQL_JSON_TYPE to the enum_field_types enumeration, to identify fields of the new type. - Adding an interface that can be used to transform between the in-memory representation of a JSON document (the in-memory representation will be implemented in WL#7909) and the binary representation. That is, serialization and deserialization. This interface will be used by the functions added by WL#7909. -------- The interface between this worklog and WL#7909 will be as follows: 1) Serialization: WL#7909 adds a JSON DOM class hierarchy and a parser that builds a DOM from a JSON text. This worklog will add a function that serializes the DOM into its binary representation in a String object: enum_serialization_result json_binary::serialize(const Json_dom *dom, String *dest); (The Json_dom class will be provided by WL#7909.) The return value of json_binary::serialize() tells the status of the serialization. It's an enum_serialization_result and can have one of the following values: - OK: The JSON document was successfully serialized into the destination buffer. - VALUE_TOO_BIG: The JSON document was too big to be stored. - KEY_TOO_BIG: The JSON document contained a key that exceeded the maximum key length. - OOM: Out of memory, the serialization failed because it could not allocate more space. - FAILED: If some other, unexpected error occurred. Code that serializes JSON values would typically look something like this: Json_dom *dom= ...; // A Json_dom object that comes from a WL#7909 function String str; // A String that will hold the binary representation switch (serialize(dom, &str)) // Perform serialization of dom into str { case OK: // Serialization went OK, now store the binary representation somewhere. store_data(str.ptr(), str.length()); break; case VALUE_TOO_BIG: // Couldn't serialize, raise appropriate error for the condition. my_error(...); break; case KEY_TOO_BIG: // Couldn't serialize, raise appropriate error for the condition. my_error(...); break; ... } 2) Deserialization: This worklog will provide the following interface that can be used by the WL#7909 functions to access the binary encoded JSON documents: /* Return a handle to the top-level JSON value represented by the supplied binary data. */ Value json_binary::parse_binary(const char *data, size_t len); class json_binary::Value { public: /* Does this value represent a valid document? */ bool is_valid() const; /* What's the JSON type of this value? OBJECT, ARRAY, STRING, INT, etc. */ enum_type type() const; /* Getters for various types. */ const char *get_data() const; // for STRING and OPAQUE size_t get_data_length() const; // for STRING and OPAQUE int64 get_int64() const; // for INT uint64 get_uint64() const; // for UINT double get_double() const; // for DOUBLE /* Size of array or object. */ size_t element_count() const; /* Get the i'th element of an object or an array. */ Value element(size_t i) const; /* Get the i'th key of an object. */ Value key(size_t i) const; /* Look up the member with the given key in an object. */ Value lookup(const char *key, size_t key_length) const; /* What's the MySQL type of this value? It only makes sense if this value is a JSON scalar that wraps an arbitrary MySQL value and type() returns OPAQUE. */ enum_field_types field_type() const; /* Copy the binary JSON representation of this value into a buffer. Returns false on success, true on error. */ bool raw_binary(String *buffer) const; } Callers will typically pass a pointer to the binary data to the json_binary::parse_binary() function, which will return a handle to the top-level document as a json_binary::Value object. The callers can then use the json_binary::Value object to inspect the value at hand, or use it to get other json_binary::Value objects that represent JSON values nested within the top-level JSON value. For example, to find a string on the path $[1].name in a JSON value, one could use the interface like this: String *str= read_data(...); // Fetch the binary data from storage somehow. Value doc= parse_binary(str.ptr(), str.length()); if (doc.type() == Value::ARRAY && doc.element_count() >= 2) { Value v1= doc.element(1); if (v1.type() == Value::OBJECT) { Value v2= v1.lookup("name"); if (v2.type() == Value::STRING) { // found the string ... std::string res= std::string(v2.get_data(), v2.get_data_length()); // ... now do something with it ... } } } Or to collect all the double values found in an array: std::vectorvec; String *str= read_data(...); // Fetch the binary data from storage somehow. Value doc= parse_binary(str.ptr(), str.length()); if (doc.type() == Value::ARRAY) { for (int i= 0; i < doc.element_count(); i++) { Value v= doc.element(i); if (v.type() == Value::DOUBLE) vec.push_back(v.get_double()); } }
* Parser changes * The new JSON type will be wired into the SQL parser by making the "type" production rule in sql_yacc.yy recognize JSON as a type. Also, to avoid breaking existing applications that use JSON as an identifier, the new JSON keyword should be registered as a non-reserved keyword. The new type will be identified as MYSQL_TYPE_JSON, which field.cc will map to Field_json. * Field class changes * The field class hierarchy will be extended with the class Field_json, which will be a subclass of Field_blob. Field_json will have these differences from Field_blob: 1. It will override store(char*, size_t, CHARSET_INFO*) and make it - Parse all text strings as JSON text and convert to binary representation before storing them, as per the conversion rules defined by WL#7909. - Reject all binary strings. 2. It will add a new function store_json() which takes a Json_dom and converts it to binary representation before storing it. This function will be used by the WL#7909 functions to store a Json_dom to a field. * Serialization * The serialization function (json_binary::serialize()) will take a Json_dom object and transform it into a string of bytes. Json_dom is the base class of a class hierarchy used for representing a JSON document as a DOM-style tree structure in memory. The Json_dom class hierarchy will be added in WL#7909. json_binary::serialize() will take the object representing the top-level document and process the DOM-tree recursively, and fill the destination buffer as it descends down the tree. The format allows for using either two bytes or four bytes for representing offsets and lengths in JSON objects and JSON arrays,. When serializing an object or and array, one doesn't know up front how big the serialized representation will be, so one cannot tell whether two bytes is sufficient to represent all the offsets and lengths. The serializer will optimistically try to use two bytes from the start, and when it finds that an object or array has grown so big that two bytes is not enough, it will restart the serialization of that object or array with four byte offsets/lengths. * Deserialization * Deserialization of a binary JSON value will be lazy, and will only look at the parts of the document that the caller asks for. To deserialize a binary JSON value, one will call the function json_binary::parse_binary(), which takes a pointer to a char buffer that holds the binary representation of a JSON document. The function will not parse the entire document. It just reads the header that tells the type of the document and returns a json_binary::Value object that points to it. The returned json_binary::Value object can be used to inspect values nested inside the top-level document. The nested values will also be returned as json_binary::Value objects, and they will also just be shallow wrappers around the buffer that holds the binary representation.
Copyright (c) 2000, 2024, Oracle Corporation and/or its affiliates. All rights reserved.