Python 库

Pytuck:为受限 Python 留一座数据库

一个面向 Ren’Py 等受限环境的纯 Python 嵌入式数据库:默认零外部依赖,也保留模型、查询与多后端能力。

并不是每一个运行着 Python 的地方,都拥有一个完整的 Python 世界。

在普通的后端项目里,需要保存结构化数据时,我大可以选择 SQLite、SQLAlchemy,或者一套更完整的数据库服务。但在 Ren’Py 这样的运行环境中,Python 只是作品运行时的一部分:解释器版本、标准库、二进制扩展和第三方依赖都可能受到限制。一个在开发机上理所当然的方案,到了玩家设备上未必还能成立。

Pytuck 就是从这道缝隙里长出来的。

它是一个纯 Python 实现的轻量级嵌入式数据库。核心安装不依赖任何第三方包,却仍然提供模型定义、Session、条件查询、索引、关系与事务等能力。它并不试图在正常环境中取代成熟数据库;它想做的,是在选择很少的地方,仍然留下一种足够清晰的数据组织方式。

从游戏存档之外开始

游戏当然可以把状态塞进字典,再用 JSON 保存。数据很少时,这种方法简单而直接。但当角色、物品、任务、对话状态与玩家行为逐渐彼此关联,字典会慢慢变成一张没有被承认的表,散落的读写逻辑也会形成一套没有名字的查询系统。

我希望这些数据能够被明确地描述:

  • 哪些字段属于一条记录;
  • 哪个字段是主键,哪些字段需要索引;
  • 查询、修改和删除采用同一套表达;
  • 存储格式可以更换,而上层模型不必随之重写;
  • 最重要的是,这一切在缺少额外依赖时仍然能够运行。

于是,Pytuck 选择了一套接近 SQLAlchemy 2.0 的 Pythonic API,但没有要求使用者编写 SQL:

from typing import Type

from pytuck import (
    Column,
    PureBaseModel,
    Session,
    Storage,
    declarative_base,
    insert,
    select,
)

db = Storage(file_path="archive.pytuck")
Base: Type[PureBaseModel] = declarative_base(db)


class Entry(Base):
    __tablename__ = "entries"

    id = Column(int, primary_key=True)
    title = Column(str, nullable=False, index=True)
    category = Column(str, nullable=False)


session = Session(db)
session.execute(
    insert(Entry).values(
        title="沿着地平线",
        category="writing",
    )
)
session.commit()

entry = session.execute(
    select(Entry).where(Entry.title == "沿着地平线")
).first()

db.flush()
db.close()

模型负责描述数据,Session 负责组织对象的变化,select()insert()update()delete() 负责表达操作,Storage 决定数据最终落在哪里。对于小型项目,也可以改用 Active Record 风格,让模型直接完成常见的增删改查。

同一套模型,落入不同的容器

Pytuck 把数据操作与持久化后端拆开。目前同一套模型可以写入 Pytuck、JSON、JSONL、CSV、SQLite、DuckDB、Excel 与 XML。

这些格式并不是为了追求“支持得越多越好”。它们对应的是不同的去处:

  • 默认的 .pytuck 单文件适合随应用分发和本地持久化;
  • JSON 便于阅读、调试和手工检查;
  • JSONL 与 CSV 适合交换或归档;
  • SQLite 与 DuckDB 适合需要原生 SQL 路径或更大数据量的场景;
  • Excel 与 XML 则面向办公交付和标准化交换。

Pytuck、JSON、JSONL、CSV 和 SQLite 都可以在基础安装中使用;DuckDB、Excel、XML 与 orjson 加速被放在可选扩展里。这个边界是刻意保留的:第三方能力可以向外生长,却不能反过来成为核心读取、查询和写入的前提。

一个文件里的表与索引

默认的 Pytuck 引擎使用自定义的 PTK7 单文件格式。重新打开文件时,它会先恢复表结构与索引目录,记录内容在真正需要时才被读取。多表数据只改动其中一部分时,未变化的延迟加载数据块可以直接沿用,不必先把所有记录解码进内存再重新写回。

这个格式也支持可选加密。新写入的加密文件使用独立的加密与认证子密钥,并通过 HMAC-SHA256 检查完整性;不需要加密时,则不会为核心路径引入额外依赖。

我更在意的并不是创造一种神秘的新文件格式,而是让它承担几件朴素的事:单文件、可迁移、能保留 Python 类型,并且在受限环境中可以由标准库独立完成读写。

它刻意没有成为另一种东西

纯 Python 带来了可移植性,也划定了 Pytuck 的上限。

它适合单进程、中小规模、本地嵌入式的数据;万级以内的热点表是更舒适的范围。当单表接近十万条,或需求开始涉及高并发、复杂联表、分析计算和大型数据集时,SQLite、DuckDB、PostgreSQL 以及成熟 ORM 往往是更合适的选择。Pytuck 支持 Relationship 与预取,但不假装自己拥有完整的 SQL JOIN。

这是我希望项目始终保持诚实的一点:它不是“用 Python 重写一个更好的 SQLite”,也不是要与 SQLAlchemy 争夺正常的应用开发。它服务的是那些无法自然带上这些工具的地方。

仍在生长

写下这篇文章时,Pytuck 的公开版本是 1.4.0。它已经能够在八种存储引擎之间保持一套统一的模型与查询方式,并支持索引、关系预取、跨 Storage 关联、类型校验、数据迁移和可插拔后端。

但对我而言,继续维护它的方向并不是不断堆叠功能。比功能数量更重要的,是跨后端语义能否一致,单文件能否可靠地再次打开,数据格式能否长期演进,以及下一次把它放进一个不完整的 Python 环境时,它是否仍然不需要解释自己的依赖。

Pytuck 的名字最终指向的,正是这样一小块被妥善收起的数据空间。

BUILD / ENTRY

Pytuck:为受限 Python 留一座数据库

一个面向 Ren’Py 等受限环境的纯 Python 嵌入式数据库:默认零外部依赖,也保留模型、查询与多后端能力。