[Tarantool-patches] [PATCH v1 9/9] schema: rework _trigger system space

Kirill Shcherbatov kshcherbatov at tarantool.org
Mon Oct 14 19:03:24 MSK 2019


This patch reworks a _trigger system space to make it
useful not only for sql triggers definitions.

The format of the updated system space is

_trigger (space id = 328)
[<name> STRING, <space_id> UINT32, <opts> MAP, <language> STR,
 <type> STR, <event_manipulation> STR,
 <action_timing> STR, <code> STR]

After insertion into this space, a new instance describing trigger
object is created using language-dependent constructor.

This volumerous refactoring is an initial step for further
introduction of persistent triggers in Lua.

Needed for #4343
---
 src/box/CMakeLists.txt                        |   1 +
 src/box/alter.cc                              | 201 +++++++++++-------
 src/box/bootstrap.snap                        | Bin 5934 -> 5981 bytes
 src/box/errcode.h                             |   1 +
 src/box/fk_constraint.c                       |   6 +-
 src/box/lua/upgrade.lua                       |  16 ++
 src/box/schema_def.h                          |   5 +
 src/box/sql.c                                 |  75 +++----
 src/box/sql.h                                 |  51 +----
 src/box/sql/build.c                           |  37 ++--
 src/box/sql/fk_constraint.c                   |   5 +-
 src/box/sql/prepare.c                         |   7 +-
 src/box/sql/sqlInt.h                          |   4 +-
 src/box/sql/tokenize.c                        |  16 +-
 src/box/sql/trigger.c                         | 149 +++----------
 src/box/trigger.c                             |  73 +++++++
 src/box/trigger.h                             |  21 ++
 src/box/trigger_def.c                         |  69 +++++-
 src/box/trigger_def.h                         |  32 ++-
 test/box-py/bootstrap.result                  |   5 +-
 test/box/access_misc.result                   | 139 ++++++------
 test/box/misc.result                          |   1 +
 test/sql/ddl.result                           |   6 +-
 test/sql/ddl.test.lua                         |   6 +-
 .../gh2141-delete-trigger-drop-table.result   |  36 ++--
 .../gh2141-delete-trigger-drop-table.test.lua |   4 +-
 test/sql/persistency.result                   |  36 ++--
 test/sql/persistency.test.lua                 |   8 +-
 test/sql/triggers.result                      |  91 +++-----
 test/sql/triggers.test.lua                    |  30 +--
 test/sql/upgrade.result                       |  16 +-
 test/sql/upgrade.test.lua                     |   4 +-
 32 files changed, 618 insertions(+), 533 deletions(-)
 create mode 100644 src/box/trigger.c

diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 2ff5cf01e..fb6279dcd 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -102,6 +102,7 @@ add_library(box STATIC
     fk_constraint.c
     func.c
     func_def.c
+    trigger.c
     trigger_def.c
     key_list.c
     alter.cc
diff --git a/src/box/alter.cc b/src/box/alter.cc
index 7a339c4d3..e4e7f910c 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -3957,30 +3957,27 @@ on_replace_dd_space_sequence(struct lua_trigger * /* trigger */, void *event)
 static void
 on_create_trigger_rollback(struct lua_trigger *trigger, void * /* event */)
 {
-	struct sql_trigger *old_trigger = (struct sql_trigger *)trigger->data;
-	struct sql_trigger *new_trigger;
-	int rc = sql_trigger_replace(sql_trigger_name(old_trigger),
-				     sql_trigger_space_id(old_trigger),
-				     NULL, &new_trigger);
-	(void)rc;
-	assert(rc == 0);
-	assert(new_trigger == old_trigger);
-	sql_trigger_delete(new_trigger);
+	struct trigger *new_trigger = (struct trigger *) trigger->data;
+	struct space *space = space_by_id(new_trigger->def->space_id);
+	assert(space != NULL);
+	assert(space_trigger_by_name(space, new_trigger->def->name,
+				new_trigger->def->name_len) == new_trigger);
+	rlist_del(&new_trigger->link);
+	trigger_delete(new_trigger);
 }
 
 /** Restore the old trigger on rollback of a DELETE statement. */
 static void
 on_drop_trigger_rollback(struct lua_trigger *trigger, void * /* event */)
 {
-	struct sql_trigger *old_trigger = (struct sql_trigger *)trigger->data;
-	struct sql_trigger *new_trigger;
+	struct trigger *old_trigger = (struct trigger *) trigger->data;
 	if (old_trigger == NULL)
 		return;
-	if (sql_trigger_replace(sql_trigger_name(old_trigger),
-				sql_trigger_space_id(old_trigger),
-				old_trigger, &new_trigger) != 0)
-		panic("Out of memory on insertion into trigger hash");
-	assert(new_trigger == NULL);
+	struct space *space = space_by_id(old_trigger->def->space_id);
+	assert(space != NULL);
+	assert(space_trigger_by_name(space, old_trigger->def->name,
+				     old_trigger->def->name_len) == NULL);
+	rlist_add(&space->trigger_list, &old_trigger->link);
 }
 
 /**
@@ -3990,13 +3987,18 @@ on_drop_trigger_rollback(struct lua_trigger *trigger, void * /* event */)
 static void
 on_replace_trigger_rollback(struct lua_trigger *trigger, void * /* event */)
 {
-	struct sql_trigger *old_trigger = (struct sql_trigger *)trigger->data;
-	struct sql_trigger *new_trigger;
-	if (sql_trigger_replace(sql_trigger_name(old_trigger),
-				sql_trigger_space_id(old_trigger),
-				old_trigger, &new_trigger) != 0)
-		panic("Out of memory on insertion into trigger hash");
-	sql_trigger_delete(new_trigger);
+	struct trigger *old_trigger = (struct trigger *) trigger->data;
+	struct space *space = space_by_id(old_trigger->def->space_id);
+	assert(space != NULL);
+	struct trigger *new_trigger =
+		space_trigger_by_name(space, old_trigger->def->name,
+				      old_trigger->def->name_len);
+	assert(new_trigger != NULL);
+	rlist_del(&new_trigger->link);
+	rlist_add(&space->trigger_list, &old_trigger->link);
+	assert(space_trigger_by_name(space, old_trigger->def->name,
+				old_trigger->def->name_len) == old_trigger);
+	trigger_delete(new_trigger);
 }
 
 /**
@@ -4006,8 +4008,72 @@ on_replace_trigger_rollback(struct lua_trigger *trigger, void * /* event */)
 static void
 on_replace_trigger_commit(struct lua_trigger *trigger, void * /* event */)
 {
-	struct sql_trigger *old_trigger = (struct sql_trigger *)trigger->data;
-	sql_trigger_delete(old_trigger);
+	struct trigger *old_trigger = (struct trigger *) trigger->data;
+	if (old_trigger != NULL)
+		trigger_delete(old_trigger);
+}
+
+/** Create a trigger definition from tuple. */
+static struct trigger_def *
+trigger_def_new_from_tuple(struct tuple *tuple)
+{
+	uint32_t name_len;
+	const char *name = tuple_field_str_xc(tuple, BOX_TRIGGER_FIELD_NAME,
+					      &name_len);
+	uint32_t space_id =
+		tuple_field_u32_xc(tuple, BOX_TRIGGER_FIELD_SPACE_ID);
+	const char *language_str =
+		tuple_field_cstr_xc(tuple, BOX_TRIGGER_FIELD_LANGUAGE);
+	enum trigger_language language = STR2ENUM(trigger_language,
+						  language_str);
+	if (language == trigger_language_MAX) {
+		tnt_raise(ClientError, ER_CREATE_TRIGGER,
+			  tt_cstr(name, name_len), "invalid language");
+	}
+	const char *type_str = tuple_field_cstr_xc(tuple,
+						   BOX_TRIGGER_FIELD_TYPE);
+	enum trigger_type type = STR2ENUM(trigger_type, type_str);
+	if (type == trigger_type_MAX) {
+		tnt_raise(ClientError, ER_CREATE_TRIGGER,
+			  tt_cstr(name, name_len), "invalid type");
+	}
+	const char *event_manipulation_str =
+		tuple_field_cstr_xc(tuple, BOX_TRIGGER_FIELD_EVENT_MANIPULATION);
+	enum trigger_event_manipulation event_manipulation =
+		STR2ENUM(trigger_event_manipulation, event_manipulation_str);
+	if (event_manipulation == trigger_event_manipulation_MAX) {
+		tnt_raise(ClientError, ER_CREATE_TRIGGER,
+			  tt_cstr(name, name_len),
+			  "invalid event_manipulation value");
+	}
+	const char *action_timing_str =
+		tuple_field_cstr_xc(tuple, BOX_TRIGGER_FIELD_ACTION_TIMING);
+	enum trigger_action_timing action_timing =
+		STR2ENUM(trigger_action_timing, action_timing_str);
+	if (action_timing == trigger_action_timing_MAX) {
+		tnt_raise(ClientError, ER_CREATE_TRIGGER,
+			  tt_cstr(name, name_len),
+			  "invalid action_timing value");
+	}
+	uint32_t code_len;
+	const char *code = tuple_field_str_xc(tuple, BOX_TRIGGER_FIELD_CODE,
+					      &code_len);
+
+	struct trigger_def *def =
+		trigger_def_new(name, name_len, space_id, language,
+				event_manipulation, action_timing,
+				code, code_len);
+	if (def == NULL)
+		diag_raise();
+	if (trigger_def_check(def) != 0) {
+		trigger_def_delete(def);
+		diag_raise();
+	}
+	assert(def->action_timing != TRIGGER_ACTION_TIMING_INSTEAD ||
+	       def->language == TRIGGER_LANGUAGE_SQL);
+	if (def->action_timing == TRIGGER_ACTION_TIMING_INSTEAD)
+		def->action_timing = TRIGGER_ACTION_TIMING_BEFORE;
+	return def;
 }
 
 /**
@@ -4021,6 +4087,12 @@ on_replace_dd_trigger(struct lua_trigger * /* trigger */, void *event)
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *old_tuple = stmt->old_tuple;
 	struct tuple *new_tuple = stmt->new_tuple;
+	/* Ignore legacy tuples. */
+	if ((old_tuple != NULL &&
+	     tuple_field_count(old_tuple) <= BOX_TRIGGER_FIELD_CODE) ||
+	    (new_tuple != NULL &&
+	     tuple_field_count(new_tuple) <= BOX_TRIGGER_FIELD_CODE))
+		return;
 
 	struct lua_trigger *on_rollback = txn_alter_trigger_new(NULL, NULL);
 	struct lua_trigger *on_commit =
@@ -4028,73 +4100,42 @@ on_replace_dd_trigger(struct lua_trigger * /* trigger */, void *event)
 
 	if (old_tuple != NULL && new_tuple == NULL) {
 		/* DROP trigger. */
-		uint32_t trigger_name_len;
-		const char *trigger_name_src =
-			tuple_field_str_xc(old_tuple, BOX_TRIGGER_FIELD_NAME,
-					   &trigger_name_len);
-		uint32_t space_id =
-			tuple_field_u32_xc(old_tuple,
-					   BOX_TRIGGER_FIELD_SPACE_ID);
-		char *trigger_name =
-			(char *)region_alloc_xc(&fiber()->gc,
-						trigger_name_len + 1);
-		memcpy(trigger_name, trigger_name_src, trigger_name_len);
-		trigger_name[trigger_name_len] = 0;
-
-		struct sql_trigger *old_trigger;
-		int rc = sql_trigger_replace(trigger_name, space_id, NULL,
-					     &old_trigger);
-		(void)rc;
-		assert(rc == 0);
+		struct trigger_def *def = trigger_def_new_from_tuple(old_tuple);
+		auto trigger_def_guard = make_scoped_guard([=] {
+			trigger_def_delete(def);
+		});
+		struct space *space = space_by_id(def->space_id);
+		assert(space != NULL);
+		struct trigger *old_trigger =
+			space_trigger_by_name(space, def->name, def->name_len);
+		assert(old_trigger != NULL);
+		rlist_del(&old_trigger->link);
 
 		on_commit->data = old_trigger;
 		on_rollback->data = old_trigger;
 		on_rollback->run = on_drop_trigger_rollback;
 	} else {
 		/* INSERT, REPLACE trigger. */
-		uint32_t trigger_name_len;
-		const char *trigger_name_src =
-			tuple_field_str_xc(new_tuple, BOX_TRIGGER_FIELD_NAME,
-					   &trigger_name_len);
-
-		const char *space_opts =
-			tuple_field_with_type_xc(new_tuple,
-						 BOX_TRIGGER_FIELD_OPTS,
-						 MP_MAP);
-		struct space_opts opts;
-		struct region *region = &fiber()->gc;
-		space_opts_decode(&opts, space_opts, region);
-		struct sql_trigger *new_trigger =
-			sql_trigger_compile(sql_get(), opts.sql);
+		struct trigger_def *def = trigger_def_new_from_tuple(new_tuple);
+		auto trigger_def_guard = make_scoped_guard([=] {
+			trigger_def_delete(def);
+		});
+		struct trigger *new_trigger = trigger_new(def);
 		if (new_trigger == NULL)
 			diag_raise();
-
 		auto new_trigger_guard = make_scoped_guard([=] {
-		    sql_trigger_delete(new_trigger);
+			trigger_delete(new_trigger);
 		});
+		trigger_def_guard.is_active = false;
 
-		const char *trigger_name = sql_trigger_name(new_trigger);
-		if (strlen(trigger_name) != trigger_name_len ||
-		    memcmp(trigger_name_src, trigger_name,
-			   trigger_name_len) != 0) {
-			tnt_raise(ClientError, ER_SQL_EXECUTE,
-				  "trigger name does not match extracted "
-				  "from SQL");
-		}
-		uint32_t space_id =
-			tuple_field_u32_xc(new_tuple,
-					   BOX_TRIGGER_FIELD_SPACE_ID);
-		if (space_id != sql_trigger_space_id(new_trigger)) {
-			tnt_raise(ClientError, ER_SQL_EXECUTE,
-				  "trigger space_id does not match the value "
-				  "resolved on AST building from SQL");
-		}
+		struct space *space = space_by_id(def->space_id);
+		assert(space != NULL);
+		struct trigger *old_trigger =
+			space_trigger_by_name(space, def->name, def->name_len);
 
-		struct sql_trigger *old_trigger;
-		if (sql_trigger_replace(trigger_name,
-					sql_trigger_space_id(new_trigger),
-					new_trigger, &old_trigger) != 0)
-			diag_raise();
+		if (old_trigger != NULL)
+			rlist_del(&old_trigger->link);
+		rlist_add(&space->trigger_list, &new_trigger->link);
 
 		on_commit->data = old_trigger;
 		if (old_tuple != NULL) {
diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index 4c9aea7f50f8ac86be32ca9f126fea9a3d2d182f..4c094e0e48d03110112fbd531b8523f40c6dc3fd 100644
GIT binary patch
literal 5981

diff --git a/src/box/errcode.h b/src/box/errcode.h
index d5d396d87..3d318d7e0 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -256,6 +256,7 @@ struct errcode_record {
 	/*201 */_(ER_NO_SUCH_FIELD_NAME,	"Field '%s' was not found in the tuple") \
 	/*202 */_(ER_FUNC_WRONG_ARG_COUNT,	"Wrong number of arguments is passed to %s(): expected %s, got %d") \
 	/*203 */_(ER_BOOTSTRAP_READONLY,	"Trying to bootstrap a local read-only instance as master") \
+	/*204 */_(ER_CREATE_TRIGGER,		"Failed to create trigger '%s': %s") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/fk_constraint.c b/src/box/fk_constraint.c
index 5b0060ff5..28236e9b0 100644
--- a/src/box/fk_constraint.c
+++ b/src/box/fk_constraint.c
@@ -49,8 +49,10 @@ const char *fk_constraint_match_strs[] = {
 void
 fk_constraint_delete(struct fk_constraint *fk)
 {
-	sql_trigger_delete(fk->on_delete_trigger);
-	sql_trigger_delete(fk->on_update_trigger);
+	if (fk->on_delete_trigger != NULL)
+		trigger_delete((struct trigger *)fk->on_delete_trigger);
+	if (fk->on_update_trigger != NULL)
+		trigger_delete((struct trigger *)fk->on_update_trigger);
 	free(fk->def);
 	free(fk);
 }
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index 2abd75dff..f4cfd617f 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -927,6 +927,22 @@ local function upgrade_to_2_3_0()
                                         datetime, datetime})
         _priv:replace{ADMIN, PUBLIC, 'function', t.id, box.priv.X}
     end
+
+    log.info("Migrate SQL Triggers")
+    local _trigger = box.space[box.schema.TRIGGER_ID]
+    for _, v in _trigger:pairs() do
+        _trigger:delete(v.name)
+        box.execute(v.opts.sql)
+    end
+    local format = {{name='name', type='string'},
+                    {name='space_id', type='unsigned'},
+                    {name='opts', type='map'},
+                    {name='language', type='string'},
+                    {name='type', type='string'},
+                    {name='event_manipulation', type='string'},
+                    {name='action_timing', type='string'},
+                    {name='code', type='string'}}
+    _trigger:format(format)
 end
 
 --------------------------------------------------------------------------------
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 85f652d52..2786688cc 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -245,6 +245,11 @@ enum {
 	BOX_TRIGGER_FIELD_NAME = 0,
 	BOX_TRIGGER_FIELD_SPACE_ID = 1,
 	BOX_TRIGGER_FIELD_OPTS = 2,
+	BOX_TRIGGER_FIELD_LANGUAGE = 3,
+	BOX_TRIGGER_FIELD_TYPE = 4,
+	BOX_TRIGGER_FIELD_EVENT_MANIPULATION = 5,
+	BOX_TRIGGER_FIELD_ACTION_TIMING = 6,
+	BOX_TRIGGER_FIELD_CODE = 7,
 };
 
 /** _fk_constraint fields. */
diff --git a/src/box/sql.c b/src/box/sql.c
index 134225dcc..140da5e46 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -580,8 +580,11 @@ int tarantoolsqlRenameTrigger(const char *trig_name,
 	uint32_t trig_name_len = strlen(trig_name);
 	uint32_t old_table_name_len = strlen(old_table_name);
 	uint32_t new_table_name_len = strlen(new_table_name);
+
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
 	uint32_t key_len = mp_sizeof_str(trig_name_len) + mp_sizeof_array(1);
-	char *key_begin = (char*) region_alloc(&fiber()->gc, key_len);
+	char *key_begin = (char*) region_alloc(region, key_len);
 	if (key_begin == NULL) {
 		diag_set(OutOfMemory, key_len, "region_alloc", "key_begin");
 		return -1;
@@ -591,54 +594,44 @@ int tarantoolsqlRenameTrigger(const char *trig_name,
 	if (box_index_get(BOX_TRIGGER_ID, 0, key_begin, key, &tuple) != 0)
 		return -1;
 	assert(tuple != NULL);
-	assert(tuple_field_count(tuple) == 3);
-	const char *field = tuple_field(tuple, BOX_TRIGGER_FIELD_SPACE_ID);
-	assert(mp_typeof(*field) == MP_UINT);
-	uint32_t space_id = mp_decode_uint(&field);
-	field = tuple_field(tuple, BOX_TRIGGER_FIELD_OPTS);
-	assert(mp_typeof(*field) == MP_MAP);
-	mp_decode_map(&field);
-	const char *sql_str = mp_decode_str(&field, &key_len);
-	if (sqlStrNICmp(sql_str, "sql", 3) != 0) {
-		diag_set(ClientError, ER_SQL_EXECUTE, "can't modify name of "\
-			 "space created not via SQL facilities");
-		return -1;
-	}
-	uint32_t trigger_stmt_len;
-	const char *trigger_stmt_old = mp_decode_str(&field, &trigger_stmt_len);
-	char *trigger_stmt = (char*)region_alloc(&fiber()->gc,
-						 trigger_stmt_len + 1);
-	if (trigger_stmt == NULL) {
-		diag_set(OutOfMemory, trigger_stmt_len + 1, "region_alloc",
+	assert(tuple_field_count(tuple) == 8);
+
+	uint32_t code_str_len;
+	const char *code_str = tuple_field_str(tuple, BOX_TRIGGER_FIELD_CODE,
+					       &code_str_len);
+	assert(code_str != NULL);
+	char *old_stmt = (char *) region_alloc(region, code_str_len + 1);
+	if (old_stmt == NULL) {
+		diag_set(OutOfMemory, code_str_len + 1, "region_alloc",
 			 "trigger_stmt");
 		return -1;
 	}
-	memcpy(trigger_stmt, trigger_stmt_old, trigger_stmt_len);
-	trigger_stmt[trigger_stmt_len] = '\0';
+	memcpy(old_stmt, code_str, code_str_len);
+	old_stmt[code_str_len] = '\0';
+
 	bool is_quoted = false;
-	trigger_stmt = rename_trigger(db, trigger_stmt, new_table_name, &is_quoted);
-
-	uint32_t trigger_stmt_new_len = trigger_stmt_len + new_table_name_len -
-					old_table_name_len + 2 * (!is_quoted);
-	assert(trigger_stmt_new_len > 0);
-	key_len = mp_sizeof_array(3) + mp_sizeof_str(trig_name_len) +
-		  mp_sizeof_map(1) + mp_sizeof_str(3) +
-		  mp_sizeof_str(trigger_stmt_new_len) +
-		  mp_sizeof_uint(space_id);
-	char *new_tuple = (char*)region_alloc(&fiber()->gc, key_len);
+	char *stmt = rename_trigger(db, old_stmt, new_table_name, &is_quoted);
+	uint32_t stmt_len = code_str_len + new_table_name_len -
+			    old_table_name_len + 2 * (!is_quoted);
+	uint32_t prefix_sz = code_str - tuple_data(tuple) -
+			     mp_sizeof_strl(code_str_len);
+	uint32_t new_tuple_sz = prefix_sz + mp_sizeof_str(stmt_len);
+	char *new_tuple = (char *) region_alloc(region, new_tuple_sz);
 	if (new_tuple == NULL) {
-		diag_set(OutOfMemory, key_len, "region_alloc", "new_tuple");
+		sqlDbFree(db, stmt);
+		region_truncate(region, region_svp);
+		diag_set(OutOfMemory, new_tuple_sz, "region_alloc",
+			 "new_tuple");
 		return -1;
 	}
-	char *new_tuple_end = mp_encode_array(new_tuple, 3);
-	new_tuple_end = mp_encode_str(new_tuple_end, trig_name, trig_name_len);
-	new_tuple_end = mp_encode_uint(new_tuple_end, space_id);
-	new_tuple_end = mp_encode_map(new_tuple_end, 1);
-	new_tuple_end = mp_encode_str(new_tuple_end, "sql", 3);
-	new_tuple_end = mp_encode_str(new_tuple_end, trigger_stmt,
-				      trigger_stmt_new_len);
+	memcpy(new_tuple, tuple_data(tuple), prefix_sz);
+	char *new_tuple_end = mp_encode_str(new_tuple + prefix_sz,
+					    stmt, stmt_len);
+	sqlDbFree(db, stmt);
 
-	return box_replace(BOX_TRIGGER_ID, new_tuple, new_tuple_end, NULL);
+	int rc = box_replace(BOX_TRIGGER_ID, new_tuple, new_tuple_end, NULL);
+	region_truncate(region, region_svp);
+	return rc;
 }
 
 int
diff --git a/src/box/sql.h b/src/box/sql.h
index 926f4ba19..8693c249d 100644
--- a/src/box/sql.h
+++ b/src/box/sql.h
@@ -139,55 +139,8 @@ sql_trigger_new(struct trigger_def *def, struct sql_trigger_expr *expr,
  * @retval NULL on error
  * @retval not NULL sql_trigger AST pointer on success.
  */
-struct sql_trigger *
-sql_trigger_compile(struct sql *db, const char *sql);
-
-/**
- * Free AST pointed by trigger.
- * @param trigger AST object.
- */
-void
-sql_trigger_delete(struct sql_trigger *trigger);
-
-/**
- * Get server triggers list by space_id.
- * @param space_id valid Space ID.
- *
- * @retval trigger AST list.
- */
-struct sql_trigger *
-space_trigger_list(uint32_t space_id);
-
-/**
- * Perform replace trigger in SQL internals with new AST object.
- * @param name a name of the trigger.
- * @param space_id of the space to insert trigger.
- * @param trigger AST object to insert.
- * @param[out] old_trigger Old object if exists.
- *
- * @retval 0 on success.
- * @retval -1 on error.
- */
-int
-sql_trigger_replace(const char *name, uint32_t space_id,
-		    struct sql_trigger *trigger,
-		    struct sql_trigger **old_trigger);
-
-/**
- * Get trigger name by trigger AST object.
- * @param trigger AST object.
- * @return trigger name string.
- */
-const char *
-sql_trigger_name(struct sql_trigger *trigger);
-
-/**
- * Get space_id of the space that trigger has been built for.
- * @param trigger AST object.
- * @return space identifier.
- */
-uint32_t
-sql_trigger_space_id(struct sql_trigger *trigger);
+struct sql_trigger_expr *
+sql_trigger_expr_compile(struct sql *db, const char *sql);
 
 /**
  * Store duplicate of a parsed expression into @a parser.
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index 8a6c8cb6c..e27a64019 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -1330,26 +1330,33 @@ sql_create_trigger(struct Parse *parse_context)
 	assert(_trigger != NULL);
 
 	int first_col = parse_context->nMem + 1;
-	parse_context->nMem += 3;
+	parse_context->nMem += 8;
 	int record = ++parse_context->nMem;
 
-	uint32_t opts_buff_sz = mp_sizeof_map(1) +
-				mp_sizeof_str(strlen("sql")) +
-				mp_sizeof_str(trigger_def->sql_len);
-	char *opts_buff = (char *) sqlDbMallocRaw(db, opts_buff_sz);
-	if (opts_buff == NULL) {
-		parse_context->is_aborted = true;
-		goto end;
-	}
-
-	char *data = mp_encode_map(opts_buff, 1);
-	data = mp_encode_str(data, "sql", strlen("sql"));
-	data = mp_encode_str(data, trigger_def->sql, trigger_def->sql_len);
 	sqlVdbeAddOp4(v, OP_String8, 0, first_col, 0, trigger_name, P4_DYNAMIC);
 	sqlVdbeAddOp2(v, OP_Integer, space_id, first_col + 1);
-	sqlVdbeAddOp4(v, OP_Blob, opts_buff_sz, first_col + 2,
+	char *opts_buff = (char *) sqlDbMallocRaw(db, mp_sizeof_map(1));
+	if (opts_buff == NULL) {
+		parse_context->is_aborted = true;
+		goto end;
+	}
+	mp_encode_map(opts_buff, 0);
+	sqlVdbeAddOp4(v, OP_Blob, mp_sizeof_map(1), first_col + 2,
 		      SQL_SUBTYPE_MSGPACK, opts_buff, P4_DYNAMIC);
-	sqlVdbeAddOp3(v, OP_MakeRecord, first_col, 3, record);
+	sqlVdbeAddOp4(v, OP_String8, 0, first_col + 3, 0,
+		      trigger_language_strs[TRIGGER_LANGUAGE_SQL], P4_STATIC);
+	sqlVdbeAddOp4(v, OP_String8, 0, first_col + 4, 0,
+		      trigger_type_strs[TRIGGER_TYPE_REPLACE], P4_STATIC);
+	sqlVdbeAddOp4(v, OP_String8, 0, first_col + 5, 0,
+		trigger_event_manipulation_strs[trigger_def->event_manipulation],
+		P4_STATIC);
+	sqlVdbeAddOp4(v, OP_String8, 0, first_col + 6, 0,
+		trigger_action_timing_strs[trigger_def->action_timing],
+		P4_STATIC);
+	sqlVdbeAddOp4(v, OP_String8, 0, first_col + 7, 0,
+		sqlDbStrNDup(db, trigger_def->sql, trigger_def->sql_len),
+		P4_DYNAMIC);
+	sqlVdbeAddOp3(v, OP_MakeRecord, first_col, 8, record);
 	sqlVdbeAddOp4(v, OP_IdxInsert, record, 0, 0, (char *)_trigger,
 		      P4_SPACEPTR);
 	sqlVdbeChangeP5(v, OPFLAG_NCHANGE);
diff --git a/src/box/sql/fk_constraint.c b/src/box/sql/fk_constraint.c
index 65c13f453..a3db6489a 100644
--- a/src/box/sql/fk_constraint.c
+++ b/src/box/sql/fk_constraint.c
@@ -735,7 +735,7 @@ sql_trigger_for_fk_constrint_new(struct Parse *parser,
 		trigger_def_new(trigger_name, strlen(trigger_name),
 				parent_space_id, TRIGGER_LANGUAGE_SQL,
 				TRIGGER_EVENT_MANIPULATION_UPDATE,
-				TRIGGER_ACTION_TIMING_BEFORE);
+				TRIGGER_ACTION_TIMING_BEFORE, NULL, 0);
 	if (def == NULL) {
 		sql_trigger_expr_delete(db, expr);
 		goto halt;
@@ -925,7 +925,8 @@ fk_constraint_action_trigger(struct Parse *pParse, struct space_def *def,
 	sql_select_delete(db, select);
 
 	if (db->mallocFailed || trigger == NULL) {
-		sql_trigger_delete(trigger);
+		if (trigger != NULL)
+			trigger_delete((struct trigger *)trigger);
 		return NULL;
 	}
 
diff --git a/src/box/sql/prepare.c b/src/box/sql/prepare.c
index 84a265f96..b5aa87849 100644
--- a/src/box/sql/prepare.c
+++ b/src/box/sql/prepare.c
@@ -268,8 +268,11 @@ sql_parser_destroy(Parse *parser)
 	case AST_TYPE_EXPR:
 		sql_expr_delete(db, parser->parsed_ast.expr, false);
 		break;
-	case AST_TYPE_TRIGGER:
-		sql_trigger_delete(parser->parsed_ast.trigger);
+	case AST_TYPE_TRIGGER_EXPR:
+		if (parser->parsed_ast.trigger_expr != NULL) {
+			sql_trigger_expr_delete(db,
+					parser->parsed_ast.trigger_expr);
+		}
 		break;
 	default:
 		assert(parser->parsed_ast_type == AST_TYPE_UNDEFINED);
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 180997ce4..8d1588c7f 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -2091,7 +2091,7 @@ enum ast_type {
 	AST_TYPE_UNDEFINED = 0,
 	AST_TYPE_SELECT,
 	AST_TYPE_EXPR,
-	AST_TYPE_TRIGGER,
+	AST_TYPE_TRIGGER_EXPR,
 	ast_type_MAX
 };
 
@@ -2225,7 +2225,7 @@ struct Parse {
 	union {
 		struct Expr *expr;
 		struct Select *select;
-		struct sql_trigger *trigger;
+		struct sql_trigger_expr *trigger_expr;
 	} parsed_ast;
 };
 
diff --git a/src/box/sql/tokenize.c b/src/box/sql/tokenize.c
index 9fa069d09..089e8eeff 100644
--- a/src/box/sql/tokenize.c
+++ b/src/box/sql/tokenize.c
@@ -466,7 +466,7 @@ sqlRunParser(Parse * pParse, const char *zSql)
 		return -1;
 	}
 	assert(pParse->create_table_def.new_space == NULL);
-	assert(pParse->parsed_ast.trigger == NULL);
+	assert(pParse->parsed_ast.trigger_expr == NULL);
 	assert(pParse->nVar == 0);
 	assert(pParse->pVList == 0);
 	while (1) {
@@ -578,19 +578,19 @@ sql_view_compile(struct sql *db, const char *view_stmt)
 	return select;
 }
 
-struct sql_trigger *
-sql_trigger_compile(struct sql *db, const char *sql)
+struct sql_trigger_expr *
+sql_trigger_expr_compile(struct sql *db, const char *sql)
 {
 	struct Parse parser;
 	sql_parser_create(&parser, db, default_flags);
 	parser.parse_only = true;
-	struct sql_trigger *trigger = NULL;
+	struct sql_trigger_expr *trigger_expr = NULL;
 	if (sqlRunParser(&parser, sql) == 0 &&
-	    parser.parsed_ast_type == AST_TYPE_TRIGGER) {
-		trigger = parser.parsed_ast.trigger;
-		parser.parsed_ast.trigger = NULL;
+	    parser.parsed_ast_type == AST_TYPE_TRIGGER_EXPR) {
+		trigger_expr = parser.parsed_ast.trigger_expr;
+		parser.parsed_ast.trigger_expr = NULL;
 	}
 
 	sql_parser_destroy(&parser);
-	return trigger;
+	return trigger_expr;
 }
diff --git a/src/box/sql/trigger.c b/src/box/sql/trigger.c
index 2efdff3be..388fc64f3 100644
--- a/src/box/sql/trigger.c
+++ b/src/box/sql/trigger.c
@@ -73,61 +73,23 @@ sql_store_trigger(struct Parse *parse)
 	assert(alter_def->entity_type == ENTITY_TYPE_TRIGGER);
 	assert(alter_def->alter_action == ALTER_ACTION_CREATE);
 
-	struct region *region = &parse->region;
-	size_t region_svp = region_used(region);
 	assert(alter_def->entity_name->nSrc == 1);
 	assert(create_def->name.n > 0);
-	char *trigger_name =
-		sql_normalized_name_region_new(region, create_def->name.z,
-					       create_def->name.n);
-	if (trigger_name == NULL)
-		goto set_tarantool_error_and_cleanup;
-	if (sqlCheckIdentifierName(parse, trigger_name) != 0)
-		goto set_tarantool_error_and_cleanup;
-	uint32_t trigger_name_len = region_used(region) - region_svp;
+	sqlSrcListDelete(db, alter_def->entity_name);
 
-	const char *table_name = alter_def->entity_name->a[0].zName;
-	uint32_t space_id = box_space_id_by_name(table_name,
-						 strlen(table_name));
-	if (space_id == BOX_ID_NIL) {
-		diag_set(ClientError, ER_NO_SUCH_SPACE, table_name);
-		goto set_tarantool_error_and_cleanup;
-	}
-
-	/* Construct a trigger object. */
-	struct trigger_def *def =
-		trigger_def_new(trigger_name, trigger_name_len,
-				space_id, TRIGGER_LANGUAGE_SQL,
-				trigger_def->event_manipulation,
-				trigger_def->action_timing);
-	if (def == NULL)
-		goto set_tarantool_error_and_cleanup;
-	struct sql_trigger_expr *expr =
+	assert(parse->parsed_ast.trigger_expr == NULL);
+	parse->parsed_ast_type = AST_TYPE_TRIGGER_EXPR;
+	parse->parsed_ast.trigger_expr =
 		sql_trigger_expr_new(db, trigger_def->cols, trigger_def->when,
 				     trigger_def->step_list);
-	if (expr == NULL) {
-		trigger_def_delete(def);
+	if (parse->parsed_ast.trigger_expr == NULL)
 		goto set_tarantool_error_and_cleanup;
-	}
-	assert(parse->parsed_ast.trigger == NULL);
-	parse->parsed_ast_type = AST_TYPE_TRIGGER;
-	parse->parsed_ast.trigger = sql_trigger_new(def, expr, false);
-	if (parse->parsed_ast.trigger == NULL) {
-		sql_trigger_expr_delete(db, expr);
-		trigger_def_delete(def);
-		goto set_tarantool_error_and_cleanup;
-	}
-
-trigger_cleanup:
-	region_truncate(region, region_svp);
-	sqlSrcListDelete(db, alter_def->entity_name);
 	return;
 set_tarantool_error_and_cleanup:
 	parse->is_aborted = true;
 	sqlIdListDelete(db, trigger_def->cols);
 	sql_expr_delete(db, trigger_def->when, false);
 	sqlDeleteTriggerStep(db, trigger_def->step_list);
-	goto trigger_cleanup;
 }
 
 struct TriggerStep *
@@ -279,6 +241,8 @@ sql_trigger_expr_delete(struct sql *db, struct sql_trigger_expr *trigger_expr)
 	free(trigger_expr);
 }
 
+static struct trigger_vtab sql_trigger_vtab;
+
 struct sql_trigger *
 sql_trigger_new(struct trigger_def *def, struct sql_trigger_expr *expr,
 		bool is_fk_constraint_trigger)
@@ -288,23 +252,36 @@ sql_trigger_new(struct trigger_def *def, struct sql_trigger_expr *expr,
 		diag_set(OutOfMemory, sizeof(*trigger), "malloc", "trigger");
 		return NULL;
 	}
+	if (expr == NULL) {
+		assert(def->code != NULL);
+		expr = sql_trigger_expr_compile(sql_get(), def->code);
+		if (expr == NULL) {
+			free(trigger);
+			return NULL;
+		}
+	}
 	rlist_create(&trigger->base.link);
 	trigger->base.def = def;
+	trigger->base.vtab = &sql_trigger_vtab;
 	trigger->expr = expr;
 	trigger->is_fk_constraint_trigger = is_fk_constraint_trigger;
 	return trigger;
 }
 
-void
-sql_trigger_delete(struct sql_trigger *trigger)
+static void
+sql_trigger_delete(struct trigger *base)
 {
-	if (trigger == NULL)
-		return;
-	trigger_def_delete(trigger->base.def);
+	assert(base->vtab == &sql_trigger_vtab);
+	assert(base != NULL && base->def->language == TRIGGER_LANGUAGE_SQL);
+	struct sql_trigger *trigger = (struct sql_trigger *) base;
 	sql_trigger_expr_delete(sql_get(), trigger->expr);
 	free(trigger);
 }
 
+static struct trigger_vtab sql_trigger_vtab = {
+	.destroy = sql_trigger_delete,
+};
+
 void
 vdbe_code_drop_trigger(struct Parse *parser, const char *trigger_name,
 		       bool account_changes)
@@ -367,82 +344,6 @@ sql_drop_trigger(struct Parse *parser)
 	sqlSrcListDelete(db, name);
 }
 
-int
-sql_trigger_replace(const char *name, uint32_t space_id,
-		    struct sql_trigger *trigger, struct sql_trigger **old_trigger)
-{
-	assert(trigger == NULL || strcmp(name, trigger->base.def->name) == 0);
-
-	struct space *space = space_cache_find(space_id);
-	assert(space != NULL);
-	*old_trigger = NULL;
-
-	if (trigger != NULL) {
-		/* Do not create a trigger on a system space. */
-		if (space_is_system(space)) {
-			diag_set(ClientError, ER_SQL_EXECUTE,
-				 "cannot create trigger on system table");
-			return -1;
-		}
-		/*
-		 * INSTEAD of triggers are only for views and
-		 * views only support INSTEAD of triggers.
-		 */
-		if (space->def->opts.is_view &&
-		    trigger->base.def->action_timing !=
-		    TRIGGER_ACTION_TIMING_INSTEAD) {
-			const char *err_msg =
-				tt_sprintf("cannot create %s trigger on "
-					   "view: %s",
-					    trigger_action_timing_strs[
-						trigger->base.def->
-						action_timing],
-					    space->def->name);
-			diag_set(ClientError, ER_SQL_EXECUTE, err_msg);
-			return -1;
-		}
-		if (!space->def->opts.is_view &&
-		    trigger->base.def->action_timing ==
-		    TRIGGER_ACTION_TIMING_INSTEAD) {
-			diag_set(ClientError, ER_SQL_EXECUTE,
-				 tt_sprintf("cannot create "\
-                         "INSTEAD OF trigger on space: %s", space->def->name));
-			return -1;
-		}
-
-		if (trigger->base.def->action_timing ==
-		    TRIGGER_ACTION_TIMING_INSTEAD) {
-			trigger->base.def->action_timing =
-				TRIGGER_ACTION_TIMING_BEFORE;
-		}
-	}
-
-	struct trigger *p;
-	rlist_foreach_entry(p, &space->trigger_list, link) {
-		if (strcmp(p->def->name, name) != 0)
-			continue;
-		*old_trigger = (struct sql_trigger *)p;
-		rlist_del(&p->link);
-		break;
-	}
-
-	if (trigger != NULL)
-		rlist_add(&space->trigger_list, &trigger->base.link);
-	return 0;
-}
-
-const char *
-sql_trigger_name(struct sql_trigger *trigger)
-{
-	return trigger->base.def->name;
-}
-
-uint32_t
-sql_trigger_space_id(struct sql_trigger *trigger)
-{
-	return trigger->base.def->space_id;
-}
-
 /*
  * pEList is the SET clause of an UPDATE statement.  Each entry
  * in pEList is of the format <id>=<expr>.  If any of the entries
diff --git a/src/box/trigger.c b/src/box/trigger.c
new file mode 100644
index 000000000..15a810d5b
--- /dev/null
+++ b/src/box/trigger.c
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "trigger.h"
+
+#include <stdbool.h>
+#include "space.h"
+#include "sql.h"
+#include "trigger_def.h"
+#include "trivia/util.h"
+
+struct trigger *
+trigger_new(struct trigger_def *def)
+{
+	struct trigger *trigger = NULL;
+	switch (def->language) {
+	case TRIGGER_LANGUAGE_SQL: {
+		trigger = (struct trigger *) sql_trigger_new(def, NULL, false);
+		break;
+	}
+	default: {
+		unreachable();
+	}
+	}
+	return trigger;
+}
+
+void
+trigger_delete(struct trigger *base)
+{
+	struct trigger_def *def = base->def;
+	base->vtab->destroy(base);
+	trigger_def_delete(def);
+}
+
+struct trigger *
+space_trigger_by_name(struct space *space, const char *name, uint32_t name_len)
+{
+	struct trigger *trigger = NULL;
+	rlist_foreach_entry(trigger, &space->trigger_list, link) {
+		if (trigger->def->name_len == name_len &&
+		    memcmp(trigger->def->name, name, name_len) == 0)
+			return trigger;
+	}
+	return NULL;
+}
diff --git a/src/box/trigger.h b/src/box/trigger.h
index 9b5853fe4..dbfa92055 100644
--- a/src/box/trigger.h
+++ b/src/box/trigger.h
@@ -35,16 +35,27 @@
 extern "C" {
 #endif /* defined(__cplusplus) */
 
+#include <inttypes.h>
 #include "small/rlist.h"
 
+struct space;
+struct trigger;
 struct trigger_def;
 
+/** Virtual method table for trigger object. */
+struct trigger_vtab {
+	/** Release implementation-specific trigger context. */
+	void (*destroy)(struct trigger *func);
+};
+
 /**
  * Structure representing trigger.
  */
 struct trigger {
 	/** The trigger definition. */
 	struct trigger_def *def;
+	/** Virtual method table. */
+	const struct trigger_vtab *vtab;
 	/**
 	 * Organize sql_trigger structs into linked list
 	 * with space::trigger_list.
@@ -52,6 +63,16 @@ struct trigger {
 	struct rlist link;
 };
 
+struct trigger *
+trigger_new(struct trigger_def *def);
+
+void
+trigger_delete(struct trigger *trigger);
+
+/** Find trigger object in space by given name and name_len. */
+struct trigger *
+space_trigger_by_name(struct space *space, const char *name, uint32_t name_len);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/src/box/trigger_def.c b/src/box/trigger_def.c
index 83e929662..8f3feec5f 100644
--- a/src/box/trigger_def.c
+++ b/src/box/trigger_def.c
@@ -30,6 +30,10 @@
  */
 #include "trigger_def.h"
 #include "diag.h"
+#include "errcode.h"
+#include "schema.h"
+#include "space.h"
+#include "tt_static.h"
 
 const char *trigger_event_manipulation_strs[] = {"DELETE", "UPDATE", "INSERT"};
 
@@ -37,13 +41,18 @@ const char *trigger_action_timing_strs[] = {"BEFORE", "AFTER", "INSTEAD"};
 
 const char *trigger_language_strs[] = {"SQL"};
 
+const char *trigger_type_strs[] = {"REPLACE"};
+
 struct trigger_def *
 trigger_def_new(const char *name, uint32_t name_len, uint32_t space_id,
 		enum trigger_language language,
 		enum trigger_event_manipulation event_manipulation,
-		enum trigger_action_timing action_timing)
+		enum trigger_action_timing action_timing, const char *code,
+		uint32_t code_len)
 {
-	uint32_t trigger_def_sz = trigger_def_sizeof(name_len);
+	uint32_t code_offset;
+	uint32_t trigger_def_sz = trigger_def_sizeof(name_len, code_len,
+						     &code_offset);
 	struct trigger_def *trigger_def =
 		(struct trigger_def *) malloc(trigger_def_sz);
 	if (trigger_def == NULL) {
@@ -56,6 +65,14 @@ trigger_def_new(const char *name, uint32_t name_len, uint32_t space_id,
 	trigger_def->action_timing = action_timing;
 	memcpy(trigger_def->name, name, name_len);
 	trigger_def->name[name_len] = '\0';
+	trigger_def->name_len = name_len;
+	if (code_len != 0) {
+		trigger_def->code = (char *) trigger_def + code_offset;
+		memcpy(trigger_def->code, code, code_len);
+		trigger_def->code[code_len] = '\0';
+	} else {
+		trigger_def->code = NULL;
+	}
 	return trigger_def;
 }
 
@@ -64,3 +81,51 @@ trigger_def_delete(struct trigger_def *trigger_def)
 {
 	free(trigger_def);
 }
+
+int
+trigger_def_check(struct trigger_def *def)
+{
+	struct space *space = space_by_id(def->space_id);
+	if (space == NULL) {
+		diag_set(ClientError, ER_CREATE_TRIGGER, def->name,
+			 tt_sprintf("Space '%s' does not exist",
+				    int2str(def->space_id)));
+		return -1;
+	}
+	if (space_is_system(space)) {
+		diag_set(ClientError, ER_CREATE_TRIGGER, def->name,
+			 "cannot create trigger on system space");
+		return -1;
+	}
+	switch (def->language) {
+	case TRIGGER_LANGUAGE_SQL: {
+		if (space->def->opts.is_view &&
+		    def->action_timing != TRIGGER_ACTION_TIMING_INSTEAD) {
+			const char *err_msg =
+				tt_sprintf("cannot create %s trigger on "
+					   "view: %s",
+					   trigger_action_timing_strs[
+						   def->action_timing],
+					   space->def->name);
+			diag_set(ClientError, ER_SQL_EXECUTE, err_msg);
+			return -1;
+		}
+		if (!space->def->opts.is_view &&
+		    def->action_timing == TRIGGER_ACTION_TIMING_INSTEAD) {
+			diag_set(ClientError, ER_SQL_EXECUTE,
+				tt_sprintf("cannot create INSTEAD OF trigger "
+					   "on space: %s", space->def->name));
+			return -1;
+		}
+		break;
+	}
+	default: {
+		/*
+		 * Only SQL triggers could define INSTEAD OF
+		 * ACTION TIMING.
+		 * */
+		unreachable();
+	}
+	}
+	return 0;
+}
diff --git a/src/box/trigger_def.h b/src/box/trigger_def.h
index 98df3a3de..68e2c3afd 100644
--- a/src/box/trigger_def.h
+++ b/src/box/trigger_def.h
@@ -79,6 +79,13 @@ enum trigger_language {
 
 extern const char *trigger_language_strs[];
 
+enum trigger_type {
+	TRIGGER_TYPE_REPLACE,
+	trigger_type_MAX,
+};
+
+extern const char *trigger_type_strs[];
+
 /**
  * Trigger object definition.
  * See trigger_def_sizeof() definition for implementation
@@ -102,6 +109,13 @@ struct trigger_def {
 	uint32_t space_id;
 	/** The language of the stored trigger. */
 	enum trigger_language language;
+	/**
+	 * The language-dependent code defining trigger
+	 * operations.
+	 */
+	char *code;
+	/** The length of the trigger name. */
+	uint32_t name_len;
 	/**
 	 * The 0-terminated string, a name of the check
 	 * constraint. Must be unique for a given space.
@@ -128,9 +142,14 @@ struct trigger_def {
  *         given parameters.
  */
 static inline uint32_t
-trigger_def_sizeof(uint32_t name_len)
+trigger_def_sizeof(uint32_t name_len, uint32_t code_len,
+		   uint32_t *code_offset)
 {
-	return sizeof(struct trigger_def) + name_len + 1;
+	uint32_t sz = sizeof(struct trigger_def) + name_len + 1;
+	*code_offset = sz;
+	if (code_len != 0)
+		sz += code_len + 1;
+	return sz;
 }
 
 /**
@@ -145,6 +164,8 @@ trigger_def_sizeof(uint32_t name_len)
  *                           trigger activates.
  * @param action_timing Whether the trigger activates before or
  *                      after the triggering event.
+ * @param code The trigger program function code string.
+ * @param code_len The length of the @a code string.
  * @retval not NULL Trigger definition object pointer on success.
  * @retval NULL Otherwise. The diag message is set.
 */
@@ -152,7 +173,8 @@ struct trigger_def *
 trigger_def_new(const char *name, uint32_t name_len, uint32_t space_id,
 		enum trigger_language language,
 		enum trigger_event_manipulation event_manipulation,
-		enum trigger_action_timing action_timing);
+		enum trigger_action_timing action_timing, const char *code,
+		uint32_t code_len);
 
 /**
  * Destroy trigger definition memory, release acquired resources.
@@ -161,6 +183,10 @@ trigger_def_new(const char *name, uint32_t name_len, uint32_t space_id,
 void
 trigger_def_delete(struct trigger_def *trigger_def);
 
+/** Check if a non-empty trigger body is correct. */
+int
+trigger_def_check(struct trigger_def *def);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result
index a59979e62..cc9bdfda2 100644
--- a/test/box-py/bootstrap.result
+++ b/test/box-py/bootstrap.result
 ...
 session = nil
 ---
diff --git a/test/box/misc.result b/test/box/misc.result
index 33e5ce886..3fa9ec351 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -533,6 +533,7 @@ t;
   201: box.error.NO_SUCH_FIELD_NAME
   202: box.error.FUNC_WRONG_ARG_COUNT
   203: box.error.BOOTSTRAP_READONLY
+  204: box.error.CREATE_TRIGGER
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/sql/ddl.result b/test/sql/ddl.result
index 28acf37ea..5f15f9f72 100644
--- a/test/sql/ddl.result
+++ b/test/sql/ddl.result
@@ -208,7 +208,7 @@ box.execute([[CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW
 
 box.begin()
 box.execute('ALTER TABLE t1 RENAME TO t1_new;')
-sql = _trigger_index:select(box.space.T1_NEW.id)[1].opts.sql
+sql = _trigger_index:select(box.space.T1_NEW.id)[1].code
 assert(sql:find('T1_NEW'))
 box.rollback()$
  | ---
@@ -218,7 +218,7 @@ test_run:cmd("setopt delimiter ''")$
  | - true
  | ...
 
-sql = _trigger_index:select(box.space.T1.id)[1].opts.sql
+sql = _trigger_index:select(box.space.T1.id)[1].code
  | ---
  | ...
 not sql:find('T1_NEW') and sql:find('t1') ~= nil
@@ -230,7 +230,7 @@ box.execute('ALTER TABLE t1 RENAME TO t1_new;')
  | ---
  | - row_count: 0
  | ...
-sql = _trigger_index:select(box.space.T1_NEW.id)[1].opts.sql
+sql = _trigger_index:select(box.space.T1_NEW.id)[1].code
  | ---
  | ...
 sql:find('T1_NEW') ~= nil
diff --git a/test/sql/ddl.test.lua b/test/sql/ddl.test.lua
index 6067b6192..aeb733366 100644
--- a/test/sql/ddl.test.lua
+++ b/test/sql/ddl.test.lua
@@ -98,16 +98,16 @@ box.execute([[CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW
 
 box.begin()
 box.execute('ALTER TABLE t1 RENAME TO t1_new;')
-sql = _trigger_index:select(box.space.T1_NEW.id)[1].opts.sql
+sql = _trigger_index:select(box.space.T1_NEW.id)[1].code
 assert(sql:find('T1_NEW'))
 box.rollback()$
 test_run:cmd("setopt delimiter ''")$
 
-sql = _trigger_index:select(box.space.T1.id)[1].opts.sql
+sql = _trigger_index:select(box.space.T1.id)[1].code
 not sql:find('T1_NEW') and sql:find('t1') ~= nil
 
 box.execute('ALTER TABLE t1 RENAME TO t1_new;')
-sql = _trigger_index:select(box.space.T1_NEW.id)[1].opts.sql
+sql = _trigger_index:select(box.space.T1_NEW.id)[1].code
 sql:find('T1_NEW') ~= nil
 
 box.execute('DROP TABLE t1_new')
diff --git a/test/sql/gh2141-delete-trigger-drop-table.result b/test/sql/gh2141-delete-trigger-drop-table.result
index 1d373f57e..961eb7a8f 100644
--- a/test/sql/gh2141-delete-trigger-drop-table.result
+++ b/test/sql/gh2141-delete-trigger-drop-table.result
@@ -38,26 +38,26 @@ box.execute("CREATE TRIGGER tt_ad AFTER DELETE ON t FOR EACH ROW BEGIN SELECT 1;
 - row_count: 1
 ...
 -- check that these triggers exist
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"")
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"")
 ---
 - metadata:
   - name: name
     type: string
-  - name: opts
-    type: map
+  - name: code
+    type: string
   rows:
-  - ['TT_AD', {'sql': 'CREATE TRIGGER tt_ad AFTER DELETE ON t FOR EACH ROW BEGIN SELECT
-        1; END'}]
-  - ['TT_AI', {'sql': 'CREATE TRIGGER tt_ai AFTER INSERT ON t FOR EACH ROW BEGIN SELECT
-        1; END'}]
-  - ['TT_AU', {'sql': 'CREATE TRIGGER tt_au AFTER UPDATE ON t FOR EACH ROW BEGIN SELECT
-        1; END'}]
-  - ['TT_BD', {'sql': 'CREATE TRIGGER tt_bd BEFORE DELETE ON t FOR EACH ROW BEGIN
-        SELECT 1; END'}]
-  - ['TT_BI', {'sql': 'CREATE TRIGGER tt_bi BEFORE INSERT ON t FOR EACH ROW BEGIN
-        SELECT 1; END'}]
-  - ['TT_BU', {'sql': 'CREATE TRIGGER tt_bu BEFORE UPDATE ON t FOR EACH ROW BEGIN
-        SELECT 1; END'}]
+  - ['TT_AD', 'CREATE TRIGGER tt_ad AFTER DELETE ON t FOR EACH ROW BEGIN SELECT 1;
+      END']
+  - ['TT_AI', 'CREATE TRIGGER tt_ai AFTER INSERT ON t FOR EACH ROW BEGIN SELECT 1;
+      END']
+  - ['TT_AU', 'CREATE TRIGGER tt_au AFTER UPDATE ON t FOR EACH ROW BEGIN SELECT 1;
+      END']
+  - ['TT_BD', 'CREATE TRIGGER tt_bd BEFORE DELETE ON t FOR EACH ROW BEGIN SELECT 1;
+      END']
+  - ['TT_BI', 'CREATE TRIGGER tt_bi BEFORE INSERT ON t FOR EACH ROW BEGIN SELECT 1;
+      END']
+  - ['TT_BU', 'CREATE TRIGGER tt_bu BEFORE UPDATE ON t FOR EACH ROW BEGIN SELECT 1;
+      END']
 ...
 -- drop table
 box.execute("DROP TABLE t")
@@ -65,12 +65,12 @@ box.execute("DROP TABLE t")
 - row_count: 1
 ...
 -- check that triggers were dropped with deleted table
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"")
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"")
 ---
 - metadata:
   - name: name
     type: string
-  - name: opts
-    type: map
+  - name: code
+    type: string
   rows: []
 ...
diff --git a/test/sql/gh2141-delete-trigger-drop-table.test.lua b/test/sql/gh2141-delete-trigger-drop-table.test.lua
index 4d21fd7e0..240ec67a2 100644
--- a/test/sql/gh2141-delete-trigger-drop-table.test.lua
+++ b/test/sql/gh2141-delete-trigger-drop-table.test.lua
@@ -13,10 +13,10 @@ box.execute("CREATE TRIGGER tt_bd BEFORE DELETE ON t FOR EACH ROW BEGIN SELECT 1
 box.execute("CREATE TRIGGER tt_ad AFTER DELETE ON t FOR EACH ROW BEGIN SELECT 1; END")
 
 -- check that these triggers exist
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"")
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"")
 
 -- drop table
 box.execute("DROP TABLE t")
 
 -- check that triggers were dropped with deleted table
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"")
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"")
diff --git a/test/sql/persistency.result b/test/sql/persistency.result
index f8f992c39..bce171eed 100644
--- a/test/sql/persistency.result
+++ b/test/sql/persistency.result
@@ -317,16 +317,16 @@ box.execute("CREATE TRIGGER tfoobar AFTER INSERT ON foobar FOR EACH ROW BEGIN IN
 ---
 - row_count: 1
 ...
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"");
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"");
 ---
 - metadata:
   - name: name
     type: string
-  - name: opts
-    type: map
+  - name: code
+    type: string
   rows:
-  - ['TFOOBAR', {'sql': 'CREATE TRIGGER tfoobar AFTER INSERT ON foobar FOR EACH ROW
-        BEGIN INSERT INTO barfoo VALUES (''trigger test'', 9999); END'}]
+  - ['TFOOBAR', 'CREATE TRIGGER tfoobar AFTER INSERT ON foobar FOR EACH ROW BEGIN
+      INSERT INTO barfoo VALUES (''trigger test'', 9999); END']
 ...
 -- Many entries
 box.execute("CREATE TABLE t1(a INT,b INT,c INT,PRIMARY KEY(b,c));")
@@ -356,16 +356,16 @@ box.execute("SELECT a FROM t1 ORDER BY b, a LIMIT 10 OFFSET 20;");
 ...
 test_run:cmd('restart server default');
 -- prove that trigger survived
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"");
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"");
 ---
 - metadata:
   - name: name
     type: string
-  - name: opts
-    type: map
+  - name: code
+    type: string
   rows:
-  - ['TFOOBAR', {'sql': 'CREATE TRIGGER tfoobar AFTER INSERT ON foobar FOR EACH ROW
-        BEGIN INSERT INTO barfoo VALUES (''trigger test'', 9999); END'}]
+  - ['TFOOBAR', 'CREATE TRIGGER tfoobar AFTER INSERT ON foobar FOR EACH ROW BEGIN
+      INSERT INTO barfoo VALUES (''trigger test'', 9999); END']
 ...
 -- ... functional
 box.execute("INSERT INTO foobar VALUES ('foobar trigger test', 8888)")
@@ -383,16 +383,16 @@ box.execute("SELECT * FROM barfoo WHERE foo = 9999");
   rows: []
 ...
 -- and still persistent
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"")
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"")
 ---
 - metadata:
   - name: name
     type: string
-  - name: opts
-    type: map
+  - name: code
+    type: string
   rows:
-  - ['TFOOBAR', {'sql': 'CREATE TRIGGER tfoobar AFTER INSERT ON foobar FOR EACH ROW
-        BEGIN INSERT INTO barfoo VALUES (''trigger test'', 9999); END'}]
+  - ['TFOOBAR', 'CREATE TRIGGER tfoobar AFTER INSERT ON foobar FOR EACH ROW BEGIN
+      INSERT INTO barfoo VALUES (''trigger test'', 9999); END']
 ...
 -- and can be dropped just once
 box.execute("DROP TRIGGER tfoobar")
@@ -406,13 +406,13 @@ box.execute("DROP TRIGGER tfoobar")
 - Trigger 'TFOOBAR' doesn't exist
 ...
 -- Should be empty
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"")
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"")
 ---
 - metadata:
   - name: name
     type: string
-  - name: opts
-    type: map
+  - name: code
+    type: string
   rows: []
 ...
 -- prove barfoo2 still exists
diff --git a/test/sql/persistency.test.lua b/test/sql/persistency.test.lua
index 196445349..035fa8eb9 100644
--- a/test/sql/persistency.test.lua
+++ b/test/sql/persistency.test.lua
@@ -51,7 +51,7 @@ box.execute("INSERT INTO barfoo VALUES ('foobar', 1000)")
 
 -- create a trigger
 box.execute("CREATE TRIGGER tfoobar AFTER INSERT ON foobar FOR EACH ROW BEGIN INSERT INTO barfoo VALUES ('trigger test', 9999); END")
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"");
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"");
 
 -- Many entries
 box.execute("CREATE TABLE t1(a INT,b INT,c INT,PRIMARY KEY(b,c));")
@@ -61,21 +61,21 @@ box.execute("SELECT a FROM t1 ORDER BY b, a LIMIT 10 OFFSET 20;");
 test_run:cmd('restart server default');
 
 -- prove that trigger survived
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"");
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"");
 
 -- ... functional
 box.execute("INSERT INTO foobar VALUES ('foobar trigger test', 8888)")
 box.execute("SELECT * FROM barfoo WHERE foo = 9999");
 
 -- and still persistent
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"")
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"")
 
 -- and can be dropped just once
 box.execute("DROP TRIGGER tfoobar")
 -- Should error
 box.execute("DROP TRIGGER tfoobar")
 -- Should be empty
-box.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"")
+box.execute("SELECT \"name\", \"code\" FROM \"_trigger\"")
 
 -- prove barfoo2 still exists
 box.execute("INSERT INTO barfoo VALUES ('xfoo', 1)")
diff --git a/test/sql/triggers.result b/test/sql/triggers.result
index 1a70ddf2b..eddc3a18a 100644
--- a/test/sql/triggers.result
+++ b/test/sql/triggers.result
@@ -11,8 +11,14 @@ box.execute('pragma sql_default_engine=\''..engine..'\'')
 ---
 - row_count: 0
 ...
+function setmap(tab) return setmetatable(tab, { __serialize = 'map' }) end
+---
+...
+MAP = setmap({})
+---
+...
 -- Get invariant part of the tuple; name and opts don't change.
- function immutable_part(data) local r = {} for i, l in pairs(data) do table.insert(r, {l.name, l.opts}) end return r end
+function immutable_part(data) local r = {} for i, l in pairs(data) do table.insert(r, {l.name, l.code}) end return r end
 ---
 ...
 --
@@ -33,43 +39,12 @@ box.execute([[CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT IN
 immutable_part(box.space._trigger:select())
 ---
 - - - T1T
-    - {'sql': 'CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(1); END'}
+    - CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1);
+      END
 ...
 space_id = box.space._space.index["name"]:get('T1').id
 ---
 ...
--- Checks for LUA tuples.
-tuple = {"T1t", space_id, {sql = "CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1); END;"}}
----
-...
-box.space._trigger:insert(tuple)
----
-- error: 'Failed to execute SQL statement: trigger name does not match extracted from
-    SQL'
-...
-tuple = {"T1t", space_id, {sql = "CREATE TRIGGER t12t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1); END;"}}
----
-...
-box.space._trigger:insert(tuple)
----
-- error: 'Failed to execute SQL statement: trigger name does not match extracted from
-    SQL'
-...
-tuple = {"T2T", box.space.T1.id + 1, {sql = "CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1); END;"}}
----
-...
-box.space._trigger:insert(tuple)
----
-- error: 'Failed to execute SQL statement: trigger space_id does not match the value
-    resolved on AST building from SQL'
-...
-immutable_part(box.space._trigger:select())
----
-- - - T1T
-    - {'sql': 'CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(1); END'}
-...
 box.execute("DROP TABLE T1;")
 ---
 - row_count: 1
@@ -89,8 +64,8 @@ box.execute([[CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT IN
 immutable_part(box.space._trigger:select())
 ---
 - - - T1T
-    - {'sql': 'CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(1); END'}
+    - CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1);
+      END
 ...
 space_id = box.space._space.index["name"]:get('T1').id
 ---
@@ -113,13 +88,13 @@ box.execute("DELETE FROM t2;")
 - row_count: 1
 ...
 -- Test triggers.
-tuple = {"T2T", space_id, {sql = "CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(2); END;"}}
+tuple = {"T2T", space_id, MAP, 'SQL', 'REPLACE', 'INSERT', 'AFTER', "CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(2); END;"}
 ---
 ...
 _ = box.space._trigger:insert(tuple)
 ---
 ...
-tuple = {"T3T", space_id, {sql = "CREATE TRIGGER t3t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(3); END;"}}
+tuple = {"T3T", space_id, MAP, 'SQL', 'REPLACE', 'INSERT', 'AFTER', "CREATE TRIGGER t3t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(3); END;"}
 ---
 ...
 _ = box.space._trigger:insert(tuple)
@@ -128,14 +103,14 @@ _ = box.space._trigger:insert(tuple)
 immutable_part(box.space._trigger:select())
 ---
 - - - T1T
-    - {'sql': 'CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(1); END'}
+    - CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1);
+      END
   - - T2T
-    - {'sql': 'CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(2); END;'}
+    - CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(2);
+      END;
   - - T3T
-    - {'sql': 'CREATE TRIGGER t3t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(3); END;'}
+    - CREATE TRIGGER t3t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(3);
+      END;
 ...
 box.execute("INSERT INTO t1 VALUES(2);")
 ---
@@ -166,8 +141,8 @@ _ = box.space._trigger:delete("T3T")
 immutable_part(box.space._trigger:select())
 ---
 - - - T1T
-    - {'sql': 'CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(1); END'}
+    - CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1);
+      END
 ...
 box.execute("INSERT INTO t1 VALUES(3);")
 ---
@@ -197,14 +172,14 @@ box.execute([[CREATE TRIGGER t3t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT IN
 immutable_part(box.space._trigger:select())
 ---
 - - - T1T
-    - {'sql': 'CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(1); END'}
+    - CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1);
+      END
   - - T2T
-    - {'sql': 'CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(2); END'}
+    - CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(2);
+      END
   - - T3T
-    - {'sql': 'CREATE TRIGGER t3t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO
-        t2 VALUES(3); END'}
+    - CREATE TRIGGER t3t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(3);
+      END
 ...
 box.execute("INSERT INTO t1 VALUES(4);")
 ---
@@ -241,7 +216,7 @@ box.execute("CREATE TABLE t1(a INT PRIMARY KEY,b INT);")
 space_id = box.space.T1.id
 ---
 ...
-tuple = {"T1T", space_id, {sql = [[create trigger t1t instead of update on t1 for each row begin delete from t1 WHERE a=old.a+2; end;]]}}
+tuple = {"T1T", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'INSTEAD', "CREATE TRIGGER T1T INSTEAD OF UPDATE ON T1 FOR EACH ROW BEGIN DELETE FROM T1 WHERE A=OLD.A+2; END;"}
 ---
 ...
 box.space._trigger:insert(tuple)
@@ -256,14 +231,14 @@ box.execute("CREATE VIEW V1 AS SELECT * FROM t1;")
 space_id = box.space.V1.id
 ---
 ...
-tuple = {"V1T", space_id, {sql = [[create trigger v1t before update on v1 for each row begin delete from t1 WHERE a=old.a+2; end;]]}}
+tuple = {"V1T", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'BEFORE', "CREATE TRIGGER V1T BEFORE UPDATE ON V1 FOR EACH ROW BEGIN DELETE FROM T1 WHERE A=OLD.A+2; END;"}
 ---
 ...
 box.space._trigger:insert(tuple)
 ---
 - error: 'Failed to execute SQL statement: cannot create BEFORE trigger on view: V1'
 ...
-tuple = {"V1T", space_id, {sql = [[create trigger v1t AFTER update on v1 for each row begin delete from t1 WHERE a=old.a+2; end;]]}}
+tuple = {"V1T", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'AFTER', "CREATE TRIGGER V1T AFTER UPDATE ON V1 FOR EACH ROW BEGIN DELETE FROM T1 WHERE A=OLD.A+2; END;"}
 ---
 ...
 box.space._trigger:insert(tuple)
@@ -273,12 +248,12 @@ box.space._trigger:insert(tuple)
 space_id =  box.space._fk_constraint.id
 ---
 ...
-tuple = {"T1T", space_id, {sql = [[create trigger t1t instead of update on "_fk_constraint" for each row begin delete from t1 WHERE a=old.a+2; end;]]}}
+tuple = {"T1T", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'BEFORE', "CREATE TRIGGER T1T INSTEAD OF UPDATE ON \"_fk_constraint\" FOR EACH ROW BEGIN DELETE FROM T1 WHERE A=OLD.A+2; END;"}
 ---
 ...
 box.space._trigger:insert(tuple)
 ---
-- error: 'Failed to execute SQL statement: cannot create trigger on system table'
+- error: 'Failed to create trigger ''T1T'': cannot create trigger on system space'
 ...
 box.execute("DROP VIEW V1;")
 ---
@@ -489,7 +464,7 @@ box.execute("CREATE TRIGGER tr1 AFTER INSERT ON t1 FOR EACH ROW WHEN new.a = ? B
 - null
 - bindings are not allowed in DDL
 ...
-tuple = {"TR1", space_id, {sql = [[CREATE TRIGGER tr1 AFTER INSERT ON t1 FOR EACH ROW WHEN new.a = ? BEGIN SELECT 1; END;]]}}
+tuple = {"TR1", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'AFTER', "CREATE TRIGGER tr1 AFTER INSERT ON t1 FOR EACH ROW WHEN new.a = ? BEGIN SELECT 1; END;"}
 ---
 ...
 box.space._trigger:insert(tuple)
diff --git a/test/sql/triggers.test.lua b/test/sql/triggers.test.lua
index f0397dc7d..d2a91d269 100644
--- a/test/sql/triggers.test.lua
+++ b/test/sql/triggers.test.lua
@@ -3,8 +3,11 @@ test_run = env.new()
 engine = test_run:get_cfg('engine')
 box.execute('pragma sql_default_engine=\''..engine..'\'')
 
+function setmap(tab) return setmetatable(tab, { __serialize = 'map' }) end
+MAP = setmap({})
+
 -- Get invariant part of the tuple; name and opts don't change.
- function immutable_part(data) local r = {} for i, l in pairs(data) do table.insert(r, {l.name, l.opts}) end return r end
+function immutable_part(data) local r = {} for i, l in pairs(data) do table.insert(r, {l.name, l.code}) end return r end
 
 --
 -- gh-3273: Move Triggers to server
@@ -17,17 +20,6 @@ immutable_part(box.space._trigger:select())
 
 space_id = box.space._space.index["name"]:get('T1').id
 
--- Checks for LUA tuples.
-tuple = {"T1t", space_id, {sql = "CREATE TRIGGER t1t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1); END;"}}
-box.space._trigger:insert(tuple)
-
-tuple = {"T1t", space_id, {sql = "CREATE TRIGGER t12t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1); END;"}}
-box.space._trigger:insert(tuple)
-
-tuple = {"T2T", box.space.T1.id + 1, {sql = "CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(1); END;"}}
-box.space._trigger:insert(tuple)
-immutable_part(box.space._trigger:select())
-
 box.execute("DROP TABLE T1;")
 immutable_part(box.space._trigger:select())
 
@@ -44,9 +36,9 @@ box.execute("DELETE FROM t2;")
 
 
 -- Test triggers.
-tuple = {"T2T", space_id, {sql = "CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(2); END;"}}
+tuple = {"T2T", space_id, MAP, 'SQL', 'REPLACE', 'INSERT', 'AFTER', "CREATE TRIGGER t2t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(2); END;"}
 _ = box.space._trigger:insert(tuple)
-tuple = {"T3T", space_id, {sql = "CREATE TRIGGER t3t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(3); END;"}}
+tuple = {"T3T", space_id, MAP, 'SQL', 'REPLACE', 'INSERT', 'AFTER', "CREATE TRIGGER t3t AFTER INSERT ON t1 FOR EACH ROW BEGIN INSERT INTO t2 VALUES(3); END;"}
 _ = box.space._trigger:insert(tuple)
 immutable_part(box.space._trigger:select())
 box.execute("INSERT INTO t1 VALUES(2);")
@@ -77,20 +69,20 @@ immutable_part(box.space._trigger:select())
 box.execute("CREATE TABLE t1(a INT PRIMARY KEY,b INT);")
 space_id = box.space.T1.id
 
-tuple = {"T1T", space_id, {sql = [[create trigger t1t instead of update on t1 for each row begin delete from t1 WHERE a=old.a+2; end;]]}}
+tuple = {"T1T", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'INSTEAD', "CREATE TRIGGER T1T INSTEAD OF UPDATE ON T1 FOR EACH ROW BEGIN DELETE FROM T1 WHERE A=OLD.A+2; END;"}
 box.space._trigger:insert(tuple)
 
 box.execute("CREATE VIEW V1 AS SELECT * FROM t1;")
 space_id = box.space.V1.id
 
-tuple = {"V1T", space_id, {sql = [[create trigger v1t before update on v1 for each row begin delete from t1 WHERE a=old.a+2; end;]]}}
+tuple = {"V1T", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'BEFORE', "CREATE TRIGGER V1T BEFORE UPDATE ON V1 FOR EACH ROW BEGIN DELETE FROM T1 WHERE A=OLD.A+2; END;"}
 box.space._trigger:insert(tuple)
 
-tuple = {"V1T", space_id, {sql = [[create trigger v1t AFTER update on v1 for each row begin delete from t1 WHERE a=old.a+2; end;]]}}
+tuple = {"V1T", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'AFTER', "CREATE TRIGGER V1T AFTER UPDATE ON V1 FOR EACH ROW BEGIN DELETE FROM T1 WHERE A=OLD.A+2; END;"}
 box.space._trigger:insert(tuple)
 
 space_id =  box.space._fk_constraint.id
-tuple = {"T1T", space_id, {sql = [[create trigger t1t instead of update on "_fk_constraint" for each row begin delete from t1 WHERE a=old.a+2; end;]]}}
+tuple = {"T1T", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'BEFORE', "CREATE TRIGGER T1T INSTEAD OF UPDATE ON \"_fk_constraint\" FOR EACH ROW BEGIN DELETE FROM T1 WHERE A=OLD.A+2; END;"}
 box.space._trigger:insert(tuple)
 
 box.execute("DROP VIEW V1;")
@@ -162,7 +154,7 @@ box.execute("DROP TABLE t;")
 box.execute("CREATE TABLE t1(a INT PRIMARY KEY, b INT);")
 space_id = box.space.T1.id
 box.execute("CREATE TRIGGER tr1 AFTER INSERT ON t1 FOR EACH ROW WHEN new.a = ? BEGIN SELECT 1; END;")
-tuple = {"TR1", space_id, {sql = [[CREATE TRIGGER tr1 AFTER INSERT ON t1 FOR EACH ROW WHEN new.a = ? BEGIN SELECT 1; END;]]}}
+tuple = {"TR1", space_id, MAP, 'SQL', 'REPLACE', 'UPDATE', 'AFTER', "CREATE TRIGGER tr1 AFTER INSERT ON t1 FOR EACH ROW WHEN new.a = ? BEGIN SELECT 1; END;"}
 box.space._trigger:insert(tuple)
 box.execute("DROP TABLE t1;")
 
diff --git a/test/sql/upgrade.result b/test/sql/upgrade.result
index b5c4ad500..7392e9c7c 100644
--- a/test/sql/upgrade.result
+++ b/test/sql/upgrade.result
@@ -27,7 +27,9 @@ test_run:switch('upgrade')
 box.space._space.index['name']:get('_trigger')
 ---
 - [328, 1, '_trigger', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'}, {'name': 'space_id',
-      'type': 'unsigned'}, {'name': 'opts', 'type': 'map'}]]
+      'type': 'unsigned'}, {'name': 'opts', 'type': 'map'}, {'name': 'language', 'type': 'string'},
+    {'name': 'type', 'type': 'string'}, {'name': 'event_manipulation', 'type': 'string'},
+    {'name': 'action_timing', 'type': 'string'}, {'name': 'code', 'type': 'string'}]]
 ...
 box.space._index:get({box.space._space.index['name']:get('_trigger').id, 0})
 ---
@@ -83,19 +85,19 @@ t1t.name
 ---
 - T1T
 ...
-t1t.opts
+t1t.code
 ---
-- {'sql': 'CREATE TRIGGER t1t AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO t_out
-    VALUES(1); END'}
+- CREATE TRIGGER t1t AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO t_out VALUES(1);
+  END
 ...
 t2t.name
 ---
 - T2T
 ...
-t2t.opts
+t2t.code
 ---
-- {'sql': 'CREATE TRIGGER t2t AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO t_out
-    VALUES(2); END'}
+- CREATE TRIGGER t2t AFTER INSERT ON t FOR EACH ROW BEGIN INSERT INTO t_out VALUES(2);
+  END
 ...
 assert(t1t.space_id == t2t.space_id)
 ---
diff --git a/test/sql/upgrade.test.lua b/test/sql/upgrade.test.lua
index 37425ae21..79231b34b 100644
--- a/test/sql/upgrade.test.lua
+++ b/test/sql/upgrade.test.lua
@@ -29,9 +29,9 @@ box.space._space.index['name']:get('T_OUT')
 t1t = box.space._trigger:get('T1T')
 t2t = box.space._trigger:get('T2T')
 t1t.name
-t1t.opts
+t1t.code
 t2t.name
-t2t.opts
+t2t.code
 assert(t1t.space_id == t2t.space_id)
 assert(t1t.space_id == box.space.T.id)
 
-- 
2.23.0



More information about the Tarantool-patches mailing list