cassandra

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 21 Imported by: 0

README

cassandra

cassandra is a Go database/sql driver for Apache Cassandra and ScyllaDB. It wraps the Apache Cassandra Go driver (gocql) and registers itself as cassandra.

go get github.com/xo/cassandra

Use

import (
	"database/sql"

	gocql "github.com/apache/cassandra-gocql-driver/v2"
	_ "github.com/xo/cassandra"
)

db, err := sql.Open("cassandra", "cassandra://cassandra:cassandra@127.0.0.1:9042/app")
if err != nil {
	return err
}
defer db.Close()

var (
	id   gocql.UUID
	name sql.Null[string]
	tags []string
)
err = db.QueryRowContext(ctx, "SELECT id, name, tags FROM users WHERE id = ?", uid).Scan(&id, &name, &tags)

All the connections of one sql.DB share one gocql session, which holds its own pool of connections to each node. The pool settings of sql.DB limit how many statements run at the same time. The numConns key limits the connections to each node.

To set a gocql option that the DSN cannot express, such as a host selection policy or a logger, build a gocql.ClusterConfig and open it with sql.OpenDB(cassandra.NewConnector(cfg)).

DSN

A DSN is a URL whose scheme is cassandra. The driver reads no other scheme and no other form (D31):

cassandra://user:password@host1:9042/keyspace?consistency=localQuorum&host=host2:9042

The user information sets the credentials, the host part holds one host, and the path names the keyspace. Each host key adds one more host, in order, and any host can be an IPv6 address: cassandra://[::1]:9042/ks?host=[::2]:9042 (D34).

The query takes these keys, and each has the default of gocql.NewCluster:

Key Value
consistency any, one, two, three, quorum, all, localQuorum, eachQuorum or localOne
keyspace the keyspace
timeout the timeout of a request, such as 10s
connectTimeout the timeout of a new connection, such as 10s
numConns the number of connections to each host
ignorePeerAddr true or false
disableInitialHostLookup true or false
writeCoalesceWaitTime a duration, such as 200µs
username, password the credentials
enableHostVerification true or false. Any TLS key turns TLS on.
certPath, keyPath, caPath the paths of the TLS files
host one more host. This is the one key that can repeat.

An unknown key is an error that wraps dbimp.ErrUnknownKey, and a scheme other than cassandra wraps dbimp.ErrScheme. A key that appears twice, and a setting in two places, such as a keyspace in the path and in a keyspace key, are errors too. A DSN with no host connects to 127.0.0.1.

Arguments

A statement takes its values at ? markers. The driver passes every value that gocql can marshal: slices, maps, gocql.UUID, gocql.Duration, *big.Int, net.IP, a struct for a user defined type, and gocql.UnsetValue. It also takes the types that a column returns: a dbimp.Date, a dbimp.LocalTime, a dbimp.Interval, an *apd.Decimal, a netip.Addr, a dbimp.Vector and the standard uuid.UUID. A collection of uuid.UUID, such as []uuid.UUID, is not supported: use []gocql.UUID. A driver.Valuer, such as sql.Null[T], works as it does with any driver.

A query option changes how one statement runs. Pass it among the arguments, or attach it to a context. An argument overrides the context, and the context overrides the DSN:

rows, err := db.QueryContext(ctx, "SELECT id FROM users WHERE org = ?", org, cassandra.WithPageSize(500))

ctx = cassandra.WithOptions(ctx, cassandra.WithConsistency(gocql.LocalOne))

The options are WithConsistency, WithSerialConsistency, WithPageSize, WithIdempotent and WithTimestamp, and the four that every xo driver takes (dbimp D109): WithTimeout, WithReadonly, WithParameter and WithDatabase. Cassandra has no read-only statement and no body of keys, so WithReadonly(true) and WithParameter fail the statement with an error that wraps dbimp.ErrNotSupported.

Scanning

Scan a column into any type that gocql can decode it into, such as *[]string, *map[string]int, *gocql.UUID or *time.Time, or into any type that database/sql can convert to. A uuid column also scans into *uuid.UUID, **uuid.UUID and sql.Null[uuid.UUID]. *any receives these Go types, which are the kinds of dbimp (dbimp D135 and D32 here):

CQL type Go type
ascii, text, varchar string
blob []byte
boolean bool
tinyint, smallint, int, bigint, counter int64
float, double float64
decimal *apd.Decimal
varint *big.Int
inet netip.Addr
uuid, timeuuid uuid.UUID, from the standard library
timestamp time.Time, in UTC
date dbimp.Date
time dbimp.LocalTime
duration dbimp.Interval
list, set, tuple []any, of the Go types of its elements
map with a text key, a user defined type map[string]any
vector of numbers dbimp.Vector[T]

docs/CASSANDRA.md holds the whole table, with the scan type and the database type of each.

A NULL scanned into a plain *string or *int64 is an error, as it is with other drivers. To read a column that can be NULL, scan into sql.Null[T], a pointer to a pointer, or *any. Cassandra stores an empty collection as NULL, so a NULL scanned into a slice or a map gives nil and no error.

Limits

  • CQL has no transactions. BeginTx returns an error that wraps dbimp.ErrNotSupported. A batch is one CQL statement: pass the whole BEGIN BATCH ... APPLY BATCH text to ExecContext.
  • ExecContext returns driver.ResultNoRows, because Cassandra reports no row count and no insert ID. To read [applied] from a lightweight transaction, run it with QueryContext.
  • Named arguments from sql.Named are refused, because gocql cannot bind a value by name.
  • gocql v2.1.2 cannot bind a tuple value. Write a tuple as a CQL literal.

Testing

The unit tests need no server:

go test -race ./...

The integration tests run against the server that CASSANDRA_DSN names, and skip when it is empty. CONTRIBUTING.md shows how to start one.

Design

Document Holds
docs/DESIGN.md the design of the driver
docs/CASSANDRA.md what is known about Cassandra and ScyllaDB, and the type table
docs/PLAN.md the purpose, what exists, the testing plan and the open questions
docs/decisions/ every decision, one file each
docs/PROGRESS.md where the work stands
docs/BACKLOG.md the planned work
AGENTS.md the rules for a coding agent
CONTRIBUTING.md the rules for a person

License

MIT. cassandra began as a fork of MichaelS11/go-cql-driver, and LICENSE keeps its notice.

Documentation

Overview

Package cassandra is a database/sql driver for Apache Cassandra. It wraps github.com/apache/cassandra-gocql-driver/v2 and registers itself as "cassandra".

Connecting

Open a database with a DSN, which is a URL whose scheme is cassandra:

db, err := sql.Open("cassandra", "cassandra://user:password@10.0.0.1,10.0.0.2:9042/keyspace?consistency=localQuorum")

The driver reads no other scheme and no older form of the DSN. See ParseDSN for every key. To set a gocql option that the DSN cannot express, build a gocql.ClusterConfig and pass it to NewConnector.

All the connections of one sql.DB share one gocql session, which holds its own pool of connections to each node. The pool settings of sql.DB, such as SetMaxOpenConns, limit how many statements run at the same time. They do not limit the connections to Cassandra. The numConns key does that.

Arguments

A statement takes its values at ? markers. Every Go value that gocql can marshal is accepted, including slices, maps, gocql.UUID, gocql.Duration, *big.Int, net.IP, structs for user defined types, and gocql.UnsetValue. The canonical types of a column bind too: a dbimp.Date, a dbimp.LocalTime, a dbimp.Interval, an *apd.Decimal, a netip.Addr and a dbimp.Vector. Named arguments from sql.Named are refused, because gocql cannot bind a value by name.

Query options

An Option, such as WithConsistency or WithPageSize, changes how one statement runs. Pass it among the arguments, or attach it to a context with WithOptions:

rows, err := db.QueryContext(ctx, "SELECT id FROM users WHERE org = ?", org, cassandra.WithPageSize(500))

Scanning

Scan a column into any type that gocql can decode it into, such as *[]string, *map[string]int or *gocql.UUID, or into any type that database/sql can convert to. A column has the Go type that fits its CQL type, by the kinds of dbimp: a decimal is an *apd.Decimal, a varint is a *big.Int, a date is a dbimp.Date, a time is a dbimp.LocalTime, a duration is a dbimp.Interval, an inet is a netip.Addr, and a uuid is the standard uuid.UUID, which the driver takes as an argument too. A NULL scanned into a plain *string or *int64 is an error, as it is with other drivers. Scan into a sql.Null[T], a pointer to a pointer, or *any to read a column that can be NULL. Cassandra stores an empty collection as NULL, so a NULL scanned into a slice or a map gives nil and no error.

Transactions, batches and lightweight transactions

CQL has no transactions, and BeginTx returns an error that wraps dbimp.ErrNotSupported. A batch is a CQL statement. Pass the whole BEGIN BATCH ... APPLY BATCH text to ExecContext. To read the [applied] column of a lightweight transaction, run it with QueryContext and scan the first column into a bool. When it is false, the row also holds the values that are there already, one column each.

ExecContext returns driver.ResultNoRows, because Cassandra reports no row count and no insert ID.

Example
package main

import (
	"context"
	"database/sql"
	"log"

	gocql "github.com/apache/cassandra-gocql-driver/v2"
)

func main() {
	db, err := sql.Open("cassandra", "cassandra://cassandra:cassandra@127.0.0.1:9042/app?consistency=localQuorum")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
	ctx := context.Background()
	rows, err := db.QueryContext(ctx, "SELECT id, name, tags FROM users WHERE org = ?", "xo")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var (
			id   gocql.UUID
			name sql.Null[string]
			tags []string
		)
		if err := rows.Scan(&id, &name, &tags); err != nil {
			log.Fatal(err)
		}
		log.Println(id, name.V, tags)
	}
	if err := rows.Err(); err != nil {
		log.Fatal(err)
	}
}
Example (Batch)
package main

import (
	"context"
	"database/sql"
	"log"
)

func main() {
	db, err := sql.Open("cassandra", "cassandra://127.0.0.1/app")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
	// A batch is one CQL statement. A logged batch is atomic, but it is not
	// isolated and it cannot roll back.
	_, err = db.ExecContext(context.Background(), `BEGIN BATCH
	INSERT INTO users (id, name) VALUES (?, ?);
	INSERT INTO users_by_name (name, id) VALUES (?, ?);
APPLY BATCH`, 1, "ken", "ken", 1)
	if err != nil {
		log.Fatal(err)
	}
}
Example (LightweightTransaction)
package main

import (
	"context"
	"database/sql"
	"log"
)

func main() {
	db, err := sql.Open("cassandra", "cassandra://127.0.0.1/app")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
	// The first column of the result is [applied]. Run the statement as a
	// query to read it. When it is false, the row also holds the values that
	// are there already, so the number of columns changes.
	rows, err := db.QueryContext(context.Background(),
		"INSERT INTO users (id, name) VALUES (?, ?) IF NOT EXISTS", 1, "ken")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	cols, err := rows.Columns()
	if err != nil {
		log.Fatal(err)
	}
	var applied bool
	dest := make([]any, len(cols))
	dest[0] = &applied
	for i := 1; i < len(dest); i++ {
		dest[i] = new(any)
	}
	for rows.Next() {
		if err := rows.Scan(dest...); err != nil {
			log.Fatal(err)
		}
	}
	if err := rows.Err(); err != nil {
		log.Fatal(err)
	}
	log.Println("applied:", applied)
}

Index

Examples

Constants

View Source
const Name = "cassandra"

Name is the name that the driver registers with database/sql, and the scheme of its DSN.

Variables

This section is empty.

Functions

func FormatDSN

func FormatDSN(cfg *gocql.ClusterConfig) (string, error)

FormatDSN writes cfg as a cassandra:// URL that ParseDSN reads back. It writes only the settings that differ from the defaults of gocql.NewCluster, and only the settings that the DSN can express. It returns an error for a value that the DSN cannot express, such as a consistency with no DSN name or an authenticator other than gocql.PasswordAuthenticator.

func ParseDSN

func ParseDSN(dsn string) (*gocql.ClusterConfig, error)

ParseDSN parses a DSN and returns the configuration of a gocql cluster. A DSN is a URL with the scheme cassandra, parsed with net/url, and no other scheme or form is read (dbimp D27):

cassandra://user:password@host1:9042/keyspace?consistency=localQuorum&host=host2

The user information sets the username and the password, the host part holds one host, and the path names the keyspace. Each host key adds one more host, in order: cassandra://h1:9042/ks?host=h2:9042&host=[::1]:9042 (D34).

The query takes these keys, and each has the default of gocql.NewCluster:

consistency               any, one, two, three, quorum, all, localQuorum, eachQuorum or localOne
keyspace                  the keyspace
timeout                   the timeout of a request, such as 10s
connectTimeout            the timeout of a new connection, such as 10s
numConns                  the number of connections to each host
ignorePeerAddr            true or false
disableInitialHostLookup  true or false
writeCoalesceWaitTime     a duration, such as 200µs
username, password        the credentials
enableHostVerification    true or false
certPath, keyPath, caPath the paths of the TLS files
host                      one more host

A key that is not in the list is an error that wraps dbimp.ErrUnknownKey. A key that appears twice wraps dbimp.ErrRepeatedKey, except host. A scheme other than cassandra wraps dbimp.ErrScheme, and any other fault wraps dbimp.ErrInvalidValue. An error never holds the DSN, because the DSN can hold a password. A DSN with no host connects to 127.0.0.1.

func WithOptions

func WithOptions(ctx context.Context, opts ...Option) context.Context

WithOptions returns a copy of ctx that carries opts. Every statement that runs with the returned context uses them. When ctx already carries options, opts come after them, so a later option wins.

Example
package main

import (
	"context"
	"database/sql"
	"log"
	"time"

	gocql "github.com/apache/cassandra-gocql-driver/v2"
	"github.com/xo/cassandra"
)

func main() {
	db, err := sql.Open("cassandra", "cassandra://127.0.0.1/app")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
	// Every statement that runs with ctx reads at LOCAL_ONE.
	ctx := cassandra.WithOptions(context.Background(), cassandra.WithConsistency(gocql.LocalOne))
	// An argument overrides the context for one statement.
	_, err = db.ExecContext(ctx, "UPDATE users SET name = ? WHERE id = ?", "ken", 1,
		cassandra.WithConsistency(gocql.Quorum), cassandra.WithTimestamp(time.Now()))
	if err != nil {
		log.Fatal(err)
	}
}

Types

type Connector

type Connector struct {
	// contains filtered or unexported fields
}

Connector opens connections to one Cassandra cluster. Every connection shares one gocql session, which the Connector creates on the first Connect. Close the Connector to close the session. sql.DB.Close closes it.

Use a Connector to set a gocql option that the DSN cannot express, such as a host selection policy, a retry policy, a logger or an observer:

cfg := gocql.NewCluster("10.0.0.1", "10.0.0.2")
cfg.Logger = logger
db := sql.OpenDB(cassandra.NewConnector(cfg))

func NewConnector

func NewConnector(cfg *gocql.ClusterConfig) *Connector

NewConnector returns a Connector for cfg. It keeps a copy of cfg, so a change to cfg after the call does not reach the Connector. It does not connect.

Example
package main

import (
	"context"
	"database/sql"
	"log"

	gocql "github.com/apache/cassandra-gocql-driver/v2"
	"github.com/xo/cassandra"
)

func main() {
	cfg := gocql.NewCluster("10.0.0.1", "10.0.0.2")
	cfg.Keyspace = "app"
	cfg.PoolConfig.HostSelectionPolicy = gocql.TokenAwareHostPolicy(gocql.RoundRobinHostPolicy())
	db := sql.OpenDB(cassandra.NewConnector(cfg))
	defer db.Close()
	if err := db.PingContext(context.Background()); err != nil {
		log.Fatal(err)
	}
}

func (*Connector) Close

func (c *Connector) Close() error

Close closes the shared session. A connection that is open stops working. Close can be called more than once.

func (*Connector) Connect

func (c *Connector) Connect(ctx context.Context) (driver.Conn, error)

Connect returns a connection over the shared session. The first call creates the session. When that fails, Connect returns the error, and the next call tries again.

gocql creates a session with no context, so the connectTimeout key of the DSN limits how long the first call takes.

func (*Connector) Driver

func (c *Connector) Driver() driver.Driver

Driver returns the cassandra driver.

type Driver

type Driver struct{}

Driver is the cassandra driver. It holds no configuration.

func (Driver) Open

func (d Driver) Open(dsn string) (driver.Conn, error)

Open returns a connection with its own session. database/sql does not call Open, because Driver implements driver.DriverContext. Closing the connection closes its session.

func (Driver) OpenConnector

func (d Driver) OpenConnector(dsn string) (driver.Connector, error)

OpenConnector parses dsn and returns a Connector for it. It does not connect.

type Error

type Error string

Error is an error that the driver reports.

const (
	// ErrConnectorClosed is returned by Connect after Close.
	ErrConnectorClosed Error = "connector is closed"
)

Error values. The other errors of the driver wrap the errors of dbimp: a DSN wraps dbimp.ErrScheme, dbimp.ErrUnknownKey, dbimp.ErrRepeatedKey or dbimp.ErrInvalidValue, and a feature that Cassandra has no form of, such as a transaction or a named argument, wraps dbimp.ErrNotSupported.

func (Error) Error

func (err Error) Error() string

Error satisfies the error interface.

type Option

type Option = dbimp.Option[options]

Option changes how one statement runs (dbimp D109). Pass an Option as an argument to a query, or attach it to a context with WithOptions. An Option in the argument list is not sent to Cassandra as a value.

The DSN sets the defaults for the whole session. An Option from the context overrides the DSN, and an Option from the arguments overrides the context. When two options set the same thing, the later one wins.

func WithConsistency

func WithConsistency(c gocql.Consistency) Option

WithConsistency sets the consistency level of the statement, as the key consistency of the DSN does for the session.

func WithDatabase

func WithDatabase(name string) Option

WithDatabase sets the keyspace of the statement. gocql sends it with the statement, which needs protocol version 5, and a server that speaks an older version refuses the statement.

func WithIdempotent

func WithIdempotent(idempotent bool) Option

WithIdempotent tells gocql whether the statement is safe to run more than once. gocql retries a statement only when it is idempotent.

func WithPageSize

func WithPageSize(n int) Option

WithPageSize sets how many rows gocql reads in one page. gocql reads the next page when the rows of the current page are used up.

func WithParameter

func WithParameter(name string, value any) Option

WithParameter sets a key of the request by its name. The native protocol has no body of keys for a statement, so any name fails the statement with an error that wraps dbimp.ErrNotSupported. Use the option of the driver for each setting that gocql lets one statement change.

func WithReadonly

func WithReadonly(readonly bool) Option

WithReadonly asks the server to refuse a write. Cassandra has no read-only statement, so WithReadonly(true) fails the statement with an error that wraps dbimp.ErrNotSupported. Create a role that can only read, and connect as that role.

func WithSerialConsistency

func WithSerialConsistency(c gocql.Consistency) Option

WithSerialConsistency sets the serial consistency level of a lightweight transaction.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout ends the statement after d, which includes the time that it takes to read every page of its rows. Zero sets no timeout of its own, and the timeout key of the DSN then applies to each request.

func WithTimestamp

func WithTimestamp(t time.Time) Option

WithTimestamp sets the write time of the statement. Cassandra stores the time in microseconds.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL