- Go 97%
- Shell 3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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> |
||
| examples | ||
| gen/spadino/v1 | ||
| proto | ||
| .gitignore | ||
| buf.gen.yaml | ||
| client.go | ||
| errors.go | ||
| generate.sh | ||
| go.mod | ||
| go.sum | ||
| query.go | ||
| README.md | ||
| sync-protos.sh | ||
| write.go | ||
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.