Tarantool development patches archive
 help / color / mirror / Atom feed
* [tarantool-patches] Re: [PATCH] sql: add index_def to Index
@ 2018-07-17  8:48 Ivan Koptelov
  0 siblings, 0 replies; only message in thread
From: Ivan Koptelov @ 2018-07-17  8:48 UTC (permalink / raw)
  To: tarantool-patches; +Cc: Ivan Koptelov

Now every sqlite struct Index is created with tnt
struct index_def inside. This allows us to use tnt
index_def in work with sqlite indexes in the same
manner as with tnt index and is a step to remove
sqlite Index with tnt index.
Fields coll_array, coll_id_array, aiColumn,
sort_order, aiRowLogEst and zName are removed from
Index. All usages of this fields changed to usage of
corresponding index_def or index_def->opts fields.
index_is_unique(), sql_index_collation() and
index_column_count() are removed with calls of
index_def corresponding fields.
Also there is small change in behavior: before the
patch a statement like
CREATE TABLE t1(a,b, PRIMARY KEY(a,b), UNIQUE(a,b))
created only one constraint index (for primary key)
and no index for UNIQUE constraint (since it is upon
the same columns), neither it is named or non-named
constraint.
After the patch index will be always created for named
constraints. It is a temporary solution. In future
it's preferable not to create an index, but to make
some record in _constraints space that this named
unique constraint implemented with the same index as
primary key constraint.

Closes: #3369, #3498
---
Issue: https://github.com/tarantool/tarantool/issues/3369
Branch: https://github.com/tarantool/tarantool/tree/sb/gh-3369-use-index-def-in-select-and-where
 src/box/errcode.h                           |   1 +
 src/box/sql.c                               |  54 +-
 src/box/sql/analyze.c                       |  85 ++-
 src/box/sql/build.c                         | 941 ++++++++++++----------------
 src/box/sql/delete.c                        |  10 +-
 src/box/sql/expr.c                          |  61 +-
 src/box/sql/fkey.c                          | 216 +++----
 src/box/sql/insert.c                        | 147 +++--
 src/box/sql/parse.y                         |  14 +-
 src/box/sql/pragma.c                        |  34 +-
 src/box/sql/select.c                        |   2 +-
 src/box/sql/sqliteInt.h                     | 145 ++---
 src/box/sql/update.c                        |  39 +-
 src/box/sql/vdbeaux.c                       |   2 +-
 src/box/sql/vdbemem.c                       |  21 +-
 src/box/sql/where.c                         | 226 ++++---
 src/box/sql/wherecode.c                     | 102 +--
 test/box/misc.result                        |   1 +
 test/sql-tap/analyze6.test.lua              |   6 +-
 test/sql-tap/collation.test.lua             |   7 +-
 test/sql-tap/colname.test.lua               |   4 +-
 test/sql-tap/gh-2931-savepoints.test.lua    |   2 +-
 test/sql-tap/gh2140-trans.test.lua          |   2 +-
 test/sql-tap/gh2259-in-stmt-trans.test.lua  |   8 +-
 test/sql-tap/gh2964-abort.test.lua          |   2 +-
 test/sql-tap/identifier-characters.test.lua |   2 +-
 test/sql-tap/identifier_case.test.lua       |   4 +-
 test/sql-tap/index1.test.lua                |  32 +-
 test/sql-tap/index7.test.lua                | 104 ++-
 test/sql-tap/intpkey.test.lua               |   4 +-
 test/sql-tap/misc1.test.lua                 |   2 +-
 test/sql-tap/unique.test.lua                |   8 +-
 test/sql-tap/update.test.lua                |   6 +-
 test/sql-tap/view.test.lua                  |   2 +-
 test/sql/insert-unique.result               |   3 +-
 test/sql/iproto.result                      |   2 +-
 test/sql/message-func-indexes.result        |   8 +-
 test/sql/on-conflict.result                 |   2 +-
 test/sql/persistency.result                 |   6 +-
 test/sql/transition.result                  |   6 +-
 test/sql/view.result                        |   2 +-
 41 files changed, 1143 insertions(+), 1182 deletions(-)

diff --git a/src/box/errcode.h b/src/box/errcode.h
index 0cf9578f8..b61b387f2 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -218,6 +218,7 @@ struct errcode_record {
 	/*163 */_(ER_TRANSACTION_YIELD,		"Transaction has been aborted by a fiber yield") \
 	/*164 */_(ER_NO_SUCH_GROUP,		"Replication group '%s' does not exist") \
 	/*165 */_(ER_NO_SUCH_MODULE,		"Module '%s' does not exist") \
+	/*166 */_(ER_NO_SUCH_COLLATION,		"Collation '%s' does not exist") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/sql.c b/src/box/sql.c
index fdce22481..d2cc0a9a0 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -1420,8 +1420,8 @@ int tarantoolSqlite3MakeTableFormat(Table *pTable, void *buf)
 
 	/* If table's PK is single column which is INTEGER, then
 	 * treat it as strict type, not affinity.  */
-	if (pk_idx && pk_idx->nColumn == 1) {
-		int pk = pk_idx->aiColumn[0];
+	if (pk_idx != NULL && pk_idx->def->key_def->part_count == 1) {
+		int pk = pk_idx->def->key_def->parts[0].fieldno;
 		if (def->fields[pk].type == FIELD_TYPE_INTEGER)
 			pk_forced_int = pk;
 	}
@@ -1532,20 +1532,19 @@ tarantoolSqlite3MakeTableOpts(Table *pTable, const char *zSql, char *buf)
  */
 int tarantoolSqlite3MakeIdxParts(SqliteIndex *pIndex, void *buf)
 {
-	struct space_def *def = pIndex->pTable->def;
-	assert(def != NULL);
+	struct field_def *fields = pIndex->pTable->def->fields;
+	struct key_def *key_def = pIndex->def->key_def;
 	const struct Enc *enc = get_enc(buf);
-	struct SqliteIndex *primary_index;
-	char *base = buf, *p;
-	int pk_forced_int = -1;
-
-	primary_index = sqlite3PrimaryKeyIndex(pIndex->pTable);
+	char *base = buf;
+	uint32_t pk_forced_int = UINT32_MAX;
+	struct SqliteIndex *primary_index =
+		sqlite3PrimaryKeyIndex(pIndex->pTable);
 
 	/* If table's PK is single column which is INTEGER, then
 	 * treat it as strict type, not affinity.  */
-	if (primary_index->nColumn == 1) {
-		int pk = primary_index->aiColumn[0];
-		if (def->fields[pk].type == FIELD_TYPE_INTEGER)
+	if (primary_index->def->key_def->part_count == 1) {
+		int pk = primary_index->def->key_def->parts[0].fieldno;
+		if (fields[pk].type == FIELD_TYPE_INTEGER)
 			pk_forced_int = pk;
 	}
 
@@ -1555,46 +1554,45 @@ int tarantoolSqlite3MakeIdxParts(SqliteIndex *pIndex, void *buf)
 	 * primary key columns. Query planner depends on this particular
 	 * data layout.
 	 */
-	int i, n = pIndex->nColumn;
-
-	p = enc->encode_array(base, n);
-	for (i = 0; i < n; i++) {
-		int col = pIndex->aiColumn[i];
-		assert(def->fields[col].is_nullable ==
-		       action_is_nullable(def->fields[col].nullable_action));
+	struct key_part *part = key_def->parts;
+	char *p = enc->encode_array(base, key_def->part_count);
+	for (uint32_t i = 0; i < key_def->part_count; ++i, ++part) {
+		uint32_t col = part->fieldno;
+		assert(fields[col].is_nullable ==
+		       action_is_nullable(fields[col].nullable_action));
 		const char *t;
 		if (pk_forced_int == col) {
 			t = "integer";
 		} else {
-			enum affinity_type affinity = def->fields[col].affinity;
-			t = convertSqliteAffinity(affinity,
-						  def->fields[col].is_nullable);
+			t = convertSqliteAffinity(fields[col].affinity,
+						  fields[col].is_nullable);
 		}
 		/* do not decode default collation */
-		uint32_t cid = pIndex->coll_id_array[i];
+		uint32_t cid = part->coll_id;
 		p = enc->encode_map(p, cid == COLL_NONE ? 5 : 6);
 		p = enc->encode_str(p, "type", sizeof("type")-1);
 		p = enc->encode_str(p, t, strlen(t));
 		p = enc->encode_str(p, "field", sizeof("field")-1);
 		p = enc->encode_uint(p, col);
 		if (cid != COLL_NONE) {
-			p = enc->encode_str(p, "collation", sizeof("collation")-1);
+			p = enc->encode_str(p, "collation",
+					    sizeof("collation") - 1);
 			p = enc->encode_uint(p, cid);
 		}
 		p = enc->encode_str(p, "is_nullable", 11);
-		p = enc->encode_bool(p, def->fields[col].is_nullable);
+		p = enc->encode_bool(p, fields[col].is_nullable);
 		p = enc->encode_str(p, "nullable_action", 15);
 		const char *action_str =
-			on_conflict_action_strs[def->fields[col].nullable_action];
+			on_conflict_action_strs[fields[col].nullable_action];
 		p = enc->encode_str(p, action_str, strlen(action_str));
 
 		p = enc->encode_str(p, "sort_order", 10);
-		enum sort_order sort_order = pIndex->sort_order[i];
+		enum sort_order sort_order = part->sort_order;
 		assert(sort_order < sort_order_MAX);
 		const char *sort_order_str = sort_order_strs[sort_order];
 		p = enc->encode_str(p, sort_order_str, strlen(sort_order_str));
 	}
-	return (int)(p - base);
+	return p - base;
 }
 
 /*
diff --git a/src/box/sql/analyze.c b/src/box/sql/analyze.c
index ea872f407..ee25de7ad 100644
--- a/src/box/sql/analyze.c
+++ b/src/box/sql/analyze.c
@@ -823,8 +823,7 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 	for (pIdx = pTab->pIndex; pIdx; pIdx = pIdx->pNext) {
 		int addrRewind;	/* Address of "OP_Rewind iIdxCur" */
 		int addrNextRow;	/* Address of "next_row:" */
-		const char *zIdxName;	/* Name of the index */
-		int nColTest;	/* Number of columns to test for changes */
+		const char *idx_name;	/* Name of the index */
 
 		if (pOnlyIdx && pOnlyIdx != pIdx)
 			continue;
@@ -832,17 +831,16 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 		 * names. Thus, for the sake of clarity, use
 		 * instead more familiar table name.
 		 */
-		if (IsPrimaryKeyIndex(pIdx)) {
-			zIdxName = pTab->def->name;
-		} else {
-			zIdxName = pIdx->zName;
-		}
-		nColTest = index_column_count(pIdx);
+		if (IsPrimaryKeyIndex(pIdx))
+			idx_name = pTab->def->name;
+		else
+			idx_name = pIdx->def->name;
+		int part_count = pIdx->def->key_def->part_count;
 
 		/* Populate the register containing the index name. */
-		sqlite3VdbeLoadString(v, regIdxname, zIdxName);
+		sqlite3VdbeLoadString(v, regIdxname, idx_name);
 		VdbeComment((v, "Analysis for %s.%s", pTab->def->name,
-			zIdxName));
+			    idx_name));
 
 		/*
 		 * Pseudo-code for loop that calls stat_push():
@@ -881,7 +879,7 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 		 * when building a record to insert into the sample column of
 		 * the _sql_stat4 table).
 		 */
-		pParse->nMem = MAX(pParse->nMem, regPrev + nColTest);
+		pParse->nMem = MAX(pParse->nMem, regPrev + part_count);
 
 		/* Open a read-only cursor on the index being analyzed. */
 		struct space *space =
@@ -890,7 +888,7 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 		assert(space != NULL);
 		sqlite3VdbeAddOp4(v, OP_OpenRead, iIdxCur, idx_id, 0,
 				  (void *) space, P4_SPACEPTR);
-		VdbeComment((v, "%s", pIdx->zName));
+		VdbeComment((v, "%s", pIdx->def->name));
 
 		/* Invoke the stat_init() function. The arguments are:
 		 *
@@ -903,8 +901,8 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 		 * The third argument is only used for STAT4
 		 */
 		sqlite3VdbeAddOp2(v, OP_Count, iIdxCur, regStat4 + 3);
-		sqlite3VdbeAddOp2(v, OP_Integer, nColTest, regStat4 + 1);
-		sqlite3VdbeAddOp2(v, OP_Integer, nColTest, regStat4 + 2);
+		sqlite3VdbeAddOp2(v, OP_Integer, part_count, regStat4 + 1);
+		sqlite3VdbeAddOp2(v, OP_Integer, part_count, regStat4 + 2);
 		sqlite3VdbeAddOp4(v, OP_Function0, 0, regStat4 + 1, regStat4,
 				  (char *)&statInitFuncdef, P4_FUNCDEF);
 		sqlite3VdbeChangeP5(v, 3);
@@ -922,11 +920,11 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 		sqlite3VdbeAddOp2(v, OP_Integer, 0, regChng);
 		addrNextRow = sqlite3VdbeCurrentAddr(v);
 
-		if (nColTest > 0) {
+		if (part_count > 0) {
 			int endDistinctTest = sqlite3VdbeMakeLabel(v);
 			int *aGotoChng;	/* Array of jump instruction addresses */
 			aGotoChng =
-			    sqlite3DbMallocRawNN(db, sizeof(int) * nColTest);
+			    sqlite3DbMallocRawNN(db, sizeof(int) * part_count);
 			if (aGotoChng == 0)
 				continue;
 
@@ -942,7 +940,7 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 			 */
 			sqlite3VdbeAddOp0(v, OP_Goto);
 			addrNextRow = sqlite3VdbeCurrentAddr(v);
-			if (nColTest == 1 && index_is_unique(pIdx)) {
+			if (part_count == 1 && pIdx->def->opts.is_unique) {
 				/* For a single-column UNIQUE index, once we have found a non-NULL
 				 * row, we know that all the rest will be distinct, so skip
 				 * subsequent distinctness tests.
@@ -951,13 +949,12 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 						  endDistinctTest);
 				VdbeCoverage(v);
 			}
-			for (i = 0; i < nColTest; i++) {
-				uint32_t id;
-				struct coll *coll =
-					sql_index_collation(pIdx, i, &id);
+			struct key_part *part = pIdx->def->key_def->parts;
+			for (i = 0; i < part_count; ++i, ++part) {
+				struct coll *coll = part->coll;
 				sqlite3VdbeAddOp2(v, OP_Integer, i, regChng);
 				sqlite3VdbeAddOp3(v, OP_Column, iIdxCur,
-						  pIdx->aiColumn[i], regTemp);
+						  part->fieldno, regTemp);
 				aGotoChng[i] =
 				    sqlite3VdbeAddOp4(v, OP_Ne, regTemp, 0,
 						      regPrev + i, (char *)coll,
@@ -965,7 +962,7 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 				sqlite3VdbeChangeP5(v, SQLITE_NULLEQ);
 				VdbeCoverage(v);
 			}
-			sqlite3VdbeAddOp2(v, OP_Integer, nColTest, regChng);
+			sqlite3VdbeAddOp2(v, OP_Integer, part_count, regChng);
 			sqlite3VdbeGoto(v, endDistinctTest);
 
 			/*
@@ -976,11 +973,11 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 			 *  ...
 			 */
 			sqlite3VdbeJumpHere(v, addrNextRow - 1);
-			for (i = 0; i < nColTest; i++) {
+			part = pIdx->def->key_def->parts;
+			for (i = 0; i < part_count; ++i, ++part) {
 				sqlite3VdbeJumpHere(v, aGotoChng[i]);
 				sqlite3VdbeAddOp3(v, OP_Column, iIdxCur,
-						  pIdx->aiColumn[i],
-						  regPrev + i);
+						  part->fieldno, regPrev + i);
 			}
 			sqlite3VdbeResolveLabel(v, endDistinctTest);
 			sqlite3DbFree(db, aGotoChng);
@@ -995,19 +992,18 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 		 */
 		assert(regKey == (regStat4 + 2));
 		Index *pPk = sqlite3PrimaryKeyIndex(pIdx->pTable);
-		int j, k, regKeyStat;
-		int nPkColumn = (int)index_column_count(pPk);
-		regKeyStat = sqlite3GetTempRange(pParse, nPkColumn);
-		for (j = 0; j < nPkColumn; j++) {
-			k = pPk->aiColumn[j];
-			assert(k >= 0 && k < (int)pTab->def->field_count);
-			sqlite3VdbeAddOp3(v, OP_Column, iIdxCur, k, regKeyStat + j);
-			VdbeComment((v, "%s",
-				pTab->def->fields[pPk->aiColumn[j]].name));
+		uint32_t pk_part_count = pPk->def->key_def->part_count;
+		int regKeyStat = sqlite3GetTempRange(pParse, pk_part_count);
+		for (uint32_t j = 0; j < pk_part_count; ++j) {
+			uint32_t k = pPk->def->key_def->parts[j].fieldno;
+			assert(k < pTab->def->field_count);
+			sqlite3VdbeAddOp3(v, OP_Column, iIdxCur, k,
+					  regKeyStat + j);
+			VdbeComment((v, "%s", pTab->def->fields[k].name));
 		}
 		sqlite3VdbeAddOp3(v, OP_MakeRecord, regKeyStat,
-				  nPkColumn, regKey);
-		sqlite3ReleaseTempRange(pParse, regKeyStat, nPkColumn);
+				  pk_part_count, regKey);
+		sqlite3ReleaseTempRange(pParse, regKeyStat, pk_part_count);
 
 		assert(regChng == (regStat4 + 1));
 		sqlite3VdbeAddOp4(v, OP_Function0, 1, regStat4, regTemp,
@@ -1030,11 +1026,11 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 		int regDLt = regStat1 + 2;
 		int regSample = regStat1 + 3;
 		int regCol = regStat1 + 4;
-		int regSampleKey = regCol + nColTest;
+		int regSampleKey = regCol + part_count;
 		int addrNext;
 		int addrIsNull;
 
-		pParse->nMem = MAX(pParse->nMem, regCol + nColTest);
+		pParse->nMem = MAX(pParse->nMem, regCol + part_count);
 
 		addrNext = sqlite3VdbeCurrentAddr(v);
 		callStatGet(v, regStat4, STAT_GET_KEY, regSampleKey);
@@ -1050,12 +1046,11 @@ analyzeOneTable(Parse * pParse,	/* Parser context */
 		 * be taken
 		 */
 		VdbeCoverageNeverTaken(v);
-		for (i = 0; i < nColTest; i++) {
-			sqlite3ExprCodeLoadIndexColumn(pParse, pIdx,
-									 iTabCur, i,
-									 regCol + i);
+		for (i = 0; i < part_count; i++) {
+			sqlite3ExprCodeLoadIndexColumn(pParse, pIdx, iTabCur, i,
+						       regCol + i);
 		}
-		sqlite3VdbeAddOp3(v, OP_MakeRecord, regCol, nColTest,
+		sqlite3VdbeAddOp3(v, OP_MakeRecord, regCol, part_count,
 				  regSample);
 		sqlite3VdbeAddOp3(v, OP_MakeRecord, regTabname, 6, regTemp);
 		sqlite3VdbeAddOp2(v, OP_IdxReplace, iStatCur + 1, regTemp);
@@ -1643,7 +1638,7 @@ index_field_tuple_est(struct Index *idx, uint32_t field)
 {
 	struct space *space = space_by_id(SQLITE_PAGENO_TO_SPACEID(idx->tnum));
 	if (space == NULL)
-		return idx->aiRowLogEst[field];
+		return idx->def->opts.stat->tuple_log_est[field];
 	struct index *tnt_idx =
 		space_index(space, SQLITE_PAGENO_TO_INDEXID(idx->tnum));
 	assert(tnt_idx != NULL);
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index 8fba373ce..a64d723a9 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -199,6 +199,8 @@ static void
 freeIndex(sqlite3 * db, Index * p)
 {
 	sql_expr_delete(db, p->pPartIdxWhere, false);
+	if (p->def != NULL)
+		index_def_delete(p->def);
 	sqlite3DbFree(db, p->zColAff);
 	sqlite3DbFree(db, p);
 }
@@ -217,7 +219,8 @@ sqlite3UnlinkAndDeleteIndex(sqlite3 * db, Index * pIndex)
 
 	struct session *user_session = current_session();
 
-	pIndex = sqlite3HashInsert(&pIndex->pTable->idxHash, pIndex->zName, 0);
+	pIndex = sqlite3HashInsert(&pIndex->pTable->idxHash,
+				   pIndex->def->name, 0);
 	if (ALWAYS(pIndex)) {
 		if (pIndex->pTable->pIndex == pIndex) {
 			pIndex->pTable->pIndex = pIndex->pNext;
@@ -322,7 +325,7 @@ deleteTable(sqlite3 * db, Table * pTable)
 		pNext = pIndex->pNext;
 		assert(pIndex->pSchema == pTable->pSchema);
 		if ((db == 0 || db->pnBytesFreed == 0)) {
-			char *zName = pIndex->zName;
+			char *zName = pIndex->def->name;
 			TESTONLY(Index *
 				 pOld =) sqlite3HashInsert(&pTable->idxHash,
 							   zName, 0);
@@ -933,7 +936,8 @@ sqlite3AddPrimaryKey(Parse * pParse,	/* Parsing context */
 				"INTEGER PRIMARY KEY or INT PRIMARY KEY");
 	} else {
 		sql_create_index(pParse, 0, 0, pList, onError, 0,
-				 0, sortOrder, false, SQLITE_IDXTYPE_PRIMARYKEY);
+				 0, sortOrder, false,
+				 SQL_INDEX_TYPE_CONSTRAINT_PK);
 		pList = 0;
 	}
 
@@ -980,7 +984,7 @@ sqlite3AddCollateType(Parse * pParse, Token * pToken)
 	Table *p = pParse->pNewTable;
 	if (p == NULL)
 		return;
-	int i = p->def->field_count - 1;
+	uint32_t i = p->def->field_count - 1;
 	sqlite3 *db = pParse->db;
 	char *zColl = sqlite3NameFromToken(db, pToken);
 	if (!zColl)
@@ -988,22 +992,21 @@ sqlite3AddCollateType(Parse * pParse, Token * pToken)
 	uint32_t *id = &p->def->fields[i].coll_id;
 	p->aCol[i].coll = sql_get_coll_seq(pParse, zColl, id);
 	if (p->aCol[i].coll != NULL) {
-		Index *pIdx;
 		/* If the column is declared as "<name> PRIMARY KEY COLLATE <type>",
 		 * then an index may have been created on this column before the
 		 * collation type was added. Correct this if it is the case.
 		 */
-		for (pIdx = p->pIndex; pIdx; pIdx = pIdx->pNext) {
-			assert(pIdx->nColumn == 1);
-			if (pIdx->aiColumn[0] == i) {
-				id = &pIdx->coll_id_array[0];
-				pIdx->coll_array[0] =
+		for (struct Index *pIdx = p->pIndex; pIdx != NULL;
+		     pIdx = pIdx->pNext) {
+			assert(pIdx->def->key_def->part_count == 1);
+			if (pIdx->def->key_def->parts[0].fieldno == i) {
+				id = &pIdx->def->key_def->parts[0].coll_id;
+				pIdx->def->key_def->parts[0].coll =
 					sql_column_collation(p->def, i, id);
 			}
 		}
-	} else {
-		sqlite3DbFree(db, zColl);
 	}
+	sqlite3DbFree(db, zColl);
 }
 
 struct coll *
@@ -1033,66 +1036,6 @@ sql_column_collation(struct space_def *def, uint32_t column, uint32_t *coll_id)
 	return space->format->fields[column].coll;
 }
 
-struct key_def*
-sql_index_key_def(struct Index *idx)
-{
-	uint32_t space_id = SQLITE_PAGENO_TO_SPACEID(idx->tnum);
-	uint32_t index_id = SQLITE_PAGENO_TO_INDEXID(idx->tnum);
-	struct space *space = space_by_id(space_id);
-	assert(space != NULL);
-	struct index *index = space_index(space, index_id);
-	assert(index != NULL && index->def != NULL);
-	return index->def->key_def;
-}
-
-struct coll *
-sql_index_collation(Index *idx, uint32_t column, uint32_t *coll_id)
-{
-	assert(idx != NULL);
-	uint32_t space_id = SQLITE_PAGENO_TO_SPACEID(idx->pTable->tnum);
-	struct space *space = space_by_id(space_id);
-
-	assert(column < idx->nColumn);
-	/*
-	 * If space is still under construction, or it is
-	 * an ephemeral space, then fetch collation from
-	 * SQL internal structure.
-	 */
-	if (space == NULL) {
-		assert(column < idx->nColumn);
-		*coll_id = idx->coll_id_array[column];
-		return idx->coll_array[column];
-	}
-
-	struct key_def *key_def = sql_index_key_def(idx);
-	assert(key_def != NULL && key_def->part_count >= column);
-	*coll_id = key_def->parts[column].coll_id;
-	return key_def->parts[column].coll;
-}
-
-enum sort_order
-sql_index_column_sort_order(Index *idx, uint32_t column)
-{
-	assert(idx != NULL);
-	uint32_t space_id = SQLITE_PAGENO_TO_SPACEID(idx->pTable->tnum);
-	struct space *space = space_by_id(space_id);
-
-	assert(column < idx->nColumn);
-	/*
-	 * If space is still under construction, or it is
-	 * an ephemeral space, then fetch collation from
-	 * SQL internal structure.
-	 */
-	if (space == NULL) {
-		assert(column < idx->nColumn);
-		return idx->sort_order[column];
-	}
-
-	struct key_def *key_def = sql_index_key_def(idx);
-	assert(key_def != NULL && key_def->part_count >= column);
-	return key_def->parts[column].sort_order;
-}
-
 struct ExprList *
 space_checks_expr_list(uint32_t space_id)
 {
@@ -1272,17 +1215,6 @@ createTableStmt(sqlite3 * db, Table * p)
 	return zStmt;
 }
 
-/* Return true if value x is found any of the first nCol entries of aiCol[]
- */
-static int
-hasColumn(const i16 * aiCol, int nCol, int x)
-{
-	while (nCol-- > 0)
-		if (x == *(aiCol++))
-			return 1;
-	return 0;
-}
-
 /*
  * This routine runs at the end of parsing a CREATE TABLE statement.
  * The job of this routine is to convert both
@@ -1299,13 +1231,12 @@ static void
 convertToWithoutRowidTable(Parse * pParse, Table * pTab)
 {
 	Index *pPk;
-	int i, j;
 	sqlite3 *db = pParse->db;
 
 	/* Mark every PRIMARY KEY column as NOT NULL (except for imposter tables)
 	 */
 	if (!db->init.imposterTable) {
-		for (i = 0; i < (int)pTab->def->field_count; i++) {
+		for (uint32_t i = 0; i < pTab->def->field_count; i++) {
 			if (pTab->aCol[i].is_primkey) {
 				pTab->def->fields[i].nullable_action
 					= ON_CONFLICT_ACTION_ABORT;
@@ -1330,27 +1261,13 @@ convertToWithoutRowidTable(Parse * pParse, Table * pTab)
 		assert(pParse->pNewTable == pTab);
 		sql_create_index(pParse, 0, 0, pList, pTab->keyConf, 0, 0,
 				 SORT_ORDER_ASC, false,
-				 SQLITE_IDXTYPE_PRIMARYKEY);
+				 SQL_INDEX_TYPE_CONSTRAINT_PK);
 		if (db->mallocFailed)
 			return;
 		pPk = sqlite3PrimaryKeyIndex(pTab);
 		pTab->iPKey = -1;
 	} else {
 		pPk = sqlite3PrimaryKeyIndex(pTab);
-
-		/*
-		 * Remove all redundant columns from the PRIMARY KEY.  For example, change
-		 * "PRIMARY KEY(a,b,a,b,c,b,c,d)" into just "PRIMARY KEY(a,b,c,d)".  Later
-		 * code assumes the PRIMARY KEY contains no repeated columns.
-		 */
-		for (i = j = 1; i < pPk->nColumn; i++) {
-			if (hasColumn(pPk->aiColumn, j, pPk->aiColumn[i])) {
-				pPk->nColumn--;
-			} else {
-				pPk->aiColumn[j++] = pPk->aiColumn[i];
-			}
-		}
-		pPk->nColumn = j;
 	}
 	assert(pPk != 0);
 }
@@ -1432,7 +1349,7 @@ createIndex(Parse * pParse, Index * pIndex, int iSpaceId, int iIndexId,
 	}
 	sqlite3VdbeAddOp4(v,
 			  OP_String8, 0, iFirstCol + 2, 0,
-			  sqlite3DbStrDup(pParse->db, pIndex->zName),
+			  sqlite3DbStrDup(pParse->db, pIndex->def->name),
 			  P4_DYNAMIC);
 	sqlite3VdbeAddOp4(v, OP_String8, 0, iFirstCol + 3, 0, "tree",
 			  P4_STATIC);
@@ -1443,7 +1360,8 @@ createIndex(Parse * pParse, Index * pIndex, int iSpaceId, int iIndexId,
 			  SQL_SUBTYPE_MSGPACK,zParts, P4_STATIC);
 	sqlite3VdbeAddOp3(v, OP_MakeRecord, iFirstCol, 6, iRecord);
 	sqlite3VdbeAddOp2(v, OP_SInsert, BOX_INDEX_ID, iRecord);
-	if (pIndex->idxType == SQLITE_IDXTYPE_APPDEF)
+	if (pIndex->index_type == SQL_INDEX_TYPE_NON_UNIQUE ||
+	    pIndex->index_type == SQL_INDEX_TYPE_UNIQUE)
 		sqlite3VdbeChangeP5(v, OPFLAG_NCHANGE);
 }
 
@@ -1463,7 +1381,7 @@ makeIndexSchemaRecord(Parse * pParse,
 
 	sqlite3VdbeAddOp4(v,
 			  OP_String8, 0, iFirstCol, 0,
-			  sqlite3DbStrDup(pParse->db, pIndex->zName),
+			  sqlite3DbStrDup(pParse->db, pIndex->def->name),
 			  P4_DYNAMIC);
 
 	if (pParse->pNewTable) {
@@ -1746,7 +1664,6 @@ sqlite3EndTable(Parse * pParse,	/* Parse context */
 				"- %s", p->def->name);
 		return;
 	}
-
 	if (db->init.busy) {
 		/*
 		 * As rebuild creates a new ExpList tree and
@@ -2415,15 +2332,16 @@ sqlite3RefillIndex(Parse * pParse, Index * pIndex, int memRootPage)
 	} else {
 		tnum = pIndex->tnum;
 	}
-	struct key_def *def = key_def_dup(sql_index_key_def(pIndex));
+	struct key_def *def = key_def_dup(pIndex->def->key_def);
 	if (def == NULL) {
 		sqlite3OomFault(db);
 		return;
 	}
 	/* Open the sorter cursor if we are to use one. */
 	iSorter = pParse->nTab++;
-	sqlite3VdbeAddOp4(v, OP_SorterOpen, iSorter, 0, pIndex->nColumn,
-			  (char *)def, P4_KEYDEF);
+	sqlite3VdbeAddOp4(v, OP_SorterOpen, iSorter, 0,
+			  pIndex->def->key_def->part_count, (char *)def,
+			  P4_KEYDEF);
 
 	/* Open the table. Loop through all rows of the table, inserting index
 	 * records into the sorter.
@@ -2455,7 +2373,8 @@ sqlite3RefillIndex(Parse * pParse, Index * pIndex, int memRootPage)
 		sqlite3VdbeGoto(v, j2);
 		addr2 = sqlite3VdbeCurrentAddr(v);
 		sqlite3VdbeAddOp4Int(v, OP_SorterCompare, iSorter, j2,
-				     regRecord, pIndex->nColumn);
+				     regRecord,
+				     pIndex->def->key_def->part_count);
 		VdbeCoverage(v);
 		parser_emit_unique_constraint(pParse, ON_CONFLICT_ACTION_ABORT,
 					      pIndex);
@@ -2475,47 +2394,6 @@ sqlite3RefillIndex(Parse * pParse, Index * pIndex, int memRootPage)
 	sqlite3VdbeAddOp1(v, OP_Close, iSorter);
 }
 
-/*
- * Allocate heap space to hold an Index object with nCol columns.
- *
- * Increase the allocation size to provide an extra nExtra bytes
- * of 8-byte aligned space after the Index object and return a
- * pointer to this extra space in *ppExtra.
- */
-Index *
-sqlite3AllocateIndexObject(sqlite3 * db,	/* Database connection */
-			   i16 nCol,	/* Total number of columns in the index */
-			   int nExtra,	/* Number of bytes of extra space to alloc */
-			   char **ppExtra	/* Pointer to the "extra" space */
-    )
-{
-	Index *p;		/* Allocated index object */
-	int nByte;		/* Bytes of space for Index object + arrays */
-
-	nByte = ROUND8(sizeof(Index)) +		    /* Index structure   */
-	    ROUND8(sizeof(struct coll *) * nCol) +  /* Index.coll_array  */
-	    ROUND8(sizeof(uint32_t) * nCol) +       /* Index.coll_id_array*/
-	    ROUND8(sizeof(LogEst) * (nCol + 1) +    /* Index.aiRowLogEst */
-		   sizeof(i16) * nCol +		    /* Index.aiColumn    */
-		   sizeof(enum sort_order) * nCol); /* Index.sort_order  */
-	p = sqlite3DbMallocZero(db, nByte + nExtra);
-	if (p) {
-		char *pExtra = ((char *)p) + ROUND8(sizeof(Index));
-		p->coll_array = (struct coll **)pExtra;
-		pExtra += ROUND8(sizeof(struct coll **) * nCol);
-		p->coll_id_array = (uint32_t *) pExtra;
-		pExtra += ROUND8(sizeof(uint32_t) * nCol);
-		p->aiRowLogEst = (LogEst *) pExtra;
-		pExtra += sizeof(LogEst) * (nCol + 1);
-		p->aiColumn = (i16 *) pExtra;
-		pExtra += sizeof(i16) * nCol;
-		p->sort_order = (enum sort_order *) pExtra;
-		p->nColumn = nCol;
-		*ppExtra = ((char *)p) + nByte;
-	}
-	return p;
-}
-
 /*
  * Generate code to determine next free Iid in the space identified by
  * the iSpaceId. Return register number holding the result.
@@ -2557,84 +2435,150 @@ getNewIid(Parse * pParse, int iSpaceId, int iCursor)
 	return iRes;
 }
 
-/*
- * Add new index to pTab indexes list
+/**
+ * Add new index to table's indexes list.
+ * We follow convention that PK comes first in list.
  *
- * When adding an index to the list of indexes for a table, we
- * maintain special order of the indexes in the list:
- * 1. PK (go first just for simplicity)
- * 2. ON_CONFLICT_ACTION_REPLACE indexes
- * 3. ON_CONFLICT_ACTION_IGNORE indexes
- * This is necessary for the correct constraint check
- * processing (in sqlite3GenerateConstraintChecks()) as part of
- * UPDATE and INSERT statements.
+ * @param index Index to be added to list.
+ * @param tab Table to which belongs given index.
  */
 static void
-addIndexToTable(Index * pIndex, Table * pTab)
+table_add_index(struct Table *tab, struct Index *index)
 {
-	if (IsPrimaryKeyIndex(pIndex)) {
-		assert(sqlite3PrimaryKeyIndex(pTab) == NULL);
-		pIndex->pNext = pTab->pIndex;
-		pTab->pIndex = pIndex;
-		return;
+	struct Index *pk = sqlite3PrimaryKeyIndex(tab);
+	if (pk != NULL) {
+		index->pNext = pk->pNext;
+		pk->pNext = index;
+	} else {
+		index->pNext = tab->pIndex;
+		tab->pIndex = index;
 	}
-	if (pIndex->onError != ON_CONFLICT_ACTION_REPLACE || pTab->pIndex == 0
-	    || pTab->pIndex->onError == ON_CONFLICT_ACTION_REPLACE) {
-		Index *pk = sqlite3PrimaryKeyIndex(pTab);
-		if (pk) {
-			pIndex->pNext = pk->pNext;
-			pk->pNext = pIndex;
-		} else {
-			pIndex->pNext = pTab->pIndex;
-			pTab->pIndex = pIndex;
+}
+
+/**
+ * Create and set index_def in the given Index.
+ *
+ * @param parse Parse context.
+ * @param index Index for which index_def should be created. It is
+ *              used only to set index_def at the end of the
+ *              function.
+ * @param table Table which is indexed by 'index' param.
+ * @param iid Index ID.
+ * @param name Index name.
+ * @param name_len Index name length.
+ * @param expr_list List of expressions, describe which columns
+ *                  of 'table' are used in index and also their
+ *                  collations, orders, etc.
+ * @param idx_type Index type: non-unique index, unique index,
+ *                 index implementing UNIQUE constraint or
+ *                 index implementing PK constraint.
+ * @param sql_stmt SQL statement, which creates the index.
+ * @retval 0 on success, -1 on error.
+ */
+static int
+index_fill_def(struct Parse *parse, struct Index *index,
+	       struct Table *table, uint32_t iid, const char *name,
+	       uint32_t name_len, struct ExprList *expr_list,
+	       enum sql_index_type idx_type, char *sql_stmt)
+{
+	struct space_def *space_def = table->def;
+	struct index_opts opts;
+	index_opts_create(&opts);
+	opts.is_unique = idx_type != SQL_INDEX_TYPE_NON_UNIQUE;
+	opts.sql = sql_stmt;
+	index->def = NULL;
+	int rc = -1;
+
+	struct key_def *key_def = key_def_new(expr_list->nExpr);
+	if (key_def == NULL)
+		goto tnt_error;
+
+	for (int i = 0; i < expr_list->nExpr; i++) {
+		struct Expr *expr = expr_list->a[i].pExpr;
+		sql_resolve_self_reference(parse, table, NC_IdxExpr, expr, 0);
+		if (parse->nErr > 0)
+			goto cleanup;
+
+		struct Expr *column_expr = sqlite3ExprSkipCollate(expr);
+		if (column_expr->op != TK_COLUMN) {
+			diag_set(ClientError, ER_UNSUPPORTED, "Tarantool",
+				 "functional indexes");
+			goto tnt_error;
 		}
-	} else {
-		Index *pOther = pTab->pIndex;
-		while (pOther->pNext && pOther->pNext->onError
-		       != ON_CONFLICT_ACTION_REPLACE) {
-			pOther = pOther->pNext;
+
+		uint32_t fieldno = column_expr->iColumn;
+		uint32_t coll_id;
+		struct coll *coll;
+		if (expr->op == TK_COLLATE) {
+			coll = sql_get_coll_seq(parse, expr->u.zToken,
+						&coll_id);
+			if (coll == NULL &&
+			    strcasecmp(expr->u.zToken, "binary") != 0) {
+				diag_set(ClientError, ER_NO_SUCH_COLLATION,
+					 expr->u.zToken);
+				goto tnt_error;
+			}
+		} else {
+			coll = sql_column_collation(space_def, fieldno,
+						    &coll_id);
 		}
-		pIndex->pNext = pOther->pNext;
-		pOther->pNext = pIndex;
+		/*
+		 * Tarantool: DESC indexes are not supported so
+		 * far.
+		 */
+		key_def_set_part(key_def, i, fieldno,
+				 space_def->fields[fieldno].type,
+				 space_def->fields[fieldno].nullable_action,
+				 coll, coll_id, SORT_ORDER_ASC);
 	}
+	/*
+	 * Index def of PK is set to be NULL since it matters
+	 * only for comparison routine. Meanwhile on front-end
+	 * side only definition is used.
+	 */
+	index->def = index_def_new(space_def->id, 0, name, name_len, TREE,
+				   &opts, key_def, NULL);
+	if (index->def == NULL)
+		goto tnt_error;
+	index->def->iid = iid;
+	rc = 0;
+cleanup:
+	if (key_def != NULL)
+		key_def_delete(key_def);
+	return rc;
+tnt_error:
+	parse->rc = SQL_TARANTOOL_ERROR;
+	++parse->nErr;
+	goto cleanup;
 }
 
-bool
-index_is_unique(Index *idx)
+/**
+ * Simple attempt at figuring out whether constraint was created
+ * with name or without.
+ */
+static bool
+constraint_is_named(const char *name)
 {
-	assert(idx != NULL);
-	uint32_t space_id = SQLITE_PAGENO_TO_SPACEID(idx->tnum);
-	uint32_t index_id = SQLITE_PAGENO_TO_INDEXID(idx->tnum);
-	struct space *space = space_by_id(space_id);
-	assert(space != NULL);
-	struct index *tnt_index = space_index(space, index_id);
-	assert(tnt_index != NULL);
-
-	return tnt_index->def->opts.is_unique;
+	return strncmp(name, "sql_autoindex_", strlen("sql_autoindex_"));
 }
 
 void
 sql_create_index(struct Parse *parse, struct Token *token,
 		 struct SrcList *tbl_name, struct ExprList *col_list,
-		 int on_error, struct Token *start, struct Expr *where,
-		 enum sort_order sort_order, bool if_not_exist, u8 idx_type)
-{
-	Table *pTab = 0;	/* Table to be indexed */
-	Index *pIndex = 0;	/* The index to be created */
-	char *zName = 0;	/* Name of the index */
-	int nName;		/* Number of characters in zName */
-	int i, j;
-	DbFixer sFix;		/* For assigning database names to pTable */
-	sqlite3 *db = parse->db;
-	struct ExprList_item *col_listItem;	/* For looping over col_list */
-	int nExtra = 0;		/* Space allocated for zExtra[] */
-	char *zExtra = 0;	/* Extra space after the Index object */
+		 enum on_conflict_action on_error, struct Token *start,
+		 struct Expr *where, enum sort_order sort_order,
+		 bool if_not_exist, enum sql_index_type idx_type) {
+	/* The index to be created. */
+	struct Index *index = NULL;
+	/* Name of the index. */
+	char *name = NULL;
+	struct sqlite3 *db = parse->db;
 	struct session *user_session = current_session();
 
-	if (db->mallocFailed || parse->nErr > 0) {
+	if (db->mallocFailed || parse->nErr > 0)
 		goto exit_create_index;
-	}
-	if (idx_type == SQLITE_IDXTYPE_APPDEF) {
+	if (idx_type == SQL_INDEX_TYPE_UNIQUE ||
+	    idx_type == SQL_INDEX_TYPE_NON_UNIQUE) {
 		Vdbe *v = sqlite3GetVdbe(parse);
 		if (v == NULL)
 			goto exit_create_index;
@@ -2643,107 +2587,123 @@ sql_create_index(struct Parse *parse, struct Token *token,
 	assert(db->pSchema != NULL);
 
 	/*
-	 * Find the table that is to be indexed.  Return early if not found.
+	 * Find the table that is to be indexed.
+	 * Return early if not found.
 	 */
+	struct Table *table = NULL;
 	if (tbl_name != NULL) {
-
-		/* Use the two-part index name to determine the database
-		 * to search for the table. 'Fix' the table name to this db
-		 * before looking up the table.
-		 */
-		assert(token && token->z);
-
-		sqlite3FixInit(&sFix, parse, "index", token);
-		if (sqlite3FixSrcList(&sFix, tbl_name)) {
-			/* Because the parser constructs tbl_name from a single identifier,
-			 * sqlite3FixSrcList can never fail.
-			 */
-			assert(0);
-		}
-		pTab = sqlite3LocateTable(parse, 0, tbl_name->a[0].zName);
-		assert(db->mallocFailed == 0 || pTab == 0);
-		if (pTab == 0)
-			goto exit_create_index;
-		sqlite3PrimaryKeyIndex(pTab);
+		assert(token != NULL && token->z != NULL);
+		table = sqlite3LocateTable(parse, 0, tbl_name->a[0].zName);
+		assert(db->mallocFailed == 0 || table == NULL);
 	} else {
 		assert(token == NULL);
 		assert(start == NULL);
-		pTab = parse->pNewTable;
-		if (!pTab)
-			goto exit_create_index;
+		table = parse->pNewTable;
 	}
 
-	assert(pTab != 0);
-	assert(parse->nErr == 0);
-	if (pTab->def->opts.is_view) {
-		sqlite3ErrorMsg(parse, "views may not be indexed");
+	if (table == NULL)
+		goto exit_create_index;
+
+	if (table->def->opts.is_view) {
+		sqlite3ErrorMsg(parse, "views can not be indexed");
 		goto exit_create_index;
 	}
 	/*
 	 * Find the name of the index.  Make sure there is not
-	 * already another index or table with the same name.
+	 * already another index with the same name.
 	 *
 	 * Exception:  If we are reading the names of permanent
 	 * indices from the Tarantool schema (because some other
 	 * process changed the schema) and one of the index names
-	 * collides with the name of a temporary table or index,
-	 * then we will continue to process this index.
+	 * collides with the name of index, then we will continue
+	 * to process this index.
 	 *
 	 * If token == NULL it means that we are dealing with a
 	 * primary key or UNIQUE constraint.  We have to invent
 	 * our own name.
+	 *
+	 * In case of UNIQUE constraint we have two options:
+	 * 1) UNIQUE constraint is named and this name will
+	 *    be a part of index name.
+	 * 2) UNIQUE constraint is non-named and standard
+	 *    auto-index name will be generated.
 	 */
-	if (token) {
-		zName = sqlite3NameFromToken(db, token);
-		if (zName == 0)
+	if (token != NULL) {
+		name = sqlite3NameFromToken(db, token);
+		if (name == NULL)
 			goto exit_create_index;
-		assert(token->z != 0);
-		if (!db->init.busy) {
-			if (sqlite3HashFind(&db->pSchema->tblHash, zName) !=
-			    NULL) {
-				sqlite3ErrorMsg(parse,
-						"there is already a table named %s",
-						zName);
-				goto exit_create_index;
-			}
-		}
-		if (sqlite3HashFind(&pTab->idxHash, zName) != NULL) {
+		assert(token->z != NULL);
+		if (sqlite3HashFind(&table->idxHash, name) != NULL) {
 			if (!if_not_exist) {
 				sqlite3ErrorMsg(parse,
 						"index %s.%s already exists",
-						pTab->def->name, zName);
-			} else {
-				assert(!db->init.busy);
+						table->def->name, name);
 			}
 			goto exit_create_index;
 		}
 	} else {
-		int n;
-		Index *pLoop;
-		for (pLoop = pTab->pIndex, n = 1; pLoop;
-		     pLoop = pLoop->pNext, n++) {
-		}
-		zName =
-		    sqlite3MPrintf(db, "sqlite_autoindex_%s_%d", pTab->def->name,
-				   n);
-		if (zName == 0) {
-			goto exit_create_index;
+		char *constraint_name = NULL;
+		if (parse->constraintName.z != NULL)
+			constraint_name =
+				sqlite3NameFromToken(db,
+						     &parse->constraintName);
+
+		if (constraint_name == NULL ||
+		    strcmp(constraint_name, "") == 0) {
+			uint32_t n = 1;
+			for (struct Index *idx = table->pIndex; idx != NULL;
+			     idx = idx->pNext, n++);
+			name = sqlite3MPrintf(db, "sql_autoindex_%s_%d",
+					      table->def->name, n);
+		} else {
+			/*
+			 * This naming is temporary. Now it's not
+			 * possible (since we implement UNIQUE
+			 * and PK constraints with indexes and
+			 * indexes can not have same names), but
+			 * in future we would use names exactly
+			 * as they are set by user.
+			 */
+			if (idx_type == SQL_INDEX_TYPE_CONSTRAINT_UNIQUE)
+				name = sqlite3MPrintf(db,
+						      "unique_constraint_%s",
+						      constraint_name);
+			if (idx_type == SQL_INDEX_TYPE_CONSTRAINT_PK)
+				name = sqlite3MPrintf(db, "pk_constraint_%s",
+						      constraint_name);
 		}
+		sqlite3DbFree(db, constraint_name);
+	}
+
+	if (name == NULL || sqlite3CheckIdentifierName(parse, name) != 0)
+		goto exit_create_index;
+
+	bool is_system_space = BOX_SYSTEM_ID_MIN < table->def->id &&
+			       table->def->id < BOX_SYSTEM_ID_MAX;
+	if (is_system_space && (idx_type == SQL_INDEX_TYPE_NON_UNIQUE ||
+				idx_type == SQL_INDEX_TYPE_UNIQUE)) {
+		diag_set(ClientError, ER_MODIFY_INDEX, name,
+			 table->def->name,
+			 "can't create index on system space");
+		parse->nErr++;
+		parse->rc = SQL_TARANTOOL_ERROR;
+		goto exit_create_index;
 	}
 
 	/*
 	 * If col_list == NULL, it means this routine was called
-	 * to make a primary key out of the last column added to
-	 * the table under construction. So create a fake list to
-	 * simulate this.
+	 * to make a primary key or unique constraint out of the
+	 * last column added to the table under construction.
+	 * So create a fake list to simulate this.
 	 */
 	if (col_list == NULL) {
-		Token prevCol;
-		uint32_t last_field = pTab->def->field_count - 1;
-		sqlite3TokenInit(&prevCol, pTab->def->fields[last_field].name);
+		struct Token prev_col;
+		uint32_t last_field = table->def->field_count - 1;
+		sqlite3TokenInit(&prev_col,
+				 table->def->fields[last_field].name);
 		col_list = sql_expr_list_append(parse->db, NULL,
 						sqlite3ExprAlloc(db, TK_ID,
-								 &prevCol, 0));
+								 &prev_col, 0));
 		if (col_list == NULL)
 			goto exit_create_index;
 		assert(col_list->nExpr == 1);
@@ -2752,191 +2712,178 @@ sql_create_index(struct Parse *parse, struct Token *token,
 		sqlite3ExprListCheckLength(parse, col_list, "index");
 	}
 
-	/* Figure out how many bytes of space are required to store explicitly
-	 * specified collation sequence names.
-	 */
-	for (i = 0; i < col_list->nExpr; i++) {
-		Expr *pExpr = col_list->a[i].pExpr;
-		assert(pExpr != 0);
-		if (pExpr->op == TK_COLLATE) {
-			nExtra += (1 + sqlite3Strlen30(pExpr->u.zToken));
-		}
+	index = sqlite3DbMallocZero(db, sizeof(*index));
+	if (index == NULL)
+		goto exit_create_index;
+
+	index->pTable = table;
+	index->onError = (u8) on_error;
+	index->index_type = idx_type;
+	index->pSchema = db->pSchema;
+	/* Tarantool have access to each column by any index. */
+	if (where != NULL) {
+		sql_resolve_self_reference(parse, table, NC_PartIdx, where,
+					   NULL);
+		index->pPartIdxWhere = where;
+		where = NULL;
 	}
 
 	/*
-	 * Allocate the index structure.
+	 * TODO: Issue a warning if two or more columns of the
+	 * index are identical.
+	 * TODO: Issue a warning if the table primary key is used
+	 * as part of the index key.
 	 */
-	nName = sqlite3Strlen30(zName);
-	pIndex = sqlite3AllocateIndexObject(db, col_list->nExpr,
-					    nName + nExtra + 1, &zExtra);
-	if (db->mallocFailed) {
-		goto exit_create_index;
+	char *sql_stmt = "";
+	if (!db->init.busy && tbl_name != NULL) {
+		int n = (int) (parse->sLastToken.z - token->z) +
+			parse->sLastToken.n;
+		if (token->z[n - 1] == ';')
+			n--;
+		sql_stmt = sqlite3MPrintf(db, "CREATE%s INDEX %.*s",
+			   		  idx_type == SQL_INDEX_TYPE_NON_UNIQUE ?
+					  "" : " UNIQUE", n, token->z);
+		if (sql_stmt == NULL)
+			goto exit_create_index;
 	}
-	assert(EIGHT_BYTE_ALIGNMENT(pIndex->aiRowLogEst));
-	assert(EIGHT_BYTE_ALIGNMENT(pIndex->coll_array));
-	pIndex->zName = zExtra;
-	zExtra += nName + 1;
-	memcpy(pIndex->zName, zName, nName + 1);
-	pIndex->pTable = pTab;
-	pIndex->onError = (u8) on_error;
 	/*
-	 * Don't make difference between UNIQUE indexes made by user
-	 * using CREATE INDEX statement and those created during
-	 * CREATE TABLE processing.
+	 * If it is parsing stage, then iid may have any value
+	 * (for the simplicity sake we set it to 1), but PK
+	 * still must have iid == 0.
 	 */
-	if (idx_type == SQLITE_IDXTYPE_APPDEF &&
-	    on_error != ON_CONFLICT_ACTION_NONE) {
-		pIndex->idxType = SQLITE_IDXTYPE_UNIQUE;
-	} else {
-		pIndex->idxType = idx_type;
-	}
-	pIndex->pSchema = db->pSchema;
-	pIndex->nColumn = col_list->nExpr;
-	/* Tarantool have access to each column by any index */
-	if (where) {
-		sql_resolve_self_reference(parse, pTab, NC_PartIdx, where,
-					   NULL);
-		pIndex->pPartIdxWhere = where;
-		where = NULL;
-	}
+	uint32_t iid = idx_type != SQL_INDEX_TYPE_CONSTRAINT_PK;
+	if (db->init.busy)
+		iid = SQLITE_PAGENO_TO_INDEXID(db->init.newTnum);
 
-	/* Analyze the list of expressions that form the terms of the index and
-	 * report any errors.  In the common case where the expression is exactly
-	 * a table column, store that column in aiColumn[].
-	 *
-	 * TODO: Issue a warning if two or more columns of the index are identical.
-	 * TODO: Issue a warning if the table primary key is used as part of the
-	 * index key.
+	if (index_fill_def(parse, index, table, iid, name, strlen(name),
+			   col_list, idx_type, sql_stmt) != 0)
+		goto exit_create_index;
+	/*
+	 * Remove all redundant columns from the PRIMARY KEY.
+	 * For example, change "PRIMARY KEY(a,b,a,b,c,b,c,d)" into
+	 * just "PRIMARY KEY(a,b,c,d)". Later code assumes the
+	 * PRIMARY KEY contains no repeated columns.
 	 */
-	for (i = 0, col_listItem = col_list->a; i < col_list->nExpr;
-	     i++, col_listItem++) {
-		Expr *pCExpr;	/* The i-th index expression */
-		sql_resolve_self_reference(parse, pTab, NC_IdxExpr,
-					   col_listItem->pExpr, NULL);
-		if (parse->nErr > 0)
-			goto exit_create_index;
-		pCExpr = sqlite3ExprSkipCollate(col_listItem->pExpr);
-		if (pCExpr->op != TK_COLUMN) {
-			sqlite3ErrorMsg(parse,
-					"functional indexes aren't supported "
-					"in the current version");
-			goto exit_create_index;
-		} else {
-			j = pCExpr->iColumn;
-			assert(j <= 0x7fff);
-			if (j < 0) {
-				j = pTab->iPKey;
-			}
-			pIndex->aiColumn[i] = (i16) j;
-		}
-		struct coll *coll;
-		uint32_t id;
-		if (col_listItem->pExpr->op == TK_COLLATE) {
-			const char *coll_name = col_listItem->pExpr->u.zToken;
-			coll = sql_get_coll_seq(parse, coll_name, &id);
-
-			if (coll == NULL &&
-			    sqlite3StrICmp(coll_name, "binary") != 0) {
-				goto exit_create_index;
-			}
-		} else if (j >= 0) {
-			coll = sql_column_collation(pTab->def, j, &id);
-		} else {
-			id = COLL_NONE;
-			coll = NULL;
+	struct key_part *parts = index->def->key_def->parts;
+	uint32_t part_count = index->def->key_def->part_count;
+	uint32_t new_part_count = 1;
+	for(uint32_t i = 1; i < part_count; i++) {
+		uint32_t j;
+		for(j = 0; j < new_part_count; j++) {
+			if(parts[i].fieldno == parts[j].fieldno)
+				break;
 		}
-		pIndex->coll_array[i] = coll;
-		pIndex->coll_id_array[i] = id;
 
-		/* Tarantool: DESC indexes are not supported so far.
-		 * See gh-3016.
-		 */
-		pIndex->sort_order[i] = SORT_ORDER_ASC;
+		if (j == new_part_count)
+			parts[new_part_count++] = parts[i];
 	}
-	if (pTab == parse->pNewTable) {
-		/* This routine has been called to create an automatic index as a
-		 * result of a PRIMARY KEY or UNIQUE clause on a column definition, or
-		 * a PRIMARY KEY or UNIQUE clause following the column definitions.
-		 * i.e. one of:
-		 *
-		 * CREATE TABLE t(x PRIMARY KEY, y);
-		 * CREATE TABLE t(x, y, UNIQUE(x, y));
-		 *
-		 * Either way, check to see if the table already has such an index. If
-		 * so, don't bother creating this one. This only applies to
-		 * automatically created indices. Users can do as they wish with
-		 * explicit indices.
-		 *
-		 * Two UNIQUE or PRIMARY KEY constraints are considered equivalent
-		 * (and thus suppressing the second one) even if they have different
-		 * sort orders.
-		 *
-		 * If there are different collating sequences or if the columns of
-		 * the constraint occur in different orders, then the constraints are
-		 * considered distinct and both result in separate indices.
-		 */
-		Index *pIdx;
-		for (pIdx = pTab->pIndex; pIdx; pIdx = pIdx->pNext) {
-			int k;
-			assert(IsUniqueIndex(pIdx));
-			assert(pIdx->idxType != SQLITE_IDXTYPE_APPDEF);
-			assert(IsUniqueIndex(pIndex));
-
-			if (pIdx->nColumn != pIndex->nColumn)
+	index->def->key_def->part_count = new_part_count;
+
+	if (!index_def_is_valid(index->def, table->def->name))
+		goto exit_create_index;
+
+	/*
+	 * Here we handle cases, when in CREATE TABLE statement
+	 * some UNIQUE constraints are putted exactly on the same
+	 * columns with PRIMARY KEY constraint. Our general
+	 * intention is to omit creating indexes for non-named
+	 * UNIQUE constraints if these constraints are putted on
+	 * the same columns as the PRIMARY KEY constraint. In
+	 * different cases it is implemented in different ways.
+	 *
+	 * 1) CREATE TABLE t(a UNIQUE PRIMARY KEY)
+	 *    CREATE TABLE t(a, UNIQUE(a), PRIMARY KEY(a))
+	 *    In these cases we firstly proceed UNIQUE(a)
+	 *    and create index for it, then proceed PRIMARY KEY,
+	 *    but don't create index for it. Instead of it we
+	 *    change UNIQUE constraint index name and index_type,
+	 *    so it becomes PRIMARY KEY index.
+	 *
+	 * 2) CREATE TABLE t(a, PRIMARY KEY(a), UNIQUE(a))
+	 *    In such cases we simply do not create index for
+	 *    UNIQUE constraint.
+	 *
+	 * Note 1: We always create new index for named UNIQUE
+	 * constraints.
+	 *
+	 * Note 2: If UNIQUE constraint (no matter named or
+	 * non-named) is putted on the same columns as PRIMARY KEY
+	 * constraint, but has different onError (behavior on
+	 * constraint violation), then an error is raised.
+	 */
+	if (table == parse->pNewTable) {
+		for (struct Index *existing_idx = table->pIndex;
+		     existing_idx != NULL; existing_idx = existing_idx->pNext) {
+			struct key_def *key_def = index->def->key_def;
+			struct key_def *exst_key_def =
+				existing_idx->def->key_def;
+
+			if (key_def->part_count != exst_key_def->part_count)
 				continue;
-			for (k = 0; k < pIdx->nColumn; k++) {
-				assert(pIdx->aiColumn[k] >= 0);
-				if (pIdx->aiColumn[k] != pIndex->aiColumn[k])
+
+			uint32_t k;
+			for (k = 0; k < key_def->part_count; k++) {
+				if (key_def->parts[k].fieldno !=
+				    exst_key_def->parts[k].fieldno)
 					break;
-				struct coll *coll1, *coll2;
-				uint32_t id;
-				coll1 = sql_index_collation(pIdx, k, &id);
-				coll2 = sql_index_collation(pIndex, k, &id);
-				if (coll1 != coll2)
+				if (key_def->parts[k].coll !=
+				    exst_key_def->parts[k].coll)
 					break;
 			}
-			if (k == pIdx->nColumn) {
-				if (pIdx->onError != pIndex->onError) {
-					/* This constraint creates the same index as a previous
-					 * constraint specified somewhere in the CREATE TABLE statement.
-					 * However the ON CONFLICT clauses are different. If both this
-					 * constraint and the previous equivalent constraint have explicit
-					 * ON CONFLICT clauses this is an error. Otherwise, use the
-					 * explicitly specified behavior for the index.
-					 */
-					if (!
-					    (pIdx->onError == ON_CONFLICT_ACTION_DEFAULT
-					     || pIndex->onError ==
-					     ON_CONFLICT_ACTION_DEFAULT)) {
-						sqlite3ErrorMsg(parse,
-								"conflicting ON CONFLICT clauses specified",
-								0);
-					}
-					if (pIdx->onError == ON_CONFLICT_ACTION_DEFAULT) {
-						pIdx->onError = pIndex->onError;
-					}
+
+			if (k != key_def->part_count)
+				continue;
+
+			if (index->onError != existing_idx->onError) {
+				if (index->onError !=
+				    ON_CONFLICT_ACTION_DEFAULT &&
+				    existing_idx->onError !=
+				    ON_CONFLICT_ACTION_DEFAULT)
+					sqlite3ErrorMsg(parse,
+							"conflicting "\
+							"ON CONFLICT "\
+							"clauses specified");
+
+				if (existing_idx->onError ==
+				    ON_CONFLICT_ACTION_DEFAULT)
+					existing_idx->onError = index->onError;
+			}
+
+			bool is_named =
+				constraint_is_named(existing_idx->def->name);
+			/* CREATE TABLE t(a, UNIQUE(a), PRIMARY KEY(a)). */
+			if (idx_type == SQL_INDEX_TYPE_CONSTRAINT_PK) {
+				if (existing_idx->index_type ==
+				    SQL_INDEX_TYPE_CONSTRAINT_UNIQUE &&
+				    !is_named) {
+					existing_idx->index_type =
+						SQL_INDEX_TYPE_CONSTRAINT_PK;
+					goto exit_create_index;
 				}
-				if (idx_type == SQLITE_IDXTYPE_PRIMARYKEY)
-					pIdx->idxType = idx_type;
-				goto exit_create_index;
 			}
+
+			/* CREATE TABLE t(a, PRIMARY KEY(a), UNIQUE(a)). */
+			if (idx_type == SQL_INDEX_TYPE_CONSTRAINT_UNIQUE &&
+			    !constraint_is_named(index->def->name))
+				goto exit_create_index;
 		}
 	}
-
-	/* Link the new Index structure to its table and to the other
-	 * in-memory database structures.
+	/*
+	 * Link the new Index structure to its table and to the
+	 * other in-memory database structures.
 	 */
 	assert(parse->nErr == 0);
 	if (db->init.busy) {
-		Index *p;
-		p = sqlite3HashInsert(&pTab->idxHash, pIndex->zName, pIndex);
-		if (p) {
-			assert(p == pIndex);	/* Malloc must have failed */
+		struct Index *p = sqlite3HashInsert(&table->idxHash,
+						    index->def->name, index);
+		if (p != NULL) {
+			/* Malloc must have failed. */
+			assert(p == index);
 			sqlite3OomFault(db);
 			goto exit_create_index;
 		}
 		user_session->sql_flags |= SQLITE_InternChanges;
-		pIndex->tnum = db->init.newTnum;
+		index->tnum = db->init.newTnum;
 	}
 
 	/*
@@ -2954,113 +2901,53 @@ sql_create_index(struct Parse *parse, struct Token *token,
 	 * initialization step can be skipped.
 	 */
 	else if (tbl_name != NULL) {
-		Vdbe *v;
-		char *zStmt;
-		int iCursor = parse->nTab++;
-		int iSpaceId, iIndexId, iFirstSchemaCol;
+		Vdbe *vdbe;
+		int cursor = parse->nTab++;
+		int space_id, index_id, first_schema_col;
 
-		v = sqlite3GetVdbe(parse);
-		if (v == 0)
+		vdbe = sqlite3GetVdbe(parse);
+		if (vdbe == 0)
 			goto exit_create_index;
 
 		sql_set_multi_write(parse, true);
-		sqlite3VdbeAddOp4(v, OP_OpenWrite, iCursor, 0, 0,
+		sqlite3VdbeAddOp4(vdbe, OP_OpenWrite, cursor, 0, 0,
 				  (void *)space_by_id(BOX_INDEX_ID),
 				  P4_SPACEPTR);
-		sqlite3VdbeChangeP5(v, OPFLAG_SEEKEQ);
+		sqlite3VdbeChangeP5(vdbe, OPFLAG_SEEKEQ);
 
-		/*
-		 * Gather the complete text of the CREATE INDEX
-		 * statement into the zStmt variable
-		 */
 		assert(start != NULL);
-		int n = (int)(parse->sLastToken.z - token->z) +
-			parse->sLastToken.n;
-		if (token->z[n - 1] == ';')
-			n--;
-		/* A named index with an explicit CREATE INDEX statement */
-		zStmt = sqlite3MPrintf(db, "CREATE%s INDEX %.*s", on_error ==
-				       ON_CONFLICT_ACTION_NONE ? "" : " UNIQUE",
-				       n, token->z);
-
-		iSpaceId = SQLITE_PAGENO_TO_SPACEID(pTab->tnum);
-		iIndexId = getNewIid(parse, iSpaceId, iCursor);
-		sqlite3VdbeAddOp1(v, OP_Close, iCursor);
-		createIndex(parse, pIndex, iSpaceId, iIndexId, zStmt);
-
-		/* consumes zStmt */
-		iFirstSchemaCol =
-		    makeIndexSchemaRecord(parse, pIndex, iSpaceId, iIndexId,
-					  zStmt);
-
-		/* Reparse the schema. Code an OP_Expire
+		space_id = SQLITE_PAGENO_TO_SPACEID(table->tnum);
+		index_id = getNewIid(parse, space_id, cursor);
+		sqlite3VdbeAddOp1(vdbe, OP_Close, cursor);
+		createIndex(parse, index, space_id, index_id, sql_stmt);
+
+		/* Consumes sql_stmt. */
+		first_schema_col = makeIndexSchemaRecord(parse, index,
+							 space_id, index_id,
+							 sql_stmt);
+
+		/*
+		 * Reparse the schema. Code an OP_Expire
 		 * to invalidate all pre-compiled statements.
 		 */
 		sqlite3ChangeCookie(parse);
-		sqlite3VdbeAddParseSchema2Op(v, iFirstSchemaCol, 4);
-		sqlite3VdbeAddOp0(v, OP_Expire);
-	}
-
-	/* When adding an index to the list of indexes for a table, we
-	 * maintain special order of the indexes in the list:
-	 * 1. PK (go first just for simplicity)
-	 * 2. ON_CONFLICT_ACTION_REPLACE indexes
-	 * 3. ON_CONFLICT_ACTION_IGNORE indexes
-	 * This is necessary for the correct constraint check
-	 * processing (in sqlite3GenerateConstraintChecks()) as part of
-	 * UPDATE and INSERT statements.
-	 */
+		sqlite3VdbeAddParseSchema2Op(vdbe, first_schema_col, 4);
+		sqlite3VdbeAddOp0(vdbe, OP_Expire);
+	}
 
 	if (!db->init.busy && tbl_name != NULL)
 		goto exit_create_index;
-	addIndexToTable(pIndex, pTab);
-	pIndex = NULL;
+	table_add_index(table, index);
+	index = NULL;
 
-	/* Clean up before exiting */
+	/* Clean up before exiting. */
  exit_create_index:
-	if (pIndex)
-		freeIndex(db, pIndex);
+	if (index != NULL)
+		freeIndex(db, index);
 	sql_expr_delete(db, where, false);
 	sql_expr_list_delete(db, col_list);
 	sqlite3SrcListDelete(db, tbl_name);
-	sqlite3DbFree(db, zName);
-}
-
-/**
- * Return number of columns in given index.
- * If space is ephemeral, use internal
- * SQL structure to fetch the value.
- */
-uint32_t
-index_column_count(const Index *idx)
-{
-	assert(idx != NULL);
-	uint32_t space_id = SQLITE_PAGENO_TO_SPACEID(idx->tnum);
-	struct space *space = space_by_id(space_id);
-	/* It is impossible to find an ephemeral space by id. */
-	if (space == NULL)
-		return idx->nColumn;
-
-	uint32_t index_id = SQLITE_PAGENO_TO_INDEXID(idx->tnum);
-	struct index *index = space_index(space, index_id);
-	assert(index != NULL);
-	return index->def->key_def->part_count;
-}
-
-/** Return true if given index is unique and not nullable. */
-bool
-index_is_unique_not_null(const Index *idx)
-{
-	assert(idx != NULL);
-	uint32_t space_id = SQLITE_PAGENO_TO_SPACEID(idx->tnum);
-	struct space *space = space_by_id(space_id);
-	assert(space != NULL);
-
-	uint32_t index_id = SQLITE_PAGENO_TO_INDEXID(idx->tnum);
-	struct index *index = space_index(space, index_id);
-	assert(index != NULL);
-	return (index->def->opts.is_unique &&
-		!index->def->key_def->is_nullable);
+	sqlite3DbFree(db, name);
 }
 
 void
@@ -3244,7 +3131,7 @@ sqlite3IdListIndex(IdList * pList, const char *zName)
 }
 
 /*
- * Expand the space allocated for the given SrcList object by
+ * Expand the space allocazted for the given SrcList object by
  * creating nExtra new slots beginning at iStart.  iStart is zero based.
  * New slots are zeroed.
  *
@@ -3680,9 +3567,9 @@ parser_emit_unique_constraint(struct Parse *parser,
 	const struct space_def *def = index->pTable->def;
 	StrAccum err_accum;
 	sqlite3StrAccumInit(&err_accum, parser->db, 0, 0, 200);
-	for (int j = 0; j < index->nColumn; ++j) {
-		assert(index->aiColumn[j] >= 0);
-		const char *col_name = def->fields[index->aiColumn[j]].name;
+	struct key_part *part = index->def->key_def->parts;
+	for (uint32_t j = 0; j < index->def->key_def->part_count; ++j, ++part) {
+		const char *col_name = def->fields[part->fieldno].name;
 		if (j != 0)
 			sqlite3StrAccumAppend(&err_accum, ", ", 2);
 		sqlite3XPrintf(&err_accum, "%s.%s", def->name, col_name);
@@ -3703,11 +3590,11 @@ static bool
 collationMatch(struct coll *coll, struct Index *index)
 {
 	assert(coll != NULL);
-	for (int i = 0; i < index->nColumn; i++) {
-		uint32_t id;
-		struct coll *idx_coll = sql_index_collation(index, i, &id);
-		assert(idx_coll != 0 || index->aiColumn[i] < 0);
-		if (index->aiColumn[i] >= 0 && coll == idx_coll)
+	struct key_part *part = index->def->key_def->parts;
+	for (uint32_t i = 0; i < index->def->key_def->part_count; i++, part++) {
+		struct coll *idx_coll = part->coll;
+		assert(idx_coll != NULL);
+		if (coll == idx_coll)
 			return true;
 	}
 	return false;
diff --git a/src/box/sql/delete.c b/src/box/sql/delete.c
index ca1e77d65..91816075c 100644
--- a/src/box/sql/delete.c
+++ b/src/box/sql/delete.c
@@ -267,11 +267,12 @@ sql_table_delete_from(struct Parse *parse, struct SrcList *tab_list,
 
 		/* Extract the primary key for the current row */
 		if (!is_view) {
-			for (int i = 0; i < pk_len; i++) {
+			struct key_part *part = pk_def->parts;
+			for (int i = 0; i < pk_len; i++, part++) {
 				struct space_def *def = space->def;
 				sqlite3ExprCodeGetColumnOfTable(v, def,
 								tab_cursor,
-								pk_def->parts[i].fieldno,
+								part->fieldno,
 								reg_pk + i);
 			}
 		} else {
@@ -548,13 +549,14 @@ sql_generate_index_key(struct Parse *parse, struct Index *index, int cursor,
 			*part_idx_label = 0;
 		}
 	}
-	int col_cnt = index_column_count(index);
+	int col_cnt = index->def->key_def->part_count;
 	int reg_base = sqlite3GetTempRange(parse, col_cnt);
 	if (prev != NULL && (reg_base != reg_prev ||
 			     prev->pPartIdxWhere != NULL))
 		prev = NULL;
 	for (int j = 0; j < col_cnt; j++) {
-		if (prev != NULL && prev->aiColumn[j] == index->aiColumn[j]) {
+		if (prev != NULL && prev->def->key_def->parts[j].fieldno ==
+				    index->def->key_def->parts[j].fieldno) {
 			/*
 			 * This column was already computed by the
 			 * previous index.
diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
index b1650cfa3..ce1e6904f 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -2405,21 +2405,28 @@ sqlite3FindInIndex(Parse * pParse,	/* Parsing context */
 			     pIdx = pIdx->pNext) {
 				Bitmask colUsed; /* Columns of the index used */
 				Bitmask mCol;	/* Mask for the current column */
-				if (pIdx->nColumn < nExpr)
+				uint32_t part_count =
+					pIdx->def->key_def->part_count;
+				struct key_part *parts =
+					pIdx->def->key_def->parts;
+				if ((int)part_count < nExpr)
 					continue;
 				/* Maximum nColumn is BMS-2, not BMS-1, so that we can compute
 				 * BITMASK(nExpr) without overflowing
 				 */
-				testcase(pIdx->nColumn == BMS - 2);
-				testcase(pIdx->nColumn == BMS - 1);
-				if (pIdx->nColumn >= BMS - 1)
+				testcase(part_count == BMS - 2);
+				testcase(part_count == BMS - 1);
+				if (part_count >= BMS - 1)
+					continue;
+				if (mustBeUnique &&
+				    ((int)part_count > nExpr ||
+				     !pIdx->def->opts.is_unique)) {
+					/*
+					 * This index is not
+					 * unique over the IN RHS
+					 * columns.
+					 */
 					continue;
-				if (mustBeUnique) {
-					if (pIdx->nColumn > nExpr
-					    || (pIdx->nColumn > nExpr
-					    && !index_is_unique(pIdx))) {
-							continue;	/* This index is not unique over the IN RHS columns */
-					}
 				}
 
 				colUsed = 0;	/* Columns of index used so far */
@@ -2432,16 +2439,15 @@ sqlite3FindInIndex(Parse * pParse,	/* Parsing context */
 					int j;
 
 					for (j = 0; j < nExpr; j++) {
-						if (pIdx->aiColumn[j] !=
-						    pRhs->iColumn) {
+						if ((int) parts[j].fieldno !=
+						    pRhs->iColumn)
 							continue;
-						}
-						struct coll *idx_coll;
-						idx_coll = sql_index_collation(pIdx, j, &id);
+
+						struct coll *idx_coll =
+							     parts[j].coll;
 						if (pReq != NULL &&
-						    pReq != idx_coll) {
+						    pReq != idx_coll)
 							continue;
-						}
 						break;
 					}
 					if (j == nExpr)
@@ -2466,7 +2472,7 @@ sqlite3FindInIndex(Parse * pParse,	/* Parsing context */
 							  0, 0, 0,
 							  sqlite3MPrintf(db,
 							  "USING INDEX %s FOR IN-OPERATOR",
-							  pIdx->zName),
+							  pIdx->def->name),
 							  P4_DYNAMIC);
 					struct space *space =
 						space_by_id(SQLITE_PAGENO_TO_SPACEID(pIdx->tnum));
@@ -2475,12 +2481,11 @@ sqlite3FindInIndex(Parse * pParse,	/* Parsing context */
 									 tnum);
 					vdbe_emit_open_cursor(pParse, iTab,
 							      idx_id, space);
-					VdbeComment((v, "%s", pIdx->zName));
+					VdbeComment((v, "%s", pIdx->def->name));
 					assert(IN_INDEX_INDEX_DESC ==
 					       IN_INDEX_INDEX_ASC + 1);
 					eType = IN_INDEX_INDEX_ASC +
-						sql_index_column_sort_order(pIdx,
-									    0);
+						parts[0].sort_order;
 
 					if (prRhsHasNull) {
 #ifdef SQLITE_ENABLE_COLUMN_USED_MASK
@@ -2502,7 +2507,7 @@ sqlite3FindInIndex(Parse * pParse,	/* Parsing context */
 							/* Tarantool: Check for null is performed on first key of the index.  */
 							sqlite3SetHasNullFlag(v,
 									      iTab,
-									      pIdx->aiColumn[0],
+									      parts[0].fieldno,
 									      *prRhsHasNull);
 						}
 					}
@@ -3140,12 +3145,12 @@ sqlite3ExprCodeIN(Parse * pParse,	/* Parsing and code generating context */
 		struct Index *pk = sqlite3PrimaryKeyIndex(tab);
 		assert(pk);
 
+		uint32_t fieldno = pk->def->key_def->parts[0].fieldno;
 		enum affinity_type affinity =
-			tab->def->fields[pk->aiColumn[0]].affinity;
-		if (pk->nColumn == 1
-		    && affinity == AFFINITY_INTEGER
-		    && pk->aiColumn[0] < nVector) {
-			int reg_pk = rLhs + pk->aiColumn[0];
+			tab->def->fields[fieldno].affinity;
+		if (pk->def->key_def->part_count == 1 &&
+		    affinity == AFFINITY_INTEGER && (int)fieldno < nVector) {
+			int reg_pk = rLhs + (int)fieldno;
 			sqlite3VdbeAddOp2(v, OP_MustBeInt, reg_pk, destIfFalse);
 		}
 	}
@@ -3477,7 +3482,7 @@ sqlite3ExprCodeLoadIndexColumn(Parse * pParse,	/* The parsing context */
 			       int regOut	/* Store the index column value in this register */
     )
 {
-	i16 iTabCol = pIdx->aiColumn[iIdxCol];
+	i16 iTabCol = pIdx->def->key_def->parts[iIdxCol].fieldno;
 	sqlite3ExprCodeGetColumnOfTable(pParse->pVdbe, pIdx->pTable->def,
 					iTabCur, iTabCol, regOut);
 }
diff --git a/src/box/sql/fkey.c b/src/box/sql/fkey.c
index face9cba2..6a918902a 100644
--- a/src/box/sql/fkey.c
+++ b/src/box/sql/fkey.c
@@ -213,7 +213,6 @@ sqlite3FkLocateIndex(Parse * pParse,	/* Parse context to store any error in */
 		     int **paiCol	/* OUT: Map of index columns in pFKey */
     )
 {
-	Index *pIdx = 0;	/* Value to return via *ppIdx */
 	int *aiCol = 0;		/* Value to return via *paiCol */
 	int nCol = pFKey->nCol;	/* Number of columns in parent key */
 	char *zKey = pFKey->aCol[0].zCol;	/* Name of left-most parent key column */
@@ -255,83 +254,86 @@ sqlite3FkLocateIndex(Parse * pParse,	/* Parse context to store any error in */
 		*paiCol = aiCol;
 	}
 
-	for (pIdx = pParent->pIndex; pIdx; pIdx = pIdx->pNext) {
-		int nIdxCol = index_column_count(pIdx);
-		if (nIdxCol == nCol && index_is_unique(pIdx)
-		    && pIdx->pPartIdxWhere == 0) {
-			/* pIdx is a UNIQUE index (or a PRIMARY KEY) and has the right number
-			 * of columns. If each indexed column corresponds to a foreign key
-			 * column of pFKey, then this index is a winner.
+	struct Index *index = NULL;
+	for (index = pParent->pIndex; index != NULL; index = index->pNext) {
+		int part_count = index->def->key_def->part_count;
+		if (part_count != nCol || !index->def->opts.is_unique ||
+		    index->pPartIdxWhere != NULL)
+			continue;
+		/*
+		 * Index is a UNIQUE index (or a PRIMARY KEY) and
+		 * has the right number of columns. If each
+		 * indexed column corresponds to a foreign key
+		 * column of pFKey, then this index is a winner.
+		 */
+		if (zKey == NULL) {
+			/*
+			 * If zKey is NULL, then this foreign key
+			 * is implicitly mapped to the PRIMARY KEY
+			 * of table pParent. The PRIMARY KEY index
+			 * may be identified by the test.
 			 */
-
-			if (zKey == 0) {
-				/* If zKey is NULL, then this foreign key is implicitly mapped to
-				 * the PRIMARY KEY of table pParent. The PRIMARY KEY index may be
-				 * identified by the test.
-				 */
-				if (IsPrimaryKeyIndex(pIdx)) {
-					if (aiCol) {
-						int i;
-						for (i = 0; i < nCol; i++)
-							aiCol[i] =
-							    pFKey->aCol[i].
-							    iFrom;
-					}
-					break;
+			if (IsPrimaryKeyIndex(index)) {
+				if (aiCol != NULL) {
+					for (int i = 0; i < nCol; i++)
+						aiCol[i] = pFKey->aCol[i].iFrom;
 				}
-			} else {
-				/* If zKey is non-NULL, then this foreign key was declared to
-				 * map to an explicit list of columns in table pParent. Check if this
-				 * index matches those columns. Also, check that the index uses
-				 * the default collation sequences for each column.
+				break;
+			}
+		} else {
+			/*
+			 * If zKey is non-NULL, then this foreign
+			 * key was declared to map to an explicit
+			 * list of columns in table pParent. Check
+			 * if this index matches those columns.
+			 * Also, check that the index uses the
+			 * default collation sequences for each
+			 * column.
+			 */
+			int i, j;
+			struct key_part *part = index->def->key_def->parts;
+			for (i = 0; i < nCol; i++, part++) {
+				/*
+				 * Index of column in parent
+				 * table.
 				 */
-				int i, j;
-				for (i = 0; i < nCol; i++) {
-					i16 iCol = pIdx->aiColumn[i];	/* Index of column in parent tbl */
-					char *zIdxCol;	/* Name of indexed column */
-
-					if (iCol < 0)
-						break;	/* No foreign keys against expression indexes */
-
-					/* If the index uses a collation sequence that is different from
-					 * the default collation sequence for the column, this index is
-					 * unusable. Bail out early in this case.
-					 */
-					struct coll *def_coll;
-					uint32_t id;
-					def_coll = sql_column_collation(pParent->def,
-									iCol,
-									&id);
-					struct coll *coll =
-						sql_index_collation(pIdx, i,
-								    &id);
-					if (def_coll != coll)
-						break;
-
-					zIdxCol =
-						pParent->def->fields[iCol].name;
-					for (j = 0; j < nCol; j++) {
-						if (strcmp
-						    (pFKey->aCol[j].zCol,
-						     zIdxCol) == 0) {
-							if (aiCol)
-								aiCol[i] =
-								    pFKey->
-								    aCol[j].
-								    iFrom;
-							break;
-						}
-					}
-					if (j == nCol)
-						break;
+				i16 iCol = (int) part->fieldno;
+				/*
+				 * If the index uses a collation
+				 * sequence that is different from
+				 * the default collation sequence
+				 * for the column, this index is
+				 * unusable. Bail out early in
+				 * this case.
+				 */
+				uint32_t id;
+				struct coll *def_coll =
+					sql_column_collation(pParent->def,
+							     iCol, &id);
+				struct coll *coll = part->coll;
+				if (def_coll != coll)
+					break;
+
+				char *zIdxCol = pParent->def->fields[iCol].name;
+				for (j = 0; j < nCol; j++) {
+					if (strcmp(pFKey->aCol[j].zCol,
+						   zIdxCol) != 0)
+						continue;
+					if (aiCol)
+						aiCol[i] = pFKey->aCol[j].iFrom;
+					break;
 				}
-				if (i == nCol)
-					break;	/* pIdx is usable */
+				if (j == nCol)
+					break;
+			}
+			if (i == nCol) {
+				/* Index is usable. */
+				break;
 			}
 		}
 	}
 
-	if (!pIdx) {
+	if (index == NULL) {
 		if (!pParse->disableTriggers) {
 			sqlite3ErrorMsg(pParse,
 					"foreign key mismatch - \"%w\" referencing \"%w\"",
@@ -341,7 +343,7 @@ sqlite3FkLocateIndex(Parse * pParse,	/* Parse context to store any error in */
 		return 1;
 	}
 
-	*ppIdx = pIdx;
+	*ppIdx = index;
 	return 0;
 }
 
@@ -461,14 +463,15 @@ fkLookupParent(Parse * pParse,	/* Parse context */
 			 */
 			if (pTab == pFKey->pFrom && nIncr == 1) {
 				int iJump =
-				    sqlite3VdbeCurrentAddr(v) + nCol + 1;
-				for (i = 0; i < nCol; i++) {
+					sqlite3VdbeCurrentAddr(v) + nCol + 1;
+				struct key_part *part =
+					pIdx->def->key_def->parts;
+				for (i = 0; i < nCol; ++i, ++part) {
 					int iChild = aiCol[i] + 1 + regData;
-					int iParent =
-					    pIdx->aiColumn[i] + 1 + regData;
-					assert(pIdx->aiColumn[i] >= 0);
+					int iParent = 1 + regData +
+						      (int)part->fieldno;
 					assert(aiCol[i] != pTab->iPKey);
-					if (pIdx->aiColumn[i] == pTab->iPKey) {
+					if ((int)part->fieldno == pTab->iPKey) {
 						/* The parent key is a composite key that includes the IPK column */
 						iParent = regData;
 					}
@@ -615,16 +618,15 @@ fkScanChildren(Parse * pParse,	/* Parse context */
     )
 {
 	sqlite3 *db = pParse->db;	/* Database handle */
-	int i;			/* Iterator variable */
 	Expr *pWhere = 0;	/* WHERE clause to scan with */
 	NameContext sNameContext;	/* Context used to resolve WHERE clause */
 	WhereInfo *pWInfo;	/* Context used by sqlite3WhereXXX() */
 	int iFkIfZero = 0;	/* Address of OP_FkIfZero */
 	Vdbe *v = sqlite3GetVdbe(pParse);
 
-	assert(pIdx == 0 || pIdx->pTable == pTab);
-	assert(pIdx == 0 || (int)index_column_count(pIdx) == pFKey->nCol);
-	assert(pIdx != 0);
+	assert(pIdx == NULL || pIdx->pTable == pTab);
+	assert(pIdx == NULL || (int) pIdx->def->key_def->part_count == pFKey->nCol);
+	assert(pIdx != NULL);
 
 	if (nIncr < 0) {
 		iFkIfZero =
@@ -640,19 +642,20 @@ fkScanChildren(Parse * pParse,	/* Parse context */
 	 * the parent key columns. The affinity of the parent key column should
 	 * be applied to each child key value before the comparison takes place.
 	 */
-	for (i = 0; i < pFKey->nCol; i++) {
+	for (int i = 0; i < pFKey->nCol; i++) {
 		Expr *pLeft;	/* Value from parent table row */
 		Expr *pRight;	/* Column ref to child table */
 		Expr *pEq;	/* Expression (pLeft = pRight) */
 		i16 iCol;	/* Index of column in child table */
-		const char *zCol;	/* Name of column in child table */
+		const char *column_name;
 
-		iCol = pIdx ? pIdx->aiColumn[i] : -1;
+		iCol = pIdx != NULL ?
+		       (int) pIdx->def->key_def->parts[i].fieldno : -1;
 		pLeft = exprTableRegister(pParse, pTab, regData, iCol);
 		iCol = aiCol ? aiCol[i] : pFKey->aCol[0].iFrom;
 		assert(iCol >= 0);
-		zCol = pFKey->pFrom->def->fields[iCol].name;
-		pRight = sqlite3Expr(db, TK_ID, zCol);
+		column_name = pFKey->pFrom->def->fields[iCol].name;
+		pRight = sqlite3Expr(db, TK_ID, column_name);
 		pEq = sqlite3PExpr(pParse, TK_EQ, pLeft, pRight);
 		pWhere = sqlite3ExprAnd(db, pWhere, pEq);
 	}
@@ -671,15 +674,14 @@ fkScanChildren(Parse * pParse,	/* Parse context */
 
 		Expr *pEq, *pAll = 0;
 		Index *pPk = sqlite3PrimaryKeyIndex(pTab);
-		assert(pIdx != 0);
-		int col_count = index_column_count(pPk);
-		for (i = 0; i < col_count; i++) {
-			i16 iCol = pIdx->aiColumn[i];
-			assert(iCol >= 0);
-			pLeft = exprTableRegister(pParse, pTab, regData, iCol);
-			pRight =
-				exprTableColumn(db, pTab->def,
-						pSrc->a[0].iCursor, iCol);
+		assert(pIdx != NULL);
+		uint32_t part_count = pPk->def->key_def->part_count;
+		for (uint32_t i = 0; i < part_count; i++) {
+			uint32_t fieldno = pIdx->def->key_def->parts[i].fieldno;
+			pLeft = exprTableRegister(pParse, pTab, regData,
+						  fieldno);
+			pRight = exprTableColumn(db, pTab->def,
+						 pSrc->a[0].iCursor, fieldno);
 			pEq = sqlite3PExpr(pParse, TK_EQ, pLeft, pRight);
 			pAll = sqlite3ExprAnd(db, pAll, pEq);
 		}
@@ -984,7 +986,6 @@ sqlite3FkCheck(Parse * pParse,	/* Parse context */
 			if (aiCol[i] == pTab->iPKey) {
 				aiCol[i] = -1;
 			}
-			assert(pIdx == 0 || pIdx->aiColumn[i] >= 0);
 		}
 
 		pParse->nTab++;
@@ -1109,19 +1110,19 @@ sqlite3FkOldmask(Parse * pParse,	/* Parse context */
 
 	if (user_session->sql_flags & SQLITE_ForeignKeys) {
 		FKey *p;
-		int i;
 		for (p = pTab->pFKey; p; p = p->pNextFrom) {
-			for (i = 0; i < p->nCol; i++)
+			for (int i = 0; i < p->nCol; i++)
 				mask |= COLUMN_MASK(p->aCol[i].iFrom);
 		}
 		for (p = sqlite3FkReferences(pTab); p; p = p->pNextTo) {
 			Index *pIdx = 0;
 			sqlite3FkLocateIndex(pParse, pTab, p, &pIdx, 0);
-			if (pIdx) {
-				int nIdxCol = index_column_count(pIdx);
-				for (i = 0; i < nIdxCol; i++) {
-					assert(pIdx->aiColumn[i] >= 0);
-					mask |= COLUMN_MASK(pIdx->aiColumn[i]);
+			if (pIdx != NULL) {
+				uint32_t part_count =
+					pIdx->def->key_def->part_count;
+				for (uint32_t i = 0; i < part_count; i++) {
+					mask |= COLUMN_MASK(pIdx->def->
+						key_def->parts[i].fieldno);
 				}
 			}
 		}
@@ -1265,11 +1266,12 @@ fkActionTrigger(struct Parse *pParse, struct Table *pTab, struct FKey *pFKey,
 			       || (pTab->iPKey >= 0
 				   && pTab->iPKey <
 				      (int)pTab->def->field_count));
-			assert(pIdx == 0 || pIdx->aiColumn[i] >= 0);
+
+			uint32_t fieldno = pIdx != NULL ?
+					   pIdx->def->key_def->parts[i].fieldno :
+					   (uint32_t)pTab->iPKey;
 			sqlite3TokenInit(&tToCol,
-					 pTab->def->fields[pIdx ? pIdx->
-						    aiColumn[i] : pTab->iPKey].
-					 name);
+					 pTab->def->fields[fieldno].name);
 			sqlite3TokenInit(&tFromCol,
 					 pFKey->pFrom->def->fields[
 						iFromCol].name);
diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c
index 2c9188e5d..75bd6bd95 100644
--- a/src/box/sql/insert.c
+++ b/src/box/sql/insert.c
@@ -89,14 +89,14 @@ sqlite3IndexAffinityStr(sqlite3 *db, Index *index)
 	 * sqliteDeleteIndex() when the Index structure itself is
 	 * cleaned up.
 	 */
-	int column_count = index_column_count(index);
+	int column_count = index->def->key_def->part_count;
 	index->zColAff = (char *) sqlite3DbMallocRaw(0, column_count + 1);
 	if (index->zColAff == NULL) {
 		sqlite3OomFault(db);
 		return NULL;
 	}
 	for (int n = 0; n < column_count; n++) {
-		uint16_t x = index->aiColumn[n];
+		uint16_t x = index->def->key_def->parts[n].fieldno;
 		index->zColAff[n] = index->pTable->def->fields[x].affinity;
 	}
 	index->zColAff[column_count] = 0;
@@ -646,7 +646,7 @@ sqlite3Insert(Parse * pParse,	/* Parser context */
 		     pIdx = pIdx->pNext, i++) {
 			assert(pIdx);
 			aRegIdx[i] = ++pParse->nMem;
-			pParse->nMem += index_column_count(pIdx);
+			pParse->nMem += pIdx->def->key_def->part_count;
 		}
 	}
 
@@ -1064,12 +1064,8 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 	Index *pIdx;		/* Pointer to one of the indices */
 	Index *pPk = 0;		/* The PRIMARY KEY index */
 	sqlite3 *db;		/* Database connection */
-	int i;			/* loop counter */
-	int ix;			/* Index loop counter */
-	int nCol;		/* Number of columns */
 	int addr1;		/* Address of jump instruction */
 	int seenReplace = 0;	/* True if REPLACE is used to resolve INT PK conflict */
-	int nPkField;		/* Number of fields in PRIMARY KEY. */
 	u8 isUpdate;		/* True if this is an UPDATE operation */
 	u8 bAffinityDone = 0;	/* True if the OP_Affinity operation has been run */
 	struct session *user_session = current_session();
@@ -1081,10 +1077,8 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 	struct space_def *def = pTab->def;
 	/* This table is not a VIEW */
 	assert(!def->opts.is_view);
-	nCol = def->field_count;
 
 	pPk = sqlite3PrimaryKeyIndex(pTab);
-	nPkField = index_column_count(pPk);
 
 	/* Record that this module has started */
 	VdbeModuleComment((v, "BEGIN: GenCnstCks(%d,%d,%d,%d,%d)",
@@ -1094,17 +1088,16 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 	enum on_conflict_action on_error;
 	/* Test all NOT NULL constraints.
 	 */
-	for (i = 0; i < nCol; i++) {
-		if (i == pTab->iPKey) {
+	for (uint32_t i = 0; i < def->field_count; i++) {
+		if ((int) i == pTab->iPKey)
 			continue;
-		}
 		if (aiChng && aiChng[i] < 0) {
 			/* Don't bother checking for NOT NULL on columns that do not change */
 			continue;
 		}
 		if (def->fields[i].is_nullable ||
 		    (pTab->tabFlags & TF_Autoincrement &&
-		     pTab->iAutoIncPKey == i))
+		     pTab->iAutoIncPKey == (int) i))
 			continue;	/* This column is allowed to be NULL */
 
 		on_error = table_column_nullable_action(pTab, i);
@@ -1174,7 +1167,7 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 		else
 			on_error = ON_CONFLICT_ACTION_ABORT;
 
-		for (i = 0; i < checks->nExpr; i++) {
+		for (int i = 0; i < checks->nExpr; i++) {
 			int allOk;
 			Expr *pExpr = checks->a[i].pExpr;
 			if (aiChng
@@ -1201,13 +1194,16 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 		}
 	}
 
-	/* Test all UNIQUE constraints by creating entries for each UNIQUE
-	 * index and making sure that duplicate entries do not already exist.
-	 * Compute the revised record entries for indices as we go.
+	/*
+	 * Test all UNIQUE constraints by creating entries for
+	 * each UNIQUE index and making sure that duplicate entries
+	 * do not already exist. Compute the revised record entries
+	 * for indices as we go.
 	 *
 	 * This loop also handles the case of the PRIMARY KEY index.
 	 */
-	for (ix = 0, pIdx = pTab->pIndex; pIdx; pIdx = pIdx->pNext, ix++) {
+	pIdx = pTab->pIndex;
+	for (int ix = 0; pIdx != NULL; pIdx = pIdx->pNext, ix++) {
 		int regIdx;	/* Range of registers hold conent for pIdx */
 		int regR;	/* Range of registers holding conflicting PK */
 		int iThisCur;	/* Cursor for this UNIQUE index */
@@ -1248,10 +1244,11 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 		 * the insert or update.  Store that record in the aRegIdx[ix] register
 		 */
 		regIdx = aRegIdx[ix] + 1;
-		int nIdxCol = (int) index_column_count(pIdx);
+		uint32_t part_count = pIdx->def->key_def->part_count;
 		if (uniqueByteCodeNeeded) {
-			for (i = 0; i < nIdxCol; ++i) {
-				int fieldno = pIdx->aiColumn[i];
+			for (uint32_t i = 0; i < part_count; ++i) {
+				uint32_t fieldno =
+					pIdx->def->key_def->parts[i].fieldno;
 				int reg;
 				/*
 				 * OP_SCopy copies value in
@@ -1262,11 +1259,10 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 				 * needed for proper UNIQUE
 				 * constraint handling.
 				 */
-				if (fieldno == pTab->iPKey)
+				if ((int) fieldno == pTab->iPKey)
 					reg = regNewData;
 				else
 					reg = fieldno + regNewData + 1;
-				assert(fieldno >= 0);
 				sqlite3VdbeAddOp2(v, OP_SCopy, reg, regIdx + i);
 				VdbeComment((v, "%s",
 					    def->fields[fieldno].name));
@@ -1278,9 +1274,12 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 		if (IsPrimaryKeyIndex(pIdx)) {
 			/* If PK is marked as INTEGER, use it as strict type,
 			 * not as affinity. Emit code for type checking */
-			if (nIdxCol == 1) {
-				reg_pk = regNewData + 1 + pIdx->aiColumn[0];
-				if (pTab->zColAff[pIdx->aiColumn[0]] ==
+			if (part_count == 1) {
+				uint32_t fieldno =
+					pIdx->def->key_def->parts[0].fieldno;
+				reg_pk = regNewData + 1 + fieldno;
+
+				if (pTab->zColAff[fieldno] ==
 				    AFFINITY_INTEGER) {
 					int skip_if_null = sqlite3VdbeMakeLabel(v);
 					if ((pTab->tabFlags & TF_Autoincrement) != 0) {
@@ -1298,7 +1297,7 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 
 			sqlite3VdbeAddOp3(v, OP_MakeRecord, regNewData + 1,
 					  def->field_count, aRegIdx[ix]);
-			VdbeComment((v, "for %s", pIdx->zName));
+			VdbeComment((v, "for %s", pIdx->def->name));
 		}
 
 		/* In an UPDATE operation, if this index is the PRIMARY KEY
@@ -1385,24 +1384,22 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 		if (uniqueByteCodeNeeded) {
 			sqlite3VdbeAddOp4Int(v, OP_NoConflict, iThisCur,
 					     addrUniqueOk, regIdx,
-					     index_column_count(pIdx));
+					     pIdx->def->key_def->part_count);
 		}
 		VdbeCoverage(v);
 
+		uint32_t pk_part_count = pPk->def->key_def->part_count;
 		/* Generate code to handle collisions */
-		regR =
-		    (pIdx == pPk) ? regIdx : sqlite3GetTempRange(pParse,
-								 nPkField);
+		regR = pIdx == pPk ? regIdx :
+		       sqlite3GetTempRange(pParse, pk_part_count);
 		if (isUpdate || on_error == ON_CONFLICT_ACTION_REPLACE) {
 			int x;
-			int nPkCol = index_column_count(pPk);
 			/* Extract the PRIMARY KEY from the end of the index entry and
 			 * store it in registers regR..regR+nPk-1
 			 */
 			if (pIdx != pPk) {
-				for (i = 0; i < nPkCol; i++) {
-					assert(pPk->aiColumn[i] >= 0);
-					x = pPk->aiColumn[i];
+				for (uint32_t i = 0; i < pk_part_count; i++) {
+					x = pPk->def->key_def->parts[i].fieldno;
 					sqlite3VdbeAddOp3(v, OP_Column,
 							  iThisCur, x, regR + i);
 					VdbeComment((v, "%s.%s", def->name,
@@ -1417,22 +1414,25 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 				 * of the matched index row are different from the original PRIMARY
 				 * KEY values of this row before the update.
 				 */
-				int addrJump =
-					sqlite3VdbeCurrentAddr(v) + nPkCol;
+				int addrJump = sqlite3VdbeCurrentAddr(v) +
+					       pk_part_count;
 				int op = OP_Ne;
-				int regCmp = (IsPrimaryKeyIndex(pIdx) ?
-					      regIdx : regR);
-
-				for (i = 0; i < nPkCol; i++) {
-					uint32_t id;
-					char *p4 = (char *)sql_index_collation(pPk, i, &id);
-					x = pPk->aiColumn[i];
-					assert(x >= 0);
-					if (i == (nPkCol - 1)) {
+				int regCmp = IsPrimaryKeyIndex(pIdx) ?
+					     regIdx : regR;
+				struct key_part *part =
+					pPk->def->key_def->parts;
+				for (uint32_t i = 0; i < pk_part_count;
+				     ++i, ++part) {
+					char *p4 = (char *) part->coll;
+					x = part->fieldno;
+					if (pPk->tnum==0)
+						x = -1;
+					if (i == (pk_part_count - 1)) {
 						addrJump = addrUniqueOk;
 						op = OP_Eq;
 					}
-					sqlite3VdbeAddOp4(v, op, regOldData + 1 + x,
+					sqlite3VdbeAddOp4(v, op,
+							  regOldData + 1 + x,
 							  addrJump, regCmp + i,
 							  p4, P4_COLLSEQ);
 					sqlite3VdbeChangeP5(v, SQLITE_NOTNULL);
@@ -1475,7 +1475,8 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 							      NULL, NULL);
 			}
 			sql_generate_row_delete(pParse, pTab, trigger,
-						iDataCur, regR, nPkField, false,
+						iDataCur, regR, pk_part_count,
+						false,
 						ON_CONFLICT_ACTION_REPLACE,
 						pIdx == pPk ? ONEPASS_SINGLE :
 						ONEPASS_OFF, -1);
@@ -1485,7 +1486,7 @@ sqlite3GenerateConstraintChecks(Parse * pParse,		/* The parser context */
 		}
 		sqlite3VdbeResolveLabel(v, addrUniqueOk);
 		if (regR != regIdx)
-			sqlite3ReleaseTempRange(pParse, regR, nPkField);
+			sqlite3ReleaseTempRange(pParse, regR, pk_part_count);
 	}
 
 	*pbMayReplace = seenReplace;
@@ -1599,8 +1600,8 @@ sqlite3OpenTableAndIndices(Parse * pParse,	/* Parsing context */
 		    IsPrimaryKeyIndex(pIdx) ||		/* Condition 2 */
 		    sqlite3FkReferences(pTab) ||	/* Condition 3 */
 		    /* Condition 4 */
-		    (index_is_unique(pIdx) && pIdx->onError !=
-		     ON_CONFLICT_ACTION_DEFAULT &&
+		    (pIdx->def->opts.is_unique &&
+		     pIdx->onError != ON_CONFLICT_ACTION_DEFAULT &&
 		     /* Condition 4.1 */
 		     pIdx->onError != ON_CONFLICT_ACTION_ABORT) ||
 		     /* Condition 4.2 */
@@ -1619,7 +1620,7 @@ sqlite3OpenTableAndIndices(Parse * pParse,	/* Parsing context */
 				sqlite3VdbeAddOp4(v, op, iIdxCur, idx_id, 0,
 						  (void *) space, P4_SPACEPTR);
 				sqlite3VdbeChangeP5(v, p5);
-				VdbeComment((v, "%s", pIdx->zName));
+				VdbeComment((v, "%s", pIdx->def->name));
 			}
 		}
 	}
@@ -1653,30 +1654,25 @@ int sqlite3_xferopt_count;
 static int
 xferCompatibleIndex(Index * pDest, Index * pSrc)
 {
-	uint32_t i;
 	assert(pDest && pSrc);
 	assert(pDest->pTable != pSrc->pTable);
-	uint32_t nDestCol = index_column_count(pDest);
-	uint32_t nSrcCol = index_column_count(pSrc);
-	if (nDestCol != nSrcCol) {
-		return 0;	/* Different number of columns */
-	}
+	uint32_t dest_idx_part_count = pDest->def->key_def->part_count;
+	uint32_t src_idx_part_count = pSrc->def->key_def->part_count;
+	if (dest_idx_part_count != src_idx_part_count)
+		return 0;
 	if (pDest->onError != pSrc->onError) {
 		return 0;	/* Different conflict resolution strategies */
 	}
-	for (i = 0; i < nSrcCol; i++) {
-		if (pSrc->aiColumn[i] != pDest->aiColumn[i]) {
+	struct key_part *src_part = pSrc->def->key_def->parts;
+	struct key_part *dest_part = pDest->def->key_def->parts;
+	for (uint32_t i = 0; i < src_idx_part_count;
+	     ++i, ++src_part, ++dest_part) {
+		if (src_part->fieldno != dest_part->fieldno)
 			return 0;	/* Different columns indexed */
-		}
-		if (sql_index_column_sort_order(pSrc, i) !=
-		    sql_index_column_sort_order(pDest, i)) {
+		if (src_part->sort_order != dest_part->sort_order)
 			return 0;	/* Different sort orders */
-		}
-		uint32_t id;
-		if (sql_index_collation(pSrc, i, &id) !=
-		    sql_index_collation(pDest, i, &id)) {
+		if (src_part->coll != dest_part->coll)
 			return 0;	/* Different collating sequences */
-		}
 	}
 	if (sqlite3ExprCompare(pSrc->pPartIdxWhere, pDest->pPartIdxWhere, -1)) {
 		return 0;	/* Different WHERE clauses */
@@ -1848,16 +1844,15 @@ xferOptimization(Parse * pParse,	/* Parser context */
 		}
 	}
 	for (pDestIdx = pDest->pIndex; pDestIdx; pDestIdx = pDestIdx->pNext) {
-		if (index_is_unique(pDestIdx)) {
+		if (pDestIdx->def->opts.is_unique)
 			destHasUniqueIdx = 1;
-		}
 		for (pSrcIdx = pSrc->pIndex; pSrcIdx; pSrcIdx = pSrcIdx->pNext) {
 			if (xferCompatibleIndex(pDestIdx, pSrcIdx))
 				break;
 		}
-		if (pSrcIdx == 0) {
-			return 0;	/* pDestIdx has no corresponding index in pSrc */
-		}
+		/* pDestIdx has no corresponding index in pSrc. */
+		if (pSrcIdx == NULL)
+			return 0;
 	}
 	/* Get server checks. */
 	ExprList *pCheck_src = space_checks_expr_list(
@@ -1935,18 +1930,18 @@ xferOptimization(Parse * pParse,	/* Parser context */
 		vdbe_emit_open_cursor(pParse, iSrc,
 				      SQLITE_PAGENO_TO_INDEXID(pSrcIdx->tnum),
 				      src_space);
-		VdbeComment((v, "%s", pSrcIdx->zName));
+		VdbeComment((v, "%s", pSrcIdx->def->name));
 		struct space *dest_space =
 			space_by_id(SQLITE_PAGENO_TO_SPACEID(pDestIdx->tnum));
 		vdbe_emit_open_cursor(pParse, iDest,
 				      SQLITE_PAGENO_TO_INDEXID(pDestIdx->tnum),
 				      dest_space);
-		VdbeComment((v, "%s", pDestIdx->zName));
+		VdbeComment((v, "%s", pDestIdx->def->name));
 		addr1 = sqlite3VdbeAddOp2(v, OP_Rewind, iSrc, 0);
 		VdbeCoverage(v);
 		sqlite3VdbeAddOp2(v, OP_RowData, iSrc, regData);
 		sqlite3VdbeAddOp2(v, OP_IdxInsert, iDest, regData);
-		if (pDestIdx->idxType == SQLITE_IDXTYPE_PRIMARYKEY)
+		if (pDestIdx->index_type == SQL_INDEX_TYPE_CONSTRAINT_PK)
 			sqlite3VdbeChangeP5(v, OPFLAG_NCHANGE);
 		sqlite3VdbeAddOp2(v, OP_Next, iSrc, addr1 + 1);
 		VdbeCoverage(v);
diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y
index ac935fd48..e8a36e4d2 100644
--- a/src/box/sql/parse.y
+++ b/src/box/sql/parse.y
@@ -282,7 +282,7 @@ ccons ::= PRIMARY KEY sortorder(Z) onconf(R) autoinc(I).
                                  {sqlite3AddPrimaryKey(pParse,0,R,I,Z);}
 ccons ::= UNIQUE onconf(R).      {sql_create_index(pParse,0,0,0,R,0,0,
 						   SORT_ORDER_ASC, false,
-						   SQLITE_IDXTYPE_UNIQUE);}
+						   SQL_INDEX_TYPE_CONSTRAINT_UNIQUE);}
 ccons ::= CHECK LP expr(X) RP.   {sql_add_check_constraint(pParse,&X);}
 ccons ::= REFERENCES nm(T) eidlist_opt(TA) refargs(R).
                                  {sqlite3CreateForeignKey(pParse,0,&T,TA,R);}
@@ -333,7 +333,7 @@ tcons ::= PRIMARY KEY LP sortlist(X) autoinc(I) RP onconf(R).
 tcons ::= UNIQUE LP sortlist(X) RP onconf(R).
                                  {sql_create_index(pParse,0,0,X,R,0,0,
 						   SORT_ORDER_ASC,false,
-						   SQLITE_IDXTYPE_UNIQUE);}
+						   SQL_INDEX_TYPE_CONSTRAINT_UNIQUE);}
 tcons ::= CHECK LP expr(E) RP onconf.
                                  {sql_add_check_constraint(pParse,&E);}
 tcons ::= FOREIGN KEY LP eidlist(FA) RP
@@ -1186,13 +1186,15 @@ paren_exprlist(A) ::= LP exprlist(X) RP.  {A = X;}
 //
 cmd ::= createkw(S) uniqueflag(U) INDEX ifnotexists(NE) nm(X)
         ON nm(Y) LP sortlist(Z) RP. {
-  sql_create_index(pParse, &X, sqlite3SrcListAppend(pParse->db,0,&Y), Z, U, &S,
-                   NULL, SORT_ORDER_ASC, NE, SQLITE_IDXTYPE_APPDEF);
+  enum on_conflict_action on_error =
+          U ? ON_CONFLICT_ACTION_ABORT : ON_CONFLICT_ACTION_NONE;
+  sql_create_index(pParse, &X, sqlite3SrcListAppend(pParse->db,0,&Y), Z,
+                   on_error, &S, NULL, SORT_ORDER_ASC, NE, U);
 }
 
 %type uniqueflag {int}
-uniqueflag(A) ::= UNIQUE.  {A = ON_CONFLICT_ACTION_ABORT;}
-uniqueflag(A) ::= .        {A = ON_CONFLICT_ACTION_NONE;}
+uniqueflag(A) ::= UNIQUE.  {A = SQL_INDEX_TYPE_UNIQUE;}
+uniqueflag(A) ::= .        {A = SQL_INDEX_TYPE_NON_UNIQUE;}
 
 
 // The eidlist non-terminal (Expression Id List) generates an ExprList
diff --git a/src/box/sql/pragma.c b/src/box/sql/pragma.c
index 8ff5bbc13..7b1f6ec40 100644
--- a/src/box/sql/pragma.c
+++ b/src/box/sql/pragma.c
@@ -411,9 +411,8 @@ sqlite3Pragma(Parse * pParse, Token * pId,	/* First part of [schema.]id field */
 			} else if (pk == NULL) {
 				k = 1;
 			} else {
-				for (k = 1; k <= def->field_count &&
-				     pk->aiColumn[k - 1] != (int) i; ++k) {
-				}
+				struct key_def *kdef = pk->def->key_def;
+				k = key_def_find(kdef, i) - kdef->parts + 1;
 			}
 			bool is_nullable = def->fields[i].is_nullable;
 			char *expr_str = def->fields[i].default_value;
@@ -456,7 +455,7 @@ sqlite3Pragma(Parse * pParse, Token * pId,	/* First part of [schema.]id field */
 					size_t avg_tuple_size_idx =
 						sql_index_tuple_size(space, idx);
 					sqlite3VdbeMultiLoad(v, 2, "sii",
-							     pIdx->zName,
+							     pIdx->def->name,
 							     avg_tuple_size_idx,
 							     index_field_tuple_est(pIdx, 0));
 					sqlite3VdbeAddOp2(v, OP_ResultRow, 1,
@@ -485,11 +484,13 @@ sqlite3Pragma(Parse * pParse, Token * pId,	/* First part of [schema.]id field */
 						 */
 						pParse->nMem = 3;
 					}
-					mx = index_column_count(pIdx);
+					mx = pIdx->def->key_def->part_count;
 					assert(pParse->nMem <=
 					       pPragma->nPragCName);
-					for (i = 0; i < mx; i++) {
-						i16 cnum = pIdx->aiColumn[i];
+					struct key_part *part =
+						pIdx->def->key_def->parts;
+					for (i = 0; i < mx; i++, part++) {
+						i16 cnum = (int) part->fieldno;
 						assert(pIdx->pTable);
 						sqlite3VdbeMultiLoad(v, 1,
 								     "iis", i,
@@ -503,19 +504,18 @@ sqlite3Pragma(Parse * pParse, Token * pId,	/* First part of [schema.]id field */
 								     name);
 						if (pPragma->iArg) {
 							const char *c_n;
-							uint32_t id;
+							uint32_t id =
+								part->coll_id;
 							struct coll *coll =
-								sql_index_collation(pIdx, i, &id);
+								part->coll;
 							if (coll != NULL)
 								c_n = coll_by_id(id)->name;
 							else
 								c_n = "BINARY";
-							enum sort_order sort_order;
-							sort_order = sql_index_column_sort_order(pIdx,
-												 i);
 							sqlite3VdbeMultiLoad(v,
 									     4,
 									     "isi",
+									     part->
 									     sort_order,
 									     c_n,
 									     i <
@@ -542,16 +542,14 @@ sqlite3Pragma(Parse * pParse, Token * pId,	/* First part of [schema.]id field */
 					for (pIdx = pTab->pIndex, i = 0; pIdx;
 					     pIdx = pIdx->pNext, i++) {
 						const char *azOrigin[] =
-						    { "c", "u", "pk" };
+						    { "c", "u", "u", "pk" };
 						sqlite3VdbeMultiLoad(v, 1,
 								     "isisi", i,
-								     pIdx->
-								     zName,
-								     index_is_unique
-								     (pIdx),
+								     pIdx->def->name,
+								     pIdx->def->opts.is_unique,
 								     azOrigin
 								     [pIdx->
-								      idxType],
+								      index_type],
 								     pIdx->
 								     pPartIdxWhere
 								     != 0);
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index ceb7e3403..34d5329e4 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -4399,7 +4399,7 @@ sqlite3IndexedByLookup(Parse * pParse, struct SrcList_item *pFrom)
 		char *zIndexedBy = pFrom->u1.zIndexedBy;
 		Index *pIdx;
 		for (pIdx = pTab->pIndex;
-		     pIdx && strcmp(pIdx->zName, zIndexedBy);
+		     pIdx && strcmp(pIdx->def->name, zIndexedBy);
 		     pIdx = pIdx->pNext) ;
 		if (!pIdx) {
 			sqlite3ErrorMsg(pParse, "no such index: %s", zIndexedBy,
diff --git a/src/box/sql/sqliteInt.h b/src/box/sql/sqliteInt.h
index 18bf9494e..54661cb8d 100644
--- a/src/box/sql/sqliteInt.h
+++ b/src/box/sql/sqliteInt.h
@@ -2065,32 +2065,31 @@ struct UnpackedRecord {
 				 */
 };
 
+/*
+ * Possible SQL index types. Note that PK and UNIQUE constraints
+ * are implemented as indexes and have their own types:
+ * SQL_INDEX_TYPE_CONSTRAINT_PK and
+ * SQL_INDEX_TYPE_CONSTRAINT_UNIQUE.
+ */
+enum sql_index_type {
+    SQL_INDEX_TYPE_NON_UNIQUE = 0,
+    SQL_INDEX_TYPE_UNIQUE,
+    SQL_INDEX_TYPE_CONSTRAINT_UNIQUE,
+    SQL_INDEX_TYPE_CONSTRAINT_PK,
+};
+
+/* Return true if index X is a PRIMARY KEY index */
+#define IsPrimaryKeyIndex(X)  ((X)->index_type==SQL_INDEX_TYPE_CONSTRAINT_PK)
+
+/* Return true if index X is a UNIQUE index */
+#define IsUniqueIndex(X)  (((X)->index_type == SQL_INDEX_TYPE_CONSTRAINT_UNIQUE) || \
+			   ((X)->index_type == SQL_INDEX_TYPE_CONSTRAINT_PK) || \
+			   ((X)->index_type == SQL_INDEX_TYPE_UNIQUE))
+
 /*
  * Each SQL index is represented in memory by an
  * instance of the following structure.
  *
- * The columns of the table that are to be indexed are described
- * by the aiColumn[] field of this structure.  For example, suppose
- * we have the following table and index:
- *
- *     CREATE TABLE Ex1(c1 int, c2 int, c3 text);
- *     CREATE INDEX Ex2 ON Ex1(c3,c1);
- *
- * In the Table structure describing Ex1, nCol==3 because there are
- * three columns in the table.  In the Index structure describing
- * Ex2, nColumn==2 since 2 of the 3 columns of Ex1 are indexed.
- * The value of aiColumn is {2, 0}.  aiColumn[0]==2 because the
- * first column to be indexed (c3) has an index of 2 in Ex1.aCol[].
- * The second column to be indexed (c1) has an index of 0 in
- * Ex1.aCol[], hence Ex2.aiColumn[1]==0.
- *
- * The Index.onError field determines whether or not the indexed columns
- * must be unique and what to do if they are not.  When Index.onError=
- * ON_CONFLICT_ACTION_NONE, it means this is not a unique index.
- * Otherwise it is a unique index and the value of Index.onError indicate
- * the which conflict resolution algorithm to employ whenever an attempt
- * is made to insert a non-unique element.
- *
  * While parsing a CREATE TABLE or CREATE INDEX statement in order to
  * generate VDBE code (as opposed to reading from Tarantool's _space
  * space as part of parsing an existing database schema), transient instances
@@ -2100,26 +2099,32 @@ struct UnpackedRecord {
  * program is executed). See convertToWithoutRowidTable() for details.
  */
 struct Index {
-	char *zName;		/* Name of this index */
-	i16 *aiColumn;		/* Which columns are used by this index.  1st is 0 */
-	LogEst *aiRowLogEst;	/* From ANALYZE: Est. rows selected by each column */
-	Table *pTable;		/* The SQL table being indexed */
-	char *zColAff;		/* String defining the affinity of each column */
-	Index *pNext;		/* The next index associated with the same table */
-	Schema *pSchema;	/* Schema containing this index */
-	/** Sorting order for each column. */
-	enum sort_order *sort_order;
-	/** Array of collation sequences for index. */
-	struct coll **coll_array;
-	/** Array of collation identifiers. */
-	uint32_t *coll_id_array;
-	Expr *pPartIdxWhere;	/* WHERE clause for partial indices */
-	int tnum;		/* DB Page containing root of this index */
-	u16 nColumn;		/* Number of columns stored in the index */
-	u8 onError;		/* ON_CONFLICT_ACTION_ABORT, _IGNORE, _REPLACE,
-				 * or _NONE
-				 */
-	unsigned idxType:2;	/* 1==UNIQUE, 2==PRIMARY KEY, 0==CREATE INDEX */
+	/** The SQL table being indexed. */
+	Table *pTable;
+	/** String defining the affinity of each column. */
+	char *zColAff;
+	/** The next index associated with the same table. */
+	Index *pNext;
+	/** Schema containing this index. */
+	Schema *pSchema;
+	/** WHERE clause for partial indices. */
+	Expr *pPartIdxWhere;
+	/** DB Page containing root of this index. */
+	int tnum;
+	/**
+	 * Conflict resolution algorithm to employ whenever an
+	 * attempt is made to insert a non-unique element in
+	 * unique index.
+	 */
+	u8 onError;
+	/**
+	 * Index type: non-unique index, unique index, index
+	 * implementing UNIQUE constraint or index implementing
+	 * PK constraint.
+	 */
+	enum sql_index_type index_type;
+	/** Index definition. */
+	struct index_def *def;
 };
 
 /**
@@ -2144,20 +2149,6 @@ struct Index {
 log_est_t
 index_field_tuple_est(struct Index *idx, uint32_t field);
 
-/*
- * Allowed values for Index.idxType
- */
-#define SQLITE_IDXTYPE_APPDEF      0	/* Created using CREATE INDEX */
-#define SQLITE_IDXTYPE_UNIQUE      1	/* Implements a UNIQUE constraint */
-#define SQLITE_IDXTYPE_PRIMARYKEY  2	/* Is the PRIMARY KEY for the table */
-
-/* Return true if index X is a PRIMARY KEY index */
-#define IsPrimaryKeyIndex(X)  ((X)->idxType==SQLITE_IDXTYPE_PRIMARYKEY)
-
-/* Return true if index X is a UNIQUE index */
-#define IsUniqueIndex(X)      (((X)->idxType == SQLITE_IDXTYPE_UNIQUE) || \
-				((X)->idxType == SQLITE_IDXTYPE_PRIMARYKEY))
-
 #ifdef DEFAULT_TUPLE_COUNT
 #undef DEFAULT_TUPLE_COUNT
 #endif
@@ -3540,34 +3531,6 @@ void sqlite3AddCollateType(Parse *, Token *);
  */
 struct coll *
 sql_column_collation(struct space_def *def, uint32_t column, uint32_t *coll_id);
-/**
- * Return name of given column collation from index.
- *
- * @param idx Index which is used to fetch column.
- * @param column Number of column.
- * @param[out] coll_id Collation identifier.
- * @retval Pointer to collation.
- */
-struct coll *
-sql_index_collation(Index *idx, uint32_t column, uint32_t *id);
-
-/**
- * Return key_def of provided struct Index.
- * @param idx Pointer to `struct Index` object.
- * @retval Pointer to `struct key_def`.
- */
-struct key_def*
-sql_index_key_def(struct Index *idx);
-
-/**
- * Return sort order of given column from index.
- *
- * @param idx Index which is used to fetch column.
- * @param column Number of column.
- * @retval Sort order of requested column.
- */
-enum sort_order
-sql_index_column_sort_order(Index *idx, uint32_t column);
 
 void sqlite3EndTable(Parse *, Token *, Token *, Select *);
 
@@ -3654,9 +3617,6 @@ void sqlite3SrcListShiftJoinType(SrcList *);
 void sqlite3SrcListAssignCursors(Parse *, SrcList *);
 void sqlite3IdListDelete(sqlite3 *, IdList *);
 void sqlite3SrcListDelete(sqlite3 *, SrcList *);
-Index *sqlite3AllocateIndexObject(sqlite3 *, i16, int, char **);
-bool
-index_is_unique(Index *);
 
 /**
  * Create a new index for an SQL table.  name is the name of the
@@ -3678,7 +3638,7 @@ index_is_unique(Index *);
  * @param on_error One of ON_CONFLICT_ACTION_ABORT, _IGNORE,
  *        _REPLACE, or _NONE.
  * @param start The CREATE token that begins this statement.
- * @param pi_where WHERE clause for partial indices.
+ * @param where WHERE clause for partial indices.
  * @param sort_order Sort order of primary key when pList==NULL.
  * @param if_not_exist Omit error if index already exists.
  * @param idx_type The index type.
@@ -3686,8 +3646,9 @@ index_is_unique(Index *);
 void
 sql_create_index(struct Parse *parse, struct Token *token,
 		 struct SrcList *tbl_name, struct ExprList *col_list,
-		 int on_error, struct Token *start, struct Expr *pi_where,
-		 enum sort_order sort_order, bool if_not_exist, u8 idx_type);
+		 enum on_conflict_action on_error, struct Token *start,
+		 struct Expr *where, enum sort_order sort_order,
+		 bool if_not_exist, enum sql_index_type idx_type);
 
 /**
  * This routine will drop an existing named index.  This routine
@@ -4541,10 +4502,6 @@ int sqlite3InvokeBusyHandler(BusyHandler *);
 int
 sql_analysis_load(struct sqlite3 *db);
 
-uint32_t
-index_column_count(const Index *);
-bool
-index_is_unique_not_null(const Index *);
 void sqlite3RegisterLikeFunctions(sqlite3 *, int);
 int sqlite3IsLikeFunction(sqlite3 *, Expr *, int *, char *);
 void sqlite3SchemaClear(sqlite3 *);
diff --git a/src/box/sql/update.c b/src/box/sql/update.c
index cbcca3432..167f425f0 100644
--- a/src/box/sql/update.c
+++ b/src/box/sql/update.c
@@ -239,17 +239,18 @@ sqlite3Update(Parse * pParse,		/* The parser context */
 	 */
 	for (j = 0, pIdx = pTab->pIndex; pIdx; pIdx = pIdx->pNext, j++) {
 		int reg;
-		int nIdxCol = index_column_count(pIdx);
+		uint32_t part_count = pIdx->def->key_def->part_count;
 		if (chngPk || hasFK || pIdx->pPartIdxWhere || pIdx == pPk) {
 			reg = ++pParse->nMem;
-			pParse->nMem += nIdxCol;
+			pParse->nMem += part_count;
 		} else {
 			reg = 0;
-			for (i = 0; i < nIdxCol; i++) {
-				i16 iIdxCol = pIdx->aiColumn[i];
-				if (iIdxCol < 0 || aXRef[iIdxCol] >= 0) {
+			for (uint32_t i = 0; i < part_count; i++) {
+				uint32_t fieldno =
+					pIdx->def->key_def->parts[i].fieldno;
+				if (aXRef[fieldno] >= 0) {
 					reg = ++pParse->nMem;
-					pParse->nMem += nIdxCol;
+					pParse->nMem += part_count;
 					break;
 				}
 			}
@@ -298,17 +299,18 @@ sqlite3Update(Parse * pParse,		/* The parser context */
 	 * In this case we have to manually load columns in order to make tuple.
 	 */
 	int iPk;	/* First of nPk memory cells holding PRIMARY KEY value */
-	i16 nPk;	/* Number of components of the PRIMARY KEY */
+	/* Number of components of the PRIMARY KEY.  */
+	uint32_t pk_part_count;
 	int addrOpen;	/* Address of the OpenEphemeral instruction */
 
 	if (is_view) {
-		nPk = nKey;
+		pk_part_count = nKey;
 	} else {
 		assert(pPk != 0);
-		nPk = index_column_count(pPk);
+		pk_part_count = pPk->def->key_def->part_count;
 	}
 	iPk = pParse->nMem + 1;
-	pParse->nMem += nPk;
+	pParse->nMem += pk_part_count;
 	regKey = ++pParse->nMem;
 	iEph = pParse->nTab++;
 	sqlite3VdbeAddOp2(v, OP_Null, 0, iPk);
@@ -317,7 +319,8 @@ sqlite3Update(Parse * pParse,		/* The parser context */
 		addrOpen = sqlite3VdbeAddOp2(v, OP_OpenTEphemeral, iEph,
 					     nKey);
 	} else {
-		addrOpen = sqlite3VdbeAddOp2(v, OP_OpenTEphemeral, iEph, nPk);
+		addrOpen = sqlite3VdbeAddOp2(v, OP_OpenTEphemeral, iEph,
+					     pk_part_count);
 		sql_vdbe_set_p4_key_def(pParse, pPk);
 	}
 
@@ -327,27 +330,27 @@ sqlite3Update(Parse * pParse,		/* The parser context */
 		goto update_cleanup;
 	okOnePass = sqlite3WhereOkOnePass(pWInfo, aiCurOnePass);
 	if (is_view) {
-		for (i = 0; i < nPk; i++) {
+		for (i = 0; i < (int) pk_part_count; i++) {
 			sqlite3VdbeAddOp3(v, OP_Column, iDataCur, i, iPk + i);
 		}
 	} else {
-		for (i = 0; i < nPk; i++) {
-			assert(pPk->aiColumn[i] >= 0);
+		for (i = 0; i < (int) pk_part_count; i++) {
 			sqlite3ExprCodeGetColumnOfTable(v, def, iDataCur,
-							pPk->aiColumn[i],
+							pPk->def->key_def->
+								parts[i].fieldno,
 							iPk + i);
 		}
 	}
 
 	if (okOnePass) {
 		sqlite3VdbeChangeToNoop(v, addrOpen);
-		nKey = nPk;
+		nKey = pk_part_count;
 		regKey = iPk;
 	} else {
 		const char *zAff = is_view ? 0 :
 				   sqlite3IndexAffinityStr(pParse->db, pPk);
-		sqlite3VdbeAddOp4(v, OP_MakeRecord, iPk, nPk, regKey,
-					  zAff, nPk);
+		sqlite3VdbeAddOp4(v, OP_MakeRecord, iPk, pk_part_count,
+				  regKey, zAff, pk_part_count);
 		sqlite3VdbeAddOp2(v, OP_IdxInsert, iEph, regKey);
 		/* Set flag to save memory allocating one by malloc. */
 		sqlite3VdbeChangeP5(v, 1);
diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
index cec0ed647..c96157a55 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -1150,7 +1150,7 @@ sql_vdbe_set_p4_key_def(struct Parse *parse, struct Index *idx)
 	struct Vdbe *v = parse->pVdbe;
 	assert(v != NULL);
 	assert(idx != NULL);
-	struct key_def *def = key_def_dup(sql_index_key_def(idx));
+	struct key_def *def = key_def_dup(idx->def->key_def);
 	if (def == NULL)
 		sqlite3OomFault(parse->db);
 	else
diff --git a/src/box/sql/vdbemem.c b/src/box/sql/vdbemem.c
index 2ce90747d..d0e16bafb 100644
--- a/src/box/sql/vdbemem.c
+++ b/src/box/sql/vdbemem.c
@@ -1087,15 +1087,15 @@ valueNew(sqlite3 * db, struct ValueNewStat4Ctx *p)
 			Index *pIdx = p->pIdx;	/* Index being probed */
 			int nByte;	/* Bytes of space to allocate */
 			int i;	/* Counter variable */
-			int nCol = index_column_count(pIdx);
+			int part_count = pIdx->def->key_def->part_count;
 
-			nByte = sizeof(Mem) * nCol +
+			nByte = sizeof(Mem) * part_count +
 				ROUND8(sizeof(UnpackedRecord));
 			pRec =
 			    (UnpackedRecord *) sqlite3DbMallocZero(db, nByte);
 			if (pRec == NULL)
 				return NULL;
-			pRec->key_def = key_def_dup(sql_index_key_def(pIdx));
+			pRec->key_def = key_def_dup(pIdx->def->key_def);
 			if (pRec->key_def == NULL) {
 				sqlite3DbFree(db, pRec);
 				sqlite3OomFault(db);
@@ -1103,7 +1103,7 @@ valueNew(sqlite3 * db, struct ValueNewStat4Ctx *p)
 			}
 			pRec->aMem = (Mem *)((char *) pRec +
 					     ROUND8(sizeof(UnpackedRecord)));
-			for (i = 0; i < nCol; i++) {
+			for (i = 0; i < (int) part_count; i++) {
 				pRec->aMem[i].flags = MEM_Null;
 				pRec->aMem[i].db = db;
 			}
@@ -1621,15 +1621,12 @@ sql_stat4_column(struct sqlite3 *db, const char *record, uint32_t col_num,
 void
 sqlite3Stat4ProbeFree(UnpackedRecord * pRec)
 {
-	if (pRec) {
-		int i;
-		int nCol = pRec->key_def->part_count;
-		Mem *aMem = pRec->aMem;
-		sqlite3 *db = aMem[0].db;
-		for (i = 0; i < nCol; i++) {
+	if (pRec != NULL) {
+		int part_count = pRec->key_def->part_count;
+		struct Mem *aMem = pRec->aMem;
+		for (int i = 0; i < part_count; i++)
 			sqlite3VdbeMemRelease(&aMem[i]);
-		}
-		sqlite3DbFree(db, pRec);
+		sqlite3DbFree(aMem[0].db, pRec);
 	}
 }
 
diff --git a/src/box/sql/where.c b/src/box/sql/where.c
index 6c07fec0c..a4ba62912 100644
--- a/src/box/sql/where.c
+++ b/src/box/sql/where.c
@@ -370,16 +370,21 @@ whereScanInit(WhereScan * pScan,	/* The WhereScan object being initialized */
 	pScan->idxaff = 0;
 	pScan->coll = NULL;
 	pScan->is_column_seen = false;
-	if (pIdx) {
+	if (pIdx != NULL) {
 		int j = iColumn;
-		iColumn = pIdx->aiColumn[j];
-		if (iColumn >= 0) {
-			char affinity =
+		/*
+		 * pIdx->def->opts.sql == "fake_autoindex" means that
+		 * pIdx is a fake integer primary key index.
+		 */
+		if (pIdx->def->opts.sql != NULL &&
+		    strcmp(pIdx->def->opts.sql, "fake_autoindex") != 0) {
+			iColumn = pIdx->def->key_def->parts[iColumn].fieldno;
+			pScan->idxaff =
 				pIdx->pTable->def->fields[iColumn].affinity;
-			pScan->idxaff = affinity;
-			uint32_t id;
-			pScan->coll = sql_index_collation(pIdx, j, &id);
+			pScan->coll = pIdx->def->key_def->parts[j].coll;
 			pScan->is_column_seen = true;
+		} else {
+			iColumn = -1;
 		}
 	}
 	pScan->opMask = opMask;
@@ -541,47 +546,24 @@ findIndexCol(Parse * pParse,	/* Parse context */
 	     Index * pIdx,	/* Index to match column of */
 	     int iCol)		/* Column of index to match */
 {
+	struct key_part *part_to_match = &pIdx->def->key_def->parts[iCol];
 	for (int i = 0; i < pList->nExpr; i++) {
 		Expr *p = sqlite3ExprSkipCollate(pList->a[i].pExpr);
-		if (p->op == TK_COLUMN &&
-		    p->iColumn == pIdx->aiColumn[iCol] &&
-		    p->iTable == iBase) {
+		if (p->op == TK_COLUMN && p->iTable == iBase &&
+		    p->iColumn == (int) part_to_match->fieldno) {
 			bool is_found;
 			uint32_t id;
 			struct coll *coll = sql_expr_coll(pParse,
 							  pList->a[i].pExpr,
 							  &is_found, &id);
-			if (is_found &&
-			    coll == sql_index_collation(pIdx, iCol, &id)) {
+			if (is_found && coll == part_to_match->coll)
 				return i;
-			}
 		}
 	}
 
 	return -1;
 }
 
-/*
- * Return TRUE if the iCol-th column of index pIdx is NOT NULL
- */
-static int
-indexColumnNotNull(Index * pIdx, int iCol)
-{
-	int j;
-	assert(pIdx != 0);
-	assert(iCol >= 0 && iCol < (int)index_column_count(pIdx));
-	j = pIdx->aiColumn[iCol];
-	if (j >= 0) {
-		return !pIdx->pTable->def->fields[j].is_nullable;
-	} else if (j == (-1)) {
-		return 1;
-	} else {
-		assert(j == (-2));
-		return 0;	/* Assume an indexed expression can always yield a NULL */
-
-	}
-}
-
 /*
  * Return true if the DISTINCT expression-list passed as the third argument
  * is redundant.
@@ -633,9 +615,9 @@ isDistinctRedundant(Parse * pParse,		/* Parsing context */
 	 *      contain a "col=X" term are subject to a NOT NULL constraint.
 	 */
 	for (pIdx = pTab->pIndex; pIdx; pIdx = pIdx->pNext) {
-		if (!index_is_unique(pIdx))
+		if (!pIdx->def->opts.is_unique)
 			continue;
-		int col_count = index_column_count(pIdx);
+		int col_count = pIdx->def->key_def->part_count;
 		for (i = 0; i < col_count; i++) {
 			if (0 ==
 			    sqlite3WhereFindTerm(pWC, iBase, i, ~(Bitmask) 0,
@@ -643,11 +625,12 @@ isDistinctRedundant(Parse * pParse,		/* Parsing context */
 				if (findIndexCol
 				    (pParse, pDistinct, iBase, pIdx, i) < 0)
 					break;
-				if (indexColumnNotNull(pIdx, i) == 0)
+				uint32_t j = pIdx->def->key_def->parts[i].fieldno;
+				if (pIdx->pTable->def->fields[j].is_nullable)
 					break;
 			}
 		}
-		if (i == (int)index_column_count(pIdx)) {
+		if (i == (int) pIdx->def->key_def->part_count) {
 			/* This index implies that the DISTINCT qualifier is redundant. */
 			return 1;
 		}
@@ -835,8 +818,7 @@ constructAutomaticIndex(Parse * pParse,			/* The parsing context */
 	}
 
 	/* Construct the Index object to describe this index */
-	pIdx =
-	    sqlite3AllocateIndexObject(pParse->db, nKeyCol + 1, 0, &zNotUsed);
+	pIdx = sqlite3DbMallocZero(pParse->db, sizeof(*pIdx));
 	if (pIdx == 0)
 		goto end_auto_index_create;
 	pLoop->pIndex = pIdx;
@@ -1184,7 +1166,7 @@ whereRangeAdjust(WhereTerm * pTerm, LogEst nNew)
 char
 sqlite3IndexColumnAffinity(sqlite3 * db, Index * pIdx, int iCol)
 {
-	assert(iCol >= 0 && iCol < (int)index_column_count(pIdx));
+	assert(iCol >= 0 && iCol < (int) pIdx->def->key_def->part_count);
 	if (!pIdx->zColAff) {
 		if (sqlite3IndexAffinityStr(db, pIdx) == 0)
 			return AFFINITY_BLOB;
@@ -1246,13 +1228,12 @@ whereRangeSkipScanEst(Parse * pParse,		/* Parsing & code generating context */
 	int nUpper = index->def->opts.stat->sample_count + 1;
 	int rc = SQLITE_OK;
 	u8 aff = sqlite3IndexColumnAffinity(db, p, nEq);
-	uint32_t id;
 
 	sqlite3_value *p1 = 0;	/* Value extracted from pLower */
 	sqlite3_value *p2 = 0;	/* Value extracted from pUpper */
 	sqlite3_value *pVal = 0;	/* Value extracted from record */
 
-	struct coll *pColl = sql_index_collation(p, nEq, &id);
+	struct coll *coll = p->def->key_def->parts[nEq].coll;
 	if (pLower) {
 		rc = sqlite3Stat4ValueFromExpr(pParse, pLower->pExpr->pRight,
 					       aff, &p1);
@@ -1273,12 +1254,12 @@ whereRangeSkipScanEst(Parse * pParse,		/* Parsing & code generating context */
 			rc = sql_stat4_column(db, samples[i].sample_key, nEq,
 					      &pVal);
 			if (rc == SQLITE_OK && p1) {
-				int res = sqlite3MemCompare(p1, pVal, pColl);
+				int res = sqlite3MemCompare(p1, pVal, coll);
 				if (res >= 0)
 					nLower++;
 			}
 			if (rc == SQLITE_OK && p2) {
-				int res = sqlite3MemCompare(p2, pVal, pColl);
+				int res = sqlite3MemCompare(p2, pVal, coll);
 				if (res >= 0)
 					nUpper++;
 			}
@@ -1448,7 +1429,7 @@ whereRangeScanEst(Parse * pParse,	/* Parsing & code generating context */
 			       || (pLower->eOperator & (WO_GT | WO_GE)) != 0);
 			assert(pUpper == 0
 			       || (pUpper->eOperator & (WO_LT | WO_LE)) != 0);
-			if (sql_index_column_sort_order(p, nEq) !=
+			if (p->def->key_def->parts[nEq].sort_order !=
 			    SORT_ORDER_ASC) {
 				/* The roles of pLower and pUpper are swapped for a DESC index */
 				SWAP(pLower, pUpper);
@@ -1598,7 +1579,7 @@ whereEqualScanEst(Parse * pParse,	/* Parsing & code generating context */
 	int bOk;
 
 	assert(nEq >= 1);
-	assert(nEq <= (int)index_column_count(p));
+	assert(nEq <= (int) p->def->key_def->part_count);
 	assert(pBuilder->nRecValid < nEq);
 
 	/* If values are not available for all fields of the index to the left
@@ -1619,7 +1600,7 @@ whereEqualScanEst(Parse * pParse,	/* Parsing & code generating context */
 
 	whereKeyStats(pParse, p, pRec, 0, a);
 	WHERETRACE(0x10, ("equality scan regions %s(%d): %d\n",
-			  p->zName, nEq - 1, (int)a[1]));
+			  p->def->name, nEq - 1, (int)a[1]));
 	*pnRow = a[1];
 
 	return rc;
@@ -1751,8 +1732,8 @@ whereLoopPrint(WhereLoop * p, WhereClause * pWC)
 			   pItem->zAlias ? pItem->zAlias : pTab->def->name);
 #endif
 	const char *zName;
-	if (p->pIndex && (zName = p->pIndex->zName) != 0) {
-		if (strncmp(zName, "sqlite_autoindex_", 17) == 0) {
+	if (p->pIndex != NULL && (zName = p->pIndex->def->name) != NULL) {
+		if (strncmp(zName, "sql_autoindex_", 17) == 0) {
 			int i = sqlite3Strlen30(zName) - 1;
 			while (zName[i] != '_')
 				i--;
@@ -2314,7 +2295,7 @@ whereRangeVectorLen(Parse * pParse,	/* Parsing context */
 	int nCmp = sqlite3ExprVectorSize(pTerm->pExpr->pLeft);
 	int i;
 
-	nCmp = MIN(nCmp, (int)(index_column_count(pIdx) - nEq));
+	nCmp = MIN(nCmp, (int)(pIdx->def->key_def->part_count - nEq));
 	for (i = 1; i < nCmp; i++) {
 		/* Test if comparison i of pTerm is compatible with column (i+nEq)
 		 * of the index. If not, exit the loop.
@@ -2335,13 +2316,11 @@ whereRangeVectorLen(Parse * pParse,	/* Parsing context */
 		 * order of the index column is the same as the sort order of the
 		 * leftmost index column.
 		 */
-		if (pLhs->op != TK_COLUMN
-		    || pLhs->iTable != iCur
-		    || pLhs->iColumn != pIdx->aiColumn[i + nEq]
-		    || sql_index_column_sort_order(pIdx, i + nEq) !=
-		       sql_index_column_sort_order(pIdx, nEq)) {
+		struct key_part *parts = pIdx->def->key_def->parts;
+		if (pLhs->op != TK_COLUMN || pLhs->iTable != iCur ||
+		    pLhs->iColumn != (int)parts[i + nEq].fieldno ||
+		    parts[i + nEq].sort_order != parts[nEq].sort_order)
 			break;
-		}
 
 		aff = sqlite3CompareAffinity(pRhs, sqlite3ExprAffinity(pLhs));
 		idxaff =
@@ -2353,7 +2332,7 @@ whereRangeVectorLen(Parse * pParse,	/* Parsing context */
 		pColl = sql_binary_compare_coll_seq(pParse, pLhs, pRhs, &id);
 		if (pColl == 0)
 			break;
-	        if (sql_index_collation(pIdx, i + nEq, &id) != pColl)
+		if (pIdx->def->key_def->parts[i + nEq].coll != pColl)
 			break;
 	}
 	return i;
@@ -2396,13 +2375,13 @@ whereLoopAddBtreeIndex(WhereLoopBuilder * pBuilder,	/* The WhereLoop factory */
 	LogEst rSize;		/* Number of rows in the table */
 	LogEst rLogSize;	/* Logarithm of table size */
 	WhereTerm *pTop = 0, *pBtm = 0;	/* Top and bottom range constraints */
-	uint32_t nProbeCol = index_column_count(pProbe);
+	uint32_t probe_part_count = pProbe->def->key_def->part_count;
 
 	pNew = pBuilder->pNew;
 	if (db->mallocFailed)
 		return SQLITE_NOMEM_BKPT;
 	WHERETRACE(0x800, ("BEGIN addBtreeIdx(%s), nEq=%d\n",
-			   pProbe->zName, pNew->nEq));
+			   pProbe->def->name, pNew->nEq));
 
 	assert((pNew->wsFlags & WHERE_TOP_LIMIT) == 0);
 	if (pNew->wsFlags & WHERE_BTM_LIMIT) {
@@ -2431,7 +2410,7 @@ whereLoopAddBtreeIndex(WhereLoopBuilder * pBuilder,	/* The WhereLoop factory */
 		stat = &surrogate_stat;
 	if (stat->is_unordered)
 		opMask &= ~(WO_GT | WO_GE | WO_LT | WO_LE);
-	assert(pNew->nEq < nProbeCol);
+	assert(pNew->nEq < probe_part_count);
 
 	saved_nEq = pNew->nEq;
 	saved_nBtm = pNew->nBtm;
@@ -2452,10 +2431,14 @@ whereLoopAddBtreeIndex(WhereLoopBuilder * pBuilder,	/* The WhereLoop factory */
 		LogEst nOutUnadjusted;	/* nOut before IN() and WHERE adjustments */
 		int nIn = 0;
 		int nRecValid = pBuilder->nRecValid;
-		if ((eOp == WO_ISNULL || (pTerm->wtFlags & TERM_VNULL) != 0)
-		    && indexColumnNotNull(pProbe, saved_nEq)
-		    ) {
-			continue;	/* ignore IS [NOT] NULL constraints on NOT NULL columns */
+		uint32_t j = pProbe->def->key_def->parts[saved_nEq].fieldno;
+		if ((eOp == WO_ISNULL || (pTerm->wtFlags & TERM_VNULL) != 0) &&
+		    !pProbe->pTable->def->fields[j].is_nullable) {
+			/*
+			 * Ignore IS [NOT] NULL constraints on NOT
+			 * NULL columns.
+			 */
+			continue;
 		}
 		if (pTerm->prereqRight & pNew->maskSelf)
 			continue;
@@ -2523,14 +2506,16 @@ whereLoopAddBtreeIndex(WhereLoopBuilder * pBuilder,	/* The WhereLoop factory */
 							 */
 			}
 		} else if (eOp & WO_EQ) {
-			int iCol = pProbe->aiColumn[saved_nEq];
+			int iCol = pProbe->def->key_def->parts[saved_nEq].fieldno;
 			pNew->wsFlags |= WHERE_COLUMN_EQ;
 			assert(saved_nEq == pNew->nEq);
-			if ((iCol > 0 && nInMul == 0
-				&& saved_nEq == nProbeCol - 1)
-			    ) {
-				if (iCol >= 0 &&
-				    !index_is_unique_not_null(pProbe)) {
+			if (iCol > 0 && nInMul == 0 &&
+			    saved_nEq == probe_part_count - 1) {
+				bool index_is_unique_not_null =
+					pProbe->def->key_def->is_nullable &&
+					pProbe->def->opts.is_unique;
+				if (pProbe->tnum != 0 &&
+				    !index_is_unique_not_null) {
 					pNew->wsFlags |= WHERE_UNQ_WANTED;
 				} else {
 					pNew->wsFlags |= WHERE_ONEROW;
@@ -2592,8 +2577,7 @@ whereLoopAddBtreeIndex(WhereLoopBuilder * pBuilder,	/* The WhereLoop factory */
 			assert(eOp & (WO_ISNULL | WO_EQ | WO_IN));
 
 			assert(pNew->nOut == saved_nOut);
-			if (pTerm->truthProb <= 0
-			    && pProbe->aiColumn[saved_nEq] >= 0) {
+			if (pTerm->truthProb <= 0 && pProbe->tnum != 0 ) {
 				assert((eOp & WO_IN) || nIn == 0);
 				testcase(eOp & WO_IN);
 				pNew->nOut += pTerm->truthProb;
@@ -2695,8 +2679,8 @@ whereLoopAddBtreeIndex(WhereLoopBuilder * pBuilder,	/* The WhereLoop factory */
 			pNew->nOut = nOutUnadjusted;
 		}
 
-		if ((pNew->wsFlags & WHERE_TOP_LIMIT) == 0
-		    && pNew->nEq < nProbeCol) {
+		if ((pNew->wsFlags & WHERE_TOP_LIMIT) == 0 &&
+		    pNew->nEq < probe_part_count) {
 			whereLoopAddBtreeIndex(pBuilder, pSrc, pProbe,
 					       nInMul + nIn);
 		}
@@ -2724,7 +2708,7 @@ whereLoopAddBtreeIndex(WhereLoopBuilder * pBuilder,	/* The WhereLoop factory */
 	 * more expensive.
 	 */
 	assert(42 == sqlite3LogEst(18));
-	if (saved_nEq == saved_nSkip && saved_nEq + 1U < nProbeCol &&
+	if (saved_nEq == saved_nSkip && saved_nEq + 1U < probe_part_count &&
 	    stat->skip_scan_enabled == true &&
 	    /* TUNING: Minimum for skip-scan */
 	    index_field_tuple_est(pProbe, saved_nEq + 1) >= 42 &&
@@ -2749,7 +2733,7 @@ whereLoopAddBtreeIndex(WhereLoopBuilder * pBuilder,	/* The WhereLoop factory */
 	}
 
 	WHERETRACE(0x800, ("END addBtreeIdx(%s), nEq=%d, rc=%d\n",
-			   pProbe->zName, saved_nEq, rc));
+			   pProbe->def->name, saved_nEq, rc));
 	return rc;
 }
 
@@ -2792,7 +2776,7 @@ indexMightHelpWithOrderBy(WhereLoopBuilder * pBuilder,
 {
 	ExprList *pOB;
 	int ii, jj;
-	int nIdxCol = index_column_count(pIndex);
+	int part_count = pIndex->def->key_def->part_count;
 	if (index_is_unordered(pIndex))
 		return 0;
 	if ((pOB = pBuilder->pWInfo->pOrderBy) == 0)
@@ -2802,8 +2786,9 @@ indexMightHelpWithOrderBy(WhereLoopBuilder * pBuilder,
 		if (pExpr->op == TK_COLUMN && pExpr->iTable == iCursor) {
 			if (pExpr->iColumn < 0)
 				return 1;
-			for (jj = 0; jj < nIdxCol; jj++) {
-				if (pExpr->iColumn == pIndex->aiColumn[jj])
+			for (jj = 0; jj < part_count; jj++) {
+				if (pExpr->iColumn == (int)
+				    pIndex->def->key_def->parts[jj].fieldno)
 					return 1;
 			}
 		}
@@ -2880,9 +2865,7 @@ whereLoopAddBtree(WhereLoopBuilder * pBuilder,	/* WHERE clause information */
 {
 	WhereInfo *pWInfo;	/* WHERE analysis context */
 	Index *pProbe;		/* An index we are evaluating */
-	Index sPk;		/* A fake index object for the primary key */
-	LogEst aiRowEstPk[2];	/* The aiRowLogEst[] value for the sPk index */
-	i16 aiColumnPk = -1;	/* The aColumn[] value for the sPk index */
+	Index fake_index;		/* A fake index object for the primary key */
 	SrcList *pTabList;	/* The FROM clause */
 	struct SrcList_item *pSrc;	/* The FROM clause btree term to add */
 	WhereLoop *pNew;	/* Template WhereLoop object */
@@ -2903,31 +2886,63 @@ whereLoopAddBtree(WhereLoopBuilder * pBuilder,	/* WHERE clause information */
 	if (pSrc->pIBIndex) {
 		/* An INDEXED BY clause specifies a particular index to use */
 		pProbe = pSrc->pIBIndex;
+		fake_index.def = NULL;
 	} else if (pTab->pIndex) {
 		pProbe = pTab->pIndex;
+		fake_index.def = NULL;
 	} else {
 		/* There is no INDEXED BY clause.  Create a fake Index object in local
-		 * variable sPk to represent the primary key index.  Make this
+		 * variable fake_index to represent the primary key index.  Make this
 		 * fake index the first in a chain of Index objects with all of the real
 		 * indices to follow
 		 */
 		Index *pFirst;	/* First of real indices on the table */
-		memset(&sPk, 0, sizeof(Index));
-		sPk.nColumn = 1;
-		sPk.aiColumn = &aiColumnPk;
-		sPk.aiRowLogEst = aiRowEstPk;
-		sPk.onError = ON_CONFLICT_ACTION_REPLACE;
-		sPk.pTable = pTab;
-		aiRowEstPk[0] = sql_space_tuple_log_count(pTab);
-		aiRowEstPk[1] = 0;
+		memset(&fake_index, 0, sizeof(Index));
+		fake_index.onError = ON_CONFLICT_ACTION_REPLACE;
+		fake_index.pTable = pTab;
+
+		struct key_def *key_def = key_def_new(1);
+		if (key_def == NULL) {
+			pWInfo->pParse->nErr++;
+			pWInfo->pParse->rc = SQL_TARANTOOL_ERROR;
+			return SQL_TARANTOOL_ERROR;
+		}
+
+		key_def_set_part(key_def, 0, 0, pTab->def->fields[0].type,
+				 ON_CONFLICT_ACTION_ABORT,
+				 NULL, COLL_NONE, SORT_ORDER_ASC);
+
+		struct index_opts opts;
+		index_opts_create(&opts);
+		opts.sql = "fake_autoindex";
+		fake_index.def =
+			index_def_new(pTab->def->id, 0,"fake_autoindex",
+					sizeof("fake_autoindex") - 1,
+					TREE, &opts, key_def, NULL);
+		key_def_delete(key_def);
+
+		if (fake_index.def == NULL) {
+			pWInfo->pParse->nErr++;
+			pWInfo->pParse->rc = SQL_TARANTOOL_ERROR;
+			return SQL_TARANTOOL_ERROR;
+		}
+
+		struct index_stat *stat =
+			(struct index_stat *) malloc(sizeof(struct index_stat));
+		stat->tuple_log_est =
+			(log_est_t *) malloc(sizeof(log_est_t) * 2);
+		stat->tuple_log_est[0] = sql_space_tuple_log_count(pTab);
+		stat->tuple_log_est[1] = 0;
+		fake_index.def->opts.stat = stat;
+
 		pFirst = pSrc->pTab->pIndex;
 		if (pSrc->fg.notIndexed == 0) {
 			/* The real indices of the table are only considered if the
 			 * NOT INDEXED qualifier is omitted from the FROM clause
 			 */
-			sPk.pNext = pFirst;
+			fake_index.pNext = pFirst;
 		}
-		pProbe = &sPk;
+		pProbe = &fake_index;
 	}
 
 #ifndef SQLITE_OMIT_AUTOMATIC_INDEX
@@ -3058,6 +3073,11 @@ whereLoopAddBtree(WhereLoopBuilder * pBuilder,	/* WHERE clause information */
 		if (pSrc->pIBIndex)
 			break;
 	}
+	if (fake_index.def != NULL)
+	{
+		free(fake_index.def->opts.stat->tuple_log_est);
+		index_def_delete(fake_index.def);
+	}
 	return rc;
 }
 
@@ -3392,8 +3412,8 @@ wherePathSatisfiesOrderBy(WhereInfo * pWInfo,	/* The WHERE clause */
 				   index_is_unordered(pIndex)) {
 				return 0;
 			} else {
-				nColumn = index_column_count(pIndex);
-				isOrderDistinct = index_is_unique(pIndex);
+				nColumn = pIndex->def->key_def->part_count;
+				isOrderDistinct = pIndex->def->opts.is_unique;
 			}
 
 			/* Loop through all columns of the index and deal with the ones
@@ -3454,9 +3474,10 @@ wherePathSatisfiesOrderBy(WhereInfo * pWInfo,	/* The WHERE clause */
 				 * (revIdx) for the j-th column of the index.
 				 */
 				if (pIndex != NULL) {
-					iColumn = pIndex->aiColumn[j];
-					revIdx = sql_index_column_sort_order(pIndex,
-									     j);
+					struct key_def *def =
+						pIndex->def->key_def;
+					iColumn = def->parts[j].fieldno;
+					revIdx = def->parts[j].sort_order;
 					if (iColumn == pIndex->pTable->iPKey)
 						iColumn = -1;
 				} else {
@@ -3506,8 +3527,7 @@ wherePathSatisfiesOrderBy(WhereInfo * pWInfo,	/* The WHERE clause */
 								      pOrderBy->a[i].pExpr,
 								      &is_found, &id);
 						struct coll *idx_coll =
-							sql_index_collation(pIndex,
-									    j, &id);
+							pIndex->def->key_def->parts[j].coll;
 						if (is_found &&
 						    coll != idx_coll)
 							continue;
@@ -4783,7 +4803,7 @@ sqlite3WhereBegin(Parse * pParse,	/* The parser context */
 					sqlite3VdbeChangeP5(v, OPFLAG_SEEKEQ);	/* Hint to COMDB2 */
 				}
 				if (pIx != NULL)
-					VdbeComment((v, "%s", pIx->zName));
+					VdbeComment((v, "%s", pIx->def->name));
 				else
 					VdbeComment((v, "%s", idx_def->name));
 #ifdef SQLITE_ENABLE_COLUMN_USED_MASK
@@ -4916,7 +4936,7 @@ sqlite3WhereEnd(WhereInfo * pWInfo)
 		if (pLevel->addrSkip) {
 			sqlite3VdbeGoto(v, pLevel->addrSkip);
 			VdbeComment((v, "next skip-scan on %s",
-				     pLoop->pIndex->zName));
+				     pLoop->pIndex->def->name));
 			sqlite3VdbeJumpHere(v, pLevel->addrSkip);
 			sqlite3VdbeJumpHere(v, pLevel->addrSkip - 2);
 		}
diff --git a/src/box/sql/wherecode.c b/src/box/sql/wherecode.c
index c35c25ac4..1976583fa 100644
--- a/src/box/sql/wherecode.c
+++ b/src/box/sql/wherecode.c
@@ -48,7 +48,7 @@
 static const char *
 explainIndexColumnName(Index * pIdx, int i)
 {
-	i = pIdx->aiColumn[i];
+	i = pIdx->def->key_def->parts[i].fieldno;
 	return pIdx->pTable->def->fields[i].name;
 }
 
@@ -243,7 +243,7 @@ sqlite3WhereExplainOneScan(Parse * pParse,	/* Parse context */
 			if (zFmt) {
 				sqlite3StrAccumAppend(&str, " USING ", 7);
 				if (pIdx != NULL)
-					sqlite3XPrintf(&str, zFmt, pIdx->zName);
+					sqlite3XPrintf(&str, zFmt, pIdx->def->name);
 				else if (idx_def != NULL)
 					sqlite3XPrintf(&str, zFmt, idx_def->name);
 				else
@@ -488,7 +488,7 @@ codeEqualityTerm(Parse * pParse,	/* The parsing context */
 		int *aiMap = 0;
 
 		if (pLoop->pIndex != 0 &&
-		    sql_index_column_sort_order(pLoop->pIndex, iEq)) {
+		    pLoop->pIndex->def->key_def->parts[iEq].sort_order) {
 			testcase(iEq == 0);
 			testcase(bRev);
 			bRev = !bRev;
@@ -736,7 +736,7 @@ codeAllEqualityTerms(Parse * pParse,	/* Parsing context */
 		sqlite3VdbeAddOp1(v, (bRev ? OP_Last : OP_Rewind), iIdxCur);
 		VdbeCoverageIf(v, bRev == 0);
 		VdbeCoverageIf(v, bRev != 0);
-		VdbeComment((v, "begin skip-scan on %s", pIdx->zName));
+		VdbeComment((v, "begin skip-scan on %s", pIdx->def->name));
 		j = sqlite3VdbeAddOp0(v, OP_Goto);
 		pLevel->addrSkip =
 		    sqlite3VdbeAddOp4Int(v, (bRev ? OP_SeekLT : OP_SeekGT),
@@ -746,7 +746,8 @@ codeAllEqualityTerms(Parse * pParse,	/* Parsing context */
 		sqlite3VdbeJumpHere(v, j);
 		for (j = 0; j < nSkip; j++) {
 			sqlite3VdbeAddOp3(v, OP_Column, iIdxCur,
-					  pIdx->aiColumn[j], regBase + j);
+					  pIdx->def->key_def->parts[j].fieldno,
+					  regBase + j);
 			VdbeComment((v, "%s", explainIndexColumnName(pIdx, j)));
 		}
 	}
@@ -1037,14 +1038,14 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 		assert(pWInfo->pOrderBy == 0
 		       || pWInfo->pOrderBy->nExpr == 1
 		       || (pWInfo->wctrlFlags & WHERE_ORDERBY_MIN) == 0);
-		int nIdxCol;
+		uint32_t part_count;
 		if (pIdx != NULL)
-			nIdxCol = index_column_count(pIdx);
+			part_count = pIdx->def->key_def->part_count;
 		else
-			nIdxCol = idx_def->key_def->part_count;
-		if ((pWInfo->wctrlFlags & WHERE_ORDERBY_MIN) != 0
-		    && pWInfo->nOBSat > 0 && (nIdxCol > nEq)) {
-			j = pIdx->aiColumn[nEq];
+			part_count = idx_def->key_def->part_count;
+		if ((pWInfo->wctrlFlags & WHERE_ORDERBY_MIN) != 0 &&
+		    pWInfo->nOBSat > 0 && part_count > nEq) {
+			j = pIdx->def->key_def->parts[nEq].fieldno;
 			/* Allow seek for column with `NOT NULL` == false attribute.
 			 * If a column may contain NULL-s, the comparator installed
 			 * by Tarantool is prepared to seek using a NULL value.
@@ -1055,8 +1056,7 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 			 * FYI: entries in an index are ordered as follows:
 			 *      NULL, ... NULL, min_value, ...
 			 */
-			if (j >= 0 &&
-			    pIdx->pTable->def->fields[j].is_nullable) {
+			if (pIdx->pTable->def->fields[j].is_nullable) {
 				assert(pLoop->nSkip == 0);
 				bSeekPastNull = 1;
 				nExtraReg = 1;
@@ -1093,16 +1093,16 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 				testcase(pIdx->aSortOrder[nEq] ==
 					 SORT_ORDER_DESC);
 				assert((bRev & ~1) == 0);
+				struct key_def *def = pIdx->def->key_def;
 				pLevel->iLikeRepCntr <<= 1;
 				pLevel->iLikeRepCntr |=
-					bRev ^ (sql_index_column_sort_order(pIdx, nEq) ==
+					bRev ^ (def->parts[nEq].sort_order ==
 						SORT_ORDER_DESC);
 			}
 #endif
 			if (pRangeStart == 0) {
-				j = pIdx->aiColumn[nEq];
-				if (j >= 0 &&
-				    pIdx->pTable->def->fields[j].is_nullable)
+				j = pIdx->def->key_def->parts[nEq].fieldno;
+				if (pIdx->pTable->def->fields[j].is_nullable)
 					bSeekPastNull = 1;
 			}
 		}
@@ -1113,10 +1113,9 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 		 * a forward order scan on a descending index, interchange the
 		 * start and end terms (pRangeStart and pRangeEnd).
 		 */
-		if ((nEq < nIdxCol &&
-		     bRev == (sql_index_column_sort_order(pIdx, nEq) ==
-			      SORT_ORDER_ASC)) ||
-		    (bRev && nIdxCol == nEq)) {
+		if ((nEq < part_count &&
+		     bRev == (pIdx->def->key_def->parts[nEq].sort_order ==
+			      SORT_ORDER_ASC)) || (bRev && part_count == nEq)) {
 			SWAP(pRangeEnd, pRangeStart);
 			SWAP(bSeekPastNull, bStopAtNull);
 			SWAP(nBtm, nTop);
@@ -1196,16 +1195,16 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 			}
 		} else {
 			pk = sqlite3PrimaryKeyIndex(pIdx->pTable);
-			affinity =
-				pIdx->pTable->def->fields[pk->aiColumn[0]].affinity;
+			uint32_t fieldno = pk->def->key_def->parts[0].fieldno;
+			affinity = pIdx->pTable->def->fields[fieldno].affinity;
 		}
 
-		int nPkCol;
+		uint32_t pk_part_count;
 		if (pk != NULL)
-			nPkCol = index_column_count(pk);
+			pk_part_count = pk->def->key_def->part_count;
 		else
-			nPkCol = idx_pk->key_def->part_count;
-		if (nPkCol == 1 && affinity == AFFINITY_INTEGER) {
+			pk_part_count = idx_pk->key_def->part_count;
+		if (pk_part_count == 1 && affinity == AFFINITY_INTEGER) {
 			/* Right now INTEGER PRIMARY KEY is the only option to
 			 * get Tarantool's INTEGER column type. Need special handling
 			 * here: try to loosely convert FLOAT to INT. If RHS type
@@ -1213,8 +1212,9 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 			 */
 			int limit = pRangeStart == NULL ? nEq : nEq + 1;
 			for (int i = 0; i < limit; i++) {
-				if ((pIdx != NULL && pIdx->aiColumn[i] ==
-				     pk->aiColumn[0]) ||
+				if ((pIdx != NULL &&
+				     pIdx->def->key_def->parts[i].fieldno ==
+				     pk->def->key_def->parts[0].fieldno) ||
 				    (idx_pk != NULL &&
 				     idx_def->key_def->parts[i].fieldno ==
 				     idx_pk->key_def->parts[0].fieldno)) {
@@ -1326,17 +1326,17 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 			/* pIdx is a covering index.  No need to access the main table. */
 		}  else if (iCur != iIdxCur) {
 			Index *pPk = sqlite3PrimaryKeyIndex(pIdx->pTable);
-			int nPkCol = index_column_count(pPk);
-			int iKeyReg = sqlite3GetTempRange(pParse, nPkCol);
-			for (j = 0; j < nPkCol; j++) {
-				k = pPk->aiColumn[j];
+			int pk_part_count = pPk->def->key_def->part_count;
+			int iKeyReg = sqlite3GetTempRange(pParse, pk_part_count);
+			for (j = 0; j < pk_part_count; j++) {
+				k = pPk->def->key_def->parts[j].fieldno;
 				sqlite3VdbeAddOp3(v, OP_Column, iIdxCur, k,
 						  iKeyReg + j);
 			}
 			sqlite3VdbeAddOp4Int(v, OP_NotFound, iCur, addrCont,
-					     iKeyReg, nPkCol);
+					     iKeyReg, pk_part_count);
 			VdbeCoverage(v);
-			sqlite3ReleaseTempRange(pParse, iKeyReg, nPkCol);
+			sqlite3ReleaseTempRange(pParse, iKeyReg, pk_part_count);
 		}
 
 		/* Record the instruction used to terminate the loop. */
@@ -1434,10 +1434,10 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 		 */
 		if ((pWInfo->wctrlFlags & WHERE_DUPLICATES_OK) == 0) {
 			Index *pPk = sqlite3PrimaryKeyIndex(pTab);
-			int nPkCol = index_column_count(pPk);
+			int pk_part_count = pPk->def->key_def->part_count;
 			regRowset = pParse->nTab++;
 			sqlite3VdbeAddOp2(v, OP_OpenTEphemeral,
-					  regRowset, nPkCol);
+					  regRowset, pk_part_count);
 			sql_vdbe_set_p4_key_def(pParse, pPk);
 			regPk = ++pParse->nMem;
 		}
@@ -1538,16 +1538,23 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 						int iSet =
 						    ((ii == pOrWc->nTerm - 1) ? -1 : ii);
 						Index *pPk = sqlite3PrimaryKeyIndex (pTab);
-						int nPk = index_column_count(pPk);
-						int iPk;
+						struct key_def *def =
+							pPk->def->key_def;
 
 						/* Read the PK into an array of temp registers. */
-						r = sqlite3GetTempRange(pParse, nPk);
-						for (iPk = 0; iPk < nPk; iPk++) {
-							int iCol = pPk->aiColumn[iPk];
+						r = sqlite3GetTempRange(pParse,
+									def->part_count);
+						for (uint32_t iPk = 0;
+						     iPk < def->part_count;
+						     iPk++) {
+							uint32_t fieldno =
+								def->parts[iPk].
+								fieldno;
 							sqlite3ExprCodeGetColumnToReg
-								(pParse, pTab->def,
-								 iCol, iCur,
+								(pParse,
+								 pTab->def,
+								 fieldno,
+								 iCur,
 								 r + iPk);
 						}
 
@@ -1567,20 +1574,21 @@ sqlite3WhereCodeOneLoopStart(WhereInfo * pWInfo,	/* Complete information about t
 							jmp1 = sqlite3VdbeAddOp4Int
 								(v, OP_Found,
 								 regRowset, 0,
-								 r, nPk);
+								 r,
+								 def->part_count);
 							VdbeCoverage(v);
 						}
 						if (iSet >= 0) {
 							sqlite3VdbeAddOp3
 								(v, OP_MakeRecord,
-								 r, nPk, regPk);
+								 r, def->part_count, regPk);
 							sqlite3VdbeAddOp2
 								(v, OP_IdxInsert,
 								 regRowset, regPk);
 						}
 
 						/* Release the array of temp registers */
-						sqlite3ReleaseTempRange(pParse, r, nPk);
+						sqlite3ReleaseTempRange(pParse, r, def->part_count);
 					}
 
 					/* Invoke the main loop body as a subroutine */
diff --git a/test/box/misc.result b/test/box/misc.result
index 2f8cacf09..7423a6663 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -490,6 +490,7 @@ t;
   163: box.error.TRANSACTION_YIELD
   164: box.error.NO_SUCH_GROUP
   165: box.error.NO_SUCH_MODULE
+  166: box.error.NO_SUCH_COLLATION
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/sql-tap/analyze6.test.lua b/test/sql-tap/analyze6.test.lua
index 7f4ce1e3e..cb1710a50 100755
--- a/test/sql-tap/analyze6.test.lua
+++ b/test/sql-tap/analyze6.test.lua
@@ -116,7 +116,7 @@ test:do_eqp_test(
     [[SELECT * FROM t201 WHERE y=5]],
     {
         -- <analyze6-2.2>
-        {0, 0, 0, "SEARCH TABLE T201 USING COVERING INDEX sqlite_autoindex_T201_1 (Y=?)"}
+        {0, 0, 0, "SEARCH TABLE T201 USING COVERING INDEX sql_autoindex_T201_1 (Y=?)"}
         -- </analyze6-2.2>
 })
 
@@ -148,7 +148,7 @@ test:do_eqp_test(
     [[SELECT * FROM t201 WHERE y=5]],
     {
         -- <analyze6-2.5>
-        {0, 0, 0, "SEARCH TABLE T201 USING COVERING INDEX sqlite_autoindex_T201_1 (Y=?)"}
+        {0, 0, 0, "SEARCH TABLE T201 USING COVERING INDEX sql_autoindex_T201_1 (Y=?)"}
         -- </analyze6-2.5>
 })
 
@@ -183,7 +183,7 @@ test:do_eqp_test(
     [[SELECT * FROM t201 WHERE y=5]],
     {
         -- <analyze6-2.8>
-        {0, 0, 0, "SEARCH TABLE T201 USING COVERING INDEX sqlite_autoindex_T201_1 (Y=?)"}
+        {0, 0, 0, "SEARCH TABLE T201 USING COVERING INDEX sql_autoindex_T201_1 (Y=?)"}
         -- </analyze6-2.8>
 })
 
diff --git a/test/sql-tap/collation.test.lua b/test/sql-tap/collation.test.lua
index 8a98de987..4d33d0c9d 100755
--- a/test/sql-tap/collation.test.lua
+++ b/test/sql-tap/collation.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(173)
+test:plan(174)
 
 local prefix = "collation-"
 
@@ -249,4 +249,9 @@ local like_testcases =
 
 test:do_catchsql_set_test(like_testcases, prefix)
 
+test:do_catchsql_test(
+        "collation-2.5.0",
+        'CREATE TABLE test3 (a int, b int, c int, PRIMARY KEY (a, a COLLATE foo, b, c))',
+        {1, "Collation 'FOO' does not exist"})
+
 test:finish_test()
diff --git a/test/sql-tap/colname.test.lua b/test/sql-tap/colname.test.lua
index c53a1e885..ddc06eea7 100755
--- a/test/sql-tap/colname.test.lua
+++ b/test/sql-tap/colname.test.lua
@@ -643,13 +643,13 @@ test:do_catchsql_test(
     "colname-11.2",
     [[CREATE TABLE t1(a, b, c, d, e, 
       PRIMARY KEY(a), UNIQUE('b' COLLATE "unicode_ci" DESC));]],
-    {1, "/functional indexes aren't supported in the current version/"})
+    {1, "/Tarantool does not support functional indexes/"})
 
 test:execsql("create table table1(a primary key, b, c)")
 
 test:do_catchsql_test(
     "colname-11.3",
     [[ CREATE INDEX t1c ON table1('c'); ]],
-    {1, "/functional indexes aren't supported in the current version/"})
+    {1, "/Tarantool does not support functional indexes/"})
 
 test:finish_test()
diff --git a/test/sql-tap/gh-2931-savepoints.test.lua b/test/sql-tap/gh-2931-savepoints.test.lua
index 9668d8fde..3861bb209 100755
--- a/test/sql-tap/gh-2931-savepoints.test.lua
+++ b/test/sql-tap/gh-2931-savepoints.test.lua
@@ -80,7 +80,7 @@ local testcases = {
 		{0,{1,2,10,11,1,2,4,10,11}}},
 	{"14",
 		[[insert into t1 values(4);]],
-		{1,"Duplicate key exists in unique index 'sqlite_autoindex_T2_1' in space 'T2'"}},
+		{1,"Duplicate key exists in unique index 'sql_autoindex_T2_1' in space 'T2'"}},
 	{"15",
 		[[select * from t1 union all select * from t2;]],
 		{0,{1,2,10,11,1,2,4,10,11}}},
diff --git a/test/sql-tap/gh2140-trans.test.lua b/test/sql-tap/gh2140-trans.test.lua
index 5539ed261..fe7af5f8e 100755
--- a/test/sql-tap/gh2140-trans.test.lua
+++ b/test/sql-tap/gh2140-trans.test.lua
@@ -32,7 +32,7 @@ for _, verb in ipairs({'ROLLBACK', 'ABORT'}) do
 	if verb == 'ROLLBACK' then
 		answer = 'UNIQUE constraint failed: T1.S1'
 	else
-		answer = "Duplicate key exists in unique index 'sqlite_autoindex_T1_1' in space 'T1'"
+		answer = "Duplicate key exists in unique index 'sql_autoindex_T1_1' in space 'T1'"
 	end
         test:do_catchsql_test('insert1_'..verb,
                               [[BEGIN;
diff --git a/test/sql-tap/gh2259-in-stmt-trans.test.lua b/test/sql-tap/gh2259-in-stmt-trans.test.lua
index 41d52feb6..e2ae1694b 100755
--- a/test/sql-tap/gh2259-in-stmt-trans.test.lua
+++ b/test/sql-tap/gh2259-in-stmt-trans.test.lua
@@ -18,7 +18,7 @@ for _, prefix in pairs({"BEFORE", "AFTER"}) do
 
     test:do_catchsql_test(prefix..'_insert1',
                           'INSERT INTO t1 VALUES(1, 2)',
-                          {1,"Duplicate key exists in unique index 'sqlite_autoindex_T2_1' in space 'T2'"})
+                          {1,"Duplicate key exists in unique index 'sql_autoindex_T2_1' in space 'T2'"})
 
     test:do_execsql_test(prefix..'_insert1_check1',
                          'SELECT *  FROM t1',
@@ -34,7 +34,7 @@ for _, prefix in pairs({"BEFORE", "AFTER"}) do
 
     test:do_catchsql_test(prefix..'_update1',
                           'UPDATE t1 SET s1=1',
-                          {1,"Duplicate key exists in unique index 'sqlite_autoindex_T2_1' in space 'T2'"})
+                          {1,"Duplicate key exists in unique index 'sql_autoindex_T2_1' in space 'T2'"})
 
     test:do_execsql_test(prefix..'_update1_check1',
                          'SELECT *  FROM t1',
@@ -52,7 +52,7 @@ for _, prefix in pairs({"BEFORE", "AFTER"}) do
 
     test:do_catchsql_test(prefix..'delete1',
                           'DELETE FROM t1;',
-                          {1, "Duplicate key exists in unique index 'sqlite_autoindex_T2_1' in space 'T2'"})
+                          {1, "Duplicate key exists in unique index 'sql_autoindex_T2_1' in space 'T2'"})
 
     -- Nothing should be inserted due to abort
     test:do_execsql_test('delete1_check1',
@@ -69,7 +69,7 @@ end
 -- Check multi-insert
 test:do_catchsql_test('insert2',
                       'INSERT INTO t1 VALUES (5, 6), (6, 7)',
-                      {1, "Duplicate key exists in unique index 'sqlite_autoindex_T2_1' in space 'T2'"})
+                      {1, "Duplicate key exists in unique index 'sql_autoindex_T2_1' in space 'T2'"})
 test:do_execsql_test('insert2_check',
                      'SELECT * FROM t1;',
                      {3, 3})
diff --git a/test/sql-tap/gh2964-abort.test.lua b/test/sql-tap/gh2964-abort.test.lua
index 193aabb25..a06b4fd15 100755
--- a/test/sql-tap/gh2964-abort.test.lua
+++ b/test/sql-tap/gh2964-abort.test.lua
@@ -13,7 +13,7 @@ test:do_catchsql_test(
     "CREATE TABLE t2 (a int primary key);")
 
 local insert_err = {1, "UNIQUE constraint failed: T2.A"}
-local insert_err_PK = {1, "Duplicate key exists in unique index 'sqlite_autoindex_T2_1' in space 'T2'"}
+local insert_err_PK = {1, "Duplicate key exists in unique index 'sql_autoindex_T2_1' in space 'T2'"}
 local data = {
 --id|TRIG TYPE|INSERT TYPE|insert error|commit error| result
  {1, "AFTER", "or abort",   insert_err_PK, {0},          {1,1,2}},
diff --git a/test/sql-tap/identifier-characters.test.lua b/test/sql-tap/identifier-characters.test.lua
index 31b45c6e7..24b5dd075 100755
--- a/test/sql-tap/identifier-characters.test.lua
+++ b/test/sql-tap/identifier-characters.test.lua
@@ -10,7 +10,7 @@ local testcases = {
 	{"table",
 	-- create
 	function (id)
-		-- sql autogenerated index name rules add "sqlite_autoindex_") prefix
+		-- sql autogenerated index name rules add "sql_autoindex_") prefix
 		if string.len(id) == box.schema.NAME_MAX then
 			id = string.sub(id, string.len(id))
 		end
diff --git a/test/sql-tap/identifier_case.test.lua b/test/sql-tap/identifier_case.test.lua
index 5e7573ac4..ed9553c6b 100755
--- a/test/sql-tap/identifier_case.test.lua
+++ b/test/sql-tap/identifier_case.test.lua
@@ -206,8 +206,8 @@ data = {
     { 3,  [["binary"]], {0}},
     { 4,  [["bInaRy"]], {0}},
     { 5,  [["unicode"]], {0}},
-    { 6,  [[ unicode ]], {1,"no such collation sequence: UNICODE"}},
-    { 7,  [["UNICODE"]], {1,"no such collation sequence: UNICODE"}}
+    { 6,  [[ unicode ]], {1,"Collation 'UNICODE' does not exist"}},
+    { 7,  [["UNICODE"]], {1,"Collation 'UNICODE' does not exist"}}
 }
 
 test:do_catchsql_test(
diff --git a/test/sql-tap/index1.test.lua b/test/sql-tap/index1.test.lua
index 4329381e2..9ee40fa13 100755
--- a/test/sql-tap/index1.test.lua
+++ b/test/sql-tap/index1.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(79)
+test:plan(78)
 
 --!./tcltestrunner.lua
 -- 2001 September 15
@@ -374,24 +374,14 @@ test:do_catchsql_test(
         -- </index-6.1c>
     })
 
-test:do_catchsql_test(
-    "index-6.2",
-    [[
-        CREATE INDEX test1 ON test2(g1)
-    ]], {
-        -- <index-6.2>
-        1, "there is already a table named TEST1"
-        -- </index-6.2>
-    })
-
 test:do_execsql_test(
-    "index-6.2b",
+    "index-6.2",
     [[
         SELECT "name" FROM "_index" WHERE "name"='INDEX1' union SELECT "name" FROM "_space" WHERE "name"='TEST1' OR "name"='TEST2'
     ]], {
-        -- <index-6.2b>
+        -- <index-6.2>
         "INDEX1", "TEST1", "TEST2"
-        -- </index-6.2b>
+        -- </index-6.2>
     })
 
 test:do_test(
@@ -454,10 +444,10 @@ test:do_execsql_test(
 test:do_execsql_test(
     "index-7.3",
     [[
-        SELECT "name" FROM "_index" WHERE "name"='sqlite_autoindex_TEST1_1'
+        SELECT "name" FROM "_index" WHERE "name"='sql_autoindex_TEST1_1'
     ]], {
         -- <index-7.3>
-        "sqlite_autoindex_TEST1_1"
+        "sql_autoindex_TEST1_1"
         -- </index-7.3>
     })
 
@@ -1015,7 +1005,7 @@ test:do_execsql_test(
     })
 
 -- Test that automatically create indices are named correctly. The current
--- convention is: "sqlite_autoindex_<table name>_<integer>"
+-- convention is: "sql_autoindex_<table name>_<integer>"
 --
 -- Then check that it is an error to try to drop any automtically created
 -- indices.
@@ -1027,18 +1017,18 @@ test:do_execsql_test(
         SELECT "_index"."name" FROM "_index" JOIN "_space" WHERE "_index"."id" = "_space"."id" AND "_space"."name"='T7';
     ]], {
         -- <index-17.1>
-        "sqlite_autoindex_T7_3", "sqlite_autoindex_T7_2", "sqlite_autoindex_T7_1"
+        "sql_autoindex_T7_3", "sql_autoindex_T7_2", "sql_autoindex_T7_1"
         -- </index-17.1>
     })
 
 -- do_test index-17.2 {
 --   catchsql {
---     DROP INDEX sqlite_autoindex_t7_1;
+--     DROP INDEX sql_autoindex_t7_1;
 --   }
 -- } {1 {index associated with UNIQUE or PRIMARY KEY constraint cannot be dropped}}
 -- do_test index-17.3 {
 --   catchsql {
---     DROP INDEX IF EXISTS sqlite_autoindex_t7_1;
+--     DROP INDEX IF EXISTS sql_autoindex_t7_1;
 --   }
 -- } {1 {index associated with UNIQUE or PRIMARY KEY constraint cannot be dropped}}
 test:do_catchsql_test(
@@ -1081,7 +1071,7 @@ test:do_execsql_test(
             INSERT INTO t7 VALUES(1);
         ]], {
             -- <index-19.2>
-            1, "Duplicate key exists in unique index 'sqlite_autoindex_T7_1' in space 'T7'"
+            1, "Duplicate key exists in unique index 'sql_autoindex_T7_1' in space 'T7'"
             -- </index-19.2>
         })
 
diff --git a/test/sql-tap/index7.test.lua b/test/sql-tap/index7.test.lua
index 336f42796..3914e3cea 100755
--- a/test/sql-tap/index7.test.lua
+++ b/test/sql-tap/index7.test.lua
@@ -1,6 +1,6 @@
 #!/usr/bin/env tarantool
 test = require("sqltester")
-test:plan(5)
+test:plan(11)
 
 --!./tcltestrunner.lua
 -- 2013-11-04
@@ -48,7 +48,7 @@ end
 -- do_test index7-1.1a {
 --   capture_pragma db out {PRAGMA index_list(t1)}
 --   db eval {SELECT "name", "partial", '|' FROM out ORDER BY "name"}
--- } {sqlite_autoindex_t1_1 0 | t1a 1 | t1b 1 |}
+-- } {sql_autoindex_t1_1 0 | t1a 1 | t1b 1 |}
 -- # Make sure the count(*) optimization works correctly with
 -- # partial indices.  Ticket [a5c8ed66cae16243be6] 2013-10-03.
 -- #
@@ -303,4 +303,104 @@ test:do_catchsql_test(
         1, "keyword \"WHERE\" is reserved"
     })
 
+-- Currently, when a user tries to create index (or primary key,
+-- since we implement them as indexes underhood) with duplicated
+-- fields (like 'CREATE INDEX i1 ON t(a, a, a, a, b, c, b)')
+-- tarantool would silently remove duplicated fields and
+-- execute 'CREATE INDEX i1 ON t(a, b, c)'.
+-- This test checks that duplicates are removed correctly.
+--
+test:do_catchsql_test(
+        "index7-8.1",
+        [[
+            CREATE TABLE t(a,b,c, PRIMARY KEY(a));
+            CREATE INDEX i1 ON t(a, a, b, c, c, b, b, b, c, b, c);
+            pragma index_info = t.i1;
+        ]],
+        {0, {0,0,"A",1,1,"B",2,2,"C"}}
+)
+
+-- There was the following bug:
+-- > CREATE TABLE t1(a,b,c,d, PRIMARY KEY(a,a,a,b,c));
+-- ...
+-- > CREATE INDEX i1 ON t1(b,c,a,c)
+-- ...
+-- But index 'i1' was not actually created and no error was raised.
+-- This test checks that this does not happen anymore (and index is
+-- created successfully).
+--
+test:do_catchsql_test(
+        "index7-8.2",
+        [[
+            CREATE TABLE test4(a,b,c,d, PRIMARY KEY(a,a,a,b,c));
+            CREATE INDEX index1 on test4(b,c,a,c);
+            SELECT "_index"."name"
+            FROM "_index" JOIN "_space" WHERE
+                "_index"."id" = "_space"."id" AND
+                "_space"."name"='TEST4'       AND
+                "_index"."name"='INDEX1';
+        ]],
+        {0, {'INDEX1'}})
+
+-- This test checks that CREATE TABLE statement with PK constraint
+-- and NON-NAMED UNIQUE constraint (declared on THE SAME COLUMNS)
+-- creates only one index - for PK constraint.
+--
+test:do_catchsql_test(
+        "index7-8.3",
+        [[
+            CREATE TABLE test5(a,b,c,d, PRIMARY KEY(a), UNIQUE(a));
+            SELECT "_index"."name", "_index"."iid"
+            FROM "_index" JOIN "_space" WHERE
+                "_index"."id" = "_space"."id" AND
+                "_space"."name"='TEST5';
+        ]],
+        {0, {"sql_autoindex_TEST5_1",0}})
+
+-- This test checks that CREATE TABLE statement with PK constraint
+-- and NAMED UNIQUE constraint (declared on THE SAME COLUMNS)
+-- creates two indexes - for PK constraint and for UNIQUE
+-- constraint.
+--
+test:do_catchsql_test(
+        "index7-8.4",
+        [[
+            CREATE TABLE test6(a,b,c,d, PRIMARY KEY(a), CONSTRAINT c1 UNIQUE(a));
+            SELECT "_index"."name", "_index"."iid"
+            FROM "_index" JOIN "_space" WHERE
+                "_index"."id" = "_space"."id" AND
+                "_space"."name"='TEST6';
+        ]],
+        {0, {"sql_autoindex_TEST6_1",0,"unique_constraint_C1",1}})
+
+-- This test checks that CREATE TABLE statement with PK constraint
+-- and UNIQUE constraint is executed correctly
+-- (no matter if UNIQUE precedes PK or not).
+--
+test:do_catchsql_test(
+        "index7-8.5",
+        [[
+            CREATE TABLE test7(a,b,c,d, UNIQUE(a), PRIMARY KEY(a));
+            SELECT "_index"."name", "_index"."iid"
+            FROM "_index" JOIN "_space" WHERE
+                "_index"."id" = "_space"."id" AND
+                "_space"."name"='TEST7';
+        ]],
+        {0, {"sql_autoindex_TEST7_1",0}})
+
+
+-- This test is the same as previous, but with named UNIQUE
+-- constraint.
+--
+test:do_catchsql_test(
+        "index7-8.6",
+        [[
+            CREATE TABLE test8(a,b,c,d, CONSTRAINT c1 UNIQUE(a), PRIMARY KEY(a));
+            SELECT "_index"."name", "_index"."iid"
+            FROM "_index" JOIN "_space" WHERE
+                "_index"."id" = "_space"."id" AND
+                "_space"."name"='TEST8';
+        ]],
+        {0, {"sql_autoindex_TEST8_2",0,"unique_constraint_C1",1}})
+
 test:finish_test()
diff --git a/test/sql-tap/intpkey.test.lua b/test/sql-tap/intpkey.test.lua
index ecff3c0ff..b5359b695 100755
--- a/test/sql-tap/intpkey.test.lua
+++ b/test/sql-tap/intpkey.test.lua
@@ -42,7 +42,7 @@ test:do_execsql_test(
         SELECT "_index"."name" FROM "_index" JOIN "_space" WHERE "_index"."id" = "_space"."id" AND "_space"."name"='T1'
     ]], {
         -- <intpkey-1.1>
-        "sqlite_autoindex_T1_1"
+        "sql_autoindex_T1_1"
         -- </intpkey-1.1>
     })
 
@@ -96,7 +96,7 @@ test:do_catchsql_test(
         INSERT INTO t1 VALUES(5,'second','entry');
     ]], {
         -- <intpkey-1.6>
-        1, "Duplicate key exists in unique index 'sqlite_autoindex_T1_1' in space 'T1'"
+        1, "Duplicate key exists in unique index 'sql_autoindex_T1_1' in space 'T1'"
         -- </intpkey-1.6>
     })
 
diff --git a/test/sql-tap/misc1.test.lua b/test/sql-tap/misc1.test.lua
index 5e1dfd1e6..a5078b5f5 100755
--- a/test/sql-tap/misc1.test.lua
+++ b/test/sql-tap/misc1.test.lua
@@ -380,7 +380,7 @@ test:do_catchsql_test(
         INSERT INTO t5 VALUES(1,2,4);
     ]], {
         -- <misc1-7.4>
-        1, "Duplicate key exists in unique index 'sqlite_autoindex_T5_1' in space 'T5'"
+        1, "Duplicate key exists in unique index 'sql_autoindex_T5_1' in space 'T5'"
         -- </misc1-7.4>
     })
 
diff --git a/test/sql-tap/unique.test.lua b/test/sql-tap/unique.test.lua
index 86d119751..63b50657b 100755
--- a/test/sql-tap/unique.test.lua
+++ b/test/sql-tap/unique.test.lua
@@ -70,7 +70,7 @@ test:do_catchsql_test(
         INSERT INTO t1(a,b,c) VALUES(1,3,4)
     ]], {
         -- <unique-1.3>
-        1, "Duplicate key exists in unique index 'sqlite_autoindex_T1_2' in space 'T1'"
+        1, "Duplicate key exists in unique index 'sql_autoindex_T1_2' in space 'T1'"
         -- </unique-1.3>
     })
 
@@ -91,7 +91,7 @@ test:do_catchsql_test(
         INSERT INTO t1(a,b,c) VALUES(3,2,4)
     ]], {
         -- <unique-1.5>
-        1, "Duplicate key exists in unique index 'sqlite_autoindex_T1_1' in space 'T1'"
+        1, "Duplicate key exists in unique index 'sql_autoindex_T1_1' in space 'T1'"
         -- </unique-1.5>
     })
 
@@ -287,7 +287,7 @@ test:do_catchsql_test(
         SELECT a,b,c,d FROM t3 ORDER BY a,b,c,d;
     ]], {
         -- <unique-3.4>
-        1, "Duplicate key exists in unique index 'sqlite_autoindex_T3_2' in space 'T3'"
+        1, "Duplicate key exists in unique index 'sql_autoindex_T3_2' in space 'T3'"
         -- </unique-3.4>
     })
 
@@ -444,7 +444,7 @@ test:do_catchsql_test(
         INSERT INTO t5 VALUES(2, 1,2,3,4,5,6);
     ]], {
         -- <unique-5.2>
-        1, "Duplicate key exists in unique index 'sqlite_autoindex_T5_2' in space 'T5'"
+        1, "Duplicate key exists in unique index 'sql_autoindex_T5_2' in space 'T5'"
         -- </unique-5.2>
     })
 
diff --git a/test/sql-tap/update.test.lua b/test/sql-tap/update.test.lua
index a4efe4015..1ed951da1 100755
--- a/test/sql-tap/update.test.lua
+++ b/test/sql-tap/update.test.lua
@@ -917,7 +917,7 @@ test:do_catchsql_test("update-10.3", [[
   SELECT * FROM t1;
 ]], {
   -- <update-10.3>
-  1, "Duplicate key exists in unique index 'sqlite_autoindex_T1_3' in space 'T1'"
+  1, "Duplicate key exists in unique index 'sql_autoindex_T1_3' in space 'T1'"
   -- </update-10.3>
 })
 
@@ -943,7 +943,7 @@ test:do_catchsql_test("update-10.6", [[
   SELECT * FROM t1;
 ]], {
   -- <update-10.6>
-  1, "Duplicate key exists in unique index 'sqlite_autoindex_T1_1' in space 'T1'"
+  1, "Duplicate key exists in unique index 'sql_autoindex_T1_1' in space 'T1'"
   -- </update-10.6>
 })
 
@@ -969,7 +969,7 @@ test:do_catchsql_test("update-10.9", [[
   SELECT * FROM t1;
 ]], {
   -- <update-10.9>
-  1, "Duplicate key exists in unique index 'sqlite_autoindex_T1_2' in space 'T1'"
+  1, "Duplicate key exists in unique index 'sql_autoindex_T1_2' in space 'T1'"
   -- </update-10.9>
 })
 
diff --git a/test/sql-tap/view.test.lua b/test/sql-tap/view.test.lua
index ac1a27d45..7818fcfe6 100755
--- a/test/sql-tap/view.test.lua
+++ b/test/sql-tap/view.test.lua
@@ -388,7 +388,7 @@ test:do_catchsql_test(
         CREATE INDEX i1v1 ON v1(xyz);
     ]], {
         -- <view-4.5>
-        1, "views may not be indexed"
+        1, "views can not be indexed"
         -- </view-4.5>
     })
 
diff --git a/test/sql/insert-unique.result b/test/sql/insert-unique.result
index 048d6284c..359ac4346 100644
--- a/test/sql/insert-unique.result
+++ b/test/sql/insert-unique.result
@@ -24,8 +24,7 @@ box.sql.execute("INSERT INTO zoobar VALUES (111, 222, 'c3', 444)")
 -- PK must be unique
 box.sql.execute("INSERT INTO zoobar VALUES (112, 222, 'c3', 444)")
 ---
-- error: Duplicate key exists in unique index 'sqlite_autoindex_ZOOBAR_1' in space
-    'ZOOBAR'
+- error: Duplicate key exists in unique index 'sql_autoindex_ZOOBAR_1' in space 'ZOOBAR'
 ...
 -- Unique index must be respected
 box.sql.execute("INSERT INTO zoobar VALUES (111, 223, 'c3', 444)")
diff --git a/test/sql/iproto.result b/test/sql/iproto.result
index 4cf790169..26ad17b3a 100644
--- a/test/sql/iproto.result
+++ b/test/sql/iproto.result
@@ -553,7 +553,7 @@ future1:wait_result()
 future2:wait_result()
 ---
 - null
-- 'Failed to execute SQL statement: Duplicate key exists in unique index ''sqlite_autoindex_TEST_1''
+- 'Failed to execute SQL statement: Duplicate key exists in unique index ''sql_autoindex_TEST_1''
   in space ''TEST'''
 ...
 future3:wait_result()
diff --git a/test/sql/message-func-indexes.result b/test/sql/message-func-indexes.result
index 37ed4ec6e..5928a8ea8 100644
--- a/test/sql/message-func-indexes.result
+++ b/test/sql/message-func-indexes.result
@@ -18,25 +18,25 @@ box.sql.execute("CREATE TABLE t2(object INTEGER PRIMARY KEY, price INTEGER, coun
 -- should return certain message.
 box.sql.execute("CREATE INDEX i1 ON t1(a+1)")
 ---
-- error: functional indexes aren't supported in the current version
+- error: Tarantool does not support functional indexes
 ...
 box.sql.execute("CREATE INDEX i2 ON t1(a)")
 ---
 ...
 box.sql.execute("CREATE INDEX i3 ON t2(price + 100)")
 ---
-- error: functional indexes aren't supported in the current version
+- error: Tarantool does not support functional indexes
 ...
 box.sql.execute("CREATE INDEX i4 ON t2(price)")
 ---
 ...
 box.sql.execute("CREATE INDEX i5 ON t2(count + 1)")
 ---
-- error: functional indexes aren't supported in the current version
+- error: Tarantool does not support functional indexes
 ...
 box.sql.execute("CREATE INDEX i6 ON t2(count * price)")
 ---
-- error: functional indexes aren't supported in the current version
+- error: Tarantool does not support functional indexes
 ...
 -- Cleaning up.
 box.sql.execute("DROP TABLE t1")
diff --git a/test/sql/on-conflict.result b/test/sql/on-conflict.result
index c0d0de08d..4080648aa 100644
--- a/test/sql/on-conflict.result
+++ b/test/sql/on-conflict.result
@@ -23,7 +23,7 @@ box.sql.execute("CREATE TABLE e (id INTEGER PRIMARY KEY ON CONFLICT REPLACE, v I
 -- Insert values and select them
 box.sql.execute("INSERT INTO t values (1, 1), (2, 2), (3, 1)")
 ---
-- error: Duplicate key exists in unique index 'sqlite_autoindex_T_1' in space 'T'
+- error: Duplicate key exists in unique index 'sql_autoindex_T_1' in space 'T'
 ...
 box.sql.execute("SELECT * FROM t")
 ---
diff --git a/test/sql/persistency.result b/test/sql/persistency.result
index e3b4662de..f64e666cf 100644
--- a/test/sql/persistency.result
+++ b/test/sql/persistency.result
@@ -26,8 +26,7 @@ box.sql.execute("INSERT INTO foobar VALUES (1000, 'foobar')")
 ...
 box.sql.execute("INSERT INTO foobar VALUES (1, 'duplicate')")
 ---
-- error: Duplicate key exists in unique index 'sqlite_autoindex_FOOBAR_1' in space
-    'FOOBAR'
+- error: Duplicate key exists in unique index 'sql_autoindex_FOOBAR_1' in space 'FOOBAR'
 ...
 -- simple select
 box.sql.execute("SELECT bar, foo, 42, 'awesome' FROM foobar")
@@ -209,8 +208,7 @@ box.sql.execute("SELECT \"name\", \"opts\" FROM \"_trigger\"")
 -- prove barfoo2 still exists
 box.sql.execute("INSERT INTO barfoo VALUES ('xfoo', 1)")
 ---
-- error: Duplicate key exists in unique index 'sqlite_autoindex_BARFOO_1' in space
-    'BARFOO'
+- error: Duplicate key exists in unique index 'sql_autoindex_BARFOO_1' in space 'BARFOO'
 ...
 box.sql.execute("SELECT * FROM barfoo")
 ---
diff --git a/test/sql/transition.result b/test/sql/transition.result
index 70fb2db40..765b0f0bf 100644
--- a/test/sql/transition.result
+++ b/test/sql/transition.result
@@ -23,8 +23,7 @@ box.sql.execute("INSERT INTO foobar VALUES (1000, 'foobar')")
 ...
 box.sql.execute("INSERT INTO foobar VALUES (1, 'duplicate')")
 ---
-- error: Duplicate key exists in unique index 'sqlite_autoindex_FOOBAR_1' in space
-    'FOOBAR'
+- error: Duplicate key exists in unique index 'sql_autoindex_FOOBAR_1' in space 'FOOBAR'
 ...
 -- simple select
 box.sql.execute("SELECT bar, foo, 42, 'awesome' FROM foobar")
@@ -142,8 +141,7 @@ box.sql.execute("INSERT INTO barfoo VALUES ('foobar', 1000)")
 -- prove barfoo2 was created
 box.sql.execute("INSERT INTO barfoo VALUES ('xfoo', 1)")
 ---
-- error: Duplicate key exists in unique index 'sqlite_autoindex_BARFOO_1' in space
-    'BARFOO'
+- error: Duplicate key exists in unique index 'sql_autoindex_BARFOO_1' in space 'BARFOO'
 ...
 box.sql.execute("SELECT foo, bar FROM barfoo")
 ---
diff --git a/test/sql/view.result b/test/sql/view.result
index 62fbceeef..2e4230429 100644
--- a/test/sql/view.result
+++ b/test/sql/view.result
@@ -19,7 +19,7 @@ box.sql.execute("CREATE VIEW v1 AS SELECT a+b FROM t1;");
 -- View can't have any indexes.
 box.sql.execute("CREATE INDEX i1 on v1(a);");
 ---
-- error: views may not be indexed
+- error: views can not be indexed
 ...
 v1 = box.space.V1;
 ---
-- 
2.14.3 (Apple Git-98)

^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2018-07-17  8:49 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2018-07-17  8:48 [tarantool-patches] Re: [PATCH] sql: add index_def to Index Ivan Koptelov

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox