Repository: TileDB-Inc/TileDB-Py

Issue:
Can't read array if dimension has unsigned domain but signed dtype
It looks like the signed-ness of the dtype is inferred from the domain, rather than declared?

tiledb-py 0.8.2
tiledb 2.2.3

```python
from tiledb import *
import numpy as np

array_name = 'unsigned'
s = ArraySchema(domain=Domain(Dim('a', dtype=np.int8, domain=(0, 126))), attrs=[Attr('x', dtype=np.int32)], sparse=True)

A = SparseArray.create(array_name, schema=s)
with SparseArray(array_name, mode='w') as A:
    A[0] = 1

with SparseArray(array_name, mode='r') as A:
    print(A[:])
```

```
Traceback (most recent call last):
  File "XXXX", line 12, in <module>
    print(A[:])
  File "tiledb/libtiledb.pyx", line 5237, in tiledb.libtiledb.SparseArrayImpl.__getitem__
  File "tiledb/libtiledb.pyx", line 5374, in tiledb.libtiledb.SparseArrayImpl.subarray
  File "tiledb/libtiledb.pyx", line 5392, in tiledb.libtiledb.SparseArrayImpl._read_sparse_subarray
tiledb.libtiledb.TileDBError: Static type (UINT8) does not match expected type (INT8)
```


Discussion:

Resolving patch:
diff --git a/tiledb/util.py b/tiledb/util.py
new file mode 100644
--- /dev/null
+++ b/tiledb/util.py
@@ -0,0 +1,37 @@
+import tiledb
+import numpy as np
+from typing import Iterable
+from tiledb.dataframe_ import ColumnInfo
+
+
+def _sparse_schema_from_dict(input_attrs, input_dims):
+    attr_infos = {k: ColumnInfo.from_values(v) for k, v in input_attrs.items()}
+    dim_infos = {k: ColumnInfo.from_values(v) for k, v in input_dims.items()}
+
+    dims = list()
+    for name, dim_info in dim_infos.items():
+        dim_dtype = np.bytes_ if dim_info.dtype == np.dtype("U") else dim_info.dtype
+        dtype_min, dtype_max = tiledb.libtiledb.dtype_range(dim_info.dtype)
+
+        if np.issubdtype(dim_dtype, np.integer):
+            dtype_max = dtype_max - 1
+        if np.issubdtype(dim_dtype, np.integer) and dtype_min < 0:
+            dtype_min = dtype_min + 1
+
+        dims.append(
+            tiledb.Dim(
+                name=name, domain=(dtype_min, dtype_max), dtype=dim_dtype, tile=1
+            )
+        )
+
+    attrs = list()
+    for name, attr_info in attr_infos.items():
+        dtype_min, dtype_max = tiledb.libtiledb.dtype_range(attr_info.dtype)
+
+        attrs.append(tiledb.Attr(name=name, dtype=dim_dtype))
+
+    return tiledb.ArraySchema(domain=tiledb.Domain(*dims), attrs=attrs, sparse=True)
+
+
+def schema_from_dict(attrs, dims):
+    return _sparse_schema_from_dict(attrs, dims)


Test patch:
diff --git a/tiledb/tests/fixtures.py b/tiledb/tests/fixtures.py
--- a/tiledb/tests/fixtures.py
+++ b/tiledb/tests/fixtures.py
@@ -7,6 +7,8 @@
 from numpy.testing import assert_array_equal
 from tiledb.tests.common import assert_subarrays_equal, rand_utf8
 
+INTEGER_DTYPES = ["u1", "u2", "u4", "u8", "i1", "i2", "i4", "i8"]
+
 
 @pytest.fixture(scope="module", params=["hilbert", "row-major"])
 def sparse_cell_order(request):
diff --git a/tiledb/tests/test_libtiledb.py b/tiledb/tests/test_libtiledb.py
--- a/tiledb/tests/test_libtiledb.py
+++ b/tiledb/tests/test_libtiledb.py
@@ -30,7 +30,9 @@
 from tiledb.tests.fixtures import (
     sparse_cell_order,
     test_incomplete_return_array,
+    INTEGER_DTYPES,
 )  # pyright: reportUnusedVariable=warning
+from tiledb.util import schema_from_dict
 
 
 def safe_dump(obj):
@@ -1085,6 +1087,15 @@ def test_array_id_point_queries(self):
         # TODO: handle queries like T[[2, 5, 10]] = ?
         pass
 
+    @pytest.mark.parametrize("dtype", INTEGER_DTYPES)
+    def test_dense_index_dtypes(self, dtype):
+        path = self.path()
+        data = np.arange(0, 3).astype(dtype)
+        with tiledb.from_numpy(path, data) as A:
+            pass
+        with tiledb.open(path) as B:
+            assert_array_equal(B[:], data)
+
     def test_array_2d(self):
         A = np.arange(10000).reshape((1000, 10))
 
@@ -2174,6 +2185,23 @@ def test_query_real_multi_index(self, sparse_cell_order):
                 "coords" not in T.query(coords=False).multi_index[-10.0:5.0]
             )
 
+    @pytest.mark.parametrize("dtype", INTEGER_DTYPES)
+    def test_sparse_index_dtypes(self, dtype):
+        path = self.path()
+        data = np.arange(0, 3).astype(dtype)
+
+        schema = schema_from_dict(attrs={"attr": data}, dims={"d0": data})
+        tiledb.SparseArray.create(path, schema)
+
+        with tiledb.open(path, "w") as A:
+            A[data] = data
+
+        with tiledb.open(path) as B:
+            assert_array_equal(B[:]["attr"], data)
+            assert B[data[0]]["attr"] == data[0]
+            assert B[data[1]]["attr"] == data[1]
+            assert B.multi_index[data[0]]["attr"] == data[0]
+
     def test_query_real_exact(self, sparse_cell_order):
         """
         Test and demo of querying at floating point representable boundaries
@@ -2583,8 +2611,12 @@ def test_sparse_2d_varlen_int(self, sparse_cell_order):
 
         tiledb.SparseArray.create(path, schema)
 
-        c1 = np.array([1, 2, 3, 4])
-        c2 = np.array([2, 1, 3, 4])
+        if tiledb.libtiledb.version() >= (2, 3) and sparse_cell_order == "hilbert":
+            c1 = np.array([2, 1, 3, 4])
+            c2 = np.array([1, 2, 3, 4])
+        else:
+            c1 = np.array([1, 2, 3, 4])
+            c2 = np.array([2, 1, 3, 4])
 
         data = np.array(
             [
@@ -2602,8 +2634,8 @@ def test_sparse_2d_varlen_int(self, sparse_cell_order):
         with tiledb.SparseArray(path) as A:
             res = A[:]
             assert_subarrays_equal(res[""], data)
-            assert_array_equal(res["__dim_0"], c1)
-            assert_array_equal(res["__dim_1"], c2)
+            assert_unordered_equal(res["__dim_0"], c1)
+            assert_unordered_equal(res["__dim_1"], c2)
 
     def test_sparse_mixed_domain_uint_float64(self, sparse_cell_order):
         path = self.path("mixed_domain_uint_float64")