From: "n.pettik" <korablev@tarantool.org> To: tarantool-patches@freelists.org Cc: Kirill Shcherbatov <kshcherbatov@tarantool.org> Subject: [tarantool-patches] Re: [PATCH v1 1/1] sql: return a tuple result from SQL Date: Mon, 27 Aug 2018 14:02:43 +0300 [thread overview] Message-ID: <47AB5153-BA63-432B-88DE-D4A37FF375F0@tarantool.org> (raw) In-Reply-To: <7e5108046f80ecc136b68e3c00d5b24de614ad99.1534863415.git.kshcherbatov@tarantool.org> > On 21 Aug 2018, at 18:00, Kirill Shcherbatov <kshcherbatov@tarantool.org> wrote: Please, add to commit message details concerning implementation. Now it looks very poor nevertheless feature itself is quite important. > > @TarantoolBot document > Title: Return a tuple result from SQL > SQL insert/update/delete requests would return > table with processed tuples. Table? Only processed tuples are returned as I see. > It is default behavior. > > tarantool> box.sql.execute("CREATE TABLE t (s1 INT, s2 INT, > s3 INT, s4 INT PRIMARY KEY);") > tarantool> t = box.sql.execute("INSERT INTO t VALUES (1,1,1,2), > (1,1,1,5),(1,1,1,6);") > - - [1, 1, 1, 2] > - [1, 1, 1, 5] > - [1, 1, 1, 6] > > tarantool> t[3].S4 > 6 I would add more informative description. For instance, add example with triggers in order to show that even implicitly inserted/deleted tuples will be returned. Another question is - should we return deleted (for instance deleted by triggers) tuples for inserts (and vice versa)? If not, I guess it deserves to be noted in comments/docs. > NetBox would no return tuple metadata: > tarantool> t_cn[1].S4 > nul > > Feature may be disabled with interactive_mode pragma: > tarantool> box.sql.execute("pragma interactive_mode=0;") > > Closes #2370. > --- > 95 files changed, 525 insertions(+), 108 deletions(-) > delete mode 100644 test/box/sql-update-with-nested-select.result > delete mode 100644 test/box/sql-update-with-nested-select.test.lua > create mode 100644 test/sql/sql-update-with-nested-select.result > create mode 100644 test/sql/sql-update-with-nested-select.test.lua Why did you move this test within current patch? > > diff --git a/src/box/execute.c b/src/box/execute.c > index 24459b4..c41758e 100644 > --- a/src/box/execute.c > +++ b/src/box/execute.c > @@ -555,20 +555,30 @@ sql_execute(sqlite3 *db, struct sqlite3_stmt *stmt, struct port *port, > struct region *region) > { > int rc, column_count = sqlite3_column_count(stmt); > - if (column_count > 0) { > - /* Either ROW or DONE or ERROR. */ > - while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { > - if (sql_row_to_port(stmt, column_count, region, > - port) != 0) > - return -1; > + if (column_count > 0 || sqlite3_tuple_result_require_flush(stmt)) { > + /* Either ROW or TUPLE or DONE or ERROR. */ > + while (true) { > + rc = sqlite3_step(stmt); > + if (rc == SQLITE_ROW) { > + if (sql_row_to_port(stmt, column_count, region, > + port) != 0) > + return -1; > + } else if (rc == SQLITE_TUPLE) { > + struct tuple *tuple = > + sqlite3_result_tuple(stmt); > + if (tuple != NULL && > + port_tuple_add(port, tuple) != 0) > + return -1; > + } else { > + break; > + } > } > assert(rc == SQLITE_DONE || rc != SQLITE_OK); > } else { > - /* No rows. Either DONE or ERROR. */ > - rc = sqlite3_step(stmt); > + while ((rc = sqlite3_step(stmt)) == SQLITE_TUPLE); > assert(rc != SQLITE_ROW && rc != SQLITE_OK); > } > - if (rc != SQLITE_DONE) { > + if (rc != SQLITE_DONE && rc != SQLITE_TUPLE) { > diag_set(ClientError, ER_SQL_EXECUTE, sqlite3_errmsg(db)); > return -1; > } > @@ -634,6 +644,15 @@ err: > /* Failed port dump destroyes the port. */ > goto err; > } > + } else if (sqlite3_tuple_result_require_flush(stmt)) { > + if (iproto_reply_array_key(out, 0, IPROTO_METADATA) != 0) > + goto err; > + keys = 2; > + if (iproto_reply_array_key(out, port_tuple->size, > + IPROTO_DATA) != 0) > + goto err; > + if (port_dump_msgpack_16(&response->port, out) < 0) > + goto err; This is almost duplicate of code in first ‘if’ branch. Please, refactor this snippet. Moreover, there’s some mess. You don’t fill <metadata> for iproto request. Thus, in tests you always have empty metadata: cn:execute('insert into test values (10, 11, NULL)') --- - metadata: [] rows: - [10, 11, null] It seems that it is impossible to set proper metadata for DML: since ALL inserted/deleted tuples are returned, some of them could have different formats. Example is quite simple: box.sql.execute("create table t1(id int primary key, b int)") box.sql.execute("create table t2(id int primary key)") box.sql.execute("create trigger tr1 insert on t1 begin insert into t2 values(new.id + 1); end;") box.sql.execute("insert into t1 values(1, 2)") --- - - [2] - [1, 2] ... Besides, I looked at patch and to be honest I don’t like your approach. Do you really need this OP_ResultTuple and SQLITE_TUPLE things? Why can’t you utilise OP_ResultRow? *TUPLE* routine almost copies one from ROW. Lets try to use already existing OP_ResultRow for this patch. > } else { > keys = 1; > assert(port_tuple->size == 0); > diff --git a/src/box/lua/sql.c b/src/box/lua/sql.c > index 17e2694..c19d450 100644 > --- a/src/box/lua/sql.c > +++ b/src/box/lua/sql.c > @@ -6,6 +6,7 @@ > #include "box/info.h" > #include "lua/utils.h" > #include "info.h" > +#include "tuple.h" > > static void > lua_push_column_names(struct lua_State *L, struct sqlite3_stmt *stmt) > @@ -83,8 +84,9 @@ lua_sql_execute(struct lua_State *L) > > int rc; > int retval_count; > - if (sqlite3_column_count(stmt) == 0) { > - while ((rc = sqlite3_step(stmt)) == SQLITE_ROW); > + if (sqlite3_column_count(stmt) == 0 && > + !sqlite3_tuple_result_require_flush(stmt)) { > + while ((rc = sqlite3_step(stmt)) == SQLITE_TUPLE); > retval_count = 0; > } else { > lua_newtable(L); > @@ -94,9 +96,19 @@ lua_sql_execute(struct lua_State *L) > lua_rawseti(L, -2, 0); > > int row_count = 0; > - while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { > - lua_push_row(L, stmt); > - lua_rawseti(L, -2, ++row_count); > + while (true) { > + rc = sqlite3_step(stmt); > + if (rc == SQLITE_ROW) { > + lua_push_row(L, stmt); > + lua_rawseti(L, -2, ++row_count); > + } else if (rc == SQLITE_TUPLE) { > + struct tuple *tuple = > + sqlite3_result_tuple(stmt); > + luaT_pushtupleornil(L, tuple); > + lua_rawseti(L, -2, ++row_count); > + } else { > + break; > + } > } > retval_count = 1; > } > diff --git a/src/box/sql.c b/src/box/sql.c > index ae12cae..907dc86 100644 > --- a/src/box/sql.c > +++ b/src/box/sql.c > @@ -65,7 +65,8 @@ static const uint32_t default_sql_flags = SQLITE_ShortColNames > | SQLITE_EnableTrigger > | SQLITE_AutoIndex > | SQLITE_RecTriggers > - | SQLITE_ForeignKeys; > + | SQLITE_ForeignKeys > + | SQLITE_InteractiveMode; Please, for the new code avoid using SQLite prefixes. > /* > diff --git a/src/box/sql/build.c b/src/box/sql/build.c > index dddeb12..76c683c 100644 > --- a/src/box/sql/build.c > +++ b/src/box/sql/build.c > @@ -2013,7 +2013,9 @@ vdbe_emit_stat_space_clear(struct Parse *parse, const char *stat_table_name, > * On memory allocation error sql_table delete_from > * releases memory for its own. > */ > + bft is_interactive = parse->pVdbe->tuple_result_flush; > sql_table_delete_from(parse, src_list, where); > + parse->pVdbe->tuple_result_flush = is_interactive; > } > > /** > diff --git a/src/box/sql/delete.c b/src/box/sql/delete.c > index 0be6883..ea22b4c 100644 > --- a/src/box/sql/delete.c > +++ b/src/box/sql/delete.c > @@ -34,6 +34,7 @@ > #include "box/schema.h" > #include "sqliteInt.h" > #include "tarantoolInt.h" > +#include "vdbeInt.h" > > struct Table * > sql_list_lookup_table(struct Parse *parse, SrcList *src_list) > @@ -551,6 +552,12 @@ sql_generate_row_delete(struct Parse *parse, struct Table *table, > if (idx_noseek >= 0) > sqlite3VdbeAddOp1(v, OP_Delete, idx_noseek); > > + struct session *user_session = current_session(); > + if (user_session->sql_flags & SQLITE_InteractiveMode) { > + sqlite3VdbeAddOp1(v, OP_ResultTuple, cursor); > + v->tuple_result_flush = true; > + } > + > if (mode == ONEPASS_MULTI) > p5 |= OPFLAG_SAVEPOSITION; > sqlite3VdbeChangeP5(v, p5); > diff --git a/src/box/sql/main.c b/src/box/sql/main.c > index a9a0385..3bad568 100644 > --- a/src/box/sql/main.c > +++ b/src/box/sql/main.c > @@ -911,6 +911,9 @@ sqlite3ErrName(int rc) > case SQLITE_ROW: > zName = "SQLITE_ROW"; > break; > + case SQLITE_TUPLE: > + zName = "SQLITE_TUPLE"; > + break; It seems that sqlite3ErrName() is unused function, so it makes no sense adding smth to it. > case SQLITE_WARNING: > zName = "SQLITE_WARNING"; > break; > diff --git a/src/box/sql/pragma.h b/src/box/sql/pragma.h > index ecc9ee8..b4e32c1 100644 > --- a/src/box/sql/pragma.h > +++ b/src/box/sql/pragma.h > @@ -176,6 +176,11 @@ static const PragmaName aPragmaName[] = { > /* ColNames: */ 13, 6, > /* iArg: */ 1}, > #endif > + { /* zName: */ "interactive_mode", > + /* ePragTyp: */ PragTyp_FLAG, > + /* ePragFlg: */ PragFlg_Result0 | PragFlg_NoColumns1, > + /* ColNames: */ 0, 0, > + /* iArg: */ SQLITE_InteractiveMode}, > #if defined(SQLITE_DEBUG) && !defined(SQLITE_OMIT_PARSER_TRACE) > { /* zName: */ "parser_trace", > /* ePragTyp: */ PragTyp_PARSER_TRACE, > diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h > index b1f2f26..56c5037 100644 > --- a/src/box/sql/sqliteInt.h > +++ b/src/box/sql/sqliteInt.h > @@ -414,6 +414,8 @@ enum sql_ret_code { > SQLITE_ROW, > /** sqlite3_step() has finished executing. */ > SQLITE_DONE, > + /** sqlite3_step() has a ready tuple. */ What is ‘ready tuple’? Moreover, it is quite similar to sqlite_row.. > + SQLITE_TUPLE, > }; > > void * > @@ -565,8 +567,30 @@ sqlite3_prepare_v2(sqlite3 * db, /* Database handle */ > const char **pzTail /* OUT: Pointer to unused portion of zSql */ > ); > > +/** > + * Get last step tuple prepared with OP_ResultTupe. > + * Valid when SQLITE_TUPLE returned with sqlite3_step. > + * > + * @param stmt Virtual database engine program. > + * @retval tuple pointer. > + */ > +struct tuple * > +sqlite3_result_tuple(sqlite3_stmt *stmt); > + > +/** > +* Execute one step of VDBE execution. > +* @param stmt Virtual database engine program. > +* > +* @retval SQLITE_ROW On of rows of DQL request, or meta of DML - > +* 'rows deleted', 'rows inserted' and etc. Result can be > +* accessed by columns using sqlite3_column_...(). > +* @retval SQLITE_TUPLE Inserted or updated tuple on DML. > +* @retval SQLITE_ERROR Vdbe is terminated by an error. > +* @retval SQLITE_DONE Vdbe successfully finished execution, and > +* can be finalized. > +*/ > int > -sqlite3_step(sqlite3_stmt *); > +sqlite3_step(sqlite3_stmt *stmt); > > const void * > sqlite3_column_blob(sqlite3_stmt *, int iCol); > @@ -710,6 +734,15 @@ sqlite3_aggregate_context(sqlite3_context *, > int > sqlite3_column_count(sqlite3_stmt * pStmt); > > +/** > + * Test, if compiled @stmt has data to be displayed to > + * user. > + * @param stmt Virtual database engine program. > + * @retval true if any, false else False otherwise. > + */ > +bool > +sqlite3_tuple_result_require_flush(struct sqlite3_stmt *stmt); The same is here: don’t use sqlite3 prefix. > + > const char * > sqlite3_column_name(sqlite3_stmt *, int N); > > @@ -1609,6 +1642,8 @@ struct sqlite3 { > #define SQLITE_VdbeAddopTrace 0x00001000 /* Trace sqlite3VdbeAddOp() calls */ > #define SQLITE_IgnoreChecks 0x00002000 /* Do not enforce check constraints */ > #define SQLITE_ReadUncommitted 0x0004000 /* For shared-cache mode */ > +/* Return operations results in SQL. */ > +#define SQLITE_InteractiveMode 0x40000000 > #define SQLITE_ReverseOrder 0x00020000 /* Reverse unordered SELECTs */ > #define SQLITE_RecTriggers 0x00040000 /* Enable recursive triggers */ > #define SQLITE_ForeignKeys 0x00080000 /* Enforce foreign key constraints */ > @@ -1631,7 +1666,6 @@ struct sqlite3 { > #define SQLITE_FactorOutConst 0x0008 /* Constant factoring */ > /* not used 0x0010 // Was: SQLITE_IdxRealAsInt */ > #define SQLITE_DistinctOpt 0x0020 /* DISTINCT using indexes */ > -#define SQLITE_CoverIdxScan 0x0040 /* Covering index scans */ Redundant diff. > #define SQLITE_OrderByIdxJoin 0x0080 /* ORDER BY of joins via index */ > #define SQLITE_SubqCoroutine 0x0100 /* Evaluate subqueries as coroutines */ > #define SQLITE_Transitive 0x0200 /* Transitive constraints */ > diff --git a/src/box/sql/tarantoolInt.h b/src/box/sql/tarantoolInt.h > index 94517f6..7c76062 100644 > --- a/src/box/sql/tarantoolInt.h > +++ b/src/box/sql/tarantoolInt.h > @@ -62,9 +62,9 @@ int tarantoolSqlite3MovetoUnpacked(BtCursor * pCur, UnpackedRecord * pIdxKey, > int *pRes); > int tarantoolSqlite3Count(BtCursor * pCur, i64 * pnEntry); > int tarantoolSqlite3Insert(struct space *space, const char *tuple, > - const char *tuple_end); > + const char *tuple_end, struct tuple **result); > int tarantoolSqlite3Replace(struct space *space, const char *tuple, > - const char *tuple_end); > + const char *tuple_end, struct tuple **result); > int tarantoolSqlite3Delete(BtCursor * pCur, u8 flags); > int > sql_delete_by_key(struct space *space, char *key, uint32_t key_size); > diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c > index dc5146f..b9ac519 100644 > --- a/src/box/sql/vdbe.c > +++ b/src/box/sql/vdbe.c > @@ -39,6 +39,7 @@ > * in this file for details. If in doubt, do not deviate from existing > * commenting and indentation practices when changing or adding code. > */ > +#include "box/tuple.h” Nitpicking: AFAIK it is desirable to put headers in alphabetic order, so put it before #include “box/txn.h" > #include "box/box.h" > #include "box/fkey.h" > #include "box/txn.h" > @@ -1364,6 +1365,25 @@ case OP_IntCopy: { /* out2 */ > break; > } > > +/* Opcode: ResultTuple P1 * * * * > + * Synopsis: output=tuple(cursor(P1)) > + * > + * The register P1 is a cursor, from which a last tuple is > + * returned. Sqlite3_step returns SQLITE_TUPLE. > + */ > +case OP_ResultTuple: { > + VdbeCursor *cursor = p->apCsr[pOp->p1]; > + assert(cursor != NULL); > + assert(cursor->eCurType == CURTYPE_TARANTOOL); > + if (cursor->uc.pCursor->last_tuple != NULL) { > + p->result_tuple = cursor->uc.pCursor->last_tuple; > + p->pc = (int)(pOp - aOp) + 1; > + rc = SQLITE_TUPLE; > + goto vdbe_return; > + } > + break; > +} > + > /* Opcode: ResultRow P1 P2 * * * > * Synopsis: output=r[P1@P2] > * > @@ -4262,16 +4282,26 @@ case OP_IdxInsert: { /* in2 */ > } else { > BtCursor *pBtCur = pC->uc.pCursor; > if (pBtCur->curFlags & BTCF_TaCursor) { > + if (pBtCur->last_tuple != NULL) > + box_tuple_unref(pBtCur->last_tuple); > + pBtCur->last_tuple = NULL; > + struct tuple *result = NULL; > /* Make sure that memory has been allocated on region. */ > assert(aMem[pOp->p2].flags & MEM_Ephem); > if (pOp->opcode == OP_IdxInsert) > rc = tarantoolSqlite3Insert(pBtCur->space, > pIn2->z, > - pIn2->z + pIn2->n); > + pIn2->z + pIn2->n, > + &result); > else > rc = tarantoolSqlite3Replace(pBtCur->space, > pIn2->z, > - pIn2->z + pIn2->n); > + pIn2->z + pIn2->n, > + &result); > + if (rc == SQLITE_OK) { Lets move from SQLITE_OK macros and make insertOrReplace() return straight result of box_process_rw(). > + pBtCur->last_tuple = result; > + tuple_ref(result); > + } > } else if (pBtCur->curFlags & BTCF_TEphemCursor) { > rc = tarantoolSqlite3EphemeralInsert(pBtCur->space, > pIn2->z, > @@ -4319,7 +4349,7 @@ case OP_SInsert: { > struct space *space = space_by_id(pOp->p1); > assert(space != NULL); > assert(space_is_system(space)); > - rc = tarantoolSqlite3Insert(space, pIn2->z, pIn2->z + pIn2->n); > + rc = tarantoolSqlite3Insert(space, pIn2->z, pIn2->z + pIn2->n, NULL); > if (rc) > goto abort_due_to_error; > if (pOp->p5 & OPFLAG_NCHANGE) > diff --git a/src/box/sql/vdbe.h b/src/box/sql/vdbe.h > index 2987d7a..5b9219a 100644 > --- a/src/box/sql/vdbe.h > +++ b/src/box/sql/vdbe.h > @@ -236,6 +236,9 @@ void sqlite3VdbeAppendP4(Vdbe *, void *pP4, int p4type); > void > sql_vdbe_set_p4_key_def(struct Parse *parse, struct Index *index); > Firstly, you forgot about comment; secondly, this function is never used. > +void > +sql_vdbe_set_tuple_result_flush(struct Vdbe *vdbe, bool is_set); > + > VdbeOp *sqlite3VdbeGetOp(Vdbe *, int); > int sqlite3VdbeMakeLabel(Vdbe *); > void sqlite3VdbeRunOnlyOnce(Vdbe *); > diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h > index ce97f49..eaa51b3 100644 > --- a/src/box/sql/vdbeInt.h > +++ b/src/box/sql/vdbeInt.h > @@ -376,7 +376,17 @@ struct Vdbe { > Mem *aMem; /* The memory locations */ > Mem **apArg; /* Arguments to currently executing user function */ > Mem *aColName; /* Column names to return */ > - Mem *pResultSet; /* Pointer to an array of results */ > + union { > + /** > + * Pointer to an array of results for SQLITE_ROW. > + */ > + struct Mem *pResultSet; > + /** > + * Result tuple, returned by an iterator for > + * SQLITE_ROW. SQLITE_TUPLE? > + */ > + struct tuple *result_tuple; > + }; > char *zErrMsg; /* Error message written here */ > VdbeCursor **apCsr; /* One element of this array for each open cursor */ > Mem *aVar; /* Values for the OP_Variable opcode. */ > @@ -393,6 +403,11 @@ struct Vdbe { > bft changeCntOn:1; /* True to update the change-counter */ > bft runOnlyOnce:1; /* Automatically expire on reset */ > bft isPrepareV2:1; /* True if prepared with prepare_v2() */ > + /** > + * Compiled SQL have OP_ResultTuple instructions that > + * export tuples to be displayed in console. > + */ > + bft tuple_result_flush:1; For predicates pls use ‘is_’ prefix. > --- a/test/sql-tap/lua/sqltester.lua > +++ b/test/sql-tap/lua/sqltester.lua > @@ -433,6 +433,7 @@ box.cfg{ > > local engine = test_run and test_run:get_cfg('engine') or 'memtx' > box.sql.execute('pragma sql_default_engine=\''..engine..'\'') > +box.sql.execute('pragma interactive_mode=0;') > > function test.engine(self) > return engine > diff --git a/test/sql/check-clear-ephemeral.result b/test/sql/check-clear-ephemeral.result > index 4ab1fe1..7b1317b 100644 > --- a/test/sql/check-clear-ephemeral.result > +++ b/test/sql/check-clear-ephemeral.result > @@ -7,6 +7,9 @@ engine = test_run:get_cfg('engine') > box.sql.execute('pragma sql_default_engine=\''..engine..'\'') > --- > ... > +box.sql.execute('pragma interactive_mode=0;’) Can you avoid setting in each test this pragma? I mean can you patch test config to set this pragma automatically? Setting manually in each tests pragma seems to be unacceptable. > diff --git a/test/sql/misc.test.lua b/test/sql/misc.test.lua > index 1ed0198..a9f23c5 100644 > --- a/test/sql/misc.test.lua > +++ b/test/sql/misc.test.lua > @@ -1,6 +1,7 @@ > test_run = require('test_run').new() > engine = test_run:get_cfg('engine') > box.sql.execute('pragma sql_default_engine=\''..engine..'\'') > +box.sql.execute('pragma interactive_mode=0;') > > -- Forbid multistatement queries. > box.sql.execute('select 1;') > @@ -11,3 +12,21 @@ box.sql.execute(';') > box.sql.execute('') > box.sql.execute(' ;') > box.sql.execute('\n\n\n\t\t\t ') > + > +-- > +-- gh-2370: Return a tuple result from SQL > +-- > +box.sql.execute("pragma interactive_mode=1;") > +box.sql.execute("CREATE TABLE t (s1 INT, s2 INT, s3 INT, s4 INT PRIMARY KEY);") > +box.sql.execute("INSERT INTO t VALUES (1,1,1,2),(1,1,1,5),(1,1,1,6);") > +box.sql.execute("UPDATE t SET s2=s2+s1 WHERE s4 IN (SELECT s4 FROM t);"); > +box.sql.execute("UPDATE t SET s2=s2+s1 WHERE s4=6 OR s4=2;”); Add tests involving triggers pls. > diff --git a/test/sql/persistency.result b/test/sql/persistency.result > index c65baa0..8abb5cd 100644 > --- a/test/sql/persistency.result > +++ b/test/sql/persistency.result > @@ -10,6 +10,9 @@ engine = test_run:get_cfg('engine') > box.sql.execute('pragma sql_default_engine=\''..engine..'\'') > --- > ... > +box.sql.execute('pragma interactive_mode=0;') > +--- > +... > -- create space > box.sql.execute("CREATE TABLE foobar (foo PRIMARY KEY, bar)") > --- > @@ -180,6 +183,8 @@ box.sql.execute("SELECT \"name\", \"opts\" FROM \"_trigger\""); > -- ... functional > box.sql.execute("INSERT INTO foobar VALUES ('foobar trigger test', 8888)") > --- > +- - ['foobar trigger test', 8888] > + - ['trigger test', 9999] Why this is trapped to output?
prev parent reply other threads:[~2018-08-27 11:02 UTC|newest] Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top 2018-08-21 15:00 [tarantool-patches] " Kirill Shcherbatov 2018-08-21 15:28 ` [tarantool-patches] " Vladislav Shpilevoy 2018-08-27 11:02 ` n.pettik [this message]
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=47AB5153-BA63-432B-88DE-D4A37FF375F0@tarantool.org \ --to=korablev@tarantool.org \ --cc=kshcherbatov@tarantool.org \ --cc=tarantool-patches@freelists.org \ --subject='[tarantool-patches] Re: [PATCH v1 1/1] sql: return a tuple result from SQL' \ /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