Library for working with tarantool from nginx with the embedded Lua module or with Openresty
Lua HTML CSS

README.md

Openresty library for querying the tarantool NoSQL database

Introduction

This is a library to connect to the tarantool NoSQL database. This database has very interesting features that make it be sort of a bridge between a traditional SQL based database and document oriented storages like CouchDB.

It's a fork of another project that I was unhappy with. It's abundantly documented and is update regarding the tarantool API. Notably wtih support for the upsert command.

Another thing to bear in minf is that the library tries to be consistent between the way the update and upsert commands are issued in the console using Lua and the way the API works. Notably the field numbers. In the console a field number takes into account the existence of a primary index as the first field. Hence any field that come afterward will have an position that accounts for it. Specifically when specifying the operators to use for the update or upsert operations.

Installation

OpenResty

If you're using OpenResty the library should be installed under: /usr/local/openresty/lualib/resty.

Debian package

I package the library for debian here. Just follow the instructions there and install it.

adhoc installation

Put the library in a place in your filesystem that you deem appropriate. Don't forget to adjust the Lua package path, either by setting package.path in Lua code or using the lua_package_path directive.

Requirements

Since tarantool uses MessagePack for serialization the lua-MessagePack package is required.

It relies on the BitOp from LuaJIT. Therefore you need a nginx Lua module that is linked against LuaJIT and not Lua 5.1.

Usage

Creating a connection

local tnt = require 'resty.tarantool'

local tar, err = tnt:new({
    host = '127.0.0.1',
    port = 3301,
    user = 'luser',
    password = 'some_password',
    socket_timeout = 2000,
})

The above creates a connection object that connects to a tarantool server instance running on the loopback in port 3301, for user luser with password some password. See the Tarantool manual in authentication for details on how to setup users and assigning privileges to them.

The socket timeout (receive and send) is 2 seconds (2000 ms).

set_timeout

settimeout(<connection object>, <timeout in ms>)

Sets both the send and receive timeouts in miliseconds for a given socket.

tnt:set_timeout(5000) -- 5s timeout for send/receive operations

The function returns true if the setting succeeds, nil if not. Note that for the timeout to take effect this function needs to be invoked before the connection is established, i.e., before invoking the connect function. Alternatively the timeout can be specified when creating the connection object (cosocket).

connect

connect(<connection object>)

Connects the socket created above to the port and address specified when creating the connection object.

tar:connect()

The function returns true if the connection succeeds, nil if not.

set_keepalive

set_keepalive(<connection object>)

Makes the connection created get pushed to a connection pool so that the connection is kept alive across multiple requests.

tar:set_keepalive()

The function returns true if the socket is successfully pushed to connection pool (set keepalive). nil if not.

disconnect

disconnect(<connection object>)

Closes a connection to a given tarantool server running on a given address and port.

tar:disconnect()

The function returns true if the connection is successfully closed. nil if not.

ping

The ping command is useful for monitoring the tarantool server to see if it's available. If it's available for queries it returns the string PONG.

tar:ping()
-- returns PONG

select

The select operation queries a given database (space) for retrieving records.

select(<connection object>, <space name>, <index>, <key>, <options>)

where <options> is an optional argument that can consists of a table that can have the following keys:

  • offset: number of records to skip when doing the query.
  • limit: the maximum number of records to return.
  • iterator: a number specifiyng the iterator to use. Specified by the table:
local iterator_keys = {
  EQ = 0, -- equality
  REQ = 1, -- reverse equality
  ALL = 2, -- all tuples in an index
  LT = 3, -- less than
  LE = 4, -- less than or equal
  GE = 5, -- greater than or equal
  GT = 6, -- greater than
  BITSET_ALL_SET = 7, -- bits in the bitmask all set
  BITSET_ANY_SET = 8, -- any of the bist in the bitmask are set
  BITSET_ALL_NOT_SET = 9, -- none on the bits on the bitmask are set
}

More details about iterators on the tarantool manual.

select examples

Query the _space space (DB) to get the space id of the _index space.
local res, err = tar:select('_space', 'name', '_index')

if err then return ngx.say(err) end

-- response:
[2881,"_index","memtx",0,"",
  [{"name":"id","type":"num"},
   {"name":"iid","type":"num"},
   {"name":"name","type":"str"},
   {"name":"type","type":"str"},
   {"name":"opts","type":"array"},
   {"name":"parts","type":"array"}]]]

The above request is equivalent to the console request:

box.space._space.index.name:select{ '_index' }

Query the space 'activities' for the activities with a price less than 300

-- N.B. price is an index of the activities space.
local res, err = tar:select('activities', 'price', 300, { iterator = 'LT' })

The above request is equivalent to the console request:

box.space.activities.index.price:select({ 300 }, { iterator = 'LT' }) 

insert

insert(<connection object>, <space name>, <tuple>)

where <tuple> is the tuple to insert into <space> while setting the primary index, which is unique, to the value specified in the tuple.

The function returns the inserted record if the operation succeeds.

insert examples

local res, err = tar:insert('activities', { 16, 120, { activity = 'surf', price = 121 } })

-- response: 
[[16,120,{"activity":"surf","price":121}]]

The above request is equivalent to the console request:

box.space.activities:insert({16, 120, { activity = 'surf', price = 121 }})

16 is the value of the primary index here. This means that for an integer type index this will be the record with primary index 16.

replace

replace(<connection object>, <space name>, <tuple>)

The replace command is similar in the invocation and signature to the insert command. But now we're looking for replacing a record that exists already instead of inserting a new one. We need again the value of a primary unique index. But now the value must exist for the operation to succeed. If the operations succeeds the record with the replaced values is returned.

replace examples

local res, err = tar:replace('activities', { 16, 120, { activity = 'surf', price = 120 } })
-- response:
[[16,120,{"activity":"surf","price":120}]]

Here we replace the former 121 price by 120. The value of the primary index, 16 matches the record we inserted above.

The above request is equivalent to the console request:

box.space.activities:update({ 16, 120, { activity = 'surf', price = 120 }})

update

update(<connection object>, <space name>, <index>, <key>, <operator list>) 

where <operator list> is the list of operators as specified n tarantool manual. The pair (, ) uniquely identifies a record, i.e., the <key> is a value of the primary (unique) <index>.

<operator list> is a table of the form:

{ <operator>, <field position>, <value> }

the operators are:

  • + for adding to a numeric field.
  • - for subtracting to a numeric field.
  • & for bitwise AND operation between two unsigned integers.
  • | for bitwise OR operation between two unsigned integers.
  • ^ for bitwise XOR operation between two unsigned integers.
  • : for string splicing.
  • ! for field insertion.
  • # for field deletion.
  • = for assigning a given value to a field.

it returns the updated record if the operation is successful.

update examples

local res, err = tar:update('activities', 'primary', 16, { { '=', 2, 341 }, { '=', 3,  { activity = 'kitesurfing', price = 341 }}} )
-- response:
[16,341,{"activity":"kitesurfing","price":341}]]

The record with primary index 16 that we inserted above was updated.

The above request is equivalent to the console request:

box.space.activities.index.primary({ 16 }, { { '=', 2, 341 }, { '=', 3,  { activity = 'kitesurfing', price = 341 }}})

upsert

upsert(<connection object>, <space name>, <key>, <operator list>, <new tuple>)

apart from the <new tuple> argument the function signature is similar to update. In fact upsert is two commands in one. update if the record specified by the pair (, ) exists and insert if not. The key is a value from a primary index, i.e., is unique. The <new tuple> is the tuple to be inserted if the <key> value doesn't exist in the <index>. It returns an empty table {} if the operation is successful. If the operation is unsuccessful it returns nil.

upsert examples

An insert.

local res, err = tar:upsert('activities', 17, { { '=', 2, 450 }, { '=', 3,  { activity = 'submarine tour 8', price = 450 }}}, { 17, 450, { activity = 'waterski', price = 365 }})
-- response:
{}

We inserted a new record with key 17 for the primary index from the tuple:

{ 18, 450, { activity = 'waterski', price = 365 }}

The above request is equivalent to the console request:

box.space.activities:upsert({ 17 }, { { '=', 2, 450 }, { '=', 3,  { activity = 'submarine tour 8', price = 450 }}}, { 17, 450, { activity = 'waterski', price = 365 }})

An update.

local res, err = tar:upsert('activities', 17, { { '=', 2, 450 }, { '=', 3,  { activity = 'submarine tour 8', price = 450 }}}, { 18, 285, { activity = 'kitesurfing', price = 285 }})
-- response:
{}

Now we perform an update of the record identified by the key 17 in de primary index (unique).

delete

delete(<connection object>, <space>, <key>)

deletes the record uniquely specified by <key> from <space>. Note that <key> must belong to a primary (unique) index. It returns the deleted record if the operation is successful.

delete examples

local response, err = tar:delete('activities', 17)
-- response:
[17,450,{"activity":"waterski","price":365}]]

We deleted the record uniquely identified by the key 17 in the primary index from the activites space.

The above request is equivalent to the console request:

box.space.activities:delete({ 17 })

call

call(<connection object>, <proc>, <args>)

Invokes a stored procedure (Lua function) in the tarantool server we're connected to. It returns the results of the invocation.

call examples

Since the tarantool console is a Lua REPL any function can be invoked as long as it is available in the environment.

local res, err = tar:call('table.concat', {{ 'hello', ' ', 'world' }})
-- response:
[["hello world"]]

We called the table.concat function from the table library to concatenate the table:

{'hello', ' ', 'world' }

The above request is equivalent to the console request:

table.concat({ 'hello', ' ', 'world' })

For many examples of tarantool stored procedures see the repository; https://github.com/mailru/tarlua

eval

eval(<connection object>, <expression>, <return object>)

Invokes the tarantool embedded Lua interpreter to evaluate the given <expression> and returns the result in the <return object>, which is usually just an empty table { }.

eval examples

local res, err = tar:eval('return 23 * 20', { })
-- response:
[460]

we invoked the interpreter to evaluate the Lua expression:

return 23 * 20

which is also the equivalent tarantool console request.

hide_version_header

hide_version_header(<connection object>)

By default each response sends a custom HTTP header X-Tarantool-Version with the version of the tarantool server.

X-Tarantool-Version: 1.6.6-191-g82d1bc3

Invoking hide_version_header removes the header.

tar:hide_version_header()

It returns no values.

TODO

  • Test setup.