Golang SDK for Spice.ai
See Go Docs at pkg.go.dev/github.com/spiceai/gospice/v9.
For full documentation visit docs.spice.ai.
- Get the gospice package.
go get github.com/spiceai/gospice/v9@latest- Import the package.
import "github.com/spiceai/gospice/v9"- Create a SpiceClient.
spice := gospice.NewSpiceClient()
defer spice.Close()- Initialize the SpiceClient with Spice Cloud, passing in your API key. Get your free API key at spice.ai.
if err := spice.Init(
gospice.WithApiKey(os.Getenv("SPICE_API_KEY")),
gospice.WithSpiceCloudAddress(),
); err != nil {
panic(fmt.Errorf("error initializing SpiceClient: %w", err))
}The client authenticates once. The runtime answers the first call's handshake with a session, and every later call — Sql, Query, and the async query actions — is sent under that session rather than paying a handshake of its own. When the runtime no longer recognises the session, after an hour of inactivity or a restart, the next call renews it and proceeds; a credential the runtime refuses outright is reported, not retried. SqlWithParams runs over an ADBC connection with its own session and re-authenticates the same way.
- Execute a query and get back an Apache Arrow Reader.
reader, err := spice.Sql(context.Background(), "SELECT 1")
if err != nil {
panic(fmt.Errorf("error querying: %w", err))
}
defer reader.Release()- Iterate through the reader to access the records.
for reader.Next() {
record := reader.RecordBatch()
fmt.Println(record)
}
if err := reader.Err(); err != nil {
panic(fmt.Errorf("error reading results: %w", err))
}The reader owns each record batch and releases it on the next call to Next, so don't call Release on it. To keep a batch past that point, call record.Retain() and Release it when you are done.
gospice v9 supports parameterized queries using ADBC (Arrow Database Connectivity), which is the recommended approach for queries with parameters to prevent SQL injection:
// Query with a single parameter
reader, err := spice.SqlWithParams(
context.Background(),
"SELECT * FROM tpch.customer WHERE c_custkey > $1 LIMIT 10",
100,
)
if err != nil {
panic(fmt.Errorf("error querying: %w", err))
}
defer reader.Release()
for reader.Next() {
record := reader.RecordBatch()
fmt.Println(record)
}
if err := reader.Err(); err != nil {
panic(fmt.Errorf("error reading results: %w", err))
}Query with multiple parameters:
reader, err := spice.SqlWithParams(
context.Background(),
"SELECT * FROM taxi_trips WHERE trip_distance > $1 AND fare_amount > $2 LIMIT 100",
5.0,
20.0,
)
if err != nil {
panic(fmt.Errorf("error querying: %w", err))
}
defer reader.Release()Supported parameter types with automatic type inference:
- Integers:
int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64 - Floating point:
float32,float64 - String:
string - Boolean:
bool - Binary:
[]byte - Null values:
nil
Typed Parameters for Advanced Use Cases:
For precise control over Arrow types, use typed parameter constructors:
import "github.com/spiceai/gospice/v9"
// Explicit type control for complex scenarios
reader, err := spice.SqlWithParams(
ctx,
"SELECT * FROM data WHERE id = $1 AND amount = $2 AND active = $3",
gospice.Int64Param(12345), // Explicitly int64
gospice.Decimal128Param(...), // Decimal with precision
gospice.BoolParam(true), // Explicitly boolean
)Available typed parameter constructors:
- Integers:
Int8Param,Int16Param,Int32Param,Int64Param,Uint8Param,Uint16Param,Uint32Param,Uint64Param - Floating point:
Float16Param,Float32Param,Float64Param - Strings:
StringParam,LargeStringParam - Binary:
BinaryParam,LargeBinaryParam,FixedSizeBinaryParam - Boolean:
BoolParam - Date/Time:
Date32Param,Date64Param,Time32Param,Time64Param,TimestampParam,DurationParam - Intervals:
MonthIntervalParam,DayTimeIntervalParam,MonthDayNanoIntervalParam - Decimals:
Decimal128Param,Decimal256Param - Null:
NullParam
Or use the generic constructors:
NewParam(value)- Creates a parameter with automatic type inferenceNewTypedParam(value, arrowType)- Creates a parameter with explicit Arrow type
Follow the quickstart guide to install and run spice locally
Initialize the SpiceClient to use local runtime connection:
if err := spice.Init(); err != nil {
panic(fmt.Errorf("error initializing SpiceClient: %w", err))
}Configure with a custom flight address:
if err := spice.Init(
gospice.WithFlightAddress("grpc://localhost:50052"),
); err != nil {
panic(fmt.Errorf("error initializing SpiceClient: %w", err))
}Query and QueryWithParams submit SQL for asynchronous execution and return an *AsyncQuery handle. Async queries run in the background on the Spice runtime and are designed for long-running analytical and batch workloads. They require the runtime to be running in distributed/scheduler mode (spiced --role scheduler with runtime.scheduler.state_location configured).
// Submit a query for async execution
query, err := spice.Query(context.Background(), "SELECT * FROM taxi_trips")
if err != nil {
panic(fmt.Errorf("error submitting query: %w", err))
}
fmt.Println("query id:", query.ID())
// Wait for completion and fetch results as an Apache Arrow reader
reader, err := query.Results(context.Background())
if err != nil {
panic(fmt.Errorf("error fetching results: %w", err))
}
defer reader.Release()
for reader.Next() {
fmt.Println(reader.RecordBatch())
}
if err := reader.Err(); err != nil {
panic(fmt.Errorf("error reading results: %w", err))
}Parameterized async queries bind positional parameters ($1, $2, ...):
query, err := spice.QueryWithParams(
context.Background(),
"SELECT * FROM taxi_trips WHERE trip_distance > $1 LIMIT $2",
5.0,
100,
)The *AsyncQuery handle provides:
ID()- the server-assigned query IDStatus(ctx)- poll the current status once (PENDING,RUNNING,SUCCEEDED,FAILED,CANCELLED,CLOSED)Wait(ctx)- block until the query reaches a terminal statusResults(ctx)- wait for completion and return results as anarray.RecordReaderCancel(ctx)- request cancellation
For synchronous, real-time streaming queries, use Sql / SqlWithParams instead.
gospice v9 provides health check methods to verify Spice instance status before executing queries:
// Check if Spice instance is healthy (unauthenticated)
ctx := context.Background()
if !spice.IsSpiceHealthy(ctx) {
log.Println("Spice instance is not healthy")
return
}
// Check if Spice Cloud is ready (requires API key)
if !spice.IsSpiceReady(ctx) {
log.Println("Spice Cloud is not ready or API key is invalid")
return
}IsSpiceHealthy(ctx)- Calls/healthendpoint (unauthenticated)IsSpiceReady(ctx)- Calls/v1/readyendpoint (requires an API key against Spice Cloud)
Both report on the runtime behind the client's HTTP endpoint, which by default is paired
with the Flight endpoint the client was initialized with — a client left on the local
Flight default checks the local runtime, not Spice Cloud. Only the local and Spice Cloud
Flight addresses have a paired HTTP endpoint; a client on any other Flight address keeps
the Spice Cloud HTTP default until WithHttpAddress names its runtime's own.
IsSpiceReady collapses the whole runtime into a single boolean. When you need to know
which component is not ready, use RuntimeStatus to get per-connection detail:
details, err := spice.RuntimeStatus(ctx)
if err != nil {
log.Fatalf("error getting runtime status: %v", err)
}
for _, d := range details {
fmt.Printf("%s (%s): %s\n", d.Name, d.Endpoint, d.Status)
}
// http (127.0.0.1:8090): Ready
// flight (127.0.0.1:50051): Ready
// metrics (N/A): Disabled
// opentelemetry (127.0.0.1:50051): ReadyEach ConnectionDetails carries the component Name (http, flight, metrics or
opentelemetry), its Endpoint, and its Status — one of Initializing, Ready,
Disabled, Error, Refreshing, ShuttingDown or NotLoaded. d.IsReady() is a
shorthand for d.Status == ComponentStatusReady.
ListActiveQueries reports the synchronous queries running in the caller's scope —
those started by Sql, SqlWithParams, FlightSQL, NSQL and Search — and
CancelActiveQuery stops one by ID.
The runtime does not hand a query's ID back to the client that submitted it, so the two are used together: list to find the query, then cancel it.
Two boundaries apply, and a query is reachable only inside both.
One runtime instance. The runtime holds active synchronous queries in memory, per
process, and these endpoints report only what the instance answering them knows. A
SpiceClient resolves its Flight and HTTP endpoints separately, so behind a load
balancer the query submitted over Flight may be running on a different instance than the
one answering here — it will not be listed, and its ID reports as not found.
One authenticated principal, not a SpiceClient. The principal is whatever
credential the runtime authenticates — an API key or a client certificate — so every
client presenting the same credential lists and cancels the same queries. Only requests
for which the runtime establishes no principal at all share the public scope.
Runtime version. Principal scoping on these two endpoints landed in spiceai/spiceai#12841 and is in no runtime release up to and including
v2.1.5. Against an earlier runtime both calls operate on every active query the instance holds, for any caller with write access. Check your runtime version before relying on the scope described above.
Both calls address the client's HTTP endpoint, which is paired with the Flight endpoint
for the two addresses that have a known pairing: a client on the local Flight address uses
http://127.0.0.1:8090, and WithSpiceCloudAddress moves both to Spice Cloud. Any other
Flight address has no paired HTTP endpoint, so pass WithHttpAddress — as you also would
when the runtime serves its HTTP API somewhere else:
if err := spice.Init(gospice.WithHttpAddress("http://127.0.0.1:8091")); err != nil {
panic(fmt.Errorf("error initializing SpiceClient: %w", err))
}ctx := context.Background()
queries, err := spice.ListActiveQueries(ctx)
if err != nil {
log.Fatalf("error listing active queries: %v", err)
}
for _, q := range queries {
fmt.Printf("%s [%s] %s (started %s)\n",
q.QueryID, q.Protocol, q.SQLPreview, q.StartedAt().Format(time.RFC3339))
}
// Cancel a long-running query by ID.
if len(queries) > 0 {
if err := spice.CancelActiveQuery(ctx, queries[0].QueryID); err != nil {
log.Fatalf("error cancelling query: %v", err)
}
}To cancel an async query job instead, use AsyncQuery.Cancel — see
Async Queries above. Async jobs require the runtime to be running in
cluster mode; the two calls here work on a default runtime.
Search finds documents similar to a piece of text, using the runtime's /v1/search endpoint. It runs against datasets that have an embedding column and a loaded embedding model — see Search & Retrieval for how to configure them.
ctx := context.Background()
limit := 3
resp, err := spice.Search(ctx, &gospice.SearchRequest{
Text: "tokyo plane tickets",
Datasets: []string{"app_messages"},
Limit: &limit,
AdditionalColumns: []string{"timestamp"},
})
if err != nil {
log.Fatalf("search failed: %v", err)
}
fmt.Printf("%d matches in %dms\n", len(resp.Results), resp.DurationMs)
for _, match := range resp.Results {
fmt.Println(match.Score, match.Dataset, match.Matches, match.Data)
}SearchRequest fields:
Text(required) - The text to find similar documents for.Datasets- Datasets to search. Leave empty to search every searchable dataset.Limit- Maximum matches to return per dataset.Where- A SQL predicate filtering candidate rows, without the leadingWHERE— for example"user_id = 42".AdditionalColumns- Extra columns to return with each match. Primary key columns are returned inPrimaryKey, the rest inData.Keywords- Keywords for the lexical pass of a hybrid search, which the runtime combines with the vector scores into a single ranking.
Each SearchMatch carries Dataset, Score (higher is more similar), Matches (matched values keyed by source column — a slice per column, since one column can contribute several chunks to a match), PrimaryKey, Data, and Metadata.
Numbers in Matches, PrimaryKey, Data, and Metadata are json.Number, not float64, so a 64-bit identifier is returned exactly rather than being rounded to float64's 53 bits of integer precision. Convert with the precision the column actually has:
id, err := match.PrimaryKey["id"].(json.Number).Int64()
ratio, err := match.Data["ratio"].(json.Number).Float64()
raw := match.Data["account_id"].(json.Number).String()Nsql answers a question in natural language, using the runtime's /v1/nsql endpoint: the configured LLM generates SQL, the runtime runs it read-only, and both the rows and the generated query come back. It requires an LLM model in the Spicepod — see Text to SQL for how to configure one.
ctx := context.Background()
resp, err := spice.Nsql(ctx, &gospice.NsqlRequest{
Query: "top 5 customers by revenue",
Datasets: []string{"sales"},
})
if err != nil {
log.Fatalf("nsql failed: %v", err)
}
fmt.Println("generated SQL:", resp.SQL)
for _, row := range resp.Data {
fmt.Println(row)
}NsqlRequest fields:
Query(required) - The question to answer, in natural language.Model- The LLM used to generate SQL. Leave empty when the Spicepod configures exactly one compatible model.Datasets- Datasets to sample when building model context. This is a sampling hint; it does not restrict which tables the generated query may reference.SampleDataEnabled- Include sample rows in the model's context. Improves generation on ambiguous schemas, at the cost of sending data values to the model.PromptCacheKey- A stable key forwarded to the model provider for prompt caching.
Values in Data are decoded from JSON, so they carry JSON's types rather than the Arrow types named in Schema — numbers arrive as json.Number, which keeps the value's original text so a 64-bit identifier is not rounded to float64's 53 bits. Convert with Int64, Float64, or String as the column requires. When Arrow-typed results matter, generate the query and run it yourself:
sql, err := spice.NsqlGenerateSQL(ctx, &gospice.NsqlRequest{Query: "top 5 customers by revenue"})
if err != nil {
log.Fatalf("nsql failed: %v", err)
}
reader, err := spice.Sql(ctx, sql)NsqlGenerateSQL is also the way to inspect or edit a generated query before running it.
Run go run ./cmd to execute a sample query and print the results to the console.
See query_test.go for examples on querying TPC-H and taxi trips datasets.
The SpiceClient implements connection retry mechanism (3 attempts by default).
The number of attempts can be configured via SetMaxRetries:
spice := gospice.NewSpiceClient()
spice.SetMaxRetries(5) // Setting to 0 will disable retriesRetries are performed for connection and system internal errors. It is the SDK user's responsibility to properly handle other errors, for example RESOURCE_EXHAUSTED (HTTP 429).
gospice v9 is a new major version with breaking changes. To upgrade:
go get github.com/spiceai/gospice/v9@latest
go mod tidyUpdate your imports:
// Before
import "github.com/spiceai/gospice/v8"
// After
import "github.com/spiceai/gospice/v9"Breaking changes in v9:
Query()andQueryWithParams()are now asynchronous and return an*AsyncQueryhandle instead of anarray.RecordReader. Use the handle'sWait()/Results()methods (see Async Queries), or switch to the synchronousSql()/SqlWithParams()methods.- Minimum Go version is now 1.25 (was 1.24).
- Upgraded to Apache Arrow v18.6.0 and ADBC v1.11.0, matching the Spice.ai runtime's DataFusion 54.
See UPGRADE_V8_TO_V9.md for the detailed migration guide.
Run all tests:
go test ./...Run tests with verbose output:
go test -v ./...Run specific test suites:
# Local runtime tests only
go test -v -run="TestLocal"
# Cloud tests only
go test -v -run="TestCloud"
# ADBC tests only
go test -v -run="TestADBC"Run all benchmarks:
go test -bench=. -benchmemRun specific benchmarks:
# Benchmark query performance
go test -bench=BenchmarkQuery -benchmem
# Benchmark parameterized queries
go test -bench=BenchmarkQueryWithParams -benchmem
# Benchmark health checks
go test -bench=BenchmarkHealthChecks -benchmem
# Benchmark client initialization
go test -bench=BenchmarkClientInitialization -benchmemRun benchmarks with custom settings:
# Run for 10 seconds each
go test -bench=. -benchtime=10s
# Run with CPU profiling
go test -bench=. -cpuprofile=cpu.prof
# Run with memory profiling
go test -bench=. -memprofile=mem.profAvailable benchmarks:
BenchmarkCloudQuery- Basic query performance against Spice CloudBenchmarkCloudQueryWithParams- Parameterized query performance (Cloud)BenchmarkLocalQuery- Query performance against local runtimeBenchmarkLocalQueryWithParams- Parameterized query performance (Local)BenchmarkParameterBinding- Parameter binding overhead with varying parameter countsBenchmarkClientInitialization- Client initialization overheadBenchmarkHealthChecks- Health check endpoint performanceBenchmarkRecordProcessing- Different record processing patterns