cassandra

package module
v0.1.1 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


Unit Tests Go Reference

About

cassandra is a Go database/sql driver for Apache Cassandra and ScyllaDB. It wraps the Apache Cassandra Go driver, which this file calls gocql, and it registers the one name cassandra.

The driver is part of the xo family of projects. usql uses it to connect to Cassandra and ScyllaDB, dburl writes its DSN, and dbmeta reads database metadata through it. Its types, options and errors follow the drivers of dbimp.

The driver was github.com/xo/cql. The name CQL is gone, because CQL is the language and cassandra is the product. CQL is the query language of Cassandra, and it looks like SQL.

Installing

Install the driver in the usual way:

go get github.com/xo/cassandra@latest

Using

Import the package for its side effect, and open a database with the name cassandra and a DSN. A DSN is the text that names the server and the settings for it. This program writes a row, reads it back, and reads a row that holds a NULL:

package main

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

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

func main() {
	ctx := context.Background()
	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()

	// An argument that is a statement option changes how this one statement
	// runs. It is not sent to Cassandra as a value.
	id := uuid.NewV7()
	_, err = db.ExecContext(ctx,
		"INSERT INTO users (id, name, tags) VALUES (?, ?, ?)",
		id, "Ada", []string{"admin", "dev"},
		cassandra.WithConsistency(gocql.LocalQuorum), cassandra.WithTimeout(5*time.Second))
	if err != nil {
		log.Fatal(err)
	}

	// A column that can be NULL scans into a sql.Null[T]. A list scans into a
	// slice, and a uuid scans into the standard uuid.UUID.
	var (
		got  uuid.UUID
		name sql.Null[string]
		tags []string
	)
	err = db.QueryRowContext(ctx, "SELECT id, name, tags FROM users WHERE id = ?", id).Scan(&got, &name, &tags)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(got, name.V, tags)

	// Read many rows in pages of 500 rows.
	rows, err := db.QueryContext(ctx, "SELECT id, name FROM users", cassandra.WithPageSize(500))
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var (
			id   uuid.UUID
			name sql.Null[string]
		)
		if err := rows.Scan(&id, &name); err != nil {
			log.Fatal(err)
		}
		fmt.Println(id, name.Valid, name.V)
	}
	if err := rows.Err(); err != nil {
		log.Fatal(err)
	}
}

All the connections of one sql.DB share one gocql session. The session keeps its own pool of connections to each node. The pool settings of sql.DB limit how many statements run at the same time, and 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 a connector:

cfg := gocql.NewCluster("10.0.0.1", "10.0.0.2")
cfg.Logger = logger
db := 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:

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

The user information holds the credentials, the host part holds one host, and the path names the keyspace. Each host key adds one more host, in order. A host can be an IPv6 address, such as cassandra://[::1]:9042/ks?host=[::2]:9042. dburl turns an alias, such as scylla:// or cql://, into this URL, so every alias works in usql.

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, when the path is empty
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, when the user information holds none
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. 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. An error never holds the DSN, because the DSN can hold a password.

Statements

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 statement 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))
Option Effect
WithConsistency the consistency level
WithSerialConsistency the serial consistency level of a lightweight transaction
WithPageSize how many rows gocql reads in one page
WithIdempotent whether gocql can run the statement more than once
WithTimestamp the write time, in microseconds
WithTimeout the time that the whole statement can take, over every page
WithDatabase the keyspace of the statement. It needs protocol version 5.
WithReadonly WithReadonly(true) fails, because Cassandra has no read-only statement
WithParameter fails, because the protocol has no body of keys

An option that Cassandra cannot honor fails the statement with an error that wraps dbimp.ErrNotSupported, so a caller never believes that a limit holds when it does not.

CQL has no transactions, so BeginTx returns an error that wraps dbimp.ErrNotSupported. A batch is one 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.

ExecContext returns driver.ResultNoRows, because Cassandra reports no row count and no insert ID. A named argument from sql.Named is an error, because gocql cannot bind a value by name. gocql v2.1.2 cannot bind a tuple, so write a tuple as a CQL literal.

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]. A *any receives these Go types, which are the kinds of dbimp:

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.

The driver works with these projects of the xo family:

Project What it does
usql A command line client for many databases. It uses this driver for Cassandra and ScyllaDB.
dburl Parses a database URL, and turns each alias, such as scylla://, into the URL that this driver reads.
dbmeta Reads the metadata of many databases, and holds dbrun, which starts the test servers.
dbimp Other database/sql drivers, and the types, options and errors that this driver shares with them.
dbtpl Generates code from a database schema, through dbmeta.
xo The home of the family of projects.

Testing

The unit tests need no server:

go test -race ./...

The integration tests run against the server that CASSANDRA_DSN names, and they skip when it is empty. CASSANDRA_ORDINARY_DSN names an ordinary user, and without it the test for that user skips. CONTRIBUTING.md shows how to start a server with dbrun.

Documents

Document Holds
docs/CASSANDRA.md what is known about Cassandra and ScyllaDB, and the type table
docs/DESIGN.md the design of the driver
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