[PATCH v6 8/8] box: specify indexes in user-friendly form

Vladimir Davydov vdavydov.dev at gmail.com
Tue Dec 18 23:58:42 MSK 2018


https://www.freelists.org/post/tarantool-patches/PATCH-v5-99-box-specify-indexes-in-userfriendly-form,1

On Mon, Dec 17, 2018 at 09:52:52AM +0300, Kirill Shcherbatov wrote:
> Implemented a more convenient interface for creating an index
> by JSON path. Instead of specifying fieldno and relative path
> it comes possible to pass full JSON path to data.
> 
> Closes #1012
> 
> @TarantoolBot document
> Title: Indexes by JSON path
> Sometimes field data could have complex document structure.
> When this structure is consistent across whole document,
> you are able to create an index by JSON path.
> 
> Example:
> s = box.schema.space.create('sample')
> format = {{'id', 'unsigned'}, {'data', 'map'}}
> s:format(format)
> -- explicit JSON index creation
> age_idx = s:create_index('age', {{2, 'number', path = ".age"}})
> -- user-friendly syntax for JSON index creation
> parts = {{'data.FIO["fname"]', 'str'}, {'data.FIO["sname"]', 'str'},
>          {'data.age', 'number'}}
> info_idx = s:create_index('info', {parts = parts}})
> s:insert({1, {FIO={fname="James", sname="Bond"}, age=35}})
> ---
>  src/box/lua/index.c       | 64 +++++++++++++++++++++++++++++++++++++++
>  src/box/lua/schema.lua    | 22 +++++++-------
>  test/engine/json.result   | 28 +++++++++++++++++
>  test/engine/json.test.lua |  8 +++++
>  4 files changed, 111 insertions(+), 11 deletions(-)
> 
> diff --git a/src/box/lua/index.c b/src/box/lua/index.c
> index 6265c044a..9b04c5d9a 100644
> --- a/src/box/lua/index.c
> +++ b/src/box/lua/index.c
> @@ -35,6 +35,9 @@
>  #include "box/box.h"
>  #include "box/index.h"
>  #include "box/lua/tuple.h"
> +#include "box/schema.h"
> +#include "box/tuple_format.h"
> +#include "json/json.h"
>  #include "box/lua/misc.h" /* lbox_encode_tuple_on_gc() */
>  
>  /** {{{ box.index Lua library: access to spaces and indexes
> @@ -328,6 +331,66 @@ lbox_index_compact(lua_State *L)
>  	return 0;
>  }
>  
> +/**
> + * Resolve field index by absolute JSON path first component and
> + * return relative JSON path.
> + */
> +static int
> +lbox_index_path_resolve(struct lua_State *L)
> +{
> +	if (lua_gettop(L) != 3 ||
> +	    !lua_isnumber(L, 1) || !lua_isnumber(L, 2) || !lua_isstring(L, 3)) {
> +		return luaL_error(L, "Usage box.internal."
> +				     "path_resolve(part_id, space_id, path)");
> +	}
> +	uint32_t part_id = lua_tonumber(L, 1);
> +	uint32_t space_id = lua_tonumber(L, 2);
> +	size_t path_len;
> +	const char *path = lua_tolstring(L, 3, &path_len);
> +	struct space *space = space_cache_find(space_id);
> +	if (space == NULL)
> +		return luaT_error(L);
> +	struct json_lexer lexer;
> +	struct json_token token;
> +	json_lexer_create(&lexer, path, path_len, TUPLE_INDEX_BASE);
> +	int rc = json_lexer_next_token(&lexer, &token);
> +	if (rc != 0) {
> +		const char *err_msg =
> +			tt_sprintf("options.parts[%d]: error in path on "
> +				   "position %d", part_id, rc);
> +		diag_set(ClientError, ER_ILLEGAL_PARAMS, err_msg);
> +		return luaT_error(L);
> +	}
> +	assert(space->format != NULL && space->format->dict != NULL);
> +	uint32_t fieldno = token.num;
> +	uint32_t field_count = tuple_format_field_count(space->format);
> +	if (token.type == JSON_TOKEN_NUM && fieldno >= field_count) {
> +		const char *err_msg =
> +			tt_sprintf("options.parts[%d]: field '%d' referenced "
> +				   "in path is greater than format field "
> +				   "count %d", part_id,
> +				   fieldno + TUPLE_INDEX_BASE, field_count);
> +		diag_set(ClientError, ER_ILLEGAL_PARAMS, err_msg);
> +		return luaT_error(L);
> +	} else if (token.type == JSON_TOKEN_STR &&
> +		   tuple_fieldno_by_name(space->format->dict, token.str,
> +					 token.len,
> +					 field_name_hash(token.str, token.len),
> +					 &fieldno) != 0) {
> +		const char *err_msg =
> +			tt_sprintf("options.parts[%d]: field was not found by "
> +				   "name '%.*s'", part_id, token.len,
> +				   token.str);
> +		diag_set(ClientError, ER_ILLEGAL_PARAMS, err_msg);
> +		return luaT_error(L);
> +	}
> +	fieldno += TUPLE_INDEX_BASE;
> +	path += lexer.offset;
> +	lua_pushnumber(L, fieldno);
> +	lua_pushstring(L, path);
> +	return 2;
> +}
> +
>  /* }}} */
>  
>  void
> @@ -365,6 +428,7 @@ box_lua_index_init(struct lua_State *L)
>  		{"truncate", lbox_truncate},
>  		{"stat", lbox_index_stat},
>  		{"compact", lbox_index_compact},
> +		{"path_resolve", lbox_index_path_resolve},
>  		{NULL, NULL}
>  	};
>  
> diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
> index 8a804f0ba..497cf197c 100644
> --- a/src/box/lua/schema.lua
> +++ b/src/box/lua/schema.lua
> @@ -575,7 +575,7 @@ local function update_index_parts_1_6_0(parts)
>      return result
>  end
>  
> -local function update_index_parts(format, parts)
> +local function update_index_parts(format, parts, space_id)
>      if type(parts) ~= "table" then
>          box.error(box.error.ILLEGAL_PARAMS,
>          "options.parts parameter should be a table")
> @@ -626,16 +626,16 @@ local function update_index_parts(format, parts)
>              box.error(box.error.ILLEGAL_PARAMS,
>                        "options.parts[" .. i .. "]: field (name or number) is expected")
>          elseif type(part.field) == 'string' then
> -            for k,v in pairs(format) do
> -                if v.name == part.field then
> -                    part.field = k
> -                    break
> -                end
> -            end
> -            if type(part.field) == 'string' then
> +            local idx, path = box.internal.path_resolve(i, space_id, part.field)
> +            if part.path ~= nil and part.path ~= path then
>                  box.error(box.error.ILLEGAL_PARAMS,
> -                          "options.parts[" .. i .. "]: field was not found by name '" .. part.field .. "'")
> +                          "options.parts[" .. i .. "]: field path '"..
> +                          part.path.." doesn't math the path '" ..
> +                          part.field .. "'")
>              end
> +            parts_can_be_simplified = parts_can_be_simplified and path == nil
> +            part.field = idx
> +            part.path = path or part.path
>          elseif part.field == 0 then
>              box.error(box.error.ILLEGAL_PARAMS,
>                        "options.parts[" .. i .. "]: field (number) must be one-based")
> @@ -792,7 +792,7 @@ box.schema.index.create = function(space_id, name, options)
>          end
>      end
>      local parts, parts_can_be_simplified =
> -        update_index_parts(format, options.parts)
> +        update_index_parts(format, options.parts, space_id)
>      -- create_index() options contains type, parts, etc,
>      -- stored separately. Remove these members from index_opts
>      local index_opts = {
> @@ -959,7 +959,7 @@ box.schema.index.alter = function(space_id, index_id, options)
>      if options.parts then
>          local parts_can_be_simplified
>          parts, parts_can_be_simplified =
> -            update_index_parts(format, options.parts)
> +            update_index_parts(format, options.parts, space_id)
>          -- save parts in old format if possible
>          if parts_can_be_simplified then
>              parts = simplify_index_parts(parts)
> diff --git a/test/engine/json.result b/test/engine/json.result
> index c33e568b3..60d71bdfa 100644
> --- a/test/engine/json.result
> +++ b/test/engine/json.result
> @@ -71,6 +71,34 @@ s:create_index('test2', {parts = {{2, 'number'}, {3, 'number', path = '.FIO.fnam
>  - error: Field ["data"]["FIO"]["fname"] has type 'string' in one index, but type 'number'
>      in another
>  ...
> +s:create_index('test3', {parts = {{2, 'number'}, {']sad.FIO["fname"]', 'str'}}})
> +---
> +- error: 'Illegal parameters, options.parts[2]: error in path on position 1'
> +...
> +s:create_index('test3', {parts = {{2, 'number'}, {'[666].FIO["fname"]', 'str'}}})
> +---
> +- error: 'Illegal parameters, options.parts[2]: field ''666'' referenced in path is
> +    greater than format field count 5'
> +...
> +s:create_index('test3', {parts = {{2, 'number'}, {'invalid.FIO["fname"]', 'str'}}})
> +---
> +- error: 'Illegal parameters, options.parts[2]: field was not found by name ''invalid'''
> +...
> +idx3 = s:create_index('test3', {parts = {{2, 'number'}, {'data.FIO["fname"]', 'str'}}})
> +---
> +...
> +assert(idx3 ~= nil)
> +---
> +- true
> +...
> +assert(idx3.parts[2].path == ".FIO[\"fname\"]")
> +---
> +- true
> +...
> +-- Vinyl has optimizations that omit index checks, so errors could differ.
> +idx3:drop()
> +---
> +...
>  s:insert{7, 7, {town = 'London', FIO = 666}, 4, 5}
>  ---
>  - error: 'Tuple field ["data"]["FIO"] type does not match one required by operation:
> diff --git a/test/engine/json.test.lua b/test/engine/json.test.lua
> index 45153743d..f91846d79 100644
> --- a/test/engine/json.test.lua
> +++ b/test/engine/json.test.lua
> @@ -19,6 +19,14 @@ s:format(format)
>  format = {{'id', 'unsigned'}, {'meta', 'unsigned'}, {'data', 'map'}, {'age', 'unsigned'}, {'level', 'unsigned'}}
>  s:format(format)
>  s:create_index('test2', {parts = {{2, 'number'}, {3, 'number', path = '.FIO.fname'}, {3, 'str', path = '["FIO"]["sname"]'}}})
> +s:create_index('test3', {parts = {{2, 'number'}, {']sad.FIO["fname"]', 'str'}}})
> +s:create_index('test3', {parts = {{2, 'number'}, {'[666].FIO["fname"]', 'str'}}})
> +s:create_index('test3', {parts = {{2, 'number'}, {'invalid.FIO["fname"]', 'str'}}})
> +idx3 = s:create_index('test3', {parts = {{2, 'number'}, {'data.FIO["fname"]', 'str'}}})
> +assert(idx3 ~= nil)
> +assert(idx3.parts[2].path == ".FIO[\"fname\"]")
> +-- Vinyl has optimizations that omit index checks, so errors could differ.
> +idx3:drop()
>  s:insert{7, 7, {town = 'London', FIO = 666}, 4, 5}
>  s:insert{7, 7, {town = 'London', FIO = {fname = 666, sname = 'Bond'}}, 4, 5}
>  s:insert{7, 7, {town = 'London', FIO = {fname = "James"}}, 4, 5}
> -- 
> 2.19.2
> 



More information about the Tarantool-patches mailing list