lfnovo/open-notebook · critical · DatabaseOperationError

Error saving record: {e}

Error message

Error saving record: {e}

What it means

save() re-raises InvalidInputError/RuntimeTime transaction conflicts untouched but wraps every other unexpected exception in DatabaseOperationError after logging 'Error saving record: {e}'. It indicates the write to SurrealDB or record serialization failed at the infrastructure level.

Source

Thrown at open_notebook/domain/base.py:195

            result_list: List[Dict[str, Any]] = (
                repo_result if isinstance(repo_result, list) else [repo_result]
            )
            for key, value in result_list[0].items():
                if hasattr(self, key):
                    if isinstance(getattr(self, key), BaseModel):
                        setattr(self, key, type(getattr(self, key))(**value))
                    else:
                        setattr(self, key, value)

        except ValidationError as e:
            logger.error(f"Validation failed: {e}")
            raise
        except RuntimeError:
            # Transaction conflicts should propagate for retry
            raise
        except Exception as e:
            logger.error(f"Error saving record: {e}")
            raise DatabaseOperationError(e)

    def _prepare_save_data(self) -> Dict[str, Any]:
        data = self.model_dump()
        return {
            key: value
            for key, value in data.items()
            if value is not None or key in self.__class__.nullable_fields
        }

    async def delete(self) -> bool:
        if self.id is None:
            raise InvalidInputError("Cannot delete object without an ID")
        try:
            logger.debug(f"Deleting record with id {self.id}")
            return await repo_delete(self.id)
        except Exception as e:
            logger.error(
                f"Error deleting {self.__class__.table_name} with id {self.id}: {str(e)}"

View on GitHub (pinned to a7de90d38a)

Solutions

  1. Check SurrealDB is up and the API connected at startup (make status)
  2. Inspect logs / e.__cause__ for the server's rejection reason
  3. If type-related, ensure model fields are JSON-serializable (datetime, str, list, dict)
  4. Catch DatabaseOperationError in callers and decide on retry vs user-facing error

Example fix

# before
await note.save()  # unhandled DatabaseOperationError

# after
from open_notebook.domain.errors import DatabaseOperationError
try:
    await note.save()
except DatabaseOperationError as e:
    logger.error(f"save failed: {e}")
    raise HTTPException(status_code=503, detail="Could not save note")
Defensive patterns

Strategy: retry

Validate before calling

null

Try / catch

from open_notebook.domain.errors import DatabaseOperationError
for attempt in range(3):
    try:
        await note.save()
        break
    except DatabaseOperationError:
        if attempt == 2:
            raise HTTPException(status_code=503, detail="Could not save")
        await asyncio.sleep(2 ** attempt)

Prevention

When it happens

Trigger: SurrealDB unreachable, unique-constraint/style server rejections, serialization failures on field types, or invalid record data the server refuses. The original exception is attached as the cause for inspection.

Common situations: DB container stopped mid-run; model fields holding types SurrealDB can't store (e.g. arbitrary objects); concurrent writes hitting server-side limits; running the worker without the database tier up.

Related errors


AI-assisted analysis of lfnovo/open-notebook@a7de90d38a (2026-08-27). Data as JSON: /api/errors/b6ec98343b07f773. Report an issue: GitHub.