diff --git a/src/box/tuple_format.c b/src/box/tuple_format.c index 1c9b3c20d4f151d230e3af373ea33aa664073122..4439ce3e019cb45cc736df715a4e20558fe96503 100644 --- a/src/box/tuple_format.c +++ b/src/box/tuple_format.c @@ -233,6 +233,11 @@ tuple_format_add_field(struct tuple_format *format, uint32_t fieldno, json_lexer_create(&lexer, path, path_len, TUPLE_INDEX_BASE); while ((rc = json_lexer_next_token(&lexer, &field->token)) == 0 && field->token.type != JSON_TOKEN_END) { + if (field->token.type == JSON_TOKEN_ANY) { + diag_set(ClientError, ER_UNSUPPORTED, + "Tarantool", "multikey indexes"); + goto fail; + } enum field_type expected_type = field->token.type == JSON_TOKEN_STR ? FIELD_TYPE_MAP : FIELD_TYPE_ARRAY; diff --git a/src/lib/json/json.c b/src/lib/json/json.c index 1b1a3ec2c0d5667cd9862629fc12704bf440754d..854158f63328568a46ab1a927907782ae4bd15a9 100644 --- a/src/lib/json/json.c +++ b/src/lib/json/json.c @@ -226,10 +226,14 @@ json_lexer_next_token(struct json_lexer *lexer, struct json_token *token) if (lexer->offset == lexer->src_len) return lexer->symbol_count; c = json_current_char(lexer); - if (c == '"' || c == '\'') + if (c == '"' || c == '\'') { rc = json_parse_string(lexer, token, c); - else + } else if (c == '*') { + json_skip_char(lexer); + token->type = JSON_TOKEN_ANY; + } else { rc = json_parse_integer(lexer, token); + } if (rc != 0) return rc; /* @@ -270,7 +274,7 @@ json_token_cmp(const struct json_token *a, const struct json_token *b) } else if (a->type == JSON_TOKEN_NUM) { ret = a->num - b->num; } else { - unreachable(); + assert(a->type == JSON_TOKEN_ANY); } return ret; } @@ -332,6 +336,9 @@ json_token_snprint(char *buf, int size, const struct json_token *token, case JSON_TOKEN_STR: len = snprintf(buf, size, "[\"%.*s\"]", token->len, token->str); break; + case JSON_TOKEN_ANY: + len = snprintf(buf, size, "[*]"); + break; default: unreachable(); } @@ -420,6 +427,9 @@ json_token_hash(struct json_token *token) } else if (token->type == JSON_TOKEN_NUM) { data = &token->num; data_size = sizeof(token->num); + } else if (token->type == JSON_TOKEN_ANY) { + data = "*"; + data_size = 1; } else { unreachable(); } diff --git a/src/lib/json/json.h b/src/lib/json/json.h index 6927d2d90e964e902df0d83852cd114ee79b71d6..c1de3e579728c0ba04771171853582a4ae1d2bad 100644 --- a/src/lib/json/json.h +++ b/src/lib/json/json.h @@ -66,6 +66,7 @@ struct json_lexer { enum json_token_type { JSON_TOKEN_NUM, JSON_TOKEN_STR, + JSON_TOKEN_ANY, /** Lexer reached end of path. */ JSON_TOKEN_END, }; @@ -98,6 +99,10 @@ struct json_token { * array match [token.num] index for JSON_TOKEN_NUM type * and are allocated sequentially for JSON_TOKEN_STR child * tokens. + * + * JSON_TOKEN_ANY is exclusive. If it's present, it must + * be the only one and have index 0 in the children array. + * It will be returned by lookup by any key. */ struct json_token **children; /** Allocation size of children array. */ @@ -264,6 +269,16 @@ json_token_is_leaf(struct json_token *token) return token->max_child_idx < 0; } +/** + * Test if a given JSON token is multikey. + */ +static inline bool +json_token_is_multikey(struct json_token *token) +{ + return token->max_child_idx == 0 && + token->children[0]->type == JSON_TOKEN_ANY; +} + /** * An snprint-style function to print the path to a token in * a JSON tree. @@ -307,11 +322,24 @@ json_tree_lookup(struct json_tree *tree, struct json_token *parent, const struct json_token *token) { struct json_token *ret = NULL; - if (likely(token->type == JSON_TOKEN_NUM)) { - ret = (int)token->num < parent->children_capacity ? - parent->children[token->num] : NULL; - } else { + if (unlikely(json_token_is_multikey(parent))) { + assert(parent->max_child_idx == 0); + return parent->children[0]; + } + switch (token->type) { + case JSON_TOKEN_NUM: + if (likely(token->num <= parent->max_child_idx)) + ret = parent->children[token->num]; + break; + case JSON_TOKEN_ANY: + if (likely(parent->max_child_idx >= 0)) + ret = parent->children[parent->max_child_idx]; + break; + case JSON_TOKEN_STR: ret = json_tree_lookup_slowpath(tree, parent, token); + break; + default: + unreachable(); } return ret; } diff --git a/test/engine/json.result b/test/engine/json.result index 1bac85eddbedd41b47a14d9396efeef15db8335c..a790cdfbcb38875f365b8732db3eb4d0342dd68f 100644 --- a/test/engine/json.result +++ b/test/engine/json.result @@ -683,3 +683,16 @@ town:select() s:drop() --- ... +-- +-- gh-1260: Multikey indexes +-- +s = box.schema.space.create('withdata') +--- +... +idx = s:create_index('idx', {parts = {{3, 'str', path = '[*].fname'}, {3, 'str', path = '[*].sname'}}}) +--- +- error: Tarantool does not support multikey indexes +... +s:drop() +--- +... diff --git a/test/engine/json.test.lua b/test/engine/json.test.lua index 9afa3daa2cfa1a8164c79bb9bb3bc92dfa0d8c38..f9a7180b1dfc2f0e771678d175e431a12aee74c5 100644 --- a/test/engine/json.test.lua +++ b/test/engine/json.test.lua @@ -192,3 +192,10 @@ town:select() name:drop() town:select() s:drop() + +-- +-- gh-1260: Multikey indexes +-- +s = box.schema.space.create('withdata') +idx = s:create_index('idx', {parts = {{3, 'str', path = '[*].fname'}, {3, 'str', path = '[*].sname'}}}) +s:drop() diff --git a/test/unit/json.c b/test/unit/json.c index 6448a32108e9b315750cb7ef153bf357cadbd362..fd320c9ebcef51ea5aec6612aa734efebc5e63e2 100644 --- a/test/unit/json.c +++ b/test/unit/json.c @@ -211,7 +211,7 @@ void test_tree() { header(); - plan(58); + plan(65); struct json_tree tree; int rc = json_tree_create(&tree); @@ -411,6 +411,52 @@ test_tree() "last node became interm - it can't be leaf anymore"); is(json_token_is_leaf(&records[3].node), true, "last node is leaf"); + json_tree_foreach_entry_safe(node, &tree.root, struct test_struct, + node, node_tmp) + json_tree_del(&tree, &node->node); + + /* Test multikey tokens. */ + records_idx = 0; + node = test_add_path(&tree, path1, strlen(path1), records, &records_idx); + is(node, &records[1], "add path '%s'", path1); + token->type = JSON_TOKEN_ANY; + node = json_tree_lookup_entry(&tree, &records[0].node, token, + struct test_struct, node); + is(node->node.num, 9, "lookup any token in non-multikey node"); + + /* Can't attach ANY token to non-leaf node. Cleanup. */ + json_tree_del(&tree, &records[1].node); + records_idx--; + + const char *path_multikey = "[1][*][\"data\"]"; + node = test_add_path(&tree, path_multikey, strlen(path_multikey), + records, &records_idx); + is(node, &records[2], "add path '%s'", path_multikey); + + node = json_tree_lookup_path_entry(&tree, &tree.root, path_multikey, + strlen(path_multikey), INDEX_BASE, + struct test_struct, node); + is(node, &records[2], "lookup path '%s'", path_multikey); + + token = &records[records_idx++].node; + token->type = JSON_TOKEN_NUM; + token->num = 3; + node = json_tree_lookup_entry(&tree, &records[0].node, token, + struct test_struct, node); + is(node, &records[1], "lookup numeric token in multikey node"); + + token->type = JSON_TOKEN_ANY; + node = json_tree_lookup_entry(&tree, &records[0].node, token, + struct test_struct, node); + is(node, &records[1], "lookup any token in multikey node"); + + token->type = JSON_TOKEN_STR; + token->str = "str"; + token->len = strlen("str"); + node = json_tree_lookup_entry(&tree, &records[0].node, token, + struct test_struct, node); + is(node, &records[1], "lookup string token in multikey node"); + json_tree_foreach_entry_safe(node, &tree.root, struct test_struct, node, node_tmp) json_tree_del(&tree, &node->node); @@ -433,7 +479,7 @@ test_path_cmp() {"Data[1][\"Info\"].fname[1]", -1}, }; header(); - plan(lengthof(rc) + 2); + plan(lengthof(rc) + 3); for (size_t i = 0; i < lengthof(rc); ++i) { const char *path = rc[i].path; int errpos = rc[i].errpos; @@ -444,8 +490,15 @@ test_path_cmp() is(rc, errpos, "path cmp result \"%s\" with \"%s\": " "have %d, expected %d", a, path, rc, errpos); } + char *multikey_a = "Data[*][\"FIO\"].fname[*]"; + char *multikey_b = "[\"Data\"][*].FIO[\"fname\"][*]"; + int ret = json_path_cmp(multikey_a, strlen(multikey_a), multikey_b, + strlen(multikey_b), INDEX_BASE); + is(ret, 0, "path cmp result \"%s\" with \"%s\": have %d, expected %d", + multikey_a, multikey_b, ret, 0); + const char *invalid = "Data[[1][\"FIO\"].fname"; - int ret = json_path_validate(a, strlen(a), INDEX_BASE); + ret = json_path_validate(a, strlen(a), INDEX_BASE); is(ret, 0, "path %s is valid", a); ret = json_path_validate(invalid, strlen(invalid), INDEX_BASE); is(ret, 6, "path %s error pos %d expected %d", invalid, ret, 6); @@ -463,14 +516,14 @@ test_path_snprint() struct json_tree tree; int rc = json_tree_create(&tree); fail_if(rc != 0); - struct test_struct records[5]; - const char *path = "[1][20][\"file\"][8]"; + struct test_struct records[6]; + const char *path = "[1][*][20][\"file\"][8]"; int path_len = strlen(path); int records_idx = 0; struct test_struct *node, *node_tmp; node = test_add_path(&tree, path, path_len, records, &records_idx); - fail_if(&node->node != &records[3].node); + fail_if(&node->node != &records[4].node); char buf[64]; int bufsz = sizeof(buf); diff --git a/test/unit/json.result b/test/unit/json.result index ee54cbe0e6670e041bf22f4fb4139acb6b0ca8c8..3a15e84bb78bac765ea1c17f1f61c0b6b4f2d8e7 100644 --- a/test/unit/json.result +++ b/test/unit/json.result @@ -101,7 +101,7 @@ ok 1 - subtests ok 2 - subtests *** test_errors: done *** *** test_tree *** - 1..58 + 1..65 ok 1 - add path '[1][10]' ok 2 - add path '[1][20].file' ok 3 - add path '[1][20].file[2]' @@ -160,17 +160,25 @@ ok 2 - subtests ok 56 - last node is leaf ok 57 - last node became interm - it can't be leaf anymore ok 58 - last node is leaf + ok 59 - add path '[1][10]' + ok 60 - lookup any token in non-multikey node + ok 61 - add path '[1][*]["data"]' + ok 62 - lookup path '[1][*]["data"]' + ok 63 - lookup numeric token in multikey node + ok 64 - lookup any token in multikey node + ok 65 - lookup string token in multikey node ok 3 - subtests *** test_tree: done *** *** test_path_cmp *** - 1..7 + 1..8 ok 1 - path cmp result "Data[1]["FIO"].fname" with "Data[1]["FIO"].fname": have 0, expected 0 ok 2 - path cmp result "Data[1]["FIO"].fname" with "["Data"][1].FIO["fname"]": have 0, expected 0 ok 3 - path cmp result "Data[1]["FIO"].fname" with "Data[1]": have 1, expected 1 ok 4 - path cmp result "Data[1]["FIO"].fname" with "Data[1]["FIO"].fname[1]": have -1, expected -1 ok 5 - path cmp result "Data[1]["FIO"].fname" with "Data[1]["Info"].fname[1]": have -1, expected -1 - ok 6 - path Data[1]["FIO"].fname is valid - ok 7 - path Data[[1]["FIO"].fname error pos 6 expected 6 + ok 6 - path cmp result "Data[*]["FIO"].fname[*]" with "["Data"][*].FIO["fname"][*]": have 0, expected 0 + ok 7 - path Data[1]["FIO"].fname is valid + ok 8 - path Data[[1]["FIO"].fname error pos 6 expected 6 ok 4 - subtests *** test_path_cmp: done *** *** test_path_snprint ***