[patches] [PATCH 1/4] sql: Return last updated tuple if requested
Vladislav Shpilevoy
v.shpilevoy at tarantool.org
Wed Mar 14 00:49:24 MSK 2018
Rabased version of a very old commit:
commit 7e9858fc06ed64604a605ad6329f6ea9fec3b768 (origin/kyukhin/gh-2618-ret-gen-values)
Author: Kirill Yukhin <kyukhin at tarantool.org>
Date: Mon Sep 25 18:00:02 2017 +0300
If requested by IPROTO protocol , return last updated tuple.
Done via extra field in VDBE engine, called sql_options.
This new field should in future adsorb most of the sqlite3
internals, making it connection-local.
Right now this structure contains single field: pointer
to pointer to last updated tuple.
If non-NULL pointer is passed, tuple_unref() must be called
upon end of handling.
Part of #2618
Signed-off-by: Vladislav Shpilevoy <v.shpilevoy at tarantool.org>
---
src/box/execute.c | 29 ++++++++++++++++++++++-------
src/box/execute.h | 2 +-
src/box/iproto.cc | 4 +++-
src/box/lua/sql.c | 9 ++++++---
src/box/sql.c | 25 ++++++++++++++++---------
src/box/sql.h | 22 ++++++++++++++++++----
src/box/sql/analyze.c | 4 ++--
src/box/sql/legacy.c | 2 +-
src/box/sql/sqlite3.h | 31 ++++++++++++++++++++++++-------
src/box/sql/tarantoolInt.h | 4 ++--
src/box/sql/vdbe.c | 12 ++++++++----
src/box/sql/vdbeInt.h | 5 +++++
src/box/sql/vdbeapi.c | 6 ++++--
src/box/sql/vdbeaux.c | 44 +++++++++++++++++++++++++++-----------------
test/unit/sql-bitvec.result | 39 ---------------------------------------
15 files changed, 139 insertions(+), 99 deletions(-)
delete mode 100644 test/unit/sql-bitvec.result
diff --git a/src/box/execute.c b/src/box/execute.c
index 48d166b5b..756db5fd6 100644
--- a/src/box/execute.c
+++ b/src/box/execute.c
@@ -548,12 +548,12 @@ sql_get_description(struct sqlite3_stmt *stmt, struct obuf *out,
static inline int
sql_execute(sqlite3 *db, struct sqlite3_stmt *stmt, int column_count,
- struct port *port, struct region *region)
+ struct port *port, struct region *region, struct sql_options *opts)
{
int rc;
if (column_count > 0) {
/* Either ROW or DONE or ERROR. */
- while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
+ while ((rc = sqlite3_step(stmt, NULL)) == SQLITE_ROW) {
if (sql_row_to_port(stmt, column_count, region,
port) != 0)
return -1;
@@ -561,7 +561,8 @@ sql_execute(sqlite3 *db, struct sqlite3_stmt *stmt, int column_count,
assert(rc == SQLITE_DONE || rc != SQLITE_OK);
} else {
/* No rows. Either DONE or ERROR. */
- rc = sqlite3_step(stmt);
+ rc = sqlite3_step(stmt, opts);
+
assert(rc != SQLITE_ROW && rc != SQLITE_OK);
}
if (rc != SQLITE_DONE) {
@@ -586,13 +587,14 @@ sql_execute(sqlite3 *db, struct sqlite3_stmt *stmt, int column_count,
*/
static inline int
sql_execute_and_encode(sqlite3 *db, struct sqlite3_stmt *stmt, struct obuf *out,
- uint64_t sync, struct region *region)
+ uint64_t sync, struct region *region,
+ struct sql_options *opts)
{
struct port port;
struct port_tuple *port_tuple = (struct port_tuple *)&port;
port_tuple_create(&port);
int column_count = sqlite3_column_count(stmt);
- if (sql_execute(db, stmt, column_count, &port, region) != 0)
+ if (sql_execute(db, stmt, column_count, &port, region, opts) != 0)
goto err_execute;
/*
@@ -647,8 +649,11 @@ err_execute:
int
sql_prepare_and_execute(const struct sql_request *request, struct obuf *out,
- struct region *region)
+ struct region *region, bool is_last_tuple_needed)
{
+ struct sql_options opts;
+ struct tuple *last_inserted_tuple = NULL;
+
const char *sql = request->sql_text;
uint32_t len;
sql = mp_decode_str(&sql, &len);
@@ -665,10 +670,20 @@ sql_prepare_and_execute(const struct sql_request *request, struct obuf *out,
assert(stmt != NULL);
if (sql_bind(request, stmt) != 0)
goto err_stmt;
+
+ if (is_last_tuple_needed)
+ sql_options_create(&opts, &last_inserted_tuple);
+ else
+ sql_options_create(&opts, NULL);
+
if (sql_execute_and_encode(db, stmt, out, request->sync,
- region) != 0)
+ region, &opts) != 0)
goto err_stmt;
sqlite3_finalize(stmt);
+
+ if (opts.last_tuple != NULL && *opts.last_tuple != NULL)
+ tuple_unref(*opts.last_tuple);
+
return 0;
err_stmt:
sqlite3_finalize(stmt);
diff --git a/src/box/execute.h b/src/box/execute.h
index 76fdbf5f5..0aef26702 100644
--- a/src/box/execute.h
+++ b/src/box/execute.h
@@ -105,7 +105,7 @@ xrow_decode_sql(const struct xrow_header *row, struct sql_request *request,
*/
int
sql_prepare_and_execute(const struct sql_request *request, struct obuf *out,
- struct region *region);
+ struct region *region, bool is_last_tuple_needed);
#if defined(__cplusplus)
} /* extern "C" { */
diff --git a/src/box/iproto.cc b/src/box/iproto.cc
index f7784ef23..9ace0d082 100644
--- a/src/box/iproto.cc
+++ b/src/box/iproto.cc
@@ -1382,13 +1382,15 @@ tx_process_sql(struct cmsg *m)
{
struct iproto_msg *msg = tx_accept_msg(m);
struct obuf *out = msg->connection->tx.p_obuf;
+ bool is_last_tuple_needed = true;
tx_fiber_init(msg->connection->session, msg->header.sync);
if (tx_check_schema(msg->header.schema_version))
goto error;
assert(msg->header.type == IPROTO_EXECUTE);
- if (sql_prepare_and_execute(&msg->sql, out, &fiber()->gc) != 0)
+ if (sql_prepare_and_execute(&msg->sql, out, &fiber()->gc,
+ is_last_tuple_needed) != 0)
goto error;
iproto_wpos_create(&msg->wpos, out);
return;
diff --git a/src/box/lua/sql.c b/src/box/lua/sql.c
index 16bf2acdb..e45122c9f 100644
--- a/src/box/lua/sql.c
+++ b/src/box/lua/sql.c
@@ -225,10 +225,12 @@ lua_sql_execute(struct lua_State *L)
l->stmt_count --;
break;
}
-
+ struct sql_options opts;
+ sql_options_create(&opts, NULL);
int column_count = sqlite3_column_count(ps->stmt);
if (column_count == 0) {
- while ((rc = sqlite3_step(ps->stmt)) == SQLITE_ROW) { ; }
+ while ((rc = sqlite3_step(ps->stmt,
+ &opts)) == SQLITE_ROW);
} else {
char *typestr;
l->column_count = column_count;
@@ -252,7 +254,8 @@ lua_sql_execute(struct lua_State *L)
lua_rawseti(L, -2, 0);
int row_count = 0;
- while ((rc = sqlite3_step(ps->stmt)) == SQLITE_ROW) {
+ while ((rc = sqlite3_step(ps->stmt,
+ &opts)) == SQLITE_ROW) {
lua_push_row(L, l);
row_count++;
lua_rawseti(L, -2, row_count);
diff --git a/src/box/sql.c b/src/box/sql.c
index e32ff2383..73fd13996 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -519,7 +519,8 @@ int tarantoolSqlite3EphemeralDrop(BtCursor *pCur)
return SQLITE_OK;
}
-static int insertOrReplace(BtCursor *pCur, int operationType)
+static int
+insertOrReplace(BtCursor *pCur, int operationType, struct tuple **tuple)
{
assert(pCur->curFlags & BTCF_TaCursor);
assert(operationType == TARANTOOL_INDEX_INSERT ||
@@ -527,27 +528,33 @@ static int insertOrReplace(BtCursor *pCur, int operationType)
int space_id = SQLITE_PAGENO_TO_SPACEID(pCur->pgnoRoot);
int rc;
+ if (tuple != NULL && *tuple != NULL) {
+ tuple_unref(*tuple);
+ *tuple = NULL;
+ }
if (operationType == TARANTOOL_INDEX_INSERT) {
rc = box_insert(space_id, pCur->pKey,
- (const char *)pCur->pKey + pCur->nKey,
- NULL /* result */);
+ (const char *)pCur->pKey + pCur->nKey, tuple);
} else {
rc = box_replace(space_id, pCur->pKey,
- (const char *)pCur->pKey + pCur->nKey,
- NULL /* result */);
+ (const char *)pCur->pKey + pCur->nKey, tuple);
}
+ if (tuple != NULL && *tuple != NULL)
+ tuple_ref(*tuple);
return rc == 0 ? SQLITE_OK : SQL_TARANTOOL_INSERT_FAIL;;
}
-int tarantoolSqlite3Insert(BtCursor *pCur)
+int
+tarantoolSqlite3Insert(BtCursor *pCur, struct tuple **tuple)
{
- return insertOrReplace(pCur, TARANTOOL_INDEX_INSERT);
+ return insertOrReplace(pCur, TARANTOOL_INDEX_INSERT, tuple);
}
-int tarantoolSqlite3Replace(BtCursor *pCur)
+int
+tarantoolSqlite3Replace(BtCursor *pCur, struct tuple **tuple)
{
- return insertOrReplace(pCur, TARANTOOL_INDEX_REPLACE);
+ return insertOrReplace(pCur, TARANTOOL_INDEX_REPLACE, tuple);
}
/*
diff --git a/src/box/sql.h b/src/box/sql.h
index 8a7c420b9..939e0b24c 100644
--- a/src/box/sql.h
+++ b/src/box/sql.h
@@ -41,10 +41,7 @@ sql_init();
void
sql_free();
-/*
- * struct sqlite3 *
- * sql_get();
- *
+/**
* Currently, this is the only SQL execution interface provided.
* If not yet initialised, returns NULL.
* Use the regular sqlite3_* API with this handle, but
@@ -55,6 +52,23 @@ sql_free();
struct sqlite3 *
sql_get();
+
+struct tuple;
+
+/*
+ * Auxilary paramters to be set and get by Tarantool.
+ */
+struct sql_options {
+ struct tuple **last_tuple;
+};
+
+static inline void
+sql_options_create(struct sql_options *opts,
+ struct tuple **last_tuple)
+{
+ opts->last_tuple = last_tuple;
+}
+
#if defined(__cplusplus)
} /* extern "C" { */
#endif
diff --git a/src/box/sql/analyze.c b/src/box/sql/analyze.c
index aec7a1a79..87846dbc6 100644
--- a/src/box/sql/analyze.c
+++ b/src/box/sql/analyze.c
@@ -1487,7 +1487,7 @@ loadStatTbl(sqlite3 * db, /* Database handle */
if (rc)
goto finalize;
- while (sqlite3_step(pStmt) == SQLITE_ROW) {
+ while (sqlite3_step(pStmt, NULL) == SQLITE_ROW) {
int nIdxCol = 1; /* Number of columns in stat4 records */
char *zTab; /* Table name */
@@ -1550,7 +1550,7 @@ loadStatTbl(sqlite3 * db, /* Database handle */
if (rc)
goto finalize;
- while (sqlite3_step(pStmt) == SQLITE_ROW) {
+ while (sqlite3_step(pStmt, NULL) == SQLITE_ROW) {
char *zTab; /* Table name */
char *zIndex; /* Index name */
Index *pIdx; /* Pointer to the index object */
diff --git a/src/box/sql/legacy.c b/src/box/sql/legacy.c
index e75709551..80a76b760 100644
--- a/src/box/sql/legacy.c
+++ b/src/box/sql/legacy.c
@@ -92,7 +92,7 @@ sqlite3_exec(sqlite3 * db, /* The database on which the SQL executes */
while (1) {
int i;
- rc = sqlite3_step(pStmt);
+ rc = sqlite3_step(pStmt, NULL);
/* Invoke the callback function if required */
if (xCallback && (SQLITE_ROW == rc ||
(SQLITE_DONE == rc && !callbackIsInit
diff --git a/src/box/sql/sqlite3.h b/src/box/sql/sqlite3.h
index 73e916991..cd5e181cc 100644
--- a/src/box/sql/sqlite3.h
+++ b/src/box/sql/sqlite3.h
@@ -3209,13 +3209,21 @@ sqlite3_prepare(sqlite3 * db, /* Database handle */
const char **pzTail /* OUT: Pointer to unused portion of zSql */
);
+/*
+ * @param db Database handle
+ * @param zSql SQL statement, UTF-8 encoded.
+ * @param nByte Maximum length of zSql in bytes.
+ * @param[out] ppStmt Statement handle.
+ * @param[out] pzTail Pointer to unused portion of zSql.
+ *
+ * @retval SQL status code.
+ */
SQLITE_API int
-sqlite3_prepare_v2(sqlite3 * db, /* Database handle */
- const char *zSql, /* SQL statement, UTF-8 encoded */
- int nByte, /* Maximum length of zSql in bytes. */
- sqlite3_stmt ** ppStmt, /* OUT: Statement handle */
- const char **pzTail /* OUT: Pointer to unused portion of zSql */
- );
+sqlite3_prepare_v2(sqlite3 *db,
+ const char *zSql,
+ int nByte,
+ sqlite3_stmt **ppStmt,
+ const char **pzTail);
/*
* CAPI3REF: Retrieving Statement SQL
* METHOD: sqlite3_stmt
@@ -3782,8 +3790,17 @@ sqlite3_column_decltype(sqlite3_stmt *, int);
* then the more specific [error codes] are returned directly
* by sqlite3_step(). The use of the "v2" interface is recommended.
*/
+/*
+ * @param stmt SQL prepared statement
+ * @param[out] opts Requested data to be returned
+ *
+ * @retval SQL status code
+ */
+
+struct sql_options;
+
SQLITE_API int
-sqlite3_step(sqlite3_stmt *);
+sqlite3_step(sqlite3_stmt *stmt, struct sql_options *opts);
/*
* CAPI3REF: Number of columns in a result set
diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h
index 39fdbcd76..505b3076b 100644
--- a/src/box/sql/tarantoolInt.h
+++ b/src/box/sql/tarantoolInt.h
@@ -74,8 +74,8 @@ int tarantoolSqlite3Previous(BtCursor * pCur, int *pRes);
int tarantoolSqlite3MovetoUnpacked(BtCursor * pCur, UnpackedRecord * pIdxKey,
int *pRes);
int tarantoolSqlite3Count(BtCursor * pCur, i64 * pnEntry);
-int tarantoolSqlite3Insert(BtCursor * pCur);
-int tarantoolSqlite3Replace(BtCursor * pCur);
+int tarantoolSqlite3Insert(BtCursor *pCur, struct tuple **tuple);
+int tarantoolSqlite3Replace(BtCursor *pCur, struct tuple **tuple);
int tarantoolSqlite3Delete(BtCursor * pCur, u8 flags);
int tarantoolSqlite3ClearTable(int iTable);
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index a194a6e72..ec1294946 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -50,6 +50,7 @@
#include "box/schema.h"
#include "box/space.h"
#include "box/sequence.h"
+#include "box/sql.h"
/*
* Invoke this macro on memory cells just prior to changing the
@@ -4448,10 +4449,13 @@ case OP_IdxInsert: { /* in2 */
if (pBtCur->curFlags & BTCF_TaCursor) {
/* Make sure that memory has been allocated on region. */
assert(aMem[pOp->p2].flags & MEM_Ephem);
- if (pOp->opcode == OP_IdxInsert)
- rc = tarantoolSqlite3Insert(pBtCur);
- else
- rc = tarantoolSqlite3Replace(pBtCur);
+ struct tuple **last_tuple = p->sql_options->last_tuple;
+ if (pOp->opcode == OP_IdxInsert) {
+ rc = tarantoolSqlite3Insert(pBtCur, last_tuple);
+ } else {
+ rc = tarantoolSqlite3Replace(pBtCur,
+ last_tuple);
+ }
} else if (pBtCur->curFlags & BTCF_TEphemCursor) {
rc = tarantoolSqlite3EphemeralInsert(pBtCur);
} else {
diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
index 1836673a4..dc2bfcc4c 100644
--- a/src/box/sql/vdbeInt.h
+++ b/src/box/sql/vdbeInt.h
@@ -355,6 +355,7 @@ struct sql_txn {
i64 nDeferredImmConsSave;
};
+struct sql_options;
/*
* An instance of the virtual machine. This structure contains the complete
* state of the virtual machine.
@@ -444,6 +445,10 @@ struct Vdbe {
int nScan; /* Entries in aScan[] */
ScanStatus *aScan; /* Scan definitions for sqlite3_stmt_scanstatus() */
#endif
+
+ struct sql_options *sql_options; /* If not NULL, data to be passed back
+ * to Tarantool.
+ */
};
/*
diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
index 9c86c4c72..79994cb56 100644
--- a/src/box/sql/vdbeapi.c
+++ b/src/box/sql/vdbeapi.c
@@ -532,7 +532,7 @@ sqlite3_result_error_nomem(sqlite3_context * pCtx)
* outer sqlite3_step() wrapper procedure.
*/
static int
-sqlite3Step(Vdbe * p)
+sqlite3Step(Vdbe *p)
{
sqlite3 *db;
int rc;
@@ -655,7 +655,7 @@ sqlite3Step(Vdbe * p)
* call sqlite3Reprepare() and try again.
*/
int
-sqlite3_step(sqlite3_stmt * pStmt)
+sqlite3_step(sqlite3_stmt *pStmt, struct sql_options *sql_options)
{
int rc; /* Result from sqlite3Step() */
int rc2 = SQLITE_OK; /* Result from sqlite3Reprepare() */
@@ -666,6 +666,8 @@ sqlite3_step(sqlite3_stmt * pStmt)
if (vdbeSafetyNotNull(v)) {
return SQLITE_MISUSE_BKPT;
}
+
+ v->sql_options = sql_options;
db = v->db;
sqlite3_mutex_enter(db->mutex);
v->doingRerun = 0;
diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
index 92bf9943b..75f8a1163 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -38,6 +38,8 @@
#include "box/schema.h"
#include "box/tuple_format.h"
#include "box/txn.h"
+#include "box/sql.h"
+#include "box/tuple.h"
#include "msgpuck/msgpuck.h"
#include "sqliteInt.h"
#include "vdbeInt.h"
@@ -604,7 +606,7 @@ sqlite3VdbeAssertMayAbort(Vdbe * v, int mayAbort)
int opcode = pOp->opcode;
if ((opcode == OP_Halt || opcode == OP_HaltIfNull) &&
(pOp->p1 & 0xff) == SQLITE_CONSTRAINT &&
- pOp->p2 == ON_CONFLICT_ACTION_ABORT){
+ pOp->p2 == ON_CONFLICT_ACTION_ABORT){
hasAbort = 1;
break;
}
@@ -2517,6 +2519,25 @@ sql_savepoint(Vdbe *p,
return pNew;
}
+/**
+ * Perform rollback of current transaction, which includes
+ * rollback and freeing all allocated cursors. Finally, since no
+ * was occured - set last inserted tuple to NULL.
+ * @param p Prepared stmt.
+ */
+static inline void
+sqlite3RollbackAndCleanup(Vdbe *p)
+{
+ if (p->sql_options != NULL && p->sql_options->last_tuple != NULL) {
+ if (*p->sql_options->last_tuple != NULL)
+ tuple_unref(*p->sql_options->last_tuple);
+ p->sql_options->last_tuple = NULL;
+ }
+ box_txn_rollback();
+ closeCursorsAndFree(p);
+ sqlite3RollbackAll(p, SQLITE_ABORT_ROLLBACK);
+}
+
/*
* This routine is called the when a VDBE tries to halt. If the VDBE
* has made changes and is in autocommit mode, then commit those
@@ -2595,10 +2616,7 @@ sqlite3VdbeHalt(Vdbe * p)
/* We are forced to roll back the active transaction. Before doing
* so, abort any other statements this handle currently has active.
*/
- box_txn_rollback();
- closeCursorsAndFree(p);
- sqlite3RollbackAll(p,
- SQLITE_ABORT_ROLLBACK);
+ sqlite3RollbackAndCleanup(p);
sqlite3CloseSavepoints(p);
p->autoCommit = 1;
p->nChange = 0;
@@ -2648,9 +2666,7 @@ sqlite3VdbeHalt(Vdbe * p)
return SQLITE_BUSY;
} else if (rc != SQLITE_OK) {
p->rc = rc;
- box_txn_rollback();
- closeCursorsAndFree(p);
- sqlite3RollbackAll(p, SQLITE_OK);
+ sqlite3RollbackAndCleanup(p);
p->nChange = 0;
} else {
p->nDeferredCons = 0;
@@ -2660,9 +2676,7 @@ sqlite3VdbeHalt(Vdbe * p)
sqlite3CommitInternalChanges();
}
} else {
- box_txn_rollback();
- closeCursorsAndFree(p);
- sqlite3RollbackAll(p, SQLITE_OK);
+ sqlite3RollbackAndCleanup(p);
p->nChange = 0;
}
p->anonymous_savepoint = NULL;
@@ -2672,9 +2686,7 @@ sqlite3VdbeHalt(Vdbe * p)
} else if (p->errorAction == ON_CONFLICT_ACTION_ABORT) {
eStatementOp = SAVEPOINT_ROLLBACK;
} else {
- box_txn_rollback();
- closeCursorsAndFree(p);
- sqlite3RollbackAll(p, SQLITE_ABORT_ROLLBACK);
+ sqlite3RollbackAndCleanup(p);
sqlite3CloseSavepoints(p);
p->autoCommit = 1;
p->nChange = 0;
@@ -2690,15 +2702,13 @@ sqlite3VdbeHalt(Vdbe * p)
if (eStatementOp) {
rc = sqlite3VdbeCloseStatement(p, eStatementOp);
if (rc) {
- box_txn_rollback();
if (p->rc == SQLITE_OK
|| (p->rc & 0xff) == SQLITE_CONSTRAINT) {
p->rc = rc;
sqlite3DbFree(db, p->zErrMsg);
p->zErrMsg = 0;
}
- closeCursorsAndFree(p);
- sqlite3RollbackAll(p, SQLITE_ABORT_ROLLBACK);
+ sqlite3RollbackAndCleanup(p);
sqlite3CloseSavepoints(p);
p->autoCommit = 1;
p->nChange = 0;
diff --git a/test/unit/sql-bitvec.result b/test/unit/sql-bitvec.result
deleted file mode 100644
index 13c23510f..000000000
--- a/test/unit/sql-bitvec.result
+++ /dev/null
@@ -1,39 +0,0 @@
-1..5
- *** main ***
- 1..2
- ok 1 - error test
- ok 2 - error test
-ok 1 - subtests
- 1..4
- ok 1 - various sizes
- ok 2 - various sizes
- ok 3 - various sizes
- ok 4 - various sizes
-ok 2 - subtests
- 1..4
- ok 1 - larger increments
- ok 2 - larger increments
- ok 3 - larger increments
- ok 4 - larger increments
-ok 3 - subtests
- 1..9
- ok 1 - clearing mechanism
- ok 2 - clearing mechanism
- ok 3 - clearing mechanism
- ok 4 - clearing mechanism
- ok 5 - clearing mechanism
- ok 6 - clearing mechanism
- ok 7 - clearing mechanism
- ok 8 - clearing mechanism
- ok 9 - clearing mechanism
-ok 4 - subtests
- 1..7
- ok 1 - random subsets
- ok 2 - random subsets
- ok 3 - random subsets
- ok 4 - random subsets
- ok 5 - random subsets
- ok 6 - random subsets
- ok 7 - random subsets
-ok 5 - subtests
- *** main: done ***
--
2.14.3 (Apple Git-98)
More information about the Tarantool-patches
mailing list