diff --git a/README.md b/README.md index acdac7cf..929442ec 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ It also provides the `crud-storage` and `crud-router` roles for +- [Quickstart](#quickstart) - [API](#api) - [Insert](#insert) - [Get](#get) @@ -37,6 +38,29 @@ It also provides the `crud-storage` and `crud-router` roles for +## Quickstart + +First, [install Tarantool](https://www.tarantool.io/en/download). + +Now you have the following options how to learn crud API and use it in a +project: + +* Play with crud on a testing dataset on a single instance: + + ```shell + $ git clone https://github.com/tarantool/crud.git + $ cd crud + $ tarantoolctl rocks make + $ ./doc/playground.lua + tarantool> crud.select('customers', {{'<=', 'age', 35}}, {first = 10}) + tarantool> crud.select('developers', nil, {first = 6}) + ``` +* Add crud into dependencies of a Cartridge application and add crud roles into + dependencies of your roles (see [Cartridge roles](#cartridge-roles) section). +* Add crud into dependencies of your application (rockspec, RPM spec -- depends + of your choice) and call crud initialization code from storage and router + code (see [API](#api) section). + ## API The CRUD operations should be called from router. diff --git a/doc/playground.lua b/doc/playground.lua new file mode 100755 index 00000000..3a5925a2 --- /dev/null +++ b/doc/playground.lua @@ -0,0 +1,150 @@ +#!/usr/bin/env tarantool + +-- How to run: +-- +-- $ ./doc/playground.lua +-- +-- Or +-- +-- $ KEEP_DATA=1 ./doc/playground.lua +-- +-- What to do next: +-- +-- Choose an example from README.md, doc/select.md or doc/pairs.md +-- and run. For example: +-- +-- tarantool> crud.select('customers', {{'<=', 'age', 35}}, {first = 10}) +-- tarantool> crud.select('developers', nil, {first = 6}) + +local fio = require('fio') +local console = require('console') +local vshard = require('vshard') +local crud = require('crud') + +-- Trick to don't leave *.snap, *.xlog files. See +-- test/tuple_keydef.test.lua in the tuple-keydef module. +if os.getenv('KEEP_DATA') ~= nil then + box.cfg() +else + local tempdir = fio.tempdir() + box.cfg({ + memtx_dir = tempdir, + wal_mode = 'none', + }) + fio.rmtree(tempdir) +end + +-- Setup vshard. +_G.vshard = vshard +box.once('guest', function() + box.schema.user.grant('guest', 'super') +end) +local uri = 'guest@localhost:3301' +local cfg = { + bucket_count = 3000, + sharding = { + [box.info().cluster.uuid] = { + replicas = { + [box.info().uuid] = { + uri = uri, + name = 'storage', + master = true, + }, + }, + }, + }, +} +vshard.storage.cfg(cfg, box.info().uuid) +vshard.router.cfg(cfg) +vshard.router.bootstrap() + +-- Create the 'customers' space. +box.once('customers', function() + box.schema.create_space('customers', { + format = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + } + }) + box.space.customers:create_index('primary_index', { + parts = { + {field = 1, type = 'unsigned'}, + }, + }) + box.space.customers:create_index('bucket_id', { + parts = { + {field = 2, type = 'unsigned'}, + }, + unique = false, + }) + box.space.customers:create_index('age', { + parts = { + {field = 4, type = 'number'}, + }, + unique = false, + }) + + -- Fill the space. + box.space.customers:insert({1, 477, 'Elizabeth', 12}) + box.space.customers:insert({2, 401, 'Mary', 46}) + box.space.customers:insert({3, 2804, 'David', 33}) + box.space.customers:insert({4, 1161, 'William', 81}) + box.space.customers:insert({5, 1172, 'Jack', 35}) + box.space.customers:insert({6, 1064, 'William', 25}) + box.space.customers:insert({7, 693, 'Elizabeth', 18}) +end) + +-- Create the developers space. +box.once('developers', function() + box.schema.create_space('developers', { + format = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'surname', type = 'string'}, + {name = 'age', type = 'number'}, + } + }) + box.space.developers:create_index('primary_index', { + parts = { + {field = 1, type = 'unsigned'}, + }, + }) + box.space.developers:create_index('bucket_id', { + parts = { + {field = 2, type = 'unsigned'}, + }, + unique = false, + }) + box.space.developers:create_index('age_index', { + parts = { + {field = 5, type = 'number'}, + }, + unique = false, + }) + box.space.developers:create_index('full_name', { + parts = { + {field = 3, type = 'string'}, + {field = 4, type = 'string'}, + }, + unique = false, + }) + + -- Fill the space. + box.space.developers:insert({1, 477, 'Alexey', 'Adams', 20}) + box.space.developers:insert({2, 401, 'Sergey', 'Allred', 21}) + box.space.developers:insert({3, 2804, 'Pavel', 'Adams', 27}) + box.space.developers:insert({4, 1161, 'Mikhail', 'Liston', 51}) + box.space.developers:insert({5, 1172, 'Dmitry', 'Jacobi', 16}) + box.space.developers:insert({6, 1064, 'Alexey', 'Sidorov', 31}) +end) + +-- Initialize crud. +crud.init_storage() +crud.init_router() + +-- Start a console. +console.start() +os.exit() diff --git a/test/doc/playground_test.lua b/test/doc/playground_test.lua new file mode 100644 index 00000000..0fa8ad5a --- /dev/null +++ b/test/doc/playground_test.lua @@ -0,0 +1,133 @@ +local yaml = require('yaml') +local t = require('luatest') +local g = t.group() + +local popen_ok, popen = pcall(require, 'popen') + +g.before_all(function() + t.skip_if(not popen_ok, 'no built-in popen module') + t.skip_if(jit.os == 'OSX', 'popen is broken on Mac OS: ' .. + 'https://github.com/tarantool/tarantool/issues/6674') +end) + +-- Run ./doc/playground.lua, execute a request and compare the +-- output with reference return values. +-- +-- The first arguments is the request string. All the following +-- arguments are expected return values (as Lua values). +-- +-- The function ignores trailing `null` values in the YAML +-- output. +local function check_request(request, ...) + local ph, err = popen.new({'./doc/playground.lua'}, { + stdin = popen.opts.PIPE, + stdout = popen.opts.PIPE, + stderr = popen.opts.DEVNULL, + }) + if ph == nil then + error('popen.new: ' .. tostring(err)) + end + + local ok, err = ph:write(request, {timeout = 1}) + if not ok then + ph:close() + error('ph:write: ' .. tostring(err)) + end + ph:shutdown({stdin = true}) + + -- Read everything until EOF. + local chunks = {} + while true do + local chunk, err = ph:read() + if chunk == nil then + ph:close() + error('ph:read: ' .. tostring(err)) + end + if chunk == '' then break end -- EOF + table.insert(chunks, chunk) + end + + local status = ph:wait() + assert(status.state == popen.state.EXITED) + + -- Glue all chunks, parse response. + local stdout = table.concat(chunks) + local response_yaml = string.match(stdout, '%-%-%-.-%.%.%.') + local response = yaml.decode(response_yaml) + + -- NB: This call does NOT differentiate `nil` and `box.NULL`. + t.assert_equals(response, {...}) +end + +local cases = { + test_select_customers = { + request = "crud.select('customers', {{'<=', 'age', 35}}, {first = 10})", + retval_1 = { + metadata = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'age', type = 'number'}, + }, + rows = { + {5, 1172, 'Jack', 35}, + {3, 2804, 'David', 33}, + {6, 1064, 'William', 25}, + {7, 693, 'Elizabeth', 18}, + {1, 477, 'Elizabeth', 12}, + }, + } + }, + test_select_developers = { + request = "crud.select('developers', nil, {first = 6})", + retval_1 = { + metadata = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'surname', type = 'string'}, + {name = 'age', type = 'number'}, + }, + rows = { + {1, 477, 'Alexey', 'Adams', 20}, + {2, 401, 'Sergey', 'Allred', 21}, + {3, 2804, 'Pavel', 'Adams', 27}, + {4, 1161, 'Mikhail', 'Liston', 51}, + {5, 1172, 'Dmitry', 'Jacobi', 16}, + {6, 1064, 'Alexey', 'Sidorov', 31}, + }, + }, + }, + test_insert = { + request = ("crud.insert('developers', %s)"):format( + "{100, nil, 'Alfred', 'Hitchcock', 123}"), + retval_1 = { + metadata = { + {name = 'id', type = 'unsigned'}, + {name = 'bucket_id', type = 'unsigned'}, + {name = 'name', type = 'string'}, + {name = 'surname', type = 'string'}, + {name = 'age', type = 'number'}, + }, + rows = { + {100, 2976, 'Alfred', 'Hitchcock', 123}, + }, + } + }, + test_error = { + request = [[ + do + local res, err = crud.select('non_existent', nil, {first = 10}) + return res, err and err.err or nil + end + ]], + retval_1 = box.NULL, + retval_2 = 'Space "non_existent" doesn\'t exist', + }, +} + +for case_name, case in pairs(cases) do + g[case_name] = function() + check_request(case.request, case.retval_1, case.retval_2) + end +end