Skip to content

v0.7.2

Choose a tag to compare

@github-actions github-actions released this 13 Oct 17:22
· 139 commits to main since this release
545fcea

No format version change. A 0.7.1 store opens under 0.7.2, and a 0.7.2 snapshot opens under 0.7.1, which ignores the two optional sections this release adds.

Most of what is here was found by running the same writes two ways and comparing: one batch against separate commits, a reopened store against the handle that never closed, a handle that snapshots against one that never does. Five entries under Fixed are in 0.7.1 and earlier and are the reason to upgrade:

  • deleting a node after a snapshot left its edges behind, and reading a neighbour of it failed (row 95);
  • a rename followed by a snapshot that kept its WAL left a store that did not open again (row 91);
  • a rename onto a key a deleted node once held, followed by any snapshot, did the same (row 94);
  • a history read on a node renamed away and back never returned (row 93);
  • a backup did not carry the map a read by date needs (row 86).

Before upgrading a store that already will not open, read the entries for rows 91 and 94: one kind opens again under this release with nothing lost, and one has to be recovered by hand first.

Known and not fixed: a crash between a truncating snapshot and the replacement of its WAL, on a store where a node was renamed before the snapshot, leaves a store that does not open. The window is two filesystem calls wide; the recovery is under row 91's entry (row 92).

Several inputs that 0.7.1 accepted are refused; each is under Changed.

Added

  • true and false are literals in the Cypher subset. In any case, wherever a literal is read: WHERE n.active = true, WHERE n.active <> false, MATCH (n {active: true}), RETURN true AS t, and CREATE and SET with a boolean value. They used to be read as variable names, so a comparison was an unbound variable error and a write was refused; WHERE n.active, the bare form, was the only way to ask and still works. A node with no such property matches neither = … nor <> …. true and false are reserved: naming a variable or an alias either one is now a parse error (row 63).
  • PageRankConfig::validate, for an embedded caller that takes a config from somewhere it does not control.
  • The MCP registry card offers the npm launcher, so a client that installs from the registry needs no Rust toolchain.

Changed

  • An /algo request with a field the config does not have is 400. POST /algo/pagerank, /algo/wcc and /algo/degree dropped such a field without a word: {"weight": "score"} answered 200 with unweighted scores, and max_iter for max_iters ran with the default. The message names the field and the ones the route accepts. {} is still every default. A caller that sends a stray field gets 400 where 0.7.1 answered (row 87).
  • PageRank refuses a damping that is not a number from 0 to 1. Python raises ValueError and HTTP answers 400. A NaN used to come back as a NaN score for every node (row 75).
  • Over MCP, find_similar refuses a min, k or limit that is not a number. It used to take the default and echo it, so a caller who sent "0.5" got results filtered at 0.8. Absent or null still takes the default (row 74).
  • remember refuses a key, predicate or label made only of invisible characters — a zero-width space, an ASCII control character, a byte-order mark — as it already refused an empty one, before it writes anything (row 78).
  • A provisional stub with a long key is linked by its whole key alone. A stub's aliases are its key and each word of it. A key that implies more than 32 — a sentence, a URL with a slug and a query string — used to get them all, past the cap an entity is held to; it now carries the one (row 53).

Fixed

  • Deleting a node after a snapshot removes its edges. On a store that had taken a snapshot — which serve --snapshot-every, mushroomdb snapshot and Python's snapshot() all do — delete_node removed the node and left every edge the snapshot held for it. Reading the neighbours of a node that had been linked to it failed with corrupt data: topology id N has no key; a pattern match returned a row with one end missing; stats and degree views went on counting the edges; and the next snapshot kept them. It is in 0.7.1 and earlier. A store already left that way is repaired when 0.7.2 opens it: the leftover edges are dropped, and the next snapshot no longer carries them. mushroomdb verify checks checksums and cannot see such edges; it reports a store in that state as intact (row 95).

  • Three more writes after a snapshot now act on what the snapshot holds. Deleting a view removes its property from every node, where the snapshot's values stayed readable. A rule deleted, snapshotted and created again derives its edges after a reopen, where the old snapshot's ownership records came back in their place. And an aggregate view created after a snapshot counts the neighbours the snapshot holds, where it started from zero. An average, minimum or maximum with nothing left to aggregate is absent, where it kept the snapshot's number; and a view over an edge type a rule derives is right after a reopen that replays writes, where it kept the snapshot's counts (row 95).

  • A view cannot take the name of a property nodes already carry, and a refused view leaves nothing behind. On a store with a snapshot the name check missed properties the snapshot held. And the refusal came after the record was written to the WAL, so the store that had just refused the view failed its next open (row 95).

  • A store that renamed a node and then took a snapshot keeping its WAL opens again. mushroomdb snapshot --keep-wal, snapshot_with(keep_wal: true), the snapshot enable_multiplicity takes and an auto-migration all leave the WAL whole, and the next open replayed all of it over the snapshot. After a rename_node that open failed with corrupt data: wal replay RenameNode n→m: duplicate node key: m and the store did not open again; after a key was deleted and inserted again the store opened holding an older value, with no error. Such a snapshot now records how much of the WAL it holds, and the open does not apply those writes a second time. It is in 0.7.1 and earlier (row 91).

    A store already in that state is not repaired by upgrading, because its snapshot carries no such record. If the store has never taken a truncating or an archiving snapshot — its directory holds no wal.<N>.archive, and it was never snapshotted with --truncate — move snapshot.bin aside and keep it: the store opens from the WAL alone, and the next snapshot is a good one. Do not do this on a store that has archives: mushroomdb snapshot and serve --snapshot-every archive by default, and without its snapshot such a store opens holding only what was written since the last archive, with no error.

    The same state can still follow a crash between a truncating snapshot and the replacement of its WAL, a window two filesystem calls wide. That is not fixed here (row 92). A store left that way opens once wal.bin is moved aside: the snapshot holds every node, edge and rule, and the full-text pairs and equality indexes have to be declared again, as may the multiplicity opt-in.

  • A node renamed onto a key a deleted node once held no longer makes the store unopenable. delete_node(b), rename_node(a, b), then any snapshot, and every later open failed with corrupt data: v8: labels section has 2 entries but id table has 1 total slots. The snapshot was right and its reader was not, so a store left that way by a truncating or an archiving snapshot opens again under this release with nothing lost. One left that way by a snapshot that kept its WAL does not come back right: it opens, and reads of the renamed node's neighbours fail. Recover it as the entry above says before opening it with this release. It is in 0.7.1 and earlier (row 94).

  • A history read on a node renamed away and back again returns. After rename_node(a, b) and rename_node(b, a), was_linked, node_history, edge_history and edges_at on that node never came back, on every surface, the MCP was_linked tool included (row 93). And was_linked now follows the node, not the name: an edge written while a node was called one thing and deleted while it was called another is gone, where it went on answering "linked" after two renames; and an earlier node that held the same key and was deleted is another node, whose edges and whose deletion no longer count.

  • A backup carries the commit-time map. mushroomdb backup, POST /backup and GraphDb::backup_to left commit_times.bin behind, and restore did not name it, so a restored store had its history and not the map a read by date resolves through. A backup taken by 0.7.1 or earlier does not hold the map and restores without it (row 86).

  • A batch sees a node it renamed. After rename_node(a, b) in a BatchBuilder or a submit_batch, the batch's own checks read b as a node with no stored properties, label or edges. So [rename a→b, insert_edge(b, d)] passed the cross-namespace guard that insert_edge(a, d) is refused by; [rename a→b, delete_edge(b, d)] deleted an edge a rule owns; [rename a→b, remove_prop(b, "name")] returned Ok and removed nothing; and a chain [a→b, b→c] left a stored id where two separate renames move it. The checks now read through to the stored node. Reachable only from a batch built in Rust: MCP, HTTP, the Python binding and the CLI each send a rename alone (row 79).

  • A key reused inside a batch names the new node. A batch that renamed a node away from a key, or deleted it, and then used the key again wrote against the old node: [delete_node a, insert_node a, set_prop(a, …)] lost the property, and [rename a→b, insert_node a, insert_edge(a, c)] put the edge on b. Also only from a batch built in Rust (row 89).

  • remember's matched counts a fact endpoint that already exists, each key once (row 48).

  • remember no longer declares full-text on an entity label it did not use. An entities entry whose key is already stored under another label is matched as stored; its own label used to be declared anyway, one pair against the cap for a label with no node. The report still does not say the label went unused (row 64).

  • The MCP create_rule schema lists approximate, which the tool accepts and suggest_rules can hand back (row 52).

  • A truncating snapshot no longer hands discarded commit numbers to the commits that follow. GraphDb::snapshot(), Python's snapshot(), mushroomdb snapshot --truncate and mushroomdb migrate discard the WAL. They left the store's floor where it was, so the replacement WAL was numbered over the old one: stats went on reporting history: complete (nothing pruned), and asof --commit 3, open_at(3), edges_at and was_linked answered a commit number taken before the snapshot with whatever commit now sat at that position — a different graph, with no error. The floor now moves to where the discarded history ended, as it already did when --retention pruned an archive. stats reports history: reaches back to commit N, and a read at a discarded commit is refused as out of range, naming the floor (row 80).

    This changes commit numbers after a truncating snapshot. What is written next is numbered above the old total, where it used to be numbered from 0, and wal_total_commits() no longer drops. A caller that relied on "commit 0 is my first write after snapshot()" must read history_floor and count up from it; the first write is not always at the floor, because a store with full-text fields or indexes re-records each declaration first. Archives taken before a truncating snapshot are removed by it: they are below the new floor.

    A store truncated by 0.7.1 or earlier is not repaired. It keeps reporting its history as complete, because nothing on disk records how many commits were discarded. Its next truncating snapshot stops numbers being reused from then on; a number held from before an older truncation can still be answered with the wrong commit.

  • A handle that has not refreshed no longer answers a history read from the wrong commits. was_linked, edges_at, node_history, edge_history and wal_total_commits read the WAL fresh from disk and numbered its frames from the floor the handle had in memory. A reader left open across another process's pruning or truncating snapshot therefore answered was_linked(a, b, KNOWS, 2) from a different commit 2. The floor is now read from disk with the frames. A Python reader process, which refreshes only when asked, was the caller most exposed (row 81).

  • An edge two rules both derive survives when one of them stops. Two rules may share a source label, a destination label and an edge type. When both wanted the same edge it had one owner, the rule that added it, and when that rule's predicate stopped holding the edge was removed — although the other rule still wanted it. It stayed gone until that rule was rebuilt or one of its own fields was written. Now, in the same write, every rule over that edge type is re-evaluated for the node that lost the edge, and a rule that still wants it takes it, at its own score. While both hold, the edge still carries one rule's score and explain names that rule only. It predates 0.7 (row 37).

  • A past-commit read whose base no longer exists is refused. After a truncating or archiving snapshot the live WAL starts above the store's first commit, on that snapshot as its base. A later keep_wal snapshot overwrote the base with the state at the head, and open_at, asof and query_at then replayed the WAL over nothing: a graph missing everything older than the WAL, with no error. Such a read now answers CommitOutOfRange. Reads of a store whose WAL does reach its first commit are unchanged (row 82).