From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: From: Kirill Shcherbatov Subject: [PATCH v8 5/5] box: specify indexes in user-friendly form Date: Wed, 16 Jan 2019 16:44:43 +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: kostja@tarantool.org, Kirill Shcherbatov List-ID: 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 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 | 60 ++++++++++++++++++++++++++++++++++----- test/engine/json.result | 42 +++++++++++++++++++++++++++ test/engine/json.test.lua | 12 ++++++++ 3 files changed, 107 insertions(+), 7 deletions(-) diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index 8a804f0ba..a55ef5b22 100644 --- a/src/box/lua/schema.lua +++ b/src/box/lua/schema.lua @@ -575,6 +575,49 @@ local function update_index_parts_1_6_0(parts) return result end +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 + +local function format_field_resolve(format, name) + local idx = nil + local field_name = nil + -- try resolve whole name + idx = format_field_index_by_name(format, name) + if idx ~= nil then + print("Case 1 "..idx) + return idx, nil + end + -- try resolve [%d] + field_name = string.match(name, "^%[(%d+)%]") + idx = tonumber(field_name) + if idx ~= nil then + print("Case 2 "..idx..string.sub(name, string.len(field_name) + 3)) + return idx, string.sub(name, string.len(field_name) + 3) + end + -- try resolve ["%s"] or ['%s'] + field_name = string.match(name, "^%[\"(.*)\"%]") or + string.match(name, "^%[\'(.*)\'%]") + idx = format_field_index_by_name(format, field_name) + if idx ~= nil then + print("Case 3 "..idx..string.sub(name, string.len(field_name) + 5)) + return idx, string.sub(name, string.len(field_name) + 5) + end + -- try to resolve .*[ and .*. + field_name = string.match(name, "^([^.%[]-)[.%[]") + idx = format_field_index_by_name(format, field_name) + if idx ~= nil then + print("Case 4 "..idx..string.sub(name, string.len(field_name) + 1)) + return idx, string.sub(name, string.len(field_name) + 1) + end + return nil, nil +end + local function update_index_parts(format, parts) if type(parts) ~= "table" then box.error(box.error.ILLEGAL_PARAMS, @@ -626,16 +669,19 @@ 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 + local idx, path = format_field_resolve(format, part.field) + if idx == nil then + box.error(box.error.ILLEGAL_PARAMS, + "options.parts[" .. i .. "]: field was not found by name '" .. part.field .. "'") end - if type(part.field) == 'string' then + 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 '".. + path.." doesn't math the path '" ..part.path.."'") 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") diff --git a/test/engine/json.result b/test/engine/json.result index 9e5accb0b..32f787a3d 100644 --- a/test/engine/json.result +++ b/test/engine/json.result @@ -70,6 +70,48 @@ s:create_index('test2', {parts = {{2, 'number'}, {3, 'number', path = 'FIO.fname - error: Field [3]["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]: field was not found by name '']sad.FIO["fname"]''' +... +s:create_index('test3', {parts = {{2, 'number'}, {'[3].FIO["fname"]', 'str', path = "FIO[\"sname\"]"}}}) +--- +- error: 'Illegal parameters, options.parts[2]: field path ''.FIO["fname"] doesn''t + math the path ''FIO["sname"]''' +... +idx3 = s:create_index('test3', {parts = {{2, 'number'}, {'[3].FIO["fname"]', 'str'}}}) +--- +... +idx3 ~= nil +--- +- true +... +idx3.parts[2].path == ".FIO[\"fname\"]" +--- +- true +... +s:create_index('test4', {parts = {{2, 'number'}, {'invalid.FIO["fname"]', 'str'}}}) +--- +- error: 'Illegal parameters, options.parts[2]: field was not found by name ''invalid.FIO["fname"]''' +... +idx4 = s:create_index('test4', {parts = {{2, 'number'}, {'data.FIO["fname"]', 'str'}}}) +--- +... +idx4 ~= nil +--- +- true +... +idx4.parts[2].path == ".FIO[\"fname\"]" +--- +- true +... +-- Vinyl has optimizations that omit index checks, so errors could differ. +idx3:drop() +--- +... +idx4:drop() +--- +... s:insert{7, 7, {town = 'London', FIO = 666}, 4, 5} --- - error: 'Tuple field [3]["FIO"] type does not match one required by operation: expected diff --git a/test/engine/json.test.lua b/test/engine/json.test.lua index a876ff1ed..cfed275b0 100644 --- a/test/engine/json.test.lua +++ b/test/engine/json.test.lua @@ -19,6 +19,18 @@ 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'}, {'[3].FIO["fname"]', 'str', path = "FIO[\"sname\"]"}}}) +idx3 = s:create_index('test3', {parts = {{2, 'number'}, {'[3].FIO["fname"]', 'str'}}}) +idx3 ~= nil +idx3.parts[2].path == ".FIO[\"fname\"]" +s:create_index('test4', {parts = {{2, 'number'}, {'invalid.FIO["fname"]', 'str'}}}) +idx4 = s:create_index('test4', {parts = {{2, 'number'}, {'data.FIO["fname"]', 'str'}}}) +idx4 ~= nil +idx4.parts[2].path == ".FIO[\"fname\"]" +-- Vinyl has optimizations that omit index checks, so errors could differ. +idx3:drop() +idx4: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