Tarantool development patches archive
 help / color / mirror / Atom feed
From: Nikita Pettik <korablev@tarantool.org>
To: tarantool-patches@dev.tarantool.org
Cc: v.shpilevoy@tarantool.org
Subject: [Tarantool-patches] [PATCH v2 3/6] sql: extend result set with collation
Date: Wed, 11 Dec 2019 16:44:55 +0300	[thread overview]
Message-ID: <a845ba0c6cdfa047ac4d921a5fd7bd82a39be8d2.1576071711.git.korablev@tarantool.org> (raw)
In-Reply-To: <cover.1576022424.git.korablev@tarantool.org>
In-Reply-To: <cover.1576071711.git.korablev@tarantool.org>

If resulting set column is of STRING type and features collation (no
matter explicit or implicit) different from "none", then metadata will
contain its name.

This patch also introduces new pragma: full_metadata. By default it is
not set. If it is turned on, then optional metadata (like collation) is
pushed to Lua stack. Note that via IProto protocol always full metadata
is send, but its decoding depends on session SQL settings.

Part of #4407
---
 src/box/execute.c               |  32 +++++++++---
 src/box/iproto_constants.h      |   1 +
 src/box/lua/execute.c           |   8 ++-
 src/box/lua/net_box.c           |  23 +++++++--
 src/box/sql/pragma.h            |   8 +++
 src/box/sql/select.c            |  19 +++++++
 src/box/sql/sqlInt.h            |   7 +++
 src/box/sql/vdbe.h              |   4 ++
 src/box/sql/vdbeInt.h           |   1 +
 src/box/sql/vdbeapi.c           |   9 ++++
 src/box/sql/vdbeaux.c           |  16 ++++++
 test/sql/engine.cfg             |   4 ++
 test/sql/full_metadata.result   | 106 ++++++++++++++++++++++++++++++++++++++++
 test/sql/full_metadata.test.lua |  43 ++++++++++++++++
 test/sql/sql-debug.result       |   1 +
 15 files changed, 272 insertions(+), 10 deletions(-)
 create mode 100644 test/sql/full_metadata.result
 create mode 100644 test/sql/full_metadata.test.lua

diff --git a/src/box/execute.c b/src/box/execute.c
index e8b012e5b..72300235a 100644
--- a/src/box/execute.c
+++ b/src/box/execute.c
@@ -268,6 +268,24 @@ error:
 	return -1;
 }
 
+static size_t
+metadata_map_sizeof(const char *name, const char *type, const char *coll)
+{
+	uint32_t members_count = 2;
+	size_t map_size = 0;
+	if (coll != NULL) {
+		members_count++;
+		map_size += mp_sizeof_uint(IPROTO_FIELD_COLL);
+		map_size += mp_sizeof_str(strlen(coll));
+	}
+	map_size += mp_sizeof_uint(IPROTO_FIELD_NAME);
+	map_size += mp_sizeof_uint(IPROTO_FIELD_TYPE);
+	map_size += mp_sizeof_str(strlen(name));
+	map_size += mp_sizeof_str(strlen(type));
+	map_size += mp_sizeof_map(members_count);
+	return map_size;
+}
+
 /**
  * Serialize a description of the prepared statement.
  * @param stmt Prepared statement.
@@ -291,9 +309,7 @@ sql_get_metadata(struct sql_stmt *stmt, struct obuf *out, int column_count)
 	pos = mp_encode_uint(pos, IPROTO_METADATA);
 	pos = mp_encode_array(pos, column_count);
 	for (int i = 0; i < column_count; ++i) {
-		size_t size = mp_sizeof_map(2) +
-			      mp_sizeof_uint(IPROTO_FIELD_NAME) +
-			      mp_sizeof_uint(IPROTO_FIELD_TYPE);
+		const char *coll = sql_column_coll(stmt, i);
 		const char *name = sql_column_name(stmt, i);
 		const char *type = sql_column_datatype(stmt, i);
 		/*
@@ -303,18 +319,22 @@ sql_get_metadata(struct sql_stmt *stmt, struct obuf *out, int column_count)
 		 */
 		assert(name != NULL);
 		assert(type != NULL);
-		size += mp_sizeof_str(strlen(name));
-		size += mp_sizeof_str(strlen(type));
+		size = metadata_map_sizeof(name, type, coll);
 		char *pos = (char *) obuf_alloc(out, size);
 		if (pos == NULL) {
 			diag_set(OutOfMemory, size, "obuf_alloc", "pos");
 			return -1;
 		}
-		pos = mp_encode_map(pos, 2);
+		uint32_t map_sz = 2 + (coll != NULL);
+		pos = mp_encode_map(pos, map_sz);
 		pos = mp_encode_uint(pos, IPROTO_FIELD_NAME);
 		pos = mp_encode_str(pos, name, strlen(name));
 		pos = mp_encode_uint(pos, IPROTO_FIELD_TYPE);
 		pos = mp_encode_str(pos, type, strlen(type));
+		if (coll != NULL) {
+			pos = mp_encode_uint(pos, IPROTO_FIELD_COLL);
+			pos = mp_encode_str(pos, coll, strlen(coll));
+		}
 	}
 	return 0;
 }
diff --git a/src/box/iproto_constants.h b/src/box/iproto_constants.h
index 5e8a7d483..fa9c029a2 100644
--- a/src/box/iproto_constants.h
+++ b/src/box/iproto_constants.h
@@ -131,6 +131,7 @@ enum iproto_key {
 enum iproto_metadata_key {
 	IPROTO_FIELD_NAME = 0,
 	IPROTO_FIELD_TYPE = 1,
+	IPROTO_FIELD_COLL = 2,
 };
 
 enum iproto_ballot_key {
diff --git a/src/box/lua/execute.c b/src/box/lua/execute.c
index ffa3d4d2e..55b8a8fef 100644
--- a/src/box/lua/execute.c
+++ b/src/box/lua/execute.c
@@ -20,9 +20,11 @@ lua_sql_get_metadata(struct sql_stmt *stmt, struct lua_State *L,
 	assert(column_count > 0);
 	lua_createtable(L, column_count, 0);
 	for (int i = 0; i < column_count; ++i) {
-		lua_createtable(L, 0, 2);
+		const char *coll = sql_column_coll(stmt, i);
 		const char *name = sql_column_name(stmt, i);
 		const char *type = sql_column_datatype(stmt, i);
+		size_t table_sz = 2 + (coll != NULL);
+		lua_createtable(L, 0, table_sz);
 		/*
 		 * Can not fail, since all column names are
 		 * preallocated during prepare phase and the
@@ -34,6 +36,10 @@ lua_sql_get_metadata(struct sql_stmt *stmt, struct lua_State *L,
 		lua_setfield(L, -2, "name");
 		lua_pushstring(L, type);
 		lua_setfield(L, -2, "type");
+		if (coll != NULL) {
+			lua_pushstring(L, coll);
+			lua_setfield(L, -2, "collation");
+		}
 		lua_rawseti(L, -2, i + 1);
 	}
 }
diff --git a/src/box/lua/net_box.c b/src/box/lua/net_box.c
index 001af95dc..025143307 100644
--- a/src/box/lua/net_box.c
+++ b/src/box/lua/net_box.c
@@ -638,6 +638,23 @@ netbox_decode_select(struct lua_State *L)
 	return 2;
 }
 
+/** Decode optional (i.e. may be present in response) metadata fields. */
+static void
+decode_metadata_optional(struct lua_State *L, const char **data,
+			 uint32_t map_size)
+{
+	/* 2 is default metadata map size (field name + field size). */
+	while (map_size-- > 2) {
+		uint32_t key = mp_decode_uint(data);
+		uint32_t len;
+		if (key == IPROTO_FIELD_COLL) {
+			const char *coll = mp_decode_str(data, &len);
+			lua_pushlstring(L, coll, len);
+			lua_setfield(L, -2, "collation");
+		}
+	}
+}
+
 /**
  * Decode IPROTO_METADATA into array of maps.
  * @param L Lua stack to push result on.
@@ -650,12 +667,11 @@ netbox_decode_metadata(struct lua_State *L, const char **data)
 	lua_createtable(L, count, 0);
 	for (uint32_t i = 0; i < count; ++i) {
 		uint32_t map_size = mp_decode_map(data);
-		assert(map_size == 2);
-		(void) map_size;
+		assert(map_size == 2 || map_size == 3);
 		uint32_t key = mp_decode_uint(data);
 		assert(key == IPROTO_FIELD_NAME);
 		(void) key;
-		lua_createtable(L, 0, 1);
+		lua_createtable(L, 0, map_size);
 		uint32_t len;
 		const char *str = mp_decode_str(data, &len);
 		lua_pushlstring(L, str, len);
@@ -665,6 +681,7 @@ netbox_decode_metadata(struct lua_State *L, const char **data)
 		const char *type = mp_decode_str(data, &len);
 		lua_pushlstring(L, type, len);
 		lua_setfield(L, -2, "type");
+		decode_metadata_optional(L, data, map_size);
 		lua_rawseti(L, -2, i + 1);
 	}
 }
diff --git a/src/box/sql/pragma.h b/src/box/sql/pragma.h
index 30a1fc41a..aa9377a13 100644
--- a/src/box/sql/pragma.h
+++ b/src/box/sql/pragma.h
@@ -144,6 +144,9 @@ static const char *const pragCName[] = {
 	/* Used by: where_trace */
 	/*  90 */ "where_trace",
 	/*  91 */ "integer",
+	/* Used by: full_metadata */
+	/*  92 */ "full_metadata",
+	/*  93 */ "integer",
 };
 
 /* Definitions of all built-in pragmas */
@@ -186,6 +189,11 @@ static const PragmaName aPragmaName[] = {
 	 /* ePragFlg:  */ PragFlg_Result0 | PragFlg_NoColumns1,
 	 /* ColNames:  */ 64, 1,
 	 /* iArg:      */ SQL_FullColNames},
+	{ /* zName:     */ "full_metadata",
+	  /* ePragTyp:  */ PragTyp_FLAG,
+	  /* ePragFlg:  */ PragFlg_Result0 | PragFlg_NoColumns1,
+	  /* ColNames:  */ 92, 1,
+	  /* iArg:      */ SQL_FullMetadata},
 	{ /* zName:     */ "index_info",
 	 /* ePragTyp:  */ PragTyp_INDEX_INFO,
 	 /* ePragFlg:  */
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index e4768121e..506c1a7c7 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -1783,6 +1783,7 @@ generate_column_metadata(struct Parse *pParse, struct SrcList *pTabList,
 	pParse->colNamesSet = 1;
 	fullNames = (pParse->sql_flags & SQL_FullColNames) != 0;
 	shortNames = (pParse->sql_flags & SQL_ShortColNames) != 0;
+	bool is_full_meta = (pParse->sql_flags & SQL_FullMetadata) != 0;
 	sqlVdbeSetNumCols(v, pEList->nExpr);
 	uint32_t var_count = 0;
 	for (i = 0; i < pEList->nExpr; i++) {
@@ -1794,6 +1795,24 @@ generate_column_metadata(struct Parse *pParse, struct SrcList *pTabList,
 			var_pos[var_count++] = i;
 		vdbe_metadata_set_col_type(v, i,
 					   field_type_strs[sql_expr_type(p)]);
+		if (is_full_meta && sql_expr_type(p) == FIELD_TYPE_STRING) {
+			bool unused;
+			uint32_t id;
+			struct coll *coll = NULL;
+			/*
+			 * If sql_expr_coll fails then it fails somewhere
+			 * above the call stack.
+			 */
+			int rc =  sql_expr_coll(pParse, p, &unused, &id, &coll);
+			assert(rc == 0);
+			(void) rc;
+			if (id != COLL_NONE) {
+				struct coll_id *coll_id = coll_by_id(id);
+				vdbe_metadata_set_col_collation(v, i,
+								coll_id->name,
+								coll_id->name_len);
+			}
+		}
 		if (p->op == TK_COLUMN || p->op == TK_AGG_COLUMN) {
 			char *zCol;
 			int iCol = p->iColumn;
diff --git a/src/box/sql/sqlInt.h b/src/box/sql/sqlInt.h
index 2594b73e0..e9ad068f8 100644
--- a/src/box/sql/sqlInt.h
+++ b/src/box/sql/sqlInt.h
@@ -576,6 +576,9 @@ sql_column_name(sql_stmt *, int N);
 const char *
 sql_column_datatype(sql_stmt *, int N);
 
+const char *
+sql_column_coll(sql_stmt *stmt, int n);
+
 int
 sql_initialize(void);
 
@@ -1181,6 +1184,10 @@ struct sql {
 #define SQL_EnableTrigger  0x01000000	/* True to enable triggers */
 #define SQL_DeferFKs       0x02000000	/* Defer all FK constraints */
 #define SQL_VdbeEQP        0x08000000	/* Debug EXPLAIN QUERY PLAN */
+#define SQL_FullMetadata   0x04000000	/* Display optional properties
+					 * (nullability, autoincrement, alias)
+					 * in metadata.
+					 */
 
 /* Bits of the sql.dbOptFlags field. */
 #define SQL_QueryFlattener 0x0001	/* Query flattening */
diff --git a/src/box/sql/vdbe.h b/src/box/sql/vdbe.h
index ddced20e1..22ba0b756 100644
--- a/src/box/sql/vdbe.h
+++ b/src/box/sql/vdbe.h
@@ -253,6 +253,10 @@ vdbe_metadata_set_col_name(struct Vdbe *v, int col_idx, const char *name);
 int
 vdbe_metadata_set_col_type(struct Vdbe *v, int col_idx, const char *type);
 
+int
+vdbe_metadata_set_col_collation(struct Vdbe *p, int idx, const char *coll,
+				size_t coll_len);
+
 void sqlVdbeCountChanges(Vdbe *);
 sql *sqlVdbeDb(Vdbe *);
 void sqlVdbeSetSql(Vdbe *, const char *z, int n, int);
diff --git a/src/box/sql/vdbeInt.h b/src/box/sql/vdbeInt.h
index 64250bee2..d3de5770b 100644
--- a/src/box/sql/vdbeInt.h
+++ b/src/box/sql/vdbeInt.h
@@ -349,6 +349,7 @@ struct ScanStatus {
 struct sql_column_metadata {
 	char *name;
 	char *type;
+	char *collation;
 };
 
 /*
diff --git a/src/box/sql/vdbeapi.c b/src/box/sql/vdbeapi.c
index 48db0bf43..e57a80334 100644
--- a/src/box/sql/vdbeapi.c
+++ b/src/box/sql/vdbeapi.c
@@ -745,6 +745,15 @@ sql_column_datatype(sql_stmt *stmt, int n)
 	return p->metadata[n].type;
 }
 
+const char *
+sql_column_coll(sql_stmt *stmt, int n)
+{
+	struct Vdbe *p = (struct Vdbe *) stmt;
+	assert(n < sql_column_count(stmt) && n >= 0);
+	return p->metadata[n].collation;
+}
+
+
 /******************************* sql_bind_  **************************
  *
  * Routines used to attach values to wildcards in a compiled SQL statement.
diff --git a/src/box/sql/vdbeaux.c b/src/box/sql/vdbeaux.c
index f2cf386bb..02d9fc6ce 100644
--- a/src/box/sql/vdbeaux.c
+++ b/src/box/sql/vdbeaux.c
@@ -1834,6 +1834,7 @@ vdbe_metadata_delete(struct Vdbe *v)
 		for (int i = 0; i < v->nResColumn; ++i) {
 			free(v->metadata[i].name);
 			free(v->metadata[i].type);
+			free(v->metadata[i].collation);
 		}
 		free(v->metadata);
 	}
@@ -1888,6 +1889,21 @@ vdbe_metadata_set_col_type(struct Vdbe *p, int idx, const char *type)
 	return 0;
 }
 
+int
+vdbe_metadata_set_col_collation(struct Vdbe *p, int idx, const char *coll,
+				size_t coll_len)
+{
+	assert(idx < p->nResColumn);
+	if (p->metadata[idx].collation != NULL)
+		free((void *)p->metadata[idx].collation);
+	p->metadata[idx].collation = strndup(coll, coll_len);
+	if (p->metadata[idx].collation == NULL) {
+		diag_set(OutOfMemory, coll_len + 1, "strndup", "collation");
+		return -1;
+	}
+	return 0;
+}
+
 /*
  * This routine checks that the sql.nVdbeActive count variable
  * matches the number of vdbe's in the list sql.pVdbe that are
diff --git a/test/sql/engine.cfg b/test/sql/engine.cfg
index 284c42082..efc42a114 100644
--- a/test/sql/engine.cfg
+++ b/test/sql/engine.cfg
@@ -9,6 +9,10 @@
         "remote": {"remote": "true"},
         "local": {"remote": "false"}
     },
+    "full_metadata.test.lua": {
+        "remote": {"remote": "true"},
+        "local": {"remote": "false"}
+    },
     "*": {
         "memtx": {"engine": "memtx"},
         "vinyl": {"engine": "vinyl"}
diff --git a/test/sql/full_metadata.result b/test/sql/full_metadata.result
new file mode 100644
index 000000000..aebb938fe
--- /dev/null
+++ b/test/sql/full_metadata.result
@@ -0,0 +1,106 @@
+-- test-run result file version 2
+netbox = require('net.box')
+ | ---
+ | ...
+test_run = require('test_run').new()
+ | ---
+ | ...
+
+box.execute("CREATE TABLE t (id INT PRIMARY KEY AUTOINCREMENT, a INT NOT NULL, c TEXT COLLATE \"unicode_ci\");")
+ | ---
+ | - row_count: 1
+ | ...
+box.execute("INSERT INTO t VALUES (1, 1, 'aSd');")
+ | ---
+ | - row_count: 1
+ | ...
+
+remote = test_run:get_cfg('remote') == 'true'
+ | ---
+ | ...
+execute = nil
+ | ---
+ | ...
+test_run:cmd("setopt delimiter ';'")
+ | ---
+ | - true
+ | ...
+if remote then
+	box.schema.user.grant('guest','read, write, execute', 'universe')
+	box.schema.user.grant('guest', 'create', 'space')
+	cn = netbox.connect(box.cfg.listen)
+	execute = function(...) return cn:execute(...) end
+else
+	execute = function(...)
+		local res, err = box.execute(...)
+		if err ~= nil then
+			error(err)
+		end
+		return res
+	end
+end;
+ | ---
+ | ...
+test_run:cmd("setopt delimiter ''");
+ | ---
+ | - true
+ | ...
+
+execute("PRAGMA full_metadata = true;")
+ | ---
+ | - row_count: 0
+ | ...
+-- Make sure collation is presented in extended metadata.
+--
+execute("SELECT 'aSd' COLLATE \"unicode_ci\";")
+ | ---
+ | - metadata:
+ |   - type: string
+ |     name: '''aSd'' COLLATE "unicode_ci"'
+ |     collation: unicode_ci
+ |   rows:
+ |   - ['aSd']
+ | ...
+execute("SELECT c FROM t;")
+ | ---
+ | - metadata:
+ |   - type: string
+ |     name: C
+ |     collation: unicode_ci
+ |   rows:
+ |   - ['aSd']
+ | ...
+execute("SELECT c COLLATE \"unicode\" FROM t;")
+ | ---
+ | - metadata:
+ |   - type: string
+ |     name: c COLLATE "unicode"
+ |     collation: unicode
+ |   rows:
+ |   - ['aSd']
+ | ...
+
+execute("PRAGMA full_metadata = false;")
+ | ---
+ | - row_count: 0
+ | ...
+
+test_run:cmd("setopt delimiter ';'")
+ | ---
+ | - true
+ | ...
+if remote then
+	cn:close()
+	box.schema.user.revoke('guest', 'read, write, execute', 'universe')
+	box.schema.user.revoke('guest', 'create', 'space')
+end;
+ | ---
+ | ...
+test_run:cmd("setopt delimiter ''");
+ | ---
+ | - true
+ | ...
+
+box.space.T:drop()
+ | ---
+ | ...
diff --git a/test/sql/full_metadata.test.lua b/test/sql/full_metadata.test.lua
new file mode 100644
index 000000000..4aa2492a1
--- /dev/null
+++ b/test/sql/full_metadata.test.lua
@@ -0,0 +1,43 @@
+netbox = require('net.box')
+test_run = require('test_run').new()
+
+box.execute("CREATE TABLE t (id INT PRIMARY KEY AUTOINCREMENT, a INT NOT NULL, c TEXT COLLATE \"unicode_ci\");")
+box.execute("INSERT INTO t VALUES (1, 1, 'aSd');")
+
+remote = test_run:get_cfg('remote') == 'true'
+execute = nil
+test_run:cmd("setopt delimiter ';'")
+if remote then
+	box.schema.user.grant('guest','read, write, execute', 'universe')
+	box.schema.user.grant('guest', 'create', 'space')
+	cn = netbox.connect(box.cfg.listen)
+	execute = function(...) return cn:execute(...) end
+else
+	execute = function(...)
+		local res, err = box.execute(...)
+		if err ~= nil then
+			error(err)
+		end
+		return res
+	end
+end;
+test_run:cmd("setopt delimiter ''");
+
+execute("PRAGMA full_metadata = true;")
+-- Make sure collation is presented in extended metadata.
+--
+execute("SELECT 'aSd' COLLATE \"unicode_ci\";")
+execute("SELECT c FROM t;")
+execute("SELECT c COLLATE \"unicode\" FROM t;")
+
+execute("PRAGMA full_metadata = false;")
+
+test_run:cmd("setopt delimiter ';'")
+if remote then
+	cn:close()
+	box.schema.user.revoke('guest', 'read, write, execute', 'universe')
+	box.schema.user.revoke('guest', 'create', 'space')
+end;
+test_run:cmd("setopt delimiter ''");
+
+box.space.T:drop()
diff --git a/test/sql/sql-debug.result b/test/sql/sql-debug.result
index b19075366..a1b97663c 100644
--- a/test/sql/sql-debug.result
+++ b/test/sql/sql-debug.result
@@ -41,6 +41,7 @@ box.execute('PRAGMA')
   - ['count_changes', 0]
   - ['defer_foreign_keys', 0]
   - ['full_column_names', 0]
+  - ['full_metadata', 0]
   - ['parser_trace', 0]
   - ['recursive_triggers', 1]
   - ['reverse_unordered_selects', 0]
-- 
2.15.1

  parent reply	other threads:[~2019-12-11 13:45 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-12-11 13:44 [Tarantool-patches] [PATCH v2 0/6] sql: extended metadata Nikita Pettik
     [not found] ` <cover.1576071711.git.korablev@tarantool.org>
2019-12-11 13:44   ` [Tarantool-patches] [PATCH v2 1/6] sql: refactor resulting set metadata Nikita Pettik
2019-12-11 13:44   ` [Tarantool-patches] [PATCH v2 2/6] sql: fix possible null dereference in sql_expr_coll() Nikita Pettik
2019-12-11 13:44   ` Nikita Pettik [this message]
2019-12-18  0:29     ` [Tarantool-patches] [PATCH v2 3/6] sql: extend result set with collation Vladislav Shpilevoy
2019-12-24  0:26       ` Nikita Pettik
2019-12-24 15:30         ` Vladislav Shpilevoy
2019-12-11 13:44   ` [Tarantool-patches] [PATCH v2 4/6] sql: extend result set with nullability Nikita Pettik
2019-12-18  0:29     ` Vladislav Shpilevoy
2019-12-24  0:26       ` Nikita Pettik
2019-12-11 13:44   ` [Tarantool-patches] [PATCH v2 5/6] sql: extend result set with autoincrement Nikita Pettik
2019-12-18  0:29     ` Vladislav Shpilevoy
2019-12-24  0:26       ` Nikita Pettik
2019-12-24 15:30         ` Vladislav Shpilevoy
2019-12-25 12:17           ` Nikita Pettik
2019-12-25 15:40             ` Vladislav Shpilevoy
2019-12-25 23:18               ` Nikita Pettik
2019-12-11 13:44   ` [Tarantool-patches] [PATCH v2 6/6] sql: extend result set with alias Nikita Pettik
2019-12-18  0:29     ` Vladislav Shpilevoy
2019-12-24  0:26       ` Nikita Pettik
2019-12-24 15:34         ` Vladislav Shpilevoy
2019-12-26 11:24           ` Nikita Pettik
2019-12-27 11:53             ` Vladislav Shpilevoy
2019-12-27 23:44             ` Nikita Pettik
2019-12-28  9:29               ` Vladislav Shpilevoy

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=a845ba0c6cdfa047ac4d921a5fd7bd82a39be8d2.1576071711.git.korablev@tarantool.org \
    --to=korablev@tarantool.org \
    --cc=tarantool-patches@dev.tarantool.org \
    --cc=v.shpilevoy@tarantool.org \
    --subject='Re: [Tarantool-patches] [PATCH v2 3/6] sql: extend result set with collation' \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

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