@drupflare/durabledb

中文文档
0.1.4发布于 2026/10/01MITTypeScriptES Module构建来源已验证近 30 天下载 2124

A codec that encodes the real platform limits of Cloudflare Durable Object SQLite: 100 bound parameters, a 50-byte LIKE pattern, and lossy integers above 2^53.

原始使用文档
文档目录10 节
  1. 📋 Table of Contents
  2. 🎯 Why
  3. 📥 Install
  4. 🧱 Limits
  5. 🔁 Codec
  6. 🔗 Bridge Contract
  7. 🔧 API
  8. 🧪 Testing
  9. 🔗 Related Repositories
  10. 📄 License

🗄️ durabledb

The real limits of Cloudflare Durable Object SQLite, encoded rather than documented

Build Prettier codecov License

A ctx.storage.sql host and a value codec that encode what the platform actually does, not what SQLite documents. ctx.storage.sql caps a statement at 100 bound parameters where local PDO allows 32,766, refuses a LIKE pattern over 50 bytes, has no named parameters at all, and reads integers above 2^53 lossily. Every one of those is a measured refusal from a deployed object, and every one has produced a defect that a local SQLite passed.


📋 Table of Contents


🎯 Why

A Durable Object's SQLite is a genuinely good home for a small database: strongly consistent, in the same isolate as the code, and synchronous to read so blocking callers compose with it. Then you write against it as though it were SQLite and it refuses things SQLite does not.

The refusals are not documented as a set, they are not tunable, and each one fails in a different register: a thrown error for the parameter cap, a silently wrong number for a wide integer. A value that comes back wrong rather than erroring cannot be told apart from correct output until much later.

So the limits live in code, as named constants and named refusals, with a test per limit.


📥 Install

bun add @drupflare/durabledb

Three entry points, and the split is not cosmetic:

ImportGives youNeeds @drupflare/cartridge
@drupflare/durabledbeverything belowyes
@drupflare/durabledb/codecencode, decode, codecGuard, PHP_CODECno
@drupflare/durabledb/do-sqliteSiteDurableObject, toPositional, bindableyes

src/do-sqlite.ts imports @drupflare/cartridge/gate and /mask; src/codec.ts imports nothing at all, so a consumer that only needs the codec pays for no dependency. There is no deep import: the exports map is the whole surface, and @drupflare/durabledb/src/codec.ts is refused by the resolver.


🧱 Limits

Each one broke something real. None is a conservative guess.

LimitValueWhat it broke
bound parameters per statement100the cache write path; a cold cache_discovery set needed 574
LIKE / GLOB pattern length50 bytesbinds plain LIKE, not only GLOB, so it is invisible to a caller
bytes per record2,199,995a heap snapshot row
statement text100,000 charsa chunked DDL replay
integer readslossy above 2^53wrote 9007199254740993, read back 9007199254740992
named parametersnone existevery Drupal query; toPositional() converts them

Two carry a further consequence:

Writing a wide integer is exact; reading one is not. ctx.storage.sql hands INTEGER columns back as JS doubles, so the precision is gone before any consumer can see it. CAST(col AS TEXT) returns every digit, so the storage is fine; the loss is in the cursor. UnreadableIntegerError exists to make that a refusal rather than a wrong answer.

"There is no smaller unit than a row" is false, and it was this project's own unverified claim, believed for a while. Three 520 KB rows each overran the record cap and looked indivisible; SQLite builds a value across statements with col = col || ?.

[!WARNING] Never widen a limit because a test passes. These came from a deployed object, and a local SQLite will happily accept all of them. If you think one is wrong, re-measure on a deployed worker and say so. Do not relax a guard.


🔁 Codec

The consumer runtime is a 32-bit PHP wasm build (PHP_INT_SIZE is 4), so any JS number at or above 2^31 wraps silently on the way in. Measured: Date.now() arrived in PHP as -397708726 instead of ~1.78e12. Two instances were found separately, a timestamp and a node id, and finding two separately means the class was still open.

The obvious fix, stringifying anything unsafe, is lossy in the other direction. PHP receives "1780000000000" and cannot tell whether that was an integer too large to represent or a genuine string; sending it back produces a string where a number belongs, and the corruption reappears one hop later.

So values that cannot cross natively are wrapped in a tagged envelope instead of flattened, and decode() is the exact inverse of encode():

EnvelopeCarries
{__t: 'i', v: '1780000000000'}integer outside 32-bit range
{__t: 'n', v: '1.5e300'}non-finite or precision-risky float
{__t: 'd', v: '1780000000000'}Date
{__t: 'b', v: '<base64>'}bytes
{__t: 'u'}undefined, which PHP has no word for

Anything representable on both sides crosses unwrapped, so the common path costs nothing.


🔗 Bridge Contract

Two entry points, and the second is the interesting one.

execSql(sql: string, params?: SqlBindings): ExecSqlResult
execTxn(req: TxnRequest): ExecTxnResult

execTxn runs a list of statements inside one ctx.storage.transactionSync(), optionally with a trailing read. commit: false runs them, evaluates the read inside the same transaction, then throws a private sentinel so the runtime rolls back, and still returns the results, which is what makes a speculative row count and insert id possible.

ExecTxnResult is a discriminated union rather than one shape with optionals: a failed replay has already been rolled back, so there are no results to read, and the discriminant is what stops a caller reading them anyway.


🔧 API

ExportWhat it is
SiteDurableObjectthe Durable Object; execSql, execTxn, installBridge, nowMs
encode / decodethe codec, exact inverses of each other
codecGuard(host)wraps a host surface so every value crossing it is encoded
PHP_CODECthe PHP half of the codec, as source, for hosts that evaluate it
toPositional(sql, params)rewrites named parameters, because the engine has none
bindable(value)the one value transform sql.exec() needs; a JS BigInt is refused
UnreadableIntegerErrora read above 2^53, refused rather than answered wrongly
ExecSqlResult{ rows, rowsRead, rowsWritten, lastInsertRowid, changes }
TxnRequest / TxnStatement / ExecTxnResultthe replay contract above
SqlBindingsunknown[] | Record<string, unknown> | null

🧪 Testing

bun run typecheck
bun run test # 87 assertions across 3 specs
bun run test:coverage

87 passing, 0 failed, at a measured 61.68% statements (codec.ts 82.29%, do-sqlite.ts 49.69%: the Durable Object routes and alarm() need a real ctx.storage.sql, so they are covered in the consumer rather than here). The lane runs in node, not workerd, and coverage uses provider: 'v8' rather than istanbul. The sibling repositories split on exactly that axis: a workerd lane must use istanbul, because the v8 provider reads coverage off the Node inspector and attributes zero from inside the isolate; a node lane uses v8.

Two rules the suite is built on:

  • The codec must round-trip. A codec that can decode a type it cannot encode is a bug to fix, not a test to skip.
  • Fail with a named refusal rather than truncating. A silently truncated value cannot be told apart from correct output until much later.

RepositoryWhat it is
drupflare/workerthe consumer: Drupal 11 on Cloudflare Workers
drupflare/romcomposer require drupflare/rom:0.*, the Drupal 11 driver that sits on top
drupflare/cartridgethe reentrancy gate and interrupt mask this package imports

📄 License

MIT (c) Gregory Mitchell 2026. See LICENSE.

版本与发布记录

@drupflare/durabledb 共发布 5 个版本,首次发布于 2026/08/14;过去 12 个月发布了 5 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.4latest2026/10/011 个月前
0.1.32026/08/292 个月前
0.1.22026/08/222 个月前
0.1.12026/08/152 个月前
0.1.02026/08/143 个月前

关于 @drupflare/durabledb 的常见问题

@drupflare/durabledb 是什么?

A codec that encodes the real platform limits of Cloudflare Durable Object SQLite: 100 bound parameters, a 50-byte LIKE pattern, and lossy integers above 2^53.

如何安装 @drupflare/durabledb?

在项目目录运行 npm install @drupflare/durabledb,或使用 pnpm add / yarn add / bun add。

@drupflare/durabledb 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@drupflare/durabledb 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@drupflare/durabledb 可以商用吗?

@drupflare/durabledb 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@drupflare/durabledb 的下载量有多少?

根据 npm 官方下载统计,@drupflare/durabledb 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,124 次,日均约 71 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@drupflare/durabledb 还在维护吗?

最新版本 0.1.4 发布于 2026/10/01(1 个月前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/08/14。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。