Delete everything except query builder

This commit is contained in:
2025-11-20 16:09:09 +01:00
parent d9ec9f889d
commit 84c58f45a3
47 changed files with 43 additions and 10582 deletions

View File

@@ -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

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
) )
// Batch is a wrapper around gocql.Batch // 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 // GetRequestTimeout returns time driver waits for single server response
// This timeout is applied to preparing statement request and for query execution requests // This timeout is applied to preparing statement request and for query execution requests
func (b *Batch) GetRequestTimeout() time.Duration { func (b *Batch) GetRequestTimeout() time.Duration {
return b.Batch.GetRequestTimeout() return 0
} }
// SetRequestTimeout sets time driver waits for server to respond // SetRequestTimeout sets time driver waits for server to respond
// This timeout is applied to preparing statement request and for query execution requests // This timeout is applied to preparing statement request and for query execution requests
func (b *Batch) SetRequestTimeout(timeout time.Duration) *Batch { func (b *Batch) SetRequestTimeout(timeout time.Duration) *Batch {
b.Batch.SetRequestTimeout(timeout)
return b 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 // string is sent, the default behavior, using the configured HostSelectionPolicy will
// be used. A hostID can be obtained from HostInfo.HostID() after calling GetHosts(). // be used. A hostID can be obtained from HostInfo.HostID() after calling GetHosts().
func (b *Batch) SetHostID(hostID string) *Batch { func (b *Batch) SetHostID(hostID string) *Batch {
b.Batch.SetHostID(hostID)
return b return b
} }

View File

@@ -11,7 +11,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/scylladb/gocqlx/v3" "github.com/scylladb/gocqlx/v3"

View File

@@ -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')
}

View File

@@ -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)
}
})
}
}

View File

@@ -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}}

View File

@@ -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"
}

View File

@@ -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)
}
})
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 => ../../..
)

View File

@@ -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=

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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")
}
}

View File

@@ -5,7 +5,7 @@
package gocqlx_test package gocqlx_test
import ( import (
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/scylladb/gocqlx/v3" "github.com/scylladb/gocqlx/v3"
"github.com/scylladb/gocqlx/v3/qb" "github.com/scylladb/gocqlx/v3/qb"

View File

@@ -13,7 +13,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"gopkg.in/inf.v0" "gopkg.in/inf.v0"
@@ -98,7 +98,7 @@ func basicCreateAndPopulateKeyspace(t *testing.T, session gocqlx.Session, keyspa
err = session.ExecStmt(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.playlists ( err = session.ExecStmt(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.playlists (
id uuid, id uuid,
title text, title text,
album text, album text,
artist text, artist text,
song_id uuid, song_id uuid,
PRIMARY KEY (id, title, album, artist))`, keyspace)) PRIMARY KEY (id, title, album, artist))`, keyspace))
@@ -752,8 +752,8 @@ func pagingEfficientFullTableScan(t *testing.T, session gocqlx.Session) {
// worker queries a token ranges generated by sequencer // worker queries a token ranges generated by sequencer
worker := func() error { worker := func() error {
const cql = `SELECT * FROM examples.paging_efficient_full_table_scan WHERE const cql = `SELECT * FROM examples.paging_efficient_full_table_scan WHERE
token(user_id) >= :start AND token(user_id) >= :start AND
token(user_id) < :end` token(user_id) < :end`
stmt, names, err := gocqlx.CompileNamedQueryString(cql) stmt, names, err := gocqlx.CompileNamedQueryString(cql)

7
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/scylladb/gocqlx/v3
go 1.25.0 go 1.25.0
require ( 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/google/go-cmp v0.7.0
github.com/psanford/memfs v0.0.0-20241019191636-4ef911798f9b github.com/psanford/memfs v0.0.0-20241019191636-4ef911798f9b
github.com/scylladb/go-reflectx v1.0.1 github.com/scylladb/go-reflectx v1.0.1
@@ -11,9 +11,6 @@ require (
gopkg.in/inf.v0 v0.9.1 gopkg.in/inf.v0 v0.9.1
) )
require ( require github.com/stretchr/testify v1.11.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
)
replace github.com/gocql/gocql => github.com/scylladb/gocql v1.17.0 replace github.com/gocql/gocql => github.com/scylladb/gocql v1.17.0

43
go.sum
View File

@@ -1,45 +1,38 @@
github.com/bitly/go-hostpool v0.1.1 h1:SsovT4BFqgJQBAESkk2QgeeL7bqKq9oJie8JnD00R+Q= github.com/apache/cassandra-gocql-driver/v2 v2.0.0 h1:Omnzb1Z/P90Dr2TbVNu54ICQL7TKVIIsJO231w484HU=
github.com/bitly/go-hostpool v0.1.1/go.mod h1:iwXQOF7+y3cO8vituSqGpBYf02TYTzxK4S2c4rf4cJs= github.com/apache/cassandra-gocql-driver/v2 v2.0.0/go.mod h1:QH/asJjB3mHvY6Dot6ZKMMpTcOrWJ8i9GhsvG1g0PK4=
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/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.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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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 h1:xzjEJAHum+mV5Dd5KyohRlCyP03o4yq6vNpEUtAJQzI=
github.com/psanford/memfs v0.0.0-20241019191636-4ef911798f9b/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI= 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 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ=
github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= 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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 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/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= 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 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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=

View File

@@ -9,7 +9,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/scylladb/go-reflectx" "github.com/scylladb/go-reflectx"
) )

View File

@@ -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

View File

@@ -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
}

View File

@@ -9,7 +9,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/scylladb/go-reflectx" "github.com/scylladb/go-reflectx"
) )

View File

@@ -14,7 +14,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"gopkg.in/inf.v0" "gopkg.in/inf.v0"
@@ -450,7 +450,7 @@ func TestIterxStruct(t *testing.T) {
} }
const insertStmt = `INSERT INTO struct_table ( const insertStmt = `INSERT INTO struct_table (
testuuid, testtimestamp, testvarchar, testbigint, testblob, testbool, testfloat, testdouble, testuuid, testtimestamp, testvarchar, testbigint, testblob, testbool, testfloat, testdouble,
testint, testdecimal, testlist, testset, testmap, testvarint, testinet, testcustom, testudt, testptrudt testint, testdecimal, testlist, testset, testmap, testvarint, testinet, testcustom, testudt, testptrudt
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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)
}
}

View File

@@ -1,9 +0,0 @@
package migrate
func IsCallback(stmt string) (name string) {
return isCallback(stmt)
}
func IsComment(stmt string) bool {
return isComment(stmt)
}

View File

@@ -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) == ""
}

View File

@@ -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)
}
}

View File

@@ -1 +0,0 @@
file

View File

@@ -12,7 +12,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/scylladb/go-reflectx" "github.com/scylladb/go-reflectx"
) )
@@ -99,6 +99,8 @@ type Queryx struct {
strict bool strict bool
} }
func (q *Queryx) Release() {}
// Query creates a new Queryx from gocql.Query using a default mapper. // Query creates a new Queryx from gocql.Query using a default mapper.
// //
// Deprecated: Use gocqlx.Session.Query API instead. // 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 // GetRequestTimeout returns time driver waits for single server response
// This timeout is applied to preparing statement request and for query execution requests // This timeout is applied to preparing statement request and for query execution requests
func (q *Queryx) GetRequestTimeout() time.Duration { func (q *Queryx) GetRequestTimeout() time.Duration {
return q.Query.GetRequestTimeout() return 0
} }
// SetRequestTimeout sets time driver waits for server to respond // SetRequestTimeout sets time driver waits for server to respond
// This timeout is applied to preparing statement request and for query execution requests // This timeout is applied to preparing statement request and for query execution requests
func (q *Queryx) SetRequestTimeout(timeout time.Duration) *Queryx { func (q *Queryx) SetRequestTimeout(timeout time.Duration) *Queryx {
q.Query.SetRequestTimeout(timeout) // q.Query.SetRequestTimeout(timeout)
return q return q
} }

View File

@@ -7,7 +7,7 @@ package gocqlx_test
import ( import (
"testing" "testing"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/scylladb/gocqlx/v3" "github.com/scylladb/gocqlx/v3"
) )

View File

@@ -8,7 +8,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
) )

View File

@@ -7,7 +7,7 @@ package gocqlx
import ( import (
"context" "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 // This file contains wrappers around gocql.Query that make Queryx expose the

View File

@@ -7,7 +7,7 @@ package gocqlx
import ( import (
"context" "context"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/scylladb/go-reflectx" "github.com/scylladb/go-reflectx"
) )

8002
testdata/people.json vendored

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ package gocqlx
import ( import (
"reflect" "reflect"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
) )
// Transformer transforms the value of the named parameter to another value. // Transformer transforms the value of the named parameter to another value.

2
udt.go
View File

@@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/gocql/gocql" gocql "github.com/apache/cassandra-gocql-driver/v2"
"github.com/scylladb/go-reflectx" "github.com/scylladb/go-reflectx"
) )