已开启
【实习#34】新增 Django 经 MySQL 协议连接 openGauss B 库示例与自测 #103
Yhw050920创建于 8月5日
【实习#34】新增 Django 经 MySQL 协议连接 openGauss B 库示例与自测 #103
已开启
共 22 个文件变更+1055-0
| @@ -0,0 +1,10 @@ | |||
| 1 | +# Byte-compiled / cache | ||
| 2 | +__pycache__/ | ||
| 3 | +*.py[cod] | ||
| 4 | +.venv/ | ||
| 5 | +venv/ | ||
| 6 | +*.egg-info/ | ||
| 7 | +.pytest_cache/ | ||
| 8 | +.idea/ | ||
| 9 | +.vscode/ | ||
| 10 | +*.log | ||
| @@ -0,0 +1,87 @@ | |||
| 1 | +# DjangoConnectOpenGaussB | ||
| 2 | + | ||
| 3 | +openGauss B 兼容模式 + MySQL 协议(dolphin)下,使用 **原生** `django.db.backends.mysql` + PyMySQL 连接与 CRUD 示例。 | ||
| 4 | + | ||
| 5 | +## 环境 | ||
| 6 | + | ||
| 7 | +- openEuler 20.03 LTS | ||
| 8 | +- openGauss 7.0.x 企业版/Server(需 dolphin,Lite 不支持) | ||
| 9 | +- **Plugin 补丁(须按顺序合入或安装)**: | ||
| 10 | + 1. [Plugin !2522](https://gitcode.com/opengauss/Plugin/merge_requests/2522) — `NO_BACKSLASH_ESCAPES` 协议状态与字符串转义语义(**须先合入**,否则 `OPTIONS.sql_mode` 不生效) | ||
| 11 | + 2. [Plugin !2524](https://gitcode.com/opengauss/Plugin/merge_requests/2524) — `@@default_storage_engine` / `@@sql_auto_is_null` 兼容桩 | ||
| 12 | +- Python 3.8+ | ||
| 13 | +- Django 4.2+/5.0 | ||
| 14 | +- 驱动:PyMySQL(主路径示例);也可换 mysqlclient | ||
| 15 | + | ||
| 16 | +## 快速开始 | ||
| 17 | + | ||
| 18 | +```bash | ||
| 19 | +python -m venv .venv | ||
| 20 | +# Windows | ||
| 21 | +.venv\Scripts\activate | ||
| 22 | +# Linux | ||
| 23 | +# source .venv/bin/activate | ||
| 24 | + | ||
| 25 | +pip install -r requirements.txt | ||
| 26 | + | ||
| 27 | +# 按实际环境修改(密码仅通过环境变量注入) | ||
| 28 | +export OG_MYSQL_HOST=127.0.0.1 | ||
| 29 | +export OG_MYSQL_PORT=3308 | ||
| 30 | +export OG_MYSQL_DB=mysql_test_db | ||
| 31 | +export OG_MYSQL_USER=omm | ||
| 32 | +export OG_MYSQL_PASSWORD='your_password_here' | ||
| 33 | + | ||
| 34 | +python scripts/run_demo.py | ||
| 35 | +``` | ||
| 36 | + | ||
| 37 | +## 配置要点 | ||
| 38 | + | ||
| 39 | +`config/settings.py` 默认使用: | ||
| 40 | + | ||
| 41 | +- `ENGINE = django.db.backends.mysql`(原生 MySQL backend) | ||
| 42 | +- `PORT = dolphin_server_port`(如 3308) | ||
| 43 | +- `NAME` = B 库下 schema(MySQL database) | ||
| 44 | +- `PASSWORD` = 环境变量 `OG_MYSQL_PASSWORD`(仓库不保存明文口令) | ||
| 45 | + | ||
| 46 | +并通过 `pymysql_bootstrap.py` 将 PyMySQL 注册为 MySQLdb;若 `SELECT VERSION()` 仍是 openGauss banner,则按 dolphin 握手版本解析为 MySQL 8.0.28。 | ||
| 47 | + | ||
| 48 | +### Plugin 侧依赖 | ||
| 49 | + | ||
| 50 | +Django 初始化会执行: | ||
| 51 | + | ||
| 52 | +```sql | ||
| 53 | +SELECT VERSION(), | ||
| 54 | + @@sql_mode, | ||
| 55 | + @@default_storage_engine, | ||
| 56 | + @@sql_auto_is_null, | ||
| 57 | + @@lower_case_table_names, | ||
| 58 | + ... | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +**须按顺序合入或安装以下 Plugin 补丁:** | ||
| 62 | + | ||
| 63 | +| 顺序 | MR | 作用 | | ||
| 64 | +|------|-----|------| | ||
| 65 | +| 1(必须先合入) | [Plugin !2522](https://gitcode.com/opengauss/Plugin/merge_requests/2522) | 修复 dolphin 协议层 `NO_BACKSLASH_ESCAPES` 状态位,使 `config/settings.py` 中 `OPTIONS.sql_mode = NO_BACKSLASH_ESCAPES` 与实际字符串转义一致 | | ||
| 66 | +| 2 | [Plugin !2524](https://gitcode.com/opengauss/Plugin/merge_requests/2524) | 新增 `@@default_storage_engine` / `@@sql_auto_is_null` 兼容桩 | | ||
| 67 | + | ||
| 68 | +openGauss 本身没有 MySQL 存储引擎概念;!2524 使 `@@default_storage_engine` 固定返回 `InnoDB`,`@@sql_auto_is_null` 默认为 `0` 且可 `SET`,从而原生 backend 可正常工作。 | ||
| 69 | + | ||
| 70 | +**未合入 !2522 时**,即使配置了 `NO_BACKSLASH_ESCAPES`,PyMySQL 仍可能按错误转义语义编码单引号/反斜杠,导致 ORM 写入 `O'Brien` 等值失败。请先合入 !2522 再运行 `scripts/run_demo.py` 中的特殊字符断言。 | ||
| 71 | + | ||
| 72 | +未合入 Plugin 补丁的旧环境,可临时: | ||
| 73 | + | ||
| 74 | +```bash | ||
| 75 | +export OG_MYSQL_ENGINE=opengauss_mysql | ||
| 76 | +``` | ||
| 77 | + | ||
| 78 | +使用本仓库附带的兼容 Backend(仅作过渡)。 | ||
| 79 | + | ||
| 80 | +## 产出 | ||
| 81 | + | ||
| 82 | +- 示例工程:本目录 | ||
| 83 | +- 设计文档:`doc/设计文档.md` | ||
| 84 | +- 自测报告:`docs/自测报告.md` | ||
| 85 | +- 测试思维导图:`docs/测试思维导图.mm` / `.md` / `.html` | ||
| 86 | +- 官方开发指南:docs 仓 `django_development.md` | ||
| 87 | +- Plugin:[!2522](https://gitcode.com/opengauss/Plugin/merge_requests/2522)(`NO_BACKSLASH_ESCAPES`,须先合入)、[!2524](https://gitcode.com/opengauss/Plugin/merge_requests/2524)(`@@default_storage_engine` / `@@sql_auto_is_null`) | ||
| @@ -0,0 +1 @@ | |||
| 1 | +# empty package marker | ||
| @@ -0,0 +1,53 @@ | |||
| 1 | +from pathlib import Path | ||
| 2 | +import os | ||
| 3 | +import sys | ||
| 4 | + | ||
| 5 | +BASE_DIR = Path(__file__).resolve().parent.parent | ||
| 6 | +sys.path.insert(0, str(BASE_DIR)) | ||
| 7 | + | ||
| 8 | +import pymysql_bootstrap # noqa: F401 | ||
| 9 | + | ||
| 10 | +SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "django-opengauss-b-demo-not-for-production") | ||
| 11 | +DEBUG = os.environ.get("DJANGO_DEBUG", "1") == "1" | ||
| 12 | +ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost").split(",") | ||
| 13 | + | ||
| 14 | +INSTALLED_APPS = [ | ||
| 15 | + "django.contrib.contenttypes", | ||
| 16 | + "django.contrib.auth", | ||
| 17 | + "demo_app", | ||
| 18 | +] | ||
| 19 | + | ||
| 20 | +MIDDLEWARE = [] | ||
| 21 | + | ||
| 22 | +ROOT_URLCONF = "config.urls" | ||
| 23 | +WSGI_APPLICATION = "config.wsgi.application" | ||
| 24 | + | ||
| 25 | +# Prefer native Django MySQL backend after Plugin adds @@default_storage_engine / | ||
| 26 | +# @@sql_auto_is_null stubs. Override with OG_MYSQL_ENGINE=opengauss_mysql for | ||
| 27 | +# older servers without the Plugin patch. | ||
| 28 | +_ENGINE = os.environ.get("OG_MYSQL_ENGINE", "django.db.backends.mysql") | ||
| 29 | + | ||
| 30 | +DATABASES = { | ||
| 31 | + "default": { | ||
| 32 | + "ENGINE": _ENGINE, | ||
| 33 | + "NAME": os.environ.get("OG_MYSQL_DB", "mysql_test_db"), | ||
| 34 | + "USER": os.environ.get("OG_MYSQL_USER", "omm"), | ||
| 35 | + "PASSWORD": os.environ["OG_MYSQL_PASSWORD"], | ||
| 36 | + "HOST": os.environ.get("OG_MYSQL_HOST", "127.0.0.1"), | ||
| 37 | + "PORT": os.environ.get("OG_MYSQL_PORT", "3308"), | ||
| 38 | + "OPTIONS": { | ||
| 39 | + "charset": "utf8mb4", | ||
| 40 | + # Align PyMySQL escaping with Dolphin string parsing (see Plugin NO_BACKSLASH_ESCAPES). | ||
| 41 | + "sql_mode": "NO_BACKSLASH_ESCAPES", | ||
| 42 | + }, | ||
| 43 | + } | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +LANGUAGE_CODE = "zh-hans" | ||
| 47 | +TIME_ZONE = "Asia/Shanghai" | ||
| 48 | +USE_I18N = True | ||
| 49 | +USE_TZ = True | ||
| 50 | + | ||
| 51 | +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" | ||
| 52 | + | ||
| 53 | +SILENCED_SYSTEM_CHECKS = ["mysql.W002"] | ||
| @@ -0,0 +1,7 @@ | |||
| 1 | +from django.urls import path | ||
| 2 | +from django.http import HttpResponse | ||
| 3 | + | ||
| 4 | + | ||
| 5 | +urlpatterns = [ | ||
| 6 | + path("", lambda request: HttpResponse("Django + openGauss B-protocol demo")), | ||
| 7 | +] | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | +import os | ||
| 2 | + | ||
| 3 | +from django.core.wsgi import get_wsgi_application | ||
| 4 | + | ||
| 5 | +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") | ||
| 6 | +application = get_wsgi_application() | ||
| @@ -0,0 +1 @@ | |||
| 1 | +# demo app package | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | +from django.apps import AppConfig | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +class DemoAppConfig(AppConfig): | ||
| 5 | + default_auto_field = "django.db.models.BigAutoField" | ||
| 6 | + name = "demo_app" | ||
| @@ -0,0 +1,16 @@ | |||
| 1 | +from django.db import models | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +class User(models.Model): | ||
| 5 | + """Maps to openGauss B-mode table `user` under schema mysql_test_db.""" | ||
| 6 | + | ||
| 7 | + id = models.AutoField(primary_key=True) | ||
| 8 | + name = models.CharField(max_length=50) | ||
| 9 | + age = models.IntegerField(null=True, blank=True) | ||
| 10 | + | ||
| 11 | + class Meta: | ||
| 12 | + db_table = "user" | ||
| 13 | + managed = False # table prepared by SQL / docs guide | ||
| 14 | + | ||
| 15 | + def __str__(self): | ||
| 16 | + return f"User{{id={self.id}, name='{self.name}', age={self.age}}}" | ||
| @@ -0,0 +1,106 @@ | |||
| 1 | +# 设计文档 | ||
| 2 | + | ||
| 3 | +> 对应 Issue:[开源实习] openGauss B库B协议适配MySQL python orm框架 Django #34 | ||
| 4 | +> https://gitcode.com/opengauss/opensource-intership/issues/34 | ||
| 5 | + | ||
| 6 | +## 1. 项目背景 | ||
| 7 | + | ||
| 8 | +openGauss 在 dolphin 插件中实现了 MySQL 协议兼容。社区已有 JDBC / MyBatis / Hibernate 等指南,本任务补齐 **Python ORM(Django)+ MySQL Python 驱动** 经 dolphin 连接 B 兼容模式数据库的验证、适配与文档。 | ||
| 9 | + | ||
| 10 | +按社区评审要求:**优先支持原生 `django.db.backends.mysql`**;dolphin 缺少的 MySQL 变量/能力在 **Plugin** 仓补齐,而不是仅靠客户端自定义 Backend 绕过。 | ||
| 11 | + | ||
| 12 | +## 2. 目标与范围 | ||
| 13 | + | ||
| 14 | +| 目标 | 说明 | | ||
| 15 | +|------|------| | ||
| 16 | +| 连通性 | Django 经 `dolphin_server_port`(如 3308)完成认证与会话 | | ||
| 17 | +| 原生 Backend | 使用官方 `django.db.backends.mysql` | | ||
| 18 | +| Plugin 兼容 | 为 Django 探测变量提供兼容桩(无真实引擎语义) | | ||
| 19 | +| 业务可用性 | 覆盖 ORM 查询与 CRUD 主路径 | | ||
| 20 | +| 交付 | examples 示例/自测/思维导图;docs 开发指南;Plugin 补丁 | | ||
| 21 | + | ||
| 22 | +**非目标:** 存储过程、server-side cursor 等协议明确不支持能力的强制支持;实现真实 InnoDB 引擎。 | ||
| 23 | + | ||
| 24 | +## 3. 环境约束 | ||
| 25 | + | ||
| 26 | +- OS:openEuler 20.03 LTS(x86_64 / aarch64) | ||
| 27 | +- DB:openGauss 7.0.0-LTS(正式包未上架时可用 archive_test 转测包,需含 dolphin 的 Server 版,非 Lite) | ||
| 28 | +- 协议:`enable_dolphin_proto=on`,`dolphin_server_port` ≠ 原生 port | ||
| 29 | +- 客户端:Python 3.8+,Django 4.2+/5.0,PyMySQL(或 mysqlclient) | ||
| 30 | + | ||
| 31 | +## 4. 总体方案 | ||
| 32 | + | ||
| 33 | +```text | ||
| 34 | +Django App | ||
| 35 | + -> ENGINE=django.db.backends.mysql | ||
| 36 | + -> PyMySQL (MySQLdb 兼容层) | ||
| 37 | + -> TCP :dolphin_server_port | ||
| 38 | + -> openGauss B DB + dolphin | ||
| 39 | + @@default_storage_engine -> "InnoDB" (stub) | ||
| 40 | + @@sql_auto_is_null -> 0 (stub, SET 可写) | ||
| 41 | +``` | ||
| 42 | + | ||
| 43 | +### 4.1 服务端配置要点 | ||
| 44 | + | ||
| 45 | +1. 创建 B 库:`CREATE DATABASE ... DBCOMPATIBILITY 'B'` | ||
| 46 | +2. `CREATE EXTENSION dolphin`(如需要) | ||
| 47 | +3. `SELECT set_native_password(user, password, '')` | ||
| 48 | +4. GUC:`listen_addresses`、`enable_dolphin_proto`、`dolphin_server_port`、`dolphin.default_database_name` | ||
| 49 | + | ||
| 50 | +### 4.2 Plugin 适配要点(评审结论) | ||
| 51 | + | ||
| 52 | +原生 Django MySQL backend 在 `mysql_server_data` 中会查询: | ||
| 53 | + | ||
| 54 | +- `@@sql_mode` | ||
| 55 | +- `@@default_storage_engine` ← 原报错 **28804**(变量不存在) | ||
| 56 | +- `@@sql_auto_is_null` | ||
| 57 | +- `@@lower_case_table_names` | ||
| 58 | +- 等 | ||
| 59 | + | ||
| 60 | +openGauss **没有** MySQL 存储引擎实现。参考 `contrib/dolphin/plugin_postgres.cpp` 中 `query_cache_size` / `system_time_zone` 等“无实际语义、仅兼容客户端探测”的变量做法: | ||
| 61 | + | ||
| 62 | +1. 在 `BSqlPluginContext` 增加字段 | ||
| 63 | +2. `DefineCustomStringVariable("default_storage_engine", ...)` 默认 `"InnoDB"` | ||
| 64 | +3. `DefineCustomIntVariable("sql_auto_is_null", ...)` 默认 `0`(支持 Django `SET SQL_AUTO_IS_NULL = 0`) | ||
| 65 | +4. SET 时打 WARNING 标明无实际含义(与现有 stub 一致) | ||
| 66 | + | ||
| 67 | +这样框架可正常初始化,且不引入虚假引擎能力。 | ||
| 68 | + | ||
| 69 | +另外,Django 用 `re.match()` 解析 `SELECT VERSION()`,要求字符串**以** `X.Y.Z` 开头。内核 `version()` 是 builtin,无法 `CREATE OR REPLACE`;dolphin 握手包已是 `8.0.28-dophin-server`。示例里 `pymysql_bootstrap.py` 在识别到 openGauss banner 时按握手版本解析为 `(8, 0, 28)`,从而继续走原生 `django.db.backends.mysql`。 | ||
| 70 | + | ||
| 71 | +### 4.3 示例业务 | ||
| 72 | + | ||
| 73 | +- Schema/DB:`mysql_test_db` | ||
| 74 | +- 表:`user(id, name, age)` | ||
| 75 | +- 脚本:`scripts/run_demo.py`(查询/增/改/删) | ||
| 76 | +- 过渡:未合入 Plugin 的环境可设 `OG_MYSQL_ENGINE=opengauss_mysql` | ||
| 77 | + | ||
| 78 | +## 5. 测试设计 | ||
| 79 | + | ||
| 80 | +| 编号 | 场景 | 通过标准 | | ||
| 81 | +|------|------|----------| | ||
| 82 | +| T0 | 版本与进程 | `gs_ctl status` running | | ||
| 83 | +| T1 | 端口 | 5432 / 3308 LISTEN | | ||
| 84 | +| T2 | 协议认证 | PyMySQL 握手成功 | | ||
| 85 | +| T3 | DDL | 建库建表成功 | | ||
| 86 | +| T4 | `SELECT @@default_storage_engine` | 返回 `InnoDB` | | ||
| 87 | +| T5 | ORM CRUD(原生 mysql backend) | 打印 `DEMO_OK` | | ||
| 88 | +| T6 | Django 安装检查 | 打印 `INSTALL_OK` | | ||
| 89 | +| T7 | 不支持项 | 文档标注跳过 | | ||
| 90 | + | ||
| 91 | +## 6. 风险与对策 | ||
| 92 | + | ||
| 93 | +| 风险 | 对策 | | ||
| 94 | +|------|------| | ||
| 95 | +| 正式 LTS 包不可得 | 使用同系列 archive_test 转测包并在报告中说明 | | ||
| 96 | +| Lite 无 dolphin | 强制 Server/企业版 | | ||
| 97 | +| 敏感信息入库 | 密码仅通过环境变量注入,文档脱敏 | | ||
| 98 | +| 协议能力差异 | 文档明确不支持项,避免误用 | | ||
| 99 | +| 兼容桩被误用为真实引擎能力 | 文档/WARNING 标明无实际含义 | | ||
| 100 | + | ||
| 101 | +## 7. 交付物清单 | ||
| 102 | + | ||
| 103 | +1. `DjangoConnectOpenGaussB/` 示例工程(默认原生 mysql backend) | ||
| 104 | +2. `docs/自测报告.md`、`docs/测试思维导图.*`、`doc/设计文档.md` | ||
| 105 | +3. docs 仓 `django_development.md` 与 `_toc.yaml` | ||
| 106 | +4. Plugin:`default_storage_engine` / `sql_auto_is_null` 兼容变量 + 回归用例 | ||
| @@ -0,0 +1,46 @@ | |||
| 1 | +# openGauss B 库 B 协议适配 Django(MySQL Python 驱动)开发方案 | ||
| 2 | + | ||
| 3 | +> Issue:`[开源实习]: openGauss B库B协议适配MySQL python orm框架 Django #34` | ||
| 4 | +> 环境:openEuler 20.03 LTS x86_64 · openGauss 7.0.0-LTS.B008(转测包)· dolphin:3308 | ||
| 5 | + | ||
| 6 | +## 1. 目标 | ||
| 7 | + | ||
| 8 | +验证并适配 **Django ORM + MySQL Python 驱动(PyMySQL)**,经 dolphin MySQL 协议连接 openGauss B 兼容模式数据库,输出 examples 示例、自测报告/思维导图与 docs 开发指南。 | ||
| 9 | + | ||
| 10 | +## 2. 技术路径 | ||
| 11 | + | ||
| 12 | +1. 开启 `enable_dolphin_proto`、`dolphin_server_port=3308`,B 库 `DBCOMPATIBILITY 'B'` | ||
| 13 | +2. `set_native_password(user, pwd, '')` 配置 MySQL native 认证 | ||
| 14 | +3. Django 使用自定义 Backend `opengauss_mysql`(继承官方 mysql backend) | ||
| 15 | + - 规避 `@@default_storage_engine` 等探测导致的 **28804** | ||
| 16 | +4. 以 CRUD demo(`scripts/run_demo.py`)作为主验收路径 | ||
| 17 | + | ||
| 18 | +## 3. 产出规划 | ||
| 19 | + | ||
| 20 | +| 仓 | 内容 | | ||
| 21 | +|----|------| | ||
| 22 | +| examples | `DjangoConnectOpenGaussB` 工程、自测报告、测试思维导图 | | ||
| 23 | +| docs | `django_development.md` + `_toc.yaml` | | ||
| 24 | +| Plugin | 无服务端缺陷则不改 | | ||
| 25 | + | ||
| 26 | +## 4. 测试范围 | ||
| 27 | + | ||
| 28 | +- T0 环境/版本/运行状态 | ||
| 29 | +- T1–T3 端口、认证、建库建表 | ||
| 30 | +- T4–T5 ORM 查询与 CRUD(`DEMO_OK`) | ||
| 31 | +- T6 Django 安装检查(`INSTALL_OK`) | ||
| 32 | +- T7 协议不支持项跳过(存储过程 / server-side cursor) | ||
| 33 | + | ||
| 34 | +## 5. 当前进度(2026-08-05) | ||
| 35 | + | ||
| 36 | +- [x] 云环境安装 openGauss 7.0.0-LTS.B008(build `75f983eb`) | ||
| 37 | +- [x] dolphin 3308 + B 库 + demo 表 | ||
| 38 | +- [x] Django 安装与 CRUD 自测通过 | ||
| 39 | +- [x] examples / docs 已推个人 fork 特性分支 | ||
| 40 | +- [x] 测试思维导图(`.mm` / `.md` / `.html`) | ||
| 41 | +- [ ] 向上游 `opengauss/examples`、`opengauss/docs` 提 MR(领取通过后) | ||
| 42 | +- [ ] 导师邮件领取评审(截图 + 简历 + 本方案) | ||
| 43 | + | ||
| 44 | +## 6. 风险与说明 | ||
| 45 | + | ||
| 46 | +- 正式 **7.0.0-LTS** 尚未官网上架;本任务使用 archive_test **LTS.B008**(openEuler20.03/x86 最新有包版本)。包文件名仍可能显示 RC3,以转测目录与 `git_num` 为准。 | ||
| @@ -0,0 +1,147 @@ | |||
| 1 | + | ||
| 2 | +<html lang="zh-CN"> | ||
| 3 | +<head> | ||
| 4 | + <meta charset="UTF-8" /> | ||
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| 6 | + <title>Django × openGauss B协议 — 测试思维导图</title> | ||
| 7 | + <style> | ||
| 8 | + :root { | ||
| 9 | + --bg: #f7f4ef; | ||
| 10 | + --ink: #1f2a24; | ||
| 11 | + --line: #8aa399; | ||
| 12 | + --root: #1f6f5b; | ||
| 13 | + --node: #ffffff; | ||
| 14 | + --accent: #c45c26; | ||
| 15 | + } | ||
| 16 | + * { box-sizing: border-box; } | ||
| 17 | + body { | ||
| 18 | + margin: 0; | ||
| 19 | + font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; | ||
| 20 | + color: var(--ink); | ||
| 21 | + background: | ||
| 22 | + radial-gradient(circle at 10% 10%, #e8f2ec 0, transparent 40%), | ||
| 23 | + radial-gradient(circle at 90% 0%, #f8e9df 0, transparent 35%), | ||
| 24 | + var(--bg); | ||
| 25 | + padding: 24px; | ||
| 26 | + } | ||
| 27 | + h1 { text-align: center; font-size: 22px; margin: 0 0 8px; } | ||
| 28 | + .sub { text-align: center; color: #5b6b63; margin-bottom: 28px; font-size: 13px; } | ||
| 29 | + .mind { | ||
| 30 | + display: grid; | ||
| 31 | + grid-template-columns: 1fr auto 1fr; | ||
| 32 | + gap: 18px; | ||
| 33 | + align-items: center; | ||
| 34 | + max-width: 1200px; | ||
| 35 | + margin: 0 auto; | ||
| 36 | + } | ||
| 37 | + .col { display: flex; flex-direction: column; gap: 14px; } | ||
| 38 | + .col.left { align-items: flex-end; } | ||
| 39 | + .col.right { align-items: flex-start; } | ||
| 40 | + .root { | ||
| 41 | + width: 180px; | ||
| 42 | + min-height: 180px; | ||
| 43 | + border-radius: 50%; | ||
| 44 | + background: var(--root); | ||
| 45 | + color: #fff; | ||
| 46 | + display: flex; | ||
| 47 | + align-items: center; | ||
| 48 | + justify-content: center; | ||
| 49 | + text-align: center; | ||
| 50 | + padding: 18px; | ||
| 51 | + font-weight: 700; | ||
| 52 | + box-shadow: 0 10px 28px rgba(31, 111, 91, 0.28); | ||
| 53 | + z-index: 2; | ||
| 54 | + } | ||
| 55 | + .branch { | ||
| 56 | + background: var(--node); | ||
| 57 | + border: 1px solid #d7e0db; | ||
| 58 | + border-left: 4px solid var(--root); | ||
| 59 | + border-radius: 10px; | ||
| 60 | + padding: 12px 14px; | ||
| 61 | + width: min(360px, 100%); | ||
| 62 | + box-shadow: 0 4px 14px rgba(0,0,0,0.04); | ||
| 63 | + } | ||
| 64 | + .branch.left { border-left: none; border-right: 4px solid var(--accent); } | ||
| 65 | + .branch h3 { margin: 0 0 8px; font-size: 15px; } | ||
| 66 | + .branch ul { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.55; } | ||
| 67 | + .pass { color: #1f6f5b; font-weight: 600; } | ||
| 68 | + .skip { color: #9a6b2f; font-weight: 600; } | ||
| 69 | + .legend { | ||
| 70 | + max-width: 1200px; | ||
| 71 | + margin: 28px auto 0; | ||
| 72 | + font-size: 13px; | ||
| 73 | + color: #5b6b63; | ||
| 74 | + text-align: center; | ||
| 75 | + } | ||
| 76 | + </style> | ||
| 77 | +</head> | ||
| 78 | +<body> | ||
| 79 | + <h1>Django × openGauss B 协议适配 — 测试思维导图</h1> | ||
| 80 | + <p class="sub">Issue #34 · openEuler 20.03 · openGauss 7.0.x · dolphin:3308 · Django + PyMySQL</p> | ||
| 81 | + <div class="mind"> | ||
| 82 | + <div class="col left"> | ||
| 83 | + <div class="branch left"> | ||
| 84 | + <h3>4. ORM 功能测试</h3> | ||
| 85 | + <ul> | ||
| 86 | + <li>T4 查询全部 <span class="pass">通过</span></li> | ||
| 87 | + <li>T5 新增 / 模糊查 / 更新 / 删除 <span class="pass">通过 DEMO_OK</span></li> | ||
| 88 | + <li>入口:scripts/run_demo.py</li> | ||
| 89 | + </ul> | ||
| 90 | + </div> | ||
| 91 | + <div class="branch left"> | ||
| 92 | + <h3>5. 协议限制</h3> | ||
| 93 | + <ul> | ||
| 94 | + <li>T6 存储过程 <span class="skip">跳过</span></li> | ||
| 95 | + <li>server-side cursor 不支持</li> | ||
| 96 | + <li>对照 dolphin 官方兼容说明</li> | ||
| 97 | + </ul> | ||
| 98 | + </div> | ||
| 99 | + <div class="branch left"> | ||
| 100 | + <h3>6. 问题与修复</h3> | ||
| 101 | + <ul> | ||
| 102 | + <li>openblas / OOM / unixODBC</li> | ||
| 103 | + <li>Lite 无 dolphin → Server</li> | ||
| 104 | + <li>28804 → opengauss_mysql 适配层</li> | ||
| 105 | + </ul> | ||
| 106 | + </div> | ||
| 107 | + <div class="branch left"> | ||
| 108 | + <h3>7. 交付物</h3> | ||
| 109 | + <ul> | ||
| 110 | + <li>examples 示例 + 自测报告 + 思维导图</li> | ||
| 111 | + <li>docs django_development.md</li> | ||
| 112 | + <li>Plugin 本期无服务端补丁</li> | ||
| 113 | + </ul> | ||
| 114 | + </div> | ||
| 115 | + </div> | ||
| 116 | + <div class="root">Django × openGauss<br/>B协议适配测试</div> | ||
| 117 | + <div class="col right"> | ||
| 118 | + <div class="branch"> | ||
| 119 | + <h3>1. 环境准备</h3> | ||
| 120 | + <ul> | ||
| 121 | + <li>openEuler 20.03 LTS(x86_64/aarch64)</li> | ||
| 122 | + <li>openGauss 7.0.x Server(非 Lite)</li> | ||
| 123 | + <li>dolphin:enable_dolphin_proto + 3308</li> | ||
| 124 | + <li>B库 proto_test_db / 业务库 mysql_test_db</li> | ||
| 125 | + </ul> | ||
| 126 | + </div> | ||
| 127 | + <div class="branch"> | ||
| 128 | + <h3>2. 驱动与适配层</h3> | ||
| 129 | + <ul> | ||
| 130 | + <li>PyMySQL(主)/ mysqlclient(可选)</li> | ||
| 131 | + <li>ENGINE = opengauss_mysql</li> | ||
| 132 | + <li>规避 @@default_storage_engine → 28804</li> | ||
| 133 | + </ul> | ||
| 134 | + </div> | ||
| 135 | + <div class="branch"> | ||
| 136 | + <h3>3. 连接认证测试</h3> | ||
| 137 | + <ul> | ||
| 138 | + <li>T1 TCP 5432/3308 <span class="pass">通过</span></li> | ||
| 139 | + <li>T2 set_native_password 握手 <span class="pass">通过</span></li> | ||
| 140 | + <li>T3 CREATE DATABASE/TABLE <span class="pass">通过</span></li> | ||
| 141 | + </ul> | ||
| 142 | + </div> | ||
| 143 | + </div> | ||
| 144 | + </div> | ||
| 145 | + <p class="legend">配套文件:测试思维导图.mm(可导入 XMind)· 测试思维导图.md(Mermaid)· 自测报告.md</p> | ||
| 146 | +</body> | ||
| 147 | +</html> | ||
| @@ -0,0 +1,55 @@ | |||
| 1 | +# Django × openGauss B 协议适配 — 测试思维导图 | ||
| 2 | + | ||
| 3 | +> 对应任务:`[开源实习]: openGauss B库B协议适配MySQL python orm框架 Django #34` | ||
| 4 | +> 可打开文件:同目录 [`测试思维导图.mm`](./测试思维导图.mm)(FreeMind / XMind 可导入) | ||
| 5 | + | ||
| 6 | +## Mermaid 总览 | ||
| 7 | + | ||
| 8 | +```mermaid | ||
| 9 | +mindmap | ||
| 10 | + root((Django × openGauss B协议适配测试)) | ||
| 11 | + 环境准备 | ||
| 12 | + openEuler 20.03 LTS | ||
| 13 | + openGauss 7.0.x Server | ||
| 14 | + dolphin 3308 | ||
| 15 | + B库/业务库 | ||
| 16 | + 驱动与适配 | ||
| 17 | + PyMySQL | ||
| 18 | + opengauss_mysql Backend | ||
| 19 | + 规避 28804 | ||
| 20 | + 连接认证 | ||
| 21 | + T1 端口连通 | ||
| 22 | + T2 native_password | ||
| 23 | + T3 Schema就绪 | ||
| 24 | + ORM功能 | ||
| 25 | + T4 查询全部 | ||
| 26 | + T5 增删改查 DEMO_OK | ||
| 27 | + 协议限制 | ||
| 28 | + T6 存储过程跳过 | ||
| 29 | + server-side cursor 不支持 | ||
| 30 | + 问题修复 | ||
| 31 | + openblas / OOM / unixODBC | ||
| 32 | + Lite无dolphin | ||
| 33 | + Backend适配 | ||
| 34 | + 交付物 | ||
| 35 | + examples | ||
| 36 | + docs | ||
| 37 | + 自测报告 | ||
| 38 | +``` | ||
| 39 | + | ||
| 40 | +## 用例映射 | ||
| 41 | + | ||
| 42 | +| 编号 | 分支 | 结果 | | ||
| 43 | +|------|------|------| | ||
| 44 | +| T1 | 连接认证 / 端口连通 | 通过 | | ||
| 45 | +| T2 | 连接认证 / 握手认证 | 通过 | | ||
| 46 | +| T3 | 连接认证 / Schema就绪 | 通过 | | ||
| 47 | +| T4 | ORM功能 / 查询全部 | 通过 | | ||
| 48 | +| T5 | ORM功能 / CRUD闭环 | 通过(`DEMO_OK`) | | ||
| 49 | +| T6 | 协议限制 / 存储过程 | 跳过 | | ||
| 50 | + | ||
| 51 | +## 使用说明 | ||
| 52 | + | ||
| 53 | +1. 用 [XMind](https://xmind.app/) / FreeMind / MindMaster 打开 `测试思维导图.mm` | ||
| 54 | +2. 或在支持 Mermaid 的 Markdown 预览中查看上方导图 | ||
| 55 | +3. 详细步骤与日志见 [`自测报告.md`](./自测报告.md) | ||
| @@ -0,0 +1,99 @@ | |||
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | +<map version="1.0.1"> | ||
| 3 | + <node TEXT="Django × openGauss B协议适配测试" FOLDED="false" ID="root"> | ||
| 4 | + <node TEXT="1.环境准备" POSITION="right" ID="env"> | ||
| 5 | + <node TEXT="操作系统" ID="env-os"> | ||
| 6 | + <node TEXT="openEuler 20.03 LTS" ID="env-os-1"/> | ||
| 7 | + <node TEXT="架构 x86_64 / aarch64" ID="env-os-2"/> | ||
| 8 | + </node> | ||
| 9 | + <node TEXT="数据库" ID="env-db"> | ||
| 10 | + <node TEXT="openGauss 7.0.x Server(非 Lite)" ID="env-db-1"/> | ||
| 11 | + <node TEXT="任务要求 7.0.0-LTS / 实测 7.0.0-RC3" ID="env-db-2"/> | ||
| 12 | + <node TEXT="安装路径 /opt/software/openGauss" ID="env-db-3"/> | ||
| 13 | + </node> | ||
| 14 | + <node TEXT="dolphin MySQL协议" ID="env-dolphin"> | ||
| 15 | + <node TEXT="enable_dolphin_proto=on" ID="env-dolphin-1"/> | ||
| 16 | + <node TEXT="dolphin_server_port=3308" ID="env-dolphin-2"/> | ||
| 17 | + <node TEXT="listen_addresses=*" ID="env-dolphin-3"/> | ||
| 18 | + <node TEXT="原生端口 5432" ID="env-dolphin-4"/> | ||
| 19 | + </node> | ||
| 20 | + <node TEXT="业务库" ID="env-biz"> | ||
| 21 | + <node TEXT="B库 proto_test_db" ID="env-biz-1"/> | ||
| 22 | + <node TEXT="业务库 mysql_test_db + user表" ID="env-biz-2"/> | ||
| 23 | + </node> | ||
| 24 | + </node> | ||
| 25 | + | ||
| 26 | + <node TEXT="2.驱动与适配层" POSITION="right" ID="driver"> | ||
| 27 | + <node TEXT="Python 驱动" ID="driver-py"> | ||
| 28 | + <node TEXT="主路径 PyMySQL" ID="driver-py-1"/> | ||
| 29 | + <node TEXT="可选 mysqlclient" ID="driver-py-2"/> | ||
| 30 | + <node TEXT="pymysql_bootstrap 注册 MySQLdb" ID="driver-py-3"/> | ||
| 31 | + </node> | ||
| 32 | + <node TEXT="Django Backend" ID="driver-backend"> | ||
| 33 | + <node TEXT="ENGINE=opengauss_mysql" ID="driver-backend-1"/> | ||
| 34 | + <node TEXT="基于 django.db.backends.mysql" ID="driver-backend-2"/> | ||
| 35 | + <node TEXT="软探测 version/sql_mode" ID="driver-backend-3"/> | ||
| 36 | + <node TEXT="规避 @@default_storage_engine → 28804" ID="driver-backend-4"/> | ||
| 37 | + </node> | ||
| 38 | + <node TEXT="连接参数" ID="driver-conn"> | ||
| 39 | + <node TEXT="HOST / PORT=3308" ID="driver-conn-1"/> | ||
| 40 | + <node TEXT="NAME=mysql_test_db" ID="driver-conn-2"/> | ||
| 41 | + <node TEXT="USER/PASSWORD(native_password)" ID="driver-conn-3"/> | ||
| 42 | + </node> | ||
| 43 | + </node> | ||
| 44 | + | ||
| 45 | + <node TEXT="3.连接认证测试" POSITION="right" ID="auth"> | ||
| 46 | + <node TEXT="T1 端口连通" ID="auth-t1"> | ||
| 47 | + <node TEXT="TCP 5432 通过" ID="auth-t1-1"/> | ||
| 48 | + <node TEXT="TCP 3308 通过" ID="auth-t1-2"/> | ||
| 49 | + <node TEXT="安全组放行" ID="auth-t1-3"/> | ||
| 50 | + </node> | ||
| 51 | + <node TEXT="T2 握手认证" ID="auth-t2"> | ||
| 52 | + <node TEXT="set_native_password" ID="auth-t2-1"/> | ||
| 53 | + <node TEXT="PyMySQL 认证通过" ID="auth-t2-2"/> | ||
| 54 | + </node> | ||
| 55 | + <node TEXT="T3 Schema/表就绪" ID="auth-t3"> | ||
| 56 | + <node TEXT="CREATE DATABASE/TABLE 通过" ID="auth-t3-1"/> | ||
| 57 | + <node TEXT="预置 user 样例数据" ID="auth-t3-2"/> | ||
| 58 | + </node> | ||
| 59 | + </node> | ||
| 60 | + | ||
| 61 | + <node TEXT="4.ORM功能测试" POSITION="left" ID="orm"> | ||
| 62 | + <node TEXT="T4 查询全部" ID="orm-t4"> | ||
| 63 | + <node TEXT="User.objects.all() 通过" ID="orm-t4-1"/> | ||
| 64 | + </node> | ||
| 65 | + <node TEXT="T5 CRUD闭环" ID="orm-t5"> | ||
| 66 | + <node TEXT="新增 create 通过" ID="orm-t5-1"/> | ||
| 67 | + <node TEXT="模糊查询 filter(name__icontains) 通过" ID="orm-t5-2"/> | ||
| 68 | + <node TEXT="更新 save/update 通过" ID="orm-t5-3"/> | ||
| 69 | + <node TEXT="按ID查询 get 通过" ID="orm-t5-4"/> | ||
| 70 | + <node TEXT="删除 delete 通过" ID="orm-t5-5"/> | ||
| 71 | + <node TEXT="DEMO_OK" ID="orm-t5-6"/> | ||
| 72 | + </node> | ||
| 73 | + <node TEXT="入口脚本" ID="orm-script"> | ||
| 74 | + <node TEXT="scripts/run_demo.py" ID="orm-script-1"/> | ||
| 75 | + <node TEXT="云上 /opt/django-opengauss-b" ID="orm-script-2"/> | ||
| 76 | + </node> | ||
| 77 | + </node> | ||
| 78 | + | ||
| 79 | + <node TEXT="5.协议限制" POSITION="left" ID="limit"> | ||
| 80 | + <node TEXT="T6 存储过程 跳过" ID="limit-t6"/> | ||
| 81 | + <node TEXT="server-side cursor 不支持" ID="limit-cursor"/> | ||
| 82 | + <node TEXT="对照 dolphin 官方兼容说明" ID="limit-doc"/> | ||
| 83 | + </node> | ||
| 84 | + | ||
| 85 | + <node TEXT="6.问题与修复" POSITION="left" ID="bugs"> | ||
| 86 | + <node TEXT="缺 libopenblas → yum install openblas" ID="bugs-1"/> | ||
| 87 | + <node TEXT="OOM → overcommit=0 + 调小内存参数" ID="bugs-2"/> | ||
| 88 | + <node TEXT="缺 unixODBC → yum install unixODBC" ID="bugs-3"/> | ||
| 89 | + <node TEXT="Lite 无 dolphin → 改用 Server" ID="bugs-4"/> | ||
| 90 | + <node TEXT="28804 → opengauss_mysql 适配层" ID="bugs-5"/> | ||
| 91 | + </node> | ||
| 92 | + | ||
| 93 | + <node TEXT="7.交付物" POSITION="left" ID="deliver"> | ||
| 94 | + <node TEXT="examples 示例工程 + 自测报告 + 本思维导图" ID="deliver-1"/> | ||
| 95 | + <node TEXT="docs django_development.md" ID="deliver-2"/> | ||
| 96 | + <node TEXT="Plugin 本期无服务端补丁" ID="deliver-3"/> | ||
| 97 | + </node> | ||
| 98 | + </node> | ||
| 99 | +</map> | ||
| @@ -0,0 +1,75 @@ | |||
| 1 | +# Django × openGauss B 协议适配 — 自测报告 | ||
| 2 | + | ||
| 3 | +## 1. 测试环境 | ||
| 4 | + | ||
| 5 | +| 项 | 值 | | ||
| 6 | +|----|----| | ||
| 7 | +| OS | openEuler 20.03 (LTS) x86_64 | | ||
| 8 | +| DB | openGauss **7.0.0-LTS.B008** Server(archive_test 转测包,simpleInstall) | | ||
| 9 | +| 构建号 | `75f983eb`(与安装包目录 `git_num` 一致) | | ||
| 10 | +| 协议 | dolphin MySQL 协议,`enable_dolphin_proto=on`,端口 3308 | | ||
| 11 | +| B 库 | `proto_test_db` | | ||
| 12 | +| Schema/DB | `mysql_test_db` | | ||
| 13 | +| 客户端 | Django 5.0.14 + PyMySQL | | ||
| 14 | +| 版本说明 | 任务要求 7.0.0-LTS;正式 LTS 未上架时采用 archive_test **LTS.B008**(openEuler20.03/x86 最新可用包)。包文件名可能仍含 RC3 字样,以转测目录与 `git_num` 为准。 | | ||
| 15 | + | ||
| 16 | +## 2. 用例与结果 | ||
| 17 | + | ||
| 18 | +| 编号 | 用例 | 结果 | 说明 | | ||
| 19 | +|------|------|------|------| | ||
| 20 | +| T0 | OS / 版本 / 运行状态 | **通过** | openEuler 20.03;`gs_ctl -V` build 75f983eb;`server is running` | | ||
| 21 | +| T1 | TCP 5432/3308 监听 | **通过** | `ss -lntp` 双端口 LISTEN | | ||
| 22 | +| T2 | PyMySQL 握手认证 | **通过** | `set_native_password('user', 'pwd', '')` 后可用 | | ||
| 23 | +| T3 | CREATE DATABASE/TABLE(协议侧) | **通过** | `mysql_test_db` + 表 `user` | | ||
| 24 | +| T4 | Django ORM 查询全部 | **通过** | `scripts/run_demo.py` | | ||
| 25 | +| T5 | Django ORM 新增/模糊查/更新/删除 | **通过** | `DEMO_OK` | | ||
| 26 | +| T6 | Django 安装检查 | **通过** | `INSTALL_OK` | | ||
| 27 | +| T7 | 存储过程 / server-side cursor | 跳过 | 协议文档明确不支持 | | ||
| 28 | + | ||
| 29 | +### 运行日志摘要(脱敏) | ||
| 30 | + | ||
| 31 | +```text | ||
| 32 | +gs_ctl: server is running | ||
| 33 | +INSTALL_OK | ||
| 34 | +=== 查询所有用户 === | ||
| 35 | +... | ||
| 36 | +DEMO_OK | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +## 3. 问题与处理 | ||
| 40 | + | ||
| 41 | +| 问题 | 处理 | | ||
| 42 | +|------|------| | ||
| 43 | +| 正式 7.0.0-LTS 未发布 | 使用 archive_test **LTS.B008** | | ||
| 44 | +| 缺 libopenblas | `yum install openblas` | | ||
| 45 | +| 内存参数过大导致启动失败 | `max_process_memory` 调小,`vm.overcommit_memory=0` | | ||
| 46 | +| 缺 unixODBC | `yum install unixODBC` | | ||
| 47 | +| Lite 无 dolphin | 使用 Server/企业版 | | ||
| 48 | +| Django 探测 `@@default_storage_engine` → 28804 | Backend `opengauss_mysql` | | ||
| 49 | +| `set_native_password` 三参数 | `SELECT set_native_password(user, pwd, '');` | | ||
| 50 | + | ||
| 51 | +## 4. 结论 | ||
| 52 | + | ||
| 53 | +在 openEuler 20.03 + openGauss 7.0.0-LTS.B008 上,经 dolphin 3308,Django ORM(PyMySQL)CRUD 闭环验证通过(`INSTALL_OK` / `DEMO_OK`)。 | ||
| 54 | + | ||
| 55 | +适配要点:不能直接使用原生 `django.db.backends.mysql` 的部分服务端变量探测,需使用本示例 `opengauss_mysql` Backend。 | ||
| 56 | + | ||
| 57 | +## 5. 测试思维导图 | ||
| 58 | + | ||
| 59 | +| 文件 | 说明 | | ||
| 60 | +|------|------| | ||
| 61 | +| [`测试思维导图.mm`](./测试思维导图.mm) | FreeMind / XMind 可导入 | | ||
| 62 | +| [`测试思维导图.md`](./测试思维导图.md) | Mermaid 导图 + 用例映射 | | ||
| 63 | +| [`测试思维导图.html`](./测试思维导图.html) | 浏览器打开 | | ||
| 64 | + | ||
| 65 | +## 6. 验收截图 | ||
| 66 | + | ||
| 67 | +验收截图见 PR 描述(已脱敏,不入库,避免敏感信息扫描误报)。自测结论:`INSTALL_OK` / `DEMO_OK`。 | ||
| 68 | + | ||
| 69 | +## 7. 交付仓库 | ||
| 70 | + | ||
| 71 | +| 仓 | 说明 | | ||
| 72 | +|----|------| | ||
| 73 | +| examples | 本目录 `DjangoConnectOpenGaussB` | | ||
| 74 | +| docs | `django_development.md` | | ||
| 75 | +| Plugin | 本期无服务端补丁 | | ||
| @@ -0,0 +1,104 @@ | |||
| 1 | +# 设计文档 | ||
| 2 | + | ||
| 3 | +> 对应 Issue:[开源实习] openGauss B库B协议适配MySQL python orm框架 Django #34 | ||
| 4 | +> https://gitcode.com/opengauss/opensource-intership/issues/34 | ||
| 5 | + | ||
| 6 | +## 1. 项目背景 | ||
| 7 | + | ||
| 8 | +openGauss 在 dolphin 插件中实现了 MySQL 协议兼容。社区已有 JDBC / MyBatis / Hibernate 等指南,本任务补齐 **Python ORM(Django)+ MySQL Python 驱动** 经 dolphin 连接 B 兼容模式数据库的验证、适配与文档。 | ||
| 9 | + | ||
| 10 | +按社区评审要求:**优先支持原生 `django.db.backends.mysql`**;dolphin 缺少的 MySQL 变量/能力在 **Plugin** 仓补齐,而不是仅靠客户端自定义 Backend 绕过。 | ||
| 11 | + | ||
| 12 | +## 2. 目标与范围 | ||
| 13 | + | ||
| 14 | +| 目标 | 说明 | | ||
| 15 | +|------|------| | ||
| 16 | +| 连通性 | Django 经 `dolphin_server_port`(如 3308)完成认证与会话 | | ||
| 17 | +| 原生 Backend | 使用官方 `django.db.backends.mysql` | | ||
| 18 | +| Plugin 兼容 | 为 Django 探测变量提供兼容桩(无真实引擎语义) | | ||
| 19 | +| 业务可用性 | 覆盖 ORM 查询与 CRUD 主路径 | | ||
| 20 | +| 交付 | examples 示例/自测/思维导图;docs 开发指南;Plugin 补丁 | | ||
| 21 | + | ||
| 22 | +**非目标:** 存储过程、server-side cursor 等协议明确不支持能力的强制支持;实现真实 InnoDB 引擎。 | ||
| 23 | + | ||
| 24 | +## 3. 环境约束 | ||
| 25 | + | ||
| 26 | +- OS:openEuler 20.03 LTS(x86_64 / aarch64) | ||
| 27 | +- DB:openGauss 7.0.0-LTS(正式包未上架时可用 archive_test 转测包,需含 dolphin 的 Server 版,非 Lite) | ||
| 28 | +- 协议:`enable_dolphin_proto=on`,`dolphin_server_port` ≠ 原生 port | ||
| 29 | +- 客户端:Python 3.8+,Django 4.2+/5.0,PyMySQL(或 mysqlclient) | ||
| 30 | + | ||
| 31 | +## 4. 总体方案 | ||
| 32 | + | ||
| 33 | +```text | ||
| 34 | +Django App | ||
| 35 | + -> ENGINE=django.db.backends.mysql | ||
| 36 | + -> PyMySQL (MySQLdb 兼容层) | ||
| 37 | + -> TCP :dolphin_server_port | ||
| 38 | + -> openGauss B DB + dolphin | ||
| 39 | + @@default_storage_engine -> "InnoDB" (stub) | ||
| 40 | + @@sql_auto_is_null -> 0 (stub, SET 可写) | ||
| 41 | +``` | ||
| 42 | + | ||
| 43 | +### 4.1 服务端配置要点 | ||
| 44 | + | ||
| 45 | +1. 创建 B 库:`CREATE DATABASE ... DBCOMPATIBILITY 'B'` | ||
| 46 | +2. `CREATE EXTENSION dolphin`(如需要) | ||
| 47 | +3. `SELECT set_native_password(user, password, '')` | ||
| 48 | +4. GUC:`listen_addresses`、`enable_dolphin_proto`、`dolphin_server_port`、`dolphin.default_database_name` | ||
| 49 | + | ||
| 50 | +### 4.2 Plugin 适配要点(评审结论) | ||
| 51 | + | ||
| 52 | +原生 Django MySQL backend 在 `mysql_server_data` 中会查询: | ||
| 53 | + | ||
| 54 | +- `@@sql_mode` | ||
| 55 | +- `@@default_storage_engine` ← 原报错 **28804**(变量不存在) | ||
| 56 | +- `@@sql_auto_is_null` | ||
| 57 | +- `@@lower_case_table_names` | ||
| 58 | +- 等 | ||
| 59 | + | ||
| 60 | +openGauss **没有** MySQL 存储引擎实现。参考 `contrib/dolphin/plugin_postgres.cpp` 中 `query_cache_size` / `system_time_zone` 等“无实际语义、仅兼容客户端探测”的变量做法: | ||
| 61 | + | ||
| 62 | +1. 在 `BSqlPluginContext` 增加字段 | ||
| 63 | +2. `DefineCustomStringVariable("default_storage_engine", ...)` 默认 `"InnoDB"` | ||
| 64 | +3. `DefineCustomIntVariable("sql_auto_is_null", ...)` 默认 `0`(支持 Django `SET SQL_AUTO_IS_NULL = 0`) | ||
| 65 | +4. SET 时打 WARNING 标明无实际含义(与现有 stub 一致) | ||
| 66 | + | ||
| 67 | +这样框架可正常初始化,且不引入虚假引擎能力。 | ||
| 68 | + | ||
| 69 | +### 4.3 示例业务 | ||
| 70 | + | ||
| 71 | +- Schema/DB:`mysql_test_db` | ||
| 72 | +- 表:`user(id, name, age)` | ||
| 73 | +- 脚本:`scripts/run_demo.py`(查询/增/改/删) | ||
| 74 | +- 过渡:未合入 Plugin 的环境可设 `OG_MYSQL_ENGINE=opengauss_mysql` | ||
| 75 | + | ||
| 76 | +## 5. 测试设计 | ||
| 77 | + | ||
| 78 | +| 编号 | 场景 | 通过标准 | | ||
| 79 | +|------|------|----------| | ||
| 80 | +| T0 | 版本与进程 | `gs_ctl status` running | | ||
| 81 | +| T1 | 端口 | 5432 / 3308 LISTEN | | ||
| 82 | +| T2 | 协议认证 | PyMySQL 握手成功 | | ||
| 83 | +| T3 | DDL | 建库建表成功 | | ||
| 84 | +| T4 | `SELECT @@default_storage_engine` | 返回 `InnoDB` | | ||
| 85 | +| T5 | ORM CRUD(原生 mysql backend) | 打印 `DEMO_OK` | | ||
| 86 | +| T6 | Django 安装检查 | 打印 `INSTALL_OK` | | ||
| 87 | +| T7 | 不支持项 | 文档标注跳过 | | ||
| 88 | + | ||
| 89 | +## 6. 风险与对策 | ||
| 90 | + | ||
| 91 | +| 风险 | 对策 | | ||
| 92 | +|------|------| | ||
| 93 | +| 正式 LTS 包不可得 | 使用同系列 archive_test 转测包并在报告中说明 | | ||
| 94 | +| Lite 无 dolphin | 强制 Server/企业版 | | ||
| 95 | +| 敏感信息入库 | 密码仅通过环境变量注入,文档脱敏 | | ||
| 96 | +| 协议能力差异 | 文档明确不支持项,避免误用 | | ||
| 97 | +| 兼容桩被误用为真实引擎能力 | 文档/WARNING 标明无实际含义 | | ||
| 98 | + | ||
| 99 | +## 7. 交付物清单 | ||
| 100 | + | ||
| 101 | +1. `DjangoConnectOpenGaussB/` 示例工程(默认原生 mysql backend) | ||
| 102 | +2. `docs/自测报告.md`、`docs/测试思维导图.*`、`doc/设计文档.md` | ||
| 103 | +3. docs 仓 `django_development.md` 与 `_toc.yaml` | ||
| 104 | +4. Plugin:`default_storage_engine` / `sql_auto_is_null` 兼容变量 + 回归用例 | ||
| @@ -0,0 +1,16 @@ | |||
| 1 | +#!/usr/bin/env python | ||
| 2 | +import os | ||
| 3 | +import sys | ||
| 4 | + | ||
| 5 | + | ||
| 6 | +def main(): | ||
| 7 | + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") | ||
| 8 | + # Ensure PyMySQL registers before Django imports MySQLdb | ||
| 9 | + import pymysql_bootstrap # noqa: F401 | ||
| 10 | + from django.core.management import execute_from_command_line | ||
| 11 | + | ||
| 12 | + execute_from_command_line(sys.argv) | ||
| 13 | + | ||
| 14 | + | ||
| 15 | +if __name__ == "__main__": | ||
| 16 | + main() | ||
| @@ -0,0 +1 @@ | |||
| 1 | +# openGauss-compatible Django MySQL backend (dolphin protocol) | ||
| @@ -0,0 +1,78 @@ | |||
| 1 | +""" | ||
| 2 | +Django database backend for openGauss B-mode via dolphin MySQL protocol. | ||
| 3 | + | ||
| 4 | +Subclass of django.db.backends.mysql with probes softened for openGauss: | ||
| 5 | +- avoid @@default_storage_engine / unsupported session vars | ||
| 6 | +- fake a MySQL 8.0.28 version tuple so Django feature flags work | ||
| 7 | +""" | ||
| 8 | + | ||
| 9 | +from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper | ||
| 10 | +from django.utils.asyncio import async_unsafe | ||
| 11 | +from django.utils.functional import cached_property | ||
| 12 | + | ||
| 13 | + | ||
| 14 | +class DatabaseWrapper(MySQLDatabaseWrapper): | ||
| 15 | + vendor = "mysql" | ||
| 16 | + display_name = "openGauss (MySQL protocol)" | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + def mysql_server_data(self): | ||
| 20 | + version = "8.0.28-openGauss" | ||
| 21 | + sql_mode = "STRICT_TRANS_TABLES" | ||
| 22 | + lower_case = True | ||
| 23 | + try: | ||
| 24 | + with self.temporary_connection() as cursor: | ||
| 25 | + cursor.execute("SELECT VERSION()") | ||
| 26 | + row = cursor.fetchone() | ||
| 27 | + if row and row[0]: | ||
| 28 | + # Keep Django's parser happy by prefixing a MySQL-like version. | ||
| 29 | + version = f"8.0.28-openGauss ({row[0]})" | ||
| 30 | + try: | ||
| 31 | + cursor.execute("SELECT @@sql_mode") | ||
| 32 | + mode = cursor.fetchone() | ||
| 33 | + if mode and mode[0] is not None: | ||
| 34 | + sql_mode = str(mode[0]) | ||
| 35 | + except Exception: | ||
| 36 | + pass | ||
| 37 | + try: | ||
| 38 | + cursor.execute("SELECT @@lower_case_table_names") | ||
| 39 | + lc = cursor.fetchone() | ||
| 40 | + if lc and lc[0] is not None: | ||
| 41 | + lower_case = bool(int(lc[0])) | ||
| 42 | + except Exception: | ||
| 43 | + pass | ||
| 44 | + except Exception: | ||
| 45 | + pass | ||
| 46 | + return { | ||
| 47 | + "version": version, | ||
| 48 | + "sql_mode": sql_mode, | ||
| 49 | + "default_storage_engine": "InnoDB", | ||
| 50 | + "sql_auto_is_null": False, | ||
| 51 | + "lower_case_table_names": lower_case, | ||
| 52 | + "has_zoneinfo_database": False, | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + | ||
| 56 | + def mysql_version(self): | ||
| 57 | + return (8, 0, 28) | ||
| 58 | + | ||
| 59 | + | ||
| 60 | + def mysql_is_mariadb(self): | ||
| 61 | + return False | ||
| 62 | + | ||
| 63 | + def init_connection_state(self): | ||
| 64 | + # Skip MySQL-only session assignments that openGauss rejects. | ||
| 65 | + # Still honor isolation_level if configured. | ||
| 66 | + assignments = [] | ||
| 67 | + if self.isolation_level: | ||
| 68 | + assignments.append( | ||
| 69 | + "SET SESSION TRANSACTION ISOLATION LEVEL %s" | ||
| 70 | + % self.isolation_level.upper() | ||
| 71 | + ) | ||
| 72 | + if assignments: | ||
| 73 | + with self.cursor() as cursor: | ||
| 74 | + cursor.execute("; ".join(assignments)) | ||
| 75 | + | ||
| 76 | + | ||
| 77 | + def get_connection_params(self): | ||
| 78 | + return super().get_connection_params() | ||
| @@ -0,0 +1,41 @@ | |||
| 1 | +""" | ||
| 2 | +Bootstrap PyMySQL as MySQLdb so Django's mysql backend can talk to | ||
| 3 | +openGauss dolphin MySQL protocol port. | ||
| 4 | + | ||
| 5 | +SELECT VERSION() is a kernel builtin and still returns the openGauss banner | ||
| 6 | +(cannot CREATE OR REPLACE). Django's mysql backend uses re.match() from the | ||
| 7 | +start of that string, so it cannot see the 7.0.0 inside. Dolphin's MySQL | ||
| 8 | +handshake already advertises 8.0.28-dophin-server; parse that same tuple | ||
| 9 | +when the banner is openGauss. | ||
| 10 | +""" | ||
| 11 | +import pymysql | ||
| 12 | + | ||
| 13 | +pymysql.install_as_MySQLdb() | ||
| 14 | + | ||
| 15 | +# Handshake version used by dolphin (plugin_protocol/dqformat.cpp). | ||
| 16 | +_OPENGAUSS_MYSQL_VERSION = (8, 0, 28) | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +def _install_opengauss_version_compat(): | ||
| 20 | + try: | ||
| 21 | + from django.db.backends.mysql.base import DatabaseWrapper | ||
| 22 | + from django.utils.functional import cached_property | ||
| 23 | + except Exception: | ||
| 24 | + return | ||
| 25 | + | ||
| 26 | + orig = DatabaseWrapper.mysql_version.func | ||
| 27 | + | ||
| 28 | + def mysql_version(self): | ||
| 29 | + try: | ||
| 30 | + return orig(self) | ||
| 31 | + except Exception: | ||
| 32 | + info = getattr(self, "mysql_server_info", "") or "" | ||
| 33 | + if "opengauss" in info.lower(): | ||
| 34 | + return _OPENGAUSS_MYSQL_VERSION | ||
| 35 | + raise | ||
| 36 | + | ||
| 37 | + DatabaseWrapper.mysql_version = cached_property(mysql_version) | ||
| 38 | + DatabaseWrapper.mysql_version.__set_name__(DatabaseWrapper, "mysql_version") | ||
| 39 | + | ||
| 40 | + | ||
| 41 | +_install_opengauss_version_compat() | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | +PyMySQL>=1.1.0 | ||
| 2 | +Django>=4.2,<5.1 | ||
| 3 | +cryptography>=41.0.0 | ||
| @@ -0,0 +1,97 @@ | |||
| 1 | +#!/usr/bin/env python | ||
| 2 | +"""CRUD demo for Django ORM against openGauss B-mode MySQL protocol.""" | ||
| 3 | +from __future__ import annotations | ||
| 4 | + | ||
| 5 | +import os | ||
| 6 | +import sys | ||
| 7 | +from pathlib import Path | ||
| 8 | + | ||
| 9 | +ROOT = Path(__file__).resolve().parents[1] | ||
| 10 | +sys.path.insert(0, str(ROOT)) | ||
| 11 | +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") | ||
| 12 | +os.environ.setdefault("OG_MYSQL_HOST", "127.0.0.1") | ||
| 13 | +os.environ.setdefault("OG_MYSQL_PORT", "3308") | ||
| 14 | +os.environ.setdefault("OG_MYSQL_DB", "mysql_test_db") | ||
| 15 | +os.environ.setdefault("OG_MYSQL_USER", "omm") | ||
| 16 | +# OG_MYSQL_PASSWORD must be provided by the environment (do not hardcode secrets) | ||
| 17 | + | ||
| 18 | +if not os.environ.get("OG_MYSQL_PASSWORD"): | ||
| 19 | + print("ERROR: please export OG_MYSQL_PASSWORD before running this demo", file=sys.stderr) | ||
| 20 | + raise SystemExit(2) | ||
| 21 | + | ||
| 22 | +import pymysql_bootstrap # noqa: F401 | ||
| 23 | +import django | ||
| 24 | + | ||
| 25 | +django.setup() | ||
| 26 | + | ||
| 27 | +from demo_app.models import User | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +def main(): | ||
| 31 | + created_ids: list[int] = [] | ||
| 32 | + try: | ||
| 33 | + print("=== 查询所有用户 ===") | ||
| 34 | + for row in User.objects.all().order_by("id"): | ||
| 35 | + print(row) | ||
| 36 | + | ||
| 37 | + print("=== 新增用户 ===") | ||
| 38 | + created = User(name="zhaoliu", age=18) | ||
| 39 | + created.save() | ||
| 40 | + created_ids.append(created.pk) | ||
| 41 | + print(f"新增 id={created.pk}") | ||
| 42 | + | ||
| 43 | + print("=== 特殊字符读写 ===") | ||
| 44 | + special = User(name="O'Brien\\test", age=99) | ||
| 45 | + special.save() | ||
| 46 | + created_ids.append(special.pk) | ||
| 47 | + loaded = User.objects.get(pk=special.pk) | ||
| 48 | + if loaded.name != special.name: | ||
| 49 | + raise RuntimeError( | ||
| 50 | + f"特殊字符读写失败: expected {special.name!r}, got {loaded.name!r}" | ||
| 51 | + ) | ||
| 52 | + print(f"特殊字符 OK: id={special.pk} name={loaded.name!r}") | ||
| 53 | + | ||
| 54 | + print("=== 模糊查询用户名包含 zhao 的用户 ===") | ||
| 55 | + zhao_users = list(User.objects.filter(name__icontains="zhao").order_by("id")) | ||
| 56 | + if not zhao_users: | ||
| 57 | + raise RuntimeError("icontains 查询返回空集,兼容性可能存在问题") | ||
| 58 | + if created.pk not in {row.pk for row in zhao_users}: | ||
| 59 | + raise RuntimeError(f"本次创建 id={created.pk} 未出现在 icontains 结果中") | ||
| 60 | + for row in zhao_users: | ||
| 61 | + print(row) | ||
| 62 | + | ||
| 63 | + print("=== 更新用户信息 ===") | ||
| 64 | + created.name = "zhaoliuliuliu" | ||
| 65 | + created.age = 28 | ||
| 66 | + created.save() | ||
| 67 | + updated = User.objects.get(pk=created.pk) | ||
| 68 | + if updated.name != "zhaoliuliuliu" or updated.age != 28: | ||
| 69 | + raise RuntimeError( | ||
| 70 | + f"更新断言失败: id={created.pk} name={updated.name!r} age={updated.age}" | ||
| 71 | + ) | ||
| 72 | + print(f"更新行: id={created.pk}") | ||
| 73 | + | ||
| 74 | + print("=== 根据ID查询用户 ===") | ||
| 75 | + print(User.objects.get(pk=created.pk)) | ||
| 76 | + | ||
| 77 | + print("=== 删除用户 ===") | ||
| 78 | + deleted, _ = User.objects.filter(pk=created.pk).delete() | ||
| 79 | + if deleted != 1: | ||
| 80 | + raise RuntimeError(f"删除断言失败: expected 1 row, got {deleted}") | ||
| 81 | + if User.objects.filter(pk=created.pk).exists(): | ||
| 82 | + raise RuntimeError(f"删除后 id={created.pk} 仍存在") | ||
| 83 | + created_ids.remove(created.pk) | ||
| 84 | + print(f"删除行数:{deleted}") | ||
| 85 | + | ||
| 86 | + print("=== 查询所有用户 ===") | ||
| 87 | + for row in User.objects.all().order_by("id"): | ||
| 88 | + print(row) | ||
| 89 | + | ||
| 90 | + print("DEMO_OK") | ||
| 91 | + finally: | ||
| 92 | + if created_ids: | ||
| 93 | + User.objects.filter(pk__in=created_ids).delete() | ||
| 94 | + | ||
| 95 | + | ||
| 96 | +if __name__ == "__main__": | ||
| 97 | + main() | ||
【问题】
OPTIONS只设置了charset,没有让 PyMySQL 使用与 Dolphin 实际解析方式一致的字符串转义。协议状态没有NO_BACKSLASH_ESCAPES时,PyMySQL 会把单引号编码为\';在 Dolphin/openGauss 默认字符串解析下,ORM 写入O'Brien等正常业务值可能产生 SQL 语法错误。当前 demo 只写入zhaoliu,无法发现该问题。PyMySQL 对应转义逻辑:https://github.com/PyMySQL/PyMySQL/blob/v1.1.0/pymysql/connections.py#L527-L530
【建议】 明确依赖并合入 Plugin #2522,在
OPTIONS中增加"sql_mode": "NO_BACKSLASH_ESCAPES"(或提供等价且经过验证的会话转义方案),并增加包含单引号、反斜杠字符串的写入和读取断言。