[PATCH v9 6/6] box: specify indexes in user-friendly form

Kirill Shcherbatov kshcherbatov at tarantool.org
Sun Feb 3 13:20:26 MSK 2019


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




More information about the Tarantool-patches mailing list