Commit Diff


commit - 774b84d7e6f3d61000d64bbf91dd9b0784f30f5c
commit + 280ede938294bba45b4157add8ea8b08a29ab373
blob - /dev/null
blob + 65c81c27a0c2fa023348db25103b81f981f243d4 (mode 644)
--- /dev/null
+++ changelogs/unreleased/gh-10396-memtx-mvcc-exclude-null-count.md
@@ -0,0 +1,5 @@
+## bugfix/memtx
+
+* Fixed a bug when `index:count()` could return a wrong number, raise the
+  last error, or fail with the `IllegalParams` error if the index has
+  the `exclude_null` attribute and MVCC is enabled (gh-10396).
blob - e3bce440c5287c1432d1df2c13dc517d8b2e064f
blob + af94b66b67f960dcabbfcaef25180f51de8681d5
--- src/box/memtx_tx.c
+++ src/box/memtx_tx.c
@@ -2902,6 +2902,17 @@ memtx_tx_index_invisible_count_slow(struct txn *txn,
 		}
 		assert(link->newer_story == NULL);
 
+		/*
+		 * Excluded tuples have their own chains consisting of the only
+		 * excluded story. Such stories must be skipped since they are
+		 * not actually inserted to index.
+		 */
+		if (tuple_key_is_excluded(story->tuple, index->def->key_def,
+					  MULTIKEY_NONE)) {
+			assert(link->older_story == NULL);
+			continue;
+		}
+
 		struct tuple *visible = NULL;
 		bool is_prepared_ok = detect_whether_prepared_ok(txn);
 		bool unused;
blob - cdb98285daf6f6afafee5ef162293db3c9224ce6
blob + 94c8ee66cde9dba2b145218c30b071854fe7d4e2
--- test/box-luatest/gh_9954_mvcc_with_exclude_null_test.lua
+++ test/box-luatest/gh_9954_mvcc_with_exclude_null_test.lua
@@ -107,3 +107,53 @@ g.test_mvcc_with_exclude_null_space_drop = function()
         box.space.test:drop()
     end)
 end
+
+-- gh-10396
+g.test_mvcc_with_exclude_null_count = function()
+    g.server:exec(function()
+        local space = box.schema.space.create("TEST", {
+            format = {
+                {name = "ID", type = "unsigned", is_nullable = false},
+                {name = "FLAG", type = "boolean", is_nullable = true},
+            },
+        })
+        space:create_index("ID", {
+            unique = true,
+            type = "TREE",
+            parts = {{field = "ID", type = "unsigned"}},
+        })
+        space:create_index("FLAG", {
+            unique = false,
+            type = "TREE",
+            parts = {{
+                field = "FLAG",
+                type = "boolean",
+                exclude_null = true,
+                is_nullable = true,
+            }},
+        })
+
+        -- Wrap into transaction so that stories won't be deleted
+        box.begin()
+        -- Insert excluded tuples and then replace half of them
+        -- with non-excluded ones
+        for i = 1, 100 do
+            space:replace{i, box.NULL}
+        end
+        for i = 1, 100, 2 do
+            space:replace{i, true}
+        end
+
+        -- Insert non-excluded tuples and then replace half of them
+        -- with excluded ones
+        for i = 101, 150 do
+            space:replace{i, false}
+        end
+        for i = 101, 150, 2 do
+            space:replace{i, box.NULL}
+        end
+        -- Check if count works correctly
+        t.assert_equals(space.index.FLAG:count({}, {iterator = 'ALL'}), 75)
+        box.commit()
+    end)
+end