Skip to content

Commit

Permalink
[ENH]: add method to delete database
Browse files Browse the repository at this point in the history
  • Loading branch information
codetheweb committed Jan 10, 2025
1 parent 457f855 commit 1b4c82b
Show file tree
Hide file tree
Showing 18 changed files with 332 additions and 0 deletions.
11 changes: 11 additions & 0 deletions chromadb/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,17 @@ def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
"""
pass

@abstractmethod
def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
"""Delete a database. Raises an error if the database does not exist.
Args:
database: The name of the database to delete.
tenant: The tenant of the database to delete.
"""
pass

@abstractmethod
def list_databases(
self,
Expand Down
11 changes: 11 additions & 0 deletions chromadb/api/async_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,17 @@ async def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Databas
"""
pass

@abstractmethod
async def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
"""Delete a database. Raises an error if the database does not exist.
Args:
database: The name of the database to delete.
tenant: The tenant of the database to delete.
"""
pass

@abstractmethod
async def list_databases(
self,
Expand Down
4 changes: 4 additions & 0 deletions chromadb/api/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,10 @@ async def create_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None
async def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
return await self._server.get_database(name=name, tenant=tenant)

@override
async def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
return await self._server.delete_database(name=name, tenant=tenant)

@override
async def list_databases(
self,
Expand Down
12 changes: 12 additions & 0 deletions chromadb/api/async_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@ async def get_database(
id=response["id"], name=response["name"], tenant=response["tenant"]
)

@trace_method("AsyncFastAPI.delete_database", OpenTelemetryGranularity.OPERATION)
@override
async def delete_database(
self,
name: str,
tenant: str = DEFAULT_TENANT,
) -> None:
await self._make_request(
"delete",
f"/tenants/{tenant}/databases/{name}",
)

@trace_method("AsyncFastAPI.list_databases", OpenTelemetryGranularity.OPERATION)
@override
async def list_databases(
Expand Down
4 changes: 4 additions & 0 deletions chromadb/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@ def create_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
return self._server.get_database(name=name, tenant=tenant)

@override
def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
return self._server.delete_database(name=name, tenant=tenant)

@override
def list_databases(
self,
Expand Down
13 changes: 13 additions & 0 deletions chromadb/api/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ def get_database(
id=resp_json["id"], name=resp_json["name"], tenant=resp_json["tenant"]
)

@trace_method("FastAPI.delete_database", OpenTelemetryGranularity.OPERATION)
@override
def delete_database(
self,
name: str,
tenant: str = DEFAULT_TENANT,
) -> None:
"""Deletes a database"""
self._make_request(
"delete",
f"/tenants/{tenant}/databases/{name}",
)

@trace_method("FastAPI.list_databases", OpenTelemetryGranularity.OPERATION)
@override
def list_databases(
Expand Down
5 changes: 5 additions & 0 deletions chromadb/api/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ def create_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> t.Database:
return self._sysdb.get_database(name=name, tenant=tenant)

@trace_method("SegmentAPI.delete_database", OpenTelemetryGranularity.OPERATION)
@override
def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
self._sysdb.delete_database(name=name, tenant=tenant)

@trace_method("SegmentAPI.list_databases", OpenTelemetryGranularity.OPERATION)
@override
def list_databases(
Expand Down
1 change: 1 addition & 0 deletions chromadb/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ class AuthzAction(str, Enum):
GET_TENANT = "tenant:get_tenant"
CREATE_DATABASE = "db:create_database"
GET_DATABASE = "db:get_database"
DELETE_DATABASE = "db:delete_database"
LIST_DATABASES = "db:list_databases"
LIST_COLLECTIONS = "db:list_collections"
COUNT_COLLECTIONS = "db:count_collections"
Expand Down
4 changes: 4 additions & 0 deletions chromadb/db/impl/grpc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
raise NotFoundError()
raise InternalError()

@overrides
def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
raise NotImplementedError()

@overrides
def list_databases(
self,
Expand Down
29 changes: 29 additions & 0 deletions chromadb/db/mixins/sysdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,35 @@ def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
tenant=tenant,
)

@override
def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
with self.tx() as cur:
databases = Table("databases")
q = (
self.querybuilder()
.from_(databases)
.where(databases.name == ParameterValue(name))
.where(databases.tenant_id == ParameterValue(tenant))
.delete()
)
sql, params = get_sql(q, self.parameter_format())
sql = sql + " RETURNING id"
result = cur.execute(sql, params).fetchone()
if not result:
raise NotFoundError(f"Database {name} not found for tenant {tenant}")

# As of 01/09/2025, cascading deletes don't work because foreign keys are not enabled.
# See https://github.com/chroma-core/chroma/issues/3456.
collections = Table("collections")
q = (
self.querybuilder()
.from_(collections)
.where(collections.database_id == ParameterValue(result[0]))
.delete()
)
sql, params = get_sql(q, self.parameter_format())
cur.execute(sql, params)

@override
def list_databases(
self,
Expand Down
5 changes: 5 additions & 0 deletions chromadb/db/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ def get_database(self, name: str, tenant: str = DEFAULT_TENANT) -> Database:
exist."""
pass

@abstractmethod
def delete_database(self, name: str, tenant: str = DEFAULT_TENANT) -> None:
"""Delete a database."""
pass

@abstractmethod
def list_databases(
self,
Expand Down
29 changes: 29 additions & 0 deletions chromadb/server/fastapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ def setup_v2_routes(self) -> None:
response_model=None,
)

self.router.add_api_route(
"/api/v2/tenants/{tenant}/databases/{database_name}",
self.delete_database,
methods=["DELETE"],
response_model=None,
)

self.router.add_api_route(
"/api/v2/tenants",
self.create_tenant,
Expand Down Expand Up @@ -559,6 +566,28 @@ async def get_database(
),
)

@trace_method("FastAPI.delete_database", OpenTelemetryGranularity.OPERATION)
async def delete_database(
self,
request: Request,
database_name: str,
tenant: str,
) -> None:
self.auth_request(
request.headers,
AuthzAction.DELETE_DATABASE,
tenant,
database_name,
None,
)

await to_thread.run_sync(
self._api.delete_database,
database_name,
tenant,
limiter=self._capacity_limiter,
)

@trace_method("FastAPI.create_tenant", OpenTelemetryGranularity.OPERATION)
async def create_tenant(
self,
Expand Down
76 changes: 76 additions & 0 deletions chromadb/test/api/test_delete_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import pytest
from chromadb.api.client import AdminClient, Client
from chromadb.config import System
from chromadb.db.impl.sqlite import SqliteDB
from chromadb.errors import InvalidCollectionException, NotFoundError
from chromadb.test.conftest import NOT_CLUSTER_ONLY, ClientFactories


def test_deletes_database(client_factories: ClientFactories) -> None:
if not NOT_CLUSTER_ONLY:
pytest.skip("This API is not yet supported by distributed")

admin_client = client_factories.create_admin_client_from_system()

admin_client.create_database("test_delete_database")

client = client_factories.create_client(database="test_delete_database")
collection = client.create_collection("foo")

admin_client.delete_database("test_delete_database")

with pytest.raises(NotFoundError):
admin_client.get_database("test_delete_database")

with pytest.raises(InvalidCollectionException):
client.get_collection("foo")

with pytest.raises(InvalidCollectionException):
collection.upsert(["foo"], [0.0, 0.0, 0.0])


def test_does_not_affect_other_databases(client_factories: ClientFactories) -> None:
if not NOT_CLUSTER_ONLY:
pytest.skip("This API is not yet supported by distributed")

admin_client = client_factories.create_admin_client_from_system()

admin_client.create_database("first")
admin_client.create_database("second")

client = client_factories.create_client(database="second")
collection = client.create_collection("test")

admin_client.delete_database("first")

assert client.get_collection("test").id == collection.id


def test_collection_was_removed(sqlite_persistent: System) -> None:
sqlite = sqlite_persistent.instance(SqliteDB)

admin_client = AdminClient.from_system(sqlite_persistent)
admin_client.create_database("test_delete_database")

client = Client.from_system(sqlite_persistent, database="test_delete_database")
client.create_collection("foo")

admin_client.delete_database("test_delete_database")

with pytest.raises(InvalidCollectionException):
client.get_collection("foo")

# Check table
with sqlite.tx() as cur:
row = cur.execute("SELECT COUNT(*) from collections").fetchone()
assert row[0] == 0


def test_errors_when_database_does_not_exist(client_factories: ClientFactories) -> None:
if not NOT_CLUSTER_ONLY:
pytest.skip("This API is not yet supported by distributed")

admin_client = client_factories.create_admin_client_from_system()

with pytest.raises(NotFoundError):
admin_client.delete_database("foo")
20 changes: 20 additions & 0 deletions clients/js/src/AdminClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,26 @@ export class AdminClient {
return { name: getDatabase.name } as Database;
}

/**
* Deletes a database.
*
* @param {Object} params - The parameters for deleting a database.
* @param {string} params.name - The name of the database.
* @param {string} params.tenantName - The name of the tenant.
*
* @returns {Promise<void>} A promise that returns nothing.
* @throws {Error} If there is an issue deleting the database.
*/
public async deleteDatabase({
name,
tenantName,
}: {
name: string;
tenantName: string;
}): Promise<void> {
await this.api.deleteDatabase(name, tenantName, this.api.options);
}

/**
* Lists database for a specific tenant.
*
Expand Down
Loading

0 comments on commit 1b4c82b

Please sign in to comment.