Skip to content
Snippets Groups Projects
Commit 0dc72812 authored by Aleksandr Lyapunov's avatar Aleksandr Lyapunov Committed by Kirill Yukhin
Browse files

Introduce fselect - formatted select

space:fselect and index:fselect fetch data like ordinal select,
but formats the result like mysql does - with columns, column
names etc. fselect converts tuple to strings using json,
extending with spaces and cutting tail if necessary. It is
designed for visual analysis of select result and shouldn't
be used stored procedures.

index:fselect(<key>, <opts>, <fselect_opts>)
space:fselect(<key>, <opts>, <fselect_opts>)

There are some options that can be specified in different ways:
 - among other common options (<opts>) with 'fselect_' prefix.
   (e.g. 'fselect_type=..')
 - in special <fselect_opts> map (with or without prefix).
 - in global variables with 'fselect_' prefix.

The possible options are:
 - type:
    - 'sql' - like mysql result (default).
    - 'gh' (or 'github' or 'markdown') - markdown syntax, for
      copy-pasting to github.
    - 'jira' - jira table syntax (for copy-pasting to jira).
 - widths: array with desired widths of columns.
 - max_width: limit entire length of a row string, longest fields
   will be cut if necessary. Set to 0 (default) to detect and use
   screen width. Set to -1 for no limit.
 - print: (default - false) - print each line instead of adding
   to result.
 - use_nbsp: (default - true) - add invisible spaces to improve
   readability in YAML output. Not applicabble when print=true.

There is also a pair of shortcuts:
index/space:gselect - same as fselect, but with type='gh'.
index/space:jselect - same as fselect, but with type='jira'.

See test/engine/select.test.lua for examples.

Closes #5161
parent ab64b120
No related branches found
No related tags found
No related merge requests found
......@@ -1456,6 +1456,226 @@ base_index_mt.bsize = function(index)
end
return tonumber(ret)
end
-- index.fselect - formatted select.
-- Options can be passed through opts, fselect_opts and global variables.
-- If an option is in opts table or set in global variable - it must have
-- prefix 'fselect_'. If an option is on fselect_opts table - it may or
-- may not have the prefix.
-- Options:
-- type:
-- 'sql' - like mysql result (default)
-- 'gh' (or 'github' or 'markdown') - markdown syntax, for pasting to github.
-- 'jira' syntax (for pasting to jira)
-- widths: array with desired widths of columns.
-- max_width: limit entire length of a row string, longest fields will be cut.
-- Set to 0 (default) to detect and use screen width. Set to -1 for no limit.
-- print: (default - false) - print each line instead of adding to result.
-- use_nbsp: (default - true) - add invisible spaces to improve readability
-- in YAML output. Not applicabble when print=true.
base_index_mt.fselect = function(index, key, opts, fselect_opts)
-- Options.
if type(fselect_opts) ~= 'table' then fselect_opts = {} end
-- Get global value, like _G[name] but wrapped with pcall for strict mode.
local function get_global(name)
local function internal() return _G[name] end
local success,result = pcall(internal)
return success and result or nil
end
-- Get a value from `opts` table and remove it from the table.
local function grab_from_opts(name)
if type(opts) ~= 'table' then return nil end
local res = opts[name]
if res ~= nil then opts[name] = nil end
return res
end
-- Find an option in opts, fselect_opts or _G by given name.
-- In opts and _G the value is searched with 'fselect_' prefix;
-- In fselect_opts - with or without prefix.
local function get_opt(name, default, expected_type)
local prefix_name = 'fselect_' .. name
local variants = {fselect_opts[prefix_name], fselect_opts[name],
grab_from_opts(prefix_name), get_global(prefix_name), default }
local min_i = 0
local min_v = nil
for i,v in pairs(variants) do
if (type(v) == expected_type and i < min_i) or min_v == nil then
min_i = i
min_v = v
end
end
return min_v
end
local fselect_type = get_opt('type', 'sql', 'string')
if fselect_type == 'gh' or fselect_type == 'github' then
fselect_type = 'markdown'
end
if fselect_type ~= 'sql' and fselect_type ~= 'markdown' and fselect_type ~= 'jira' then
fselect_type = 'sql'
end
local widths = get_opt('widths', {}, 'table')
local default_max_width = 0
if #widths > 0 then default_max_width = -1 end
local max_width = get_opt('max_width', default_max_width, 'number')
local use_print = get_opt('print', false, 'boolean')
local use_nbsp = get_opt('use_nbsp', true, 'boolean')
local min_col_width = 5
local max_col_width = 1000
if use_print then use_nbsp = false end
-- Screen size autodetection.
local function detect_width()
local ffi = require('ffi')
ffi.cdef('void rl_get_screen_size(int *rows, int *cols);')
local colsp = ffi.new('int[1]')
ffi.C.rl_get_screen_size(nil, colsp)
return colsp[0]
end
if max_width == 0 then
max_width = detect_width()
-- YAML uses several additinal symbols in output, we should shink line.
local waste_size = use_print and 0 or 5
if max_width > waste_size then
max_width = max_width - waste_size
else
max_width = fselect_type == 'sql' and 140 or 260
end
end
-- select and stringify.
local tab = { }
local json = require('json')
for _,t in index:pairs(key, opts) do
local row = { }
for _,f in t:pairs() do
table.insert(row, json.encode(f))
end
table.insert(tab, row)
end
local num_rows = #tab
local space = box.space[index.space_id]
local fmt = space:format()
local num_cols = math.max(#fmt, 1)
for i = 1,num_rows do
num_cols = math.max(num_cols, #tab[i])
end
local names = {}
for j = 1,num_cols do
table.insert(names, fmt[j] and fmt[j].name or 'col' .. tostring(j))
end
local real_width = num_cols + 1 -- including '|' symbols
for j = 1,num_cols do
if type(widths[j]) ~= 'number' then
local width = names[j]:len()
if fselect_type == 'jira' then
width = width + 1
end
for i = 1,num_rows do
if tab[i][j] then
width = math.max(width, tab[i][j]:len())
end
end
widths[j] = width
end
widths[j] = math.max(widths[j], min_col_width)
widths[j] = math.min(widths[j], max_col_width)
real_width = real_width + widths[j]
end
-- cut some columns if its width is too big
while max_width > 0 and real_width > max_width do
local max_j = 1
for j = 2,num_cols do
if widths[j] >= widths[max_j] then max_j = j end
end
widths[max_j] = widths[max_j] - 1
real_width = real_width - 1
end
-- Yaml wraps all strings that contain spaces with single quotes, and
-- does not wrap otherwise. Let's add some invisible spaces to every line
-- in order to make them similar in output.
local prefix = string.char(0xE2) .. string.char(0x80) .. string.char(0x8B)
if not use_nbsp then prefix = '' end
local header_row_delim = fselect_type == 'jira' and '||' or '|'
local result_row_delim = '|'
local delim_row_delim = fselect_type == 'sql' and '+' or '|'
local delim_row = prefix .. delim_row_delim
for j = 1,num_cols do
delim_row = delim_row .. string.rep('-', widths[j]) .. delim_row_delim
end
-- format string - cut or fill with spaces to make is exactly n symbols.
-- also replace spaces with non-break spaces.
local fmt_str = function(x, n)
if not x then x = '' end
local str
if x:len() <= n then
local add = n - x:len()
local addl = math.floor(add/2)
local addr = math.ceil(add/2)
str = string.rep(' ', addl) .. x .. string.rep(' ', addr)
else
str = x:sub(1, n)
end
if use_nbsp then
-- replace spaces with &nbsp
return str:gsub("%s", string.char(0xC2) .. string.char(0xA0))
else
return str
end
end
local res = {}
-- insert into res a string with formatted row.
local res_insert = function(row, is_header)
local delim = is_header and header_row_delim or result_row_delim
local str_row = prefix .. delim
local shrink = fselect_type == 'jira' and is_header and 1 or 0
for j = 1,num_cols do
str_row = str_row .. fmt_str(row[j], widths[j] - shrink) .. delim
end
table.insert(res, str_row)
end
-- format result
if fselect_type == 'sql' then
table.insert(res, delim_row)
end
res_insert(names, true)
if fselect_type ~= 'jira' then
table.insert(res, delim_row)
end
for i = 1,num_rows do
res_insert(tab[i], false)
end
if fselect_type == 'sql' then
table.insert(res, delim_row)
end
if use_print then
for _,line in ipairs(res) do
print(line)
end
return {}
end
return res
end
base_index_mt.gselect = function(index, key, opts, fselect_opts)
if type(fselect_opts) ~= 'table' then fselect_opts = {} end
fselect_opts['type'] = 'gh'
return base_index_mt.fselect(index, key, opts, fselect_opts)
end
base_index_mt.jselect = function(index, key, opts, fselect_opts)
if type(fselect_opts) ~= 'table' then fselect_opts = {} end
fselect_opts['type'] = 'jira'
return base_index_mt.fselect(index, key, opts, fselect_opts)
end
-- Lua 5.2 compatibility
base_index_mt.__len = base_index_mt.len
-- min and max
......@@ -1697,6 +1917,18 @@ space_mt.select = function(space, key, opts)
check_space_arg(space, 'select')
return check_primary_index(space):select(key, opts)
end
space_mt.fselect = function(space, key, opts, fselect_opts)
check_space_arg(space, 'select')
return check_primary_index(space):fselect(key, opts, fselect_opts)
end
space_mt.gselect = function(space, key, opts, fselect_opts)
check_space_arg(space, 'select')
return check_primary_index(space):gselect(key, opts, fselect_opts)
end
space_mt.jselect = function(space, key, opts, fselect_opts)
check_space_arg(space, 'select')
return check_primary_index(space):jselect(key, opts, fselect_opts)
end
space_mt.insert = function(space, tuple)
check_space_arg(space, 'insert')
return internal.insert(space.id, tuple);
......
......@@ -3429,3 +3429,246 @@ index3:select{1235}
space:drop()
---
...
--https://github.com/tarantool/tarantool/issues/5161
--formatted select
s = box.schema.space.create('test', { engine = engine })
---
...
i1 = s:create_index('test1')
---
...
i2 = s:create_index('test2', {parts={{2, 'unsigned'}}})
---
...
_ = s:replace{1, 2, 3, 4, box.NULL, 5}
---
...
_ = s:replace{3, 4, true, {1, 2, 3}}
---
...
_ = s:replace{5, 6, false, {1, 2, 3, ['key']='value'}}
---
...
_ = s:replace{3, 4, true, {1, {3, {aa=1,bb=2}}, 3}}
---
...
_ = s:replace{7, 8, 'asdgsdgswegg', 'sdf', 'dsgfsdgsegasges' }
---
...
s:fselect()
---
- - ​+-----+-----+--------------+---------------------------------+-----------------+-----+
- ​|col1 |col2 | col3 | col4 | col5 |col6 |
- ​+-----+-----+--------------+---------------------------------+-----------------+-----+
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
- ​+-----+-----+--------------+---------------------------------+-----------------+-----+
...
s:fselect({5}, {iterator='le'})
---
- - ​+-----+-----+-----+---------------------------------+-----+-----+
- ​|col1 |col2 |col3 | col4 |col5 |col6 |
- ​+-----+-----+-----+---------------------------------+-----+-----+
- ​| 5 | 6 |false|{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 3 | 4 |true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 1 | 2 | 3 | 4 |null | 5 |
- ​+-----+-----+-----+---------------------------------+-----+-----+
...
s:fselect({5}, {iterator='le', fselect_max_width=40})
---
- - ​+-----+-----+-----+--------+-----+-----+
- ​|col1 |col2 |col3 | col4 |col5 |col6 |
- ​+-----+-----+-----+--------+-----+-----+
- ​| 5 | 6 |false|{"1":1,"| | |
- ​| 3 | 4 |true |[1,[3,{"| | |
- ​| 1 | 2 | 3 | 4 |null | 5 |
- ​+-----+-----+-----+--------+-----+-----+
...
i1:fselect({3})
---
- - ​+-----+-----+-----+-------------------------+
- ​|col1 |col2 |col3 | col4 |
- ​+-----+-----+-----+-------------------------+
- ​| 3 | 4 |true |[1,[3,{"aa":1,"bb":2}],3]|
- ​+-----+-----+-----+-------------------------+
...
i2:fselect({6})
---
- - ​+-----+-----+-----+---------------------------------+
- ​|col1 |col2 |col3 | col4 |
- ​+-----+-----+-----+---------------------------------+
- ​| 5 | 6 |false|{"1":1,"2":2,"3":3,"key":"value"}|
- ​+-----+-----+-----+---------------------------------+
...
i1:fselect({2})
---
- - ​+-----+
- ​|col1 |
- ​+-----+
- ​+-----+
...
i2:fselect({5})
---
- - ​+-----+
- ​|col1 |
- ​+-----+
- ​+-----+
...
s:format{{name='name', type='unsigned'}, {name='veeeeeeeery long name', type='unsigned'}}
---
...
s:fselect()
---
- - ​+-----+---------------------+--------------+---------------------------------+-----------------+-----+
- ​|name |veeeeeeeery long name| col3 | col4 | col5 |col6 |
- ​+-----+---------------------+--------------+---------------------------------+-----------------+-----+
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
- ​+-----+---------------------+--------------+---------------------------------+-----------------+-----+
...
s:fselect({}, nil, {max_width=40})
---
- - ​+-----+------+------+------+-----+-----+
- ​|name |veeeee| col3 | col4 |col5 |col6 |
- ​+-----+------+------+------+-----+-----+
- ​| 1 | 2 | 3 | 4 |null | 5 |
- ​| 3 | 4 | true |[1,[3,| | |
- ​| 5 | 6 |false |{"1":1| | |
- ​| 7 | 8 |"asdgs|"sdf" |"dsgf| |
- ​+-----+------+------+------+-----+-----+
...
i1:fselect({3})
---
- - ​+-----+---------------------+-----+-------------------------+
- ​|name |veeeeeeeery long name|col3 | col4 |
- ​+-----+---------------------+-----+-------------------------+
- ​| 3 | 4 |true |[1,[3,{"aa":1,"bb":2}],3]|
- ​+-----+---------------------+-----+-------------------------+
...
i2:fselect({6})
---
- - ​+-----+---------------------+-----+---------------------------------+
- ​|name |veeeeeeeery long name|col3 | col4 |
- ​+-----+---------------------+-----+---------------------------------+
- ​| 5 | 6 |false|{"1":1,"2":2,"3":3,"key":"value"}|
- ​+-----+---------------------+-----+---------------------------------+
...
i1:fselect({2})
---
- - ​+-----+---------------------+
- ​|name |veeeeeeeery long name|
- ​+-----+---------------------+
- ​+-----+---------------------+
...
i2:fselect({5})
---
- - ​+-----+---------------------+
- ​|name |veeeeeeeery long name|
- ​+-----+---------------------+
- ​+-----+---------------------+
...
s:fselect({}, {fselect_type='gh'})
---
- - ​|name |veeeeeeeery long name| col3 | col4 | col5 |col6 |
- ​|-----|---------------------|--------------|---------------------------------|-----------------|-----|
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
...
s:fselect({}, nil, {type='gh'})
---
- - ​|name |veeeeeeeery long name| col3 | col4 | col5 |col6 |
- ​|-----|---------------------|--------------|---------------------------------|-----------------|-----|
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
...
s:fselect({}, nil, {fselect_type='github'})
---
- - ​|name |veeeeeeeery long name| col3 | col4 | col5 |col6 |
- ​|-----|---------------------|--------------|---------------------------------|-----------------|-----|
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
...
s:gselect{}
---
- - ​|name |veeeeeeeery long name| col3 | col4 | col5 |col6 |
- ​|-----|---------------------|--------------|---------------------------------|-----------------|-----|
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
...
fselect_type = 'gh'
---
...
s:fselect()
---
- - ​|name |veeeeeeeery long name| col3 | col4 | col5 |col6 |
- ​|-----|---------------------|--------------|---------------------------------|-----------------|-----|
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
...
fselect_type = nil
---
...
s:fselect()
---
- - ​+-----+---------------------+--------------+---------------------------------+-----------------+-----+
- ​|name |veeeeeeeery long name| col3 | col4 | col5 |col6 |
- ​+-----+---------------------+--------------+---------------------------------+-----------------+-----+
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
- ​+-----+---------------------+--------------+---------------------------------+-----------------+-----+
...
s:fselect({}, {fselect_type='jira'})
---
- - ​||name||veeeeeeeery long name|| col3 || col4 || col5 ||col6||
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
...
s:jselect()
---
- - ​||name||veeeeeeeery long name|| col3 || col4 || col5 ||col6||
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |
- ​| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
...
s:fselect({}, {fselect_widths={8, 8, nil, 10}})
---
- - ​+--------+--------+--------------+----------+-----------------+-----+
- ​| name |veeeeeee| col3 | col4 | col5 |col6 |
- ​+--------+--------+--------------+----------+-----------------+-----+
- ​| 1 | 2 | 3 | 4 | null | 5 |
- ​| 3 | 4 | true |[1,[3,{"aa| | |
- ​| 5 | 6 | false |{"1":1,"2"| | |
- ​| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |
- ​+--------+--------+--------------+----------+-----------------+-----+
...
s:fselect(nil, {fselect_use_nbsp=false})
---
- - +-----+---------------------+--------------+---------------------------------+-----------------+-----+
- '|name |veeeeeeeery long name| col3 | col4 | col5 |col6
|'
- +-----+---------------------+--------------+---------------------------------+-----------------+-----+
- '| 1 | 2 | 3 | 4 | null | 5 |'
- '| 3 | 4 | true | [1,[3,{"aa":1,"bb":2}],3] | | |'
- '| 5 | 6 | false |{"1":1,"2":2,"3":3,"key":"value"}| | |'
- '| 7 | 8 |"asdgsdgswegg"| "sdf" |"dsgfsdgsegasges"| |'
- +-----+---------------------+--------------+---------------------------------+-----------------+-----+
...
......@@ -111,3 +111,40 @@ index2:get{104}
index3:get{9}
index3:select{1235}
space:drop()
--https://github.com/tarantool/tarantool/issues/5161
--formatted select
s = box.schema.space.create('test', { engine = engine })
i1 = s:create_index('test1')
i2 = s:create_index('test2', {parts={{2, 'unsigned'}}})
_ = s:replace{1, 2, 3, 4, box.NULL, 5}
_ = s:replace{3, 4, true, {1, 2, 3}}
_ = s:replace{5, 6, false, {1, 2, 3, ['key']='value'}}
_ = s:replace{3, 4, true, {1, {3, {aa=1,bb=2}}, 3}}
_ = s:replace{7, 8, 'asdgsdgswegg', 'sdf', 'dsgfsdgsegasges' }
s:fselect()
s:fselect({5}, {iterator='le'})
s:fselect({5}, {iterator='le', fselect_max_width=40})
i1:fselect({3})
i2:fselect({6})
i1:fselect({2})
i2:fselect({5})
s:format{{name='name', type='unsigned'}, {name='veeeeeeeery long name', type='unsigned'}}
s:fselect()
s:fselect({}, nil, {max_width=40})
i1:fselect({3})
i2:fselect({6})
i1:fselect({2})
i2:fselect({5})
s:fselect({}, {fselect_type='gh'})
s:fselect({}, nil, {type='gh'})
s:fselect({}, nil, {fselect_type='github'})
s:gselect{}
fselect_type = 'gh'
s:fselect()
fselect_type = nil
s:fselect()
s:fselect({}, {fselect_type='jira'})
s:jselect()
s:fselect({}, {fselect_widths={8, 8, nil, 10}})
s:fselect(nil, {fselect_use_nbsp=false})
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment