Tarantool development patches archive
 help / color / mirror / Atom feed
From: Vladislav Shpilevoy <v.shpilevoy@tarantool.org>
To: Kirill Shcherbatov <kshcherbatov@tarantool.org>,
	tarantool-patches@freelists.org
Subject: [tarantool-patches] Re: [PATCH v1 4/4] sql: move Triggers to server
Date: Mon, 4 Jun 2018 16:27:33 +0300	[thread overview]
Message-ID: <4816bb71-cf0a-f508-6709-d2a526c532b9@tarantool.org> (raw)
In-Reply-To: <59f11ba9-27a5-5b09-09de-416881ba3b89@tarantool.org>

Thanks for the patch!

At first, please, do not mess the comments. Try to respond on them
in the same order as I had wrote.

At second, please, do not skip them.

At third, check the travis - it fails on box/misc test.

See 22 comments below. Most of them are one-line minors.

On 01/06/2018 23:25, Kirill Shcherbatov wrote:
>  From c0775801fcdb475ee6e77dff483f4f6a84c1afc3 Mon Sep 17 00:00:00 2001
> Message-Id: <c0775801fcdb475ee6e77dff483f4f6a84c1afc3.1527884700.git.kshcherbatov@gmail.com>
> From: Kirill Shcherbatov <kshcherbatov@tarantool.org>
> Date: Wed, 30 May 2018 16:03:43 +0300
> Subject: [PATCH] sql: move Triggers to server

1. Please, do not send SMTP headers in the body.

> 
> Introduced sql_triggers field in space structure.
> Changed parser logic to do not insert built triggers, just only
> to do parsing. All triggers insertions and deletions are operated
> via on_replace_dd_trigger now.
> 
> Resolves #3273.
> ---
>   src/box/alter.cc                      |  37 ++++-
>   src/box/errcode.h                     |   2 +-
>   src/box/space.h                       |   2 +
>   src/box/sql.c                         |  48 ++----
>   src/box/sql.h                         |  44 ++++++
>   src/box/sql/build.c                   |   8 +-
>   src/box/sql/fkey.c                    |   1 -
>   src/box/sql/insert.c                  |   6 +-
>   src/box/sql/sqliteInt.h               |   6 +-
>   src/box/sql/tokenize.c                |  38 ++++-
>   src/box/sql/trigger.c                 | 287 +++++++++++++++++-----------------
>   src/box/sql/vdbe.c                    |  76 ++-------
>   src/box/sql/vdbe.h                    |   1 -
>   src/box/sql/vdbeaux.c                 |   9 --
>   test/sql-tap/identifier_case.test.lua |   4 +-
>   test/sql-tap/trigger1.test.lua        |   4 +-
>   test/sql/triggers.test.lua            |  72 +++++++++
>   17 files changed, 370 insertions(+), 275 deletions(-)
>   create mode 100644 test/sql/triggers.test.lua
> 
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index f2bf85d..bf170a5 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -551,6 +551,9 @@ space_swap_triggers(struct space *new_space, struct space *old_space)
>   	rlist_swap(&new_space->before_replace, &old_space->before_replace);
>   	rlist_swap(&new_space->on_replace, &old_space->on_replace);
>   	rlist_swap(&new_space->on_stmt_begin, &old_space->on_stmt_begin);
> +	/** Copy SQL Triggers pointer. */
> +	new_space->sql_triggers = old_space->sql_triggers;
> +	old_space->sql_triggers = NULL;

2. I just have noticed it is not actual swap. Here new_space->sql_triggers are
forgotten. It leads to bug on rollback, when swap_triggers_is_called again
to swap new/old back.

>   }
>   
>   /**
> @@ -732,7 +735,6 @@ alter_space_commit(struct trigger *trigger, void *event)
>   	}
>   
>   	trigger_run_xc(&on_alter_space, alter->new_space);
> -
>   	alter->new_space = NULL; /* for alter_space_delete(). */

3. Garbage diff.

>   	/*
>   	 * Delete the old version of the space, we are not
> @@ -3100,6 +3102,39 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
>   {
>   	struct txn *txn = (struct txn *) event;
>   	txn_check_singlestatement_xc(txn, "Space _trigger");
> +
> +	struct txn_stmt *stmt = txn_current_stmt(txn);
> +	if (stmt->old_tuple != NULL) {
> +		uint32_t trigger_name_len;
> +		const char *trigger_name_src =
> +			tuple_field_str_xc(stmt->old_tuple, 0,
> +					   &trigger_name_len);
> +		if (sql_trigger_delete_by_name(sql_get(), trigger_name_src,
> +					       trigger_name_len) != 0)
> +			diag_raise();
> +	}

4. You still have not fixed the point '3.' from the previous review.
If WAL write fails, you loss the deleted trigger, but must restore it back.

> +	if (stmt->new_tuple != NULL) {
> +		uint32_t trigger_name_len;
> +		const char *trigger_name_src =
> +			tuple_field_str_xc(stmt->new_tuple, 0,
> +					   &trigger_name_len);
> +		const char *space_opts =
> +			tuple_field_with_type_xc(stmt->new_tuple, 1, MP_MAP);
> +		struct space_opts opts;
> +		struct region *region = &fiber()->gc;
> +		space_opts_decode(&opts, space_opts, region);
> +		struct Trigger *trigger =
> +			sql_trigger_compile(sql_get(), opts.sql);
> +		if (trigger == NULL)
> +			diag_raise();
> +		auto trigger_guard = make_scoped_guard([=] {
> +		    sql_trigger_delete(sql_get(), trigger);
> +		});
> +		if (sql_trigger_insert(trigger, trigger_name_src,
> +				       trigger_name_len) != 0)
> +			diag_raise();
> +		trigger_guard.is_active = false;

5. Why do you need the guard here? You can delete trigger explicitly before
diag_raise() if insert() != 0.

> +	}
>   }
>   
> diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h
> index ecbd573..4c07480 100644
> --- a/src/box/sql/sqliteInt.h
> +++ b/src/box/sql/sqliteInt.h
> @@ -1935,7 +1935,6 @@ struct Table {
>   #ifndef SQLITE_OMIT_ALTERTABLE
>   	int addColOffset;	/* Offset in CREATE TABLE stmt to add a new column */
>   #endif
> -	Trigger *pTrigger;	/* List of triggers stored in pSchema */
>   	Schema *pSchema;	/* Schema that contains this table */
>   	Table *pNextZombie;	/* Next on the Parse.pZombieTab list */
>   	/** Space definition with Tarantool metadata. */
> @@ -3032,14 +3031,14 @@ struct Parse {
>    */
>   struct Trigger {
>   	char *zName;		/* The name of the trigger                        */
> -	char *table;		/* The table or view to which the trigger applies */
> +	/** The ID of space the triggers is refer to. */

6. triggers? There is one trigger. And 'is refer to' is not correct sentence. 'Refer'
is a verb.

> +	uint32_t space_id;
>   	u8 op;			/* One of TK_DELETE, TK_UPDATE, TK_INSERT         */
>   	u8 tr_tm;		/* One of TRIGGER_BEFORE, TRIGGER_AFTER */
>   	Expr *pWhen;		/* The WHEN clause of the expression (may be NULL) */
>   	IdList *pColumns;	/* If this is an UPDATE OF <column-list> trigger,
>   				   the <column-list> is stored here */
>   	Schema *pSchema;	/* Schema containing the trigger */
> -	Schema *pTabSchema;	/* Schema containing the table */
>   	TriggerStep *step_list;	/* Link list of trigger program steps             */
>   	Trigger *pNext;		/* Next trigger associated with the table */
>   };
> diff --git a/src/box/sql/tokenize.c b/src/box/sql/tokenize.c
> index 9b65064..0c851c9 100644
> --- a/src/box/sql/tokenize.c
> +++ b/src/box/sql/tokenize.c
> @@ -39,6 +39,7 @@
>   #include <stdlib.h>
>   #include <unicode/utf8.h>
>   #include <unicode/uchar.h>
> +#include "box/schema.h"
>   
>   #include "say.h"
>   #include "sqliteInt.h"
> @@ -122,6 +123,7 @@ static const char sql_ascii_class[] = {
>    * the #include below.
>    */
>   #include "keywordhash.h"
> +#include "tarantoolInt.h"
>   
>   #define maybe_utf8(c) ((sqlite3CtypeMap[c] & 0x40) != 0)
>   
> @@ -510,8 +512,13 @@ sqlite3RunParser(Parse * pParse, const char *zSql, char **pzErrMsg)
>   	}
>   	if (pParse->rc != SQLITE_OK && pParse->rc != SQLITE_DONE
>   	    && pParse->zErrMsg == 0) {
> -		pParse->zErrMsg =
> -		    sqlite3MPrintf(db, "%s", sqlite3ErrStr(pParse->rc));
> +		const char *error;
> +		if (is_tarantool_error(pParse->rc) && tarantoolErrorMessage()) {

7. != NULL

> +			error = tarantoolErrorMessage();
> +		} else {
> +			error = sqlite3ErrStr(pParse->rc);
> +		}

8. Do not use {} when both 'if' and 'else' are one lines.

> +		pParse->zErrMsg = sqlite3MPrintf(db, "%s", error);
>   	}
>   	assert(pzErrMsg != 0);
>   	if (pParse->zErrMsg) {
> @@ -528,7 +535,12 @@ sqlite3RunParser(Parse * pParse, const char *zSql, char **pzErrMsg)
>   
>   	if (pParse->pWithToFree)
>   		sqlite3WithDelete(db, pParse->pWithToFree);
> -	sql_trigger_delete(db, pParse->pNewTrigger);
> +	/*
> +	 * Trigger is exporting with pNewTrigger field when

9. is exported.

> +	 * parse_only flag is set.
> +	 */
> +	if (!pParse->parse_only)
> +		sql_trigger_delete(db, pParse->pNewTrigger);
>   	sqlite3DbFree(db, pParse->pVList);
>   	while (pParse->pZombieTab) {
>   		Table *p = pParse->pZombieTab;
> @@ -569,3 +581,23 @@ cleanup:
>   	sql_parser_destroy(&parser);
>   	return expression;
>   }
> +
> +struct Trigger *
> +sql_trigger_compile(struct sqlite3 *db, const char *sql)
> +{
> +	struct Parse parser;
> +	sql_parser_create(&parser, db);
> +	parser.parse_only = true;
> +	char *sql_error;
> +	struct Trigger *trigger = NULL;
> +	if (sqlite3RunParser(&parser, sql, &sql_error) != SQLITE_OK) {
> +		char *error = tt_static_buf();
> +		snprintf(error, TT_STATIC_BUF_LEN, "%s", sql_error);
> +		diag_set(ClientError, ER_SQL_EXECUTE, sql);

10. Same as in the previous patch: error and sql_error are unused.

> +		goto cleanup;
> +	}
> +	trigger = parser.pNewTrigger;
> +cleanup:

11. You should avoid 'goto cleanup' here.

> +	sql_parser_destroy(&parser);
> +	return trigger;
> +}
> diff --git a/src/box/sql/trigger.c b/src/box/sql/trigger.c
> index 053dadb..beaad11 100644
> --- a/src/box/sql/trigger.c
> +++ b/src/box/sql/trigger.c
> @@ -33,6 +33,8 @@
>    * This file contains the implementation for TRIGGERs
>    */
>   
> +#include "box/box.h"

12. Unused.

> +#include "box/schema.h"
>   #include "sqliteInt.h"
>   #include "tarantoolInt.h"
>   #include "vdbeInt.h"
> @@ -81,102 +83,121 @@ sqlite3BeginTrigger(Parse * pParse,	/* The parse context of the CREATE TRIGGER s
>       )
>   {
>   	Trigger *pTrigger = 0;	/* The new trigger */
> -	Table *pTab;		/* Table that the trigger fires off of */
> +	/* Table that the trigger fires off of. */
> +	struct Table *table = NULL;
>   	char *zName = 0;	/* Name of the trigger */
>   	sqlite3 *db = pParse->db;	/* The database connection */
>   	DbFixer sFix;		/* State vector for the DB fixer */
>   
> -	/* Do not account nested operations: the count of such
> -	 * operations depends on Tarantool data dictionary internals,
> -	 * such as data layout in system spaces.
> +	/*
> +	 * Do not account nested operations: the count of such
> +	 * operations depends on Tarantool data dictionary
> +	 * internals, such as data layout in system spaces.
>   	 */
>   	if (!pParse->nested) {
>   		Vdbe *v = sqlite3GetVdbe(pParse);
>   		if (v != NULL)
>   			sqlite3VdbeCountChanges(v);
>   	}
> -	assert(pName != 0);	/* pName->z might be NULL, but not pName itself */
> +	/* pName->z might be NULL, but not pName itself. */
> +	assert(pName != 0);
>   	assert(op == TK_INSERT || op == TK_UPDATE || op == TK_DELETE);
>   	assert(op > 0 && op < 0xff);
>   
> -	if (!pTableName || db->mallocFailed) {
> +	if (!pTableName || db->mallocFailed)
>   		goto trigger_cleanup;
> -	}
>   
> -	/* Ensure the table name matches database name and that the table exists */
> +	/*
> +	 * Ensure the table name matches database name and that
> +	 * the table exists.
> +	 */
>   	if (db->mallocFailed)
>   		goto trigger_cleanup;
>   	assert(pTableName->nSrc == 1);
>   	sqlite3FixInit(&sFix, pParse, "trigger", pName);
> -	if (sqlite3FixSrcList(&sFix, pTableName)) {
> +	if (sqlite3FixSrcList(&sFix, pTableName))

13. != 0

>   
> -	/* INSTEAD OF triggers can only appear on views and BEFORE triggers
> +	/*
> +	 * INSTEAD OF triggers can only appear on views and BEFORE triggers
>   	 * cannot appear on views.  So we might as well translate every
>   	 * INSTEAD OF trigger into a BEFORE trigger.  It simplifies code
>   	 * elsewhere.
>   	 */
> -	if (tr_tm == TK_INSTEAD) {
> +	if (tr_tm == TK_INSTEAD)
>   		tr_tm = TK_BEFORE;
> -	}
>   
> -	/* Build the Trigger object */
> +	/* Build the Trigger object. */
>   	pTrigger = (Trigger *) sqlite3DbMallocZero(db, sizeof(Trigger));
>   	if (pTrigger == 0)
>   		goto trigger_cleanup;
>   	pTrigger->zName = zName;
>   	zName = 0;
> -	pTrigger->table = sqlite3DbStrDup(db, pTableName->a[0].zName);
> +	/* Initialize space_id on trigger insert. */
> +	if (schema_find_id(BOX_SPACE_ID, 2, pTableName->a[0].zName,
> +			strlen(pTableName->a[0].zName),

14. Wrong indentation.

> +			   &pTrigger->space_id) != 0) {
> +		pParse->rc = SQL_TARANTOOL_ERROR;
> +		goto trigger_cleanup;
> +	}
>   	pTrigger->pSchema = db->pSchema;
> -	pTrigger->pTabSchema = pTab->pSchema;
>   	pTrigger->op = (u8) op;
>   	pTrigger->tr_tm = tr_tm == TK_BEFORE ? TRIGGER_BEFORE : TRIGGER_AFTER;
>   	pTrigger->pWhen = sqlite3ExprDup(db, pWhen, EXPRDUP_REDUCE);
>   	pTrigger->pColumns = sqlite3IdListDup(db, pColumns);
> +	if ((pWhen != NULL && pTrigger->pWhen == NULL) ||
> +		(pColumns != NULL && pTrigger->pColumns == NULL))

15. Same.

> +		goto trigger_cleanup;
>   	assert(pParse->pNewTrigger == 0);
>   	pParse->pNewTrigger = pTrigger;
>   
> @@ -227,10 +248,11 @@ sqlite3FinishTrigger(Parse * pParse,	/* Parser context */
>   		goto triggerfinish_cleanup;
>   	}
>   
> -	/* if we are not initializing,
> -	 * generate byte code to insert a new trigger into Tarantool.
> +	/*
> +	 * Generate byte code to insert a new trigger into
> +	 * Tarantool for non-parsig mode or export trigger.

16. typo

>   	 */
> -	if (!db->init.busy) {
> +	if (!pParse->parse_only) {
>   		Vdbe *v;
>   		int zOptsSz;
>   		Table *pSysTrigger;
> @@ -565,34 +550,68 @@ sqlite3DropTriggerPtr(Parse * pParse, Trigger * pTrigger)
>   			sqlite3VdbeChangeP5(v, OPFLAG_NCHANGE);
>   
>   		sqlite3ChangeCookie(pParse);
> -		sqlite3VdbeAddOp4(v, OP_DropTrigger, 0, 0, 0, pTrigger->zName,
> -				  0);
>   	}
>   }
>   
> -/*
> - * Remove a trigger from the hash tables of the sqlite* pointer.
> - */
> -void
> -sqlite3UnlinkAndDeleteTrigger(sqlite3 * db, const char *zName)
> +int
> +sql_trigger_delete_by_name(struct sqlite3 *db, const char *name, int name_len)
>   {
> -	Trigger *pTrigger;
> -	Hash *pHash;
> -	struct session *user_session = current_session();
> +	/* Name from Tarantool tuple may be not normalized. */
> +	uint32_t used = region_used(&fiber()->gc);
> +	char *trigger_name = region_alloc(&fiber()->gc, name_len + 3);

17. Why +3?

> +	if (trigger_name == NULL) {
> +		diag_set(OutOfMemory, name_len + 3, "region_alloc",
> +			 "trigger_name");
> +		return -1;
> +	}
> +	memcpy(trigger_name, name, name_len);
> +	trigger_name[name_len] = 0;
> +
> +	struct Hash *hash = &(db->pSchema->trigHash);
> +	struct Trigger *trigger = sqlite3HashInsert(hash, trigger_name, NULL);
> +	assert(trigger != NULL);

18. Why != NULL? Can't sqlite3HashInsert fail?

> +
> +	struct space *space = space_by_id(trigger->space_id);
> +	/* Space could be already deleted. */
> +	if (space != NULL) {
> +		struct Trigger **pp;
> +		for (pp = &space->sql_triggers;
> +		     *pp != trigger;
> +		     pp = &((*pp)->pNext));

19. Can't it fit in a single line?

> +		*pp = (*pp)->pNext;
> +	}
>   


> +int
> +sql_trigger_insert(struct Trigger *trigger, const char *name, int name_len)
> +{
> +	if (strncmp(name, trigger->zName, name_len) != 0) {
> +		diag_set(ClientError, ER_SQL,
> +			 "tuple trigger name does not match extracted from SQL");

20. There are no tuples. Please, do this check in alter.cc. For this you can
declare trigger_name() helper.

> +		return -1;
> +	}
> +
> +	struct space *space = space_cache_find(trigger->space_id);
> +	assert(space != NULL);
> +	struct Hash *hash = &trigger->pSchema->trigHash;
> +	if (sqlite3HashFind(hash, trigger->zName) != NULL) {
> +		diag_set(ClientError, ER_TRIGGER_EXISTS, trigger->zName);> +		return -1;
> +	}
> +
>   /*
> @@ -631,22 +650,17 @@ sqlite3TriggersExist(Table * pTab,	/* The table the contains the triggers */
>       )
>   {
>   	int mask = 0;
> -	Trigger *pList = 0;
> -	Trigger *p;
> +	struct Trigger *trigger_list = NULL;
>   	struct session *user_session = current_session();
> -
> -	if ((user_session->sql_flags & SQLITE_EnableTrigger) != 0) {
> -		pList = pTab->pTrigger;
> -	}
> -	for (p = pList; p; p = p->pNext) {
> -		if (p->op == op && checkColumnOverlap(p->pColumns, pChanges)) {
> +	if ((user_session->sql_flags & SQLITE_EnableTrigger) != 0)
> +		trigger_list = space_trigger_list(pTab->def->id);
> +	for (struct Trigger *p = trigger_list; p; p = p->pNext) {
> +		if (p->op == op && checkColumnOverlap(p->pColumns, pChanges))
>   			mask |= p->tr_tm;
> -		}
>   	}
> -	if (pMask) {
> +	if (pMask)

21. != NULL

>   		*pMask = mask;
> -	}
> -	return (mask ? pList : 0);
> +	return mask ? trigger_list : 0;
>   }
>   
>   /*
> diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
> index 3fe5875..bba9bf1 100644
> --- a/src/box/sql/vdbe.c
> +++ b/src/box/sql/vdbe.c
> @@ -4799,19 +4757,21 @@ case OP_RenameTable: {
>   		goto abort_due_to_error;
>   	}
>   
> -	pTab = sqlite3HashFind(&db->pSchema->tblHash, zNewTableName);
> -	pTab->pTrigger = pTrig;
> +	space = space_by_id(space_id);
> +	assert(space != NULL);
>   
> -	/* Rename all trigger created on this table.*/
> -	for (; pTrig; pTrig = pTrig->pNext) {
> -		sqlite3DbFree(db, pTrig->table);
> -		pTrig->table = sqlite3DbStrNDup(db, zNewTableName,
> -						sqlite3Strlen30(zNewTableName));
> -		pTrig->pTabSchema = pTab->pSchema;
> -		rc = tarantoolSqlite3RenameTrigger(pTrig->zName,
> +	/* Rename all trigger created on this table. */

22. triggers

> +	for (struct Trigger *trigger = space->sql_triggers; trigger != NULL; ) {
> +		struct Trigger *next_trigger = trigger->pNext;
> +		rc = tarantoolSqlite3RenameTrigger(trigger->zName,
>   						   zOldTableName, zNewTableName);
> -		if (rc) goto abort_due_to_error;
> +		if (rc != SQLITE_OK) {
> +			sqlite3ResetAllSchemasOfConnection(db);
> +			goto abort_due_to_error;
> +		}
> +		trigger = next_trigger;
>   	}
> +
>   	sqlite3DbFree(db, (void*)zOldTableName);
>   	sqlite3DbFree(db, (void*)zSqlStmt);
>   	break;

  reply	other threads:[~2018-06-04 13:27 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-05-31 11:22 [tarantool-patches] [PATCH v1 0/4] sql: remove " Kirill Shcherbatov
2018-05-31 11:22 ` [tarantool-patches] [PATCH v1 1/4] box: move db->pShchema init to sql_init Kirill Shcherbatov
2018-05-31 17:36   ` [tarantool-patches] " Vladislav Shpilevoy
2018-06-01 20:24     ` Kirill Shcherbatov
2018-05-31 11:22 ` [tarantool-patches] [PATCH v1 2/4] sql: fix sql len in tarantoolSqlite3RenameTrigger Kirill Shcherbatov
2018-05-31 11:22 ` [tarantool-patches] [PATCH v1 3/4] box: introduce box_space_id_by_name Kirill Shcherbatov
2018-05-31 17:36   ` [tarantool-patches] " Vladislav Shpilevoy
2018-06-01 20:24     ` Kirill Shcherbatov
2018-06-04 13:27       ` Vladislav Shpilevoy
2018-06-04 19:21         ` Kirill Shcherbatov
2018-06-05 13:31           ` Vladislav Shpilevoy
2018-05-31 11:22 ` [tarantool-patches] [PATCH v1 4/4] sql: move Triggers to server Kirill Shcherbatov
2018-05-31 17:36   ` [tarantool-patches] " Vladislav Shpilevoy
2018-06-01 20:24     ` Kirill Shcherbatov
2018-06-01 20:25       ` Kirill Shcherbatov
2018-06-04 13:27         ` Vladislav Shpilevoy [this message]
2018-06-04 19:21           ` Kirill Shcherbatov
2018-06-05 13:31             ` Vladislav Shpilevoy
2018-06-09  9:32               ` Kirill Shcherbatov
2018-06-01 18:51   ` Konstantin Osipov
2018-05-31 17:36 ` [tarantool-patches] Re: [PATCH v1 0/4] sql: remove " Vladislav Shpilevoy
2018-06-04 13:27 ` Vladislav Shpilevoy
2018-06-05 13:31 ` Vladislav Shpilevoy

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=4816bb71-cf0a-f508-6709-d2a526c532b9@tarantool.org \
    --to=v.shpilevoy@tarantool.org \
    --cc=kshcherbatov@tarantool.org \
    --cc=tarantool-patches@freelists.org \
    --subject='[tarantool-patches] Re: [PATCH v1 4/4] sql: move Triggers to server' \
    /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