* [PATCH 1/4] schema: use tuple field names in Lua
  2019-05-15 10:33 [PATCH 0/4] A few fixes/improvements for autoincrement indexes Vladimir Davydov
@ 2019-05-15 10:33 ` Vladimir Davydov
  2019-05-15 10:33 ` [PATCH 2/4] schema: fix error while altering index with sequence Vladimir Davydov
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 17+ messages in thread
From: Vladimir Davydov @ 2019-05-15 10:33 UTC (permalink / raw)
  To: tarantool-patches
When schema.lua was introduced, there was no such thing as space format
and we had to access tuple fields by no. Now we can use human readable
names. Let's do it - this should improve code readability.
A note about box/alter.test.lua: for some reason it clears format of
_space and _index system spaces, which apparently breaks our assumption
about field names. Let's zap those pointless test cases.
---
 src/box/lua/schema.lua  | 98 ++++++++++++++++++++++++-------------------------
 test/box/alter.result   | 15 --------
 test/box/alter.test.lua |  6 ---
 3 files changed, 49 insertions(+), 70 deletions(-)
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index f31cf7f2..036e5f2d 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -155,7 +155,7 @@ local function user_or_role_resolve(user)
     if tuple == nil then
         return nil
     end
-    return tuple[1]
+    return tuple.id
 end
 
 local function role_resolve(name_or_id)
@@ -166,10 +166,10 @@ local function role_resolve(name_or_id)
     elseif type(name_or_id) ~= 'nil' then
         tuple = _vuser:get{name_or_id}
     end
-    if tuple == nil or tuple[4] ~= 'role' then
+    if tuple == nil or tuple.type ~= 'role' then
         return nil
     else
-        return tuple[1]
+        return tuple.id
     end
 end
 
@@ -181,10 +181,10 @@ local function user_resolve(name_or_id)
     elseif type(name_or_id) ~= 'nil' then
         tuple = _vuser:get{name_or_id}
     end
-    if tuple == nil or tuple[4] ~= 'user' then
+    if tuple == nil or tuple.type ~= 'user' then
         return nil
     else
-        return tuple[1]
+        return tuple.id
     end
 end
 
@@ -197,7 +197,7 @@ local function sequence_resolve(name_or_id)
         tuple = _vsequence:get{name_or_id}
     end
     if tuple ~= nil then
-        return tuple[1], tuple
+        return tuple.id, tuple
     else
         return nil
     end
@@ -209,7 +209,7 @@ local function revoke_object_privs(object_type, object_id)
     local _priv = box.space[box.schema.PRIV_ID]
     local privs = _vpriv.index.object:select{object_type, object_id}
     for k, tuple in pairs(privs) do
-        local uid = tuple[2]
+        local uid = tuple.grantee
         _priv:delete{uid, object_type, object_id}
     end
 end
@@ -453,13 +453,13 @@ box.schema.space.create = function(name, options)
         local _schema = box.space._schema
         local max_id = _schema:update({'max_id'}, {{'+', 2, 1}})
         if max_id == nil then
-            id = _space.index.primary:max()[1]
+            id = _space.index.primary:max().id
             if id < box.schema.SYSTEM_ID_MAX then
                 id = box.schema.SYSTEM_ID_MAX
             end
             max_id = _schema:insert{'max_id', id + 1}
         end
-        id = max_id[2]
+        id = max_id.value
     end
     local uid = session.euid()
     if options.user then
@@ -492,7 +492,7 @@ function box.schema.space.format(id, format)
         if tuple == nil then
             box.error(box.error.NO_SUCH_SPACE, '#' .. tostring(id))
         end
-        return tuple[7]
+        return tuple.format
     else
         check_param(format, 'format', 'table')
         format = update_format(format)
@@ -514,9 +514,9 @@ box.schema.space.drop = function(space_id, space_name, opts)
     local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
     local _fk_constraint = box.space[box.schema.FK_CONSTRAINT_ID]
     local sequence_tuple = _space_sequence:delete{space_id}
-    if sequence_tuple ~= nil and sequence_tuple[3] == true then
+    if sequence_tuple ~= nil and sequence_tuple.is_generated == true then
         -- Delete automatically generated sequence.
-        box.schema.sequence.drop(sequence_tuple[2])
+        box.schema.sequence.drop(sequence_tuple.sequence_id)
     end
     for _, t in _trigger.index.space_id:pairs({space_id}) do
         _trigger:delete({t.name})
@@ -527,7 +527,7 @@ box.schema.space.drop = function(space_id, space_name, opts)
     local keys = _vindex:select(space_id)
     for i = #keys, 1, -1 do
         local v = keys[i]
-        _index:delete{v[1], v[2]}
+        _index:delete{v.id, v.iid}
     end
     revoke_object_privs('space', space_id)
     _truncate:delete{space_id}
@@ -847,9 +847,9 @@ box.schema.index.create = function(space_id, name, options)
         local tuple = _vindex.index[0]
             :select(space_id, { limit = 1, iterator = 'LE' })[1]
         if tuple then
-            local id = tuple[1]
+            local id = tuple.id
             if id == space_id then
-                iid = tuple[2] + 1
+                iid = tuple.iid + 1
             end
         end
     end
@@ -923,9 +923,9 @@ box.schema.index.drop = function(space_id, index_id)
     if index_id == 0 then
         local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
         local sequence_tuple = _space_sequence:delete{space_id}
-        if sequence_tuple ~= nil and sequence_tuple[3] == true then
+        if sequence_tuple ~= nil and sequence_tuple.is_generated == true then
             -- Delete automatically generated sequence.
-            box.schema.sequence.drop(sequence_tuple[2])
+            box.schema.sequence.drop(sequence_tuple.sequence_id)
         end
     end
     local _index = box.space[box.schema.INDEX_ID]
@@ -993,25 +993,23 @@ box.schema.index.alter = function(space_id, index_id, options)
     local tuple = _index:get{space_id, index_id }
     local parts = {}
     local index_opts = {}
-    local OPTS = 5
-    local PARTS = 6
-    if type(tuple[OPTS]) == 'number' then
+    if type(tuple.opts) == 'number' then
         -- old format
-        index_opts.unique = tuple[OPTS] == 1
-        local part_count = tuple[PARTS]
+        index_opts.unique = tuple[5] == 1
+        local part_count = tuple[6]
         for i = 1, part_count do
             table.insert(parts, {tuple[2 * i + 4], tuple[2 * i + 5]});
         end
     else
         -- new format
-        index_opts = tuple[OPTS]
-        parts = tuple[PARTS]
+        index_opts = tuple.opts
+        parts = tuple.parts
     end
     if options.name == nil then
-        options.name = tuple[3]
+        options.name = tuple.name
     end
     if options.type == nil then
-        options.type = tuple[4]
+        options.type = tuple.type
     end
     for k, t in pairs(index_options) do
         if options[k] ~= nil then
@@ -1048,7 +1046,7 @@ box.schema.index.alter = function(space_id, index_id, options)
         end
     end
     if sequence == true then
-        if sequence_tuple == nil or sequence_tuple[3] == false then
+        if sequence_tuple == nil or sequence_tuple.is_generated == false then
             sequence = box.schema.sequence.create(space.name .. '_seq')
             sequence = sequence.id
             sequence_is_generated = true
@@ -1070,10 +1068,10 @@ box.schema.index.alter = function(space_id, index_id, options)
     if sequence then
         _space_sequence:replace{space_id, sequence, sequence_is_generated}
     end
-    if sequence_tuple ~= nil and sequence_tuple[3] == true and
-       sequence_tuple[2] ~= sequence then
+    if sequence_tuple ~= nil and sequence_tuple.is_generated == true and
+       sequence_tuple.sequence_id ~= sequence then
         -- Delete automatically generated sequence.
-        box.schema.sequence.drop(sequence_tuple[2])
+        box.schema.sequence.drop(sequence_tuple.sequence_id)
     end
 end
 
@@ -1662,13 +1660,13 @@ end
 
 local function sequence_on_alter(old_tuple, new_tuple)
     if old_tuple and not new_tuple then
-        local old_name = old_tuple[3]
+        local old_name = old_tuple.name
         box.sequence[old_name] = nil
     elseif not old_tuple and new_tuple then
         local seq = sequence_new(new_tuple)
         box.sequence[seq.name] = seq
     else
-        local old_name = old_tuple[3]
+        local old_name = old_tuple.name
         local seq = box.sequence[old_name]
         if not seq then
             seq = sequence_new(seq, new_tuple)
@@ -1919,7 +1917,7 @@ local function object_resolve(object_type, object_name)
             func = _vfunc:get{object_name}
         end
         if func then
-            return func[1]
+            return func.id
         else
             box.error(box.error.NO_SUCH_FUNCTION, object_name)
         end
@@ -1945,8 +1943,8 @@ local function object_resolve(object_type, object_name)
         else
             role_or_user = _vuser:get{object_name}
         end
-        if role_or_user and role_or_user[4] == object_type then
-            return role_or_user[1]
+        if role_or_user and role_or_user.type == object_type then
+            return role_or_user.id
         elseif object_type == 'role' then
             box.error(box.error.NO_SUCH_ROLE, object_name)
         else
@@ -1973,7 +1971,7 @@ local function object_name(object_type, object_id)
     else
         box.error(box.error.UNKNOWN_SCHEMA_OBJECT, object_type)
     end
-    return space:get{object_id}[3]
+    return space:get{object_id}.name
 end
 
 box.schema.func = {}
@@ -2010,7 +2008,7 @@ box.schema.func.drop = function(name, opts)
         tuple = _vfunc:get{name}
     end
     if tuple then
-        fid = tuple[1]
+        fid = tuple.id
     end
     if fid == nil then
         if not opts.if_exists then
@@ -2096,7 +2094,7 @@ end
 box.internal.collation.id_by_name = function(name)
     local _coll = box.space[box.schema.COLLATION_ID]
     local coll = _coll.index.name:get{name}
-    return coll[1]
+    return coll.id
 end
 
 box.schema.user = {}
@@ -2148,7 +2146,7 @@ box.schema.user.create = function(name, opts)
         auth_mech_list["chap-sha1"] = box.schema.user.password(opts.password)
     end
     local _user = box.space[box.schema.USER_ID]
-    uid = _user:auto_increment{session.euid(), name, 'user', auth_mech_list}[1]
+    uid = _user:auto_increment{session.euid(), name, 'user', auth_mech_list}.id
     -- grant role 'public' to the user
     box.schema.user.grant(uid, 'public')
     -- Grant privilege 'alter' on itself, so that it can
@@ -2201,7 +2199,7 @@ local function grant(uid, name, privilege, object_type,
     local tuple = _vpriv:get{uid, object_type, oid}
     local old_privilege
     if tuple ~= nil then
-        old_privilege = tuple[5]
+        old_privilege = tuple.privilege
     else
         old_privilege = 0
     end
@@ -2255,8 +2253,8 @@ local function revoke(uid, name, privilege, object_type, object_name, options)
                       object_type, object_name)
         end
     end
-    local old_privilege = tuple[5]
-    local grantor = tuple[1]
+    local old_privilege = tuple.privilege
+    local grantor = tuple.grantor
     -- sic:
     -- a user may revoke more than he/she granted
     -- (erroneous user input)
@@ -2282,25 +2280,25 @@ local function drop(uid, opts)
     local _vpriv = box.space[box.schema.VPRIV_ID]
     local spaces = box.space[box.schema.VSPACE_ID].index.owner:select{uid}
     for k, tuple in pairs(spaces) do
-        box.space[tuple[1]]:drop()
+        box.space[tuple.id]:drop()
     end
     local funcs = box.space[box.schema.VFUNC_ID].index.owner:select{uid}
     for k, tuple in pairs(funcs) do
-        box.schema.func.drop(tuple[1])
+        box.schema.func.drop(tuple.id)
     end
     -- if this is a role, revoke this role from whoever it was granted to
     local grants = _vpriv.index.object:select{'role', uid}
     for k, tuple in pairs(grants) do
-        revoke(tuple[2], tuple[2], uid)
+        revoke(tuple.grantee, tuple.grantee, uid)
     end
     local sequences = box.space[box.schema.VSEQUENCE_ID].index.owner:select{uid}
     for k, tuple in pairs(sequences) do
-        box.schema.sequence.drop(tuple[1])
+        box.schema.sequence.drop(tuple.id)
     end
     -- xxx: hack, we have to revoke session and usage privileges
     -- of a user using a setuid function in absence of create/drop
     -- privileges and grant option
-    if box.space._vuser:get{uid}[4] == 'user' then
+    if box.space._vuser:get{uid}.type == 'user' then
         box.session.su('admin', box.schema.user.revoke, uid,
                        'session,usage', 'universe', nil, {if_exists = true})
     end
@@ -2309,7 +2307,8 @@ local function drop(uid, opts)
     for k, tuple in pairs(privs) do
         -- we need an additional box.session.su() here, because of
         -- unnecessary check for privilege PRIV_REVOKE in priv_def_check()
-        box.session.su("admin", revoke, uid, uid, tuple[5], tuple[3], tuple[4])
+        box.session.su("admin", revoke, uid, uid, tuple.privilege,
+                       tuple.object_type, tuple.object_id)
     end
     box.space[box.schema.USER_ID]:delete{uid}
 end
@@ -2369,7 +2368,8 @@ local function info(id)
     for _, v in pairs(_priv:select{id}) do
         table.insert(
             privs,
-            {privilege_name(v[5]), v[3], object_name(v[3], v[4])}
+            {privilege_name(v.privilege), v.object_type,
+             object_name(v.object_type, v.object_id)}
         )
     end
     return privs
diff --git a/test/box/alter.result b/test/box/alter.result
index c1b1de13..75d6dae2 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -52,25 +52,10 @@ _space:insert{_space.id, ADMIN, '_space', 'memtx', 0, EMPTY_MAP, {}}
 ---
 - error: Duplicate key exists in unique index 'primary' in space '_space'
 ...
-_space:replace{_space.id, ADMIN, '_space', 'memtx', 0, EMPTY_MAP, {}}
----
-- [280, 1, '_space', 'memtx', 0, {}, []]
-...
 _space:insert{_index.id, ADMIN, '_index', 'memtx', 0, EMPTY_MAP, {}}
 ---
 - error: Duplicate key exists in unique index 'primary' in space '_space'
 ...
-_space:replace{_index.id, ADMIN, '_index', 'memtx', 0, EMPTY_MAP, {}}
----
-- [288, 1, '_index', 'memtx', 0, {}, []]
-...
---
--- Can't change properties of a space
---
-_space:replace{_space.id, ADMIN, '_space', 'memtx', 0, EMPTY_MAP, {}}
----
-- [280, 1, '_space', 'memtx', 0, {}, []]
-...
 --
 -- Can't drop a system space
 --
diff --git a/test/box/alter.test.lua b/test/box/alter.test.lua
index 733d27c5..3cb0c4f8 100644
--- a/test/box/alter.test.lua
+++ b/test/box/alter.test.lua
@@ -24,13 +24,7 @@ _space:insert{_space.id, ADMIN, 'test', 'world', 0, EMPTY_MAP, {}}
 -- There is already a tuple for the system space
 --
 _space:insert{_space.id, ADMIN, '_space', 'memtx', 0, EMPTY_MAP, {}}
-_space:replace{_space.id, ADMIN, '_space', 'memtx', 0, EMPTY_MAP, {}}
 _space:insert{_index.id, ADMIN, '_index', 'memtx', 0, EMPTY_MAP, {}}
-_space:replace{_index.id, ADMIN, '_index', 'memtx', 0, EMPTY_MAP, {}}
---
--- Can't change properties of a space
---
-_space:replace{_space.id, ADMIN, '_space', 'memtx', 0, EMPTY_MAP, {}}
 --
 -- Can't drop a system space
 --
-- 
2.11.0
^ permalink raw reply	[flat|nested] 17+ messages in thread* [PATCH 3/4] schema: allow to set sequence for any index part, not just the first
  2019-05-15 10:33 [PATCH 0/4] A few fixes/improvements for autoincrement indexes Vladimir Davydov
  2019-05-15 10:33 ` [PATCH 1/4] schema: use tuple field names in Lua Vladimir Davydov
  2019-05-15 10:33 ` [PATCH 2/4] schema: fix error while altering index with sequence Vladimir Davydov
@ 2019-05-15 10:33 ` Vladimir Davydov
  2019-05-16  7:45   ` [tarantool-patches] " Konstantin Osipov
  2019-05-15 10:33 ` [PATCH 4/4] schema: explicitly forbid setting sequence for json path key part Vladimir Davydov
  2019-05-21 10:42 ` [tarantool-patches] Re: [PATCH 0/4] A few fixes/improvements for autoincrement indexes Kirill Yukhin
  4 siblings, 1 reply; 17+ messages in thread
From: Vladimir Davydov @ 2019-05-15 10:33 UTC (permalink / raw)
  To: tarantool-patches
Closes #4009
@TarantoolBot document
Title: Sequence can now be set for an index part other than the first
Initially one could attach a sequence (aka autoincrement) only to the
first index part. Now it's possible to attach a sequence to any primary
index part. The part still must be integer though.
Syntax:
```
box.schema.space.create('test')
box.space.test:create_index('primary', {
    parts = {{1, 'string'}, {2, 'unsigned'}, {3, 'unsigned'}},
    sequence = true, sequence_part = 2
})
box.space.test:insert{'a', box.null, 1} -- inserts {'a', 1, 1}
```
Note, `sequence_part` option is 1-base.
If `sequence_part` is omitted, 1 is used, which assures backward
compatibility with the original behavior.
One can also attach a sequence to another index part using
`index.alter` (the code below continues the example above):
```
box.space.test.index.primary:alter{sequence_part = 3}
box.space.test:insert{'a', 1, box.null, 'x'} -- inserts {'a', 1, 2, 'x'}
```
---
 src/box/alter.cc             |  27 +++++++--
 src/box/bootstrap.snap       | Bin 4374 -> 4379 bytes
 src/box/lua/schema.lua       |  68 +++++++++++++++------
 src/box/lua/space.cc         |   7 +++
 src/box/lua/upgrade.lua      |  35 ++++++++++-
 src/box/request.c            |   2 +-
 src/box/schema_def.h         |   1 +
 src/box/space.h              |   5 ++
 src/box/sql/build.c          |   7 ++-
 src/box/sql/insert.c         |   2 +-
 test/box-py/bootstrap.result |   5 +-
 test/box/access_misc.result  |   3 +-
 test/box/sequence.result     | 141 +++++++++++++++++++++++++++++++++++++++++--
 test/box/sequence.test.lua   |  50 +++++++++++++--
 14 files changed, 312 insertions(+), 41 deletions(-)
diff --git a/src/box/alter.cc b/src/box/alter.cc
index 9279426d..2d43a9d2 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -124,9 +124,14 @@ access_check_ddl(const char *name, uint32_t object_id, uint32_t owner_uid,
  * is incompatible with a sequence.
  */
 static void
-index_def_check_sequence(struct index_def *index_def, const char *space_name)
+index_def_check_sequence(struct index_def *index_def, uint32_t sequence_part,
+			 const char *space_name)
 {
-	enum field_type type = index_def->key_def->parts[0].type;
+	if (sequence_part >= index_def->key_def->part_count) {
+		tnt_raise(ClientError, ER_MODIFY_INDEX, index_def->name,
+			  space_name, "sequence part is out of bounds");
+	}
+	enum field_type type = index_def->key_def->parts[sequence_part].type;
 	if (type != FIELD_TYPE_UNSIGNED && type != FIELD_TYPE_INTEGER) {
 		tnt_raise(ClientError, ER_MODIFY_INDEX, index_def->name,
 			  space_name, "sequence cannot be used with "
@@ -279,7 +284,8 @@ index_def_new_from_tuple(struct tuple *tuple, struct space *space)
 	index_def_check_xc(index_def, space_name(space));
 	space_check_index_def_xc(space, index_def);
 	if (index_def->iid == 0 && space->sequence != NULL)
-		index_def_check_sequence(index_def, space_name(space));
+		index_def_check_sequence(index_def, space->sequence_part,
+					 space_name(space));
 	index_def_guard.is_active = false;
 	return index_def;
 }
@@ -855,6 +861,7 @@ alter_space_do(struct txn *txn, struct alter_space *alter)
 	space_prepare_alter_xc(alter->old_space, alter->new_space);
 
 	alter->new_space->sequence = alter->old_space->sequence;
+	alter->new_space->sequence_part = alter->old_space->sequence_part;
 	memcpy(alter->new_space->access, alter->old_space->access,
 	       sizeof(alter->old_space->access));
 
@@ -3333,6 +3340,12 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
 				BOX_SPACE_SEQUENCE_FIELD_SEQUENCE_ID);
 	bool is_generated = tuple_field_bool_xc(tuple,
 				BOX_SPACE_SEQUENCE_FIELD_IS_GENERATED);
+	/* Sequence part was added in 2.2.1. */
+	uint32_t sequence_part = 0;
+	if (tuple_field_count(tuple) > BOX_SPACE_SEQUENCE_FIELD_PART) {
+		sequence_part = tuple_field_u32_xc(tuple,
+					BOX_SPACE_SEQUENCE_FIELD_PART);
+	}
 
 	struct space *space = space_cache_find_xc(space_id);
 	struct sequence *seq = sequence_cache_find(sequence_id);
@@ -3365,17 +3378,21 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
 
 	if (stmt->new_tuple != NULL) {			/* INSERT, UPDATE */
 		struct index *pk = index_find_xc(space, 0);
-		index_def_check_sequence(pk->def, space_name(space));
-		if (seq->is_generated) {
+		index_def_check_sequence(pk->def, sequence_part,
+					 space_name(space));
+		if (seq->is_generated && seq != space->sequence) {
 			tnt_raise(ClientError, ER_ALTER_SPACE,
 				  space_name(space),
 				  "can not attach generated sequence");
 		}
 		seq->is_generated = is_generated;
 		space->sequence = seq;
+		space->sequence_part = sequence_part;
 	} else {					/* DELETE */
 		assert(space->sequence == seq);
+		assert(space->sequence_part == sequence_part);
 		space->sequence = NULL;
+		space->sequence_part = 0;
 	}
 }
 
diff --git a/src/box/bootstrap.snap b/src/box/bootstrap.snap
index 871a93f9856f97636ad55b7f5260e5c6957fef36..8c3b10e8417494edba07e6eba78377226ee5a31a 100644
GIT binary patch
delta 3671
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
delta 3647
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
Rfj#7n<+o@q*9X-Qt?gkf;xqsN
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 14ad4de1..91900395 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -748,6 +748,20 @@ local function simplify_index_parts(parts)
     return new_parts
 end
 
+local function check_sequence_part(parts, sequence_part,
+                                   index_name, space_name)
+    if sequence_part <= 0 or sequence_part > #parts then
+        box.error(box.error.MODIFY_INDEX, index_name, space_name,
+                  "sequence part is out of bounds")
+    end
+    sequence_part = parts[sequence_part]
+    local sequence_part_type = sequence_part.type or sequence_part[2]
+    if sequence_part_type ~= 'integer' and sequence_part_type ~= 'unsigned' then
+        box.error(box.error.MODIFY_INDEX, index_name, space_name,
+                  "sequence cannot be used with a non-integer key")
+    end
+end
+
 -- Historically, some properties of an index
 -- are stored as tuple fields, others in a
 -- single field containing msgpack map.
@@ -773,6 +787,7 @@ local alter_index_template = {
     type = 'string',
     parts = 'table',
     sequence = 'boolean, number, string',
+    sequence_part = 'number',
 }
 for k, v in pairs(index_options) do
     alter_index_template[k] = v
@@ -885,16 +900,18 @@ box.schema.index.create = function(space_id, name, options)
     local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
     local sequence_is_generated = false
     local sequence = options.sequence or nil -- ignore sequence = false
+    local sequence_part = options.sequence_part
+    if sequence_part ~= nil and sequence == nil then
+        box.error(box.error.MODIFY_INDEX, options.name, space.name,
+                  "sequence part cannot be used without sequence")
+    end
     if sequence ~= nil then
         if iid ~= 0 then
             box.error(box.error.MODIFY_INDEX, name, space.name,
                       "sequence cannot be used with a secondary key")
         end
-        if #parts >= 1 and parts[1].type ~= 'integer' and
-                           parts[1].type ~= 'unsigned' then
-            box.error(box.error.MODIFY_INDEX, name, space.name,
-                      "sequence cannot be used with a non-integer key")
-        end
+        sequence_part = sequence_part or 1
+        check_sequence_part(parts, sequence_part, name, space.name)
         if sequence == true then
             sequence = box.schema.sequence.create(space.name .. '_seq')
             sequence = sequence.id
@@ -912,7 +929,8 @@ box.schema.index.create = function(space_id, name, options)
     end
     _index:insert{space_id, iid, name, options.type, index_opts, parts}
     if sequence ~= nil then
-        _space_sequence:insert{space_id, sequence, sequence_is_generated}
+        _space_sequence:insert{space_id, sequence, sequence_is_generated,
+                               sequence_part - 1}
     end
     return space.index[name]
 end
@@ -1028,32 +1046,45 @@ box.schema.index.alter = function(space_id, index_id, options)
     local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
     local sequence_is_generated = false
     local sequence = options.sequence
+    local sequence_part = options.sequence_part
     local sequence_tuple
     if index_id ~= 0 then
-        if sequence then
+        if sequence or sequence_part ~= nil then
             box.error(box.error.MODIFY_INDEX, options.name, space.name,
                       "sequence cannot be used with a secondary key")
         end
         -- ignore 'sequence = false' for secondary indexes
         sequence = nil
-    else
+    end
+    if sequence ~= nil or sequence_part ~= nil then
         sequence_tuple = _space_sequence:get(space_id)
-        if (sequence or (sequence ~= false and sequence_tuple ~= nil)) and
-           #parts >= 1 and (parts[1].type or parts[1][2]) ~= 'integer' and
-                           (parts[1].type or parts[1][2]) ~= 'unsigned' then
-            box.error(box.error.MODIFY_INDEX, options.name, space.name,
-                      "sequence cannot be used with a non-integer key")
+        if sequence_tuple ~= nil then
+            -- Inherit omitted options from the attached sequence.
+            if sequence == nil then
+                sequence = sequence_tuple.sequence_id
+                sequence_is_generated = sequence_tuple.is_generated
+            end
+            if sequence and sequence_part == nil then
+                sequence_part = sequence_tuple.sequence_part
+            end
         end
     end
+    if sequence then
+        sequence_part = sequence_part or 1
+        check_sequence_part(parts, sequence_part, options.name, space.name)
+    elseif sequence_part ~= nil then
+        box.error(box.error.MODIFY_INDEX, options.name, space.name,
+                  "sequence part cannot be used without sequence")
+    end
     if sequence == true then
         if sequence_tuple == nil or sequence_tuple.is_generated == false then
             sequence = box.schema.sequence.create(space.name .. '_seq')
             sequence = sequence.id
-            sequence_is_generated = true
         else
             -- Space already has an automatically generated sequence.
-            sequence = nil
+            sequence = sequence_tuple.sequence_id
         end
+        sequence_is_generated = true
     elseif sequence then
         sequence = sequence_resolve(sequence)
         if sequence == nil then
@@ -1065,8 +1096,11 @@ box.schema.index.alter = function(space_id, index_id, options)
     end
     _index:replace{space_id, index_id, options.name, options.type,
                    index_opts, parts}
-    if sequence then
-        _space_sequence:replace{space_id, sequence, sequence_is_generated}
+    if sequence and (sequence_tuple == nil or
+                     sequence_tuple.sequence_id ~= sequence or
+                     sequence_tuple.sequence_part ~= sequence_part) then
+        _space_sequence:replace{space_id, sequence, sequence_is_generated,
+                                sequence_part - 1}
     end
     if sequence ~= nil and sequence_tuple ~= nil and
        sequence_tuple.is_generated == true and
diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc
index 100da0a7..e342bfcc 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -309,6 +309,13 @@ lbox_fillspace(struct lua_State *L, struct space *space, int i)
 		 */
 		lua_rawset(L, -3);
 
+		lua_pushstring(L, "sequence_part");
+		if (k == 0 && space->sequence != NULL)
+			lua_pushnumber(L, space->sequence_part + 1);
+		else
+			lua_pushnil(L);
+		lua_rawset(L, -3);
+
 		if (space_is_vinyl(space)) {
 			lua_pushstring(L, "options");
 			lua_newtable(L);
diff --git a/src/box/lua/upgrade.lua b/src/box/lua/upgrade.lua
index 89d6e3d5..23f4df01 100644
--- a/src/box/lua/upgrade.lua
+++ b/src/box/lua/upgrade.lua
@@ -625,8 +625,12 @@ local function upgrade_to_2_1_2()
     update_collation_strength_field()
 end
 
+--------------------------------------------------------------------------------
+-- Tarantool 2.1.3
+--------------------------------------------------------------------------------
+
 -- Add new collations
-local function upgrade_to_2_1_3()
+local function upgrade_collation_to_2_1_3()
     local coll_lst = {
         {name="af", loc_str="af"},  -- Afrikaans
         {name="am", loc_str="am"},  -- Amharic (no character changes, just re-ordering)
@@ -737,6 +741,34 @@ local function upgrade_to_2_1_3()
     end
 end
 
+local function upgrade_to_2_1_3()
+    upgrade_collation_to_2_1_3()
+end
+
+--------------------------------------------------------------------------------
+-- Tarantool 2.2.1
+--------------------------------------------------------------------------------
+
+-- Add sequence part field to _space_sequence table
+local function upgrade_sequence_to_2_2_1()
+    log.info("add key part field to space _space_sequence")
+    local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
+    for _, v in _space_sequence:pairs() do
+        if #v == 3 then
+            _space_sequence:update(v[1], {{'!', 4, 0}})
+        end
+    end
+    local format = _space_sequence:format()
+    format[4] = {name = 'part', type = 'unsigned'}
+    _space_sequence:format(format)
+end
+
+local function upgrade_to_2_2_1()
+    upgrade_sequence_to_2_2_1()
+end
+
+--------------------------------------------------------------------------------
+
 local function get_version()
     local version = box.space._schema:get{'version'}
     if version == nil then
@@ -768,6 +800,7 @@ local function upgrade(options)
         {version = mkversion(2, 1, 1), func = upgrade_to_2_1_1, auto = true},
         {version = mkversion(2, 1, 2), func = upgrade_to_2_1_2, auto = true},
         {version = mkversion(2, 1, 3), func = upgrade_to_2_1_3, auto = true},
+        {version = mkversion(2, 2, 1), func = upgrade_to_2_2_1, auto = true},
     }
 
     for _, handler in ipairs(handlers) do
diff --git a/src/box/request.c b/src/box/request.c
index 44a43ee1..9d3287f9 100644
--- a/src/box/request.c
+++ b/src/box/request.c
@@ -163,7 +163,7 @@ request_handle_sequence(struct request *request, struct space *space)
 	const char *data = request->tuple;
 	const char *data_end = request->tuple_end;
 	int len = mp_decode_array(&data);
-	int fieldno = pk->def->key_def->parts[0].fieldno;
+	int fieldno = pk->def->key_def->parts[space->sequence_part].fieldno;
 	if (unlikely(len < fieldno + 1))
 		return 0;
 
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index eeeeb950..dea3fad1 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -216,6 +216,7 @@ enum {
 	BOX_SPACE_SEQUENCE_FIELD_ID = 0,
 	BOX_SPACE_SEQUENCE_FIELD_SEQUENCE_ID = 1,
 	BOX_SPACE_SEQUENCE_FIELD_IS_GENERATED = 2,
+	BOX_SPACE_SEQUENCE_FIELD_PART = 3,
 };
 
 /** _trigger fields. */
diff --git a/src/box/space.h b/src/box/space.h
index 13a220d1..c3eef71c 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -176,6 +176,11 @@ struct space {
 	struct space_def *def;
 	/** Sequence attached to this space or NULL. */
 	struct sequence *sequence;
+	/**
+	 * Auto increment part of the primary index.
+	 * Makes sense only if sequence is set.
+	 */
+	uint32_t sequence_part;
 	/** Enable/disable triggers. */
 	bool run_triggers;
 	/**
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index 6051a252..91b977de 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -951,9 +951,14 @@ emitNewSysSpaceSequenceRecord(Parse *pParse, int space_id, const char reg_seq_id
 	
 	/* 2. Sequence id  */
 	sqlVdbeAddOp2(v, OP_IntCopy, reg_seq_id, first_col + 2);
+
+	/* 3. Autogenerated. */
 	sqlVdbeAddOp2(v, OP_Bool, true, first_col + 3);
-	sqlVdbeAddOp3(v, OP_MakeRecord, first_col + 1, 3, first_col);
 
+	/* 4. Part id. */
+	sqlVdbeAddOp2(v, OP_Integer, 0, first_col + 4);
+
+	sqlVdbeAddOp3(v, OP_MakeRecord, first_col + 1, 4, first_col);
 	return first_col;
 }
 
diff --git a/src/box/sql/insert.c b/src/box/sql/insert.c
index c2aac553..1261ab9c 100644
--- a/src/box/sql/insert.c
+++ b/src/box/sql/insert.c
@@ -98,7 +98,7 @@ sql_space_autoinc_fieldno(struct space *space)
 	if (pk == NULL || pk->def->key_def->part_count != 1 ||
 	    space->sequence == NULL)
 		return UINT32_MAX;
-	return pk->def->key_def->parts[0].fieldno;
+	return pk->def->key_def->parts[space->sequence_part].fieldno;
 }
 
 /**
diff --git a/test/box-py/bootstrap.result b/test/box-py/bootstrap.result
index 379f6c51..de90beee 100644
--- a/test/box-py/bootstrap.result
+++ b/test/box-py/bootstrap.result
@@ -4,7 +4,7 @@ box.internal.bootstrap()
 box.space._schema:select{}
 ---
 - - ['max_id', 511]
-  - ['version', 2, 1, 3]
+  - ['version', 2, 2, 1]
 ...
 box.space._cluster:select{}
 ---
@@ -72,7 +72,8 @@ box.space._space:select{}
   - [330, 1, '_truncate', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'count',
         'type': 'unsigned'}]]
   - [340, 1, '_space_sequence', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'},
-      {'name': 'sequence_id', 'type': 'unsigned'}, {'name': 'is_generated', 'type': 'boolean'}]]
+      {'name': 'sequence_id', 'type': 'unsigned'}, {'name': 'is_generated', 'type': 'boolean'},
+      {'name': 'part', 'type': 'unsigned'}]]
   - [356, 1, '_fk_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'},
       {'name': 'child_id', 'type': 'unsigned'}, {'name': 'parent_id', 'type': 'unsigned'},
       {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'},
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index 36ebfae0..877a9b53 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -812,7 +812,8 @@ box.space._space:select()
   - [330, 1, '_truncate', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'}, {'name': 'count',
         'type': 'unsigned'}]]
   - [340, 1, '_space_sequence', 'memtx', 0, {}, [{'name': 'id', 'type': 'unsigned'},
-      {'name': 'sequence_id', 'type': 'unsigned'}, {'name': 'is_generated', 'type': 'boolean'}]]
+      {'name': 'sequence_id', 'type': 'unsigned'}, {'name': 'is_generated', 'type': 'boolean'},
+      {'name': 'part', 'type': 'unsigned'}]]
   - [356, 1, '_fk_constraint', 'memtx', 0, {}, [{'name': 'name', 'type': 'string'},
       {'name': 'child_id', 'type': 'unsigned'}, {'name': 'parent_id', 'type': 'unsigned'},
       {'name': 'is_deferred', 'type': 'boolean'}, {'name': 'match', 'type': 'string'},
diff --git a/test/box/sequence.result b/test/box/sequence.result
index 5eed0ef4..4f962347 100644
--- a/test/box/sequence.result
+++ b/test/box/sequence.result
@@ -590,6 +590,48 @@ s:create_index('pk', {parts = {1, 'number'}, sequence = 'test'}) -- error
 - error: 'Can''t create or modify index ''pk'' in space ''test'': sequence cannot
     be used with a non-integer key'
 ...
+s:create_index('pk', {sequence_part = 1}) -- error
+---
+- error: 'Can''t create or modify index ''nil'' in space ''test'': sequence part cannot
+    be used without sequence'
+...
+s:create_index('pk', {sequence = true, sequence_part = 2}) -- error
+---
+- error: 'Can''t create or modify index ''pk'' in space ''test'': sequence part is
+    out of bounds'
+...
+s:create_index('pk', {parts = {1, 'unsigned', 2, 'string'}, sequence = true, sequence_part = 2}) -- error
+---
+- error: 'Can''t create or modify index ''pk'' in space ''test'': sequence cannot
+    be used with a non-integer key'
+...
+pk = s:create_index('pk', {parts = {1, 'string', 2, 'unsigned'}}) -- ok
+---
+...
+pk:alter{sequence_part = 1} -- error
+---
+- error: 'Can''t create or modify index ''pk'' in space ''test'': sequence part cannot
+    be used without sequence'
+...
+pk:alter{sequence = true, sequence_part = 1} -- error
+---
+- error: 'Can''t create or modify index ''pk'' in space ''test'': sequence cannot
+    be used with a non-integer key'
+...
+pk:alter{sequence = true, sequence_part = 2} -- ok
+---
+...
+pk:alter{sequence = false, sequence_part = 2} -- error
+---
+- error: 'Can''t create or modify index ''pk'' in space ''test'': sequence part cannot
+    be used without sequence'
+...
+pk:alter{sequence = false} -- ok
+---
+...
+pk:drop()
+---
+...
 pk = s:create_index('pk', {parts = {1, 'integer'}, sequence = 'test'}) -- ok
 ---
 ...
@@ -615,6 +657,11 @@ s:create_index('secondary', {parts = {2, 'unsigned'}, sequence = true}) -- error
 - error: 'Can''t create or modify index ''secondary'' in space ''test'': sequence
     cannot be used with a secondary key'
 ...
+s:create_index('secondary', {parts = {2, 'unsigned'}, sequence_part = 1}) -- error
+---
+- error: 'Can''t create or modify index ''nil'' in space ''test'': sequence part cannot
+    be used without sequence'
+...
 sk = s:create_index('secondary', {parts = {2, 'unsigned'}}) -- ok
 ---
 ...
@@ -700,18 +747,23 @@ sk:alter{sequence = 'test'} -- error
 - error: 'Can''t create or modify index ''sk'' in space ''test'': sequence cannot
     be used with a secondary key'
 ...
-box.space._space_sequence:insert{s.id, sq.id, false} -- error
+box.space._space_sequence:insert{s.id, sq.id, false, 0} -- error
 ---
 - error: 'Can''t create or modify index ''pk'' in space ''test'': sequence cannot
     be used with a non-integer key'
 ...
+box.space._space_sequence:insert{s.id, sq.id, false, 2} -- error
+---
+- error: 'Can''t create or modify index ''pk'' in space ''test'': sequence part is
+    out of bounds'
+...
 sk:drop()
 ---
 ...
 pk:drop()
 ---
 ...
-box.space._space_sequence:insert{s.id, sq.id, false} -- error
+box.space._space_sequence:insert{s.id, sq.id, false, 0} -- error
 ---
 - error: 'No index #0 is defined in space ''test'''
 ...
@@ -1121,7 +1173,7 @@ _ = s2:create_index('pk', {sequence = 'test1_seq'}) -- error
 ---
 - error: 'Can''t modify space ''test2'': can not attach generated sequence'
 ...
-box.space._space_sequence:insert{s2.id, box.sequence.test1_seq.id, false} -- error
+box.space._space_sequence:insert{s2.id, box.sequence.test1_seq.id, false, 0} -- error
 ---
 - error: 'Can''t modify space ''test2'': can not attach generated sequence'
 ...
@@ -1612,15 +1664,15 @@ s1.index.pk:alter({sequence = 'seq1'}) -- error
 ---
 - error: Alter access to space 'space1' is denied for user 'user'
 ...
-box.space._space_sequence:replace{s1.id, sq1.id, false} -- error
+box.space._space_sequence:replace{s1.id, sq1.id, false, 0} -- error
 ---
 - error: Read access to sequence 'seq1' is denied for user 'user'
 ...
-box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
+box.space._space_sequence:replace{s1.id, sq2.id, false, 0} -- error
 ---
 - error: Alter access to space 'space1' is denied for user 'user'
 ...
-box.space._space_sequence:replace{s2.id, sq1.id, false} -- error
+box.space._space_sequence:replace{s2.id, sq1.id, false, 0} -- error
 ---
 - error: Read access to sequence 'seq1' is denied for user 'user'
 ...
@@ -1904,3 +1956,80 @@ sequence_id == s.index.pk.sequence_id
 s:drop()
 ---
 ...
+--
+-- gh-4009: setting sequence for an index part other than the first.
+--
+s = box.schema.space.create('test')
+---
+...
+_ = s:create_index('pk', {parts = {1, 'string', 2, 'unsigned', 3, 'unsigned'}, sequence = true, sequence_part = 2})
+---
+...
+sequence_id = s.index.pk.sequence_id
+---
+...
+sequence_id ~= nil
+---
+- true
+...
+s.index.pk.sequence_part == 2
+---
+- true
+...
+s:insert{'a', box.null, 1}
+---
+- ['a', 1, 1]
+...
+s:insert{'a', box.null, 2}
+---
+- ['a', 2, 2]
+...
+s:insert{'b', 10, 10}
+---
+- ['b', 10, 10]
+...
+s:insert{'b', box.null, 11}
+---
+- ['b', 11, 11]
+...
+s.index.pk:alter{sequence_part = 3}
+---
+...
+s.index.pk.sequence_part == 3
+---
+- true
+...
+s.index.pk.sequence_id == sequence_id
+---
+- true
+...
+s:insert{'c', 100, 100, 'x'}
+---
+- ['c', 100, 100, 'x']
+...
+s:insert{'c', 101, box.null, 'y'}
+---
+- ['c', 101, 101, 'y']
+...
+s.index.pk:alter{sequence = true, sequence_part = 2}
+---
+...
+s.index.pk.sequence_part == 2
+---
+- true
+...
+s.index.pk.sequence_id == sequence_id
+---
+- true
+...
+s:insert{'d', 1000, 1000}
+---
+- ['d', 1000, 1000]
+...
+s:insert{'d', box.null, 1001}
+---
+- ['d', 1001, 1001]
+...
+s:drop()
+---
+...
diff --git a/test/box/sequence.test.lua b/test/box/sequence.test.lua
index 6459419e..d419e369 100644
--- a/test/box/sequence.test.lua
+++ b/test/box/sequence.test.lua
@@ -196,6 +196,18 @@ s:create_index('pk', {parts = {1, 'string'}, sequence = 'test'}) -- error
 s:create_index('pk', {parts = {1, 'scalar'}, sequence = 'test'}) -- error
 s:create_index('pk', {parts = {1, 'number'}, sequence = 'test'}) -- error
 
+s:create_index('pk', {sequence_part = 1}) -- error
+s:create_index('pk', {sequence = true, sequence_part = 2}) -- error
+s:create_index('pk', {parts = {1, 'unsigned', 2, 'string'}, sequence = true, sequence_part = 2}) -- error
+
+pk = s:create_index('pk', {parts = {1, 'string', 2, 'unsigned'}}) -- ok
+pk:alter{sequence_part = 1} -- error
+pk:alter{sequence = true, sequence_part = 1} -- error
+pk:alter{sequence = true, sequence_part = 2} -- ok
+pk:alter{sequence = false, sequence_part = 2} -- error
+pk:alter{sequence = false} -- ok
+pk:drop()
+
 pk = s:create_index('pk', {parts = {1, 'integer'}, sequence = 'test'}) -- ok
 pk:drop()
 pk = s:create_index('pk', {parts = {1, 'unsigned'}, sequence = 'test'}) -- ok
@@ -204,6 +216,7 @@ pk:drop()
 pk = s:create_index('pk') -- ok
 s:create_index('secondary', {parts = {2, 'unsigned'}, sequence = 'test'}) -- error
 s:create_index('secondary', {parts = {2, 'unsigned'}, sequence = true}) -- error
+s:create_index('secondary', {parts = {2, 'unsigned'}, sequence_part = 1}) -- error
 sk = s:create_index('secondary', {parts = {2, 'unsigned'}}) -- ok
 sk:alter{sequence = 'test'} -- error
 sk:alter{sequence = true} -- error
@@ -227,10 +240,11 @@ box.space._index:delete{s.id, pk.id} -- error
 pk:alter{parts = {1, 'string'}, sequence = false} -- ok
 sk = s:create_index('sk', {parts = {2, 'unsigned'}})
 sk:alter{sequence = 'test'} -- error
-box.space._space_sequence:insert{s.id, sq.id, false} -- error
+box.space._space_sequence:insert{s.id, sq.id, false, 0} -- error
+box.space._space_sequence:insert{s.id, sq.id, false, 2} -- error
 sk:drop()
 pk:drop()
-box.space._space_sequence:insert{s.id, sq.id, false} -- error
+box.space._space_sequence:insert{s.id, sq.id, false, 0} -- error
 
 s:create_index('pk', {sequence = {}}) -- error
 s:create_index('pk', {sequence = 'abc'}) -- error
@@ -358,7 +372,7 @@ s1 = box.schema.space.create('test1')
 _ = s1:create_index('pk', {sequence = true})
 s2 = box.schema.space.create('test2')
 _ = s2:create_index('pk', {sequence = 'test1_seq'}) -- error
-box.space._space_sequence:insert{s2.id, box.sequence.test1_seq.id, false} -- error
+box.space._space_sequence:insert{s2.id, box.sequence.test1_seq.id, false, 0} -- error
 
 s1:drop()
 s2:drop()
@@ -538,9 +552,9 @@ box.schema.user.grant('user', 'read', 'space', '_space_sequence')
 box.session.su('user')
 _ = s2:create_index('pk', {sequence = 'seq1'}) -- error
 s1.index.pk:alter({sequence = 'seq1'}) -- error
-box.space._space_sequence:replace{s1.id, sq1.id, false} -- error
-box.space._space_sequence:replace{s1.id, sq2.id, false} -- error
-box.space._space_sequence:replace{s2.id, sq1.id, false} -- error
+box.space._space_sequence:replace{s1.id, sq1.id, false, 0} -- error
+box.space._space_sequence:replace{s1.id, sq2.id, false, 0} -- error
+box.space._space_sequence:replace{s2.id, sq1.id, false, 0} -- error
 s2.index.pk:alter({sequence = 'seq2'}) -- ok
 box.session.su('admin')
 
@@ -647,3 +661,27 @@ s.index.pk.parts[1].type
 s.index.pk:alter{sequence = true}
 sequence_id == s.index.pk.sequence_id
 s:drop()
+
+--
+-- gh-4009: setting sequence for an index part other than the first.
+--
+s = box.schema.space.create('test')
+_ = s:create_index('pk', {parts = {1, 'string', 2, 'unsigned', 3, 'unsigned'}, sequence = true, sequence_part = 2})
+sequence_id = s.index.pk.sequence_id
+sequence_id ~= nil
+s.index.pk.sequence_part == 2
+s:insert{'a', box.null, 1}
+s:insert{'a', box.null, 2}
+s:insert{'b', 10, 10}
+s:insert{'b', box.null, 11}
+s.index.pk:alter{sequence_part = 3}
+s.index.pk.sequence_part == 3
+s.index.pk.sequence_id == sequence_id
+s:insert{'c', 100, 100, 'x'}
+s:insert{'c', 101, box.null, 'y'}
+s.index.pk:alter{sequence = true, sequence_part = 2}
+s.index.pk.sequence_part == 2
+s.index.pk.sequence_id == sequence_id
+s:insert{'d', 1000, 1000}
+s:insert{'d', box.null, 1001}
+s:drop()
-- 
2.11.0
^ permalink raw reply	[flat|nested] 17+ messages in thread