Jepsen 测试 ParticleDB 1.1.4 至 1.1.18.11.rc-22:发现 96 个问题
Jepsen: ParticleDB 1.1.4
Jepsen 对面向 AI 负载的多模型数据库 ParticleDB 1.1.4 至 1.1.18.11.rc-22 进行测试,共发现 96 个问题,多数只需单节点单客户端、无故障注入即可复现。
译文尚不完整,完整内容请切换到原文。
ParticleDB 是一款为 AI 工作负载设计的多模型数据库。我们测试了 1.1.4 到 1.1.18.11.rc-22 的各个版本,发现 ParticleDB 所宣称的“完整 ACID”有些言过其实。我们列出了 ParticleDB 中的 96 个问题,从平淡无奇的(崩溃、死锁、数据丢失等)到辛辣有趣的(部分不可见且无法删除的行、WHERE 4 = 5、被删除的表以扭曲的形式重新出现、列混淆、字符串变成整数等等)。大多数问题只需要单台服务器、单个客户端,且无需注入故障即可复现。到 1.1.18.11.rc-22 版本时,除十个问题外,其余问题似乎都已解决。然而,ParticleDB 在默认的 occ 事务模式下,于正常运行期间仍会丢失已提交的写入,并且在最强的 serializable 设置下仍允许内部异常和丢失更新。ParticleDB 的开发商 Orkes 此后发布了 1.1.19-rc.13 版本;他们声称该版本解决了本报告中讨论的所有遗留问题。本研究由 Orkes, Inc. 资助,并按照 Jepsen 伦理政策进行。
背景
现代应用程序常常将状态分散到各种系统中:数据库、缓存、队列、数据仓库、搜索索引等等。协调这些系统颇具挑战:缓存常常是过期的,分析平台可能比主数据库滞后数天,搜索索引可能丢失数据,而跨异构系统的事务性更新仍然是一个开放的研究问题。
ParticleDB1 旨在将这些工作负载统一到单一引擎中,并特别关注 AI 代理和应用程序。它提供事务性 SQL、键值存储、向量搜索、实时分析以及称为 Particles 的用户定义函数。2 为了与现有系统集成,客户端可以通过多种接口连接,包括 Postgres 和 Redis 协议,以及它自己的 gRPC 和 HTTP2 API。ParticleDB 用 Rust 从头编写,声称比 Postgres 快几个数量级,同时保持相同的安全特性。
ParticleDB 由 Orkes 构建,用于支持其工作流编排平台 Conductor。Conductor 最初将数据存储在 Redis 和 Postgres 中,但为了提高性能,Orkes 选择编写自己的数据库。ParticleDB 1.0.0 提供了专门针对 Conductor 查询的有限功能集,并作为其部分 Conductor 集群的后端数据库部署。一些 Orkes 客户也纷纷效仿。为了准备更广泛的发布,Orkes 对 ParticleDB 进行了重大重构,增加了自定义网络协议、更多数据类型、额外的 OLAP 查询等。在 Orkes 及其客户继续运行 1.0.0 的同时,他们在 particledb.ai 上软发布了重构后的版本。Orkes 委托 Jepsen 测试这些版本的 ParticleDB,从 1.1.4 开始。
虽然 ParticleDB 可以作为单节点运行,但它也提供了使用 Raft 共识协议的复制集群。此复制功能仅在企业云部署中可用。Orkes 提供的构建版本不支持复制,因此本报告仅涵盖单节点行为。
安全性
ParticleDB 的主页宣称 ParticleDB 已通过 TLA+ 证明“形式化验证”。然而,在我们的合作期间,并没有可用的 TLA+ 规范。目前不清楚可能证明了哪些性质,也不清楚他们描述的是否是实际构建出的 ParticleDB。
ParticleDB 提供了三种策略来同步其预写日志(WAL)。sync 模式在每次提交时调用 fsync,groupsync 将多个事务批量合并为单次 fsync,而 nosync 则完全跳过 fsync。事务引擎文档将 sync 描述为提供比 groupsync “更强的持久性”。另一方面,particledb start --help 表示 groupsync 模式确保“COMMIT 等待刷盘”,Orkes 的内部运维指南也持相同看法:groupsync 提供“与 sync 相同的持久性”。
ParticleDB 的主页自豪地宣称“完整 ACID。159K TPS。”然而,紧随其后的文字却写着“快照隔离”,而快照隔离并非完全隔离。
大多数 SQL 数据库使用 SET TRANSACTION ISOLATION LEVEL 语句来控制事务的并发语义。然而,ParticleDB 忽略或拒绝了这些语句。在某些情况下,它指示用户选择 READ COMMITTED,这是一个相对较弱的隔离级别。在其他情况下,它接受了针对不同隔离级别的请求,却静默忽略它们(尽管文档中有相反的说法)。
相反,事务隔离由启动时传入的 --txn-mode 设置控制。事务引擎文档描述了四种模式:
| 模式 | 行为 | 适用场景 |
|---|---|---|
fast |
无锁;写时提交 | 批量加载、单写入者基准测试 |
occ |
覆盖层 + 提交时 OCC 验证(默认) | 通用 OLTP——在并发下安全 |
zero-copy-occ |
分区固定 OCC;单分区事务跳过跨分区协调 | 分区亲和型工作负载上的高吞吐 OLTP(TPC-C、键值) |
table-2pl |
首次访问时加排他表锁 | 最严格的串行化,语义简单 |
这张表难以解读。虽然最佳实践文档警告用户应“刻意选择 --txn-mode”,因为“它对正确性和吞吐量特性的改变比 SQL 文本更大”,但文档并未解释这些模式实际保证了什么。fast 模式显然旨在提供较弱的隔离,但那是读原子性、读未提交,还是其他什么?occ 模式同样不明确。“提交时 OCC 验证”可以描述从读未提交到强可串行化的广泛一致性模型,但“在并发下安全”暗示它可能是可串行化的。zero-copy-occ 模式描述了一种性能优化,其安全性可能与 occ 相当,但它并未被描述为“在并发下安全”——这引出了一个问题:它在哪些方面不安全?最后,如果 table-2pl 提供“最严格的串行化”,那么 occ 是否是某种较弱的隔离级别?
CLI 参考页面包含另一张表,其中将 occ 和 table-2pl 描述为“Jepsen 安全”,而 fast 则不是“Jepsen 安全”,但并未解释它们的安全性质。由于 Jepsen 是依据文档和营销声明来评估安全性的,因此不清楚应如何解读这些术语。3
运行 particledb start --help 会提供以下表格:
| 模式 | 描述 |
|---|---|
fast |
无锁,最大 OLTP 吞吐量(并发下不安全) |
occ |
表锁 + 提交时验证(对大多数 OLTP 安全) |
row-2pl |
occ 的别名 |
zero-copy-occ |
分区固定 OCC(最高吞吐量的安全模式) |
table-2pl |
排他表锁,完全串行化(最严格) |
serializable |
table-2pl 的别名 |
serializable-ssi |
自适应 SSI(Cahill 2008),PL-3 采用乐观语义(v1 脚手架——见 SSI_ADAPTIVE_DESIGN.md) |
乐观并发控制(OCC)的要点通常在于避免加锁,因此提到表锁以及一个 row-2pl4 别名令人意外。此外,“对大多数 OLTP 安全”削弱了 occ“在并发下安全”的说法;它暗示有些 OLTP 工作负载并不安全!
Orkes 解释说这份文档有误:occ 实际上是乐观的,而 row-2pl 别名是为了与早期的两阶段锁机制保持兼容。他们还提供了一份未公开的运维指南,详细描述了各种事务模式。这份文档难以解读,但确实指定了以下一致性模型:
| 模式 | 一致性模型 |
|---|---|
fast |
未知 |
occ-v3(又称 occ、occ-v2、row-2pl) |
快照隔离 |
zero-copy-occ(又称 zocc) |
快照隔离 |
serializable-ssi(又称 ssi) |
可串行化 |
table-2pl(又称 serializable) |
强可串行化 |
各方对默认事务模式和 WAL 同步模式的描述不一致,但总体而言,occ-v3 和 groupsync 似乎是默认值。
测试设计
我们使用 Jepsen 测试库为 ParticleDB 编写了一套测试套件。我们在独立的 Debian 13 节点上运行了 ParticleDB 1.1.4 至 1.1.18.11.rc-22 版本,既在容器中也在 EC2 虚拟机上。我们的测试框架可以通过杀死 ParticleDB 来模拟进程崩溃,使用 LazyFS 模拟未 fsync 写入的丢失,以及截断或损坏磁盘文件。我们使用每种事务模式和全部三种 WAL 同步模式运行了 ParticleDB。
我们首先尝试了 ParticleDB 的官方 Java SDK。虽然文档表明该库可作为 Maven 依赖使用,但 JAR 和源代码实际上都不可用。于是我们改用官方 Postgres JDBC 客户端 42.7.11 版本,后来又增加了对 ParticleDB Java SDK 1.0.0 至 1.0.2 版本的支持。
工作负载
我们使用了来自其他 SQL 数据库的若干 Jepsen 工作负载,并专门为 ParticleDB 设计了新的工作负载。
rw 工作负载执行随机生成的事务,涉及对由唯一整数键标识的整数值的读写。我们将每个值存储在单行中,并通过主键和无索引的二级键访问它们。键和值被编码为多种数据类型,包括整数、小数和字符串。我们假设每个事务内写入发生在读取之后,且各个键是顺序一致的,并利用这些假设推断对版本顺序和事务依赖图的约束。随后,Jepsen 的事务检查器 Elle 在该依赖图中搜索环,以及各种非环现象,如 G1a(中止读取)。
append 工作负载同样对由唯一整数键标识的行执行随机生成的事务,但值是整数列表。事务可以按键读取其中一个列表,或按键向列表追加一个唯一整数。我们将每个列表编码为逗号分隔的 text 字段,并使用 SQL concat 向其追加。行可以通过主键或无索引的二级键选择,也可以通过连接到间接表选择。在此基础上,我们使用 Elle 推断版本顺序和依赖图,并搜索各种环和非环现象。
当这些工作负载在单个事务内暴露出不一致性违规时,我们设计了一个internal工作负载来对内部一致性进行压力测试。该工作负载在一个简单的键值表上执行一系列随机生成的事务,涉及INSERT、UPDATE、DELETE和SELECT语句,并验证在每个单独的事务内,读取操作观察到了先前的写入。
为了调查internal工作负载中诱人的异常现象,我们设计了compare工作负载。该工作负载使用Clojure的test.check生成测试库来生成随机表和简单的SQL语句,然后针对ParticleDB和PostgreSQL执行这些语句。如果ParticleDB的行为与PostgreSQL不等价,比较工作负载会搜索一个更简单的语句序列,以重现相同的差异。尽管我们的SQL生成器相对简单——只有几种基本数据类型、字面量、算术、比较、布尔逻辑、事务以及一些简单的LIMIT、OFFSET和ORDER BY子句——但它被证明非常富有成效。
借鉴这些成功经验,我们设计了一个混合internal-sim工作负载,以在更复杂的事务中搜索内部一致性的违规情况。该工作负载生成一组随机表,然后在其上并发执行随机事务。随后它独立检查每个事务,以确保其每条语句观察到的结果与之前的语句一致。例如,如果一个事务执行了DELETE ... WHERE 2 < c,然后观察到一行具有c = 3,我们就可以证明该事务在内部不一致。
结果
我们发现了ParticleDB的96个问题,下面按几个类别呈现。除了三个问题外,所有这些问题都发生在健康的单节点ParticleDB实例中。忙碌的读者可能希望跳到讨论部分,那里总结了我们的发现。
更多结果见附录A:
可用性与性能
在正常操作下,ParticleDB运行缓慢、死锁,并毫无预警地崩溃。
使用table-2pl时的糟糕性能(#62)
在ParticleDB 1.1.4至1.1.18.4中,--txn-mode table-2pl慢到基本上无法使用。虽然有些事务可能在几毫秒内完成,但大多数需要10、20甚至30秒,然后失败。
例如,以这次测试运行为例,单节点运行1.1.18.4,事务模式为table-2pl,WAL模式为groupsync。在十个并发客户端的情况下,大多数事务阻塞数十秒,然后抛出could not serialize access due to concurrent update或IO错误。
高延迟(#62)似乎在1.1.18.6中有所改善,该版本可以在至少几秒内以低于100毫秒的延迟执行事务。
神秘崩溃(#81)
虽然 1.1.18.6 改善了延迟,但它也频繁崩溃。崩溃会在各种事务模式、WAL 模式和客户端接口下自发出现;没有明显原因。节点会运行几百到几千秒后退出。没有打印任何错误消息。这些崩溃(#81)在 1.1.18.11.rc-18 中不再出现。
两个事务后死锁(#27)
在 ParticleDB 1.1.18.8 中,向一个健康的单节点部署提交少至两个并发事务就可能导致其死锁。使用 Postgres 和 ParticleDB 接口时都会发生这种情况。节点会记录类似 [DEADLOCK] 1 cycles detected at SystemTime ... 的错误消息,以及一个空回溯。在某些情况下,新客户端可以连接并执行事务;在另一些情况下,新客户端也会无限期挂起。这可能反映了多个死锁缺陷。
在使用 append 工作负载时,版本 1.1.18.9 通常无法运行超过 45 秒而不锁死。版本 1.1.18.11.rc-1 可以可靠运行两分钟而不锁死,我们宣布该问题(#27)已解决。
使用 table-2pl 时死锁(#71)
虽然版本 1.1.18.11.rc-1 修复了最频繁的死锁,但在使用 table-2pl 运行时,我们继续在 1.1.18.11.rc-6 及之前版本中发现死锁。一个原本健康的 ParticleDB 节点会执行事务约 30 秒,然后永远卡住。节点会向 #27 记录类似的死锁错误消息,并且所有使用 ParticleDB 客户端的请求都会在默认 100 秒超时后抛出 PDB JDBC: null。这些死锁(#71)在 1.1.18.11.rc-7 中似乎已解决。
损坏的功能
Orkes 的公开文档声称广泛支持 Postgres 功能,内部运维指南也同意:
该引擎原生使用 PostgreSQL v3 线协议——任何使用 pgjdbc / libpq / psycopg2 / pq 的东西都可以不变地连接。
BEGIN、COMMIT、ROLLBACK、预处理语句、参数化查询和扩展查询流水线都按预期工作。
这些说法有些过于乐观。ParticleDB 缺少对大量 Postgres 功能的支持,其他功能则严重损坏。
CASE 内无聚合(#20)
在 ParticleDB 1.1.4 到 1.1.18.4 中,CASE 表达式内不支持像 COUNT(*) 这样的聚合表达式。
SELECT
CASE
WHEN COUNT(*) = 2
THEN 'CASE_COUNT_STAR_OK'
ELSE 'CASE_COUNT_STAR_BAD'
END
FROM t;这会抛出 Error: cannot evaluate wildcard expression。ParticleDB 在 1.1.18.6 版本中获得了对该功能(#20)的支持。
不支持的幂运算(#28)
ParticleDB 1.1.4 到 1.1.18.8 不支持 Postgres 幂运算符:
root=> SELECT 2 ^ 3;
ERROR: unsupported binary operator: PGExp版本 1.1.18.9 添加了对 ^ 运算符(#28)的支持。
预处理语句的畸形响应(#2)
在 ParticleDB 1.1.4 到 1.1.18.9 中,任何使用 Postgres JDBC 驱动程序执行预处理语句的尝试都会导致类似 Execution error (NoSuchElementException) at java.util.ArrayDeque/removeFirst 的异常。由于 JDBC 驱动程序在最初几次调用 execute() 后会自动切换到预处理语句,这使数据库基本上无法使用。作为变通方法,我们禁用了客户端对预处理语句的使用。
预处理语句使用 Postgres 的扩展查询协议执行,该协议分几个(可能可重用的)阶段进行:Parse、Bind、Execute 等。Orkes 认为 ParticleDB 可能未能为其中一个阶段返回响应,或返回了多余的消息,导致客户端队列下溢。到版本 1.1.18.11.rc-1,此问题(#2)似乎已修复。
大多数 Postgres 查询失败(#83)
在 ParticleDB 1.1.18.11.rc-7 至 1.1.18.11.rc-19 中,许多使用 Postgres 接口的查询会抛出
Bad value for type long: some_column。这使数据库在很大程度上无法使用。
CREATE TABLE ints (a int);
SELECT * FROM ints;
-> Bad value for type long : aOrkes 认为此问题(#83)已在 1.1.19-rc.12 中修复。
无字段的元组 (#111)
在 1.1.18.11.rc-18 和 rc-19 中,使用 Postgres 接口的查询偶尔会抛出如下错误:
Received resultset tuples, but no field structure for them。5 例如,以 这个测试为例,版本为 1.1.18.11.rc-19,无故障,使用新的
strict-serializable 事务模式,以及 WAL 模式
sync。它执行了如下事务:
BEGIN;
SELECT * FROM t_38;
INSERT INTO u_38 (d, a, c, b) VALUES
(null, null, false, "oudt1THxi");
INSERT INTO t_38 (d, b) VALUES
(false, "BCczDbueXLz9zXnpgbdCeFbC5y6XSEys3");
COMMIT;该错误抱怨结果集元组没有字段结构,表明 ParticleDB 的 Postgres 协议状态机存在错误。虽然 Jepsen 尚未测试此版本,但 Orkes 报告此问题(#111)已在版本 1.1.19-rc.13 中修复。
Postgres 认证损坏 (#1)
Postgres 线路协议通常使用用户名和密码对客户端进行认证。ParticleDB 有一个 --pg-password
选项,但 1.1.4 至 1.1.18.8 缺少任何设置用户名的选项。部分
文档指出默认用户名为
admin,而其他
页面
则说是 particledb。两者似乎都不起作用;唯一
选项是使用 --no-auth 完全禁用认证。版本 1.1.18.11.rc-1 引入了
--pg-username 选项(#1)。
Java 客户端无法执行 事务(#3)
官方 ParticleDB Java 客户端 1.0.0 版拒绝执行事务的每一次尝试。任何开始 JDBC 事务的调用,
或执行 BEGIN 语句的调用,都会导致错误
消息:
PDB wire error 1064: SQL plan error: transaction/session statements do not produce a query plan。
ParticleDB 1.1.4 可以通过其 Postgres 接口执行事务,但使用官方 Java SDK 基本上不可能。 此问题(#3)似乎 已由 ParticleDB 1.1.15 解决。
谓词
ParticleDB 难以解释谓词。WHERE
子句无法匹配本应匹配的行,或匹配了意外或不存在的行。这些问题在单服务器、单
客户端且无故障的情况下发生。
SELECT 忽略
WHERE 子句(#40)
在 ParticleDB 1.1.4 至 1.1.18.9 中,不带表的 SELECT 查询
接受 WHERE 子句,但会忽略
它。
root=> SELECT 5 WHERE FALSE;
5
---
5
(1 row)这应返回零行。版本 1.1.18.11.rc-1 似乎 已解决此问题(#40)。
WHERE 1 < (a + 2)(#47)
版本 1.1.1.18.9 引入了谓词的新问题:在事务内,执行足够
复杂浮点运算的 WHERE 子句会静默地无法匹配任何行。
例如:
root=> CREATE TABLE t (a real);
root=> INSERT INTO t (a) VALUES (3);
root=> BEGIN;
root=*> SELECT * FROM t WHERE 1 < (a + 2);
a
---
(0 rows)同样的问题也适用于 SELECT、UPDATE
和 DELETE:像
WHERE (a * 2) IS NOT NULL 这样的谓词会静默地无法匹配。
然而,像 WHERE a IS NOT NULL 这样更简单的谓词会
正确匹配。如果列是整数,该效应就会消失。
此外,一些在事务外会抛出错误的谓词在事务内会静默地匹配零行,表明这些谓词根本没有被求值:
root=> SELECT * FROM t WHERE 1 < (5.0 % 3);
ERROR: internal error
root=> BEGIN;
root=*> SELECT * FROM t WHERE 1 < (5.0 % 3);
a
---
(0 rows)此问题(#47) 似乎已由 1.1.18.11.rc-1 解决。
WHERE 4 = 5
(#18)
ParticleDB 1.1.4 至 1.1.17 激进地将
WHERE something = x 解释为对
x 的主键查找,无论 something 的值如何。
root=> CREATE TABLE t
(a int primary key, b int);
root=> INSERT INTO t VALUES (4, 5), (5, 6);
root=> SELECT * FROM t WHERE b = 4;
a | b
---+---
4 | 5ParticleDB 会忽略包含 b = 5 的行,而偏向
a = 5:
root=> SELECT * FROM t WHERE b = 5;
a | b
---+---
5 | 6
(1 row)你可以对不存在的列编写谓词:
root=> SELECT * FROM t
WHERE definitely_not_a_column = 4;
a | b
---+----
4 | 5
(1 row)或者询问是否 4 = 5:6
root=> SELECT * FROM t WHERE 4 = 5;
a | b
---+----
5 | 6
(1 row)到版本 1.1.18.4,ParticleDB 已认识到并非每个相等 比较都涉及主键(#18)。
事务中的 SELECT
返回无意义结果(#35)
在 ParticleDB 1.1.4 至 1.1.18.8 版本中,事务内 SELECT 的行为令人十分困惑。设想一个客户端连接到全新的单节点 ParticleDB 1.1.18.8 实例。它创建了一个简单的整数表,包含两行 1 和 2,并开启一个事务:
root=> CREATE TABLE t (a INT UNIQUE);
root=> INSERT INTO t VALUES (1), (2);
root=> BEGIN;
root=*> SELECT * FROM t;
a
---
1
2
(2 rows)到目前为止一切正常。但现在考虑:
root=*> SELECT * FROM t WHERE a = 1;
a
---
2
(1 row)
root=*> SELECT * FROM t WHERE a = 3;
a
---
2
(1 row)
root=*> SELECT * FROM t WHERE a IN (1, 2);
a
---
2
(1 row)
root=*> SELECT * FROM t WHERE FALSE;
a
---
2
(1 row)人们可能会认为每个带谓词的 SELECT 都会从所请求的表中返回单行——也许是最新插入的行。但事实并非如此,因为许多求值为 TRUE 的谓词什么也没返回。
root=*> SELECT * FROM t WHERE TRUE;
a
---
(0 rows)
root=*> SELECT * FROM t WHERE 3 = 3;
a
---
(0 rows)这一行为(#35)似乎在 ParticleDB 1.1.18.9 中得到了改善。
约束
ParticleDB 经常违反 NOT NULL 或 UNIQUE 等约束,即使只有一个服务器、一个客户端且没有故障。
非空约束允许空值(#8)
虽然 ParticleDB 允许用户为列添加 NOT NULL 约束,但 1.1.4 至 1.1.17 版本忽略了它们,导致出现奇怪的状态。
root=> CREATE TABLE t (a BOOLEAN NOT NULL);
root=> INSERT INTO t (a) VALUES (NULL);
INSERT 0 1虽然结果行在某种意义上存在,但它部分不可见。
root=> SELECT * FROM t;
a
---
(0 rows)
root=> SELECT COUNT(*) FROM t;
COUNT
-------
1
(1 row)
root=> SELECT COUNT(*) FROM t WHERE a IS NULL;
COUNT
-------
0
(1 row)在 1.1.18.4 版本中,ParticleDB 开始强制执行空值约束(#8)。
唯一约束允许重复(#9)
同样,虽然 ParticleDB 允许用户为列添加唯一约束,但 1.1.4 至 1.1.18.4 版本直接忽略了它们。
root=> CREATE TABLE t (a boolean UNIQUE);
root=> INSERT INTO t (a) VALUES (TRUE);
INSERT 0 1
root=> INSERT INTO t (a) VALUES (TRUE);
INSERT 0 1
root=> SELECT * FROM t;
a
---
t
t
(2 rows)在 1.1.18.6 版本中,ParticleDB 部分强制执行了唯一约束(#9)。
重复主键(#29)
尽管 1.1.18.6 使 UNIQUE 约束在某种程度上更加健壮,但 1.1.4 至 1.1.18.8 允许一个 UPDATE 产生多行具有相同主键的行。
root=> CREATE TABLE t (id text PRIMARY KEY,
desc text);
root=> INSERT INTO t VALUES ('a', 'a');
root=> INSERT INTO t VALUES ('b', 'b');
root=> UPDATE t SET id = 'c';
UPDATE 2按主键选择行只返回两行中的一行,但其他谓词可能返回两行:
root=> SELECT * FROM t WHERE id = 'c';
id | desc
----+------
c | b
(1 row)
root=> SELECT * FROM t WHERE id LIKE 'c';
id | desc
----+------
c | a
c | b此问题(#29)似乎已在 1.1.18.9 中修复。
空主键(#57)
根据 SQL 标准,主键不能为空。这符合直觉,因为 NULL 表示未知值。ParticleDB 不允许为主键插入空值。然而,1.1.4 至 1.1.18.11.rc-1 版本非常乐意将一行(或多行!)的主键更新为 NULL。
root=> CREATE TABLE t (a int PRIMARY KEY);
root=> INSERT INTO t (a) VALUES (3), (4);
root=> UPDATE t SET a = NULL;
root=> SELECT * FROM t;
a
---
(2 rows)ParticleDB 1.1.18.11.rc-3 拒绝了将主键设置为 NULL 的尝试(#57)。
主键与已删除行的冲突(#82)
在 ParticleDB 1.1.18.4 至 1.1.18.11.rc-11 中,一个事务删除一行,然后插入具有相同主键的新行,会因主键约束冲突而中止。例如:
CREATE TABLE t (a int PRIMARY KEY);
INSERT INTO t (a) VALUES (7);
BEGIN;
DELETE FROM t;
INSERT INTO t (a) VALUES (7);
COMMIT;第二个 INSERT 会抛出 PDB wire error 1064: SQL execution error: duplicate key value violates unique constraint (key=7),但仅在使用 ParticleDB 接口时——Postgres 接口不受影响。Orkes 在 1.1.18.11.rc-9 版本中部分解决了此问题(#82),并在 1.1.18.11.rc-18 中修复了该问题的另一种情况。
与现存行缺乏主键冲突(#90)
在 ParticleDB 1.1.18.11.rc-9 中,与其他事务并发执行的事务可以读取具有某主键的行,然后成功插入具有相同主键的行。例如,看这个 internal-sim 测试,它创建了一个包含两个整数列的表:
CREATE TABLE u_14 (b int, a int PRIMARY KEY);一个事务运行了 SELECT * FROM u_14 并观察到若干行,包括 {:b 1020026655, :a -1}。然后它运行了 INSERT INTO u_14 (b, a) VALUES (-27, -1),这应该与现存的主键冲突,但却成功了。它接着再次运行 SELECT * FROM u_14,观察到了新插入的行,但没有观察到先前存在的行。这违反了内部一致性。
截至 1.1.18.11.rc-19 版本,我们不再观察到这些内部一致性违规(#90)。
连接
ParticleDB 在表连接方面表现不佳。以下问题在单服务器、单客户端且无故障的情况下出现。
无法将整数与浮点数连接(#58)
即使已经认同 2.0 = 2,ParticleDB 1.1.4 到 1.1.18.9 仍拒绝在 int 和 real 列上连接两个表:
root=> CREATE TABLE t (a real);
root=> CREATE TABLE u (a int);
root=> INSERT INTO t (a) VALUES (2.0);
root=> INSERT INTO u (a) VALUES (2);
root=> SELECT t.a FROM t JOIN u ON u.a = t.a;
ERROR: join key must be integer type像 Postgres 这样的数据库在这里会返回单行,因为 2.0 = 2。ParticleDB 在 1.1.18.11.rc-1 中获得了将浮点数与整数连接的能力(#58)。
列名不正确(#54)
由于两个表可以拥有同名列,SQL 数据库通常同时返回表名和列名,以便你分辨哪一列属于哪个表。ParticleDB 的 ParticleDB 和 Postgres 两种接口都忽略了返回表名。相反,1.1.4 到 1.1.18.11.rc-1 版本将连接表中的列 a 重命名为 right.a、right.right.a 等。考虑以下在两个表 t 和 u(两者都有一个整数列 a)之间的简单连接:
SELECT * FROM t FULL OUTER JOIN u
ON t.a = u.a;在 Postgres 中,这会产生如下行:
[{:t/a 1, :u/a 1.0}
{:t/a 2, :u/a 2.0}]这里表达式 :t/a 表示表 t 的列 a。在 ParticleDB 中,同一查询返回:
[{:a 1, :right.a 1.0}
{:a 2, :right.a 2.0}]表名完全缺失,并且 ParticleDB 将列 a 的名称改成了 right.a。期望典型 SQL 行为的程序在尝试读取不存在的列时会抛出错误,或者可能静默读取空值。
ParticleDB 1.1.18.11.rc-3 不再篡改列名,但数据库仍然省略表名。由此产生的名称冲突破坏了信息。
[{:a 1}
{:a 2}]虽然 Jepsen 尚未有机会测试此版本,但 Orkes 声称此问题(#54)已在 1.1.19-rc.13 版本中解决。
强制转换为字符串(#76)
在 ParticleDB 1.1.4 到 1.1.18.11.rc-19 中,连接操作会在 ParticleDB 的 Postgres 接口中将某些值强制转换为字符串。例如,考虑以下连接:
CREATE TABLE t (a int);
CREATE TABLE u (a real);
INSERT INTO u (a) VALUES (3.2);
INSERT INTO t (a) VALUES (5);
SELECT u.a FROM t CROSS JOIN u;使用 ParticleDB 接口时,这产生数字 3.2。使用 Postgres 接口时,它产生字符串 "3.2"。此问题(#76)似乎在 1.1.18.11.rc-21 中已修复。
缺失行(#59)
在 ParticleDB 1.1.4 到 1.1.18.11.rc-1 中,将非空表与空表进行外连接时常常返回零行,即使查询本应产生非空表中的所有行。例如:
root=> CREATE TABLE t (a int);
root=> CREATE TABLE u (a int);
root=> INSERT INTO t (a) VALUES (1), (2);
root=> SELECT t.a AS t, u.a AS u
FROM t LEFT JOIN u ON u.a = t.a;
t | u
---+---
(0 rows)到 1.1.18.11.rc-3 版本,ParticleDB 在与空表连接时正确返回了行(#59)。
列混淆(#77)
真实数据库中的许多表包含同名列,如 id 或 created_at。当被要求连接两个这样的表时,ParticleDB 1.1.4 到 1.1.18.11.rc-6 变得严重混乱,即使所有列都完全限定。例如:
root=> CREATE TABLE t (a text);
root=> CREATE TABLE u (a text);
root=> INSERT INTO t VALUES ('this'), ('that');
root=> INSERT INTO u VALUES
('is not the right column');
root=> SELECT t.a AS ta, u.a AS ua FROM t
CROSS JOIN u WHERE (u.a = 'this');
ta | ua
------+-------------------------
this | is not the right column
(1 row)这本来不应匹配任何内容——表 u 中没有满足 u.a = 'this' 的行。相反,ParticleDB 匹配了表 t 中满足 t.a = 'this' 的一行,然后将其与 u 中的所有行进行交叉连接。更糟的是,1.1.4 到 1.1.18.9 似乎交换了输出列,产生了以下表:
ta | ua
-------------------------+------
is not the right column | this如果列 t.a 与 u.a 类型不同,ParticleDB 会尝试将类型强制转换为兼容形式,产生奇怪的错误消息或令人愉快的怪异结果。1.1.18.11.rc-7 版本在连接包含相同列名的表时混乱程度大大降低(#77)。
僵尸行
ParticleDB 经常出现无法删除或更新的行,或者这些行在删除后以某种(可能是损坏的)形式持续存在。这些问题在单服务器、单客户端且无故障的情况下出现。
全面回忆(#26)
在 ParticleDB 1.1.17 至 1.1.18.8 中,似乎无法在事务内删除行。例如:
root=> CREATE TABLE t (a boolean);
root=> INSERT INTO t (a) VALUES (TRUE);
root=> BEGIN;
root=*> DELETE FROM t;
DELETE 0
root=*> COMMIT;
root=> SELECT * FROM t;
a
---
t
(1 row)我们未能找到允许删除记录的谓词(#26)。 版本 1.1.18.9 允许事务在大多数情况下7删除行。
Memento Delere(#45)
1.1.18.9 解决了问题 #26,但已删除的行仍在某种程度上残留。虽然事务无法直接选择已删除的行,但可以通过聚合函数和各种谓词观察到它们。
root=> CREATE TABLE t (a boolean);
root=> INSERT INTO t (a) VALUES (TRUE);
root=> BEGIN;
root=*> DELETE FROM t;
DELETE 1
root=*> SELECT COUNT(*) FROM t WHERE a = TRUE;
COUNT
-------
1
(1 row)
root=*> SELECT COUNT(*) FROM t WHERE a = FALSE;
COUNT
-------
0
(1 row)从某种意义上说,该行必定仍然存在,因为这些聚合查询可以通过谓词观察到它的值。然而,尝试直接读取该行却返回空:
root=*> SELECT a FROM t WHERE a = TRUE;
a
---
(0 rows)版本 1.1.18.11.rc-1 不再允许已删除记录的残留痕迹在聚合查询中持续存在(#45)。
活死整数之夜(#23)
在 1.1.4 至 1.1.17 版本中,更新整数列可能导致行无法删除。例如:
root=> CREATE TABLE t (i int);
root=> INSERT INTO t VALUES (0);
root=> UPDATE t SET i = 1.5;
root=> DELETE FROM t;
DELETE 0
root=> SELECT * FROM t;
i
---
1
(1 row)虽然该行似乎包含整数 1,但该整数是特殊的,因为 UPDATE 语句提供的值是 1.5。该行无法被删除。它确实出现在 SELECT 中,并且显然等于 1,而不是 1.5。奇怪的是,使用相同的 1.5 值按主键更新行并不会产生僵尸行;该问题(#23)在某种程度上与谓词有关。ParticleDB 1.1.18.4 不再表现出此行为。
火箭队(#84)
带有 double precision 列的表也遭遇了类似问题:它们可以用整数值更新,但这样做会产生既无法再次更新也无法删除的僵尸行。
ROOT=> CREATE TABLE t (a double precision);
ROOT=> INSERT INTO t (a) VALUES (1.0);
ROOT=> UPDATE t SET a = 2;
ROOT=> DELETE FROM t;
DELETE 0
root=> SELECT * FROM t;
a
---
2
(1 row)如果双精度列是主键,这些更新会产生已更新行的重复副本。此问题(#84)影响 ParticleDB 1.1.18.11.rc-1 至 1.1.18.11.rc-8,并在 1.1.18.11.rc-9 中修复。
幽灵在技术上不是数字(#78)
在 ParticleDB 1.1.18.11.rc-1 至 1.1.18.11.rc-6 中,存储在 real 或 double precision 列中的 NaN 值既无法更新也无法删除。
DROP TABLE IF EXISTS t;
CREATE TABLE t (a real);
INSERT INTO t (a) VALUES ('NaN');
UPDATE t SET a = 5.0;
-> #:next.jdbc{:update-count 0}
DELETE FROM t;
-> #:next.jdbc{:update-count 0}
SELECT * FROM t;
-> [{:a ##NaN}]此问题(#78)在 psql shell 中未出现,但在 ParticleDB 和 Postgres JDBC 客户端中均会出现。版本 1.1.18.11.rc-7 似乎已修复该问题。
搁浅的字符串(#74)
在 ParticleDB 1.1.18.11.rc-1 至 1.1.18.11.rc-6 中,除非使用恰好正确的字符数创建,否则定宽字符串不可修改。虽然 ParticleDB 会正确地将字符串 'hi' 填充至 'hi ',但生成的行既无法更新也无法删除。
root=> CREATE TABLE t (a character(3));
root=> INSERT INTO t (a) VALUES ('hi');
root=> DELETE FROM t;
DELETE 0
root=> UPDATE t SET a = 'sup';
UPDATE 0
root=> SELECT * FROM t;
a
-----
hi
(1 row)如果定宽列被用作主键,它可以被更新,但只能更新一次。旧副本和新副本都会显示为不同的行。
CREATE TABLE t
(a character(3) PRIMARY KEY);
INSERT INTO t (a) VALUES ('um');
UPDATE t SET a = 'no';
SELECT a FROM t;
-> [{:a "um "} {:a "no "}]任何进一步修改这些行的尝试都会产生重复键错误,尽管涉及的所有值都各不相同。定宽字符串也无法使用 = 运算符进行比较。版本 1.1.18.11.rc-7 解决了这些问题(#74)。
宠物坟场(#46)
在 SQL 中,DROP TABLE 语句会完全销毁表。在 ParticleDB 1.1.18.9 至 1.1.18.11.rc-1 中,它会留下表的影子,其行会在之后具有相同名称和大致兼容模式的表中重新出现。为演示此行为,让我们创建一个只有一行的表:
root=> CREATE TABLE t (a text, b real);
root=> BEGIN;
root=*> INSERT INTO t VALUES ('boo', 18.83);
root=*> COMMIT;然后我们删除该表,并证明它已不存在:
root=> DROP TABLE t;
root=> SELECT * FROM t;
ERROR: table not found如果我们随后创建一个同名且模式(schema)大致兼容的新表,被删除表中的行会重新出现在新表中。然而,这些复活的记录并非完全与原来相同。浮点值 18.83 变成了整数 19。
root=> CREATE TABLE t (a text, b int);
root=> BEGIN;
root=*> SELECT * FROM t;
a | b
-----+----
boo | 19
(1 row)此行为(#46)似乎与事务有关,并且在 1.1.18.11.rc-3 中不再出现。
状态损坏
ParticleDB 有多种方式可以创建无法读取的值、意外更改值、创建列不正确或缺失的行,或以其他方式损坏逻辑状态。本节中的问题仅涉及单个服务器、单个客户端,且无故障。
无法读取的无穷大(#15)
在 1.1.4 到 1.1.17 版本中,ParticleDB 的 Postgres 接口未能正确序列化像 Infinity 这样的浮点值。可以写入无穷大值,但尝试读取它们会导致客户端抛出错误。
CREATE TABLE t (a real);
INSERT INTO t (a) VALUES ('Infinity');
SELECT * FROM t
=> PSQLException: "Bad value for type
double : inf"使用 Postgres 时,这正确返回了 JVM 的 Double/INFINITY 值。NaN 被正确序列化。在 psql shell 中,存储的值返回为 inf,而不是 Infinity。在 1.1.18.6 版本中,ParticleDB 正确序列化了无穷大值(#15)。
NaN 有时被转换为 0.0(#21)
ParticleDB 1.1.4 到 1.1.18.4 有时(但并非总是!)会将浮点值 NaN 转换为 0.0。例如:
root=> CREATE TABLE t (a real);
root=> INSERT INTO t (a) VALUES (2.5);
root=> INSERT INTO t (a) VALUES ('NaN');
root=> SELECT * FROM t;
a
-----
2.5
0
(2 rows)有趣的是,这种 NaN 损坏是否发生取决于行插入的顺序。如果插入表中的第一个值是 NaN,那么该值及后续的 NaN 将被保留。1.1.18.6 版本似乎正确存储了 NaN(#21)。
不兼容类型的赋值(#7)
在 ParticleDB 1.1.4 到 1.1.18.4 中,你可以将一种类型的列更新为另一种不兼容类型的列——产生像 null 或 0 这样的值。例如:
root=> CREATE TABLE t (a INT, b BOOLEAN);
root=> INSERT INTO t VALUES (2, FALSE);
root=> SELECT * FROM t WHERE a = b;
ERROR: arrow compute: Invalid argument error:
Invalid comparison operation: Int64 == Boolean
root=> UPDATE t SET a = b;
root=> SELECT * FROM t;
a | b
---+---
0 | f在 Postgres 中,此 UPDATE 会抛出 ERROR: column "a" is of type text but expression is of type int。ParticleDB 拒绝将此字符串与整数进行比较(至少在此情况下),但非常乐意让一个等于另一个,结果为 NULL。1.1.18.6 版本似乎解决了此问题(#7)。
插入列顺序表损坏(#16)
在 SQL 中,通常使用类似 INSERT INTO people (name, age) VALUES ('xanthippe', 15) 的语句向表中插入一行。ParticleDB 1.1.4 到 1.1.17 忽略了 INSERT 语句的列,使用最初创建表时所用的列顺序。这产生了具有令人兴奋特性的行。考虑以下一对插入:一个列顺序与表模式相同,另一个顺序不同:
root=> CREATE TABLE t
(a TEXT NOT NULL PRIMARY KEY,
b boolean);
root=> INSERT INTO t (a, b) VALUES ('x', TRUE);
root=> INSERT INTO t (b, a) VALUES (FALSE, 'y');
root=> SELECT COUNT(*) FROM t;
COUNT
-------
2
(1 row)虽然 ParticleDB 认为此表包含两行,但两者都无法通过 SELECT * 读取:
root=> SELECT * FROM t;
a | b
---+---
(0 rows)通常,任何恰好遇到以不同列顺序插入的行的查询都会静默产生零结果。然而,每一行都可以通过主键读取!注意输出中不同的列顺序:
root=> SELECT * FROM t WHERE a = 'x';
a | b
---+---
x | t
(1 row)
root=> SELECT * FROM t WHERE a = 'y';
b | a
---+---
f | y
(1 row)在前面的示例中,两列的类型(某种程度上)不兼容。如果类型兼容会发生什么?
root=> CREATE TABLE t (a INT, b REAL);
root=> INSERT INTO t (a, b)
VALUES (1, 1.23E32);
root=> INSERT INTO t (b, a)
VALUES (2.34E32, 2);
root=> SELECT * FROM t;
a | b
---------------------+---------
1 | 1.23e32
9223372036854775807 | 2
(2 rows)现在表可读了,但数据已损坏。第二行的 a 值出现在 b 下,反之亦然。浮点值 2.34E32 变成了整数 263 − 1。
如果我们尝试删除表中的每一行,会发生非常奇怪的事情。DELETE 将不同顺序的行留在原处,并且其最初插入的值现在完好无损地出现!
root=> DELETE FROM t;
DELETE 1
root=> SELECT * FROM t;
a | b
---+---------
2 | 2.34e32
(1 row)第二条 DELETE 语句——此时表中只包含按列顺序 (b, a) 插入的行——本可以成功删除剩余的那一行。这些列顺序问题(#16)在 1.1.18.4 版本中不再出现。
更新可能插入损坏的行(#24、#25)
在 ParticleDB 1.1.4 至 1.1.18.8 中,一个未匹配任何行的更新反而可能在表中创建一行新的、可能已损坏的行,带来令人费解的后果。考虑一个将整数键映射到整数值的表:
root=> CREATE TABLE t
(id int not null primary key,
val integer);
root=> BEGIN;
root=*> UPDATE t SET val = 10 WHERE id = 0;
UPDATE 1
root=*> COMMIT;这个表刚刚创建。它不包含任何行,因此该更新本应影响零行,而不是一行。然而它现在却包含了一行!
root=> SELECT COUNT (*) FROM t;
COUNT
-------
1
(1 row)然而,这一行对常规查询是不可见的:
root=> SELECT * FROM t;
id | val
----+-----
(0 rows)
root=> SELECT * FROM t WHERE id = 0;
id | val
----+-----
(0 rows)
root=> SELECT * FROM t WHERE val = 10;
id | val
----+-----
(0 rows)虽然无法直接通过主键看到这一行,但它可能与某些插入操作发生冲突。
root=> INSERT INTO t (id, val) VALUES (0, 100);
INSERT 0 1
root=> INSERT INTO t (id, val) VALUES (10, 110);
ERROR: duplicate key value violates unique
constraint (key=10)“等等,”一位敏锐的读者可能会惊呼。“我们据称更新的那一行具有 id = 0 并设置了 val = 10。为什么它没有与插入 id = 0 发生冲突?”
完全正确,亲爱的读者!那它为什么反而与 id = 10 发生冲突呢?
这些都是极好的问题——而我们并不知道答案。继续:
root=> CREATE TABLE t
(id int not null primary key,
val integer);
root=> BEGIN;
root=*> UPDATE t SET val = 50 WHERE id = 5;
UPDATE 1
root=*> SELECT * FROM t WHERE id = 5;
val
-----
50
(1 row)
root=*> COMMIT;在该事务内,此更新产生的行可以通过其主键 id = 5 读取。尽管该表有两列,但该行打印出来却像只有一列:val = 50。
与 #16 中一样,这些损坏的行导致遇到它们的查询静默返回零行,即使还有其他本应匹配的有效行。
root=> INSERT INTO t VALUES (6, 60);
root=> SELECT * FROM t WHERE id IN (6);
id | val
----+-----
6 | 60
(1 row)
root=> SELECT * FROM t WHERE id IN (6, 7);
id | val
----+-----
(0 rows)尽管我们的损坏行具有 id = 5(或者也许是 50),但向 IN 谓词添加第二个值似乎使 ParticleDB“注意到”了这个损坏的行。随后它未能返回具有 id = 6 的有效行。
取决于表结构和 UPDATE 语句,由更新创建的行可能在结构上有效,但在逻辑上已损坏:
root=> CREATE TABLE t (a INT UNIQUE);
root=> INSERT INTO t VALUES (1);
root=> BEGIN;
root=*> UPDATE t SET a = 3 WHERE a = 2;
UPDATE 1
root=*> COMMIT;
root=> SELECT * FROM t;
a
---
1
3
(2 rows)这个缺陷(#24)还使得本应保持某行值不变的更新反而抛出重复键异常,因为它们(据推测)与自身发生了冲突(#25)。截至 1.1.18.9 版本,这两个问题似乎都已解决。
有时,过于冗长的字符串……会带来诅咒(#37)
在 1.1.15 至 1.1.17 版本中,text 字符串可能意外地变形为八位整数。我们在使用官方 ParticleDB 客户端的 append 测试中遇到了这一问题,例如这个。append 工作负载将逗号分隔的整数列表存储在一个名为 val 的 SQL TEXT 列中,并使用 CONCAT 向其追加值。大多数对 val 的读取都合理地返回了字符串:
["select (val) from txn2 where id = ? " 2946]
-> [{:val "1"}]然而,大约 30 秒后,ParticleDB 客户端开始为键 2946 返回八位整数。在这里,该键的值变成了 JVM Byte 1:
["select (val) from txn2 where sk = ? " 2946]
-> [{:val 1}]从这时起,一些键以字符串形式出现,而另一些则返回整数。读取键 2961 返回了字符串 "2",而键 2865 返回了整数 1——片刻之后又返回了字符串 "1,2"。有时整数值与先前写入产生的字符串值相对应,但并非总是如此。例如,一个事务将键 2980 读取为整数 0——而其字符串中唯一追加过的元素是 "1" 和 "2"。此后不久,单个事务两次读取键 2980,先发现一个字符串,然后是一个显然无关的整数:
["select (val) from txn2 where id = ? " 2980]
-> [{:val "2,1"}]
["select (val) from txn0 where sk = ? " 2981]
-> [{:val "1"}]
["select (val) from txn2 where sk = ? " 2980]
-> [{:val 3}]这个问题(#37)似乎在 1.1.18.11.rc-1 中消失了。
一尘不染的表的永恒阳光(#72)
在 ParticleDB 1.1.18.11.rc-1 中,table-2pl 事务模式的 append 测试经常抛出如下错误:
PDB wire error 1064: SQL execution error: failed to create RecordBatch: Invalid argument error: Column ‘id’ is declared as non-nullable but contains null values
这很奇怪。append 工作负载总是为每次写入选择一个唯一的整数 ID,并且从不尝试写入 NULL ID。它也从不更新 ID——ID 只在行创建时设置一次。这里有一个示例,其中在事务之外发出的单条 SELECT 语句报告了空主键。这表明表状态不知何故已损坏。
我们在 1.1.18.11.rc-7 中不再观察到这个错误(#72)。
事务与并发
我们发现 ParticleDB 的事务和并发控制存在几个问题。所有这些问题都发生在没有故障注入的单节点实例中。
嵌套事务破坏进行中的事务(#44)
ParticleDB 1.1.4 到 1.1.18.9 允许客户端开始嵌套事务,而不是警告事务已在进行中。外层事务执行的任何写入都会丢失,无论是在嵌套事务内还是在提交之后。
root=> CREATE TABLE t (a int);
root=> BEGIN;
root=*> INSERT INTO t VALUES (3);
root=*> BEGIN;
root=*> SELECT * FROM t;
a
---
(0 rows)
root=*> COMMIT;ParticleDB 1.1.18.11.rc-1 似乎已解决此问题(#44):在事务内发出 BEGIN 会警告 there is already a transaction in progress,并保留进行中事务的写入。
已中止的事务可以提交(#43)
在 1.1.17 和 1.1.18.4 版本之间,ParticleDB 对已中止事务的处理不规则。使用 Postgres 接口时,尝试提交已中止的事务会抛出错误,正如人们所预期的那样:
root=> BEGIN;
root=> INSERT INTO t (a) VALUES (3);
root=> THIS IS NOT VALID SQL;
ERROR: sql parser error: Expected: an SQL
statement, found: THIS at Line: 1, Column: 1
root=> COMMIT;
ROLLBACK然而,已中止事务执行的写入却持续存在:
SELECT * FROM t;
-> [{:a 3}]ParticleDB 客户端有一种不同的、同样出乎意料的行为:在失败的事务上执行 COMMIT 语句返回 [#:next.jdbc{:update-count 0}],而不是错误。尽管事务显然已提交,但其效果不可见。截至 1.1.18.11.rc-18 版本,两个接口都返回错误,并且不暴露已中止事务的效果(#43)。
聚合有时无法观察到事务的行(#48)
在 ParticleDB 1.1.4 到 1.1.18.9 中,执行聚合查询(如 SELECT COUNT(*))的事务可能会漏掉本应匹配的行,并返回行的重复副本,具体取决于谓词和事务中的先前查询。例如:
root=> CREATE TABLE t (a int);
root=> INSERT INTO t (a) VALUES (2);
root=> BEGIN;
root=*> INSERT INTO t (a) VALUES (3);
root=*> SELECT COUNT(*) FROM t;
COUNT
-------
1
(1 row)此表显然包含两行:2 和 3。然而,虽然第二行对 SELECT * 可见,但对 SELECT COUNT(*) 不可见。奇怪的是,与列值交互的聚合函数(如 SELECT COUNT(*) FROM t WHERE a < 5)有时会观察到正确的状态。在其他情况下,SELECT 查询似乎会在事务内创建行的重复副本,导致每次查询观察到越来越多的行。这些行为(#48)在 1.1.18.11.rc-1 版本中不再出现。
双重麻烦(#89)
ParticleDB 1.1.18.11.rc-9 偶尔会返回单行的重复副本,即使该行有本应确保唯一性的主键。例如,看这个追加测试,其中对键 10 的读取返回了两行,而不是一行。
[{:val "1,2,5,10"}
{:val "1,2,5,10"}]我们在 1.1.18.11.rc-18 中不再观察到重复行(#89)。
对零值的更新有时会静默失败(#91)
在 ParticleDB 1.1.18.4 中,事务可以将表中的所有行设置为某个值,收到每行都已更新的确认,然后读取表却发现某些行仍保留其先前的值。这个错误似乎取决于插入具有特殊“零”值的行:整数为 0,浮点数为 0.0,各种字符串列为 ''。例如:
root=> CREATE TABLE t (a INT);
root=> INSERT INTO t (a) VALUES (0);
root=> BEGIN;
root=*> UPDATE t SET a = 1;
root=*> INSERT INTO t (a) VALUES (0);
root=*> UPDATE t SET a = 2;
UPDATE 2
root=*> SELECT t.a FROM t;
a
---
2
0
(2 rows)ParticleDB 声称它更新了该表中的全部两行,但其中一行显示的是 0 的原始值。该问题(#91)似乎在 1.1.18.11.rc-9.2 中已修复。
以 TEXT 为主键的表大多不可见(#41)
版本 1.1.18.9 修复了若干安全问题,但也引入了新问题。例如,以 text 为主键的表对事务大多不可见:
root=> CREATE TABLE t (a TEXT PRIMARY KEY);
root=> INSERT INTO t (a) VALUES ('mood');
root=> BEGIN;
root=*> SELECT * FROM t;
a
---
(0 rows)SELECT、UPDATE 和 DELETE 均受影响。行对多种谓词不可见,包括 WHERE a LIKE 'moo%' 和 WHERE a IS NOT NULL。不过,行仍可通过主键访问:
root=*> SELECT * FROM t WHERE a = 'mood';
a
------
mood
(1 row)版本 1.1.18.11.rc-1 不再出现这些大多不可见的表(#41)。
千表图(#38、#66、#68、#69)
或许是由于上文讨论的单线程问题,ParticleDB 1.1.4 至 1.1.18.9 在我们能测量的几乎所有方面都频繁出现严重的事务异常。我们经常观察到已提交写入丢失、内部一致性违反、G1a(中止读)、G1b(中间读)和 P4(丢失更新)。在 append 工作负载中,成功追加的值会经常从后续对给定键的读取中消失。我们的测试套件推断出各种循环异常,包括 G0、G1c、G-Single 和 G2-item,但其中许多可归因于更基本的问题,如写入丢失。这些问题在单服务器且无故障的情况下发生,并影响了测试的每种事务模式、接口和 WAL 模式。8
由于我们已详细讨论了单线程异常,此处省略对这些问题的详细讨论。到版本 1.1.18.11.rc-1 时,ParticleDB 最明显的 G1a(#66)、G1b(#38)、内部一致性异常(#68)、写入丢失(#69)等示例已大幅减少。
G-Single(#73)
在版本 1.1.18.11.rc-1 至 1.1.18.11.rc-22 中,我们偶尔在无故障的单节点上观察到 G-single,即违反快照隔离。例如,以下是 一个 append 测试 中的两个事务,使用事务模式 occ-v3 和 WAL 模式 groupsync:
上方事务读取键 9,发现它不存在,然后向其追加 8。下方事务向键 9 追加 6。由于每次读取键 9 都观察到类似 [6 8 12 ...] 的值,我们知道 6 的追加发生在 8 的追加之前。上方事务未能观察到下方事务,尽管下方事务必然发生在下方事务之前。这与 P4(丢失更新)类似,并违反了快照隔离。
类似行为(#73)出现在 zero-copy-occ 和 1.1.18.11.rc-22 的 occ 中。Orkes 正在调查。在我们的合作之后,Orkes 报告版本 1.1.19-rc.13 不再出现 G-Single。
Brazil(#67)
在版本 1.1.18.9 中,我们发现通过唯一常量整数 ID 查询特定行时,偶尔会返回完全不同的行的值。例如,考虑 这个 append 测试 在单个健康节点上运行,事务模式为 occ-v3,WAL 模式为 groupsync。对键 12 的读取大多观察到与对键 12 的追加一致的值,但短暂时间内观察到空值,随后观察到键 7 的值:
| 时间(秒) | 值 |
|---|---|
| 6.02 | 1, 2, 7, … 82 |
| 6.05 | 1, 2, 7, … 82 |
| 6.17 | |
| 6.22 | |
| 6.24 | 1, 2, 7, … 82, 92 |
| 6.24 | |
| 6.25 | 1, 5, 8, … 181, 86 |
| 6.29 | |
| 6.29 | 1, 5, 8, … 181, 86 |
| 6.40 | |
| 6.49 | |
| 6.68 | 1, 2, 7, … 82, 92, … 139, 142 |
值 [1, 5, 8, ... 181, 86] 与读取键 7 的结果一致,而非键 12。在测试的剩余部分,读取观察到的值与键 12 的原始版本一致:[1, 2, 7, ...]。此问题(#67)在版本 1.1.18.11.rc-1 之后的测试中未再出现。
使用 table-2pl 时的丢失写入(#70)
在版本 1.1.18.11.rc-1 至 1.1.18.11.rc-6 中,ParticleDB 的 table-2pl 事务模式在正常操作期间经常丢失已提交的写入。例如,这个 append 测试使用 ParticleDB Java 客户端,在短短几秒内就丢失了对数十个键的写入。以下是它对键 187 的一些读取结果:
| 时间(秒) | 值 |
|---|---|
| 10.89 | 5 |
| 10.89 | |
| 10.92 | 1, 6 |
| 10.92 | |
| 10.92 | |
| 10.92 | 1, 6, 10, 11 |
| 10.93 | 2 |
| 11.09 | 2, 13 |
由于 append 工作负载只向行追加数据而从不删除,每次读取都应观察到列表最长版本的一个前缀。然而,看起来对 5 的追加,随后对 1、6、10 和 11 的追加都丢失了。此外,其中一些写入在事务中途丢失,导致内部一致性错误。
到版本 1.1.18.11.rc-7 时,table-2pl 不再如此频繁地丢失写入(#70)。
使用 occ-v3 时的内部异常(#96)
ParticleDB 1.1.18.11.rc-9.2 在使用事务模式 occ-v3 和 WAL 同步模式 groupsync 时偶尔会违反内部一致性。例如,这个 append 测试中,单个事务先读取了 nil,然后对键 8 读取了 [1, 4]:
[[:r 8 nil] [:r 8 [1 4]]]在 Read Atomic 及以上隔离级别,对同一键的两次连续读取不应返回不同结果。这也违反了快照隔离和可串行化。此缺陷(#96)可能在 1.1.18.11.rc-19 中已解决。
没有反模因部门(#65)
ParticleDB 1.1.18.11.rc-1 through 1.1.18.11.rc-9.1 would often forget
a column existed. A query like
SELECT * FROM snacks WHERE sk = 4 would throw an error like
PDB wire error 1064: column not found: sk. This issue (#65)
occurred in every transaction mode, but seemed to be resolved by version
1.1.18.11.rc-9.2.
Six Data Blockers and a Large Iced Coffee (#31)
Versions 1.1.18.8 through 1.1.18.11.rc-22 exhibited perplexing
nondeterministic failures in the compare workload. Each
test case established a fresh database client, created a globally unique
table, and performed operations against that table synchronously. At no
point should there have been two clients concurrently accessing a table.
And yet sometimes, rows failed to appear in queries:
root=> CREATE TABLE t_876 (a boolean);
root=> INSERT INTO t_876 (a) VALUES (NULL);
root=> INSERT INTO t_876 (a) VALUES (NULL);
root=> SELECT * FROM t_876 WHERE TRUE;
a
------
NULL
(1 row)One could update rows using the predicate WHERE NULL,
which should never match anything:
root=> CREATE TABLE t_40 (a boolean);
root=> INSERT INTO t_40 (a) VALUES (FALSE);
root=> UPDATE t_40 SET a = (a <= a) WHERE NULL;
UPDATE 1In many cases single SQL statements threw errors complaining of concurrent updates, even though at most one client should ever have accessed a given table.
root=> CREATE TABLE t_101 (
a text COLLATE unicode PRIMARY KEY
);
root=> INSERT INTO t_101 (a) VALUES (' ');
root=> UPDATE t_101 SET a = NULL;
ERROR: could not serialize access due to
concurrent update on table "t_101"
key " "Deleted rows could also remain visible:
root=> CREATE TABLE t_27763 (
a boolean,
b boolean NOT NULL UNIQUE PRIMARY KEY
);
root=> INSERT INTO t_27763 (a, b)
VALUES (NULL, TRUE);
root=> DELETE FROM t_27763;
root=> SELECT t_27763.a AS j FROM t_27763
WHERE t_27763.b ORDER BY t_27763.b;
j
------
NULL
(1 row)These anomalies (#31) occurred with all transaction modes and WAL sync modes, but attempts to reproduce them deterministically, or to narrow down their cause, were unsuccessful. They may be artifacts of a bug in the test suite, but we lacked time to investigate fully. After our collaboration, Orkes reported 1.1.19-rc.13 resolved this issue.
Read Skew With
table-2pl (#106)
With the ParticleDB interface, WAL mode groupsync, and
table-2pl, version 1.1.18.11.rc-18 exhibited G-single
under normal operation. Here’s an
example:
T1: [[:r 112 nil]
[:append 112 1]
[:append 103 29]
[:append 67 105]]
T2: [[:r 110 [10 14 13 20 16 28 30 33 48 35]]
[:r 112 [1]]
[:r 110 [10 14 13 20 16 28 30 33 48 35]]
[:r 103 [1 4 11 15 17 20 24 25]]]The first transaction appended 1 to key 112
and 29 to key 103. The second transaction
observed the append to key 112, but missed the append to
key 103. Since it observed but also failed to observe
another transaction, this is A5A (Read Skew).
Since the first transaction read-write depends on the second, and the
second write-read depends on the first, it is also G-single. Both are
violations of Snapshot Isolation.
We no longer observed Read Skew with table-2pl (#106) by
version 1.1.18.11.rc-19.
Stale Read With
strict-serializable (#109)
Version 1.1.18.11.rc-18 added a new transaction mode,
strict-serializable, which may have been renamed to
serializable in 1.1.18.11.rc-22.9
Strict Serializability (also known as Strong Serializability) guarantees
that all transactions appear to execute in real-time order: if T1 completes before T2 begins, then T1 appears to execute
before T2. However,
this was not true in 1.1.18.11.rc-18. Healthy ParticleDB nodes
infrequently but regularly exhibited Stale
Read, even with WAL mode sync. In one of our testing
environments, these anomalies appeared roughly every 300,000
transactions, using Elle’s exponential key distribution and 10
concurrent clients pushing ~80 transactions per second.
Stale reads all seemed to share the same structure: a cycle comprising a real-time, read-write, then write-read edge. Here’s an example involving a single key:
The top transaction appended 66 to key
22209, which was read by the middle transaction. After the
middle transaction completed, the bottom transaction began, and read key
22209 without 66. This is a violation of
Strong Snapshot Isolation, and therefore Strong Serializability as
well.
After the conclusion of our testing work, Orkes reported that version 1.1.19-rc.10 no longer exhibits stale read (#109).
Lost Writes (#110)
In version 1.1.18.11.rc-18 through 1.1.18.11.rc-22, a healthy
ParticleDB node running occ-v3, zero-copy-occ,
table-2pl, or serializable-ssi would
occasionally discard acknowledged writes. For example, take this
append test, which ran with WAL mode sync. Out of
approximately 20,000 keys, 31 lost writes. Here are some of the reads of
key 16647:
| Time (s) | Value |
|---|---|
| 2798.88 | |
| 2798.88 | 1, 2, 3 |
| 2798.88 | 1, 2, 3 |
| 2798.90 | 1, 2, 3 |
| 2798.90 | 1, 2, 3 |
| 2798.93 | 1, 2, 3 |
| 2798.96 | 1, 2, 3 |
| 2798.98 | 4, 5, 6, 7 |
| 2798.98 | 4, 5, 6, 7 |
| 2799.02 | 4, 5, 6, 7, 27 |
| 2799.02 | 1, 2, 3 |
| 2799.02 | 1, 2, 3 |
| 2799.02 | 1, 2, 3 |
| 2799.02 | 4, 5, 6, 7, 27 |
While the transaction which wrote 3 committed
successfully, and its write was visible to several other transactions,
the elements 1, 2, and 3 were
replaced by 4, 5, 6, ... after 2799.02 seconds, and never
reappeared. This occurred every few minutes in our tests, with both the
Postgres and ParticleDB interfaces.
A transaction could also read an acknowledged write, modify that row, and find that the modification apparently destroyed the written value, leading to internal anomalies as well as data loss. After we finished testing Orkes released version 1.1.19-rc.13, and believes that it fixes this issue (#110).
With Faults
The issues described above occurred in healthy, single-node instances of ParticleDB. When we introduced faults, new behaviors emerged.
Data Loss on WAL Bitflips (#108)
ParticleDB uses checksums to detect corruption or incomplete writes to its Write-Ahead Log (WAL) files. Sometimes these checks prevented the server from starting up with corrupt data. In other cases, ParticleDB would discover that its data files were corrupt, then start up anyway, serving data to clients.
Take this
append test of 1.1.18.11.rc-18. At 09:25:09 we
introduced two single-bit errors into one of ParticleDB’s
.wal files. At 09:25:39 we read key 1754 and
observed a list of 44 elements:
[[:r 1754 [3 5 7 9 12 13 18 19 20 22 24 25 28
30 31 33 34 36 35 37 38 39 42 43 44
45 47 50 51 53 58 59 62 63 64 65 67
68 69 70 73 75 79 80]] ...A few milliseconds later, we killed and restarted ParticleDB a few times. When the server came back online at 09:26:07, it logged:
2026-07-22T14:26:07.985223Z WARN ThreadId(01)
recover_streaming{dir=/opt/particledb/data/wal}:
spanner_wal::segment: checksum mismatch in
segment tail, stopping read segment_id=0
2026-07-22T14:26:07.985235Z WARN ThreadId(01)
recover_streaming{dir=/opt/particledb/data/wal}:
spanner_wal::recovery: replaying valid prefix
of already-sealed corrupt WAL segment (.corrupt
marker present) and continuing with later
segments of this stripe stripe_id=0 segment_id=0
corruption_offset=25184 valid_size=25184
recovered_entries=242 detail="checksum mismatch:
stored 0xf3b8aa37, computed 0x4a774da2"
However, the server went on to start up and serve client requests
normally. At 09:26:09, we read key 1754 and found those 44
elements had disappeared:
[... [:r 1754 [81]] ...]Several other keys also lost some or all of their acknowledged writes. Orkes is investigating this issue (#108). While Jepsen has not tested this version, Orkes reports 1.1.19-rc.12 detects bitflips correctly and refuses to start.
Total Data Loss With Simulated Misdirected Reads/Writes (#112)
One of our file corruption faults divided a file into four-kilobyte chunks, then randomly replaced a small fraction of those chunks with other chunks from the same file. The goal is to simulate misdirected reads or writes, a type of silent data corruption that happens in some storage systems. In 1.1.18.11.rc-22, ParticleDB sometimes responded to this type of fault by forgetting every table and (apparently) deleting everything in its WAL file.
For example, take this
test run. At 09:53:56, the test harness shuffled 13 four-kilobyte
chunks of the first .wal file around—approximately
one-tenth of the file’s contents. It then killed and restarted the node.
Although this file had hundreds of kilobytes in it, when ParticleDB
started up it logged that there was no data on disk:
2026-07-30T14:53:58.715589Z INFO ThreadId(01)
spanner_exec::engine::context: No data found in
storage — starting freshAt 09:53:58, ParticleDB began throwing
ERROR: table not found for every query. No request ever
succeeded after this time.
It appears that ParticleDB may have deleted the contents of its WAL file. A few seconds later, the test found its size was zero bytes, and the same held true at the end of the test. After we finished testing Orkes released version 1.1.19-rc.12; they believe it resolves this issue (#112).
Internal Anomalies With
strict-serializable and Process Kills (#107)
With strict-serializable, WAL mode
groupsync, and the ParticleDB interface, single nodes
running 1.1.18.11.rc-18 exhibited internal anomalies with process kills.
Here’s an
example:
{:op {:index 190704,
:time 100211183125,
:type :ok,
:process 59,
:f :txn,
:value [[:r 2879 nil]
[:append 2879 16]
[:r
2879
[1 2 3 4 5 7 8 16]]]},
:mop [:r 2879 [1 2 3 4 5 7 8 16]],
:expected [16]}This transaction read key 2879 as nil, then
appended to it, and read its value—only to find that seven elements had
been appended in the intervening time. Transactions could also exhibit
G-single and P4 (G-single), but we suspect these could be due to the
same underlying bug. While Jepsen has not had a chance to test this
release, Orkes claims this issue (#107) was
resolved in version 1.1.19-rc.12.
| № | Summary | Event Required | Fixed in |
|---|---|---|---|
| #62 | Abysmal performance with
table-2pl |
None | 1.1.18.6 |
| #81 | Mystery Crash | None | 1.1.18.11.rc-18 |
| #27 | Deadlock after two transactions | None | 1.1.18.11.rc-1 |
| #71 | Deadlock with table-2pl |
None | 1.1.18.11.rc-7 |
| #33 | Ordering by an unused column | None | 1.1.18.9 |
| #6 | No static check for comparators of incomparable types | None | 1.1.18.6 |
| #94 | Decimals not comparable to floats | None | 1.1.18.11.rc-18 |
| #12 | Filter predicates reject
NULL |
None | 1.1.18.6 |
| #13 | Cannot compare values to
NULL |
None | 1.1.18.6 |
| #30 | Boolean logic on integers | None | 1.1.18.9 |
| #50 | More boolean logic on integers | None | 1.1.18.11.rc-8 |
| #5 | Multiple assignment to a single column | None | 1.1.18.6 |
| #10 | Predicates can refer to nonexistent columns | None | 1.1.18.6 |
| #20 | No aggregates within
CASE |
None | 1.1.18.6 |
| #28 | No exponent operator (^) |
None | 1.1.18.9 |
| #2 | Malformed responses for prepared statements | None | 1.1.18.11.rc-1 |
| #83 | select throws
Bad value for type long |
None | 1.1.19-rc.12 |
| #111 | Tuples with no field structure | None | 1.1.19-rc.13 |
| #1 | Broken Postgres authentication | None | 1.1.18.11.rc-1 |
| #3 | Java client could not execute transactions | None | 1.1.15 |
| #14 | No large integer literals | None | 1.1.18.6 |
| #85 | No exponentiation on
Infinity |
None | 1.1.18.11.rc-9 |
| #49 | -1 ^ 0 = -1 |
None | 1.1.18.11.rc-3 |
| #88 | 1.0 + 1E-39 = 1.0 |
None | 1.1.18.11.rc-9.2 |
| #56 | Large literals become
Infinity |
None | 1.1.18.11.rc-3 |
| #61 | Exponent underflow | None | 1.1.18.11.rc-3 |
| #34 | Division by integer 0 |
None | 1.1.18.9 |
| #51 | Modulo operator cannot handle floats | None | 1.1.18.11.rc-1 |
| #36 | NaN arithmetic throws
errors |
None | 1.1.18.9 |
| #92 | NULL arithmetic throws |
None | 1.1.18.11.rc-18 |
| #60 | UPDATE arithmetic throws
errors |
None | 1.1.18.11.rc-1 |
| #32 | Same precision for real and
double precision |
None | 1.1.18.9 |
| #11 | Integers allow out-of-range values | None | 1.1.18.6 |
| #86 | Dividing nearby numbers fails | None | 1.1.18.11.rc-9 |
| #87 | Floating-point errors in integer values | None | 1.1.18.11.rc-9 |
| #19 | Floats not equal to quoted representation | None | 1.1.18.6 |
| #63 | 0 is not equal to
0.0 |
None | 1.1.18.8 |
| #80 | 5 appears equal to
0.90... |
None | 1.1.18.11.rc-9 |
| #64 | CHARACTER(n) allows more than
n characters |
None | 1.1.18.6 |
| #52 | CHARACTER(n) allows more than
n characters again |
None | 1.1.18.11.rc-3 |
| #55 | CHARACTER(n) allows fewer
than n characters |
None | 1.1.18.11.rc-1 |
| #22 | Default text collation ignores
LOCALE |
None | 1.1.18.9 |
| #42 | Mis-sorted text | None | 1.1.18.11.rc-7 |
| #93 | More mis-sorted text | None | 1.1.18.11.rc-21 |
| #40 | SELECT 5 WHERE FALSE returns
5 |
None | 1.1.18.11.rc-1 |
| #47 | WHERE clauses with math match
nothing |
None | 1.1.18.11.rc-1 |
| #18 | WHERE 4 = 5 matches primary
key 5 |
None | 1.1.18.4 |
| #35 | Transactional selects return nonsense | None | 1.1.18.9 |
| #8 | NOT NULL constraints
don’t |
None | 1.1.18.4 |
| #9 | UNIQUE constraints don’t |
None | 1.1.18.6 |
| #29 | Updates can create duplicate primary keys | None | 1.1.18.9 |
| #57 | Primary keys may be NULL |
None | 1.1.18.11.rc-3 |
| #82 | Primary key conflicts with deleted rows | None | 1.1.18.11.rc-18 |
| #90 | Duplicate primary keys | None | 1.1.18.11.rc-19 |
| #58 | Can’t join integers to floats | None | 1.1.18.11.rc-1 |
| #54 | Joins omit table names, mangle columns instead | None | 1.1.19-rc.13 |
| #76 | Joins coerce values to strings | None | 1.1.18.11.rc-21 |
| #59 | Outer joins to empty tables often produce no rows | None | 1.1.18.11.rc-3 |
| #77 | Joins confuse columns with same name | None | 1.1.18.11.rc-7 |
| #26 | Transactions can’t delete anything | None | 1.1.18.9 |
| #45 | Transactions which delete leave shades behind | None | 1.1.18.11.rc-1 |
| #23 | Updating integers makes rows not-deletable | None | 1.1.18.4 |
| #84 | double precision primary keys
yield zombie twins |
None | 1.1.18.11.rc-9 |
| #78 | NaN values are immutable |
None | 1.1.18.11.rc-7 |
| #74 | Fixed-width strings are deeply cursed | None | 1.1.18.11.rc-7 |
| #46 | Dropped tables can be resurrected, in distorted forms | None | 1.1.18.11.rc-3 |
| #15 | Infinity is writable, but
unreadable |
None | 1.1.18.6 |
| #21 | NaN sometimes becomes
0.0 |
None | 1.1.18.6 |
| #7 | Assignment of incompatible types yields
(e.g.) NULL |
None | 1.1.18.6 |
| #16 | Values assigned to wrong columns, yielding cursed rows | None | 1.1.18.4 |
| #24 | Updates of no rows create new, corrupt rows instead | None | 1.1.18.9 |
| #25 | No-op updates cause duplicate key violations | None | 1.1.18.9 |
| #37 | Strings are sometimes returned as 8-bit integers | None | 1.1.18.11.rc-1 |
| #72 | Primary keys mysteriously become
null |
None | 1.1.18.11.rc-7 |
| #44 | Nested transactions destroy ongoing ones | None | 1.1.18.11.rc-1 |
| #43 | Aborted transactions can commit | None | 1.1.18.11.rc-18 |
| #48 | Aggregates sometimes miss a transaction’s rows | None | 1.1.18.11.rc-1 |
| #89 | Queries sometimes observe duplicate copies of rows | None | 1.1.18.11.rc-18 |
| #91 | Updates to special values can silently fail | None | 1.1.18.11.rc-9.2 |
| #41 | Tables with text primary keys
are mostly invisible |
None | 1.1.18.11.rc-1 |
| #66 | G1a (Aborted Read) | None | 1.1.18.11.rc-1 |
| #38 | G1b (Intermediate Read) | None | 1.1.18.11.rc-1 |
| #68 | Internal consistency anomalies | None | 1.1.18.11.rc-1 |
| #69 | Lost write | None | 1.1.18.11.rc-1 |
| #73 | G-single | None | 1.1.19-rc.13 |
| #67 | Reads observe data from different row | None | 1.1.18.11.rc-1 |
| #70 | Lost write with
table-2pl |
None | 1.1.18.11.rc-7 |
| #96 | Occasional internal consistency anomalies
with occ-v3 |
None | 1.1.18.11.rc-19 |
| #65 | Columns sometimes vanish | None | 1.1.18.11.rc-9.2 |
| #31 | Odd nondeterministic single-threaded anomalies | None | 1.1.19-rc.13 |
| #106 | A5A (Read Skew) with
table-2pl |
None | 1.1.18.11.rc-19 |
| #109 | Stale Read with
strict-serializable |
None | 1.1.19-rc.10 |
| #110 | Lost writes | None | 1.1.19-rc.13 |
| #108 | Silent(ish) data loss | WAL truncation/bitflips | 1.1.19-rc.12 |
| #112 | Total data loss | Misdirected reads/writes | 1.1.19-rc.12 |
| #107 | Spurious nil reads |
Process crash | 1.1.19-rc.12 |
Discussion
ParticleDB 1.1.4 claimed to be a Postgres-compatible database which
offered Snapshot Isolation with its occ-v3 and
zero-copy-occ transaction modes, Serializability with
serializable-ssi, and Strong Serializability with
table-2pl.
Instead, we found a library of confusion.10
In versions 1.1.4 through 1.1.18.11.rc-19, basic functionality like
transactions (#3), prepared statements (#2), and Postgres auth (#1) were
unusably broken. The server spontaneously deadlocked (#27, #71) or
crashed (#81) every few minutes. Integers stood in for booleans (#30,
#50), comparisons to NULL threw errors (#13), and you could
assign multiple values to the same column (#5). Large floating-point
numbers quietly became infinite (#56); Infinity,
NaN, and NULL arithmetic were broken (#36,
#92, #15, #21); and out-of-range values were improperly stored or
clipped to fit (#32, #11). In ParticleDB, 0.0 was not equal
to 0 (#63), but 0.90... wasn’t not
equal to 5 (#80). Strings allowed too many (#52, #64) and
too few (#55) characters. A WHERE clause with
floating-point operations would silently fail to match any rows (#47),
WHERE 4 = 5 returned rows with primary key 5
(#18), WHERE a = 1 matched the row {a: 2}, and
WHERE TRUE matched nothing (#35). Null (#8) and unique (#9)
constraints did nothing, and primary keys could be duplicated (#29),
null (#57), or both. Primary keys failed to conflict with rows that did
exist (#90), and conflicted with rows that didn’t (#82). Joins returned
incorrect column names (#54), coerced values to strings (#76), failed to
return rows (#59), and confused similarly-named columns (#77). It was
impossible to delete rows within a transaction (#26, #45), rows with
integers set to floats (#23), or with floats set to integers (#84), or
those containing NaNs (#78), or fixed-width strings
(#74).11 Rows from dropped tables could
re-emerge in mangled forms (#46). Columns could be set to values of the
wrong type, silently altering or corrupting data (#7). Inserts ignored
provided columns, creating rows best described as “deeply cursed”
(#16).12 Updates could create new, shambolic
rows where none existed (#24, #25). Strings sometimes turned into
eight-bit integers (#37), and primary keys occasionally became
NULL (#72). Aborted transactions could commit, or fail with
their effects still visible (#43). Queries sometimes missed (#48),
duplicated (#89), or returned the wrong (#67) rows. Tables with a
text primary key were mostly invisible (#41). Special
values were sometimes inalterable (#91). Entire columns occasionally
vanished for no obvious reason (#65). Uncontended statements on freshly
created tables observed nonsensical results or conflicted (apparently)
with themselves (#31). Each transaction and WAL mode exhibited
essentially every transactional anomaly our tests could detect: write
loss, internal consistency violations, G0, G1a, G1b, G1c, G-Single,
G-nonadjacent, P4, A5A, G2, and real-time variants thereof (#38, #66,
#68, #69, #73, #70, #96, #109, #110, etc.).
These behaviors occurred in single-node instances of ParticleDB,
without fault injection, often with just a single client. When we killed
ParticleDB, it allowed internal anomalies, P4 (Lost Update), and
G-single (#107), even with the new serializable transaction
mode. Corruption or truncation of WAL files caused partial data loss
(#108). Simulating misdirected reads or writes could cause total data
loss (#112).
As we concluded our engagement, version 1.1.18.11.rc-22 had just
become available. It abandoned the table-2pl and
serializable-ssi transaction modes in favor of
fast, occ, and serializable,
which were intended to offer no consistency, Snapshot Isolation, and
Strong Serializability, respectively. While 1.1.18.11.rc-22 was
significantly safer than 1.1.4, we were still finding new issues. The
default occ mode regularly lost data in normal operation
(#110). The new serializable mode appeared significantly
safer, but still allowed stale read (#109) in normal operation, and with
process kills, sometimes failed to read extant rows. This led to
internal anomalies, Lost Update, and more (#107).
After we finished testing, Orkes updated ParticleDB’s documentation
to claim that every transaction mode discussed here was “experimental”.
This claim is difficult to support. To the best of Jepsen’s knowledge,
none of the ParticleDB documentation described
these modes as experimental or unsupported during our engagement.
Orkes’ unpublished operator guide did describe one mode,
serializable-occ, as an “internal A/B variant… do not use
for correctness runs”. We considered that mode experimental and omitted
it from this report. However, the others had no such caveats.
Orkes appears to
have reworked ParticleDB’s approach to consistency. It now supports
the standard SQL isolation level statements for
READ COMMITTED, REPEATABLE READ (meaning
Snapshot Isolation), and SERIALIZABLE. The
--txn-mode option is gone, and a new
--concurrency-control setting has none,
optimistic, and strict-optimistic options. The
documentation
claims that every mode discussed in this report has been removed
from version 1.1.19-rc.13. However, it goes on to say that
occ-v3 and strict-serializable were “earlier
names” of the “optimistic engine” and “strict serializable mode”,
respectively. It is not clear whether these modes were removed or simply
renamed.
Jepsen has not evaluated versions of ParticleDB after 1.1.18.11.rc-22, and cannot speak to these isolation levels or concurrency control modes. However, Orkes claims that all issues from this report have been addressed as of version 1.1.19-rc.13.
As always, Jepsen takes an experimental approach to safety verification: we can prove the presence of bugs, but not their absence. While we make extensive efforts to find problems, we cannot prove correctness.
Impact
Orkes believes that the issues we found in 1.1.4 were likely due to
an extensive refactor of ParticleDB in spring 2026, and that Orkes and
their customers, who continued to run an earlier version, were likely
unaffected. Near the end of our collaboration we briefly evaluated an
internal build of version 1.0.0. While it exhibited some string and
arithmetic issues, that version did not seem susceptible to most of the
serious safety errors we found in 1.1.4, and passed cursory
rw and append tests. We tested only a single
node at a time, so we cannot speak to the safety of the replication
system.
From May through July 2026, the ParticleDB download page
offered users (at least) versions 1.1.4 through 1.1.18.11.rc-18.13 All exhibited serious safety errors
during normal operation. It seems likely that users who ran these
versions of ParticleDB would have lost or corrupted data. In version
1.1.18.11.rc-22, using WAL mode sync and transaction mode
serializable significantly reduced the frequency and
severity of these issues, but did not prevent internal anomalies or Lost
Update.
Given the severity and extent of the safety errors we found, and the process which produced them, Jepsen recommends users halt further deployment of ParticleDB and migrate to other databases as soon as possible. Consider checking and/or rebuilding database state from more reliable sources, if available.
The Unreasonable Effectiveness of Generative Testing
The compare workload generated random schemas and SQL
statements over them, then ran both against ParticleDB and Postgres to
see if they produced equivalent results. This should not have worked.
There are many ways in which two “Postgres-compatible” databases can
legitimately differ, such as floating-point details, which error message
takes precedence, or the order of unsorted results. The space of SQL
queries is large and difficult to cover. Many queries trivially return
equivalent results (e.g. SELECT ... WHERE FALSE) and
therefore tell us little information. We can test only sequential
behavior, which is easy for a database’s standard test suite to
check.
Nevertheless, roughly two
thousand lines of generative tests found a wealth of bugs in
ParticleDB, and unlike Jepsen’s typical workloads, the examples
generated were usually easy to reproduce. ParticleDB offered a good deal
of “low-hanging fruit” that should have been caught by example-based
tests—but the compare workload also found more subtle
errors. For example, the fact that 0.90... was apparently
equal to 5 hinged on a high-precision floating-point
literal. Inspired by this success, we extracted part of the ParticleDB
SQL generator into a shared
library so it could be used in other tests, like the new
internal-sim workload.
Jepsen has traditionally focused on thorough analysis of a small set
of carefully chosen transactions. This is partly because checking (e.g.)
Serializability in general is NP-complete,
and also because we assume that if transactions of (e.g.) reads and
appends work correctly on strings, so will other operations on other
datatypes. Our intuition is that the “hard part” is in the concurrency
control mechanism, and sequential datatype behavior is generally covered
by existing tests. This was not the case in ParticleDB. We’ve expanded
Jepsen’s list-append and rw tests to use
alternate encodings and transaction structures, in an attempt to cover
more codepaths. This revealed new bugs in ParticleDB, and we hope it
will in other databases too.
Unfortunately, compare became less useful as Orkes fixed
major bugs and left smaller incompatibilities in place. For example,
Postgres checks unique constraints when updating each row of a table,
but ParticleDB only checks after all have been updated. This means that
a statement like UPDATE bools SET x = NOT x can swap the
values of TRUE and FALSE in ParticleDB without
violating a unique constraint. Toward the end of our engagement, the
compare test generally hit these minor inconsistencies
early, then spent most of its time searching for minimal cases of
them—potentially missing more serious bugs.
Quality Assurance
Orkes said they ran Jepsen’s existing Postgres test harness to find and resolve issues in ParticleDB prior to this work. However, that same test harness failed immediately when run against ParticleDB 1.1.4. Orkes also stated they tested ParticleDB with SQLancer, which generates far more complex queries than the ones we tested. It is therefore perplexing that nearly every feature we tested exhibited glaring errors, and that our findings seemed to come as a surprise.
Most databases have a test suite which exercises at least basic
examples of each feature, and it is customary to run those tests before
release, if not continuously. Where were those tests for ParticleDB? How
does one add support for NOT NULL constraints without a
test which tries to insert at least one NULL value? What
development process adds a CHARACTER(10) type without
trying to write a string of (e.g.) five characters?
Our findings suggest structural deficiencies in the ParticleDB QA process. Perhaps tests were not run prior to release. Perhaps the test suite was run, but failed to exercise basic functionality. Perhaps engineers entrusted Large Language Models with the work of testing.
Jepsen recommends that Orkes (and all database vendors!) build and exercise an automated test suite for their database. This suite should include at least simple, example-based tests covering important database features. These tests should fail when the feature is unimplemented or malformed, and pass when it is complete. Observing these failures is an important part of the development process: it validates that the test measures something. Tests should be run at least once before each release, and results disseminated to the team. Afterwards consider more sophisticated generative tests, establishing adversarial QA and implementation teams, and subtly breaking safety mechanisms to determine whether the tests are still effective.
Documentation
ParticleDB’s website gave the appearance of a mature database project. It made strong claims: a general-purpose OLTP/OLAP SQL database a thousand times faster than PostgreSQL, “formally verified” by a TLA+ proof, multiple APIs, and client libraries in thirteen languages. It offered a variety of deployment options and extensive documentation on database features and operations.
On closer inspection, that documentation was riddled with omissions, inconsistencies, and confabulations. The TLA+ proof was not published, and in any event did not prevent disaster—perhaps it was incorrect, formalized the wrong properties, or did not describe the system as built. Safety-critical configuration settings like the transaction mode were described in vague terms, like “safe under concurrency”. The documentation for the Java client library referred to non-existent dependency coordinates and seemed to make up class and package names which were nowhere to be found in the library itself. The quickstart and client examples made up a variety of default usernames for ParticleDB, all of which were wrong. The stored procedure documentation even fabricated an imaginary email address for the author of this report. In Orkes’ unpublished operator guide, the descriptions of transaction modes omitted basic phenomena like G0 and confused G1a (Aborted Read) with G1c (Circular Information Flow). The Snapshot Isolation modes made no mention of G-single or the other boundary phenomena proscribed by Snapshot Isolation, and non-real-time models made no mention of the real-time phenomena they allowed, like Stale Read.14
Technical writing is important work that requires skill and care. Jepsen recommends that Orkes work with a human technical writer to rewrite ParticleDB’s documentation, with an eye toward concisely and accurately communicating intended behavior and the guarantees users should expect.
Jepsen is no stranger to working with early software projects, and there is nothing intrinsically wrong with offering buggy or badly broken software. However, it would be polite for a maintainer aware of serious safety issues to display a prominent warning on the website. One might place a large red box on the download page which reads, “This project is in early development and is not suitable for production”, or more bluntly, “This software has lost data and will do so again”. Orkes should clearly explain the general status of ParticleDB, and advise users of specific known safety issues.
Orkes has since published a release history which describes versions 1.1.4 through 1.1.18.11.rc-22 as “unsupported pre-release builds”. It is not clear how users would have understood versions like 1.1.4 or 1.1.18.9 as “pre-release”. Pre-release software generally has a version number beginning with 0, or including “rc”, “alpha”, etc. The documentation did not describe these versions as pre-release or experimental at the time. Indeed, the Docker install page told users how to deploy these versions “for production”.
LLM Tradeoffs
Large Language Models enable small teams to produce software and documentation of staggering scope and complexity in a remarkably short time. Five years ago, writing a distributed SQL database of this scope might have taken hundreds of person-years.15 With LLM assistance, a handful of engineers at Orkes were able to produce a highly sophisticated database system.
However, our work on ParticleDB (and other databases) suggests there are serious tradeoffs to relying on LLMs. Quality suffered: ParticleDB was a fractal of correctness errors. LLM tooling repeatedly claimed that bugs had been fixed when, in fact, they had not. ParticleDB also seemed more difficult to understand than typical databases. As with most Jepsen analyses, Orkes engineers offered detailed explanations of the causes and resolutions of the bugs we found in Conductor. However, Orkes seemed unable to explain the causes or resolutions of nearly every issue we found in ParticleDB. New builds simply materialized, sans changelogs, each hopefully containing a slew of improvements. Why did ParticleDB sometimes turn strings into integers? Why did it stop in 1.1.18.11.rc-1? Perhaps no one knows. With LLMs, perhaps no one needs to.
More broadly, the use of LLMs erodes trust in documentation, in software, and in the organizations which produce them. Perhaps engineers will begin to treat any new software project with suspicion—the signals we used to rely on as markers of a mature system are now easier to counterfeit.
Where correctness matters—and surely it ought to matter for databases!—our industry must acknowledge and mitigate these tradeoffs.16 In the age of LLMs, what can maintainers do to ensure correct software and accurate documentation? Having done that work, how can they demonstrate its quality to users?
Future Work
As of 1.1.18.11.rc-22, our append and rw
workloads were still finding anomalies in every transaction mode. We
recommend further testing as development continues. The
compare workload also revealed a number of minor
compatibility bugs which we have not discussed here. These small
differences could mask more serious errors. Bringing ParticleDB into
closer alignment with Postgres could help the compare
workload reveal them.
Toward the end of our collaboration we tested ParticleDB’s on-disk format, and found that corruption in WAL files could cause data loss. The variety of error messages we found suggests that there might be multiple paths to data loss—further tests should stress on-disk storage more thoroughly. Furthermore, in almost all our tests ParticleDB wrote only WAL files, and stored its tables entirely in memory. We recommend Orkes add a configuration option to reduce the thresholds for on-disk storage, so that future tests can exercise those codepaths.
The ParticleDB client has a number of slow network timeouts we did not know how to change. These timeouts could conceal safety errors by preventing the test from observing the database during a critical moment. We worked around this by using the Postgres client (whose timeouts are configurable), but we recommend testing the ParticleDB client with lower timeouts as well.
ParticleDB offered a massive set of features, and we only tested a small portion of them. The SQL specification and Postgres’ dialect of it are astonishingly complex languages, and we used only a small fraction of their syntax and vocabulary. We also did not evaluate ParticleDB’s Redis or HTTP APIs, temporal or geospatial queries, materialized views, vector search, or multi-tenant features. The replication system remains to be tested.
Orkes is now adding stored procedures to ParticleDB. These user-defined functions, called Particles, are written in Python or Rust and executed by the database server. Both Rust and Python have large surface areas, and executing user-provided binaries in a database environment seems like an exciting opportunity. Our previous work with stored procedures suggests this feature is worth further investigation.
ParticleDB claimed to be “1087x faster than PostgreSQL”. In our tests ParticleDB was nowhere near this fast, and over the course of an hour we observed a linear decline in throughput and a significant increase in median latencies—even over small datasets. Once ParticleDB reaches a baseline level of correctness, revisiting performance benchmarks could be worthwhile.
Our thanks to the ParticleDB team at Orkes, including Jeu George and Nikhil Kala. Jepsen also wishes to thank Irene Kannyo for her editorial support. This research was funded by Orkes, Inc., and conducted in accordance with the Jepsen ethics policy.
Appendix A
This appendix contains additional results involving types, syntax, numbers, and strings.
Type Safety and Syntax
We found several incompatibilities or bugs involving static or dynamic type safety, or behaviors that would normally have been caught by static analysis of a query, such as referring to a nonexistent column. These issues occurred with a single server, single client, and no faults.
Ordering By an Unused Column (#33)
In ParticleDB 1.1.4 through 1.1.18.8, you could order an aggregate
query’s results by a column that was not used in the aggregate
expressions or GROUP BY clause. This seems like a minor
issue, but it does differ from Postgres behavior:
root=> CREATE TABLE t (a boolean);
root=> SELECT COUNT(*) FROM t
WHERE (a >= a) ORDER BY a LIMIT 0;
COUNT
-------
(0 rows)Orkes resolved this issue (#33) in 1.1.18.9.
Comparison of Incomparable Types (#6)
In ParticleDB 1.1.4 through 1.1.18.4, you could write predicates for
UPDATE and DELETE which made equality
comparisons between columns of different types.
root=> CREATE TABLE t (a text, b smallint);
root=> DELETE FROM t WHERE (a = b);
DELETE 0In Postgres, this threw an error like
ERROR: operator does not exist: text = smallint. You could
compare strings to boolean columns, or literal values like
false. ParticleDB allowed these comparisons on empty
tables, but would throw an error if a record was present. In 1.1.18.6,
ParticleDB threw an error for empty tables as well (#6).
Cannot Compare Decimals to Floats (#94)
ParticleDB 1.1.18.11.rc-9.2 forgot how to compare large decimal literals to 64-bit floating-point numbers:
root=> CREATE TABLE t (a double precision);
root=> INSERT INTO t (a) VALUES (1.0);
root=> SELECT * FROM t WHERE
((0 / 1.2002480805601245E26) >= t.a);
ERROR: arrow compute: Invalid argument error:
Invalid comparison operation:
Decimal256(76, 44) >= Float64This issue (#94) no longer appeared by version 1.1.18.11.rc-18.
Filter Predicates Reject
NULL (#12)
SQL has three boolean values: TRUE, FALSE,
and NULL, which represents an unknown value. In ParticleDB
1.1.4 through 1.1.18.4, predicates in SELECT, UPDATE, etc. could not
evaluate to NULL, even though the SQL specification states
that search conditions are boolean expressions. For example:
root=> CREATE TABLE t (a boolean);
root=> INSERT INTO t (a) VALUES (TRUE);
root=> SELECT * FROM t WHERE NULL;
ERROR: filter predicate must produce boolean
resultIn Postgres this would return an empty relation, because
NULL is not TRUE. In 1.1.18.6,
NULL predicates evaluated correctly (#12).
Cannot Compare Values to
NULL (#13)
Per the ANSI SQL standard, the value of comparing anything to
NULL is unknown, represented as the boolean value
NULL. However, ParticleDB 1.1.4 through 1.1.17 sometimes
refused to evaluate predicates which compared (e.g.) integers to
NULL.
root=> CREATE TABLE t (a int);
root=> INSERT INTO t (a) VALUES (2), (NULL);
root=> UPDATE t SET a = 5 WHERE a = NULL;
ERROR: arrow compute: Invalid argument error:
Invalid comparison operation: Int64 == NullOddly enough, ParticleDB correctly evaluated statements like
SELECT 1 = NULL. Version 1.1.18.6 correctly compared
integers to NULL (#13).
Boolean Logic on Integers (#30)
The SQL standard is clear that boolean logical expressions like
A AND B evaluate to booleans. In ParticleDB 1.1.4 through
1.1.18.8, they could sometimes be other things:
root=> SELECT TRUE AND 5; # => 5
root=> SELECT 5 AND TRUE; # => 5
root=> SELECT 5 AND FALSE; # => FALSE
root=> SELECT 5 OR TRUE; # => TRUE
root=> SELECT 'cat' OR FALSE; # => 'cat'
root=> SELECT 'cat' OR 'dog';
ERROR: OR requires boolean operands
root=> SELECT 1 AND 2;
ERROR: AND conjunct must produce booleanBy version 1.1.18.9, ParticleDB rejected boolean expressions over non-boolean literals (#30).
More Boolean/Integer Confusion (#50)
ParticleDB 1.1.18.9 disallowed boolean logic on integer literals, but allowed it on columns.
root=> CREATE TABLE t (a int);
root=> UPDATE t SET a = NOT a;
UPDATE 0Likewise, you could do integer operations on booleans:
root=> CREATE TABLE t (a boolean);
root=> UPDATE t SET a = ((a + a) IS NULL)
WHERE a;
UPDATE 0Orkes seems to have resolved this issue (#50) by version 1.1.18.11.rc-8.
Multiple Assignment to a Single Column (#5)
The SQL standard specifies that an UPDATE statement
shall not contain a column name more than once. However, ParticleDB
1.1.4 through 1.1.18.4 allowed an UPDATE to assign multiple
values to the same column. It seemed to pick the last value,
lexically.
root=> CREATE TABLE t (a boolean, b boolean);
root=> INSERT INTO t (a, b)
VALUES (FALSE, FALSE);
root=> UPDATE t SET a = FALSE, a = TRUE;
UPDATE 1In Postgres this returned an error:
multiple assignments to same column "a". However,
ParticleDB applied the update, producing:
root=> select * from t;
a | b
---+---
t | f
(1 row)By version 1.1.18.6, multiple updates to the same column threw an error (#5).
Predicates Referring to Non-Existent Columns (#10)
In ParticleDB 1.1.4 through 1.1.17, predicates in
SELECT, UPDATE, and DELETE
statements could refer to columns which did not exist. For example:
root=> CREATE TABLE t (a INT);
root=> SELECT * FROM t WHERE b = 1;
a
---
(0 rows)In Postgres, expressions like this would throw
ERROR: column "b" does not exist. By version 1.1.18.6,
ParticleDB did as well (#10).
Arithmetic
ParticleDB often performed arithmetic in unexpected ways. These issues occurred with a single server, single client, and no faults.
No Large Integer Literals (#14)
Postgres allows large integer literals like
9223372036854775808 (263). However, ParticleDB 1.1.4
through 1.1.17 refused these literals, throwing an
invalid integer error:
root=> SELECT 9223372036854775808;
ERROR: invalid integer: 9223372036854775808In ParticleDB 1.1.18.6, one could write large integer literals (#14).
To Infinity, and Beyond (#85)
ParticleDB 1.1.18.9 gained an exponentiation operator ^,
but could not apply it to Infinity.
SELECT 'Infinity' ^ 2;This returned 'Infinity' in Postgres, but threw
operator does not exist: ^ requires numeric operands in
ParticleDB. By 1.1.18.11.rc-9, Orkes seems to have resolved this issue
(#85).
-1 ^ 0 = -1
(#49)
ParticleDB 1.1.18.9 considered -1 ^ 0 to be equal to
-1, rather than 1.
root=> select -1 ^ 0;
-1
----
-1
(1 row)By version 1.1.18.11.rc-3, it correctly returned 1 (#49).
1.0 + 1E-39 = 1.0 (#88)
ParticleDB 1.1.18.6 gained support for literals using scientific
notation—for example, using 3E12 to denote 3 × 1012. However, the parser
turned this notation into floating-point values, rather than precise
decimals, as in Postgres. This resulted in floating-point errors:
root=> SELECT 1.0 + 1E-39 = 1.0;
?column?
----------
t
(1 row)This issue (#88) was fixed by 1.1.18.11.rc-9.2.
What’s the Largest Number You Can Think Of? (#56)
ParticleDB 1.1.4 through 1.1.18.11.rc-1 silently converted large floating-point literals to infinities:
root=> SELECT -4.56E500;
?column?
-----------
-Infinity
(1 row)By version 1.1.18.11.rc-3, ParticleDB threw an error like
"4.56E500" is out of range for type double precision (#56).
Exponent Underflow (#61)
In ParticleDB 1.1.18.9, the exponent operator checked to see if its arguments would cause an overflow, but did not check for underflow:
root=> SELECT 200 ^ 200;
ERROR: value out of range: overflow
root=> SELECT -200 ^ -200;
-0
----
0
(1 row)By version 1.1.18.11.rc-3, ParticleDB threw an underflow error (#61).
Division by Integer Zero (#34)
In ParticleDB 1.1.4 through 1.1.18.8, you could divide floating-point
values by integer 0 and receive Infinity,
rather than a division by zero error, as in Postgres.
root=> SELECT 1.0 / 0;
1 / 0
-------
inf
(1 row)ParticleDB 1.1.18.9 returned an error instead (#34).
Modulo Operator Cannot Handle Floats (#51)
In ParticleDB 1.1.18.9, the modulo operator % threw an
internal error when either argument was a floating-point number.
root=> SELECT 3.0 % 2;
ERROR: internal errorThe MOD function was unaffected, and this issue (#51) was
fixed by 1.1.18.11.rc-1.
Broken NaN
Arithmetic (#36)
In ParticleDB 1.1.4 through 1.1.18.8, arithmetic operations on
NaN would throw an error:
root=> SELECT 'NaN' - 1.0;
ERROR: arrow compute: Invalid argument error:
Invalid arithmetic operation: Utf8 - Float64By 1.1.18.9, arithmetic on NaN correctly returned
NaN (#36).
Broken NULL
Arithmetic (#92)
In ParticleDB 1.1.18.11.rc-9.2, arithmetic operations involving
sufficiently detailed scientific notation literals and NULL
threw internal errors:
root=> CREATE TABLE t (a int);
root=> INSERT INTO t (a) VALUES (NULL);
root=> SELECT * FROM t WHERE
(t.a + 2.0000000000000000E-23) IS NULL;
ERROR: internal error: called `Option::unwrap()`
on a `None` value (at crates/exec/...)This error (#92) no longer appeared in version 1.1.18.11.rc-18.
UPDATE Can’t Do Math (#60)
ParticleDB 1.1.18.9 introduced a new bug which rendered it unable to
do arithmetic operations on floating-point columns in an
UPDATE statement.17 For example:
root=> CREATE TABLE t (a real);
CREATE TABLE
root=> INSERT INTO t (a) VALUES (1.2);
INSERT 0 1
root=> UPDATE t SET a = a + 1;
ERROR: internal errorIn 1.1.18.11.rc-1, ParticleDB was (mostly) able to add numbers (#60).
Same Precision For
real and double precision (#32)
Per the SQL standard, the double precision data type
should have a greater precision than real. In Postgres,
real and double precision are (usually) a
32-bit and 64-bit floating-point value, respectively. In ParticleDB,
both appear to be implemented as 64-bit floats.
CREATE TABLE t (a real NOT NULL);
INSERT INTO t (a) VALUES (4.134917993754883E8);
SELECT * FROM t ORDER BY a;
ParticleDB:
[{:a 4.134917993754883E8}]
Postgres:
[{:a 4.134918E8}]Version 1.1.18.9 seemed to introduce a 32-bit real type
(#32).
Big Integers (#11)
Similarly, integer fields (e.g. smallint,
int, …) in ParticleDB 1.1.4 through 1.1.17 allowed values
much bigger than their typical ranges. It appeared as if ParticleDB
internally represented all integer types as signed 64-bit integers. For
example, here are four values too big to fit in a Postgres
smallint:
root=> CREATE TABLE t (a smallint);
root=> INSERT INTO t (a) VALUES (32768);
root=> INSERT INTO t (a)
VALUES (9223372036854775807);
root=> INSERT INTO t (a)
VALUES (9223372036854775808);
root=> INSERT INTO t (a) VALUES (1.234E40);
root=> SELECT * FROM t;
a
---------------------
32768
9223372036854775807
0
9223372036854775807
(5 rows)In Postgres smallint is a 16-bit integer; each
INSERT would throw
ERROR: smallint out of range. However, ParticleDB stored
each of these values with varying results. It preserved 32,768 and 263 − 1 (the largest
two’s-complement signed 64-bit integer). 263 and larger integers were
silently converted to 0. On the other hand, the large
floating-point number was clipped to 263 − 1.
Similar behavior occurred with int and
bigint. ParticleDB 1.1.18.6 rejected oversized integers (#11).
Dividing Nearby Numbers (#86)
ParticleDB 1.1.18.9 through 1.1.18.11.rc-8 struggled to divide
floating-point numbers, often throwing Numeric out of range
when asked to divide values which were well within the range of a 32-bit
float. For example:
root=> SELECT 81938.0 / 81937.82012987136848;
ERROR: numeric out of rangeThis value should be approximately 1.00000219. Orkes
seems to have resolved this issue (#86) in
version 1.1.18.11.rc-9.
Floating-Point Errors in Integer Values (#87)
In ParticleDB 1.1.18.11.rc-1 through 1.1.18.11.rc-8, adding small
numbers (e.g. 0.1) to large integer columns could cause
their values to change by surprising amounts. For example:
root=> CREATE TABLE t (a bigint);
root=> INSERT INTO t (a) VALUES
(58950791971326900);
root=> UPDATE t SET a = (a + 1E-1);
root=> SELECT a FROM t;
a
-------------------
58950791971326896Adding 0.1 to this column actually subtracted
4. Jepsen suspects this bug (#87) was
caused by performing these operations on floating-point values (which
cannot represent all integers) then coercing the result back to the
nearest integer value. Version 1.1.18.11.rc-9 seems to have fixed
this.
Floats Are Not Equal to Quoted Forms (#19)
Postgres considers floating-point numbers like 0.0 to be
equal to their quoted representations—this is useful because some
floating-point values, like 'Infinity', are written with
quotes. ParticleDB 1.1.4 through 1.1.18.4 thought otherwise:
root=> SELECT 0.0 = '0.0';
false
-------
f
(1 row)This issue (#19) appeared resolved as of version 1.1.18.6.
Floats Are Not Equal to Integers (#63)
In ParticleDB 1.1.4 through 1.1.18.6, floats were not equal to integers.
root=> SELECT 0.0 = 0;
false
-------
f
(1 row)By version 1.1.18.8, reasonably-sized integers were equal to their floating-point counterparts (#63).
Double Precision Floats Appear Equal to Almost All Integers (#80)
Version 1.1.18.9 introduced a new bug which made predicates like
WHERE x != y fail to match for most double precision floats
in (-1.0, 1.0). It was as if all small floats were equal to all nonzero
integers. If we took a table containing a few integers, and performed
some simple queries that should return every row…
CREATE TABLE t (a int);
INSERT INTO t (a) VALUES (-1), (0), (5);
SELECT * FROM t WHERE a != (0.1 ^ 25);
SELECT * FROM t WHERE
a != 0.90000000000000000000000000000000000000;
SELECT * FROM t WHERE
a != -0.34656654456890453890613489540952456545;All three of these queries returned only a single row,
0—implying that 0.90... was equal to
-1 and 5. The only exception seemed to be
WHERE a != 0.0..., which returned no rows at all. This was
also wrong, but somewhat less so. Version 1.1.18.11.rc-9 seems to have
resolved the matter (#80).
Strings
We found several problems with strings in ParticleDB, including datatypes that could hold more or fewer characters than they ought, and improper sort orders. These issues occurred with a single server, single client, and no faults.
CHARACTER(n)
Allows More Than n Characters (#64, #52)
The SQL standard defines a variety of fixed-width string datatypes
like character(3), which holds a string of exactly three
characters. Unfortunately, ParticleDB 1.1.4 through 1.1.18.4 were happy
to store much larger strings in those columns.18
root=> CREATE TABLE t (a character(3));
root=> INSERT INTO t
VALUES ('large language model');
root=> SELECT * FROM t;
a
----------------------
large language model
(1 row)Version 1.1.18.6 rejected inserts and updates of too-long string
literals (#64), but
appeared to perform these checks only at runtime; one could still say
UPDATE t SET a = 'far too long' so long as it matched no
rows (#52). That
issue was resolved in version 1.1.18.11.rc-3.
CHARACTER(n)
Allows Fewer Than n Characters (#55)
While 1.1.18.6 rejected strings that were too large for a fixed-width string column, 1.1.4 through 1.1.18.9 also allowed strings that were too short. Normally, shorter strings are padded with spaces to the specified length, but in ParticleDB, short strings were returned unchanged.
CREATE TABLE t (a character(16));
INSERT INTO t VALUES ('smol');
SELECT * FROM t;
-> {:a "smol"}This should have returned "smol ", with
padding spaces. ParticleDB 1.1.18.11.rc-1 seems to have resolved the
issue (#55).
Default Text Collation (#22)
On a platform with LC_COLLATE = en_US.UTF-8, Postgres
uses that collation by default. ParticleDB appeared to collate strings
differently. Sorting via queries like
SELECT ... ORDER BY name, or comparisons like
'a' < 'B', returned different results between the two
databases. Where Postgres considered 'a' < 'B',
ParticleDB believed the opposite.
Experiments suggest ParticleDB sorted by Unicode code point order,
rather than the Unicode
Collation Algorithm (UCA) with the locale’s collation rules. It may
have been compatible with ucs_basic, C, or
pg_c_utf8. ParticleDB version 1.1.18.9 seemed to choose a
default collation more like UCA (#22).
Mis-Sorted Text (#42, #93)
Throughout our collaboration ParticleDB struggled to sort things.
Versions 1.1.4 through 1.1.18.8 accepted an explicit
unicode text collation, then ignored it:
root=> CREATE TABLE t (b text COLLATE unicode);
root=> INSERT INTO t (b)
VALUES ('a'), ('b'), ('B');
root=> SELECT * FROM t ORDER BY b;
b
---
B
a
b
(3 rows)In Postgres’ unicode collation, letters like
a and A sort similarly, despite being of
different cases. This query would have returned
'a', 'b', 'B'.
ParticleDB 1.1.18.9 sorted English uppercase and lowercase letters
correctly, but only for some queries—adding a LIMIT clause
to a SELECT statement would cause it to revert to the old
sort order. Version 1.1.18.11.rc-1 resolved that, but sorted other
characters, like punctuation, oddly:
root=> CREATE TABLE t (a text COLLATE unicode);
root=> INSERT INTO t VALUES
('_aa'), ('a'), ('aa'), ('aaa');
root=> SELECT * FROM t ORDER BY a ASC;
a
-----
a
_aa
aa
aaa
(4 rows)Different sorting of whitespace and punctuation (#42) seemed better in version 1.1.18.11.rc-7, but reappeared in 1.1.18.11.rc-9 through 1.1.18.11.rc-19 (#93). Text sorting seemed improved in 1.1.18.11.rc-21.
There are also a handful of unrelated databases called ParticleDB, mainly used in particle physics—for example, the database of particles used in the Belle II physics experiment, or this one at the Large Hadron Collider.↩︎
The Particles feature was not available during our work; we focus on ParticleDB’s SQL interfaces.↩︎
One envisions a world of meta-recursive database documentation in the style of Raymond Smullyan. “One page of the documentation always tells the truth. Another tells only lies…”↩︎
Presumably Two-Phase Locking.↩︎
With apologies to Deleuze & Guattari—Thus, the tuple without fields is opposed less to fields as such than to the organization of the fields insofar as it composes a row. Le n-uplet sans composants is not a deleted tuple but a living tuple all the more alive and teeming once it has blown apart the client and its memory model. Structs commingling on the heap. Self-modifying Perl. The full tuple without fields is a tuple populated by multiplicities.↩︎
Madred, 2369.↩︎
He explained that the Database—the Denormalized, the Data Lakehouse, the giant Monad—is a table without tuples. Le tableau sans n-uplets is permeated by unformed, unstable values, by dataflows in all directions, by free JSON or nomadic singletons, by mad or transitory transactions. That, however, was not the question at hand. For there simultaneously occurs upon the database a very important, inevitable phenomenon that is beneficial in many respects and unfortunate in many others: schematization. Schemata are Layers, Belts. They consist of giving form to values, of imprisoning arrays or locking singletons into systems of record and replication, of producing upon the body of the database records large and small and organizing them into normal aggregates. Schemata are acts of capture, they are like “black holes” or occlusions striving to seize whatever comes within their reach. They operate by coding and normalization upon the database; they proceed simultaneously by code and normality. The Schemata are judgements of God; schematization in general is the entire system of the judgement of God (but the database, or the table without tuples, constantly eludes that judgement, flees and becomes deschematized, destructured, denormalized).↩︎
To make matters more confusing,
serializablein versions 1.1.4 through 1.1.18.11.rc-11.2 was (depending on which version of the binary you asked) either an alias fortable-2plor possiblyocc-v3. In 1.1.18.11.rc-22,serializablereappeared as a new Strong Serializable transaction mode, also based on Serializable Snapshot Isolation (SSI), but perhaps not theserializable-ssilevel present in earlier builds, which was distinct fromstrict-serializable.↩︎De Lioncourt, 1793, presaging Borges, 1941.↩︎
See ANSI/NISO Z39.19-2005 (R2010) “Celestial Emporium of Benevolent Knowledge” for the full taxonomy of database records: rows belonging to the DBA, tombstones, those which from afar look like JSON, et cetera, etc.↩︎
Jepsen encourages the consistency model research community to come up with more official terms for this.↩︎
Running
particledb --versionmight not return the actual version. For instance, 1.1.16 through 1.1.18.11.rc-18 all identified themselves as “spanner 1.1.16”.↩︎Barthes can rejoice: not only is the author dead, but they were never alive to begin with. Nothing is true, everything is permitted.↩︎
MongoDB had 962 employees in 2018, and Cockroach Labs had 400 in 2022. Databases are hard!↩︎
Or perhaps not. Not every application requires perfect correctness. Vibrational Database Management Systems might be fine, at least some of the time.↩︎
Danielewski, 2000.↩︎
来源:Hacker News · jepsen.io