Compare commits

..

11 Commits

Author SHA1 Message Date
84c58f45a3 Delete everything except query builder 2025-11-20 16:09:09 +01:00
renovate[bot]
d9ec9f889d chore(deps): update module github.com/scylladb/gocql to v1.17.0 (#357)
Some checks failed
Build / Build (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 20:55:31 -04:00
renovate[bot]
b5ec0d955f fix(deps): update module golang.org/x/sync to v0.18.0 (#356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-09 01:14:58 -04:00
Dmitry Kropachev
38001d64ac Update gocql version to v1.16.1 (#353)
* Update gocql version to v1.16.1

1. Update gocql to v1.16.1
2. Update golang to 1.25, since new gocql version requres it

* Update golangci to 2.5.0

It is needed since 1.64.8 does not support golang 1.25.
1. Update golangci to 2.5.0
2. Migrate from golangci config v1 to v2
3. Integrate fieldaligment to golangci
4. Drop fieldaligment from Makefile
5. Address complaints
2025-10-28 14:52:22 -04:00
Dmitry Kropachev
96f0b49bdd Update golang to 1.24.x and dependencies (#349) 2025-10-16 14:25:19 -04:00
Dmitry Kropachev
20fef0caf4 Make renovate to maintain only 2 PRs 2025-10-16 13:30:07 -04:00
Dmitry Kropachev
ce13945e3f Update README.md (#347)
* Update README.md

1. Remove test code, replace it with production code, t.Log -> log.
2. Add import part
3. Add gocqlx instalation step

* Fix typo in installation instructions
2025-09-17 07:41:11 -04:00
renovate[bot]
acb61eb06f fix(deps): update module github.com/scylladb/gocqlx/v3 to v3.0.4 (#345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 22:48:29 -04:00
Dmitry Kropachev
cdd1c07b35 Add claude files to .gitignore 2025-09-16 22:34:31 -04:00
Dmitry Kropachev
2dd6668d11 Add renovate.json 2025-09-16 22:34:11 -04:00
Fuad Hasan
101d498909 Add comment handling and related tests for migration functionality (#344) 2025-09-13 10:04:04 -04:00
55 changed files with 174 additions and 10645 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.20
uses: actions/setup-go@v6
with:
go-version: "1.20"
- 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

4
.gitignore vendored
View File

@@ -17,3 +17,7 @@ vendor
cmd/schemagen/schemagen cmd/schemagen/schemagen
cmd/schemagen/testdata/go.sum cmd/schemagen/testdata/go.sum
# Claude / AI assistant metadata
CLAUDE.md
.claude/

View File

@@ -1,51 +1,32 @@
run: version: "2"
deadline: 5m
tests: true
allow-parallel-runners: true
modules-download-mode: readonly
build-tags: [ all, integration ]
linters-settings: formatters:
revive: enable:
rules: - goimports
- name: package-comments
disabled: true settings:
goimports: goimports:
local-prefixes: github.com/scylladb/gocqlx local-prefixes:
gofumpt: - github.com/scylladb/gocqlx
extra-rules: true golines:
govet: max-len: 120
enable-all: true gofumpt:
disable: extra-rules: true
- shadow
errcheck:
check-blank: true
gocognit:
min-complexity: 50
gocritic:
enabled-tags:
- diagnostic
- performance
- style
disabled-checks:
- commentedOutCode
- evalOrder
- hugeParam
- importShadow
- yodaStyleExpr
- whyNoLint
lll:
line-length: 180
linters: linters:
disable-all: true exclusions:
rules:
- path: '(.+)_test\.go'
text: "fieldalignment"
linters:
- govet
default: none
enable: enable:
- nolintlint
- errcheck - errcheck
- gocritic - gocritic
- gofumpt
- goheader - goheader
- goimports
- gosimple
- govet - govet
- ineffassign - ineffassign
- lll - lll
@@ -55,26 +36,41 @@ linters:
- staticcheck - staticcheck
- thelper - thelper
- tparallel - tparallel
- typecheck
- unused - unused
- forbidigo - forbidigo
settings:
revive:
rules:
- name: package-comments
disabled: true
govet:
enable-all: true
disable:
- shadow
errcheck:
check-blank: false
gocognit:
min-complexity: 50
gocritic:
enabled-tags:
- diagnostic
- performance
- style
disabled-checks:
- commentedOutCode
- evalOrder
- hugeParam
- importShadow
- yodaStyleExpr
- whyNoLint
lll:
line-length: 180
nolintlint:
allow-no-explanation: [ golines ]
require-explanation: true
require-specific: true
issues: run:
new: true build-tags:
new-from-rev: origin/master - integration
exclude-use-default: false - all
exclude:
- composite literal uses unkeyed fields
- Error return value of `.+\.Close` is not checked
- method Json should be JSON
exclude-rules:
- path: (.*_test.go|migrate/example|gocqlxtest/)
linters:
- fieldalignment
- govet
- errcheck
- path: doc_test.go
linters:
- unused
- revive

View File

@@ -24,8 +24,7 @@ export PATH := $(GOBIN):$(PATH)
GOOS := $(shell uname | tr '[:upper:]' '[:lower:]') GOOS := $(shell uname | tr '[:upper:]' '[:lower:]')
GOARCH := $(shell go env GOARCH) GOARCH := $(shell go env GOARCH)
GOLANGCI_VERSION := 1.64.8 GOLANGCI_VERSION := 2.5.0
FIELDALIGNMENT_VERSION := 0.24.0
ifeq ($(GOARCH),arm64) ifeq ($(GOARCH),arm64)
GOLANGCI_DOWNLOAD_URL := "https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_VERSION)/golangci-lint-$(GOLANGCI_VERSION)-$(GOOS)-arm64.tar.gz" GOLANGCI_DOWNLOAD_URL := "https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_VERSION)/golangci-lint-$(GOLANGCI_VERSION)-$(GOOS)-arm64.tar.gz"
@@ -97,7 +96,6 @@ get-deps:
get-tools: get-tools:
@echo "==> Installing tools at $(GOBIN)..." @echo "==> Installing tools at $(GOBIN)..."
@$(MAKE) install-golangci-lint @$(MAKE) install-golangci-lint
@$(MAKE) install-fieldalignment
.require-golangci-lint: .require-golangci-lint:
ifeq ($(shell if golangci-lint --version 2>/dev/null | grep ${GOLANGCI_VERSION} 1>/dev/null 2>&1; then echo "ok"; else echo "need-install"; fi), need-install) ifeq ($(shell if golangci-lint --version 2>/dev/null | grep ${GOLANGCI_VERSION} 1>/dev/null 2>&1; then echo "ok"; else echo "need-install"; fi), need-install)
@@ -108,15 +106,6 @@ install-golangci-lint:
@echo "==> Installing golangci-lint ${GOLANGCI_VERSION} at $(GOBIN)..." @echo "==> Installing golangci-lint ${GOLANGCI_VERSION} at $(GOBIN)..."
$(call dl_tgz,golangci-lint,$(GOLANGCI_DOWNLOAD_URL)) $(call dl_tgz,golangci-lint,$(GOLANGCI_DOWNLOAD_URL))
.require-fieldalignment:
ifeq ($(shell if fieldalignment -V=full 1>/dev/null 2>&1; then echo "ok"; else echo "need-install"; fi), need-install)
$(MAKE) install-golangci-lint
endif
install-fieldalignment:
@echo "==> Installing fieldalignment ${FIELDALIGNMENT_VERSION} at $(GOBIN)..."
@go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@v${FIELDALIGNMENT_VERSION}
define dl_tgz define dl_tgz
@mkdir "$(GOBIN)" 2>/dev/null || true @mkdir "$(GOBIN)" 2>/dev/null || true
@echo "Downloading $(GOBIN)/$(1)"; @echo "Downloading $(GOBIN)/$(1)";

View File

@@ -11,7 +11,7 @@ If you are using GocqlX v3.0.0 or newer, you must ensure your `go.mod` includes
```go ```go
// Use the latest version of scylladb/gocql; check for updates at https://github.com/scylladb/gocql/releases // Use the latest version of scylladb/gocql; check for updates at https://github.com/scylladb/gocql/releases
replace github.com/gocql/gocql => github.com/scylladb/gocql v1.15.3 replace github.com/gocql/gocql => github.com/scylladb/gocql v1.16.0
``` ```
This is required because GocqlX relies on ScyllaDB-specific extensions and bug fixes introduced in the gocql fork. Attempting to use the standard gocql driver with GocqlX v3.0.0+ may lead to build or runtime issues. This is required because GocqlX relies on ScyllaDB-specific extensions and bug fixes introduced in the gocql fork. Attempting to use the standard gocql driver with GocqlX v3.0.0+ may lead to build or runtime issues.
@@ -30,8 +30,18 @@ Subpackages provide additional functionality:
* CRUD operations based on table model ([package table](https://github.com/scylladb/gocqlx/blob/master/table)) * CRUD operations based on table model ([package table](https://github.com/scylladb/gocqlx/blob/master/table))
* Database migrations ([package migrate](https://github.com/scylladb/gocqlx/blob/master/migrate)) * Database migrations ([package migrate](https://github.com/scylladb/gocqlx/blob/master/migrate))
## Installation ## Installation GocqlX
Add GocqlX to your Go module:
```bash
go get github.com/scylladb/gocqlx/v3
```
## Installation schemagen
Unfortunately you can't install it via `go install`, since `go.mod` contains `replace` directive.
So, you have to check it out and install manually:
```bash ```bash
git clone git@github.com:scylladb/gocqlx.git git clone git@github.com:scylladb/gocqlx.git
cd gocqlx/cmd/schemagen/ cd gocqlx/cmd/schemagen/
@@ -40,6 +50,20 @@ go install .
## Getting started ## Getting started
First, import the required packages:
```go
import (
"fmt"
"log"
"github.com/gocql/gocql"
"github.com/scylladb/gocqlx/v3"
"github.com/scylladb/gocqlx/v3/qb"
"github.com/scylladb/gocqlx/v3/table"
)
```
Wrap gocql Session: Wrap gocql Session:
```go ```go
@@ -48,8 +72,9 @@ cluster := gocql.NewCluster(hosts...)
// Wrap session on creation, gocqlx session embeds gocql.Session pointer. // Wrap session on creation, gocqlx session embeds gocql.Session pointer.
session, err := gocqlx.WrapSession(cluster.CreateSession()) session, err := gocqlx.WrapSession(cluster.CreateSession())
if err != nil { if err != nil {
t.Fatal(err) log.Fatal(err)
} }
defer session.Close()
``` ```
Specify table model: Specify table model:
@@ -90,7 +115,7 @@ p := Person{
} }
q := session.Query(personTable.Insert()).BindStruct(p) q := session.Query(personTable.Insert()).BindStruct(p)
if err := q.ExecRelease(); err != nil { if err := q.ExecRelease(); err != nil {
t.Fatal(err) log.Fatal(err)
} }
``` ```
@@ -104,9 +129,9 @@ p := Person{
} }
q := session.Query(personTable.Get()).BindStruct(p) q := session.Query(personTable.Get()).BindStruct(p)
if err := q.GetRelease(&p); err != nil { if err := q.GetRelease(&p); err != nil {
t.Fatal(err) log.Fatal(err)
} }
t.Log(p) fmt.Println(p)
// stdout: {Michał Matczuk [michal@scylladb.com]} // stdout: {Michał Matczuk [michal@scylladb.com]}
``` ```
@@ -116,9 +141,9 @@ Load all rows in to a slice:
var people []Person var people []Person
q := session.Query(personTable.Select()).BindMap(qb.M{"first_name": "Michał"}) q := session.Query(personTable.Select()).BindMap(qb.M{"first_name": "Michał"})
if err := q.SelectRelease(&people); err != nil { if err := q.SelectRelease(&people); err != nil {
t.Fatal(err) log.Fatal(err)
} }
t.Log(people) fmt.Println(people)
// stdout: [{Michał Matczuk [michal@scylladb.com]}] // stdout: [{Michał Matczuk [michal@scylladb.com]}]
``` ```

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

@@ -162,12 +162,13 @@ func BenchmarkBaseGocqlSelect(b *testing.B) {
v := make([]*benchPerson, 100) v := make([]*benchPerson, 100)
p := new(benchPerson) p := new(benchPerson)
for iter.Scan(&p.ID, &p.FirstName, &p.LastName, &p.Email, &p.Gender, &p.IPAddress) { for iter.Scan(&p.ID, &p.FirstName, &p.LastName, &p.Email, &p.Gender, &p.IPAddress) {
v = append(v, p) // nolint:staticcheck v = append(v, p)
p = new(benchPerson) p = new(benchPerson)
} }
if err := iter.Close(); err != nil { if err := iter.Close(); err != nil {
b.Fatal(err) b.Fatal(err)
} }
_ = v
} }
} }

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.21
require (
github.com/gocql/gocql v1.7.0
github.com/google/go-cmp v0.7.0
github.com/scylladb/gocqlx/v3 v3.0.0
)
require (
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // 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.15.3
github.com/scylladb/gocqlx/v3 => ../../..
)

View File

@@ -1,53 +0,0 @@
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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.4.0/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/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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.15.1 h1:t75NkDFys0XxipPsnTrSEbwx8B8R/jTUt5OAY9W7i+c=
github.com/scylladb/gocql v1.15.1/go.mod h1:+rInt+HjERaMEYC4N8LocQQEAdREhYKU4QPkE00K5dA=
github.com/scylladb/gocql v1.15.2 h1:Vv7iaIyTMMjMtux1INQMi0waH8j8O/ppKS6JcM1vh14=
github.com/scylladb/gocql v1.15.2/go.mod h1:+rInt+HjERaMEYC4N8LocQQEAdREhYKU4QPkE00K5dA=
github.com/scylladb/gocql v1.15.3/go.mod h1:+rInt+HjERaMEYC4N8LocQQEAdREhYKU4QPkE00K5dA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

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"
@@ -29,6 +29,8 @@ func ExampleUDT() {
FirstName string FirstName string
LastName string LastName string
} }
_ = FullName{}
} }
func ExampleUDT_wraper() { func ExampleUDT_wraper() {
@@ -42,4 +44,6 @@ func ExampleUDT_wraper() {
gocqlx.UDT gocqlx.UDT
*FullName *FullName
} }
_ = FullNameUDT{}
} }

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"
@@ -35,7 +35,7 @@ func TestExample(t *testing.T) {
} }
defer session.Close() defer session.Close()
session.ExecStmt(`DROP KEYSPACE examples`) _ = session.ExecStmt(`DROP KEYSPACE examples`)
basicCreateAndPopulateKeyspace(t, session, "examples") basicCreateAndPopulateKeyspace(t, session, "examples")
createAndPopulateKeyspaceAllTypes(t, session) createAndPopulateKeyspaceAllTypes(t, session)
@@ -270,7 +270,7 @@ func createAndPopulateKeyspaceAllTypes(t *testing.T, session gocqlx.Session) {
copy(byteID[:], id) copy(byteID[:], id)
date := time.Date(2021, time.December, 11, 10, 23, 0, 0, time.UTC) date := time.Date(2021, time.December, 11, 10, 23, 0, 0, time.UTC)
var double float64 = 1.2 // nolint:revive var double float64 = 1.2 //nolint:staticcheck // type needs to be enforces
var float float32 = 1.3 var float float32 = 1.3
var integer int32 = 123 var integer int32 = 123
listInt := []int32{1, 2, 3} listInt := []int32{1, 2, 3}

13
go.mod
View File

@@ -1,19 +1,16 @@
module github.com/scylladb/gocqlx/v3 module github.com/scylladb/gocqlx/v3
go 1.21 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
golang.org/x/sync v0.11.0 golang.org/x/sync v0.18.0
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.17.9 // indirect
)
replace github.com/gocql/gocql => github.com/scylladb/gocql v1.15.3 replace github.com/gocql/gocql => github.com/scylladb/gocql v1.17.0

60
go.sum
View File

@@ -1,54 +1,38 @@
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/apache/cassandra-gocql-driver/v2 v2.0.0 h1:Omnzb1Z/P90Dr2TbVNu54ICQL7TKVIIsJO231w484HU=
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/go.mod h1:QH/asJjB3mHvY6Dot6ZKMMpTcOrWJ8i9GhsvG1g0PK4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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.15.3 h1:0vJT5pm7g5v8/pCs3tuXuRAfSRWvc1kib8J846Z+Z4g= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/scylladb/gocql v1.15.3/go.mod h1:+rInt+HjERaMEYC4N8LocQQEAdREhYKU4QPkE00K5dA= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

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"
) )
@@ -20,15 +20,15 @@ func structOnlyError(t reflect.Type) error {
return fmt.Errorf("expected a struct but got %s", t.Kind()) return fmt.Errorf("expected a struct but got %s", t.Kind())
} }
if isUnmarshaller := reflect.PtrTo(t).Implements(unmarshallerInterface); isUnmarshaller { if isUnmarshaller := reflect.PointerTo(t).Implements(unmarshallerInterface); isUnmarshaller {
return fmt.Errorf("expected a struct but the provided struct type %s implements gocql.Unmarshaler", t.Name()) return fmt.Errorf("expected a struct but the provided struct type %s implements gocql.Unmarshaler", t.Name())
} }
if isUDTUnmarshaller := reflect.PtrTo(t).Implements(udtUnmarshallerInterface); isUDTUnmarshaller { if isUDTUnmarshaller := reflect.PointerTo(t).Implements(udtUnmarshallerInterface); isUDTUnmarshaller {
return fmt.Errorf("expected a struct but the provided struct type %s implements gocql.UDTUnmarshaler", t.Name()) return fmt.Errorf("expected a struct but the provided struct type %s implements gocql.UDTUnmarshaler", t.Name())
} }
if isAutoUDT := reflect.PtrTo(t).Implements(autoUDTInterface); isAutoUDT { if isAutoUDT := reflect.PointerTo(t).Implements(autoUDTInterface); isAutoUDT {
return fmt.Errorf("expected a struct but the provided struct type %s implements gocqlx.UDT", t.Name()) return fmt.Errorf("expected a struct but the provided struct type %s implements gocqlx.UDT", t.Name())
} }

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"
) )
@@ -62,7 +62,7 @@ func (iter *Iterx) StructOnly() *Iterx {
// If no rows were selected, ErrNotFound is returned. // If no rows were selected, ErrNotFound is returned.
func (iter *Iterx) Get(dest interface{}) error { func (iter *Iterx) Get(dest interface{}) error {
iter.scanAny(dest) iter.scanAny(dest)
iter.Close() _ = iter.Close()
return iter.checkErrAndNotFound() return iter.checkErrAndNotFound()
} }
@@ -119,7 +119,7 @@ func (iter *Iterx) scanAny(dest interface{}) bool {
// If no rows were selected, ErrNotFound is NOT returned. // If no rows were selected, ErrNotFound is NOT returned.
func (iter *Iterx) Select(dest interface{}) error { func (iter *Iterx) Select(dest interface{}) error {
iter.scanAll(dest) iter.scanAll(dest)
iter.Close() _ = iter.Close()
return iter.err return iter.err
} }
@@ -209,7 +209,7 @@ func (iter *Iterx) scanAll(dest interface{}) bool {
// - it is not a struct // - it is not a struct
// - it has no exported fields // - it has no exported fields
func (iter *Iterx) isScannable(t reflect.Type) bool { func (iter *Iterx) isScannable(t reflect.Type) bool {
ptr := reflect.PtrTo(t) ptr := reflect.PointerTo(t)
switch { switch {
case ptr.Implements(unmarshallerInterface): case ptr.Implements(unmarshallerInterface):
return true return true
@@ -260,7 +260,7 @@ func (iter *Iterx) structScan(value reflect.Value) bool {
} }
if iter.fields == nil { if iter.fields == nil {
columns := columnNames(iter.Iter.Columns()) columns := columnNames(iter.Columns())
cas := len(columns) > 0 && columns[0] == appliedColumn cas := len(columns) > 0 && columns[0] == appliedColumn
iter.fields = iter.Mapper.TraversalsByName(value.Type(), columns) iter.fields = iter.Mapper.TraversalsByName(value.Type(), columns)
@@ -342,7 +342,7 @@ func (iter *Iterx) Close() error {
func (iter *Iterx) checkErrAndNotFound() error { func (iter *Iterx) checkErrAndNotFound() error {
if iter.err != nil { if iter.err != nil {
return iter.err return iter.err
} else if iter.Iter.NumRows() == 0 { } else if iter.NumRows() == 0 {
return gocql.ErrNotFound return gocql.ErrNotFound
} }
return nil return nil

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"
@@ -260,7 +260,7 @@ func TestIterxUDT(t *testing.T) {
} }
t.Cleanup(func() { t.Cleanup(func() {
session.Query(deleteStmt, nil).Bind(testuuid).ExecRelease() // nolint:errcheck _ = session.Query(deleteStmt, nil).Bind(testuuid).ExecRelease()
}) })
t.Run("insert-bind", func(t *testing.T) { t.Run("insert-bind", func(t *testing.T) {
@@ -930,7 +930,9 @@ func TestIterxPaging(t *testing.T) {
AllowFiltering(). AllowFiltering().
Columns("id", "val").ToCql() Columns("id", "val").ToCql()
iter := session.Query(stmt, names).Bind(100).PageSize(10).Iter() iter := session.Query(stmt, names).Bind(100).PageSize(10).Iter()
defer iter.Close() defer func() {
_ = iter.Close()
}()
var cnt int var cnt int
for { for {

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,34 +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 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,5 +0,0 @@
package migrate
func IsCallback(stmt string) (name string) {
return isCallback(stmt)
}

View File

@@ -1,317 +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
}
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)
if cb := isCallback(stmt); cb != "" {
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 {
q := session.ContextQuery(ctx, stmt, nil).RetryPolicy(nil)
if err := q.ExecRelease(); err != nil {
return fmt.Errorf("statement %d: %s", i, err)
}
}
// 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]
}

View File

@@ -1,319 +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(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(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(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(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(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(4)
writeFile(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(0)
f.WriteFile("0.cql", []byte(fmt.Sprintf(insertMigrate, 0)+";"+fmt.Sprintf(insertMigrate, 1)), fs.ModePerm)
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 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 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(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(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(4)
writeFile(f, 4, "\n-- CALL Foo;\n")
writeFile(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(n int) *memfs.FS {
f := memfs.New()
for i := 0; i < n; i++ {
writeFile(f, i, fmt.Sprintf(insertMigrate, i)+";")
}
return f
}
func writeFile(f *memfs.FS, i int, text string) {
f.WriteFile(fmt.Sprint(i, ".cql"), []byte(text), fs.ModePerm)
}

View File

@@ -1 +0,0 @@
file

View File

@@ -132,7 +132,7 @@ func (b *SelectBuilder) From(table string) *SelectBuilder {
} }
// Json sets the clause of the query. // Json sets the clause of the query.
func (b *SelectBuilder) Json() *SelectBuilder { // nolint: revive func (b *SelectBuilder) Json() *SelectBuilder {
b.json = true b.json = true
return b return b
} }

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
} }
@@ -181,7 +183,7 @@ func (q *Queryx) bindStructArgs(arg0 interface{}, arg1 map[string]interface{}) (
err := q.Mapper.TraversalsByNameFunc(v.Type(), q.Names, func(i int, t []int) error { err := q.Mapper.TraversalsByNameFunc(v.Type(), q.Names, func(i int, t []int) error {
if len(t) != 0 { if len(t) != 0 {
val := reflectx.FieldByIndexesReadOnly(v, t) // nolint:scopelint val := reflectx.FieldByIndexesReadOnly(v, t)
arglist = append(arglist, val.Interface()) arglist = append(arglist, val.Interface())
} else { } else {
val, ok := arg1[q.Names[i]] val, ok := arg1[q.Names[i]]

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"
) )
@@ -16,7 +16,10 @@ func BenchmarkCompileNamedQuery(b *testing.B) {
q := []byte("INSERT INTO cycling.cyclist_name (id, user_uuid, firstname, stars) VALUES (:id, :user_uuid, :firstname, :stars)") q := []byte("INSERT INTO cycling.cyclist_name (id, user_uuid, firstname, stars) VALUES (:id, :user_uuid, :firstname, :stars)")
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
gocqlx.CompileNamedQuery(q) _, _, err := gocqlx.CompileNamedQuery(q)
if err != nil {
b.Fatal(err)
}
} }
} }

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

@@ -3,51 +3,5 @@
"extends": [ "extends": [
"config:recommended" "config:recommended"
], ],
"schedule": [ "prConcurrentLimit": 2
"every weekend"
],
"labels": [
"renovate",
"dependencies"
],
"enabledManagers": [
"gomod",
"github-actions",
"helm-values"
],
"packageRules": [
{
"groupName": "All dependencies",
"groupSlug": "all-dependencies",
"matchManagers": [
"gomod"
],
"matchPackageNames": [
"!github.com/scylladb/gocql"
],
"enabled": true
},
{
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch",
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
],
"enabled": false
},
{
"matchPackageNames": [
"github.com/scylladb/gocql"
],
"enabled": true
}
],
"vulnerabilityAlerts": {
"enabled": true
},
"osvVulnerabilityAlerts": true
} }

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

View File

@@ -37,7 +37,7 @@ type Table struct {
} }
// New creates new Table based on table schema read from Metadata. // New creates new Table based on table schema read from Metadata.
func New(m Metadata) *Table { // nolint: gocritic func New(m Metadata) *Table {
t := &Table{ t := &Table{
metadata: m, metadata: m,
} }

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