diff --git a/unittest/gunit/villagesql/CMakeLists.txt b/unittest/gunit/villagesql/CMakeLists.txt
index 4213298ceda..157d5eb570f 100644
--- a/unittest/gunit/villagesql/CMakeLists.txt
+++ b/unittest/gunit/villagesql/CMakeLists.txt
@@ -20,6 +20,7 @@ SET(VILLAGESQL_UNIT_TESTS
abi_v1_check-t
abi_v2_check-t
custom_indexes-t
+ extension_uninstall_checks-t
semver-t
type_context-t
type_descriptor-t
@@ -63,6 +64,7 @@ ENDMACRO()
ADD_VILLAGESQL_TEST(abi_v1_check-t)
ADD_VILLAGESQL_TEST(abi_v2_check-t)
ADD_VILLAGESQL_TEST(custom_indexes-t)
+ADD_VILLAGESQL_TEST(extension_uninstall_checks-t)
ADD_VILLAGESQL_TEST(semver-t)
ADD_VILLAGESQL_TEST(type_context-t)
ADD_VILLAGESQL_TEST(type_descriptor-t)
diff --git a/unittest/gunit/villagesql/extension_uninstall_checks-t.cc b/unittest/gunit/villagesql/extension_uninstall_checks-t.cc
new file mode 100644
index 00000000000..ef8511a67b5
--- /dev/null
+++ b/unittest/gunit/villagesql/extension_uninstall_checks-t.cc
@@ -0,0 +1,84 @@
+/* Copyright (c) 2026 VillageSQL Contributors
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ */
+
+// TODO(villagesql-indexing): When CREATE INDEX ... USING EXTENDED is wired
+// end-to-end through VEF, add a functional mysql-test
+// (custom_index_prevents_extension_uninstall.test) that exercises this
+// gating through the SQL surface. Until then, the unit tests below pin the
+// predicate directly.
+
+#include
+
+#include
+#include
+
+#include "unittest/gunit/test_utils.h"
+#include "villagesql/schema/systable/custom_indexes.h"
+#include "villagesql/schema/systable/extensions.h"
+#include "villagesql/schema/systable/helpers.h"
+#include "villagesql/veb/extension_uninstall_checks.h"
+
+namespace villagesql_unittest {
+
+using namespace villagesql;
+
+class ExtensionUninstallChecksTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ villagesql::test_set_lower_case_table_names(0);
+ system_charset_info = &my_charset_utf8mb4_0900_ai_ci;
+ }
+};
+
+TEST_F(ExtensionUninstallChecksTest, CustomIndexPreventsExtensionUninstall) {
+ ExtensionEntry ext(ExtensionKey("vsql_test"), "1.0.0", /*hash=*/{});
+
+ IndexEntry idx(IndexKey("mydb", "t", "idx"), /*id=*/1, "vsql_test", "1.0.0",
+ "test_idx");
+ std::vector all_indexes = {&idx};
+
+ EXPECT_TRUE(check_for_indexes_of_extension(ext, all_indexes));
+}
+
+TEST_F(ExtensionUninstallChecksTest,
+ OtherExtensionsIndexDoesNotPreventUninstall) {
+ ExtensionEntry ext(ExtensionKey("vsql_test"), "1.0.0", /*hash=*/{});
+
+ IndexEntry idx(IndexKey("mydb", "t", "idx"), /*id=*/1, "other_ext", "1.0.0",
+ "test_idx");
+ std::vector all_indexes = {&idx};
+
+ EXPECT_FALSE(check_for_indexes_of_extension(ext, all_indexes));
+}
+
+TEST_F(ExtensionUninstallChecksTest, VersionMismatchDoesNotPreventUninstall) {
+ ExtensionEntry ext(ExtensionKey("vsql_test"), "2.0.0", /*hash=*/{});
+
+ IndexEntry idx(IndexKey("mydb", "t", "idx"), /*id=*/1, "vsql_test", "1.0.0",
+ "test_idx");
+ std::vector all_indexes = {&idx};
+
+ EXPECT_FALSE(check_for_indexes_of_extension(ext, all_indexes));
+}
+
+TEST_F(ExtensionUninstallChecksTest, NoIndexesIsAllowed) {
+ ExtensionEntry ext(ExtensionKey("vsql_test"), "1.0.0", /*hash=*/{});
+ std::vector all_indexes = {};
+
+ EXPECT_FALSE(check_for_indexes_of_extension(ext, all_indexes));
+}
+
+} // namespace villagesql_unittest
diff --git a/villagesql/veb/CMakeLists.txt b/villagesql/veb/CMakeLists.txt
index 2b2f59f72f4..86d1db9f6e9 100644
--- a/villagesql/veb/CMakeLists.txt
+++ b/villagesql/veb/CMakeLists.txt
@@ -14,6 +14,7 @@
# along with this program; if not, see .
SET(VILLAGESQL_VEB_SOURCES
+ extension_uninstall_checks.cc
register.cc
sql_extension.cc
validate.cc
diff --git a/villagesql/veb/extension_uninstall_checks.cc b/villagesql/veb/extension_uninstall_checks.cc
new file mode 100644
index 00000000000..d279d15d513
--- /dev/null
+++ b/villagesql/veb/extension_uninstall_checks.cc
@@ -0,0 +1,52 @@
+/* Copyright (c) 2026 VillageSQL Contributors
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ */
+
+#include "villagesql/veb/extension_uninstall_checks.h"
+
+#include "my_sys.h"
+
+#include "villagesql/include/error.h"
+
+namespace villagesql {
+
+bool check_for_indexes_of_extension(
+ const ExtensionEntry &ext_entry,
+ const std::vector &all_indexes) {
+ const IndexEntry *first = nullptr;
+ int count = 0;
+
+ for (const auto *entry : all_indexes) {
+ if (entry->extension_name == ext_entry.extension_name() &&
+ entry->extension_version == ext_entry.extension_version) {
+ if (count == 0) first = entry;
+ count++;
+ }
+ }
+
+ if (first != nullptr) {
+ villagesql_error(
+ "Cannot drop extension `%s` as %d custom index(es) depend on it, "
+ "e.g. %s.%s.%s uses index type %s",
+ MYF(0), ext_entry.extension_name().c_str(), count,
+ first->db_name().c_str(), first->table_name().c_str(),
+ first->index_name().c_str(), first->index_type_name.c_str());
+ return true;
+ }
+
+ return false;
+}
+
+} // namespace villagesql
diff --git a/villagesql/veb/extension_uninstall_checks.h b/villagesql/veb/extension_uninstall_checks.h
new file mode 100644
index 00000000000..3b21d884f05
--- /dev/null
+++ b/villagesql/veb/extension_uninstall_checks.h
@@ -0,0 +1,37 @@
+/* Copyright (c) 2026 VillageSQL Contributors
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ */
+
+#ifndef VILLAGESQL_VEB_EXTENSION_UNINSTALL_CHECKS_H_
+#define VILLAGESQL_VEB_EXTENSION_UNINSTALL_CHECKS_H_
+
+#include
+
+#include "villagesql/schema/systable/custom_indexes.h"
+#include "villagesql/schema/systable/extensions.h"
+
+namespace villagesql {
+
+// Returns true and emits villagesql_error if any IndexEntry in
+// `all_indexes` belongs to `ext_entry` (matched on extension_name +
+// extension_version). RESTRICT semantics: the caller aborts uninstall when
+// this returns true. No system-table mutations occur.
+bool check_for_indexes_of_extension(
+ const ExtensionEntry &ext_entry,
+ const std::vector &all_indexes);
+
+} // namespace villagesql
+
+#endif // VILLAGESQL_VEB_EXTENSION_UNINSTALL_CHECKS_H_
diff --git a/villagesql/veb/sql_extension.cc b/villagesql/veb/sql_extension.cc
index b03e9441b06..eb16dae6b27 100644
--- a/villagesql/veb/sql_extension.cc
+++ b/villagesql/veb/sql_extension.cc
@@ -54,6 +54,7 @@
#include "villagesql/services/status_vars.h"
#include "villagesql/services/sys_vars.h"
#include "villagesql/sql/metadata_modifier.h"
+#include "villagesql/veb/extension_uninstall_checks.h"
#include "villagesql/veb/register.h"
#include "villagesql/veb/validate.h"
#include "villagesql/veb/veb_file.h"
@@ -412,6 +413,11 @@ bool remove_extension_from_victionary(
return true;
}
+ const auto &all_indexes = victionary.custom_indexes().get_all_committed();
+ if (villagesql::check_for_indexes_of_extension(*ext_entry, all_indexes)) {
+ return true;
+ }
+
// Check for active references to VDFs, TypeContexts, and TypeDescriptors.
// A use_count > 1 means something other than Victionary holds a reference
// (e.g., an executing query).