Skip to content
spiceaiPublic

About

Golang SDK for Spice.ai

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

73 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gospice

Golang SDK for Spice.ai

See Go Docs at pkg.go.dev/github.com/spiceai/gospice/v9.

For full documentation visit docs.spice.ai.

Usage

  1. Get the gospice package.
go get github.com/spiceai/gospice/v9@latest
  1. Import the package.
import "github.com/spiceai/gospice/v9"
  1. Create a SpiceClient.
spice := gospice.NewSpiceClient()
defer spice.Close()
  1. 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.

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

Using Parameterized Queries (Recommended)

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 inference
  • NewTypedParam(value, arrowType) - Creates a parameter with explicit Arrow type

Using local spice runtime

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

Async Queries

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 ID
  • Status(ctx) - poll the current status once (PENDING, RUNNING, SUCCEEDED, FAILED, CANCELLED, CLOSED)
  • Wait(ctx) - block until the query reaches a terminal status
  • Results(ctx) - wait for completion and return results as an array.RecordReader
  • Cancel(ctx) - request cancellation

For synchronous, real-time streaming queries, use Sql / SqlWithParams instead.

Health Checks

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 /health endpoint (unauthenticated)
  • IsSpiceReady(ctx) - Calls /v1/ready endpoint (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.

Runtime Status

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): Ready

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

Listing and Cancelling Running Queries

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

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 leading WHERE — for example "user_id = 42".
  • AdditionalColumns - Extra columns to return with each match. Primary key columns are returned in PrimaryKey, the rest in Data.
  • 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()

Text-to-SQL (NSQL)

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.

Example

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.

Connection retry

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 retries

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

Upgrading from v8 to v9

gospice v9 is a new major version with breaking changes. To upgrade:

go get github.com/spiceai/gospice/v9@latest
go mod tidy

Update your imports:

// Before
import "github.com/spiceai/gospice/v8"

// After
import "github.com/spiceai/gospice/v9"

Breaking changes in v9:

  • Query() and QueryWithParams() are now asynchronous and return an *AsyncQuery handle instead of an array.RecordReader. Use the handle's Wait() / Results() methods (see Async Queries), or switch to the synchronous Sql() / 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.

Testing and Benchmarking

Running Tests

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"

Running Benchmarks

Run all benchmarks:

go test -bench=. -benchmem

Run 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 -benchmem

Run 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.prof

Available benchmarks:

  • BenchmarkCloudQuery - Basic query performance against Spice Cloud
  • BenchmarkCloudQueryWithParams - Parameterized query performance (Cloud)
  • BenchmarkLocalQuery - Query performance against local runtime
  • BenchmarkLocalQueryWithParams - Parameterized query performance (Local)
  • BenchmarkParameterBinding - Parameter binding overhead with varying parameter counts
  • BenchmarkClientInitialization - Client initialization overhead
  • BenchmarkHealthChecks - Health check endpoint performance
  • BenchmarkRecordProcessing - Different record processing patterns

About

Golang SDK for Spice.ai

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages