{"_id":"@drupflare/durabledb","_rev":"5-14f341b38f1ccdf53b4245518aabb07a","name":"@drupflare/durabledb","dist-tags":{"latest":"0.1.4"},"versions":{"0.1.0":{"name":"@drupflare/durabledb","version":"0.1.0","keywords":["cloudflare","durable-objects","sqlite","sql","codec","workers"],"author":{"name":"Gregory Mitchell","email":"me@gmitch215.xyz"},"license":"MIT","_id":"@drupflare/durabledb@0.1.0","maintainers":[{"name":"gmitch215","email":"me@gmitch215.xyz"}],"homepage":"https://github.com/drupflare/durabledb","bugs":{"url":"https://github.com/drupflare/durabledb/issues"},"dist":{"shasum":"d5cbc5b0f880b572b82c02597f89ea534455ae3a","tarball":"https://registry.npmjs.org/@drupflare/durabledb/-/durabledb-0.1.0.tgz","fileCount":6,"integrity":"sha512-YwX5/d3gzrQQRbw3fLB/jnV7zia8H1vCn7yg/I4QygP4tXhEhTKF+tdMmSugpvkXlaCWI8GL0qNEKWNbyn1unw==","signatures":[{"sig":"MEQCIHY7FR9CPv/AUi27kASVG6BAr5Mgw1tiREkSWXOZ3hJQAiAWIblkpKNkjacxldX/hS1KL6wBAoMghal1yU4kzXO+oQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":43112},"main":"./src/index.ts","type":"module","types":"./src/index.ts","exports":{".":"./src/index.ts","./codec":"./src/codec.ts","./do-sqlite":"./src/do-sqlite.ts","./package.json":"./package.json"},"gitHead":"2da15caeb31ed0ba6653c172444b3afefe3434eb","scripts":{"test":"vitest run","prepare":"husky","prettier":"prettier --write .","typecheck":"tsc -p tsconfig.json --noEmit","docs:build":"typedoc","docs:serve":"typedoc --watch","test:watch":"vitest","test:coverage":"vitest run --coverage","prettier:check":"prettier --check ."},"_npmUser":{"name":"gmitch215","email":"me@gmitch215.xyz"},"repository":{"url":"git+https://github.com/drupflare/durabledb.git","type":"git"},"_npmVersion":"11.6.1","description":"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.","directories":{},"lint-staged":{"*.{ts,js,mjs,css,md,json,jsonc}":"prettier --write"},"sideEffects":false,"_nodeVersion":"24.11.0","dependencies":{"@drupflare/cartridge":"^0.1.0"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","vitest":"^4.1.0","typedoc":"^0.28.20","prettier":"^3.9.6","typescript":"^7.0.0","lint-staged":"^17.3.0","prettier-plugin-sh":"^0.19.0","@vitest/coverage-v8":"^4.1.0","@cloudflare/workers-types":"^5.20260811.1","prettier-plugin-organize-imports":"^4.3.0"},"_npmOperationalInternal":{"tmp":"tmp/durabledb_0.1.0_1786685782565_0.814192165247507","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@drupflare/durabledb","version":"0.1.1","keywords":["cloudflare","durable-objects","sqlite","sql","codec","workers"],"author":{"name":"Gregory Mitchell","email":"me@gmitch215.xyz"},"license":"MIT","_id":"@drupflare/durabledb@0.1.1","maintainers":[{"name":"gmitch215","email":"me@gmitch215.xyz"}],"homepage":"https://github.com/drupflare/durabledb","bugs":{"url":"https://github.com/drupflare/durabledb/issues"},"dist":{"shasum":"9585033c85b1c8839b9897010c915942c5015836","tarball":"https://registry.npmjs.org/@drupflare/durabledb/-/durabledb-0.1.1.tgz","fileCount":6,"integrity":"sha512-ie3kI7bDxlww6uKHegdlYIzPSYJPtN4rb7qrXQCL9hN61wljO9LhxuMtJLMOvv3FKDM1n1uSbsD/9MDULNZcCQ==","signatures":[{"sig":"MEUCIH0K34a+A1GuabtsaWhScrqyfJ3uI3lEcP8U3ghodfyEAiEAyJBCDhpMThGgpMN90IF8dWON6V7DW8+Av+lmttCV3mI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@drupflare%2fdurabledb@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":43114},"main":"./src/index.ts","type":"module","types":"./src/index.ts","exports":{".":"./src/index.ts","./codec":"./src/codec.ts","./do-sqlite":"./src/do-sqlite.ts","./package.json":"./package.json"},"gitHead":"139448976db5484891930a5a9f8a9f00af80300a","scripts":{"test":"vitest run","prepare":"husky","prettier":"prettier --write .","typecheck":"tsc -p tsconfig.json --noEmit","docs:build":"typedoc","docs:serve":"typedoc --watch","test:watch":"vitest","test:coverage":"vitest run --coverage","prettier:check":"prettier --check ."},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1c5230ae-c2f0-4ee9-bdb8-53298efbad8b"}},"repository":{"url":"git+https://github.com/drupflare/durabledb.git","type":"git"},"_npmVersion":"11.17.0","description":"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.","directories":{},"lint-staged":{"*.{ts,js,mjs,css,md,json,jsonc}":"prettier --write"},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@drupflare/cartridge":"^0.1.2"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","vitest":"^4.1.10","typedoc":"^0.28.20","prettier":"^3.9.6","typescript":"^5.9.3","lint-staged":"^17.3.0","prettier-plugin-sh":"^0.19.0","@vitest/coverage-v8":"^4.1.10","@cloudflare/workers-types":"^5.20260814.1","prettier-plugin-organize-imports":"^4.3.0"},"_npmOperationalInternal":{"tmp":"tmp/durabledb_0.1.1_1786746296223_0.26453583105194056","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@drupflare/durabledb","version":"0.1.2","keywords":["cloudflare","durable-objects","sqlite","sql","codec","workers"],"author":{"name":"Gregory Mitchell","email":"me@gmitch215.xyz"},"license":"MIT","_id":"@drupflare/durabledb@0.1.2","maintainers":[{"name":"gmitch215","email":"me@gmitch215.xyz"}],"homepage":"https://github.com/drupflare/durabledb","bugs":{"url":"https://github.com/drupflare/durabledb/issues"},"dist":{"shasum":"c2575702a78d83287fa76e04bd94fd62d2d0826f","tarball":"https://registry.npmjs.org/@drupflare/durabledb/-/durabledb-0.1.2.tgz","fileCount":6,"integrity":"sha512-fpEiLVnUWum9qj+yiGoJEOYan7TXCMZdaRded1cnPL05n23cdgMyIc7fOMgRm1U9Iax70qXV/r0sVQxsQvdLHg==","signatures":[{"sig":"MEUCIQCrlKrothiH0RZwfKTmkCVy3rdLjvMKbCvK0kbCyKBSDQIgI9RtlA3zNoNXvyAy8UJ7fDlWeICwn7G4YVmc2ejoDOs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@drupflare%2fdurabledb@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":42784},"main":"./src/index.ts","type":"module","types":"./src/index.ts","exports":{".":"./src/index.ts","./codec":"./src/codec.ts","./do-sqlite":"./src/do-sqlite.ts","./package.json":"./package.json"},"gitHead":"bd12111ceeb4f51a736c47522903b0e3c02baa6c","scripts":{"test":"vitest run","prepare":"husky","prettier":"prettier --write .","typecheck":"tsc -p tsconfig.json --noEmit","docs:build":"typedoc","docs:serve":"typedoc --watch","test:watch":"vitest","test:coverage":"vitest run --coverage","prettier:check":"prettier --check ."},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1c5230ae-c2f0-4ee9-bdb8-53298efbad8b"}},"repository":{"url":"git+https://github.com/drupflare/durabledb.git","type":"git"},"_npmVersion":"11.17.0","description":"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.","directories":{},"lint-staged":{"*.{ts,js,mjs,css,md,json,jsonc}":"prettier --write"},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@drupflare/cartridge":"^0.1.3"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","vitest":"^4.1.11","typedoc":"^0.28.20","prettier":"^3.9.6","typescript":"^5.9.3","lint-staged":"^17.3.0","prettier-plugin-sh":"^0.19.0","@vitest/coverage-v8":"^4.1.11","@cloudflare/workers-types":"^5.20260821.1","prettier-plugin-organize-imports":"^4.3.0"},"_npmOperationalInternal":{"tmp":"tmp/durabledb_0.1.2_1787343343623_0.17372963484320958","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@drupflare/durabledb","version":"0.1.3","keywords":["cloudflare","durable-objects","sqlite","sql","codec","workers"],"author":{"name":"Gregory Mitchell","email":"me@gmitch215.xyz"},"license":"MIT","_id":"@drupflare/durabledb@0.1.3","maintainers":[{"name":"gmitch215","email":"me@gmitch215.xyz"}],"homepage":"https://github.com/drupflare/durabledb","bugs":{"url":"https://github.com/drupflare/durabledb/issues"},"dist":{"shasum":"6d65decfdb12ff04508e37a27dc5f3320540aa8a","tarball":"https://registry.npmjs.org/@drupflare/durabledb/-/durabledb-0.1.3.tgz","fileCount":6,"integrity":"sha512-4HeIOG/XFs+qjRbgx3N0o/1Xfmm0PjQiA2pcbkwh5G1WN9M7zlUj1bGVHub/xxhEO+pm4vexvYmgr6ZQ8KXTSw==","signatures":[{"sig":"MEUCIHP2UBiPcO1x/THMW7PEBOIkfOkcVTr+VJTbXWdpleTZAiEAyLs0SU9v6pJEwCpBmG85aiYz/ZZgmMN9rPBpaBDlxSY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@drupflare%2fdurabledb@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":42776},"main":"./src/index.ts","type":"module","types":"./src/index.ts","exports":{".":"./src/index.ts","./codec":"./src/codec.ts","./do-sqlite":"./src/do-sqlite.ts","./package.json":"./package.json"},"gitHead":"cd3f76be877d4f4918ab1c3be6d6ff46837a02e9","scripts":{"test":"vitest run","prepare":"husky","prettier":"prettier --write .","typecheck":"tsc -p tsconfig.json --noEmit","docs:build":"typedoc","docs:serve":"typedoc --watch","test:watch":"vitest","test:coverage":"vitest run --coverage","prettier:check":"prettier --check ."},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1c5230ae-c2f0-4ee9-bdb8-53298efbad8b"}},"repository":{"url":"git+https://github.com/drupflare/durabledb.git","type":"git"},"_npmVersion":"11.17.0","description":"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.","directories":{},"lint-staged":{"*.{ts,js,mjs,css,md,json,jsonc}":"prettier --write"},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@drupflare/cartridge":"^0.2.0"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","vitest":"^4.1.11","typedoc":"^0.28.20","prettier":"^3.9.6","typescript":"^5.9.3","lint-staged":"^17.4.1","prettier-plugin-sh":"^0.19.0","@vitest/coverage-v8":"^4.1.11","@cloudflare/workers-types":"^5.20260829.1","prettier-plugin-organize-imports":"^4.3.0"},"_npmOperationalInternal":{"tmp":"tmp/durabledb_0.1.3_1787986877486_0.11234567843892296","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"_id":"@drupflare/durabledb@0.1.4","bugs":{"url":"https://github.com/drupflare/durabledb/issues"},"dist":{"shasum":"d7a45a722cac5830716ef89665086cac0a03352b","tarball":"https://registry.npmjs.org/@drupflare/durabledb/-/durabledb-0.1.4.tgz","fileCount":6,"integrity":"sha512-jvElGvxOz3n6s1TEArsKoFpXxSwe0fP3vab45RYYYLgXoPI3f0ivNqUedIlUca81wy5FHpPjPL2ngG/e5tvq6w==","signatures":[{"sig":"MEYCIQDXG+Kw8VAZn1KoKjcn7+INem9U9EsKuaAR8VfR4TPucQIhAJqHRwjhl3FA1p63ME2q1oRJ6ZVoxe36C5EQQozfJi/J","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIE4Pw2OPXGhO8+cWNm1Y/iMGXKoFnILIO1eyIC5qQeiPAiB9C8+BL4lUtxUVGg5FJ2MweKF9RtRqPN8wn7fei0vZaw=="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@drupflare%2fdurabledb@0.1.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":42764},"main":"./src/index.ts","name":"@drupflare/durabledb","type":"module","types":"./src/index.ts","author":{"name":"Gregory Mitchell","email":"me@gmitch215.xyz"},"exports":{".":"./src/index.ts","./codec":"./src/codec.ts","./do-sqlite":"./src/do-sqlite.ts","./package.json":"./package.json"},"gitHead":"4f7a78c662e5723be48507a4316fdfb107503bd0","license":"MIT","scripts":{"test":"vitest run","prepare":"husky","prettier":"prettier --write .","typecheck":"tsc -p tsconfig.json --noEmit","docs:build":"typedoc","docs:serve":"typedoc --watch","test:watch":"vitest","test:coverage":"vitest run --coverage","prettier:check":"prettier --check ."},"version":"0.1.4","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"1c5230ae-c2f0-4ee9-bdb8-53298efbad8b"}},"homepage":"https://github.com/drupflare/durabledb","keywords":["cloudflare","durable-objects","sqlite","sql","codec","workers"],"repository":{"url":"git+https://github.com/drupflare/durabledb.git","type":"git"},"_npmVersion":"11.19.0","description":"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.","directories":{},"lint-staged":{"*.{ts,js,mjs,css,md,json,jsonc}":"prettier --write"},"maintainers":[{"name":"gmitch215","email":"me@gmitch215.xyz"}],"sideEffects":false,"_nodeVersion":"24.21.0","dependencies":{"@drupflare/cartridge":"^0.2.0 || ^0.3.0"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","vitest":"^4.1.11","typedoc":"^0.28.20","prettier":"^3.9.6","typescript":"^5.9.3","lint-staged":"^17.4.1","prettier-plugin-sh":"^0.20.0","@vitest/coverage-v8":"^4.1.11","@cloudflare/workers-types":"^5.20260829.1","prettier-plugin-organize-imports":"^4.3.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/durabledb_0.1.4_1790795257644_0.23326402862298412"}}},"time":{"created":"2026-08-14T05:36:22.363Z","modified":"2026-09-30T19:07:38.118Z","0.1.0":"2026-08-14T05:36:22.762Z","0.1.1":"2026-08-14T22:24:56.355Z","0.1.2":"2026-08-21T20:15:43.777Z","0.1.3":"2026-08-29T07:01:17.603Z","0.1.4":"2026-09-30T19:07:37.742Z"},"bugs":{"url":"https://github.com/drupflare/durabledb/issues"},"author":{"name":"Gregory Mitchell","email":"me@gmitch215.xyz"},"license":"MIT","homepage":"https://github.com/drupflare/durabledb","keywords":["cloudflare","durable-objects","sqlite","sql","codec","workers"],"repository":{"url":"git+https://github.com/drupflare/durabledb.git","type":"git"},"description":"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.","maintainers":[{"name":"gmitch215","email":"me@gmitch215.xyz"}],"readme":"# 🗄️ durabledb\n\n> The real limits of Cloudflare Durable Object SQLite, encoded rather than documented\n\n[![Build](https://github.com/drupflare/durabledb/actions/workflows/build.yml/badge.svg)](https://github.com/drupflare/durabledb/actions/workflows/build.yml)\n[![Prettier](https://github.com/drupflare/durabledb/actions/workflows/prettier.yml/badge.svg)](https://github.com/drupflare/durabledb/actions/workflows/prettier.yml)\n[![codecov](https://codecov.io/gh/drupflare/durabledb/branch/master/graph/badge.svg)](https://codecov.io/gh/drupflare/durabledb)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n**A `ctx.storage.sql` host and a value codec that encode what the platform actually does, not\nwhat SQLite documents.** `ctx.storage.sql` caps a statement at 100 bound parameters where\nlocal PDO allows 32,766, refuses a `LIKE` pattern over 50 bytes, has **no named parameters\nat all**, and reads integers above **2^53** lossily. Every one of those is a measured refusal\nfrom a deployed object, and every one has produced a defect that a local SQLite passed.\n\n---\n\n## 📋 Table of Contents\n\n- [Why](#-why)\n- [Install](#-install)\n- [Limits](#-limits)\n- [Codec](#-codec)\n- [Bridge Contract](#-bridge-contract)\n- [API](#-api)\n- [Testing](#-testing)\n- [Related Repositories](#-related-repositories)\n- [License](#-license)\n\n---\n\n## 🎯 Why\n\nA Durable Object's SQLite is a genuinely good home for a small database: strongly consistent,\nin the same isolate as the code, and synchronous to read so blocking callers compose with it.\nThen you write against it as though it were SQLite and it refuses things SQLite does not.\n\nThe refusals are not documented as a set, they are not tunable, and each one fails in a\ndifferent register: a thrown error for the parameter cap, a **silently wrong number** for a\nwide integer. A value that comes back wrong rather than erroring cannot be told apart from correct\noutput until much later.\n\nSo the limits live in code, as named constants and named refusals, with a test per limit.\n\n---\n\n## 📥 Install\n\n```sh\nbun add @drupflare/durabledb\n```\n\nThree entry points, and the split is not cosmetic:\n\n| Import                           | Gives you                                       | Needs `@drupflare/cartridge` |\n| -------------------------------- | ----------------------------------------------- | ---------------------------- |\n| `@drupflare/durabledb`           | everything below                                | **yes**                      |\n| `@drupflare/durabledb/codec`     | `encode`, `decode`, `codecGuard`, `PHP_CODEC`   | no                           |\n| `@drupflare/durabledb/do-sqlite` | `SiteDurableObject`, `toPositional`, `bindable` | **yes**                      |\n\n`src/do-sqlite.ts` imports `@drupflare/cartridge/gate` and `/mask`; `src/codec.ts` imports nothing at\nall, so a consumer that only needs the codec pays for no dependency. There is no deep import: the\n`exports` map is the whole surface, and `@drupflare/durabledb/src/codec.ts` is refused by the\nresolver.\n\n---\n\n## 🧱 Limits\n\nEach one broke something real. None is a conservative guess.\n\n| Limit                          | Value                | What it broke                                                           |\n| ------------------------------ | -------------------- | ----------------------------------------------------------------------- |\n| bound parameters per statement | **100**              | the cache write path; a cold `cache_discovery` set needed 574           |\n| `LIKE` / `GLOB` pattern length | **50 bytes**         | binds **plain `LIKE`**, not only `GLOB`, so it is invisible to a caller |\n| bytes per record               | **2,199,995**        | a heap snapshot row                                                     |\n| statement text                 | **100,000 chars**    | a chunked DDL replay                                                    |\n| integer reads                  | **lossy above 2^53** | wrote `9007199254740993`, read back `9007199254740992`                  |\n| named parameters               | **none exist**       | every Drupal query; `toPositional()` converts them                      |\n\nTwo carry a further consequence:\n\n**Writing a wide integer is exact; reading one is not.** `ctx.storage.sql` hands INTEGER columns\nback as JS doubles, so the precision is gone before any consumer can see it. `CAST(col AS TEXT)`\nreturns every digit, so the storage is fine; the loss is in the cursor. `UnreadableIntegerError`\nexists to make that a refusal rather than a wrong answer.\n\n**\"There is no smaller unit than a row\" is false**, and it was this project's own unverified\nclaim, believed for a while. Three 520 KB rows each overran the record cap and looked\nindivisible; SQLite builds a value across statements with `col = col || ?`.\n\n> [!WARNING]\n> **Never widen a limit because a test passes.** These came from a deployed object, and a local\n> SQLite will happily accept all of them. If you think one is wrong, re-measure on a deployed\n> worker and say so. Do not relax a guard.\n\n---\n\n## 🔁 Codec\n\nThe consumer runtime is a **32-bit** PHP wasm build (`PHP_INT_SIZE` is 4), so any JS number at\nor above 2^31 wraps silently on the way in. Measured: `Date.now()` arrived in PHP as\n`-397708726` instead of ~1.78e12. Two instances were found separately, a timestamp and a node\nid, and finding two separately means the class was still open.\n\nThe obvious fix, stringifying anything unsafe, **is lossy in the other direction.** PHP receives\n`\"1780000000000\"` and cannot tell whether that was an integer too large to represent or a\ngenuine string; sending it back produces a string where a number belongs, and the corruption\nreappears one hop later.\n\nSo values that cannot cross natively are wrapped in a tagged envelope instead of flattened, and\n`decode()` is the exact inverse of `encode()`:\n\n| Envelope                         | Carries                                |\n| -------------------------------- | -------------------------------------- |\n| `{__t: 'i', v: '1780000000000'}` | integer outside 32-bit range           |\n| `{__t: 'n', v: '1.5e300'}`       | non-finite or precision-risky float    |\n| `{__t: 'd', v: '1780000000000'}` | `Date`                                 |\n| `{__t: 'b', v: '<base64>'}`      | bytes                                  |\n| `{__t: 'u'}`                     | `undefined`, which PHP has no word for |\n\nAnything representable on both sides crosses unwrapped, so the common path costs nothing.\n\n---\n\n## 🔗 Bridge Contract\n\nTwo entry points, and the second is the interesting one.\n\n```ts\nexecSql(sql: string, params?: SqlBindings): ExecSqlResult\nexecTxn(req: TxnRequest): ExecTxnResult\n```\n\n`execTxn` runs a list of statements inside one `ctx.storage.transactionSync()`, optionally with\na trailing read. `commit: false` runs them, evaluates the read inside the same transaction, then\nthrows a private sentinel so the runtime rolls back, and **still returns the results**, which\nis what makes a speculative row count and insert id possible.\n\n`ExecTxnResult` is a discriminated union rather than one shape with optionals: a\nfailed replay has already been rolled back, so there are no results to read, and the\ndiscriminant is what stops a caller reading them anyway.\n\n---\n\n## 🔧 API\n\n| Export                                          | What it is                                                           |\n| ----------------------------------------------- | -------------------------------------------------------------------- |\n| `SiteDurableObject`                             | the Durable Object; `execSql`, `execTxn`, `installBridge`, `nowMs`   |\n| `encode` / `decode`                             | the codec, exact inverses of each other                              |\n| `codecGuard(host)`                              | wraps a host surface so every value crossing it is encoded           |\n| `PHP_CODEC`                                     | the PHP half of the codec, as source, for hosts that evaluate it     |\n| `toPositional(sql, params)`                     | rewrites named parameters, because the engine has none               |\n| `bindable(value)`                               | the one value transform `sql.exec()` needs; a JS `BigInt` is refused |\n| `UnreadableIntegerError`                        | a read above 2^53, refused rather than answered wrongly              |\n| `ExecSqlResult`                                 | `{ rows, rowsRead, rowsWritten, lastInsertRowid, changes }`          |\n| `TxnRequest` / `TxnStatement` / `ExecTxnResult` | the replay contract above                                            |\n| `SqlBindings`                                   | `unknown[] \\| Record<string, unknown> \\| null`                       |\n\n---\n\n## 🧪 Testing\n\n```sh\nbun run typecheck\nbun run test # 87 assertions across 3 specs\nbun run test:coverage\n```\n\n**87 passing, 0 failed**, at a measured **61.68% statements** (`codec.ts` 82.29%, `do-sqlite.ts`\n49.69%: the Durable Object routes and `alarm()` need a real `ctx.storage.sql`, so they are covered\nin the consumer rather than here). The lane runs in **node**, not workerd, and coverage uses `provider: 'v8'`\nrather than `istanbul`. The sibling repositories split on exactly that axis: a workerd lane must use\nistanbul, because the v8 provider reads coverage off the Node inspector and attributes zero from\ninside the isolate; a node lane uses v8.\n\nTwo rules the suite is built on:\n\n- **The codec must round-trip.** A codec that can decode a type it cannot encode is a bug to fix,\n  not a test to skip.\n- **Fail with a named refusal rather than truncating.** A silently truncated value cannot be told\n  apart from correct output until much later.\n\n---\n\n## 🔗 Related Repositories\n\n| Repository                                                      | What it is                                                                  |\n| --------------------------------------------------------------- | --------------------------------------------------------------------------- |\n| [`drupflare/worker`](https://github.com/drupflare/worker)       | the consumer: Drupal 11 on Cloudflare Workers                               |\n| [`drupflare/rom`](https://github.com/drupflare/rom)             | `composer require drupflare/rom:0.*`, the Drupal 11 driver that sits on top |\n| [`drupflare/cartridge`](https://github.com/drupflare/cartridge) | the reentrancy gate and interrupt mask this package imports                 |\n\n---\n\n## 📄 License\n\nMIT (c) Gregory Mitchell 2026. See [LICENSE](LICENSE).\n","readmeFilename":"README.md"}