24_social_network_cli

command
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 25 Imported by: 0

README

Example 24 — Social Network CLI

A one-shot command-line tool that walks every layer of GoGraph end to end on a small social-network domain:

  • a labelled property graph (graph/lpg) for users, posts, comments, follows, likes and reply threads;
  • WAL-backed transactional writes (store/wal + store/txn) and recovery from a snapshot plus the WAL tail (store/recovery);
  • manual checkpoints via store/snapshot.WriteSnapshotFull;
  • Cypher reads via cypher.NewEngineWithStore + Engine.RunInTx, streamed back as JSON Lines.
go run ./examples/24_social_network_cli <subcommand> -d <data-dir> [args]

Schema

                +-----------------+
                |     User        |  username, display_name, created_at
                +-----------------+
                  ^   |    |   |
        FOLLOWS   |   |    |   | AUTHORED
                  |   |    |   v
                +-+   |    |   +-----------------+
                | ... |    +-> |     Post        |
                +-----+        +-----------------+
                                  ^         ^
                              ON  |         | LIKED
                                  |         |
                                +-----------------+
                                |    Comment      |
                                +-----------------+
                                   |    ^      ^
                          REPLY_OF |    |      | LIKED
                                   v    |      |
                                +-----------------+
                                |    Comment      |
                                +-----------------+

Labels and relationship types are declared as constants in schema.go; the seed fixture and every helper share those names so a rename surfaces compilation errors in one place.

Subcommands

Subcommand What it does Reply
init -d <dir> Creates the data directory if missing and writes an empty initial snapshot. Idempotent. {"data_dir":"<abs>","status":"ok"}
seed -d <dir> [-users N] [-friends K] [-seed S] [-evidence] Inserts the deterministic fixture (5 users, 8 FOLLOWS, 3 Posts, 5 Comments, 7 LIKED) and, with -users N, an opt-in seeded synthetic population of N extra users with K FOLLOWS each. {"seeded":<bool>,"status":"ok"} (+ # telemetry with -evidence)
query -d <dir> [cypher] Runs a Cypher query (read or single-node write) and emits each record as one JSONL line. The query is taken from the positional argument or, if absent, from the entire stdin stream. one JSON object per row
snapshot -d <dir> Builds a CSR view of the current in-memory graph and writes a full snapshot (manifest + csr.bin + labels.bin + properties.bin + mapper.bin) alongside the WAL. The v3 manifest is self-sufficient: recovery can rebuild the graph from the snapshot alone, even when the WAL is empty or truncated. {"snapshot_dir":"<abs>","status":"ok"}
stats -d <dir> [-evidence] Runs the eight MATCH count(*) queries and returns one alphabetically-keyed JSON object. {"authored":N,"comments":N,…,"users":N} (+ # telemetry with -evidence)

Exit codes:

  • 0 on success;
  • 1 on runtime failure (Cypher error, I/O error, validation);
  • 2 on usage error (unknown subcommand, missing/malformed flags).

End-to-end session

DATA_DIR=/tmp/social
go run ./examples/24_social_network_cli init  -d "$DATA_DIR"
go run ./examples/24_social_network_cli seed  -d "$DATA_DIR"
go run ./examples/24_social_network_cli stats -d "$DATA_DIR"
go run ./examples/24_social_network_cli query -d "$DATA_DIR" \
    'MATCH (u:User) RETURN u.username AS username ORDER BY username'
go run ./examples/24_social_network_cli snapshot -d "$DATA_DIR"

A representative stats reply on a freshly-seeded directory:

{"authored":8,"comments":5,"follows":8,"likes":7,"on":5,"posts":3,"replies":2,"users":5}

A representative query (all users alphabetically) emits one JSONL record per row:

{"display_name":"Alice","username":"alice"}
{"display_name":"Bob","username":"bob"}
{"display_name":"Carol","username":"carol"}
{"display_name":"Dave","username":"dave"}
{"display_name":"Erin","username":"erin"}

The query subcommand also reads from stdin, so it pipes naturally into jq:

echo 'MATCH (u:User)-[:FOLLOWS]->(v:User) RETURN u.username AS from, v.username AS to' \
  | go run ./examples/24_social_network_cli query -d "$DATA_DIR" \
  | jq -c '{from, to}'

Scale and flags

The fixture above is a hand-written demonstration shape. To exercise the module at a size where its behaviour is actually observable, the seed subcommand takes three opt-in scale knobs that layer a seeded, reproducible synthetic population on top of the fixture. All three are off by default, so the deterministic output is unchanged unless you ask for more.

Flag Meaning Default Representative large value
-users N Number of extra seeded :User nodes to append (0 = canonical fixture only). 0 1000000
-friends K :FOLLOWS out-degree per synthetic user (must be < users). 8 50
-seed S RNG seed; fixes the synthetic data shape exactly. 1 any int64
-evidence Print # telemetry after the JSON reply (on both seed and stats). off

The synthetic users carry the same username / display_name / created_at shape as the fixture and use a namespaced key (u_<hex>), so they collide with neither the fixture keys (alice..erin) nor the Cypher CREATE synthetic keys. They are counted by stats and walked by FOLLOWS traversals exactly like the fixture, so a scaled run drives the WAL, recovery, CSR snapshot, and Cypher engine at size. The whole seed — fixture plus synthetic population — commits in a single durable transaction.

go run ./examples/24_social_network_cli init -d "$DATA_DIR"            # small deterministic default
go run ./examples/24_social_network_cli seed -d "$DATA_DIR" \
    -users 1000000 -friends 50 -seed 7 -evidence                       # observable-scale run

A given -seed yields byte-identical deterministic facts on any machine; only the # telemetry varies per run.

Evidence it collects

With -evidence, the two heaviest subcommands report the persistence-and-Cypher evidence dimensions from docs/examples-standard.md. Facts are bare lines (pinned by the tests); telemetry is # -prefixed and ignored by the tests.

seed -evidence reports the synthetic build:

{"seeded":true,"status":"ok"}
# scale.users=1000000          # fact-shaped but scale-dependent, so telemetry
# scale.follows=50000000
# seed.elapsed=...             # varies per run / machine
# seed.node_rate=... nodes/s
# seed.edge_rate=... edges/s
# mem.heap_alloc=... GiB
# mem.heap_growth=... GiB

stats -evidence reports graph size, live heap, and per-query latency:

{"authored":8,"comments":5,"follows":50000008,"likes":7,"on":5,"posts":3,"replies":2,"users":1000005}
# graph.order=1000013
# graph.size=50000030
# mem.heap_alloc=... GiB
# q.users.latency=...          # one line per count query
# q.follows.latency=...
# ...

When you scale up, watch mem.heap_alloc and # bytes-shaped figures for the resident footprint, and the # q.*.latency lines for which count queries (label scans vs relationship scans) dominate at size.

Architecture

        ┌──────────────┐
        │  os.Args     │
        └──────┬───────┘
               │
               v
        ┌──────────────┐        ┌─────────────────────┐
        │  dispatch    │  ───►  │  cmdInit / cmdSeed  │
        │  main.go     │        │  cmdQuery /          │
        │              │        │  cmdSnapshot / cmdStats │
        └──────┬───────┘        └─────────┬───────────┘
               │                          │
               │     openedStore.Close    │ openStore(ctx, dir)
               │       fsyncs the WAL     │
               v                          v
        ┌──────────────────────────────────────────────┐
        │  recovery.Open[string, float64](dir, opts)   │  read snapshot + WAL
        │  wal.Open(<dir>/wal)                         │  append-only WAL writer
        │  txn.NewStoreWithOptions(graph, wal, opts)   │  WAL-backed store
        │  cypher.NewEngineWithStore(store)            │  Cypher engine
        └──────────────────────────────────────────────┘
                                │
                                │  RunInTx / WriteSnapshotFull
                                v
                       ┌────────────────┐
                       │   data dir     │
                       │ ─ snapshot/    │
                       │ ─ wal          │
                       └────────────────┘

store_helpers.go centralises the wiring: openStore is the single entry point every read/write subcommand uses, and initEmpty is the single bootstrap. The shared [string, float64] codec pair (txn.NewStringCodec, txn.NewFloat64WeightCodec) is pinned in dataDirOptions so every layer agrees on encoding.

Tests

go test -race ./examples/24_social_network_cli/...

The package's cli_test.go walks the full init → seed → query → snapshot → stats cycle in one process, captures each subcommand's stdout via os.Pipe, and compares the byte stream against the goldens under testdata/. TestMain plugs in go.uber.org/goleak so every test in the package doubles as a goroutine-leak check, and the cross-process tests build the binary and drive the lifecycle as separate processes to prove durability and determinism survive a kill -9-style restart.

scale_test.go covers the opt-in scale and evidence paths: it asserts that the default (no flags) output is byte-for-byte unchanged, that a scaled seed's deterministic counts match 5 + N users and 8 + N·K follows, that a fixed -seed reproduces the same facts, and that the JSON fact line is identical whether -evidence is on or off — the only difference being the # telemetry block, which the tests never assert on.

History

The example originally documented three engine constraints — CREATE with RETURN, multi-edge CREATE / MATCH+CREATE-relationship, and cross-process snapshot label drift. All three were fixed in Sprint 56 of the gograph roadmap (tasks #498, #499, #500). The seed subcommand still uses the direct txn.Tx API rather than Cypher CREATE so it mirrors examples/04_persistence and stays independent of the Cypher write planner.

Documentation

Overview

Package main implements `24_social_network_cli`, an example one-shot CLI that demonstrates how to build, persist and query a labelled property graph for a social-network domain using GoGraph.

The example exercises four pillars of the module in a single deliverable:

  1. Graph initialisation with a labelled property graph (LPG) backend.
  2. Crash-safe ACID persistence via a write-ahead log plus snapshots (recovery.Open[string, float64] and snapshot.WriteSnapshotFull).
  3. CRUD via Cypher through a WAL-backed engine (cypher.NewEngineWithStore and Engine.RunInTx).
  4. A small CLI surface that accepts ad-hoc Cypher queries from positional arguments or stdin and streams results as JSON Lines.

Schema

Node labels and their natural keys:

  • User — `username` (string, unique natural key)
  • Post — `id` (string, unique natural key)
  • Comment — `id` (string, unique natural key)

Node properties (all timestamps are ISO-8601 UTC strings, fixed values in the seed fixture so the regression baseline is byte-deterministic):

  • User.username string
  • User.display_name string
  • User.created_at string
  • Post.id string
  • Post.text string
  • Post.created_at string
  • Comment.id string
  • Comment.text string
  • Comment.created_at string

Relationship types:

  • (:User)-[:FOLLOWS]->(:User) a user follows another user
  • (:User)-[:AUTHORED]->(:Post|:Comment) authorship of a post or comment
  • (:Comment)-[:ON]->(:Post) a comment is attached to a post
  • (:Comment)-[:REPLY_OF]->(:Comment) a comment is a reply to another
  • (:User)-[:LIKED]->(:Post|:Comment) polymorphic like edge

Subcommands

All subcommands require the data directory flag `-d <dir>`. The directory holds the WAL plus snapshot files managed by `recovery` and `snapshot`. The CLI exits with code 0 on success, 1 on runtime failure (I/O, Cypher, validation), or 2 on usage errors (unknown subcommand, missing or malformed flags).

init -d <dir>
    Open or create the data directory. If the directory does not
    exist it is created (mkdir -p). An empty initial snapshot is
    written so that subsequent reopens via recovery.Open succeed
    even before any writes. Idempotent: running init twice
    on the same directory is a no-op.
    On success prints one JSON object:
        {"data_dir":"<absolute path>","status":"ok"}

seed -d <dir> [-users N] [-friends K] [-seed S] [-evidence]
    Populate the graph with a deterministic fixture: 5 users
    (alice, bob, carol, dave, erin), 8 :FOLLOWS edges, 3 :Post
    nodes with their :AUTHORED edges, 5 :Comment nodes attached
    via :ON (some chained via :REPLY_OF) and 7 :LIKED edges
    spanning both posts and comments. The writes go through the
    direct txn.Store / txn.Tx API, mirroring the canonical
    pattern in examples/04_persistence so the seed remains
    independent of the Cypher write planner. Idempotent: running
    seed twice is a no-op when at least one :User node is
    already present. The reply is:
        {"seeded":<bool>,"status":"ok"}

    Scale knobs (all opt-in; the default reproduces the fixture
    byte-for-byte so the regression goldens stay valid):
        -users N    append N extra seeded :User nodes on top of the
                    fixture (0, the default, = fixture only).
        -friends K  :FOLLOWS out-degree per synthetic user (default 8).
        -seed S     RNG seed that fixes the synthetic data shape
                    (default 1). The same seed yields byte-identical
                    deterministic facts on any machine.
    The synthetic users carry the same username / display_name /
    created_at shape as the fixture and share keys with neither the
    fixture (alice..erin) nor the Cypher CREATE counter, so they are
    counted by stats and walked by FOLLOWS traversals just like the
    hand-written fixture. The whole seed — fixture plus synthetic
    population — commits in one durable transaction.

    With -evidence the JSON reply is followed by "# "-prefixed
    telemetry: synthetic-population throughput (nodes/s, edges/s),
    elapsed wall-clock, and live Go heap. Telemetry varies per run
    and per machine and is never part of the deterministic output.

query -d <dir> [cypher]
    Run a Cypher query (read or write) against the data directory.
    The query is read from the positional argument; if absent, the
    entire stdin stream is consumed and used as the query. Each
    Cypher Record is emitted as a single JSON object on its own
    line (JSON Lines, RFC 7464 framing). No envelope, no summary.
    Examples:
        social query -d data 'MATCH (u:User) RETURN u.username AS username'
        echo 'MATCH (p:Post) RETURN p.id AS id' | social query -d data

snapshot -d <dir>
    Force a manual checkpoint by calling
    snapshot.WriteSnapshotFull on the current in-memory state.
    On success prints one JSON object containing the snapshot
    directory and the manifest path:
        {"snapshot_dir":"<dir>","status":"ok"}

stats -d <dir> [-evidence]
    Count nodes by label and edges by relationship type. The
    output is a single JSON object with alphabetically ordered
    integer keys:
        {"authored":N,"comments":N,"follows":N,"likes":N,
         "on":N,"posts":N,"replies":N,"users":N}
    (`replies` counts :REPLY_OF edges.)

    With -evidence the JSON object is followed by "# "-prefixed
    telemetry: the graph order and size read from the adjacency
    list, the live Go heap, and the per-query wall-clock latency of
    each of the eight count queries. The counts stay the
    deterministic facts; only the telemetry varies per run.

Output Format (JSON Lines)

Every Cypher Record returned by `query` is encoded as one JSON object per line, terminated by `\n`. Map keys are emitted in alphabetical order so that the byte stream is reproducible.

Value type mapping from the Cypher runtime value model (expr.Value) to JSON, performed by output.go's jsonValue / jsonExprValue helpers:

  • expr.IntegerValue -> JSON integer
  • expr.FloatValue -> JSON float
  • expr.StringValue -> JSON string
  • expr.BoolValue -> JSON boolean
  • expr.ListValue -> JSON array
  • expr.MapValue -> JSON object (alphabetically keyed)
  • expr.NodeValue -> JSON object with the leading-underscore fields {_id, _labels, _properties} (neo4j-go-driver compatible)
  • expr.RelationshipValue -> JSON object with the fields {_id, _type, _start, _end, _properties}
  • expr.Null -> JSON null
  • graph.NodeID -> JSON integer
  • native Go scalars -> passthrough
  • []byte -> JSON string (avoids base64)
  • other values -> Stringer or %v fallback

A write-only Cypher statement (CREATE / SET / DELETE without RETURN) produces one synthetic empty row that the engine uses to drive its pipeline; query filters those rows out so the stream stays a faithful "rows" view.

Persistence Contract

The data directory contains <dir>/wal (the append-only write-ahead log) and <dir>/snapshot/* (manifest plus csr.bin, labels.bin, properties.bin, mapper.bin and any per-index files). The v3 manifest emitted for string-keyed graphs (every graph the CLI produces) is self-sufficient: the snapshot alone carries enough state to rebuild the in-memory graph without any WAL frames. On open, recovery.Open (the canonical [string, float64] generic entry point) restores the natural-key interning table from mapper.bin, applies the CSR adjacency, attaches labels.bin and properties.bin, then replays any WAL tail on top. Every write performed through Engine.RunInTx is appended to the WAL with fsync at commit, so a process crash mid-write leaves the data directory recoverable.

The CLI is one-shot: every invocation opens the data directory, performs its operation, and closes the recovery handle. There is no background process and no long-running file lock between invocations.

Example Invocation

A typical end-to-end session:

go run ./examples/24_social_network_cli init     -d /tmp/social
go run ./examples/24_social_network_cli seed     -d /tmp/social
go run ./examples/24_social_network_cli stats    -d /tmp/social
go run ./examples/24_social_network_cli query    -d /tmp/social \
    'MATCH (u:User)-[:FOLLOWS]->(v:User) RETURN u.username AS from, v.username AS to'
go run ./examples/24_social_network_cli snapshot -d /tmp/social

An observable-scale session that seeds a synthetic population and reports the evidence (build throughput, live heap, per-query latency):

go run ./examples/24_social_network_cli init  -d /tmp/social
go run ./examples/24_social_network_cli seed  -d /tmp/social \
    -users 100000 -friends 20 -seed 7 -evidence
go run ./examples/24_social_network_cli stats -d /tmp/social -evidence

Evidence

The CLI follows the persistence-and-Cypher evidence taxonomy from docs/examples-standard.md. The seed subcommand reports synthetic-build throughput (nodes/s, edges/s) and live heap; the stats subcommand reports graph order and size, live heap, and per-query latency for each count. Deterministic facts (the JSON replies, the counts) are printed as bare lines and pinned by the regression tests; volatile telemetry is printed only as "# "-prefixed lines so a test ignores it. The synthetic population is produced by a seeded math/rand generator, so a given -seed fixes the deterministic facts exactly across machines.

History

The three engine constraints originally documented here — CREATE with RETURN, multi-edge CREATE, and cross-process snapshot drift — were fixed in Sprint 56 of the gograph roadmap (tasks #498, #499, #500). The corresponding regression tests live in cypher/write_with_return_test.go, cypher/multi_edge_create_test.go, graph/mapper_stable_test.go and the cross-process round-trip in cross_process_test.go in this package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL