gocqlx-orm is an opinionated ORM extension for ScyllaDB and Cassandra on top of scylladb/gocqlx.
It reduces repetitive schema and CRUD boilerplate by deriving table metadata directly from Go structs and tags.
- Schema bootstrap from entities: auto-create tables, indexes, and UDTs (
IF NOT EXISTS) - Consistent mapping: one source of truth for Go fields, CQL columns, and key definitions
- Practical repository layer: reusable CRUD and query helpers with pagination and filtering support
- Advanced type support: nested UDTs, collection of UDTs, and tuple columns
- Auto create table from model metadata
- Auto create secondary indexes from tags
- Auto create UDTs (including nested dependencies)
- Base repository for common CRUD operations:
Save,SaveManySaveWithTTL,SaveManyWithTTLFindAll,Find,FindByPrimaryKey,FindByPartitionKey,FindWithOptionCountAll,CountDeleteAll,DeleteByPrimaryKey,DeleteManyByPrimaryKey,DeleteByPartitionKey
go get github.com/saivnct/gocqlx-orm@latesttype Person struct {
ID gocql.UUID `db:"id" pk:"1"`
FirstName string `db:"first_name" index:"true"`
LastName string `db:"last_name" pk:"2"`
Email string `db:"email" index:"true"`
CreatedAt time.Time `db:"created_at" ck:"1"`
}
func (Person) TableName() string {
return "person"
}cluster := gocql.NewCluster(hosts...)
session, err := gocqlx.WrapSession(cluster.CreateSession())
if err != nil {
log.Fatal(err)
}
defer session.Close()type PersonRepository struct {
cqlxoRepository.BaseScyllaRepository
}
func NewPersonRepository(session gocqlx.Session) (*PersonRepository, error) {
d := &PersonRepository{}
if err := d.InitRepository(session, Person{}); err != nil {
return nil, err
}
return d, nil
}personRepository, err := NewPersonRepository(session)
if err != nil {
log.Fatal(err)
}
err = personRepository.Save(Person{
ID: gocql.TimeUUID(),
FirstName: "Ada",
LastName: "Lovelace",
Email: "ada@example.com",
CreatedAt: time.Now(),
})TTL values are in seconds (CQL USING TTL unit):
// expires after 1 hour
err = personRepository.SaveWithTTL(person, 3600)
// expires after 10 minutes
err = personRepository.SaveManyWithTTL([]cqlxoEntity.BaseScyllaEntityInterface{
person1, person2,
}, 600)TTL validation behavior:
ttl <= 0returnscqlxoRepository.InvalidTTL- TTL is applied per written row using
INSERT ... USING TTL ?
SaveMany, SaveManyWithTTL, and DeleteManyByPrimaryKey use CQL batch execution with safe defaults:
- default chunk size:
50 - default batch type:
gocql.UnloggedBatch
You can override this per repository:
personRepository.SetBatchSaveConfig(cqlxoRepository.BatchSaveConfig{
ChunkSize: 100,
Type: gocql.LoggedBatch,
})Fallback rules:
ChunkSize <= 0falls back to50- unsupported batch type falls back to
gocql.UnloggedBatch
BatchSaveConfig.Type accepts:
gocql.UnloggedBatch(default)gocql.LoggedBatchgocql.CounterBatch
Choose batch type based on your write pattern:
gocql.UnloggedBatch: best default for mostSaveManyworkloads where rows are independent. Lower coordinator overhead than logged batches.gocql.LoggedBatch: use when you need atomicity across multiple statements in the same batch (all-or-nothing semantics). This has higher overhead.gocql.CounterBatch: use only for counter table updates.
Practical guidance:
- Prefer small chunk sizes even with batches; very large batches can hurt performance.
- For ordinary bulk insert/update of non-counter data, start with
UnloggedBatch. - Switch to
LoggedBatchonly when atomic multi-statement behavior is required.
flowchart LR
A[Entity Struct + Tags] --> B[codec.ParseTableMetaData]
B --> C[EntityInfo + table.Metadata]
C --> D[Repository.InitRepository]
D --> E[Auto CREATE TYPE / TABLE / INDEX]
D --> F[CRUD APIs]
F --> G[gocqlx Query Builder]
G --> H[gocql / ScyllaDB]
gocqlx-orm reads metadata from struct tags:
db: column name (optional). Defaults toCamelToSnakeASCII(fieldName).pk: partition key order (required for primary-key columns).ck: clustering key order (optional).index: set to"true"to auto-create a secondary index.dbType: explicit CQL type (optional). If omitted, inferred from Go type.
In addition to primitive and collection types, the library supports:
- UDT columns
- Nested UDT fields
- Collections of UDTs (for example,
[]MyUDT) - Tuple columns
- Byte array/blob columns (for example,
[]byte)
type Address struct {
gocqlx.UDT
Street string `db:"street"`
City string `db:"city"`
Zip int `db:"zip"`
}
type DeliveryProfile struct {
gocqlx.UDT
PrimaryAddress Address `db:"primary_address"`
AddressHistory []Address `db:"address_history"`
}type Coordinate struct {
Lat float64 `db:"lat"`
Lng float64 `db:"lng"`
}
func (Coordinate) Tuple() string {
return "coordinate"
}FindWithOption supports:
AllowFilteringPage/ItemsPerPageOrderBy/Order
Use it for page-by-page reads while preserving repository-level mapping.
Integration examples are available under test/:
test/01_example_test.gotest/02_example_test.gotest/03_example_test.gotest/04_example_test.gotest/05_example_test.gotest/06_example_test.go(byte array / blob + TTL)
The integration tests use testcontainers-go with ScyllaDB containers.
Run all tests:
go test ./...Run focused integration examples:
go test ./test -run TestExample04_NestedUDT_SliceUDT_Tuple -count=1This project targets developer productivity first, while keeping runtime overhead low by delegating query execution to gocqlx / gocql.
For your workload, benchmark with your own schema and consistency profile:
go test ./... -run '^$' -bench . -benchmemRecommended benchmark dimensions:
- single-row insert/read latency (
Save,FindByPrimaryKey) - bulk insert throughput (
SaveMany,SaveManyWithTTL) - paginated read latency (
FindWithOption) - impact of indexed vs non-indexed predicates
- impact of nested UDT / tuple payload size
If you currently use gocqlx directly, migration is incremental:
- Keep your existing cluster/session setup.
- Add entity structs with ORM tags (
db,pk,ck,index,dbType). - Create a repository per entity embedding
cqlxoRepository.BaseScyllaRepository. - Replace hand-written bootstrap DDL with
InitRepository(...). - Move common CRUD queries to repository methods (
Save,Find*,Count*,Delete*). - Keep complex custom CQL where needed; repository usage does not block direct session access.
Before/after pattern:
// Before (plain gocqlx): manual schema + hand-written query wiring.
// After (gocqlx-orm): schema bootstrap + reusable repository from entity metadata.This library is focused on pragmatic schema-first ORM support for Scylla/Cassandra applications that want:
- low ceremony data access,
- explicit struct-based modeling,
- and fast startup with automatic schema bootstrap.
Issues and pull requests are welcome.
When contributing:
- include tests for behavior changes,
- keep APIs backward compatible where possible,
- and prefer clear, explicit mappings over hidden magic.
License file is not included yet. Add LICENSE to the repository root to enable license badge and package metadata visibility.