From b9550f192673581dcbd8432867ae2e55ec264676 Mon Sep 17 00:00:00 2001
From: Georgiy Lebedev <g.lebedev@tarantool.org>
Date: Sat, 15 Apr 2023 20:16:22 +0300
Subject: [PATCH] box: support space and index names in IPROTO requests

Add support for accepting IPROTO requests with space or index name instead
of identifier (name is preferred over identifier to disambiguate missing
identifiers from zero identifiers): mark space identifier request
key as present upon encountering space name, and delay resolution of
identifier until request gets to transaction thread.

Add support for sending DML requests from net.box connection objects with
disabled schema fetching by manually specifying space or index name or
identifier: when schema fetching is disabled, the space and index tables of
connections return wrapper tables that store necessary context (space or
index name or identifier, determined by type, connection object and space
for indexes) for performing requests. The space and index tables cache the
wrapper table they return.

Closes #8146

@TarantoolBot document
Title: Space and index name in IPROTO requests

Refer to design document for details:
https://www.notion.so/tarantool/Schemafull-IPROTO-cc315ad6bdd641dea66ad854992d8cbf?pvs=4#f4d4b3fa2b3646f1949319866428b6c0
---
 ...pace-and-index-names-in-iproto-requests.md |   4 +
 src/box/iproto.cc                             |  44 ++-
 src/box/iproto_constants.c                    |   2 +
 src/box/iproto_constants.h                    |  12 +
 src/box/iproto_features.c                     |   2 +
 src/box/iproto_features.h                     |  10 +-
 src/box/lua/net_box.c                         |  82 +++--
 src/box/lua/net_box.lua                       |  94 ++++--
 src/box/lua/schema.lua                        |   4 +-
 src/box/xrow.c                                |  17 +-
 src/box/xrow.h                                |  31 +-
 ...ort_iproto_constants_and_features_test.lua |   6 +-
 .../gh_8146_data/00000000000000000004.snap    | Bin 0 -> 5093 bytes
 ...and_index_name_in_iproto_requests_test.lua | 304 ++++++++++++++++++
 test/box-luatest/net_box_test.lua             |   2 +-
 test/box-py/iproto.result                     |   4 +-
 test/box/net.box_iproto_id.result             |  11 +-
 17 files changed, 560 insertions(+), 69 deletions(-)
 create mode 100644 changelogs/unreleased/gh-8146-space-and-index-names-in-iproto-requests.md
 create mode 100644 test/box-luatest/gh_8146_data/00000000000000000004.snap
 create mode 100644 test/box-luatest/gh_8146_space_and_index_name_in_iproto_requests_test.lua

diff --git a/changelogs/unreleased/gh-8146-space-and-index-names-in-iproto-requests.md b/changelogs/unreleased/gh-8146-space-and-index-names-in-iproto-requests.md
new file mode 100644
index 0000000000..9e14e1c365
--- /dev/null
+++ b/changelogs/unreleased/gh-8146-space-and-index-names-in-iproto-requests.md
@@ -0,0 +1,4 @@
+## feature/box
+
+* Added support for accepting IPROTO requests with a space or index name instead
+  of an identifier (gh-8146).
diff --git a/src/box/iproto.cc b/src/box/iproto.cc
index 30dcce91f1..951c1d283f 100644
--- a/src/box/iproto.cc
+++ b/src/box/iproto.cc
@@ -1666,8 +1666,8 @@ iproto_msg_decode(struct iproto_msg *msg, struct cmsg_hop **route)
 		assert(type < sizeof(iproto_thread->dml_route) /
 			      sizeof(*iproto_thread->dml_route));
 		*route = iproto_thread->dml_route[type];
-		if (xrow_decode_dml(&msg->header, &msg->dml,
-				    dml_request_key_map(type)))
+		if (xrow_decode_dml_iproto(&msg->header, &msg->dml,
+					   dml_request_key_map(type)) != 0)
 			return -1;
 		/*
 		 * In contrast to replication requests, for a client request
@@ -2143,6 +2143,42 @@ tx_process_rollback(struct cmsg *m)
 	tx_end_msg(msg, &header);
 }
 
+/*
+ * In case the request does not contain a space or identifier but contains a
+ * corresponding name, tries to resolve the name.
+ */
+static int
+tx_resolve_space_and_index_name(struct request *dml)
+{
+	struct space *space = NULL;
+	if (dml->space_name != NULL) {
+		space = space_by_name(dml->space_name, dml->space_name_len);
+		if (space == NULL) {
+			diag_set(ClientError, ER_NO_SUCH_SPACE,
+				 tt_cstr(dml->space_name, dml->space_name_len));
+			return -1;
+		}
+		dml->space_id = space->def->id;
+	}
+	if ((dml->type == IPROTO_SELECT || dml->type == IPROTO_UPDATE ||
+	     dml->type == IPROTO_DELETE) && dml->index_name != NULL) {
+		if (space == NULL)
+			space = space_cache_find(dml->space_id);
+		if (space == NULL)
+			return -1;
+		struct index *idx = space_index_by_name(space, dml->index_name,
+							dml->index_name_len);
+		if (idx == NULL) {
+			diag_set(ClientError, ER_NO_SUCH_INDEX_NAME,
+				 tt_cstr(dml->index_name, dml->index_name_len),
+				 space->def->name);
+			return -1;
+		}
+		dml->index_id = idx->dense_id;
+	}
+	return 0;
+}
+
 static void
 tx_process1(struct cmsg *m)
 {
@@ -2154,6 +2190,8 @@ tx_process1(struct cmsg *m)
 	struct obuf_svp svp;
 	struct obuf *out;
 	tx_inject_delay();
+	if (tx_resolve_space_and_index_name(&msg->dml) != 0)
+		goto error;
 	if (box_process1(&msg->dml, &tuple) != 0)
 		goto error;
 	out = msg->connection->tx.p_obuf;
@@ -2190,6 +2228,8 @@ tx_process_select(struct cmsg *m)
 		goto error;
 
 	tx_inject_delay();
+	if (tx_resolve_space_and_index_name(&msg->dml) != 0)
+		goto error;
 	packed_pos = req->after_position;
 	packed_pos_end = req->after_position_end;
 	if (packed_pos != NULL) {
diff --git a/src/box/iproto_constants.c b/src/box/iproto_constants.c
index 7cd41f145c..ecbdaeb06d 100644
--- a/src/box/iproto_constants.c
+++ b/src/box/iproto_constants.c
@@ -154,6 +154,8 @@ const unsigned char iproto_key_type[iproto_key_MAX] =
 	/* 0x5b */	MP_STR, /* IPROTO_AUTH_TYPE */
 	/* 0x5c */	MP_STR, /* IPROTO_REPLICASET_NAME */
 	/* 0x5d */	MP_STR, /* IPROTO_INSTANCE_NAME */
+	/* 0x5e */	MP_STR, /* IPROTO_SPACE_NAME */
+	/* 0x5f */	MP_STR, /* IPROTO_INDEX_NAME */
 	/* }}} */
 };
 
diff --git a/src/box/iproto_constants.h b/src/box/iproto_constants.h
index 6b441f28dd..29f48b4a3a 100644
--- a/src/box/iproto_constants.h
+++ b/src/box/iproto_constants.h
@@ -195,6 +195,18 @@ extern const size_t iproto_flag_constants_size;
 	_(IPROTO_AUTH_TYPE, 0x5b)					\
 	_(IPROTO_REPLICASET_NAME, 0x5c)					\
 	_(IPROTO_INSTANCE_NAME, 0x5d)					\
+	/**
+	 * Space name used instead of identifier (IPROTO_SPACE_ID) in DML
+	 * requests. Preferred when identifier is present (i.e., the identifier
+	 * is ignored).
+	 */								\
+	_(IPROTO_SPACE_NAME, 0x5e)					\
+	/**
+	 * Index name used instead of identifier (IPROTO_INDEX_ID) in
+	 * IPROTO_SELECT, IPROTO_UPDATE, and IPROTO_DELETE requests. Preferred
+	 * when identifier is present (i.e., the identifier is ignored).
+	 */								\
+	_(IPROTO_INDEX_NAME, 0x5f)					\
 
 ENUM(iproto_key, IPROTO_KEYS);
 /**
diff --git a/src/box/iproto_features.c b/src/box/iproto_features.c
index b5724381fb..9fcc731046 100644
--- a/src/box/iproto_features.c
+++ b/src/box/iproto_features.c
@@ -75,4 +75,6 @@ iproto_features_init(void)
 			    IPROTO_FEATURE_WATCHERS);
 	iproto_features_set(&IPROTO_CURRENT_FEATURES,
 			    IPROTO_FEATURE_PAGINATION);
+	iproto_features_set(&IPROTO_CURRENT_FEATURES,
+			    IPROTO_FEATURE_SPACE_AND_INDEX_NAMES);
 }
diff --git a/src/box/iproto_features.h b/src/box/iproto_features.h
index 755fde7a67..39eece83d3 100644
--- a/src/box/iproto_features.h
+++ b/src/box/iproto_features.h
@@ -50,6 +50,14 @@ extern "C" {
 	 * request fields and IPROTO_POSITION response field.
 	 */								\
 	_(IPROTO_FEATURE_PAGINATION, 4)					\
+	/**
+	 * Using space [index] names instead of identifiers support:
+	 * IPROTO_SPACE_NAME and IPROTO_INDEX_NAME fields in IPROTO_SELECT,
+	 * IPROTO_UPDATE and IPROTO_DELETE request body;
+	 * IPROTO_SPACE_NAME field in IPROTO_INSERT, IPROTO_REPLACE,
+	 * IPROTO_UPDATE and IPROTO_UPSERT request body.
+	 */								\
+	_(IPROTO_FEATURE_SPACE_AND_INDEX_NAMES,  5)			\
 
 ENUM(iproto_feature_id, IPROTO_FEATURES);
 
@@ -72,7 +80,7 @@ struct iproto_features {
  * `box.iproto.protocol_version` needs to be updated correspondingly.
  */
 enum {
-	IPROTO_CURRENT_VERSION = 4,
+	IPROTO_CURRENT_VERSION = 5,
 };
 
 /**
diff --git a/src/box/lua/net_box.c b/src/box/lua/net_box.c
index 7ca5f17f5a..f43cb97834 100644
--- a/src/box/lua/net_box.c
+++ b/src/box/lua/net_box.c
@@ -80,7 +80,7 @@ enum {
 	/**
 	 * IPROTO protocol version supported by the netbox connector.
 	 */
-	NETBOX_IPROTO_VERSION = 4,
+	NETBOX_IPROTO_VERSION = 5,
 };
 
 /**
@@ -781,6 +781,44 @@ netbox_encode_eval(lua_State *L, int idx, struct mpstream *stream,
 	netbox_end_encode(stream, svp);
 }
 
+/*
+ * Depending on the type of the argument (see also net.box space metatable)
+ * encode either a space identifier or a space name.
+ */
+static void
+netbox_encode_space_id_or_name(lua_State *L, int idx, struct mpstream *stream)
+{
+	if (lua_type(L, idx) == LUA_TNUMBER) {
+		uint32_t space_id = lua_tonumber(L, idx);
+		mpstream_encode_uint(stream, IPROTO_SPACE_ID);
+		mpstream_encode_uint(stream, space_id);
+	} else {
+		size_t len;
+		const char *space_name = lua_tolstring(L, idx, &len);
+		mpstream_encode_uint(stream, IPROTO_SPACE_NAME);
+		mpstream_encode_strn(stream, space_name, len);
+	}
+}
+
+/*
+ * Depending on the type of the argument (see also net.box index metatable)
+ * encode either a index identifier or an index name.
+ */
+static void
+netbox_encode_index_id_or_name(lua_State *L, int idx, struct mpstream *stream)
+{
+	if (lua_type(L, idx) == LUA_TNUMBER) {
+		uint32_t space_id = lua_tonumber(L, idx);
+		mpstream_encode_uint(stream, IPROTO_INDEX_ID);
+		mpstream_encode_uint(stream, space_id);
+	} else {
+		size_t len;
+		const char *space_name = lua_tolstring(L, idx, &len);
+		mpstream_encode_uint(stream, IPROTO_INDEX_NAME);
+		mpstream_encode_strn(stream, space_name, len);
+	}
+}
+
 /* Encode select request. */
 static void
 netbox_encode_select(lua_State *L, int idx, struct mpstream *stream,
@@ -801,19 +839,13 @@ netbox_encode_select(lua_State *L, int idx, struct mpstream *stream,
 	if (fetch_pos)
 		map_size++;
 	mpstream_encode_map(stream, map_size);
-	uint32_t space_id = lua_tonumber(L, idx);
-	uint32_t index_id = lua_tonumber(L, idx + 1);
 	int iterator = lua_tointeger(L, idx + 2);
 	uint32_t offset = lua_tonumber(L, idx + 3);
 	uint32_t limit = lua_tonumber(L, idx + 4);
 
-	/* encode space_id */
-	mpstream_encode_uint(stream, IPROTO_SPACE_ID);
-	mpstream_encode_uint(stream, space_id);
+	netbox_encode_space_id_or_name(L, idx, stream);
 
-	/* encode index_id */
-	mpstream_encode_uint(stream, IPROTO_INDEX_ID);
-	mpstream_encode_uint(stream, index_id);
+	netbox_encode_index_id_or_name(L, idx + 1, stream);
 
 	/* encode iterator */
 	mpstream_encode_uint(stream, IPROTO_ITERATOR);
@@ -865,10 +897,7 @@ netbox_encode_insert_or_replace(lua_State *L, int idx, struct mpstream *stream,
 
 	mpstream_encode_map(stream, 2);
 
-	/* encode space_id */
-	uint32_t space_id = lua_tonumber(L, idx);
-	mpstream_encode_uint(stream, IPROTO_SPACE_ID);
-	mpstream_encode_uint(stream, space_id);
+	netbox_encode_space_id_or_name(L, idx, stream);
 
 	/* encode args */
 	mpstream_encode_uint(stream, IPROTO_TUPLE);
@@ -903,15 +932,9 @@ netbox_encode_delete(lua_State *L, int idx, struct mpstream *stream,
 
 	mpstream_encode_map(stream, 3);
 
-	/* encode space_id */
-	uint32_t space_id = lua_tonumber(L, idx);
-	mpstream_encode_uint(stream, IPROTO_SPACE_ID);
-	mpstream_encode_uint(stream, space_id);
+	netbox_encode_space_id_or_name(L, idx, stream);
 
-	/* encode space_id */
-	uint32_t index_id = lua_tonumber(L, idx + 1);
-	mpstream_encode_uint(stream, IPROTO_INDEX_ID);
-	mpstream_encode_uint(stream, index_id);
+	netbox_encode_index_id_or_name(L, idx + 1, stream);
 
 	/* encode key */
 	mpstream_encode_uint(stream, IPROTO_KEY);
@@ -930,15 +953,9 @@ netbox_encode_update(lua_State *L, int idx, struct mpstream *stream,
 
 	mpstream_encode_map(stream, 5);
 
-	/* encode space_id */
-	uint32_t space_id = lua_tonumber(L, idx);
-	mpstream_encode_uint(stream, IPROTO_SPACE_ID);
-	mpstream_encode_uint(stream, space_id);
+	netbox_encode_space_id_or_name(L, idx, stream);
 
-	/* encode index_id */
-	uint32_t index_id = lua_tonumber(L, idx + 1);
-	mpstream_encode_uint(stream, IPROTO_INDEX_ID);
-	mpstream_encode_uint(stream, index_id);
+	netbox_encode_index_id_or_name(L, idx + 1, stream);
 
 	/* encode index_id */
 	mpstream_encode_uint(stream, IPROTO_INDEX_BASE);
@@ -965,10 +982,7 @@ netbox_encode_upsert(lua_State *L, int idx, struct mpstream *stream,
 
 	mpstream_encode_map(stream, 4);
 
-	/* encode space_id */
-	uint32_t space_id = lua_tonumber(L, idx);
-	mpstream_encode_uint(stream, IPROTO_SPACE_ID);
-	mpstream_encode_uint(stream, space_id);
+	netbox_encode_space_id_or_name(L, idx, stream);
 
 	/* encode index_base */
 	mpstream_encode_uint(stream, IPROTO_INDEX_BASE);
@@ -2994,6 +3008,8 @@ luaopen_net_box(struct lua_State *L)
 			    IPROTO_FEATURE_WATCHERS);
 	iproto_features_set(&NETBOX_IPROTO_FEATURES,
 			    IPROTO_FEATURE_PAGINATION);
+	iproto_features_set(&NETBOX_IPROTO_FEATURES,
+			    IPROTO_FEATURE_SPACE_AND_INDEX_NAMES);
 
 	lua_pushcfunction(L, luaT_netbox_request_iterator_next);
 	luaT_netbox_request_iterator_next_ref = luaL_ref(L, LUA_REGISTRYINDEX);
diff --git a/src/box/lua/net_box.lua b/src/box/lua/net_box.lua
index 670cfdf59e..abf5654666 100644
--- a/src/box/lua/net_box.lua
+++ b/src/box/lua/net_box.lua
@@ -58,6 +58,7 @@ local IPROTO_FEATURE_NAMES = {
     [2]     = 'error_extension',
     [3]     = 'watchers',
     [4]     = 'pagination',
+    [5]     = 'space_and_index_names',
 }
 
 local REQUEST_OPTION_TYPES = {
@@ -388,6 +389,49 @@ local function new_sm(uri, opts)
                     })
                 end
             end
+            if not opts.fetch_schema then
+                remote.space = setmetatable({}, {
+                    __index = function(_, space_key)
+                        local id_or_name
+                        if type(space_key) == 'number' then
+                            id_or_name = 'id'
+                        elseif type(space_key) == 'string' then
+                            if not features.space_and_index_names then
+                                return nil
+                            end
+                            id_or_name = 'name'
+                        else
+                            return nil
+                        end
+                        local space = {[id_or_name] = space_key,
+                                       _id_or_name = space_key}
+                        space.index = setmetatable({}, {
+                            __index = function(_, idx_key)
+                                local id_or_name
+                                if type(idx_key) == 'number' then
+                                    id_or_name = 'id'
+                                elseif type(idx_key) == 'string' then
+                                    if not features.space_and_index_names then
+                                        return nil
+                                    end
+                                    id_or_name = 'name'
+                                else
+                                    return nil
+                                end
+                                local idx_wrapper = setmetatable({
+                                    [id_or_name] = idx_key,
+                                    _id_or_name = idx_key,
+                                    space = space
+                                }, remote._index_mt)
+                                space.index[idx_key] = idx_wrapper
+                                return idx_wrapper
+                            end})
+                        local space_wrapper = setmetatable(space,
+                                                           remote._space_mt)
+                        remote.space[space_key] = space_wrapper
+                        return space_wrapper
+                    end})
+            end
         elseif what == 'did_fetch_schema' then
             remote:_install_schema(...)
         elseif what == 'event' then
@@ -932,6 +976,7 @@ function remote_methods:_install_schema(schema_version, spaces, indices,
         end
         s.id = id
         s.name = name
+        s._id_or_name = id
         s.engine = engine
         s.field_count = field_count
         s.enabled = true
@@ -969,6 +1014,7 @@ function remote_methods:_install_schema(schema_version, spaces, indices,
             space   = index[1],
             id      = index[2],
             name    = index[3],
+            _id_or_name = index[2],
             type    = string.upper(index[4]),
             parts   = {},
         }
@@ -1057,14 +1103,14 @@ space_metatable = function(remote)
         check_space_arg(self, 'insert')
         check_param_table(opts, REQUEST_OPTION_TYPES)
         return remote:_request(M_INSERT, opts, self._format_cdata,
-                               self._stream_id, self.id, tuple)
+                               self._stream_id, self._id_or_name, tuple)
     end
 
     function methods:replace(tuple, opts)
         check_space_arg(self, 'replace')
         check_param_table(opts, REQUEST_OPTION_TYPES)
         return remote:_request(M_REPLACE, opts, self._format_cdata,
-                               self._stream_id, self.id, tuple)
+                               self._stream_id, self._id_or_name, tuple)
     end
 
     function methods:select(key, opts)
@@ -1086,7 +1132,8 @@ space_metatable = function(remote)
         check_space_arg(self, 'upsert')
         check_param_table(opts, REQUEST_OPTION_TYPES)
         return nothing_or_data(remote:_request(M_UPSERT, opts, nil,
-                                               self._stream_id, self.id,
+                                               self._stream_id,
+                                               self._id_or_name,
                                                key, oplist))
     end
 
@@ -1125,8 +1172,9 @@ index_metatable = function(remote)
         local res
         local method = fetch_pos and M_SELECT_FETCH_POS or M_SELECT
         res = (remote:_request(method, opts, self.space._format_cdata,
-                               self._stream_id, self.space.id, self.id,
-                               iterator, offset, limit, key, after, fetch_pos))
+                               self._stream_id, self.space._id_or_name,
+                               self._id_or_name, iterator, offset, limit, key,
+                               after, fetch_pos))
         if type(res) ~= 'table' or not fetch_pos or opts and opts.is_async then
             return res
         end
@@ -1142,9 +1190,9 @@ index_metatable = function(remote)
         return nothing_or_data(remote:_request(M_GET, opts,
                                                self.space._format_cdata,
                                                self._stream_id,
-                                               self.space.id, self.id,
-                                               box.index.EQ, 0, 2, key,
-                                               nil, false))
+                                               self.space._id_or_name,
+                                               self._id_or_name, box.index.EQ,
+                                               0, 2, key, nil, false))
     end
 
     function methods:min(key, opts)
@@ -1156,9 +1204,9 @@ index_metatable = function(remote)
         return nothing_or_data(remote:_request(M_MIN, opts,
                                                self.space._format_cdata,
                                                self._stream_id,
-                                               self.space.id, self.id,
-                                               box.index.GE, 0, 1, key,
-                                               nil, false))
+                                               self.space._id_or_name,
+                                               self._id_or_name, box.index.GE,
+                                               0, 1, key, nil, false))
     end
 
     function methods:max(key, opts)
@@ -1170,9 +1218,9 @@ index_metatable = function(remote)
         return nothing_or_data(remote:_request(M_MAX, opts,
                                                self.space._format_cdata,
                                                self._stream_id,
-                                               self.space.id, self.id,
-                                               box.index.LE, 0, 1, key,
-                                               nil, false))
+                                               self.space._id_or_name,
+                                               self._id_or_name, box.index.LE,
+                                               0, 1, key, nil, false))
     end
 
     function methods:count(key, opts)
@@ -1181,8 +1229,12 @@ index_metatable = function(remote)
         if opts and opts.buffer then
             error("index:count() doesn't support `buffer` argument")
         end
-        local code = string.format('box.space.%s.index.%s:count',
-                                   self.space.name, self.name)
+        local code = 'box.space[' .. (self.space.name ~= nil and
+                                      '"' .. self.space.name .. '"' or
+                                      self.space.id) ..
+                     '].index[' .. (self.name ~= nil and
+                                    '"' .. self.name .. '"' or self.id) ..
+                     ']:count'
         return remote:_request(M_COUNT, opts, nil, self._stream_id,
                                code, { key, opts })
     end
@@ -1192,8 +1244,9 @@ index_metatable = function(remote)
         check_param_table(opts, REQUEST_OPTION_TYPES)
         return nothing_or_data(remote:_request(M_DELETE, opts,
                                                self.space._format_cdata,
-                                               self._stream_id, self.space.id,
-                                               self.id, key))
+                                               self._stream_id,
+                                               self.space._id_or_name,
+                                               self._id_or_name, key))
     end
 
     function methods:update(key, oplist, opts)
@@ -1201,8 +1254,9 @@ index_metatable = function(remote)
         check_param_table(opts, REQUEST_OPTION_TYPES)
         return nothing_or_data(remote:_request(M_UPDATE, opts,
                                                self.space._format_cdata,
-                                               self._stream_id, self.space.id,
-                                               self.id, key, oplist))
+                                               self._stream_id,
+                                               self.space._id_or_name,
+                                               self._id_or_name, key, oplist))
     end
 
     return { __index = methods, __metatable = false }
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index b8642d4e53..53d456b1b4 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -1858,7 +1858,7 @@ local port_c = ffi.cast('struct port_c *', port)
 
 -- Helper function to check space:method() usage
 local function check_space_arg(space, method)
-    if type(space) ~= 'table' or space.id == nil then
+    if type(space) ~= 'table' or (space.id == nil and space.name == nil) then
         local fmt = 'Use space:%s(...) instead of space.%s(...)'
         error(string.format(fmt, method, method))
     end
@@ -1877,7 +1877,7 @@ end
 
 -- Helper function to check index:method() usage
 local function check_index_arg(index, method)
-    if type(index) ~= 'table' or index.id == nil then
+    if type(index) ~= 'table' or (index.id == nil and index.name == nil) then
         local fmt = 'Use index:%s(...) instead of index.%s(...)'
         error(string.format(fmt, method, method))
     end
diff --git a/src/box/xrow.c b/src/box/xrow.c
index dcaec90607..a7805c1317 100644
--- a/src/box/xrow.c
+++ b/src/box/xrow.c
@@ -894,8 +894,8 @@ iproto_send_event(struct obuf *out, uint64_t sync,
 }
 
 int
-xrow_decode_dml(struct xrow_header *row, struct request *request,
-		uint64_t key_map)
+xrow_decode_dml_internal(struct xrow_header *row, struct request *request,
+			 uint64_t key_map, bool accept_space_name)
 {
 	memset(request, 0, sizeof(*request));
 	request->header = row;
@@ -925,7 +925,8 @@ xrow_decode_dml(struct xrow_header *row, struct request *request,
 		if (key >= iproto_key_MAX ||
 		    iproto_key_type[key] != mp_typeof(*value))
 			goto error;
-		key_map &= ~iproto_key_bit(key);
+		if (key < 64)
+			key_map &= ~iproto_key_bit(key);
 		switch (key) {
 		case IPROTO_SPACE_ID:
 			request->space_id = mp_decode_uint(&value);
@@ -980,10 +981,20 @@ xrow_decode_dml(struct xrow_header *row, struct request *request,
 			request->after_tuple = value;
 			request->after_tuple_end = data;
 			break;
+		case IPROTO_SPACE_NAME:
+			request->space_name =
+				mp_decode_str(&value, &request->space_name_len);
+			break;
+		case IPROTO_INDEX_NAME:
+			request->index_name =
+				mp_decode_str(&value, &request->index_name_len);
+			break;
 		default:
 			break;
 		}
 	}
+	if (accept_space_name && request->space_name != NULL)
+		key_map &= ~iproto_key_bit(IPROTO_SPACE_ID);
 done:
 	if (key_map) {
 		enum iproto_key key = (enum iproto_key) bit_ctz_u64(key_map);
diff --git a/src/box/xrow.h b/src/box/xrow.h
index e74ec75c49..a7b6190ce0 100644
--- a/src/box/xrow.h
+++ b/src/box/xrow.h
@@ -216,6 +216,14 @@ struct request {
 	int index_base;
 	/** Send position of last selected tuple in response if true. */
 	bool fetch_position;
+	/** Name of requested space, points to the request's input buffer. */
+	const char *space_name;
+	/** Length of @space_name. */
+	uint32_t space_name_len;
+	/** Name of requested index, points to the request's input buffer. */
+	const char *index_name;
+	/** Length of @index_name. */
+	uint32_t index_name_len;
 };
 
 /**
@@ -229,12 +237,33 @@ request_str(const struct request *request);
  * @param[out] request DML request to decode to.
  * @param key_map a bit map of keys that are required by the caller,
  *        @sa request_key_map().
+ * @param accept_space_name space name is accepted instead of space identifier.
  * @retval 0 on success
  * @retval -1 on error
  */
 int
+xrow_decode_dml_internal(struct xrow_header *xrow, struct request *request,
+			 uint64_t key_map, bool accept_space_name);
+
+/**
+ * Decode DML from system request (recovery or replication).
+ */
+static inline int
 xrow_decode_dml(struct xrow_header *xrow, struct request *request,
-		uint64_t key_map);
+		uint64_t key_map)
+{
+	return xrow_decode_dml_internal(xrow, request, key_map, false);
+}
+
+/**
+ * Decode DML from IPROTO request.
+ */
+static inline int
+xrow_decode_dml_iproto(struct xrow_header *xrow, struct request *request,
+		       uint64_t key_map)
+{
+	return xrow_decode_dml_internal(xrow, request, key_map, true);
+}
 
 /**
  * Encode the request fields to iovec using region_alloc().
diff --git a/test/box-luatest/gh_7894_export_iproto_constants_and_features_test.lua b/test/box-luatest/gh_7894_export_iproto_constants_and_features_test.lua
index e784ae7fab..457667775a 100644
--- a/test/box-luatest/gh_7894_export_iproto_constants_and_features_test.lua
+++ b/test/box-luatest/gh_7894_export_iproto_constants_and_features_test.lua
@@ -82,6 +82,8 @@ local reference_table = {
         AUTH_TYPE = 0x5b,
         REPLICASET_NAME = 0x5c,
         INSTANCE_NAME = 0x5d,
+        SPACE_NAME = 0x5e,
+        INDEX_NAME = 0x5f,
     },
 
     -- `iproto_metadata_key` enumeration.
@@ -160,7 +162,7 @@ local reference_table = {
     },
 
     -- `IPROTO_CURRENT_VERSION` constant
-    protocol_version = 4,
+    protocol_version = 5,
 
     -- `feature_id` enumeration
     protocol_features = {
@@ -169,6 +171,7 @@ local reference_table = {
         error_extension = true,
         watchers = true,
         pagination = true,
+        space_and_index_names = true,
     },
     feature = {
         streams = 0,
@@ -176,6 +179,7 @@ local reference_table = {
         error_extension = 2,
         watchers = 3,
         pagination = 4,
+        space_and_index_names = 5,
     },
 }
 
diff --git a/test/box-luatest/gh_8146_data/00000000000000000004.snap b/test/box-luatest/gh_8146_data/00000000000000000004.snap
new file mode 100644
index 0000000000000000000000000000000000000000..d5dfe125e06889e9caa9fe769c55b0eb11d0fb93
GIT binary patch
literal 5093
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
H)ex=i0PnX5

literal 0
HcmV?d00001

diff --git a/test/box-luatest/gh_8146_space_and_index_name_in_iproto_requests_test.lua b/test/box-luatest/gh_8146_space_and_index_name_in_iproto_requests_test.lua
new file mode 100644
index 0000000000..5f9dfb9332
--- /dev/null
+++ b/test/box-luatest/gh_8146_space_and_index_name_in_iproto_requests_test.lua
@@ -0,0 +1,304 @@
+local fio = require('fio')
+local msgpack = require('msgpack')
+local netbox = require('net.box')
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group(nil, t.helpers.matrix{connection = {'connection', 'stream'}})
+
+g.before_all(function(cg)
+    cg.server = server:new()
+    cg.server:start()
+end)
+
+g.after_all(function(cg)
+    cg.server:drop()
+end)
+
+-- Checks contents of net.box connections with disabled schema fetching.
+g.test_net_box_conn_with_disabled_schema_fetching = function(cg)
+    t.skip_if(cg.params.connection == 'stream')
+
+    local c = netbox:connect(cg.server.net_box_uri, {fetch_schema = false})
+    t.assert_equals(c.space, {})
+    t.assert_not_equals(getmetatable(c.space).__index, nil)
+    local s = c.space.s
+    t.assert_equals(c.space, {s = s})
+    t.assert_equals(s, {_id_or_name = 's', name = 's', index = {}})
+    t.assert_not_equals(getmetatable(s.index).__index, nil)
+    local i = s.index.i
+    t.assert_equals(s.index, {i = i})
+    t.assert_equals(i, {_id_or_name = 'i', name = 'i', space = s})
+
+    local s512 = c.space[512]
+    t.assert_equals(c.space, {[512] = s512, s = s})
+    t.assert_equals(s512, {_id_or_name = 512, id = 512, index = {}})
+    local i1 = s.index[1]
+    t.assert_equals(s.index, {[1] = i1, i = i})
+    t.assert_equals(i1, {_id_or_name = 1, id = 1, space = s})
+
+    t.assert_equals(c.space[{}], nil)
+    t.assert_equals(c.space[true], nil)
+    t.assert_equals(c.space[function() end], nil)
+    t.assert_equals(c.space.s.index[{}], nil)
+    t.assert_equals(c.space.s.index[true], nil)
+    t.assert_equals(c.space.s.index[function() end], nil)
+end
+
+g.before_test('test_net_box_conn_with_disabled_schema_fetching_errinj',
+              function(cg)
+    cg.server:exec(function()
+        box.error.injection.set('ERRINJ_IPROTO_FLIP_FEATURE',
+                                box.iproto.feature.space_and_index_names)
+    end)
+end)
+
+-- Checks contents of net.box connections with disabled schema fetching when
+-- space and index names IPROTO feature is disabled via error injection.
+g.test_net_box_conn_with_disabled_schema_fetching_errinj = function(cg)
+    t.tarantool.skip_if_not_debug()
+    t.skip_if(cg.params.connection == 'stream')
+
+    local c = netbox:connect(cg.server.net_box_uri, {fetch_schema = false})
+
+    t.assert_equals(c.space.s, nil)
+
+    local s512 = c.space[512]
+    t.assert_equals(c.space, {[512] = s512})
+    t.assert_equals(s512, {_id_or_name = 512, id = 512, index = {}})
+    t.assert_equals(s512.index.i, nil)
+    local i1 = s512.index[1]
+    t.assert_equals(s512.index, {[1] = i1})
+    t.assert_equals(i1, {_id_or_name = 1, id = 1, space = s512})
+end
+
+g.after_test('test_net_box_conn_with_disabled_schema_fetching_errinj',
+              function(cg)
+    cg.server:exec(function()
+        box.error.injection.set('ERRINJ_IPROTO_FLIP_FEATURE', -1)
+    end)
+end)
+
+g.before_test('test_net_box_conn_space_and_index_wrapper_tables', function(cg)
+    cg.s_id = cg.server:exec(function()
+        local s = box.schema.create_space('s')
+        s:create_index('i')
+        s:create_index('s', {parts = {2, 'unsigned'}})
+        s:insert{0, 1}
+        s:insert{1, 0}
+        local s_space = box.schema.create_space(' s ')
+        s_space:create_index(' i ')
+        s_space:insert{0}
+        return s.id
+    end)
+end)
+
+-- Checks that space and index wrapper tables of net.box connections with
+-- disabled schema fetching work correctly.
+g.test_net_box_conn_space_and_index_wrapper_tables = function(cg)
+    local c = netbox:connect(cg.server.net_box_uri, {fetch_schema = false})
+    if cg.params.connection == 'stream' then
+        c = c:new_stream()
+    end
+    local s = c.space.s
+    local i = s.index.i
+
+    t.assert_equals(s:select(), {{0, 1}, {1, 0}})
+    t.assert_equals(s:insert{2, 2}, {2, 2})
+    t.assert_equals(s:update({2}, {{'=', 2, 3}}), {2, 3})
+    t.assert_equals(i:update({2}, {{'=', 2, 2}}), {2, 2})
+    t.assert_equals(i:select({0}), {{0, 1}})
+    t.assert_equals(i:delete{2}, {2, 2})
+    t.assert_equals(i:count(), 2)
+    t.assert_equals(i:min(), {0, 1})
+    t.assert_equals(i:max(), {1, 0})
+    t.assert_equals(i:get{0}, {0, 1})
+
+    s = c.space[cg.s_id]
+    i = s.index[1]
+    t.assert_equals(s:replace{1, 2}, {1, 2})
+    t.assert_equals(i:select{2}, {{1, 2}})
+    t.assert_equals(s:update({0}, {{'=', 2, 3}}), {0, 3})
+    t.assert_equals(i:update({3}, {{'=', 2, 0}}), {0, 0})
+    t.assert_equals(s:upsert({2, 4}, {{'=', 2, 5}}), nil)
+    t.assert_equals(s:upsert({2, 4}, {{'=', 2, 5}}), nil)
+    t.assert_equals(i:delete{5}, {2, 5})
+    t.assert_equals(i:count(), 2)
+    t.assert_equals(i:min(), {0, 0})
+    t.assert_equals(i:max(), {1, 2})
+    t.assert_equals(i:get{2}, {1, 2})
+
+    t.assert_equals(c.space[cg.s_id].index.i:count(), 2)
+    t.assert_equals(c.space.s.index[1]:count(), 2)
+    t.assert_equals(c.space[' s '].index[' i ']:count(), 1)
+
+    local err_msg = "Space 'nonexistent' does not exist"
+    t.assert_error_msg_content_equals(err_msg, function()
+        c.space.nonexistent:select{}
+    end)
+    local err_msg = "Space 'nonexistent' does not exist"
+    t.assert_error_msg_content_equals(err_msg, function()
+        c.space.nonexistent:insert{}
+    end)
+    err_msg = "Space '777' does not exist"
+    t.assert_error_msg_content_equals(err_msg, function()
+        c.space[777]:select{}
+    end)
+    err_msg = "Space '777' does not exist"
+    t.assert_error_msg_content_equals(err_msg, function()
+        c.space[777]:insert{}
+    end)
+    err_msg = "Space '777' does not exist"
+    t.assert_error_msg_content_equals(err_msg, function()
+        c.space[777].index.i:select{}
+    end)
+    err_msg = "No index 'nonexistent' is defined in space 's'"
+    t.assert_error_msg_content_equals(err_msg, function()
+        c.space[cg.s_id].index.nonexistent:select{}
+    end)
+    err_msg = "No index #777 is defined in space 's'"
+    t.assert_error_msg_content_equals(err_msg, function()
+        c.space[cg.s_id].index[777]:select{}
+    end)
+end
+
+g.after_test('test_net_box_conn_space_and_index_wrapper_tables', function(cg)
+    cg.server:exec(function()
+        box.space.s:drop()
+        box.space[' s ']:drop()
+    end)
+end)
+
+g.before_test('test_space_and_index_name_resolution', function(cg)
+    cg.s_id, cg.s1_id, cg.s2_id = cg.server:exec(function()
+        local s = box.schema.create_space('s')
+        s:create_index('i1')
+        s:create_index('i2', {parts = {2}})
+        s:insert{1, 2}
+        s:insert{2, 1}
+        local s1 = box.schema.create_space('s1')
+        s1:create_index('i')
+        s1:insert{1}
+        local s2 = box.schema.create_space('s2')
+        s2:create_index('i')
+        s2:insert{2}
+        return s.id, s1.id
+    end)
+end)
+
+local function inject_select(c, sid, space_name, iid, idx_name, key)
+    local header = msgpack.encode({
+        [box.iproto.key.REQUEST_TYPE] = box.iproto.type.SELECT,
+        [box.iproto.key.SYNC] = c:_next_sync(),
+        [box.iproto.key.STREAM_ID] = c._stream_id or 0,
+    })
+    local body = msgpack.encode({
+        [box.iproto.key.SPACE_ID] = sid,
+        [box.iproto.key.SPACE_NAME] = space_name,
+        [box.iproto.key.INDEX_ID] = iid,
+        [box.iproto.key.INDEX_NAME] = idx_name,
+        [box.iproto.key.LIMIT] = 1,
+        [box.iproto.key.KEY] = setmetatable({key}, {__serialize = 'array'}),
+    })
+    local size = msgpack.encode(#header + #body)
+    local request = size .. header .. body
+    return c:_inject(request)
+end
+
+local function inject_insert_or_replace(c, request, space_name, index_name)
+    local header = msgpack.encode({
+        [box.iproto.key.REQUEST_TYPE] = box.iproto.type[request],
+        [box.iproto.key.SYNC] = c:_next_sync(),
+        [box.iproto.key.STREAM_ID] = c._stream_id or 0,
+    })
+    local body = msgpack.encode({
+        [box.iproto.key.SPACE_NAME] = space_name,
+        [box.iproto.key.INDEX_NAME] = index_name,
+        [box.iproto.key.TUPLE] = {3, 3},
+    })
+    local size = msgpack.encode(#header + #body)
+    local request = size .. header .. body
+    return c:_inject(request)
+end
+
+-- Checks that space and index name resolution works correctly.
+g.test_space_and_index_name_resolution = function(cg)
+    local c = netbox:connect(cg.server.net_box_uri, {fetch_schema = false})
+    if cg.params.connection == 'stream' then
+        c = c:new_stream()
+    end
+
+    t.assert_equals(inject_select(c, cg.s1_id, 's2'), {{2}})
+    t.assert_equals(inject_select(c, 777, 's2'), {{2}})
+    t.assert_equals(inject_select(c, 0, 's2'), {{2}})
+    t.assert_equals(inject_select(c, cg.s2_id, 's1'), {{1}})
+    t.assert_equals(inject_select(c, 777, 's1'), {{1}})
+    t.assert_equals(inject_select(c, 0, 's1'), {{1}})
+
+    t.assert_equals(inject_select(c, nil, 's', 0, 'i2', 2), {{1, 2}})
+    t.assert_equals(inject_select(c, nil, 's', 777, 'i2', 2), {{1, 2}})
+    t.assert_equals(inject_select(c, nil, 's', 1, 'i1', 2), {{2, 1}})
+    t.assert_equals(inject_select(c, nil, 's', 777, 'i1', 2), {{2, 1}})
+
+    local err_msg = "Space 'nonexistent' does not exist"
+    t.assert_error_msg_content_equals(err_msg, function()
+        inject_select(c, cg.s_id, 'nonexistent')
+    end)
+    err_msg = "No index 'nonexistent' is defined in space 's'"
+    t.assert_error_msg_content_equals(err_msg, function()
+        inject_select(c, nil, 's', 0, 'nonexistent')
+    end)
+
+    t.assert_equals(inject_insert_or_replace(c, 'INSERT', 's', 'nonexistent'),
+                    {{3, 3}})
+    t.assert_equals(inject_insert_or_replace(c, 'REPLACE', 's', 'nonexistent'),
+                    {{3, 3}})
+end
+
+g.after_test('test_space_and_index_name_resolution', function(cg)
+    cg.server:exec(function()
+        box.space.s:drop()
+    end)
+end)
+
+-- Checks that space name is not accepted instead of space identifier in
+-- recovery requests.
+--
+-- Snapshot generation instruction:
+-- 1. Patch this place make the replace request body contain IPROTO_SPACE_NAME
+-- instead OF IPROTO_SPACE_ID:
+-- luacheck: no max comment line length
+-- https://github.com/tarantool/tarantool/blob/5ce3114436bc94ab8414c88e4675e3e50923c199/src/box/iproto_constants.h#L497-L499
+-- For instance, add this snippet:
+-- ```
+--   if (space_id == 512) {
+--     body->k_space_id = 0x5c;
+--     body->m_space_id = 0xa4; /* 4-byte string */
+--     char space_name[5] = {};
+--     strcpy(space_name, "name");
+--     space_id = *(uint32_t *)space_name;
+--     body->v_space_id = space_id;
+--   } else {
+--     body->k_space_id = IPROTO_SPACE_ID;
+--     body->m_space_id = 0xce; /* uint32 */
+--     body->v_space_id = mp_bswap_u32(space_id);
+--   }
+-- ```
+-- 2. Build and run Tarantool, call `box.cfg`;
+-- 3. Create a user space, a primary index for it and insert a tuple into it.
+-- 4. Call `box.snapshot`.
+g.test_space_name_in_snapshot = function(cg)
+    local s = server:new{
+        alias = 'recovery_' .. cg.params.connection,
+        datadir = 'test/box-luatest/gh_8146_data'
+    }
+    s:start{wait_until_ready = false}
+    local log = fio.pathjoin(s.workdir, s.alias .. '.log')
+    t.helpers.retrying({}, function()
+        t.assert_not_equals(s:grep_log("can't initialize storage: " ..
+                                       "Missing mandatory field 'space id' " ..
+                                       "in request", nil,
+                                       {filename = log}), nil)
+    end)
+    s:drop()
+end
diff --git a/test/box-luatest/net_box_test.lua b/test/box-luatest/net_box_test.lua
index 47072dc6db..1eb9175dc7 100644
--- a/test/box-luatest/net_box_test.lua
+++ b/test/box-luatest/net_box_test.lua
@@ -268,7 +268,7 @@ g.test_schemaless = function()
     c = net.connect(g.server.net_box_uri, {fetch_schema = false})
     t.assert_equals(c.state, 'active')
     t.assert_equals(c.opts.fetch_schema, false)
-    t.assert_equals(c.space, nil)
+    t.assert_not_equals(c.space, nil)
 
     c:on_schema_reload(function()
         schema_update_counter = schema_update_counter + 1
diff --git a/test/box-py/iproto.result b/test/box-py/iproto.result
index 9c2f65c1ae..840fa71a80 100644
--- a/test/box-py/iproto.result
+++ b/test/box-py/iproto.result
@@ -210,9 +210,9 @@ Invalid MsgPack - request body
 # Invalid auth_type
 Invalid MsgPack - request body
 # Empty request body
-version=4, features=[0, 1, 2, 3, 4], auth_type=chap-sha1
+version=5, features=[0, 1, 2, 3, 4, 5], auth_type=chap-sha1
 # Unknown version and features
-version=4, features=[0, 1, 2, 3, 4], auth_type=chap-sha1
+version=5, features=[0, 1, 2, 3, 4, 5], auth_type=chap-sha1
 
 #
 # gh-6257 Watchers
diff --git a/test/box/net.box_iproto_id.result b/test/box/net.box_iproto_id.result
index 77e4a5d798..052b37b45a 100644
--- a/test/box/net.box_iproto_id.result
+++ b/test/box/net.box_iproto_id.result
@@ -15,7 +15,7 @@ c = net.connect(box.cfg.listen)
  | ...
 c.peer_protocol_version
  | ---
- | - 4
+ | - 5
  | ...
 c.peer_protocol_features
  | ---
@@ -24,6 +24,7 @@ c.peer_protocol_features
  |   error_extension: true
  |   streams: true
  |   pagination: true
+ |   space_and_index_names: true
  | ...
 c:close()
  | ---
@@ -52,6 +53,7 @@ c.peer_protocol_features
  |   error_extension: false
  |   streams: false
  |   pagination: false
+ |   space_and_index_names: false
  | ...
 errinj.set('ERRINJ_IPROTO_DISABLE_ID', false)
  | ---
@@ -101,6 +103,7 @@ c.peer_protocol_features
  |   error_extension: true
  |   streams: true
  |   pagination: true
+ |   space_and_index_names: true
  | ...
 c:close()
  | ---
@@ -146,7 +149,7 @@ c.error -- error
  | ...
 c.peer_protocol_version
  | ---
- | - 4
+ | - 5
  | ...
 c.peer_protocol_features
  | ---
@@ -155,6 +158,7 @@ c.peer_protocol_features
  |   error_extension: true
  |   streams: true
  |   pagination: true
+ |   space_and_index_names: true
  | ...
 c:close()
  | ---
@@ -173,7 +177,7 @@ c.error -- error
  | ...
 c.peer_protocol_version
  | ---
- | - 4
+ | - 5
  | ...
 c.peer_protocol_features
  | ---
@@ -182,6 +186,7 @@ c.peer_protocol_features
  |   error_extension: true
  |   streams: true
  |   pagination: true
+ |   space_and_index_names: true
  | ...
 c:close()
  | ---
-- 
GitLab