Repository navigation
fix(driver-turso): the remote face joins a large $and / $or as balanced halves, so hosted comment and attachment reads fit the depth limit (cloud#3485) - #30143
Conversation
…alanced halves (cloud#3485) Claude-Session: https://claude.ai/code/session_01QUs3BtjC5etzjN1qcjYgiQ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUs3BtjC5etzjN1qcjYgiQ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUs3BtjC5etzjN1qcjYgiQ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0a6a579a9707b9738e6fc8e531d169e38e543eb4 && git checkout 0a6a579a9707b9738e6fc8e531d169e38e543eb4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55337f4ce35dbe62434b23517547a0b5052b936c d16d50d82aea7cacd72846c4de1cc6d81b1cf77a && git checkout -B drift-repro 55337f4ce35dbe62434b23517547a0b5052b936c && git merge --no-ff d16d50d82aea7cacd72846c4de1cc6d81b1cf77a
node scripts/docs-audit/affected-docs.mjs --json 55337f4ce35dbe62434b23517547a0b5052b936c |
|
repo:cloud#2 · os-zhuang · session_0123kRqqdpAJWYMgsgdAJX4G 席位核对 — ACCEPT at d16d50d
The seat marks this PR ready and arms automerge; it merges when CI is green. The cloud pin round then carries the hosted e2e and Generated by Claude Code |
Requested by Sam · project thread
Refs objectstack-ai/cloud#3485 · follows #29888 (PR #29911)
What changes
TursoDriver's remote face (RemoteTransport) now joins a large$and/$orgroup as balanced halves, by the same rule the local face's knex path has used since #29888:packages/drivers/driver-sql/src/sql-driver-balanced-group.ts:joinBalancedGroup(members, joiner), the text twin ofemitBalancedGroup. At mostFLAT_GROUP_MAX_MEMBERS(8) members join flat, byte-identical tomembers.join(joiner). Above that, the two halves join once, each in its own parentheses, recursively. Exported from@objectstack/driver-sqlwithFLAT_GROUP_MAX_MEMBERS, so the rule has one source. This follows the precedent ofrecoverUnencodedJsonText.packages/drivers/driver-turso/src/remote-transport.ts: the$andarm and the$orarm ofcompileWhereSQLcall it in place of a flatjoin. Nothing else in the compiler changes: not the TRUE/FALSE identities, the leaf arms, theReadableIdssemi-join or the tenant wall.packages/drivers/driver-turso/src/turso-remote-balanced-group.test.ts: the new test, described under Verification.patchfordriver-tursoanddriver-sql.Why
On every hosted environment with HotCRM installed, production included, every read of
sys_commentandsys_attachmentanswers 500DATABASE_ERROR. Record pages show no comments and no attachments. The tenant database refuses the statement with "Expression tree is too large (maximum depth 100)" (container log, cloud#3485, 2026-11-04 13:30Z).#29911 balanced the parent-record gate's
$orinSqlDriver's knex path. A hosted environment does not read through that path. Its driver isTursoDriverwith alibsql://url, so the remote face compiles every read with its own compiler, and that compiler still joined each group flat (subClauses.join(' OR ')). The comment gate (plugin-audit) and the attachment gate (service-storage) OR oneReadableIdsbranch per registered parent object. Joined flat, that is one expression level per parent: about 80 on a bare environment, and more than 100 once an app is installed. The #29888 dogfood test proved the fix on sql.js, the local face, so it passed while the hosted face stayed broken.The gates' shape, and therefore the authorization boundary, does not change.
ANDandORare associative and commutative over SQL's three-valued logic, so the regrouped expression selects exactly the rows the flat one did. The members keep their order, and so do their bind values.On depth: the gate's OR now adds
7 + ceil(log2(N / 8))levels at most for N branches, for example 12 at 130 and 18 at 10,000. Its total depth is that plus the deepest single branch, which is the parent's own scope and does not grow with the number of parents. The other shapes the card suggested (UNION, CTE or id set) would mean a new filter-protocol construct across every driver (ADR-0139 territory) and would depend on hosted limits nobody has measured, such as the compound-SELECT term limit. This change uses only the binaryOR/ANDevery face already emits.Verification
Readings taken on this branch (
objectstack-ai/objectstack, head ofclaude/project-thread-mnya23) in a cloud container.The instrument. The stub is better-sqlite3 (
makeLibsqlSqliteStub), which allows 1000 levels and exposes nosqlite3_limit. The test re-prepares the statement the remote face sent with its top-level WHERE under 900AND 1levels, so it fits exactly when the WHERE fits 100. A binary search over that spine reads a WHERE's depth exactly. Both are calibrated on raw SQL first: a 99-term OR chain fits and a 100-term chain is refused, and k terms read as k + 1 levels for k ∈ {1, 2, 9, 77, 300}. The gate-shaped reads are also served with the budget applied to every SELECT the driver sends.turso-remote-balanced-group.test.ts(18 cases, remote face):$orand$and. A 9-member group is pinned as two halves (5 + 4), with bind values in member order.$orof 9, 130 and 1,200 members, and$andof 130, select the expected rows and fit the budget. A large$orunder$notinside an$andkeeps its answer.{ object_name: P, record_id: { $in: ReadableIds.compose(…) } }with a sharing-shaped scope, and one refused parent answered$in: []. The test runs the card's reads: unfiltered, sorted bycreated_at, filtered bythread_id, by an unopenable thread, by the parent pair, and bybody. Each is served under the budget with exactly the rows the fixture oracle allows: rows on closed records, a foreign-tenant record, a deleted record, an unlisted object or no parent stay hidden.countandaggregateagree withfind.$or28ce3d1b)Expression tree is too largeSuites (one run after a full DTS build of the
driver-tursoclosure):@objectstack/driver-turso97 files, 2,514 passed (34 skipped).@objectstack/driver-sql238 files, 3,859 passed (216 skipped).typecheckis clean on both packages.eslintis clean on the four files.check:empty-changeset,check:changeset-fixed,check:cross-package-test-inputsandcheck:test-source-aliasare green.End to end in cloud. The cloud pin round carrying this change adds a hosted-composition e2e:
ArtifactKernelFactorywith the hosted slate, served over HTTP, onTursoDriver's remote face over a native@libsql/clientfile:database, with 75 extra objects. It reads the card's HTTP queries under the same budget. That PR links here.Not changed, and noted for the lane:
service-analyticsread-scope-sql.tsjoins$and/$orflat too, as #29911's report recorded. It compiles analytics read scopes, not these gated reads.Clause-②: no
Metadata-protocol: none
Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01QUs3BtjC5etzjN1qcjYgiQ