From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: From: Kirill Shcherbatov Subject: [PATCH v9 6/6] box: specify indexes in user-friendly form Date: Sun, 3 Feb 2019 13:20:26 +0300 Message-Id: In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit To: tarantool-patches@freelists.org, vdavydov.dev@gmail.com Cc: Kirill Shcherbatov List-ID: Implemented a more convenient interface for creating an index by JSON path. Instead of specifying fieldno and relative path it is now 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 space, 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/schema.lua | 100 ++++++++++++++++++++++++++++++-------- test/engine/json.result | 94 +++++++++++++++++++++++++++++++++++ test/engine/json.test.lua | 27 ++++++++++ 3 files changed, 202 insertions(+), 19 deletions(-) diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index 8a804f0ba..aa9fd4b96 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -575,6 +575,78 @@ local function update_index_parts_1_6_0(parts) return result end +-- +-- Get field index by format field name. +-- +local function format_field_index_by_name(format, name) + for k, v in pairs(format) do + if v.name == name then + return k + end + end + return nil +end + +-- +-- Get field 0-based index and relative JSON path to data by +-- field 1-based index or full JSON path. A particular case of a +-- full JSON path is the format field name. +-- +local function format_field_resolve(format, path, part_idx) + assert(type(path) == 'number' or type(path) == 'string') + local idx = nil + local relative_path = nil + local field_name = nil + -- Path doesn't require resolve. + if type(path) == 'number' then + idx = path + goto done + end + -- An attempt to interpret a path as the full field name. + idx = format_field_index_by_name(format, path) + if idx ~= nil then + relative_path = nil + goto done + end + -- Check if the initial part of the JSON path is a token of + -- the form [%d]. + field_name = string.match(path, "^%[(%d+)%]") + idx = tonumber(field_name) + if idx ~= nil then + relative_path = string.sub(path, string.len(field_name) + 3) + goto done + end + -- Check if the initial part of the JSON path is a token of + -- the form ["%s"] or ['%s']. + field_name = string.match(path, '^%[\"([^%]]+)\"%].*') or + string.match(path, "^%[\'([^%]]+)\'%].*") + idx = format_field_index_by_name(format, field_name) + if idx ~= nil then + relative_path = string.sub(path, string.len(field_name) + 5) + goto done + end + -- Check if the initial part of the JSON path is a string + -- token: assume that it ends with .*[ or .*. + field_name = string.match(path, "^([^.[]+)") + idx = format_field_index_by_name(format, field_name) + if idx ~= nil then + relative_path = string.sub(path, string.len(field_name) + 1) + goto done + end + -- Can't resolve field index by path. + assert(idx == nil) + box.error(box.error.ILLEGAL_PARAMS, "options.parts[" .. part_idx .. "]: " .. + "field was not found by name '" .. path .. "'") + +::done:: + if idx <= 0 then + box.error(box.error.ILLEGAL_PARAMS, + "options.parts[" .. part_idx .. "]: " .. + "field (number) must be one-based") + end + return idx - 1, relative_path +end + local function update_index_parts(format, parts) if type(parts) ~= "table" then box.error(box.error.ILLEGAL_PARAMS, @@ -622,25 +694,16 @@ local function update_index_parts(format, parts) end end end - if type(part.field) ~= 'number' and type(part.field) ~= 'string' then - 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 - box.error(box.error.ILLEGAL_PARAMS, - "options.parts[" .. i .. "]: field was not found by name '" .. part.field .. "'") - end - elseif part.field == 0 then - box.error(box.error.ILLEGAL_PARAMS, - "options.parts[" .. i .. "]: field (number) must be one-based") + if type(part.field) == 'number' or type(part.field) == 'string' then + local idx, path = format_field_resolve(format, part.field, i) + part.field = idx + part.path = path or part.path + parts_can_be_simplified = parts_can_be_simplified and part.path == nil + else + box.error(box.error.ILLEGAL_PARAMS, "options.parts[" .. i .. "]: " .. + "field (name or number) is expected") end - local fmt = format[part.field] + local fmt = format[part.field + 1] if part.type == nil then if fmt and fmt.type then part.type = fmt.type @@ -666,7 +729,6 @@ local function update_index_parts(format, parts) parts_can_be_simplified = false end end - part.field = part.field - 1 table.insert(result, part) end return result, parts_can_be_simplified diff --git a/test/engine/json.result b/test/engine/json.result index 3a5f472bc..2d50976e3 100644 --- a/test/engine/json.result +++ b/test/engine/json.result @@ -122,6 +122,100 @@ idx:max() s:drop() --- ... +-- Test user-friendly index creation interface. +s = box.schema.space.create('withdata', {engine = engine}) +--- +... +format = {{'data', 'map'}, {'meta', 'str'}} +--- +... +s:format(format) +--- +... +s:create_index('pk_invalid', {parts = {{']sad.FIO["sname"]', 'str'}}}) +--- +- error: 'Illegal parameters, options.parts[1]: field was not found by name '']sad.FIO["sname"]''' +... +s:create_index('pk_unexistent', {parts = {{'unexistent.FIO["sname"]', 'str'}}}) +--- +- error: 'Illegal parameters, options.parts[1]: field was not found by name ''unexistent.FIO["sname"]''' +... +pk = s:create_index('pk', {parts = {{'data.FIO["sname"]', 'str'}}}) +--- +... +pk ~= nil +--- +- true +... +sk2 = s:create_index('sk2', {parts = {{'["data"]FIO["sname"]', 'str'}}}) +--- +... +sk2 ~= nil +--- +- true +... +sk3 = s:create_index('sk3', {parts = {{'[\'data\']FIO["sname"]', 'str'}}}) +--- +... +sk3 ~= nil +--- +- true +... +sk4 = s:create_index('sk4', {parts = {{'[1]FIO["sname"]', 'str'}}}) +--- +... +sk4 ~= nil +--- +- true +... +pk.fieldno == sk2.fieldno +--- +- true +... +sk2.fieldno == sk3.fieldno +--- +- true +... +sk3.fieldno == sk4.fieldno +--- +- true +... +pk.path == sk2.path +--- +- true +... +sk2.path == sk3.path +--- +- true +... +sk3.path == sk4.path +--- +- true +... +s:insert{{town = 'London', FIO = {fname = 'James', sname = 'Bond'}}, "mi6"} +--- +- [{'town': 'London', 'FIO': {'fname': 'James', 'sname': 'Bond'}}, 'mi6'] +... +s:insert{{town = 'Moscow', FIO = {fname = 'Max', sname = 'Isaev', data = "extra"}}, "test"} +--- +- [{'town': 'Moscow', 'FIO': {'fname': 'Max', 'data': 'extra', 'sname': 'Isaev'}}, + 'test'] +... +pk:get({'Bond'}) == sk2:get({'Bond'}) +--- +- true +... +sk2:get({'Bond'}) == sk3:get({'Bond'}) +--- +- true +... +sk3:get({'Bond'}) == sk4:get({'Bond'}) +--- +- true +... +s:drop() +--- +... -- Test upsert of JSON-indexed data. s = box.schema.create_space('withdata', {engine = engine}) --- diff --git a/test/engine/json.test.lua b/test/engine/json.test.lua index 181eae02c..7a67e341e 100644 --- a/test/engine/json.test.lua +++ b/test/engine/json.test.lua @@ -34,6 +34,33 @@ idx:min() idx:max() s:drop() +-- Test user-friendly index creation interface. +s = box.schema.space.create('withdata', {engine = engine}) +format = {{'data', 'map'}, {'meta', 'str'}} +s:format(format) +s:create_index('pk_invalid', {parts = {{']sad.FIO["sname"]', 'str'}}}) +s:create_index('pk_unexistent', {parts = {{'unexistent.FIO["sname"]', 'str'}}}) +pk = s:create_index('pk', {parts = {{'data.FIO["sname"]', 'str'}}}) +pk ~= nil +sk2 = s:create_index('sk2', {parts = {{'["data"]FIO["sname"]', 'str'}}}) +sk2 ~= nil +sk3 = s:create_index('sk3', {parts = {{'[\'data\']FIO["sname"]', 'str'}}}) +sk3 ~= nil +sk4 = s:create_index('sk4', {parts = {{'[1]FIO["sname"]', 'str'}}}) +sk4 ~= nil +pk.fieldno == sk2.fieldno +sk2.fieldno == sk3.fieldno +sk3.fieldno == sk4.fieldno +pk.path == sk2.path +sk2.path == sk3.path +sk3.path == sk4.path +s:insert{{town = 'London', FIO = {fname = 'James', sname = 'Bond'}}, "mi6"} +s:insert{{town = 'Moscow', FIO = {fname = 'Max', sname = 'Isaev', data = "extra"}}, "test"} +pk:get({'Bond'}) == sk2:get({'Bond'}) +sk2:get({'Bond'}) == sk3:get({'Bond'}) +sk3:get({'Bond'}) == sk4:get({'Bond'}) +s:drop() + -- Test upsert of JSON-indexed data. s = box.schema.create_space('withdata', {engine = engine}) parts = {} -- 2.20.1