Docs: v0.27.2 -> v1.0.0-rc1 (#20338)

This commit is contained in:
Wang Qi authored and GitHub committed 2026-09-29 13:20:25 +08:00
1 parent dc7656eb92
commit 1be9590a4a
16 files changed
+23 -3717

No files matched your search

+6 -6
View File
@@ -15,9 +15,9 @@
#
# CI (.github/workflows/sep-tests.yml) appends `true` here on every run: the
# image stamps its own git-describe output into /ragflow/VERSION, so a CI build
# reports v0.27.x against the v1.0.0-rc1.dev1 marker it migrated to moments
# reports v0.27.x against the v1.0.0-rc1 marker it migrated to moments
# earlier. Do not treat that generated value as the default.
RAGFLOW_DEV_MODE=false
# RAGFLOW_DEV_MODE=false
# ------------------------------
# docker env var for specifying vector db type at startup
@@ -261,7 +261,7 @@ SVR_MCP_PORT=9382
ADMIN_DEFAULT_PASSWORD=admin
# API_PROXY_SCHEME=hybrid # go and python hybrid deploy mode
API_PROXY_SCHEME=go # use go server deployment
API_PROXY_SCHEME=go
#API_PROXY_SCHEME=python # use pure python server deployment
# Development-only: set to 1 to bypass host safety checks for test_db_connection and allow private/local database hosts.
@@ -269,11 +269,11 @@ API_PROXY_SCHEME=go # use go server deployment
ALLOW_ANY_HOST=0
# The RAGFlow Docker image to download. v0.22+ doesn't include embedding models.
RAGFLOW_IMAGE=infiniflow/ragflow:v1.0.0
RAGFLOW_IMAGE=infiniflow/ragflow:v1.0.0-rc1
# If you cannot download the RAGFlow Docker image:
# RAGFLOW_IMAGE=swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:v1.0.0
# RAGFLOW_IMAGE=registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow:v1.0.0
# RAGFLOW_IMAGE=swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:v1.0.0-rc1
# RAGFLOW_IMAGE=registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow:v1.0.0-rc1
#
# - For the `nightly` edition, uncomment either of the following:
# RAGFLOW_IMAGE=swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow:nightly
+2 -2
View File
@@ -25,7 +25,7 @@ When `VERSION` is omitted, the installer resolves the latest GitHub Release and
```bash
curl -fsSL https://raw.githubusercontent.com/infiniflow/ragflow/main/tools/scripts/install.sh \
| VERSION=v0.27.2 sh
| VERSION=v1.0.0-rc1 sh
```
Both forms are supported. Pin a version for production or reproducible installations so that a later Release does not change the installed version.
@@ -46,7 +46,7 @@ When no version is specified, the Windows installer also uses the latest GitHub
```powershell
irm https://raw.githubusercontent.com/infiniflow/ragflow/main/tools/scripts/install.ps1 -OutFile install.ps1
./install.ps1 -Version v0.27.2
./install.ps1 -Version v1.0.0-rc1
```
The Windows installer installs `ragflow-cli.exe` under `%LOCALAPPDATA%\Programs\RAGFlow` by default and adds that directory to the user `PATH`. Restart the terminal if the installer reports that `PATH` was updated.
@@ -32,10 +32,10 @@ Startup order in `InitDB` ([database.go](https://github.com/infiniflow/ragflow/b
| Database version | Scope | Gate constant |
|---|---|---|
| `v0.26.0` | Rebuild the tenant model tables from `tenant_llm` and normalize stored model ids | `modelMigrationBaseVersion` |
| `v0.27.2` | Seed factory-declared models, merge `model_type` into an integer bitmask, populate the `tenant_*_id` columns | `modelMigrationTargetVersion` |
| `v1.0.0-rc1` | Seed factory-declared models, merge `model_type` into an integer bitmask, populate the `tenant_*_id` columns | `modelMigrationTargetVersion` |
| `v1.0.0-rc1.dev1` | Split conversation message and reference payloads into child tables | `conversationHistoryTargetVersion` |
The two tenant model steps are cumulative: a database at `v0.26.0` still runs the `v0.27.2` step, and a database at or above `v0.27.2` runs neither.
The two tenant model steps are cumulative: a database at `v0.26.0` still runs the `v1.0.0-rc1` step, and a database at or above `v1.0.0-rc1` runs neither.
---
@@ -50,7 +50,7 @@ The two tenant model steps are cumulative: a database at `v0.26.0` still runs th
| `tenant_model_instance` | Created if missing. Columns: `id` (PK), `instance_name`, `provider_id`, `api_key`, `status`, `extra`, base timestamps. |
| `tenant_model` | Created if missing. Columns: `id` (PK), `model_name`, `provider_id`, `instance_id`, `model_type`, `status`, `extra`, base timestamps. |
The three tables are created only when the legacy `tenant_llm` table exists and the target table does not, mirroring the `CREATE TABLE IF NOT EXISTS` the Python stages run. Existing tables are never altered by this step: `tenant_model.model_type` deliberately keeps the text shape the Python migration left behind so the `v0.27.2` merge can still read the legacy model names.
The three tables are created only when the legacy `tenant_llm` table exists and the target table does not, mirroring the `CREATE TABLE IF NOT EXISTS` the Python stages run. Existing tables are never altered by this step: `tenant_model.model_type` deliberately keeps the text shape the Python migration left behind so the `v1.0.0-rc1` merge can still read the legacy model names.
### Data migration
@@ -28,7 +28,7 @@ The standalone command runs the full sequence. Ordinary server startup connects
## Database version and startup check
The Go migration marker is stored in `system_settings` under the fixed key `mysql_migration.database.version`. The key name is retained for database compatibility and does not select another migration implementation. The Go implementation writes `v0.26.0` after the base tenant-model step and `v0.27.2` after the follow-up model-data step. When the stored version is below `v1.0.0-rc1.dev1`, the conversation-history migration writes that value after it completes, even if the old history columns or rows are absent and no data needs copying. A step already covered by the stored version is skipped. The last completed step therefore determines the value that remains in the row. This marker is **not** a complete version number for every table or schema change. A missing or unparseable marker does not prove that the database is up to date.
The Go migration marker is stored in `system_settings` under the fixed key `mysql_migration.database.version`. The key name is retained for database compatibility and does not select another migration implementation. The Go implementation writes `v0.26.0` after the base tenant-model step and `v0.27.2` after the follow-up model-data step. When the stored version is below `v1.0.0-rc1`, the conversation-history migration writes that value after it completes, even if the old history columns or rows are absent and no data needs copying. A step already covered by the stored version is skipped. The last completed step therefore determines the value that remains in the row. This marker is **not** a complete version number for every table or schema change. A missing or unparseable marker does not prove that the database is up to date.
Each Go server process compares its code version with this marker after database initialization. That initialization may already have updated runtime tables, indexes, or built-in templates before the check runs. The comparison uses the release-number portion of each version; development suffixes are not used to establish ordering. If the database version is newer, startup fails with `Refusing to start: database was migrated by a newer version`. If the marker is absent or the versions cannot be compared, this downgrade check does not block startup. The check does not apply pending migrations, and the standalone `--migrate` command does not run the downgrade check. Verify the binary and target database before invoking it.
+5 -5
View File
@@ -64,16 +64,16 @@ To upgrade RAGFlow, you must upgrade **both** your code **and** your Docker imag
git pull
```
3. Switch to the latest, officially published release, e.g., `v0.27.2`:
3. Switch to the latest, officially published release, e.g., `v1.0.0-rc1`:
```bash
git checkout -f v0.27.2
git checkout -f v1.0.0-rc1
```
4. Update **docker/.env**:
```bash
RAGFLOW_IMAGE=infiniflow/ragflow:v0.27.2
RAGFLOW_IMAGE=infiniflow/ragflow:v1.0.0-rc1
```
5. Update the RAGFlow image and restart RAGFlow:
@@ -94,10 +94,10 @@ No, you do not need to. Upgrading RAGFlow in itself will *not* remove your uploa
1. From an environment with Internet access, pull the required Docker image.
2. Save the Docker image to a **.tar** file.
```bash
docker save -o ragflow.v0.27.2.tar infiniflow/ragflow:v0.27.2
docker save -o ragflow.v1.0.0-rc1.tar infiniflow/ragflow:v1.0.0-rc1
```
3. Copy the **.tar** file to the target server.
4. Load the **.tar** file into Docker:
```bash
docker load -i ragflow.v0.27.2.tar
docker load -i ragflow.v1.0.0-rc1.tar
```
+1 -1
View File
@@ -252,7 +252,7 @@ otherwise, they will encounter errors when sending heartbeats.
`--migrate` writes the database version marker that server modes check on
startup, and a development branch regularly records a version for a release
that has not been tagged yet — a build from a `v0.27.x` commit that writes
`v1.0.0-rc1.dev1` refuses to start afterwards, because the recorded version
`v1.0.0-rc1` refuses to start afterwards, because the recorded version
looks newer than the code. Set `RAGFLOW_DEV_MODE=true` (see `docker/.env`) for
such a checkout: it turns the "code version must not be older than the database
version" guard off. Leave it off in production.
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "ragflow"
version = "0.27.2"
version = "1.0.0-rc1"
description = "[RAGFlow](https://ragflow.io/) is an open-source RAG (Retrieval-Augmented Generation) engine based on deep document understanding. It offers a streamlined RAG workflow for businesses of any scale, combining LLM (Large Language Models) to provide truthful question-answering capabilities, backed by well-founded citations from various complex formatted data."
authors = [{ name = "Zhichang Yu", email = "yuzhichang@gmail.com" }]
license-files = ["LICENSE"]
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "ragflow-sdk"
version = "0.27.2"
version = "1.0.0-rc1"
description = "Python client sdk of [RAGFlow](https://github.com/infiniflow/ragflow). RAGFlow is an open-source RAG (Retrieval-Augmented Generation) engine based on deep document understanding."
authors = [{ name = "Zhichang Yu", email = "yuzhichang@gmail.com" }]
license = { text = "Apache License, Version 2.0" }
+1 -1
View File
@@ -250,7 +250,7 @@ wheels = [
[[package]]
name = "ragflow-sdk"
version = "0.27.2"
version = "1.0.0-rc1"
source = { virtual = "." }
dependencies = [
{ name = "beartype" },
+1 -1
View File
@@ -33,7 +33,7 @@ uv pip install sdk/python
```env
COMPOSE_PROFILES=${COMPOSE_PROFILES},tei-cpu
TEI_MODEL=BAAI/bge-small-en-v1.5
RAGFLOW_IMAGE=infiniflow/ragflow:v0.27.2 #Replace with the image you are using
RAGFLOW_IMAGE=infiniflow/ragflow:v1.0.0-rc1 #Replace with the image you are using
```
-346
View File
@@ -1,346 +0,0 @@
# Database Scripts
This directory contains database-related utility scripts for RAGFlow.
- **mysql_migration.py**: Data migration between tables with stage-based execution
- **db_schema_sync.py**: Database schema synchronization using peewee-migrate
---
# mysql_migration.py
A flexible MySQL data migration tool for migrating data between tables with stage-based execution.
## Overview
This script provides stage-based data migration between MySQL tables. Currently supports:
- `tenant_model_provider`
- `tenant_model_instance`
- `tenant_model`
### Migration Stages
| Stage | Source Table | Target Table | Description |
|-------|-------------|--------------|-------------|
| `tenant_model_provider` | `tenant_llm` | `tenant_model_provider` | Extracts distinct `(tenant_id, llm_factory)` pairs |
| `tenant_model_instance` | `tenant_llm` + `tenant_model_provider` | `tenant_model_instance` | Creates instances with distinct `(tenant_id, llm_factory, api_key)` |
| `tenant_model` | `tenant_llm` + `tenant_model_provider` + `tenant_model_instance` | `tenant_model` | Migrates model configurations (only `status='0'` records) |
### Stage Dependencies
```
tenant_model_provider (no dependencies)
↓
tenant_model_instance (depends on tenant_model_provider)
↓
tenant_model (depends on tenant_model_provider and tenant_model_instance)
```
### Field Mapping Rules
#### tenant_model_provider
| Target Field | Source | Rule |
|--------------|--------|------|
| `id` | - | Random 32-character UUID1 |
| `provider_name` | `tenant_llm.llm_factory` | Direct mapping |
| `tenant_id` | `tenant_llm.tenant_id` | Direct mapping |
- **Deduplication**: Groups by `(tenant_id, llm_factory)` and takes distinct pairs
#### tenant_model_instance
| Target Field | Source | Rule |
|--------------|--------|------|
| `id` | - | Random 32-character UUID1 |
| `instance_name` | `tenant_llm.llm_factory` | Direct mapping |
| `provider_id` | `tenant_model_provider.id` | JOIN on `tenant_id` and `provider_name=llm_factory` |
| `api_key` | `tenant_llm.api_key` | Direct mapping |
| `status` | `tenant_llm.status` | Direct mapping |
- **Deduplication**: Groups by `(tenant_id, llm_factory, api_key)` and takes distinct records
#### tenant_model
| Target Field | Source | Rule |
|--------------|--------|------|
| `id` | - | Random 32-character UUID1 |
| `model_name` | `tenant_llm.llm_name` | Direct mapping |
| `provider_id` | `tenant_model_provider.id` | JOIN on `tenant_id` and `provider_name=llm_factory` |
| `instance_id` | `tenant_model_instance.id` | JOIN on `provider_id` and `api_key` |
| `model_type` | `tenant_llm.model_type` | Direct mapping |
| `status` | `tenant_llm.status` | Direct mapping |
- **Filter**: Only migrates records where `tenant_llm.status='0'`
## Usage
### Command Line Arguments
```
python mysql_migration.py [OPTIONS]
```
| Option | Short | Description | Default |
|--------|-------|-------------|---------|
| `--host` | - | MySQL host | `localhost` |
| `--port` | - | MySQL port | `3306` |
| `--user` | - | MySQL user | `root` |
| `--password` | - | MySQL password | (empty) |
| `--database` | - | MySQL database name | `rag_flow` |
| `--config` | `-c` | Path to YAML config file | - |
| `--stages` | `-s` | Comma-separated list of stages to run | - |
| `--list-stages` | `-l` | List available stages and exit | - |
| `--execute` | `-e` | Execute full migration (create tables and migrate data) | `False` |
| `--create-table-only` | - | Only create target tables, skip data migration | `False` |
> **Note**: MySQL connection can be configured via command line arguments (`--host`, `--port`, `--user`, `--password`, `--database`) or via a YAML config file (`--config`). Command line arguments take precedence over config file values.
### Execution Modes
The script has three mutually exclusive modes:
1. **Dry-Run Mode** (default): Check only, no database writes
```bash
# Using config file
python mysql_migration.py --stages tenant_model_provider --config config.yaml
# Using command line MySQL connection
python mysql_migration.py --stages tenant_model_provider --host localhost --port 3306 --user root
```
2. **Create Table Only Mode**: Create target tables without migrating data
```bash
python mysql_migration.py --stages tenant_model_provider --config config.yaml --create-table-only
```
3. **Execute Mode**: Create tables and migrate data
```bash
python mysql_migration.py --stages tenant_model_provider --config config.yaml --execute
```
### Configuration File
Create a YAML configuration file with MySQL connection settings:
```yaml
database:
host: localhost
port: 3306
user: root
password: your_password
name: rag_flow
```
Alternative keys are also supported:
```yaml
mysql:
host: localhost
port: 3306
user: root
password: your_password
database: rag_flow
```
### Examples
```bash
# List all available stages
python mysql_migration.py --list-stages
# Dry run single stage using command line MySQL connection
python mysql_migration.py --stages tenant_model_provider --host localhost --port 3306 --user root --password secret
# Dry run single stage using config file
python mysql_migration.py --stages tenant_model_provider --config /path/to/config.yaml
# Create tables only for multiple stages
python mysql_migration.py --stages tenant_model_provider,tenant_model_instance --config /path/to/config.yaml --create-table-only
# Execute full migration for all stages (in dependency order)
python mysql_migration.py --stages tenant_model_provider,tenant_model_instance,tenant_model --config /path/to/config.yaml --execute
# Use config file with command line password override
python mysql_migration.py --stages tenant_model_provider --config /path/to/config.yaml --password mypassword --execute
```
## Output Interpretation
### Stage Execution Log
Each stage displays a header showing progress:
```
============================================================
Stage [1/3]: tenant_model_provider
============================================================
```
The stage then performs:
1. Check phase: Verifies source/target tables exist and counts records to migrate
2. Execute phase: Creates tables (if needed) and migrates data in batches
### Dry-Run Output
In dry-run mode, the script outputs what it would do without writing:
```
[DRY RUN] Would insert 150 records
instance_name=OpenAI, provider_id=abc123, api_key=***
... and 145 more records
```
### Migration Summary
After all stages complete, a summary is printed:
```
============================================================
Migration Summary
============================================================
Total Duration: 2.45s
Total Rows Processed: 350
Tables Operated: tenant_model_provider, tenant_model_instance
------------------------------------------------------------
Stage Details:
[tenant_model_provider] Tables: tenant_model_provider, Rows: 50, Duration: 0.82s
[tenant_model_instance] Tables: tenant_model_instance, Rows: 300, Duration: 1.63s
============================================================
```
### Common Messages
| Message | Meaning |
|---------|-------------------------------------------------------------------------|
| `No new data to migrate` | All records already exist in target table |
| `[DRY RUN] Target table does not exist` | Target table missing, use `--execute` or `--create-table-only`to create |
| `Dependency table does not exist` | Required table from previous stage missing |
| `Inserted batch X: Y records` | Successfully inserted batch of records |
---
# db_schema_sync.py
A database schema synchronization tool that uses peewee-migrate to detect and manage schema changes.
## Overview
This script:
1. Reads model definitions from `api/db/db_models.py`
2. Compares with existing database tables specified via command line
3. Generates migration files in `tools/migrate/{version}/`
### Detected Change Types
| Change Type | Description | Auto-included? |
|-------------|-------------|----------------|
| New table | Model class with no corresponding DB table | Yes |
| New field | Model field not present in DB table | Yes |
| Field type change | Model field type differs from DB column type | Yes |
| Removed field | DB column not present in model definition | No (requires `--drop`) |
> **Warning**: Removed fields are **not** included in migrations by default. You must explicitly use `--drop` to generate `DROP COLUMN` statements, as this operation permanently deletes data.
## Prerequisites
Install peewee-migrate:
```bash
pip install peewee-migrate
```
## Usage
### Command Line Arguments
```
python db_schema_sync.py [OPTIONS]
```
| Option | Short | Description |
|--------|-------|-------------|
| `--host` | - | MySQL host (required) |
| `--port` | - | MySQL port (default: 3306) |
| `--user` | - | MySQL user (required) |
| `--password` | - | MySQL password (required) |
| `--database` | - | MySQL database name (required) |
| `--version` | `-v` | Version number in format `vxx.xx.xx` (required) |
| `--list` | `-l` | List all migrations |
| `--create` | - | Create a new migration (auto-detect changes) |
| `--migrate` | `-m` | Run pending migrations |
| `--diff` | `-d` | Show schema differences |
| `--name` | `-n` | Migration name (default: auto) |
| `--drop` | - | Include `DROP COLUMN` for fields removed from models (destructive - permanently deletes data!) |
### Version Format
Version must be in format `vxx.xx.xx` where `xx` are digits:
- Valid: `v0.27.2`, `v1.0.0`, `v10.20.30`
- Invalid: `0.27.2`, `v0.25`, `v0.27.2.1`
### Migration File Location
Migration files are stored in:
```
tools/migrate/{version_dir}/
```
Where `{version_dir}` is the version with `.` replaced by `_`.
Example: Version `v0.27.2` → Directory `tools/migrate/v0_27_2/`
### Examples
```bash
# List all migrations
python db_schema_sync.py --list \
--host localhost --port 3306 --user root --password xxx --database rag_flow \
--version v0.27.2
# Create a new auto-detected migration (new tables, new fields, type changes only)
python db_schema_sync.py --create \
--host localhost --port 3306 --user root --password xxx --database rag_flow \
--version v0.27.2
# Create a migration including dropped fields (destructive!)
python db_schema_sync.py --create --drop \
--host localhost --port 3306 --user root --password xxx --database rag_flow \
--version v0.27.2
# Create a named migration
python db_schema_sync.py --create --name add_user_table \
--host localhost --port 3306 --user root --password xxx --database rag_flow \
--version v0.27.2
# Run all pending migrations
python db_schema_sync.py --migrate \
--host localhost --port 3306 --user root --password xxx --database rag_flow \
--version v0.27.2
# Show schema differences (including removed fields)
python db_schema_sync.py --diff \
--host localhost --port 3306 --user root --password xxx --database rag_flow \
--version v0.27.2
```
## How It Works
1. **Load Models**: Imports all model classes from `api/db/db_models.py`
2. **Connect Database**: Creates MySQL connection from command line arguments
3. **Detect Changes**: Compares model definitions with actual database schema:
- New tables → `create_model`
- New fields → `ALTER TABLE ADD COLUMN`
- Field type changes → `ALTER TABLE MODIFY COLUMN`
- Removed fields → `ALTER TABLE DROP COLUMN` (only with `--drop`)
4. **Generate Migration**: Creates Python migration file with `migrate()` and `rollback()` functions
### Rollback Behavior
| Forward Operation | Rollback Operation |
|-------------------|--------------------|
| `CREATE TABLE` | `remove_model` |
| `ADD COLUMN` | `DROP COLUMN` |
| `MODIFY COLUMN` | `MODIFY COLUMN` (restore original type) |
| `DROP COLUMN` | `ADD COLUMN` (restore column definition; **data is lost**) |
> **Note**: Rolling back a `DROP COLUMN` will re-add the column structure, but the data that was in it cannot be recovered.
-942
View File
@@ -1,942 +0,0 @@
#
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
Database Schema Sync Script
This script synchronizes database models defined in api/db/db_models.py
with the actual database schema using peewee-migrate.
Features:
1. Reads model definitions from api/db/db_models.py
2. Compares with existing database tables specified via command line
3. Generates migration files in tools/migrate/{version}/
"""
import argparse
import importlib.util
import inspect
import logging
import os
import re
import sys
from peewee import Field, Model, MySQLDatabase
from peewee_migrate import Router
# Add project root to path for imports
PROJECT_BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, PROJECT_BASE)
# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
def validate_version(version: str) -> bool:
"""Validate version format: vxx.xx.xx where xx are digits"""
pattern = r"^v\d+\.\d+\.\d+$"
return bool(re.match(pattern, version))
def version_to_dirname(version: str) -> str:
"""Convert version string to valid directory name (e.g., 'v0.27.2' -> 'v0_27_2')"""
return version.replace(".", "_")
def load_db_models():
"""Load database models from api/db/db_models.py"""
models_path = os.path.join(PROJECT_BASE, "api", "db", "db_models.py")
if not os.path.exists(models_path):
raise FileNotFoundError(f"db_models.py not found at {models_path}")
# Import the module
spec = importlib.util.spec_from_file_location("db_models", models_path)
db_models = importlib.util.module_from_spec(spec)
spec.loader.exec_module(db_models)
# Get all Model subclasses
models = []
for name, obj in inspect.getmembers(db_models):
if inspect.isclass(obj) and issubclass(obj, Model) and obj is not Model:
# Skip base model classes
if obj.__name__ in ["BaseModel", "DataBaseModel"]:
continue
# Check if it has a database attribute (is a proper model)
if hasattr(obj._meta, "database"):
models.append(obj)
return models, db_models
def create_database_connection(host: str, port: int, user: str, password: str, database: str):
"""Create MySQL database connection from command line arguments"""
db = MySQLDatabase(database, host=host, port=port, user=user, password=password, charset="utf8mb4")
return db
# MySQL type to Peewee field type mapping
# Fields peewee/BaseModel manage on their own. compare_fields() and both of its
# callers must exclude the same set, or columns filtered out on one side get
# reported as removed on the other.
AUTO_GENERATED_FIELDS = {"id", "create_time", "create_date", "update_time", "update_date"}
MYSQL_TO_PEEWEE_TYPE = {
"varchar": "CharField",
"char": "CharField",
"text": "TextField",
"longtext": "TextField",
"mediumtext": "TextField",
"int": "IntegerField",
"integer": "IntegerField",
"bigint": "BigIntegerField",
"float": "FloatField",
"double": "FloatField",
"decimal": "FloatField",
"datetime": "DateTimeField",
"timestamp": "DateTimeField",
"tinyint(1)": "BooleanField",
"tinyint": "IntegerField",
"smallint": "IntegerField",
"mediumint": "IntegerField",
}
PEEWEE_TO_MYSQL_TYPE = {
"CharField": "varchar",
"TextField": "text",
"IntegerField": "int",
"BigIntegerField": "bigint",
"FloatField": "float",
"BooleanField": "tinyint",
"DateTimeField": "datetime",
}
def get_table_columns(db, table_name: str) -> dict:
"""Get column information from database table
Returns:
dict: {column_name: {type, nullable, default, ...}}
"""
cursor = db.execute_sql(
"""
SELECT
column_name,
data_type,
column_type,
is_nullable,
column_default,
column_key,
extra
FROM information_schema.columns
WHERE table_schema = %s AND table_name = %s
ORDER BY ordinal_position
""",
(db.database, table_name),
)
columns = {}
for row in cursor.fetchall():
col_name = row[0]
data_type = row[1].lower()
column_type = row[2].lower()
is_nullable = row[3] == "YES"
column_default = row[4]
column_key = row[5]
extra = row[6] or ""
# Determine peewee type
if column_type.startswith("tinyint(1)"):
peewee_type = "BooleanField"
else:
peewee_type = MYSQL_TO_PEEWEE_TYPE.get(data_type, "TextField")
columns[col_name] = {
"data_type": data_type,
"column_type": column_type,
"peewee_type": peewee_type,
"nullable": is_nullable,
"default": column_default,
"is_primary": column_key == "PRI",
"extra": extra,
}
return columns
def get_peewee_field_type(field: Field) -> str:
"""Get peewee field type name"""
field_class = field.__class__.__name__
return field_class
def get_base_field_type(field: Field) -> str:
"""Get base peewee field type by walking the MRO chain.
Custom field types (like DateTimeTzField, JSONField) inherit from standard types.
This function returns the underlying standard type for comparison.
"""
# Standard peewee field types we consider as "base" types
STANDARD_TYPES = {
"CharField",
"TextField",
"IntegerField",
"BigIntegerField",
"FloatField",
"BooleanField",
"DateTimeField",
"DateField",
"TimeField",
"DecimalField",
"ForeignKeyField",
"ManyToManyField",
"PrimaryKeyField",
"AutoField",
}
# Walk through the MRO (Method Resolution Order) to find standard type
for cls in field.__class__.__mro__:
class_name = cls.__name__
if class_name in STANDARD_TYPES:
return class_name
# Fallback to TextField if no standard type found
return "TextField"
def normalize_field_type(field: Field) -> str:
"""Normalize field type for comparison using base type"""
return get_base_field_type(field)
def compare_fields(model_fields: dict, db_columns: dict) -> dict:
"""Compare model fields with database columns
Returns:
dict: {
'added': {field_name: field_obj}, # New fields not in DB
'changed': {field_name: (old_info, new_field)}, # Type changed
'removed': {field_name: col_info}, # Fields in DB but not in model
}
"""
result = {
"added": {},
"changed": {},
"removed": {},
}
skip_fields = AUTO_GENERATED_FIELDS
logger.debug("Excluding auto-generated fields from schema comparison: %s", sorted(skip_fields))
for field_name, field in model_fields.items():
if field_name in skip_fields:
continue
# Check if field exists in database
if field_name not in db_columns:
result["added"][field_name] = field
logger.info(f" New field detected: {field_name} ({field.__class__.__name__})")
else:
# Check if type changed
db_col = db_columns[field_name]
model_base_type = normalize_field_type(field)
db_type = db_col["peewee_type"]
# Type mismatch
if model_base_type != db_type:
result["changed"][field_name] = (db_col, field)
logger.info(f" Field type changed: {field_name} ({db_type} -> {model_base_type}, actual: {field.__class__.__name__})")
# Detect removed fields: columns in DB but not in model
for col_name, col_info in db_columns.items():
if col_name in skip_fields:
continue
if col_name not in model_fields:
result["removed"][col_name] = col_info
logger.info(f" Removed field detected: {col_name} ({col_info['column_type']})")
return result
def generate_field_code(field: Field, field_name: str) -> str:
"""Generate peewee field definition code"""
field_class = field.__class__.__name__
# Map custom field types to standard peewee types for migration
# These custom types will be stored as their underlying standard type
custom_to_standard = {
"LongTextField": "TextField",
"JSONField": "TextField",
"ListField": "TextField",
"SerializedField": "TextField",
"DateTimeTzField": "CharField",
}
# Use standard type for custom fields
pw_field_class = custom_to_standard.get(field_class, field_class)
# Build field arguments
args = []
# max_length for CharField
if pw_field_class == "CharField" and hasattr(field, "max_length") and field.max_length is not None:
args.append(f"max_length={field.max_length}")
# null
if field.null:
args.append("null=True")
# default
if field.default is not None:
default_val = field.default
if isinstance(default_val, str):
# Escape quotes in string
escaped = default_val.replace("'", "\\'")
args.append(f"default='{escaped}'")
elif isinstance(default_val, bool):
args.append(f"default={'True' if default_val else 'False'}")
elif isinstance(default_val, (int, float)):
args.append(f"default={default_val}")
elif isinstance(default_val, dict):
args.append(f"default={default_val}")
elif isinstance(default_val, list):
args.append(f"default={default_val}")
# index
if getattr(field, "index", False):
args.append("index=True")
# unique
if getattr(field, "unique", False):
args.append("unique=True")
args_str = ", ".join(args)
return f"pw.{pw_field_class}({args_str})"
def generate_add_field_sql(table_name: str, field: Field, field_name: str) -> str:
"""Generate raw SQL for adding a field to MySQL table.
This is used for existing tables where migrator.add_fields doesn't work
because the model is not registered in migrator.orm.
"""
field_class = field.__class__.__name__
# Determine MySQL column type
mysql_type_map = {
"CharField": f"VARCHAR({field.max_length})" if hasattr(field, "max_length") and field.max_length else "VARCHAR(255)",
"TextField": "LONGTEXT",
"LongTextField": "LONGTEXT",
"JSONField": "LONGTEXT",
"ListField": "LONGTEXT",
"SerializedField": "LONGTEXT",
"IntegerField": "INT",
"BigIntegerField": "BIGINT",
"FloatField": "DOUBLE",
"BooleanField": "TINYINT(1)",
"DateTimeField": "DATETIME",
"DateTimeTzField": f"VARCHAR({field.max_length})" if hasattr(field, "max_length") and field.max_length else "VARCHAR(255)",
}
mysql_type = mysql_type_map.get(field_class, "LONGTEXT")
# Build column definition
parts = [f"`{field_name}`", mysql_type]
# NULL/NOT NULL
if field.null:
parts.append("NULL")
else:
parts.append("NOT NULL")
# DEFAULT
if field.default is not None:
default_val = field.default
if isinstance(default_val, str):
escaped = default_val.replace("'", "''")
parts.append(f"DEFAULT '{escaped}'")
elif isinstance(default_val, bool):
parts.append(f"DEFAULT {1 if default_val else 0}")
elif isinstance(default_val, (int, float)):
parts.append(f"DEFAULT {default_val}")
elif isinstance(default_val, dict) or isinstance(default_val, list):
import json
escaped = json.dumps(default_val).replace("'", "''")
parts.append(f"DEFAULT '{escaped}'")
# COMMENT
if hasattr(field, "help_text") and field.help_text:
escaped = field.help_text.replace("'", "''")
parts.append(f"COMMENT '{escaped}'")
sql = f"ALTER TABLE `{table_name}` ADD COLUMN {' '.join(parts)}"
# Add index if needed
index_sql = None
if getattr(field, "index", False):
index_sql = f"CREATE INDEX `idx_{table_name}_{field_name}` ON `{table_name}` (`{field_name}`)"
return sql, index_sql
def generate_drop_field_sql(table_name: str, field_name: str) -> str:
"""Generate SQL for dropping a field from a table."""
return f"ALTER TABLE `{table_name}` DROP COLUMN `{field_name}`"
def generate_rollback_field_sql(table_name: str, field_name: str) -> str:
"""Generate SQL for removing a field."""
return f"ALTER TABLE `{table_name}` DROP COLUMN `{field_name}`"
def generate_rollback_add_field_sql(table_name: str, col_info: dict, field_name: str) -> str:
"""Generate SQL for rolling back a dropped field (re-adding it).
This reconstructs the ADD COLUMN statement from the column info
that was captured before the field was dropped.
"""
mysql_type = col_info.get("column_type", "LONGTEXT")
parts = [f"`{field_name}`", mysql_type]
# NULL/NOT NULL
if col_info.get("nullable", True):
parts.append("NULL")
else:
parts.append("NOT NULL")
# DEFAULT
default_val = col_info.get("default")
if default_val is not None:
if isinstance(default_val, str):
escaped = default_val.replace("'", "''")
parts.append(f"DEFAULT '{escaped}'")
elif isinstance(default_val, bool):
parts.append(f"DEFAULT {1 if default_val else 0}")
elif isinstance(default_val, (int, float)):
parts.append(f"DEFAULT {default_val}")
sql = f"ALTER TABLE `{table_name}` ADD COLUMN {' '.join(parts)}"
# Re-add index if it was a non-primary key
index_sql = None
if col_info.get("column_key") == "MUL":
index_sql = f"CREATE INDEX `idx_{table_name}_{field_name}` ON `{table_name}` (`{field_name}`)"
return sql, index_sql
def generate_rollback_modify_sql(table_name: str, old_info: dict, field_name: str) -> str:
"""Generate SQL for rolling back a field type change.
Note: This restores the column type, but data values may need manual handling
if the type conversion caused data loss or transformation.
"""
# Reconstruct MySQL type from old_info
mysql_type = old_info.get("column_type", "LONGTEXT")
# Build column definition
parts = [f"`{field_name}`", mysql_type]
# NULL/NOT NULL
if old_info.get("nullable", True):
parts.append("NULL")
else:
parts.append("NOT NULL")
# DEFAULT (if available)
if old_info.get("default") is not None:
default_val = old_info["default"]
if isinstance(default_val, str):
escaped = default_val.replace("'", "''")
parts.append(f"DEFAULT '{escaped}'")
elif isinstance(default_val, bool):
parts.append(f"DEFAULT {1 if default_val else 0}")
elif isinstance(default_val, (int, float)):
parts.append(f"DEFAULT {default_val}")
return f"ALTER TABLE `{table_name}` MODIFY COLUMN {' '.join(parts)}"
def generate_modify_field_sql(table_name: str, field: Field, field_name: str) -> str:
"""Generate SQL for modifying a field in MySQL table."""
field_class = field.__class__.__name__
# Determine MySQL column type
mysql_type_map = {
"CharField": f"VARCHAR({field.max_length})" if hasattr(field, "max_length") and field.max_length else "VARCHAR(255)",
"TextField": "LONGTEXT",
"LongTextField": "LONGTEXT",
"JSONField": "LONGTEXT",
"ListField": "LONGTEXT",
"SerializedField": "LONGTEXT",
"IntegerField": "INT",
"BigIntegerField": "BIGINT",
"FloatField": "DOUBLE",
"BooleanField": "TINYINT(1)",
"DateTimeField": "DATETIME",
"DateTimeTzField": f"VARCHAR({field.max_length})" if hasattr(field, "max_length") and field.max_length else "VARCHAR(255)",
}
mysql_type = mysql_type_map.get(field_class, "LONGTEXT")
# Build column definition
parts = [f"`{field_name}`", mysql_type]
# NULL/NOT NULL
if field.null:
parts.append("NULL")
else:
parts.append("NOT NULL")
# DEFAULT
if field.default is not None:
default_val = field.default
if isinstance(default_val, str):
escaped = default_val.replace("'", "''")
parts.append(f"DEFAULT '{escaped}'")
elif isinstance(default_val, bool):
parts.append(f"DEFAULT {1 if default_val else 0}")
elif isinstance(default_val, (int, float)):
parts.append(f"DEFAULT {default_val}")
elif isinstance(default_val, dict) or isinstance(default_val, list):
import json
escaped = json.dumps(default_val).replace("'", "''")
parts.append(f"DEFAULT '{escaped}'")
# COMMENT
if hasattr(field, "help_text") and field.help_text:
escaped = field.help_text.replace("'", "''")
parts.append(f"COMMENT '{escaped}'")
return f"ALTER TABLE `{table_name}` MODIFY COLUMN {' '.join(parts)}"
def generate_migration_content(new_tables: list, field_changes: dict, migrate_dir: str, migration_name: str, drop_fields: bool = False) -> str:
"""Generate migration file content"""
lines = [
'"""Peewee migrations."""',
"",
"from contextlib import suppress",
"",
"import peewee as pw",
"from peewee_migrate import Migrator",
"",
"",
"with suppress(ImportError):",
" import playhouse.postgres_ext as pw_pext",
"",
"",
"def migrate(migrator: Migrator, database: pw.Database, *, fake=False):",
' """Write your migrations here."""',
"",
]
# Generate create_model for new tables
for model in new_tables:
table_name = model._meta.table_name
model_name = model.__name__
lines.append(" @migrator.create_model")
lines.append(f" class {model_name}(pw.Model):")
# Get all fields
fields = model._meta.fields
for field_name, field in fields.items():
field_code = generate_field_code(field, field_name)
lines.append(f" {field_name} = {field_code}")
lines.append("")
lines.append(" class Meta:")
lines.append(f' table_name = "{table_name}"')
# Add indexes if defined
indexes = getattr(model._meta, "indexes", None)
if indexes:
lines.append(f" indexes = {indexes}")
lines.append("")
# Generate SQL for adding new fields to existing tables
for table_name, changes in field_changes.items():
if changes.get("added"):
for field_name, field in changes["added"].items():
sql, index_sql = generate_add_field_sql(table_name, field, field_name)
lines.append(f' migrator.sql("{sql}")')
if index_sql:
lines.append(f' migrator.sql("{index_sql}")')
lines.append("")
# Generate SQL for modifying fields in existing tables
for table_name, changes in field_changes.items():
if changes.get("changed"):
for field_name, (old_info, field) in changes["changed"].items():
modify_sql = generate_modify_field_sql(table_name, field, field_name)
lines.append(f' migrator.sql("{modify_sql}")')
lines.append("")
# Generate SQL for dropping removed fields from existing tables
if drop_fields:
for table_name, changes in field_changes.items():
if changes.get("removed"):
for field_name, col_info in changes["removed"].items():
drop_sql = generate_drop_field_sql(table_name, field_name)
lines.append(f" # WARNING: Dropping column `{field_name}` from `{table_name}` - this will permanently delete data!")
lines.append(f' migrator.sql("{drop_sql}")')
lines.append("")
# Generate rollback
lines.append("")
lines.append("def rollback(migrator: Migrator, database: pw.Database, *, fake=False):")
lines.append(' """Write your rollback migrations here."""')
lines.append("")
# Rollback: re-add dropped fields (before other rollbacks, since they may depend on these fields)
if drop_fields:
for table_name, changes in field_changes.items():
if changes.get("removed"):
for field_name, col_info in changes["removed"].items():
add_sql, index_sql = generate_rollback_add_field_sql(table_name, col_info, field_name)
lines.append(f" # Re-add dropped column `{field_name}` to `{table_name}` (data is lost)")
lines.append(f' migrator.sql("{add_sql}")')
if index_sql:
lines.append(f' migrator.sql("{index_sql}")')
# Rollback: reverse field type changes first (before removing added fields)
for table_name, changes in field_changes.items():
if changes.get("changed"):
for field_name, (old_info, field) in changes["changed"].items():
rollback_modify_sql = generate_rollback_modify_sql(table_name, old_info, field_name)
lines.append(" # Note: Data values may need manual handling if type conversion caused data loss")
lines.append(f' migrator.sql("{rollback_modify_sql}")')
# Rollback: remove added fields using SQL
for table_name, changes in field_changes.items():
if changes.get("added"):
for field_name in changes["added"].keys():
rollback_sql = generate_rollback_field_sql(table_name, field_name)
lines.append(f' migrator.sql("{rollback_sql}")')
# Rollback: remove tables (in reverse order)
for model in reversed(new_tables):
table_name = model._meta.table_name
lines.append(f' migrator.remove_model("{table_name}")')
lines.append("")
return "\n".join(lines)
def create_migration(router: Router, models: list, db, name: str = "auto", drop_fields: bool = False):
"""Create a new migration by auto-detecting model changes
Detects:
1. New tables -> generate create_model
2. New fields in existing tables -> generate add_fields
3. Field type changes -> generate change_fields
4. Removed fields (only when --drop is specified) -> generate drop_fields
Args:
router: peewee-migrate Router instance
models: List of model classes to compare against database
db: Database connection
name: Migration name
drop_fields: Whether to include DROP COLUMN for removed fields
"""
try:
# Get existing tables from database
cursor = db.execute_sql("SELECT table_name FROM information_schema.tables WHERE table_schema = %s", (db.database,))
existing_tables = {row[0] for row in cursor.fetchall()}
new_tables = []
field_changes = {}
for model in models:
table_name = model._meta.table_name
if table_name not in existing_tables:
# New table
new_tables.append(model)
logger.info(f"New table detected: {table_name}")
else:
# Existing table - check for field changes
logger.info(f"Checking existing table: {table_name}")
# Get model fields (exclude auto-generated)
model_fields = {}
for field_name, field in model._meta.fields.items():
# Skip id and base model fields
if field_name in AUTO_GENERATED_FIELDS:
continue
if hasattr(field, "_auto_created") and field._auto_created:
continue
model_fields[field_name] = field
# Get database columns
db_columns = get_table_columns(db, table_name)
# Compare
changes = compare_fields(model_fields, db_columns)
if changes["added"] or changes["changed"] or changes["removed"]:
field_changes[table_name] = changes
# Check if any changes detected
has_removed = any(changes.get("removed") for changes in field_changes.values())
if not drop_fields and has_removed:
removed_details = []
for table_name, changes in field_changes.items():
if changes.get("removed"):
for col_name in changes["removed"]:
removed_details.append(f"{table_name}.{col_name}")
logger.warning(f"Removed fields detected (not included in migration, use --drop to include): {', '.join(removed_details)}")
# Remove 'removed' from changes since we're not acting on them
for table_name in field_changes:
field_changes[table_name]["removed"] = {}
if not new_tables and not any(changes["added"] or changes["changed"] for changes in field_changes.values()):
if not (drop_fields and has_removed):
logger.info("No schema changes detected, migration not created")
return None
# Generate migration file content
migration_content = generate_migration_content(new_tables, field_changes, router.migrate_dir, name, drop_fields=drop_fields)
# Get next migration number (count existing migration files)
existing_migrations = [f for f in os.listdir(router.migrate_dir) if f.endswith(".py") and not f.startswith("_")]
migration_num = len(existing_migrations) + 1
migration_file = os.path.join(router.migrate_dir, f"{migration_num:03d}_{name}.py")
with open(migration_file, "w") as f:
f.write(migration_content)
logger.info(f"Created migration: {migration_file}")
return migration_file
except Exception as e:
logger.error(f"Failed to create migration: {e}")
raise
def run_migrations(router: Router):
"""Run all pending migrations"""
try:
diff = router.diff
if not diff:
logger.info("No pending migrations to run")
return
router.run()
logger.info("Migrations completed successfully")
except Exception as e:
logger.error(f"Failed to run migrations: {e}")
raise
def list_migrations(router: Router):
"""List all migrations"""
todo = router.todo
if not todo:
logger.info("No migration files found")
return
logger.info("Available migrations:")
done = set(router.done)
for migration in todo:
status = "applied" if migration in done else "pending"
logger.info(f" [{status}] {migration}")
def diff_schema(models: list, db):
"""Show schema differences between models and database"""
logger.info("Checking schema differences...")
# Tables to ignore (managed by peewee-migrate)
IGNORE_TABLES = {"migratehistory"}
# Get all model table names
model_tables = set()
for model in models:
table_name = model._meta.table_name
model_tables.add(table_name)
logger.info(f"Found {len(model_tables)} model tables")
# Get existing tables from database
cursor = db.execute_sql("SELECT table_name FROM information_schema.tables WHERE table_schema = %s", (db.database,))
existing_tables = {row[0] for row in cursor.fetchall() if row[0] not in IGNORE_TABLES}
# Find tables that exist in models but not in database
missing_tables = model_tables - existing_tables
if missing_tables:
logger.warning(f"Tables not in database ({len(missing_tables)}): {', '.join(sorted(missing_tables))}")
# Find tables that exist in database but not in models
extra_tables = existing_tables - model_tables
if extra_tables:
logger.info(f"Tables in database but not in models: {', '.join(sorted(extra_tables))}")
# Check field differences for existing tables
common_tables = model_tables & existing_tables
if common_tables:
logger.info(f"\nChecking field differences for {len(common_tables)} existing tables...")
total_added = 0
total_changed = 0
total_removed = 0
for model in models:
table_name = model._meta.table_name
if table_name not in common_tables:
continue
# Get model fields
model_fields = {}
for field_name, field in model._meta.fields.items():
if field_name in AUTO_GENERATED_FIELDS:
continue
model_fields[field_name] = field
# Get database columns
db_columns = get_table_columns(db, table_name)
# Compare
changes = compare_fields(model_fields, db_columns)
if changes["added"]:
total_added += len(changes["added"])
field_details = [f"{k}:{v.__class__.__name__}" for k, v in changes["added"].items()]
logger.info(f" {table_name}: {len(changes['added'])} new field(s) - {field_details}")
if changes["changed"]:
total_changed += len(changes["changed"])
field_details = [f"{k}:{v[1].__class__.__name__}" for k, v in changes["changed"].items()]
logger.info(f" {table_name}: {len(changes['changed'])} changed field(s) - {field_details}")
if changes["removed"]:
total_removed += len(changes["removed"])
field_details = [f"{k}:{v['column_type']}" for k, v in changes["removed"].items()]
logger.warning(f" {table_name}: {len(changes['removed'])} removed field(s) - {field_details}")
logger.info(f"\nSummary: {total_added} new fields, {total_changed} changed fields, {total_removed} removed fields")
def main():
parser = argparse.ArgumentParser(
description="Database Schema Synchronization Tool using peewee-migrate",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# List all migrations
python db_schema_sync.py --list --host localhost --port 3306 --user root --password xxx --database rag_flow --version v0.27.2
# Create migration from model changes
python db_schema_sync.py --create --host localhost --port 3306 --user root --password xxx --database rag_flow --version v0.27.2
# Create migration including dropped fields (destructive!)
python db_schema_sync.py --create --drop --host localhost --port 3306 --user root --password xxx --database rag_flow --version v0.27.2
# Run all pending migrations
python db_schema_sync.py --migrate --host localhost --port 3306 --user root --password xxx --database rag_flow --version v0.27.2
# Show schema differences
python db_schema_sync.py --diff --host localhost --port 3306 --user root --password xxx --database rag_flow --version v0.27.2
""",
)
# Database connection options
parser.add_argument("--host", type=str, required=True, help="MySQL host")
parser.add_argument("--port", type=int, default=3306, help="MySQL port (default: 3306)")
parser.add_argument("--user", type=str, required=True, help="MySQL user")
parser.add_argument("--password", type=str, required=True, help="MySQL password")
parser.add_argument("--database", type=str, required=True, help="MySQL database name")
# Version option
parser.add_argument("--version", "-v", type=str, required=True, help="Version number in format vxx.xx.xx (e.g., v0.27.2)")
# Action options
parser.add_argument("--list", "-l", action="store_true", help="List all migrations")
parser.add_argument("--create", "-c", action="store_true", help="Create migration from model changes (auto-detect)")
parser.add_argument("--migrate", "-m", action="store_true", help="Run pending migrations")
parser.add_argument("--diff", "-d", action="store_true", help="Show schema differences")
# Migration options
parser.add_argument("--name", "-n", type=str, default="auto", help="Migration name")
parser.add_argument("--drop", action="store_true", help="Include DROP COLUMN for fields removed from models (destructive - will permanently delete data!)")
args = parser.parse_args()
# Validate version format
if not validate_version(args.version):
logger.error(f"Invalid version format: {args.version}. Expected format: vxx.xx.xx (e.g., v0.27.2)")
sys.exit(1)
# Validate at least one action is specified
if not any([args.list, args.create, args.migrate, args.diff]):
parser.print_help()
logger.error("Please specify at least one action: --list, --create, --migrate, or --diff")
sys.exit(1)
# Convert version to directory name
version_dir = version_to_dirname(args.version)
migrate_dir = os.path.join(PROJECT_BASE, "tools", "migrate", version_dir)
logger.info(f"Version: {args.version}")
logger.info(f"Migration directory: {migrate_dir}")
# Create migration directory if it doesn't exist
os.makedirs(migrate_dir, exist_ok=True)
# Load database models
logger.info("Loading database models from api/db/db_models.py...")
models, _ = load_db_models()
logger.info(f"Found {len(models)} model classes")
# Create database connection
db = create_database_connection(host=args.host, port=args.port, user=args.user, password=args.password, database=args.database)
try:
db.connect()
logger.info(f"Connected to database: {args.database}")
# Create router
router = Router(db, migrate_dir, ignore=["basemodel", "base_model", "migratehistory"])
# Execute requested actions
if args.list:
list_migrations(router)
if args.create:
create_migration(router, models, db, args.name, drop_fields=args.drop)
if args.migrate:
run_migrations(router)
if args.diff:
diff_schema(models, db)
finally:
if not db.is_closed():
db.close()
logger.info("Database connection closed")
logger.info("Done.")
if __name__ == "__main__":
main()
File diff suppressed because it is too large. Load diff
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""Rename provider name fields in conf/models/*.json to match llm_factories.json."""
import json
import os
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
MODELS_DIR = os.path.join(REPO_ROOT, "conf", "models")
# Valid mappings: models current name → llm_factories name
CHANGES = {
"avian": "Avian",
"Baichuan": "BaiChuan",
"Baidu": "BaiduYiyan",
"CoHere": "Cohere",
"FishAudio": "Fish Audio",
"Gitee": "GiteeAI",
"Google": "Gemini",
"JieKouAI": "Jiekou.AI",
"lmstudio": "LM-Studio",
"localai": "LocalAI",
"modelscope": "ModelScope",
"Novita": "NovitaAI",
"Nvidia": "NVIDIA",
"ollama": "Ollama",
"SiliconFlow": "SILICONFLOW",
"HunYuan": "Tencent Hunyuan",
"Aliyun": "Tongyi-Qianwen",
"vllm": "VLLM",
"Voyage": "Voyage AI",
"xinference": "Xinference",
"XunFei": "XunFei Spark",
}
def main():
# Build current name → filename mapping
file_map = {}
for fn in os.listdir(MODELS_DIR):
if not fn.endswith(".json"):
continue
with open(os.path.join(MODELS_DIR, fn)) as f:
d = json.load(f)
file_map[d["name"]] = fn
for old_name, new_name in CHANGES.items():
fn = file_map[old_name]
path = os.path.join(MODELS_DIR, fn)
with open(path) as f:
d = json.load(f)
d["name"] = new_name
with open(path, "w") as f:
json.dump(d, f, indent=2, ensure_ascii=False)
f.write("\n")
print(f'{fn}: "{old_name}" -> "{new_name}"')
if __name__ == "__main__":
main()
-61
View File
@@ -1,61 +0,0 @@
#!/bin/bash
# -----------------------------------------------------------------------------
# Shared migration script for model provider tables.
#
# Called by docker/entrypoint.sh and docker/launch_backend_service.sh.
# Keeps migration stages and versions in one place to avoid divergence.
#
# Usage:
# PY=python3 tools/scripts/run_migrations.sh [--config CONFIG_PATH]
#
# Environment variables:
# PY - Python interpreter path (default: python3)
# -----------------------------------------------------------------------------
set -e
PY="${PY:-python3}"
CONFIG="conf/service_conf.yaml"
while [ $# -gt 0 ]; do
case "$1" in
--config)
# Require a real file. mysql_migration.py:90 turns any config-load
# failure into a warning and falls back to host=localhost user=root
# database=rag_flow, and this script also passes --execute, so an
# empty, whitespace, flag-shaped or missing path would silently
# migrate the default database instead of failing.
if [ $# -lt 2 ] || [ -z "${2//[[:space:]]/}" ] || [ ! -f "$2" ] || [ "${2#-}" != "$2" ]; then
echo "Error: --config requires the path of an existing file" >&2
exit 1
fi
CONFIG="$2"
shift 2
;;
*)
echo "Error: unknown argument: $1" >&2
echo "Usage: PY=python3 tools/scripts/run_migrations.sh [--config CONFIG_PATH]" >&2
exit 1
;;
esac
done
echo "Running model provider table migrations..."
# Step 1: Create base model provider tables
"$PY" tools/scripts/mysql_migration.py \
--stages tenant_model_provider,tenant_model_instance,tenant_model,model_id_config \
--config "$CONFIG" \
--execute \
--database-version "v0.26.0" \
--mark-database-version-on-success
# Step 2: Seed, merge model types, and migrate model IDs
"$PY" tools/scripts/mysql_migration.py \
--stages tenant_model_seeding,model_type_merge,tenant_model_id_migration \
--config "$CONFIG" \
--execute \
--database-version "v0.27.2" \
--mark-database-version-on-success
echo "Model provider table migrations completed."
Generated
+1 -1
View File
@@ -1924,7 +1924,7 @@ wheels = [
[[package]]
name = "ragflow"
version = "0.27.2"
version = "1.0.0-rc1"
source = { virtual = "." }
dependencies = [
{ name = "agentrun-sdk" },