simonw/sqlite-chronicle

GitHub: simonw/sqlite-chronicle

利用 SQLite 触发器自动追踪表中行的插入、更新和删除时间与版本号的 Python 库。

Stars: 56 | Forks: 0

# sqlite-chronicle [![PyPI](https://img.shields.io/pypi/v/sqlite-chronicle.svg)](https://pypi.org/project/sqlite-chronicle/) [![更新日志](https://img.shields.io/github/v/release/simonw/sqlite-chronicle?include_prereleases&label=changelog)](https://github.com/simonw/sqlite-chronicle/releases) [![测试](https://static.pigsec.cn/wp-content/uploads/repos/cas/ce/ce733292a922c08274cf5a2096f8fa4cf01023bfa51a36ef6beecaaef371a9d9.svg)](https://github.com/simonw/sqlite-chronicle/actions?query=workflow%3ATest) [![许可证](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/sqlite-chronicle/blob/main/LICENSE) 使用 trigger 跟踪 SQLite 表中行的插入、更新或删除时间 ## 核心概念 创建一个 `_chronicle_{table_name}` 表来伴随常规表。常规表中的每一行在 chronicle 表中都有对应的行,用于记录其添加或最后更新的时间戳以及递增的版本号,所有这些都通过 SQLite trigger 来维护。这让你能够高效地识别自上次时间戳或版本以来,哪些行被添加、更新或删除了。 ## 安装 ``` pip install sqlite-chronicle ``` ## 命令行界面 你可以使用命令行界面为 SQLite 数据库中的特定表启用 chronicle,传入一个或多个表名: ``` sqlite-chronicle database.db table_1 table_2 ``` 要禁用 chronicle 跟踪,请使用 `--disable` 标志: ``` sqlite-chronicle database.db table_1 --disable ``` 你也可以使用 [uvx](https://docs.astral.sh/uv/concepts/tools/) 在未安装此工具的情况下直接运行它: ``` uvx sqlite-chronicle --help ``` ## Python API 此包暴露了几个用于配置和使用 chronicle 表的 Python 函数: ### enable_chronicle(conn, table_name) 该模块提供了一个函数:`sqlite_chronicle.enable_chronicle(conn, table_name)`,它执行以下操作: 1. 检查 `_chronicle_{table_name}` 表是否已存在。如果存在,则什么也不做。否则... 2. 创建该表,其具有与原表相同的主键列,外加整数列 `__added_ms`、`__updated_ms`、`__version` 和 `__deleted` 3. 在 chronicle 表中为原表中的每一行创建对应的新行,将 `__added_ms` 和 `__updated_ms` 设置为当前时间戳(以毫秒为单位),并将 `__version` 列的起始值设为 1,后续行依次递增 4. 在表上设置四个 trigger: - 一个 BEFORE INSERT trigger,它将现有行数据快照到 helper 表中,以支持 `INSERT OR REPLACE` 的变更检测 - 一个 AFTER INSERT trigger,它在 chronicle 表中创建一个新行,将 `__added_ms` 和 `__updated_ms` 设置为当前时间,并将 `__version` 设置为比该表当前最大版本号高 1。对于 `INSERT OR REPLACE`,它会与快照进行比较以检测数据是否实际发生了更改 - 一个 AFTER UPDATE trigger,它更新 `__updated_ms` 时间戳并递增 `__version` —— 但前提是该行中至少有一列发生了更改 - 一个 AFTER DELETE trigger,它更新 `__updated_ms`,递增 `__version` 并在 `deleted` 列中置为 `1` —— 但仅针对真正的删除,不包括 `INSERT OR REPLACE` 内部的隐式删除 如果表不存在,或者没有单一或复合主键,该函数将抛出 `sqlite_chronicle.ChronicleError` 异常。 请注意,一个表的 `__version` 是一个全局递增的数字,因此每次设置时,都会被设置为整个表当前的 `max(__version)` + 1。 最终生成的 chronicle 表如下所示: | id | __added_ms | __updated_ms | __version | __deleted | |-----|---------------|---------|--------|---------| | 47 | 1694408890954 | 1694408890954 | 2 | 0 | | 48 | 1694408874863 | 1694408874863 | 3 | 1 | | 1 | 1694408825192 | 1694408825192 | 4 | 0 | | 2 | 1694408825192 | 1694408825192 | 5 | 0 | | 3 | 1694408825192 | 1694408825192 | 6 | 0 | ### disable_chronicle(conn, table_name) 使用 `sqlite_chronicle.disable_chronicle(conn, table_name)` 移除表的 chronicle 跟踪。这将: 1. 删除 `_chronicle_{table_name}` 表 2. 移除所有相关的 trigger 3. 移除版本索引 如果禁用了 chronicle 则返回 `True`,如果该表不存在 chronicle 表则返回 `False`。 ``` import sqlite_chronicle # 禁用 dogs 表上的 chronicle result = sqlite_chronicle.disable_chronicle(conn, "dogs") if result: print("Chronicle disabled") else: print("No chronicle found") ``` ### is_chronicle_enabled(conn, table_name) 使用 `sqlite_chronicle.is_chronicle_enabled(conn, table_name)` 检查表是否启用了 chronicle 跟踪。 如果给定表存在 chronicle 表则返回 `True`,否则返回 `False`。 ``` import sqlite_chronicle if sqlite_chronicle.is_chronicle_enabled(conn, "dogs"): print("Chronicle is enabled for dogs") else: print("Chronicle is not enabled for dogs") ``` ### list_chronicled_tables(conn) 使用 `sqlite_chronicle.list_chronicled_tables(conn)` 获取所有启用了 chronicle 跟踪的表的列表。 ``` import sqlite_chronicle tables = sqlite_chronicle.list_chronicled_tables(conn) print(f"Chronicle is enabled for: {tables}") # 输出:Chronicle 已为以下表启用:['dogs', 'cats'] ``` ### upgrade_chronicle(conn, table_name) 此函数检测指定表先前是否已为其创建了旧版本的 chronicle 表和 trigger,如果是,它将把该表升级到最新的实现,同时保留现有时间戳和版本数据。 ### updates_since(conn, table_name, since=None, batch_size=1000) `sqlite_chronicle.updates_since()` 函数返回一个生成器,遍历 `Change` 对象的列表。 这些对象表示自 `since` 版本号以来表中行发生的更改;如果未提供 `since`,则表示自最初以来发生的更改。 - `conn` 是一个 SQLite 连接对象 - `table_name` 是一个字符串,包含要获取更改的表名 - `since` 是一个可选的整数版本号 —— 如果未提供,则返回所有更改 - `batch_size` 是一个内部细节,控制一次从数据库返回的行数。你不需要更改它,因为该函数实现了自己的内部分页。 从生成器返回的每个 `Change` 类似于这样: ``` Change( pks=(5,), added_ms=1701836971223, updated_ms=1701836971223, version=5, row={'id': 5, 'name': 'Simon'}, deleted=False ) ``` `Change` 是一个 dataclass,具有以下属性: - `pks` 是该行主键值的元组 —— 对于普通主键,这是一个只有单个项的元组;对于复合主键,则包含多个项 - `added_ms` 是行被添加时的时间戳(以毫秒为单位) - `updated_ms` 是行被最后更新时的时间戳(以毫秒为单位) - `version` 是该行的版本号 —— 你可以将其用作 `since` 值来获取该点之后的更改 - `row` 是一个字典,包含该行的当前值 —— 如果该行已被删除,则这些值将为 `None`(主键除外) - `deleted` 如果该行未被删除则为 `0`,如果已被删除则为 `1` 每次调用此方法时,你都应该记录下你看到的最后一个 `version` 号,这样你就可以在未来的调用中将其作为 `since` 值传入,以获取该点之后发生的更改。 请注意,如果在两次调用此函数期间某一行发生了多次更新,你仍然只会看到该行对应的一个 `Change` 对象 —— `updated_ms` 和 `version` 将反映最近一次更新。 ## 实现说明 - 完整支持 `INSERT OR REPLACE` 和 `INSERT ... ON CONFLICT DO UPDATE` (UPSERT)。如果 `INSERT OR REPLACE` 向现有行写入了相同的数据,则不会发生版本提升。 - 如果某行被删除后又被重新插入相同的主键,`__added_ms` 将重置为重新插入的时间(并且 `__deleted` 翻转回 `0`)。这会将“取消删除”视为一次全新添加,而不是原始行历史的延续。 - 目前不支持对作为记录主键一部分的列进行更新。 ## 潜在应用 Chronicle 表可用于高效回答“自上次检查以来,哪些行已被插入、更新或删除”的问题 —— 通过查看 `version` 列,该列具有索引,可以快速回答此问题。 这有许多潜在的应用,包括: - 同步和复制:其他数据库可以“订阅”表,跟踪它们上次刷新副本的时间,并仅请求自上次以来发生更改的行 —— 并删除已标记为已删除的行。 - 索引:如果你需要更新 Elasticsearch 索引或向量数据库的 embedding 索引等,你可以仅针对自上次运行以来发生更改的记录执行此操作 —— 另请参阅[非规范化查询引擎设计模式](https://2017.djangocon.us/talks/the-denormalized-query-engine-design-pattern/) - 增强:[datasette-enrichments](https://github.com/datasette/datasette-enrichments) 需要持久化类似于“每个地址列都应被地理编码”的状态信息 —— 然后让一个增强任务每 X 秒运行一次,查找新插入或更新的行并仅对它们进行增强。 - 向人们展示自上次访问以来发生了什么变化 —— 比如“自昨天以来已有 52 行被更新,16 行被删除”之类的事情。 ## SQL schema 示例 这是一个 SQL schema 示例,展示了为 `dogs` 表创建的 `_chronicle_dogs` 表及其 trigger:
标签:Python, SQLite, 数据变更跟踪, 数据同步, 数据库工具, 无后门, 触发器, 逆向工具