# Go SDK

[Read this page in HTML](https://www.scopedb.io/docs/guides/sdks/go)

Query ScopeDB and write events from Go applications. This guide uses [goscopedb 0.7.0](https://pkg.go.dev/github.com/scopedb/goscopedb@v0.7.0).

> **Note: Upgrading from Go SDK 0.6?**
>
> Review the [0.7.0 release notes and upgrade steps](https://www.scopedb.io/docs/release-notes/2026-10-23-go-sdk-0-7-0.md#upgrade-from-06) before updating.

## Install and configure

Use Go 1.26 or later. In your application's Go module, install the SDK:

```bash
go get github.com/scopedb/goscopedb@v0.7.0
```

Commit `go.mod` and `go.sum`. Follow the [application connection guide](https://www.scopedb.io/docs/guides/connect-to-scopedb.md) to obtain an endpoint and API key, then set them in the shell where you run the examples:

```bash
export SCOPEDB_ENDPOINT="https://<endpoint>"
export SCOPEDB_API_KEY="<api-key>"
```

Keep the key on your server. SDKs do not reuse the CLI's interactive login.

## Run a query

Save this complete program as `main.go` and run `go run .`. It verifies the connection without creating a table or requiring sample data.

```go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	scopedb "github.com/scopedb/goscopedb"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	client, err := scopedb.NewClient(scopedb.Config{
		Endpoint: os.Getenv("SCOPEDB_ENDPOINT"),
		APIKey:   os.Getenv("SCOPEDB_API_KEY"),
	})
	if err != nil {
		return err
	}
	defer client.Close()

	ctx := context.Background()
	result, err := client.Query(ctx, "SELECT 1 AS ready")
	if err != nil {
		return err
	}
	rows, err := result.ToObjects()
	if err != nil {
		return err
	}
	fmt.Printf("ready: %v\n", rows[0]["ready"])
	return nil
}
```

Expected output:

```text
ready: 1
```

`Query` waits for execution to finish; `ToObjects` returns rows keyed by column name. Reuse the client, and add the following examples inside `run` before `return nil`. `Close` releases idle connections owned by the SDK; an application-supplied `Config.HTTPClient` remains the application's responsibility.

## Write rows

Create the `events` table used by the [quickstart](https://www.scopedb.io/docs/guides/quickstart.md#1-create-an-event-table). This statement leaves an existing table unchanged; if you already have `events`, check that its columns match before continuing.

```go
if _, err := client.Query(ctx, `
	CREATE TABLE IF NOT EXISTS events (
		time timestamp,
		service string,
		name string,
		message string,
		var object
	)
`); err != nil {
	return err
}
```

Append one event and wait for it to commit:

```go
table := client.Table("events")
row := map[string]any{
	"time":    "2026-10-23T09:00:00Z",
	"service": "checkout",
	"name":    "payment_failed",
	"message": "Credit card gateway timed out",
	"var":     map[string]string{"region": "apac", "gateway": "credit_card"},
}
stream, err := table.AppendStream(scopedb.AppendStreamOptions{})
if err != nil {
	return err
}
if err := stream.Send(ctx, row); err != nil {
	_, _ = stream.Shutdown(ctx)
	return err
}
report, err := stream.Shutdown(ctx)
if err != nil {
	return err
}
fmt.Printf("Written rows: %d\n", report.CommittedRows)
```

`Send` accepts a row into the SDK's buffer. `Shutdown` sends remaining rows, waits for delivery, and closes the stream. With this default failure policy, a failed batch returns an error. A successful run prints `Written rows: 1`.

Read the event back after shutdown:

```go
written, err := client.Query(ctx, `
	FROM events
	WHERE time = '2026-10-23T09:00:00Z'::timestamp
	  AND service = 'checkout'
	  AND name = 'payment_failed'
	SELECT service, name, var['gateway']::string AS gateway
	LIMIT 10
`)
if err != nil {
	return err
}
writtenRows, err := written.ToObjects()
if err != nil {
	return err
}
fmt.Println(writtenRows)
```

The result includes `checkout`, `payment_failed`, and `credit_card`. Each run appends another event. The examples use the default `scopedb` database and `public` schema; set the table's `Database` and `Schema` fields for another table location.

## Work with results

Use `ToObjects` for named columns or `ToValues` for positional rows. An empty result produces no rows. `First` also tells you whether a row was found:

```go
first, found, err := written.First()
if err != nil {
	return err
}
if found {
	fmt.Println(first["name"])
}
```

Object conversion rejects duplicate column names; use unique aliases or positional results. Integers become `int64` or `uint64`, timestamps become `time.Time`, intervals become `time.Duration`, and binary values become `[]byte`. Array, object, and any values remain strings; NULL becomes nil.

Use `RawRows` when you need the original result text. See [ResultSet](https://pkg.go.dev/github.com/scopedb/goscopedb@v0.7.0#ResultSet) for conversion methods and errors.

## Manage table definitions

Read the table's complete definition and ETag:

```go
catalog := table.Catalog()
snapshot, err := catalog.Get(ctx)
if err != nil {
	return err
}
fmt.Println(snapshot.Definition.Columns, snapshot.ETag)
```

To change its comment, preserve the definition and preview the replacement. Add `encoding/json` to the program's imports:

```go
snapshot.Definition.Comment = new("Checkout events")
plan, err := catalog.Apply(ctx, snapshot.Definition, scopedb.CatalogOptions{
	DryRun:  true,
	IfMatch: snapshot.ETag,
})
if err != nil {
	return err
}
preview, err := json.MarshalIndent(plan, "", "  ")
if err != nil {
	return err
}
fmt.Println(string(preview))
```

Review `Before`, `After`, and `Destructive`. Apply the same definition with the preview's ETag:

```go
applied, err := catalog.Apply(ctx, snapshot.Definition, scopedb.CatalogOptions{
	IfMatch: plan.ETag,
})
if err != nil {
	return err
}
fmt.Println(*applied.After.Comment)
```

`Apply` replaces the complete definition; omitted fields are cleared. Typed reads reject unknown fields; use `Raw()` to retain them during exports or edits. An ETag conflict requires a fresh read and preview. Potentially destructive changes also require `AllowDestructive: true`; this comment edit does not. See the [Catalog contract](https://www.scopedb.io/docs/api-reference/catalog.md) for resource selection and interrupted writes.

## Configure queries and writes

### Query execution

Use a statement to set execution limits or retain a handle for progress and cancellation. Add `time` to the program's imports:

```go
statement := client.Statement("SELECT 1 AS ready")
statement.MaxElapsed = 60 * time.Second
handle, err := statement.Submit(ctx)
if err != nil {
	return err
}
fmt.Println(handle.Snapshot().Status)
if _, err := handle.Wait(ctx); err != nil {
	return err
}
```

`Snapshot` reads cached state; `Refresh(ctx)` fetches an active statement's state. `client.StatementHandle(id)` resumes tracking a known ID. `MaxElapsed` includes queueing; `MaxEstimated` separately limits estimated duration.

Cancelling a context stops local requests and waiting. To request server-side cancellation, call `handle.Cancel` with a fresh, bounded context and inspect its returned snapshot. Execution may have finished already or may still be active.

### Batching and delivery

Use `Flush` to wait for buffered writes while keeping a stream open. Streams target 4 MiB batches and allow four concurrent requests by default. Each SDK request is capped at 8 MiB and 200,000 rows.

For a long-running producer that must continue after a failed batch:

```go
telemetry, err := table.AppendStream(scopedb.AppendStreamOptions{
	FailurePolicy:        scopedb.AppendFailureContinue,
	MaxConcurrentBatches: 1,
})
if err != nil {
	return err
}
if err := telemetry.TrySend(row); err != nil {
	fmt.Println("Not queued:", err)
}

// Stop producers before shutting down the stream.
delivery, err := telemetry.Shutdown(ctx)
if err != nil {
	return err
}
fmt.Println(delivery.Outcome, delivery.CommittedRows, delivery.FailedRows, delivery.UnknownRows)
```

`TrySend` returns an error when a row cannot enter the local buffer. Continue mode reports failures without retaining failed payloads for replay; use durable storage when loss is unacceptable. Inspect failed and unknown rows as well as committed rows. See [AppendStreamOptions](https://pkg.go.dev/github.com/scopedb/goscopedb@v0.7.0#AppendStreamOptions) for buffering and [AppendRetryOptions](https://pkg.go.dev/github.com/scopedb/goscopedb@v0.7.0#AppendRetryOptions) for retry controls.

Streams retry confirmed temporary rejections by default. `RetryUnknown` explicitly allows replay of uncertain writes and can duplicate rows. Client timeouts and cancellations are never replayed. Cancelling a `Flush` or `Shutdown` wait does not abandon accepted rows; wait again with a live context to settle them.

### Row encoding

Streams encode structs with JSON tags or maps through `encoding/json`. For existing NDJSON, use [Table.AppendNDJSON](https://pkg.go.dev/github.com/scopedb/goscopedb@v0.7.0#Table.AppendNDJSON), which makes one attempt. Shape values in the producer; Append does not run a ScopeQL transformation. See the shared [JSON input types](https://www.scopedb.io/docs/api-reference/append.md#json-input-types) for accepted field values and schema rules.

### Timeouts

Queries and Catalog reads have no SDK client deadline by default; use a caller context or `Config.HTTPClient` to bound waiting.

`Config.WriteTimeout` bounds each append or Catalog mutation attempt, including response-body reads. Omit it for 30 seconds, set `new(2 * time.Minute)` to override it, or `new(time.Duration(0))` to disable it. `AppendStreamOptions.AttemptTimeout` inherits when nil; a pointer overrides it, with zero disabling the attempt limit. Negative durations are invalid.

The stream's five-minute default delivery/retry budget still applies. Encoding, queue admission, retry backoff, and the complete `Flush` or `Shutdown` wait are outside the per-attempt timeout.

## Handle errors

Use `errors.As` to inspect a ScopeDB failure without matching its message text. Add `errors` to the program's imports:

```go
if _, err := client.Query(ctx, "SELECT 1 AS ready"); err != nil {
	var apiError *scopedb.Error
	if errors.As(err, &apiError) {
		fmt.Println(apiError.Code, apiError.HTTPStatus, apiError.RequestID)
		fmt.Println(apiError.StatementDetails, apiError.WriteDetails)
	}
	return err
}
```

For an Append failure, inspect `AppendDetails.Status` and its row diagnostics. `WriteDetails` identifies a timeout or cancellation after sending, with an unknown outcome. Reconcile an uncertain write before replaying it; temporary or retryable classification alone does not establish that replay is safe.

Catalog writes never retry automatically. `catalog_commit_outcome_unknown` requires reading the current definition before another mutation; dry runs cannot commit. Follow the [HTTP error and retry contract](https://www.scopedb.io/docs/api-reference.md#errors-and-retries) for operation-specific decisions.

## Related contents

* [SDKs](https://www.scopedb.io/docs/guides/sdks.md)

* [Connect a client](https://www.scopedb.io/docs/guides/connect-to-scopedb.md)

* [API reference](https://www.scopedb.io/docs/api-reference.md)

* [Go package reference](https://pkg.go.dev/github.com/scopedb/goscopedb@v0.7.0)
