From 03d305fefea4a928223e2749a4aa6f517013877b Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Sun, 1 Nov 2026 20:39:23 +0900 Subject: [PATCH] native: drop_type() drops populated graph types; a held VectorIndex re-reads after a drop (#164, #165) Two native/JVM differences found by an audit of the native code paths after #162: - drop_type ran SQL DROP TYPE, which refuses a vertex or edge type that holds records; the JVM build calls LocalSchema.dropType, which drops it, as the docstring says. The native call now runs DROP TYPE ... UNSAFE. - get_size(), get_metadata() and get_quantization() of a held native VectorIndex answered after DROP INDEX (a SQL row count, a memoized engine_info). They now re-read the metadata first, as get_stats() did: a dropped index raises with its name, a re-created one reports its own configuration. Shared tests: drop of a populated vertex and edge type; every read of a dropped index raises; a re-created index with quantization NONE is reported as NONE. Against the pre-fix 26.11.1.dev0 wheels the native wheel fails all four, the JVM wheel passes all four. Closes #164 Closes #165 Co-Authored-By: Claude Opus 5.5 --- src/arcadedb/_native/database.py | 4 +++- src/arcadedb/_native/schema_objects.py | 19 ++++++++++++----- tests/test_schema.py | 25 ++++++++++++++++++++++ tests/test_vector_index_introspection.py | 27 +++++++++++++++++++++--- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/arcadedb/_native/database.py b/src/arcadedb/_native/database.py index 21b0592..3f955df 100644 --- a/src/arcadedb/_native/database.py +++ b/src/arcadedb/_native/database.py @@ -1319,7 +1319,9 @@ def exists_type(self, name): return bool(rows) def drop_type(self, name): - self._run(f"DROP TYPE `{name}`") + # UNSAFE: the engine's dropType, which the JPype build calls, drops a type with its records; plain DROP TYPE refuses + # a vertex or edge type that still holds records + self._run(f"DROP TYPE `{name}` UNSAFE") def create_property(self, type_name, property_name, property_type, of_type=None): t = getattr(property_type, "value", property_type) diff --git a/src/arcadedb/_native/schema_objects.py b/src/arcadedb/_native/schema_objects.py index ceb647a..c593064 100644 --- a/src/arcadedb/_native/schema_objects.py +++ b/src/arcadedb/_native/schema_objects.py @@ -300,11 +300,20 @@ def _lookup_query_vector_by_key(self, key): ) return row.get("query_vector") + def _live_info(self): + # read afresh, the native side of the JPype wrapper's _check_live: a dropped index raises here (the glue names it), + # REBUILD INDEX builds new bucket indexes, and a re-created index has its own configuration; a memoized read + # answered all three from the old index + self._shim.__dict__.pop("_engine_info", None) + return self._shim.engine_info() + + def get_size(self): + self._live_info() + return super().get_size() + def get_stats(self): """The engine's live counters of the primary bucket index, from its ``schema:index:`` row (the row's own columns left out).""" - # read afresh: REBUILD INDEX builds new bucket indexes, so the name a memoized read holds can be gone - self._shim.__dict__.pop("_engine_info", None) - bucket = self._shim.engine_info()["bucket"] + bucket = self._live_info()["bucket"] rows = self._database.query( "sql", f"SELECT FROM schema:index:`{bucket}`", # nosec B608 - a backtick-quoted index name the engine reported @@ -317,7 +326,7 @@ def get_stats(self): def get_metadata(self): """The engine's configuration of the index: what this session created and what an index opened from disk carries, defaults included.""" - info = self._shim.engine_info() + info = self._live_info() return { "index_name": self._shim._name, "bucket_index_name": info["bucket"], @@ -329,7 +338,7 @@ def get_metadata(self): } def get_quantization(self): - return str(self._shim.engine_info()["quantization"] or "NONE") + return str(self._live_info()["quantization"] or "NONE") # the descriptive columns of a schema:index row; every other column is a counter of IndexInternal.getStats() diff --git a/tests/test_schema.py b/tests/test_schema.py index 37b3750..3108056 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -201,6 +201,31 @@ def test_drop_type(self, test_db): schema.drop_type("TempType") assert not schema.exists_type("TempType") + @pytest.mark.parametrize("kind", ["vertex", "edge"]) + def test_drop_type_drops_a_populated_graph_type(self, test_db, kind): + """``drop_type`` drops a type and all its data, records included, on both builds. + + The native build ran SQL ``DROP TYPE``, which refuses a vertex or edge type that holds records unless + ``UNSAFE`` is given; the JVM build calls the engine's ``dropType``, which does not check for records. + """ + schema = test_db.schema + schema.create_vertex_type("Person") + schema.create_edge_type("Knows") + with test_db.transaction(): + test_db.command("sql", "CREATE VERTEX Person SET name = 'a'") + test_db.command("sql", "CREATE VERTEX Person SET name = 'b'") + test_db.command( + "sql", + "CREATE EDGE Knows FROM (SELECT FROM Person WHERE name = 'a') " + "TO (SELECT FROM Person WHERE name = 'b')", + ) + name = "Person" if kind == "vertex" else "Knows" + assert test_db.count_type(name) > 0 + + schema.drop_type(name) + + assert not schema.exists_type(name) + def test_drop_non_existent_type_fails(self, test_db): """Test dropping non-existent type raises error.""" schema = test_db.schema diff --git a/tests/test_vector_index_introspection.py b/tests/test_vector_index_introspection.py index 4a6631e..ce8e678 100644 --- a/tests/test_vector_index_introspection.py +++ b/tests/test_vector_index_introspection.py @@ -369,7 +369,28 @@ def test_dropped_index_raises_and_never_answers_empty(self, temp_db): if arcadedb.__backend__ == "jvm": # the JVM wrapper names the index itself; the native one passes the engine's own error on assert "Doc[embedding]" in str(excinfo.value) + # every read raises on both builds: the native wrapper answered get_size() with the row count and + # get_metadata()/get_quantization() from a memoized read of the dropped index + for read in ( + index.get_stats, + index.get_size, + index.get_metadata, + index.get_quantization, + ): with pytest.raises(arcadedb.ArcadeDBError, match=r"Doc\[embedding\]"): - index.get_stats() - with pytest.raises(arcadedb.ArcadeDBError, match=r"Doc\[embedding\]"): - index.get_size() + read() + + def test_a_recreated_index_reports_its_own_configuration(self, temp_db): + index = self._held_index(temp_db) + assert ( + index.get_quantization() != "NONE" + ) # create_vector_index defaults to INT8 + + temp_db.command("sql", "DROP INDEX `Doc[embedding]`") + temp_db.create_vector_index( + "Doc", "embedding", dimensions=3, quantization="NONE" + ) + + assert index.get_quantization() == "NONE" + assert index.get_metadata()["quantization"] == "NONE" + assert index.get_size() == len(self.ROWS)