From 037e2e449be8468fbf86bf9fccb60f2849eedc6b Mon Sep 17 00:00:00 2001
From: Nikita Pettik <korablev@tarantool.org>
Date: Sun, 23 Dec 2018 18:10:11 +0200
Subject: [PATCH] sql: clean-up affinity from SQL source code

Replace remains of affinity usage in SQL parser, query optimizer and
VDBE. Don't add affinity to field definition when table is encoded into
msgpack.  Remove field type <-> affinity converters, since now we can
operate directly on field type.

Part of #3698
---
 src/box/field_def.h     |  16 ++++
 src/box/sql.c           |   6 +-
 src/box/sql/build.c     |  53 -------------
 src/box/sql/delete.c    |   2 +-
 src/box/sql/expr.c      | 101 +++++++++++++-----------
 src/box/sql/fkey.c      |   4 +-
 src/box/sql/insert.c    |  41 ++--------
 src/box/sql/select.c    |  54 ++++++-------
 src/box/sql/sqliteInt.h |  83 ++++----------------
 src/box/sql/vdbe.c      |  51 +++++-------
 src/box/sql/vdbeInt.h   |   3 +-
 src/box/sql/vdbeapi.c   |   2 +-
 src/box/sql/vdbeaux.c   |   5 +-
 src/box/sql/vdbemem.c   |  28 +++----
 src/box/sql/where.c     |  11 +--
 src/box/sql/wherecode.c | 166 ++++++++++++++++++++--------------------
 src/box/sql/whereexpr.c |   9 ++-
 test/sql/types.result   |  15 ++--
 test/sql/upgrade.result |   6 +-
 19 files changed, 256 insertions(+), 400 deletions(-)

diff --git a/src/box/field_def.h b/src/box/field_def.h
index 93e38ea552..9673dab4e9 100644
--- a/src/box/field_def.h
+++ b/src/box/field_def.h
@@ -87,6 +87,22 @@ affinity_type_str(enum affinity_type type);
 
 /** \endcond public */
 
+enum {
+	/**
+	 * This mask allows to store in VdbeOp.p5 operand of
+	 * OP_Eq, OP_Lt etc opcodes field type alongside with
+	 * flags.
+	 */
+	FIELD_TYPE_MASK = 15
+};
+
+/**
+ * For detailed explanation see context of OP_Eq, OP_Lt etc
+ * opcodes in vdbe.c.
+ */
+static_assert((int) field_type_MAX <= (int) FIELD_TYPE_MASK,
+	      "values of enum field_type should fit into 4 bits of VdbeOp.p5");
+
 extern const char *field_type_strs[];
 
 extern const char *on_conflict_action_strs[];
diff --git a/src/box/sql.c b/src/box/sql.c
index baa67da397..39b5fb2f72 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -992,7 +992,7 @@ sql_encode_table(struct region *region, struct Table *table, uint32_t *size)
 		uint32_t cid = def->fields[i].coll_id;
 		struct field_def *field = &def->fields[i];
 		const char *default_str = field->default_value;
-		int base_len = 5;
+		int base_len = 4;
 		if (cid != COLL_NONE)
 			base_len += 1;
 		if (default_str != NULL)
@@ -1004,10 +1004,6 @@ sql_encode_table(struct region *region, struct Table *table, uint32_t *size)
 		assert(def->fields[i].is_nullable ==
 		       action_is_nullable(def->fields[i].nullable_action));
 		mpstream_encode_str(&stream, field_type_strs[field->type]);
-		mpstream_encode_str(&stream, "affinity");
-		enum affinity_type aff =
-			sql_field_type_to_affinity(def->fields[i].type);
-		mpstream_encode_uint(&stream, aff);
 		mpstream_encode_str(&stream, "is_nullable");
 		mpstream_encode_bool(&stream, def->fields[i].is_nullable);
 		mpstream_encode_str(&stream, "nullable_action");
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index b8503c6b18..9b3580ef78 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -485,59 +485,6 @@ sql_field_retrieve(Parse *parser, Table *table, uint32_t id)
 	return field;
 }
 
-enum field_type
-sql_affinity_to_field_type(enum affinity_type affinity)
-{
-	switch (affinity) {
-		case AFFINITY_INTEGER:
-			return FIELD_TYPE_INTEGER;
-		case AFFINITY_REAL:
-			return FIELD_TYPE_NUMBER;
-		case AFFINITY_TEXT:
-			return FIELD_TYPE_STRING;
-		case AFFINITY_BLOB:
-			return FIELD_TYPE_SCALAR;
-		case AFFINITY_UNDEFINED:
-			return FIELD_TYPE_ANY;
-		default:
-			unreachable();
-	}
-}
-
-enum affinity_type
-sql_field_type_to_affinity(enum field_type field_type)
-{
-	switch (field_type) {
-		case FIELD_TYPE_INTEGER:
-		case FIELD_TYPE_UNSIGNED:
-			return AFFINITY_INTEGER;
-		case FIELD_TYPE_NUMBER:
-			return AFFINITY_REAL;
-		case FIELD_TYPE_STRING:
-			return AFFINITY_TEXT;
-		case FIELD_TYPE_SCALAR:
-			return AFFINITY_BLOB;
-		case FIELD_TYPE_ANY:
-			return AFFINITY_UNDEFINED;
-		default:
-			unreachable();
-	}
-}
-
-enum field_type *
-sql_affinity_str_to_field_type_str(const char *affinity_str, uint32_t len)
-{
-	if (affinity_str == NULL)
-		return NULL;
-	size_t sz = (len + 1) * sizeof(enum field_type);
-	enum field_type *types =
-		(enum field_type *) sqlite3DbMallocRaw(sql_get(), sz);
-	for (uint32_t i = 0; i < len; ++i)
-		types[i] = sql_affinity_to_field_type(affinity_str[i]);
-	types[len] = field_type_MAX;
-	return types;
-}
-
 /*
  * Add a new column to the table currently being constructed.
  *
diff --git a/src/box/sql/delete.c b/src/box/sql/delete.c
index a6e7547f5c..5a3be48623 100644
--- a/src/box/sql/delete.c
+++ b/src/box/sql/delete.c
@@ -587,7 +587,7 @@ sql_generate_index_key(struct Parse *parse, struct index *index, int cursor,
 		sqlite3ExprCodeGetColumnOfTable(v, space->def, cursor, tabl_col,
 						reg_base + j);
 		/*
-		 * If the column affinity is REAL but the number
+		 * If the column type is NUMBER but the number
 		 * is an integer, then it might be stored in the
 		 * table as an integer (using a compact
 		 * representation) then converted to REAL by an
diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
index 36da1ecdb5..06359523dc 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -121,6 +121,19 @@ sql_expr_type(struct Expr *pExpr)
 	return pExpr->type;
 }
 
+enum field_type *
+field_type_sequence_dup(struct Parse *parse, enum field_type *types,
+			uint32_t len)
+{
+	uint32_t sz = (len + 1) * sizeof(enum field_type);
+	enum field_type *ret_types = sqlite3DbMallocRaw(parse->db, sz);
+	if (ret_types == NULL)
+		return NULL;
+	memcpy(ret_types, types, sz);
+	ret_types[len] = field_type_MAX;
+	return ret_types;
+}
+
 /*
  * Set the collating sequence for expression pExpr to be the collating
  * sequence named by pToken.   Return a pointer to a new Expr node that
@@ -283,8 +296,7 @@ binaryCompareP5(Expr * pExpr1, Expr * pExpr2, int jumpIfNull)
 {
 	enum field_type lhs = sql_expr_type(pExpr2);
 	enum field_type rhs = sql_expr_type(pExpr1);
-	u8 type_mask = sql_field_type_to_affinity(sql_type_result(rhs, lhs)) |
-		       (u8) jumpIfNull;
+	u8 type_mask = sql_type_result(rhs, lhs) | (u8) jumpIfNull;
 	return type_mask;
 }
 
@@ -2354,15 +2366,15 @@ sqlite3FindInIndex(Parse * pParse,	/* Parsing context */
 		pTab = p->pSrc->a[0].pTab;
 		assert(v);	/* sqlite3GetVdbe() has always been previously called */
 
-		int affinity_ok = 1;
+		bool type_is_suitable = true;
 		int i;
 
-		/* Check that the affinity that will be used to perform each
-		 * comparison is the same as the affinity of each column in table
+		/* Check that the type that will be used to perform each
+		 * comparison is the same as the type of each column in table
 		 * on the RHS of the IN operator.  If it not, it is not possible to
 		 * use any index of the RHS table.
 		 */
-		for (i = 0; i < nExpr && affinity_ok; i++) {
+		for (i = 0; i < nExpr && type_is_suitable; i++) {
 			Expr *pLhs = sqlite3VectorFieldSubexpr(pX->pLeft, i);
 			int iCol = pEList->a[i].pExpr->iColumn;
 			/* RHS table */
@@ -2374,10 +2386,10 @@ sqlite3FindInIndex(Parse * pParse,	/* Parsing context */
 			 * of columns match.
 			 */
 			if (idx_type != lhs_type)
-				affinity_ok = 0;
+				type_is_suitable = false;
 		}
 
-		if (affinity_ok) {
+		if (type_is_suitable) {
 			/*
 			 * Here we need real space since further
 			 * it is used in cursor opening routine.
@@ -2494,7 +2506,7 @@ sqlite3FindInIndex(Parse * pParse,	/* Parsing context */
 					sqlite3VdbeJumpHere(v, iAddr);
 				}
 			}	/* End loop over indexes */
-		}	/* End if( affinity_ok ) */
+		}
 	}
 
 	/* End attempt to optimize using an index */
@@ -2543,22 +2555,22 @@ sqlite3FindInIndex(Parse * pParse,	/* Parsing context */
 
 /*
  * Argument pExpr is an (?, ?...) IN(...) expression. This
- * function allocates and returns a nul-terminated string containing
- * the affinities to be used for each column of the comparison.
+ * function allocates and returns a terminated string containing
+ * the types to be used for each column of the comparison.
  *
  * It is the responsibility of the caller to ensure that the returned
  * string is eventually freed using sqlite3DbFree().
  */
-static char *
-exprINAffinity(Parse * pParse, Expr * pExpr)
+static enum field_type *
+expr_in_type(Parse *pParse, Expr *pExpr)
 {
 	Expr *pLeft = pExpr->pLeft;
 	int nVal = sqlite3ExprVectorSize(pLeft);
 	Select *pSelect = (pExpr->flags & EP_xIsSelect) ? pExpr->x.pSelect : 0;
-	char *zRet;
 
 	assert(pExpr->op == TK_IN);
-	zRet = sqlite3DbMallocZero(pParse->db, nVal + 1);
+	uint32_t sz = (nVal + 1) * sizeof(enum field_type);
+	enum field_type *zRet = sqlite3DbMallocZero(pParse->db, sz);
 	if (zRet) {
 		int i;
 		for (i = 0; i < nVal; i++) {
@@ -2567,12 +2579,12 @@ exprINAffinity(Parse * pParse, Expr * pExpr)
 			if (pSelect) {
 				struct Expr *e = pSelect->pEList->a[i].pExpr;
 				enum field_type rhs = sql_expr_type(e);
-				zRet[i] = sql_field_type_to_affinity(sql_type_result(rhs, lhs));
+				zRet[i] = sql_type_result(rhs, lhs);
 			} else {
-				zRet[i] = sql_field_type_to_affinity(lhs);
+				zRet[i] = lhs;
 			}
 		}
-		zRet[nVal] = '\0';
+		zRet[nVal] = field_type_MAX;
 	}
 	return zRet;
 }
@@ -2686,12 +2698,12 @@ sqlite3CodeSubselect(Parse * pParse,	/* Parsing context */
 			 * SELECT or the <exprlist>.
 			 *
 			 * If the 'x' expression is a column value, or the SELECT...
-			 * statement returns a column value, then the affinity of that
+			 * statement returns a column value, then the type of that
 			 * column is used to build the index keys. If both 'x' and the
-			 * SELECT... statement are columns, then numeric affinity is used
-			 * if either column has NUMERIC or INTEGER affinity. If neither
-			 * 'x' nor the SELECT... statement are columns, then numeric affinity
-			 * is used.
+			 * SELECT... statement are columns, then NUMBER type is used
+			 * if either column has NUMBER or INTEGER type. If neither
+			 * 'x' nor the SELECT... statement are columns,
+			 * then NUMBER type is used.
 			 */
 			pExpr->iTable = pParse->nTab++;
 			pExpr->is_ephemeral = 1;
@@ -2721,8 +2733,8 @@ sqlite3CodeSubselect(Parse * pParse,	/* Parsing context */
 					int i;
 					sqlite3SelectDestInit(&dest, SRT_Set,
 							      pExpr->iTable, reg_eph);
-					dest.zAffSdst =
-					    exprINAffinity(pParse, pExpr);
+					dest.dest_type =
+						expr_in_type(pParse, pExpr);
 					assert((pExpr->iTable & 0x0000FFFF) ==
 					       pExpr->iTable);
 					pSelect->iLimit = 0;
@@ -2731,12 +2743,12 @@ sqlite3CodeSubselect(Parse * pParse,	/* Parsing context */
 					if (sqlite3Select
 					    (pParse, pSelect, &dest)) {
 						sqlite3DbFree(pParse->db,
-							      dest.zAffSdst);
+							      dest.dest_type);
 						sql_key_info_unref(key_info);
 						return 0;
 					}
 					sqlite3DbFree(pParse->db,
-						      dest.zAffSdst);
+						      dest.dest_type);
 					assert(pEList != 0);
 					assert(pEList->nExpr > 0);
 					for (i = 0; i < nVal; i++) {
@@ -2753,8 +2765,8 @@ sqlite3CodeSubselect(Parse * pParse,	/* Parsing context */
 				 *
 				 * For each expression, build an index key from the evaluation and
 				 * store it in the temporary table. If <expr> is a column, then use
-				 * that columns affinity when building index keys. If <expr> is not
-				 * a column, use numeric affinity.
+				 * that columns types when building index keys. If <expr> is not
+				 * a column, use NUMBER type.
 				 */
 				int i;
 				ExprList *pList = pExpr->x.pList;
@@ -2790,8 +2802,8 @@ sqlite3CodeSubselect(Parse * pParse,	/* Parsing context */
 	 				sqlite3VdbeAddOp4(v, OP_MakeRecord, r3,
 							  1, r2, (char *)types,
 							  sizeof(types));
-					sqlite3ExprCacheAffinityChange(pParse,
-								       r3, 1);
+					sql_expr_type_cache_change(pParse,
+								   r3, 1);
 					sqlite3VdbeAddOp2(v, OP_IdxInsert, r2,
 							  reg_eph);
 				}
@@ -2942,7 +2954,6 @@ sqlite3ExprCodeIN(Parse * pParse,	/* Parsing and code generating context */
 	int rLhsOrig;		/* LHS values prior to reordering by aiMap[] */
 	Vdbe *v;		/* Statement under construction */
 	int *aiMap = 0;		/* Map from vector field to index column */
-	char *zAff = 0;		/* Affinity string for comparisons */
 	int nVector;		/* Size of vectors for this IN operator */
 	int iDummy;		/* Dummy parameter to exprCodeVector() */
 	Expr *pLeft;		/* The LHS of the IN operator */
@@ -2956,7 +2967,8 @@ sqlite3ExprCodeIN(Parse * pParse,	/* Parsing and code generating context */
 	pLeft = pExpr->pLeft;
 	if (sqlite3ExprCheckIN(pParse, pExpr))
 		return;
-	zAff = exprINAffinity(pParse, pExpr);
+	/* Type sequence for comparisons. */
+	enum field_type *zAff = expr_in_type(pParse, pExpr);
 	nVector = sqlite3ExprVectorSize(pExpr->pLeft);
 	aiMap =
 	    (int *)sqlite3DbMallocZero(pParse->db,
@@ -3102,10 +3114,14 @@ sqlite3ExprCodeIN(Parse * pParse,	/* Parsing and code generating context */
 	 * of the RHS using the LHS as a probe.  If found, the result is
 	 * true.
 	 */
-	enum field_type *types = sql_affinity_str_to_field_type_str(zAff,
-								    nVector);
-	sqlite3VdbeAddOp4(v, OP_ApplyType, rLhs, nVector, 0, (char *)types,
+	zAff[nVector] = field_type_MAX;
+	sqlite3VdbeAddOp4(v, OP_ApplyType, rLhs, nVector, 0, (char*)zAff,
 			  P4_DYNAMIC);
+	/*
+	 * zAff will be freed at the end of VDBE execution, since
+	 * it was passed with P4_DYNAMIC flag.
+	 */
+	zAff = NULL;
 	if (destIfFalse == destIfNull) {
 		/* Combine Step 3 and Step 5 into a single opcode */
 		sqlite3VdbeAddOp4Int(v, OP_NotFound, pExpr->iTable,
@@ -3494,11 +3510,11 @@ sqlite3ExprCacheClear(Parse * pParse)
 }
 
 /*
- * Record the fact that an affinity change has occurred on iCount
+ * Record the fact that an type change has occurred on iCount
  * registers starting with iStart.
  */
 void
-sqlite3ExprCacheAffinityChange(Parse * pParse, int iStart, int iCount)
+sql_expr_type_cache_change(Parse *pParse, int iStart, int iCount)
 {
 	sqlite3ExprCacheRemove(pParse, iStart, iCount);
 }
@@ -3734,7 +3750,7 @@ sqlite3ExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 			}
 			sqlite3VdbeAddOp2(v, OP_Cast, target, pExpr->type);
 			testcase(usedAsColumnCache(pParse, inReg, inReg));
-			sqlite3ExprCacheAffinityChange(pParse, inReg, 1);
+			sql_expr_type_cache_change(pParse, inReg, 1);
 			return inReg;
 		}
 #endif				/* SQLITE_OMIT_CAST */
@@ -3934,7 +3950,7 @@ sqlite3ExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 			} else {
 				/*
 				 * Otherwise, use first arg as
-				 * expression affinity.
+				 * expression type.
 				 */
 				if (pFarg && pFarg->nExpr > 0)
 					pExpr->type = pFarg->a[0].pExpr->type;
@@ -4162,11 +4178,8 @@ sqlite3ExprCodeTarget(Parse * pParse, Expr * pExpr, int target)
 					pExpr->iColumn].name, target));
 
 #ifndef SQLITE_OMIT_FLOATING_POINT
-			/* If the column has REAL affinity, it may currently be stored as an
+			/* If the column has type NUMBER, it may currently be stored as an
 			 * integer. Use OP_Realify to make sure it is really real.
-			 *
-			 * EVIDENCE-OF: R-60985-57662 SQLite will convert the value back to
-			 * floating point when extracting it from the record.
 			 */
 			if (pExpr->iColumn >= 0 && def->fields[
 				pExpr->iColumn].type == FIELD_TYPE_NUMBER) {
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index d5a7283827..01fe1c84d3 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -423,7 +423,7 @@ fkey_scan_children(struct Parse *parser, struct SrcList *src, struct Table *tab,
 	 * <parent-key1> = <child-key1> AND <parent-key2> = <child-key2> ...
 	 *
 	 * The collation sequence used for the comparison should
-	 * be that of the parent key columns. The affinity of the
+	 * be that of the parent key columns. The type of the
 	 * parent key column should be applied to each child key
 	 * value before the comparison takes place.
 	 */
@@ -783,7 +783,7 @@ fkey_action_trigger(struct Parse *pParse, struct Table *pTab, struct fkey *fkey,
 		 * Create the expression "old.to_col = from_col".
 		 * It is important that the "old.to_col" term is
 		 * on the LHS of the = operator, so that the
-		 * affinity and collation sequence associated with
+		 * type and collation sequence associated with
 		 * the parent table are used for the comparison.
 		 */
 		struct Expr *to_col =
diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c
index 35f30f191b..c69476117a 100644
--- a/src/box/sql/insert.c
+++ b/src/box/sql/insert.c
@@ -41,33 +41,6 @@
 #include "bit/bit.h"
 #include "box/box.h"
 
-char *
-sql_space_index_affinity_str(struct sqlite3 *db, struct space_def *space_def,
-			     struct index_def *idx_def)
-{
-	uint32_t column_count = idx_def->key_def->part_count;
-	char *aff = (char *)sqlite3DbMallocRaw(db, column_count + 1);
-	if (aff == NULL)
-		return NULL;
-	/*
-	 * Table may occasionally come from non-SQL API, so lets
-	 * gentle process this case by setting default affinity
-	 * for it.
-	 */
-	if (space_def->fields == NULL) {
-		memset(aff, AFFINITY_BLOB, column_count);
-	} else {
-		for (uint32_t i = 0; i < column_count; i++) {
-			aff[i] = sql_space_index_part_affinity(space_def,
-							       idx_def, i);
-			if (aff[i] == AFFINITY_UNDEFINED)
-				aff[i] = AFFINITY_BLOB;
-		}
-	}
-	aff[column_count] = '\0';
-	return aff;
-}
-
 enum field_type *
 sql_index_type_str(struct sqlite3 *db, const struct index_def *idx_def)
 {
@@ -630,7 +603,7 @@ sqlite3Insert(Parse * pParse,	/* Parser context */
 		/* If this is an INSERT on a view with an INSTEAD OF INSERT trigger,
 		 * do not attempt any conversions before assembling the record.
 		 * If this is a real table, attempt conversions as required by the
-		 * table column affinities.
+		 * table column types.
 		 */
 		if (!is_view)
 			sql_emit_table_types(v, pTab->def, regCols + 1);
@@ -1225,12 +1198,12 @@ xferOptimization(Parse * pParse,	/* Parser context */
 	if (dest->def->field_count != src->def->field_count)
 		return 0;
 	for (i = 0; i < (int)dest->def->field_count; i++) {
-		enum affinity_type dest_affinity =
-			dest->def->fields[i].affinity;
-		enum affinity_type src_affinity =
-			src->def->fields[i].affinity;
-		/* Affinity must be the same on all columns. */
-		if (dest_affinity != src_affinity)
+		enum field_type dest_type =
+			dest->def->fields[i].type;
+		enum field_type src_type =
+			src->def->fields[i].type;
+		/* Type must be the same on all columns. */
+		if (dest_type != src_type)
 			return 0;
 		uint32_t id;
 		if (sql_column_collation(dest->def, i, &id) !=
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index 7407b559f7..4d2894616b 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -134,7 +134,7 @@ sqlite3SelectDestInit(SelectDest * pDest, int eDest, int iParm, int reg_eph)
 	pDest->eDest = (u8) eDest;
 	pDest->iSDParm = iParm;
 	pDest->reg_eph = reg_eph;
-	pDest->zAffSdst = 0;
+	pDest->dest_type = NULL;
 	pDest->iSdst = 0;
 	pDest->nSdst = 0;
 }
@@ -1200,18 +1200,16 @@ selectInnerLoop(Parse * pParse,		/* The parser context */
 					       regOrig, nResultCol, nPrefixReg);
 			} else {
 				int r1 = sqlite3GetTempReg(pParse);
-				assert(sqlite3Strlen30(pDest->zAffSdst) ==
-				       (unsigned int)nResultCol);
 				enum field_type *types =
-					sql_affinity_str_to_field_type_str(
-						pDest->zAffSdst,
-						strlen(pDest->zAffSdst));
+					field_type_sequence_dup(pParse,
+								pDest->dest_type,
+								nResultCol);
 				sqlite3VdbeAddOp4(v, OP_MakeRecord, regResult,
 						  nResultCol, r1, (char *)types,
 						  P4_DYNAMIC);
-				sqlite3ExprCacheAffinityChange(pParse,
-							       regResult,
-							       nResultCol);
+				sql_expr_type_cache_change(pParse,
+							   regResult,
+							   nResultCol);
 				sqlite3VdbeAddOp2(v, OP_IdxInsert, r1, pDest->reg_eph);
 				sqlite3ReleaseTempReg(pParse, r1);
 			}
@@ -1255,9 +1253,9 @@ selectInnerLoop(Parse * pParse,		/* The parser context */
 			} else {
 				sqlite3VdbeAddOp2(v, OP_ResultRow, regResult,
 						  nResultCol);
-				sqlite3ExprCacheAffinityChange(pParse,
-							       regResult,
-							       nResultCol);
+				sql_expr_type_cache_change(pParse,
+							   regResult,
+							   nResultCol);
 			}
 			break;
 		}
@@ -1628,16 +1626,13 @@ generateSortTail(Parse * pParse,	/* Parsing context */
 			break;
 		}
 	case SRT_Set:{
-			assert((unsigned int)nColumn ==
-			       sqlite3Strlen30(pDest->zAffSdst));
-
 			enum field_type *types =
-				sql_affinity_str_to_field_type_str(pDest->zAffSdst,
-								   strlen(pDest->zAffSdst));
+				field_type_sequence_dup(pParse, pDest->dest_type,
+							nColumn);
 			sqlite3VdbeAddOp4(v, OP_MakeRecord, regRow, nColumn,
 					  regTupleid, (char *)types,
 					  P4_DYNAMIC);
-			sqlite3ExprCacheAffinityChange(pParse, regRow, nColumn);
+			sql_expr_type_cache_change(pParse, regRow, nColumn);
 			sqlite3VdbeAddOp2(v, OP_IdxInsert, regTupleid, pDest->reg_eph);
 			break;
 		}
@@ -1652,9 +1647,9 @@ generateSortTail(Parse * pParse,	/* Parsing context */
 			if (eDest == SRT_Output) {
 				sqlite3VdbeAddOp2(v, OP_ResultRow, pDest->iSdst,
 						  nColumn);
-				sqlite3ExprCacheAffinityChange(pParse,
-							       pDest->iSdst,
-							       nColumn);
+				sql_expr_type_cache_change(pParse,
+							   pDest->iSdst,
+							   nColumn);
 			} else {
 				sqlite3VdbeAddOp1(v, OP_Yield, pDest->iSDParm);
 			}
@@ -1968,9 +1963,7 @@ sqlite3SelectAddColumnTypeAndCollation(Parse * pParse,		/* Parsing contexts */
 	a = pSelect->pEList->a;
 	for (uint32_t i = 0; i < pTab->def->field_count; i++) {
 		p = a[i].pExpr;
-		enum field_type type = sql_expr_type(p);
-		pTab->def->fields[i].affinity = sql_field_type_to_affinity(type);
-		pTab->def->fields[i].type = type;
+		pTab->def->fields[i].type = sql_expr_type(p);
 		bool is_found;
 		uint32_t coll_id;
 
@@ -3090,13 +3083,13 @@ generateOutputSubroutine(struct Parse *parse, struct Select *p,
 			testcase(in->nSdst > 1);
 			r1 = sqlite3GetTempReg(parse);
 			enum field_type *types =
-				sql_affinity_str_to_field_type_str(dest->zAffSdst,
-								   strlen(dest->zAffSdst));
+				field_type_sequence_dup(parse, dest->dest_type,
+							in->nSdst);
 			sqlite3VdbeAddOp4(v, OP_MakeRecord, in->iSdst,
 					  in->nSdst, r1, (char *)types,
 					  P4_DYNAMIC);
-			sqlite3ExprCacheAffinityChange(parse, in->iSdst,
-						       in->nSdst);
+			sql_expr_type_cache_change(parse, in->iSdst,
+						   in->nSdst);
 			sqlite3VdbeAddOp2(v, OP_IdxInsert, r1, dest->reg_eph);
 			sqlite3ReleaseTempReg(parse, r1);
 			break;
@@ -3141,8 +3134,7 @@ generateOutputSubroutine(struct Parse *parse, struct Select *p,
 			assert(dest->eDest == SRT_Output);
 			sqlite3VdbeAddOp2(v, OP_ResultRow, in->iSdst,
 					  in->nSdst);
-			sqlite3ExprCacheAffinityChange(parse, in->iSdst,
-						       in->nSdst);
+			sql_expr_type_cache_change(parse, in->iSdst, in->nSdst);
 			break;
 		}
 	}
@@ -5370,7 +5362,7 @@ updateAccumulator(Parse * pParse, AggInfo * pAggInfo)
 		sqlite3VdbeAddOp3(v, OP_AggStep0, 0, regAgg, pF->iMem);
 		sqlite3VdbeAppendP4(v, pF->pFunc, P4_FUNCDEF);
 		sqlite3VdbeChangeP5(v, (u8) nArg);
-		sqlite3ExprCacheAffinityChange(pParse, regAgg, nArg);
+		sql_expr_type_cache_change(pParse, regAgg, nArg);
 		sqlite3ReleaseTempRange(pParse, regAgg, nArg);
 		if (addrNext) {
 			sqlite3VdbeResolveLabel(v, addrNext);
diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h
index 61f3165501..7d4675f592 100644
--- a/src/box/sql/sqliteInt.h
+++ b/src/box/sql/sqliteInt.h
@@ -1629,7 +1629,7 @@ struct sqlite3 {
 #define SQLITE_MAGIC_ZOMBIE   0x64cffc7f	/* Close with last statement close */
 
 /**
- * SQL type definition. Now it is an alias to affinity, but in
+ * SQL type definition. Now it is an alias to type, but in
  * future it will have some attributes like number of chars in
  * VARCHAR(<number of chars>).
  */
@@ -1793,23 +1793,17 @@ struct Savepoint {
 				 (X) == FIELD_TYPE_UNSIGNED)
 
 /*
- * The AFFINITY_MASK values masks off the significant bits of an
- * affinity value.
- */
-#define AFFINITY_MASK     0x47
-
-/*
- * Additional bit values that can be ORed with an affinity without
- * changing the affinity.
+ * Additional bit values that can be ORed with an type without
+ * changing the type.
  *
  * The SQLITE_NOTNULL flag is a combination of NULLEQ and JUMPIFNULL.
  * It causes an assert() to fire if either operand to a comparison
  * operator is NULL.  It is added to certain comparison operators to
  * prove that the operands are always NOT NULL.
  */
-#define SQLITE_KEEPNULL     0x08	/* Used by vector == or <> */
 #define SQLITE_JUMPIFNULL   0x10	/* jumps if either operand is NULL */
 #define SQLITE_STOREP2      0x20	/* Store result in reg[P2] rather than jump */
+#define SQLITE_KEEPNULL     0x40	/* Used by vector == or <> */
 #define SQLITE_NULLEQ       0x80	/* NULL=NULL */
 #define SQLITE_NOTNULL      0x90	/* Assert that operands are never NULL */
 
@@ -2556,7 +2550,7 @@ struct Select {
  *
  *     SRT_Set         The result must be a single column.  Store each
  *                     row of result as the key in table pDest->iSDParm.
- *                     Apply the affinity pDest->affSdst before storing
+ *                     Apply the type pDest->det_type before storing
  *                     results.  Used to implement "IN (SELECT ...)".
  *
  *     SRT_EphemTab    Create an temporary table pDest->iSDParm and store
@@ -2615,7 +2609,8 @@ struct Select {
  */
 struct SelectDest {
 	u8 eDest;		/* How to dispose of the results.  On of SRT_* above. */
-	char *zAffSdst;		/* Affinity used when eDest==SRT_Set */
+	/** Type used when eDest==SRT_Set */
+	enum field_type *dest_type;
 	int iSDParm;		/* A parameter used by the eDest disposal method */
 	/** Register containing ephemeral's space pointer. */
 	int reg_eph;
@@ -3445,19 +3440,6 @@ sql_create_view(struct Parse *parse_context, struct Token *begin,
 		struct Token *name, struct ExprList *aliases,
 		struct Select *select, bool if_exists);
 
-/**
- * Helper to convert SQLite affinity to corresponding
- * Tarantool field type.
- **/
-enum field_type
-sql_affinity_to_field_type(enum affinity_type affinity);
-
-enum affinity_type
-sql_field_type_to_affinity(enum field_type field_type);
-
-enum field_type *
-sql_affinity_str_to_field_type_str(const char *affinity_str, uint32_t len);
-
 /**
  * Compile view, i.e. create struct Select from
  * 'CREATE VIEW...' string, and assign cursors to each table from
@@ -3658,7 +3640,7 @@ void sqlite3ExprCachePush(Parse *);
 void sqlite3ExprCachePop(Parse *);
 void sqlite3ExprCacheRemove(Parse *, int, int);
 void sqlite3ExprCacheClear(Parse *);
-void sqlite3ExprCacheAffinityChange(Parse *, int, int);
+void sql_expr_type_cache_change(Parse *, int, int);
 void sqlite3ExprCode(Parse *, Expr *, int);
 void sqlite3ExprCodeCopy(Parse *, Expr *, int);
 void sqlite3ExprCodeFactorable(Parse *, Expr *, int);
@@ -4239,33 +4221,6 @@ int sqlite3VarintLen(u64 v);
 #define getVarint    sqlite3GetVarint
 #define putVarint    sqlite3PutVarint
 
-/**
- * Return a pointer to the column affinity string associated with
- * given index. A column affinity string has one character for
- * each column in the table, according to the affinity of the
- * column:
- *
- *  Character      Column affinity
- *  ------------------------------
- *  'A'            BLOB
- *  'B'            TEXT
- *  'C'            NUMERIC
- *  'D'            INTEGER
- *  'F'            REAL
- *
- * Memory for the buffer containing the column index affinity string
- * is allocated on heap.
- *
- * @param db Database handle.
- * @param space_def Definition of space index belongs to.
- * @param idx_def Definition of index from which affinity
- *                to be extracted.
- * @retval Allocated affinity string, or NULL on OOM.
- */
-char *
-sql_space_index_affinity_str(struct sqlite3 *db, struct space_def *space_def,
-			     struct index_def *idx_def);
-
 /** Return string consisting of fields types of given index. */
 enum field_type *
 sql_index_type_str(struct sqlite3 *db, const struct index_def *idx_def);
@@ -4311,6 +4266,13 @@ expr_cmp_mutual_type(struct Expr *pExpr);
 enum field_type
 sql_expr_type(struct Expr *pExpr);
 
+/**
+ * This function duplicates first @len entries of types array
+ * and terminates new array with field_type_MAX member.
+ */
+enum field_type *
+field_type_sequence_dup(struct Parse *parse, enum field_type *types,
+			uint32_t len);
 
 /**
  * Convert z to a 64-bit signed integer.  z must be decimal. This
@@ -4414,7 +4376,7 @@ void sqlite3ValueFree(sqlite3_value *);
 sqlite3_value *sqlite3ValueNew(sqlite3 *);
 int sqlite3ValueFromExpr(sqlite3 *, Expr *, enum field_type type,
 			 sqlite3_value **);
-void sqlite3ValueApplyAffinity(sqlite3_value *, enum field_type type);
+void sql_value_apply_type(sqlite3_value *val, enum field_type type);
 #ifndef SQLITE_AMALGAMATION
 extern const unsigned char sqlite3OpcodeProperty[];
 extern const char sqlite3StrBINARY[];
@@ -4662,19 +4624,6 @@ int
 sql_stat4_column(struct sqlite3 *db, const char *record, uint32_t col_num,
 		 sqlite3_value **res);
 
-/**
- * Return the affinity for a single column of an index.
- *
- * @param def Definition of space @idx belongs to.
- * @param idx Index to be investigated.
- * @param partno Affinity of this part to be returned.
- *
- * @retval Affinity of @partno index part.
- */
-enum affinity_type
-sql_space_index_part_affinity(struct space_def *def, struct index_def *idx,
-			      uint32_t partno);
-
 /*
  * The interface to the LEMON-generated parser
  */
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 02abc00888..51ba9ac9f3 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -289,7 +289,7 @@ allocateCursor(
  * if there is an exact integer representation of the quantity.
  */
 static int
-applyNumericAffinity(Mem *pRec, int bTryForInt)
+mem_apply_numeric_type(Mem *pRec, int bTryForInt)
 {
 	double rValue;
 	i64 iValue;
@@ -301,7 +301,7 @@ applyNumericAffinity(Mem *pRec, int bTryForInt)
 	} else {
 		pRec->u.r = rValue;
 		pRec->flags |= MEM_Real;
-		if (bTryForInt) sqlite3VdbeIntegerAffinity(pRec);
+		if (bTryForInt) mem_apply_integer_type(pRec);
 	}
 	return 0;
 }
@@ -382,7 +382,7 @@ int sqlite3_value_numeric_type(sqlite3_value *pVal) {
 	int eType = sqlite3_value_type(pVal);
 	if (eType==SQLITE_TEXT) {
 		Mem *pMem = (Mem*)pVal;
-		applyNumericAffinity(pMem, 0);
+		mem_apply_numeric_type(pMem, 0);
 		eType = sqlite3_value_type(pVal);
 	}
 	return eType;
@@ -393,7 +393,7 @@ int sqlite3_value_numeric_type(sqlite3_value *pVal) {
  * not the internal Mem* type.
  */
 void
-sqlite3ValueApplyAffinity(
+sql_value_apply_type(
 	sqlite3_value *pVal,
 	enum field_type type)
 {
@@ -421,7 +421,7 @@ static u16 SQLITE_NOINLINE computeNumericType(Mem *pMem)
  * Return the numeric type for pMem, either MEM_Int or MEM_Real or both or
  * none.
  *
- * Unlike applyNumericAffinity(), this routine does not modify pMem->flags.
+ * Unlike mem_apply_numeric_type(), this routine does not modify pMem->flags.
  * But it does set pMem->u.r and pMem->u.i appropriately.
  */
 static u16 numericType(Mem *pMem)
@@ -1681,7 +1681,7 @@ case OP_Remainder: {           /* same as TK_REM, in1, in2, out3 */
 		pOut->u.r = rB;
 		MemSetTypeFlag(pOut, MEM_Real);
 		if (((type1|type2)&MEM_Real)==0 && !bIntint) {
-			sqlite3VdbeIntegerAffinity(pOut);
+			mem_apply_integer_type(pOut);
 		}
 #endif
 	}
@@ -2023,14 +2023,6 @@ case OP_Cast: {                  /* in1 */
  * jump to address P2.  Or if the SQLITE_STOREP2 flag is set in P5, then
  * store the result of comparison in register P2.
  *
- * The AFFINITY_MASK portion of P5 must be an affinity character -
- * AFFINITY_TEXT, AFFINITY_INTEGER, and so forth. An attempt is made
- * to coerce both inputs according to this affinity before the
- * comparison is made. If the AFFINITY_MASK is 0x00, then numeric
- * affinity is used. Note that the affinity conversions are stored
- * back into the input registers P1 and P3.  So this opcode can cause
- * persistent changes to registers P1 and P3.
- *
  * Once any conversions have taken place, and neither value is NULL,
  * the values are compared. If both values are blobs then memcmp() is
  * used to determine the results of the comparison.  If both values
@@ -2046,6 +2038,10 @@ case OP_Cast: {                  /* in1 */
  * of comparison is true.  If either operand is NULL then the result is false.
  * If neither operand is NULL the result is the same as it would be if
  * the SQLITE_NULLEQ flag were omitted from P5.
+ * P5 also can contain type to be applied to operands. Note that
+ * the type conversions are stored back into the input registers
+ * P1 and P3.  So this opcode can cause persistent changes to
+ * registers P1 and P3.
  *
  * If both SQLITE_STOREP2 and SQLITE_KEEPNULL flags are set then the
  * content of r[P2] is only changed if the new value is NULL or 0 (false).
@@ -2073,14 +2069,6 @@ case OP_Cast: {                  /* in1 */
  * reg(P3) is NULL then the take the jump.  If the SQLITE_JUMPIFNULL
  * bit is clear then fall through if either operand is NULL.
  *
- * The AFFINITY_MASK portion of P5 must be an affinity character -
- * AFFINITY_TEXT, AFFINITY_INTEGER, and so forth. An attempt is made
- * to coerce both inputs according to this affinity before the
- * comparison is made. If the AFFINITY_MASK is 0x00, then numeric
- * affinity is used. Note that the affinity conversions are stored
- * back into the input registers P1 and P3.  So this opcode can cause
- * persistent changes to registers P1 and P3.
- *
  * Once any conversions have taken place, and neither value is NULL,
  * the values are compared. If both values are blobs then memcmp() is
  * used to determine the results of the comparison.  If both values
@@ -2119,7 +2107,6 @@ case OP_Le:               /* same as TK_LE, jump, in1, in3 */
 case OP_Gt:               /* same as TK_GT, jump, in1, in3 */
 case OP_Ge: {             /* same as TK_GE, jump, in1, in3 */
 	int res, res2;      /* Result of the comparison of pIn1 against pIn3 */
-	char affinity;      /* Affinity to use for comparison */
 	u32 flags1;         /* Copy of initial value of pIn1->flags */
 	u32 flags3;         /* Copy of initial value of pIn3->flags */
 
@@ -2164,17 +2151,16 @@ case OP_Ge: {             /* same as TK_GE, jump, in1, in3 */
 			break;
 		}
 	} else {
-		/* Neither operand is NULL.  Do a comparison. */
-		affinity = pOp->p5 & AFFINITY_MASK;
-		if (affinity>=AFFINITY_INTEGER) {
+		enum field_type type = pOp->p5 & FIELD_TYPE_MASK;
+		if (sql_type_is_numeric(type)) {
 			if ((flags1 | flags3)&MEM_Str) {
 				if ((flags1 & (MEM_Int|MEM_Real|MEM_Str))==MEM_Str) {
-					applyNumericAffinity(pIn1,0);
+					mem_apply_numeric_type(pIn1, 0);
 					testcase( flags3!=pIn3->flags); /* Possible if pIn1==pIn3 */
 					flags3 = pIn3->flags;
 				}
 				if ((flags3 & (MEM_Int|MEM_Real|MEM_Str))==MEM_Str) {
-					if (applyNumericAffinity(pIn3,0) != 0) {
+					if (mem_apply_numeric_type(pIn3, 0) != 0) {
 						diag_set(ClientError,
 							 ER_SQL_TYPE_MISMATCH,
 							 sqlite3_value_text(pIn3),
@@ -2194,7 +2180,7 @@ case OP_Ge: {             /* same as TK_GE, jump, in1, in3 */
 				res = 0;
 				goto compare_op;
 			}
-		} else if (affinity==AFFINITY_TEXT) {
+		} else if (type == FIELD_TYPE_STRING) {
 			if ((flags1 & MEM_Str)==0 && (flags1 & (MEM_Int|MEM_Real))!=0) {
 				testcase( pIn1->flags & MEM_Int);
 				testcase( pIn1->flags & MEM_Real);
@@ -2842,7 +2828,7 @@ case OP_ApplyType: {
  * in an index.  The OP_Column opcode can decode the record later.
  *
  * P4 may be a string that is P2 characters long.  The nth character of the
- * string indicates the column affinity that should be used for the nth
+ * string indicates the column type that should be used for the nth
  * field of the index key.
  *
  * If P4 is NULL then all index fields have type SCALAR.
@@ -2885,8 +2871,7 @@ case OP_MakeRecord: {
 	pOut = &aMem[pOp->p3];
 	memAboutToChange(p, pOut);
 
-	/* Apply the requested affinity to all inputs
-	 */
+	/* Apply the requested types to all inputs */
 	assert(pData0<=pLast);
 	if (types != NULL) {
 		pRec = pData0;
@@ -3520,7 +3505,7 @@ case OP_SeekGT: {       /* jump, in3 */
 		 */
 		pIn3 = &aMem[reg_ipk];
 		if ((pIn3->flags & (MEM_Int|MEM_Real|MEM_Str))==MEM_Str) {
-			applyNumericAffinity(pIn3, 0);
+			mem_apply_numeric_type(pIn3, 0);
 		}
 		int64_t i;
 		if ((pIn3->flags & MEM_Int) == MEM_Int) {
diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
index 32b1b6c174..1720fd1c7d 100644
--- a/src/box/sql/vdbeInt.h
+++ b/src/box/sql/vdbeInt.h
@@ -231,7 +231,6 @@ struct Mem {
 #define MEM_Blob      0x0010	/* Value is a BLOB */
 #define MEM_Bool      0x0020    /* Value is a bool */
 #define MEM_Ptr       0x0040	/* Value is a generic pointer */
-#define MEM_AffMask   0x003f	/* Mask of affinity bits */
 #define MEM_Frame     0x0080	/* Value is a VdbeFrame object */
 #define MEM_Undefined 0x0100	/* Value is undefined */
 #define MEM_Cleared   0x0200	/* NULL set by OP_Null, not from data */
@@ -480,7 +479,7 @@ int sqlite3VdbeMemStringify(Mem *, u8);
 int sqlite3VdbeIntValue(Mem *, int64_t *);
 int sqlite3VdbeMemIntegerify(Mem *, bool is_forced);
 int sqlite3VdbeRealValue(Mem *, double *);
-int sqlite3VdbeIntegerAffinity(Mem *);
+int mem_apply_integer_type(Mem *);
 int sqlite3VdbeMemRealify(Mem *);
 int sqlite3VdbeMemNumerify(Mem *);
 int sqlite3VdbeMemCast(Mem *, enum field_type type);
diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
index 9e57af051f..07c496207d 100644
--- a/src/box/sql/vdbeapi.c
+++ b/src/box/sql/vdbeapi.c
@@ -280,7 +280,7 @@ sqlite3_value_type(sqlite3_value * pVal)
 		SQLITE_INTEGER,	/* 0x1e */
 		SQLITE_NULL,	/* 0x1f */
 	};
-	return aType[pVal->flags & MEM_AffMask];
+	return aType[pVal->flags & MEM_TypeMask];
 }
 
 /* Make a copy of an sqlite3_value object
diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
index 975f114df3..5f2cb8448d 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -3493,8 +3493,7 @@ sqlite3VdbeDb(Vdbe * v)
 /*
  * Return a pointer to an sqlite3_value structure containing the value bound
  * parameter iVar of VM v. Except, if the value is an SQL NULL, return
- * 0 instead. Unless it is NULL, apply affinity aff (one of the AFFINITY_*
- * constants) to the value before returning it.
+ * 0 instead. Unless it is NULL, apply type to the value before returning it.
  *
  * The returned value must be freed by the caller using sqlite3ValueFree().
  */
@@ -3508,7 +3507,7 @@ sqlite3VdbeGetBoundValue(Vdbe * v, int iVar, u8 aff)
 			sqlite3_value *pRet = sqlite3ValueNew(v->db);
 			if (pRet) {
 				sqlite3VdbeMemCopy((Mem *) pRet, pMem);
-				sqlite3ValueApplyAffinity(pRet, aff);
+				sql_value_apply_type(pRet, aff);
 			}
 			return pRet;
 		}
diff --git a/src/box/sql/vdbemem.c b/src/box/sql/vdbemem.c
index a5dd7400a0..2865fd68c3 100644
--- a/src/box/sql/vdbemem.c
+++ b/src/box/sql/vdbemem.c
@@ -504,7 +504,7 @@ sqlite3VdbeRealValue(Mem * pMem, double *v)
  * MEM_Int if we can.
  */
 int
-sqlite3VdbeIntegerAffinity(Mem * pMem)
+mem_apply_integer_type(Mem *pMem)
 {
 	int rc;
 	i64 ix;
@@ -584,7 +584,7 @@ sqlite3VdbeMemNumerify(Mem * pMem)
 				return SQLITE_ERROR;
 			pMem->u.r = v;
 			MemSetTypeFlag(pMem, MEM_Real);
-			sqlite3VdbeIntegerAffinity(pMem);
+			mem_apply_integer_type(pMem);
 		}
 	}
 	assert((pMem->flags & (MEM_Int | MEM_Real | MEM_Null)) != 0);
@@ -593,10 +593,10 @@ sqlite3VdbeMemNumerify(Mem * pMem)
 }
 
 /*
- * Cast the datatype of the value in pMem according to the affinity
- * "aff".  Casting is different from applying affinity in that a cast
+ * Cast the datatype of the value in pMem according to the type
+ * @type.  Casting is different from applying type in that a cast
  * is forced.  In other words, the value is converted into the desired
- * affinity even if that results in loss of data.  This routine is
+ * type even if that results in loss of data.  This routine is
  * used (for example) to implement the SQL "cast()" operator.
  */
 int
@@ -643,7 +643,7 @@ sqlite3VdbeMemCast(Mem * pMem, enum field_type type)
 		assert(type == FIELD_TYPE_STRING);
 		assert(MEM_Str == (MEM_Blob >> 3));
 		pMem->flags |= (pMem->flags & MEM_Blob) >> 3;
-		sqlite3ValueApplyAffinity(pMem, FIELD_TYPE_STRING);
+			sql_value_apply_type(pMem, FIELD_TYPE_STRING);
 		assert(pMem->flags & MEM_Str || pMem->db->mallocFailed);
 		pMem->flags &= ~(MEM_Int | MEM_Real | MEM_Blob | MEM_Zero);
 		return SQLITE_OK;
@@ -1152,7 +1152,7 @@ valueNew(sqlite3 * db, struct ValueNewStat4Ctx *p)
  * error occurs, output parameter (*ppVal) is set to point to a value
  * object containing the result before returning SQLITE_OK.
  *
- * Affinity aff is applied to the result of the function before returning.
+ * Type @type is applied to the result of the function before returning.
  * If the result is a text value, the sqlite3_value object uses encoding
  * enc.
  *
@@ -1222,7 +1222,7 @@ valueFromFunction(sqlite3 * db,	/* The database connection */
 		rc = ctx.isError;
 		sqlite3ErrorMsg(pCtx->pParse, "%s", sqlite3_value_text(pVal));
 	} else {
-		sqlite3ValueApplyAffinity(pVal, type);
+		sql_value_apply_type(pVal, type);
 		assert(rc == SQLITE_OK);
 	}
 	pCtx->pParse->rc = rc;
@@ -1285,7 +1285,7 @@ valueFromExpr(sqlite3 * db,	/* The database connection */
 		testcase(rc != SQLITE_OK);
 		if (*ppVal) {
 			sqlite3VdbeMemCast(*ppVal, pExpr->type);
-			sqlite3ValueApplyAffinity(*ppVal, type);
+			sql_value_apply_type(*ppVal, type);
 		}
 		return rc;
 	}
@@ -1318,9 +1318,9 @@ valueFromExpr(sqlite3 * db,	/* The database connection */
 		}
 		if ((op == TK_INTEGER || op == TK_FLOAT) &&
 		    type == FIELD_TYPE_SCALAR) {
-			sqlite3ValueApplyAffinity(pVal, FIELD_TYPE_NUMBER);
+			sql_value_apply_type(pVal, FIELD_TYPE_NUMBER);
 		} else {
-			sqlite3ValueApplyAffinity(pVal, type);
+			sql_value_apply_type(pVal, type);
 		}
 		if (pVal->flags & (MEM_Int | MEM_Real))
 			pVal->flags &= ~MEM_Str;
@@ -1339,7 +1339,7 @@ valueFromExpr(sqlite3 * db,	/* The database connection */
 			} else {
 				pVal->u.i = -pVal->u.i;
 			}
-			sqlite3ValueApplyAffinity(pVal, type);
+			sql_value_apply_type(pVal, type);
 		}
 	} else if (op == TK_NULL) {
 		pVal = valueNew(db, pCtx);
@@ -1502,7 +1502,7 @@ stat4ValueFromExpr(Parse * pParse,	/* Parse context */
 				rc = sqlite3VdbeMemCopy((Mem *) pVal,
 							&v->aVar[iBindVar - 1]);
 				if (rc == SQLITE_OK) {
-					sqlite3ValueApplyAffinity(pVal, type);
+					sql_value_apply_type(pVal, type);
 				}
 				pVal->db = pParse->db;
 			}
@@ -1536,7 +1536,7 @@ stat4ValueFromExpr(Parse * pParse,	/* Parse context */
  * vector components that match either of the two latter criteria listed
  * above.
  *
- * Before any value is appended to the record, the affinity of the
+ * Before any value is appended to the record, the type of the
  * corresponding column within index pIdx is applied to it. Before
  * this function returns, output parameter *pnExtract is set to the
  * number of values appended to the record.
diff --git a/src/box/sql/where.c b/src/box/sql/where.c
index f602f90620..bd6f15bf5f 100644
--- a/src/box/sql/where.c
+++ b/src/box/sql/where.c
@@ -295,7 +295,7 @@ whereScanNext(WhereScan * pScan)
 					}
 					if ((pTerm->eOperator & pScan->
 					     opMask) != 0) {
-						/* Verify the affinity and collating sequence match */
+						/* Verify the type and collating sequence match */
 						if ((pTerm->eOperator & WO_ISNULL) == 0) {
 							pX = pTerm->pExpr;
 							enum field_type expr_type =
@@ -1139,15 +1139,6 @@ whereRangeAdjust(WhereTerm * pTerm, LogEst nNew)
 	return nRet;
 }
 
-enum affinity_type
-sql_space_index_part_affinity(struct space_def *def, struct index_def *idx,
-			      uint32_t partno)
-{
-	assert(partno < idx->key_def->part_count);
-	uint32_t fieldno = idx->key_def->parts[partno].fieldno;
-	return def->fields[fieldno].affinity;
-}
-
 /*
  * This function is called to estimate the number of rows visited by a
  * range-scan on a skip-scan index. For example:
diff --git a/src/box/sql/wherecode.c b/src/box/sql/wherecode.c
index 519111d4e9..99906b1963 100644
--- a/src/box/sql/wherecode.c
+++ b/src/box/sql/wherecode.c
@@ -363,71 +363,68 @@ disableTerm(WhereLevel * pLevel, WhereTerm * pTerm)
 	}
 }
 
-/*
- * Code an OP_ApplyType opcode to apply the column type string types
- * to the n registers starting at base.
+/**
+ * Code an OP_ApplyType opcode to apply the column type string
+ * @types to the n registers starting at @base.
  *
- * As an optimization, AFFINITY_BLOB entries (which are no-ops) at the
- * beginning and end of zAff are ignored.  If all entries in zAff are
- * AFFINITY_BLOB, then no code gets generated.
+ * As an optimization, SCALAR entries (which are no-ops) at the
+ * beginning and end of @types are ignored.  If all entries in
+ * @types are SCALAR, then no code gets generated.
  *
- * This routine makes its own copy of zAff so that the caller is free
- * to modify zAff after this routine returns.
+ * This routine makes its own copy of @types so that the caller is
+ * free to modify @types after this routine returns.
  */
 static void
-codeApplyAffinity(Parse * pParse, int base, int n, char *zAff)
+emit_apply_type(Parse *pParse, int base, int n, enum field_type *types)
 {
 	Vdbe *v = pParse->pVdbe;
-	if (zAff == 0) {
+	if (types == NULL) {
 		assert(pParse->db->mallocFailed);
 		return;
 	}
 	assert(v != 0);
 
-	/* Adjust base and n to skip over AFFINITY_BLOB entries at the beginning
-	 * and end of the affinity string.
+	/*
+	 * Adjust base and n to skip over SCALAR entries at the
+	 * beginning and end of the type sequence.
 	 */
-	while (n > 0 && zAff[0] == AFFINITY_BLOB) {
+	while (n > 0 && types[0] == FIELD_TYPE_SCALAR) {
 		n--;
 		base++;
-		zAff++;
+		types++;
 	}
-	while (n > 1 && zAff[n - 1] == AFFINITY_BLOB) {
+	while (n > 1 && types[n - 1] == FIELD_TYPE_SCALAR) {
 		n--;
 	}
 
 	if (n > 0) {
-		enum field_type *types =
-			sql_affinity_str_to_field_type_str(zAff, n);
-		sqlite3VdbeAddOp4(v, OP_ApplyType, base, n, 0, (char *)types,
-				  P4_DYNAMIC);
-		sqlite3ExprCacheAffinityChange(pParse, base, n);
+		enum field_type *types_dup = field_type_sequence_dup(pParse,
+								     types, n);
+		sqlite3VdbeAddOp4(v, OP_ApplyType, base, n, 0,
+				  (char *) types_dup, P4_DYNAMIC);
+		sql_expr_type_cache_change(pParse, base, n);
 	}
 }
 
-/*
- * Expression pRight, which is the RHS of a comparison operation, is
+/**
+ * Expression @rhs, which is the RHS of a comparison operation, is
  * either a vector of n elements or, if n==1, a scalar expression.
- * Before the comparison operation, affinity zAff is to be applied
- * to the pRight values. This function modifies characters within the
- * affinity string to AFFINITY_BLOB if either:
+ * Before the comparison operation, types @types are to be applied
+ * to the @rhs values. This function modifies entries within the
+ * field sequence to SCALAR if either:
  *
- *   * the comparison will be performed with no affinity, or
- *   * the affinity change in zAff is guaranteed not to change the value.
+ *   * the comparison will be performed with no type, or
+ *   * the type change in @types is guaranteed not to change the value.
  */
 static void
-updateRangeAffinityStr(Expr * pRight,	/* RHS of comparison */
-		       int n,		/* Number of vector elements in comparison */
-		       char *zAff)	/* Affinity string to modify */
+expr_cmp_update_rhs_type(struct Expr *rhs, int n, enum field_type *types)
 {
-	int i;
-	for (i = 0; i < n; i++) {
-		enum field_type type = sql_affinity_to_field_type(zAff[i]);
-		Expr *p = sqlite3VectorFieldSubexpr(pRight, i);
+	for (int i = 0; i < n; i++) {
+		Expr *p = sqlite3VectorFieldSubexpr(rhs, i);
 		enum field_type expr_type = sql_expr_type(p);
-		if (sql_type_result(expr_type, type) == FIELD_TYPE_SCALAR ||
-		    sql_expr_needs_no_type_change(p, type)) {
-			zAff[i] = AFFINITY_BLOB;
+		if (sql_type_result(expr_type, types[i]) == FIELD_TYPE_SCALAR ||
+		    sql_expr_needs_no_type_change(p, types[i])) {
+			types[i] = FIELD_TYPE_SCALAR;
 		}
 	}
 }
@@ -651,7 +648,7 @@ codeEqualityTerm(Parse * pParse,	/* The parsing context */
  * In the example above nEq==2.  But this subroutine works for any value
  * of nEq including 0.  If nEq==0, this routine is nearly a no-op.
  * The only thing it does is allocate the pLevel->iMem memory cell and
- * compute the affinity string.
+ * compute the types array.
  *
  * The nExtraReg parameter is 0 or 1.  It is 0 if all WHERE clause constraints
  * are == or IN and are covered by the nEq.  nExtraReg is 1 if there is
@@ -666,27 +663,27 @@ codeEqualityTerm(Parse * pParse,	/* The parsing context */
  * this routine allocates an additional nEq memory cells for internal
  * use.
  *
- * Before returning, *pzAff is set to point to a buffer containing a
- * copy of the column affinity string of the index allocated using
+ * Before returning, @types is set to point to a buffer containing a
+ * copy of the column types array of the index allocated using
  * sqlite3DbMalloc(). Except, entries in the copy of the string associated
- * with equality constraints that use BLOB or NONE affinity are set to
- * AFFINITY_BLOB. This is to deal with SQL such as the following:
+ * with equality constraints that use SCALAR type are set to
+ * SCALAR. This is to deal with SQL such as the following:
  *
- *   CREATE TABLE t1(a TEXT PRIMARY KEY, b);
+ *   CREATE TABLE t1(a TEXT PRIMARY KEY, b BLOB);
  *   SELECT ... FROM t1 AS t2, t1 WHERE t1.a = t2.b;
  *
- * In the example above, the index on t1(a) has TEXT affinity. But since
- * the right hand side of the equality constraint (t2.b) has BLOB/NONE affinity,
+ * In the example above, the index on t1(a) has STRING type. But since
+ * the right hand side of the equality constraint (t2.b) has SCALAR type,
  * no conversion should be attempted before using a t2.b value as part of
- * a key to search the index. Hence the first byte in the returned affinity
- * string in this example would be set to AFFINITY_BLOB.
+ * a key to search the index. Hence the first byte in the returned type
+ * string in this example would be set to SCALAR.
  */
 static int
 codeAllEqualityTerms(Parse * pParse,	/* Parsing context */
 		     WhereLevel * pLevel,	/* Which nested loop of the FROM we are coding */
 		     int bRev,		/* Reverse the order of IN operators */
 		     int nExtraReg,	/* Number of extra registers to allocate */
-		     char **pzAff)	/* OUT: Set to point to affinity string */
+		     enum field_type **res_type)
 {
 	u16 nEq;		/* The number of == or IN constraints to code */
 	u16 nSkip;		/* Number of left-most columns to skip */
@@ -710,12 +707,8 @@ codeAllEqualityTerms(Parse * pParse,	/* Parsing context */
 	nReg = pLoop->nEq + nExtraReg;
 	pParse->nMem += nReg;
 
-
-	struct space *space = space_by_id(idx_def->space_id);
-	assert(space != NULL);
-	char *zAff = sql_space_index_affinity_str(pParse->db, space->def,
-						  idx_def);
-	assert(zAff != 0 || pParse->db->mallocFailed);
+	enum field_type *type = sql_index_type_str(pParse->db, idx_def);
+	assert(type != NULL || pParse->db->mallocFailed);
 
 	if (nSkip) {
 		int iIdxCur = pLevel->iIdxCur;
@@ -740,7 +733,6 @@ codeAllEqualityTerms(Parse * pParse,	/* Parsing context */
 
 	/* Evaluate the equality constraints
 	 */
-	assert(zAff == 0 || (int)strlen(zAff) >= nEq);
 	for (j = nSkip; j < nEq; j++) {
 		int r1;
 		pTerm = pLoop->aLTerm[j];
@@ -762,13 +754,13 @@ codeAllEqualityTerms(Parse * pParse,	/* Parsing context */
 		}
 		if (pTerm->eOperator & WO_IN) {
 			if (pTerm->pExpr->flags & EP_xIsSelect) {
-				/* No affinity ever needs to be (or should be) applied to a value
+				/* No type ever needs to be (or should be) applied to a value
 				 * from the RHS of an "? IN (SELECT ...)" expression. The
 				 * sqlite3FindInIndex() routine has already ensured that the
-				 * affinity of the comparison has been applied to the value.
+				 * type of the comparison has been applied to the value.
 				 */
-				if (zAff)
-					zAff[j] = AFFINITY_BLOB;
+				if (type != NULL)
+					type[j] = FIELD_TYPE_SCALAR;
 			}
 		} else if ((pTerm->eOperator & WO_ISNULL) == 0) {
 			Expr *pRight = pTerm->pExpr->pRight;
@@ -777,21 +769,19 @@ codeAllEqualityTerms(Parse * pParse,	/* Parsing context */
 						  pLevel->addrBrk);
 				VdbeCoverage(v);
 			}
-			if (zAff) {
-				enum field_type type =
+			if (type != NULL) {
+				enum field_type rhs_type =
 					sql_expr_type(pRight);
-				enum field_type idx_type =
-					sql_affinity_to_field_type(zAff[j]);
-				if (sql_type_result(type, idx_type) ==
+				if (sql_type_result(rhs_type, type[j]) ==
 				    FIELD_TYPE_SCALAR) {
-					zAff[j] = AFFINITY_BLOB;
+					type[j] = FIELD_TYPE_SCALAR;
 				}
-				if (sql_expr_needs_no_type_change(pRight, idx_type))
-					zAff[j] = AFFINITY_BLOB;
+				if (sql_expr_needs_no_type_change(pRight, type[j]))
+					type[j] = FIELD_TYPE_SCALAR;
 			}
 		}
 	}
-	*pzAff = zAff;
+	*res_type = type;
 	return regBase;
 }
 
@@ -1000,8 +990,10 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 		int iIdxCur;	/* The VDBE cursor for the index */
 		int nExtraReg = 0;	/* Number of extra registers needed */
 		int op;		/* Instruction opcode */
-		char *zStartAff;	/* Affinity for start of range constraint */
-		char *zEndAff = 0;	/* Affinity for end of range constraint */
+		/* Types for start of range constraint. */
+		enum field_type *start_types;
+		/* Types for end of range constraint */
+		enum field_type *end_types = NULL;
 		u8 bSeekPastNull = 0;	/* True to seek past initial nulls */
 		u8 bStopAtNull = 0;	/* Add condition to terminate at NULLs */
 		int force_integer_reg = -1;  /* If non-negative: number of
@@ -1115,10 +1107,14 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 		 */
 		regBase =
 		    codeAllEqualityTerms(pParse, pLevel, bRev, nExtraReg,
-					 &zStartAff);
-		assert(zStartAff == 0 || sqlite3Strlen30(zStartAff) >= nEq);
-		if (zStartAff && nTop) {
-			zEndAff = sqlite3DbStrDup(db, &zStartAff[nEq]);
+					 &start_types);
+		if (start_types != NULL && nTop) {
+			uint32_t len = 0;
+			for (enum field_type *tmp = &start_types[nEq];
+			     *tmp != field_type_MAX; tmp++, len++);
+			uint32_t sz = len * sizeof(enum field_type);
+			end_types = sqlite3DbMallocRaw(db, sz);
+			memcpy(end_types, &start_types[nEq], sz);
 		}
 		addrNxt = pLevel->addrNxt;
 
@@ -1146,9 +1142,9 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 				VdbeCoverage(v);
 			}
 
-			if (zStartAff) {
-				updateRangeAffinityStr(pRight, nBtm,
-						       &zStartAff[nEq]);
+			if (start_types) {
+				expr_cmp_update_rhs_type(pRight, nBtm,
+							 &start_types[nEq]);
 			}
 			nConstraint += nBtm;
 			testcase(pRangeStart->wtFlags & TERM_VIRTUAL);
@@ -1192,8 +1188,8 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 				}
 			}
 		}
-		codeApplyAffinity(pParse, regBase, nConstraint - bSeekPastNull,
-				  zStartAff);
+		emit_apply_type(pParse, regBase, nConstraint - bSeekPastNull,
+				start_types);
 		if (pLoop->nSkip > 0 && nConstraint == pLoop->nSkip) {
 			/* The skip-scan logic inside the call to codeAllEqualityConstraints()
 			 * above has already left the cursor sitting on the correct row,
@@ -1243,10 +1239,10 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 						  addrNxt);
 				VdbeCoverage(v);
 			}
-			if (zEndAff) {
-				updateRangeAffinityStr(pRight, nTop, zEndAff);
-				codeApplyAffinity(pParse, regBase + nEq, nTop,
-						  zEndAff);
+			if (end_types) {
+				expr_cmp_update_rhs_type(pRight, nTop, end_types);
+				emit_apply_type(pParse, regBase + nEq, nTop,
+						end_types);
 			} else {
 				assert(pParse->db->mallocFailed);
 			}
@@ -1263,8 +1259,8 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 			endEq = 0;
 			nConstraint++;
 		}
-		sqlite3DbFree(db, zStartAff);
-		sqlite3DbFree(db, zEndAff);
+		sqlite3DbFree(db, start_types);
+		sqlite3DbFree(db, end_types);
 
 		/* Top of the loop body */
 		pLevel->p2 = sqlite3VdbeCurrentAddr(v);
diff --git a/src/box/sql/whereexpr.c b/src/box/sql/whereexpr.c
index 1fb5fa5933..b016d18efe 100644
--- a/src/box/sql/whereexpr.c
+++ b/src/box/sql/whereexpr.c
@@ -272,7 +272,7 @@ like_optimization_is_valid(Parse *pParse, Expr *pExpr, Expr **ppPrefix,
 	    sql_expr_type(pLeft) != FIELD_TYPE_STRING) {
 		/* IMP: R-02065-49465 The left-hand side of the
 		 * LIKE operator must be the name of an indexed
-		 * column with TEXT affinity.
+		 * column with STRING type.
 		 */
 		return 0;
 	}
@@ -284,7 +284,8 @@ like_optimization_is_valid(Parse *pParse, Expr *pExpr, Expr **ppPrefix,
 		Vdbe *pReprepare = pParse->pReprepare;
 		int iCol = pRight->iColumn;
 		pVal =
-		    sqlite3VdbeGetBoundValue(pReprepare, iCol, AFFINITY_BLOB);
+		    sqlite3VdbeGetBoundValue(pReprepare, iCol,
+					     FIELD_TYPE_SCALAR);
 		if (pVal && sqlite3_value_type(pVal) == SQLITE_TEXT) {
 			z = (char *)sqlite3_value_text(pVal);
 		}
@@ -748,7 +749,7 @@ exprAnalyzeOrTerm(SrcList * pSrc,	/* the FROM clause */
 				} else if (pOrTerm->u.leftColumn != iColumn) {
 					okToChngToIN = 0;
 				} else {
-					/* If the right-hand side is also a column, then the affinities
+					/* If the right-hand side is also a column, then the types
 					 * of both right and left sides must be such that no type
 					 * conversions are required on the right.  (Ticket #2249)
 					 */
@@ -824,7 +825,7 @@ exprAnalyzeOrTerm(SrcList * pSrc,	/* the FROM clause */
  *   1.  The SQLITE_Transitive optimization must be enabled
  *   2.  Must be either an == or an IS operator
  *   3.  Not originating in the ON clause of an OUTER JOIN
- *   4.  The affinities of A and B must be compatible
+ *   4.  The types of A and B must be compatible
  *   5a. Both operands use the same collating sequence OR
  *   5b. The overall collating sequence is BINARY
  * If this routine returns TRUE, that means that the RHS can be substituted
diff --git a/test/sql/types.result b/test/sql/types.result
index 85d83e11e2..9043cbfd50 100644
--- a/test/sql/types.result
+++ b/test/sql/types.result
@@ -33,20 +33,19 @@ box.sql.execute("CREATE TABLE t1 (id TEXT PRIMARY KEY, a REAL, b INT, c TEXT, d
 ...
 box.space.T1:format()
 ---
-- [{'affinity': 66, 'type': 'string', 'nullable_action': 'abort', 'name': 'ID', 'is_nullable': false},
-  {'affinity': 69, 'type': 'number', 'nullable_action': 'none', 'name': 'A', 'is_nullable': true},
-  {'affinity': 68, 'type': 'integer', 'nullable_action': 'none', 'name': 'B', 'is_nullable': true},
-  {'affinity': 66, 'type': 'string', 'nullable_action': 'none', 'name': 'C', 'is_nullable': true},
-  {'affinity': 65, 'type': 'scalar', 'nullable_action': 'none', 'name': 'D', 'is_nullable': true}]
+- [{'type': 'string', 'nullable_action': 'abort', 'name': 'ID', 'is_nullable': false},
+  {'type': 'number', 'nullable_action': 'none', 'name': 'A', 'is_nullable': true},
+  {'type': 'integer', 'nullable_action': 'none', 'name': 'B', 'is_nullable': true},
+  {'type': 'string', 'nullable_action': 'none', 'name': 'C', 'is_nullable': true},
+  {'type': 'scalar', 'nullable_action': 'none', 'name': 'D', 'is_nullable': true}]
 ...
 box.sql.execute("CREATE VIEW v1 AS SELECT b + a, b - a FROM t1;")
 ---
 ...
 box.space.V1:format()
 ---
-- [{'affinity': 69, 'type': 'number', 'nullable_action': 'none', 'name': 'b + a',
-    'is_nullable': true}, {'affinity': 69, 'type': 'number', 'nullable_action': 'none',
-    'name': 'b - a', 'is_nullable': true}]
+- [{'type': 'number', 'nullable_action': 'none', 'name': 'b + a', 'is_nullable': true},
+  {'type': 'number', 'nullable_action': 'none', 'name': 'b - a', 'is_nullable': true}]
 ...
 -- gh-2494: index's part also features correct declared type.
 --
diff --git a/test/sql/upgrade.result b/test/sql/upgrade.result
index 79c7eb2458..02ab9b42b9 100644
--- a/test/sql/upgrade.result
+++ b/test/sql/upgrade.result
@@ -80,12 +80,12 @@ box.sql.execute("CREATE TRIGGER t2t AFTER INSERT ON t BEGIN INSERT INTO t_out VA
 ...
 box.space._space.index['name']:get('T')
 ---
-- [513, 1, 'T', 'memtx', 1, {}, [{'affinity': 68, 'type': 'integer', 'nullable_action': 'abort',
-      'name': 'X', 'is_nullable': false}]]
+- [513, 1, 'T', 'memtx', 1, {}, [{'type': 'integer', 'nullable_action': 'abort', 'name': 'X',
+      'is_nullable': false}]]
 ...
 box.space._space.index['name']:get('T_OUT')
 ---
-- [514, 1, 'T_OUT', 'memtx', 1, {}, [{'affinity': 68, 'type': 'integer', 'nullable_action': 'abort',
+- [514, 1, 'T_OUT', 'memtx', 1, {}, [{'type': 'integer', 'nullable_action': 'abort',
       'name': 'X', 'is_nullable': false}]]
 ...
 t1t = box.space._trigger:get('T1T')
-- 
GitLab