diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua index 8a804f0ba691b4e092da0998a8190a26142bd743..0daf484b86d9e31f9380a81d81850df83aadc238 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 3a5f472bced407fde262ca718768347cc8274324..1bac85eddbedd41b47a14d9396efeef15db8335c 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 181eae02cd9e7d23cda8233b133dfa76f5a30eaf..9afa3daa2cfa1a8164c79bb9bb3bc92dfa0d8c38 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 = {}