2024-11-29 07:47:54 -08:00
|
|
|
#include "json/dom.h"
|
|
|
|
#include "json/json.h"
|
|
|
|
#include "json/stats.h"
|
|
|
|
#include "json/selector.h"
|
|
|
|
#include <cstring>
|
|
|
|
#include <memory>
|
|
|
|
#include <iostream>
|
|
|
|
#include <iomanip>
|
|
|
|
#include <cmath>
|
|
|
|
#include "json/rapidjson_includes.h"
|
|
|
|
|
|
|
|
#define STATIC /* decorator for static functions, remove so that backtrace symbols include these */
|
|
|
|
|
|
|
|
#define CHECK_DOCUMENT_SIZE_LIMIT(ctx, curr_doc_size, input_json_val_size) \
|
|
|
|
{ \
|
|
|
|
if (ctx != nullptr && !(ValkeyModule_GetContextFlags(ctx) & VALKEYMODULE_CTX_FLAGS_REPLICATED) && \
|
|
|
|
json_get_max_document_size() > 0 && (curr_doc_size + input_json_val_size > json_get_max_document_size())) { \
|
|
|
|
ValkeyModule_Log(ctx, "debug", \
|
|
|
|
"Document size limit is exceeded. The attempted operation will result in a document with %lu bytes of " \
|
|
|
|
"memory size.", curr_doc_size + input_json_val_size); \
|
|
|
|
return JSONUTIL_DOCUMENT_SIZE_LIMIT_EXCEEDED; \
|
|
|
|
} \
|
|
|
|
}
|
|
|
|
|
|
|
|
#define CHECK_DOCUMENT_PATH_LIMIT(ctx, selector, new_val) \
|
|
|
|
{ \
|
|
|
|
size_t __depth_would_be = selector.getMaxPathDepth() + new_val.GetMaxDepth(); \
|
|
|
|
if (__depth_would_be > json_get_max_path_limit()) { \
|
|
|
|
ValkeyModule_Log(ctx, "debug", \
|
|
|
|
"Document path limit is exceeded. The attempted operation will result in a document with %lu nesting" \
|
|
|
|
" levels.", __depth_would_be); \
|
|
|
|
return JSONUTIL_DOCUMENT_PATH_LIMIT_EXCEEDED; \
|
|
|
|
} else { \
|
|
|
|
jsonstats_update_max_depth_ever_seen(__depth_would_be); \
|
|
|
|
} \
|
|
|
|
}
|
|
|
|
|
|
|
|
// the one true allocator
|
|
|
|
RapidJsonAllocator allocator;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* We want to avoid all redundant creations of an allocator -- for performance reasons.
|
|
|
|
* So we use the constructor to detect that situation. It's free after startup. If you trip
|
|
|
|
* this trap, then you let a rapidjson allocator instance get defaulted to 0 somewhere in your code.
|
|
|
|
*/
|
|
|
|
RapidJsonAllocator::RapidJsonAllocator() {
|
|
|
|
ValkeyModule_Assert(this == &allocator); // Only this one is allowed :)
|
|
|
|
}
|
|
|
|
|
|
|
|
JValue& dom_get_value(JDocument &doc) {
|
|
|
|
return doc.GetJValue();
|
|
|
|
}
|
|
|
|
|
|
|
|
JParser& JParser::Parse(const char *json, size_t len) {
|
|
|
|
int64_t begin_val = jsonstats_begin_track_mem();
|
|
|
|
RJParser::Parse(json, len);
|
|
|
|
int64_t delta = jsonstats_end_track_mem(begin_val);
|
|
|
|
ValkeyModule_Assert(delta >= 0);
|
|
|
|
allocated_size = static_cast<size_t>(delta);
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
|
|
|
JParser& JParser::Parse(const std::string_view &sv) {
|
|
|
|
return Parse(sv.data(), sv.length());
|
|
|
|
}
|
|
|
|
|
|
|
|
jsn::string validate(const JDocument *doc) {
|
|
|
|
std::string s = doc->GetJValue().Validate();
|
|
|
|
return jsn::string(s.c_str(), s.length());
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC JDocument *create_doc() {
|
|
|
|
return new JDocument();
|
|
|
|
}
|
|
|
|
|
|
|
|
void dom_free_doc(JDocument *doc) {
|
|
|
|
ValkeyModule_Assert(doc != nullptr);
|
|
|
|
delete doc;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t dom_get_doc_size(const JDocument *doc) {
|
|
|
|
return doc->size;
|
|
|
|
}
|
|
|
|
|
2025-01-05 19:14:42 -05:00
|
|
|
size_t dom_get_doc_size_no_opt(const JDocument *doc) {
|
|
|
|
return sizeof(JDocument) + doc->GetJValue().ComputeMallocedSize();
|
|
|
|
}
|
|
|
|
|
2024-11-29 07:47:54 -08:00
|
|
|
void dom_set_doc_size(JDocument *doc, const size_t size) {
|
|
|
|
doc->size = size;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t dom_get_bucket_id(const JDocument *doc) {
|
|
|
|
return doc->bucket_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
void dom_set_bucket_id(JDocument *doc, const uint32_t bucket_id) {
|
|
|
|
doc->bucket_id = bucket_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_parse(ValkeyModuleCtx *ctx, const char *json_buf, const size_t buf_len, JDocument **doc) {
|
|
|
|
*doc = nullptr;
|
2025-01-05 19:14:42 -05:00
|
|
|
// begin tracking memory
|
|
|
|
int64_t begin_val = jsonstats_begin_track_mem();
|
|
|
|
|
|
|
|
{
|
|
|
|
JParser parser;
|
|
|
|
if (parser.Parse(json_buf, buf_len).HasParseError()) {
|
|
|
|
return parser.GetParseErrorCode();
|
|
|
|
}
|
|
|
|
CHECK_DOCUMENT_SIZE_LIMIT(ctx, size_t(0), parser.GetJValueSize())
|
|
|
|
*doc = create_doc();
|
|
|
|
(*doc)->SetJValue(parser.GetJValue());
|
|
|
|
jsonstats_update_max_depth_ever_seen(parser.GetMaxDepth());
|
|
|
|
}
|
|
|
|
|
|
|
|
// end tracking memory
|
|
|
|
int64_t delta = jsonstats_end_track_mem(begin_val);
|
|
|
|
dom_set_doc_size(*doc, dom_get_doc_size(*doc) + delta);
|
|
|
|
|
2024-11-29 07:47:54 -08:00
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC bool has_custom_format(const PrintFormat *format) {
|
|
|
|
return (format != nullptr && (format->indent != nullptr || format->space != nullptr || format->newline != nullptr));
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Serialize a value.
|
|
|
|
* @param json OUTPUT param, serialized string is appended to the param.
|
|
|
|
*/
|
|
|
|
STATIC void serialize_value(const JValue &val, size_t initialLevel, const PrintFormat *format,
|
|
|
|
rapidjson::StringBuffer &oss) {
|
|
|
|
size_t max_depth = 0;
|
|
|
|
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(oss);
|
|
|
|
if (has_custom_format(format)) {
|
|
|
|
if (format && format->newline) writer.SetNewline(std::string_view(format->newline, strlen(format->newline)));
|
|
|
|
if (format && format->indent) writer.SetIndent(std::string_view(format->indent, strlen(format->indent)));
|
|
|
|
if (format && format->space) writer.SetSpace(std::string_view(format->space, strlen(format->space)));
|
|
|
|
writer.SetInitialLevel(initialLevel);
|
|
|
|
val.Accept(writer);
|
|
|
|
jsonstats_update_max_depth_ever_seen(writer.GetMaxDepth());
|
|
|
|
} else {
|
|
|
|
writer.FastWrite(val, &max_depth);
|
|
|
|
jsonstats_update_max_depth_ever_seen(max_depth);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC void serialize_value(const JValue &val, size_t initialLevel, const PrintFormat *format, ReplyBuffer& oss) {
|
|
|
|
size_t max_depth = 0;
|
|
|
|
rapidjson::PrettyWriter<ReplyBuffer> writer(oss);
|
|
|
|
if (has_custom_format(format)) {
|
|
|
|
if (format && format->newline) writer.SetNewline(std::string_view(format->newline, strlen(format->newline)));
|
|
|
|
if (format && format->indent) writer.SetIndent(std::string_view(format->indent, strlen(format->indent)));
|
|
|
|
if (format && format->space) writer.SetSpace(std::string_view(format->space, strlen(format->space)));
|
|
|
|
writer.SetInitialLevel(initialLevel);
|
|
|
|
val.Accept(writer);
|
|
|
|
jsonstats_update_max_depth_ever_seen(writer.GetMaxDepth());
|
|
|
|
} else {
|
|
|
|
writer.FastWrite(val, &max_depth);
|
|
|
|
jsonstats_update_max_depth_ever_seen(max_depth);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void dom_serialize(JDocument *doc, const PrintFormat *format, rapidjson::StringBuffer &oss) {
|
|
|
|
serialize_value(*(doc), 0, format, oss);
|
|
|
|
}
|
|
|
|
|
|
|
|
void dom_serialize_value(const JValue &val, const PrintFormat *format, rapidjson::StringBuffer &oss) {
|
|
|
|
serialize_value(val, 0, format, oss);
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_set_value(ValkeyModuleCtx *ctx, JDocument *doc, const char *json_path, const char *new_val_json,
|
|
|
|
size_t new_val_size, const bool is_create_only, const bool is_update_only) {
|
|
|
|
if (is_create_only && is_update_only) return JSONUTIL_NX_XX_SHOULD_BE_MUTUALLY_EXCLUSIVE;
|
|
|
|
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.prepareSetValues(doc->GetJValue(), json_path);
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
if (is_create_only && selector.hasUpdates()) return JSONUTIL_NX_XX_CONDITION_NOT_SATISFIED;
|
|
|
|
if (is_update_only && selector.hasInserts()) return JSONUTIL_NX_XX_CONDITION_NOT_SATISFIED;
|
|
|
|
|
|
|
|
JParser new_val;
|
|
|
|
if (new_val.Parse(new_val_json, new_val_size).HasParseError()) {
|
|
|
|
return new_val.GetParseErrorCode();
|
|
|
|
}
|
|
|
|
|
|
|
|
CHECK_DOCUMENT_PATH_LIMIT(ctx, selector, new_val)
|
|
|
|
CHECK_DOCUMENT_SIZE_LIMIT(ctx, doc->size, new_val.GetJValueSize())
|
|
|
|
|
|
|
|
selector.commit(new_val);
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
template<typename OutputBuffer>
|
|
|
|
STATIC void PutString(OutputBuffer& oss, const char *str) {
|
|
|
|
while (*str) oss.Put(*str++);
|
|
|
|
}
|
|
|
|
|
|
|
|
template<typename OutputBuffer>
|
|
|
|
STATIC void PutEscapedString(OutputBuffer& oss, const char *str) {
|
|
|
|
JValue tmp;
|
|
|
|
tmp.SetString(str, strlen(str));
|
|
|
|
serialize_value(tmp, 0, nullptr, oss);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build stringified JSON array directly from a vector of values.
|
|
|
|
template<typename T>
|
|
|
|
STATIC void build_json_array(const jsn::vector<JValue*> &values, const PrintFormat *format, T &oss) {
|
|
|
|
bool has_format = has_custom_format(format);
|
|
|
|
oss.Put('[');
|
|
|
|
if (has_format && format->newline) PutString(oss, format->newline);
|
|
|
|
for (size_t i=0; i < values.size(); i++) {
|
|
|
|
if (has_format && format->indent) PutString(oss, format->indent);
|
|
|
|
serialize_value(*values[i], 1, format, oss);
|
|
|
|
if (i < values.size() - 1) oss.Put(',');
|
|
|
|
if (has_format && format->newline) PutString(oss, format->newline);
|
|
|
|
}
|
|
|
|
oss.Put(']');
|
|
|
|
}
|
|
|
|
|
|
|
|
template STATIC void build_json_array(const jsn::vector<JValue*> &values, const PrintFormat *format, ReplyBuffer &oss);
|
|
|
|
template STATIC void build_json_array(const jsn::vector<JValue*> &values, const PrintFormat *format,
|
|
|
|
rapidjson::StringBuffer &oss);
|
|
|
|
|
|
|
|
template<typename T>
|
|
|
|
JsonUtilCode dom_get_value_as_str(JDocument *doc, const char *json_path, const PrintFormat *format,
|
|
|
|
T &oss, const bool update_stats) {
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(*doc, json_path);
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// If legacy path, return either the first value, or NONEXISTENT error if no value is found.
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) {
|
|
|
|
if (values.empty()) {
|
|
|
|
return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
} else {
|
|
|
|
serialize_value(*values[0], 0, format, oss);
|
|
|
|
// update stats
|
|
|
|
if (update_stats) jsonstats_update_stats_on_read(oss.GetLength());
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// v2 path: return an array of values.
|
|
|
|
if (values.empty()) {
|
|
|
|
// return an empty array
|
|
|
|
oss.Put('[');
|
|
|
|
oss.Put(']');
|
|
|
|
} else {
|
|
|
|
// Multiple values are returned to the client as a JSON array.
|
|
|
|
build_json_array(values, format, oss);
|
|
|
|
}
|
|
|
|
|
|
|
|
// update stats
|
|
|
|
if (update_stats) jsonstats_update_stats_on_read(oss.GetLength());
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
template JsonUtilCode dom_get_value_as_str(JDocument *doc, const char *json_path, const PrintFormat *format,
|
|
|
|
ReplyBuffer &oss, const bool update_stats);
|
|
|
|
template JsonUtilCode dom_get_value_as_str(JDocument *doc, const char *json_path, const PrintFormat *format,
|
|
|
|
rapidjson::StringBuffer &oss, const bool update_stats);
|
|
|
|
|
|
|
|
STATIC void appendPathAndValue(const char *key, const JValue &val, const bool isLastPath,
|
|
|
|
const bool has_format, const PrintFormat *format, ReplyBuffer &oss) {
|
|
|
|
if (has_format && format->indent) PutString(oss, format->indent);
|
|
|
|
PutEscapedString(oss, key);
|
|
|
|
oss.Put(':');
|
|
|
|
if (has_format && format->space) PutString(oss, format->space);
|
|
|
|
serialize_value(val, 1, format, oss);
|
|
|
|
if (!isLastPath) oss.Put(',');
|
|
|
|
if (has_format && format->newline) PutString(oss, format->newline);
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC void appendPathAndValues(const char *key, const jsn::vector<JValue*> &values, const bool isLastPath,
|
|
|
|
const bool has_format, const PrintFormat *format, ReplyBuffer &oss) {
|
|
|
|
if (has_format && format->indent) PutString(oss, format->indent);
|
|
|
|
PutEscapedString(oss, key);
|
|
|
|
oss.Put(':');
|
|
|
|
if (has_format && format->space) PutString(oss, format->space);
|
|
|
|
oss.Put('[');
|
|
|
|
if (has_format && format->newline) PutString(oss, format->newline);
|
|
|
|
|
|
|
|
for (size_t i=0; i < values.size(); i++) {
|
|
|
|
if (has_format && format->indent) {
|
|
|
|
PutString(oss, format->indent);
|
|
|
|
PutString(oss, format->indent);
|
|
|
|
}
|
|
|
|
serialize_value(*values[i], 2, format, oss);
|
|
|
|
if (i < values.size() - 1) oss.Put(',');
|
|
|
|
if (has_format && format->newline) PutString(oss, format->newline);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (has_format && format->indent) PutString(oss, format->indent);
|
|
|
|
oss.Put(']');
|
|
|
|
if (!isLastPath) oss.Put(',');
|
|
|
|
if (has_format && format->newline) PutString(oss, format->newline);
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC JsonUtilCode buildJsonForMultiPaths(JDocument *doc, const char **paths, const int num_paths,
|
|
|
|
const bool is_v2path, const PrintFormat *format,
|
|
|
|
ReplyBuffer &oss) {
|
|
|
|
bool has_format = has_custom_format(format);
|
|
|
|
Selector selector(is_v2path);
|
|
|
|
JsonUtilCode rc;
|
|
|
|
oss.Put('{');
|
|
|
|
if (has_format && format->newline) PutString(oss, format->newline);
|
|
|
|
for (int i = 0; i < num_paths; i++) {
|
|
|
|
rc = selector.getValues(*doc, paths[i]);
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (!is_v2path) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
if (!is_v2path) { // legacy path
|
|
|
|
if (values.empty()) {
|
|
|
|
return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
} else {
|
|
|
|
appendPathAndValue(paths[i], *values[0], (i == num_paths - 1), has_format, format, oss);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
appendPathAndValues(paths[i], values, (i == num_paths - 1), has_format, format, oss);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
oss.Put('}');
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_get_values_as_str(JDocument *doc, const char **paths, const int num_paths,
|
|
|
|
PrintFormat *format, ReplyBuffer &oss, const bool update_stats) {
|
|
|
|
// If there are multiple paths mixed with both v1 and v2 syntax, the returned value should conform to the V2
|
|
|
|
// behavior (returning an array of values).
|
|
|
|
// We can't start processing the first element until we know if we should conform to V1 or V2 behavior.
|
|
|
|
// Example:
|
|
|
|
// cmd1: json.get wikipedia .foo .address
|
|
|
|
// cmd2: json.get wikipedia .foo $.address
|
|
|
|
// The expected behavior is: Cmd1 should fail because .foo does not exist, while cmd2 should succeed because
|
|
|
|
// overall the command should conform to V2 behavior (as the 2nd path is V2 path). Cmd2 should return the
|
|
|
|
// following result:
|
|
|
|
// 127.0.0.1:6379> json.get wikipedia .foo $.address
|
|
|
|
// {"$.address":[{"street":"21 2nd Street","city":"New York","state":"NY","zipcode":"10021-3100"}],".foo":[]}
|
|
|
|
//
|
|
|
|
// Without the pre-knowledge of V1 vs V2, both commands would fail, because when the selector first runs ".foo",
|
|
|
|
// it would think it is V1 and returns an error. The loop below would then exit without attempting the 2nd path.
|
|
|
|
bool is_v2path = Selector::has_at_least_one_v2path(paths, num_paths);
|
|
|
|
|
|
|
|
// Values at multiple paths are combined to form a serialized JSON object string, in which each path is a key.
|
|
|
|
JsonUtilCode rc = buildJsonForMultiPaths(doc, paths, num_paths, is_v2path, format, oss);
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
// update stats
|
|
|
|
if (update_stats) jsonstats_update_stats_on_read(oss.GetLength());
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_delete_value(JDocument *doc, const char *json_path, size_t &num_vals_deleted) {
|
|
|
|
Selector selector;
|
|
|
|
return selector.deleteValues(doc->GetJValue(), json_path, num_vals_deleted);
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if there is at least one number value
|
|
|
|
STATIC bool has_number_value(jsn::vector<JValue*> &values) {
|
|
|
|
for (auto &v : values) {
|
|
|
|
if (v->IsNumber()) return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_increment_by(JDocument *doc, const char *json_path, const JValue *incr_by,
|
|
|
|
jsn::vector<double> &out_vals, bool &is_v2_path) {
|
|
|
|
out_vals.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), json_path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path:
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no number value is selected
|
|
|
|
if (!has_number_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_NUMBER;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &val : selector.getUniqueResultSet()) {
|
|
|
|
if (val.first->IsNumber()) {
|
|
|
|
if (val.first->IsInt64() && incr_by->IsInt64()) {
|
|
|
|
// All are integers
|
|
|
|
int64_t res;
|
|
|
|
rc = jsonutil_add_int64(val.first->GetInt64(), incr_by->GetInt64(), &res);
|
|
|
|
if (rc == JSONUTIL_SUCCESS) {
|
|
|
|
val.first->SetInt64(res);
|
|
|
|
out_vals.push_back(res);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
double res;
|
|
|
|
rc = jsonutil_add_double(val.first->GetDouble(), incr_by->GetDouble(), &res);
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
char double_string[BUF_SIZE_DOUBLE_JSON];
|
|
|
|
size_t len = jsonutil_double_to_string(res, double_string, sizeof(double_string));
|
|
|
|
val.first->SetDouble(double_string, len, allocator);
|
|
|
|
|
|
|
|
out_vals.push_back(res);
|
|
|
|
} else {
|
|
|
|
out_vals.push_back(std::nan("NaN")); // indicates the value is not number
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_multiply_by(JDocument *doc, const char *json_path, const JValue *mult_by,
|
|
|
|
jsn::vector<double> &out_vals, bool &is_v2_path) {
|
|
|
|
out_vals.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), json_path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path:
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no number value is selected
|
|
|
|
if (!has_number_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_NUMBER;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &val : selector.getUniqueResultSet()) {
|
|
|
|
if (val.first->IsNumber()) {
|
|
|
|
double res;
|
|
|
|
rc = jsonutil_multiply_double(val.first->GetDouble(), mult_by->GetDouble(), &res);
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
if (jsonutil_is_int64(res)) {
|
|
|
|
val.first->SetInt64(static_cast<int64_t>(res));
|
|
|
|
} else {
|
|
|
|
char double_string[BUF_SIZE_DOUBLE_JSON];
|
|
|
|
size_t len = jsonutil_double_to_string(res, double_string, sizeof(double_string));
|
|
|
|
val.first->SetDouble(double_string, len, allocator);
|
|
|
|
}
|
|
|
|
|
|
|
|
out_vals.push_back(res);
|
|
|
|
} else {
|
|
|
|
out_vals.push_back(std::nan("NaN")); // indicates the value is not number
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if there is at least one boolean value
|
|
|
|
STATIC bool has_boolean_value(jsn::vector<JValue*> &values) {
|
|
|
|
for (auto &v : values) {
|
|
|
|
if (v->IsBool()) return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_toggle(JDocument *doc, const char *path, jsn::vector<int> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no boolean value is selected
|
|
|
|
if (!has_boolean_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_BOOL;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &v : selector.getUniqueResultSet()) {
|
|
|
|
if (v.first->IsBool()) {
|
|
|
|
bool res = v.first->GetBool();
|
|
|
|
res = !res;
|
|
|
|
v.first->SetBool(res);
|
|
|
|
vec.push_back(res? 1 : 0);
|
|
|
|
} else {
|
|
|
|
vec.push_back(-1); // -1 means the source value is not boolean
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if there is at least one string value
|
|
|
|
STATIC bool has_string_value(jsn::vector<JValue*> &values) {
|
|
|
|
for (auto &v : values) {
|
|
|
|
if (v->IsString()) return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_string_length(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no string value is selected
|
|
|
|
if (!has_string_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_STRING;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &v : values) {
|
|
|
|
if (v->IsString()) {
|
|
|
|
vec.push_back(v->GetStringLength());
|
|
|
|
} else {
|
|
|
|
vec.push_back(SIZE_MAX); // indicates non-string value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_string_append(JDocument *doc, const char *path, const char *json, const size_t json_len,
|
|
|
|
jsn::vector<size_t> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path:
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no string value is selected
|
|
|
|
if (!has_string_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_STRING;
|
|
|
|
}
|
|
|
|
|
|
|
|
// verify the input json string is a valid
|
|
|
|
JParser appendVal;
|
|
|
|
if (appendVal.Parse(json, json_len).HasParseError()) return appendVal.GetParseErrorCode();
|
|
|
|
if (!appendVal.GetJValue().IsString()) return JSONUTIL_VALUE_NOT_STRING;
|
|
|
|
|
|
|
|
jsn::string str_append = jsn::string(appendVal.GetString());
|
|
|
|
for (auto &v : selector.getUniqueResultSet()) {
|
|
|
|
if (v.first->IsString()) {
|
|
|
|
jsn::string new_string = jsn::string(v.first->GetString()) + str_append;
|
|
|
|
v.first->SetString(new_string.c_str(), new_string.length(), allocator);
|
|
|
|
vec.push_back(new_string.length());
|
|
|
|
} else {
|
|
|
|
vec.push_back(SIZE_MAX); // indicates non-string value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if there is at least one object value
|
|
|
|
STATIC bool has_object_value(jsn::vector<JValue*> &values) {
|
|
|
|
for (auto &v : values) {
|
|
|
|
if (v->IsObject()) return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_object_length(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no object value is selected
|
|
|
|
if (!has_object_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_OBJECT;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &v : values) {
|
|
|
|
if (v->IsObject()) {
|
|
|
|
vec.push_back(v->MemberCount());
|
|
|
|
} else {
|
|
|
|
vec.push_back(SIZE_MAX); // indicates non-object value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_object_keys(JDocument *doc, const char *path,
|
|
|
|
jsn::vector<jsn::vector<jsn::string>> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no object value is selected
|
|
|
|
if (!has_object_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_OBJECT;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &v : values) {
|
|
|
|
jsn::vector<jsn::string> keys;
|
|
|
|
if (v->IsObject()) {
|
|
|
|
for (auto &m : v->GetObject()) {
|
|
|
|
keys.push_back(std::move(jsn::string(m.name.GetString(), m.name.GetStringLength())));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
vec.push_back(keys);
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if there is at least one array value
|
|
|
|
STATIC bool has_array_value(jsn::vector<JValue*> &values) {
|
|
|
|
for (auto &v : values) {
|
|
|
|
if (v->IsArray()) return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_array_length(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no array value is selected
|
|
|
|
if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &v : values) {
|
|
|
|
if (v->IsArray()) {
|
|
|
|
vec.push_back(v->Size());
|
|
|
|
} else {
|
|
|
|
vec.push_back(SIZE_MAX); // indicates non-array value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_array_append(ValkeyModuleCtx *ctx, JDocument *doc, const char *path,
|
|
|
|
const char **jsons, size_t *json_lens, const size_t num_values,
|
|
|
|
jsn::vector<size_t> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path:
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no array value is selected
|
|
|
|
if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse json values
|
|
|
|
jsn::vector<JParser> appendVals(num_values);
|
|
|
|
size_t totalJValueSize = 0;
|
|
|
|
for (size_t i=0; i < num_values; i++) {
|
|
|
|
if (appendVals[i].Parse(jsons[i], json_lens[i]).HasParseError()) {
|
|
|
|
return appendVals[i].GetParseErrorCode();
|
|
|
|
}
|
|
|
|
CHECK_DOCUMENT_PATH_LIMIT(ctx, selector, appendVals[i])
|
|
|
|
totalJValueSize += appendVals[i].GetJValueSize();
|
|
|
|
}
|
|
|
|
CHECK_DOCUMENT_SIZE_LIMIT(ctx, doc->size, totalJValueSize)
|
|
|
|
|
|
|
|
for (auto &v : selector.getUniqueResultSet()) {
|
|
|
|
if (v.first->IsArray()) {
|
|
|
|
for (size_t i=0; i < num_values; i++) {
|
|
|
|
// Need to make a copy of the value because after the first call of JValue::PushBack,
|
|
|
|
// the object is moved and can no longer be pushed into anther array.
|
|
|
|
JValue copy(appendVals[i], allocator);
|
|
|
|
v.first->PushBack(copy, allocator);
|
|
|
|
}
|
|
|
|
vec.push_back(v.first->Size());
|
|
|
|
} else {
|
|
|
|
vec.push_back(SIZE_MAX); // indicates non-array value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC void internal_array_pop(JValue &arrVal, int64_t index, jsn::vector<rapidjson::StringBuffer> &vec,
|
|
|
|
rapidjson::StringBuffer &oss) {
|
|
|
|
// Convert negative index to positive
|
|
|
|
int64_t size = arrVal.Size();
|
|
|
|
if (index < 0) index = (arrVal.Size() == 0 ? 0 : size + index);
|
|
|
|
|
|
|
|
// Out-of-bound index is rounded to respective array bounds
|
|
|
|
if (index >= size) index = size - 1;
|
|
|
|
if (index < 0) index = 0;
|
|
|
|
|
|
|
|
serialize_value(arrVal[index], 0, nullptr, oss);
|
|
|
|
arrVal.Erase(arrVal.Begin() + index);
|
|
|
|
vec.push_back(std::move(oss));
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_array_pop(JDocument *doc, const char *path, int64_t index,
|
|
|
|
jsn::vector<rapidjson::StringBuffer> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path:
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no array value is selected
|
|
|
|
if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &v : selector.getUniqueResultSet()) {
|
|
|
|
rapidjson::StringBuffer oss;
|
|
|
|
if (v.first->IsArray()) {
|
|
|
|
if (v.first->Empty()) {
|
|
|
|
vec.push_back(std::move(oss)); // empty array, oss is empty
|
|
|
|
} else {
|
|
|
|
internal_array_pop(*v.first, index, vec, oss);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
vec.push_back(std::move(oss)); // non-array value, oss is empty
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC JsonUtilCode internal_array_insert(JValue &arrVal, jsn::vector<JParser> &insertVals,
|
|
|
|
const size_t num_values, int64_t index, jsn::vector<size_t> &vec) {
|
|
|
|
size_t size = arrVal.Size();
|
|
|
|
|
|
|
|
// Negative index values are interpreted as starting from the end.
|
|
|
|
if (index < 0) index = (arrVal.Size() == 0 ? 0 : size + index);
|
|
|
|
|
|
|
|
// Return error if the index is out of bounds.
|
|
|
|
// If index is size-1, we are inserting before the last element.
|
|
|
|
// If index is size, we are appending to the array.
|
|
|
|
if (index < 0 || index > static_cast<int64_t>(size)) return JSONUTIL_INDEX_OUT_OF_ARRAY_BOUNDARIES;
|
|
|
|
|
|
|
|
// append num_values empty values
|
|
|
|
for (size_t i=0; i < num_values; i++) {
|
|
|
|
JValue empty;
|
|
|
|
arrVal.PushBack(empty, allocator);
|
|
|
|
}
|
|
|
|
|
|
|
|
// shift values [index+1..end-num_values] to the right by num_values positions
|
|
|
|
for (int64_t i = arrVal.Size() - 1; i >= static_cast<int64_t>(num_values) + index; i--) {
|
|
|
|
arrVal[i] = arrVal[i - num_values];
|
|
|
|
}
|
|
|
|
|
|
|
|
// overwrite values [index..index+num_values-1]
|
|
|
|
for (int64_t i=index; i < index + static_cast<int64_t>(num_values); i++) {
|
|
|
|
// Need to make a copy of the value to insert because after the value is assigned,
|
|
|
|
// is is moved and can no longer be assigned into anther value.
|
|
|
|
JValue copy(insertVals[i - index].GetJValue(), allocator);
|
|
|
|
arrVal[i] = copy;
|
|
|
|
}
|
|
|
|
|
|
|
|
vec.push_back(arrVal.Size());
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_array_insert(ValkeyModuleCtx *ctx, JDocument *doc, const char *path, int64_t index,
|
|
|
|
const char **jsons, size_t *json_lens, const size_t num_values,
|
|
|
|
jsn::vector<size_t> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path:
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no array value is selected
|
|
|
|
if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse json values
|
|
|
|
jsn::vector<JParser> insertVals(num_values);
|
|
|
|
size_t totalJValueSize = 0;
|
|
|
|
for (size_t i=0; i < num_values; i++) {
|
|
|
|
if (insertVals[i].Parse(jsons[i], json_lens[i]).HasParseError()) {
|
|
|
|
return insertVals[i].GetParseErrorCode();
|
|
|
|
}
|
|
|
|
CHECK_DOCUMENT_PATH_LIMIT(ctx, selector, insertVals[i])
|
|
|
|
totalJValueSize += insertVals[i].GetJValueSize();
|
|
|
|
}
|
|
|
|
CHECK_DOCUMENT_SIZE_LIMIT(ctx, doc->size, totalJValueSize)
|
|
|
|
|
|
|
|
for (auto &v : selector.getUniqueResultSet()) {
|
|
|
|
if (v.first->IsArray()) {
|
|
|
|
rc = internal_array_insert(*v.first, insertVals, num_values, index, vec);
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
} else {
|
|
|
|
vec.push_back(SIZE_MAX); // indicates non-array value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC void internal_array_trim(JValue &arrVal, int64_t start, int64_t stop, jsn::vector<size_t> &vec) {
|
|
|
|
int64_t size = static_cast<int64_t>(arrVal.Size());
|
|
|
|
if (size == 0) {
|
|
|
|
vec.push_back(0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if start < 0, set it to 0.
|
|
|
|
if (start < 0) start = 0;
|
|
|
|
|
|
|
|
// if stop >= size, set it to size-1
|
|
|
|
if (stop >= size) stop = size - 1;
|
|
|
|
|
|
|
|
if (start >= size || start > stop) {
|
|
|
|
// If start >= size or start > stop, empty the array and return *new_len as 0.
|
|
|
|
arrVal.Erase(arrVal.Begin(), arrVal.End());
|
|
|
|
vec.push_back(0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stop < size-1)
|
|
|
|
arrVal.Erase(arrVal.Begin() + stop + 1, arrVal.Begin() + size);
|
|
|
|
if (start > 0)
|
|
|
|
arrVal.Erase(arrVal.Begin(), arrVal.Begin() + start);
|
|
|
|
|
|
|
|
vec.push_back(arrVal.Size());
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_array_trim(JDocument *doc, const char *path, int64_t start, int64_t stop,
|
|
|
|
jsn::vector<size_t> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path:
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no array value is selected
|
|
|
|
if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &v : selector.getUniqueResultSet()) {
|
|
|
|
if (v.first->IsArray()) {
|
|
|
|
internal_array_trim(*v.first, start, stop, vec);
|
|
|
|
} else {
|
|
|
|
vec.push_back(SIZE_MAX); // indicates non-array value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_clear(JDocument *doc, const char *path, size_t &elements_cleared) {
|
|
|
|
elements_cleared = 0;
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
if (rc != JSONUTIL_SUCCESS) return rc;
|
|
|
|
|
|
|
|
for (auto &v : selector.getUniqueResultSet()) {
|
|
|
|
if (v.first->IsArray()) {
|
|
|
|
if (!v.first->Empty()) {
|
|
|
|
v.first->Erase(v.first->Begin(), v.first->End());
|
|
|
|
elements_cleared++;
|
|
|
|
}
|
|
|
|
} else if (v.first->IsObject()) {
|
|
|
|
if (!v.first->ObjectEmpty()) {
|
|
|
|
v.first->RemoveAllMembers();
|
|
|
|
elements_cleared++;
|
|
|
|
}
|
|
|
|
} else if (v.first->IsBool()) {
|
|
|
|
if (v.first->IsTrue()) {
|
|
|
|
v.first->SetBool(false);
|
|
|
|
elements_cleared++;
|
|
|
|
}
|
|
|
|
} else if (v.first->IsString()) {
|
|
|
|
if (v.first->GetStringLength() > 0) {
|
|
|
|
v.first->SetString("");
|
|
|
|
elements_cleared++;
|
|
|
|
}
|
|
|
|
} else if (v.first->IsInt()) {
|
|
|
|
if (v.first->GetInt() != 0) {
|
|
|
|
v.first->SetInt(0);
|
|
|
|
elements_cleared++;
|
|
|
|
}
|
|
|
|
} else if (v.first->IsInt64()) {
|
|
|
|
if (v.first->GetInt64() !=0) {
|
|
|
|
v.first->SetInt64(0);
|
|
|
|
elements_cleared++;
|
|
|
|
}
|
|
|
|
} else if (v.first->IsUint()) {
|
|
|
|
if (v.first->GetUint() != 0) {
|
|
|
|
v.first->SetUint(0);
|
|
|
|
elements_cleared++;
|
|
|
|
}
|
|
|
|
} else if (v.first->IsUint64()) {
|
|
|
|
if (v.first->GetUint64() != 0) {
|
|
|
|
v.first->SetUint64(0);
|
|
|
|
elements_cleared++;
|
|
|
|
}
|
|
|
|
} else if (v.first->IsDouble()) {
|
|
|
|
if (v.first->GetDouble() < 0.0 || v.first->GetDouble() > 0.0) {
|
|
|
|
v.first->SetDouble("0.0", 3, allocator);
|
|
|
|
elements_cleared++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC void internal_array_index_of(const JValue &arrVal, const JValue &inputVal, int64_t start, int64_t stop,
|
|
|
|
jsn::vector<int64_t> &vec) {
|
|
|
|
int64_t size = static_cast<int64_t>(arrVal.Size());
|
|
|
|
if (size == 0) {
|
|
|
|
vec.push_back(-1);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if stop == 0 or -1, the last element is included.
|
|
|
|
if (stop == 0 || stop == -1) stop = size;
|
|
|
|
|
|
|
|
// if stop > size, set it to size.
|
|
|
|
if (stop > size) stop = size;
|
|
|
|
|
|
|
|
if (start > stop) {
|
|
|
|
vec.push_back(-1);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int64_t i=start; i < stop; i++) {
|
|
|
|
if (arrVal[i] == inputVal) {
|
|
|
|
vec.push_back(i);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
vec.push_back(-1); // not found
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_array_index_of(JDocument *doc, const char *path, const char *scalar_val,
|
|
|
|
const size_t scalar_val_len, int64_t start, int64_t stop,
|
|
|
|
jsn::vector<int64_t> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
jsn::vector<JValue*> values;
|
|
|
|
selector.getSelectedValues(values);
|
|
|
|
|
|
|
|
// Legacy path
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (values.empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
// return WRONGTYPE error if no array value is selected
|
|
|
|
if (!has_array_value(values)) return JSONUTIL_JSON_ELEMENT_NOT_ARRAY;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if start < 0, set it to 0.
|
|
|
|
if (start < 0) start = 0;
|
|
|
|
|
|
|
|
// verify the input value is valid JSON
|
|
|
|
JParser inputVal;
|
|
|
|
if (inputVal.Parse(scalar_val, scalar_val_len).HasParseError()) return inputVal.GetParseErrorCode();
|
|
|
|
|
|
|
|
for (auto &v : values) {
|
|
|
|
if (v->IsArray()) {
|
|
|
|
internal_array_index_of(*v, inputVal.GetJValue(), start, stop, vec);
|
|
|
|
} else {
|
|
|
|
vec.push_back(INT64_MAX); // indicates non-array value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_value_type(JDocument *doc, const char *path, jsn::vector<jsn::string> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Legacy path
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (selector.getResultSet().empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
}
|
|
|
|
|
|
|
|
// JSON type name returned to client for command JSON.TYPE
|
|
|
|
static const char *TYPE_NAMES[] = {"null", "boolean", "string", "number", "integer", "object", "array"};
|
|
|
|
|
|
|
|
for (auto &v : selector.getResultSet()) {
|
|
|
|
switch (v.first->GetType()) {
|
|
|
|
case rapidjson::kNullType:
|
|
|
|
vec.push_back(std::move(jsn::string(TYPE_NAMES[0])));
|
|
|
|
break;
|
|
|
|
case rapidjson::kTrueType:
|
|
|
|
case rapidjson::kFalseType:
|
|
|
|
vec.push_back(std::move(jsn::string(TYPE_NAMES[1])));
|
|
|
|
break;
|
|
|
|
case rapidjson::kStringType:
|
|
|
|
vec.push_back(std::move(jsn::string(TYPE_NAMES[2])));
|
|
|
|
break;
|
|
|
|
case rapidjson::kNumberType: {
|
|
|
|
if (v.first->IsDouble())
|
|
|
|
vec.push_back(std::move(jsn::string(TYPE_NAMES[3])));
|
|
|
|
else
|
|
|
|
vec.push_back(std::move(jsn::string(TYPE_NAMES[4])));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case rapidjson::kObjectType:
|
|
|
|
vec.push_back(std::move(jsn::string(TYPE_NAMES[5])));
|
|
|
|
break;
|
|
|
|
case rapidjson::kArrayType:
|
|
|
|
vec.push_back(std::move(jsn::string(TYPE_NAMES[6])));
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
ValkeyModule_Assert(false);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC void dom_reply_with_resp_internal(ValkeyModuleCtx *ctx, const JValue& val) {
|
|
|
|
switch (val.GetType()) {
|
|
|
|
case rapidjson::kObjectType: {
|
|
|
|
ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_ARRAY_LEN);
|
|
|
|
ValkeyModule_ReplyWithSimpleString(ctx, "{");
|
|
|
|
long len = 1;
|
|
|
|
for (auto &m : val.GetObject()) {
|
|
|
|
ValkeyModule_ReplyWithArray(ctx, 2);
|
|
|
|
ValkeyModule_ReplyWithStringBuffer(ctx, m.name.GetString(), m.name.GetStringLength());
|
|
|
|
dom_reply_with_resp_internal(ctx, m.value);
|
|
|
|
len++;
|
|
|
|
}
|
|
|
|
ValkeyModule_ReplySetArrayLength(ctx, len);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case rapidjson::kArrayType: {
|
|
|
|
ValkeyModule_ReplyWithArray(ctx, VALKEYMODULE_POSTPONED_ARRAY_LEN);
|
|
|
|
ValkeyModule_ReplyWithSimpleString(ctx, "[");
|
|
|
|
for (auto &m : val.GetArray()) {
|
|
|
|
dom_reply_with_resp_internal(ctx, m);
|
|
|
|
}
|
|
|
|
ValkeyModule_ReplySetArrayLength(ctx, val.Size() + 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case rapidjson::kNullType:
|
|
|
|
ValkeyModule_ReplyWithNull(ctx);
|
|
|
|
break;
|
|
|
|
case rapidjson::kTrueType:
|
|
|
|
ValkeyModule_ReplyWithSimpleString(ctx, "true");
|
|
|
|
break;
|
|
|
|
case rapidjson::kFalseType:
|
|
|
|
ValkeyModule_ReplyWithSimpleString(ctx, "false");
|
|
|
|
break;
|
|
|
|
case rapidjson::kNumberType: {
|
|
|
|
if (val.IsInt()) {
|
|
|
|
ValkeyModule_ReplyWithLongLong(ctx, val.GetInt());
|
|
|
|
} else if (val.IsInt64()) {
|
|
|
|
ValkeyModule_ReplyWithLongLong(ctx, val.GetInt64());
|
|
|
|
} else if (val.IsUint()) {
|
|
|
|
ValkeyModule_ReplyWithLongLong(ctx, val.GetUint());
|
|
|
|
} else if (val.IsUint64()) {
|
|
|
|
ValkeyModule_ReplyWithLongLong(ctx, val.GetUint64());
|
|
|
|
} else {
|
|
|
|
ValkeyModule_Assert(val.IsDouble());
|
|
|
|
char str[BUF_SIZE_DOUBLE_RAPID_JSON];
|
|
|
|
size_t len = jsonutil_double_to_string_rapidjson(val.GetDouble(), str, sizeof(str));
|
|
|
|
ValkeyModule_ReplyWithStringBuffer(ctx, str, len);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case rapidjson::kStringType:
|
|
|
|
ValkeyModule_ReplyWithStringBuffer(ctx, val.GetString(), val.GetStringLength());
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
ValkeyModule_Assert(false);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_reply_with_resp(ValkeyModuleCtx *ctx, JDocument *doc, const char *path) {
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (selector.getResultSet().empty()) {
|
|
|
|
if (!selector.isV2Path) {
|
|
|
|
// Legacy path, return NONEXISTENT
|
|
|
|
return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
} else {
|
|
|
|
// JSONPath, return empty array
|
|
|
|
ValkeyModule_ReplyWithEmptyArray(ctx);
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (selector.isV2Path) ValkeyModule_ReplyWithArray(ctx, selector.getResultSet().size());
|
|
|
|
|
|
|
|
for (auto &v : selector.getResultSet()) {
|
|
|
|
dom_reply_with_resp_internal(ctx, *v.first);
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_mem_size(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path,
|
|
|
|
bool default_path) {
|
|
|
|
vec.clear();
|
|
|
|
// Optimization:
|
|
|
|
// The size of the whole document should be obtained from the meta data attached to document object.
|
|
|
|
if (jsonutil_is_root_path(path) && default_path) {
|
|
|
|
vec.push_back(dom_get_doc_size(doc));
|
|
|
|
is_v2_path = !strcmp(path, "$");
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Legacy path
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (selector.getResultSet().empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &v : selector.getResultSet()) {
|
2025-01-05 19:14:42 -05:00
|
|
|
if (jsonutil_is_root_path(path)) {
|
|
|
|
vec.push_back(dom_get_doc_size_no_opt(static_cast<const JDocument*>(v.first)));
|
|
|
|
} else {
|
|
|
|
vec.push_back(sizeof(*v.first) + v.first->ComputeMallocedSize());
|
|
|
|
}
|
2024-11-29 07:47:54 -08:00
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC size_t num_fields_internal(JValue& v) {
|
|
|
|
size_t num_fields = 1;
|
|
|
|
if (v.IsObject()) {
|
|
|
|
for (auto &m : v.GetObject())
|
|
|
|
num_fields += num_fields_internal(m.value);
|
|
|
|
} else if (v.IsArray()) {
|
|
|
|
for (auto &m : v.GetArray())
|
|
|
|
num_fields += num_fields_internal(m);
|
|
|
|
}
|
|
|
|
return num_fields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If the top-level JSON value is a container (object or array), we want to return number of fields IN the container,
|
|
|
|
* not to count the container itself. For a nested container object, we want to count the container itself.
|
|
|
|
* e.g., { "address": { "street": "21 2nd Street", "city": "New York", "state": "NY", "zipcode": "10021-3100" } }.
|
|
|
|
* If we are counting number of fields in the root doc, the address field is counted. So, there are 5 fields.
|
|
|
|
* But If we are counting number of fields for the "address" value, the answer is 4, excluding address field itself.
|
|
|
|
*/
|
|
|
|
JsonUtilCode dom_num_fields(JDocument *doc, const char *path, jsn::vector<size_t> &vec, bool &is_v2_path) {
|
|
|
|
vec.clear();
|
|
|
|
Selector selector;
|
|
|
|
JsonUtilCode rc = selector.getValues(doc->GetJValue(), path);
|
|
|
|
is_v2_path = selector.isV2Path;
|
|
|
|
if (rc != JSONUTIL_SUCCESS) {
|
|
|
|
if (selector.isLegacyJsonPathSyntax()) return rc;
|
|
|
|
// For v2 path, return error code only if it's a syntax error.
|
|
|
|
if (selector.isSyntaxError(rc)) return rc;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Legacy path
|
|
|
|
if (!is_v2_path) {
|
|
|
|
// return NONEXISTENT error if no value is selected
|
|
|
|
if (selector.getResultSet().empty()) return JSONUTIL_JSON_PATH_NOT_EXIST;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto &v : selector.getResultSet()) {
|
|
|
|
size_t count = num_fields_internal(*v.first);
|
|
|
|
if (v.first->IsObject() || v.first->IsArray())
|
|
|
|
count--; // exclude the container itself
|
|
|
|
vec.push_back(count);
|
|
|
|
}
|
|
|
|
return JSONUTIL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
STATIC void find_path_depth_internal(JValue& v, size_t d, size_t *max_depth) {
|
|
|
|
*max_depth = std::max(d, *max_depth);
|
|
|
|
if (v.IsObject()) {
|
|
|
|
for (auto &m : v.GetObject())
|
|
|
|
find_path_depth_internal(m.value, d+1, max_depth);
|
|
|
|
} else if (v.IsArray()) {
|
|
|
|
for (auto &m : v.GetArray())
|
|
|
|
find_path_depth_internal(m, d+1, max_depth);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void dom_path_depth(JDocument *doc, size_t *depth) {
|
|
|
|
*depth = 0;
|
|
|
|
find_path_depth_internal(doc->GetJValue(), 0, depth);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Make a copy of this document
|
|
|
|
*/
|
|
|
|
JDocument *dom_copy(const JDocument *src) {
|
|
|
|
int64_t begin_val = jsonstats_begin_track_mem();
|
|
|
|
|
|
|
|
JDocument *dst = create_doc();
|
|
|
|
dst->CopyFrom(*src, allocator);
|
|
|
|
|
|
|
|
int64_t delta = jsonstats_end_track_mem(begin_val);
|
|
|
|
ValkeyModule_Assert(delta > 0);
|
|
|
|
dom_set_doc_size(dst, static_cast<size_t>(delta));
|
|
|
|
|
|
|
|
return dst;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* RDB File Format.
|
|
|
|
*
|
|
|
|
* Each JValue in RDB file format has a type code followed by type-specific data
|
|
|
|
*/
|
|
|
|
enum meta_codes {
|
|
|
|
JSON_METACODE_NULL = 0x01, // Nothing follows
|
|
|
|
JSON_METACODE_STRING = 0x02, // Followed by the string
|
|
|
|
JSON_METACODE_DOUBLE = 0x04, // Followed by the double
|
|
|
|
JSON_METACODE_INTEGER = 0x08, // Coded as a 64-bit Signed Integer
|
|
|
|
JSON_METACODE_BOOLEAN = 0x10, // Coded as the string '1' or '0'
|
|
|
|
JSON_METACODE_OBJECT = 0x20, // Followed by a member count, and then N "pairs"
|
|
|
|
JSON_METACODE_ARRAY = 0x40, // Followed by a element count and then n JValue elements
|
|
|
|
JSON_METACODE_PAIR = 0x80 // Codes an object Memory, a string(member name) and a JValue
|
|
|
|
};
|
|
|
|
|
|
|
|
//
|
|
|
|
// save a JValue, recurse as required for object and array
|
|
|
|
//
|
|
|
|
STATIC void store_JValue(ValkeyModuleIO *rdb, const JValue *val) {
|
|
|
|
if (val->IsNull()) {
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_NULL);
|
|
|
|
} else if (val->IsString()) {
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_STRING);
|
|
|
|
ValkeyModule_SaveStringBuffer(rdb, val->GetString(), val->GetStringLength());
|
|
|
|
} else if (val->IsNumber()) {
|
|
|
|
if (val->IsDouble()) {
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_DOUBLE);
|
|
|
|
ValkeyModule_SaveDouble(rdb, val->GetDouble());
|
|
|
|
} else {
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_INTEGER);
|
|
|
|
if (val->IsInt64() || val->IsInt()) {
|
|
|
|
ValkeyModule_SaveSigned(rdb, val->GetInt64());
|
|
|
|
} else {
|
|
|
|
// rdb format doesn't understand unsigned, fail on numbers that aren't handled correctly
|
|
|
|
ValkeyModule_Assert(val->GetUint64() < static_cast<uint64_t>(1L << 63));
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, val->GetUint64());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (val->IsFalse()) {
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_BOOLEAN);
|
|
|
|
ValkeyModule_SaveStringBuffer(rdb, "0", 1);
|
|
|
|
} else if (val->IsTrue()) {
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_BOOLEAN);
|
|
|
|
ValkeyModule_SaveStringBuffer(rdb, "1", 1);
|
|
|
|
} else if (val->IsObject()) {
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_OBJECT);
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, val->MemberCount());
|
|
|
|
for (auto m = val->MemberBegin(); m != val->MemberEnd(); ++m) {
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_PAIR);
|
|
|
|
ValkeyModule_SaveStringBuffer(rdb, m->name.GetString(), m->name.GetStringLength());
|
|
|
|
store_JValue(rdb, &m->value);
|
|
|
|
}
|
|
|
|
} else if (val->IsArray()) {
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, JSON_METACODE_ARRAY);
|
|
|
|
ValkeyModule_SaveUnsigned(rdb, val->Size());
|
|
|
|
for (size_t i = 0; i < val->Size(); ++i) {
|
|
|
|
store_JValue(rdb, &(*val)[i]);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ValkeyModule_Assert(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void dom_save(const JDocument *doc, ValkeyModuleIO *rdb, int encver) {
|
|
|
|
switch (encver) {
|
|
|
|
case 3: {
|
|
|
|
rapidjson::StringBuffer oss;
|
|
|
|
serialize_value(*(doc), 0, nullptr, oss);
|
|
|
|
ValkeyModule_SaveStringBuffer(rdb, oss.GetString(), oss.GetLength());
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 0:
|
|
|
|
store_JValue(rdb, doc);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
ValkeyModule_Assert(0);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Helper function, read string into a JValue
|
|
|
|
STATIC JValue readStringAsJValue(ValkeyModuleIO *rdb) {
|
|
|
|
// The modern take is that we have doubles as strings
|
|
|
|
size_t str_len;
|
|
|
|
char *str = ValkeyModule_LoadStringBuffer(rdb, &str_len);
|
|
|
|
if (str) {
|
|
|
|
JValue v(str, str_len, allocator);
|
|
|
|
ValkeyModule_Free(str);
|
|
|
|
return v;
|
|
|
|
} else {
|
|
|
|
ValkeyModule_LogIOError(rdb, "error", "Unable to read string or double");
|
|
|
|
return JValue();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Helper function, read legacy double into a new string double JValue
|
|
|
|
STATIC JValue readLegacyDoubleAsJValue(ValkeyModuleIO *rdb) {
|
|
|
|
double d = ValkeyModule_LoadDouble(rdb);
|
|
|
|
char str[BUF_SIZE_DOUBLE_JSON];
|
|
|
|
size_t str_len = jsonutil_double_to_string(d, str, sizeof(str));
|
|
|
|
if (str) {
|
|
|
|
JValue v(str, str_len, allocator, false, /* isdouble */ true);
|
|
|
|
return v;
|
|
|
|
} else {
|
|
|
|
ValkeyModule_LogIOError(rdb, "error", "Unable to read legacy double");
|
|
|
|
return JValue();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* One instance of this is passed to all recursive invokations of rdbLoadJValue
|
|
|
|
*/
|
|
|
|
typedef struct load_params {
|
|
|
|
ValkeyModuleIO *rdb;
|
|
|
|
unsigned nestLevel;
|
|
|
|
JsonUtilCode status;
|
|
|
|
} load_params;
|
|
|
|
|
|
|
|
JValue rdbLoadJValue(load_params *params) {
|
|
|
|
uint64_t code = ValkeyModule_LoadUnsigned(params->rdb);
|
|
|
|
switch (code) {
|
|
|
|
case JSON_METACODE_NULL:
|
|
|
|
return JValue();
|
|
|
|
case JSON_METACODE_STRING:
|
|
|
|
return readStringAsJValue(params->rdb);
|
|
|
|
case JSON_METACODE_DOUBLE:
|
|
|
|
return readLegacyDoubleAsJValue(params->rdb);
|
|
|
|
case JSON_METACODE_INTEGER:
|
|
|
|
return JValue(ValkeyModule_LoadSigned(params->rdb));
|
|
|
|
case JSON_METACODE_BOOLEAN: {
|
|
|
|
size_t strlen;
|
|
|
|
char *s = ValkeyModule_LoadStringBuffer(params->rdb, &strlen);
|
|
|
|
char c = (s && strlen == 1) ? *s : 0;
|
|
|
|
ValkeyModule_Free(s);
|
|
|
|
switch (c) {
|
|
|
|
case '1': return JValue(true);
|
|
|
|
case '0': return JValue(false);
|
|
|
|
default:
|
|
|
|
params->status = JSONUTIL_INVALID_RDB_FORMAT;
|
|
|
|
ValkeyModule_LogIOError(params->rdb, "error", "invalid boolean format");
|
|
|
|
return JValue();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case JSON_METACODE_OBJECT: {
|
|
|
|
uint64_t members = ValkeyModule_LoadUnsigned(params->rdb);
|
|
|
|
JValue obj;
|
|
|
|
obj.SetObject();
|
|
|
|
if (params->nestLevel >= json_get_max_path_limit()) {
|
|
|
|
ValkeyModule_LogIOError(params->rdb, "error", "document path limit exceeded");
|
|
|
|
params->status = JSONUTIL_DOCUMENT_PATH_LIMIT_EXCEEDED;
|
|
|
|
return JValue();
|
|
|
|
}
|
|
|
|
params->nestLevel++;
|
|
|
|
while (members--) {
|
|
|
|
uint64_t paircode = ValkeyModule_LoadUnsigned(params->rdb);
|
|
|
|
if (paircode != JSON_METACODE_PAIR) {
|
|
|
|
params->status = JSONUTIL_INVALID_RDB_FORMAT;
|
|
|
|
ValkeyModule_LogIOError(params->rdb, "error", "Invalid pair code");
|
|
|
|
params->nestLevel--;
|
|
|
|
return JValue();
|
|
|
|
}
|
|
|
|
JValue key = readStringAsJValue(params->rdb);
|
|
|
|
JValue value = rdbLoadJValue(params);
|
|
|
|
obj.AddMember(key, value, allocator);
|
|
|
|
}
|
|
|
|
params->nestLevel--;
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
case JSON_METACODE_ARRAY: {
|
|
|
|
uint64_t length = ValkeyModule_LoadUnsigned(params->rdb);
|
|
|
|
JValue array;
|
|
|
|
array.SetArray();
|
|
|
|
array.Reserve(length, allocator);
|
|
|
|
if (params->nestLevel >= json_get_max_path_limit()) {
|
|
|
|
params->status = JSONUTIL_DOCUMENT_PATH_LIMIT_EXCEEDED;
|
|
|
|
ValkeyModule_LogIOError(params->rdb, "error", "document path limit exceeded");
|
|
|
|
return JValue();
|
|
|
|
}
|
|
|
|
params->nestLevel++;
|
|
|
|
while (length--) {
|
|
|
|
array.PushBack(rdbLoadJValue(params), allocator);
|
|
|
|
}
|
|
|
|
params->nestLevel--;
|
|
|
|
return array;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
ValkeyModule_LogIOError(params->rdb, "error", "Invalid metadata code %lx", code);
|
|
|
|
params->status = JSONUTIL_INVALID_RDB_FORMAT;
|
|
|
|
return JValue();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
JsonUtilCode dom_load(JDocument **doc, ValkeyModuleIO *ctx, int encver) {
|
|
|
|
*doc = nullptr;
|
|
|
|
ValkeyModule_Log(nullptr, "debug", "Begin dom_load, encver:%d", encver);
|
|
|
|
switch (encver) {
|
|
|
|
case 3: {
|
|
|
|
//
|
|
|
|
// New encoding, data is stored as wire-format JSON
|
|
|
|
//
|
|
|
|
size_t json_len;
|
|
|
|
char *json = ValkeyModule_LoadStringBuffer(ctx, &json_len);
|
|
|
|
if (!json) return JSONUTIL_INVALID_RDB_FORMAT;
|
|
|
|
JsonUtilCode rc = dom_parse(nullptr, json, json_len, doc);
|
|
|
|
ValkeyModule_Free(json);
|
|
|
|
return rc;
|
|
|
|
}
|
|
|
|
case 0: {
|
|
|
|
//
|
|
|
|
// Encoding Version 0, Data is stored JSON node by node.
|
|
|
|
//
|
|
|
|
load_params params;
|
|
|
|
params.rdb = ctx;
|
|
|
|
params.nestLevel = 0;
|
|
|
|
params.status = JSONUTIL_SUCCESS;
|
|
|
|
JValue loadedValue = rdbLoadJValue(¶ms);
|
|
|
|
if (params.status == JSONUTIL_SUCCESS) {
|
|
|
|
*doc = create_doc();
|
|
|
|
(*doc)->SetJValue(loadedValue);
|
|
|
|
}
|
|
|
|
return params.status;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
ValkeyModule_Log(nullptr, "warning", "JSON: Unrecognized rdb encoding level %d", encver);
|
|
|
|
return JSONUTIL_INVALID_RDB_FORMAT;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Compute Digest
|
|
|
|
//
|
|
|
|
STATIC void compute_digest(ValkeyModuleDigest *ctx, const JValue& v) {
|
|
|
|
switch (v.GetType()) {
|
|
|
|
case rapidjson::Type::kNullType:
|
|
|
|
ValkeyModule_DigestAddLongLong(ctx, -1);
|
|
|
|
ValkeyModule_DigestEndSequence(ctx);
|
|
|
|
break;
|
|
|
|
case rapidjson::Type::kFalseType:
|
|
|
|
ValkeyModule_DigestAddLongLong(ctx, 0);
|
|
|
|
ValkeyModule_DigestEndSequence(ctx);
|
|
|
|
break;
|
|
|
|
case rapidjson::Type::kTrueType:
|
|
|
|
ValkeyModule_DigestAddLongLong(ctx, 1);
|
|
|
|
ValkeyModule_DigestEndSequence(ctx);
|
|
|
|
break;
|
|
|
|
case rapidjson::Type::kArrayType:
|
|
|
|
ValkeyModule_DigestAddLongLong(ctx, v.Size());
|
|
|
|
ValkeyModule_DigestEndSequence(ctx);
|
|
|
|
for (size_t i = 0; i < v.Size(); ++i) {
|
|
|
|
compute_digest(ctx, v[i]);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case rapidjson::Type::kNumberType:
|
|
|
|
if (v.IsDouble()) {
|
|
|
|
double d = v.GetDouble();
|
|
|
|
int64_t bits;
|
|
|
|
memcpy(&bits, &d, sizeof(bits));
|
|
|
|
ValkeyModule_DigestAddLongLong(ctx, bits);
|
|
|
|
} else if (v.IsUint64()) {
|
|
|
|
uint64_t ui = v.GetUint64();
|
|
|
|
int64_t bits;
|
|
|
|
memcpy(&bits, &ui, sizeof(bits));
|
|
|
|
ValkeyModule_DigestAddLongLong(ctx, bits);
|
|
|
|
} else {
|
|
|
|
ValkeyModule_DigestAddLongLong(ctx, v.GetInt64());
|
|
|
|
}
|
|
|
|
ValkeyModule_DigestEndSequence(ctx);
|
|
|
|
break;
|
|
|
|
case rapidjson::Type::kObjectType:
|
|
|
|
ValkeyModule_DigestAddLongLong(ctx, v.MemberCount());
|
|
|
|
ValkeyModule_DigestEndSequence(ctx);
|
|
|
|
for (auto m = v.MemberBegin(); m != v.MemberEnd(); ++m) {
|
|
|
|
const char *b = m->name.GetString();
|
|
|
|
ValkeyModule_DigestAddStringBuffer(ctx, b, m->name.GetStringLength());
|
|
|
|
compute_digest(ctx, m->value);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case rapidjson::Type::kStringType:
|
|
|
|
ValkeyModule_DigestAddStringBuffer(ctx, v.GetString(), v.GetStringLength());
|
|
|
|
ValkeyModule_DigestEndSequence(ctx);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
ValkeyModule_Assert(false);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void dom_compute_digest(ValkeyModuleDigest *ctx, const JDocument *doc) {
|
|
|
|
compute_digest(ctx, doc->GetJValue());
|
|
|
|
}
|
|
|
|
|
|
|
|
void dom_dump_value(JValue &v) {
|
|
|
|
(void)v;
|
|
|
|
rapidjson::StringBuffer sb;
|
|
|
|
rapidjson::Writer<rapidjson::StringBuffer> writer(sb);
|
|
|
|
v.Accept(writer);
|
|
|
|
std::cout << "DEBUG DOM\tvalue: " << sb.GetString() << std::endl;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ========================= functions consumed by unit tests ======================== */
|
|
|
|
|
|
|
|
jsn::string dom_get_string(JDocument *d) {
|
|
|
|
return d->GetString();
|
|
|
|
}
|
|
|
|
|