From fae2f88baa96535cbd46306898cf767635bba114 Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Sun, 1 Nov 2026 20:51:04 +0900 Subject: [PATCH 1/2] native: drop_index(force=True) ignores a missing index; unique FULL_TEXT/SPARSE_VECTOR/GEOSPATIAL index is refused Found by the native/JVM parity probe (P6, P9). P6: JVM drop_index(force=True) skips the existence check, so a missing index is a no-op; native always ran DROP INDEX and raised "Index not found". Native now appends IF EXISTS when force is set (DropIndexStatement.ifExists). An existing index drops through the same engine path either way. P9: the engine's index factories for FULL_TEXT, LSM_SPARSE_VECTOR and GEOSPATIAL throw "... cannot be unique"; native dropped the unique flag for those types and silently built a non-unique index. Native now raises ArcadeDBError before any statement, naming the type and the uniqueness. Shared tests in tests/test_schema.py (both builds). Against the pre-fix 26.11.1.dev0 wheels: native fails both new tests (Index not found: Nope; DID NOT RAISE), JVM passes them; the force control passes on both. Closes #169 Co-Authored-By: Claude Opus 5.5 --- src/arcadedb/_native/database.py | 13 ++++++++++- tests/test_schema.py | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/arcadedb/_native/database.py b/src/arcadedb/_native/database.py index 21b0592e..1bb479bf 100644 --- a/src/arcadedb/_native/database.py +++ b/src/arcadedb/_native/database.py @@ -1347,10 +1347,20 @@ def drop_property(self, type_name, property_name): ("HASH", False): "NOTUNIQUE_HASH", } + # Index types whose engine factory refuses a unique index (LSMTreeFullTextIndex, LSMSparseVectorIndex, + # LSMTreeGeoIndex: "... cannot be unique"). LSM_TREE and HASH take it; LSM_VECTOR does not check it. + _NEVER_UNIQUE = frozenset({"FULL_TEXT", "LSM_SPARSE_VECTOR", "GEOSPATIAL"}) + def create_index( self, type_name, property_names, unique=False, index_type="LSM_TREE" ): kind = getattr(index_type, "value", index_type) + if unique and str(kind).upper() in self._NEVER_UNIQUE: + # Refuse before any statement runs, as the engine does, and worded like the JPype path + raise ArcadeDBError( + f"Failed to create index on '{type_name}[{','.join(property_names)}]': " + f"{str(kind).upper()} index cannot be unique" + ) kind = self._INDEX_KIND.get((kind, bool(unique)), kind) cols = ", ".join(f"`{p}`" for p in property_names) self._run(f"CREATE INDEX ON `{type_name}` ({cols}) {kind}") @@ -1364,7 +1374,8 @@ def exists_index(self, index_name): ) def drop_index(self, index_name, force=False): - self._run(f"DROP INDEX `{index_name}`") + # force skips the existence check, as on the JVM build (a missing index is then a no-op) + self._run(f"DROP INDEX `{index_name}`{' IF EXISTS' if force else ''}") # -- reads, as small objects with the engine's accessors (schema_objects.py) diff --git a/tests/test_schema.py b/tests/test_schema.py index 37b37504..1826fc1b 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -341,6 +341,17 @@ def test_create_full_text_index(self, test_db): ) assert idx is not None + def test_create_unique_full_text_index_is_refused(self, test_db): + """The engine refuses a unique full-text index; no index is left behind.""" + schema = test_db.schema + schema.create_document_type("T") + schema.create_property("T", "p", PropertyType.STRING) + + with pytest.raises(ArcadeDBError): + schema.create_index("T", ["p"], unique=True, index_type=IndexType.FULL_TEXT) + + assert [i for i in schema.get_indexes() if i.getTypeName() == "T"] == [] + def test_create_lsm_tree_index(self, test_db): """Test creating an LSM tree index explicitly.""" schema = test_db.schema @@ -435,6 +446,34 @@ def test_drop_index(self, test_db): schema.drop_index(index_name) assert schema.exists_index(index_name) is False + def test_drop_missing_index_force_is_a_no_op(self, test_db): + """force=True skips the existence check: a missing index is not an error.""" + schema = test_db.schema + schema.create_vertex_type("User") + schema.create_property("User", "temp", PropertyType.STRING) + schema.create_index("User", ["temp"]) + before = sorted(i.getName() for i in schema.get_indexes()) + + schema.drop_index("Nope", force=True) + + assert sorted(i.getName() for i in schema.get_indexes()) == before + with pytest.raises(ArcadeDBError): + schema.drop_index("Nope", force=False) + + def test_drop_existing_index_force(self, test_db): + """force=True still drops an index that exists.""" + schema = test_db.schema + schema.create_vertex_type("User") + schema.create_property("User", "temp", PropertyType.STRING) + schema.create_index("User", ["temp"]) + name = next( + i.getName() for i in schema.get_indexes() if i.getTypeName() == "User" + ) + + schema.drop_index(name, force=True) + + assert schema.exists_index(name) is False + def test_drop_non_existent_index_fails(self, test_db): """Test dropping non-existent index raises error.""" schema = test_db.schema From 69895bf18338152ca54186586f646c9d33c63087 Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Sun, 1 Nov 2026 20:55:47 +0900 Subject: [PATCH 2/2] native: get_identity() keeps an RID that is already one (#170) A row can carry @rid as an RID object (vector-search rows of engine e353330c5c); get_identity() wrapped it again, so str() of the identity raised "__str__ returned non-string". It now returns an RID as is. Native-only unit test with an RID-typed and a text RID; against the pre-fix native wheel it fails with that TypeError. Closes #170 Co-Authored-By: Claude Opus 5.5 --- src/arcadedb/_native/records.py | 4 +++- tests/test_native_point_read.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/arcadedb/_native/records.py b/src/arcadedb/_native/records.py index c727ff3c..6d815024 100644 --- a/src/arcadedb/_native/records.py +++ b/src/arcadedb/_native/records.py @@ -81,7 +81,9 @@ def get_rid(self) -> str: return self._rid or _UNSAVED def get_identity(self): - return RID(self.get_rid()) + # a row can already carry @rid as an RID (vector-search rows of some engine builds): wrapping it again made str() fail + rid = self.get_rid() + return rid if isinstance(rid, RID) else RID(rid) def get_type_name(self) -> str: self._check_open() diff --git a/tests/test_native_point_read.py b/tests/test_native_point_read.py index c4cd53c3..a86943d9 100644 --- a/tests/test_native_point_read.py +++ b/tests/test_native_point_read.py @@ -324,3 +324,14 @@ def test_exec_query_replies_as_call_bin_does(temp_db): assert (short[1] is None) == (long[1] is None) if flags == 4: assert short[1] is not None and np.array_equal(short[1], long[1]) + + +def test_identity_of_a_record_whose_rid_came_as_an_rid_object(): + # the vector-search rows of engine e353330c5c carry @rid as an RID, not text: get_identity() wrapped it twice and + # str(record.get_identity()) raised "__str__ returned non-string" + from arcadedb._native.records import Document as NativeDocument + + for rid in (codec.RID("#3:7"), "#3:7"): + identity = NativeDocument(None, "T", rid, {}).get_identity() + assert str(identity) == "#3:7" + assert identity == codec.RID("#3:7")