* Update gocql version to v1.16.1 1. Update gocql to v1.16.1 2. Update golang to 1.25, since new gocql version requres it * Update golangci to 2.5.0 It is needed since 1.64.8 does not support golang 1.25. 1. Update golangci to 2.5.0 2. Migrate from golangci config v1 to v2 3. Integrate fieldaligment to golangci 4. Drop fieldaligment from Makefile 5. Address complaints
979 lines
24 KiB
Go
979 lines
24 KiB
Go
// Copyright (C) 2017 ScyllaDB
|
|
// Use of this source code is governed by a ALv2-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//go:build all || integration
|
|
// +build all integration
|
|
|
|
package gocqlx_test
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gocql/gocql"
|
|
"golang.org/x/sync/errgroup"
|
|
"gopkg.in/inf.v0"
|
|
|
|
"github.com/scylladb/gocqlx/v3"
|
|
"github.com/scylladb/gocqlx/v3/gocqlxtest"
|
|
"github.com/scylladb/gocqlx/v3/qb"
|
|
"github.com/scylladb/gocqlx/v3/table"
|
|
)
|
|
|
|
// Running examples locally:
|
|
// make run-scylla
|
|
// make run-examples
|
|
func TestExample(t *testing.T) {
|
|
cluster := gocqlxtest.CreateCluster()
|
|
|
|
session, err := gocqlx.WrapSession(cluster.CreateSession())
|
|
if err != nil {
|
|
t.Fatal("create session:", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
_ = session.ExecStmt(`DROP KEYSPACE examples`)
|
|
|
|
basicCreateAndPopulateKeyspace(t, session, "examples")
|
|
createAndPopulateKeyspaceAllTypes(t, session)
|
|
basicReadScyllaVersion(t, session)
|
|
|
|
datatypesBlob(t, session)
|
|
datatypesUserDefinedType(t, session)
|
|
datatypesUserDefinedTypeWrapper(t, session)
|
|
datatypesJSON(t, session)
|
|
|
|
pagingForwardPaging(t, session)
|
|
pagingEfficientFullTableScan(t, session)
|
|
|
|
lwtLock(t, session)
|
|
unsetEmptyValues(t, session)
|
|
}
|
|
|
|
type Song struct {
|
|
ID gocql.UUID
|
|
Title string
|
|
Album string
|
|
Artist string
|
|
Tags []string
|
|
Data []byte
|
|
}
|
|
|
|
type PlaylistItem struct {
|
|
ID gocql.UUID
|
|
Title string
|
|
Album string
|
|
Artist string
|
|
SongID gocql.UUID
|
|
}
|
|
|
|
// This example shows how to use query builders and table models to build
|
|
// queries. It uses "BindStruct" function for parameter binding and "Select"
|
|
// function for loading data to a slice.
|
|
func basicCreateAndPopulateKeyspace(t *testing.T, session gocqlx.Session, keyspace string) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(fmt.Sprintf(
|
|
`CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`,
|
|
keyspace,
|
|
))
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.songs (
|
|
id uuid PRIMARY KEY,
|
|
title text,
|
|
album text,
|
|
artist text,
|
|
tags set<text>,
|
|
data blob)`, keyspace))
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.playlists (
|
|
id uuid,
|
|
title text,
|
|
album text,
|
|
artist text,
|
|
song_id uuid,
|
|
PRIMARY KEY (id, title, album, artist))`, keyspace))
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
playlistMetadata := table.Metadata{
|
|
Name: fmt.Sprintf("%s.playlists", keyspace),
|
|
Columns: []string{"id", "title", "album", "artist", "song_id"},
|
|
PartKey: []string{"id"},
|
|
SortKey: []string{"title", "album", "artist", "song_id"},
|
|
}
|
|
playlistTable := table.New(playlistMetadata)
|
|
|
|
// Insert song using query builder.
|
|
insertSong := qb.Insert(fmt.Sprintf("%s.songs", keyspace)).
|
|
Columns("id", "title", "album", "artist", "tags", "data").Query(session)
|
|
|
|
insertSong.BindStruct(Song{
|
|
ID: mustParseUUID("756716f7-2e54-4715-9f00-91dcbea6cf50"),
|
|
Title: "La Petite Tonkinoise",
|
|
Album: "Bye Bye Blackbird",
|
|
Artist: "Joséphine Baker",
|
|
Tags: []string{"jazz", "2013"},
|
|
Data: []byte("music"),
|
|
})
|
|
if err := insertSong.ExecRelease(); err != nil {
|
|
t.Fatal("ExecRelease() failed:", err)
|
|
}
|
|
|
|
// Insert playlist using table model.
|
|
insertPlaylist := playlistTable.InsertQuery(session)
|
|
|
|
insertPlaylist.BindStruct(PlaylistItem{
|
|
ID: mustParseUUID("2cc9ccb7-6221-4ccb-8387-f22b6a1b354d"),
|
|
Title: "La Petite Tonkinoise",
|
|
Album: "Bye Bye Blackbird",
|
|
Artist: "Joséphine Baker",
|
|
SongID: mustParseUUID("756716f7-2e54-4715-9f00-91dcbea6cf50"),
|
|
})
|
|
if err := insertPlaylist.ExecRelease(); err != nil {
|
|
t.Fatal("ExecRelease() failed:", err)
|
|
}
|
|
|
|
// Query and displays data.
|
|
queryPlaylist := playlistTable.SelectQuery(session)
|
|
|
|
queryPlaylist.BindStruct(&PlaylistItem{
|
|
ID: mustParseUUID("2cc9ccb7-6221-4ccb-8387-f22b6a1b354d"),
|
|
})
|
|
|
|
var items []*PlaylistItem
|
|
if err := queryPlaylist.Select(&items); err != nil {
|
|
t.Fatal("Select() failed:", err)
|
|
}
|
|
|
|
for _, i := range items {
|
|
t.Logf("%+v", *i)
|
|
}
|
|
}
|
|
|
|
// This example shows how to use query builders and table models to build
|
|
// queries with all types. It uses "BindStruct" function for parameter binding and "Select"
|
|
// function for loading data to a slice.
|
|
func createAndPopulateKeyspaceAllTypes(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
// generated with schemagen
|
|
type CheckTypesStruct struct {
|
|
AsciI string
|
|
BigInt int64
|
|
BloB []byte
|
|
BooleaN bool
|
|
DatE time.Time
|
|
DecimaL inf.Dec
|
|
DoublE float64
|
|
DuratioN gocql.Duration
|
|
FloaT float32
|
|
ID [16]byte
|
|
InT int32
|
|
IneT string
|
|
ListInt []int32
|
|
MapIntText map[int32]string
|
|
SetInt []int32
|
|
SmallInt int16
|
|
TexT string
|
|
TimE time.Duration
|
|
TimestamP time.Time
|
|
TimeuuiD [16]byte
|
|
TinyInt int8
|
|
VarChar string
|
|
VarInt int64
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.check_types (
|
|
asci_i ascii,
|
|
big_int bigint,
|
|
blo_b blob,
|
|
boolea_n boolean,
|
|
dat_e date,
|
|
decima_l decimal,
|
|
doubl_e double,
|
|
duratio_n duration,
|
|
floa_t float,
|
|
ine_t inet,
|
|
in_t int,
|
|
small_int smallint,
|
|
tex_t text,
|
|
tim_e time,
|
|
timestam_p timestamp,
|
|
timeuui_d timeuuid,
|
|
tiny_int tinyint,
|
|
id uuid PRIMARY KEY,
|
|
var_char varchar,
|
|
var_int varint,
|
|
map_int_text map<int, text>,
|
|
list_int list<int>,
|
|
set_int set<int>)`)
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
// generated with schemagen
|
|
checkTypesTable := table.New(table.Metadata{
|
|
Name: "examples.check_types",
|
|
Columns: []string{
|
|
"asci_i",
|
|
"big_int",
|
|
"blo_b",
|
|
"boolea_n",
|
|
"dat_e",
|
|
"decima_l",
|
|
"doubl_e",
|
|
"duratio_n",
|
|
"floa_t",
|
|
"id",
|
|
"in_t",
|
|
"ine_t",
|
|
"list_int",
|
|
"map_int_text",
|
|
"set_int",
|
|
"small_int",
|
|
"tex_t",
|
|
"tim_e",
|
|
"timestam_p",
|
|
"timeuui_d",
|
|
"tiny_int",
|
|
"var_char",
|
|
"var_int",
|
|
},
|
|
PartKey: []string{"id"},
|
|
SortKey: []string{},
|
|
})
|
|
|
|
// Insert song using query builder.
|
|
insertCheckTypes := qb.Insert("examples.check_types").
|
|
Columns("asci_i", "big_int", "blo_b", "boolea_n", "dat_e", "decima_l", "doubl_e", "duratio_n", "floa_t",
|
|
"ine_t", "in_t", "small_int", "tex_t", "tim_e", "timestam_p", "timeuui_d", "tiny_int", "id", "var_char",
|
|
"var_int", "map_int_text", "list_int", "set_int").Query(session)
|
|
|
|
var byteID [16]byte
|
|
id := []byte("756716f7-2e54-4715-9f00-91dcbea6cf50")
|
|
copy(byteID[:], id)
|
|
|
|
date := time.Date(2021, time.December, 11, 10, 23, 0, 0, time.UTC)
|
|
var double float64 = 1.2 //nolint:staticcheck // type needs to be enforces
|
|
var float float32 = 1.3
|
|
var integer int32 = 123
|
|
listInt := []int32{1, 2, 3}
|
|
mapIntStr := map[int32]string{
|
|
1: "a",
|
|
2: "b",
|
|
}
|
|
setInt := []int32{2, 4, 6}
|
|
var smallInt int16 = 12
|
|
var tinyInt int8 = 14
|
|
var varInt int64 = 20
|
|
|
|
insertCheckTypes.BindStruct(CheckTypesStruct{
|
|
AsciI: "test qscci",
|
|
BigInt: 9223372036854775806, // MAXINT64 - 1,
|
|
BloB: []byte("this is blob test"),
|
|
BooleaN: false,
|
|
DatE: date,
|
|
DecimaL: *inf.NewDec(1, 1),
|
|
DoublE: double,
|
|
DuratioN: gocql.Duration{Months: 1, Days: 1, Nanoseconds: 86400},
|
|
FloaT: float,
|
|
ID: byteID,
|
|
InT: integer,
|
|
IneT: "127.0.0.1",
|
|
ListInt: listInt,
|
|
MapIntText: mapIntStr,
|
|
SetInt: setInt,
|
|
SmallInt: smallInt,
|
|
TexT: "text example",
|
|
TimE: 86400000000,
|
|
TimestamP: date,
|
|
TimeuuiD: gocql.TimeUUID(),
|
|
TinyInt: tinyInt,
|
|
VarChar: "test varchar",
|
|
VarInt: varInt,
|
|
})
|
|
if err := insertCheckTypes.ExecRelease(); err != nil {
|
|
t.Fatal("ExecRelease() failed:", err)
|
|
}
|
|
|
|
// Query and displays data.
|
|
queryCheckTypes := checkTypesTable.SelectQuery(session)
|
|
|
|
queryCheckTypes.BindStruct(&CheckTypesStruct{
|
|
ID: byteID,
|
|
})
|
|
|
|
var items []*CheckTypesStruct
|
|
if err := queryCheckTypes.Select(&items); err != nil {
|
|
t.Fatal("Select() failed:", err)
|
|
}
|
|
|
|
for _, i := range items {
|
|
t.Logf("%+v", *i)
|
|
}
|
|
}
|
|
|
|
// This example shows how to load a single value using "Get" function.
|
|
// Get can also work with UDTs and types that implement gocql marshalling functions.
|
|
func basicReadScyllaVersion(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
var releaseVersion string
|
|
|
|
err := session.Query("SELECT release_version FROM system.local", nil).Get(&releaseVersion)
|
|
if err != nil {
|
|
t.Fatal("Get() failed:", err)
|
|
}
|
|
|
|
t.Logf("Scylla version is: %s", releaseVersion)
|
|
}
|
|
|
|
// This examples shows how to bind data from a map using "BindMap" function,
|
|
// override field name mapping using the "db" tags, with the default mechanism of
|
|
// handling situations where driver returns more coluns that we are ready to
|
|
// consume.
|
|
func datatypesBlob(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.blobs(k int PRIMARY KEY, b blob, m map<text, blob>)`)
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
// One way to get a byte buffer is to allocate it and fill it yourself:
|
|
var buf [16]byte
|
|
for i := range buf {
|
|
buf[i] = 0xff
|
|
}
|
|
|
|
insert := qb.Insert("examples.blobs").Columns("k", "b", "m").Query(session)
|
|
insert.BindMap(qb.M{
|
|
"k": 1,
|
|
"b": buf[:],
|
|
"m": map[string][]byte{"test": buf[:]},
|
|
})
|
|
|
|
if err := insert.ExecRelease(); err != nil {
|
|
t.Fatal("ExecRelease() failed:", err)
|
|
}
|
|
|
|
row := &struct {
|
|
Buffer []byte `db:"b"`
|
|
Mapping map[string][]byte `db:"m"`
|
|
}{}
|
|
q := qb.Select("examples.blobs").Where(qb.EqLit("k", "1")).Query(session)
|
|
|
|
// By default missing UDT fields are treated as null instead of failing
|
|
if err := q.Iter().Get(row); err != nil {
|
|
t.Fatal("Get() failed:", err)
|
|
}
|
|
|
|
t.Logf("%+v", row.Buffer)
|
|
t.Logf("%+v", row.Mapping)
|
|
}
|
|
|
|
type Coordinates struct {
|
|
gocqlx.UDT
|
|
X int
|
|
Y int
|
|
}
|
|
|
|
// This example shows how to add User Defined Type marshalling capabilities by
|
|
// adding a single line - embedding gocqlx.UDT.
|
|
func datatypesUserDefinedType(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TYPE IF NOT EXISTS examples.coordinates(x int, y int)`)
|
|
if err != nil {
|
|
t.Fatal("create type:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.udts(k int PRIMARY KEY, c coordinates)`)
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
coordinates1 := Coordinates{X: 12, Y: 34}
|
|
coordinates2 := Coordinates{X: 56, Y: 78}
|
|
|
|
insert := qb.Insert("examples.udts").Columns("k", "c").Query(session)
|
|
insert.BindMap(qb.M{
|
|
"k": 1,
|
|
"c": coordinates1,
|
|
})
|
|
if err := insert.Exec(); err != nil {
|
|
t.Fatal("Exec() failed:", err)
|
|
}
|
|
insert.BindMap(qb.M{
|
|
"k": 2,
|
|
"c": coordinates2,
|
|
})
|
|
if err := insert.Exec(); err != nil {
|
|
t.Fatal("Exec() failed:", err)
|
|
}
|
|
|
|
var coordinates []Coordinates
|
|
q := qb.Select("examples.udts").Columns("c").Query(session)
|
|
if err := q.Select(&coordinates); err != nil {
|
|
t.Fatal("Select() failed:", err)
|
|
}
|
|
|
|
for _, c := range coordinates {
|
|
t.Logf("%+v", c)
|
|
}
|
|
}
|
|
|
|
type coordinates struct {
|
|
X int
|
|
Y int
|
|
}
|
|
|
|
// This example shows how to add User Defined Type marshalling capabilities to
|
|
// types that we cannot modify, like library or transfer objects, without
|
|
// rewriting them in runtime.
|
|
func datatypesUserDefinedTypeWrapper(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TYPE IF NOT EXISTS examples.coordinates(x int, y int)`)
|
|
if err != nil {
|
|
t.Fatal("create type:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.udts_wrapper(k int PRIMARY KEY, c coordinates)`)
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
// Embed coordinates within CoordinatesUDT
|
|
c1 := &coordinates{X: 12, Y: 34}
|
|
c2 := &coordinates{X: 56, Y: 78}
|
|
|
|
type CoordinatesUDT struct {
|
|
gocqlx.UDT
|
|
*coordinates
|
|
}
|
|
|
|
coordinates1 := CoordinatesUDT{coordinates: c1}
|
|
coordinates2 := CoordinatesUDT{coordinates: c2}
|
|
|
|
insert := qb.Insert("examples.udts_wrapper").Columns("k", "c").Query(session)
|
|
insert.BindMap(qb.M{
|
|
"k": 1,
|
|
"c": coordinates1,
|
|
})
|
|
if err := insert.Exec(); err != nil {
|
|
t.Fatal("Exec() failed:", err)
|
|
}
|
|
insert.BindMap(qb.M{
|
|
"k": 2,
|
|
"c": coordinates2,
|
|
})
|
|
if err := insert.Exec(); err != nil {
|
|
t.Fatal("Exec() failed:", err)
|
|
}
|
|
|
|
var coordinates []Coordinates
|
|
q := qb.Select("examples.udts_wrapper").Columns("c").Query(session)
|
|
if err := q.Select(&coordinates); err != nil {
|
|
t.Fatal("Select() failed:", err)
|
|
}
|
|
|
|
for _, c := range coordinates {
|
|
t.Logf("%+v", c)
|
|
}
|
|
}
|
|
|
|
// This example shows how to use query builder to work with
|
|
func datatypesJSON(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.querybuilder_json(id int PRIMARY KEY, name text, specs map<text, text>)`)
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
insert := qb.Insert("examples.querybuilder_json").Json().Query(session)
|
|
|
|
insert.Bind(`{ "id": 1, "name": "Mouse", "specs": { "color": "silver" } }`)
|
|
if err := insert.Exec(); err != nil {
|
|
t.Fatal("Exec() failed:", err)
|
|
}
|
|
insert.Bind(`{ "id": 2, "name": "Keyboard", "specs": { "layout": "qwerty" } }`)
|
|
if err := insert.Exec(); err != nil {
|
|
t.Fatal("Exec() failed:", err)
|
|
}
|
|
|
|
// fromJson lets you provide individual columns as JSON:
|
|
insertFromJSON := qb.Insert("examples.querybuilder_json").
|
|
Columns("id", "name").
|
|
FuncColumn("specs", qb.Fn("fromJson", "json")).
|
|
Query(session)
|
|
|
|
insertFromJSON.BindMap(qb.M{
|
|
"id": 3,
|
|
"name": "Screen",
|
|
"json": `{ "size": "24-inch" }`,
|
|
})
|
|
if err := insertFromJSON.Exec(); err != nil {
|
|
t.Fatal("Exec() failed:", err)
|
|
}
|
|
|
|
// Reading the whole row as a JSON object:
|
|
q := qb.Select("examples.querybuilder_json").
|
|
Json().
|
|
Where(qb.EqLit("id", "1")).
|
|
Query(session)
|
|
|
|
var jsonString string
|
|
|
|
if err := q.Get(&jsonString); err != nil {
|
|
t.Fatal("Get() failed:", err)
|
|
}
|
|
t.Logf("Entry #1 as JSON: %s", jsonString)
|
|
|
|
// Extracting a particular column as JSON:
|
|
q = qb.Select("examples.querybuilder_json").
|
|
Columns("id", "toJson(specs) AS json_specs").
|
|
Where(qb.EqLit("id", "2")).
|
|
Query(session)
|
|
|
|
row := &struct {
|
|
ID int
|
|
JSONSpecs string
|
|
}{}
|
|
if err := q.Get(row); err != nil {
|
|
t.Fatal("Get() failed:", err)
|
|
}
|
|
t.Logf("Entry #%d's specs as JSON: %s", row.ID, row.JSONSpecs)
|
|
}
|
|
|
|
type Video struct {
|
|
UserID int
|
|
UserName string
|
|
Added time.Time
|
|
VideoID int
|
|
Title string
|
|
}
|
|
|
|
func pagingFillTable(t *testing.T, insert *gocqlx.Queryx) {
|
|
t.Helper()
|
|
|
|
// 3 users
|
|
for i := 0; i < 3; i++ {
|
|
// 49 videos each
|
|
for j := 0; j < 49; j++ {
|
|
insert.BindStruct(Video{
|
|
UserID: i,
|
|
UserName: fmt.Sprint("user ", i),
|
|
Added: time.Unix(int64(j)*100, 0),
|
|
VideoID: i*100 + j,
|
|
Title: fmt.Sprint("video ", i*100+j),
|
|
})
|
|
|
|
if err := insert.Exec(); err != nil {
|
|
t.Fatal("Exec() failed:", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This example shows how to use stateful paging and how "Select" function
|
|
// can be used to fetch single page only.
|
|
func pagingForwardPaging(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.paging_forward_paging(
|
|
user_id int,
|
|
user_name text,
|
|
added timestamp,
|
|
video_id int,
|
|
title text,
|
|
PRIMARY KEY (user_id, added, video_id)
|
|
) WITH CLUSTERING ORDER BY (added DESC, video_id ASC)`)
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
videoMetadata := table.Metadata{
|
|
Name: "examples.paging_forward_paging",
|
|
Columns: []string{"user_id", "user_name", "added", "video_id", "title"},
|
|
PartKey: []string{"user_id"},
|
|
SortKey: []string{"added", "video_id"},
|
|
}
|
|
videoTable := table.New(videoMetadata)
|
|
|
|
pagingFillTable(t, videoTable.InsertQuery(session))
|
|
|
|
// Query and displays data. Iterate over videos of user "1" 10 entries per request.
|
|
|
|
const itemsPerPage = 10
|
|
|
|
getUserVideos := func(userID int, page []byte) (userVideos []Video, nextPage []byte, err error) {
|
|
q := videoTable.SelectQuery(session).Bind(userID)
|
|
defer q.Release()
|
|
q.PageState(page)
|
|
q.PageSize(itemsPerPage)
|
|
|
|
iter := q.Iter()
|
|
return userVideos, iter.PageState(), iter.Select(&userVideos)
|
|
}
|
|
|
|
var (
|
|
userVideos []Video
|
|
nextPage []byte
|
|
)
|
|
|
|
for i := 1; ; i++ {
|
|
userVideos, nextPage, err = getUserVideos(1, nextPage)
|
|
if err != nil {
|
|
t.Fatalf("load page %d: %s", i, err)
|
|
}
|
|
|
|
t.Logf("Page %d:", i)
|
|
for _, v := range userVideos {
|
|
t.Logf("%+v", v)
|
|
}
|
|
if len(nextPage) == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// This example shows how to efficiently process all rows in a table using
|
|
// the "token" function. It implements idea from blog post [1]:
|
|
// As a bonus we use "CompileNamedQueryString" to get named parameters out of
|
|
// CQL query placeholders like in Python or Java driver.
|
|
//
|
|
// [1] https://www.scylladb.com/2017/02/13/efficient-full-table-scans-with-scylla-1-6/.
|
|
func pagingEfficientFullTableScan(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.paging_efficient_full_table_scan(
|
|
user_id int,
|
|
user_name text,
|
|
added timestamp,
|
|
video_id int,
|
|
title text,
|
|
PRIMARY KEY (user_id, added, video_id)
|
|
) WITH CLUSTERING ORDER BY (added DESC, video_id ASC)`)
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
videoMetadata := table.Metadata{
|
|
Name: "examples.paging_efficient_full_table_scan",
|
|
Columns: []string{"user_id", "user_name", "added", "video_id", "title"},
|
|
PartKey: []string{"user_id"},
|
|
SortKey: []string{"added", "video_id"},
|
|
}
|
|
videoTable := table.New(videoMetadata)
|
|
|
|
pagingFillTable(t, videoTable.InsertQuery(session))
|
|
|
|
// Calculate optimal number of workers for the cluster:
|
|
var (
|
|
nodesInCluster = 1
|
|
coresInNode = 1
|
|
smudgeFactor = 3
|
|
)
|
|
workers := nodesInCluster * coresInNode * smudgeFactor
|
|
|
|
t.Logf("Workers %d", workers)
|
|
|
|
type tokenRange struct {
|
|
Start int64
|
|
End int64
|
|
}
|
|
buf := make(chan tokenRange)
|
|
|
|
// sequencer pushes token ranges to buf
|
|
sequencer := func() error {
|
|
span := int64(math.MaxInt64 / (50 * workers))
|
|
|
|
tr := tokenRange{math.MinInt64, math.MinInt64 + span}
|
|
for tr.End > tr.Start {
|
|
buf <- tr
|
|
tr.Start = tr.End
|
|
tr.End += span
|
|
}
|
|
|
|
tr.End = math.MaxInt64
|
|
buf <- tr
|
|
close(buf)
|
|
|
|
return nil
|
|
}
|
|
|
|
// worker queries a token ranges generated by sequencer
|
|
worker := func() error {
|
|
const cql = `SELECT * FROM examples.paging_efficient_full_table_scan WHERE
|
|
token(user_id) >= :start AND
|
|
token(user_id) < :end`
|
|
|
|
stmt, names, err := gocqlx.CompileNamedQueryString(cql)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
q := session.Query(stmt, names)
|
|
defer q.Release()
|
|
|
|
var v Video
|
|
for {
|
|
tr, ok := <-buf
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
iter := q.BindStruct(tr).Iter()
|
|
for iter.StructScan(&v) {
|
|
t.Logf("%+v:", v)
|
|
}
|
|
if err := iter.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Query and displays data.
|
|
|
|
var wg errgroup.Group
|
|
wg.Go(sequencer)
|
|
for i := 0; i < workers; i++ {
|
|
wg.Go(worker)
|
|
}
|
|
|
|
if err := wg.Wait(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// This example shows how to use Lightweight Transactions (LWT) aka.
|
|
// Compare-And-Set (CAS) functions.
|
|
// See: https://docs.scylladb.com/using-scylla/lwt/ for more details.
|
|
func lwtLock(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
type Lock struct {
|
|
Name string
|
|
Owner string
|
|
TTL int64
|
|
}
|
|
|
|
err = session.ExecStmt(`CREATE TABLE examples.lock (name text PRIMARY KEY, owner text)`)
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
extend := func(lock Lock) bool {
|
|
q := qb.Update("examples.lock").
|
|
Set("owner").
|
|
Where(qb.Eq("name")).
|
|
If(qb.Eq("owner")).
|
|
TTLNamed("ttl").
|
|
Query(session).
|
|
SerialConsistency(gocql.Serial).
|
|
BindStruct(lock)
|
|
|
|
applied, err := q.ExecCASRelease()
|
|
if err != nil {
|
|
t.Fatal("ExecCASRelease() failed:", err)
|
|
}
|
|
return applied
|
|
}
|
|
|
|
acquire := func(lock Lock) (applied bool) {
|
|
var prev Lock
|
|
|
|
defer func() {
|
|
t.Logf("Acquire %+v applied %v owner %+v)", lock, applied, prev)
|
|
}()
|
|
|
|
q := qb.Insert("examples.lock").
|
|
Columns("name", "owner").
|
|
TTLNamed("ttl").
|
|
Unique().
|
|
Query(session).
|
|
SerialConsistency(gocql.Serial).
|
|
BindStruct(lock)
|
|
|
|
applied, err = q.GetCASRelease(&prev)
|
|
if err != nil {
|
|
t.Fatal("GetCASRelease() failed:", err)
|
|
}
|
|
if applied {
|
|
return true
|
|
}
|
|
if prev.Owner == lock.Owner {
|
|
return extend(lock)
|
|
}
|
|
return false
|
|
}
|
|
|
|
const (
|
|
resource = "acme"
|
|
ttl = time.Second
|
|
)
|
|
|
|
l1 := Lock{
|
|
Name: resource,
|
|
Owner: "1",
|
|
TTL: qb.TTL(ttl),
|
|
}
|
|
|
|
l2 := Lock{
|
|
Name: resource,
|
|
Owner: "2",
|
|
TTL: qb.TTL(ttl),
|
|
}
|
|
|
|
if !acquire(l1) {
|
|
t.Fatal("l1 failed to acquire lock")
|
|
}
|
|
if acquire(l2) {
|
|
t.Fatal("unexpectedly l2 acquired lock")
|
|
}
|
|
if !acquire(l1) {
|
|
t.Fatal("l1 failed to extend lock")
|
|
}
|
|
time.Sleep(time.Second)
|
|
if !acquire(l2) {
|
|
t.Fatal("l2 failed to acquire lock")
|
|
}
|
|
if acquire(l1) {
|
|
t.Fatal("unexpectedly l1 acquired lock")
|
|
}
|
|
}
|
|
|
|
// This example shows how to reuse the same insert statement with
|
|
// partially filled parameters without generating tombstones for empty columns.
|
|
func unsetEmptyValues(t *testing.T, session gocqlx.Session) {
|
|
t.Helper()
|
|
|
|
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
|
if err != nil {
|
|
t.Fatal("create keyspace:", err)
|
|
}
|
|
|
|
type Operation struct {
|
|
ID string
|
|
ClientID string
|
|
Type string
|
|
PaymentID string
|
|
Fee *inf.Dec
|
|
}
|
|
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.operations (
|
|
id text PRIMARY KEY,
|
|
client_id text,
|
|
type text,
|
|
payment_id text,
|
|
fee decimal)`)
|
|
if err != nil {
|
|
t.Fatal("create table:", err)
|
|
}
|
|
|
|
insertOperation := qb.Insert("examples.operations").
|
|
Columns("id", "client_id", "type", "payment_id", "fee")
|
|
|
|
// Insert operation with empty paymentID.
|
|
insertQuery := insertOperation.Query(session).
|
|
WithBindTransformer(gocqlx.UnsetEmptyTransformer).
|
|
BindStruct(Operation{
|
|
ID: "1",
|
|
ClientID: "42",
|
|
Type: "Transfer",
|
|
Fee: inf.NewDec(1, 1),
|
|
})
|
|
if err := insertQuery.ExecRelease(); err != nil {
|
|
t.Fatal("ExecRelease() failed:", err)
|
|
}
|
|
|
|
// Set default transformer to avoid setting it for each query.
|
|
gocqlx.DefaultBindTransformer = gocqlx.UnsetEmptyTransformer
|
|
defer func() {
|
|
gocqlx.DefaultBindTransformer = nil
|
|
}()
|
|
|
|
// Insert operation with empty fee.
|
|
insertQuery = insertOperation.Query(session).
|
|
BindStruct(Operation{
|
|
ID: "2",
|
|
ClientID: "42",
|
|
Type: "Input",
|
|
PaymentID: "1",
|
|
})
|
|
if err := insertQuery.ExecRelease(); err != nil {
|
|
t.Fatal("ExecRelease() failed:", err)
|
|
}
|
|
|
|
// Query and displays data.
|
|
var ops []*Operation
|
|
if err := qb.Select("examples.operations").Query(session).Select(&ops); err != nil {
|
|
t.Fatal("Select() failed:", err)
|
|
}
|
|
|
|
for _, op := range ops {
|
|
t.Logf("%+v", *op)
|
|
}
|
|
}
|
|
|
|
func mustParseUUID(s string) gocql.UUID {
|
|
u, err := gocql.ParseUUID(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return u
|
|
}
|