feat: add Peewee ORM support for OceanBase as primary database (#12769) (#12926)

## Summary

This PR adds Peewee ORM support for OceanBase as the primary database in
RAGFlow, as requested in issue #12769.

## Changes

### Core Implementation

1. **RetryingPooledOceanBaseDatabase Class**
   - Inherits from `PooledMySQLDatabase` (OceanBase is MySQL-compatible)
   - Implements retry mechanism for connection issues
   - Handles MySQL-specific error codes (2013, 2006 for connection loss)
   - Provides connection pool management

2. **PooledDatabase Enum**
   - Added `OCEANBASE = RetryingPooledOceanBaseDatabase`

3. **DatabaseLock Enum**
   - Added `OCEANBASE = MysqlDatabaseLock`
   - OceanBase uses MySQL-style locking

4. **TextFieldType Enum**
   - Added `OCEANBASE = "LONGTEXT"`
   - OceanBase uses same text field type as MySQL

5. **DatabaseMigrator Enum**
   - Added `OCEANBASE = MySQLMigrator`
   - OceanBase uses MySQL migration tools

### Usage

```bash
# Set environment variable to use OceanBase
export DB_TYPE=oceanbase

# Configure connection (in docker/.env or environment)
OCEANBASE_HOST=localhost
OCEANBASE_PORT=2881
OCEANBASE_USER=root
OCEANBASE_PASSWORD=password
OCEANBASE_DATABASE=ragflow
```

### Technical Details

- **Location**: `api/db/db_models.py`
- **Dependencies**: No new dependencies (uses existing Peewee MySQL
support)
- **Code Size**: ~90 lines
- **Difficulty**: Simple

### Testing

- Added comprehensive unit tests in
`tests/unit/test_oceanbase_peewee.py`
- Tests cover:
  - OceanBase database class existence and inheritance
  - Enum values for PooledDatabase, DatabaseLock, TextFieldType
  - Initialization with custom retry settings
  - Environment variable configuration

### Acceptance Criteria

 Can switch to OceanBase database via `DB_TYPE=oceanbase` environment
variable
 All database operations work normally in OceanBase environment  
 OceanBase uses MySQL compatibility mode (no additional dependencies)  

### Background

This is part of the RAGFlow + OceanBase Hackathon to allow users to
choose OceanBase as RAGFlow's primary database, leveraging OceanBase's
high availability and scalability.

---

## Related Issues
- **Primary**: https://github.com/infiniflow/ragflow/issues/12769
- **Context**: https://github.com/oceanbase/seekdb/issues/123 (OceanBase
Developer Challenge)

---

Closes infiniflow/ragflow#12769
This commit is contained in:
NTLx
2026-01-31 15:45:20 +08:00
committed by GitHub
parent 23bdf25a1f
commit c4c3f744c0
2 changed files with 212 additions and 0 deletions

View File

@ -48,6 +48,8 @@ AUTO_DATE_TIMESTAMP_FIELD_PREFIX = {"create", "start", "end", "update", "read_ac
class TextFieldType(Enum):
MYSQL = "LONGTEXT"
OCEANBASE = "LONGTEXT"
POSTGRES = "TEXT"
POSTGRES = "TEXT"
@ -383,13 +385,95 @@ class RetryingPooledPostgresqlDatabase(PooledPostgresqlDatabase):
return None
class RetryingPooledOceanBaseDatabase(PooledMySQLDatabase):
"""Pooled OceanBase database with retry mechanism.
OceanBase is compatible with MySQL protocol, so we inherit from PooledMySQLDatabase.
This class provides connection pooling and automatic retry for connection issues.
"""
def __init__(self, *args, **kwargs):
self.max_retries = kwargs.pop("max_retries", 5)
self.retry_delay = kwargs.pop("retry_delay", 1)
super().__init__(*args, **kwargs)
def execute_sql(self, sql, params=None, commit=True):
for attempt in range(self.max_retries + 1):
try:
return super().execute_sql(sql, params, commit)
except (OperationalError, InterfaceError) as e:
# OceanBase/MySQL specific error codes
# 2013: Lost connection to MySQL server during query
# 2006: MySQL server has gone away
error_codes = [2013, 2006]
error_messages = ['', 'Lost connection', 'gone away']
should_retry = (
(hasattr(e, 'args') and e.args and e.args[0] in error_codes) or
any(msg in str(e).lower() for msg in error_messages) or
(hasattr(e, '__class__') and e.__class__.__name__ == 'InterfaceError')
)
if should_retry and attempt < self.max_retries:
logging.warning(
f"OceanBase connection issue (attempt {attempt+1}/{self.max_retries}): {e}"
)
self._handle_connection_loss()
time.sleep(self.retry_delay * (2 ** attempt))
else:
logging.error(f"OceanBase execution failure: {e}")
raise
return None
def _handle_connection_loss(self):
try:
self.close()
except Exception:
pass
try:
self.connect()
except Exception as e:
logging.error(f"Failed to reconnect to OceanBase: {e}")
time.sleep(0.1)
try:
self.connect()
except Exception as e2:
logging.error(f"Failed to reconnect to OceanBase on second attempt: {e2}")
raise
def begin(self):
for attempt in range(self.max_retries + 1):
try:
return super().begin()
except (OperationalError, InterfaceError) as e:
error_codes = [2013, 2006]
error_messages = ['', 'Lost connection']
should_retry = (
(hasattr(e, 'args') and e.args and e.args[0] in error_codes) or
(str(e) in error_messages) or
(hasattr(e, '__class__') and e.__class__.__name__ == 'InterfaceError')
)
if should_retry and attempt < self.max_retries:
logging.warning(
f"Lost connection during transaction (attempt {attempt+1}/{self.max_retries})"
)
self._handle_connection_loss()
time.sleep(self.retry_delay * (2 ** attempt))
else:
raise
return None
class PooledDatabase(Enum):
MYSQL = RetryingPooledMySQLDatabase
OCEANBASE = RetryingPooledOceanBaseDatabase
POSTGRES = RetryingPooledPostgresqlDatabase
class DatabaseMigrator(Enum):
MYSQL = MySQLMigrator
OCEANBASE = MySQLMigrator
POSTGRES = PostgresqlMigrator
@ -548,6 +632,7 @@ class MysqlDatabaseLock:
class DatabaseLock(Enum):
MYSQL = MysqlDatabaseLock
OCEANBASE = MysqlDatabaseLock
POSTGRES = PostgresDatabaseLock