No description
  • Go 97%
  • Shell 3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Yannick Koechlin faa907f32b
spadino-sdk-go: standalone Go SDK (extracted from spadino@M6)
Vendored spadino.v1 protos with provenance, generated connect clients, typed
error policy tables (NotWriter/Overloaded/LimitExceeded/DedupHorizonExceeded),
thin receipt wrapper. go build/vet/test + gofmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 19:46:46 +02:00
examples spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
gen/spadino/v1 spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
proto spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
.gitignore spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
buf.gen.yaml spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
client.go spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
errors.go spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
generate.sh spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
go.mod spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
go.sum spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
query.go spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
README.md spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
sync-protos.sh spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00
write.go spadino-sdk-go: standalone Go SDK (extracted from spadino@M6) 2026-08-12 19:46:46 +02:00

spadino-sdk-go — Go client for the spada search engine

The official Go SDK for spada's public spadino.v1 gRPC surface: an idiomatic wrapper over generated stubs, with the semantics the wire cannot express encoded in the API — durability, coverage receipts, and table-driven retry policy.

go get git.teixos.net/yannick/spadino-sdk-go

Quickstart

import (
    "context"
    spada "git.teixos.net/yannick/spadino-sdk-go"
)

client, err := spadino.Connect("127.0.0.1:50051", spadino.Options{})
if err != nil { /* … */ }
defer client.Close()
ctx := context.Background()

// Idempotent, flushed write: the ack is a durability barrier.
out, err := client.Write(ctx, "articles", []spadino.Doc{
    {DocID: "munich", Text: "# Munich\n\nBayern beats the rain.",
        Fields: map[string]string{"lang": "en"}},
}, spadino.WriteOptions{MutationID: "ingest-0001", Flush: true})

// Query — and READ THE RECEIPT: EXACT is a proof, not a mood.
answer, err := client.Search(ctx, "articles", spadino.Query{
    Terms: []string{"bayern"}, K: 10,
})
fmt.Println("receipt:", answer.Receipt()) // e.g. "EXACT"
for _, hit := range answer.Hits {
    fmt.Printf("%s %.3f\n", hit.DocId, hit.Score)
}

Runnable versions: examples/quickstart and examples/typederrors — start a dev server with spada serve --data-dir /tmp/demo --listen 127.0.0.1:50051, then go run ./examples/quickstart.

The three contracts

1. Durability: Flush is the barrier, SealTriggered is not

A non-flush write's ack means accepted and query-visible. The durable checkpoint completes in the background (spada seals through a pipelined worker pool — the response's flag means a seal was triggered). When "this call returning" must mean "on disk", set WriteOptions.Flush. A flush drains the server's seal pool and can block for seconds; the SDK gives flushing calls the longer FlushTimeout (default 120 s) automatically.

Client.Ingest streams a corpus over one WriteStream in batches bounded by both count and bytes, flushing on the last message — its report means durable. Note WriteStream's contract: strictly serial, one response, no cross-message transaction — a mid-stream failure leaves prior messages applied.

2. Receipts: EXACT is a proof, everything else is a disclosure

r := answer.Receipt()
if !r.IsExact() {
    log.Printf("not certified: %s", r) // APPROXIMATE [stages] / UNPROVEN (why)
}
if r.UnitsDropped() > 0 {
    // A referenced segment was unreadable: the answer is PARTIAL.
}

Watch for the stage "query-embed-skipped": a hybrid query silently degraded to lexical-only because no embedding model was available. If your application must never act on an uncertified answer, set Query.ExactnessRequired — the server refuses (typed) instead of degrading.

3. Errors: the detail type is the retry policy

Since spada S-150, structured refusals ride the standard grpc-status-details-bin trailer as a genuine google.rpc.Status — this SDK decodes it through the stock google.golang.org/grpc/status machinery, no spada-specific transport code. Never parse error messages:

if _, err := client.Search(ctx, ns, q); err != nil {
    if e, ok := spadino.Classify(err); ok {
        switch e.Policy() {
        case spadino.RetryWithBackoff: // transient: back off, retry here
        case spadino.RetryElsewhere:   // re-resolve routing, retry there
        case spadino.OperatorAction:   // page someone; do NOT spin
        case spadino.RetryNever:       // fix the request
        }
        if l, ok := e.Detail.(*pb.LimitExceeded); ok {
            log.Printf("raise %s (limit %d, asked %d)", l.SettingKey, l.Limit, l.Actual)
        }
    }
}

The two details that matter most: HomeReleased — the one FAILED_PRECONDITION that IS retryable (the namespace moved home; re-resolve and retry there) — and SealerFailed — the server fail-stopped writes; retrying can never succeed and even backing off is wrong; surface it to an operator (reads keep working). A status with no detail is normal (the code alone yields the safe policy), and an unknown detail type from a newer server degrades to that same safe reading: spadino.v1 is additive-only, machine-enforced upstream.

Idempotent writes

Set WriteOptions.MutationID on any write you might retry. A retry with the same id returns the original result verbatim (Deduplicated) and writes nothing. The horizon is finite (write.dedup_history server-side): past it a retry is refused typed, never silently re-executed — bound your retry window well inside it. And cite DocVersions for identity, never DocID (a mutable attribute over a succession of versions).

Beyond the wrapper

client.Raw() exposes the full generated spadino.v1 client (Export, QueryStream, MultiQuery, BatchQuery, schema, recall evaluation), with client.WithAuth(ctx) to carry the bearer token. Two rules when you drop down: always read a QueryStream to its terminal receipt frame (stopping early leaves you holding an unverified result), and never set the x-spada-forwarded metadata key (reserved; setting it suppresses routing).

TLS and auth

ca, _ := os.ReadFile("ca.pem")
client, err := spadino.Connect("spadino.example:50051", spadino.Options{
    TLSCAPEM: ca,
    Token:    os.Getenv("SPADA_TOKEN"),
})

Client.Health returns DevModeNoAuth — if true, the server runs without authorization and must not be exposed to a network. Surface it.

Protos, codegen, compatibility

The spadino.v1 protos are vendored under proto/ with their source commit pinned in proto/PROVENANCE.md. Regenerate with ./generate.sh (buf + protoc-gen-go/protoc-gen-go-grpc); refresh from a spada checkout with ./sync-protos.sh <path>. The surface is frozen additive-only and machine-enforced upstream. Only the public package is vendored — spadino.internal.v1 and spadino.model.v1 carry no compatibility promise and a client must never speak them.

License

Apache-2.0.