# Phase 5 — Go Client SDK > A typed Go client with subscription-first design — subscribe to a live query in 5 lines, deltas arrive on a channel. **Previous**: [Phase 4 — Client API](./04-client-api.md) | **Next**: [Phase 6 — Low-Code Full-Stack](./06-lowcode-fullstack.md) | **Index**: [database.md](../database.md) --- Concrete scenario: a developer writes a Go backend that serves a web frontend, and uses the `.wo` database as the store. They must be able to **subscribe to a query in ~5 lines of idiomatic Go** and have deltas arrive on a channel. Everything else the SDK does — connect, query, mutate, transact — is table stakes covered by every existing Go DB driver. Subscriptions are what this SDK has to get right. ## Target API Surface The engine speaks `.wo` on the wire. Every SDK method — typed or untyped — is a thin wrapper over one primitive: **send a `.wo` source string with `$name` parameters, get back a uniform `Result`**. ```go import "go.writeonce.dev/wo" // 1. Connect client, err := wo.Connect(ctx, "wo://db.example.com:5555", wo.WithAPIKey(os.Getenv("WO_KEY")), wo.WithTLS(tlsConfig), ) defer client.Close() // 2. Wo — the primitive: run ANY .wo source (one statement or a whole // BEGIN...COMMIT block mixing SQL, Cypher, and document updates). Params // use the $name rule from the .wo language spec. result, err := client.Wo(ctx, ` UPDATE products SET inventory.on_hand -= $qty WHERE id = $pid AND inventory.on_hand >= $qty RETURNING id AS pid; INSERT INTO orders (user_id, total_cents, status) VALUES ($uid, $total, 'pending') RETURNING id AS oid; MATCH (u:user {id: $uid}), (p:product {id: $pid}) CREATE (u)-[:PURCHASED {order_id: $oid, qty: $qty, at: now()}]->(p); `, wo.Params{ "uid": uid, "pid": 42, "qty": 2, "total": 9800, }) // Result layout: // result.Rows — rows from trailing SELECT/MATCH/RETURNING statements // result.Aliases — the RETURNING alias table: {"pid": 42, "oid": 17} // result.Affected — rows touched by INSERT/UPDATE/DELETE, per-statement // 3. Query — sugar over Wo that scans trailing rows into a typed destination. var products []Product err = client.Query(ctx, "SELECT id, sku, price_cents, meta FROM products WHERE price_cents < $max", wo.Params{"max": 5000}, ).Scan(&products) // 4. Exec — sugar for writes that don't return rows. _, err = client.Exec(ctx, "UPDATE products SET price_cents = $new WHERE id = $id", wo.Params{"new": 4900, "id": 42}, ) // 5. Tx — wraps Wo/Query/Exec in a server-side BEGIN...COMMIT. The callback's // return value decides commit vs rollback. Useful when the program needs // to branch between statements on intermediate results. err = client.Tx(ctx, func(tx *wo.Tx) error { r, err := tx.Wo(ctx, `UPDATE products SET inventory.on_hand -= $qty WHERE id = $pid AND inventory.on_hand >= $qty RETURNING id AS pid;`, wo.Params{"pid": 42, "qty": 2}) if err != nil { return err } if r.Affected[0] == 0 { return wo.ErrInsufficientInventory } _, err = tx.Wo(ctx, ` INSERT INTO orders (user_id, status) VALUES ($uid, 'pending') RETURNING id AS oid; MATCH (u:user {id: $uid}), (p:product {id: $pid}) CREATE (u)-[:PURCHASED {order_id: $oid, qty: $qty}]->(p); `, wo.Params{"uid": uid, "pid": 42, "qty": 2}) return err }) ``` `Wo` is the primitive; `Query`, `Exec`, and `Subscribe` are typed sugar. If the engine accepts the `.wo` source on disk, `client.Wo` accepts the same string over the wire. ### When to use raw `.wo` vs typed codegen Both styles coexist in the same program; they share the connection pool. | Use raw `.wo` (`client.Wo`) when | Use typed codegen (`client.Orders.Create`, etc.) when | | --- | --- | | Ad-hoc queries, admin tools, one-off scripts | The app's hot path — compile-time schema checking + IDE autocomplete pay for themselves | | Cross-cutting queries that join multiple generated types | Per-type CRUD + subscriptions | | Multi-statement transactions threading `RETURNING` aliases | Single-statement operations | | DB repair, data migration, ad-hoc analytics | Anything the codegen already covers | | You want to paste a block from a `.wo` source file straight into Go | You want refactor-safe struct field access | The canonical rule: **write typed code first, drop to raw `.wo` when the type system gets in the way**. They interleave freely — a typed `client.Orders.Subscribe(...)` can run next to a raw `client.Wo(...)` admin query in the same handler. ## Subscribe — The Primary Use Case Idiomatic Go for a stream of values is a channel read inside a `for` loop, cancelled by `context.Context`. That is the exact shape a `.wo` subscription should take: ```go sub, err := client.Subscribe(ctx, "LIVE SELECT sku, inventory.on_hand FROM products WHERE sku IN $skus", wo.Params{"skus": cartSkus}, ) if err != nil { return err } defer sub.Close() for delta := range sub.Deltas() { switch d := delta.(type) { case wo.Insert: log.Printf("new row: %+v", d.Row) case wo.Update: log.Printf("sku=%s on_hand=%d -> %d", d.Key, d.Old["on_hand"], d.New["on_hand"]) case wo.Delete: log.Printf("removed: %s", d.Key) case wo.Resync: // server dropped our queue — refetch and resume currentState = refetch() } } // loop exits when: // - ctx cancelled (client shutdown) // - sub.Close() called (defer) // - server sent COMPLETE (schema change, permission revoked) // sub.Err() returns the reason if err := sub.Err(); err != nil { log.Fatal(err) } ``` **Contract:** - `sub.Deltas()` returns `<-chan wo.Delta` — standard read-only channel. The SDK closes it when the subscription ends. - `ctx` cancellation immediately stops deliveries and closes the channel. No leaked goroutines. - Ordering: deltas arrive in commit order. A `DELTA` on the wire always reflects a committed transaction. - Back-pressure: the channel has a bounded buffer (default 1024). If it fills, the SDK's policy kicks in (see below). ## Typed SDK via `.wo` Schema Codegen The `.wo` **schema layer** ([Phase 2](./02-wo-language.md)) declares types. `wo-gen` reads the type DSL — not the underlying `##sql`/`##doc`/`##graph` blocks — as its input; that way one Go struct corresponds to one entity, with embedded documents and graph-edge projections folded in naturally. ```wo type Product { id: Id sku: SKU @unique price: Money meta: { title: Text, description: Markdown, images: [Url], reviews: [Review] } inventory: { on_hand: Int @check(>= 0), reserved: Int = 0, reorder_at: Int } purchased_by: multi User via Purchase -- inverse graph link } ``` ```bash wo-gen --schema ./schema.wo --out ./internal/wodb ``` Produces: ```go package wodb // from `type Product` — embedded structs compile from the inline `{...}` fields type Product struct { ID int64 `wo:"id"` SKU string `wo:"sku"` Price Money `wo:"price"` Meta ProductMeta `wo:"meta"` Inventory InventoryLvl `wo:"inventory"` PurchasedBy []PurchaseEdge `wo:"purchased_by"` // link-with-props → edge struct } // embedded document inside Product.Meta type ProductMeta struct { Title string `wo:"title"` Description string `wo:"description"` Images []string `wo:"images"` Attributes map[string]string `wo:"attributes"` Reviews []Review `wo:"reviews"` } // graph link carrying properties — target + edge props in one struct type PurchaseEdge struct { Target User `wo:"target"` Order int64 `wo:"order"` Qty int `wo:"qty"` At time.Time `wo:"at"` } // registered live queries become typed helpers func InventoryChanged(ctx context.Context, c *wo.Client, skus []string) (*wo.TypedSubscription[InventoryLvl], error) ``` One type declaration → one Go struct. Zero-property graph edges (`multi User @edge(:FOLLOWS)`) generate `Friends []User`; link-with-properties types generate `[]EdgeStruct`; computed fields become read-only struct fields populated by the planner. **Transition path.** While the Phase 2 prototype is still authored directly in `##sql/##doc/##graph` blocks (the query layer), `wo-gen` accepts either — a file of type declarations, or the raw paradigm blocks — and emits the same Go output. The type DSL becomes mandatory only once Phase 6 full-stack blocks (which attach to types) start shipping. Typed subscription loop loses all `interface{}` ceremony: ```go sub, err := wodb.InventoryChanged(ctx, client, cartSkus) if err != nil { return err } defer sub.Close() for d := range sub.C { switch d.Kind { case wo.DeltaUpdate: log.Printf("sku=%s now %d in stock", d.Key, d.New.OnHand) } } ``` Generics (Go 1.18+) make `TypedSubscription[T]` a single parameterized type — no per-query generated struct. Only the `T` struct itself is generated. ## Connection Lifecycle ```go type Client struct { // opaque; holds a connection pool, codec, session registry } func Connect(ctx context.Context, dsn string, opts ...Option) (*Client, error) func (c *Client) Close() error func (c *Client) Ping(ctx context.Context) error ``` Inside the SDK, one TCP connection per client is fine for native protocol (multiplexed), but a small pool (2–4) helps when one connection's receive goroutine is saturated decoding a large result set. Connection state: - **Connecting** → `HELLO` sent, waiting for `WELCOME` - **Ready** → normal operation - **Reconnecting** → transient network error; automatic exponential backoff; all subscriptions queued for re-registration - **Closed** → terminal **Reconnection semantics for subscriptions** (the subtle part): on reconnect, the SDK re-sends every active `SUBSCRIBE` frame. The server replies with a `RESYNC` marker and the current matching state. The app's subscription channel emits a single `wo.Resync{}` value so the consumer knows to rebuild local state. No delta is silently lost, no delta is silently duplicated. ## Options Fluent options, not a bloated config struct: ```go wo.WithAPIKey(key string) wo.WithJWT(token string) wo.WithMTLS(cert tls.Certificate) wo.WithTLS(cfg *tls.Config) wo.WithPoolSize(n int) // default 2 wo.WithSubscriptionBuffer(n int) // default 1024 wo.WithOverflowPolicy(wo.DropAndResync | wo.Coalesce | wo.Disconnect) wo.WithLogger(l *slog.Logger) wo.WithRetry(wo.RetryPolicy{...}) wo.WithProtocol(wo.ProtocolNative | wo.ProtocolGraphQL) // native default ``` ## Transactions `client.Tx` maps to [Phase 2's](./02-wo-language.md) `BEGIN ... COMMIT`. The callback's return value decides commit vs rollback: - `return nil` → `COMMIT` sent, error only if server rejects commit - `return err` → `ROLLBACK` sent, original `err` surfaced to caller - `panic` → `ROLLBACK` sent, panic re-raised - `ctx` cancel → `ROLLBACK` sent, `ctx.Err()` returned Nested `tx.Wo`/`tx.Query`/`tx.Exec` route to the same server-side transaction — no connection hopping. The SDK enforces this by pinning the transaction to one connection for its lifetime. `RETURNING` aliases bound by one statement in the txn are visible to every later statement in the same txn through the server-side alias table (see [Phase 2 — Transaction Coordinator](./02-wo-language.md#cross-paradigm-transaction-coordinator)), so a Go `tx.Wo` call can leave `$oid` set and the next `tx.Wo` call can use it. ## Back-Pressure Handling in the SDK The server's back-pressure policy from [Phase 4](./04-client-api.md) is mirrored client-side: | Client situation | SDK behavior | | --- | --- | | Consumer reading channel fast enough | Normal delivery | | Channel buffer full (1024 unread deltas) | Per `WithOverflowPolicy`: drop buffered + emit `wo.Resync`, or coalesce same-key updates, or close the subscription with `ErrOverflow` | | Network stalled | `ctx.Deadline` + keepalive `PING` every 10s; stall > 30s → disconnect and reconnect | | Server closed subscription | Channel closed, `sub.Err()` returns reason (schema change, permission revoked, engine shutdown) | **Never block the receive goroutine on a full channel.** The SDK drains the socket no matter what; overflow policy decides what to do with the deltas it can't deliver. ## Full Cart-Inventory Example End-to-end: Go HTTP handler that renders a cart page and keeps its inventory line live via Server-Sent Events to the browser. The SDK drives the upstream subscription to `.wo`: ```go func (h *Handler) cartInventoryStream(w http.ResponseWriter, r *http.Request) { skus := parseSkus(r.URL.Query().Get("skus")) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") flusher := w.(http.Flusher) sub, err := wodb.InventoryChanged(r.Context(), h.db, skus) if err != nil { http.Error(w, err.Error(), 500); return } defer sub.Close() for d := range sub.C { payload, _ := json.Marshal(d) fmt.Fprintf(w, "event: inventory\ndata: %s\n\n", payload) flusher.Flush() } } ``` The browser connects once with `new EventSource('/cart/inventory?skus=...')`. The Go handler holds one subscription to `.wo`. When inventory commits in the database, the delta flows: engine → subscription registry → Go SDK channel → SSE stream → DOM update. Zero polling anywhere in the chain. ## Go-Specific Design Details | Go idiom | Application | | --- | --- | | `context.Context` threading | Every method takes `ctx` as first arg; cancellation propagates to the wire | | `io.Closer` | `Client`, `Tx`, `Subscription` all implement `Close() error` | | Small interfaces | `type Runner interface { Wo(ctx, src, params) (wo.Result, error) }` — `*Client` and `*Tx` both satisfy it, so helper functions compose cleanly | | `database/sql`-style `Scan` | `client.Query(...).Scan(&dest)` accepts struct, slice of struct, or primitives | | `sql.Null*` analogues | `wo.NullString`, `wo.NullInt64`, `wo.NullDoc` for optional doc columns | | Struct tags | `wo:"column_name"` + JSON-style for nested doc fields (`wo:"meta.title"`) | | `errors.Is` / `errors.As` | `errors.Is(err, wo.ErrConflict)`, `wo.AsError(err, &woErr)` | | No goroutine leaks | Every background goroutine tied to ctx or a sync.WaitGroup closed in `Client.Close()` | | Testing via interfaces | `wo.DB` interface; provide `wotest.NewMock()` for unit tests; real embedded engine for integration | ## Comparison With Existing Go DB SDKs | SDK | Query style | Subscriptions | Transactions | Typed results | | --- | --- | --- | --- | --- | | `database/sql` + `pq` | SQL strings | No | Yes | Manual `Scan` | | `pgx` | SQL strings | `LISTEN/NOTIFY` only (no row-level) | Yes | Manual or `pgxscan` | | `sqlc` | Generated Go funcs from `.sql` | No | Yes | Generated structs | | `ent` | ORM | No | Yes | Generated | | `go-redis` | Commands | Pub/sub + keyspace notifications (no query) | Multi/Exec | Manual | | `surrealdb/surrealdb.go` | Raw queries | `Live()` returning channel | Yes | Manual | | `gqlgen` / `machinebox/graphql` | GraphQL docs | WebSocket subscriptions | N/A | Generated | | **`sa`** (this design) | `.wo` queries | **Native `LIVE` → typed channel** | Yes | Codegen from schema | The reference points are `sqlc` (for the codegen pipeline) and `surrealdb-go` (for the subscription channel API). Combining their best ideas and tightening the subscription contract is what this SDK is. ## Reference Implementations To Steal From - **surrealdb/surrealdb.go** — `Live()` returns a channel; closest API precedent. - **jackc/pgx** — reference quality for a Go database driver. Connection pool, copy protocol, prepared statements all done right. - **sqlc-dev/sqlc** — codegen from SQL to typed Go. The model for `.wo` → Go. - **Khan/genqlient** — generated typed GraphQL client. Ergonomic precedent for typed query helpers. - **nats-io/nats.go** — subscription-first API, back-pressure handled well. `sub.NextMsg(ctx)` and channel-based `ChanSubscribe` both supported. - **hasura/go-graphql-client** — GraphQL subscriptions over WebSocket in Go. ## SDK Delivery | Artifact | Purpose | | --- | --- | | `go.writeonce.dev/wo` | Runtime package: client, query, subscribe | | `go.writeonce.dev/wo/wotest` | Mock client + in-memory engine for unit tests | | `wo-gen` binary | Reads `schema.wo`, emits typed Go code | | Go module example repo | Cart + inventory demo wired end-to-end | | Generated docs | `go doc` + hosted examples | Publishing strategy: semantic versioning, `v0.x` while the wire protocol is unstable, `v1.0` only after the protocol is frozen. ## Why The SDK Matters As Much As The Engine A database with a beautiful engine and a painful client is a database no one uses. The e-commerce Go backends this targets are built under deadline — if `Subscribe` is not as easy as opening a channel, developers will reach for polling (`time.Tick` + `SELECT`) and defeat the whole architecture. The success metric is blunt: **a developer who has never seen `.wo` before should have a working subscription to a live query inside 15 minutes**, counting install, schema codegen, and the first delta landing on their channel. If the SDK is any harder than that, the rest of this doc is academic. ## Future SDKs Same shape, other languages: - **TypeScript / browser** — fetch + WebSocket for GraphQL subscriptions; types via codegen from `.wo`. Highest priority after Go for a web-first product. - **Rust** — direct native protocol, `tokio`-friendly, `impl Stream` for subscriptions. - **Python** — async/await, `async for delta in sub` idiom. - **Java / Kotlin** — Flow (Kotlin) or Reactive Streams (Java) for subscriptions. Each follows the same rule: subscribe-to-query must be the shortest, most obvious thing in the API.