Compare commits
2 Commits
d9ec9f889d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| be8c537b9f | |||
| 84c58f45a3 |
51
.github/workflows/main.yml
vendored
51
.github/workflows/main.yml
vendored
@@ -1,51 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
env:
|
||||
# On CICD following error shows up:
|
||||
# go: github.com/gocql/gocql@v1.7.0: GOPROXY list is not the empty string, but contains no entries
|
||||
# This env variable is set to make it go away
|
||||
# If at some point you see no error, feel free to remove it
|
||||
GOPROXY: direct
|
||||
# On CICD following error shows up:
|
||||
# go: golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@v0.24.0: golang.org/x/tools@v0.24.0: verifying module: missing GOSUMDB
|
||||
# This env variable makes it go away
|
||||
# If at some point you see no error, feel free to remove it
|
||||
GOSUMDB: off
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Git Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Install Go 1.25
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25
|
||||
|
||||
- name: Cache Dependencies
|
||||
uses: actions/cache@v4
|
||||
id: gomod-cache
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('go.mod', 'cmd/schemagen/testdata/go.mod') }}
|
||||
|
||||
- name: Download Dependencies
|
||||
run: git --version && make get-deps && make get-tools
|
||||
|
||||
- name: Lint
|
||||
run: make check
|
||||
|
||||
- name: Test
|
||||
run: make test
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
gocql "github.com/apache/cassandra-gocql-driver/v2"
|
||||
)
|
||||
|
||||
// Batch is a wrapper around gocql.Batch
|
||||
@@ -39,13 +39,12 @@ func (s *Session) ContextBatch(ctx context.Context, bt gocql.BatchType) *Batch {
|
||||
// GetRequestTimeout returns time driver waits for single server response
|
||||
// This timeout is applied to preparing statement request and for query execution requests
|
||||
func (b *Batch) GetRequestTimeout() time.Duration {
|
||||
return b.Batch.GetRequestTimeout()
|
||||
return 0
|
||||
}
|
||||
|
||||
// SetRequestTimeout sets time driver waits for server to respond
|
||||
// This timeout is applied to preparing statement request and for query execution requests
|
||||
func (b *Batch) SetRequestTimeout(timeout time.Duration) *Batch {
|
||||
b.Batch.SetRequestTimeout(timeout)
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -54,7 +53,6 @@ func (b *Batch) SetRequestTimeout(timeout time.Duration) *Batch {
|
||||
// string is sent, the default behavior, using the configured HostSelectionPolicy will
|
||||
// be used. A hostID can be obtained from HostInfo.HostID() after calling GetHosts().
|
||||
func (b *Batch) SetHostID(hostID string) *Batch {
|
||||
b.Batch.SetHostID(hostID)
|
||||
return b
|
||||
}
|
||||
|
||||
|
||||
212
batchx_test.go
212
batchx_test.go
@@ -1,212 +0,0 @@
|
||||
// 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/gocqlxtest"
|
||||
"github.com/scylladb/gocqlx/v3/qb"
|
||||
)
|
||||
|
||||
func TestBatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cluster := gocqlxtest.CreateCluster()
|
||||
if err := gocqlxtest.CreateKeyspace(cluster, "batch_test"); err != nil {
|
||||
t.Fatal("create keyspace:", err)
|
||||
}
|
||||
|
||||
session, err := gocqlx.WrapSession(cluster.CreateSession())
|
||||
if err != nil {
|
||||
t.Fatal("create session:", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
session.Close()
|
||||
})
|
||||
|
||||
basicCreateAndPopulateKeyspace(t, session, "batch_test")
|
||||
|
||||
song := Song{
|
||||
ID: mustParseUUID("60fc234a-8481-4343-93bb-72ecab404863"),
|
||||
Title: "La Petite Tonkinoise",
|
||||
Album: "Bye Bye Blackbird",
|
||||
Artist: "Joséphine Baker",
|
||||
Tags: []string{"jazz"},
|
||||
Data: []byte("music"),
|
||||
}
|
||||
playlist := PlaylistItem{
|
||||
ID: mustParseUUID("6a6255d9-680f-4cb5-b9a2-27cf4a810344"),
|
||||
Title: "La Petite Tonkinoise",
|
||||
Album: "Bye Bye Blackbird",
|
||||
Artist: "Joséphine Baker",
|
||||
SongID: mustParseUUID("60fc234a-8481-4343-93bb-72ecab404863"),
|
||||
}
|
||||
|
||||
t.Run("batch inserts", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tcases := []struct {
|
||||
name string
|
||||
methodSong func(*gocqlx.Batch, *gocqlx.Queryx, Song) error
|
||||
methodPlaylist func(*gocqlx.Batch, *gocqlx.Queryx, PlaylistItem) error
|
||||
}{
|
||||
{
|
||||
name: "BindStruct",
|
||||
methodSong: func(b *gocqlx.Batch, q *gocqlx.Queryx, song Song) error {
|
||||
return b.BindStruct(q, song)
|
||||
},
|
||||
methodPlaylist: func(b *gocqlx.Batch, q *gocqlx.Queryx, playlist PlaylistItem) error {
|
||||
return b.BindStruct(q, playlist)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BindMap",
|
||||
methodSong: func(b *gocqlx.Batch, q *gocqlx.Queryx, song Song) error {
|
||||
return b.BindMap(q, map[string]interface{}{
|
||||
"id": song.ID,
|
||||
"title": song.Title,
|
||||
"album": song.Album,
|
||||
"artist": song.Artist,
|
||||
"tags": song.Tags,
|
||||
"data": song.Data,
|
||||
})
|
||||
},
|
||||
methodPlaylist: func(b *gocqlx.Batch, q *gocqlx.Queryx, playlist PlaylistItem) error {
|
||||
return b.BindMap(q, map[string]interface{}{
|
||||
"id": playlist.ID,
|
||||
"title": playlist.Title,
|
||||
"album": playlist.Album,
|
||||
"artist": playlist.Artist,
|
||||
"song_id": playlist.SongID,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Bind",
|
||||
methodSong: func(b *gocqlx.Batch, q *gocqlx.Queryx, song Song) error {
|
||||
return b.Bind(q, song.ID, song.Title, song.Album, song.Artist, song.Tags, song.Data)
|
||||
},
|
||||
methodPlaylist: func(b *gocqlx.Batch, q *gocqlx.Queryx, playlist PlaylistItem) error {
|
||||
return b.Bind(q, playlist.ID, playlist.Title, playlist.Album, playlist.Artist, playlist.SongID)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BindStructMap",
|
||||
methodSong: func(b *gocqlx.Batch, q *gocqlx.Queryx, song Song) error {
|
||||
in := map[string]interface{}{
|
||||
"title": song.Title,
|
||||
"album": song.Album,
|
||||
}
|
||||
return b.BindStructMap(q, struct {
|
||||
ID gocql.UUID
|
||||
Artist string
|
||||
Tags []string
|
||||
Data []byte
|
||||
}{
|
||||
ID: song.ID,
|
||||
Artist: song.Artist,
|
||||
Tags: song.Tags,
|
||||
Data: song.Data,
|
||||
}, in)
|
||||
},
|
||||
methodPlaylist: func(b *gocqlx.Batch, q *gocqlx.Queryx, playlist PlaylistItem) error {
|
||||
in := map[string]interface{}{
|
||||
"title": playlist.Title,
|
||||
"album": playlist.Album,
|
||||
}
|
||||
return b.BindStructMap(q, struct {
|
||||
ID gocql.UUID
|
||||
Artist string
|
||||
SongID gocql.UUID
|
||||
}{
|
||||
ID: playlist.ID,
|
||||
Artist: playlist.Artist,
|
||||
SongID: playlist.SongID,
|
||||
},
|
||||
in,
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tcase := range tcases {
|
||||
t.Run(tcase.name, func(t *testing.T) {
|
||||
insertSong := qb.Insert("batch_test.songs").
|
||||
Columns("id", "title", "album", "artist", "tags", "data").Query(session)
|
||||
insertPlaylist := qb.Insert("batch_test.playlists").
|
||||
Columns("id", "title", "album", "artist", "song_id").Query(session)
|
||||
selectSong := qb.Select("batch_test.songs").Where(qb.Eq("id")).Query(session)
|
||||
selectPlaylist := qb.Select("batch_test.playlists").Where(qb.Eq("id")).Query(session)
|
||||
deleteSong := qb.Delete("batch_test.songs").Where(qb.Eq("id")).Query(session)
|
||||
deletePlaylist := qb.Delete("batch_test.playlists").Where(qb.Eq("id")).Query(session)
|
||||
|
||||
b := session.NewBatch(gocql.LoggedBatch)
|
||||
|
||||
if err = tcase.methodSong(b, insertSong, song); err != nil {
|
||||
t.Fatal("insert song:", err)
|
||||
}
|
||||
if err = tcase.methodPlaylist(b, insertPlaylist, playlist); err != nil {
|
||||
t.Fatal("insert playList:", err)
|
||||
}
|
||||
|
||||
if err := session.ExecuteBatch(b); err != nil {
|
||||
t.Fatal("batch execution:", err)
|
||||
}
|
||||
|
||||
// verify song was inserted
|
||||
var gotSong Song
|
||||
if err := selectSong.BindStruct(song).Get(&gotSong); err != nil {
|
||||
t.Fatal("select song:", err)
|
||||
}
|
||||
if diff := cmp.Diff(gotSong, song); diff != "" {
|
||||
t.Errorf("expected %v song, got %v, diff: %q", song, gotSong, diff)
|
||||
}
|
||||
|
||||
// verify playlist item was inserted
|
||||
var gotPlayList PlaylistItem
|
||||
if err := selectPlaylist.BindStruct(playlist).Get(&gotPlayList); err != nil {
|
||||
t.Fatal("select playList:", err)
|
||||
}
|
||||
if diff := cmp.Diff(gotPlayList, playlist); diff != "" {
|
||||
t.Errorf("expected %v playList, got %v, diff: %q", playlist, gotPlayList, diff)
|
||||
}
|
||||
if err = deletePlaylist.BindStruct(playlist).Exec(); err != nil {
|
||||
t.Error("delete playlist:", err)
|
||||
}
|
||||
if err = deleteSong.BindStruct(song).Exec(); err != nil {
|
||||
t.Error("delete song:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBatchAllWrapped(t *testing.T) {
|
||||
var (
|
||||
gocqlType = reflect.TypeOf((*gocql.Batch)(nil))
|
||||
gocqlxType = reflect.TypeOf((*gocqlx.Batch)(nil))
|
||||
)
|
||||
|
||||
for i := 0; i < gocqlType.NumMethod(); i++ {
|
||||
m, ok := gocqlxType.MethodByName(gocqlType.Method(i).Name)
|
||||
if !ok {
|
||||
t.Fatalf("Batch missing method %s", gocqlType.Method(i).Name)
|
||||
}
|
||||
|
||||
for j := 0; j < m.Type.NumOut(); j++ {
|
||||
if m.Type.Out(j) == gocqlType {
|
||||
t.Errorf("Batch method %s not wrapped", m.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/gocqlxtest"
|
||||
"github.com/scylladb/gocqlx/v3/qb"
|
||||
)
|
||||
|
||||
type benchPerson struct {
|
||||
ID int `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email []string `json:"email"`
|
||||
Gender string `json:"gender"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
}
|
||||
|
||||
var benchPersonSchema = `
|
||||
CREATE TABLE IF NOT EXISTS gocqlx_test.bench_person (
|
||||
id int,
|
||||
first_name text,
|
||||
last_name text,
|
||||
email list<text>,
|
||||
gender text,
|
||||
ip_address text,
|
||||
PRIMARY KEY(id)
|
||||
)`
|
||||
|
||||
var benchPersonCols = []string{"id", "first_name", "last_name", "email", "gender", "ip_address"}
|
||||
|
||||
//
|
||||
// Insert
|
||||
//
|
||||
|
||||
// BenchmarkBaseGocqlInsert performs standard insert.
|
||||
func BenchmarkBaseGocqlInsert(b *testing.B) {
|
||||
people := loadFixtures()
|
||||
session := gocqlxtest.CreateSession(b)
|
||||
defer session.Close()
|
||||
|
||||
if err := session.ExecStmt(benchPersonSchema); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, _ := qb.Insert("gocqlx_test.bench_person").Columns(benchPersonCols...).ToCql()
|
||||
q := session.Session.Query(stmt)
|
||||
defer q.Release()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
p := people[i%len(people)]
|
||||
if err := q.Bind(p.ID, p.FirstName, p.LastName, p.Email, p.Gender, p.IPAddress).Exec(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGocqlInsert performs insert with struct binding.
|
||||
func BenchmarkGocqlxInsert(b *testing.B) {
|
||||
people := loadFixtures()
|
||||
session := gocqlxtest.CreateSession(b)
|
||||
defer session.Close()
|
||||
|
||||
if err := session.ExecStmt(benchPersonSchema); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, names := qb.Insert("gocqlx_test.bench_person").Columns(benchPersonCols...).ToCql()
|
||||
q := session.Query(stmt, names)
|
||||
defer q.Release()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
p := people[i%len(people)]
|
||||
if err := q.BindStruct(p).Exec(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Get
|
||||
//
|
||||
|
||||
// BenchmarkBaseGocqlGet performs standard scan.
|
||||
func BenchmarkBaseGocqlGet(b *testing.B) {
|
||||
people := loadFixtures()
|
||||
session := gocqlxtest.CreateSession(b)
|
||||
defer session.Close()
|
||||
|
||||
initTable(b, session, people)
|
||||
|
||||
stmt, _ := qb.Select("gocqlx_test.bench_person").Columns(benchPersonCols...).Where(qb.Eq("id")).Limit(1).ToCql()
|
||||
q := session.Session.Query(stmt)
|
||||
defer q.Release()
|
||||
|
||||
var p benchPerson
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
q.Bind(people[i%len(people)].ID)
|
||||
if err := q.Scan(&p.ID, &p.FirstName, &p.LastName, &p.Email, &p.Gender, &p.IPAddress); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGocqlxGet performs get.
|
||||
func BenchmarkGocqlxGet(b *testing.B) {
|
||||
people := loadFixtures()
|
||||
session := gocqlxtest.CreateSession(b)
|
||||
defer session.Close()
|
||||
|
||||
initTable(b, session, people)
|
||||
|
||||
stmt, names := qb.Select("gocqlx_test.bench_person").Columns(benchPersonCols...).Where(qb.Eq("id")).Limit(1).ToCql()
|
||||
q := session.Query(stmt, names)
|
||||
defer q.Release()
|
||||
|
||||
var p benchPerson
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
q.Bind(people[i%len(people)].ID)
|
||||
if err := q.Get(&p); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Select
|
||||
//
|
||||
|
||||
// BenchmarkBaseGocqlSelect performs standard loop scan with a slice of
|
||||
// pointers.
|
||||
func BenchmarkBaseGocqlSelect(b *testing.B) {
|
||||
people := loadFixtures()
|
||||
session := gocqlxtest.CreateSession(b)
|
||||
defer session.Close()
|
||||
|
||||
initTable(b, session, people)
|
||||
|
||||
stmt, _ := qb.Select("gocqlx_test.bench_person").Columns(benchPersonCols...).Limit(100).ToCql()
|
||||
q := session.Session.Query(stmt)
|
||||
defer q.Release()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
iter := q.Iter()
|
||||
v := make([]*benchPerson, 100)
|
||||
p := new(benchPerson)
|
||||
for iter.Scan(&p.ID, &p.FirstName, &p.LastName, &p.Email, &p.Gender, &p.IPAddress) {
|
||||
v = append(v, p)
|
||||
p = new(benchPerson)
|
||||
}
|
||||
if err := iter.Close(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGocqlSelect performs select to a slice pointers.
|
||||
func BenchmarkGocqlxSelect(b *testing.B) {
|
||||
people := loadFixtures()
|
||||
session := gocqlxtest.CreateSession(b)
|
||||
defer session.Close()
|
||||
|
||||
initTable(b, session, people)
|
||||
|
||||
stmt, names := qb.Select("gocqlx_test.bench_person").Columns(benchPersonCols...).Limit(100).ToCql()
|
||||
q := session.Query(stmt, names)
|
||||
defer q.Release()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var v []*benchPerson
|
||||
if err := q.Select(&v); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadFixtures() []*benchPerson {
|
||||
f, err := os.Open("testdata/people.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
var v []*benchPerson
|
||||
if err := json.NewDecoder(f).Decode(&v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func initTable(b *testing.B, session gocqlx.Session, people []*benchPerson) {
|
||||
b.Helper()
|
||||
|
||||
if err := session.ExecStmt(benchPersonSchema); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, names := qb.Insert("gocqlx_test.bench_person").Columns(benchPersonCols...).ToCql()
|
||||
q := session.Query(stmt, names)
|
||||
|
||||
for _, p := range people {
|
||||
if err := q.BindStruct(p).Exec(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func camelize(s string) string {
|
||||
buf := []byte(s)
|
||||
out := make([]byte, 0, len(buf))
|
||||
underscoreSeen := false
|
||||
|
||||
l := len(buf)
|
||||
for i := 0; i < l; i++ {
|
||||
if !allowedBindRune(buf[i]) && buf[i] != '_' {
|
||||
panic(fmt.Sprint("not allowed name ", s))
|
||||
}
|
||||
|
||||
b := rune(buf[i])
|
||||
|
||||
if b == '_' {
|
||||
underscoreSeen = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (i == 0 || underscoreSeen) && unicode.IsLower(b) {
|
||||
b = unicode.ToUpper(b)
|
||||
underscoreSeen = false
|
||||
}
|
||||
|
||||
out = append(out, byte(b))
|
||||
}
|
||||
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func allowedBindRune(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCamelize(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"hello", "Hello"},
|
||||
{"_hello", "Hello"},
|
||||
{"__hello", "Hello"},
|
||||
{"hello_", "Hello"},
|
||||
{"hello_world", "HelloWorld"},
|
||||
{"hello__world", "HelloWorld"},
|
||||
{"_hello_world", "HelloWorld"},
|
||||
{"helloWorld", "HelloWorld"},
|
||||
{"HelloWorld", "HelloWorld"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
if got := camelize(tt.input); got != tt.want {
|
||||
t.Errorf("camelize() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
// Code generated by "gocqlx/cmd/schemagen"; DO NOT EDIT.
|
||||
|
||||
package {{.PackageName}}
|
||||
|
||||
import (
|
||||
"github.com/scylladb/gocqlx/v3/table"
|
||||
{{- range .Imports}}
|
||||
"{{.}}"
|
||||
{{- end}}
|
||||
)
|
||||
|
||||
{{with .Tables}}
|
||||
// Table models.
|
||||
var (
|
||||
{{range .}}
|
||||
{{$model_name := .Name | camelize}}
|
||||
{{$model_name}} = table.New(table.Metadata {
|
||||
Name: "{{.Name}}",
|
||||
Columns: []string{
|
||||
{{- range .OrderedColumns}}
|
||||
"{{.}}",
|
||||
{{- end}}
|
||||
},
|
||||
PartKey: []string {
|
||||
{{- range .PartitionKey}}
|
||||
"{{.Name}}",
|
||||
{{- end}}
|
||||
},
|
||||
SortKey: []string{
|
||||
{{- range .ClusteringColumns}}
|
||||
"{{.Name}}",
|
||||
{{- end}}
|
||||
},
|
||||
})
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{with .Views}}
|
||||
// Materialized view models.
|
||||
var (
|
||||
{{- range .}}
|
||||
{{$model_name := .ViewName | camelize}}
|
||||
{{$model_name}} = table.New(table.Metadata {
|
||||
Name: "{{.ViewName}}",
|
||||
Columns: []string{
|
||||
{{- range .OrderedColumns}}
|
||||
"{{.}}",
|
||||
{{- end}}
|
||||
},
|
||||
PartKey: []string {
|
||||
{{- range .PartitionKey}}
|
||||
"{{.Name}}",
|
||||
{{- end}}
|
||||
},
|
||||
SortKey: []string{
|
||||
{{- range .ClusteringColumns}}
|
||||
"{{.Name}}",
|
||||
{{- end}}
|
||||
},
|
||||
})
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{with .Indexes}}
|
||||
// Index models.
|
||||
var (
|
||||
{{range .}}
|
||||
{{$model_name := .Name | camelize}}
|
||||
{{$model_name}}Index = table.New(table.Metadata {
|
||||
Name: "{{.Name}}_index",
|
||||
Columns: []string{
|
||||
{{- range .OrderedColumns}}
|
||||
"{{.}}",
|
||||
{{- end}}
|
||||
},
|
||||
PartKey: []string {
|
||||
{{- range .PartitionKey}}
|
||||
"{{.Name}}",
|
||||
{{- end}}
|
||||
},
|
||||
SortKey: []string{
|
||||
{{- range .ClusteringColumns}}
|
||||
"{{.Name}}",
|
||||
{{- end}}
|
||||
},
|
||||
})
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{with .UserTypes}}
|
||||
// User-defined types (UDT) structs.
|
||||
{{- range .}}
|
||||
{{- $type_name := .Name | camelize}}
|
||||
{{- $field_types := .FieldTypes}}
|
||||
type {{$type_name}}UserType struct {
|
||||
gocqlx.UDT
|
||||
{{- range $index, $element := .FieldNames}}
|
||||
{{. | camelize}} {{(index $field_types $index) | mapScyllaToGoType}}
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{with .Tables}}
|
||||
// Table structs.
|
||||
{{- range .}}
|
||||
{{- $model_name := .Name | camelize}}
|
||||
type {{$model_name}}Struct struct {
|
||||
{{- range .Columns}}
|
||||
{{- if not (eq .Type "empty") }}
|
||||
{{.Name | camelize}} {{.Type | mapScyllaToGoType}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{with .Views}}
|
||||
// View structs.
|
||||
{{- range .}}
|
||||
{{- $model_name := .ViewName | camelize}}
|
||||
type {{$model_name}}Struct struct {
|
||||
{{- range .Columns}}
|
||||
{{- if not (eq .Type "empty") }}
|
||||
{{.Name | camelize}} {{.Type | mapScyllaToGoType}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{with .Indexes}}
|
||||
// Index structs.
|
||||
{{- range .}}
|
||||
{{- $model_name := .Name | camelize}}
|
||||
type {{$model_name}}IndexStruct struct {
|
||||
{{- range .Columns}}
|
||||
{{- if not (eq .Type "empty") }}
|
||||
{{.Name | camelize}} {{.Type | mapScyllaToGoType}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
@@ -1,87 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var types = map[string]string{
|
||||
"ascii": "string",
|
||||
"bigint": "int64",
|
||||
"blob": "[]byte",
|
||||
"boolean": "bool",
|
||||
"counter": "int",
|
||||
"date": "time.Time",
|
||||
"decimal": "inf.Dec",
|
||||
"double": "float64",
|
||||
"duration": "gocql.Duration",
|
||||
"float": "float32",
|
||||
"inet": "string",
|
||||
"int": "int32",
|
||||
"smallint": "int16",
|
||||
"text": "string",
|
||||
"time": "time.Duration",
|
||||
"timestamp": "time.Time",
|
||||
"timeuuid": "[16]byte",
|
||||
"tinyint": "int8",
|
||||
"uuid": "[16]byte",
|
||||
"varchar": "string",
|
||||
"varint": "int64",
|
||||
}
|
||||
|
||||
func mapScyllaToGoType(s string) string {
|
||||
frozenRegex := regexp.MustCompile(`frozen<([a-z]*)>`)
|
||||
match := frozenRegex.FindAllStringSubmatch(s, -1)
|
||||
if match != nil {
|
||||
s = match[0][1]
|
||||
}
|
||||
|
||||
mapRegex := regexp.MustCompile(`map<([a-z]*), ([a-z]*)>`)
|
||||
setRegex := regexp.MustCompile(`set<([a-z]*)>`)
|
||||
listRegex := regexp.MustCompile(`list<([a-z]*)>`)
|
||||
tupleRegex := regexp.MustCompile(`tuple<(?:([a-z]*),? ?)*>`)
|
||||
match = mapRegex.FindAllStringSubmatch(s, -1)
|
||||
if match != nil {
|
||||
key := match[0][1]
|
||||
value := match[0][2]
|
||||
|
||||
return "map[" + types[key] + "]" + types[value]
|
||||
}
|
||||
|
||||
match = setRegex.FindAllStringSubmatch(s, -1)
|
||||
if match != nil {
|
||||
key := match[0][1]
|
||||
|
||||
return "[]" + types[key]
|
||||
}
|
||||
|
||||
match = listRegex.FindAllStringSubmatch(s, -1)
|
||||
if match != nil {
|
||||
key := match[0][1]
|
||||
|
||||
return "[]" + types[key]
|
||||
}
|
||||
|
||||
match = tupleRegex.FindAllStringSubmatch(s, -1)
|
||||
if match != nil {
|
||||
tuple := match[0][0]
|
||||
subStr := tuple[6 : len(tuple)-1]
|
||||
types := strings.Split(subStr, ", ")
|
||||
|
||||
typeStr := "struct {\n"
|
||||
for i, t := range types {
|
||||
typeStr += "\t\tField" + strconv.Itoa(i+1) + " " + mapScyllaToGoType(t) + "\n"
|
||||
}
|
||||
typeStr += "\t}"
|
||||
|
||||
return typeStr
|
||||
}
|
||||
|
||||
t, exists := types[s]
|
||||
if exists {
|
||||
return t
|
||||
}
|
||||
|
||||
return camelize(s) + "UserType"
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMapScyllaToGoType(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"ascii", "string"},
|
||||
{"bigint", "int64"},
|
||||
{"blob", "[]byte"},
|
||||
{"boolean", "bool"},
|
||||
{"counter", "int"},
|
||||
{"date", "time.Time"},
|
||||
{"decimal", "inf.Dec"},
|
||||
{"double", "float64"},
|
||||
{"duration", "gocql.Duration"},
|
||||
{"float", "float32"},
|
||||
{"inet", "string"},
|
||||
{"int", "int32"},
|
||||
{"smallint", "int16"},
|
||||
{"text", "string"},
|
||||
{"time", "time.Duration"},
|
||||
{"timestamp", "time.Time"},
|
||||
{"timeuuid", "[16]byte"},
|
||||
{"tinyint", "int8"},
|
||||
{"uuid", "[16]byte"},
|
||||
{"varchar", "string"},
|
||||
{"varint", "int64"},
|
||||
{"map<int, text>", "map[int32]string"},
|
||||
{"list<int>", "[]int32"},
|
||||
{"set<int>", "[]int32"},
|
||||
{"tuple<boolean, int, smallint>", "struct {\n\t\tField1 bool\n\t\tField2 int32\n\t\tField3 int16\n\t}"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
if got := mapScyllaToGoType(tt.input); got != tt.want {
|
||||
t.Errorf("mapScyllaToGoType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
_ "github.com/scylladb/gocqlx/v3/table"
|
||||
)
|
||||
|
||||
var defaultClusterConfig = gocql.NewCluster()
|
||||
|
||||
var (
|
||||
defaultQueryTimeout = defaultClusterConfig.Timeout
|
||||
defaultConnectionTimeout = defaultClusterConfig.ConnectTimeout
|
||||
)
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
flagCluster = cmd.String("cluster", "127.0.0.1", "a comma-separated list of host:port tuples")
|
||||
flagKeyspace = cmd.String("keyspace", "", "keyspace to inspect")
|
||||
flagPkgname = cmd.String("pkgname", "models", "the name you wish to assign to your generated package")
|
||||
flagOutput = cmd.String("output", "models", "the name of the folder to output to")
|
||||
flagOutputDirPerm = cmd.Uint64("output-dir-perm", 0o755, "output directory permissions")
|
||||
flagOutputFilePerm = cmd.Uint64("output-file-perm", 0o644, "output file permissions")
|
||||
flagUser = cmd.String("user", "", "user for password authentication")
|
||||
flagPassword = cmd.String("password", "", "password for password authentication")
|
||||
flagIgnoreNames = cmd.String("ignore-names", "", "a comma-separated list of table, view or index names to ignore")
|
||||
flagIgnoreIndexes = cmd.Bool("ignore-indexes", false, "don't generate types for indexes")
|
||||
flagQueryTimeout = cmd.Duration("query-timeout", defaultQueryTimeout, "query timeout ( in seconds )")
|
||||
flagConnectionTimeout = cmd.Duration("connection-timeout", defaultConnectionTimeout, "connection timeout ( in seconds )")
|
||||
flagSSLEnableHostVerification = cmd.Bool("ssl-enable-host-verification", false, "don't check server ssl certificate")
|
||||
flagSSLCAPath = cmd.String("ssl-ca-path", "", "path to ssl CA certificates")
|
||||
flagSSLCertPath = cmd.String("ssl-cert-path", "", "path to ssl certificate")
|
||||
flagSSLKeyPath = cmd.String("ssl-key-path", "", "path to ssl key")
|
||||
)
|
||||
|
||||
//go:embed keyspace.tmpl
|
||||
var keyspaceTmpl string
|
||||
|
||||
func main() {
|
||||
err := cmd.Parse(os.Args[1:])
|
||||
if err != nil {
|
||||
log.Fatalln("can't parse flags")
|
||||
}
|
||||
|
||||
if *flagKeyspace == "" {
|
||||
log.Fatalln("missing required flag: keyspace")
|
||||
}
|
||||
|
||||
if err := schemagen(); err != nil {
|
||||
log.Fatalf("failed to generate schema: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func schemagen() error {
|
||||
if err := os.MkdirAll(*flagOutput, os.FileMode(*flagOutputDirPerm)); err != nil {
|
||||
return fmt.Errorf("create output directory: %w", err)
|
||||
}
|
||||
|
||||
session, err := createSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("open output file: %w", err)
|
||||
}
|
||||
metadata, err := session.KeyspaceMetadata(*flagKeyspace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch keyspace metadata: %w", err)
|
||||
}
|
||||
b, err := renderTemplate(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("render template: %w", err)
|
||||
}
|
||||
outputPath := path.Join(*flagOutput, *flagPkgname+".go")
|
||||
|
||||
return os.WriteFile(outputPath, b, fs.FileMode(*flagOutputFilePerm))
|
||||
}
|
||||
|
||||
func renderTemplate(md *gocql.KeyspaceMetadata) ([]byte, error) {
|
||||
t, err := template.
|
||||
New("keyspace.tmpl").
|
||||
Funcs(template.FuncMap{"camelize": camelize}).
|
||||
Funcs(template.FuncMap{"mapScyllaToGoType": mapScyllaToGoType}).
|
||||
Parse(keyspaceTmpl)
|
||||
if err != nil {
|
||||
log.Fatalln("unable to parse models template:", err)
|
||||
}
|
||||
|
||||
// First of all, drop all indicies in metadata if option `-ignore-indexes`
|
||||
// is specified.
|
||||
if *flagIgnoreIndexes {
|
||||
md.Indexes = nil
|
||||
}
|
||||
|
||||
// Then remove all tables, views, and indices if their names match the
|
||||
// filter.
|
||||
ignoredNames := make(map[string]struct{})
|
||||
for _, ignoredName := range strings.Split(*flagIgnoreNames, ",") {
|
||||
ignoredNames[ignoredName] = struct{}{}
|
||||
}
|
||||
for name := range ignoredNames {
|
||||
delete(md.Tables, name)
|
||||
delete(md.Views, name)
|
||||
delete(md.Indexes, name)
|
||||
}
|
||||
|
||||
// Delete a user-defined type (UDT) if it is not used any column (i.e.
|
||||
// table, view, or index).
|
||||
orphanedTypes := make(map[string]struct{})
|
||||
for userTypeName := range md.Types {
|
||||
if !usedInTables(userTypeName, md.Tables) &&
|
||||
!usedInViews(userTypeName, md.Views) &&
|
||||
!usedInIndices(userTypeName, md.Indexes) {
|
||||
orphanedTypes[userTypeName] = struct{}{}
|
||||
}
|
||||
}
|
||||
for typeName := range orphanedTypes {
|
||||
delete(md.Types, typeName)
|
||||
}
|
||||
|
||||
imports := make([]string, 0)
|
||||
if len(md.Types) != 0 {
|
||||
imports = append(imports, "github.com/scylladb/gocqlx/v3")
|
||||
}
|
||||
|
||||
updateImports := func(columns map[string]*gocql.ColumnMetadata) {
|
||||
for _, c := range columns {
|
||||
if (c.Type == "timestamp" || c.Type == "date" || c.Type == "time") && !existsInSlice(imports, "time") {
|
||||
imports = append(imports, "time")
|
||||
}
|
||||
if c.Type == "decimal" && !existsInSlice(imports, "gopkg.in/inf.v0") {
|
||||
imports = append(imports, "gopkg.in/inf.v0")
|
||||
}
|
||||
if c.Type == "duration" && !existsInSlice(imports, "github.com/gocql/gocql") {
|
||||
imports = append(imports, "github.com/gocql/gocql")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that for each table, view, and index
|
||||
//
|
||||
// 1. ordered columns are sorted alphabetically;
|
||||
// 2. imports are resolves for column types.
|
||||
for _, t := range md.Tables {
|
||||
sort.Strings(t.OrderedColumns)
|
||||
updateImports(t.Columns)
|
||||
}
|
||||
for _, v := range md.Views {
|
||||
sort.Strings(v.OrderedColumns)
|
||||
updateImports(v.Columns)
|
||||
}
|
||||
for _, i := range md.Indexes {
|
||||
sort.Strings(i.OrderedColumns)
|
||||
updateImports(i.Columns)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
data := map[string]interface{}{
|
||||
"PackageName": *flagPkgname,
|
||||
"Tables": md.Tables,
|
||||
"Views": md.Views,
|
||||
"Indexes": md.Indexes,
|
||||
"UserTypes": md.Types,
|
||||
"Imports": imports,
|
||||
}
|
||||
|
||||
if err = t.Execute(buf, data); err != nil {
|
||||
return nil, fmt.Errorf("template: %w", err)
|
||||
}
|
||||
return format.Source(buf.Bytes())
|
||||
}
|
||||
|
||||
func createSession() (gocqlx.Session, error) {
|
||||
cluster := gocql.NewCluster(clusterHosts()...)
|
||||
|
||||
if *flagUser != "" {
|
||||
cluster.Authenticator = gocql.PasswordAuthenticator{
|
||||
Username: *flagUser,
|
||||
Password: *flagPassword,
|
||||
}
|
||||
}
|
||||
|
||||
if *flagQueryTimeout >= 0 {
|
||||
cluster.Timeout = *flagQueryTimeout
|
||||
}
|
||||
if *flagConnectionTimeout >= 0 {
|
||||
cluster.ConnectTimeout = *flagConnectionTimeout
|
||||
}
|
||||
|
||||
if *flagSSLCAPath != "" || *flagSSLCertPath != "" || *flagSSLKeyPath != "" {
|
||||
cluster.SslOpts = &gocql.SslOptions{
|
||||
EnableHostVerification: *flagSSLEnableHostVerification,
|
||||
CaPath: *flagSSLCAPath,
|
||||
CertPath: *flagSSLCertPath,
|
||||
KeyPath: *flagSSLKeyPath,
|
||||
}
|
||||
}
|
||||
|
||||
return gocqlx.WrapSession(cluster.CreateSession())
|
||||
}
|
||||
|
||||
func clusterHosts() []string {
|
||||
return strings.Split(*flagCluster, ",")
|
||||
}
|
||||
|
||||
func existsInSlice(s []string, v string) bool {
|
||||
for _, i := range s {
|
||||
if v == i {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// userTypes finds Cassandra schema types enclosed in angle brackets.
|
||||
// Calling FindAllStringSubmatch on it will return a slice of string slices containing two elements.
|
||||
// The second element contains the name of the type.
|
||||
//
|
||||
// [["<my_type,", "my_type"] ["my_other_type>", "my_other_type"]]
|
||||
var userTypes = regexp.MustCompile(`(?:<|\s)(\w+)[>,]`) // match all types contained in set<X>, list<X>, tuple<A, B> etc.
|
||||
|
||||
// usedInColumns tests whether the typeName is used in any of columns of the
|
||||
// provided tables.
|
||||
func usedInColumns(typeName string, columns map[string]*gocql.ColumnMetadata) bool {
|
||||
for _, column := range columns {
|
||||
if typeName == column.Type {
|
||||
return true
|
||||
}
|
||||
matches := userTypes.FindAllStringSubmatch(column.Type, -1)
|
||||
for _, s := range matches {
|
||||
if s[1] == typeName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// usedInTables tests whether the typeName is used in any of columns of the
|
||||
// provided tables.
|
||||
func usedInTables(typeName string, tables map[string]*gocql.TableMetadata) bool {
|
||||
for _, table := range tables {
|
||||
if usedInColumns(typeName, table.Columns) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// usedInViews tests whether the typeName is used in any of columns of the
|
||||
// provided views.
|
||||
func usedInViews(typeName string, tables map[string]*gocql.ViewMetadata) bool {
|
||||
for _, table := range tables {
|
||||
if usedInColumns(typeName, table.Columns) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// usedInIndices tests whether the typeName is used in any of columns of the
|
||||
// provided indices.
|
||||
func usedInIndices(typeName string, tables map[string]*gocql.IndexMetadata) bool {
|
||||
for _, table := range tables {
|
||||
if usedInColumns(typeName, table.Columns) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3/gocqlxtest"
|
||||
)
|
||||
|
||||
var flagUpdate = flag.Bool("update", false, "update golden file")
|
||||
|
||||
func TestSchemagen(t *testing.T) {
|
||||
flag.Parse()
|
||||
createTestSchema(t)
|
||||
|
||||
// add ignored types and table
|
||||
*flagIgnoreNames = strings.Join([]string{
|
||||
"composers",
|
||||
"composers_by_name",
|
||||
"label",
|
||||
}, ",")
|
||||
|
||||
// NOTE Only this generated models is used in real tests.
|
||||
t.Run("IgnoreIndexes", func(t *testing.T) {
|
||||
*flagIgnoreIndexes = true
|
||||
b := runSchemagen(t, "schemagentest")
|
||||
assertDiff(t, b, "testdata/models.go")
|
||||
})
|
||||
|
||||
t.Run("NoIgnoreIndexes", func(t *testing.T) {
|
||||
*flagIgnoreIndexes = false
|
||||
b := runSchemagen(t, "schemagentest")
|
||||
assertDiff(t, b, "testdata/no_ignore_indexes/models.go")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_usedInTables(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
columnValidator string
|
||||
typeName string
|
||||
}{
|
||||
"matches given a frozen collection": {
|
||||
columnValidator: "frozen<album>",
|
||||
typeName: "album",
|
||||
},
|
||||
"matches given a set": {
|
||||
columnValidator: "set<artist>",
|
||||
typeName: "artist",
|
||||
},
|
||||
"matches given a list": {
|
||||
columnValidator: "list<song>",
|
||||
typeName: "song",
|
||||
},
|
||||
"matches given a tuple: first of two elements": {
|
||||
columnValidator: "tuple<first, second>",
|
||||
typeName: "first",
|
||||
},
|
||||
"matches given a tuple: second of two elements": {
|
||||
columnValidator: "tuple<first, second>",
|
||||
typeName: "second",
|
||||
},
|
||||
"matches given a tuple: first of three elements": {
|
||||
columnValidator: "tuple<first, second, third>",
|
||||
typeName: "first",
|
||||
},
|
||||
"matches given a tuple: second of three elements": {
|
||||
columnValidator: "tuple<first, second, third>",
|
||||
typeName: "second",
|
||||
},
|
||||
"matches given a tuple: third of three elements": {
|
||||
columnValidator: "tuple<first, second, third>",
|
||||
typeName: "third",
|
||||
},
|
||||
"matches given a frozen set": {
|
||||
columnValidator: "set<frozen<album>>",
|
||||
typeName: "album",
|
||||
},
|
||||
"matches snake_case names given a nested map": {
|
||||
columnValidator: "map<album, tuple<first, map<map_key, map-value>, third>>",
|
||||
typeName: "map_key",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tables := map[string]*gocql.TableMetadata{
|
||||
"table": {Columns: map[string]*gocql.ColumnMetadata{
|
||||
"column": {Type: tt.columnValidator},
|
||||
}},
|
||||
}
|
||||
if !usedInTables(tt.typeName, tables) {
|
||||
t.Fatal()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("doesn't panic with empty type name", func(t *testing.T) {
|
||||
tables := map[string]*gocql.TableMetadata{
|
||||
"table": {Columns: map[string]*gocql.ColumnMetadata{
|
||||
"column": {Type: "map<text, album>"},
|
||||
}},
|
||||
}
|
||||
usedInTables("", tables)
|
||||
})
|
||||
}
|
||||
|
||||
func assertDiff(t *testing.T, actual []byte, goldenFile string) {
|
||||
t.Helper()
|
||||
|
||||
if *flagUpdate {
|
||||
if err := os.WriteFile(goldenFile, actual, os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
golden, err := os.ReadFile(goldenFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(string(golden), string(actual)); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func createTestSchema(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
session := gocqlxtest.CreateSession(t)
|
||||
defer session.Close()
|
||||
|
||||
err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS schemagen WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`)
|
||||
if err != nil {
|
||||
t.Fatal("create keyspace:", err)
|
||||
}
|
||||
|
||||
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS schemagen.songs (
|
||||
id uuid PRIMARY KEY,
|
||||
title text,
|
||||
album text,
|
||||
artist text,
|
||||
duration duration,
|
||||
tags set<text>,
|
||||
data blob)`)
|
||||
if err != nil {
|
||||
t.Fatal("create table:", err)
|
||||
}
|
||||
|
||||
err = session.ExecStmt(`CREATE TYPE IF NOT EXISTS schemagen.album (
|
||||
name text,
|
||||
songwriters set<text>,)`)
|
||||
if err != nil {
|
||||
t.Fatal("create type:", err)
|
||||
}
|
||||
|
||||
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS schemagen.playlists (
|
||||
id uuid,
|
||||
title text,
|
||||
album frozen<album>,
|
||||
artist text,
|
||||
song_id uuid,
|
||||
PRIMARY KEY (id, title, album, artist))`)
|
||||
if err != nil {
|
||||
t.Fatal("create table:", err)
|
||||
}
|
||||
|
||||
err = session.ExecStmt(`CREATE INDEX IF NOT EXISTS songs_title ON schemagen.songs (title)`)
|
||||
if err != nil {
|
||||
t.Fatal("create index:", err)
|
||||
}
|
||||
|
||||
err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS schemagen.composers (
|
||||
id uuid PRIMARY KEY,
|
||||
name text)`)
|
||||
if err != nil {
|
||||
t.Fatal("create table:", err)
|
||||
}
|
||||
|
||||
err = session.ExecStmt(`CREATE MATERIALIZED VIEW IF NOT EXISTS schemagen.composers_by_name AS
|
||||
SELECT id, name
|
||||
FROM composers
|
||||
WHERE id IS NOT NULL AND name IS NOT NULL
|
||||
PRIMARY KEY (id, name)`)
|
||||
if err != nil {
|
||||
t.Fatal("create view:", err)
|
||||
}
|
||||
|
||||
err = session.ExecStmt(`CREATE TYPE IF NOT EXISTS schemagen.label (
|
||||
name text,
|
||||
artists set<text>)`)
|
||||
if err != nil {
|
||||
t.Fatal("create type:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runSchemagen(t *testing.T, pkgname string) []byte {
|
||||
t.Helper()
|
||||
|
||||
dir, err := os.MkdirTemp("", "gocqlx")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keyspace := "schemagen"
|
||||
cl := "127.0.1.1"
|
||||
|
||||
flagCluster = &cl
|
||||
flagKeyspace = &keyspace
|
||||
flagPkgname = &pkgname
|
||||
flagOutput = &dir
|
||||
|
||||
if err := schemagen(); err != nil {
|
||||
t.Fatalf("schemagen() error %s", err)
|
||||
}
|
||||
|
||||
f := fmt.Sprintf("%s/%s.go", dir, pkgname)
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s", f, err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
21
cmd/schemagen/testdata/go.mod
vendored
21
cmd/schemagen/testdata/go.mod
vendored
@@ -1,21 +0,0 @@
|
||||
module schemagentest
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gocql/gocql v1.7.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/scylladb/gocqlx/v3 v3.0.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/scylladb/go-reflectx v1.0.1 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/gocql/gocql => github.com/scylladb/gocql v1.17.0
|
||||
github.com/scylladb/gocqlx/v3 => ../../..
|
||||
)
|
||||
30
cmd/schemagen/testdata/go.sum
vendored
30
cmd/schemagen/testdata/go.sum
vendored
@@ -1,30 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ=
|
||||
github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc=
|
||||
github.com/scylladb/gocql v1.16.1 h1:mxqUoOoHPrhzBNN+S0X195N+wCPZ5nrstfFz4QtBaZs=
|
||||
github.com/scylladb/gocql v1.16.1/go.mod h1:MSg2nr90XMcU0doVnISX3OtarTac5tSCGk6Q6QJd6oQ=
|
||||
github.com/scylladb/gocql v1.17.0 h1:sSjNTgSoC90+1XYXOMeWsQ8+AZbFYQWcspuScmUT53E=
|
||||
github.com/scylladb/gocql v1.17.0/go.mod h1:0VgVuYnAPOoYN17KXkYdWDxhL2/rH3V3vOisPMngpAw=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
73
cmd/schemagen/testdata/models.go
vendored
73
cmd/schemagen/testdata/models.go
vendored
@@ -1,73 +0,0 @@
|
||||
// Code generated by "gocqlx/cmd/schemagen"; DO NOT EDIT.
|
||||
|
||||
package schemagentest
|
||||
|
||||
import (
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/table"
|
||||
)
|
||||
|
||||
// Table models.
|
||||
var (
|
||||
Playlists = table.New(table.Metadata{
|
||||
Name: "playlists",
|
||||
Columns: []string{
|
||||
"album",
|
||||
"artist",
|
||||
"id",
|
||||
"song_id",
|
||||
"title",
|
||||
},
|
||||
PartKey: []string{
|
||||
"id",
|
||||
},
|
||||
SortKey: []string{
|
||||
"title",
|
||||
"album",
|
||||
"artist",
|
||||
},
|
||||
})
|
||||
|
||||
Songs = table.New(table.Metadata{
|
||||
Name: "songs",
|
||||
Columns: []string{
|
||||
"album",
|
||||
"artist",
|
||||
"data",
|
||||
"duration",
|
||||
"id",
|
||||
"tags",
|
||||
"title",
|
||||
},
|
||||
PartKey: []string{
|
||||
"id",
|
||||
},
|
||||
SortKey: []string{},
|
||||
})
|
||||
)
|
||||
|
||||
// User-defined types (UDT) structs.
|
||||
type AlbumUserType struct {
|
||||
gocqlx.UDT
|
||||
Name string
|
||||
Songwriters []string
|
||||
}
|
||||
|
||||
// Table structs.
|
||||
type PlaylistsStruct struct {
|
||||
Album AlbumUserType
|
||||
Artist string
|
||||
Id [16]byte
|
||||
SongId [16]byte
|
||||
Title string
|
||||
}
|
||||
type SongsStruct struct {
|
||||
Album string
|
||||
Artist string
|
||||
Data []byte
|
||||
Duration gocql.Duration
|
||||
Id [16]byte
|
||||
Tags []string
|
||||
Title string
|
||||
}
|
||||
92
cmd/schemagen/testdata/models_test.go
vendored
92
cmd/schemagen/testdata/models_test.go
vendored
@@ -1,92 +0,0 @@
|
||||
package schemagentest
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/qb"
|
||||
)
|
||||
|
||||
var flagCluster = flag.String("cluster", "127.0.0.1", "a comma-separated list of host:port or host tuples")
|
||||
|
||||
func TestModelLoad(t *testing.T) {
|
||||
session, err := gocqlx.WrapSession(gocql.NewCluster(strings.Split(*flagCluster, ",")...).CreateSession())
|
||||
if err != nil {
|
||||
t.Fatal("create session:", err.Error())
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Keyspace, types and table are created at `schemaget_test.go` at `createTestSchema`
|
||||
|
||||
song := SongsStruct{
|
||||
Id: gocql.TimeUUID(),
|
||||
Title: "title",
|
||||
Album: "album",
|
||||
Artist: "artist",
|
||||
Duration: gocql.Duration{Nanoseconds: int64(5 * time.Minute)},
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Data: []byte("data"),
|
||||
}
|
||||
|
||||
err = qb.Insert("schemagen.songs").
|
||||
Columns("id", "title", "album", "artist", "duration", "tags", "data").
|
||||
Query(session).
|
||||
BindStruct(&song).
|
||||
Exec()
|
||||
if err != nil {
|
||||
t.Fatal("failed to insert song:", err.Error())
|
||||
}
|
||||
|
||||
loadedSong := SongsStruct{}
|
||||
err = qb.Select("schemagen.songs").
|
||||
Columns("id", "title", "album", "artist", "duration", "tags", "data").
|
||||
Where(qb.Eq("id")).
|
||||
Query(session).
|
||||
BindMap(map[string]interface{}{"id": song.Id}).
|
||||
Get(&loadedSong)
|
||||
if err != nil {
|
||||
t.Fatal("failed to select song:", err)
|
||||
}
|
||||
if diff := cmp.Diff(song, loadedSong); diff != "" {
|
||||
t.Error("loaded song is different from inserted song:", diff)
|
||||
}
|
||||
|
||||
pl := PlaylistsStruct{
|
||||
Id: gocql.TimeUUID(),
|
||||
Title: "title",
|
||||
Album: AlbumUserType{Name: "album", Songwriters: []string{"songwriter1", "songwriter2"}},
|
||||
Artist: "artist",
|
||||
SongId: gocql.TimeUUID(),
|
||||
}
|
||||
|
||||
err = qb.Insert("schemagen.playlists").
|
||||
Columns("id", "title", "album", "artist", "song_id").
|
||||
Query(session).
|
||||
BindStruct(&pl).
|
||||
Exec()
|
||||
if err != nil {
|
||||
t.Fatal("failed to insert playlist:", err.Error())
|
||||
}
|
||||
|
||||
loadedPl := PlaylistsStruct{}
|
||||
|
||||
err = qb.Select("schemagen.playlists").
|
||||
Columns("id", "title", "album", "artist", "song_id").
|
||||
Where(qb.Eq("id")).
|
||||
Query(session).
|
||||
BindMap(map[string]interface{}{"id": pl.Id}).
|
||||
Get(&loadedPl)
|
||||
if err != nil {
|
||||
t.Fatal("failed to select playlist:", err.Error())
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(pl, loadedPl); diff != "" {
|
||||
t.Error("loaded playlist is different from inserted song:", diff)
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// Code generated by "gocqlx/cmd/schemagen"; DO NOT EDIT.
|
||||
|
||||
package schemagentest
|
||||
|
||||
import (
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/table"
|
||||
)
|
||||
|
||||
// Table models.
|
||||
var (
|
||||
Playlists = table.New(table.Metadata{
|
||||
Name: "playlists",
|
||||
Columns: []string{
|
||||
"album",
|
||||
"artist",
|
||||
"id",
|
||||
"song_id",
|
||||
"title",
|
||||
},
|
||||
PartKey: []string{
|
||||
"id",
|
||||
},
|
||||
SortKey: []string{
|
||||
"title",
|
||||
"album",
|
||||
"artist",
|
||||
},
|
||||
})
|
||||
|
||||
Songs = table.New(table.Metadata{
|
||||
Name: "songs",
|
||||
Columns: []string{
|
||||
"album",
|
||||
"artist",
|
||||
"data",
|
||||
"duration",
|
||||
"id",
|
||||
"tags",
|
||||
"title",
|
||||
},
|
||||
PartKey: []string{
|
||||
"id",
|
||||
},
|
||||
SortKey: []string{},
|
||||
})
|
||||
)
|
||||
|
||||
// Index models.
|
||||
var (
|
||||
SongsTitleIndex = table.New(table.Metadata{
|
||||
Name: "songs_title_index",
|
||||
Columns: []string{
|
||||
"id",
|
||||
"idx_token",
|
||||
"title",
|
||||
},
|
||||
PartKey: []string{
|
||||
"title",
|
||||
},
|
||||
SortKey: []string{
|
||||
"idx_token",
|
||||
"id",
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// User-defined types (UDT) structs.
|
||||
type AlbumUserType struct {
|
||||
gocqlx.UDT
|
||||
Name string
|
||||
Songwriters []string
|
||||
}
|
||||
|
||||
// Table structs.
|
||||
type PlaylistsStruct struct {
|
||||
Album AlbumUserType
|
||||
Artist string
|
||||
Id [16]byte
|
||||
SongId [16]byte
|
||||
Title string
|
||||
}
|
||||
type SongsStruct struct {
|
||||
Album string
|
||||
Artist string
|
||||
Data []byte
|
||||
Duration gocql.Duration
|
||||
Id [16]byte
|
||||
Tags []string
|
||||
Title string
|
||||
}
|
||||
|
||||
// Index structs.
|
||||
type SongsTitleIndexStruct struct {
|
||||
Id [16]byte
|
||||
IdxToken int64
|
||||
Title string
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package dbutil provides various utilities built on top of core gocqlx modules.
|
||||
package dbutil
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dbutil
|
||||
|
||||
import (
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/table"
|
||||
)
|
||||
|
||||
// RewriteTable rewrites src table to dst table.
|
||||
// Rows can be transformed using the transform function.
|
||||
// If row map is empty after transformation the row is skipped.
|
||||
// Additional options can be passed to modify the insert query.
|
||||
func RewriteTable(session gocqlx.Session, dst, src *table.Table, transform func(map[string]interface{}), options ...func(q *gocqlx.Queryx)) error {
|
||||
insert := dst.InsertQuery(session)
|
||||
defer insert.Release()
|
||||
|
||||
// Apply query options
|
||||
for _, o := range options {
|
||||
o(insert)
|
||||
}
|
||||
|
||||
// Iterate over all rows and reinsert them to dst table
|
||||
iter := session.Query(src.SelectAll()).Iter()
|
||||
m := make(map[string]interface{})
|
||||
for iter.MapScan(m) {
|
||||
if transform != nil {
|
||||
transform(m)
|
||||
}
|
||||
if len(m) == 0 {
|
||||
continue // map is empty - no need to clean
|
||||
}
|
||||
if err := insert.BindMap(m).Exec(); err != nil {
|
||||
return err
|
||||
}
|
||||
m = map[string]interface{}{}
|
||||
}
|
||||
return iter.Close()
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
// 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 dbutil_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3/dbutil"
|
||||
"github.com/scylladb/gocqlx/v3/gocqlxtest"
|
||||
"github.com/scylladb/gocqlx/v3/qb"
|
||||
"github.com/scylladb/gocqlx/v3/table"
|
||||
)
|
||||
|
||||
func TestRewriteTableTTL(t *testing.T) {
|
||||
session := gocqlxtest.CreateSession(t)
|
||||
defer session.Close()
|
||||
|
||||
if err := session.ExecStmt(`CREATE TABLE gocqlx_test.rewrite_table (testtext text PRIMARY KEY)`); err != nil {
|
||||
t.Fatal("create table:", err)
|
||||
}
|
||||
|
||||
tbl := table.New(table.Metadata{
|
||||
Name: "gocqlx_test.rewrite_table",
|
||||
Columns: []string{"testtext"},
|
||||
PartKey: []string{"testtext"},
|
||||
})
|
||||
|
||||
// Insert data with 500ms TTL
|
||||
q := tbl.InsertBuilder().TTL(500 * time.Millisecond).Query(session)
|
||||
if err := q.Bind("a").Exec(); err != nil {
|
||||
t.Fatal("insert:", err)
|
||||
}
|
||||
if err := q.Bind("b").Exec(); err != nil {
|
||||
t.Fatal("insert:", err)
|
||||
}
|
||||
if err := q.Bind("c").Exec(); err != nil {
|
||||
t.Fatal("insert:", err)
|
||||
}
|
||||
|
||||
// Rewrite data without TTL
|
||||
if err := dbutil.RewriteTable(session, tbl, tbl, nil); err != nil {
|
||||
t.Fatal("rewrite:", err)
|
||||
}
|
||||
|
||||
// Wait and check if data persisted
|
||||
time.Sleep(time.Second)
|
||||
|
||||
var n int
|
||||
if err := qb.Select(tbl.Name()).CountAll().Query(session).Scan(&n); err != nil {
|
||||
t.Fatal("scan:", err)
|
||||
}
|
||||
if n != 3 {
|
||||
t.Fatal("expected 3 entries")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteTableClone(t *testing.T) {
|
||||
session := gocqlxtest.CreateSession(t)
|
||||
defer session.Close()
|
||||
|
||||
if err := session.ExecStmt(`CREATE TABLE gocqlx_test.rewrite_table_clone_src (testtext text PRIMARY KEY, testint int)`); err != nil {
|
||||
t.Fatal("create table:", err)
|
||||
}
|
||||
|
||||
src := table.New(table.Metadata{
|
||||
Name: "gocqlx_test.rewrite_table_clone_src",
|
||||
Columns: []string{"testtext", "testint"},
|
||||
PartKey: []string{"testtext"},
|
||||
})
|
||||
|
||||
if err := session.ExecStmt(`CREATE TABLE gocqlx_test.rewrite_table_clone_dst (testtext text PRIMARY KEY, testfloat float)`); err != nil {
|
||||
t.Fatal("create table:", err)
|
||||
}
|
||||
|
||||
dst := table.New(table.Metadata{
|
||||
Name: "gocqlx_test.rewrite_table_clone_dst",
|
||||
Columns: []string{"testtext", "testfloat"},
|
||||
PartKey: []string{"testtext"},
|
||||
})
|
||||
|
||||
// Insert data
|
||||
q := src.InsertBuilder().Query(session)
|
||||
if err := q.Bind("a", 1).Exec(); err != nil {
|
||||
t.Fatal("insert:", err)
|
||||
}
|
||||
if err := q.Bind("b", 2).Exec(); err != nil {
|
||||
t.Fatal("insert:", err)
|
||||
}
|
||||
if err := q.Bind("c", 3).Exec(); err != nil {
|
||||
t.Fatal("insert:", err)
|
||||
}
|
||||
|
||||
transformer := func(m map[string]interface{}) {
|
||||
m["testfloat"] = float32(m["testint"].(int))
|
||||
}
|
||||
|
||||
// Rewrite data
|
||||
if err := dbutil.RewriteTable(session, dst, src, transformer); err != nil {
|
||||
t.Fatal("rewrite:", err)
|
||||
}
|
||||
|
||||
var n int
|
||||
if err := qb.Select(dst.Name()).CountAll().Query(session).Scan(&n); err != nil {
|
||||
t.Fatal("scan:", err)
|
||||
}
|
||||
if n != 3 {
|
||||
t.Fatal("expected 3 entries")
|
||||
}
|
||||
var f float32
|
||||
if err := dst.GetQuery(session, "testfloat").Bind("a").Scan(&f); err != nil {
|
||||
t.Fatal("scan:", err)
|
||||
}
|
||||
if f != 1 {
|
||||
t.Fatal("expected 1")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
package gocqlx_test
|
||||
|
||||
import (
|
||||
"github.com/gocql/gocql"
|
||||
gocql "github.com/apache/cassandra-gocql-driver/v2"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/qb"
|
||||
|
||||
978
example_test.go
978
example_test.go
@@ -1,978 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
7
go.mod
7
go.mod
@@ -3,7 +3,7 @@ module github.com/scylladb/gocqlx/v3
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gocql/gocql v1.7.0
|
||||
github.com/apache/cassandra-gocql-driver/v2 v2.0.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/psanford/memfs v0.0.0-20241019191636-4ef911798f9b
|
||||
github.com/scylladb/go-reflectx v1.0.1
|
||||
@@ -11,9 +11,6 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
)
|
||||
require github.com/stretchr/testify v1.11.1 // indirect
|
||||
|
||||
replace github.com/gocql/gocql => github.com/scylladb/gocql v1.17.0
|
||||
|
||||
43
go.sum
43
go.sum
@@ -1,45 +1,38 @@
|
||||
github.com/bitly/go-hostpool v0.1.1 h1:SsovT4BFqgJQBAESkk2QgeeL7bqKq9oJie8JnD00R+Q=
|
||||
github.com/bitly/go-hostpool v0.1.1/go.mod h1:iwXQOF7+y3cO8vituSqGpBYf02TYTzxK4S2c4rf4cJs=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/apache/cassandra-gocql-driver/v2 v2.0.0 h1:Omnzb1Z/P90Dr2TbVNu54ICQL7TKVIIsJO231w484HU=
|
||||
github.com/apache/cassandra-gocql-driver/v2 v2.0.0/go.mod h1:QH/asJjB3mHvY6Dot6ZKMMpTcOrWJ8i9GhsvG1g0PK4=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4=
|
||||
github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/psanford/memfs v0.0.0-20241019191636-4ef911798f9b h1:xzjEJAHum+mV5Dd5KyohRlCyP03o4yq6vNpEUtAJQzI=
|
||||
github.com/psanford/memfs v0.0.0-20241019191636-4ef911798f9b/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ=
|
||||
github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc=
|
||||
github.com/scylladb/gocql v1.16.1 h1:mxqUoOoHPrhzBNN+S0X195N+wCPZ5nrstfFz4QtBaZs=
|
||||
github.com/scylladb/gocql v1.16.1/go.mod h1:MSg2nr90XMcU0doVnISX3OtarTac5tSCGk6Q6QJd6oQ=
|
||||
github.com/scylladb/gocql v1.17.0 h1:sSjNTgSoC90+1XYXOMeWsQ8+AZbFYQWcspuScmUT53E=
|
||||
github.com/scylladb/gocql v1.17.0/go.mod h1:0VgVuYnAPOoYN17KXkYdWDxhL2/rH3V3vOisPMngpAw=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
gocql "github.com/apache/cassandra-gocql-driver/v2"
|
||||
"github.com/scylladb/go-reflectx"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package gocqlxtest provides test helpers for integration tests.
|
||||
package gocqlxtest
|
||||
@@ -1,121 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gocqlxtest
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
flagCluster = flag.String("cluster", "127.0.0.1", "a comma-separated list of host:port tuples")
|
||||
flagKeyspace = flag.String("keyspace", "gocqlx_test", "keyspace name")
|
||||
flagProto = flag.Int("proto", 0, "protcol version")
|
||||
flagCQL = flag.String("cql", "3.0.0", "CQL version")
|
||||
flagRF = flag.Int("rf", 1, "replication factor for test keyspace")
|
||||
flagRetry = flag.Int("retries", 5, "number of times to retry queries")
|
||||
flagCompressTest = flag.String("compressor", "", "compressor to use")
|
||||
flagTimeout = flag.Duration("gocql.timeout", 5*time.Second, "sets the connection `timeout` for all operations")
|
||||
)
|
||||
|
||||
var initOnce sync.Once
|
||||
|
||||
// CreateSession creates a new gocqlx session from flags.
|
||||
func CreateSession(tb testing.TB) gocqlx.Session {
|
||||
tb.Helper()
|
||||
|
||||
cluster := CreateCluster()
|
||||
return createSessionFromCluster(tb, cluster)
|
||||
}
|
||||
|
||||
// CreateCluster creates gocql ClusterConfig from flags.
|
||||
func CreateCluster() *gocql.ClusterConfig {
|
||||
if !flag.Parsed() {
|
||||
flag.Parse()
|
||||
}
|
||||
clusterHosts := strings.Split(*flagCluster, ",")
|
||||
|
||||
cluster := gocql.NewCluster(clusterHosts...)
|
||||
cluster.ProtoVersion = *flagProto
|
||||
cluster.CQLVersion = *flagCQL
|
||||
cluster.Timeout = *flagTimeout
|
||||
cluster.Consistency = gocql.Quorum
|
||||
cluster.MaxWaitSchemaAgreement = 2 * time.Minute // travis might be slow
|
||||
cluster.ReconnectionPolicy = &gocql.ConstantReconnectionPolicy{
|
||||
MaxRetries: 10,
|
||||
Interval: 3 * time.Second,
|
||||
}
|
||||
if *flagRetry > 0 {
|
||||
cluster.RetryPolicy = &gocql.SimpleRetryPolicy{NumRetries: *flagRetry}
|
||||
}
|
||||
|
||||
switch *flagCompressTest {
|
||||
case "snappy":
|
||||
cluster.Compressor = &gocql.SnappyCompressor{}
|
||||
case "":
|
||||
default:
|
||||
panic("invalid compressor: " + *flagCompressTest)
|
||||
}
|
||||
|
||||
return cluster
|
||||
}
|
||||
|
||||
// CreateKeyspace creates keyspace with SimpleStrategy and RF derived from flags.
|
||||
func CreateKeyspace(cluster *gocql.ClusterConfig, keyspace string) error {
|
||||
c := *cluster
|
||||
c.Keyspace = "system"
|
||||
c.Timeout = 30 * time.Second
|
||||
|
||||
session, err := gocqlx.WrapSession(c.CreateSession())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
{
|
||||
err := session.ExecStmt(`DROP KEYSPACE IF EXISTS ` + keyspace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("drop keyspace: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err := session.ExecStmt(fmt.Sprintf(`CREATE KEYSPACE %s WITH replication = {'class' : 'SimpleStrategy', 'replication_factor' : %d}`, keyspace, *flagRF))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create keyspace: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSessionFromCluster(tb testing.TB, cluster *gocql.ClusterConfig) gocqlx.Session {
|
||||
tb.Helper()
|
||||
if !flag.Parsed() {
|
||||
flag.Parse()
|
||||
}
|
||||
// Drop and re-create the keyspace once. Different tests should use their own
|
||||
// individual tables, but can assume that the table does not exist before.
|
||||
initOnce.Do(func() {
|
||||
if err := CreateKeyspace(cluster, *flagKeyspace); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
cluster.Keyspace = *flagKeyspace
|
||||
session, err := gocqlx.WrapSession(cluster.CreateSession())
|
||||
if err != nil {
|
||||
tb.Fatal("CreateSession:", err)
|
||||
}
|
||||
return session
|
||||
}
|
||||
2
iterx.go
2
iterx.go
@@ -9,7 +9,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
gocql "github.com/apache/cassandra-gocql-driver/v2"
|
||||
"github.com/scylladb/go-reflectx"
|
||||
)
|
||||
|
||||
|
||||
1015
iterx_test.go
1015
iterx_test.go
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
# 🚀 GocqlX Migrations
|
||||
|
||||
`migrate` reads migrations from a flat directory containing CQL files.
|
||||
There is no imposed naming schema. Migration name is file name.
|
||||
The order of migrations is the lexicographical order of file names in the directory.
|
||||
You can inject execution of Go code before processing of a migration file, after processing of a migration file, or between statements in a migration file.
|
||||
|
||||
For details see [example](example) migration.
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
)
|
||||
|
||||
// CallbackEvent specifies type of the event when calling CallbackFunc.
|
||||
type CallbackEvent uint8
|
||||
|
||||
// enumeration of CallbackEvents
|
||||
const (
|
||||
BeforeMigration CallbackEvent = iota
|
||||
AfterMigration
|
||||
CallComment
|
||||
)
|
||||
|
||||
// CallbackFunc enables execution of arbitrary Go code during migration.
|
||||
// If error is returned the migration is aborted.
|
||||
// BeforeMigration and AfterMigration are triggered before and after processing
|
||||
// of each migration file respectively.
|
||||
// CallComment is triggered for each comment in a form `-- CALL <name>;` (note the semicolon).
|
||||
type CallbackFunc func(ctx context.Context, session gocqlx.Session, ev CallbackEvent, name string) error
|
||||
|
||||
// Callback is means of executing Go code during migrations.
|
||||
// Use this variable to register a global callback dispatching function.
|
||||
// See CallbackFunc for details.
|
||||
var Callback CallbackFunc
|
||||
|
||||
type nameEvent struct {
|
||||
name string
|
||||
event CallbackEvent
|
||||
}
|
||||
|
||||
// CallbackRegister allows to register a handlers for an event type and a name.
|
||||
// It dispatches calls to the registered handlers.
|
||||
// If there is no handler registered for CallComment an error is returned.
|
||||
type CallbackRegister map[nameEvent]CallbackFunc
|
||||
|
||||
// Add registers a callback handler.
|
||||
func (r CallbackRegister) Add(ev CallbackEvent, name string, f CallbackFunc) {
|
||||
r[nameEvent{name, ev}] = f
|
||||
}
|
||||
|
||||
// Find returns the registered handler.
|
||||
func (r CallbackRegister) Find(ev CallbackEvent, name string) CallbackFunc {
|
||||
return r[nameEvent{name, ev}]
|
||||
}
|
||||
|
||||
// Callback is CallbackFunc.
|
||||
func (r CallbackRegister) Callback(ctx context.Context, session gocqlx.Session, ev CallbackEvent, name string) error {
|
||||
f, ok := r[nameEvent{name, ev}]
|
||||
if !ok {
|
||||
if ev == CallComment {
|
||||
return errors.New("missing handler")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return f(ctx, session, ev, name)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
var encode = hex.EncodeToString
|
||||
|
||||
func checksum(b []byte) string {
|
||||
v := md5.Sum(b)
|
||||
return encode(v[:])
|
||||
}
|
||||
|
||||
func fileChecksum(f fs.FS, path string) (string, error) {
|
||||
file, err := f.Open(path)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
v := h.Sum(nil)
|
||||
return encode(v), nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileChecksum(t *testing.T) {
|
||||
c, err := fileChecksum(os.DirFS("testdata"), "file")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c != "bbe02f946d5455d74616fc9777557c22" {
|
||||
t.Fatal(c)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package migrate reads migrations from a flat directory containing CQL files.
|
||||
// There is no imposed naming schema. Migration name is file name.
|
||||
// The order of migrations is the lexicographical order of file names in the directory.
|
||||
// You can inject execution of Go code before processing of a migration file, after processing of a migration file, or between statements in a migration file.
|
||||
package migrate
|
||||
@@ -1,15 +0,0 @@
|
||||
// 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 cql
|
||||
|
||||
import "embed"
|
||||
|
||||
// Files contains *.cql schema migration files.
|
||||
//
|
||||
//go:embed *.cql
|
||||
var Files embed.FS
|
||||
@@ -1,15 +0,0 @@
|
||||
-- Comment
|
||||
|
||||
CREATE TABLE bar ( id int PRIMARY KEY);
|
||||
|
||||
INSERT INTO bar (id) VALUES (1);
|
||||
|
||||
-- CALL 1;
|
||||
|
||||
INSERT INTO bar (id) VALUES (2);
|
||||
|
||||
-- CALL 2;
|
||||
|
||||
INSERT INTO bar (id) VALUES (3);
|
||||
|
||||
-- CALL 3;
|
||||
@@ -1,68 +0,0 @@
|
||||
// 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 example
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/gocqlxtest"
|
||||
"github.com/scylladb/gocqlx/v3/migrate"
|
||||
"github.com/scylladb/gocqlx/v3/migrate/example/cql"
|
||||
)
|
||||
|
||||
// Running examples locally:
|
||||
// make start-scylla
|
||||
// make run-examples
|
||||
func TestExample(t *testing.T) {
|
||||
const ks = "migrate_example"
|
||||
|
||||
// Create keyspace
|
||||
cluster := gocqlxtest.CreateCluster()
|
||||
cluster.Keyspace = ks
|
||||
if err := gocqlxtest.CreateKeyspace(cluster, ks); err != nil {
|
||||
t.Fatal("CreateKeyspace:", err)
|
||||
}
|
||||
|
||||
// Create session using the keyspace
|
||||
session, err := gocqlx.WrapSession(cluster.CreateSession())
|
||||
if err != nil {
|
||||
t.Fatal("CreateSession:", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Add callback prints
|
||||
log := func(ctx context.Context, session gocqlx.Session, ev migrate.CallbackEvent, name string) error {
|
||||
t.Log(ev, name)
|
||||
return nil
|
||||
}
|
||||
reg := migrate.CallbackRegister{}
|
||||
reg.Add(migrate.BeforeMigration, "m1.cql", log)
|
||||
reg.Add(migrate.AfterMigration, "m1.cql", log)
|
||||
reg.Add(migrate.CallComment, "1", log)
|
||||
reg.Add(migrate.CallComment, "2", log)
|
||||
reg.Add(migrate.CallComment, "3", log)
|
||||
migrate.Callback = reg.Callback
|
||||
|
||||
pending, err := migrate.Pending(context.Background(), session, cql.Files)
|
||||
if err != nil {
|
||||
t.Fatal("Pending:", err)
|
||||
}
|
||||
t.Log("Pending migrations:", len(pending))
|
||||
|
||||
// First run prints data
|
||||
if err := migrate.FromFS(context.Background(), session, cql.Files); err != nil {
|
||||
t.Fatal("Migrate:", err)
|
||||
}
|
||||
|
||||
// Second run skips the processed files
|
||||
if err := migrate.FromFS(context.Background(), session, cql.Files); err != nil {
|
||||
t.Fatal("Migrate:", err)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package migrate
|
||||
|
||||
func IsCallback(stmt string) (name string) {
|
||||
return isCallback(stmt)
|
||||
}
|
||||
|
||||
func IsComment(stmt string) bool {
|
||||
return isComment(stmt)
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/qb"
|
||||
)
|
||||
|
||||
// DefaultAwaitSchemaAgreement controls whether checking for cluster schema agreement
|
||||
// is disabled or if it is checked before each file or statement is applied.
|
||||
// The default is not checking before each file or statement but only once after every
|
||||
// migration has been run.
|
||||
var DefaultAwaitSchemaAgreement = AwaitSchemaAgreementDisabled
|
||||
|
||||
type awaitSchemaAgreement int
|
||||
|
||||
// Options for checking schema agreement.
|
||||
const (
|
||||
AwaitSchemaAgreementDisabled awaitSchemaAgreement = iota
|
||||
AwaitSchemaAgreementBeforeEachFile
|
||||
AwaitSchemaAgreementBeforeEachStatement
|
||||
)
|
||||
|
||||
// ShouldAwait decides whether to await schema agreement for the configured DefaultAwaitSchemaAgreement option above.
|
||||
func (as awaitSchemaAgreement) ShouldAwait(stage awaitSchemaAgreement) bool {
|
||||
return as == stage
|
||||
}
|
||||
|
||||
const (
|
||||
infoSchema = `CREATE TABLE IF NOT EXISTS gocqlx_migrate (
|
||||
name text,
|
||||
checksum text,
|
||||
done int,
|
||||
start_time timestamp,
|
||||
end_time timestamp,
|
||||
PRIMARY KEY(name)
|
||||
)`
|
||||
selectInfo = "SELECT * FROM gocqlx_migrate"
|
||||
)
|
||||
|
||||
// Info contains information on migration applied on a database.
|
||||
type Info struct {
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Name string
|
||||
Checksum string
|
||||
Done int
|
||||
}
|
||||
|
||||
// List provides a listing of applied migrations.
|
||||
func List(ctx context.Context, session gocqlx.Session) ([]*Info, error) {
|
||||
if err := ensureInfoTable(ctx, session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := session.ContextQuery(ctx, selectInfo, nil)
|
||||
|
||||
var v []*Info
|
||||
if err := q.SelectRelease(&v); err == gocql.ErrNotFound {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return v, err
|
||||
}
|
||||
|
||||
sort.Slice(v, func(i, j int) bool {
|
||||
return v[i].Name < v[j].Name
|
||||
})
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Pending provides a listing of pending migrations.
|
||||
func Pending(ctx context.Context, session gocqlx.Session, f fs.FS) ([]*Info, error) {
|
||||
applied, err := List(ctx, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a set of applied migration names
|
||||
appliedNames := make(map[string]struct{}, len(applied))
|
||||
for _, migration := range applied {
|
||||
appliedNames[migration.Name] = struct{}{}
|
||||
}
|
||||
|
||||
fm, err := fs.Glob(f, "*.cql")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list migrations: %w", err)
|
||||
}
|
||||
|
||||
pending := make([]*Info, 0)
|
||||
|
||||
for _, name := range fm {
|
||||
baseName := filepath.Base(name)
|
||||
// Check if the migration is not in the applied set
|
||||
if _, exists := appliedNames[baseName]; !exists {
|
||||
c, err := fileChecksum(f, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calculate checksum for %q: %w", name, err)
|
||||
}
|
||||
|
||||
info := &Info{
|
||||
Name: baseName,
|
||||
StartTime: time.Now(),
|
||||
Checksum: c,
|
||||
}
|
||||
|
||||
pending = append(pending, info)
|
||||
}
|
||||
}
|
||||
|
||||
return pending, nil
|
||||
}
|
||||
|
||||
func ensureInfoTable(ctx context.Context, session gocqlx.Session) error {
|
||||
return session.ContextQuery(ctx, infoSchema, nil).ExecRelease()
|
||||
}
|
||||
|
||||
// Migrate is a wrapper around FromFS.
|
||||
// It executes migrations from a directory on disk.
|
||||
//
|
||||
// Deprecated: use FromFS instead
|
||||
func Migrate(ctx context.Context, session gocqlx.Session, dir string) error {
|
||||
return FromFS(ctx, session, os.DirFS(dir))
|
||||
}
|
||||
|
||||
// FromFS executes new CQL files from a file system abstraction (io/fs.FS).
|
||||
// The provided FS has to be a flat directory containing *.cql files.
|
||||
//
|
||||
// It supports code based migrations, see Callback and CallbackFunc.
|
||||
// Any comment in form `-- CALL <name>;` will trigger an CallComment callback.
|
||||
func FromFS(ctx context.Context, session gocqlx.Session, f fs.FS) error {
|
||||
// get database migrations
|
||||
dbm, err := List(ctx, session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list migrations: %s", err)
|
||||
}
|
||||
|
||||
// get file migrations
|
||||
fm, err := fs.Glob(f, "*.cql")
|
||||
if err != nil {
|
||||
return fmt.Errorf("list migrations: %w", err)
|
||||
}
|
||||
if len(fm) == 0 {
|
||||
return fmt.Errorf("no migration files found")
|
||||
}
|
||||
sort.Strings(fm)
|
||||
|
||||
// verify migrations
|
||||
if len(dbm) > len(fm) {
|
||||
return fmt.Errorf("database is ahead")
|
||||
}
|
||||
|
||||
for i := 0; i < len(dbm); i++ {
|
||||
if dbm[i].Name != fm[i] {
|
||||
return fmt.Errorf("inconsistent migrations found, expected %q got %q at %d", dbm[i].Name, fm[i], i)
|
||||
}
|
||||
c, err := fileChecksum(f, fm[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("calculate checksum for %q: %s", fm[i], err)
|
||||
}
|
||||
if dbm[i].Checksum != c {
|
||||
return fmt.Errorf("file %q was tampered with, expected md5 %s", fm[i], dbm[i].Checksum)
|
||||
}
|
||||
}
|
||||
|
||||
// apply migrations
|
||||
if len(dbm) > 0 {
|
||||
last := len(dbm) - 1
|
||||
if err := applyMigration(ctx, session, f, fm[last], dbm[last].Done); err != nil {
|
||||
return fmt.Errorf("apply migration %q: %s", fm[last], err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := len(dbm); i < len(fm); i++ {
|
||||
if err := applyMigration(ctx, session, f, fm[i], 0); err != nil {
|
||||
return fmt.Errorf("apply migration %q: %s", fm[i], err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = session.AwaitSchemaAgreement(ctx); err != nil {
|
||||
return fmt.Errorf("awaiting schema agreement: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyMigration executes a single migration file by parsing and applying its statements.
|
||||
// It handles three types of content in migration files:
|
||||
// - SQL statements: executed against the database
|
||||
// - Callback commands: processed via registered callback handlers (format: -- CALL function_name;)
|
||||
// - Regular comments: silently skipped (format: -- any comment text)
|
||||
//
|
||||
// The function maintains migration state by tracking the number of completed statements,
|
||||
// allowing for resumption of partially completed migrations.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: context for cancellation and timeouts
|
||||
// - session: database session for executing statements
|
||||
// - f: filesystem containing the migration file
|
||||
// - path: path to the migration file within the filesystem
|
||||
// - done: number of statements already completed (for resuming partial migrations)
|
||||
//
|
||||
// Returns an error if the migration fails at any point.
|
||||
func applyMigration(ctx context.Context, session gocqlx.Session, f fs.FS, path string, done int) error {
|
||||
file, err := f.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(file)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info := Info{
|
||||
Name: filepath.Base(path),
|
||||
StartTime: time.Now(),
|
||||
Checksum: checksum(b),
|
||||
}
|
||||
|
||||
stmt, names := qb.Insert("gocqlx_migrate").Columns(
|
||||
"name",
|
||||
"checksum",
|
||||
"done",
|
||||
"start_time",
|
||||
"end_time",
|
||||
).ToCql()
|
||||
|
||||
update := session.ContextQuery(ctx, stmt, names)
|
||||
defer update.Release()
|
||||
|
||||
if DefaultAwaitSchemaAgreement.ShouldAwait(AwaitSchemaAgreementBeforeEachFile) {
|
||||
if err = session.AwaitSchemaAgreement(ctx); err != nil {
|
||||
return fmt.Errorf("awaiting schema agreement: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
i := 0
|
||||
r := bytes.NewBuffer(b)
|
||||
for {
|
||||
stmt, err := r.ReadString(';')
|
||||
if err == io.EOF {
|
||||
if strings.TrimSpace(stmt) != "" {
|
||||
// handle missing semicolon after last statement
|
||||
err = nil
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i++
|
||||
|
||||
if i <= done {
|
||||
continue
|
||||
}
|
||||
|
||||
if Callback != nil && i == 1 {
|
||||
if err := Callback(ctx, session, BeforeMigration, info.Name); err != nil {
|
||||
return fmt.Errorf("before migration callback: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if DefaultAwaitSchemaAgreement.ShouldAwait(AwaitSchemaAgreementBeforeEachStatement) {
|
||||
if err = session.AwaitSchemaAgreement(ctx); err != nil {
|
||||
return fmt.Errorf("awaiting schema agreement: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// trim new lines and all whitespace characters
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
|
||||
// Process statement based on its type
|
||||
if cb := isCallback(stmt); cb != "" {
|
||||
// Handle callback commands (e.g., "-- CALL function_name;")
|
||||
if Callback == nil {
|
||||
return fmt.Errorf("statement %d: missing callback handler while trying to call %s", i, cb)
|
||||
}
|
||||
if err := Callback(ctx, session, CallComment, cb); err != nil {
|
||||
return fmt.Errorf("callback %s: %s", cb, err)
|
||||
}
|
||||
} else if stmt != "" && !isComment(stmt) {
|
||||
// Execute SQL statements (skip empty statements and comments)
|
||||
q := session.ContextQuery(ctx, stmt, nil).RetryPolicy(nil)
|
||||
if err := q.ExecRelease(); err != nil {
|
||||
return fmt.Errorf("statement %d: %s", i, err)
|
||||
}
|
||||
}
|
||||
// Regular comments and empty statements are silently skipped
|
||||
|
||||
// update info
|
||||
info.Done = i
|
||||
info.EndTime = time.Now()
|
||||
if err := update.BindStruct(info).Exec(); err != nil {
|
||||
return fmt.Errorf("migration statement %d: %s", i, err)
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
return fmt.Errorf("no migration statements found in %q", info.Name)
|
||||
}
|
||||
|
||||
if Callback != nil && i > done {
|
||||
if err := Callback(ctx, session, AfterMigration, info.Name); err != nil {
|
||||
return fmt.Errorf("after migration callback: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var cbRegexp = regexp.MustCompile("^-- *CALL +(.+);$")
|
||||
|
||||
func isCallback(stmt string) (name string) {
|
||||
s := cbRegexp.FindStringSubmatch(stmt)
|
||||
if len(s) == 0 {
|
||||
return ""
|
||||
}
|
||||
return s[1]
|
||||
}
|
||||
|
||||
// isComment returns true if the statement is a SQL comment that should be ignored.
|
||||
// It distinguishes between regular comments (which should be skipped) and
|
||||
// callback commands (which should be processed).
|
||||
func isComment(stmt string) bool {
|
||||
return strings.HasPrefix(stmt, "--") && isCallback(stmt) == ""
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
// 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 migrate_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/psanford/memfs"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
"github.com/scylladb/gocqlx/v3/gocqlxtest"
|
||||
"github.com/scylladb/gocqlx/v3/migrate"
|
||||
)
|
||||
|
||||
var migrateSchema = `
|
||||
CREATE TABLE IF NOT EXISTS gocqlx_test.migrate_table (
|
||||
testint int,
|
||||
testuuid timeuuid,
|
||||
PRIMARY KEY(testint, testuuid)
|
||||
)
|
||||
`
|
||||
|
||||
var insertMigrate = `INSERT INTO gocqlx_test.migrate_table (testint, testuuid) VALUES (%d, now())`
|
||||
|
||||
func recreateTables(tb testing.TB, session gocqlx.Session) {
|
||||
tb.Helper()
|
||||
|
||||
if err := session.ExecStmt("DROP TABLE IF EXISTS gocqlx_test.gocqlx_migrate"); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
if err := session.ExecStmt(migrateSchema); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
if err := session.ExecStmt("TRUNCATE gocqlx_test.migrate_table"); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPending(t *testing.T) {
|
||||
session := gocqlxtest.CreateSession(t)
|
||||
defer session.Close()
|
||||
recreateTables(t, session)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("pending", func(t *testing.T) {
|
||||
defer recreateTables(t, session)
|
||||
|
||||
f := memfs.New()
|
||||
writeFile(t, f, 0, fmt.Sprintf(insertMigrate, 0)+";")
|
||||
|
||||
pending, err := migrate.Pending(ctx, session, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pending) != 1 {
|
||||
t.Fatal("expected 2 pending migrations got", len(pending))
|
||||
}
|
||||
|
||||
err = migrate.FromFS(ctx, session, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pending, err = migrate.Pending(ctx, session, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pending) != 0 {
|
||||
t.Fatal("expected no pending migrations got", len(pending))
|
||||
}
|
||||
|
||||
for i := 1; i < 3; i++ {
|
||||
writeFile(t, f, i, fmt.Sprintf(insertMigrate, i)+";")
|
||||
}
|
||||
|
||||
pending, err = migrate.Pending(ctx, session, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pending) != 2 {
|
||||
t.Fatal("expected 2 pending migrations got", len(pending))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigration(t *testing.T) {
|
||||
session := gocqlxtest.CreateSession(t)
|
||||
defer session.Close()
|
||||
recreateTables(t, session)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("init", func(t *testing.T) {
|
||||
if err := migrate.FromFS(ctx, session, makeTestFS(t, 2)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c := countMigrations(t, session); c != 2 {
|
||||
t.Fatal("expected 2 migration got", c)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
if err := migrate.FromFS(ctx, session, makeTestFS(t, 4)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c := countMigrations(t, session); c != 4 {
|
||||
t.Fatal("expected 4 migration got", c)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ahead", func(t *testing.T) {
|
||||
err := migrate.FromFS(ctx, session, makeTestFS(t, 2))
|
||||
if err == nil || !strings.Contains(err.Error(), "ahead") {
|
||||
t.Fatal("expected error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tempered with file", func(t *testing.T) {
|
||||
f := makeTestFS(t, 4)
|
||||
writeFile(t, f, 3, "SELECT * FROM bla;")
|
||||
|
||||
if err := migrate.FromFS(ctx, session, f); err == nil || !strings.Contains(err.Error(), "tampered") {
|
||||
t.Fatal("expected error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrationNoSemicolon(t *testing.T) {
|
||||
session := gocqlxtest.CreateSession(t)
|
||||
defer session.Close()
|
||||
recreateTables(t, session)
|
||||
|
||||
if err := session.ExecStmt(migrateSchema); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f := makeTestFS(t, 0)
|
||||
err := f.WriteFile("0.cql", []byte(fmt.Sprintf(insertMigrate, 0)+";"+fmt.Sprintf(insertMigrate, 1)), fs.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := migrate.FromFS(ctx, session, f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c := countMigrations(t, session); c != 2 {
|
||||
t.Fatal("expected 2 migration got", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrationWithTrailingComment(t *testing.T) {
|
||||
session := gocqlxtest.CreateSession(t)
|
||||
defer session.Close()
|
||||
recreateTables(t, session)
|
||||
|
||||
if err := session.ExecStmt(migrateSchema); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f := makeTestFS(t, 0)
|
||||
// Create a migration with a trailing comment (this should reproduce the issue)
|
||||
migrationContent := fmt.Sprintf(insertMigrate, 0) + "; -- ttl 1 hour"
|
||||
err := f.WriteFile("0.cql", []byte(migrationContent), fs.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := migrate.FromFS(ctx, session, f); err != nil {
|
||||
t.Fatal("Migration should succeed with trailing comment, but got error:", err)
|
||||
}
|
||||
if c := countMigrations(t, session); c != 1 {
|
||||
t.Fatal("expected 1 migration got", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCallback(t *testing.T) {
|
||||
table := []struct {
|
||||
Name string
|
||||
Stmt string
|
||||
Cb string
|
||||
}{
|
||||
{
|
||||
Name: "CQL statement",
|
||||
Stmt: "SELECT * from X;",
|
||||
},
|
||||
{
|
||||
Name: "CQL comment",
|
||||
Stmt: "-- Item",
|
||||
},
|
||||
{
|
||||
Name: "CALL without space",
|
||||
Stmt: "--CALL Foo;",
|
||||
Cb: "Foo",
|
||||
},
|
||||
{
|
||||
Name: "CALL with space",
|
||||
Stmt: "-- CALL Foo;",
|
||||
Cb: "Foo",
|
||||
},
|
||||
{
|
||||
Name: "CALL with many spaces",
|
||||
Stmt: "-- CALL Foo;",
|
||||
Cb: "Foo",
|
||||
},
|
||||
{
|
||||
Name: "CALL with many spaces 2",
|
||||
Stmt: "-- CALL Foo;",
|
||||
Cb: "Foo",
|
||||
},
|
||||
{
|
||||
Name: "CALL with unicode",
|
||||
Stmt: "-- CALL α;",
|
||||
Cb: "α",
|
||||
},
|
||||
}
|
||||
|
||||
for i := range table {
|
||||
test := table[i]
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
if migrate.IsCallback(test.Stmt) != test.Cb {
|
||||
t.Errorf("IsCallback(%s)=%s, expected %s", test.Stmt, migrate.IsCallback(test.Stmt), test.Cb)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsComment(t *testing.T) {
|
||||
table := []struct {
|
||||
Name string
|
||||
Stmt string
|
||||
IsComment bool
|
||||
}{
|
||||
{
|
||||
Name: "CQL statement",
|
||||
Stmt: "SELECT * from X;",
|
||||
IsComment: false,
|
||||
},
|
||||
{
|
||||
Name: "Regular comment",
|
||||
Stmt: "-- This is a comment",
|
||||
IsComment: true,
|
||||
},
|
||||
{
|
||||
Name: "Comment with additional text",
|
||||
Stmt: "-- ttl 1 hour",
|
||||
IsComment: true,
|
||||
},
|
||||
{
|
||||
Name: "Callback command (not a regular comment)",
|
||||
Stmt: "-- CALL Foo;",
|
||||
IsComment: false,
|
||||
},
|
||||
{
|
||||
Name: "Callback with spaces (not a regular comment)",
|
||||
Stmt: "-- CALL Bar;",
|
||||
IsComment: false,
|
||||
},
|
||||
{
|
||||
Name: "Empty statement",
|
||||
Stmt: "",
|
||||
IsComment: false,
|
||||
},
|
||||
{
|
||||
Name: "Whitespace only",
|
||||
Stmt: " ",
|
||||
IsComment: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range table {
|
||||
test := table[i]
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
result := migrate.IsComment(test.Stmt)
|
||||
if result != test.IsComment {
|
||||
t.Errorf("IsComment(%q) = %v, expected %v", test.Stmt, result, test.IsComment)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrationCallback(t *testing.T) {
|
||||
var (
|
||||
beforeCalled int
|
||||
afterCalled int
|
||||
inCalled int
|
||||
)
|
||||
migrate.Callback = func(ctx context.Context, session gocqlx.Session, ev migrate.CallbackEvent, name string) error {
|
||||
switch ev {
|
||||
case migrate.BeforeMigration:
|
||||
beforeCalled++
|
||||
case migrate.AfterMigration:
|
||||
afterCalled++
|
||||
case migrate.CallComment:
|
||||
inCalled++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
migrate.Callback = nil
|
||||
}()
|
||||
|
||||
reset := func() {
|
||||
beforeCalled = 0
|
||||
afterCalled = 0
|
||||
inCalled = 0
|
||||
}
|
||||
|
||||
assertCallbacks := func(t *testing.T, before, afer, in int) {
|
||||
t.Helper()
|
||||
|
||||
if beforeCalled != before {
|
||||
t.Fatalf("expected %d before calls got %d", before, beforeCalled)
|
||||
}
|
||||
if afterCalled != afer {
|
||||
t.Fatalf("expected %d after calls got %d", afer, afterCalled)
|
||||
}
|
||||
if inCalled != in {
|
||||
t.Fatalf("expected %d in calls got %d", in, inCalled)
|
||||
}
|
||||
}
|
||||
|
||||
session := gocqlxtest.CreateSession(t)
|
||||
defer session.Close()
|
||||
recreateTables(t, session)
|
||||
|
||||
if err := session.ExecStmt(migrateSchema); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("init", func(t *testing.T) {
|
||||
f := makeTestFS(t, 2)
|
||||
reset()
|
||||
|
||||
if err := migrate.FromFS(ctx, session, f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCallbacks(t, 2, 2, 0)
|
||||
})
|
||||
|
||||
t.Run("no duplicate calls", func(t *testing.T) {
|
||||
f := makeTestFS(t, 4)
|
||||
reset()
|
||||
|
||||
if err := migrate.FromFS(ctx, session, f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCallbacks(t, 2, 2, 0)
|
||||
})
|
||||
|
||||
t.Run("in calls", func(t *testing.T) {
|
||||
f := makeTestFS(t, 4)
|
||||
writeFile(t, f, 4, "\n-- CALL Foo;\n")
|
||||
writeFile(t, f, 5, "\n-- CALL Bar;\n")
|
||||
reset()
|
||||
|
||||
if err := migrate.FromFS(ctx, session, f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCallbacks(t, 2, 2, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func countMigrations(tb testing.TB, session gocqlx.Session) int {
|
||||
tb.Helper()
|
||||
|
||||
var v int
|
||||
if err := session.Query("SELECT COUNT(*) FROM gocqlx_test.migrate_table", nil).Get(&v); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func makeTestFS(tb testing.TB, n int) *memfs.FS {
|
||||
tb.Helper()
|
||||
f := memfs.New()
|
||||
for i := 0; i < n; i++ {
|
||||
writeFile(tb, f, i, fmt.Sprintf(insertMigrate, i)+";")
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func writeFile(tb testing.TB, f *memfs.FS, i int, text string) {
|
||||
tb.Helper()
|
||||
err := f.WriteFile(fmt.Sprint(i, ".cql"), []byte(text), fs.ModePerm)
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
}
|
||||
1
migrate/testdata/file
vendored
1
migrate/testdata/file
vendored
@@ -1 +0,0 @@
|
||||
file
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkBatchBuilder(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Batch().Add(mockBuilder{"INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) ", []string{"id", "user_uuid", "firstname"}}).ToCql()
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
type mockBuilder struct {
|
||||
stmt string
|
||||
names []string
|
||||
}
|
||||
|
||||
func (b mockBuilder) ToCql() (stmt string, names []string) {
|
||||
return b.stmt, b.names
|
||||
}
|
||||
|
||||
func TestBatchBuilder(t *testing.T) {
|
||||
m := mockBuilder{"INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) ", []string{"id", "user_uuid", "firstname"}}
|
||||
|
||||
table := []struct {
|
||||
B *BatchBuilder
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
// Basic test for Batch
|
||||
{
|
||||
B: Batch().Add(m),
|
||||
S: "BEGIN BATCH INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) ; APPLY BATCH ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
// Add statement
|
||||
{
|
||||
B: Batch().
|
||||
AddWithPrefix("a", m).
|
||||
AddWithPrefix("b", m),
|
||||
S: "BEGIN BATCH INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) ; " +
|
||||
"INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) ; APPLY BATCH ",
|
||||
N: []string{"a.id", "a.user_uuid", "a.firstname", "b.id", "b.user_uuid", "b.firstname"},
|
||||
},
|
||||
// Add UNLOGGED
|
||||
{
|
||||
B: Batch().UnLogged(),
|
||||
S: "BEGIN UNLOGGED BATCH APPLY BATCH ",
|
||||
},
|
||||
// Add COUNTER
|
||||
{
|
||||
B: Batch().Counter(),
|
||||
S: "BEGIN COUNTER BATCH APPLY BATCH ",
|
||||
},
|
||||
// Add TTL
|
||||
{
|
||||
B: Batch().TTL(time.Second),
|
||||
S: "BEGIN BATCH USING TTL 1 APPLY BATCH ",
|
||||
},
|
||||
{
|
||||
B: Batch().TTLNamed("ttl"),
|
||||
S: "BEGIN BATCH USING TTL ? APPLY BATCH ",
|
||||
N: []string{"ttl"},
|
||||
},
|
||||
// Add TIMESTAMP
|
||||
{
|
||||
B: Batch().Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "BEGIN BATCH USING TIMESTAMP 1115251200000000 APPLY BATCH ",
|
||||
},
|
||||
{
|
||||
B: Batch().TimestampNamed("ts"),
|
||||
S: "BEGIN BATCH USING TIMESTAMP ? APPLY BATCH ",
|
||||
N: []string{"ts"},
|
||||
},
|
||||
// Add TIMEOUT
|
||||
{
|
||||
B: Batch().Timeout(time.Second),
|
||||
S: "BEGIN BATCH USING TIMEOUT 1s APPLY BATCH ",
|
||||
},
|
||||
{
|
||||
B: Batch().TimeoutNamed("to"),
|
||||
S: "BEGIN BATCH USING TIMEOUT ? APPLY BATCH ",
|
||||
N: []string{"to"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := test.B.ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkCmp(b *testing.B) {
|
||||
buf := bytes.Buffer{}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
c := cmps{
|
||||
Eq("id"),
|
||||
Lt("user_uuid"),
|
||||
LtOrEq("firstname"),
|
||||
Gt("stars"),
|
||||
}
|
||||
c.writeCql(&buf)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkDeleteBuilder(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Delete("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars").Where(Eq("id")).ToCql()
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestDeleteBuilder(t *testing.T) {
|
||||
w := EqNamed("id", "expr")
|
||||
|
||||
table := []struct {
|
||||
B *DeleteBuilder
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
// Basic test for delete
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w),
|
||||
S: "DELETE FROM cycling.cyclist_name WHERE id=? ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Change table name
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w).From("Foobar"),
|
||||
S: "DELETE FROM Foobar WHERE id=? ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add column
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w).Columns("stars"),
|
||||
S: "DELETE stars FROM cycling.cyclist_name WHERE id=? ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add WHERE
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w, Gt("firstname")),
|
||||
S: "DELETE FROM cycling.cyclist_name WHERE id=? AND firstname>? ",
|
||||
N: []string{"expr", "firstname"},
|
||||
},
|
||||
// Add a tuple column
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(EqTuple("id", 2)).Columns("stars"),
|
||||
S: "DELETE stars FROM cycling.cyclist_name WHERE id=(?,?) ",
|
||||
N: []string{"id[0]", "id[1]"},
|
||||
},
|
||||
// Add WHERE for tuple column
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w, GtTuple("firstname", 2)),
|
||||
S: "DELETE FROM cycling.cyclist_name WHERE id=? AND firstname>(?,?) ",
|
||||
N: []string{"expr", "firstname[0]", "firstname[1]"},
|
||||
},
|
||||
// Add WHERE for all tuple columns
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(EqTuple("id", 2), GtTuple("firstname", 2)),
|
||||
S: "DELETE FROM cycling.cyclist_name WHERE id=(?,?) AND firstname>(?,?) ",
|
||||
N: []string{"id[0]", "id[1]", "firstname[0]", "firstname[1]"},
|
||||
},
|
||||
// Add IF
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w).If(Gt("firstname")),
|
||||
S: "DELETE FROM cycling.cyclist_name WHERE id=? IF firstname>? ",
|
||||
N: []string{"expr", "firstname"},
|
||||
},
|
||||
// Add TIMESTAMP
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w).Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP 1115251200000000 WHERE id=? ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w).TimestampNamed("ts"),
|
||||
S: "DELETE FROM cycling.cyclist_name USING TIMESTAMP ? WHERE id=? ",
|
||||
N: []string{"ts", "expr"},
|
||||
},
|
||||
// Add TIMEOUT
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w).Timeout(time.Second),
|
||||
S: "DELETE FROM cycling.cyclist_name USING TIMEOUT 1s WHERE id=? ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w).TimeoutNamed("to"),
|
||||
S: "DELETE FROM cycling.cyclist_name USING TIMEOUT ? WHERE id=? ",
|
||||
N: []string{"to", "expr"},
|
||||
},
|
||||
// Add IF EXISTS
|
||||
{
|
||||
B: Delete("cycling.cyclist_name").Where(w).Existing(),
|
||||
S: "DELETE FROM cycling.cyclist_name WHERE id=? IF EXISTS ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := test.B.ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkInsertBuilder(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname", "stars").ToCql()
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestInsertBuilder(t *testing.T) {
|
||||
table := []struct {
|
||||
B *InsertBuilder
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
// Basic test for insert
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
// Basic test for insert JSON
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Json(),
|
||||
S: "INSERT INTO cycling.cyclist_name JSON ?",
|
||||
N: nil,
|
||||
},
|
||||
// Change table name
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Into("Foobar"),
|
||||
S: "INSERT INTO Foobar (id,user_uuid,firstname) VALUES (?,?,?) ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
// Add columns
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Columns("stars"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname,stars) VALUES (?,?,?,?) ",
|
||||
N: []string{"id", "user_uuid", "firstname", "stars"},
|
||||
},
|
||||
// Add a named column
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").NamedColumn("stars", "stars_name"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname,stars) VALUES (?,?,?,?) ",
|
||||
N: []string{"id", "user_uuid", "firstname", "stars_name"},
|
||||
},
|
||||
// Add a literal column
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").LitColumn("stars", "stars_lit"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname,stars) VALUES (?,?,?,stars_lit) ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
// Add TTL
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTL(time.Second),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TTL 1 ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TTLNamed("ttl"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TTL ? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "ttl"},
|
||||
},
|
||||
// Add TIMESTAMP
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP 1115251200000000 ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TimestampNamed("ts"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP ? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "ts"},
|
||||
},
|
||||
// Add TIMESTAMP
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP 1115251200000000 ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TimestampNamed("ts"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP ? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "ts"},
|
||||
},
|
||||
// Add TIMESTAMP
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP 1115251200000000 ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TimestampNamed("ts"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMESTAMP ? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "ts"},
|
||||
},
|
||||
// Add TIMEOUT
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Timeout(time.Second),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMEOUT 1s ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").TimeoutNamed("to"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) USING TIMEOUT ? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "to"},
|
||||
},
|
||||
// Add TupleColumn
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").TupleColumn("id", 2),
|
||||
S: "INSERT INTO cycling.cyclist_name (id) VALUES ((?,?)) ",
|
||||
N: []string{"id[0]", "id[1]"},
|
||||
},
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").TupleColumn("id", 2).Columns("user_uuid"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid) VALUES ((?,?),?) ",
|
||||
N: []string{"id[0]", "id[1]", "user_uuid"},
|
||||
},
|
||||
// Add IF NOT EXISTS
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Unique(),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid,firstname) VALUES (?,?,?) IF NOT EXISTS ",
|
||||
N: []string{"id", "user_uuid", "firstname"},
|
||||
},
|
||||
// Add FuncColumn
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").FuncColumn("id", Now()),
|
||||
S: "INSERT INTO cycling.cyclist_name (id) VALUES (now()) ",
|
||||
N: nil,
|
||||
},
|
||||
{
|
||||
B: Insert("cycling.cyclist_name").FuncColumn("id", Now()).Columns("user_uuid"),
|
||||
S: "INSERT INTO cycling.cyclist_name (id,user_uuid) VALUES (now(),?) ",
|
||||
N: []string{"user_uuid"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := test.B.ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkSelectBuilder(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Select("cycling.cyclist_name").
|
||||
Columns("id", "user_uuid", "firstname", "surname", "stars").
|
||||
Where(Eq("id")).
|
||||
ToCql()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSelectBuildAssign(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
cols := []string{
|
||||
"id", "user_uuid", "firstname",
|
||||
"surname", "stars",
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
Select("cycling.cyclist_name").
|
||||
Columns(cols...).
|
||||
Where(Eq("id")).
|
||||
ToCql()
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestSelectBuilder(t *testing.T) {
|
||||
w := EqNamed("id", "expr")
|
||||
|
||||
table := []struct {
|
||||
B *SelectBuilder
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
// Basic test for select *
|
||||
{
|
||||
B: Select("cycling.cyclist_name"),
|
||||
S: "SELECT * FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Basic test for select columns
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Columns("id", "user_uuid", "firstname"),
|
||||
S: "SELECT id,user_uuid,firstname FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Add a SELECT AS column
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Columns("id", "user_uuid", As("firstname", "name")),
|
||||
S: "SELECT id,user_uuid,firstname AS name FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Basic test for select columns as JSON
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Columns("id", "user_uuid", "firstname").Json(),
|
||||
S: "SELECT JSON id,user_uuid,firstname FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Add a SELECT AS column as JSON
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Columns("id", "user_uuid", As("firstname", "name")).Json(),
|
||||
S: "SELECT JSON id,user_uuid,firstname AS name FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Add a SELECT AS column 2
|
||||
{
|
||||
B: Select("cycling.cyclist_name").
|
||||
Columns(As("firstname", "name"), "id", As("user_uuid", "user")),
|
||||
S: "SELECT firstname AS name,id,user_uuid AS user FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Basic test for select distinct
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Distinct("id"),
|
||||
S: "SELECT DISTINCT id FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Change table name
|
||||
{
|
||||
B: Select("cycling.cyclist_name").From("Foobar"),
|
||||
S: "SELECT * FROM Foobar ",
|
||||
},
|
||||
// Add WHERE
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w, Gt("firstname")),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? AND firstname>? ",
|
||||
N: []string{"expr", "firstname"},
|
||||
},
|
||||
// Add WHERE with tuple
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(EqTuple("id", 2), Gt("firstname")),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=(?,?) AND firstname>? ",
|
||||
N: []string{"id[0]", "id[1]", "firstname"},
|
||||
},
|
||||
// Add WHERE with only tuples
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(EqTuple("id", 2), GtTuple("firstname", 2)),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=(?,?) AND firstname>(?,?) ",
|
||||
N: []string{"id[0]", "id[1]", "firstname[0]", "firstname[1]"},
|
||||
},
|
||||
// Add TIMEOUT
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w, Gt("firstname")).Timeout(time.Second),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? AND firstname>? USING TIMEOUT 1s ",
|
||||
N: []string{"expr", "firstname"},
|
||||
},
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w, Gt("firstname")).TimeoutNamed("to"),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? AND firstname>? USING TIMEOUT ? ",
|
||||
N: []string{"expr", "firstname", "to"},
|
||||
},
|
||||
// Add GROUP BY
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Columns("MAX(stars) as max_stars").GroupBy("id"),
|
||||
S: "SELECT id,MAX(stars) as max_stars FROM cycling.cyclist_name GROUP BY id ",
|
||||
},
|
||||
// Add GROUP BY
|
||||
{
|
||||
B: Select("cycling.cyclist_name").GroupBy("id"),
|
||||
S: "SELECT id FROM cycling.cyclist_name GROUP BY id ",
|
||||
},
|
||||
// Add GROUP BY two columns
|
||||
{
|
||||
B: Select("cycling.cyclist_name").GroupBy("id", "user_uuid"),
|
||||
S: "SELECT id,user_uuid FROM cycling.cyclist_name GROUP BY id,user_uuid ",
|
||||
},
|
||||
// Add ORDER BY
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).OrderBy("firstname", ASC),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? ORDER BY firstname ASC ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add ORDER BY
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).OrderBy("firstname", DESC),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? ORDER BY firstname DESC ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add ORDER BY two columns
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).OrderBy("firstname", ASC).OrderBy("lastname", DESC),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? ORDER BY firstname ASC,lastname DESC ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add LIMIT
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).Limit(10),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? LIMIT 10 ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add named LIMIT
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).LimitNamed("limit"),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? LIMIT ? ",
|
||||
N: []string{"expr", "limit"},
|
||||
},
|
||||
// Add PER PARTITION LIMIT
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).LimitPerPartition(10),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? PER PARTITION LIMIT 10 ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add named PER PARTITION LIMIT
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).LimitPerPartitionNamed("partition_limit"),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? PER PARTITION LIMIT ? ",
|
||||
N: []string{"expr", "partition_limit"},
|
||||
},
|
||||
// Add PER PARTITION LIMIT and LIMIT
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).LimitPerPartition(2).Limit(10),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? PER PARTITION LIMIT 2 LIMIT 10 ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add named PER PARTITION LIMIT and LIMIT
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).LimitPerPartitionNamed("partition_limit").LimitNamed("limit"),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? PER PARTITION LIMIT ? LIMIT ? ",
|
||||
N: []string{"expr", "partition_limit", "limit"},
|
||||
},
|
||||
// Add ALLOW FILTERING
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).AllowFiltering(),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? ALLOW FILTERING ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add ALLOW FILTERING and BYPASS CACHE
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).AllowFiltering().BypassCache(),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? ALLOW FILTERING BYPASS CACHE ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add BYPASS CACHE
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Where(w).BypassCache(),
|
||||
S: "SELECT * FROM cycling.cyclist_name WHERE id=? BYPASS CACHE ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add COUNT all
|
||||
{
|
||||
B: Select("cycling.cyclist_name").CountAll().Where(Gt("stars")),
|
||||
S: "SELECT count(*) FROM cycling.cyclist_name WHERE stars>? ",
|
||||
N: []string{"stars"},
|
||||
},
|
||||
// Add COUNT with GROUP BY
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Count("stars").GroupBy("id"),
|
||||
S: "SELECT id,count(stars) FROM cycling.cyclist_name GROUP BY id ",
|
||||
},
|
||||
// Add Min
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Min("stars"),
|
||||
S: "SELECT min(stars) FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Add Sum
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Sum("*"),
|
||||
S: "SELECT sum(*) FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Add Avg
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Avg("stars"),
|
||||
S: "SELECT avg(stars) FROM cycling.cyclist_name ",
|
||||
},
|
||||
// Add Max
|
||||
{
|
||||
B: Select("cycling.cyclist_name").Max("stars"),
|
||||
S: "SELECT max(stars) FROM cycling.cyclist_name ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := test.B.ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
136
qb/token_test.go
136
qb/token_test.go
@@ -1,136 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestToken(t *testing.T) {
|
||||
table := []struct {
|
||||
C Cmp
|
||||
S string
|
||||
N []string
|
||||
}{
|
||||
// Basic comparators
|
||||
{
|
||||
C: Token("a", "b").Eq(),
|
||||
S: "token(a,b)=token(?,?)",
|
||||
N: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").Lt(),
|
||||
S: "token(a,b)<token(?,?)",
|
||||
N: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").LtOrEq(),
|
||||
S: "token(a,b)<=token(?,?)",
|
||||
N: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").Gt(),
|
||||
S: "token(a,b)>token(?,?)",
|
||||
N: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").GtOrEq(),
|
||||
S: "token(a,b)>=token(?,?)",
|
||||
N: []string{"a", "b"},
|
||||
},
|
||||
|
||||
// Custom bind names
|
||||
{
|
||||
C: Token("a", "b").EqNamed("c", "d"),
|
||||
S: "token(a,b)=token(?,?)",
|
||||
N: []string{"c", "d"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").LtNamed("c", "d"),
|
||||
S: "token(a,b)<token(?,?)",
|
||||
N: []string{"c", "d"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").LtOrEqNamed("c", "d"),
|
||||
S: "token(a,b)<=token(?,?)",
|
||||
N: []string{"c", "d"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").GtNamed("c", "d"),
|
||||
S: "token(a,b)>token(?,?)",
|
||||
N: []string{"c", "d"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").GtOrEqNamed("c", "d"),
|
||||
S: "token(a,b)>=token(?,?)",
|
||||
N: []string{"c", "d"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").EqValue(),
|
||||
S: "token(a,b)=?",
|
||||
N: []string{"token"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").EqValueNamed("c"),
|
||||
S: "token(a,b)=?",
|
||||
N: []string{"c"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").LtValue(),
|
||||
S: "token(a,b)<?",
|
||||
N: []string{"token"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").LtValueNamed("c"),
|
||||
S: "token(a,b)<?",
|
||||
N: []string{"c"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").LtOrEqValue(),
|
||||
S: "token(a,b)<=?",
|
||||
N: []string{"token"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").LtOrEqValueNamed("c"),
|
||||
S: "token(a,b)<=?",
|
||||
N: []string{"c"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").GtValue(),
|
||||
S: "token(a,b)>?",
|
||||
N: []string{"token"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").GtValueNamed("c"),
|
||||
S: "token(a,b)>?",
|
||||
N: []string{"c"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").GtOrEqValue(),
|
||||
S: "token(a,b)>=?",
|
||||
N: []string{"token"},
|
||||
},
|
||||
{
|
||||
C: Token("a", "b").GtOrEqValueNamed("c"),
|
||||
S: "token(a,b)>=?",
|
||||
N: []string{"c"},
|
||||
},
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
for _, test := range table {
|
||||
buf.Reset()
|
||||
name := test.C.writeCql(&buf)
|
||||
if diff := cmp.Diff(test.S, buf.String()); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, name); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkUpdateBuilder(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname", "stars").Where(Eq("id")).ToCql()
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestUpdateBuilder(t *testing.T) {
|
||||
w := EqNamed("id", "expr")
|
||||
|
||||
table := []struct {
|
||||
B *UpdateBuilder
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
// Basic test for update
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w),
|
||||
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
// Change table name
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Table("Foobar"),
|
||||
S: "UPDATE Foobar SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
// Add SET
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Set("stars"),
|
||||
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=?,stars=? WHERE id=? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "stars", "expr"},
|
||||
},
|
||||
// Add SET literal
|
||||
{
|
||||
B: Update("cycling.cyclist_name").SetLit("user_uuid", "literal_uuid").Where(w).Set("stars"),
|
||||
S: "UPDATE cycling.cyclist_name SET user_uuid=literal_uuid,stars=? WHERE id=? ",
|
||||
N: []string{"stars", "expr"},
|
||||
},
|
||||
|
||||
// Add SET tuple
|
||||
{
|
||||
B: Update("cycling.cyclist_name").SetTuple("id", 2).Set("user_uuid", "firstname").Where(EqTuple("id", 2)),
|
||||
S: "UPDATE cycling.cyclist_name SET id=(?,?),user_uuid=?,firstname=? WHERE id=(?,?) ",
|
||||
N: []string{"id[0]", "id[1]", "user_uuid", "firstname", "id[0]", "id[1]"},
|
||||
},
|
||||
// Add SET SetFunc
|
||||
{
|
||||
B: Update("cycling.cyclist_name").SetFunc("user_uuid", Fn("someFunc", "param_0", "param_1")).Where(w).Set("stars"),
|
||||
S: "UPDATE cycling.cyclist_name SET user_uuid=someFunc(?,?),stars=? WHERE id=? ",
|
||||
N: []string{"param_0", "param_1", "stars", "expr"},
|
||||
},
|
||||
// Add SET Add
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Add("total").Where(w),
|
||||
S: "UPDATE cycling.cyclist_name SET total=total+? WHERE id=? ",
|
||||
N: []string{"total", "expr"},
|
||||
},
|
||||
// Add SET AddNamed
|
||||
{
|
||||
B: Update("cycling.cyclist_name").AddNamed("total", "inc").Where(w),
|
||||
S: "UPDATE cycling.cyclist_name SET total=total+? WHERE id=? ",
|
||||
N: []string{"inc", "expr"},
|
||||
},
|
||||
// Add SET AddLit
|
||||
{
|
||||
B: Update("cycling.cyclist_name").AddLit("total", "1").Where(w),
|
||||
S: "UPDATE cycling.cyclist_name SET total=total+1 WHERE id=? ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add SET Remove
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Remove("total").Where(w),
|
||||
S: "UPDATE cycling.cyclist_name SET total=total-? WHERE id=? ",
|
||||
N: []string{"total", "expr"},
|
||||
},
|
||||
// Add SET RemoveNamed
|
||||
{
|
||||
B: Update("cycling.cyclist_name").RemoveNamed("total", "dec").Where(w),
|
||||
S: "UPDATE cycling.cyclist_name SET total=total-? WHERE id=? ",
|
||||
N: []string{"dec", "expr"},
|
||||
},
|
||||
// Add SET RemoveLit
|
||||
{
|
||||
B: Update("cycling.cyclist_name").RemoveLit("total", "1").Where(w),
|
||||
S: "UPDATE cycling.cyclist_name SET total=total-1 WHERE id=? ",
|
||||
N: []string{"expr"},
|
||||
},
|
||||
// Add WHERE
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w, Gt("firstname")),
|
||||
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? AND firstname>? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "expr", "firstname"},
|
||||
},
|
||||
// Add IF
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).If(Gt("firstname")),
|
||||
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? IF firstname>? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "expr", "firstname"},
|
||||
},
|
||||
// Add TTL
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TTL(time.Second),
|
||||
S: "UPDATE cycling.cyclist_name USING TTL 1 SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TTLNamed("ttl"),
|
||||
S: "UPDATE cycling.cyclist_name USING TTL ? SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||
N: []string{"ttl", "id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
// Add TIMESTAMP
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "UPDATE cycling.cyclist_name USING TIMESTAMP 1115251200000000 SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TimestampNamed("ts"),
|
||||
S: "UPDATE cycling.cyclist_name USING TIMESTAMP ? SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||
N: []string{"ts", "id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
// Add TIMEOUT
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Timeout(time.Second),
|
||||
S: "UPDATE cycling.cyclist_name USING TIMEOUT 1s SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||
N: []string{"id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).TimeoutNamed("to"),
|
||||
S: "UPDATE cycling.cyclist_name USING TIMEOUT ? SET id=?,user_uuid=?,firstname=? WHERE id=? ",
|
||||
N: []string{"to", "id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
// Add IF EXISTS
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).Existing(),
|
||||
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? IF EXISTS ",
|
||||
N: []string{"id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
// Add SET column
|
||||
{
|
||||
B: Update("cycling.cyclist_name").SetNamed("firstname", "name"),
|
||||
S: "UPDATE cycling.cyclist_name SET firstname=? ",
|
||||
N: []string{"name"},
|
||||
},
|
||||
// Add AddFunc
|
||||
{
|
||||
B: Update("cycling.cyclist_name").AddFunc("timestamp", Now()),
|
||||
S: "UPDATE cycling.cyclist_name SET timestamp=timestamp+now() ",
|
||||
N: nil,
|
||||
},
|
||||
// Add RemoveFunc
|
||||
{
|
||||
B: Update("cycling.cyclist_name").RemoveFunc("timestamp", Now()),
|
||||
S: "UPDATE cycling.cyclist_name SET timestamp=timestamp-now() ",
|
||||
N: nil,
|
||||
},
|
||||
// Add ALLOW FILTERING
|
||||
{
|
||||
B: Update("cycling.cyclist_name").Set("id", "user_uuid", "firstname").Where(w).AllowFiltering(),
|
||||
S: "UPDATE cycling.cyclist_name SET id=?,user_uuid=?,firstname=? WHERE id=? ALLOW FILTERING ",
|
||||
N: []string{"id", "user_uuid", "firstname", "expr"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := test.B.ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
160
qb/using_test.go
160
qb/using_test.go
@@ -1,160 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestTTL(t *testing.T) {
|
||||
if TTL(time.Second*86400) != 86400 {
|
||||
t.Fatal("wrong ttl")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestamp(t *testing.T) {
|
||||
if Timestamp(time.Unix(0, 0).Add(time.Microsecond*123456789)) != 123456789 {
|
||||
t.Fatal("wrong timestamp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsing(t *testing.T) {
|
||||
table := []struct {
|
||||
B *using
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
// TTL
|
||||
{
|
||||
B: new(using).TTL(time.Second),
|
||||
S: "USING TTL 1 ",
|
||||
},
|
||||
// TTLNamed
|
||||
{
|
||||
B: new(using).TTLNamed("ttl"),
|
||||
S: "USING TTL ? ",
|
||||
N: []string{"ttl"},
|
||||
},
|
||||
// Timestamp
|
||||
{
|
||||
B: new(using).Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "USING TIMESTAMP 1115251200000000 ",
|
||||
},
|
||||
// TimestampNamed
|
||||
{
|
||||
B: new(using).TimestampNamed("ts"),
|
||||
S: "USING TIMESTAMP ? ",
|
||||
N: []string{"ts"},
|
||||
},
|
||||
// Timeout
|
||||
{
|
||||
B: new(using).Timeout(time.Second),
|
||||
S: "USING TIMEOUT 1s ",
|
||||
},
|
||||
// Timeout faction
|
||||
{
|
||||
B: new(using).Timeout(time.Second + 100*time.Millisecond),
|
||||
S: "USING TIMEOUT 1s100ms ",
|
||||
},
|
||||
// TimeoutNamed
|
||||
{
|
||||
B: new(using).TimeoutNamed("to"),
|
||||
S: "USING TIMEOUT ? ",
|
||||
N: []string{"to"},
|
||||
},
|
||||
// TTL Timestamp
|
||||
{
|
||||
B: new(using).TTL(time.Second).Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "USING TTL 1 AND TIMESTAMP 1115251200000000 ",
|
||||
},
|
||||
// TTL TimestampNamed
|
||||
{
|
||||
B: new(using).TTL(time.Second).TimestampNamed("ts"),
|
||||
S: "USING TTL 1 AND TIMESTAMP ? ",
|
||||
N: []string{"ts"},
|
||||
},
|
||||
// TTLNamed TimestampNamed
|
||||
{
|
||||
B: new(using).TTLNamed("ttl").TimestampNamed("ts"),
|
||||
S: "USING TTL ? AND TIMESTAMP ? ",
|
||||
N: []string{"ttl", "ts"},
|
||||
},
|
||||
// TTLNamed Timestamp
|
||||
{
|
||||
B: new(using).TTLNamed("ttl").Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "USING TTL ? AND TIMESTAMP 1115251200000000 ",
|
||||
N: []string{"ttl"},
|
||||
},
|
||||
// TTL Timeout
|
||||
{
|
||||
B: new(using).TTL(time.Second).Timeout(time.Second),
|
||||
S: "USING TTL 1 AND TIMEOUT 1s ",
|
||||
},
|
||||
// TTL TimeoutNamed
|
||||
{
|
||||
B: new(using).TTL(time.Second).TimeoutNamed("to"),
|
||||
S: "USING TTL 1 AND TIMEOUT ? ",
|
||||
N: []string{"to"},
|
||||
},
|
||||
// TTL Timestamp Timeout
|
||||
{
|
||||
B: new(using).TTL(time.Second).Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)).Timeout(time.Second),
|
||||
S: "USING TTL 1 AND TIMESTAMP 1115251200000000 AND TIMEOUT 1s ",
|
||||
},
|
||||
// TTL with no duration
|
||||
{
|
||||
B: new(using).TTL(0 * time.Second),
|
||||
S: "USING TTL 0 ",
|
||||
},
|
||||
{
|
||||
B: new(using).TTL(-1 * time.Second),
|
||||
S: "USING TTL 0 ",
|
||||
},
|
||||
{
|
||||
// TODO patch this maybe in the future
|
||||
B: new(using).TTL(-2 * time.Second),
|
||||
S: "USING TTL -2 ",
|
||||
},
|
||||
// TTL TTLNamed
|
||||
{
|
||||
B: new(using).TTL(time.Second).TTLNamed("ttl"),
|
||||
S: "USING TTL ? ",
|
||||
N: []string{"ttl"},
|
||||
},
|
||||
// TTLNamed TTL
|
||||
{
|
||||
B: new(using).TTLNamed("ttl").TTL(time.Second),
|
||||
S: "USING TTL 1 ",
|
||||
},
|
||||
// Timestamp TimestampNamed
|
||||
{
|
||||
B: new(using).Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)).TimestampNamed("ts"),
|
||||
S: "USING TIMESTAMP ? ",
|
||||
N: []string{"ts"},
|
||||
},
|
||||
// TimestampNamed Timestamp
|
||||
{
|
||||
B: new(using).TimestampNamed("ts").Timestamp(time.Date(2005, 5, 5, 0, 0, 0, 0, time.UTC)),
|
||||
S: "USING TIMESTAMP 1115251200000000 ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
names := test.B.writeCql(buf)
|
||||
stmt := buf.String()
|
||||
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package qb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFormatDuration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input time.Duration
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Zero duration",
|
||||
input: 0,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
input: 500 * time.Millisecond,
|
||||
expected: "500ms",
|
||||
},
|
||||
{
|
||||
input: 10 * time.Second,
|
||||
expected: "10s",
|
||||
},
|
||||
{
|
||||
input: 3 * time.Minute,
|
||||
expected: "3m",
|
||||
},
|
||||
{
|
||||
input: (2 * time.Minute) + (30 * time.Second),
|
||||
expected: "2m30s",
|
||||
},
|
||||
{
|
||||
input: (15 * time.Second) + (250 * time.Millisecond),
|
||||
expected: "15s250ms",
|
||||
},
|
||||
{
|
||||
input: (1 * time.Minute) + (45 * time.Second) + (123 * time.Millisecond),
|
||||
expected: "1m45s123ms",
|
||||
},
|
||||
{
|
||||
input: (5 * time.Minute) + (1 * time.Second) + (999 * time.Millisecond),
|
||||
expected: "5m1s999ms",
|
||||
},
|
||||
{
|
||||
input: (2 * time.Second) + (1500 * time.Millisecond), // 3 seconds, 500ms
|
||||
expected: "3s500ms",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := formatDuration(tt.input)
|
||||
if actual != tt.expected {
|
||||
t.Errorf("got %q, want %q", actual, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
gocql "github.com/apache/cassandra-gocql-driver/v2"
|
||||
"github.com/scylladb/go-reflectx"
|
||||
)
|
||||
|
||||
@@ -99,6 +99,8 @@ type Queryx struct {
|
||||
strict bool
|
||||
}
|
||||
|
||||
func (q *Queryx) Release() {}
|
||||
|
||||
// Query creates a new Queryx from gocql.Query using a default mapper.
|
||||
//
|
||||
// Deprecated: Use gocqlx.Session.Query API instead.
|
||||
@@ -151,13 +153,13 @@ func (q *Queryx) BindStructMap(arg0 interface{}, arg1 map[string]interface{}) *Q
|
||||
// GetRequestTimeout returns time driver waits for single server response
|
||||
// This timeout is applied to preparing statement request and for query execution requests
|
||||
func (q *Queryx) GetRequestTimeout() time.Duration {
|
||||
return q.Query.GetRequestTimeout()
|
||||
return 0
|
||||
}
|
||||
|
||||
// SetRequestTimeout sets time driver waits for server to respond
|
||||
// This timeout is applied to preparing statement request and for query execution requests
|
||||
func (q *Queryx) SetRequestTimeout(timeout time.Duration) *Queryx {
|
||||
q.Query.SetRequestTimeout(timeout)
|
||||
// q.Query.SetRequestTimeout(timeout)
|
||||
return q
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ package gocqlx_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
gocql "github.com/apache/cassandra-gocql-driver/v2"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3"
|
||||
)
|
||||
|
||||
233
queryx_test.go
233
queryx_test.go
@@ -1,233 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gocqlx
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestCompileQuery(t *testing.T) {
|
||||
table := []struct {
|
||||
Q, R string
|
||||
V []string
|
||||
}{
|
||||
// Basic test for named parameters, invalid char ',' terminating
|
||||
{
|
||||
Q: `INSERT INTO foo (a,b,c,d) VALUES (:name, :age, :first, :last)`,
|
||||
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
|
||||
V: []string{"name", "age", "first", "last"},
|
||||
},
|
||||
// This query tests a named parameter ending the string as well as numbers
|
||||
{
|
||||
Q: `SELECT * FROM a WHERE first_name=:name1 AND last_name=:name2`,
|
||||
R: `SELECT * FROM a WHERE first_name=? AND last_name=?`,
|
||||
V: []string{"name1", "name2"},
|
||||
},
|
||||
{
|
||||
Q: `SELECT "::foo" FROM a WHERE first_name=:name1 AND last_name=:name2`,
|
||||
R: `SELECT ":foo" FROM a WHERE first_name=? AND last_name=?`,
|
||||
V: []string{"name1", "name2"},
|
||||
},
|
||||
{
|
||||
Q: `SELECT 'a::b::c' || first_name, '::::ABC::_::' FROM person WHERE first_name=:first_name AND last_name=:last_name`,
|
||||
R: `SELECT 'a:b:c' || first_name, '::ABC:_:' FROM person WHERE first_name=? AND last_name=?`,
|
||||
V: []string{"first_name", "last_name"},
|
||||
},
|
||||
/* This unicode awareness test sadly fails, because of our byte-wise worldview.
|
||||
* We could certainly iterate by Rune instead, though it's a great deal slower,
|
||||
* it's probably the RightWay(tm)
|
||||
{
|
||||
Q: `INSERT INTO foo (a,b,c,d) VALUES (:あ, :b, :キコ, :名前)`,
|
||||
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
|
||||
},
|
||||
*/
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
qr, names, err := CompileNamedQuery([]byte(test.Q))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if qr != test.R {
|
||||
t.Error("expected", test.R, "got", qr)
|
||||
}
|
||||
if diff := cmp.Diff(names, test.V); diff != "" {
|
||||
t.Error("names mismatch", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryxBindStruct(t *testing.T) {
|
||||
v := &struct {
|
||||
Name string
|
||||
Age int
|
||||
First string
|
||||
Last string
|
||||
}{
|
||||
Name: "name",
|
||||
Age: 30,
|
||||
First: "first",
|
||||
Last: "last",
|
||||
}
|
||||
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
names := []string{"name", "age", "first", "last"}
|
||||
args, err := Query(nil, names).bindStructArgs(v, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(args, []interface{}{"name", 30, "first", "last"}); diff != "" {
|
||||
t.Error("args mismatch", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with transformer", func(t *testing.T) {
|
||||
tr := func(name string, val interface{}) interface{} {
|
||||
if name == "age" {
|
||||
return 42
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
names := []string{"name", "age", "first", "last"}
|
||||
args, err := Query(nil, names).WithBindTransformer(tr).bindStructArgs(v, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(args, []interface{}{"name", 42, "first", "last"}); diff != "" {
|
||||
t.Error("args mismatch", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
names := []string{"name", "age", "first", "not_found"}
|
||||
_, err := Query(nil, names).bindStructArgs(v, nil)
|
||||
if err == nil {
|
||||
t.Fatal("unexpected error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallback", func(t *testing.T) {
|
||||
names := []string{"name", "age", "first", "not_found"}
|
||||
m := map[string]interface{}{
|
||||
"not_found": "last",
|
||||
}
|
||||
args, err := Query(nil, names).bindStructArgs(v, m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(args, []interface{}{"name", 30, "first", "last"}); diff != "" {
|
||||
t.Error("args mismatch", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallback with transformer", func(t *testing.T) {
|
||||
tr := func(name string, val interface{}) interface{} {
|
||||
if name == "not_found" {
|
||||
return "map_found"
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
names := []string{"name", "age", "first", "not_found"}
|
||||
m := map[string]interface{}{
|
||||
"not_found": "last",
|
||||
}
|
||||
args, err := Query(nil, names).WithBindTransformer(tr).bindStructArgs(v, m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(args, []interface{}{"name", 30, "first", "map_found"}); diff != "" {
|
||||
t.Error("args mismatch", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallback error", func(t *testing.T) {
|
||||
names := []string{"name", "age", "first", "not_found", "really_not_found"}
|
||||
m := map[string]interface{}{
|
||||
"not_found": "last",
|
||||
}
|
||||
_, err := Query(nil, names).bindStructArgs(v, m)
|
||||
if err == nil {
|
||||
t.Fatal("unexpected error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryxBindMap(t *testing.T) {
|
||||
v := map[string]interface{}{
|
||||
"name": "name",
|
||||
"age": 30,
|
||||
"first": "first",
|
||||
"last": "last",
|
||||
}
|
||||
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
names := []string{"name", "age", "first", "last"}
|
||||
args, err := Query(nil, names).bindMapArgs(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(args, []interface{}{"name", 30, "first", "last"}); diff != "" {
|
||||
t.Error("args mismatch", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with transformer", func(t *testing.T) {
|
||||
tr := func(name string, val interface{}) interface{} {
|
||||
if name == "age" {
|
||||
return 42
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
names := []string{"name", "age", "first", "last"}
|
||||
args, err := Query(nil, names).WithBindTransformer(tr).bindMapArgs(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(args, []interface{}{"name", 42, "first", "last"}); diff != "" {
|
||||
t.Error("args mismatch", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
names := []string{"name", "first", "not_found"}
|
||||
_, err := Query(nil, names).bindMapArgs(v)
|
||||
if err == nil {
|
||||
t.Fatal("unexpected error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryxAllWrapped(t *testing.T) {
|
||||
var (
|
||||
gocqlQueryPtr = reflect.TypeOf((*gocql.Query)(nil))
|
||||
queryxPtr = reflect.TypeOf((*Queryx)(nil))
|
||||
)
|
||||
|
||||
for i := 0; i < gocqlQueryPtr.NumMethod(); i++ {
|
||||
m, ok := queryxPtr.MethodByName(gocqlQueryPtr.Method(i).Name)
|
||||
if !ok {
|
||||
t.Fatalf("Queryx missing method %s", gocqlQueryPtr.Method(i).Name)
|
||||
}
|
||||
|
||||
for j := 0; j < m.Type.NumOut(); j++ {
|
||||
if m.Type.Out(j) == gocqlQueryPtr {
|
||||
t.Errorf("Queryx method %s not wrapped", m.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ package gocqlx
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
gocql "github.com/apache/cassandra-gocql-driver/v2"
|
||||
)
|
||||
|
||||
// This file contains wrappers around gocql.Query that make Queryx expose the
|
||||
|
||||
@@ -7,7 +7,7 @@ package gocqlx
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
gocql "github.com/apache/cassandra-gocql-driver/v2"
|
||||
"github.com/scylladb/go-reflectx"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
// Copyright (C) 2017 ScyllaDB
|
||||
// Use of this source code is governed by a ALv2-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/scylladb/gocqlx/v3/qb"
|
||||
)
|
||||
|
||||
func TestTableGet(t *testing.T) {
|
||||
table := []struct {
|
||||
M Metadata
|
||||
C []string
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
SortKey: []string{"b"},
|
||||
},
|
||||
N: []string{"a", "b"},
|
||||
S: "SELECT * FROM table WHERE a=? AND b=? ",
|
||||
},
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
},
|
||||
N: []string{"a"},
|
||||
S: "SELECT * FROM table WHERE a=? ",
|
||||
},
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
},
|
||||
C: []string{"d"},
|
||||
N: []string{"a"},
|
||||
S: "SELECT d FROM table WHERE a=? ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := New(test.M).Get(test.C...)
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
|
||||
// run GetBuilder on the same data set
|
||||
for _, test := range table {
|
||||
stmt, names := New(test.M).GetBuilder(test.C...).ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableSelect(t *testing.T) {
|
||||
table := []struct {
|
||||
M Metadata
|
||||
C []string
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
SortKey: []string{"b"},
|
||||
},
|
||||
N: []string{"a"},
|
||||
S: "SELECT * FROM table WHERE a=? ",
|
||||
},
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
SortKey: []string{"b"},
|
||||
},
|
||||
C: []string{"d"},
|
||||
N: []string{"a"},
|
||||
S: "SELECT d FROM table WHERE a=? ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := New(test.M).Select(test.C...)
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
|
||||
// run SelectBuilder on the same data set
|
||||
for _, test := range table {
|
||||
stmt, names := New(test.M).SelectBuilder(test.C...).ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableInsert(t *testing.T) {
|
||||
table := []struct {
|
||||
M Metadata
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
SortKey: []string{"b"},
|
||||
},
|
||||
N: []string{"a", "b", "c", "d"},
|
||||
S: "INSERT INTO table (a,b,c,d) VALUES (?,?,?,?) ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := New(test.M).Insert()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableUpdate(t *testing.T) {
|
||||
table := []struct {
|
||||
M Metadata
|
||||
C []string
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
SortKey: []string{"b"},
|
||||
},
|
||||
C: []string{"d"},
|
||||
N: []string{"d", "a", "b"},
|
||||
S: "UPDATE table SET d=? WHERE a=? AND b=? ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := New(test.M).Update(test.C...)
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
|
||||
// run UpdateBuilder on the same data set
|
||||
for _, test := range table {
|
||||
stmt, names := New(test.M).UpdateBuilder(test.C...).ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableDelete(t *testing.T) {
|
||||
table := []struct {
|
||||
M Metadata
|
||||
C []string
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
SortKey: []string{"b"},
|
||||
},
|
||||
N: []string{"a", "b"},
|
||||
S: "DELETE FROM table WHERE a=? AND b=? ",
|
||||
},
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
},
|
||||
N: []string{"a"},
|
||||
S: "DELETE FROM table WHERE a=? ",
|
||||
},
|
||||
{
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
},
|
||||
C: []string{"d"},
|
||||
N: []string{"a"},
|
||||
S: "DELETE d FROM table WHERE a=? ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
stmt, names := New(test.M).Delete(test.C...)
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
|
||||
// run DeleteBuilder on the same data set
|
||||
for _, test := range table {
|
||||
stmt, names := New(test.M).DeleteBuilder(test.C...).ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableConcurrentUsage(t *testing.T) {
|
||||
table := []struct {
|
||||
Name string
|
||||
M Metadata
|
||||
C []string
|
||||
N []string
|
||||
S string
|
||||
}{
|
||||
{
|
||||
Name: "Full select",
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
SortKey: []string{"b"},
|
||||
},
|
||||
N: []string{"a", "b"},
|
||||
S: "SELECT * FROM table WHERE a=? AND b=? ",
|
||||
},
|
||||
{
|
||||
Name: "Sub select",
|
||||
M: Metadata{
|
||||
Name: "table",
|
||||
Columns: []string{"a", "b", "c", "d"},
|
||||
PartKey: []string{"a"},
|
||||
SortKey: []string{"b"},
|
||||
},
|
||||
C: []string{"d"},
|
||||
N: []string{"a", "b"},
|
||||
S: "SELECT d FROM table WHERE a=? AND b=? ",
|
||||
},
|
||||
}
|
||||
|
||||
parallelCount := 3
|
||||
// run SelectBuilder on the data set in parallel
|
||||
for _, test := range table {
|
||||
var wg sync.WaitGroup
|
||||
testTable := New(test.M)
|
||||
wg.Add(parallelCount)
|
||||
for i := 0; i < parallelCount; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
stmt, names := testTable.SelectBuilder(test.C...).
|
||||
Where(qb.Eq("b")).ToCql()
|
||||
if diff := cmp.Diff(test.S, stmt); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.N, names); diff != "" {
|
||||
t.Error(diff, names)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
8002
testdata/people.json
vendored
8002
testdata/people.json
vendored
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ package gocqlx
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
gocql "github.com/apache/cassandra-gocql-driver/v2"
|
||||
)
|
||||
|
||||
// Transformer transforms the value of the named parameter to another value.
|
||||
|
||||
Reference in New Issue
Block a user