From 44a055d733b049fc07f7ad510f698d84b278714c Mon Sep 17 00:00:00 2001
From: Mergen Imeev <imeevma@tarantool.org>
Date: Tue, 3 Oct 2023 17:07:32 +0300
Subject: [PATCH] sql: second lookup for column names

This patch add second lookup for column name in the following cases:
- columns in tuple foreign key creation clause;
- columns in field foreign key creation clause;
- columns in primary key creation clause;
- columns in unique constraint creation clause;
- columns in expressions;
- columns in UPDATE TABLE statement;
- columns in INSERT INTO statement;
- columns in CREATE INDEX statement;

Also, second lookup for table name in expressions also now supported.

Part of #4467

NO_DOC=will be added later
NO_CHANGELOG=will be added later
---
 src/box/sql.c                                 |   5 +-
 src/box/sql/build.c                           |   5 +
 src/box/sql/expr.c                            |  21 +-
 src/box/sql/resolve.c                         | 221 ++++++++++--------
 src/box/sql/select.c                          |  13 +-
 src/box/sql/sqlInt.h                          |  27 ++-
 src/box/sql/update.c                          |  37 ++-
 ...67_sql_id_backwards_compatibility_test.lua |  50 ++--
 8 files changed, 229 insertions(+), 150 deletions(-)

diff --git a/src/box/sql.c b/src/box/sql.c
index df74301d2a..7918f8a05b 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -1695,7 +1695,10 @@ sql_fieldno_by_token(const struct space *space, const struct Token *name)
 uint32_t
 sql_fieldno_by_id(const struct space *space, const struct IdList_item *id)
 {
-	return sql_space_fieldno(space, id->zName);
+	uint32_t res = sql_space_fieldno(space, id->zName);
+	if (res != UINT32_MAX || id->legacy_name == NULL)
+		return res;
+	return sql_space_fieldno(space, id->legacy_name);
 }
 
 uint32_t
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index bccd2567af..bd5de51a58 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -2922,6 +2922,10 @@ sql_id_list_append(struct IdList *list, struct Token *name_token)
 	list->a = sqlArrayAllocate(list->a, sizeof(list->a[0]), &list->nId, &i);
 	assert(i >= 0);
 	list->a[i].zName = sql_name_from_token(name_token);
+	if (name_token->z[0] != '"') {
+		list->a[i].legacy_name = sql_legacy_name_new(name_token->z,
+							     name_token->n);
+	}
 	return list;
 }
 
@@ -2933,6 +2937,7 @@ sqlIdListDelete(struct IdList *pList)
 		return;
 	for (i = 0; i < pList->nId; i++) {
 		sql_xfree(pList->a[i].zName);
+		sql_xfree(pList->a[i].legacy_name);
 	}
 	sql_xfree(pList->a);
 	sql_xfree(pList);
diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
index c95b50fa45..64b6fb0c87 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -1628,6 +1628,7 @@ sql_expr_list_dup(struct ExprList *p, int flags)
 		}
 		pItem->zName = sql_xstrdup(pOldItem->zName);
 		pItem->zSpan = sql_xstrdup(pOldItem->zSpan);
+		pItem->legacy_name = sql_xstrdup(pOldItem->legacy_name);
 		pItem->sort_order = pOldItem->sort_order;
 		pItem->done = 0;
 		pItem->bSpanIsTab = pOldItem->bSpanIsTab;
@@ -1702,6 +1703,7 @@ sqlIdListDup(struct IdList *p)
 		struct IdList_item *pNewItem = &pNew->a[i];
 		struct IdList_item *pOldItem = &p->a[i];
 		pNewItem->zName = sql_xstrdup(pOldItem->zName);
+		pNewItem->legacy_name = sql_xstrdup(pOldItem->legacy_name);
 		pNewItem->idx = pOldItem->idx;
 	}
 	return pNew;
@@ -1808,7 +1810,10 @@ sqlExprListAppendVector(Parse * pParse,	/* Parsing context */
 		pList = sql_expr_list_append(pList, pSubExpr);
 		assert(pList->nExpr == iFirst + i + 1);
 		pList->a[pList->nExpr - 1].zName = pColumns->a[i].zName;
+		pList->a[pList->nExpr - 1].legacy_name =
+			pColumns->a[i].legacy_name;
 		pColumns->a[i].zName = 0;
+		pColumns->a[i].legacy_name = NULL;
 	}
 
 	if (pExpr->op == TK_SELECT) {
@@ -1870,6 +1875,7 @@ sqlExprListSetName(Parse * pParse,	/* Parsing context */
 	assert(item->zName == NULL);
 	if (dequote) {
 		item->zName = sql_name_new(pName->z, pName->n);
+		item->legacy_name = sql_legacy_name_new(pName->z, pName->n);
 	} else {
 		item->zName = sql_xstrndup(pName->z, pName->n);
 	}
@@ -1900,6 +1906,7 @@ exprListDeleteNN(struct ExprList *pList)
 		sql_expr_delete(pItem->pExpr);
 		sql_xfree(pItem->zName);
 		sql_xfree(pItem->zSpan);
+		sql_xfree(pItem->legacy_name);
 	}
 	sql_xfree(pList->a);
 	sql_xfree(pList);
@@ -5439,13 +5446,23 @@ uint32_t
 sql_fieldno_by_expr(const struct space *space, const struct Expr *expr)
 {
 	assert(expr->op == TK_ID);
-	return sql_space_fieldno(space, expr->u.zToken);
+	uint32_t res = sql_space_fieldno(space, expr->u.zToken);
+	if (res != UINT32_MAX || (expr->flags & EP_Lookup2) == 0)
+		return res;
+	char *old_name_str = sql_legacy_name_new0(expr->u.zToken);
+	res = sql_space_fieldno(space, old_name_str);
+	sql_xfree(old_name_str);
+	return res;
+
 }
 
 uint32_t
 sql_fieldno_by_item(const struct space *space, const struct ExprList_item *item)
 {
-	return sql_space_fieldno(space, item->zName);
+	uint32_t res = sql_space_fieldno(space, item->zName);
+	if (res != UINT32_MAX || item->legacy_name == NULL)
+		return res;
+	return sql_space_fieldno(space, item->legacy_name);
 }
 
 uint32_t
diff --git a/src/box/sql/resolve.c b/src/box/sql/resolve.c
index 1f62b958f6..6c7e5e5826 100644
--- a/src/box/sql/resolve.c
+++ b/src/box/sql/resolve.c
@@ -130,48 +130,67 @@ resolveAlias(struct ExprList *pEList, int iCol, struct Expr *pExpr,
 	sql_xfree(pDup);
 }
 
-/*
- * Return TRUE if the name zCol occurs anywhere in the USING clause.
- *
- * Return FALSE if the USING clause is NULL or if it does not contain
- * zCol.
+/**
+ * Return TRUE if the name occurs anywhere in the USING clause.
+ * Return FALSE if the USING clause is NULL or if it does not contain the name.
  */
-static int
-nameInUsingClause(IdList * pUsing, const char *zCol)
+static bool
+nameInUsingClause(struct IdList *pUsing, const char *zCol, const char *old_col)
 {
 	if (pUsing) {
-		int k;
-		for (k = 0; k < pUsing->nId; k++) {
+		for (int k = 0; k < pUsing->nId; k++) {
 			if (strcmp(pUsing->a[k].zName, zCol) == 0)
-				return 1;
+				return true;
+		}
+		for (int i = 0; i < pUsing->nId; i++) {
+			if (strcmp(pUsing->a[i].zName, old_col) == 0)
+				return true;
 		}
 	}
-	return 0;
+	return false;
 }
 
-/*
- * Subqueries stores the original database, table and column names for their
- * result sets in ExprList.a[].zSpan, in the form "DATABASE.TABLE.COLUMN".
- * Check to see if the zSpan given to this routine matches the zTab,
- * and zCol.  If any of zTab, and zCol are NULL then those fields will
- * match anything.
- */
-int
-sqlMatchSpanName(const char *zSpan,
-		     const char *zCol, const char *zTab
-	)
+bool
+sqlMatchSpanName(const char *span, const char *col, const char *tab,
+		 const char *old_col, const char *old_tab)
 {
-	int n;
-	for (n = 0; ALWAYS(zSpan[n]) && zSpan[n] != '.'; n++) {
+	size_t n;
+	const char *name = span;
+	size_t len = strlen(span);
+	for (n = 0; n < len && span[n] != '.'; n++) {
 	}
-	if (zTab && (sqlStrNICmp(zSpan, zTab, n) != 0 || zTab[n] != 0)) {
-		return 0;
+	if (tab != NULL) {
+		if ((strlen(tab) != n || strncmp(name, tab, n) != 0) &&
+		    (old_tab == NULL || strlen(old_tab) != n ||
+		     strncmp(name, old_tab, n) != 0))
+			return false;
+		name += n + 1;
+		len -= n + 1;
 	}
-	zSpan += n + 1;
-	if (zCol && strcmp(zSpan, zCol) != 0) {
-		return 0;
+	if (col == NULL)
+		return true;
+	if ((strlen(col) != len || strncmp(name, col, len) != 0) &&
+	    (old_col == NULL || strlen(old_col) != len ||
+	     strncmp(name, old_col, len) != 0))
+		return false;
+	return true;
+}
+
+/**
+ * Match spans for all expressions of the list with the table name and the
+ * column name.
+ */
+static int
+sql_find_column_expr(const struct ExprList *list, const char *tab_name,
+		     const char *col_name, const char *old_tab,
+		     const char *old_col)
+{
+	for (int j = 0; j < list->nExpr; j++) {
+		if (sqlMatchSpanName(list->a[j].zSpan, col_name, tab_name,
+				     old_tab, old_col))
+			return j;
 	}
-	return 1;
+	return -1;
 }
 
 /*
@@ -199,13 +218,30 @@ sqlMatchSpanName(const char *zSpan,
  * in pParse and return WRC_Abort.  Return WRC_Prune on success.
  */
 static int
-lookupName(Parse * pParse,	/* The parsing context */
-	   const char *zTab,	/* Name of table containing column, or NULL */
-	   const char *zCol,	/* Name of the column. */
-	   NameContext * pNC,	/* The name context used to resolve the name */
-	   Expr * pExpr		/* Make this EXPR node point to the selected column */
-    )
+lookupName(struct Parse *pParse, struct Expr *pExpr, struct NameContext *pNC)
 {
+	const char *zTab = NULL;
+	const char *zCol = NULL;
+	char *old_tab = NULL;
+	char *old_col = NULL;
+	const struct Expr *col = NULL;
+	if (pExpr->op == TK_DOT) {
+		assert(pExpr->pLeft->op == TK_ID && pExpr->pRight->op == TK_ID);
+		zTab = pExpr->pLeft->u.zToken;
+		if ((pExpr->pLeft->flags & EP_Lookup2) != 0)
+			old_tab = sql_legacy_name_new0(zTab);
+		zCol = pExpr->pRight->u.zToken;
+		if ((pExpr->pRight->flags & EP_Lookup2) != 0)
+			old_col = sql_legacy_name_new0(zCol);
+		col = pExpr->pRight;
+	} else {
+		assert(pExpr->op == TK_ID);
+		zCol = pExpr->u.zToken;
+		if ((pExpr->flags & EP_Lookup2) != 0)
+			old_col = sql_legacy_name_new0(zCol);
+		col = pExpr;
+	}
+
 	int i, j;		/* Loop counters */
 	int cnt = 0;		/* Number of matching column names */
 	int cntTab = 0;		/* Number of matching table names */
@@ -241,16 +277,14 @@ lookupName(Parse * pParse,	/* The parsing context */
 					selFlags & SF_NestedFrom) != 0) {
 					int hit = 0;
 					pEList = pItem->pSelect->pEList;
-					for (j = 0; j < pEList->nExpr; j++) {
-						if (sqlMatchSpanName
-						    (pEList->a[j].zSpan, zCol,
-						     zTab)) {
-							cnt++;
-							cntTab = 2;
-							pMatch = pItem;
-							pExpr->iColumn = j;
-							hit = 1;
-						}
+					j = sql_find_column_expr(pEList, zTab,
+								 zCol, old_tab,
+								 old_col);
+					if (j >= 0) {
+						cnt++;
+						cntTab = 2;
+						pMatch = pItem;
+						pExpr->iColumn = j;
 					}
 					if (hit || zTab == 0)
 						continue;
@@ -260,37 +294,33 @@ lookupName(Parse * pParse,	/* The parsing context */
 					    pItem->zAlias ? pItem->
 					    zAlias : space_def->name;
 					assert(zTabName != 0);
-					if (strcmp(zTabName, zTab) != 0) {
+					if (strcmp(zTabName, zTab) != 0 &&
+					    (old_tab == NULL ||
+					     strcmp(zTabName, old_tab) != 0))
 						continue;
-					}
 				}
 				if (0 == (cntTab++)) {
 					pMatch = pItem;
 				}
-				for (j = 0; j < (int)space_def->field_count;
-				     j++) {
-					if (strcmp(space_def->fields[j].name,
-						   zCol) == 0) {
-						/* If there has been exactly one prior match and this match
-						 * is for the right-hand table of a NATURAL JOIN or is in a
-						 * USING clause, then skip this match.
-						 */
-						if (cnt == 1) {
-							if (pItem->fg.
-							    jointype &
-							    JT_NATURAL)
-								continue;
-							if (nameInUsingClause
-							    (pItem->pUsing,
-							     zCol))
-								continue;
-						}
-						cnt++;
-						pMatch = pItem;
-						pExpr->iColumn = (i16) j;
-						break;
-					}
-				}
+				uint32_t fieldno =
+					sql_fieldno_by_expr(pItem->space, col);
+				if (fieldno == UINT32_MAX)
+					continue;
+				j = fieldno;
+				/*
+				 * If there has been exactly one prior match and
+				 * this match is for the right-hand table of a
+				 * NATURAL JOIN or is in a USING clause, then
+				 * skip this match.
+				 */
+				if (cnt == 1 &&
+				    ((pItem->fg.jointype & JT_NATURAL) != 0 ||
+				     nameInUsingClause(pItem->pUsing, zCol,
+						       old_col)))
+					continue;
+				cnt++;
+				pMatch = pItem;
+				pExpr->iColumn = (i16) j;
 			}
 			if (pMatch) {
 				pExpr->iTable = pMatch->iCursor;
@@ -326,10 +356,12 @@ lookupName(Parse * pParse,	/* The parsing context */
 				cntTab++;
 				for (iCol = 0; iCol <
 				     (int)space_def->field_count; iCol++) {
-					if (strcmp(space_def->fields[iCol].name,
-						   zCol) == 0) {
+					const char *name =
+						space_def->fields[iCol].name;
+					if (strcmp(name, zCol) == 0 ||
+					    (old_col != NULL &&
+					     strcmp(name, old_col) == 0))
 						break;
-					}
 				}
 				if (iCol < (int)space_def->field_count) {
 					cnt++;
@@ -364,7 +396,9 @@ lookupName(Parse * pParse,	/* The parsing context */
 		if ((pEList = pNC->pEList) != 0 && zTab == 0 && cnt == 0) {
 			for (j = 0; j < pEList->nExpr; j++) {
 				char *zAs = pEList->a[j].zName;
-				if (zAs != 0 && strcmp(zAs, zCol) == 0) {
+				if (zAs != 0 && (strcmp(zAs, zCol) == 0 ||
+						 (old_col != NULL &&
+						  strcmp(zAs, old_col) == 0))) {
 					Expr *pOrig;
 					assert(pExpr->pLeft == 0
 					       && pExpr->pRight == 0);
@@ -379,14 +413,14 @@ lookupName(Parse * pParse,	/* The parsing context */
 							 ER_SQL_PARSER_GENERIC,
 							 tt_sprintf(err, zAs));
 						pParse->is_aborted = true;
-						return WRC_Abort;
+						goto lookupname_end;
 					}
 					if (sqlExprVectorSize(pOrig) != 1) {
 						diag_set(ClientError,
 							 ER_SQL_PARSER_GENERIC,
 							 "row value misused");
 						pParse->is_aborted = true;
-						return WRC_Abort;
+						goto lookupname_end;
 					}
 					resolveAlias(pEList, j, pExpr, "",
 						     nSubquery);
@@ -457,7 +491,9 @@ lookupName(Parse * pParse,	/* The parsing context */
 	pExpr->pRight = 0;
 	pExpr->op = (isTrigger ? TK_TRIGGER : TK_COLUMN_REF);
  lookupname_end:
-	if (cnt == 1) {
+	sql_xfree(old_tab);
+	sql_xfree(old_col);
+	if (cnt == 1 && !pParse->is_aborted) {
 		assert(pNC != 0);
 		/* Increment the nRef value on all name contexts from TopNC up to
 		 * the point where the name matched.
@@ -549,31 +585,12 @@ resolveExprStep(Walker * pWalker, Expr * pExpr)
 	case TK_ID:{
 			if ((pNC->ncFlags & NC_AllowAgg) != 0)
 				pNC->ncFlags |= NC_HasUnaggregatedId;
-			return lookupName(pParse, 0, pExpr->u.zToken, pNC,
-					  pExpr);
+			return lookupName(pParse, pExpr, pNC);
 		}
 
-		/* A table name and column name:     ID.ID
-		 * Or a database, table and column:  ID.ID.ID
-		 */
-	case TK_DOT:{
-			const char *zColumn;
-			const char *zTable;
-			Expr *pRight;
-
-			/* if( pSrcList==0 ) break; */
-			pRight = pExpr->pRight;
-			if (pRight->op == TK_ID) {
-				zTable = pExpr->pLeft->u.zToken;
-				zColumn = pRight->u.zToken;
-			} else {
-				assert(pRight->op == TK_DOT);
-				zTable = pRight->pLeft->u.zToken;
-				zColumn = pRight->pRight->u.zToken;
-			}
-			return lookupName(pParse, zTable, zColumn, pNC,
-					  pExpr);
-		}
+	/* A table name and column name: ID.ID. */
+	case TK_DOT:
+		return lookupName(pParse, pExpr, pNC);
 
 		/* Resolve function names
 		 */
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index 65c4a4eaa9..d2e339f6d7 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -4260,10 +4260,14 @@ flattenSubquery(Parse * pParse,		/* Parsing context */
 		pList = pParent->pEList;
 		for (i = 0; i < pList->nExpr; i++) {
 			if (pList->a[i].zName == 0) {
-				char *str = pList->a[i].zSpan;
-				int len = strlen(str);
+				const char *str = pList->a[i].zSpan;
+				size_t len = strlen(str);
 				char *name = sql_name_new(str, len);
 				pList->a[i].zName = name;
+				if (pList->a[i].zSpan[0] != '"') {
+					pList->a[i].legacy_name =
+						sql_legacy_name_new(str, len);
+				}
 			}
 		}
 		if (pSub->pOrderBy) {
@@ -5004,9 +5008,11 @@ selectExpander(Walker * pWalker, Select * p)
 			pNew = sql_expr_list_append(pNew, a[k].pExpr);
 			pNew->a[pNew->nExpr - 1].zName = a[k].zName;
 			pNew->a[pNew->nExpr - 1].zSpan = a[k].zSpan;
+			pNew->a[pNew->nExpr - 1].legacy_name = a[k].legacy_name;
 			a[k].zName = 0;
 			a[k].zSpan = 0;
 			a[k].pExpr = 0;
+			a[k].legacy_name = NULL;
 			continue;
 		}
 		/*
@@ -5047,7 +5053,8 @@ selectExpander(Walker * pWalker, Select * p)
 				assert(zName != NULL);
 				if (zTName != NULL && pSub != NULL &&
 				    sqlMatchSpanName(pSub->pEList->a[j].zSpan,
-						     0, zTName) == 0)
+						     NULL, zTName, NULL,
+						     NULL) == 0)
 					continue;
 				tableSeen = 1;
 
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 62a44e1cf6..9d36196e95 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -1495,6 +1495,8 @@ struct ExprList {
 		Expr *pExpr;	/* The list of expressions */
 		char *zName;	/* Token associated with this expression */
 		char *zSpan;	/* Original text of the expression */
+		/** Normalized name for the second lookup. */
+		char *legacy_name;
 		enum sort_order sort_order;
 		unsigned done:1;	/* A flag to indicate when processing is finished */
 		unsigned bSpanIsTab:1;	/* zSpan holds DB.TABLE.COLUMN */
@@ -1538,6 +1540,8 @@ struct ExprSpan {
 struct IdList {
 	struct IdList_item {
 		char *zName;	/* Name of the identifier */
+		/** Normalized name for the second lookup. */
+		char *legacy_name;
 		int idx;	/* Index in some Table.aCol[] of a column named zName */
 	} *a;
 	int nId;		/* Number of identifiers on the list */
@@ -3138,21 +3142,26 @@ sql_fieldno_by_token(const struct space *space, const struct Token *name);
 
 /**
  * Return the fieldno of the field with the name defined by the element of
- * IdList. Return UINT32_MAX if the field was not found.
+ * IdList. A second lookup will be performed if the field is not found on the
+ * first try and field legacy_name of the id is not NULL. Return UINT32_MAX if
+ * the field was not found.
  */
 uint32_t
 sql_fieldno_by_id(const struct space *space, const struct IdList_item *id);
 
 /**
- * Return the fieldno of the field with the name defined by the expression.
- * Return UINT32_MAX if the field was not found.
+ * Return the fieldno of the field with the name defined by the expression. A
+ * second lookup will be performed if the field is not found on the first try
+ * and flag EP_Lookup2 is set. Return UINT32_MAX if the field was not found.
  */
 uint32_t
 sql_fieldno_by_expr(const struct space *space, const struct Expr *expr);
 
 /**
  * Return the fieldno of the field with the name defined by the name of
- * expression from exprList. Return UINT32_MAX if the field was not found.
+ * expression from exprList. A second lookup will be performed if the field is
+ * not found on the first try and field legacy_name of element of ExprList is
+ * not NULL. Return UINT32_MAX if the field was not found.
  */
 uint32_t
 sql_fieldno_by_item(const struct space *space, const struct ExprList_item *it);
@@ -4077,7 +4086,15 @@ void sqlSelectPrep(Parse *, Select *, NameContext *);
 const char *
 sql_select_op_name(int id);
 
-int sqlMatchSpanName(const char *, const char *, const char *);
+/**
+ * Subqueries stores the original table and column names for their result sets
+ * in ExprList.a[].zSpan, in the form "TABLE.COLUMN". Check to see if the span
+ * given to this routine matches given table or column names.
+ */
+bool
+sqlMatchSpanName(const char *span, const char *col, const char *tab,
+		 const char *old_col, const char *old_tab);
+
 int sqlResolveExprNames(NameContext *, Expr *);
 int sqlResolveExprListNames(NameContext *, ExprList *);
 void sqlResolveSelectNames(Parse *, Select *, NameContext *);
diff --git a/src/box/sql/update.c b/src/box/sql/update.c
index 33306aa54e..b91e1f5b51 100644
--- a/src/box/sql/update.c
+++ b/src/box/sql/update.c
@@ -134,35 +134,24 @@ sqlUpdate(Parse * pParse,		/* The parser context */
 		if (sqlResolveExprNames(&sNC, pChanges->a[i].pExpr)) {
 			goto update_cleanup;
 		}
-		for (j = 0; j < (int)def->field_count; j++) {
-			if (strcmp(def->fields[j].name,
-				   pChanges->a[i].zName) == 0) {
-				if (pPk &&
-				    sql_space_column_is_in_pk(space, j))
-					is_pk_modified = true;
-				if (aXRef[j] != -1) {
-					const char *err =
-						"set id list: duplicate "\
-						"column name %s";
-					err = tt_sprintf(err,
-							 pChanges->a[i].zName);
-					diag_set(ClientError,
-						 ER_SQL_PARSER_GENERIC,
-						 err);
-					pParse->is_aborted = true;
-					goto update_cleanup;
-				}
-				aXRef[j] = i;
-				upd_cols_cnt++;
-				break;
-			}
-		}
-		if (j >= (int)def->field_count) {
+		uint32_t fieldno = sql_fieldno_by_item(space, &pChanges->a[i]);
+		if (fieldno == UINT32_MAX) {
 			diag_set(ClientError, ER_NO_SUCH_FIELD_NAME_IN_SPACE,
 				 pChanges->a[i].zName, def->name);
 			pParse->is_aborted = true;
 			goto update_cleanup;
 		}
+		if (pPk != NULL && sql_space_column_is_in_pk(space, fieldno))
+			is_pk_modified = true;
+		if (aXRef[fieldno] != -1) {
+			diag_set(ClientError, ER_SQL_PARSER_GENERIC,
+				 tt_sprintf("set id list: duplicate column "
+					    "name %s", pChanges->a[i].zName));
+			pParse->is_aborted = true;
+			goto update_cleanup;
+		}
+		aXRef[fieldno] = i;
+		upd_cols_cnt++;
 	}
 	/*
 	 * The SET expressions are not actually used inside the
diff --git a/test/sql-luatest/gh_4467_sql_id_backwards_compatibility_test.lua b/test/sql-luatest/gh_4467_sql_id_backwards_compatibility_test.lua
index 9ff8d22d21..60ee8b1db6 100644
--- a/test/sql-luatest/gh_4467_sql_id_backwards_compatibility_test.lua
+++ b/test/sql-luatest/gh_4467_sql_id_backwards_compatibility_test.lua
@@ -113,11 +113,12 @@ g.test_create_tuple_foreign_key = function()
         t.assert(box.space.tab.foreign_key.one ~= nil);
         box.space.tab:drop()
 
-        -- Make sure table name is looked up twice in tuple FK creation clause.
+        -- Make sure table name and column names are looked up twice in tuple FK
+        -- creation clause.
         box.execute([[CREATE TABLE ASD(QWE INT PRIMARY KEY);]])
         local sql = [[CREATE TABLE ASD1(QWE1 INT PRIMARY KEY, ZXC1 INT,
-                      CONSTRAINT f1 FOREIGN KEY (ZXC1) REFERENCES Asd(QWE),
-                      CONSTRAINT f2 FOREIGN KEY (ZXC1) REFERENCES asD1(QWE1));]]
+                      CONSTRAINT f1 FOREIGN KEY (zXC1) REFERENCES Asd(QwE),
+                      CONSTRAINT f2 FOREIGN KEY (ZXc1) REFERENCES asD1(qwe1));]]
         t.assert_equals(box.execute(sql), {row_count = 1})
         local foreign_key = box.space.ASD1.foreign_key
         t.assert_equals(foreign_key.f1.space, box.space.ASD.id)
@@ -136,11 +137,12 @@ g.test_create_field_foreign_key = function()
         t.assert(box.space.tab:format()[2].foreign_key.one ~= nil);
         box.space.tab:drop()
 
-        -- Make sure table name is looked up twice in field FK creation clause.
+        -- Make sure table name and column names are looked up twice in field FK
+        -- creation clause.
         box.execute([[CREATE TABLE ASD(QWE INT PRIMARY KEY);]])
         local sql = [[CREATE TABLE ASD1(QWE1 INT PRIMARY KEY, ZXC1 INT
-                      CONSTRAINT f1 REFERENCES Asd(QWE)
-                      CONSTRAINT f2 REFERENCES asD1(QWE1));]]
+                      CONSTRAINT f1 REFERENCES Asd(qwE)
+                      CONSTRAINT f2 REFERENCES asD1(Qwe1));]]
         t.assert_equals(box.execute(sql), {row_count = 1})
         local foreign_key = box.space.ASD1:format()[2].foreign_key
         t.assert_equals(foreign_key.f1.space, box.space.ASD.id)
@@ -164,6 +166,14 @@ g.test_expression = function()
         t.assert_equals(box.execute(sql).rows, {{124}})
         box.space.Tab:drop()
         box.func.fUn:drop()
+
+        -- Make sure table names and column names are looked up twice in
+        -- expressions.
+        box.execute([[CREATE TABLE ASD(QWE INT PRIMARY KEY, ZXC INT);]])
+        box.space.ASD:insert({12, 21})
+        local rows = box.execute([[SELECT AsD.qWe * 2 - zxc FROM asd;]]).rows
+        t.assert_equals(rows, {{3}})
+        box.space.ASD:drop()
     end)
 end
 
@@ -174,6 +184,12 @@ g.test_create_primary_key = function()
                       CONSTRAINT Three PRIMARY KEY (Second, First));]])
         t.assert_equals(box.space.Tab.index[0].name, 'Three');
         box.space.Tab:drop()
+
+        -- Make sure column names in primary key definition are looked up twice.
+        box.execute([[CREATE TABLE ASD(QWE INT, ZXC INT,
+                      CONSTRAINT THREE PRIMARY KEY (Qwe, zxc));]])
+        t.assert_equals(box.space.ASD.index[0].name, 'THREE');
+        box.space.ASD:drop()
     end)
 end
 
@@ -185,6 +201,13 @@ g.test_create_unique = function()
                       CONSTRAINT Four UNIQUE (Second, First));]])
         t.assert_equals(box.space.Tab.index[1].name, 'Four');
         box.space.Tab:drop()
+
+        -- Make sure column names in unique constraint definition are looked up
+        -- twice.
+        box.execute([[CREATE TABLE ASD(QWE INT PRIMARY KEY, ZXC INT,
+                      CONSTRAINT FOUR UNIQUE (Qwe, zxc));]])
+        t.assert_equals(box.space.ASD.index[1].name, 'FOUR');
+        box.space.ASD:drop()
     end)
 end
 
@@ -302,13 +325,13 @@ g.test_update = function()
         t.assert_equals(box.space.Tab:select(), {{3, 11, -1}})
         box.space.Tab:drop()
 
-        -- Make sure table name is looked up twice in UPDATE.
+        -- Make sure table name and column names are looked up twice in UPDATE.
         box.execute([[CREATE TABLE ASD(PK INT PRIMARY KEY, QWE INT, ZXC INT);]])
         box.space.ASD:insert({3, 5, 7})
-        sql = [[UPDATE aSd SET QWE = 1, ZXC = 9;]]
+        sql = [[UPDATE aSd SET qWE = 1, ZXc = 9;]]
         t.assert_equals(box.execute(sql), {row_count = 1});
         t.assert_equals(box.space.ASD:select(), {{3, 1, 9}})
-        sql = [[UPDATE asd SET (QWE, ZXC) = (11, -1);]]
+        sql = [[UPDATE asd SET (qwE, Zxc) = (11, -1);]]
         t.assert_equals(box.execute(sql), {row_count = 1});
         t.assert_equals(box.space.ASD:select(), {{3, 11, -1}})
         box.space.ASD:drop()
@@ -323,9 +346,9 @@ g.test_insert = function()
         t.assert_equals(box.space.Tab:select(), {{123, nil, 321}})
         box.space.Tab:drop()
 
-        -- Make sure table name is looked up twice in INSERT.
+        -- Make sure table name and column names are looked up twice in INSERT.
         box.execute([[CREATE TABLE ASD(PK INT PRIMARY KEY, QWE INT, ZXC INT);]])
-        box.execute([[INSERT INTO asd(PK, ZXC) VALUES (123, 321);]])
+        box.execute([[INSERT INTO asd(pK, zxc) VALUES (123, 321);]])
         t.assert_equals(box.space.ASD:select(), {{123, nil, 321}})
         box.space.ASD:drop()
     end)
@@ -341,9 +364,10 @@ g.test_create_index = function()
         t.assert(box.space.Tab.index.In1 ~= nil)
         box.space.Tab:drop()
 
-        -- Make sure table name is looked up twice in CREATE INDEX.
+        -- Make sure table name and column names looked up twice in
+        -- CREATE INDEX.
         box.execute([[CREATE TABLE ASD(PK INT PRIMARY KEY, QWE INT, ZXC INT);]])
-        sql = [[CREATE INDEX IND ON Asd(ZXC, QWE);]]
+        sql = [[CREATE INDEX IND ON Asd(zxc, qWe);]]
         t.assert_equals(box.execute(sql), {row_count = 1})
         t.assert(box.space.ASD.index.IND ~= nil)
         box.space.ASD:drop()
-- 
GitLab