Skip to content

VFS: accept a caller-supplied entry identity so a rebuilt tree keeps its own #102

Open

Description

@akcorca

Summary

Let a caller supply an entry's identity when it creates one, so a namespace rebuilt from an external source of truth keeps the identities that source already issued.

Split from #89, which asked for identity in general and is answered by #96. What is left is the case that survives it: ino is stable through every operation that mutates a tree, and there is no way to preserve it through one that rebuilds a tree.

Why the general case is already closed

#96 put the identity on StatBase.ino, made it non-reusing, and cost nothing measurable. #98 made every content replacement — write, append, cp over a file — keep it. A consumer keying a per-file room, a grant, or an index row to a file is served by that for as long as the tree is mutated in place.

What a rebuild is, and why it is different

Restore a version, fork a project, re-materialise a workspace: the tree is reconstructed rather than mutated. Every entry is created fresh, so every identity is new, and everything keyed to the old ones is orphaned. A restore that produces a tree of new files holding the old content is not a restore.

Nothing in the library can be worked around here the way the earlier cases could. A host can keep its own identity map and rewrite it from vfs.mutation when paths move, because a move is a change it observes. A rebuild is not a change to observe — the host is the one performing it, and it has the identities in hand and no way to say them.

What changed since this was judged too expensive

#89 left this open on the grounds that a supplied identity would need a unique index to enforce, and that the index was the cost #96 avoided. That was true of the opaque-string design and is not true of the one that shipped:

  • id INTEGER PRIMARY KEY is an alias for the rowid. WHERE id = ? is a seek on the table's own key, and an INSERT with an id that exists fails with SQLITE_CONSTRAINT_PRIMARYKEY. Verified against the schema: SEARCH vfs_entries USING INTEGER PRIMARY KEY (rowid=?), and a duplicate insert raises.
  • There is no index to add. The constraint is the table.
  • The high-water mark already has the right shape. updateUsage writes MAX(next_ino, ?), so accepting a supplied identity is a matter of advancing the counter past it rather than new bookkeeping.

The honest limit

The primary key enforces uniqueness among live entries, not against retired identities. An identity whose entry was removed is free at the SQLite level, so a caller could supply one and the namespace would accept it. Nothing in the schema can tell reinstating the file that had it from recycling the number onto a different one — only the caller knows which it is doing.

That is an argument for saying so, not for refusing. VirtualFileSystem is already the trusted administration capability, and this is the same shape as setOwnership: the host asserts something the namespace cannot verify because the host is the only thing that can know it. A guard refusing anything below next_ino would be free and would make ordinary misuse impossible, but it forbids the case the feature exists for.

Scope

  • An optional identity on the creating operations — at minimum writeFile, mkdir, touch, and symlink. copy and the opaque commit are worth deciding rather than assuming: a copy is a new file, so a supplied identity there may not mean anything.
  • Advance next_ino past a supplied identity, so a later allocation cannot collide with one.
  • Refuse a supplied identity that a live entry holds, which the primary key does; decide the error code rather than surfacing a raw constraint failure.
  • Decide whether a retired identity may be supplied. Recommended yes, documented as trusted, with the reasoning above.
  • Decide whether the credential-bound view may supply one. Recommended no — it is administration, like opaque upload and the change feed.

Constraints

  • Cost nothing when unused. Supplying an identity is a bound parameter on a statement that already runs; the allocation path must not gain work, and the metered counts must not move.
  • Keep copy set-based. It numbers its run from a base plus ROW_NUMBER(), and whatever is decided for supplied identities there must not turn it into one statement per entry.
  • Do not weaken non-reuse. The counter must end up above anything ever handed out, supplied or allocated.

Acceptance criteria

  • A tree rebuilt with supplied identities reports the same ino values as the tree it was rebuilt from.
  • A supplied identity held by a live entry is refused with a documented code.
  • After supplying an identity above the counter, the next allocated one does not collide with it.
  • Trusted-path statement, row-read, and row-written counts are unchanged when no identity is supplied.
  • docs/operations.md states what the namespace verifies and what it trusts the caller for.

Raised by the consumer in #89, who should weight accordingly.

Activity

  1. akcorca commented on Aug 23, 2026

    @akcorca
    ContributorAuthor

    Parked, with the reasoning, rather than closed

    Deferred until a second consumer asks. The argument came from the consumer that asked for it in the first place, and it is better than the one in the issue body.

    Two of the three motivating cases do not need it.

    • A fork is a different project whose files are legitimately different files. New identities are correct there, not a loss.
    • An import into a fresh workspace has no prior identities to preserve.

    The third is avoidable, and the avoidance is better semantics. A restore is better done as a diff against the current tree, applying the writes, moves and removes that reconcile it. That preserves identities natively — every operation in that set already keeps them, per #96, #98 and #101 — and it does not churn revisions on files that did not change, which a full rebuild does.

    And it is the only one of the identity issues that ships with a permanent caveat. The primary key enforces uniqueness among live entries only, so reinstating a retired identity is indistinguishable from recycling one onto a different file. That is explainable, and #96/#98/#101 need no such explanation — but it is carried forever, for every consumer, in exchange for a case the one consumer who wanted it can handle in its own code.

    Two general things beats three. If in-place restore turns out not to work, this is where the reasoning is.

    #105 and #107 are the live parts of the identity seam.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions