diff --git a/docs/commands.md b/docs/commands.md index 6e9aeb52b..7dfbcfa4a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -24,6 +24,7 @@ * [`JSON.OBJKEYS`](#objkeys) returns the keys in an object * [`JSON.OBJLEN`](#objlen) reports the number of keys in an object * [Other commands](#other-commands) + * [`JSON.MEMORY`](#memory) returns the memory usage of a ReJSON key * [`JSON.RESP`](#resp) returns a JSON value using Redis Serialization Protocol ## JSON @@ -324,6 +325,17 @@ If the `key` does not exist, null is returned This command is an alias for [`JSON.DEL`](#del). +### `JSON.MEMORY ` + +> **Available since 1.0.0.** +> **Time complexity:** O(N), where N is the size of the JSON value. + +Compute the size in bytes of a JSON value. + +#### Return value + +[Integer][2], specifically the size in bytes of the value. + ### `JSON.RESP ` > **Available since 1.0.0.** diff --git a/src/module.c b/src/module.c index c677334cb..0aff5cb42 100644 --- a/src/module.c +++ b/src/module.c @@ -135,7 +135,8 @@ int NodeFromJSONPath(Node *root, const RedisModuleString *path, JSONPathNode_t * } void ReplyWithPathTypeError(RedisModuleCtx *ctx, NodeType expected, NodeType actual) { - sds err = sdscatfmt(sdsempty(), REJSON_ERROR_PATH_WRONGTYPE, NodeTypeStr(expected), NodeTypeStr(actual)); + sds err = sdscatfmt(sdsempty(), REJSON_ERROR_PATH_WRONGTYPE, NodeTypeStr(expected), + NodeTypeStr(actual)); RedisModule_ReplyWithError(ctx, err); sdsfree(err); } @@ -152,23 +153,22 @@ void ReplyWithPathError(RedisModuleCtx *ctx, const JSONPathNode_t *jpn) { case E_BADTYPE: if (NT_KEY == epn->type) { err = sdscatfmt(err, "ERR invalid index '[\"%s\"]' at level %i in path", - epn->value.key, jpn->errlevel); + epn->value.key, jpn->errlevel); } else { - err = sdscatfmt(err, "ERR invalid key '[%i]' at level %i in path", - epn->value.index, jpn->errlevel); + err = sdscatfmt(err, "ERR invalid key '[%i]' at level %i in path", epn->value.index, + jpn->errlevel); } break; case E_NOINDEX: err = sdscatfmt(err, "ERR index '[%i]' out of range at level %i in path", - epn->value.index, jpn->errlevel); + epn->value.index, jpn->errlevel); break; case E_NOKEY: - err = sdscatfmt(err, "ERR key '%s' does not exist at level %i in path", - epn->value.key, jpn->errlevel); + err = sdscatfmt(err, "ERR key '%s' does not exist at level %i in path", epn->value.key, + jpn->errlevel); break; default: - err = sdscatfmt(err, "ERR unknown path error at level %i in path", - jpn->errlevel); + err = sdscatfmt(err, "ERR unknown path error at level %i in path", jpn->errlevel); break; } // switch (err) RedisModule_ReplyWithError(ctx, err); @@ -224,7 +224,10 @@ void JSONTypeAofRewrite(RedisModuleIO *aof, RedisModuleString *key, void *value) * Reply: Array, specifically the JSON's RESP form. */ int JSONResp_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { - if ((argc != 2)) return RedisModule_WrongArity(ctx); + if ((argc != 2)) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key must be empty (reply with null) or a JSON type @@ -234,7 +237,10 @@ int JSONResp_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int arg RedisModule_ReplyWithNull(ctx); return REDISMODULE_OK; } else if (RedisModule_ModuleTypeGetType(key) != JSONType) { - return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); + { + RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); + return REDISMODULE_ERR; + } } Object *objRoot = RedisModule_ModuleTypeGetValue(key); @@ -242,6 +248,33 @@ int JSONResp_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int arg return REDISMODULE_OK; } +/** + * JSON.MEMORY + * Reply: Integer, specifically the memory usage of the key +*/ +int JSONMemory_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if ((argc != 2)) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } + RedisModule_AutoMemory(ctx); + + // key must be empty (reply with null) or a JSON type + RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); + int type = RedisModule_KeyType(key); + if (REDISMODULE_KEYTYPE_EMPTY == type) { + RedisModule_ReplyWithNull(ctx); + return REDISMODULE_OK; + } else if (RedisModule_ModuleTypeGetType(key) != JSONType) { + RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); + return REDISMODULE_ERR; + } + + Object *objRoot = RedisModule_ModuleTypeGetValue(key); + RedisModule_ReplyWithLongLong(ctx, ObjectTypeMemoryUsage(objRoot)); + return REDISMODULE_OK; +} + /** * JSON.TYPE * Reports the type of JSON value at `path`. @@ -250,7 +283,10 @@ int JSONResp_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int arg */ int JSONType_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if (argc != 3) return RedisModule_WrongArity(ctx); + if (argc != 3) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key must be empty or a JSON type @@ -304,7 +340,10 @@ int JSONType_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int arg */ int JSONLen_GenericCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if (argc != 3) return RedisModule_WrongArity(ctx); + if (argc != 3) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // the actual command @@ -371,7 +410,10 @@ int JSONLen_GenericCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int ar */ int JSONObjKeys_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if (argc != 3) return RedisModule_WrongArity(ctx); + if (argc != 3) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key must be empty or a JSON type @@ -430,16 +472,19 @@ int JSONObjKeys_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int /** * JSON.SET * Sets the JSON value at `path` in `key` - * + * * For new keys the `path` must be the root. For existing keys, when the entire `path` exists, the * value that it contains is replaced with the `json` value. A key (with its respective value) is * added to a JSON Object only if it is the last child in the `path`. - * + * * Reply: Simple string, OK. */ int JSONSet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if (argc != 4) return RedisModule_WrongArity(ctx); + if (argc != 4) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key must be empty or a JSON type @@ -553,9 +598,9 @@ int JSONSet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc * JSON.GET [INDENT indentation-string] [NEWLINE newline-string] [SPACE space-string] * [path ...] * Return the value at `path` in JSON serialized form. - * + * * This command accepts multiple `path`s, and defaults to the value's root when none are given. - * + * * The following subcommands change the reply's and are all set to the empty string by default: * - `INDENT` sets the indentation string for nested levels * - `NEWLINE` sets the string that's printed at the end of each line @@ -567,7 +612,10 @@ int JSONSet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc * is a key. */ int JSONGet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { - if ((argc < 2)) return RedisModule_WrongArity(ctx); + if ((argc < 2)) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key must be empty (reply with null) or an object type @@ -683,10 +731,14 @@ int JSONGet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc * JSON.MGET [ ...] * Returns the values at `path` from multiple `key`s. Non-existing keys and non-existing paths are * reported as null. - * Reply: Array of Bulk Strings, specifically the JSON serialization of the value at each key's path. + * Reply: Array of Bulk Strings, specifically the JSON serialization of the value at each key's + * path. */ int JSONMGet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { - if ((argc < 2)) return RedisModule_WrongArity(ctx); + if ((argc < 2)) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } if (RedisModule_IsKeysPositionRequest(ctx)) { for (int i = 2; i < argc - 2; i++) RedisModule_KeyAtPos(ctx, i); return REDISMODULE_OK; @@ -767,7 +819,10 @@ int JSONMGet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int arg */ int JSONDel_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if (argc != 3) return RedisModule_WrongArity(ctx); + if (argc != 3) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key must be empty or a JSON type @@ -838,7 +893,10 @@ int JSONDel_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc * Reply: String, specifically the resulting JSON number value */ int JSONNum_GenericCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { - if ((argc < 4)) return RedisModule_WrongArity(ctx); + if ((argc < 4)) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); const char *cmd = RedisModule_StringPtrLen(argv[0], NULL); @@ -960,7 +1018,10 @@ int JSONNum_GenericCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int ar */ int JSONStrAppend_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if (argc != 4) return RedisModule_WrongArity(ctx); + if (argc != 4) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key can't be empty and must be a JSON type @@ -1016,7 +1077,7 @@ int JSONStrAppend_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, in // the value must be a string if (N_STRING != NODETYPE(jo)) { sds err = sdscatfmt(sdsempty(), "ERR wrong type of value - expected %s but found %s", - NodeTypeStr(N_STRING), NodeTypeStr(NODETYPE(jpn.n))); + NodeTypeStr(N_STRING), NodeTypeStr(NODETYPE(jpn.n))); RedisModule_ReplyWithError(ctx, err); sdsfree(err); } @@ -1044,7 +1105,10 @@ int JSONStrAppend_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, in */ int JSONArrInsert_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if (argc < 5) return RedisModule_WrongArity(ctx); + if (argc < 5) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key can't be empty and must be a JSON type @@ -1152,7 +1216,10 @@ int JSONArrInsert_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, in */ int JSONArrAppend_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if (argc < 4) return RedisModule_WrongArity(ctx); + if (argc < 4) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key can't be empty and must be a JSON type @@ -1252,7 +1319,10 @@ int JSONArrAppend_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, in */ int JSONArrIndex_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if ((argc < 4) || (argc > 6)) return RedisModule_WrongArity(ctx); + if ((argc < 4) || (argc > 6)) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key can't be empty and must be a JSON type @@ -1343,7 +1413,10 @@ int JSONArrIndex_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int */ int JSONArrTrim_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // check args - if (argc != 5) return RedisModule_WrongArity(ctx); + if (argc != 5) { + RedisModule_WrongArity(ctx); + return REDISMODULE_ERR; + } RedisModule_AutoMemory(ctx); // key can't be empty and must be a JSON type @@ -1434,6 +1507,10 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx) { REDISMODULE_ERR) return REDISMODULE_ERR; + if (RedisModule_CreateCommand(ctx, "json.memory", JSONMemory_RedisCommand, "readonly", 1, 1, + 1) == REDISMODULE_ERR) + return REDISMODULE_ERR; + if (RedisModule_CreateCommand(ctx, "json.type", JSONType_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) return REDISMODULE_ERR; @@ -1472,8 +1549,8 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx) { 1) == REDISMODULE_ERR) return REDISMODULE_ERR; - if (RedisModule_CreateCommand(ctx, "json.strappend", JSONStrAppend_RedisCommand, "write deny-oom", 1, - 1, 1) == REDISMODULE_ERR) + if (RedisModule_CreateCommand(ctx, "json.strappend", JSONStrAppend_RedisCommand, + "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) return REDISMODULE_ERR; /* JSON array commands matey. */ diff --git a/src/object_type.c b/src/object_type.c index a34c93743..9478732bd 100644 --- a/src/object_type.c +++ b/src/object_type.c @@ -215,10 +215,54 @@ void _ObjectTypeToResp_Begin(Node *n, void *ctx) { } } -void ObjectTypeToRespReply(RedisModuleCtx *ctx, Node *node) { +void ObjectTypeToRespReply(RedisModuleCtx *ctx, const Node *node) { NodeSerializerOpt nso = {0}; nso.fBegin = _ObjectTypeToResp_Begin; nso.xBegin = 0xff; // mask for all basic types Node_Serializer(node, &nso, ctx); +} + +void _ObjectTypeMemoryUsage(Node *n, void *ctx) { + long long *memory = (long long *)ctx; + + if (!n) { + // the null node takes no memory + return; + } else { + // account for the struct's size + *memory += sizeof(Node); + switch (n->type) { + case N_BOOLEAN: + case N_INTEGER: + case N_NUMBER: + case N_NULL: // keeps the compiler from complaining + // these are stored in the node itself + return; + case N_STRING: + *memory += n->value.strval.len; + return; + case N_KEYVAL: + *memory += strlen(n->value.kvval.key); + return; + case N_DICT: + *memory += n->value.dictval.cap * sizeof(Node *); + return; + case N_ARRAY: + *memory += n->value.arrval.cap * sizeof(Node *); + return; + } + } +} + +long long ObjectTypeMemoryUsage(const Node *node) { + + NodeSerializerOpt nso = {0}; + long long memory = 0; + + nso.fBegin = _ObjectTypeMemoryUsage; + nso.xBegin = 0xff; // mask for all basic types + Node_Serializer(node, &nso, &memory); + + return memory; } \ No newline at end of file diff --git a/src/object_type.h b/src/object_type.h index ae5e42efb..3fe8f82b4 100644 --- a/src/object_type.h +++ b/src/object_type.h @@ -29,6 +29,9 @@ void ObjectTypeRdbSave(RedisModuleIO *rdb, void *value); void ObjectTypeFree(void *value); /* Replies with a RESP representation of the node. */ -void ObjectTypeToRespReply(RedisModuleCtx *ctx, Node *node); +void ObjectTypeToRespReply(RedisModuleCtx *ctx, const Node *node); + +/* Reports the memory usage (in bytes) of the node. */ +long long ObjectTypeMemoryUsage(const Node *node); #endif \ No newline at end of file