Skip to content

fix(shell): use range scans instead of LIKE in Workspace rm/glob#1716

Open
mattzcarey wants to merge 2 commits into
mainfrom
fix/1539-workspace-rm-range-scan
Open

fix(shell): use range scans instead of LIKE in Workspace rm/glob#1716
mattzcarey wants to merge 2 commits into
mainfrom
fix/1539-workspace-rm-range-scan

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Note

This is a Fable experiment (model-authored PR). Please close if it adds no value.

Fixes #1539

Problem

Workspace.rm(path, { recursive: true }) deletes descendants via deleteDescendants(), which used WHERE path LIKE ? ESCAPE ?. On D1 this can fail with:

D1_ERROR: LIKE or GLOB pattern too complex: SQLITE_ERROR

The glob() prefilter used the same LIKE/ESCAPE construction and had the same exposure.

Fix

Replace the LIKE queries with range predicates over the path primary key:

  • deleteDescendants(dirPath) — both the R2-key collection and the SQLite delete now use path >= '${dirPath}/' AND path < '${dirPath}0'. '0' is the character immediately after '/', so the half-open range covers exactly the paths strictly under dirPath — same semantics as the old LIKE '${dir}/%', including the prefix-sibling boundary (/skill-extra, /skill0 survive an rm -r /skill).
  • glob(pattern)getGlobPrefix returns either a /-terminated directory prefix (range scan as above) or, for wildcard-free patterns, the whole pattern (now an exact path = ? match; the regex filter already reduced it to exact).
  • Removed the now-unused escapeLike() helper and LIKE_ESCAPE constant — there are no remaining LIKE queries in the package.

The range form also scans the path primary key index directly instead of going through SQLite's LIKE optimizer, and needs no escaping of %/_ in path names.

Tests

Added regression tests in packages/shell/src/tests/workspace.test.ts:

  • rm -r removes a deep skill-shaped tree (the exact reproduction shape from Workspace.rm recursive: replace LIKE pattern with range scan #1539: SKILL.md, scripts/, references/, assets/)
  • rm -r /skill preserves prefix siblings /skill-extra/… and /skill.txt
  • rm -r /dir preserves the upper range boundary: /dir0 and /dir00/… survive (the '0'-successor edge specifically)
  • glob matches inside directories with %/_ in the name and doesn't leak into prefix-sibling dirs (/a-extra, /a0) outside the glob prefix

Full shell suite: 235 passed, 1 skipped. Existing LIKE-injection security tests (%/_ dir names) still pass unchanged.

Notes for maintainers

  • dirPath can never be / here — rm() throws EPERM for root before reaching deleteDescendants — so the ${dirPath}/ lower bound is always well-formed.
  • Comparison semantics are unchanged: the path column uses the default BINARY collation, and the boundary characters are ASCII, so the range compares bytewise identically under SQLite (DO storage) and D1.
  • The workerd-backed test SQLite won't reproduce D1's "pattern too complex" rejection itself; the tests pin the semantics of the range scan (boundaries, special chars), and the fix removes the failing code path entirely.
  • Changeset included (@cloudflare/shell patch).
  • Same conclusion as the community patch linked in the issue comments (lower bound must be ${dirPath}/, range applied to both R2 collection and delete); implemented independently with a ${dirPath}0 upper bound instead of ${dirPath}/� so paths containing astral-plane characters (which sort above U+FFFF in UTF-8 byte order) are still covered.

Open in Devin Review

D1 can reject the LIKE ? ESCAPE ? queries in deleteDescendants and the
glob prefilter with 'D1_ERROR: LIKE or GLOB pattern too complex'.
Replace them with range predicates on the path primary key, which avoid
the LIKE limit, use the index directly, and need no %/_ escaping.

Fixes #1539
@changeset-bot

changeset-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3ccfe70

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/shell Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

@pkg-pr-new

pkg-pr-new Bot commented Jun 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1716

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1716

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1716

create-think

npm i https://pkg.pr.new/create-think@1716

hono-agents

npm i https://pkg.pr.new/hono-agents@1716

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1716

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1716

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1716

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1716

commit: 3ccfe70

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workspace.rm recursive: replace LIKE pattern with range scan

1 participant