From 0afe1f78a51990d7849a11339fe9c5af06165bd4 Mon Sep 17 00:00:00 2001
From: Serge Petrenko <sergepetrenko@tarantool.org>
Date: Thu, 12 Aug 2021 22:50:19 +0300
Subject: [PATCH] lua: introduce table.equals method

Introduce table.equals for comparing tables.
The method respects __eq metamethod, if provided.

Needed-for #5894

@TarantoolBot document
Title: lua: new method table.equals

Document the new lua method table.equals
It compares two tables deeply. For example:
```
tarantool> t1 = {a=3}
---
...

tarantool> t2 = {a=3}
---
...

tarantool> t1 == t2
---
- false
...

tarantool> table.equals(t1, t2)
---
- true
...
```
The method respects the __eq metamethod. When both tables being compared
have the same __eq metamethod, it's used for comparison (just like this
is done in Lua 5.1)
---
 .../unreleased/introduce-table-equals.md      |  4 ++
 src/lua/table.lua                             | 29 +++++++++++
 test/app-tap/table.test.lua                   | 49 ++++++++++++++++++-
 3 files changed, 81 insertions(+), 1 deletion(-)
 create mode 100644 changelogs/unreleased/introduce-table-equals.md

diff --git a/changelogs/unreleased/introduce-table-equals.md b/changelogs/unreleased/introduce-table-equals.md
new file mode 100644
index 0000000000..282b56c43b
--- /dev/null
+++ b/changelogs/unreleased/introduce-table-equals.md
@@ -0,0 +1,4 @@
+## feature/lua
+
+* Introduce method `table.equals`. It compares 2 tables by value and respects
+  `__eq` metamethod.
diff --git a/src/lua/table.lua b/src/lua/table.lua
index 8fa9b876ac..edd60d1be5 100644
--- a/src/lua/table.lua
+++ b/src/lua/table.lua
@@ -57,6 +57,34 @@ local function table_shallowcopy(orig)
     return copy
 end
 
+--- Compare two lua tables
+-- Supports __eq metamethod for comparing custom tables with metatables
+-- @function equals
+-- @return true when the two tables are equal (false otherwise).
+local function table_equals(a, b)
+    if type(a) ~= 'table' or type(b) ~= 'table' then
+        return type(a) == type(b) and a == b
+    end
+    local mta = getmetatable(a)
+    local mtb = getmetatable(b)
+    -- Let Lua decide what should happen when at least one of the tables has a
+    -- metatable.
+    if mta and mta.__eq or mtb and mtb.__eq then
+        return a == b
+    end
+    for k, v in pairs(a) do
+        if not table_equals(v, b[k]) then
+            return false
+        end
+    end
+    for k, _ in pairs(b) do
+        if not a[k] then
+            return false
+        end
+    end
+    return true
+end
+
 -- table library extension
 local table = require('table')
 -- require modifies global "table" module and adds "clear" function to it.
@@ -65,3 +93,4 @@ require('table.clear')
 
 table.copy     = table_shallowcopy
 table.deepcopy = table_deepcopy
+table.equals   = table_equals
diff --git a/test/app-tap/table.test.lua b/test/app-tap/table.test.lua
index 60c095fdfc..ae6fdfa2d1 100755
--- a/test/app-tap/table.test.lua
+++ b/test/app-tap/table.test.lua
@@ -8,7 +8,7 @@ yaml.cfg{
     encode_invalid_as_nil  = true,
 }
 local test = require('tap').test('table')
-test:plan(31)
+test:plan(44)
 
 do -- check basic table.copy (deepcopy)
     local example_table = {
@@ -223,4 +223,51 @@ do -- check usage of not __copy metamethod on second level + shallow
     )
 end
 
+do -- check table.equals
+    test:ok(table.equals({}, {}), "table.equals for empty tables")
+    test:is(table.equals({}, {1}), false, "table.equals with one empty table")
+    test:is(table.equals({1}, {}), false, "table.equals with one empty table")
+    test:is(table.equals({key = box.NULL}, {key = nil}), false,
+            "table.equals for box.NULL and nil")
+    test:is(table.equals({key = nil}, {key = box.NULL}), false,
+            "table.equals for box.NULL and nil")
+    local tbl_a = {
+        first = {
+            1,
+            2,
+            {},
+        },
+        second = {
+            a = {
+                {'something'},
+            },
+            b = 'something else',
+        },
+        [3] = 'some value',
+    }
+    local tbl_b = table.deepcopy(tbl_a)
+    local tbl_c = table.copy(tbl_a)
+    test:ok(table.equals(tbl_a, tbl_b), "table.equals for complex tables")
+    test:ok(table.equals(tbl_a, tbl_c),
+            "table.equals for shallow copied tables")
+    tbl_c.second.a = 'other thing'
+    test:ok(table.equals(tbl_a, tbl_c),
+            "table.equals for shallow copied tables after modification")
+    test:is(table.equals(tbl_a, tbl_b), false, "table.equals does a deep check")
+    local mt = {
+        __eq = function(a, b) -- luacheck: no unused args
+            return true
+        end}
+    local tbl_d = setmetatable({a = 15}, mt)
+    local tbl_e = setmetatable({b = 2, c = 3}, mt)
+    test:ok(table.equals(tbl_d, tbl_e), "table.equals respects __eq")
+    test:is(table.equals(tbl_d, {a = 15}), false,
+            "table.equals when metatables don't match")
+    test:is(table.equals({a = 15}, tbl_d), false,
+            "table.equals when metatables don't match")
+    local tbl_f = setmetatable({a = 15}, {__eq = function() return true end})
+    test:is(table.equals(tbl_d, tbl_f), false,
+            "table.equals when metatables don't match")
+end
+
 os.exit(test:check() == true and 0 or 1)
-- 
GitLab