Tarantool development patches archive
 help / color / mirror / Atom feed
From: Kirill Shcherbatov <kshcherbatov@tarantool.org>
To: tarantool-patches@freelists.org, vdavydov.dev@gmail.com
Cc: Kirill Shcherbatov <kshcherbatov@tarantool.org>
Subject: [PATCH v9 6/6] box: specify indexes in user-friendly form
Date: Sun,  3 Feb 2019 13:20:26 +0300	[thread overview]
Message-ID: <a9476fdc808d1e6b9fe02bf4740f3362f910b3e6.1549187339.git.kshcherbatov@tarantool.org> (raw)
In-Reply-To: <cover.1549187339.git.kshcherbatov@tarantool.org>

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

  parent reply	other threads:[~2019-02-03 10:20 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-02-03 10:20 [PATCH v9 0/6] box: Indexes by JSON path Kirill Shcherbatov
2019-02-03 10:20 ` [PATCH v9 1/6] lib: update msgpuck library Kirill Shcherbatov
2019-02-04  9:48   ` Vladimir Davydov
2019-02-03 10:20 ` [PATCH v9 2/6] box: introduce tuple_field_raw_by_path routine Kirill Shcherbatov
2019-02-04 10:37   ` Vladimir Davydov
2019-02-03 10:20 ` [PATCH v9 3/6] box: introduce JSON Indexes Kirill Shcherbatov
2019-02-04 12:26   ` Vladimir Davydov
2019-02-03 10:20 ` [PATCH v9 4/6] box: introduce has_json_paths flag in templates Kirill Shcherbatov
2019-02-04 12:31   ` Vladimir Davydov
2019-02-03 10:20 ` [PATCH v9 5/6] box: introduce offset_slot cache in key_part Kirill Shcherbatov
2019-02-04 12:56   ` Vladimir Davydov
2019-02-04 13:02     ` [tarantool-patches] " Kirill Shcherbatov
2019-02-04 15:10   ` Vladimir Davydov
2019-02-03 10:20 ` Kirill Shcherbatov [this message]
2019-02-04 15:30   ` [PATCH v9 6/6] box: specify indexes in user-friendly form 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=a9476fdc808d1e6b9fe02bf4740f3362f910b3e6.1549187339.git.kshcherbatov@tarantool.org \
    --to=kshcherbatov@tarantool.org \
    --cc=tarantool-patches@freelists.org \
    --cc=vdavydov.dev@gmail.com \
    --subject='Re: [PATCH v9 6/6] 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