# gocqlx: a query builder, ORM and migration tool for Scylla in Go

> GocqlX makes working with Scylla easy and less error-prone, by binding query parameters to struct fields and scanning results back into structs by field name, with a table model for CRUD, a CQL query builder, a migration tool and a code generator that writes your models from a live keyspace. It is Apache licensed, and from version 3 it depends on ScyllaDB's fork of the gocql driver.

**scylladb/gocqlx** — All-In-One: CQL query builder, ORM and migration tool

- Repository: https://github.com/scylladb/gocqlx
- Stars: 1,034 · Forks: 132
- Language: Go
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/scylladb-gocqlx

## Binding CQL to structs instead of to strings

GocqlX makes working with Scylla easy and less error-prone. It is inspired by Sqlx, the tool for working with SQL databases, but it goes beyond what Sqlx provides.

The core idea is not complicated and it is the thing you would build first if you were tired of writing CQL by hand. Query parameters are bound from struct fields, from a map, or from both. Query results are scanned into structs based on field names. And there are convenience functions for the common tasks, such as loading a single row into a struct or all rows into a slice of structs.

The field-name mapping is the detail that makes it pleasant. Field names are converted to snake case by default, so a Go field named HairColor maps to a column hair_color without a tag. A field is skipped when it carries the db:"-" tag or when it is unexported, so computed fields and internal state stay out of the query without being renamed.

There is a fourth feature that is unusual enough to notice. Making any struct a UDT without implementing marshalling functions. In Cassandra and Scylla, a user-defined type is normally a piece of work, and here it is a matter of declaring the struct.

On performance the README makes a plain claim: GocqlX is fast, its performance is comparable to the raw driver, and benchmarks are linked in the document. That is the right standard to hold it to, since the whole argument for a convenience layer over a driver is that it does not cost you the thing the driver was fast at.

## Version 3 requires ScyllaDB's driver, and the README is blunt about it

There is one piece of housekeeping that has to be done before anything else, and it is the first thing in the compatibility section.

Versions of GocqlX prior to v3.0.0 are compatible with both Apache Cassandra's gocql and ScyllaDB's fork. Starting with v3.0.0, GocqlX exclusively supports the scylladb/gocql driver. If you are using GocqlX v3.0.0 or newer, you must ensure your go.mod includes a replace directive to point to ScyllaDB's fork:

```go
// Use the latest version of scylladb/gocql; check for updates at https://github.com/scylladb/gocql/releases
replace github.com/gocql/gocql => github.com/scylladb/gocql v1.16.0
```

The reason is given plainly. GocqlX relies on ScyllaDB-specific extensions and bug fixes introduced in the gocql fork, and attempting to use the standard gocql driver with GocqlX v3.0.0 or newer may lead to build or runtime issues.

That is a real constraint on adoption, and it is worth being clear about what it means. A project that targets Scylla specifically can satisfy it with one line in go.mod. A project that targets both Scylla and Apache Cassandra cannot use version 3 without giving up the standard driver, and is better served by the version 2 line, which the README confirms still works with both.

The pinned version in the example is 1.16.0 of the fork, and the comment tells you to check the releases page for updates, which is the right instruction given that the fork moves with ScyllaDB's own release cycle.

## Table models: metadata, keys and the tag that skips a field

The table package is where the ORM part lives, and it works on a metadata value that names the table, its columns, and its partition and sort keys.

```go
// metadata specifies table name and columns it must be in sync with schema.
var personMetadata = table.Metadata{
	Name:    "person",
	Columns: []string{"first_name", "last_name", "email"},
	PartKey: []string{"first_name"},
	SortKey: []string{"last_name"},
}

// personTable allows for simple CRUD operations based on personMetadata.
var personTable = table.New(personMetadata)

// Person represents a row in person table.
// Field names are converted to snake case by default, no need to add special tags.
// A field will not be persisted by adding the `db:"-"` tag or making it unexported.
type Person struct {
	FirstName string
	LastName  string
	Email     []string
	HairColor string `db:"-"`  // exported and skipped
	eyeColor  string           // unexported also skipped
}
```

The comment on the metadata is the important line: the columns must be in sync with the schema. That is a constraint, and a useful one, because it means the metadata is a declaration of what the table looks like, and a mismatch is a bug the author has to fix rather than a silent runtime error.

Two things about the struct are worth pausing on. The list field, Email []string, maps to a CQL list column without extra ceremony. And the two skipped fields, one exported with the db tag and one unexported, show the two ways to keep a field out of the persistence layer, which is what you need for a value you compute rather than store.

The metadata also carries the keys, which is what makes a model more than a column list. The partition key and the sort key are part of the contract, because they determine what queries are even possible against the table.

## Queries read as a chain, and the release semantics

The query API is consistent across operations, and the pattern is worth reading in full because it is short.

Binding a struct and inserting:

```go
q := session.Query(personTable.Insert()).BindStruct(p)
if err := q.ExecRelease(); err != nil {
	log.Fatal(err)
}
```

Loading a single row into a struct:

```go
q := session.Query(personTable.Get()).BindStruct(p)
if err := q.GetRelease(&p); err != nil {
	log.Fatal(err)
}
```

And loading all rows into a slice, this time binding a map rather than a struct:

```go
var people []Person
q := session.Query(personTable.Select()).BindMap(qb.M{"first_name": "Michał"})
if err := q.SelectRelease(&people); err != nil {
	log.Fatal(err)
}
```

The session itself is a gocql session wrapped at creation, and the wrapped session embeds the gocql.Session pointer, so the gocql API is still there underneath when you need it.

The naming of the execute methods is worth noting. ExecRelease, GetRelease and SelectRelease execute the operation and release the query. A released query cannot be reused. These method names do not by themselves guarantee write durability or change the database consistency settings.

## schemagen writes the models from a live keyspace

Writing the metadata by hand is fine for one table and tedious for thirty, so there is a generator, and it works against a running cluster.

Installing it is slightly awkward, and the README explains why. You cannot install it with go install, since the go.mod contains a replace directive, so you check it out and install manually:

```bash
git clone git@github.com:scylladb/gocqlx.git
cd gocqlx/cmd/schemagen/
go install .
```

The usage section also gives a go get -u form for the cmd/schemagen path, which is a slightly different route to the same binary, and it is worth knowing both exist since the manual route is the one the compatibility note explains.

The flags are few. The cluster is a comma-separated list of host and port tuples defaulting to 127.0.0.1, the keyspace to inspect is required, the output folder defaults to models and the package name defaults to models as well. A typical run is:

```bash
schemagen -cluster="127.0.0.1:9042" -keyspace="examples" -output="models" -pkgname="models"
```

What comes out is a Go file that starts with a generated-code marker and a table model per table in the keyspace, with the columns and the keys filled in from the cluster's own schema. That last point is the value of the tool. The metadata in the earlier example says the columns must be in sync with the schema, and generating the metadata from the schema is how you guarantee that, rather than discovering the drift when a query fails.

## The three subpackages, and the migration tool

The README groups the functionality into the core package and three subpackages, and each solves a different part of the problem.

The qb package is the CQL query builder, and it provides the map type used in the BindMap call above, qb.M. The table package provides the CRUD operations based on the table model, which is the Metadata and New pair and the Insert, Get and Select methods on the resulting table value.

The migrate package is database migrations, and it is the piece that answers the question every schema-carrying library eventually has to answer: how do you evolve a table's columns in a database where altering a schema is not a single statement. The README's feature list mentions it directly, and for a ScyllaDB or Cassandra deployment, where schema changes are not free, that is a substantial part of the work.

The fourth item in the feature list, the ability to make any struct a UDT without implementing marshalling functions, is not a subpackage but a feature of the core, and it is arguably the one that would be hardest to replicate by hand.

Taken together, the package is closer to a data access layer for a specific database than to a general ORM. The table models are Cassandra-shaped, with a partition key and a sort key, and the migration tool exists because the database's own constraints demand it. That is the right shape for the problem, and it is also the limit: nothing here maps cleanly onto a relational schema.

## Where gocqlx is the wrong choice

The first wrong-tool case is a project on Apache Cassandra that must stay on the upstream gocql driver. Version 3 of GocqlX requires the ScyllaDB fork, and the README says the standard driver may lead to build or runtime issues. A team with a policy against depending on a vendor fork is better served by the version 2 line, if it is still maintained, or by writing the binding layer directly against gocql.

The second is a project that is not on Scylla or Cassandra at all. The table model assumes a partition key and a sort key, and the query builder speaks CQL. The structure of the library is the structure of that data model, and there is no subset of it that is a general-purpose data layer.

The third is a team that wants a query builder over string SQL. GocqlX is explicitly inspired by Sqlx, and it is worth noticing where it deliberately departs from that model. There is no query string to write and no placeholder syntax to learn, because the query is a method on the model, and that is a better fit for a schemaless family of tables and a worse fit if you want the same code to work against a relational database.

A practical note on the generated models. schemagen marks its output as generated code that should not be edited, which means the metadata stays in step with the cluster, but it also means your hand-written changes to a model are lost the next time someone runs it.

## Conclusion

Adopt gocqlx if you are writing a Go service against Scylla and want struct-based binding, generated models and a migration story instead of hand-written CQL strings. Do not adopt it if your database is Apache Cassandra and you are pinned to the upstream gocql driver, because version 3 requires the ScyllaDB fork and the README is explicit that the standard driver may fail to build or misbehave. Before you upgrade from version 2, plan the replace directive, and treat the generated models as generated code, since schemagen marks them as do-not-edit and they will be overwritten on the next run.

## FAQ

### What does gocqlx provide over the raw gocql driver?

Binding query parameters from struct fields, a map, or both, scanning results into structs based on field names, convenience functions for loading a single row or all rows into structs, the ability to make any struct a UDT without writing marshalling functions, and three subpackages: a CQL query builder, table-based CRUD and database migrations.

### Why does gocqlx v3 need a replace directive?

From v3.0.0 GocqlX exclusively supports the scylladb/gocql driver, because it relies on ScyllaDB-specific extensions and bug fixes. You must add replace github.com/gocql/gocql => github.com/scylladb/gocql v1.16.0 to your go.mod, and using the standard driver with v3.0.0 or newer may lead to build or runtime issues.

### How do I generate table models with gocqlx?

Use schemagen, which inspects a keyspace on a running cluster and writes a models file. Install it by cloning the repository and running go install in the cmd/schemagen directory, because go.mod contains a replace directive, then run schemagen -cluster="127.0.0.1:9042" -keyspace="examples" -output="models" -pkgname="models".

### How does gocqlx decide which struct fields to persist?

Field names are converted to snake case by default, so no tags are needed. A field is not persisted if it carries the db:"-" tag or if it is unexported, which is how computed or internal fields are kept out of the query.

### What licence is gocqlx released under?

Apache-2.0. The most recent listed release is v3.0.4, dated 2025-09-13, the default branch is master, and the last push was on 2026-10-06.

## Sources

- [Issues](https://github.com/scylladb/gocqlx/issues)
- [License: Apache-2.0](https://github.com/scylladb/gocqlx/blob/master/LICENSE)
- [README](https://github.com/scylladb/gocqlx/blob/master/README.md)
- [Releases](https://github.com/scylladb/gocqlx/releases)
- [scylladb/gocqlx on GitHub](https://github.com/scylladb/gocqlx)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/scylladb-gocqlx
