Tarantool development patches archive
 help / color / mirror / Atom feed
From: Vladimir Davydov <vdavydov.dev@gmail.com>
To: Kirill Shcherbatov <kshcherbatov@tarantool.org>
Cc: tarantool-patches@freelists.org, kostja@tarantool.org
Subject: Re: [PATCH v6 8/8] box: specify indexes in user-friendly form
Date: Tue, 18 Dec 2018 23:58:42 +0300	[thread overview]
Message-ID: <20181218205842.utabeqkxyt2mpl7f@esperanza> (raw)
In-Reply-To: <0cab9a9f155f9b2b3bf982acd083c4db132ba4a4.1544995259.git.kshcherbatov@tarantool.org>

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
> 

  reply	other threads:[~2018-12-18 20:58 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-12-17  6:52 [PATCH v6 0/8] box: Indexes by JSON path Kirill Shcherbatov
2018-12-17  6:52 ` [PATCH v6 1/8] box: refactor tuple_validate_raw Kirill Shcherbatov
2018-12-17  6:52 ` [PATCH v6 2/8] box: refactor ER_{FIELD_TYPE, ACTION_MISMATCH} Kirill Shcherbatov
2018-12-17  6:52 ` [PATCH v6 3/8] box: build path to field string uniformly on error Kirill Shcherbatov
2018-12-17  6:52 ` [PATCH v6 4/8] box: introduce JSON Indexes Kirill Shcherbatov
2018-12-17  6:52 ` [PATCH v6 5/8] box: introduce has_json_paths flag in templates Kirill Shcherbatov
2018-12-27 11:51   ` [tarantool-patches] " Konstantin Osipov
2018-12-27 11:52     ` Konstantin Osipov
2018-12-27 11:57       ` [tarantool-patches] " Konstantin Osipov
2018-12-17  6:52 ` [PATCH v6 6/8] box: tune tuple_field_raw_by_path for indexed data Kirill Shcherbatov
2018-12-17  6:52 ` [PATCH v6 7/8] box: introduce offset_slot cache in key_part Kirill Shcherbatov
2018-12-17  6:52 ` [PATCH v6 8/8] box: specify indexes in user-friendly form Kirill Shcherbatov
2018-12-18 20:58   ` Vladimir Davydov [this message]
2018-12-18 20:46 ` [PATCH v6 0/8] box: Indexes by JSON path Vladimir Davydov

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20181218205842.utabeqkxyt2mpl7f@esperanza \
    --to=vdavydov.dev@gmail.com \
    --cc=kostja@tarantool.org \
    --cc=kshcherbatov@tarantool.org \
    --cc=tarantool-patches@freelists.org \
    --subject='Re: [PATCH v6 8/8] box: specify indexes in user-friendly form' \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox