From 4d8ea27d11250af8f90752d808a015b6a93b0d21 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 20 Feb 2024 17:15:47 -0600 Subject: [PATCH 01/32] go-sdk: support for rowset with metadata v1.0.2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f9ff29..72eb78c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# SQLite Cloud Client SDK for Go +# SQLite Cloud Client SDK for Go v1.0.2 The SQLite Cloud Client SDK for Go (sqlitecloud/go-sdk) is the Go Programming Language application programmer's interface to [SQLite Cloud](https://sqlitecloud.io/). It is a set of library functions that allow client programs to pass queries and SQL commands to the SQLite Cloud backend server and to receive the results of these queries. In addition to the standard SQLite statements, several other [commands](https://docs.sqlitecloud.io/docs/commands) are supported. From 982e6f290b39b1ad4ed92c080b3834306a46ff03 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Mon, 10 Jun 2024 08:38:01 +0000 Subject: [PATCH 02/32] chore: remove symlink --- connection.go | 4 ++-- src | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 120000 src diff --git a/connection.go b/connection.go index 4ab63ed..599c5a1 100644 --- a/connection.go +++ b/connection.go @@ -75,7 +75,7 @@ func New(config SQCloudConfig) *SQCloud { return &SQCloud{SQCloudConfig: config} } -// init registers the sqlitecloud scheme in the connection steing parser. +// init registers the sqlitecloud scheme in the connection string parser. func init() { dburl.Register(dburl.Scheme{ Driver: "sc", // sqlitecloud @@ -186,7 +186,7 @@ func ParseTlsString(tlsconf string) (secure bool, tlsInsecureSkipVerify bool, pe // If a given value does not fulfill the above criteria's, an error is returned. func (this *SQCloud) CheckConnectionParameter() error { if strings.TrimSpace(this.Host) == "" { - return errors.New(fmt.Sprintf("Invalid hostname (%s)", this.Host)) + return fmt.Errorf("Invalid hostname (%s)", this.Host) } ip := net.ParseIP(this.Host) diff --git a/src b/src deleted file mode 120000 index 945c9b4..0000000 --- a/src +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file From 980753cd794d3f16694421cff96bb7529ff20a4b Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Wed, 12 Jun 2024 08:00:37 +0000 Subject: [PATCH 03/32] feat(connection): implement available parameters See: https://github.com/sqlitecloud/sqlitecloud-js/issues/68 --- .devcontainer/devcontainer.json | 22 ++ .env.example | 7 + .github/dependabot.yml | 12 ++ .gitignore | 3 + .vscode/settings.json | 5 + cli/sqlc.go | 2 +- connection.go | 187 ++++++++++++----- server.go | 21 +- test/commons_test.go | 27 +++ test/compress_test.go | 7 +- test/go.mod | 9 +- test/go.sum | 12 ++ test/literals_test.go | 5 +- test/pubsub_test.go | 9 +- test/selectarray_test.go | 11 +- test/server_test.go | 14 +- test/test_commons.go | 23 --- test/unit/connection_test.go | 347 ++++++++++++++++++++++++++++++++ 18 files changed, 632 insertions(+), 91 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .env.example create mode 100644 .github/dependabot.yml create mode 100644 .vscode/settings.json create mode 100644 test/commons_test.go delete mode 100644 test/test_commons.go create mode 100644 test/unit/connection_test.go diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6dc2589 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go +{ + "name": "Go", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/go:1-1-bullseye" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d75af5e --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +SQLITE_CONNECTION_STRING=sqlitecloud://myhost.sqlite.cloud +SQLITE_USER=admin +SQLITE_PASSWORD= +SQLITE_API_KEY= +SQLITE_HOST=myhost.sqlite.cloud +SQLITE_DB=chinook.sqlite +SQLITE_PORT=8860 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.gitignore b/.gitignore index 3004869..7552c31 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ cli/.DS_Store .DS_Store cli/.vscode .vscode/launch.json +pkg/ +bin +.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b8e81c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "gopls": { + "ui.semanticTokens": true + } +} \ No newline at end of file diff --git a/cli/sqlc.go b/cli/sqlc.go index 3d30302..493b632 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -514,7 +514,7 @@ func main() { case ".timeout": parameter.Timeout = getNextTokenValueAsInteger(out, parameter.Timeout, 10, tokens, ¶meter) case ".compress": - parameter.Compress = getNextTokenValueAsString(out, parameter.Compress, "NO", "|no|lz4|", tokens, ¶meter) + parameter.Compress = getNextTokenValueAsString(out, parameter.Compress, sqlitecloud.CompressModeNo, "|no|lz4|", tokens, ¶meter) db.Compress(parameter.Compress) case ".format": diff --git a/connection.go b/connection.go index 599c5a1..ac2f76d 100644 --- a/connection.go +++ b/connection.go @@ -37,11 +37,20 @@ type SQCloudConfig struct { Username string Password string Database string - Timeout time.Duration - CompressMode string - Secure bool - TlsInsecureSkipVerify bool - Pem string + PasswordHashed bool // Password is hashed + Timeout time.Duration // Optional query timeout passed directly to TLS socket + CompressMode string // eg: LZ4 + Compression bool // Enable compression + Zerotext bool // Tell the server to zero-terminate strings + Memory bool // Database will be created in memory + Create bool // Create the database if it doesn't exist? + Secure bool // Connect using plain TCP port, without TLS encryption, NOT RECOMMENDED (insecure) + NonLinearizable bool // Request for immediate responses from the server node without waiting for linerizability guarantees + TlsInsecureSkipVerify bool // Accept invalid TLS certificates (no_verify_certificate) + Pem string // TODO: ? + RootCertificate string + ClientCertificate string + ClientCertificateKey string ApiKey string NoBlob bool // flag to tell the server to not send BLOB columns MaxData int // value to tell the server to not send columns with more than max_data bytes @@ -69,6 +78,9 @@ type SQCloud struct { ErrorMessage string } +const CompressModeNo = "NO" +const CompressModeLZ4 = "LZ4" + const SQLiteCloudCA = "SQLiteCloudCA" func New(config SQCloudConfig) *SQCloud { @@ -104,15 +116,23 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err config.Password, _ = u.User.Password() config.Database = strings.TrimPrefix(u.Path, "/") config.Timeout = 0 - config.CompressMode = "NO" + config.Compression = false + config.CompressMode = CompressModeNo + config.Zerotext = false + config.Memory = false + config.Create = false config.Secure = true + config.NonLinearizable = false config.TlsInsecureSkipVerify = false config.Pem = "" - config.ApiKey = "" + config.RootCertificate = "" + config.ClientCertificate = "" + config.ClientCertificateKey = "" config.NoBlob = false config.MaxData = 0 config.MaxRows = 0 config.MaxRowset = 0 + config.ApiKey = "" sPort := strings.TrimSpace(u.Port()) if len(sPort) > 0 { @@ -133,8 +153,50 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err case "compress": config.CompressMode = strings.ToUpper(lastLiteral) + if config.CompressMode == CompressModeLZ4 { + config.Compression = true + } + case "compression": + if b, err := parseBool(lastLiteral, config.Compression); err == nil && b { + config.Compression = true + config.CompressMode = CompressModeLZ4 + } + case "zerotext": + if b, err := parseBool(lastLiteral, config.Zerotext); err == nil { + config.Zerotext = b + } + case "memory": + if b, err := parseBool(lastLiteral, config.Memory); err == nil { + config.Memory = b + } + case "create": + if b, err := parseBool(lastLiteral, config.Create); err == nil { + config.Create = b + } + case "secure": + if b, err := parseBool(lastLiteral, config.Secure); err == nil { + config.Secure = b + } + case "insecure": + if b, err := parseBool(lastLiteral, config.Secure); err == nil { + config.Secure = !b + } + case "non_linearizable", "nonlinearizable": + if b, err := parseBool(lastLiteral, config.NonLinearizable); err == nil { + config.NonLinearizable = b + } + case "no_verify_certificate": + if b, err := parseBool(lastLiteral, config.TlsInsecureSkipVerify); err == nil { + config.TlsInsecureSkipVerify = b + } case "tls": config.Secure, config.TlsInsecureSkipVerify, config.Pem = ParseTlsString(lastLiteral) + case "root_certificate": + config.RootCertificate = lastLiteral + case "client_certificate": + config.ClientCertificate = lastLiteral + case "client_certificate_key": + config.ClientCertificateKey = lastLiteral case "apikey": config.ApiKey = lastLiteral case "noblob": @@ -210,8 +272,8 @@ func (this *SQCloud) CheckConnectionParameter() error { return errors.New(fmt.Sprintf("Invalid Timeout (%s)", this.Timeout.String())) } - switch strings.ToUpper(this.CompressMode) { - case "NO", "LZ4": + switch this.CompressMode { + case CompressModeNo, CompressModeLZ4: default: return errors.New(fmt.Sprintf("Invalid compression method (%s)", this.CompressMode)) } @@ -340,45 +402,7 @@ func (this *SQCloud) reconnect() error { } } - commands := "" - args := []interface{}{} - - if strings.TrimSpace(this.Username) != "" { - c, a := authCommand(this.Username, this.Password) - commands += c - args = append(args, a...) - - } else if strings.TrimSpace(this.ApiKey) != "" { - c, a := authWithKeyCommand(this.ApiKey) - commands += c - args = append(args, a...) - } - - if strings.TrimSpace(this.Database) != "" { - c, a := useDatabaseCommand(this.Database) - commands += c - args = append(args, a...) - } - - if this.NoBlob { - commands += noblobCommand(this.NoBlob) - } - - if this.MaxData > 0 { - commands += maxdataCommand(this.MaxData) - } - - if this.MaxRows > 0 { - commands += maxrowsCommand(this.MaxRows) - } - - if this.MaxRowset > 0 { - commands += maxrowsetCommand(this.MaxRowset) - } - - if this.CompressMode != "NO" { - commands += compressCommand(this.CompressMode) - } + commands, args := connectionCommands(this.SQCloudConfig) if commands != "" { if len(args) > 0 { @@ -421,6 +445,53 @@ func (this *SQCloud) Close() error { return nil } +func connectionCommands(config SQCloudConfig) (string, []interface{}) { + buffer := "" + args := []interface{}{} + + // it must be executed before authentication command + if config.NonLinearizable { + buffer += nonlinearizableCommand(config.NonLinearizable) + } + + if config.ApiKey != "" { + c, a := authWithKeyCommand(config.ApiKey) + buffer += c + args = append(args, a...) + } + + if config.Username != "" && config.Password != "" { + c, a := authCommand(config.Username, config.Password, config.PasswordHashed) + buffer += c + args = append(args, a...) + } + + if config.Database != "" { + create := config.Create && !config.Memory + c, a := useDatabaseCommand(config.Database, create) + buffer += c + args = append(args, a...) + } + + buffer += compressCommand(config.CompressMode) + + if config.Zerotext { + buffer += zerotextCommand(config.Zerotext) + } + + if config.NoBlob { + buffer += noblobCommand(config.NoBlob) + } + + buffer += maxdataCommand(config.MaxData) + + buffer += maxrowsCommand(config.MaxRows) + + buffer += maxrowsetCommand(config.MaxRowset) + + return buffer, args +} + func noblobCommand(NoBlob bool) string { if NoBlob { return "SET CLIENT KEY NOBLOB TO 1;" @@ -443,15 +514,31 @@ func maxrowsetCommand(v int) string { func compressCommand(CompressMode string) string { switch compression := strings.ToUpper(CompressMode); { - case compression == "NO": + case compression == CompressModeNo: return "SET CLIENT KEY COMPRESSION TO 0;" - case compression == "LZ4": + case compression == CompressModeLZ4: return "SET CLIENT KEY COMPRESSION TO 1;" default: return "" } } +func nonlinearizableCommand(NonLinearizable bool) string { + if NonLinearizable { + return "SET CLIENT KEY NONLINEARIZABLE TO 1;" + } else { + return "SET CLIENT KEY NONLINEARIZABLE TO 0;" + } +} + +func zerotextCommand(Zerotext bool) string { + if Zerotext { + return "SET CLIENT KEY ZEROTEXT TO 1;" + } else { + return "SET CLIENT KEY ZEROTEXT TO 0;" + } +} + // Compress enabled or disables data compression for this connection. // If enabled, the data is compressed with the LZ4 compression algorithm, otherwise no compression is applied the data. func (this *SQCloud) Compress(CompressMode string) error { diff --git a/server.go b/server.go index 1813142..5b48491 100644 --- a/server.go +++ b/server.go @@ -246,13 +246,18 @@ func (this *SQCloud) ListDatabaseConnections(Database string) ([]SQCloudConnecti // Auth Functions -func authCommand(Username string, Password string) (string, []interface{}) { - return "AUTH USER ? PASSWORD ?;", []interface{}{Username, Password} +func authCommand(Username string, Password string, IsPasswordHashed bool) (string, []interface{}) { + command := "PASSWORD" + if IsPasswordHashed { + command = "HASH" + } + + return fmt.Sprintf("AUTH USER ? %s ?;", command), []interface{}{Username, Password} } // Auth - INTERNAL SERVER COMMAND: Authenticates User with the given credentials. func (this *SQCloud) Auth(Username string, Password string) error { - return this.ExecuteArray(authCommand(Username, Password)) + return this.ExecuteArray(authCommand(Username, Password, false)) } func authWithKeyCommand(Key string) (string, []interface{}) { @@ -318,8 +323,12 @@ func (this *SQCloud) GetDatabase() (string, error) { return "", err } -func useDatabaseCommand(Database string) (string, []interface{}) { - return "USE DATABASE ?;", []interface{}{Database} +func useDatabaseCommand(Database string, create bool) (string, []interface{}) { + command := "USE DATABASE ?;" + if create { + command = "CREATE DATABASE ? IF NOT EXISTS;" + } + return command, []interface{}{Database} } // UseDatabase - INTERNAL SERVER COMMAND: Selects the specified Database for usage. @@ -327,7 +336,7 @@ func useDatabaseCommand(Database string) (string, []interface{}) { // An error is returned if the specified Database was not found or the user has not the necessary access rights to work with this Database. func (this *SQCloud) UseDatabase(Database string) error { this.Database = Database - return this.ExecuteArray(useDatabaseCommand(Database)) + return this.ExecuteArray(useDatabaseCommand(Database, false)) } // UseDatabase - INTERNAL SERVER COMMAND: Releases the actual Database. diff --git a/test/commons_test.go b/test/commons_test.go new file mode 100644 index 0000000..ca4fd5b --- /dev/null +++ b/test/commons_test.go @@ -0,0 +1,27 @@ +package test + +import ( + "flag" + "log" + "os" + + "github.com/joho/godotenv" +) + +func init() { + if err := godotenv.Load("./../.env"); err != nil { + log.Print("No .env file found") + } + + testConnectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + flag.StringVar(&testConnectionString, "server", testConnectionString, "Connection String") +} + +func contains[T comparable](s []T, e T) bool { + for _, v := range s { + if v == e { + return true + } + } + return false +} diff --git a/test/compress_test.go b/test/compress_test.go index 01955a0..44922f4 100644 --- a/test/compress_test.go +++ b/test/compress_test.go @@ -21,6 +21,7 @@ import ( "fmt" "math/rand" "net/url" + "os" "testing" sqlitecloud "github.com/sqlitecloud/go-sdk" @@ -31,7 +32,11 @@ const testCompressKey = "compress" const testCompressValue = "LZ4" func TestCompress(t *testing.T) { - url, err := url.Parse(testConnectionString) + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "?apikey=" + apikey + + url, err := url.Parse(connectionString) values := url.Query() values.Add(testCompressKey, testCompressValue) url.RawQuery = values.Encode() diff --git a/test/go.mod b/test/go.mod index 071503e..afb7862 100644 --- a/test/go.mod +++ b/test/go.mod @@ -2,15 +2,22 @@ module github.com/sqlitecloud/go-sdk/test go 1.18 -require github.com/sqlitecloud/go-sdk v0.0.0 +require ( + github.com/joho/godotenv v1.5.1 + github.com/sqlitecloud/go-sdk v0.0.0 + github.com/stretchr/testify v1.9.0 +) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xo/dburl v0.13.1 // indirect golang.org/x/sys v0.7.0 // indirect golang.org/x/term v0.6.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/sqlitecloud/go-sdk v0.0.0 => ../ diff --git a/test/go.sum b/test/go.sum index 9d229bd..12d63d6 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,7 +1,11 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -9,11 +13,19 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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= github.com/xo/dburl v0.13.1 h1:EV+BCdo539sc/mBrny0VxaEGLM0b1U0mJA9RpP80ux0= github.com/xo/dburl v0.13.1/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/literals_test.go b/test/literals_test.go index bbd4671..2ac679a 100644 --- a/test/literals_test.go +++ b/test/literals_test.go @@ -18,6 +18,7 @@ package test import ( + "os" "testing" sqlitecloud "github.com/sqlitecloud/go-sdk" @@ -31,7 +32,9 @@ func TestLiterals(t *testing.T) { var err error // start := time.Now() - if db, err = sqlitecloud.Connect(testConnectionString); err != nil { + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + if db, err = sqlitecloud.Connect(connectionString + "?apikey=" + apikey); err != nil { t.Fatal("CONNECT: ", err.Error()) } defer db.Close() diff --git a/test/pubsub_test.go b/test/pubsub_test.go index 21df12b..32fb425 100644 --- a/test/pubsub_test.go +++ b/test/pubsub_test.go @@ -20,6 +20,7 @@ package test import ( "encoding/json" "fmt" + "os" "reflect" "testing" "time" @@ -32,13 +33,17 @@ const testPubsubChannelName = "TestPubsubChannel" var testPubsubMessage = map[string]string{"msg_id": "12345", "msg_content": "this is the content"} func TestPubsub(t *testing.T) { - db1, err := sqlitecloud.Connect(testConnectionString) + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "?apikey=" + apikey + + db1, err := sqlitecloud.Connect(connectionString) if err != nil { t.Fatal("Connect 1: ", err.Error()) } defer db1.Close() - db2, err := sqlitecloud.Connect(testConnectionString) + db2, err := sqlitecloud.Connect(connectionString) if err != nil { t.Fatal("Connect 2: ", err.Error()) } diff --git a/test/selectarray_test.go b/test/selectarray_test.go index 144f6b5..4a2eaf4 100644 --- a/test/selectarray_test.go +++ b/test/selectarray_test.go @@ -18,6 +18,7 @@ package test import ( + "os" "testing" sqlitecloud "github.com/sqlitecloud/go-sdk" @@ -27,8 +28,11 @@ const testDbnameSelectArray = "test-gosdk-selectarray-db.sqlite" func TestSelectArray(t *testing.T) { // Server API test + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "?apikey=" + apikey - config, err1 := sqlitecloud.ParseConnectionString(testConnectionString) + config, err1 := sqlitecloud.ParseConnectionString(connectionString) if err1 != nil { t.Fatal(err1.Error()) } @@ -104,8 +108,11 @@ func TestSelectArray(t *testing.T) { func TestSelectArrayTableNameWithQuotes(t *testing.T) { // Server API test + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "?apikey=" + apikey - config, err1 := sqlitecloud.ParseConnectionString(testConnectionString) + config, err1 := sqlitecloud.ParseConnectionString(connectionString) if err1 != nil { t.Fatal(err1.Error()) } diff --git a/test/server_test.go b/test/server_test.go index f3994de..353aeb7 100644 --- a/test/server_test.go +++ b/test/server_test.go @@ -19,6 +19,7 @@ package test import ( "fmt" + "os" "strings" "testing" @@ -28,19 +29,24 @@ import ( const testDbnameServer = "test-gosdk-server-db.sqlite" func TestServer(t *testing.T) { - db, err := sqlitecloud.Connect(testConnectionString) + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + db, err := sqlitecloud.Connect(connectionString + "?apikey=" + apikey) if err != nil { t.Fatal("Connect: ", err.Error()) } // Checking wrong AUTH - if err := db.Auth(testUsername, "wrong password"); err == nil { + username, _ := os.LookupEnv("SQLITE_USER") + password, _ := os.LookupEnv("SQLITE_PASSWORD") + + if err := db.Auth(username, "wrong password"); err == nil { t.Fatal("AUTH: Expected authorization failed, got authorized") } db.Close() // reopen the connection (it was closed because of the auth command with wrong credentials) - db, err = sqlitecloud.Connect(testConnectionString) + db, err = sqlitecloud.Connect(connectionString + "?apikey=" + apikey) if err != nil { t.Fatal(err.Error()) } @@ -52,7 +58,7 @@ func TestServer(t *testing.T) { } // Checking AUTH - if err := db.Auth(testUsername, testPassword); err != nil { + if err := db.Auth(username, password); err != nil { t.Fatal("Checking AUTH: ", err.Error()) } diff --git a/test/test_commons.go b/test/test_commons.go deleted file mode 100644 index a781287..0000000 --- a/test/test_commons.go +++ /dev/null @@ -1,23 +0,0 @@ -package test - -import "flag" - -const testConnectionStringLocalhost = "sqlitecloud://admin:admin@localhost:8860?tls=skip" - -const testUsername = "admin" -const testPassword = "admin" - -var testConnectionString string = testConnectionStringLocalhost - -func init() { - flag.StringVar(&testConnectionString, "server", testConnectionStringLocalhost, "Connection String") -} - -func contains[T comparable](s []T, e T) bool { - for _, v := range s { - if v == e { - return true - } - } - return false -} diff --git a/test/unit/connection_test.go b/test/unit/connection_test.go new file mode 100644 index 0000000..c16a5e5 --- /dev/null +++ b/test/unit/connection_test.go @@ -0,0 +1,347 @@ +package test + +import ( + "fmt" + "reflect" + "testing" + "time" + + sqlitecloud "github.com/sqlitecloud/go-sdk" + "github.com/stretchr/testify/assert" +) + +func TestParseConnectionString(t *testing.T) { + connectionString := "sqlitecloud://myhost.sqlite.cloud:8860/mydatabase?timeout=11&compress=yes" + + expectedConfig := &sqlitecloud.SQCloudConfig{ + Host: "myhost.sqlite.cloud", + Port: 8860, + Username: "", + Password: "", + Database: "mydatabase", + Timeout: time.Duration(11) * time.Second, + CompressMode: "YES", + Secure: true, + TlsInsecureSkipVerify: false, + Pem: "", + ApiKey: "", + NoBlob: false, + MaxData: 0, + MaxRows: 0, + MaxRowset: 0, + } + + config, err := sqlitecloud.ParseConnectionString(connectionString) + if err != nil { + t.Fatalf("Failed to parse connection string: %v", err) + } + + if !reflect.DeepEqual(config, expectedConfig) { + t.Fatalf("Parsed config does not match expected config.\nExpected: %+v\nGot: %+v", expectedConfig, config) + } +} + +func TestParseConnectionStringWithAPIKey(t *testing.T) { + connectionString := "sqlitecloud://user:pass@host.com:8860/dbname?apikey=abc123&timeout=11&compress=true" + expectedConfig := &sqlitecloud.SQCloudConfig{ + Host: "host.com", + Port: 8860, + Username: "user", + Password: "pass", + Database: "dbname", + Timeout: time.Duration(11) * time.Second, + CompressMode: "TRUE", + Secure: true, + TlsInsecureSkipVerify: false, + Pem: "", + ApiKey: "abc123", + NoBlob: false, + MaxData: 0, + MaxRows: 0, + MaxRowset: 0, + } + + config, err := sqlitecloud.ParseConnectionString(connectionString) + assert.NoError(t, err) + assert.Truef(t, reflect.DeepEqual(expectedConfig, config), "Expected: %+v\nGot: %+v", expectedConfig, config) +} + +func TestParseConnectionStringWithCredentials(t *testing.T) { + connectionString := "sqlitecloud://user:pass@host.com:8860" + config, err := sqlitecloud.ParseConnectionString(connectionString) + + assert.NoError(t, err) + assert.Equal(t, "user", config.Username) + assert.Equal(t, "pass", config.Password) + assert.Equal(t, "host.com", config.Host) + assert.Equal(t, 8860, config.Port) + assert.Empty(t, config.Database) +} + +func TestParseConnectionStringWithoutCredentials(t *testing.T) { + connectionString := "sqlitecloud://host.com" + config, err := sqlitecloud.ParseConnectionString(connectionString) + + assert.NoError(t, err) + assert.Empty(t, config.Username) + assert.Empty(t, config.Password) + assert.Equal(t, "host.com", config.Host) +} + +func TestParseConnectionStringWithParameters(t *testing.T) { + tests := []struct { + param string + configParam string + value string + expectedValue any + }{ + { + param: "timeout", + configParam: "Timeout", + value: "11", + expectedValue: time.Duration(11) * time.Second, + }, + { + param: "compression", + configParam: "Compression", + value: "true", + expectedValue: true, + }, + { + param: "zerotext", + configParam: "Zerotext", + value: "true", + expectedValue: true, + }, + { + param: "memory", + configParam: "Memory", + value: "true", + expectedValue: true, + }, + { + param: "create", + configParam: "Create", + value: "true", + expectedValue: true, + }, + { + param: "insecure", + configParam: "Secure", + value: "true", + expectedValue: false, + }, + { + param: "secure", + configParam: "Secure", + value: "false", + expectedValue: false, + }, + { + param: "non_linearizable", + configParam: "NonLinearizable", + value: "true", + expectedValue: true, + }, + { + param: "nonlinearizable", + configParam: "NonLinearizable", + value: "true", + expectedValue: true, + }, + { + param: "no_verify_certificate", + configParam: "TlsInsecureSkipVerify", + value: "true", + expectedValue: true, + }, + { + param: "root_certificate", + configParam: "RootCertificate", + value: "cert1234", + expectedValue: "cert1234", + }, + { + param: "client_certificate", + configParam: "ClientCertificate", + value: "cert1234", + expectedValue: "cert1234", + }, + { + param: "client_certificate_key", + configParam: "ClientCertificateKey", + value: "certkey123", + expectedValue: "certkey123", + }, + { + param: "noblob", + configParam: "NoBlob", + value: "true", + expectedValue: true, + }, + { + param: "maxdata", + configParam: "MaxData", + value: "10", + expectedValue: 10, + }, + { + param: "maxrows", + configParam: "MaxRows", + value: "11", + expectedValue: 11, + }, + { + param: "maxrowset", + configParam: "MaxRowset", + value: "12", + expectedValue: 12, + }, + { + param: "apikey", + configParam: "ApiKey", + value: "abc123", + expectedValue: "abc123", + }, + } + + for _, tt := range tests { + t.Run(tt.param, func(t *testing.T) { + config, err := sqlitecloud.ParseConnectionString("sqlitecloud://myhost.sqlite.cloud/mydatabase?" + tt.param + "=" + tt.value) + + assert.NoError(t, err) + + actualValue := reflect.ValueOf(*config).FieldByName(tt.configParam) + if !actualValue.IsValid() { + t.Fatalf("Field %s not found in config", tt.configParam) + } else { + assert.Equal(t, tt.expectedValue, actualValue.Interface()) + } + }) + } +} + +func TestParseConnectionStringWithTLSParameter(t *testing.T) { + tests := []struct { + tlsValue string + expectedSecure bool + expectedInsecure bool + expectedPem string + }{ + { + tlsValue: "true", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "false", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "y", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "n", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "yes", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "no", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "true", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "on", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "off", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "enable", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "disable", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "enabled", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "disabled", + expectedSecure: false, + expectedInsecure: false, + expectedPem: "", + }, + { + tlsValue: "skip", + expectedSecure: true, + expectedInsecure: true, + expectedPem: "", + }, + { + tlsValue: "intern", + expectedSecure: true, + expectedInsecure: false, + expectedPem: sqlitecloud.SQLiteCloudCA, + }, + { + tlsValue: "", + expectedSecure: true, + expectedInsecure: false, + expectedPem: sqlitecloud.SQLiteCloudCA, + }, + { + tlsValue: "anything-default", + expectedSecure: true, + expectedInsecure: false, + expectedPem: "anything-default", + }, + } + + for _, tt := range tests { + t.Run(tt.tlsValue, func(t *testing.T) { + connectionString := fmt.Sprintf("sqlitecloud://myhost.sqlite.cloud:8860/mydatabase?tls=%s", tt.tlsValue) + + config, err := sqlitecloud.ParseConnectionString(connectionString) + assert.NoError(t, err) + + assert.Equal(t, tt.expectedSecure, config.Secure) + assert.Equal(t, tt.expectedInsecure, config.TlsInsecureSkipVerify) + assert.Equal(t, tt.expectedPem, config.Pem) + }) + } +} From 7f4f1015466ff9b2261b2ad8857b681bdf9194b9 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Wed, 12 Jun 2024 13:35:45 +0000 Subject: [PATCH 04/32] feat(cli): add connection parameters See: https://github.com/sqlitecloud/sqlitecloud-js/issues/68 --- cli/sqlc.go | 97 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/cli/sqlc.go b/cli/sqlc.go index 493b632..aa7ec7f 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -91,6 +91,15 @@ Connection Options: -w, --password PASSWORD Use PASSWORD for authentication -t, --timeout SECS Set Timeout for network operations to SECS seconds [default::10] -c, --compress (NO|LZ4) Use line compression [default::NO] + -z, --zerotext Text ends with 0 value + -m, --memory Use in-memory database + -e, --create Create database if it does not exist + -i, --nonlinearizable Use non-linearizable mode for queries + -a, --apikey KEY Use API key for authentication + -n, --noblob Disable BLOB support + -x, --maxdata SIZE Set maximum data size for queries + -y, --maxrows ROWS Set maximum number of rows for queries + -r, --maxrowset ROWS Set maximum number of rows per rowset for queries --tls [YES|NO|INTERN|FILE] Encrypt the database connection using the host's root CA set (YES), a custom CA with a PEM from FILE (FILE), the internal SQLiteCloud CA (INTERN), or disable the encryption (NO) [default::YES] ` @@ -131,16 +140,20 @@ type Parameter struct { Format string `docopt:"--format"` OutPutFormat int `docopt:"--outputformat"` - Host string `docopt:"--host"` - Port int `docopt:"--port"` - User string `docopt:"--user"` - Password string `docopt:"--password"` - Database string `docopt:"--dbname"` - ApiKey string `docopt:"--apikey"` - NoBlob bool `docopt:"--noblob"` - MaxData int `docopt:"--maxdata"` - MaxRows int `docopt:"--maxrows"` - MaxRowset int `docopt:"--maxrowset"` + Host string `docopt:"--host"` + Port int `docopt:"--port"` + User string `docopt:"--user"` + Password string `docopt:"--password"` + Database string `docopt:"--dbname"` + Zerotext bool `docopt:"--zerotext"` + Memory bool `docopt:"--memory"` + Create bool `docopt:"--create"` + NonLinearizable bool `docopt:"--nonlinearizable"` + ApiKey string `docopt:"--apikey"` + NoBlob bool `docopt:"--noblob"` + MaxData int `docopt:"--maxdata"` + MaxRows int `docopt:"--maxrows"` + MaxRowset int `docopt:"--maxrowset"` Timeout int `docopt:"--timeout"` Compress string `docopt:"--compress"` @@ -245,23 +258,46 @@ func parseParameters() (Parameter, error) { p["--user"] = getFirstNoneEmptyString([]string{dropError(p.String("--user")), conf.Username}) p["--password"] = getFirstNoneEmptyString([]string{dropError(p.String("--password")), conf.Password}) p["--dbname"] = getFirstNoneEmptyString([]string{dropError(p.String("--dbname")), conf.Database}) - p["--host"] = getFirstNoneEmptyString([]string{dropError(p.String("--host")), conf.Host}) - p["--compress"] = getFirstNoneEmptyString([]string{dropError(p.String("--compress")), conf.CompressMode}) if conf.Port > 0 { p["--port"] = getFirstNoneEmptyString([]string{dropError(p.String("--port")), fmt.Sprintf("%d", conf.Port)}) } if conf.Timeout > 0 { p["--timeout"] = getFirstNoneEmptyString([]string{dropError(p.String("--timeout")), fmt.Sprintf("%d", conf.Timeout)}) } + if conf.Compression { + p["--compress"] = getFirstNoneEmptyString([]string{dropError(p.String("--compress")), conf.CompressMode}) + } + if conf.Zerotext { + if b, err := p.Bool("--zerotext"); err == nil { + p["--zerotext"] = b || conf.Zerotext + } + } + if conf.Memory { + if b, err := p.Bool("--memory"); err == nil { + p["--memory"] = b || conf.Memory + } + } + if conf.Create { + if b, err := p.Bool("--create"); err == nil { + p["--create"] = b || conf.Create + } + } + if conf.NonLinearizable { + if b, err := p.Bool("--nonlinearizable"); err == nil { + p["--nonlinearizable"] = b || conf.NonLinearizable + } + } p["--tls"] = getFirstNoneEmptyString([]string{dropError(p.String("--tls")), conf.Pem}) - if conf.Secure == false { + if !conf.Secure { p["--tls"] = "NO" - } else if conf.TlsInsecureSkipVerify == true { + } else if conf.TlsInsecureSkipVerify { p["--tls"] = "SKIP" } p["--apikey"] = getFirstNoneEmptyString([]string{dropError(p.String("--apikey")), conf.ApiKey}) if conf.NoBlob { - p["--noblob"] = getFirstNoneEmptyString([]string{dropError(p.String("--noblob")), strconv.FormatBool(conf.NoBlob)}) + if b, err := p.Bool("--noblob"); err == nil { + p["--noblob"] = b || conf.NoBlob + } } if conf.MaxData > 0 { p["--maxdata"] = getFirstNoneEmptyString([]string{dropError(p.String("--maxdata")), fmt.Sprintf("%d", conf.MaxData)}) @@ -284,6 +320,9 @@ func parseParameters() (Parameter, error) { p["--compress"] = getFirstNoneEmptyString([]string{dropError(p.String("--compress")), "NO"}) p["--tls"] = getFirstNoneEmptyString([]string{dropError(p.String("--tls")), "YES"}) p["--separator"] = getFirstNoneEmptyString([]string{dropError(p.String("--separator")), dropError(sqlitecloud.GetDefaultSeparatorForOutputFormat(outputformat)), "|"}) + p["--maxdata"] = getFirstNoneEmptyString([]string{dropError(p.String("--maxdata")), "0"}) + p["--maxrows"] = getFirstNoneEmptyString([]string{dropError(p.String("--maxrows")), "0"}) + p["--maxrowset"] = getFirstNoneEmptyString([]string{dropError(p.String("--maxrowset")), "0"}) // Fix invalid(=unset) parameters, quotation & control-chars for k, v := range p { @@ -375,18 +414,22 @@ func main() { // print( out, fmt.Sprintf( "%s %s, %s", long_name, version, copyright ), ¶meter ) config := sqlitecloud.SQCloudConfig{ - Host: parameter.Host, - Port: parameter.Port, - Username: parameter.User, - Password: parameter.Password, - Database: parameter.Database, - Timeout: time.Duration(parameter.Timeout) * time.Second, - CompressMode: parameter.Compress, - ApiKey: parameter.ApiKey, - NoBlob: parameter.NoBlob, - MaxData: parameter.MaxData, - MaxRows: parameter.MaxRows, - MaxRowset: parameter.MaxRowset, + Host: parameter.Host, + Port: parameter.Port, + Username: parameter.User, + Password: parameter.Password, + Database: parameter.Database, + Timeout: time.Duration(parameter.Timeout) * time.Second, + CompressMode: parameter.Compress, + Zerotext: parameter.Zerotext, + Memory: parameter.Memory, + Create: parameter.Create, + NonLinearizable: parameter.NonLinearizable, + ApiKey: parameter.ApiKey, + NoBlob: parameter.NoBlob, + MaxData: parameter.MaxData, + MaxRows: parameter.MaxRows, + MaxRowset: parameter.MaxRowset, } config.Secure, config.TlsInsecureSkipVerify, config.Pem = sqlitecloud.ParseTlsString(parameter.Tls) From c65a65eaf279df8bc48e7d6e05e1c50c604a9022 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Wed, 12 Jun 2024 15:31:06 +0000 Subject: [PATCH 05/32] typo: remove todo --- .vscode/settings.json | 2 +- connection.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b8e81c8..3bd92f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,4 @@ "gopls": { "ui.semanticTokens": true } -} \ No newline at end of file +} diff --git a/connection.go b/connection.go index ac2f76d..b4bc3e6 100644 --- a/connection.go +++ b/connection.go @@ -47,7 +47,7 @@ type SQCloudConfig struct { Secure bool // Connect using plain TCP port, without TLS encryption, NOT RECOMMENDED (insecure) NonLinearizable bool // Request for immediate responses from the server node without waiting for linerizability guarantees TlsInsecureSkipVerify bool // Accept invalid TLS certificates (no_verify_certificate) - Pem string // TODO: ? + Pem string RootCertificate string ClientCertificate string ClientCertificateKey string From b29323c57f16e8db912a366f2c1f39e0243bd00d Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Wed, 12 Jun 2024 16:04:35 +0000 Subject: [PATCH 06/32] fix(certificates): remove parameters for custom certificates --- connection.go | 12 ------------ test/unit/connection_test.go | 18 ------------------ 2 files changed, 30 deletions(-) diff --git a/connection.go b/connection.go index b4bc3e6..6c7db29 100644 --- a/connection.go +++ b/connection.go @@ -48,9 +48,6 @@ type SQCloudConfig struct { NonLinearizable bool // Request for immediate responses from the server node without waiting for linerizability guarantees TlsInsecureSkipVerify bool // Accept invalid TLS certificates (no_verify_certificate) Pem string - RootCertificate string - ClientCertificate string - ClientCertificateKey string ApiKey string NoBlob bool // flag to tell the server to not send BLOB columns MaxData int // value to tell the server to not send columns with more than max_data bytes @@ -125,9 +122,6 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err config.NonLinearizable = false config.TlsInsecureSkipVerify = false config.Pem = "" - config.RootCertificate = "" - config.ClientCertificate = "" - config.ClientCertificateKey = "" config.NoBlob = false config.MaxData = 0 config.MaxRows = 0 @@ -191,12 +185,6 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err } case "tls": config.Secure, config.TlsInsecureSkipVerify, config.Pem = ParseTlsString(lastLiteral) - case "root_certificate": - config.RootCertificate = lastLiteral - case "client_certificate": - config.ClientCertificate = lastLiteral - case "client_certificate_key": - config.ClientCertificateKey = lastLiteral case "apikey": config.ApiKey = lastLiteral case "noblob": diff --git a/test/unit/connection_test.go b/test/unit/connection_test.go index c16a5e5..4d795de 100644 --- a/test/unit/connection_test.go +++ b/test/unit/connection_test.go @@ -155,24 +155,6 @@ func TestParseConnectionStringWithParameters(t *testing.T) { value: "true", expectedValue: true, }, - { - param: "root_certificate", - configParam: "RootCertificate", - value: "cert1234", - expectedValue: "cert1234", - }, - { - param: "client_certificate", - configParam: "ClientCertificate", - value: "cert1234", - expectedValue: "cert1234", - }, - { - param: "client_certificate_key", - configParam: "ClientCertificateKey", - value: "certkey123", - expectedValue: "certkey123", - }, { param: "noblob", configParam: "NoBlob", From bafcb64a1a0b8168a477664ab719b486a5077a22 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Thu, 13 Jun 2024 16:05:30 +0000 Subject: [PATCH 07/32] feat(compression): enabled by default --- cli/sqlc.go | 3 +- connection.go | 32 ++++---- test/{commons_test.go => commons.go} | 16 ++++ test/server_test.go | 14 ++++ test/unit/connection_test.go | 108 ++++++++++++++++++++++++++- 5 files changed, 154 insertions(+), 19 deletions(-) rename test/{commons_test.go => commons.go} (53%) diff --git a/cli/sqlc.go b/cli/sqlc.go index aa7ec7f..97d6521 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -317,7 +317,7 @@ func parseParameters() (Parameter, error) { p["--host"] = getFirstNoneEmptyString([]string{dropError(p.String("--host")), "localhost"}) p["--port"] = getFirstNoneEmptyString([]string{dropError(p.String("--port")), "8860"}) p["--timeout"] = getFirstNoneEmptyString([]string{dropError(p.String("--timeout")), "10"}) - p["--compress"] = getFirstNoneEmptyString([]string{dropError(p.String("--compress")), "NO"}) + p["--compress"] = getFirstNoneEmptyString([]string{dropError(p.String("--compress")), sqlitecloud.CompressModeLZ4}) p["--tls"] = getFirstNoneEmptyString([]string{dropError(p.String("--tls")), "YES"}) p["--separator"] = getFirstNoneEmptyString([]string{dropError(p.String("--separator")), dropError(sqlitecloud.GetDefaultSeparatorForOutputFormat(outputformat)), "|"}) p["--maxdata"] = getFirstNoneEmptyString([]string{dropError(p.String("--maxdata")), "0"}) @@ -420,6 +420,7 @@ func main() { Password: parameter.Password, Database: parameter.Database, Timeout: time.Duration(parameter.Timeout) * time.Second, + Compression: parameter.Compress == sqlitecloud.CompressModeLZ4, CompressMode: parameter.Compress, Zerotext: parameter.Zerotext, Memory: parameter.Memory, diff --git a/connection.go b/connection.go index 6c7db29..d8efdf5 100644 --- a/connection.go +++ b/connection.go @@ -113,8 +113,8 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err config.Password, _ = u.User.Password() config.Database = strings.TrimPrefix(u.Path, "/") config.Timeout = 0 - config.Compression = false - config.CompressMode = CompressModeNo + config.Compression = true // enabled by default + config.CompressMode = CompressModeLZ4 config.Zerotext = false config.Memory = false config.Create = false @@ -146,14 +146,18 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err } case "compress": - config.CompressMode = strings.ToUpper(lastLiteral) - if config.CompressMode == CompressModeLZ4 { - config.Compression = true + mode := strings.ToUpper(lastLiteral) + if mode == CompressModeNo { + config.CompressMode = CompressModeNo + config.Compression = false + } else if enable, err := parseBool(mode, config.Compression); err == nil && !enable { + config.CompressMode = CompressModeNo + config.Compression = false } case "compression": - if b, err := parseBool(lastLiteral, config.Compression); err == nil && b { - config.Compression = true - config.CompressMode = CompressModeLZ4 + if enable, err := parseBool(lastLiteral, config.Compression); err == nil && !enable { + config.Compression = false + config.CompressMode = CompressModeNo } case "zerotext": if b, err := parseBool(lastLiteral, config.Zerotext); err == nil { @@ -239,12 +243,12 @@ func (this *SQCloud) CheckConnectionParameter() error { return fmt.Errorf("Invalid hostname (%s)", this.Host) } - ip := net.ParseIP(this.Host) - if ip == nil { - if _, err := net.LookupHost(this.Host); err != nil { - return errors.New(fmt.Sprintf("Can't resolve hostname (%s)", this.Host)) - } - } + // ip := net.ParseIP(this.Host) + // if ip == nil { + // if _, err := net.LookupHost(this.Host); err != nil { + // return errors.New(fmt.Sprintf("Can't resolve hostname (%s)", this.Host)) + // } + // } if this.Port == 0 { this.Port = 8860 diff --git a/test/commons_test.go b/test/commons.go similarity index 53% rename from test/commons_test.go rename to test/commons.go index ca4fd5b..c172604 100644 --- a/test/commons_test.go +++ b/test/commons.go @@ -4,8 +4,10 @@ import ( "flag" "log" "os" + "testing" "github.com/joho/godotenv" + sqlitecloud "github.com/sqlitecloud/go-sdk" ) func init() { @@ -25,3 +27,17 @@ func contains[T comparable](s []T, e T) bool { } return false } + +func setupDatabase(t *testing.T) (*sqlitecloud.SQCloud, func()) { + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + + db, err := sqlitecloud.Connect(connectionString + "?apikey=" + apikey) + if err != nil { + t.Fatal("Connection error: ", err.Error()) + } + + return db, func() { + db.Close() + } +} diff --git a/test/server_test.go b/test/server_test.go index 353aeb7..85dd9dc 100644 --- a/test/server_test.go +++ b/test/server_test.go @@ -24,6 +24,7 @@ import ( "testing" sqlitecloud "github.com/sqlitecloud/go-sdk" + "github.com/stretchr/testify/assert" ) const testDbnameServer = "test-gosdk-server-db.sqlite" @@ -213,3 +214,16 @@ func TestServer(t *testing.T) { // } // fmt.Printf( "ok.\r\n" ) } + +func TestCompressionEnabledByDefault(t *testing.T) { + db, cleaup := setupDatabase(t) + defer cleaup() + + result, _ := db.Select("GET CLIENT KEY COMPRESSION") + value, err := result.GetString() + if err != nil { + t.Fatal(err.Error()) + } + + assert.Equal(t, "1", value) +} diff --git a/test/unit/connection_test.go b/test/unit/connection_test.go index 4d795de..8c58256 100644 --- a/test/unit/connection_test.go +++ b/test/unit/connection_test.go @@ -20,7 +20,8 @@ func TestParseConnectionString(t *testing.T) { Password: "", Database: "mydatabase", Timeout: time.Duration(11) * time.Second, - CompressMode: "YES", + Compression: true, + CompressMode: sqlitecloud.CompressModeLZ4, Secure: true, TlsInsecureSkipVerify: false, Pem: "", @@ -50,7 +51,8 @@ func TestParseConnectionStringWithAPIKey(t *testing.T) { Password: "pass", Database: "dbname", Timeout: time.Duration(11) * time.Second, - CompressMode: "TRUE", + Compression: true, + CompressMode: sqlitecloud.CompressModeLZ4, Secure: true, TlsInsecureSkipVerify: false, Pem: "", @@ -104,8 +106,8 @@ func TestParseConnectionStringWithParameters(t *testing.T) { { param: "compression", configParam: "Compression", - value: "true", - expectedValue: true, + value: "false", + expectedValue: false, }, { param: "zerotext", @@ -203,6 +205,104 @@ func TestParseConnectionStringWithParameters(t *testing.T) { } } +func TestParseConnectionStringCompressionCombinations(t *testing.T) { + tests := []struct { + param string + configParam string + value string + expectedValue any + }{ + { + param: "compression", + configParam: "Compression", + value: "false", + expectedValue: false, + }, + { + param: "compression", + configParam: "Compression", + value: "disabled", + expectedValue: false, + }, + { + param: "compression", + configParam: "Compression", + // value not supported + value: "no", + expectedValue: true, + }, + { + param: "compression", + configParam: "Compression", + value: "0", + expectedValue: false, + }, + { + param: "compression", + configParam: "Compression", + value: "true", + expectedValue: true, + }, + { + param: "compression", + configParam: "Compression", + value: "enabled", + expectedValue: true, + }, + { + param: "compression", + configParam: "Compression", + value: "yes", + expectedValue: true, + }, + { + param: "compression", + configParam: "Compression", + value: "1", + expectedValue: true, + }, + { + param: "compress", + configParam: "CompressMode", + value: "lz4", + expectedValue: sqlitecloud.CompressModeLZ4, + }, + { + param: "compress", + configParam: "Compression", + value: "lz4", + expectedValue: true, + }, + { + param: "compress", + configParam: "CompressMode", + value: "no", + expectedValue: sqlitecloud.CompressModeNo, + }, + { + param: "compress", + configParam: "Compression", + value: "no", + expectedValue: false, + }, + } + + for _, tt := range tests { + t.Run(tt.param, func(t *testing.T) { + config, err := sqlitecloud.ParseConnectionString("sqlitecloud://myhost.sqlite.cloud/mydatabase?" + tt.param + "=" + tt.value) + + assert.NoError(t, err) + + actualValue := reflect.ValueOf(*config).FieldByName(tt.configParam) + if !actualValue.IsValid() { + t.Fatalf("Field %s not found in config", tt.configParam) + } else { + assert.Equal(t, tt.expectedValue, actualValue.Interface()) + } + }) + } +} + func TestParseConnectionStringWithTLSParameter(t *testing.T) { tests := []struct { tlsValue string From 71b6112ad997e81c37ef30dd40883e1f1b263623 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Fri, 14 Jun 2024 10:18:43 +0000 Subject: [PATCH 08/32] fix: do not lookup for host before connecting --- connection.go | 130 +++++++++++++++++++----------------------- test/compress_test.go | 4 +- 2 files changed, 60 insertions(+), 74 deletions(-) diff --git a/connection.go b/connection.go index d8efdf5..6e64e2d 100644 --- a/connection.go +++ b/connection.go @@ -75,8 +75,12 @@ type SQCloud struct { ErrorMessage string } -const CompressModeNo = "NO" -const CompressModeLZ4 = "LZ4" +const SQLiteDefaultPort = 8860 + +const ( + CompressModeNo = "NO" + CompressModeLZ4 = "LZ4" +) const SQLiteCloudCA = "SQLiteCloudCA" @@ -108,7 +112,7 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err config = &SQCloudConfig{} config.Host = u.Hostname() - config.Port = 0 + config.Port = SQLiteDefaultPort config.Username = u.User.Username() config.Password, _ = u.User.Password() config.Database = strings.TrimPrefix(u.Path, "/") @@ -243,74 +247,10 @@ func (this *SQCloud) CheckConnectionParameter() error { return fmt.Errorf("Invalid hostname (%s)", this.Host) } - // ip := net.ParseIP(this.Host) - // if ip == nil { - // if _, err := net.LookupHost(this.Host); err != nil { - // return errors.New(fmt.Sprintf("Can't resolve hostname (%s)", this.Host)) - // } - // } - - if this.Port == 0 { - this.Port = 8860 - } - if this.Port < 1 || this.Port >= 0xFFFF { - return errors.New(fmt.Sprintf("Invalid Port (%d)", this.Port)) - } - - // if this.Timeout == 0 { - // this.Timeout = 10 * time.Second - // } if this.Timeout < 0 { return errors.New(fmt.Sprintf("Invalid Timeout (%s)", this.Timeout.String())) } - switch this.CompressMode { - case CompressModeNo, CompressModeLZ4: - default: - return errors.New(fmt.Sprintf("Invalid compression method (%s)", this.CompressMode)) - } - - if this.Secure { - var pool *x509.CertPool = nil - pem := []byte{} - - switch _, _, trimmed := ParseTlsString(this.Pem); trimmed { - case "": - break - case SQLiteCloudCA: - pem = []byte(sqliteCloudCAPEM) - default: - // check if it is a filepath - _, err := os.Stat(trimmed) - if os.IsNotExist(err) { - // not a filepath, use the string as a pem string - pem = []byte(trimmed) - } else { - // its a file, read its content into the pem string - switch bytes, err := os.ReadFile(trimmed); { - case err != nil: - return errors.New(fmt.Sprintf("Could not open PEM file in '%s'", trimmed)) - default: - pem = bytes - } - } - } - - if len(pem) > 0 { - pool = x509.NewCertPool() - - if !pool.AppendCertsFromPEM(pem) { - return errors.New(fmt.Sprintf("Could not append certs from PEM")) - } - } - - this.cert = &tls.Config{ - RootCAs: pool, - InsecureSkipVerify: this.TlsInsecureSkipVerify, - MinVersion: tls.VersionTLS12, - } - } - return nil } @@ -353,12 +293,11 @@ func Connect(ConnectionString string) (*SQCloud, error) { func (this *SQCloud) Connect() error { this.reset() // also closes an open connection - switch err := this.CheckConnectionParameter(); { - case err != nil: + if err := this.CheckConnectionParameter(); err != nil { return err - default: - return this.reconnect() } + + return this.reconnect() } // reconnect closes and then reopens a connection to the SQLite Cloud database server. @@ -369,6 +308,14 @@ func (this *SQCloud) reconnect() error { this.resetError() + if this.Secure { + cert, err := getTlsConfig(&this.SQCloudConfig) + if err != nil { + return err + } + this.cert = cert + } + var dialer = net.Dialer{} dialer.Timeout = this.Timeout dialer.DualStack = true @@ -437,6 +384,47 @@ func (this *SQCloud) Close() error { return nil } +func getTlsConfig(config *SQCloudConfig) (*tls.Config, error) { + var pool *x509.CertPool = nil + pem := []byte{} + + switch _, _, trimmed := ParseTlsString(config.Pem); trimmed { + case "": + break + case SQLiteCloudCA: + pem = []byte(sqliteCloudCAPEM) + default: + // check if it is a filepath + _, err := os.Stat(trimmed) + if os.IsNotExist(err) { + // not a filepath, use the string as a pem string + pem = []byte(trimmed) + } else { + // its a file, read its content into the pem string + switch bytes, err := os.ReadFile(trimmed); { + case err != nil: + return nil, fmt.Errorf("could not open PEM file in '%s'", trimmed) + default: + pem = bytes + } + } + } + + if len(pem) > 0 { + pool = x509.NewCertPool() + + if !pool.AppendCertsFromPEM(pem) { + return nil, fmt.Errorf("could not append certs from PEM") + } + } + + return &tls.Config{ + RootCAs: pool, + InsecureSkipVerify: config.TlsInsecureSkipVerify, + MinVersion: tls.VersionTLS12, + }, nil +} + func connectionCommands(config SQCloudConfig) (string, []interface{}) { buffer := "" args := []interface{}{} diff --git a/test/compress_test.go b/test/compress_test.go index 44922f4..40d377d 100644 --- a/test/compress_test.go +++ b/test/compress_test.go @@ -28,8 +28,6 @@ import ( ) const testDbnameCompress = "test-gosdk-compress-db.sqlite" -const testCompressKey = "compress" -const testCompressValue = "LZ4" func TestCompress(t *testing.T) { connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") @@ -38,7 +36,7 @@ func TestCompress(t *testing.T) { url, err := url.Parse(connectionString) values := url.Query() - values.Add(testCompressKey, testCompressValue) + values.Add("compress", sqlitecloud.CompressModeLZ4) url.RawQuery = values.Encode() connstring := url.String() From 465a187f5b38df69a357b7141bb95db105225c55 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Fri, 14 Jun 2024 14:19:01 +0000 Subject: [PATCH 09/32] fix(cli): remove useless commands on connect - removed list databases and list tables for autocompletion --- cli/sqlc.go | 4 ---- connection.go | 12 +++++++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cli/sqlc.go b/cli/sqlc.go index 97d6521..a253006 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -502,16 +502,12 @@ func main() { prompt = "\\u@\\H:\\p/\\d > " refreshDB := false - if parameter.Database != "" { - refreshDB = true - } Loop: for { out.Flush() if refreshDB { db.Database, _ = db.GetDatabase() - go func() { dynamic_tokens = db.GetAutocompleteTokens() }() // Update the dynamic tokens in the background... } renderdPrompt := prompt diff --git a/connection.go b/connection.go index 6e64e2d..1592941 100644 --- a/connection.go +++ b/connection.go @@ -463,11 +463,17 @@ func connectionCommands(config SQCloudConfig) (string, []interface{}) { buffer += noblobCommand(config.NoBlob) } - buffer += maxdataCommand(config.MaxData) + if config.MaxData > 0 { + buffer += maxdataCommand(config.MaxData) + } - buffer += maxrowsCommand(config.MaxRows) + if config.MaxRows > 0 { + buffer += maxrowsCommand(config.MaxRows) + } - buffer += maxrowsetCommand(config.MaxRowset) + if config.MaxRowset > 0 { + buffer += maxrowsetCommand(config.MaxRowset) + } return buffer, args } From fb393b1070772f778670a8e65ba0a97c2acd7497 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Fri, 14 Jun 2024 15:14:21 +0000 Subject: [PATCH 10/32] fix: update repository reference --- .devcontainer/devcontainer.json | 9 ++++++++- Makefile | 4 ++-- README.md | 8 ++++---- cli/go.mod | 4 ++-- cli/sqlc.go | 2 +- go.mod | 2 +- test/commons.go | 2 +- test/compress_test.go | 2 +- test/go.mod | 6 +++--- test/literals_test.go | 2 +- test/pubsub_test.go | 2 +- test/selectarray_test.go | 2 +- test/server_test.go | 2 +- test/unit/connection_test.go | 2 +- 14 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6dc2589..0685112 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,14 @@ { "name": "Go", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/go:1-1-bullseye" + "image": "mcr.microsoft.com/devcontainers/go:1-1-bullseye", + "customizations": { + "vscode": { + "extensions": [ + "eamodio.gitlens" + ] + } + } // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, diff --git a/Makefile b/Makefile index b39d711..2a13b04 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ $(GOPATH)/bin/sqlc: *.go cli/sqlc.go cli: $(GOPATH)/bin/sqlc github: - open https://github.com/sqlitecloud/go-sdk + open https://github.com/sqlitecloud/sqlitecloud-go diff: git difftool @@ -43,7 +43,7 @@ ifeq ($(wildcard ./src),) ln -s . src endif @echo "Hit CRTL-C to stop the documentation server..." - @( sleep 1 && open http://localhost:6060/pkg/github.com/sqlitecloud/go-sdk/ ) & + @( sleep 1 && open http://localhost:6060/pkg/github.com/sqlitecloud/sqlitecloud-go/ ) & @$(GOPATH)/bin/godoc -http=:6060 -index -play clean: diff --git a/README.md b/README.md index 72eb78c..58854fa 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ The SQLite Cloud Client SDK for Go (sqlitecloud/go-sdk) is the Go Programming La 1. Import the package in your Go source code: ```go - import sqlitecloud "github.com/sqlitecloud/go-sdk" + import sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" ``` 2. Download the package, and run the [`go mod tidy` command](https://go.dev/ref/mod#go-mod-tidy) to synchronize your module's dependencies: ``` $ go mod tidy - go: downloading github.com/sqlitecloud/go-sdk v1.0.0 + go: downloading github.com/sqlitecloud/sqlitecloud-go v1.0.0 ``` 3. Connect to a SQLite Cloud database with a valid [connection string](#get-a-connection-string): @@ -41,7 +41,7 @@ import ( "fmt" "strings" - sqlitecloud "github.com/sqlitecloud/go-sdk" + sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" ) const connectionString = "sqlitecloud://admin:password@host.sqlite.cloud:8860/dbname.sqlite" @@ -81,4 +81,4 @@ To get a valid connection string, follow these instructions: ## API Documentation -The complete documentation of the sqlitecloud/go-sdk library is available at: https://pkg.go.dev/github.com/sqlitecloud/go-sdk \ No newline at end of file +The complete documentation of the sqlitecloud/go-sdk library is available at: https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go \ No newline at end of file diff --git a/cli/go.mod b/cli/go.mod index 1af66a3..e47889a 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -17,6 +17,6 @@ require ( golang.org/x/sys v0.7.0 // indirect ) -require github.com/sqlitecloud/go-sdk v0.0.0 +require github.com/sqlitecloud/sqlitecloud-go v0.0.0 -replace github.com/sqlitecloud/go-sdk v0.0.0 => ../ +replace github.com/sqlitecloud/sqlitecloud-go v0.0.0 => ../ diff --git a/cli/sqlc.go b/cli/sqlc.go index a253006..5087f54 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -18,7 +18,7 @@ package main import ( - sqlitecloud "github.com/sqlitecloud/go-sdk" + sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" "bufio" "errors" diff --git a/go.mod b/go.mod index 1926127..d8bd281 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/sqlitecloud/go-sdk +module github.com/sqlitecloud/sqlitecloud-go go 1.18 diff --git a/test/commons.go b/test/commons.go index c172604..4f1510e 100644 --- a/test/commons.go +++ b/test/commons.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/joho/godotenv" - sqlitecloud "github.com/sqlitecloud/go-sdk" + sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" ) func init() { diff --git a/test/compress_test.go b/test/compress_test.go index 40d377d..9d4998f 100644 --- a/test/compress_test.go +++ b/test/compress_test.go @@ -24,7 +24,7 @@ import ( "os" "testing" - sqlitecloud "github.com/sqlitecloud/go-sdk" + sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" ) const testDbnameCompress = "test-gosdk-compress-db.sqlite" diff --git a/test/go.mod b/test/go.mod index afb7862..1375454 100644 --- a/test/go.mod +++ b/test/go.mod @@ -1,10 +1,10 @@ -module github.com/sqlitecloud/go-sdk/test +module github.com/sqlitecloud/sqlitecloud-go/test go 1.18 require ( github.com/joho/godotenv v1.5.1 - github.com/sqlitecloud/go-sdk v0.0.0 + github.com/sqlitecloud/sqlitecloud-go v0.0.0 github.com/stretchr/testify v1.9.0 ) @@ -20,4 +20,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/sqlitecloud/go-sdk v0.0.0 => ../ +replace github.com/sqlitecloud/sqlitecloud-go v0.0.0 => ../ diff --git a/test/literals_test.go b/test/literals_test.go index 2ac679a..51efed6 100644 --- a/test/literals_test.go +++ b/test/literals_test.go @@ -21,7 +21,7 @@ import ( "os" "testing" - sqlitecloud "github.com/sqlitecloud/go-sdk" + sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" ) const testDbnameLiteral = "test-gosdk-literal-db.sqlite" diff --git a/test/pubsub_test.go b/test/pubsub_test.go index 32fb425..303ef95 100644 --- a/test/pubsub_test.go +++ b/test/pubsub_test.go @@ -25,7 +25,7 @@ import ( "testing" "time" - sqlitecloud "github.com/sqlitecloud/go-sdk" + sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" ) const testPubsubChannelName = "TestPubsubChannel" diff --git a/test/selectarray_test.go b/test/selectarray_test.go index 4a2eaf4..fee27ca 100644 --- a/test/selectarray_test.go +++ b/test/selectarray_test.go @@ -21,7 +21,7 @@ import ( "os" "testing" - sqlitecloud "github.com/sqlitecloud/go-sdk" + sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" ) const testDbnameSelectArray = "test-gosdk-selectarray-db.sqlite" diff --git a/test/server_test.go b/test/server_test.go index 85dd9dc..8de23ef 100644 --- a/test/server_test.go +++ b/test/server_test.go @@ -23,7 +23,7 @@ import ( "strings" "testing" - sqlitecloud "github.com/sqlitecloud/go-sdk" + sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" "github.com/stretchr/testify/assert" ) diff --git a/test/unit/connection_test.go b/test/unit/connection_test.go index 8c58256..2576880 100644 --- a/test/unit/connection_test.go +++ b/test/unit/connection_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - sqlitecloud "github.com/sqlitecloud/go-sdk" + sqlitecloud "github.com/sqlitecloud/sqlitecloud-go" "github.com/stretchr/testify/assert" ) From 3831b540ffa242a6a9f0a81f354afe07057ffad8 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Mon, 17 Jun 2024 08:38:45 +0000 Subject: [PATCH 11/32] feat(code-quality): add and run tools, update go --- .devcontainer/Dockerfile | 8 +++++++ .devcontainer/devcontainer.json | 4 +++- .gitignore | 2 +- .pre-commit-config.yaml | 34 ++++++++++++++++++++++++++++ .vscode/settings.json | 6 ++++- ERRORs.md | 4 ++-- MAKEFILE.md | 2 +- Makefile | 16 ++++++------- PUBSUB.md | 6 ++--- README.md | 4 ++-- cli/ComandLineCompatibility.txt | 2 +- cli/OutputFormatExamples.txt | 8 +++---- cli/README.md | 40 ++++++++++++++++----------------- cli/go.mod | 10 +++++---- cli/go.sum | 13 ++++++----- cli/script.sql | 1 - cli/sqlc.go | 2 +- go.mod | 10 +++++---- go.sum | 12 +++++----- test/go.mod | 10 +++++---- test/go.sum | 13 ++++++----- test/server_testcases.txt | 2 +- 22 files changed, 132 insertions(+), 77 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .pre-commit-config.yaml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..03babbc --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/devcontainers/go:1-1-bullseye + +# Python environment +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -y install python3-pip git zlib1g-dev unzip + +# Pre-Commit to run pre-commit tools +RUN pip3 install pre-commit diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0685112..8d8a12a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,9 @@ { "name": "Go", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/go:1-1-bullseye", + "build": { + "dockerfile": "Dockerfile" + }, "customizations": { "vscode": { "extensions": [ diff --git a/.gitignore b/.gitignore index 7552c31..4c41f13 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ cli/.vscode .vscode/launch.json pkg/ bin -.env \ No newline at end of file +.env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..796deed --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +# Pre-Commit +# https://pre-commit.com +# Run formatter and more before git commit. +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: detect-private-key + - id: check-merge-conflict +- repo: local + hooks: + - id: gofmt + name: gofmt + language: golang + entry: fmt + files: \.go$ + require_serial: true +- repo: local + hooks: + - id: goimports + name: goimports + language: golang + entry: goimports + files: \.go$ + args: + - -w + require_serial: true +- repo: https://github.com/golangci/golangci-lint + rev: v1.59.1 + hooks: + - id: golangci-lint + exclude: ^pkg/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 3bd92f0..da7b87d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,9 @@ { "gopls": { "ui.semanticTokens": true - } + }, + "go.lintTool": "golangci-lint", + "go.lintFlags": [ + "--fast" + ] } diff --git a/ERRORs.md b/ERRORs.md index 0ec516a..2365346 100644 --- a/ERRORs.md +++ b/ERRORs.md @@ -3,7 +3,7 @@ 12334 │││││ ││││└─ Error Reason -│││└── Data Type +│││└── Data Type ││└─── Class │└──── Module ``` @@ -37,4 +37,4 @@ ## Module | Code | Alternative | Explanation | -|------|-------------|-------------| \ No newline at end of file +|------|-------------|-------------| diff --git a/MAKEFILE.md b/MAKEFILE.md index b20cf0f..a4a8e39 100644 --- a/MAKEFILE.md +++ b/MAKEFILE.md @@ -16,4 +16,4 @@ If you want to see the Documentation: `make doc` - Warning: A browser window wil - Check files with gosec: `make checksec` - Open the repo in github: `make github`. - See changes: `make diff` -- Clean dependencies and precompiled code: `make clean` \ No newline at end of file +- Clean dependencies and precompiled code: `make clean` diff --git a/Makefile b/Makefile index 2a13b04..121f3f8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ GOPATH = $(shell go env GOPATH) # Test SDK -test: +test: cd test; go test -v # GO SDK @@ -11,15 +11,15 @@ sdk: *.go # CLI App $(GOPATH)/bin/sqlc: *.go cli/sqlc.go cd cli; go build -o $(GOPATH)/bin/sqlc - + cli: $(GOPATH)/bin/sqlc github: - open https://github.com/sqlitecloud/sqlitecloud-go - + open https://github.com/sqlitecloud/sqlitecloud-go + diff: git difftool - + # gosec gosec: @@ -40,15 +40,15 @@ endif doc: godoc ifeq ($(wildcard ./src),) - ln -s . src + ln -s . src endif @echo "Hit CRTL-C to stop the documentation server..." @( sleep 1 && open http://localhost:6060/pkg/github.com/sqlitecloud/sqlitecloud-go/ ) & @$(GOPATH)/bin/godoc -http=:6060 -index -play clean: - rm -rf $(GOPATH)/bin/sqlc* + rm -rf $(GOPATH)/bin/sqlc* all: sdk cli test -.PHONY: test sdk +.PHONY: test sdk diff --git a/PUBSUB.md b/PUBSUB.md index 9e397a4..2f9a8bd 100644 --- a/PUBSUB.md +++ b/PUBSUB.md @@ -8,14 +8,14 @@ SQLiteCloud listens to a default port (default 8860) where clients can connect ( When a client send the first **LISTEN channel** command a special reply is sent back to it. This reply is a special command that must be re-executed as is on server side. Command is: -**PAUTH** **client_uuid** **client_secret** +**PAUTH** **client_uuid** **client_secret** - When executed on **client-side**, the client opens a new connection to the server and a new thread is created listening for read events on this new socket. - When executed on **server-side**, client is recognized (using uuid) and authenticated (using secret) and its pubsub socket is set to the current socket used by this new connection. This socket will be used exclusively to deliver notifications. Socket flow is different from the default one because it involves WRITE operations only. The following commands are related to PUB/SUB: -1. **LISTEN channel**: registers the current client as a listener on the notification channel named `channel`. If the current session is already registered as a listener for this notification channel, nothing is done. If channel has the same name of the current database (if any) table then all WRITE operations will be notified. If channel is `*` the all WRITE operations of all the tables of the current database (if any) will be notified. LISTEN takes effect at transaction commit. If channel is not `*` and it is not the name of table in the current database (if any), then it represents a named channel that can be notified only by NOTIFY channel commands. +1. **LISTEN channel**: registers the current client as a listener on the notification channel named `channel`. If the current session is already registered as a listener for this notification channel, nothing is done. If channel has the same name of the current database (if any) table then all WRITE operations will be notified. If channel is `*` the all WRITE operations of all the tables of the current database (if any) will be notified. LISTEN takes effect at transaction commit. If channel is not `*` and it is not the name of table in the current database (if any), then it represents a named channel that can be notified only by NOTIFY channel commands. 2. **UNLISTEN channel**: remove an existing registration for `NOTIFY` events. `UNLISTEN` cancels any existing registration of the current SQLiteCloud session as a listener on the notification channel named `channel`. The special wildcard `*` cancels all listener registrations for the current session. 3. **NOTIFY channel [, payload]**: The `NOTIFY` command sends a notification event together with an optional "payload" string to each client application that has previously executed `LISTEN channel` for the specified channel name in the current database. The payload (if any) is broadcast as is to all other connections without any modification on server side. @@ -140,4 +140,4 @@ UPDATE foo SET id=14,col1='test200' WHERE id=15; "col2": "test101" }] } -``` \ No newline at end of file +``` diff --git a/README.md b/README.md index 58854fa..0f3df2c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The SQLite Cloud Client SDK for Go (sqlitecloud/go-sdk) is the Go Programming La 2. Download the package, and run the [`go mod tidy` command](https://go.dev/ref/mod#go-mod-tidy) to synchronize your module's dependencies: ``` - $ go mod tidy + $ go mod tidy go: downloading github.com/sqlitecloud/sqlitecloud-go v1.0.0 ``` @@ -81,4 +81,4 @@ To get a valid connection string, follow these instructions: ## API Documentation -The complete documentation of the sqlitecloud/go-sdk library is available at: https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go \ No newline at end of file +The complete documentation of the sqlitecloud/go-sdk library is available at: https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go diff --git a/cli/ComandLineCompatibility.txt b/cli/ComandLineCompatibility.txt index c207d4b..068ba23 100644 --- a/cli/ComandLineCompatibility.txt +++ b/cli/ComandLineCompatibility.txt @@ -222,4 +222,4 @@ Usage: mysql [OPTIONS] [database] ❌ --safe-updates ❌ --secure-auth Refuse client connecting to server if it uses old ❌ (pre-4.1.1) protocol -🤔 --show-warnings Show warnings after every statement. \ No newline at end of file +🤔 --show-warnings Show warnings after every statement. diff --git a/cli/OutputFormatExamples.txt b/cli/OutputFormatExamples.txt index e8f8386..f6ba005 100644 --- a/cli/OutputFormatExamples.txt +++ b/cli/OutputFormatExamples.txt @@ -6,7 +6,7 @@ Enter ".help" for usage hints. sqlite> SELECT * FROM Dummy; Andreas|Langholzstraße |91099 Werner|Raiffeisenstr. 4|96145 - + # sqlite3 -csv test.db @@ -41,7 +41,7 @@ SQLite version 3.35.4 2021-04-02 15:20:15 Enter ".help" for usage hints. sqlite> SELECT * FROM Dummy; Names = Andreas -Address = Langholzstraße +Address = Langholzstraße ZIP = 91099 Names = Werner @@ -108,7 +108,7 @@ sqlite> SELECT * FROM Dummy; │ Andreas │ Langholzstraße │ 91099 │ │ Werner │ Raiffeisenstr. 4 │ 96145 │ └─────────┴──────────────────┴───────┘ - + # sqlite3 -column test.db @@ -234,4 +234,4 @@ res.DumpToWriter( out, sqlitecloud.OUTFORMAT_BOX, "|", 0, false ) │ 117 │ One │ More │ 91099 │ Poxdorf │ Langholzstr. 4 │ └─────┴───────────┴──────────┴───────┴──────────┴─────────────────────┘ -res.DumpToWriter( out, sqlitecloud.OUTFORMAT_XML, "|", 0, false ) \ No newline at end of file +res.DumpToWriter( out, sqlitecloud.OUTFORMAT_XML, "|", 0, false ) diff --git a/cli/README.md b/cli/README.md index 7112c36..e1ab088 100644 --- a/cli/README.md +++ b/cli/README.md @@ -169,12 +169,12 @@ General Options: -d, --dbname NAME Use database NAME -b, --bail Stop after hitting an error -?, --help Show this screen - --version Display version information - -Output Format Options: + --version Display version information + +Output Format Options: -o, --output FILE Switch to BATCH mode, execute SQL Commands and send output to FILE, then exit. In BATCH mode, the default output format is switched to QUOTE. - + --echo Disables --quiet, print command(s) before execution --quiet Disables --echo, run command(s) quietly (no messages, only query output) --noheader Turn headers off @@ -209,16 +209,16 @@ Connection Options: .separator TEXT Set output column separator [default: ""] .format [LIST|CSV|QUOTE|TABS|LINE|JSON|HTML|XML|MARKDOWN|TABLE|BOX] Specify the Output mode [default: BOX] -.width [-1|0|] Sets the maximum allowed query result length per line to the +.width [-1|0|] Sets the maximum allowed query result length per line to the terminal width(-1), unlimited (0) or any other width() [default: -1] .timeout Set Timeout for network operations to SECS seconds [default: 10] .compress Use line compression [default: NO] .exit, .quit Exit this program If no parameter is specified, then the default value is used as the parameter value. -Boolean settings are toggled if no parameter is specified. +Boolean settings are toggled if no parameter is specified. -hostname:X > +hostname:X > ``` @@ -227,10 +227,10 @@ hostname:X > ### Starting a new session ```console ./bin/sqlc --host=hostname --dbname=X --tls=INTERN - _____ + _____ / / SQLite Cloud Command Line Application, version 1.0.1 / ___/ / (c) 2021 by SQLite Cloud Inc. - \ ___/ / + \ ___/ / \_ ___/ Enter ".help" for usage hints. hostname:X > @@ -250,7 +250,7 @@ hostname:X > SELECT * FROM Dummy; └─────┴───────────┴──────────┴───────┴──────────┴─────────────────────┘ Rows: 4 - Cols: 6: 282 Bytes Time: 86.43071ms -hostname:X > +hostname:X > ``` ### DELETE'ing a row @@ -258,7 +258,7 @@ hostname:X > hostname:X > DELETE FROM Dummy WHERE ID = 372; OK -hostname:X > +hostname:X > ``` @@ -295,7 +295,7 @@ hostname:X > SELECT * FROM Dummy; Rows: 3 - Cols: 6: 229 Bytes Time: 82.762014ms -hostname:X > +hostname:X > ``` ### Changing the outformat back @@ -308,7 +308,7 @@ hostname:X > .format table hostname:X > SELECT * FROM Dummy; +-----+-----------+----------+-------+----------+--------------- ------+ -| ID | FirstName | LastName | ZIP | City | Address +| ID | FirstName | LastName | ZIP | City | Address | +-----+-----------+----------+-------+----------+--------------- ------+ @@ -335,7 +335,7 @@ y":"Poxdorf","Address":"Langholzstr. 4",}, ] Rows: 3 - Cols: 6: 229 Bytes Time: 87.014696ms -hostname:X > +hostname:X > ``` You can see a nasty line break in the middle of the result line that can easily ruin the screen reading experience. To avoid this annoyance, sqlc build in line trucation mechanism trims its output line in a terminal session by default. The result looks like this: @@ -360,10 +360,10 @@ hostname:X > SELECT * FROM Dummy; ] Rows: 3 - Cols: 6: 229 Bytes Time: 88.874433ms -hostname:X > +hostname:X > ``` -If an output line was trimed to a certain width, the truncation can easily be spoted by the `…` character at the very end of a line. In batch mode, all output is sent to an output file, no line truncation will occure. You can switch off this autotrunction behaviour with a `.width 0` command. To switch back to auto truncation, use `.width -1`. Truncation to any other width is also possible with, for exampel a `.width 35` command. +If an output line was trimed to a certain width, the truncation can easily be spoted by the `…` character at the very end of a line. In batch mode, all output is sent to an output file, no line truncation will occure. You can switch off this autotrunction behaviour with a `.width 0` command. To switch back to auto truncation, use `.width -1`. Truncation to any other width is also possible with, for exampel a `.width 35` command. ### Using Autocomplete To use the build in autocomplete feature, use the [TAB] key. The [TAB] key will try to guess what SQL command you was trying to use and autocomplete this SQL command for you. If autocomplete has guessed the wrong command, keep pressing [TAB] until the right command shows up. The Autocomplete knows all available SQLite Cloud server and SQLite Cloud SQL commands and functions. If you have selected a database (`USE DATABASE ...`), autocomplete will also help you with the Table and Colum names. "UPDATING'ing some data" shows a simple example session: @@ -371,7 +371,7 @@ To use the build in autocomplete feature, use the [TAB] key. The [TAB] key will ### UPDATING'ing some data ```console hostname:X > sel[TAB] -hostname:X > SELECT +hostname:X > SELECT hostname:X > SELECT Fi[TAB] hostname:X > SELECT FirstName hostname:X > SELECT FirstName, Dum[TAB][TAB][TAB][TAB] @@ -391,7 +391,7 @@ Rows: 3 - Cols: 2: 74 Bytes Time: 81.865386ms hostname:X > up[TAB] hostname:X > UPDATE D[TAB] hostname:X > UPDATE Dummy SET La[TAB] -hostname:X > UPDATE Dummy SET LastName +hostname:X > UPDATE Dummy SET LastName hostname:X > UPDATE Dummy SET LastName = "ONE" WH[TAB] hostname:X > UPDATE Dummy SET LastName = "ONE" WHERE id=369[RETURN] OK @@ -406,7 +406,7 @@ hostname:X > SELECT * FROM Dummy; └─────┴───────────┴──────────┴───────┴──────────┴─────────────────────┘ Rows: 3 - Cols: 6: 229 Bytes Time: 82.797135ms -hostname:X > +hostname:X > ``` ### Setting the prompt @@ -472,4 +472,4 @@ echo "LIST DATABASES" > script.sql; ./bin/sqlc sqlitecloud://hostname/X?tls=INTE - [ ] Add --log feature - [x] Remove the table "sqlite_sequence" from dynamic autocomplete scanning - [ ] Implement the Auth command to use the Password feature -- [ ] Add more Test example commands \ No newline at end of file +- [ ] Add more Test example commands diff --git a/cli/go.mod b/cli/go.mod index e47889a..b77b0cd 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -1,11 +1,13 @@ module github.com/sqlitecloud/go-cli -go 1.18 +go 1.22 + +toolchain go1.22.3 require ( github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 github.com/peterh/liner v1.2.2 - golang.org/x/term v0.6.0 + golang.org/x/term v0.21.0 ) require ( @@ -13,8 +15,8 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-runewidth v0.0.3 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect - github.com/xo/dburl v0.13.1 // indirect - golang.org/x/sys v0.7.0 // indirect + github.com/xo/dburl v0.23.2 // indirect + golang.org/x/sys v0.21.0 // indirect ) require github.com/sqlitecloud/sqlitecloud-go v0.0.0 diff --git a/cli/go.sum b/cli/go.sum index a2a0e86..6c0c425 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -2,6 +2,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -17,10 +18,10 @@ github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/xo/dburl v0.13.1 h1:EV+BCdo539sc/mBrny0VxaEGLM0b1U0mJA9RpP80ux0= -github.com/xo/dburl v0.13.1/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= +github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= +github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= diff --git a/cli/script.sql b/cli/script.sql index e39e658..6f7e5b1 100644 --- a/cli/script.sql +++ b/cli/script.sql @@ -1,4 +1,3 @@ LIST DATABASES PING LIST COMMANDS - diff --git a/cli/sqlc.go b/cli/sqlc.go index 5087f54..c6e04e2 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -74,7 +74,7 @@ General Options: Output Format Options: -o, --output FILE Switch to BATCH mode, execute SQL Commands and send output to FILE, then exit. In BATCH mode, the default output format is switched to QUOTE. - + --echo Disables --quiet, print command(s) before execution --quiet Disables --echo, run command(s) quietly (no messages, only query output) --noheader Turn headers off diff --git a/go.mod b/go.mod index d8bd281..1135fcc 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,16 @@ module github.com/sqlitecloud/sqlitecloud-go -go 1.18 +go 1.22 + +toolchain go1.22.3 require ( github.com/pierrec/lz4 v2.6.1+incompatible - github.com/xo/dburl v0.13.1 - golang.org/x/term v0.6.0 + github.com/xo/dburl v0.23.2 + golang.org/x/term v0.21.0 ) require ( github.com/frankban/quicktest v1.14.3 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index 15aa01b..117353b 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,12 @@ github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9F github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/xo/dburl v0.13.1 h1:EV+BCdo539sc/mBrny0VxaEGLM0b1U0mJA9RpP80ux0= -github.com/xo/dburl v0.13.1/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= +github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/test/go.mod b/test/go.mod index 1375454..3bb5bde 100644 --- a/test/go.mod +++ b/test/go.mod @@ -1,6 +1,8 @@ module github.com/sqlitecloud/sqlitecloud-go/test -go 1.18 +go 1.22 + +toolchain go1.22.3 require ( github.com/joho/godotenv v1.5.1 @@ -14,9 +16,9 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/xo/dburl v0.13.1 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/term v0.6.0 // indirect + github.com/xo/dburl v0.23.2 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/test/go.sum b/test/go.sum index 12d63d6..d40deaa 100644 --- a/test/go.sum +++ b/test/go.sum @@ -2,6 +2,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -19,12 +20,12 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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= -github.com/xo/dburl v0.13.1 h1:EV+BCdo539sc/mBrny0VxaEGLM0b1U0mJA9RpP80ux0= -github.com/xo/dburl v0.13.1/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= +github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/test/server_testcases.txt b/test/server_testcases.txt index f8a88df..98b762c 100644 --- a/test/server_testcases.txt +++ b/test/server_testcases.txt @@ -34,4 +34,4 @@ ✅ = Implemented, working. 💣 = Implemented, not working, bug. -💤 = Waiting for Server support or postponed. \ No newline at end of file +💤 = Waiting for Server support or postponed. From 19f2c5a15ed90673c2eb4adcf9f774fc1f8a4e45 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Mon, 17 Jun 2024 14:19:44 +0000 Subject: [PATCH 12/32] feat(workflow): add testing and release --- .github/workflows/release.yaml | 42 ++++++++++++++++++++++++++++++++++ .github/workflows/testing.yaml | 42 ++++++++++++++++++++++++++++++++++ Makefile | 9 +++++++- cli/go.mod | 10 ++++---- cli/go.sum | 13 +++++------ go.mod | 10 ++++---- go.sum | 12 +++++----- test/compress_test.go | 2 +- test/go.mod | 10 ++++---- test/go.sum | 13 +++++------ 10 files changed, 123 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/testing.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..ed3cd32 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,42 @@ +# Create an incremental tag on Github using SemVer https://semver.org: x.y.z +# A tag is a release version on pkg.go.dev, which is +# notified with the publishing go command. + +name: Release + +on: + workflow_dispatch: + inputs: + choice: + type: choice + description: "Release types (x.y.patch / x.minor.z / major.y.z)" + options: + - patch + - minor + - major + +jobs: + release: + if: ${{ github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + name: Tag for release + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Last version + id: last-version + run: echo "::set-output name=tag::$(git describe --tags `git rev-list --tags --max-count=1`)" + - name: Bump version + id: bump-version + uses: olegsu/semver-action@v1 + with: + version: ${{ steps.last-version.outputs.tag }} + bump: ${{ inputs.choice }} + - name: Create tag as version for the package on pkg.go.dev + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "GitHub Actions" + git tag ${{ steps.bump-version.outputs.version }} + git push origin ${{ steps.bump-version.outputs.version }} diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml new file mode 100644 index 0000000..2fc1d21 --- /dev/null +++ b/.github/workflows/testing.yaml @@ -0,0 +1,42 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Test and QA + +on: + push: + workflow_dispatch: + +jobs: + + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.18' + - name: Quality Assurance + run: | + go install golang.org/x/tools/cmd/goimports@latest + gofmt -l ./*.go + goimports -e -d ./*.go + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + - name: Tests + env: + SQLITE_CONNECTION_STRING: ${{ vars.SQLITE_CONNECTION_STRING }} + SQLITE_USER: ${{ secrets.SQLITE_USER }} + SQLITE_PASSWORD: ${{ secrets.SQLITE_PASSWORD }} + SQLITE_API_KEY: ${{ secrets.SQLITE_API_KEY }} + SQLITE_HOST: ${{ vars.SQLITE_HOST }} + SQLITE_DB: ${{ vars.SQLITE_DB }} + SQLITE_PORT: ${{ vars.SQLITE_PORT }} + run: make test-codecov + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Makefile b/Makefile index 121f3f8..c58fa0b 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,15 @@ GOPATH = $(shell go env GOPATH) +setup-ide: + pre-commit install + go install golang.org/x/tools/cmd/goimports@latest + # Test SDK test: - cd test; go test -v + cd test; go test -v . + +test-codecov: + cd test; go test -v -race -coverprofile=coverage.txt -covermode=atomic . # GO SDK sdk: *.go diff --git a/cli/go.mod b/cli/go.mod index b77b0cd..e47889a 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -1,13 +1,11 @@ module github.com/sqlitecloud/go-cli -go 1.22 - -toolchain go1.22.3 +go 1.18 require ( github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 github.com/peterh/liner v1.2.2 - golang.org/x/term v0.21.0 + golang.org/x/term v0.6.0 ) require ( @@ -15,8 +13,8 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-runewidth v0.0.3 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect - github.com/xo/dburl v0.23.2 // indirect - golang.org/x/sys v0.21.0 // indirect + github.com/xo/dburl v0.13.1 // indirect + golang.org/x/sys v0.7.0 // indirect ) require github.com/sqlitecloud/sqlitecloud-go v0.0.0 diff --git a/cli/go.sum b/cli/go.sum index 6c0c425..a2a0e86 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -2,7 +2,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -18,10 +17,10 @@ github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= -github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= +github.com/xo/dburl v0.13.1 h1:EV+BCdo539sc/mBrny0VxaEGLM0b1U0mJA9RpP80ux0= +github.com/xo/dburl v0.13.1/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= diff --git a/go.mod b/go.mod index 1135fcc..d8bd281 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,14 @@ module github.com/sqlitecloud/sqlitecloud-go -go 1.22 - -toolchain go1.22.3 +go 1.18 require ( github.com/pierrec/lz4 v2.6.1+incompatible - github.com/xo/dburl v0.23.2 - golang.org/x/term v0.21.0 + github.com/xo/dburl v0.13.1 + golang.org/x/term v0.6.0 ) require ( github.com/frankban/quicktest v1.14.3 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index 117353b..15aa01b 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,12 @@ github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9F github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= -github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +github.com/xo/dburl v0.13.1 h1:EV+BCdo539sc/mBrny0VxaEGLM0b1U0mJA9RpP80ux0= +github.com/xo/dburl v0.13.1/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/test/compress_test.go b/test/compress_test.go index 9d4998f..1b26fb6 100644 --- a/test/compress_test.go +++ b/test/compress_test.go @@ -34,7 +34,7 @@ func TestCompress(t *testing.T) { apikey, _ := os.LookupEnv("SQLITE_API_KEY") connectionString += "?apikey=" + apikey - url, err := url.Parse(connectionString) + url, _ := url.Parse(connectionString) values := url.Query() values.Add("compress", sqlitecloud.CompressModeLZ4) url.RawQuery = values.Encode() diff --git a/test/go.mod b/test/go.mod index 3bb5bde..1375454 100644 --- a/test/go.mod +++ b/test/go.mod @@ -1,8 +1,6 @@ module github.com/sqlitecloud/sqlitecloud-go/test -go 1.22 - -toolchain go1.22.3 +go 1.18 require ( github.com/joho/godotenv v1.5.1 @@ -16,9 +14,9 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/xo/dburl v0.23.2 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect + github.com/xo/dburl v0.13.1 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/term v0.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/test/go.sum b/test/go.sum index d40deaa..12d63d6 100644 --- a/test/go.sum +++ b/test/go.sum @@ -2,7 +2,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -20,12 +19,12 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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= -github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= -github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +github.com/xo/dburl v0.13.1 h1:EV+BCdo539sc/mBrny0VxaEGLM0b1U0mJA9RpP80ux0= +github.com/xo/dburl v0.13.1/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 5a0cd25775bcb6be520a1d404963cc9879e1db30 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Tue, 18 Jun 2024 08:15:56 +0000 Subject: [PATCH 13/32] chore(code-quality): errors in code standard --- auxiliary.go | 4 ++-- chunk.go | 57 +++++++++++++++++++++------------------------ connection.go | 5 ++-- helper.go | 38 ------------------------------ result.go | 34 +++++++++++++-------------- row.go | 28 +++++++++++----------- test/commons.go | 4 +++- test/result_test.go | 25 ++++++++++++++++++++ 8 files changed, 89 insertions(+), 106 deletions(-) create mode 100644 test/result_test.go diff --git a/auxiliary.go b/auxiliary.go index 733e299..403fcee 100644 --- a/auxiliary.go +++ b/auxiliary.go @@ -46,7 +46,7 @@ func (this *SQCloud) GetAutocompleteTokens() (tokens []string) { if table != "sqlite_sequence" { tokens = append(tokens, table) for _, column := range this.ListColumns(table) { - tokens = append(tokens, fmt.Sprintf("%s", column)) + tokens = append(tokens, column) tokens = append(tokens, fmt.Sprintf("%s.%s", table, column)) } } @@ -239,7 +239,7 @@ func resultToMap(result *Result, err error) (map[string]interface{}, error) { keyValueList[key.GetString()] = nil // case val.IsJSON(): default: - return map[string]interface{}{}, errors.New(fmt.Sprintf("ERROR: Value type %v not supported", val.GetType())) + return map[string]interface{}{}, fmt.Errorf("ERROR: Value type %v not supported", val.GetType()) } } return keyValueList, nil diff --git a/chunk.go b/chunk.go index 1a3e81f..9f97b70 100644 --- a/chunk.go +++ b/chunk.go @@ -60,26 +60,26 @@ func (this *Chunk) Uncompress() error { var err error var hStartIndex uint64 = 1 // Index of the start of the uncompressed header in chunk (*0 NROWS NCOLS ...) - var zStartIndex uint64 = 0 // Index of the start of the compressed buffer in this chunk () + var zStartIndex uint64 // Index of the start of the compressed buffer in this chunk () - var LEN uint64 = 0 - var lLEN uint64 = 0 + var LEN uint64 + var lLEN uint64 - var COMPRESSED uint64 = 0 - var cLEN uint64 = 0 + var COMPRESSED uint64 + var cLEN uint64 - var UNCOMPRESSED uint64 = 0 - var iUNCOMPRESSED int = 0 - var uLEN uint64 = 0 + var UNCOMPRESSED uint64 + var iUNCOMPRESSED int + var uLEN uint64 - LEN, _, lLEN, err = this.readUInt64At(hStartIndex) // "%TLEN " - hStartIndex += lLEN // hStartIndex -> "CLEN ULEN *0 NROWS NCOLS " + LEN, _, lLEN, _ = this.readUInt64At(hStartIndex) // "%TLEN " + hStartIndex += lLEN // hStartIndex -> "CLEN ULEN *0 NROWS NCOLS " - COMPRESSED, _, cLEN, err = this.readUInt64At(hStartIndex) // "CLEN " - hStartIndex += cLEN // hStartIndex -> "ULEN *0 NROWS NCOLS " + COMPRESSED, _, cLEN, _ = this.readUInt64At(hStartIndex) // "CLEN " + hStartIndex += cLEN // hStartIndex -> "ULEN *0 NROWS NCOLS " - UNCOMPRESSED, _, uLEN, err = this.readUInt64At(hStartIndex) // "ULEN " - hStartIndex += uLEN // hStartIndex -> "*0 NROWS NCOLS " + UNCOMPRESSED, _, uLEN, _ = this.readUInt64At(hStartIndex) // "ULEN " + hStartIndex += uLEN // hStartIndex -> "*0 NROWS NCOLS " zStartIndex = LEN - COMPRESSED + lLEN + 1 // zStartIndex -> "" hLEN := zStartIndex - hStartIndex // = len( "*0 NROWS NCOLS " ) @@ -142,7 +142,6 @@ func (this *Chunk) readUInt64At(offset uint64) (uint64, uint64, uint64, error) { } bytesRead++ } - return 0, 0, 0, errors.New("Overflow") } func (this *Chunk) readValueAt(offset uint64) (Value, uint64, error) { @@ -222,21 +221,19 @@ func (this *Value) readBufferAt(chunk *Chunk, offset uint64) (uint64, error) { } func protocolBufferFromValue(v interface{}) [][]byte { - if v == nil { + switch v := v.(type) { + case nil: return protocolBufferFromNull() - } else { - switch v.(type) { - case int, int8, int16, int32, int64: - return protocolBufferFromInt(v) - case float32, float64: - return protocolBufferFromFloat(v) - case string: - return protocolBufferFromString(v.(string), true) - case []byte: - return protocolBufferFromBytes(v.([]byte)) - default: - return make([][]byte, 0) - } + case int, int8, int16, int32, int64: + return protocolBufferFromInt(v) + case float32, float64: + return protocolBufferFromFloat(v) + case string: + return protocolBufferFromString(v, true) + case []byte: + return protocolBufferFromBytes(v) + default: + return make([][]byte, 0) } } @@ -404,8 +401,6 @@ func (this *SQCloud) sendArray(command string, values []interface{}) (int, error if bytesSent != bytesToSend { return bytesSent, errors.New("Partitial data sent") } - } else { - bytesSent = 0 } } diff --git a/connection.go b/connection.go index 1592941..b4d137b 100644 --- a/connection.go +++ b/connection.go @@ -248,7 +248,7 @@ func (this *SQCloud) CheckConnectionParameter() error { } if this.Timeout < 0 { - return errors.New(fmt.Sprintf("Invalid Timeout (%s)", this.Timeout.String())) + return fmt.Errorf("Invalid Timeout (%s)", this.Timeout.String()) } return nil @@ -318,7 +318,6 @@ func (this *SQCloud) reconnect() error { var dialer = net.Dialer{} dialer.Timeout = this.Timeout - dialer.DualStack = true switch { case this.cert != nil: @@ -532,7 +531,7 @@ func (this *SQCloud) Compress(CompressMode string) error { case this.sock == nil: return errors.New("Not connected") case c == "": - return errors.New(fmt.Sprintf("Invalid method (%s)", CompressMode)) + return fmt.Errorf("Invalid method (%s)", CompressMode) default: return this.Execute(c) } diff --git a/helper.go b/helper.go index c45d337..6c3056a 100644 --- a/helper.go +++ b/helper.go @@ -20,7 +20,6 @@ package sqlitecloud import ( "errors" "fmt" - "strconv" "strings" ) @@ -102,40 +101,3 @@ func parseBool(value string, defaultValue bool) (bool, error) { return false, errors.New("ERROR: Not a Boolean value") } } - -// parseInt parses the given string value and tries to extract its value as an int value. -// If value was an empty string, the defaultValue is evaluated instead. -// If the given string value does not resemble a numeric value or its numeric value is smaler than minValue or exceeds maxValue, an error describing the problem is returned. -func parseInt(value string, defaultValue int, minValue int, maxValue int) (int, error) { - // println( "ParseInt = " + value ) - value = strings.TrimSpace(value) - if value == "" { - value = fmt.Sprintf("%d", defaultValue) - } - if v, err := strconv.Atoi(value); err == nil { - if v < minValue { - return 0, errors.New("ERROR: The given Number is too small") - } - if v > maxValue { - return 0, errors.New("ERROR: The given Number is too large") - } - return v, nil - } else { - return 0, err - } -} - -// parseString returns a non empty string. -// The given string value is trimmed. -// If the given string is an empty string, the defaultValue is evaluated instead. -// If the given string and the defaultValue are emptry strings, an error is returned. -func parseString(value string, defaultValue string) (string, error) { - value = strings.TrimSpace(value) - if value == "" { - value = strings.TrimSpace(defaultValue) - } - if value == "" { - return "", errors.New("ERROR: Empty value") - } - return value, nil -} diff --git a/result.go b/result.go index d6c4de4..86cdbc3 100644 --- a/result.go +++ b/result.go @@ -880,8 +880,8 @@ func (this *SQCloud) readResult() (*Result, error) { // Array case CMD_ARRAY: var offset uint64 = 1 // skip the first type byte - var N uint64 = 0 - var bytesRead uint64 = 0 + var N uint64 + var bytesRead uint64 if _, _, bytesRead, err = chunk.readUInt64At(offset); err != nil { return nil, err @@ -900,14 +900,14 @@ func (this *SQCloud) readResult() (*Result, error) { for row := uint64(0); row < N; row++ { result.rows[row].result = &result - result.rows[row].index = row - result.rows[row].columns = make([]Value, 1) + result.rows[row].Index = row + result.rows[row].Columns = make([]Value, 1) - switch result.rows[row].columns[0], bytesRead, err = chunk.readValueAt(offset); { + switch result.rows[row].Columns[0], bytesRead, err = chunk.readValueAt(offset); { case err != nil: return nil, err default: - columnLength := result.rows[row].columns[0].GetLength() + columnLength := result.rows[row].Columns[0].GetLength() if result.Width[0] < columnLength { result.Width[0] = columnLength } @@ -929,12 +929,12 @@ func (this *SQCloud) readResult() (*Result, error) { // RowSet Chunk: /LEN IDX:VERSION ROWS COLS DATA var offset uint64 = 1 // skip the first type byte - var bytesRead uint64 = 0 - var LEN uint64 = 0 - var IDX uint64 = 0 - var VERSION uint64 = 0 - var NROWS uint64 = 0 - var NCOLS uint64 = 0 + var bytesRead uint64 + var LEN uint64 + var IDX uint64 + var VERSION uint64 + var NROWS uint64 + var NCOLS uint64 // Detect end of Rowset Chunk directly without parsing... if Type == CMD_ROWSET_CHUNK { @@ -965,7 +965,7 @@ func (this *SQCloud) readResult() (*Result, error) { } // 0..columns-1 offset += bytesRead - LEN = LEN + offset // check for overreading... + LEN += offset // check for overreading... if Type == CMD_ROWSET_CHUNK && NROWS == 0 && NCOLS == 0 { return &result, nil @@ -1105,17 +1105,17 @@ func (this *SQCloud) readResult() (*Result, error) { for row := uint64(0); row < NROWS; row++ { rows[row].result = &result - rows[row].index = rowIndex - rows[row].columns = make([]Value, int(NCOLS)) + rows[row].Index = rowIndex + rows[row].Columns = make([]Value, int(NCOLS)) rowIndex++ for column := uint64(0); column < NCOLS; column++ { - switch rows[row].columns[column], bytesRead, err = chunk.readValueAt(offset); { + switch rows[row].Columns[column], bytesRead, err = chunk.readValueAt(offset); { case err != nil: return nil, err default: - columnLength := rows[row].columns[column].GetLength() + columnLength := rows[row].Columns[column].GetLength() if result.Width[column] < columnLength { result.Width[column] = columnLength } diff --git a/row.go b/row.go index 3fec23d..35e1db8 100644 --- a/row.go +++ b/row.go @@ -29,8 +29,8 @@ import ( type ResultRow struct { result *Result - index uint64 `json:"Index"` // 0, 1, ... rows-1 - columns []Value `json:"ColumnValues"` + Index uint64 `json:"Index"` // 0, 1, ... rows-1 + Columns []Value `json:"ColumnValues"` } // ToJSON returns a JSON representation of this query result row. @@ -42,7 +42,7 @@ func (this *ResultRow) IsFirst() bool { case this.result.GetNumberOfRows() < 1: return false default: - return this.index == 0 + return this.Index == 0 } } @@ -52,7 +52,7 @@ func (this *ResultRow) IsLast() bool { case this.result.GetNumberOfRows() < 1: return false default: - return this.index == this.result.GetNumberOfRows()-1 + return this.Index == this.result.GetNumberOfRows()-1 } } @@ -62,7 +62,7 @@ func (this *ResultRow) IsEOF() bool { case this.result.GetNumberOfRows() < 1: return true default: - return this.index >= this.result.GetNumberOfRows() + return this.Index >= this.result.GetNumberOfRows() } } @@ -78,7 +78,7 @@ func (this *ResultRow) Rewind() *ResultRow { // Next fetches the next row in this query result and returns it, otherwise if there is no next row, nil is returned. func (this *ResultRow) Next() *ResultRow { - switch row, err := this.result.GetRow(this.index + 1); { + switch row, err := this.result.GetRow(this.Index + 1); { case err != nil: return nil default: @@ -86,14 +86,14 @@ func (this *ResultRow) Next() *ResultRow { } } -func (this *ResultRow) GetNumberOfColumns() uint64 { return uint64(len(this.columns)) } +func (this *ResultRow) GetNumberOfColumns() uint64 { return uint64(len(this.Columns)) } func (this *ResultRow) GetValue(Column uint64) (*Value, error) { switch { case Column >= this.GetNumberOfColumns(): return nil, errors.New("Column index out of bounds") default: - return &this.columns[Column], nil + return &this.Columns[Column], nil } } @@ -211,37 +211,37 @@ func (this *ResultRow) IsText(Column uint64) bool { // GetStringValue returns the contents in column Column of this query result row as string. // The Column index is an unsigned int in the range of 0...GetNumberOfColumns() - 1. func (this *ResultRow) GetString(Column uint64) (string, error) { - return this.result.GetStringValue(this.index, Column) + return this.result.GetStringValue(this.Index, Column) } // GetInt32Value returns the contents in column Column of this query result row as int32. // The Column index is an unsigned int in the range of 0...GetNumberOfColumns() - 1. func (this *ResultRow) GetInt32(Column uint64) (int32, error) { - return this.result.GetInt32Value(this.index, Column) + return this.result.GetInt32Value(this.Index, Column) } // GetInt64Value returns the contents in column Column of this query result row as int64. // The Column index is an unsigned int in the range of 0...GetNumberOfColumns() - 1. func (this *ResultRow) GetInt64(Column uint64) (int64, error) { - return this.result.GetInt64Value(this.index, Column) + return this.result.GetInt64Value(this.Index, Column) } // GetFloat32Value returns the contents in column Column of this query result row as float32. // The Column index is an unsigned int in the range of 0...GetNumberOfColumns() - 1. func (this *ResultRow) GetFloat32(Column uint64) (float32, error) { - return this.result.GetFloat32Value(this.index, Column) + return this.result.GetFloat32Value(this.Index, Column) } // GetFloat64Value returns the contents in column Column of this query result row as float64. // The Column index is an unsigned int in the range of 0...GetNumberOfColumns() - 1. func (this *ResultRow) GetFloat64(Column uint64) (float64, error) { - return this.result.GetFloat64Value(this.index, Column) + return this.result.GetFloat64Value(this.Index, Column) } // GetSQLDateTime parses this query result value in column Column as an SQL-DateTime and returns its value. // The Column index is an unsigned int in the range of 0...GetNumberOfColumns() - 1. func (this *ResultRow) GetSQLDateTime(Column uint64) (time.Time, error) { - return this.result.GetSQLDateTime(this.index, Column) + return this.result.GetSQLDateTime(this.Index, Column) } //////// diff --git a/test/commons.go b/test/commons.go index 4f1510e..ef73b2b 100644 --- a/test/commons.go +++ b/test/commons.go @@ -2,6 +2,7 @@ package test import ( "flag" + "fmt" "log" "os" "testing" @@ -31,8 +32,9 @@ func contains[T comparable](s []T, e T) bool { func setupDatabase(t *testing.T) (*sqlitecloud.SQCloud, func()) { connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") apikey, _ := os.LookupEnv("SQLITE_API_KEY") + dbname, _ := os.LookupEnv("SQLITE_DB") - db, err := sqlitecloud.Connect(connectionString + "?apikey=" + apikey) + db, err := sqlitecloud.Connect(fmt.Sprintf("%s/%s?apikey=%s", connectionString, dbname, apikey)) if err != nil { t.Fatal("Connection error: ", err.Error()) } diff --git a/test/result_test.go b/test/result_test.go new file mode 100644 index 0000000..cabeb71 --- /dev/null +++ b/test/result_test.go @@ -0,0 +1,25 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRowToJson(t *testing.T) { + db, cleanup := setupDatabase(t) + defer cleanup() + + result, err := db.Select("SELECT * FROM albums WHERE AlbumId = 1 LIMIT 1;") + + assert.Nil(t, err) + assert.Equal(t, result.GetNumberOfRows(), uint64(1)) + + row, err := result.GetRow(0) + assert.Nil(t, err) + + json, err := row.ToJSON() + assert.Nil(t, err) + + assert.JSONEq(t, `{"Index":0,"ColumnValues":[{"Type":58,"Buffer":"MQ=="},{"Type":43,"Buffer":"Rm9yIFRob3NlIEFib3V0IFRvIFJvY2sgV2UgU2FsdXRlIFlvdQ=="},{"Type":58,"Buffer":"MQ=="}]}`, string(json)) +} From c06e4d763f3d4c01351b1e59878d673755f6bce0 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Tue, 18 Jun 2024 08:56:07 +0000 Subject: [PATCH 14/32] feat(workflow): tag and publish for release --- .github/workflows/release.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ed3cd32..97a5e06 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -34,9 +34,11 @@ jobs: with: version: ${{ steps.last-version.outputs.tag }} bump: ${{ inputs.choice }} - - name: Create tag as version for the package on pkg.go.dev + - name: Create tag as version for the package run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "GitHub Actions" git tag ${{ steps.bump-version.outputs.version }} git push origin ${{ steps.bump-version.outputs.version }} + - name: Publish on pkg.go.dev + run: GOPROXY=proxy.golang.org go list -m github.com/sqlitecloud/sqlitecloud-go@${{ steps.bump-version.outputs.version }} From fae10cdcee6b96f2038e877b5ccce0249fbe0958 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Tue, 18 Jun 2024 09:35:58 +0000 Subject: [PATCH 15/32] feat(readme): update --- .devcontainer/devcontainer.json | 3 ++- MAKEFILE.md | 2 +- Makefile | 3 +++ README.md | 46 +++++++++++++++++++++++++++------ 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8d8a12a..c76a3c9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,8 @@ "customizations": { "vscode": { "extensions": [ - "eamodio.gitlens" + "eamodio.gitlens", + "golang.go" ] } } diff --git a/MAKEFILE.md b/MAKEFILE.md index a4a8e39..c2600ea 100644 --- a/MAKEFILE.md +++ b/MAKEFILE.md @@ -1,4 +1,4 @@ -# SQLite Cloud go-sdk Makefile +# SQLite Cloud sqlitecloud-go Makefile ### Run the test for the SDK If you want to run the Test programs: `make test` diff --git a/Makefile b/Makefile index c58fa0b..bc7e59d 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,9 @@ GOPATH = $(shell go env GOPATH) setup-ide: pre-commit install go install golang.org/x/tools/cmd/goimports@latest + go mod tidy + cd test; go mod tidy + cd cli; go mod tidy # Test SDK test: diff --git a/README.md b/README.md index 0f3df2c..1920bb3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,31 @@ -# SQLite Cloud Client SDK for Go v1.0.2 +# Driver for SQLite Cloud -The SQLite Cloud Client SDK for Go (sqlitecloud/go-sdk) is the Go Programming Language application programmer's interface to [SQLite Cloud](https://sqlitecloud.io/). It is a set of library functions that allow client programs to pass queries and SQL commands to the SQLite Cloud backend server and to receive the results of these queries. In addition to the standard SQLite statements, several other [commands](https://docs.sqlitecloud.io/docs/commands) are supported. +

+ SQLite Cloud logo +

-## Getting Started +- [Driver for SQLite Cloud](#driver-for-sqlite-cloud) +- [Example](#example) +- [Get a Connection String](#get-a-connection-string) +- [Setting up the IDE](#setting-up-the-ide) -### Use the SQLite Cloud Client SDK in your Go code +[![Test and QA](https://github.com/sqlitecloud/sqlitecloud-go/actions/workflows/testing.yaml/badge.svg?branch=main)](https://github.com/sqlitecloud/sqlitecloud-go/actions/workflows/testing.yaml) +[![codecov](https://codecov.io/gh/sqlitecloud/sqlitecloud-go/graph/badge.svg?token=5MAG3G4X01)](https://codecov.io/gh/sqlitecloud/sqlitecloud-go) +[![GitHub Tag](https://img.shields.io/github/v/tag/sqlitecloud/sqlitecloud-go?label=version&link=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsqlitecloud%2Fsqlitecloud-go)](https://github.com/sqlitecloud/sqlitecloud-go) +[![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/sqlitecloud/sqlitecloud-go?link=https%3A%2F%2Fgithub.com%2Fsqlitecloud%2Fsqlitecloud-go)](https://github.com/sqlitecloud/sqlitecloud-go) + +--- + +[SQLite Cloud](https://sqlitecloud.io) for Go is a powerful package that allows you to interact with the SQLite Cloud database seamlessly. It provides methods for various database operations. This package is designed to simplify database operations in Go applications, making it easier than ever to work with SQLite Cloud. In addition to the standard SQLite statements, several other [commands](https://docs.sqlitecloud.io/docs/commands) are supported. + +- Documentation: https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go +- Source: [https://github.com/sqlitecloud/sqlitecloud-go](https://github.com/sqlitecloud/sqlitecloud-go) +- Site: [https://sqlitecloud.io](https://sqlitecloud.io/developers) + +## Example + +### Use SQLite Cloud in your Go code 1. Import the package in your Go source code: @@ -15,7 +35,7 @@ The SQLite Cloud Client SDK for Go (sqlitecloud/go-sdk) is the Go Programming La 2. Download the package, and run the [`go mod tidy` command](https://go.dev/ref/mod#go-mod-tidy) to synchronize your module's dependencies: - ``` + ```bash $ go mod tidy go: downloading github.com/sqlitecloud/sqlitecloud-go v1.0.0 ``` @@ -65,7 +85,7 @@ func main() { } ``` -## Get a connection string +## Get a Connection String You can connect to any cloud database using a special connection string in the form: @@ -79,6 +99,16 @@ To get a valid connection string, follow these instructions: - Get the connection string by clicking on the node address in the [Dashboard Nodes](https://docs.sqlitecloud.io/docs/introduction/nodes) section. A valid connection string will be copied to your clipboard. - Add the database name to your connection string. -## API Documentation -The complete documentation of the sqlitecloud/go-sdk library is available at: https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go + +## Setting up the IDE + +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) + +To start working on this project, follow these steps: + +1. Open the project folder in Visual Studio Code (VSCode) using the remote container feature. +2. In the terminal, run the command `make setup-ide` to install the necessary development tools. +3. To ensure code quality, we have integrated [pre-commit](https://github.com/pre-commit/pre-commit) into the workflow. Before committing your changes to Git, pre-commit will run several tasks defined in the `.pre-commit-config.yaml` file. + +By following these steps, you will have a fully set up development environment and be ready to contribute to the project. From d23727300d88eaa48e2e432506b7a4bd886d17eb Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Tue, 18 Jun 2024 14:21:46 +0000 Subject: [PATCH 16/32] fix(release): add prefix version `v` --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 97a5e06..739fee3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -32,7 +32,7 @@ jobs: id: bump-version uses: olegsu/semver-action@v1 with: - version: ${{ steps.last-version.outputs.tag }} + version: v${{ steps.last-version.outputs.tag }} bump: ${{ inputs.choice }} - name: Create tag as version for the package run: | From 74db2b075b88e55b75431d91a6e977c3428d83ad Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Tue, 18 Jun 2024 14:25:36 +0000 Subject: [PATCH 17/32] fix(release): add prefix version `v` --- .github/workflows/release.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 739fee3..6c08f7b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -32,13 +32,13 @@ jobs: id: bump-version uses: olegsu/semver-action@v1 with: - version: v${{ steps.last-version.outputs.tag }} + version: ${{ steps.last-version.outputs.tag }} bump: ${{ inputs.choice }} - name: Create tag as version for the package run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "GitHub Actions" - git tag ${{ steps.bump-version.outputs.version }} - git push origin ${{ steps.bump-version.outputs.version }} + git tag v${{ steps.bump-version.outputs.version }} + git push origin v${{ steps.bump-version.outputs.version }} - name: Publish on pkg.go.dev - run: GOPROXY=proxy.golang.org go list -m github.com/sqlitecloud/sqlitecloud-go@${{ steps.bump-version.outputs.version }} + run: GOPROXY=proxy.golang.org go list -m github.com/sqlitecloud/sqlitecloud-go@v${{ steps.bump-version.outputs.version }} From a509172834e0c5cf809c81692320639da7a232f3 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Tue, 18 Jun 2024 16:23:17 +0000 Subject: [PATCH 18/32] feat(cli): add release workflow --- .github/workflows/release-cli.yaml | 104 +++++++++++++++++++++++++++++ .github/workflows/release.yaml | 3 +- Makefile | 2 +- README.md | 6 +- 4 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release-cli.yaml diff --git a/.github/workflows/release-cli.yaml b/.github/workflows/release-cli.yaml new file mode 100644 index 0000000..c85f524 --- /dev/null +++ b/.github/workflows/release-cli.yaml @@ -0,0 +1,104 @@ +# Create an incremental tag (like `cli-v1.2.0`) on Github using SemVer https://semver.org: x.y.z +# Create the Release (like `cli-v1.2.0`) based on this tag and with the same name. +# Build the CLI for all OS and upload them as assets to the release. + +name: Release CLI + +on: + workflow_dispatch: + inputs: + choice: + type: choice + description: "Release types (x.y.patch / x.minor.z / major.y.z)" + options: + - patch + - minor + - major + +jobs: + release-cli: + if: ${{ github.ref == 'refs/heads/main' }} + name: Release CLI + strategy: + matrix: + include: + - goarch: amd64 + goos: linux + + - goarch: amd64 + goos: windows + + - goarch: arm64 + goos: darwin + + - goarch: amd64 + goos: darwin + + runs-on: ubuntu-latest + env: + OSNAME: ${{matrix.goos == 'darwin' && 'macos' || matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOOS: ${{ matrix.goos }} + ARCHNAME: ${{ matrix.goarch == 'amd64' && 'x86-64' || matrix.goarch }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set env var + run: echo "ZIPFILE=sqlitecloud-go-${{ steps.tag-and-release.outputs.name }}-${{ env.OSNAME }}-${{ env.ARCHNAME }}.zip" >> $GITHUB_ENV + + - name: Build CLI + run: | + cd GO/cli + go build -o ../sqlc + cd .. + zip ${{ env.ZIPFILE }} sqlc + + - name: Last version + id: last-version + # last tag that starts with 'cli-v', eg: cli-v1.2.0 but outputs it as: v1.2.0 + run: echo "::set-output name=number::$(git tag --list 'cli-v*' | sort -V | tail -n1 | sed 's/cli-//')" + + - name: Bump version + id: bump-version + uses: olegsu/semver-action@v1 + with: + version: ${{ steps.last-version.outputs.number }} + bump: ${{ inputs.choice }} + + - name: Tag and Release name + id: tag-and-release + # eg: cli-v1.2.0 + run: echo "::set-output name=name::cli-v$(git tag --list 'v*' | sort -V | tail -n1)" + + - name: Create Release for CLI + id: release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag-and-release.outputs.name }} + name: Release ${{ steps.tag-and-release.outputs.name }} + draft: false + generate_release_notes: true + make_latest: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + if: matrix.goos != 'darwin' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.release.outputs.upload_url }} + asset_path: ./GO/${{ env.ZIPFILE }} + asset_name: ${{ env.ZIPFILE }} + asset_content_type: application/zip + + - name: Upload binary artifact + uses: actions/upload-artifact@v3 + if: matrix.goos == 'darwin' + with: + name: ${{ env.ZIPFILE }} + path: ./GO/${{ env.ZIPFILE }} + if-no-files-found: error diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6c08f7b..99a65ba 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -27,7 +27,8 @@ jobs: fetch-depth: 0 - name: Last version id: last-version - run: echo "::set-output name=tag::$(git describe --tags `git rev-list --tags --max-count=1`)" + # last tag that starts with 'v', eg: v1.0.3 + run: echo "::set-output name=tag::$(git tag --list 'v*' | sort -V | tail -n1)" - name: Bump version id: bump-version uses: olegsu/semver-action@v1 diff --git a/Makefile b/Makefile index bc7e59d..6c36b7a 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ test: cd test; go test -v . test-codecov: - cd test; go test -v -race -coverprofile=coverage.txt -covermode=atomic . + cd test; go test -v -race -coverprofile=coverage.out -covermode=atomic . # GO SDK sdk: *.go diff --git a/README.md b/README.md index 1920bb3..4e7c9fd 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ [![Test and QA](https://github.com/sqlitecloud/sqlitecloud-go/actions/workflows/testing.yaml/badge.svg?branch=main)](https://github.com/sqlitecloud/sqlitecloud-go/actions/workflows/testing.yaml) [![codecov](https://codecov.io/gh/sqlitecloud/sqlitecloud-go/graph/badge.svg?token=5MAG3G4X01)](https://codecov.io/gh/sqlitecloud/sqlitecloud-go) -[![GitHub Tag](https://img.shields.io/github/v/tag/sqlitecloud/sqlitecloud-go?label=version&link=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsqlitecloud%2Fsqlitecloud-go)](https://github.com/sqlitecloud/sqlitecloud-go) -[![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/sqlitecloud/sqlitecloud-go?link=https%3A%2F%2Fgithub.com%2Fsqlitecloud%2Fsqlitecloud-go)](https://github.com/sqlitecloud/sqlitecloud-go) +[![GitHub Tag](https://img.shields.io/github/v/tag/sqlitecloud/sqlitecloud-go?label=version&link=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsqlitecloud%2Fsqlitecloud-go)](https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go) +[![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/sqlitecloud/sqlitecloud-go?link=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsqlitecloud%2Fsqlitecloud-go)](https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go) --- [SQLite Cloud](https://sqlitecloud.io) for Go is a powerful package that allows you to interact with the SQLite Cloud database seamlessly. It provides methods for various database operations. This package is designed to simplify database operations in Go applications, making it easier than ever to work with SQLite Cloud. In addition to the standard SQLite statements, several other [commands](https://docs.sqlitecloud.io/docs/commands) are supported. -- Documentation: https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go +- Documentation: [https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go#section-documentation](https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-go#section-documentation) - Source: [https://github.com/sqlitecloud/sqlitecloud-go](https://github.com/sqlitecloud/sqlitecloud-go) - Site: [https://sqlitecloud.io](https://sqlitecloud.io/developers) From 53fece6cfcfe6f12c0f01794b67c2f59f8e7b30d Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 18 Jun 2024 15:35:05 -0600 Subject: [PATCH 19/32] fix(cli): fix the usage string, some unexpected character were causing a bug in the library used to parse arguments. For example, the Newline setting had an empty string value instead of the usual default value. --- cli/sqlc.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/sqlc.go b/cli/sqlc.go index c6e04e2..d1748ea 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -74,13 +74,13 @@ General Options: Output Format Options: -o, --output FILE Switch to BATCH mode, execute SQL Commands and send output to FILE, then exit. In BATCH mode, the default output format is switched to QUOTE. - + --echo Disables --quiet, print command(s) before execution --quiet Disables --echo, run command(s) quietly (no messages, only query output) --noheader Turn headers off - --nullvalue TEXT Set text string for NULL values [default: "NULL"] + --nullvalue TEXT Set text string for NULL values [default: "NULL"] --newline SEP Set output row separator [default: "\r\n"] - --separator SEP Set output column separator [default::"|"] + --separator SEP Set output column separator [default: "|"] --format (LIST|CSV|QUOTE|TABS|LINE|JSON|HTML|XML|MARKDOWN|TABLE|BOX) Specify the Output mode [default::BOX] From 88a5b93d3126281dffaacf8bb4fd286ed13832af Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Tue, 18 Jun 2024 19:46:11 +0000 Subject: [PATCH 20/32] fix(cli): release workflow --- .github/workflows/release-cli.yaml | 141 +++++++++++++----- .../{release.yaml => release-sdk.yaml} | 17 ++- .github/workflows/testing.yaml | 1 + 3 files changed, 118 insertions(+), 41 deletions(-) rename .github/workflows/{release.yaml => release-sdk.yaml} (79%) diff --git a/.github/workflows/release-cli.yaml b/.github/workflows/release-cli.yaml index c85f524..cf47299 100644 --- a/.github/workflows/release-cli.yaml +++ b/.github/workflows/release-cli.yaml @@ -1,6 +1,6 @@ # Create an incremental tag (like `cli-v1.2.0`) on Github using SemVer https://semver.org: x.y.z -# Create the Release (like `cli-v1.2.0`) based on this tag and with the same name. -# Build the CLI for all OS and upload them as assets to the release. +# Create the Release (like `cli-v1.2.0`) related to the tag and with the same name. +# Build the CLI for all OS and upload them to the release as assets. name: Release CLI @@ -16,9 +16,37 @@ on: - major jobs: - release-cli: - if: ${{ github.ref == 'refs/heads/main' }} - name: Release CLI + set-releasename: + runs-on: ubuntu-latest + name: New release name + outputs: + RELEASENAME: ${{ steps.set-outputs.outputs.RELEASENAME }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # download tags + fetch-depth: 0 + + - name: Last version + id: last-version + run: echo "LASTVERSION=$(git tag --list 'cli-v*' | sort -V | tail -n1 | sed 's/cli-v//')" >> $GITHUB_ENV + + - name: Bump version + id: bump-version + uses: olegsu/semver-action@v1 + with: + version: ${{ env.LASTVERSION }} + bump: ${{ inputs.choice }} + + - name: Output release name + id: set-outputs + run: echo "RELEASENAME=cli-v${{ steps.bump-version.outputs.version }}" >> "$GITHUB_OUTPUT" + + build-cli: + # if: ${{ github.ref == 'refs/heads/main' }} + needs: set-releasename + name: Build CLI strategy: matrix: include: @@ -36,53 +64,104 @@ jobs: runs-on: ubuntu-latest env: + RELEASENAME: ${{ needs.set-releasename.outputs.RELEASENAME }} OSNAME: ${{matrix.goos == 'darwin' && 'macos' || matrix.goos }} GOARCH: ${{ matrix.goarch }} GOOS: ${{ matrix.goos }} ARCHNAME: ${{ matrix.goarch == 'amd64' && 'x86-64' || matrix.goarch }} + outputs: + RELEASENAME: ${{ steps.set-outputs.outputs.RELEASENAME }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Set env var - run: echo "ZIPFILE=sqlitecloud-go-${{ steps.tag-and-release.outputs.name }}-${{ env.OSNAME }}-${{ env.ARCHNAME }}.zip" >> $GITHUB_ENV + - name: Set zipfile name + run: echo "ZIPFILE=sqlitecloud-go-${{ env.RELEASENAME }}-${{ env.OSNAME }}-${{ env.ARCHNAME }}.zip" >> $GITHUB_ENV - name: Build CLI run: | - cd GO/cli + cd cli go build -o ../sqlc cd .. zip ${{ env.ZIPFILE }} sqlc - - name: Last version - id: last-version - # last tag that starts with 'cli-v', eg: cli-v1.2.0 but outputs it as: v1.2.0 - run: echo "::set-output name=number::$(git tag --list 'cli-v*' | sort -V | tail -n1 | sed 's/cli-//')" - - - name: Bump version - id: bump-version - uses: olegsu/semver-action@v1 + # Upload assets to be used in the last job + - name: Upload binary artifact + uses: actions/upload-artifact@v4 with: - version: ${{ steps.last-version.outputs.number }} - bump: ${{ inputs.choice }} + name: ${{ env.ZIPFILE }} + path: ./${{ env.ZIPFILE }} + if-no-files-found: error + + - name: Set outputs + id: set-outputs + run: | + echo "RELEASENAME=${{ env.RELEASENAME }}" >> "$GITHUB_OUTPUT" - - name: Tag and Release name - id: tag-and-release - # eg: cli-v1.2.0 - run: echo "::set-output name=name::cli-v$(git tag --list 'v*' | sort -V | tail -n1)" + release-cli: + name: Release CLI + needs: build-cli + runs-on: ubuntu-latest + env: + RELEASENAME: ${{ needs.build-cli.outputs.RELEASENAME }} + outputs: + RELEASENAME: ${{ steps.set-outputs.outputs.RELEASENAME }} + UPLOADURL: ${{ steps.set-outputs.outputs.UPLOADURL }} + steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Create Release for CLI id: release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ steps.tag-and-release.outputs.name }} - name: Release ${{ steps.tag-and-release.outputs.name }} + tag_name: ${{ env.RELEASENAME }} + name: Release ${{ env.RELEASENAME }} draft: false generate_release_notes: true make_latest: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Set outputs + id: set-outputs + run: | + echo "RELEASENAME=${{ env.RELEASENAME }}" >> "$GITHUB_OUTPUT" + echo "UPLOADURL=${{ steps.release.outputs.upload_url }}" >> "$GITHUB_OUTPUT" + + upload-artifacts: + needs: release-cli + runs-on: ubuntu-latest + strategy: + matrix: + include: + - goarch: amd64 + goos: linux + + - goarch: amd64 + goos: windows + + - goarch: arm64 + goos: darwin + + - goarch: amd64 + goos: darwin + env: + RELEASENAME: ${{ needs.release-cli.outputs.RELEASENAME }} + UPLOADURL: ${{ needs.release-cli.outputs.UPLOADURL }} + OSNAME: ${{matrix.goos == 'darwin' && 'macos' || matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOOS: ${{ matrix.goos }} + ARCHNAME: ${{ matrix.goarch == 'amd64' && 'x86-64' || matrix.goarch }} + steps: + - name: Set zip filename + run: echo "ZIPFILE=sqlitecloud-go-${{ env.RELEASENAME }}-${{ env.OSNAME }}-${{ env.ARCHNAME }}.zip" >> $GITHUB_ENV + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: ${{ env.ZIPFILE }} + - name: Upload Release Asset id: upload-release-asset uses: actions/upload-release-asset@v1 @@ -90,15 +169,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.release.outputs.upload_url }} - asset_path: ./GO/${{ env.ZIPFILE }} + upload_url: ${{ env.UPLOADURL }} + asset_path: ./${{ env.ZIPFILE }} asset_name: ${{ env.ZIPFILE }} asset_content_type: application/zip - - - name: Upload binary artifact - uses: actions/upload-artifact@v3 - if: matrix.goos == 'darwin' - with: - name: ${{ env.ZIPFILE }} - path: ./GO/${{ env.ZIPFILE }} - if-no-files-found: error diff --git a/.github/workflows/release.yaml b/.github/workflows/release-sdk.yaml similarity index 79% rename from .github/workflows/release.yaml rename to .github/workflows/release-sdk.yaml index 99a65ba..757ed0b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release-sdk.yaml @@ -2,7 +2,7 @@ # A tag is a release version on pkg.go.dev, which is # notified with the publishing go command. -name: Release +name: Release SDK on: workflow_dispatch: @@ -16,30 +16,35 @@ on: - major jobs: - release: + release-sdk: if: ${{ github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest name: Tag for release steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: + # download tags fetch-depth: 0 + - name: Last version id: last-version - # last tag that starts with 'v', eg: v1.0.3 - run: echo "::set-output name=tag::$(git tag --list 'v*' | sort -V | tail -n1)" + # last tag that starts with 'v', eg: v1.0.3 but outputs it as: 1.0.3 + run: echo "LASTVERSION=$(git tag --list 'v*' | sort -V | tail -n1 | sed 's/v//')" >> $GITHUB_ENV + - name: Bump version id: bump-version uses: olegsu/semver-action@v1 with: - version: ${{ steps.last-version.outputs.tag }} + version: ${{ env.LASTVERSION }} bump: ${{ inputs.choice }} + - name: Create tag as version for the package run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "GitHub Actions" git tag v${{ steps.bump-version.outputs.version }} git push origin v${{ steps.bump-version.outputs.version }} + - name: Publish on pkg.go.dev run: GOPROXY=proxy.golang.org go list -m github.com/sqlitecloud/sqlitecloud-go@v${{ steps.bump-version.outputs.version }} diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 2fc1d21..7a30f94 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -40,3 +40,4 @@ jobs: uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} + files: ./test/coverage.out From fdca2a7c3c190775830268dfbe159b123ba14c87 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Wed, 28 Aug 2024 13:16:30 -0600 Subject: [PATCH 21/32] fix the encoding from float array values to SCSP protocol, the protocol does not support scientific notation --- chunk.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chunk.go b/chunk.go index 9f97b70..0f72878 100644 --- a/chunk.go +++ b/chunk.go @@ -254,7 +254,7 @@ func protocolBufferFromInt(v interface{}) [][]byte { } func protocolBufferFromFloat(v interface{}) [][]byte { - return [][]byte{[]byte(fmt.Sprintf("%c%v ", CMD_FLOAT, v))} + return [][]byte{[]byte(fmt.Sprintf("%c%s ", CMD_FLOAT, strconv.FormatFloat(v.(float64), 'f', -1, 64)))} } // func protocolBufferFromFloat(v interface{}) [][]byte { From 28c3eff365d877284de380633587550f3ae83532 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Mon, 15 Dec 2025 12:25:37 +0100 Subject: [PATCH 22/32] feat(token): support to access tokens auth --- .devcontainer/Dockerfile | 2 +- .github/workflows/testing.yaml | 2 +- .gitignore | 1 + .golangci.yml | 9 +++++++++ .pre-commit-config.yaml | 2 +- Makefile | 4 ++-- connection.go | 22 ++++++++++++-------- connection_internal_test.go | 37 ++++++++++++++++++++++++++++++++++ go.mod | 3 ++- go.sum | 8 ++++---- server.go | 28 ++++++++++++++++--------- test/go.mod | 1 - test/go.sum | 3 +-- test/unit/connection_test.go | 32 +++++++++++++++++++++++++++++ 14 files changed, 124 insertions(+), 30 deletions(-) create mode 100644 .golangci.yml create mode 100644 connection_internal_test.go diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 03babbc..aa5b98b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/go:1-1-bullseye +FROM mcr.microsoft.com/devcontainers/go:1.18 # Python environment RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 7a30f94..0c5e262 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -19,7 +19,7 @@ jobs: go-version: '1.18' - name: Quality Assurance run: | - go install golang.org/x/tools/cmd/goimports@latest + go install golang.org/x/tools/cmd/goimports@v0.24.0 gofmt -l ./*.go goimports -e -d ./*.go - name: golangci-lint diff --git a/.gitignore b/.gitignore index 4c41f13..783be10 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ cli/.vscode pkg/ bin .env +.cache diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d848fef --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,9 @@ +linters: + exclusions: + # Disable ST1006 (receiver name style) to allow existing receiver names in server.go + rules: + - path: server.go + linters: + - staticcheck + - stylecheck + text: ST1006 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 796deed..cf9e76d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: - -w require_serial: true - repo: https://github.com/golangci/golangci-lint - rev: v1.59.1 + rev: v1.50.1 hooks: - id: golangci-lint exclude: ^pkg/ diff --git a/Makefile b/Makefile index 6c36b7a..3b3ae20 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,10 @@ setup-ide: # Test SDK test: - cd test; go test -v . + cd test; go mod tidy && go test -v . test-codecov: - cd test; go test -v -race -coverprofile=coverage.out -covermode=atomic . + cd test; go mod tidy && go test -v -race -coverprofile=coverage.out -covermode=atomic . # GO SDK sdk: *.go diff --git a/connection.go b/connection.go index b4d137b..199df4a 100644 --- a/connection.go +++ b/connection.go @@ -49,10 +49,11 @@ type SQCloudConfig struct { TlsInsecureSkipVerify bool // Accept invalid TLS certificates (no_verify_certificate) Pem string ApiKey string - NoBlob bool // flag to tell the server to not send BLOB columns - MaxData int // value to tell the server to not send columns with more than max_data bytes - MaxRows int // value to control rowset chunks based on the number of rows - MaxRowset int // value to control the maximum allowed size for a rowset + Token string // Access Token for authentication + NoBlob bool // flag to tell the server to not send BLOB columns + MaxData int // value to tell the server to not send columns with more than max_data bytes + MaxRows int // value to control rowset chunks based on the number of rows + MaxRowset int // value to control the maximum allowed size for a rowset } type SQCloud struct { @@ -131,6 +132,7 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err config.MaxRows = 0 config.MaxRowset = 0 config.ApiKey = "" + config.Token = "" sPort := strings.TrimSpace(u.Port()) if len(sPort) > 0 { @@ -195,6 +197,8 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err config.Secure, config.TlsInsecureSkipVerify, config.Pem = ParseTlsString(lastLiteral) case "apikey": config.ApiKey = lastLiteral + case "token": + config.Token = lastLiteral case "noblob": if b, err := parseBool(lastLiteral, config.NoBlob); err == nil { config.NoBlob = b @@ -434,12 +438,14 @@ func connectionCommands(config SQCloudConfig) (string, []interface{}) { } if config.ApiKey != "" { - c, a := authWithKeyCommand(config.ApiKey) + c, a := authWithApiKeyCommand(config.ApiKey) buffer += c args = append(args, a...) - } - - if config.Username != "" && config.Password != "" { + } else if config.Token != "" { + c, a := authWithTokenCommand(config.Token) + buffer += c + args = append(args, a...) + } else if config.Username != "" && config.Password != "" { c, a := authCommand(config.Username, config.Password, config.PasswordHashed) buffer += c args = append(args, a...) diff --git a/connection_internal_test.go b/connection_internal_test.go new file mode 100644 index 0000000..625c4c2 --- /dev/null +++ b/connection_internal_test.go @@ -0,0 +1,37 @@ +package sqlitecloud + +import ( + "reflect" + "strings" + "testing" +) + +func TestConnectionCommandsAuthPreference(t *testing.T) { + tests := []struct { + name string + config SQCloudConfig + wantCmd string + wantArgs []interface{} + }{ + {"api key wins", SQCloudConfig{ApiKey: "k", Token: "t", Username: "u", Password: "p"}, "AUTH APIKEY ?;", []interface{}{"k"}}, + {"token next", SQCloudConfig{Token: "t", Username: "u", Password: "p"}, "AUTH TOKEN ?;", []interface{}{"t"}}, + {"user/pass fallback", SQCloudConfig{Username: "u", Password: "p"}, "AUTH USER ? PASSWORD ?;", []interface{}{"u", "p"}}, + {"hashed password", SQCloudConfig{Username: "u", Password: "hash", PasswordHashed: true}, "AUTH USER ? HASH ?;", []interface{}{"u", "hash"}}, + {"no credentials", SQCloudConfig{}, "", []interface{}{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotBuf, gotArgs := connectionCommands(tt.config) + if tt.wantCmd == "" && gotBuf != "" { + t.Fatalf("expected no auth command, got %q", gotBuf) + } + if tt.wantCmd != "" && !strings.Contains(gotBuf, tt.wantCmd) { + t.Fatalf("expected %q in buffer, got %q", tt.wantCmd, gotBuf) + } + if !reflect.DeepEqual(gotArgs, tt.wantArgs) { + t.Fatalf("args mismatch: want %v got %v", tt.wantArgs, gotArgs) + } + }) + } +} diff --git a/go.mod b/go.mod index d8bd281..20aebad 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,6 @@ require ( require ( github.com/frankban/quicktest v1.14.3 // indirect - golang.org/x/sys v0.7.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + golang.org/x/sys v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index 15aa01b..8d17d51 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,9 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -16,11 +17,10 @@ github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBO github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/xo/dburl v0.13.1 h1:EV+BCdo539sc/mBrny0VxaEGLM0b1U0mJA9RpP80ux0= github.com/xo/dburl v0.13.1/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/server.go b/server.go index 5b48491..2125299 100644 --- a/server.go +++ b/server.go @@ -54,6 +54,7 @@ const ( type SQCloudNode struct { NodeID int64 + PublicAddr string NodeInterface string ClusterInterface string Status SQCloudNodeStatus @@ -157,16 +158,17 @@ func (this *SQCloud) ListNodes() ([]SQCloudNode, error) { if err == nil { if result != nil { defer result.Free() - if result.GetNumberOfColumns() == 7 { + if result.GetNumberOfColumns() == 8 { for row, rows := uint64(0), result.GetNumberOfRows(); row < rows; row++ { node := SQCloudNode{} node.NodeID, _ = result.GetInt64Value(row, 0) - node.NodeInterface, _ = result.GetStringValue(row, 1) - node.ClusterInterface, _ = result.GetStringValue(row, 2) - node.Status, _ = stringToSQCloudNodeStatus(result.GetStringValue_(row, 3)) - node.Progress, _ = stringToSQCloudNodeProgress(result.GetStringValue_(row, 4)) - node.Match, _ = result.GetInt64Value(row, 5) - node.LastActivity, _ = result.GetSQLDateTime(row, 6) + node.PublicAddr = result.GetStringValue_(row, 1) + node.NodeInterface, _ = result.GetStringValue(row, 2) + node.ClusterInterface, _ = result.GetStringValue(row, 3) + node.Status, _ = stringToSQCloudNodeStatus(result.GetStringValue_(row, 4)) + node.Progress, _ = stringToSQCloudNodeProgress(result.GetStringValue_(row, 5)) + node.Match, _ = result.GetInt64Value(row, 6) + node.LastActivity, _ = result.GetSQLDateTime(row, 7) list = append(list, node) } return list, nil @@ -260,13 +262,21 @@ func (this *SQCloud) Auth(Username string, Password string) error { return this.ExecuteArray(authCommand(Username, Password, false)) } -func authWithKeyCommand(Key string) (string, []interface{}) { +func authWithApiKeyCommand(Key string) (string, []interface{}) { return "AUTH APIKEY ?;", []interface{}{Key} } +func authWithTokenCommand(Token string) (string, []interface{}) { + return "AUTH TOKEN ?;", []interface{}{Token} +} + // Auth - INTERNAL SERVER COMMAND: Authenticates User with the given API KEY. func (this *SQCloud) AuthWithKey(Key string) error { - return this.ExecuteArray(authWithKeyCommand(Key)) + return this.ExecuteArray(authWithApiKeyCommand(Key)) +} + +func (this *SQCloud) AuthWithToken(Token string) error { + return this.ExecuteArray(authWithTokenCommand(Token)) } // Database funcitons diff --git a/test/go.mod b/test/go.mod index 1375454..8ff8e6d 100644 --- a/test/go.mod +++ b/test/go.mod @@ -10,7 +10,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/test/go.sum b/test/go.sum index 12d63d6..8211dea 100644 --- a/test/go.sum +++ b/test/go.sum @@ -2,8 +2,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/test/unit/connection_test.go b/test/unit/connection_test.go index 2576880..a10b696 100644 --- a/test/unit/connection_test.go +++ b/test/unit/connection_test.go @@ -68,6 +68,32 @@ func TestParseConnectionStringWithAPIKey(t *testing.T) { assert.Truef(t, reflect.DeepEqual(expectedConfig, config), "Expected: %+v\nGot: %+v", expectedConfig, config) } +func TestParseConnectionStringWithToken(t *testing.T) { + connectionString := "sqlitecloud://host.com:8860/dbname?token=123|tok123&compress=true" + expectedConfig := &sqlitecloud.SQCloudConfig{ + Host: "host.com", + Port: 8860, + Username: "", + Password: "", + Database: "dbname", + Timeout: 0, + Compression: true, + CompressMode: sqlitecloud.CompressModeLZ4, + Secure: true, + TlsInsecureSkipVerify: false, + Pem: "", + Token: "123|tok123", + NoBlob: false, + MaxData: 0, + MaxRows: 0, + MaxRowset: 0, + } + + config, err := sqlitecloud.ParseConnectionString(connectionString) + assert.NoError(t, err) + assert.Truef(t, reflect.DeepEqual(expectedConfig, config), "Expected: %+v\nGot: %+v", expectedConfig, config) +} + func TestParseConnectionStringWithCredentials(t *testing.T) { connectionString := "sqlitecloud://user:pass@host.com:8860" config, err := sqlitecloud.ParseConnectionString(connectionString) @@ -187,6 +213,12 @@ func TestParseConnectionStringWithParameters(t *testing.T) { value: "abc123", expectedValue: "abc123", }, + { + param: "token", + configParam: "Token", + value: "123|tok123", + expectedValue: "123|tok123", + }, } for _, tt := range tests { From 372652514d35ab7b695cc4341aaad5b69b861fe6 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Tue, 16 Dec 2025 15:13:53 +0000 Subject: [PATCH 23/32] feat(cli): token option --- cli/go.mod | 1 - cli/go.sum | 3 +-- cli/sqlc.go | 6 +++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index e47889a..fe83033 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -9,7 +9,6 @@ require ( ) require ( - github.com/google/go-cmp v0.5.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-runewidth v0.0.3 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect diff --git a/cli/go.sum b/cli/go.sum index a2a0e86..e52f226 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -2,8 +2,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/cli/sqlc.go b/cli/sqlc.go index d1748ea..84855b0 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -74,7 +74,7 @@ General Options: Output Format Options: -o, --output FILE Switch to BATCH mode, execute SQL Commands and send output to FILE, then exit. In BATCH mode, the default output format is switched to QUOTE. - + --echo Disables --quiet, print command(s) before execution --quiet Disables --echo, run command(s) quietly (no messages, only query output) --noheader Turn headers off @@ -96,6 +96,7 @@ Connection Options: -e, --create Create database if it does not exist -i, --nonlinearizable Use non-linearizable mode for queries -a, --apikey KEY Use API key for authentication + -k, --token TOKEN Use TOKEN for authentication -n, --noblob Disable BLOB support -x, --maxdata SIZE Set maximum data size for queries -y, --maxrows ROWS Set maximum number of rows for queries @@ -150,6 +151,7 @@ type Parameter struct { Create bool `docopt:"--create"` NonLinearizable bool `docopt:"--nonlinearizable"` ApiKey string `docopt:"--apikey"` + Token string `docopt:"--token"` NoBlob bool `docopt:"--noblob"` MaxData int `docopt:"--maxdata"` MaxRows int `docopt:"--maxrows"` @@ -294,6 +296,7 @@ func parseParameters() (Parameter, error) { p["--tls"] = "SKIP" } p["--apikey"] = getFirstNoneEmptyString([]string{dropError(p.String("--apikey")), conf.ApiKey}) + p["--token"] = getFirstNoneEmptyString([]string{dropError(p.String("--token")), conf.Token}) if conf.NoBlob { if b, err := p.Bool("--noblob"); err == nil { p["--noblob"] = b || conf.NoBlob @@ -418,6 +421,7 @@ func main() { Port: parameter.Port, Username: parameter.User, Password: parameter.Password, + Token: parameter.Token, Database: parameter.Database, Timeout: time.Duration(parameter.Timeout) * time.Second, Compression: parameter.Compress == sqlitecloud.CompressModeLZ4, From c3d9eb108270ebeb91efb89edf19ebb39567a575 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Tue, 16 Dec 2025 15:39:52 +0000 Subject: [PATCH 24/32] feat(config): projectID from connection string's hostname as config property --- connection.go | 7 +++++++ test/unit/connection_test.go | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/connection.go b/connection.go index 199df4a..8bc1070 100644 --- a/connection.go +++ b/connection.go @@ -34,6 +34,7 @@ import ( type SQCloudConfig struct { Host string Port int + ProjectID string // ProjectID to identify the user's node Username string Password string Database string @@ -141,6 +142,12 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err } } + // eg: project ID "abvqqetyhq" in "abvqqetyhq.global3.ryujaz.sqlite.cloud" + config.ProjectID = strings.Split(config.Host, ".")[0] + if config.ProjectID == "" { + return nil, fmt.Errorf("invalid connection string: missing project ID in host") + } + for key, values := range u.Query() { lastLiteral := strings.TrimSpace(values[len(values)-1]) switch strings.ToLower(strings.TrimSpace(key)) { diff --git a/test/unit/connection_test.go b/test/unit/connection_test.go index a10b696..4e6ccc9 100644 --- a/test/unit/connection_test.go +++ b/test/unit/connection_test.go @@ -11,11 +11,12 @@ import ( ) func TestParseConnectionString(t *testing.T) { - connectionString := "sqlitecloud://myhost.sqlite.cloud:8860/mydatabase?timeout=11&compress=yes" + connectionString := "sqlitecloud://myproject.sqlite.cloud:8860/mydatabase?timeout=11&compress=yes" expectedConfig := &sqlitecloud.SQCloudConfig{ - Host: "myhost.sqlite.cloud", + Host: "myproject.sqlite.cloud", Port: 8860, + ProjectID: "myproject", Username: "", Password: "", Database: "mydatabase", From 333ebb9cd8618bf402de5d78740de3ddce36a423 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Thu, 11 Dec 2025 23:50:24 +0100 Subject: [PATCH 25/32] fix(command): trim command --- .pre-commit-config.yaml | 2 ++ .vscode/settings.json | 3 +- chunk.go | 3 ++ test/compress_test.go | 21 +++++++++++++ test/selectarray_test.go | 68 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf9e76d..7268669 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,6 +24,8 @@ repos: language: golang entry: goimports files: \.go$ + additional_dependencies: + - golang.org/x/tools/cmd/goimports@latest args: - -w require_serial: true diff --git a/.vscode/settings.json b/.vscode/settings.json index da7b87d..41e7209 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "go.lintTool": "golangci-lint", "go.lintFlags": [ "--fast" - ] + ], + "go.testTimeout": "600s" } diff --git a/chunk.go b/chunk.go index 0f72878..38657ad 100644 --- a/chunk.go +++ b/chunk.go @@ -23,6 +23,7 @@ import ( "io" "net" "strconv" + "strings" "time" "github.com/pierrec/lz4" @@ -363,6 +364,8 @@ func (this *SQCloud) sendArray(command string, values []interface{}) (int, error } } + command = strings.Trim(command, " \t\r\n") + // convert values to buffers encoded with whe sqlitecloud protocol buffers := [][]byte{protocolBufferFromString(command, true)[0]} for _, v := range values { diff --git a/test/compress_test.go b/test/compress_test.go index 1b26fb6..71adb43 100644 --- a/test/compress_test.go +++ b/test/compress_test.go @@ -127,3 +127,24 @@ func TestCompress(t *testing.T) { t.Fatal(err.Error()) } } + +func TestRowsetChunkCompressed(t *testing.T) { + var db *sqlitecloud.SQCloud + var err error + + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + if db, err = sqlitecloud.Connect(connectionString + "?apikey=" + apikey); err != nil { + t.Fatal("CONNECT: ", err.Error()) + } + defer db.Close() + + switch res, err := db.Select("TEST ROWSET_CHUNK_COMPRESSED"); { // ROWSET + case err != nil: + t.Fatal("TEST ROWSET_CHUNK_COMPRESSED: ", err.Error()) + case res == nil: + t.Fatal("TEST ROWSET_CHUNK_COMPRESSED: nil result") + case !res.IsRowSet(): + t.Fatal("TEST ROWSET_CHUNK_COMPRESSED: invalid type") + } +} diff --git a/test/selectarray_test.go b/test/selectarray_test.go index fee27ca..50e8ac4 100644 --- a/test/selectarray_test.go +++ b/test/selectarray_test.go @@ -141,3 +141,71 @@ func TestSelectArrayTableNameWithQuotes(t *testing.T) { t.Fatal(err.Error()) } } + +func TestSelectArrayWithLeadingChars(t *testing.T) { + // Server API test + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "/" + os.Getenv("SQLITE_DB") + "?apikey=" + apikey + + config, err1 := sqlitecloud.ParseConnectionString(connectionString) + if err1 != nil { + t.Fatal(err1.Error()) + } + + db := sqlitecloud.New(*config) + err := db.Connect() + + if err != nil { + t.Fatalf(err.Error()) + } + + defer db.Close() + + // test select with leading spaces + result, err := db.SelectArray(" SELECT 1 AS value;", nil) + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } + + // test select with leading tabs + result, err = db.SelectArray("\t\t\tSELECT 1 AS value;", nil) + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } + + // test select with leading new lines + result, err = db.SelectArray("\n\n\nSELECT 1 AS value;", nil) + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } + + // test with leading carriage returns + result, err = db.SelectArray("\r\r\rSELECT 1 AS value;", nil) + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } + + // test select with mixed leading characters + result, err = db.SelectArray(` + SELECT 1 AS value; + `, nil) + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } +} From 65a0809450fc12bbd5e8c55b981bf804221ed840 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Mon, 22 Dec 2025 16:08:57 +0000 Subject: [PATCH 26/32] chore: test --- .pre-commit-config.yaml | 2 +- chunk.go | 2 ++ test/select_test.go | 76 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 test/select_test.go diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7268669..d028f45 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: entry: goimports files: \.go$ additional_dependencies: - - golang.org/x/tools/cmd/goimports@latest + - golang.org/x/tools/cmd/goimports@v0.24.0 args: - -w require_serial: true diff --git a/chunk.go b/chunk.go index 38657ad..7eb120c 100644 --- a/chunk.go +++ b/chunk.go @@ -287,6 +287,8 @@ func (this *SQCloud) sendString(data string) (int, error) { } } + data = strings.Trim(data, " \t\r\n") + rawBuffer := protocolBufferFromString(data, false)[0] bytesToSend = len(rawBuffer) diff --git a/test/select_test.go b/test/select_test.go new file mode 100644 index 0000000..11372c7 --- /dev/null +++ b/test/select_test.go @@ -0,0 +1,76 @@ +package test + +import ( + "os" + "testing" + + "github.com/sqlitecloud/sqlitecloud-go" +) + +func TestSelectWithLeadingChars(t *testing.T) { + // Server API test + connectionString, _ := os.LookupEnv("SQLITE_CONNECTION_STRING") + apikey, _ := os.LookupEnv("SQLITE_API_KEY") + connectionString += "/" + os.Getenv("SQLITE_DB") + "?apikey=" + apikey + + config, err1 := sqlitecloud.ParseConnectionString(connectionString) + if err1 != nil { + t.Fatal(err1.Error()) + } + + db := sqlitecloud.New(*config) + err := db.Connect() + + if err != nil { + t.Fatalf(err.Error()) + } + + defer db.Close() + + // test select with leading spaces + result, err := db.Select(" SELECT 1 AS value;") + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } + + // test select with leading tabs + result, err = db.Select("\t\t\tSELECT 1 AS value;") + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } + + // test select with leading new lines + result, err = db.Select("\n\n\nSELECT 1 AS value;") + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } + + // test with leading carriage returns + result, err = db.Select("\r\r\rSELECT 1 AS value;") + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } + + // test select with mixed leading characters + result, err = db.Select(` + SELECT 1 AS value; + `) + if err != nil { + t.Fatal(err.Error()) + } + if result.GetNumberOfRows() != 1 { + t.Fatalf("Expected 1 row, got %d rows", result.GetNumberOfRows()) + } +} From 5830ecb0db65a294c515ed217ba6dd1065b7915f Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Wed, 7 Jan 2026 14:53:25 +0000 Subject: [PATCH 27/32] fix(cli): missing new line --- cli/sqlc.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/sqlc.go b/cli/sqlc.go index 84855b0..74200ae 100644 --- a/cli/sqlc.go +++ b/cli/sqlc.go @@ -344,6 +344,9 @@ func parseParameters() (Parameter, error) { if err := p.Bind(¶meter); err != nil { return Parameter{}, err } + if parameter.NewLine == "" { + parameter.NewLine = "\r\n" + } // Postprocessing... if parameter.OutFile != "" { From e5ab79c35b09e95ccbe9d5f63263a05114132ed1 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 17 Feb 2026 11:37:18 -0600 Subject: [PATCH 28/32] fix(protocol): handle missing types in protocolBufferFromValue - Add uint, uint8, uint16, uint32, uint64 to the integer type switch so unsigned integers are serialized instead of silently dropped - Fix float32 panic in protocolBufferFromFloat caused by unconditional float64 type assertion - Dereference pointer types (*int, *string, etc.) via reflect, mapping nil pointers to NULL and non-nil pointers to their underlying value - Add unit tests for all supported types and a mixed-array regression test that reproduces the original buffer count mismatch --- chunk.go | 19 ++++++- chunk_internal_test.go | 109 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 chunk_internal_test.go diff --git a/chunk.go b/chunk.go index 7eb120c..2781ec3 100644 --- a/chunk.go +++ b/chunk.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "net" + "reflect" "strconv" "strings" "time" @@ -225,7 +226,7 @@ func protocolBufferFromValue(v interface{}) [][]byte { switch v := v.(type) { case nil: return protocolBufferFromNull() - case int, int8, int16, int32, int64: + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: return protocolBufferFromInt(v) case float32, float64: return protocolBufferFromFloat(v) @@ -234,6 +235,13 @@ func protocolBufferFromValue(v interface{}) [][]byte { case []byte: return protocolBufferFromBytes(v) default: + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr { + if rv.IsNil() { + return protocolBufferFromNull() + } + return protocolBufferFromValue(rv.Elem().Interface()) + } return make([][]byte, 0) } } @@ -255,7 +263,14 @@ func protocolBufferFromInt(v interface{}) [][]byte { } func protocolBufferFromFloat(v interface{}) [][]byte { - return [][]byte{[]byte(fmt.Sprintf("%c%s ", CMD_FLOAT, strconv.FormatFloat(v.(float64), 'f', -1, 64)))} + var f float64 + switch v := v.(type) { + case float32: + f = float64(v) + case float64: + f = v + } + return [][]byte{[]byte(fmt.Sprintf("%c%s ", CMD_FLOAT, strconv.FormatFloat(f, 'f', -1, 64)))} } // func protocolBufferFromFloat(v interface{}) [][]byte { diff --git a/chunk_internal_test.go b/chunk_internal_test.go new file mode 100644 index 0000000..5c2d70b --- /dev/null +++ b/chunk_internal_test.go @@ -0,0 +1,109 @@ +package sqlitecloud + +import ( + "testing" +) + +func TestProtocolBufferFromValue(t *testing.T) { + tests := []struct { + name string + value interface{} + wantLen int // expected number of []byte buffers returned + wantType byte + }{ + // Basic types + {"nil", nil, 1, CMD_NULL}, + {"string", "hello", 1, CMD_ZEROSTRING}, + {"int", int(42), 1, CMD_INT}, + {"int8", int8(8), 1, CMD_INT}, + {"int16", int16(16), 1, CMD_INT}, + {"int32", int32(32), 1, CMD_INT}, + {"int64", int64(64), 1, CMD_INT}, + {"float32", float32(3.14), 1, CMD_FLOAT}, + {"float64", float64(2.71), 1, CMD_FLOAT}, + {"[]byte", []byte("blob"), 2, CMD_BLOB}, // header + data + + // Unsigned integers + {"uint", uint(1), 1, CMD_INT}, + {"uint8", uint8(1), 1, CMD_INT}, + {"uint16", uint16(1), 1, CMD_INT}, + {"uint32", uint32(1), 1, CMD_INT}, + {"uint64", uint64(1), 1, CMD_INT}, + + // Pointer types (dereferenced) + {"*int", intPtr(42), 1, CMD_INT}, + {"*string", strPtr("hello"), 1, CMD_ZEROSTRING}, + {"*int nil", (*int)(nil), 1, CMD_NULL}, + {"*string nil", (*string)(nil), 1, CMD_NULL}, + + // Unsupported types still return empty buffers + {"bool", true, 0, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buffers := protocolBufferFromValue(tt.value) + if len(buffers) != tt.wantLen { + t.Errorf("protocolBufferFromValue(%T(%v)): got %d buffers, want %d", tt.value, tt.value, len(buffers), tt.wantLen) + } + if tt.wantLen > 0 && len(buffers) > 0 { + if buffers[0][0] != tt.wantType { + t.Errorf("protocolBufferFromValue(%T(%v)): got type %c, want %c", tt.value, tt.value, buffers[0][0], tt.wantType) + } + } + }) + } +} + +func TestProtocolBufferFromValueMixedArray(t *testing.T) { + // Simulates the loop in sendArray: builds buffers from a mixed values slice + // and checks that the number of buffer groups matches the number of values. + pInt := intPtr(99) + values := []interface{}{ + "hello", // string -> 1 buffer + int(42), // int -> 1 buffer + nil, // nil -> 1 buffer + pInt, // *int -> 1 buffer (dereferenced to int) + float64(3), // float64 -> 1 buffer + uint(7), // uint -> 1 buffer + []byte("x"), // []byte -> 2 buffers (header+data) + } + + // Count how many values produce at least one buffer + buffersPerValue := make([]int, len(values)) + totalBuffers := 0 + missingValues := 0 + + for i, v := range values { + bufs := protocolBufferFromValue(v) + buffersPerValue[i] = len(bufs) + totalBuffers += len(bufs) + if len(bufs) == 0 { + missingValues++ + t.Errorf("value[%d] (%T = %v) produced 0 buffers — will be silently dropped", i, v, v) + } + } + + if missingValues > 0 { + t.Errorf("%d out of %d values produced no buffers and will be missing from the protocol message", missingValues, len(values)) + } + + // Reproduce the exact loop from sendArray + buffers := [][]byte{} + for _, v := range values { + buffers = append(buffers, protocolBufferFromValue(v)...) + } + + t.Logf("values count: %d, total buffers: %d, buffers per value: %v", len(values), len(buffers), buffersPerValue) + + // Every value must produce at least 1 buffer ([]byte produces 2) + expectedMinBuffers := len(values) + if len(buffers) < expectedMinBuffers { + t.Errorf("buffers array has %d elements, expected at least %d (one per value). %d values were silently dropped.", + len(buffers), expectedMinBuffers, missingValues) + } +} + +// helpers +func intPtr(v int) *int { return &v } +func strPtr(v string) *string { return &v } From eb667869c7a76c4039754cfff3c57f917a2db0b5 Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Tue, 17 Feb 2026 11:48:02 -0600 Subject: [PATCH 29/32] test: add internal unit test to "testing" github workflow --- .github/workflows/testing.yaml | 6 ++++-- Makefile | 9 ++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 0c5e262..54395f9 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -26,7 +26,9 @@ jobs: uses: golangci/golangci-lint-action@v6 with: version: latest - - name: Tests + - name: Unit Tests + run: make test-unit-codecov + - name: Integration Tests env: SQLITE_CONNECTION_STRING: ${{ vars.SQLITE_CONNECTION_STRING }} SQLITE_USER: ${{ secrets.SQLITE_USER }} @@ -40,4 +42,4 @@ jobs: uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./test/coverage.out + files: ./coverage-unit.out,./test/coverage.out diff --git a/Makefile b/Makefile index 3b3ae20..4591f5f 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,14 @@ setup-ide: cd test; go mod tidy cd cli; go mod tidy -# Test SDK +# Unit tests (root package) +test-unit: + go test -v . + +test-unit-codecov: + go test -v -race -coverprofile=coverage-unit.out -covermode=atomic . + +# Integration tests (test/ directory) test: cd test; go mod tidy && go test -v . From 1601ba43b8460aa1012f3de974e53049cda3c2dd Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Thu, 5 Mar 2026 14:38:45 +0000 Subject: [PATCH 30/32] feat: support conversion of custom types fix: return error for unsupported type --- chunk.go | 54 ++++++++++++++++++++++++++------ chunk_internal_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 chunk_internal_test.go diff --git a/chunk.go b/chunk.go index 7eb120c..8140367 100644 --- a/chunk.go +++ b/chunk.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "net" + "reflect" "strconv" "strings" "time" @@ -221,20 +222,49 @@ func (this *Value) readBufferAt(chunk *Chunk, offset uint64) (uint64, error) { return 0, errors.New("Unsuported type") } -func protocolBufferFromValue(v interface{}) [][]byte { +func protocolBufferFromValue(v interface{}) ([][]byte, error) { switch v := v.(type) { case nil: - return protocolBufferFromNull() - case int, int8, int16, int32, int64: - return protocolBufferFromInt(v) - case float32, float64: - return protocolBufferFromFloat(v) + return protocolBufferFromNull(), nil + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return protocolBufferFromInt(v), nil + case float32: + return protocolBufferFromFloat(float64(v)), nil + case float64: + return protocolBufferFromFloat(v), nil case string: - return protocolBufferFromString(v, true) + return protocolBufferFromString(v, true), nil case []byte: - return protocolBufferFromBytes(v) + return protocolBufferFromBytes(v), nil default: - return make([][]byte, 0) + rv := reflect.ValueOf(v) + if !rv.IsValid() { + return protocolBufferFromNull(), nil + } + if rv.Kind() == reflect.Pointer { + if rv.IsNil() { + return protocolBufferFromNull(), nil + } + return protocolBufferFromValue(rv.Elem().Interface()) + } + + switch rv.Kind() { + case reflect.String: + return protocolBufferFromString(rv.String(), true), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return protocolBufferFromInt(rv.Int()), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return protocolBufferFromInt(rv.Uint()), nil + case reflect.Float32, reflect.Float64: + return protocolBufferFromFloat(rv.Convert(reflect.TypeOf(float64(0))).Float()), nil + case reflect.Bool: + if rv.Bool() { + return protocolBufferFromInt(1), nil + } + return protocolBufferFromInt(0), nil + default: + return nil, fmt.Errorf("unsupported parameter type %T", v) + } } } @@ -371,7 +401,11 @@ func (this *SQCloud) sendArray(command string, values []interface{}) (int, error // convert values to buffers encoded with whe sqlitecloud protocol buffers := [][]byte{protocolBufferFromString(command, true)[0]} for _, v := range values { - buffers = append(buffers, protocolBufferFromValue(v)...) + valueBuffers, err := protocolBufferFromValue(v) + if err != nil { + return 0, err + } + buffers = append(buffers, valueBuffers...) } // calculate the array header diff --git a/chunk_internal_test.go b/chunk_internal_test.go new file mode 100644 index 0000000..74d1977 --- /dev/null +++ b/chunk_internal_test.go @@ -0,0 +1,70 @@ +package sqlitecloud + +import ( + "fmt" + "strings" + "testing" +) + +type testStringEnum string +type testIntEnum int + +func TestProtocolBufferFromValueSupportsStringAlias(t *testing.T) { + val := testStringEnum("active") + buffers, err := protocolBufferFromValue(val) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(buffers) != 1 { + t.Fatalf("expected 1 buffer, got %d", len(buffers)) + } + got := string(buffers[0]) + want := fmt.Sprintf("%c%d %s\x00", CMD_ZEROSTRING, len("active")+1, "active") + if got != want { + t.Fatalf("unexpected encoded value: want %q got %q", want, got) + } +} + +func TestProtocolBufferFromValueSupportsIntAliasPointer(t *testing.T) { + raw := testIntEnum(7) + buffers, err := protocolBufferFromValue(&raw) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(buffers) != 1 { + t.Fatalf("expected 1 buffer, got %d", len(buffers)) + } + got := string(buffers[0]) + want := fmt.Sprintf("%c%d ", CMD_INT, 7) + if got != want { + t.Fatalf("unexpected encoded value: want %q got %q", want, got) + } +} + +func TestProtocolBufferFromValueSupportsFloat32(t *testing.T) { + buffers, err := protocolBufferFromValue(float32(2.5)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(buffers) != 1 { + t.Fatalf("expected 1 buffer, got %d", len(buffers)) + } + got := string(buffers[0]) + if !strings.HasPrefix(got, fmt.Sprintf("%c", CMD_FLOAT)) { + t.Fatalf("expected float buffer prefix, got %q", got) + } +} + +func TestProtocolBufferFromValueUnsupportedTypeReturnsError(t *testing.T) { + type unsupported struct { + Name string + } + + _, err := protocolBufferFromValue(unsupported{Name: "x"}) + if err == nil { + t.Fatalf("expected error for unsupported type") + } +} From 6997cc1f809300374a148c479be0aae5ef5b6ff2 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Thu, 5 Mar 2026 14:38:45 +0000 Subject: [PATCH 31/32] feat: support conversion of custom types fix: return error for unsupported type --- chunk.go | 57 ++++++++++++------ chunk_internal_test.go | 133 +++++++++++++++-------------------------- 2 files changed, 85 insertions(+), 105 deletions(-) diff --git a/chunk.go b/chunk.go index 2781ec3..8140367 100644 --- a/chunk.go +++ b/chunk.go @@ -222,27 +222,49 @@ func (this *Value) readBufferAt(chunk *Chunk, offset uint64) (uint64, error) { return 0, errors.New("Unsuported type") } -func protocolBufferFromValue(v interface{}) [][]byte { +func protocolBufferFromValue(v interface{}) ([][]byte, error) { switch v := v.(type) { case nil: - return protocolBufferFromNull() + return protocolBufferFromNull(), nil case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - return protocolBufferFromInt(v) - case float32, float64: - return protocolBufferFromFloat(v) + return protocolBufferFromInt(v), nil + case float32: + return protocolBufferFromFloat(float64(v)), nil + case float64: + return protocolBufferFromFloat(v), nil case string: - return protocolBufferFromString(v, true) + return protocolBufferFromString(v, true), nil case []byte: - return protocolBufferFromBytes(v) + return protocolBufferFromBytes(v), nil default: rv := reflect.ValueOf(v) - if rv.Kind() == reflect.Ptr { + if !rv.IsValid() { + return protocolBufferFromNull(), nil + } + if rv.Kind() == reflect.Pointer { if rv.IsNil() { - return protocolBufferFromNull() + return protocolBufferFromNull(), nil } return protocolBufferFromValue(rv.Elem().Interface()) } - return make([][]byte, 0) + + switch rv.Kind() { + case reflect.String: + return protocolBufferFromString(rv.String(), true), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return protocolBufferFromInt(rv.Int()), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return protocolBufferFromInt(rv.Uint()), nil + case reflect.Float32, reflect.Float64: + return protocolBufferFromFloat(rv.Convert(reflect.TypeOf(float64(0))).Float()), nil + case reflect.Bool: + if rv.Bool() { + return protocolBufferFromInt(1), nil + } + return protocolBufferFromInt(0), nil + default: + return nil, fmt.Errorf("unsupported parameter type %T", v) + } } } @@ -263,14 +285,7 @@ func protocolBufferFromInt(v interface{}) [][]byte { } func protocolBufferFromFloat(v interface{}) [][]byte { - var f float64 - switch v := v.(type) { - case float32: - f = float64(v) - case float64: - f = v - } - return [][]byte{[]byte(fmt.Sprintf("%c%s ", CMD_FLOAT, strconv.FormatFloat(f, 'f', -1, 64)))} + return [][]byte{[]byte(fmt.Sprintf("%c%s ", CMD_FLOAT, strconv.FormatFloat(v.(float64), 'f', -1, 64)))} } // func protocolBufferFromFloat(v interface{}) [][]byte { @@ -386,7 +401,11 @@ func (this *SQCloud) sendArray(command string, values []interface{}) (int, error // convert values to buffers encoded with whe sqlitecloud protocol buffers := [][]byte{protocolBufferFromString(command, true)[0]} for _, v := range values { - buffers = append(buffers, protocolBufferFromValue(v)...) + valueBuffers, err := protocolBufferFromValue(v) + if err != nil { + return 0, err + } + buffers = append(buffers, valueBuffers...) } // calculate the array header diff --git a/chunk_internal_test.go b/chunk_internal_test.go index 5c2d70b..74d1977 100644 --- a/chunk_internal_test.go +++ b/chunk_internal_test.go @@ -1,109 +1,70 @@ package sqlitecloud import ( + "fmt" + "strings" "testing" ) -func TestProtocolBufferFromValue(t *testing.T) { - tests := []struct { - name string - value interface{} - wantLen int // expected number of []byte buffers returned - wantType byte - }{ - // Basic types - {"nil", nil, 1, CMD_NULL}, - {"string", "hello", 1, CMD_ZEROSTRING}, - {"int", int(42), 1, CMD_INT}, - {"int8", int8(8), 1, CMD_INT}, - {"int16", int16(16), 1, CMD_INT}, - {"int32", int32(32), 1, CMD_INT}, - {"int64", int64(64), 1, CMD_INT}, - {"float32", float32(3.14), 1, CMD_FLOAT}, - {"float64", float64(2.71), 1, CMD_FLOAT}, - {"[]byte", []byte("blob"), 2, CMD_BLOB}, // header + data +type testStringEnum string +type testIntEnum int - // Unsigned integers - {"uint", uint(1), 1, CMD_INT}, - {"uint8", uint8(1), 1, CMD_INT}, - {"uint16", uint16(1), 1, CMD_INT}, - {"uint32", uint32(1), 1, CMD_INT}, - {"uint64", uint64(1), 1, CMD_INT}, - - // Pointer types (dereferenced) - {"*int", intPtr(42), 1, CMD_INT}, - {"*string", strPtr("hello"), 1, CMD_ZEROSTRING}, - {"*int nil", (*int)(nil), 1, CMD_NULL}, - {"*string nil", (*string)(nil), 1, CMD_NULL}, - - // Unsupported types still return empty buffers - {"bool", true, 0, 0}, +func TestProtocolBufferFromValueSupportsStringAlias(t *testing.T) { + val := testStringEnum("active") + buffers, err := protocolBufferFromValue(val) + if err != nil { + t.Fatalf("unexpected error: %v", err) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buffers := protocolBufferFromValue(tt.value) - if len(buffers) != tt.wantLen { - t.Errorf("protocolBufferFromValue(%T(%v)): got %d buffers, want %d", tt.value, tt.value, len(buffers), tt.wantLen) - } - if tt.wantLen > 0 && len(buffers) > 0 { - if buffers[0][0] != tt.wantType { - t.Errorf("protocolBufferFromValue(%T(%v)): got type %c, want %c", tt.value, tt.value, buffers[0][0], tt.wantType) - } - } - }) + if len(buffers) != 1 { + t.Fatalf("expected 1 buffer, got %d", len(buffers)) + } + got := string(buffers[0]) + want := fmt.Sprintf("%c%d %s\x00", CMD_ZEROSTRING, len("active")+1, "active") + if got != want { + t.Fatalf("unexpected encoded value: want %q got %q", want, got) } } -func TestProtocolBufferFromValueMixedArray(t *testing.T) { - // Simulates the loop in sendArray: builds buffers from a mixed values slice - // and checks that the number of buffer groups matches the number of values. - pInt := intPtr(99) - values := []interface{}{ - "hello", // string -> 1 buffer - int(42), // int -> 1 buffer - nil, // nil -> 1 buffer - pInt, // *int -> 1 buffer (dereferenced to int) - float64(3), // float64 -> 1 buffer - uint(7), // uint -> 1 buffer - []byte("x"), // []byte -> 2 buffers (header+data) +func TestProtocolBufferFromValueSupportsIntAliasPointer(t *testing.T) { + raw := testIntEnum(7) + buffers, err := protocolBufferFromValue(&raw) + if err != nil { + t.Fatalf("unexpected error: %v", err) } - // Count how many values produce at least one buffer - buffersPerValue := make([]int, len(values)) - totalBuffers := 0 - missingValues := 0 - - for i, v := range values { - bufs := protocolBufferFromValue(v) - buffersPerValue[i] = len(bufs) - totalBuffers += len(bufs) - if len(bufs) == 0 { - missingValues++ - t.Errorf("value[%d] (%T = %v) produced 0 buffers — will be silently dropped", i, v, v) - } + if len(buffers) != 1 { + t.Fatalf("expected 1 buffer, got %d", len(buffers)) + } + got := string(buffers[0]) + want := fmt.Sprintf("%c%d ", CMD_INT, 7) + if got != want { + t.Fatalf("unexpected encoded value: want %q got %q", want, got) } +} - if missingValues > 0 { - t.Errorf("%d out of %d values produced no buffers and will be missing from the protocol message", missingValues, len(values)) +func TestProtocolBufferFromValueSupportsFloat32(t *testing.T) { + buffers, err := protocolBufferFromValue(float32(2.5)) + if err != nil { + t.Fatalf("unexpected error: %v", err) } - // Reproduce the exact loop from sendArray - buffers := [][]byte{} - for _, v := range values { - buffers = append(buffers, protocolBufferFromValue(v)...) + if len(buffers) != 1 { + t.Fatalf("expected 1 buffer, got %d", len(buffers)) + } + got := string(buffers[0]) + if !strings.HasPrefix(got, fmt.Sprintf("%c", CMD_FLOAT)) { + t.Fatalf("expected float buffer prefix, got %q", got) } +} - t.Logf("values count: %d, total buffers: %d, buffers per value: %v", len(values), len(buffers), buffersPerValue) +func TestProtocolBufferFromValueUnsupportedTypeReturnsError(t *testing.T) { + type unsupported struct { + Name string + } - // Every value must produce at least 1 buffer ([]byte produces 2) - expectedMinBuffers := len(values) - if len(buffers) < expectedMinBuffers { - t.Errorf("buffers array has %d elements, expected at least %d (one per value). %d values were silently dropped.", - len(buffers), expectedMinBuffers, missingValues) + _, err := protocolBufferFromValue(unsupported{Name: "x"}) + if err == nil { + t.Fatalf("expected error for unsupported type") } } - -// helpers -func intPtr(v int) *int { return &v } -func strPtr(v string) *string { return &v } From 50feec67e82a139587acbf6292b9d7efae6c0216 Mon Sep 17 00:00:00 2001 From: Daniele Briggi Date: Fri, 3 Apr 2026 11:13:46 +0200 Subject: [PATCH 32/32] chore: UPDATE RETURNING test --- test/server_test.go | 78 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/test/server_test.go b/test/server_test.go index 8de23ef..c3c8c54 100644 --- a/test/server_test.go +++ b/test/server_test.go @@ -227,3 +227,81 @@ func TestCompressionEnabledByDefault(t *testing.T) { assert.Equal(t, "1", value) } + +func TestUpdateReturningWithBindingsAlwaysReturnsRowset(t *testing.T) { + db, cleanup := setupDatabase(t) + defer cleanup() + + const tableName = "test_update_returning_subquery_bindings" + + if err := db.Execute(fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName)); err != nil { + t.Fatal(err.Error()) + } + defer func() { + if err := db.Execute(fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName)); err != nil { + t.Fatalf("DROP TABLE cleanup: %s", err.Error()) + } + }() + + if err := db.Execute(fmt.Sprintf("CREATE TABLE %s (id INTEGER PRIMARY KEY, status TEXT, attempts INTEGER, payload TEXT)", tableName)); err != nil { + t.Fatal(err.Error()) + } + + if err := db.ExecuteArray( + fmt.Sprintf("INSERT INTO %s (id, status, attempts, payload) VALUES (?, ?, ?, ?), (?, ?, ?, ?)", tableName), + []any{1, "queued", 0, "first", 2, "done", 3, "second"}, + ); err != nil { + t.Fatal(err.Error()) + } + + sql := fmt.Sprintf(` + UPDATE %s + SET status = ?, attempts = attempts + 1, payload = ? + WHERE id = ( + SELECT id + FROM %s + WHERE status = ? AND attempts < ? + ORDER BY id ASC + LIMIT 1 + ) + RETURNING id, status, attempts, payload; + `, tableName, tableName) + + result, err := db.SelectArray( + sql, + []any{"running", "picked", "queued", 5}, + ) + if err != nil { + t.Fatal(err.Error()) + } + defer result.Free() + + assert.True(t, result.IsRowSet()) + assert.Equal(t, uint64(4), result.GetNumberOfColumns()) + assert.Equal(t, uint64(1), result.GetNumberOfRows()) + assert.Equal(t, "id", result.GetName_(0)) + assert.Equal(t, "status", result.GetName_(1)) + assert.Equal(t, "attempts", result.GetName_(2)) + assert.Equal(t, "payload", result.GetName_(3)) + assert.Equal(t, "1", result.GetStringValue_(0, 0)) + assert.Equal(t, "running", result.GetStringValue_(0, 1)) + assert.Equal(t, "1", result.GetStringValue_(0, 2)) + assert.Equal(t, "picked", result.GetStringValue_(0, 3)) + + emptyResult, err := db.SelectArray( + sql, + []any{"running", "picked-again", "missing", 5}, + ) + if err != nil { + t.Fatal(err.Error()) + } + defer emptyResult.Free() + + assert.True(t, emptyResult.IsRowSet()) + assert.Equal(t, uint64(4), emptyResult.GetNumberOfColumns()) + assert.Equal(t, uint64(0), emptyResult.GetNumberOfRows()) + assert.Equal(t, "id", emptyResult.GetName_(0)) + assert.Equal(t, "status", emptyResult.GetName_(1)) + assert.Equal(t, "attempts", emptyResult.GetName_(2)) + assert.Equal(t, "payload", emptyResult.GetName_(3)) +}