Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(worker): allow a missing :block/order on a direct child - #13081

Open
ChipNowacek wants to merge 1 commit into
logseq:masterfrom
ChipNowacek:fix/direct-child-order-nil
Open

fix(worker): allow a missing :block/order on a direct child#13081
ChipNowacek wants to merge 1 commit into
logseq:masterfrom
ChipNowacek:fix/direct-child-order-nil

Conversation

@ChipNowacek

Copy link
Copy Markdown
Contributor

Why this is a PR and not an issue

Filing it as a patch because a patch is the most specific statement of the
problem I can make — it names the exact line, the exact value, and what I think
should happen instead, with tests that pin both halves.

Please close it without ceremony if it isn't useful. I'm not looking to
defend this. If the guard is deliberate, if this area is mid-rewrite, or if you'd
rather solve it a different way, throwing it away costs me nothing and the
report below still stands on its own. I'd rather hand you something concrete and
disposable than something vague you have to interrogate.


What happens

Opening a page whose open subtree contains a direct child with no :block/order
fails the whole page:

POST http://127.0.0.1:PORT/v1/invoke  500 (Internal Server Error)
Error: Invalid direct-child order

The throw is in direct-children-membership
(src/main/frontend/worker/handler/block.cljs:219), reaches the renderer as a
500 from the worker, and is caught by the React ErrorBoundary — so the user gets
"Something went wrong" and the app is unusable until relaunch.

Because open-block-tree calls direct-children-membership recursively, the
page that fails can be far above the block that causes it. On the graph where I
hit this, one page four levels down made the top-level Library page
impossible to open.

Why I think the rejected value is legal

:block/order is {:optional true} for pages:

;; deps/db/src/logseq/db/frontend/malli_schema.cljs
(def normal-page
  (vec (concat
    [:map {:error/path ["normal-page"]}
     [:block/journal-day {:optional true} :int]
     [:block/parent      {:optional true} :int]
     [:block/order       {:optional true} block-order]]   ; :308
    page-attrs page-or-block-attrs)))

(def block-attrs                                          ; :406
  [[:block/title  :string]
   [:block/parent :int]
   [:block/order  block-order]                            ; required — normal blocks
   …])

It is required for normal blocks and optional for pages. The block in
question is a page (:block/name, tagged :logseq.class/Page) nested under
another page, which this graph does for its own hierarchy.

Corroborating that reading:

  • logseq graph validate scoped to the page returns
    {:pages_scanned 1, :blocks_scanned 4, :findings []}.
  • The pre-worker-refactor build (rev 9a11243) renders the same graph fine.
    Only the post-refactor build (rev 71db2e3) fails.
  • The page's attribute set is otherwise identical to a sibling page that renders
    — same tags, same parent, same shape. The order is the only difference.

Why the guard doesn't seem load-bearing

The very next expression is (sort-by second), and cljs.core/compare already
orders nil ahead of any string. That is exactly what ldb/sort-by-order — a
bare (sort-by :block/order blocks) — relies on everywhere else in the tree.
So the surrounding code already tolerates what this guard rejects.

The patch accepts nil and lets it sort first, while still rejecting a
non-string value, which would be a real defect:

(when-not (or (nil? order) (string? order))
  (fail-render-read! "Invalid direct-child order" …))

Tests

  • direct-children-membership-allows-missing-child-order-test — an order-less
    child no longer removes its siblings from the result, and sorts first.
  • direct-children-membership-rejects-non-string-child-order-test — a numeric
    order still throws.

Verified against master by reverting the one-line source change and rerunning
the namespace: the first test errors with the same message seen in the wild,

ERROR in (direct-children-membership-allows-missing-child-order-test)
#error {:message "Invalid direct-child order", :block-order nil}
  at frontend/worker/handler/block.cljs:218

and passes with it. Full namespace: 16 tests, 100 assertions, 0 failures.

No regressions across every frontend.worker.* and frontend.db.* namespace —
1071 tests, 4989 assertions, 0 failures, 0 errors.

Deliberately not in this PR

Three things I noticed but didn't touch, because they're design calls that are
yours to make:

  1. Whether these guards should throw at all. There are ten fail-render-read!
    sites in this file. Failing an entire page's read because one child is odd is
    a large blast radius for a render path; skipping the child and rendering the
    rest is the other obvious design. I only changed the predicate.
  2. Whether the UI should name the failing block. The ex-info already carries
    :parent-uuid, :block-uuid and :block-order, and none of it reaches the
    user. "Invalid direct-child order" alone is hard to act on.
  3. Whether there should be a repair path. On the affected build I could not
    fix the block from anywhere: the UI can't render the page that holds it,
    logseq upsert block refuses page sources with invalid-source: source must be a non-page block, and logseq.Editor.moveBlock silently no-ops on a page
    (returns null, changes nothing, raises nothing — the same call on a normal
    block works). I repaired it by dragging the page in an older build. That
    silent moveBlock no-op may be worth its own issue; say the word and I'll
    open one.

What I could not determine

What wrote a page with no :block/order in the first place. It was created
2026-08-02; the graph's client-ops log only begins 2026-08-07, so it can't be
inspected. Every ordinary write path I can test on the current build assigns an
order correctly — creating a page under a parent, and even recycling one. So I
can't tell you whether there's an upstream write bug here or a one-off from an
older build. If that origin matters more to you than the read-path behaviour,
I'm happy to dig further.

Environment

Logseq DB graph, desktop macOS arm64, app 2.0.1, build rev 71db2e3.
Not reproducible on rev 9a11243. Present on master at 047acf36ee.

direct-children-membership rejects any direct child whose :block/order
is not a string, and the rejection is a throw that discards the whole
membership read rather than the one child. Because open-block-tree
calls it recursively over the open tree, a single such child makes
every ancestor page unopenable -- the throw returns to the renderer as
a 500 from the worker and is caught by the React ErrorBoundary, so the
user gets "Something went wrong" with no indication of which page or
which block is involved, though the ex-info already carries both.

The rejected value is legal. :block/order is {:optional true} for pages
in normal-page, `graph validate` reports nothing against such a page,
and the pre-worker renderer displayed it without complaint. Only
block-attrs makes the attribute mandatory, and that governs normal
blocks; a page nested under another page may carry none.

Nothing downstream needs the guard either. The very next expression is
(sort-by second), and cljs.core/compare already orders nil ahead of any
string -- which is what ldb/sort-by-order, a bare (sort-by :block/order),
relies on everywhere else in the codebase. So accept nil and let it
sort first, consistent with the rest of the tree, while still rejecting
a non-string value as the real defect it would be.

Hit on a live DB graph: one page created without an order made the
top-level Library page, four levels above it, impossible to open.

Adds tests for both halves -- an order-less child no longer removes its
siblings, and a numeric order still throws. The first fails on the old
implementation with the same error seen in the wild.
@tiensonqin
tiensonqin self-requested a review August 21, 2026 08:00
@tiensonqin tiensonqin self-assigned this Aug 21, 2026
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.

2 participants