Skip to main content

Database Service Interface

Overview​

All database implementations in Gofannon must inherit from the DatabaseService abstract base class and implement four core methods. This document provides detailed specifications for the interface.

Abstract Base Class​

Location: base.py

from abc import ABC, abstractmethod
from typing import Any, Dict, List

class DatabaseService(ABC):
"""Abstract base class for a generic database service."""

@abstractmethod
def get(self, db_name: str, doc_id: str) -> Dict[str, Any]:
"""Retrieve a document by ID."""
pass

@abstractmethod
def save(self, db_name: str, doc_id: str, doc: Dict[str, Any]) -> Dict[str, Any]:
"""Save (create or update) a document."""
pass

@abstractmethod
def delete(self, db_name: str, doc_id: str) -> None:
"""Delete a document by ID."""
pass

@abstractmethod
def list_all(self, db_name: str) -> List[Dict[str, Any]]:
"""List all documents in a database/collection."""
pass

Method Specifications​

get()​

Retrieve a single document by its identifier.

Signature:

def get(self, db_name: str, doc_id: str) -> Dict[str, Any]

Parameters:

  • db_name (str): The database/collection/table name
  • doc_id (str): The unique document identifier

Returns:

  • Dict[str, Any]: Dictionary containing the document data
  • Must include _id field with the document ID

Raises:

  • HTTPException(404): If document does not exist

Example:

doc = db.get("agents", "550e8400-e29b-41d4-a716-446655440000")
# Returns: {
# "_id": "550e8400-e29b-41d4-a716-446655440000",
# "name": "MyAgent",
# "description": "...",
# ...
# }

Implementation Requirements:

  1. Check if document exists
  2. If not found, raise HTTPException(status_code=404, detail="Document not found")
  3. Add _id field to returned document
  4. Return document as dictionary

Error Handling:

from fastapi import HTTPException

def get(self, db_name: str, doc_id: str) -> Dict[str, Any]:
doc = self._fetch_from_database(db_name, doc_id)
if doc is None:
raise HTTPException(
status_code=404,
detail=f"Document {doc_id} not found in {db_name}"
)
doc["_id"] = doc_id
return doc

save()​

Create a new document or update an existing document.

Signature:

def save(self, db_name: str, doc_id: str, doc: Dict[str, Any]) -> Dict[str, Any]

Parameters:

  • db_name (str): The database/collection/table name
  • doc_id (str): The unique document identifier
  • doc (Dict[str, Any]): The document data to save

Returns:

  • Dict[str, Any]: Metadata about the save operation
  • Must include at minimum:
    • "id": The document ID (echo of doc_id parameter)
    • "rev": Revision identifier (implementation-specific, can be placeholder)

Raises:

  • HTTPException(409): If there's a conflict (optional, for MVCC databases)
  • HTTPException(500): For other errors

Example:

doc = {
"name": "MyAgent",
"description": "An example agent",
"code": "def handler(event): return {}"
}
result = db.save("agents", "550e8400-e29b-41d4-a716-446655440000", doc)
# Returns: {"id": "550e8400-e29b-41d4-a716-446655440000", "rev": "1-abc123"}

Implementation Requirements:

  1. Add _id field to document: doc["_id"] = doc_id
  2. Create collection/table if it doesn't exist (database-specific)
  3. Handle update vs. insert logic
  4. For MVCC databases (like CouchDB), handle revision conflicts
  5. Return dictionary with id and rev fields

Upsert Behavior: The save() method should perform an upsert operation:

  • If document with doc_id exists: Update it
  • If document doesn't exist: Create it

Example Implementation:

def save(self, db_name: str, doc_id: str, doc: Dict[str, Any]) -> Dict[str, Any]:
# Add ID to document
doc["_id"] = doc_id

# Ensure collection exists
self._ensure_collection_exists(db_name)

# Perform upsert
try:
result = self._upsert_document(db_name, doc_id, doc)
return {
"id": doc_id,
"rev": result.get("revision", "v1")
}
except ConflictError:
raise HTTPException(status_code=409, detail="Document conflict")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

Revision Handling (for MVCC databases):

# CouchDB example
def save(self, db_name: str, doc_id: str, doc: Dict[str, Any]):
db = self.couch[db_name]
doc["_id"] = doc_id

# Get current revision if document exists
try:
existing = db[doc_id]
doc["_rev"] = existing["_rev"] # Include current revision
except ResourceNotFound:
pass # New document, no revision needed

doc_id, rev = db.save(doc)
return {"id": doc_id, "rev": rev}

delete()​

Delete a document by its identifier.

Signature:

def delete(self, db_name: str, doc_id: str) -> None

Parameters:

  • db_name (str): The database/collection/table name
  • doc_id (str): The unique document identifier

Returns:

  • None: No return value on success

Raises:

  • HTTPException(404): If document does not exist

Example:

db.delete("agents", "550e8400-e29b-41d4-a716-446655440000")
# Returns: None (or raises 404 if not found)

Implementation Requirements:

  1. Check if document exists
  2. If not found, raise HTTPException(status_code=404)
  3. Delete the document
  4. Return nothing (None)

Example Implementation:

def delete(self, db_name: str, doc_id: str) -> None:
exists = self._document_exists(db_name, doc_id)
if not exists:
raise HTTPException(
status_code=404,
detail=f"Document {doc_id} not found"
)

self._delete_document(db_name, doc_id)

Note on Return Value: Unlike save(), the delete() method returns None. The absence of an exception indicates success.


list_all()​

List all documents in a collection/table.

Signature:

def list_all(self, db_name: str) -> List[Dict[str, Any]]

Parameters:

  • db_name (str): The database/collection/table name

Returns:

  • List[Dict[str, Any]]: List of dictionaries, each containing a document
  • Each document must include its _id field
  • Returns empty list [] if collection doesn't exist or is empty

Raises:

  • Generally should not raise exceptions
  • Return [] for non-existent collections

Example:

docs = db.list_all("agents")
# Returns: [
# {"_id": "id-1", "name": "Agent 1", ...},
# {"_id": "id-2", "name": "Agent 2", ...},
# {"_id": "id-3", "name": "Agent 3", ...}
# ]

Implementation Requirements:

  1. Return empty list if collection doesn't exist
  2. Return all documents in the collection
  3. Each document must include _id field
  4. Order is not guaranteed (implementation-specific)

Example Implementation:

def list_all(self, db_name: str) -> List[Dict[str, Any]]:
try:
# Ensure collection exists
if not self._collection_exists(db_name):
return []

# Fetch all documents
documents = self._fetch_all_documents(db_name)

# Ensure _id field is present in each document
for doc in documents:
if "_id" not in doc:
doc["_id"] = doc.get("id") or str(uuid.uuid4())

return documents

except Exception:
# For non-existent collections or other errors, return empty list
return []

Pagination Considerations:

For large collections, consider implementing pagination internally:

def list_all(self, db_name: str) -> List[Dict[str, Any]]:
all_docs = []
page_token = None

while True:
# Fetch page of documents
page, page_token = self._fetch_page(db_name, page_token)
all_docs.extend(page)

if page_token is None:
break # No more pages

return all_docs

Performance Warning: list_all() can be expensive for large collections. Consider:

  • Implementing application-level pagination
  • Adding optional limit/offset parameters in future
  • Warning users about large result sets

Interface Conventions​

Database Name Parameter​

The db_name parameter represents a collection or table name, not a database server or instance.

Mapping by Implementation:

Implementationdb_name maps to
MemoryDictionary key namespace
CouchDBDatabase name
FirestoreCollection name
DynamoDBTable name

Example:

db.get("agents", "doc-id")  # "agents" is the collection/table
db.get("users", "doc-id") # "users" is a different collection/table

Document ID Field​

All documents must include an _id field containing their unique identifier.

Requirements:

  • Field name: _id (with underscore prefix)
  • Type: str
  • Must match the doc_id parameter
  • Present in all returned documents

Example:

doc = {
"_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "MyAgent",
# ... other fields
}

Error Handling​

Use FastAPI's HTTPException for all errors:

from fastapi import HTTPException

# Document not found
raise HTTPException(status_code=404, detail="Document not found")

# Conflict (MVCC)
raise HTTPException(status_code=409, detail="Document conflict")

# General errors
raise HTTPException(status_code=500, detail="Database operation failed")

Standard Status Codes:

  • 404: Document or collection not found
  • 409: Conflict (concurrent modification)
  • 500: Internal database error

Return Value Contracts​

get() Returns​

{
"_id": "document-id",
"field1": "value1",
"field2": "value2",
# ... document fields
}

save() Returns​

{
"id": "document-id", # Echo of doc_id parameter
"rev": "revision-id" # Version/revision identifier
}

delete() Returns​

None  # No return value

list_all() Returns​

[
{"_id": "id1", "field": "value"},
{"_id": "id2", "field": "value"},
# ... more documents
]

Type Annotations​

All methods should include proper type hints:

from typing import Any, Dict, List

def get(self, db_name: str, doc_id: str) -> Dict[str, Any]:
pass

def save(self, db_name: str, doc_id: str, doc: Dict[str, Any]) -> Dict[str, Any]:
pass

def delete(self, db_name: str, doc_id: str) -> None:
pass

def list_all(self, db_name: str) -> List[Dict[str, Any]]:
pass

Testing the Interface​

Every implementation should pass these interface tests:

def test_interface_compliance(db_service):
"""Test that implementation follows interface contract."""

# Test save() returns correct format
result = db_service.save("test", "id1", {"data": "value"})
assert "id" in result
assert "rev" in result
assert result["id"] == "id1"

# Test get() returns document with _id
doc = db_service.get("test", "id1")
assert "_id" in doc
assert doc["_id"] == "id1"

# Test delete() returns None
result = db_service.delete("test", "id1")
assert result is None

# Test list_all() returns list
docs = db_service.list_all("test")
assert isinstance(docs, list)

# Test get() raises 404 for non-existent document
with pytest.raises(HTTPException) as exc:
db_service.get("test", "nonexistent")
assert exc.value.status_code == 404

# Test delete() raises 404 for non-existent document
with pytest.raises(HTTPException) as exc:
db_service.delete("test", "nonexistent")
assert exc.value.status_code == 404

Usage Examples​

Creating a Document​

from services.database_service import get_database_service
from config import Settings

settings = Settings()
db = get_database_service(settings)

# Create a new agent
agent = {
"name": "MyAgent",
"description": "Example agent",
"code": "def handler(event): return {'status': 'ok'}"
}

result = db.save("agents", "550e8400-e29b-41d4-a716-446655440000", agent)
print(f"Saved agent with revision: {result['rev']}")

Retrieving a Document​

try:
agent = db.get("agents", "550e8400-e29b-41d4-a716-446655440000")
print(f"Agent name: {agent['name']}")
except HTTPException as e:
if e.status_code == 404:
print("Agent not found")

Updating a Document​

# Retrieve, modify, and save
agent = db.get("agents", "550e8400-e29b-41d4-a716-446655440000")
agent["description"] = "Updated description"
db.save("agents", "550e8400-e29b-41d4-a716-446655440000", agent)

Deleting a Document​

try:
db.delete("agents", "550e8400-e29b-41d4-a716-446655440000")
print("Agent deleted")
except HTTPException as e:
if e.status_code == 404:
print("Agent not found")

Listing All Documents​

agents = db.list_all("agents")
print(f"Found {len(agents)} agents:")
for agent in agents:
print(f" - {agent['_id']}: {agent.get('name', 'Unnamed')}")

Last Updated: 2026-01-11