From d69aa687a0e9bb26b37d21b45e17374f9aa91908 Mon Sep 17 00:00:00 2001
From: Ilya Verbin <iverbin@tarantool.org>
Date: Thu, 29 Jun 2023 20:31:11 +0300
Subject: [PATCH] box: pin coll_id by space format and by indexes

Pin in cache the collation identifiers that are referenced by space format
and/or indexes, so that they can't be deleted.

Closes #4544

NO_DOC=bugfix
---
 ...h-4544-segfault-if-i-delete-a-collation.md |   3 +
 src/box/alter.cc                              |  22 +-
 src/box/coll_id.c                             |   2 +
 src/box/coll_id.h                             |   6 +
 src/box/coll_id_cache.c                       |  48 ++-
 src/box/coll_id_cache.h                       |  63 +++-
 src/box/errcode.h                             |   2 +-
 src/box/index_def.h                           |   2 +-
 src/box/space.c                               |  52 ++++
 src/box/space.h                               |  18 ++
 src/box/sql/show.c                            |  11 +-
 test/box/net.box_is_nullable_gh-3256.result   |   4 +-
 test/box/net.box_is_nullable_gh-3256.test.lua |   2 +-
 .../gh_4544_collation_drop_test.lua           | 291 ++++++++++++++++++
 test/sql-luatest/show_create_table_test.lua   |   8 -
 test/sql/collation.result                     |   2 +-
 16 files changed, 508 insertions(+), 28 deletions(-)
 create mode 100644 changelogs/unreleased/gh-4544-segfault-if-i-delete-a-collation.md

diff --git a/changelogs/unreleased/gh-4544-segfault-if-i-delete-a-collation.md b/changelogs/unreleased/gh-4544-segfault-if-i-delete-a-collation.md
new file mode 100644
index 0000000000..bd3d6c4207
--- /dev/null
+++ b/changelogs/unreleased/gh-4544-segfault-if-i-delete-a-collation.md
@@ -0,0 +1,3 @@
+## bugfix/core
+
+* Fixed a crash when a collation used by a space was deleted (gh-4544).
diff --git a/src/box/alter.cc b/src/box/alter.cc
index b0ef9ef423..647c25f9d5 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -903,6 +903,7 @@ alter_space_rollback(struct trigger *trigger, void * /* event */)
 	space_swap_triggers(alter->new_space, alter->old_space);
 	space_swap_constraint_ids(alter->new_space, alter->old_space);
 	space_reattach_constraints(alter->old_space);
+	space_pin_collations(alter->old_space);
 	space_cache_replace(alter->new_space, alter->old_space);
 	alter_space_delete(alter);
 	return 0;
@@ -1030,6 +1031,7 @@ alter_space_do(struct txn_stmt *stmt, struct alter_space *alter)
 	 */
 	space_cache_replace(alter->old_space, alter->new_space);
 	space_detach_constraints(alter->old_space);
+	space_unpin_collations(alter->old_space);
 	/*
 	 * Install transaction commit/rollback triggers to either
 	 * finish or rollback the DDL depending on the results of
@@ -1737,6 +1739,7 @@ on_drop_space_rollback(struct trigger *trigger, void *event)
 	struct space *space = (struct space *)trigger->data;
 	space_cache_replace(NULL, space);
 	space_reattach_constraints(space);
+	space_pin_collations(space);
 	return 0;
 }
 
@@ -2239,6 +2242,7 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 		 * on commit or reattached on rollback.
 		 */
 		space_detach_constraints(old_space);
+		space_unpin_collations(old_space);
 		/**
 		 * The space must be deleted from the space
 		 * cache right away to achieve linearisable
@@ -3620,10 +3624,6 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
 			txn_alter_trigger_new(on_drop_collation_rollback, NULL);
 		if (on_commit == NULL || on_rollback == NULL)
 			return -1;
-		/*
-		 * TODO: Check that no index uses the collation
-		 * identifier.
-		 */
 		uint32_t out;
 		if (tuple_field_u32(old_tuple, BOX_COLLATION_FIELD_ID, &out) != 0)
 			return -1;
@@ -3645,6 +3645,20 @@ on_replace_dd_collation(struct trigger * /* trigger */, void *event)
 				 old_coll_id->owner_id, SC_COLLATION,
 				 PRIV_D) != 0)
 			return -1;
+		/*
+		 * Don't allow user to drop a collation identifier that is
+		 * currently used.
+		 */
+		enum coll_id_holder_type pinned_type;
+		if (coll_id_is_pinned(old_coll_id, &pinned_type)) {
+			const char *type_str =
+				coll_id_holder_type_strs[pinned_type];
+			diag_set(ClientError, ER_DROP_COLLATION,
+				 old_coll_id->name,
+				 tt_sprintf("collation is referenced by %s",
+					    type_str));
+			return -1;
+		}
 		/*
 		 * Set on_commit/on_rollback triggers after
 		 * deletion from the cache to make trigger logic
diff --git a/src/box/coll_id.c b/src/box/coll_id.c
index 5abeaed21f..3ea61ddcc8 100644
--- a/src/box/coll_id.c
+++ b/src/box/coll_id.c
@@ -53,12 +53,14 @@ coll_id_new(const struct coll_id_def *def)
 	coll_id->name_len = def->name_len;
 	memcpy(coll_id->name, def->name, def->name_len);
 	coll_id->name[coll_id->name_len] = 0;
+	rlist_create(&coll_id->cache_pin_list);
 	return coll_id;
 }
 
 void
 coll_id_delete(struct coll_id *coll_id)
 {
+	assert(rlist_empty(&coll_id->cache_pin_list));
 	coll_unref(coll_id->coll);
 	free(coll_id);
 }
diff --git a/src/box/coll_id.h b/src/box/coll_id.h
index 82e50d11be..e436150a1a 100644
--- a/src/box/coll_id.h
+++ b/src/box/coll_id.h
@@ -32,6 +32,7 @@
  */
 #include <stddef.h>
 #include <stdint.h>
+#include "small/rlist.h"
 
 #if defined(__cplusplus)
 extern "C" {
@@ -56,6 +57,11 @@ struct coll_id {
 	uint32_t owner_id;
 	/** Collation object. */
 	struct coll *coll;
+	/**
+	 * Holders of the same collation identifier are linked into ring list by
+	 * `coll_id_cache_holder::in_coll_id`.
+	 */
+	struct rlist cache_pin_list;
 	/** Collation name. */
 	size_t name_len;
 	char name[0];
diff --git a/src/box/coll_id_cache.c b/src/box/coll_id_cache.c
index d02d3ea165..8aab57d5f5 100644
--- a/src/box/coll_id_cache.c
+++ b/src/box/coll_id_cache.c
@@ -38,6 +38,11 @@ static struct mh_strnptr_t *coll_cache_name = NULL;
 /** mhash table (id -> collation) */
 static struct mh_i32ptr_t *coll_id_cache = NULL;
 
+const char *coll_id_holder_type_strs[COLL_ID_HOLDER_MAX] = {
+	[COLL_ID_HOLDER_SPACE_FORMAT] = "space format",
+	[COLL_ID_HOLDER_INDEX] = "index",
+};
+
 void
 coll_id_cache_init(void)
 {
@@ -73,8 +78,9 @@ coll_id_cache_replace(struct coll_id *coll_id, struct coll_id **replaced_id)
 }
 
 void
-coll_id_cache_delete(const struct coll_id *coll_id)
+coll_id_cache_delete(struct coll_id *coll_id)
 {
+	assert(rlist_empty(&coll_id->cache_pin_list));
 	mh_int_t id_i = mh_i32ptr_find(coll_id_cache, coll_id->id, NULL);
 	mh_i32ptr_del(coll_id_cache, id_i, NULL);
 	mh_int_t name_i = mh_strnptr_find_str(coll_cache_name, coll_id->name,
@@ -102,3 +108,43 @@ coll_by_name(const char *name, uint32_t len)
 		return NULL;
 	return mh_strnptr_node(coll_cache_name, pos)->val;
 }
+
+void
+coll_id_pin(struct coll_id *coll_id, struct coll_id_cache_holder *holder,
+	    enum coll_id_holder_type type)
+{
+	assert(coll_by_id(coll_id->id) != NULL);
+	holder->coll_id = coll_id;
+	holder->type = type;
+	rlist_add_tail(&coll_id->cache_pin_list, &holder->in_coll_id);
+}
+
+void
+coll_id_unpin(struct coll_id_cache_holder *holder)
+{
+	assert(coll_by_id(holder->coll_id->id) != NULL);
+#ifndef NDEBUG
+	/* Paranoid check that the coll_id is pinned by holder. */
+	bool is_in_list = false;
+	struct rlist *tmp;
+	rlist_foreach(tmp, &holder->coll_id->cache_pin_list) {
+		is_in_list = is_in_list || tmp == &holder->in_coll_id;
+	}
+	assert(is_in_list);
+#endif
+	rlist_del(&holder->in_coll_id);
+	holder->coll_id = NULL;
+}
+
+bool
+coll_id_is_pinned(struct coll_id *coll_id, enum coll_id_holder_type *type)
+{
+	assert(coll_by_id(coll_id->id) != NULL);
+	if (rlist_empty(&coll_id->cache_pin_list))
+		return false;
+	struct coll_id_cache_holder *h =
+		rlist_first_entry(&coll_id->cache_pin_list,
+				  struct coll_id_cache_holder, in_coll_id);
+	*type = h->type;
+	return true;
+}
diff --git a/src/box/coll_id_cache.h b/src/box/coll_id_cache.h
index d5325b6aee..60576cc2a5 100644
--- a/src/box/coll_id_cache.h
+++ b/src/box/coll_id_cache.h
@@ -31,6 +31,8 @@
  * SUCH DAMAGE.
  */
 #include <stdint.h>
+#include <stdbool.h>
+#include "small/rlist.h"
 
 #if defined(__cplusplus)
 extern "C" {
@@ -38,6 +40,39 @@ extern "C" {
 
 struct coll_id;
 
+/**
+ * Type of a holder that can pin coll_id. See `struct coll_id_cache_holder`.
+ */
+enum coll_id_holder_type {
+	COLL_ID_HOLDER_SPACE_FORMAT,
+	COLL_ID_HOLDER_INDEX,
+	COLL_ID_HOLDER_MAX,
+};
+
+/**
+ * Lowercase name of each type.
+ */
+extern const char *coll_id_holder_type_strs[COLL_ID_HOLDER_MAX];
+
+/**
+ * Definition of a holder that pinned some coll_id. Pinning of a coll_id is
+ * a mechanism that is designed for preventing of deletion of some coll_id from
+ * coll_id cache by storing links to holders that prevented that.
+ */
+struct coll_id_cache_holder {
+	/** Link in `space::coll_id_holders`. */
+	struct rlist in_space;
+	/** Link in `coll_id::cache_pin_list`. */
+	struct rlist in_coll_id;
+	/** Actual pointer to coll_id. */
+	struct coll_id *coll_id;
+	/**
+	 * Type of holder, mostly for better error generation, but also can be
+	 * used for proper container_of application.
+	 */
+	enum coll_id_holder_type type;
+};
+
 /**
  * Create global hash tables.
  */
@@ -62,7 +97,7 @@ coll_id_cache_replace(struct coll_id *coll_id, struct coll_id **replaced_id);
  * @param coll_id Collation to delete.
  */
 void
-coll_id_cache_delete(const struct coll_id *coll_id);
+coll_id_cache_delete(struct coll_id *coll_id);
 
 /**
  * Find a collation object by its id.
@@ -76,6 +111,32 @@ coll_by_id(uint32_t id);
 struct coll_id *
 coll_by_name(const char *name, uint32_t len);
 
+/**
+ * Register that there is a `holder` of type `type` that is dependent on
+ * coll_id. coll_id must be in cache (asserted).
+ * If coll_id has holders, it must not be deleted (asserted).
+ */
+void
+coll_id_pin(struct coll_id *coll_id, struct coll_id_cache_holder *holder,
+	    enum coll_id_holder_type type);
+
+/**
+ * Notify that `holder` does not depend anymore on coll_id.
+ * coll_id must be in cache (asserted).
+ * If coll_id has no holders, it can be deleted.
+ */
+void
+coll_id_unpin(struct coll_id_cache_holder *holder);
+
+/**
+ * Check whether coll_id has holders or not.
+ * If it has, `type` argument is set to the first holder's type.
+ * coll_id must be in cache (asserted).
+ * If coll_id has holders, it must not be deleted (asserted).
+ */
+bool
+coll_id_is_pinned(struct coll_id *coll_id, enum coll_id_holder_type *type);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/src/box/errcode.h b/src/box/errcode.h
index 28fe97aba1..ebfb5c780d 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -225,7 +225,7 @@ struct errcode_record {
 	/*170 */_(ER_CONSTRAINT_EXISTS,		"%s constraint '%s' already exists in space '%s'") \
 	/*171 */_(ER_SQL_TYPE_MISMATCH,		"Type mismatch: can not convert %s to %s") \
 	/*172 */_(ER_ROWID_OVERFLOW,            "Rowid is overflowed: too many entries in ephemeral space") \
-	/*173 */_(ER_DROP_COLLATION,		"Can't drop collation %s : %s") \
+	/*173 */_(ER_DROP_COLLATION,		"Can't drop collation '%s': %s") \
 	/*174 */_(ER_ILLEGAL_COLLATION_MIX,	"Illegal mix of collations") \
 	/*175 */_(ER_SQL_NO_SUCH_PRAGMA,	"Pragma '%s' does not exist") \
 	/*176 */_(ER_SQL_CANT_RESOLVE_FIELD,	"Can’t resolve field '%s'") \
diff --git a/src/box/index_def.h b/src/box/index_def.h
index 5f2e577be5..ca0681713d 100644
--- a/src/box/index_def.h
+++ b/src/box/index_def.h
@@ -370,7 +370,7 @@ index_def_list_add(struct rlist *index_def_list, struct index_def *index_def)
 }
 
 /**
- * Create a new index definition definition.
+ * Create a new index definition.
  *
  * @param key_def  key definition, must be fully built
  * @param pk_def   primary key definition, pass non-NULL
diff --git a/src/box/space.c b/src/box/space.c
index 3ca58ebbd3..5f97d07093 100644
--- a/src/box/space.c
+++ b/src/box/space.c
@@ -53,6 +53,7 @@
 #include "tuple_constraint_func.h"
 #include "tuple_constraint_fkey.h"
 #include "wal_ext.h"
+#include "coll_id_cache.h"
 
 int
 access_check_space(struct space *space, user_access_t access)
@@ -237,6 +238,53 @@ space_cleanup_constraints(struct space *space)
 	return 0;
 }
 
+/**
+ * Pin collation identifier with `id` in the cache, so that it can't be deleted.
+ */
+static void
+space_pin_collations_helper(struct space *space, uint32_t id,
+			    enum coll_id_holder_type holder_type)
+{
+	if (id == COLL_NONE)
+		return;
+	struct coll_id *coll_id = coll_by_id(id);
+	assert(coll_id != NULL);
+	struct coll_id_cache_holder *h = xmalloc(sizeof(*h));
+	rlist_add_tail_entry(&space->coll_id_holders, h, in_space);
+	coll_id_pin(coll_id, h, holder_type);
+}
+
+void
+space_pin_collations(struct space *space)
+{
+	struct tuple_format *format = space->format;
+	for (uint32_t i = 0; i < tuple_format_field_count(format); i++) {
+		struct tuple_field *field = tuple_format_field(format, i);
+		space_pin_collations_helper(space, field->coll_id,
+					    COLL_ID_HOLDER_SPACE_FORMAT);
+	}
+
+	for (uint32_t i = 0; i < space->index_count; i++) {
+		struct key_def *key_def = space->index[i]->def->key_def;
+		for (uint32_t i = 0; i < key_def->part_count; i++) {
+			struct key_part *part = &key_def->parts[i];
+			space_pin_collations_helper(space, part->coll_id,
+						    COLL_ID_HOLDER_INDEX);
+		}
+	}
+}
+
+void
+space_unpin_collations(struct space *space)
+{
+	struct coll_id_cache_holder *h, *tmp;
+	rlist_foreach_entry_safe(h, &space->coll_id_holders, in_space, tmp) {
+		coll_id_unpin(h);
+		free(h);
+	}
+	rlist_create(&space->coll_id_holders);
+}
+
 int
 space_create(struct space *space, struct engine *engine,
 	     const struct space_vtab *vtab, struct space_def *def,
@@ -265,6 +313,7 @@ space_create(struct space *space, struct engine *engine,
 	space->index_id_max = index_id_max;
 	rlist_create(&space->before_replace);
 	rlist_create(&space->on_replace);
+	rlist_create(&space->coll_id_holders);
 	space->run_triggers = true;
 
 	space->format = format;
@@ -303,6 +352,7 @@ space_create(struct space *space, struct engine *engine,
 	rlist_create(&space->space_cache_pin_list);
 	if (space_init_constraints(space) != 0)
 		goto fail_free_indexes;
+	space_pin_collations(space);
 
 	/*
 	 * Check if there are unique indexes that are contained
@@ -348,6 +398,7 @@ space_create(struct space *space, struct engine *engine,
 		space_cleanup_constraints(space);
 		tuple_format_unref(space->format);
 	}
+	space_unpin_collations(space);
 	return -1;
 }
 
@@ -403,6 +454,7 @@ space_delete(struct space *space)
 		space_cleanup_constraints(space);
 		tuple_format_unref(space->format);
 	}
+	space_unpin_collations(space);
 	trigger_destroy(&space->before_replace);
 	trigger_destroy(&space->on_replace);
 	if (space->upgrade != NULL)
diff --git a/src/box/space.h b/src/box/space.h
index e8a10b52b0..9d27a767ea 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -248,6 +248,11 @@ struct space {
 	 * (i.e. if not NULL WAL entries may contain extra fields).
 	 */
 	struct space_wal_ext *wal_ext;
+	/**
+	 * List of collation identifier holders.
+	 * Linked by `coll_id_cache_holder::in_space`.
+	 */
+	struct rlist coll_id_holders;
 };
 
 /** Space alter statement. */
@@ -272,6 +277,19 @@ space_detach_constraints(struct space *space);
 void
 space_reattach_constraints(struct space *space);
 
+/**
+ * Pin in cache the collation identifiers that are referenced by space format
+ * and/or indexes, so that they can't be deleted.
+ */
+void
+space_pin_collations(struct space *space);
+
+/**
+ * Unpin collation identifiers.
+ */
+void
+space_unpin_collations(struct space *space);
+
 /** Initialize a base space instance. */
 int
 space_create(struct space *space, struct engine *engine,
diff --git a/src/box/sql/show.c b/src/box/sql/show.c
index 589828886a..d54326db00 100644
--- a/src/box/sql/show.c
+++ b/src/box/sql/show.c
@@ -278,14 +278,9 @@ sql_describe_field(struct sql_desc *desc, const struct field_def *field)
 
 	if (field->coll_id != 0) {
 		struct coll_id *coll_id = coll_by_id(field->coll_id);
-		if (coll_id == NULL) {
-			sql_desc_error(desc, "collation",
-				       tt_sprintf("%d", field->coll_id),
-				       "collation does not exist");
-		} else {
-			sql_desc_append(desc, " COLLATE ");
-			sql_desc_append_name(desc, coll_id->name);
-		}
+		assert(coll_id != NULL);
+		sql_desc_append(desc, " COLLATE ");
+		sql_desc_append_name(desc, coll_id->name);
 	}
 	if (!field->is_nullable)
 		sql_desc_append(desc, " NOT NULL");
diff --git a/test/box/net.box_is_nullable_gh-3256.result b/test/box/net.box_is_nullable_gh-3256.result
index d8f9557afc..df95ad2c66 100644
--- a/test/box/net.box_is_nullable_gh-3256.result
+++ b/test/box/net.box_is_nullable_gh-3256.result
@@ -79,10 +79,10 @@ parts[1].collation == 'test'
 c:close()
 ---
 ...
-box.internal.collation.drop('test')
+space:drop()
 ---
 ...
-space:drop()
+box.internal.collation.drop('test')
 ---
 ...
 c.state
diff --git a/test/box/net.box_is_nullable_gh-3256.test.lua b/test/box/net.box_is_nullable_gh-3256.test.lua
index 695c30ae22..926681af6d 100644
--- a/test/box/net.box_is_nullable_gh-3256.test.lua
+++ b/test/box/net.box_is_nullable_gh-3256.test.lua
@@ -26,7 +26,7 @@ parts[1].type == 'string'
 parts[1].is_nullable == false
 parts[1].collation == 'test'
 c:close()
-box.internal.collation.drop('test')
 space:drop()
+box.internal.collation.drop('test')
 c.state
 c = nil
diff --git a/test/engine-luatest/gh_4544_collation_drop_test.lua b/test/engine-luatest/gh_4544_collation_drop_test.lua
index 4f4f532405..166630dbdd 100644
--- a/test/engine-luatest/gh_4544_collation_drop_test.lua
+++ b/test/engine-luatest/gh_4544_collation_drop_test.lua
@@ -60,3 +60,294 @@ g1.test_keydef_replace_coll_different = function(cg)
         box.internal.collation.drop(new_name)
     end)
 end
+
+local g2 = t.group('gh-4544-2', {{engine = 'memtx'}, {engine = 'vinyl'}})
+g2.before_all(before_all)
+g2.after_all(after_all)
+
+-- Pin/unpin collation by the space format, but not by the index.
+g2.test_coll_pin_format = function(cg)
+    local coll_name = 'my coll 1'
+
+    local function init()
+        cg.server:exec(function(engine, coll_name)
+            box.internal.collation.create(coll_name, 'ICU', 'ru-RU', {})
+            local s = box.schema.space.create('test', {engine = engine})
+            s:format({{name = 'p'},
+                      {name = 's', type = 'string', collation = coll_name}})
+            s:create_index('pk')
+        end, {cg.params.engine, coll_name})
+    end
+
+    local function check_references()
+        cg.server:exec(function(coll_name)
+            t.assert_error_msg_equals(
+                "Can't drop collation '" .. coll_name .. "': collation is " ..
+                "referenced by space format",
+                box.internal.collation.drop, coll_name)
+        end, {coll_name})
+    end
+
+    -- Check that collation can not be dropped.
+    init()
+    check_references()
+
+    cg.server:restart()
+    check_references()
+
+    cg.server:eval('box.snapshot()')
+    cg.server:restart()
+    check_references()
+
+    -- Check that collation is unpinned on space drop.
+    cg.server:exec(function(coll_name)
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+    end, {coll_name})
+
+    -- Check that collation can be dropped in one transaction with space drop.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+        box.commit()
+    end, {coll_name})
+
+    -- Check that collation is still pinned after rollback.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+        box.rollback()
+    end, {coll_name})
+    check_references()
+
+    -- Check that collation is unpinned on space alter.
+    cg.server:exec(function(coll_name)
+        box.space.test:alter({format = {{name = 'p'}, {name = 's'}}})
+        box.internal.collation.drop(coll_name)
+        box.space.test:drop()
+    end, {coll_name})
+
+    -- Check that collation can be dropped in one transaction with space alter.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test:alter({format = {{name = 'p'}, {name = 's'}}})
+        box.internal.collation.drop(coll_name)
+        box.commit()
+        box.space.test:drop()
+    end, {coll_name})
+
+    -- Check that collation is still pinned after rollback.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test:alter({format = {{name = 'p'}, {name = 's'}}})
+        box.internal.collation.drop(coll_name)
+        box.rollback()
+    end, {coll_name})
+    check_references()
+
+    -- Cleanup.
+    cg.server:exec(function(coll_name)
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+    end, {coll_name})
+end
+
+-- Pin/unpin collation by the index, but not by the space format.
+g2.test_coll_pin_index = function(cg)
+    local coll_name = 'my coll 2'
+
+    local function init()
+        cg.server:exec(function(engine, coll_name)
+            box.internal.collation.create(coll_name, 'ICU', 'ru-RU', {})
+            local s = box.schema.space.create('test', {engine = engine})
+            s:create_index('pk')
+            s:create_index('sk', {parts = {2, 'string',
+                                           collation = coll_name}})
+        end, {cg.params.engine, coll_name})
+    end
+
+    local function check_references()
+        cg.server:exec(function(coll_name)
+            t.assert_error_msg_equals(
+                "Can't drop collation '" .. coll_name .. "': collation is " ..
+                "referenced by index",
+                box.internal.collation.drop, coll_name)
+        end, {coll_name})
+    end
+
+    -- Check that collation can not be dropped.
+    init()
+    check_references()
+
+    cg.server:restart()
+    check_references()
+
+    cg.server:eval('box.snapshot()')
+    cg.server:restart()
+    check_references()
+
+    -- Check that collation is unpinned on space drop.
+    cg.server:exec(function(coll_name)
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+    end, {coll_name})
+
+    -- Check that collation can be dropped in one transaction with space drop.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+        box.commit()
+    end, {coll_name})
+
+    -- Check that collation is still pinned after rollback.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+        box.rollback()
+    end, {coll_name})
+    check_references()
+
+    -- Check that collation is unpinned on index drop.
+    cg.server:exec(function(coll_name)
+        box.space.test.index.sk:drop()
+        box.internal.collation.drop(coll_name)
+        box.space.test:drop()
+    end, {coll_name})
+
+    -- Check that collation can be dropped in one transaction with index drop.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test.index.sk:drop()
+        box.internal.collation.drop(coll_name)
+        box.commit()
+        box.space.test:drop()
+    end, {coll_name})
+
+    -- Check that collation is still pinned after rollback.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test.index.sk:drop()
+        box.internal.collation.drop(coll_name)
+        box.rollback()
+    end, {coll_name})
+    check_references()
+
+    -- Cleanup.
+    cg.server:exec(function(coll_name)
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+    end, {coll_name})
+end
+
+-- Pin/unpin collation by both: space format and the index.
+g2.test_coll_pin_format_index = function(cg)
+    local coll_name = 'my coll 3'
+
+    local function init()
+        cg.server:exec(function(engine, coll_name)
+            box.internal.collation.create(coll_name, 'ICU', 'ru-RU', {})
+            local s = box.schema.space.create('test', {engine = engine})
+            s:format({{name = 'p'},
+                      {name = 's', type = 'string', collation = coll_name}})
+            s:create_index('pk')
+            s:create_index('sk', {parts = {'s'}})
+        end, {cg.params.engine, coll_name})
+    end
+
+    local function check_references()
+        cg.server:exec(function(coll_name)
+            t.assert_error_msg_equals(
+                "Can't drop collation '" .. coll_name .. "': collation is " ..
+                "referenced by space format",
+                box.internal.collation.drop, coll_name)
+        end, {coll_name})
+    end
+
+    -- Check that collation can not be dropped.
+    init()
+    check_references()
+
+    cg.server:restart()
+    check_references()
+
+    cg.server:eval('box.snapshot()')
+    cg.server:restart()
+    check_references()
+
+    -- Check that collation is unpinned on space drop.
+    cg.server:exec(function(coll_name)
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+    end, {coll_name})
+
+    -- Check that collation can be dropped in one transaction with space drop.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+        box.commit()
+    end, {coll_name})
+
+    -- Check that collation is still pinned after rollback.
+    init()
+    check_references()
+    cg.server:exec(function(coll_name)
+        box.begin()
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+        box.rollback()
+    end, {coll_name})
+    check_references()
+
+    -- Check that collation is still pinned after index drop.
+    cg.server:exec(function()
+        box.space.test.index.sk:drop()
+    end)
+    check_references()
+
+    -- Cleanup.
+    cg.server:exec(function(coll_name)
+        box.space.test:drop()
+        box.internal.collation.drop(coll_name)
+    end, {coll_name})
+end
+
+-- Check that collation is pinned from SQL.
+g2.test_sql = function(cg)
+    cg.server:exec(function(engine)
+        local coll_name = 'unicode_af_s2'
+        local sql = [[CREATE TABLE test (id STRING COLLATE "%s" PRIMARY KEY)
+                      WITH ENGINE = '%s']]
+        box.execute(string.format(sql, coll_name, engine))
+        t.assert_error_msg_equals(
+            "Can't drop collation 'unicode_af_s2': collation is referenced " ..
+            "by space format",
+            box.internal.collation.drop, coll_name)
+
+        -- Check that collation is unpinned on table drop.
+        box.execute("DROP TABLE test")
+        box.internal.collation.drop(coll_name)
+    end, {cg.params.engine})
+end
diff --git a/test/sql-luatest/show_create_table_test.lua b/test/sql-luatest/show_create_table_test.lua
index e20102931e..00da38d8bd 100644
--- a/test/sql-luatest/show_create_table_test.lua
+++ b/test/sql-luatest/show_create_table_test.lua
@@ -412,14 +412,6 @@ g.test_wrong_collation = function()
                      "WITH ENGINE = 'memtx';"}
         _G.check('"a"', res)
 
-        -- Collations does not exists.
-        box.space._collation:delete(col.id)
-        res = {'CREATE TABLE "a"(\n"i" STRING NOT NULL,\n'..
-               'CONSTRAINT "i" PRIMARY KEY("i"))\nWITH ENGINE = \'memtx\';'}
-        local err = {"Problem with collation '277': collation does not exist."}
-        _G.check('"a"', res, err)
-
-        box.space._collation:insert(col)
         box.space.a:drop()
         box.space._collation:delete(col.id)
 
diff --git a/test/sql/collation.result b/test/sql/collation.result
index bd8b209695..af9a529bd4 100644
--- a/test/sql/collation.result
+++ b/test/sql/collation.result
@@ -225,7 +225,7 @@ box.space._collation:select{0}
 ...
 box.space._collation:delete{0}
 ---
-- error: 'Can''t drop collation none : system collation'
+- error: 'Can''t drop collation ''none'': system collation'
 ...
 -- gh-3185: collations of LHS and RHS must be compatible.
 --
-- 
GitLab