rename
This commit is contained in:
1 parent
03d7b07bf6
commit
400f527a88
7 files changed
No files matched your search
+112
@@ -0,0 +1,112 @@
|
||||
#prjs
|
||||
*.db
|
||||
*.bin
|
||||
*.build
|
||||
*.dist
|
||||
*.onefile-build
|
||||
nuitka-crash-report.xml
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 卿松
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
# 跨数据库通用查询服务(基于 SQLAlchemy Core)
|
||||
|
||||
一个安全的 HTTP 服务,通过结构化 JSON 描述查询意图,后端自动转换为对应数据库的 SQL,无需手写 SQL,即可无缝切换 SQLite、PostgreSQL、MySQL 等数据库。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- **跨数据库**:基于 SQLAlchemy Core 表达式语言,一套接口适配多种数据库
|
||||
- **零 SQL 注入**:所有条件、值均通过 SQLAlchemy 参数绑定处理
|
||||
- **统一 JSON 接口**:`action` + `table` + `conditions` 模式
|
||||
- **支持增删改查**:select, insert, update, delete 全覆盖
|
||||
- **单线程本地监听**:waitress + 127.0.0.1:8005,简单可靠
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
pip install flask sqlalchemy waitress
|
||||
# 根据需要安装数据库驱动,例如:
|
||||
# pip install psycopg2-binary # PostgreSQL
|
||||
# pip install pymysql # MySQL
|
||||
```
|
||||
|
||||
### 启动服务(默认 SQLite)
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
服务运行在 `http://127.0.0.1:8005`。
|
||||
|
||||
使用其他数据库:
|
||||
```bash
|
||||
export DATABASE_URL="postgresql://user:pass@localhost/dbname"
|
||||
python app.py
|
||||
```
|
||||
|
||||
**注意**:新接口依赖表已存在(通过反射获取结构)。首次使用时请先手动建表:
|
||||
```bash
|
||||
sqlite3 database.db "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"
|
||||
```
|
||||
|
||||
## API 文档
|
||||
|
||||
### 请求格式
|
||||
|
||||
**端点**:`POST /execute`
|
||||
**Content-Type**:`application/json`
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "操作类型", // select | insert | update | delete
|
||||
"table": "表名",
|
||||
"columns": ["列1", "列2"], // select/insert 时可选
|
||||
"values": { // insert/update 时必填
|
||||
"列名": "值"
|
||||
},
|
||||
"where": { // 条件,支持组合操作符
|
||||
"age": {"ge": 18, "lt": 60},
|
||||
"name": "Alice"
|
||||
},
|
||||
"order_by": [{"column": "age", "desc": true}],
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}
|
||||
```
|
||||
|
||||
### where 条件支持的运算符
|
||||
|
||||
| 运算符 | 说明 | 示例 |
|
||||
|--------|------|------|
|
||||
| 直接值 | 等于 | `"name": "Alice"` |
|
||||
| eq | 等于 | `"age": {"eq": 30}` |
|
||||
| ne | 不等于 | `"status": {"ne": "deleted"}` |
|
||||
| gt | 大于 | `"score": {"gt": 80}` |
|
||||
| ge | 大于等于 | `"score": {"ge": 60}` |
|
||||
| lt | 小于 | `"price": {"lt": 100}` |
|
||||
| le | 小于等于 | `"price": {"le": 200}` |
|
||||
| like | 模糊匹配 | `"name": {"like": "%Smith%"}` |
|
||||
| in | 包含于列表 | `"id": {"in": [1,2,3]}` |
|
||||
|
||||
多个条件自动使用 **AND** 连接。
|
||||
|
||||
### 响应格式
|
||||
|
||||
成功:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [ // 查询结果数组(insert/update/delete 时包含影响行数或 lastrowid)
|
||||
{"id": 1, "name": "Alice"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "错误描述"
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例(curl)
|
||||
|
||||
**插入**:
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8005/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action":"insert","table":"users","values":{"name":"Bob","age":25}}'
|
||||
```
|
||||
|
||||
**查询**:
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8005/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action":"select","table":"users","where":{"age":{"gt":20}},"order_by":[{"column":"id","desc":true}],"limit":5}'
|
||||
```
|
||||
|
||||
**更新**:
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8005/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action":"update","table":"users","values":{"age":26},"where":{"name":"Bob"}}'
|
||||
```
|
||||
|
||||
**删除**:
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8005/execute \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action":"delete","table":"users","where":{"id":{"in":[1,2]}}}'
|
||||
```
|
||||
|
||||
## 安全机制
|
||||
|
||||
- 强制使用 SQLAlchemy 参数绑定,杜绝 SQL 注入。
|
||||
- 仅反射已有表,无法创建或删除表(保证结构安全)。
|
||||
- 所有异常自动回滚事务。
|
||||
- 默认监听 `127.0.0.1`,仅本机可访问。
|
||||
|
||||
## 切换数据库
|
||||
|
||||
只需修改环境变量 `DATABASE_URL`,并安装对应驱动:
|
||||
|
||||
- SQLite:`sqlite:///database.db`
|
||||
- PostgreSQL:`postgresql://user:pass@localhost/dbname` (需 `pip install psycopg2`)
|
||||
- MySQL:`mysql+pymysql://user:pass@localhost/dbname` (需 `pip install pymysql`)
|
||||
|
||||
SQLAlchemy 会自动生成适配各数据库的 SQL 方言,无需修改接口调用。
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
source ./venv/bin/activate
|
||||
exec ./main.bin
|
||||
#exec python3 main.py
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
nuitka --onefile --jobs=1 main.py
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import os
|
||||
from flask import Flask, request, jsonify, g
|
||||
from sqlalchemy import (
|
||||
create_engine, MetaData, Table, select, insert, update, delete, text
|
||||
)
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from waitress import serve
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL = os.environ.get('DATABASE_URL', 'sqlite:///database.db')
|
||||
engine = create_engine(DATABASE_URL)
|
||||
|
||||
# 缓存反射的元数据(避免每次请求都反射)
|
||||
metadata = MetaData()
|
||||
|
||||
def get_table(table_name):
|
||||
"""反射表对象(若已缓存则直接返回)"""
|
||||
if table_name not in metadata.tables:
|
||||
Table(table_name, metadata, autoload_with=engine)
|
||||
return metadata.tables[table_name]
|
||||
|
||||
def build_where(table, conditions):
|
||||
"""将条件字典转换为 SQLAlchemy 条件表达式"""
|
||||
if not conditions:
|
||||
return None
|
||||
clauses = []
|
||||
for col, val in conditions.items():
|
||||
column = table.columns[col]
|
||||
if isinstance(val, dict):
|
||||
# 支持多种比较操作
|
||||
if 'eq' in val:
|
||||
clauses.append(column == val['eq'])
|
||||
if 'ne' in val:
|
||||
clauses.append(column != val['ne'])
|
||||
if 'gt' in val:
|
||||
clauses.append(column > val['gt'])
|
||||
if 'lt' in val:
|
||||
clauses.append(column < val['lt'])
|
||||
if 'ge' in val:
|
||||
clauses.append(column >= val['ge'])
|
||||
if 'le' in val:
|
||||
clauses.append(column <= val['le'])
|
||||
if 'like' in val:
|
||||
clauses.append(column.like(val['like']))
|
||||
if 'in' in val:
|
||||
clauses.append(column.in_(val['in']))
|
||||
else:
|
||||
# 直接等值比较
|
||||
clauses.append(column == val)
|
||||
from sqlalchemy import and_
|
||||
return and_(*clauses)
|
||||
|
||||
@app.route('/execute', methods=['POST'])
|
||||
def execute_structured():
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': '请求体不能为空'}), 400
|
||||
|
||||
action = data.get('action', 'select')
|
||||
table_name = data.get('table')
|
||||
if not table_name:
|
||||
return jsonify({'success': False, 'error': '缺少 table 字段'}), 400
|
||||
|
||||
try:
|
||||
table = get_table(table_name)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'表加载失败: {str(e)}'}), 400
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = engine.connect()
|
||||
result = []
|
||||
if action == 'select':
|
||||
columns = data.get('columns', None)
|
||||
if columns:
|
||||
cols = [table.columns[c] for c in columns]
|
||||
else:
|
||||
cols = [table] # select *
|
||||
|
||||
stmt = select(*cols)
|
||||
where_cond = build_where(table, data.get('where'))
|
||||
if where_cond is not None:
|
||||
stmt = stmt.where(where_cond)
|
||||
|
||||
# 排序
|
||||
order_by = data.get('order_by')
|
||||
if order_by:
|
||||
for item in order_by:
|
||||
col = table.columns[item['column']]
|
||||
stmt = stmt.order_by(col.desc() if item.get('desc') else col.asc())
|
||||
|
||||
# 分页
|
||||
limit = data.get('limit')
|
||||
if limit is not None:
|
||||
stmt = stmt.limit(limit)
|
||||
offset = data.get('offset')
|
||||
if offset is not None:
|
||||
stmt = stmt.offset(offset)
|
||||
|
||||
res = conn.execute(stmt)
|
||||
rows = res.fetchall()
|
||||
result = [dict(row._mapping) for row in rows]
|
||||
|
||||
elif action == 'insert':
|
||||
values = data.get('values', {})
|
||||
stmt = insert(table).values(**values)
|
||||
res = conn.execute(stmt)
|
||||
conn.commit()
|
||||
result = [{'lastrowid': res.lastrowid}] if hasattr(res, 'lastrowid') else []
|
||||
|
||||
elif action == 'update':
|
||||
values = data.get('values', {})
|
||||
where_cond = build_where(table, data.get('where'))
|
||||
stmt = update(table).values(**values)
|
||||
if where_cond is not None:
|
||||
stmt = stmt.where(where_cond)
|
||||
res = conn.execute(stmt)
|
||||
conn.commit()
|
||||
result = [{'rowcount': res.rowcount}]
|
||||
|
||||
elif action == 'delete':
|
||||
where_cond = build_where(table, data.get('where'))
|
||||
stmt = delete(table)
|
||||
if where_cond is not None:
|
||||
stmt = stmt.where(where_cond)
|
||||
res = conn.execute(stmt)
|
||||
conn.commit()
|
||||
result = [{'rowcount': res.rowcount}]
|
||||
|
||||
else:
|
||||
return jsonify({'success': False, 'error': f'不支持的操作: {action}'}), 400
|
||||
|
||||
return jsonify({'success': True, 'data': result})
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 单线程单进程,绑定 127.0.0.1
|
||||
serve(app, host='127.0.0.1', port=8005, threads=1)
|
||||
@@ -0,0 +1,3 @@
|
||||
nuitka
|
||||
flask
|
||||
waitress
|
||||
Reference in new issue
Block a user