Repository: opendatacube/datacube-core

Issue:
datacube dataset index incorrect comparison of geometry
linked issue: https://github.com/opendatacube/odc-tools/issues/479 

### Expected behaviour
when performing `--update-if-exists` `geometry.coordinates` and `grid_spatial.projection.valid_data.coordinates` shouldn't mark `unsafe` when the values are identical but the type is a tuple

### Actual behaviour

```
root@546115d5f0ab:/# s3-to-dc --stac --update-if-exists ss3://sentinel-cogs/sentinel-s2-l2a-cogs/4/T/UM/2022/1/S2A_42TUM_20220102_0_L2A/*.json s2_l2a
Usage: s3-to-dc [OPTIONS] URI PRODUCT
Try 's3-to-dc --help' for help.

Error: No such option: --update-if-exist (Possible options: --update, --update-if-exists)
root@546115d5f0ab:/# s3-to-dc --stac --update-if-exists s3://sentinel-cogs/sentinel-s2-l2a-cogs/42/T/UM/2022/1/S2A_42TUM_20220102_0_L2A/*.json s2_l2a
Unsafe change in geometry.coordinates from [[[300001.0, 4590241.0], [300001.0, 4700039.0], [406734.0, 4700039.0], [383003.0, 4620099.0], [373985.0, 4590241.0], [300001.0, 4590241.0]]] to (((300001.0, 4590241.0), (300001.0, 4700039.0), (406734.0, 4700039.0), (383003.0, 4620099.0), (373985.0, 4590241.0), (300001.0, 4590241.0)),)
Unsafe change in grid_spatial.projection.valid_data.coordinates from [[[300001.0, 4590241.0], [300001.0, 4700039.0], [406734.0, 4700039.0], [383003.0, 4620099.0], [373985.0, 4590241.0], [300001.0, 4590241.0]]] to (((300001.0, 4590241.0), (300001.0, 4700039.0), (406734.0, 4700039.0), (383003.0, 4620099.0), (373985.0, 4590241.0), (300001.0, 4590241.0)),)
ERROR:root:Failed to index dataset s3://sentinel-cogs/sentinel-s2-l2a-cogs/42/T/UM/2022/1/S2A_42TUM_20220102_0_L2A/S2A_42TUM_20220102_0_L2A.json with error Updating the dataset raised an exception: Unsafe changes in 22250846-0ba8-5ee5-8dbe-38c2adc1f4ab: geometry.coordinates, grid_spatial.projection.valid_data.coordinates
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/odc/apps/dc_tools/utils.py", line 163, in index_update_dataset
    dc.index.datasets.update(ds, updates_allowed=updates)
  File "/usr/local/lib/python3.8/dist-packages/datacube/index/_datasets.py", line 302, in update
    raise ValueError(f"Unsafe changes in {dataset.id}: " + (
ValueError: Unsafe changes in 22250846-0ba8-5ee5-8dbe-38c2adc1f4ab: geometry.coordinates, grid_spatial.projection.valid_data.coordinates

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/odc/apps/dc_tools/s3_to_dc.py", line 58, in dump_to_odc
    index_update_dataset(metadata, uri, dc, doc2ds, update, update_if_exists, allow_unsafe)
  File "/usr/local/lib/python3.8/dist-packages/odc/apps/dc_tools/utils.py", line 165, in index_update_dataset
    raise IndexingException(
odc.apps.dc_tools.utils.IndexingException: Updating the dataset raised an exception: Unsafe changes in 22250846-0ba8-5ee5-8dbe-38c2adc1f4ab: geometry.coordinates, grid_spatial.projection.valid_data.coordinates
Added 0 datasets and failed 1 datasets.
```

### Steps to reproduce the behaviour

1. add a product https://explorer.digitalearth.africa/products/s2_l2a.odc-product.yaml
2. bring up datacube-index to run `s3-to-dc --stac`, OR
3. create a yaml dataset with the same stac json content
4. index first
5. then run `--update-if-exists`

### Environment information

* Which ``datacube --version`` are you using?
```
root@546115d5f0ab:/# datacube --version
Open Data Cube core, version 1.8.6
```

* What datacube deployment/enviornment are you running against?
- datacube-index docker container

> **Note:** Stale issues will be automatically closed after a period of six months with no activity. 
> To ensure critical issues are not closed, tag them with the Github `pinned` tag.
> If you are a community member and not a maintainer please escalate this issue to maintainers via
> [GIS StackExchange](https://gis.stackexchange.com/questions/tagged/open-data-cube) or [Slack](http://slack.opendatacube.org).



Discussion:

Resolving patch:
diff --git a/datacube/utils/changes.py b/datacube/utils/changes.py
--- a/datacube/utils/changes.py
+++ b/datacube/utils/changes.py
@@ -5,6 +5,8 @@
 """
 Validation of document/dictionary changes.
 """
+import numpy
+
 from itertools import zip_longest
 from typing import Any, Callable, List, Mapping, Sequence, Tuple, Union
 
@@ -14,6 +16,7 @@
 # More accurate recursive definition:
 # Changable = Union[str, int, None, Sequence["Changable"], Mapping[str, "Changable"]]
 
+
 def contains(v1: Changable, v2: Changable, case_sensitive: bool = False) -> bool:
     """
     Check that v1 is a superset of v2.
@@ -59,7 +62,7 @@ def __repr__(self):
 def get_doc_changes(original: Changable,
                     new: Changable,
                     base_prefix: Offset = ()
-                   ) -> List[Change]:
+                    ) -> List[Change]:
     """
     Return a list of `changed fields` between two dict structures.
 
@@ -89,6 +92,9 @@ def get_doc_changes(original: Changable,
     elif isinstance(original, list) and isinstance(new, list):
         for idx, (orig_item, new_item) in enumerate(zip_longest(original, new)):
             changed_fields.extend(get_doc_changes(orig_item, new_item, base_prefix + (idx, )))
+    elif isinstance(original, tuple) or isinstance(new, tuple):
+        if not numpy.array_equal(original, new):
+            changed_fields.append((base_prefix, original, new))
     else:
         changed_fields.append((base_prefix, original, new))
 
@@ -120,6 +126,7 @@ def check_doc_unchanged(original: Changable, new: Changable, doc_name: str) -> N
 
 AllowPolicy = Callable[[Offset, Offset, ChangedValue, ChangedValue], bool]
 
+
 def allow_truncation(key: Offset, offset: Offset,
                      old_value: ChangedValue, new_value: ChangedValue) -> bool:
     return bool(offset) and key == offset[:-1] and new_value == MISSING
@@ -146,7 +153,7 @@ def allow_any(key: Offset, offset: Offset,
 
 
 def classify_changes(changes: List[Change], allowed_changes: Mapping[Offset, AllowPolicy]
-                    ) -> Tuple[List[Change], List[Change]]:
+                     ) -> Tuple[List[Change], List[Change]]:
     """
     Classify list of changes into good(allowed) and bad(not allowed) based on allowed changes.
 


Test patch:
diff --git a/tests/test_utils_docs.py b/tests/test_utils_docs.py
--- a/tests/test_utils_docs.py
+++ b/tests/test_utils_docs.py
@@ -55,7 +55,23 @@
     ({'a': 1}, {'b': 1}, [(('a',), 1, MISSING), (('b',), MISSING, 1)]),
     ({'a': {'b': 1}}, {'a': {'b': 2}}, [(('a', 'b'), 1, 2)]),
     ({}, {'b': 1}, [(('b',), MISSING, 1)]),
-    ({'a': {'c': 1}}, {'a': {'b': 1}}, [(('a', 'b'), MISSING, 1), (('a', 'c'), 1, MISSING)])
+    ({'a': {'c': 1}}, {'a': {'b': 1}}, [(('a', 'b'), MISSING, 1), (('a', 'c'), 1, MISSING)]),
+    # Test tuple vs list, for geometry coordinates handling and expect no changes
+    (
+        [[635235.0, -2930535.0], [635235.0, -2930535.0]],
+        ((635235.0, -2930535.0), (635235.0, -2930535.0),),
+        []
+    ),
+    (
+        [[[635235.0, -2930535.0], [635235.0, -2930535.0]]],
+        (((635235.0, -2930535.0), (635235.0, -2930535.0)),),
+        []
+    ),
+    (
+        {'coordinates': [[[635235.0, -2930535.0], [635235.0, -2930535.0]]]},
+        {'coordinates': (((635235.0, -2930535.0), (635235.0, -2930535.0)),)},
+        []
+    )
 ]
 
 
@@ -509,11 +525,10 @@ def test_jsonify():
                                     'c': datetime(2016, 3, 11),
                                     'd': np.dtype('int16'),
                                     }).items()) == [
-                                    ('a', (1.0, 2.0, 3.0)),
-                                    ('b', 'Infinity'),
-                                    ('c', '2016-03-11T00:00:00'),
-                                    ('d', 'int16'),
-                                    ]
+                                        ('a', (1.0, 2.0, 3.0)),
+                                        ('b', 'Infinity'),
+                                        ('c', '2016-03-11T00:00:00'),
+                                        ('d', 'int16'), ]
 
     # Converts keys to strings:
     assert sorted(jsonify_document({1: 'a', '2': Decimal('2')}).items()) == [