24_social_network_cli

command
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 21 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> Inserts the deterministic fixture (5 users, 8 FOLLOWS, 3 Posts, 5 Comments, 7 LIKED). {"seeded":<bool>,"status":"ok"}
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> Runs the eight MATCH count(*) queries and returns one alphabetically-keyed JSON object. {"authored":N,"comments":N,…,"users":N}

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}'

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.

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>
    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"}

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>
    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.)

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

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