sqlalchemy-lite 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.3
2
+ Name: sqlalchemy-lite
3
+ Version: 0.1.0
4
+ Summary: A future-proof, greenlet-free adapter for SQLAlchemy 2.0 and databases.
5
+ Keywords: sqlalchemy,asyncio,pydantic,databases,sqlite,mysql,postgresql,embedded,arm,lite
6
+ Author: gnakw
7
+ Author-email: gnakw <gnakw@outlook.com>
8
+ Requires-Dist: aiosqlite>=0.22.1
9
+ Requires-Dist: databases>=0.9.0
10
+ Requires-Dist: pydantic>=2.12.5
11
+ Requires-Dist: sqlalchemy>=2.0.46
12
+ Requires-Dist: aiomysql ; extra == 'mysql'
13
+ Requires-Dist: pymysql ; extra == 'mysql'
14
+ Requires-Dist: asyncpg ; extra == 'postgres'
15
+ Requires-Dist: psycopg2-binary ; extra == 'postgres'
16
+ Requires-Dist: aiosqlite ; extra == 'sqlite'
17
+ Requires-Python: >=3.11
18
+ Project-URL: Homepage, https://github.com/gnakw/sqlalchemy-lite
19
+ Project-URL: Documentation, https://github.com/gnakw/sqlalchemy-lite#readme
20
+ Project-URL: Repository, https://github.com/gnakw/sqlalchemy-lite.git
21
+ Project-URL: Bug Tracker, https://github.com/gnakw/sqlalchemy-lite/issues
22
+ Project-URL: Changelog, https://github.com/gnakw/sqlalchemy-lite/releases
23
+ Provides-Extra: mysql
24
+ Provides-Extra: postgres
25
+ Provides-Extra: sqlite
26
+ Description-Content-Type: text/markdown
27
+
28
+ # SQLAlchemy-Lite 🚀
29
+
30
+ **SQLAlchemy-Lite** 是一个专为受限环境(如老旧 ARM 设备、嵌入式系统)设计的轻量级异步数据库适配层。
31
+
32
+ > **核心定位**:A future-proof, greenlet-free adapter for SQLAlchemy 2.0 and databases.
33
+
34
+ 它通过缝合 **SQLAlchemy 2.0 的表达式能力**、**databases 的异步驱动桥接** 以及 **Pydantic 的数据校验**,在彻底摆脱 `greenlet` 依赖的同时,提供了一套现代化的开发体验。
35
+
36
+ ---
37
+
38
+ ## 🌟 核心特性
39
+
40
+ - **去 Greenlet 化**: 彻底避开原生 `AsyncSession` 对 `greenlet` 的硬依赖,解决在特定硬件上无法编译或运行的问题。
41
+ - **Schema 驱动查询**: 配合 `select_for` 工具,自动根据 Pydantic 模型生成精简的 SQL 投影,仅查询所需字段,极大压榨老旧设备的 IO 性能。
42
+ - **2.0 风格语法**: 100% 兼容 SQLAlchemy 2.0 的 `select`, `insert`, `update`, `delete` 表达式构造。
43
+ - **多数据库适配**: 原生支持 **SQLite**, **MySQL**, 及 **PostgreSQL**,支持连接池管理。
44
+ - **类型安全**: 内置 `py.typed`,对 Mypy 和 IDE 自动补全友好。
45
+ - **原生分页支持**:内置 `fetch_page` 异步工具,支持物理分页与总数自动统计,并提供包含 `total_pages`、`has_next` 等智能属性的返回容器。
46
+
47
+ ---
48
+
49
+ ## 📦 安装
50
+
51
+ 使用 [uv](https://github.com/astral-sh/uv) 或 pip 进行安装:
52
+
53
+ ```bash
54
+ # 基础安装 (含核心逻辑)
55
+ uv add sqlalchemy-lite
56
+
57
+ # 根据需求安装数据库驱动扩展
58
+ uv add "sqlalchemy-lite[sqlite]" # 默认 SQLite
59
+ uv add "sqlalchemy-lite[mysql]" # MySQL 支持
60
+ uv add "sqlalchemy-lite[postgres]" # PostgreSQL 支持
61
+
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 🛠️ 快速上手
67
+
68
+ ### 1. 定义数据结构
69
+
70
+ ```python
71
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
72
+ from pydantic import BaseModel
73
+
74
+ class Base(DeclarativeBase): pass
75
+
76
+ # 数据库模型
77
+ class User(Base):
78
+ __tablename__ = "users"
79
+ id: Mapped[int] = mapped_column(primary_key=True)
80
+ username: Mapped[str] = mapped_column()
81
+ email: Mapped[str] = mapped_column()
82
+ bio: Mapped[str] = mapped_column() # 大字段,非必要不查询
83
+
84
+ # 业务视图模型
85
+ class UserSimple(BaseModel):
86
+ username: str
87
+ email: str
88
+
89
+ ```
90
+
91
+ ### 2. 核心查询示例
92
+
93
+ ```python
94
+ from sqlalchemy_lite import Engine, select_for
95
+
96
+ async def main():
97
+ db = Engine("sqlite+aiosqlite:///app.db")
98
+ db.init_db(Base.metadata)
99
+
100
+ await db.connect()
101
+
102
+ async with db.session() as sess:
103
+ # 自动生成精简 SQL: SELECT username, email FROM users
104
+ stmt = select_for(User, UserSimple)
105
+ result = await sess.execute(stmt)
106
+
107
+ # 映射为 Pydantic 对象列表
108
+ users = [UserSimple.model_validate(m) for m in result.mappings()]
109
+
110
+ await db.disconnect()
111
+
112
+ ```
113
+
114
+ ---
115
+
116
+ ## 📖 标准业务服务模板 (Best Practice)
117
+
118
+ 为了确保代码的健壮性与可移植性,推荐采用以下模式:
119
+
120
+ ```python
121
+ from sqlalchemy_lite import auto_query, PageResult, select_for, fetch_page
122
+
123
+ class UserService:
124
+ def __init__(self, db: Engine):
125
+ self.db = db
126
+
127
+ @auto_query(User, UserSimple, single=True)
128
+ async def get_by_name(self, stmt, name: str):
129
+ """使用装饰器:自动处理 session 开启、SQL 投影与模型验证"""
130
+ return stmt.where(User.username == name)
131
+
132
+ async def list_paged(self, page: int, size: int) -> PageResult[UserSimple]:
133
+ """标准分页:计算总数 + 物理分页 + 结果包装"""
134
+ async with self.db.session() as sess:
135
+ base_stmt = select_for(User, UserSimple)
136
+ return await fetch_page(sess, base_stmt, UserSimple, page, size)
137
+
138
+ ```
139
+
140
+ ---
141
+
142
+ ## 🛡️ 模板关键原则 (The Principles)
143
+
144
+ | 维度 | 推荐做法 (未来证明) | 禁忌做法 |
145
+ | --- | --- | --- |
146
+ | **查询列** | 使用 `select_for` 或明确指定列 | 严禁 `select(User)` (全实体查询) |
147
+ | **单条转换** | `Schema.model_validate(dict(row))` | 严禁依赖 ORM 的延迟加载属性 |
148
+ | **结果访问** | 使用 `result.mappings()` 或 `result.scalar()` | 严禁依赖 `result.scalars().all()` 获取整个对象 |
149
+ | **事务** | 始终使用 `async with session.begin():` | 手动显式调用 `commit()` |
150
+
151
+ ---
152
+
153
+ ## 🔗 高级配置与连接池
154
+
155
+ 对于 **MySQL** 或 **PostgreSQL**,建议配置连接池以提升性能:
156
+
157
+ ```python
158
+ db = Engine(
159
+ url="mysql+aiomysql://root:pass@localhost/db",
160
+ min_size=5,
161
+ max_size=20,
162
+ pool_recycle=3600
163
+ )
164
+
165
+ ```
166
+
167
+ ---
168
+
169
+ ## 💎 未来证明 (Future-Proofing)
170
+
171
+ **SQLAlchemy-Lite** 的设计理念是“不产生负担”。当你不再受限于硬件环境,想要迁移回官方的 SQLAlchemy `AsyncSession` 时:
172
+
173
+ 1. **零逻辑修改**: 由于 `select_for` 生成的是标准 SQLAlchemy 语句,你的业务函数体无需任何修改。
174
+ 2. **零迁移成本**: 我们的 `Session` 和 `Result` 接口高度模拟了官方 API。你只需将 `Engine` 替换为 `create_async_engine`,并调整 `Session` 获取方式即可。
175
+
176
+ ---
177
+
178
+ ## ⚖️ 开源协议
179
+
180
+ 本项目采用 **MIT** 协议。
181
+
182
+
183
+ ## Acknowledgment
184
+
185
+ This project was developed with the assistance of AI (Gemini). While the core architecture and logic were human-steered and rigorously reviewed to ensure security and compliance with SQLAlchemy 2.0 standards, this collaboration allowed for a more rapid exploration of lite-weight patterns for restricted environments.
186
+
@@ -0,0 +1,159 @@
1
+ # SQLAlchemy-Lite 🚀
2
+
3
+ **SQLAlchemy-Lite** 是一个专为受限环境(如老旧 ARM 设备、嵌入式系统)设计的轻量级异步数据库适配层。
4
+
5
+ > **核心定位**:A future-proof, greenlet-free adapter for SQLAlchemy 2.0 and databases.
6
+
7
+ 它通过缝合 **SQLAlchemy 2.0 的表达式能力**、**databases 的异步驱动桥接** 以及 **Pydantic 的数据校验**,在彻底摆脱 `greenlet` 依赖的同时,提供了一套现代化的开发体验。
8
+
9
+ ---
10
+
11
+ ## 🌟 核心特性
12
+
13
+ - **去 Greenlet 化**: 彻底避开原生 `AsyncSession` 对 `greenlet` 的硬依赖,解决在特定硬件上无法编译或运行的问题。
14
+ - **Schema 驱动查询**: 配合 `select_for` 工具,自动根据 Pydantic 模型生成精简的 SQL 投影,仅查询所需字段,极大压榨老旧设备的 IO 性能。
15
+ - **2.0 风格语法**: 100% 兼容 SQLAlchemy 2.0 的 `select`, `insert`, `update`, `delete` 表达式构造。
16
+ - **多数据库适配**: 原生支持 **SQLite**, **MySQL**, 及 **PostgreSQL**,支持连接池管理。
17
+ - **类型安全**: 内置 `py.typed`,对 Mypy 和 IDE 自动补全友好。
18
+ - **原生分页支持**:内置 `fetch_page` 异步工具,支持物理分页与总数自动统计,并提供包含 `total_pages`、`has_next` 等智能属性的返回容器。
19
+
20
+ ---
21
+
22
+ ## 📦 安装
23
+
24
+ 使用 [uv](https://github.com/astral-sh/uv) 或 pip 进行安装:
25
+
26
+ ```bash
27
+ # 基础安装 (含核心逻辑)
28
+ uv add sqlalchemy-lite
29
+
30
+ # 根据需求安装数据库驱动扩展
31
+ uv add "sqlalchemy-lite[sqlite]" # 默认 SQLite
32
+ uv add "sqlalchemy-lite[mysql]" # MySQL 支持
33
+ uv add "sqlalchemy-lite[postgres]" # PostgreSQL 支持
34
+
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 🛠️ 快速上手
40
+
41
+ ### 1. 定义数据结构
42
+
43
+ ```python
44
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
45
+ from pydantic import BaseModel
46
+
47
+ class Base(DeclarativeBase): pass
48
+
49
+ # 数据库模型
50
+ class User(Base):
51
+ __tablename__ = "users"
52
+ id: Mapped[int] = mapped_column(primary_key=True)
53
+ username: Mapped[str] = mapped_column()
54
+ email: Mapped[str] = mapped_column()
55
+ bio: Mapped[str] = mapped_column() # 大字段,非必要不查询
56
+
57
+ # 业务视图模型
58
+ class UserSimple(BaseModel):
59
+ username: str
60
+ email: str
61
+
62
+ ```
63
+
64
+ ### 2. 核心查询示例
65
+
66
+ ```python
67
+ from sqlalchemy_lite import Engine, select_for
68
+
69
+ async def main():
70
+ db = Engine("sqlite+aiosqlite:///app.db")
71
+ db.init_db(Base.metadata)
72
+
73
+ await db.connect()
74
+
75
+ async with db.session() as sess:
76
+ # 自动生成精简 SQL: SELECT username, email FROM users
77
+ stmt = select_for(User, UserSimple)
78
+ result = await sess.execute(stmt)
79
+
80
+ # 映射为 Pydantic 对象列表
81
+ users = [UserSimple.model_validate(m) for m in result.mappings()]
82
+
83
+ await db.disconnect()
84
+
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 📖 标准业务服务模板 (Best Practice)
90
+
91
+ 为了确保代码的健壮性与可移植性,推荐采用以下模式:
92
+
93
+ ```python
94
+ from sqlalchemy_lite import auto_query, PageResult, select_for, fetch_page
95
+
96
+ class UserService:
97
+ def __init__(self, db: Engine):
98
+ self.db = db
99
+
100
+ @auto_query(User, UserSimple, single=True)
101
+ async def get_by_name(self, stmt, name: str):
102
+ """使用装饰器:自动处理 session 开启、SQL 投影与模型验证"""
103
+ return stmt.where(User.username == name)
104
+
105
+ async def list_paged(self, page: int, size: int) -> PageResult[UserSimple]:
106
+ """标准分页:计算总数 + 物理分页 + 结果包装"""
107
+ async with self.db.session() as sess:
108
+ base_stmt = select_for(User, UserSimple)
109
+ return await fetch_page(sess, base_stmt, UserSimple, page, size)
110
+
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 🛡️ 模板关键原则 (The Principles)
116
+
117
+ | 维度 | 推荐做法 (未来证明) | 禁忌做法 |
118
+ | --- | --- | --- |
119
+ | **查询列** | 使用 `select_for` 或明确指定列 | 严禁 `select(User)` (全实体查询) |
120
+ | **单条转换** | `Schema.model_validate(dict(row))` | 严禁依赖 ORM 的延迟加载属性 |
121
+ | **结果访问** | 使用 `result.mappings()` 或 `result.scalar()` | 严禁依赖 `result.scalars().all()` 获取整个对象 |
122
+ | **事务** | 始终使用 `async with session.begin():` | 手动显式调用 `commit()` |
123
+
124
+ ---
125
+
126
+ ## 🔗 高级配置与连接池
127
+
128
+ 对于 **MySQL** 或 **PostgreSQL**,建议配置连接池以提升性能:
129
+
130
+ ```python
131
+ db = Engine(
132
+ url="mysql+aiomysql://root:pass@localhost/db",
133
+ min_size=5,
134
+ max_size=20,
135
+ pool_recycle=3600
136
+ )
137
+
138
+ ```
139
+
140
+ ---
141
+
142
+ ## 💎 未来证明 (Future-Proofing)
143
+
144
+ **SQLAlchemy-Lite** 的设计理念是“不产生负担”。当你不再受限于硬件环境,想要迁移回官方的 SQLAlchemy `AsyncSession` 时:
145
+
146
+ 1. **零逻辑修改**: 由于 `select_for` 生成的是标准 SQLAlchemy 语句,你的业务函数体无需任何修改。
147
+ 2. **零迁移成本**: 我们的 `Session` 和 `Result` 接口高度模拟了官方 API。你只需将 `Engine` 替换为 `create_async_engine`,并调整 `Session` 获取方式即可。
148
+
149
+ ---
150
+
151
+ ## ⚖️ 开源协议
152
+
153
+ 本项目采用 **MIT** 协议。
154
+
155
+
156
+ ## Acknowledgment
157
+
158
+ This project was developed with the assistance of AI (Gemini). While the core architecture and logic were human-steered and rigorously reviewed to ensure security and compliance with SQLAlchemy 2.0 standards, this collaboration allowed for a more rapid exploration of lite-weight patterns for restricted environments.
159
+
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "sqlalchemy-lite"
3
+ version = "0.1.0"
4
+ description = "A future-proof, greenlet-free adapter for SQLAlchemy 2.0 and databases."
5
+ keywords = [
6
+ "sqlalchemy",
7
+ "asyncio",
8
+ "pydantic",
9
+ "databases",
10
+ "sqlite",
11
+ "mysql",
12
+ "postgresql",
13
+ "embedded",
14
+ "arm",
15
+ "lite"
16
+ ]
17
+ readme = "README.md"
18
+ authors = [
19
+ { name = "gnakw", email = "gnakw@outlook.com" }
20
+ ]
21
+ requires-python = ">=3.11"
22
+ dependencies = [
23
+ "aiosqlite>=0.22.1",
24
+ "databases>=0.9.0",
25
+ "pydantic>=2.12.5",
26
+ "sqlalchemy>=2.0.46",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ sqlite = ["aiosqlite"]
31
+ mysql = ["aiomysql", "pymysql"]
32
+ postgres = ["asyncpg", "psycopg2-binary"]
33
+
34
+ [project.urls]
35
+ "Homepage" = "https://github.com/gnakw/sqlalchemy-lite"
36
+ "Documentation" = "https://github.com/gnakw/sqlalchemy-lite#readme"
37
+ "Repository" = "https://github.com/gnakw/sqlalchemy-lite.git"
38
+ "Bug Tracker" = "https://github.com/gnakw/sqlalchemy-lite/issues"
39
+ "Changelog" = "https://github.com/gnakw/sqlalchemy-lite/releases"
40
+
41
+ [build-system]
42
+ requires = ["uv_build>=0.9.29,<0.10.0"]
43
+ build-backend = "uv_build"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "aiosqlite>=0.22.1",
48
+ "pytest>=9.0.2",
49
+ "pytest-asyncio>=1.3.0",
50
+ "ruff>=0.15.0",
51
+ ]
52
+
53
+ [tool.ruff.lint]
54
+ select = ["I", "E", "F"]
@@ -0,0 +1,15 @@
1
+ from .engine import Engine
2
+ from .ext import auto_query
3
+ from .proxy import Result, Session
4
+ from .types import PageResult
5
+ from .utils import fetch_page, select_for
6
+
7
+ __all__ = [
8
+ "Engine",
9
+ "Session",
10
+ "Result",
11
+ "PageResult",
12
+ "auto_query",
13
+ "select_for",
14
+ "fetch_page",
15
+ ]
@@ -0,0 +1,56 @@
1
+ import logging
2
+ from typing import TypeVar
3
+
4
+ import sqlalchemy
5
+ from databases import Database
6
+ from pydantic import BaseModel
7
+ from sqlalchemy import create_engine
8
+
9
+ from .proxy import Session
10
+
11
+ logger = logging.getLogger(__name__)
12
+ T = TypeVar("T", bound=BaseModel)
13
+
14
+
15
+ class Engine:
16
+ def __init__(self, url: str, **kwargs):
17
+ self.url = url
18
+ self.db = Database(url, **kwargs)
19
+
20
+ sync_url = (
21
+ url.replace("+aiosqlite", "")
22
+ .replace("+aiomysql", "+pymysql") # MySQL 异步转同步
23
+ .replace("+asyncpg", "") # PostgreSQL 异步转同步
24
+ )
25
+ self._sync_engine = create_engine(sync_url)
26
+
27
+ def init_db(self, metadata: sqlalchemy.MetaData):
28
+ """
29
+ 根据定义的 Base.metadata 自动在数据库中创建所有缺失的表。
30
+ """
31
+ try:
32
+ metadata.create_all(self._sync_engine)
33
+ logger.info(">>> [SQLAlchemy-Lite] 数据库表结构初始化成功")
34
+ except Exception as e:
35
+ logger.exception(f">>> [SQLAlchemy-Lite] 数据库初始化失败: {e}")
36
+ raise
37
+
38
+ async def connect(self):
39
+ await self.db.connect()
40
+
41
+ async def disconnect(self):
42
+ await self.db.disconnect()
43
+
44
+ from contextlib import asynccontextmanager
45
+
46
+ @asynccontextmanager
47
+ async def session(self):
48
+ async with self.db.connection() as conn:
49
+ yield Session(conn)
50
+
51
+ # def select_for(self, model: Any, schema: Type[T]):
52
+ # """根据 Pydantic Schema 自动投影列"""
53
+ # cols = [
54
+ # getattr(model, f) for f in schema.model_fields.keys() if hasattr(model, f)
55
+ # ]
56
+ # return select(*(cols or [model]))
@@ -0,0 +1,40 @@
1
+ from functools import wraps
2
+ from typing import Type
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from sqlalchemy_lite.utils import select_for
7
+
8
+
9
+ def auto_query(db_model, schema: Type[BaseModel], single: bool = False):
10
+ """业务级装饰器, 自动查询
11
+
12
+ Args:
13
+ db_model (_type_): SQLAlchemy 模型
14
+ schema (Type[BaseModel]): Pydantic 模型 (用于 select_for)
15
+ single (bool, optional): 是否只返回单条数据 (result.first vs result.all)
16
+
17
+ """
18
+
19
+ def decorator(func):
20
+ @wraps(func)
21
+ async def wrapper(self, *args, **kwargs):
22
+ # 1. 自动根据 Schema 构建基础语句
23
+ base_stmt = select_for(db_model, schema)
24
+
25
+ # 2. 执行原函数获取过滤条件 (如 .where())
26
+ final_stmt = await func(self, base_stmt, *args, **kwargs)
27
+
28
+ # 3. 统一处理 Session 执行逻辑
29
+ async with self.db.session() as sess:
30
+ result = await sess.execute(final_stmt)
31
+
32
+ if single:
33
+ row = result.first()
34
+ return schema.model_validate(dict(row)) if row else None
35
+
36
+ return [schema.model_validate(m) for m in result.mappings()]
37
+
38
+ return wrapper
39
+
40
+ return decorator
@@ -0,0 +1,97 @@
1
+ from typing import Any, Dict, List, Optional, Union
2
+
3
+ from databases.interfaces import Record
4
+ from sqlalchemy.exc import MultipleResultsFound, NoResultFound
5
+
6
+
7
+ class ScalarResult:
8
+ """处理 .scalars() 的降维结果集"""
9
+
10
+ def __init__(self, rows: List[Record]):
11
+ self._data = [row[0] for row in rows] if rows else []
12
+
13
+ def all(self) -> List[Any]:
14
+ return self._data
15
+
16
+ def first(self) -> Optional[Any]:
17
+ return self._data[0] if self._data else None
18
+
19
+ def __iter__(self):
20
+ return iter(self._data)
21
+
22
+
23
+ class Result:
24
+ """模拟 SQLAlchemy 2.0 Result 接口"""
25
+
26
+ def __init__(self, rows: Union[List[Record], Any]):
27
+ self._rows = (
28
+ rows if isinstance(rows, list) else ([rows] if rows is not None else [])
29
+ )
30
+
31
+ def all(self) -> List[Record]:
32
+ return self._rows
33
+
34
+ def first(self) -> Optional[Record]:
35
+ return self._rows[0] if self._rows else None
36
+
37
+ def one_or_none(self):
38
+ """取第一行,如果有第二行则报错"""
39
+ if len(self._rows) > 1:
40
+ raise MultipleResultsFound(
41
+ f"Expected at most one result, got {len(self._rows)}"
42
+ )
43
+ return self._rows[0] if self._rows else None
44
+
45
+ def one(self):
46
+ """必须有一行,且只能有一行"""
47
+ if not self._rows:
48
+ raise NoResultFound("No result found")
49
+ if len(self._rows) > 1:
50
+ raise MultipleResultsFound(
51
+ f"Expected exactly one result, got {len(self._rows)}"
52
+ )
53
+ return self._rows[0]
54
+
55
+ def scalar(self) -> Any:
56
+ return self._rows[0][0] if self._rows and len(self._rows[0]) > 0 else None
57
+
58
+ def scalar_one_or_none(self):
59
+ """安全返回单值"""
60
+ row = self.one_or_none()
61
+ return row[0] if row else None
62
+
63
+ def scalars(self) -> ScalarResult:
64
+ return ScalarResult(self._rows)
65
+
66
+ def mappings(self) -> List[Dict[str, Any]]:
67
+ return [dict(row) for row in self._rows]
68
+
69
+ def __iter__(self):
70
+ return iter(self._rows)
71
+
72
+
73
+ class Session:
74
+ """映射 SQLAlchemy Session 到 databases.Connection"""
75
+
76
+ def __init__(self, conn):
77
+ self._conn = conn
78
+
79
+ from contextlib import asynccontextmanager
80
+
81
+ @asynccontextmanager
82
+ async def begin(self):
83
+ async with self._conn.transaction():
84
+ yield self
85
+
86
+ async def execute(self, statement: Any) -> Result:
87
+ raw_sql = str(statement).strip().upper()
88
+ # 简单判别 SELECT 逻辑,亦可扩展更严谨的判断
89
+ if raw_sql.startswith("SELECT") or "RETURNING" in raw_sql:
90
+ rows = await self._conn.fetch_all(statement)
91
+ return Result(rows)
92
+ res = await self._conn.execute(statement)
93
+ return Result(res)
94
+
95
+ async def scalar(self, statement):
96
+ res = await self.execute(statement)
97
+ return res.scalar()
File without changes
@@ -0,0 +1,32 @@
1
+ import math
2
+ from typing import Generic, List, TypeVar
3
+
4
+ from pydantic import BaseModel, ConfigDict, computed_field
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ class PageResult(BaseModel, Generic[T]):
10
+ items: List[T]
11
+ total: int
12
+ page: int
13
+ size: int
14
+
15
+ @computed_field
16
+ @property
17
+ def total_pages(self) -> int:
18
+ if self.size <= 0:
19
+ return 0
20
+ return math.ceil(self.total / self.size)
21
+
22
+ @computed_field
23
+ @property
24
+ def has_next(self) -> bool:
25
+ return self.page < self.total_pages
26
+
27
+ @computed_field
28
+ @property
29
+ def has_prev(self) -> bool:
30
+ return self.page > 1
31
+
32
+ model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,52 @@
1
+ from typing import Any, Type, TypeVar
2
+
3
+ from pydantic import BaseModel
4
+ from sqlalchemy import func, select
5
+
6
+ from sqlalchemy_lite.types import PageResult
7
+
8
+ T = TypeVar("T", bound=BaseModel)
9
+
10
+
11
+ def get_select_columns(db_model: Any, schema: Type[T]):
12
+ """
13
+ 根据 Pydantic Schema 提取 SQLAlchemy Model 的 Column 对象
14
+ """
15
+ selected_columns = [
16
+ getattr(db_model, field_name)
17
+ for field_name in schema.model_fields.keys()
18
+ if hasattr(db_model, field_name)
19
+ ]
20
+
21
+ # 如果没有匹配到任何列,默认返回模型本身 (SELECT *)
22
+ return selected_columns if selected_columns else [db_model]
23
+
24
+
25
+ def select_for(db_model: Any, schema: Type[T]):
26
+ """
27
+ 根据 Pydantic 模型自动生成精简字段的SQLAlchemy select 语句
28
+ """
29
+ columns = get_select_columns(db_model, schema)
30
+ return select(*columns)
31
+
32
+
33
+ async def fetch_page(
34
+ session, stmt, schema: Type[T], page: int, size: int
35
+ ) -> PageResult[T]:
36
+ safe_page = max(1, page)
37
+ safe_size = max(1, size)
38
+
39
+ count_stmt = select(func.count()).select_from(stmt.subquery())
40
+ total = await session.scalar(count_stmt) or 0
41
+
42
+ total_pages = (total + safe_size - 1) // safe_size if total > 0 else 1
43
+
44
+ if safe_page > total_pages or total == 0:
45
+ return PageResult(items=[], total=total, page=safe_page, size=safe_size)
46
+
47
+ offset = (safe_page - 1) * safe_size
48
+ paged_stmt = stmt.offset(offset).limit(safe_size)
49
+ result = await session.execute(paged_stmt)
50
+
51
+ items = [schema.model_validate(m) for m in result.mappings()]
52
+ return PageResult(items=items, total=total, page=safe_page, size=safe_size)