diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000..aa5b98b
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,8 @@
+FROM mcr.microsoft.com/devcontainers/go:1.18
+
+# 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
new file mode 100644
index 0000000..c76a3c9
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,32 @@
+// 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
+ "build": {
+ "dockerfile": "Dockerfile"
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "eamodio.gitlens",
+ "golang.go"
+ ]
+ }
+ }
+
+ // 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/.github/workflows/release-cli.yaml b/.github/workflows/release-cli.yaml
new file mode 100644
index 0000000..cf47299
--- /dev/null
+++ b/.github/workflows/release-cli.yaml
@@ -0,0 +1,175 @@
+# 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`) 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
+
+on:
+ workflow_dispatch:
+ inputs:
+ choice:
+ type: choice
+ description: "Release types (x.y.patch / x.minor.z / major.y.z)"
+ options:
+ - patch
+ - minor
+ - major
+
+jobs:
+ 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:
+ - goarch: amd64
+ goos: linux
+
+ - goarch: amd64
+ goos: windows
+
+ - goarch: arm64
+ goos: darwin
+
+ - goarch: amd64
+ goos: darwin
+
+ 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@v4
+
+ - name: Set zipfile name
+ run: echo "ZIPFILE=sqlitecloud-go-${{ env.RELEASENAME }}-${{ env.OSNAME }}-${{ env.ARCHNAME }}.zip" >> $GITHUB_ENV
+
+ - name: Build CLI
+ run: |
+ cd cli
+ go build -o ../sqlc
+ cd ..
+ zip ${{ env.ZIPFILE }} sqlc
+
+ # Upload assets to be used in the last job
+ - name: Upload binary artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.ZIPFILE }}
+ path: ./${{ env.ZIPFILE }}
+ if-no-files-found: error
+
+ - name: Set outputs
+ id: set-outputs
+ run: |
+ echo "RELEASENAME=${{ env.RELEASENAME }}" >> "$GITHUB_OUTPUT"
+
+ 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: ${{ 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
+ if: matrix.goos != 'darwin'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ env.UPLOADURL }}
+ asset_path: ./${{ env.ZIPFILE }}
+ asset_name: ${{ env.ZIPFILE }}
+ asset_content_type: application/zip
diff --git a/.github/workflows/release-sdk.yaml b/.github/workflows/release-sdk.yaml
new file mode 100644
index 0000000..757ed0b
--- /dev/null
+++ b/.github/workflows/release-sdk.yaml
@@ -0,0 +1,50 @@
+# 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 SDK
+
+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-sdk:
+ if: ${{ github.ref == 'refs/heads/main' }}
+ runs-on: ubuntu-latest
+ name: Tag for release
+ steps:
+ - name: Checkout code
+ 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 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: ${{ 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
new file mode 100644
index 0000000..54395f9
--- /dev/null
+++ b/.github/workflows/testing.yaml
@@ -0,0 +1,45 @@
+# 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@v0.24.0
+ gofmt -l ./*.go
+ goimports -e -d ./*.go
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v6
+ with:
+ version: latest
+ - name: Unit Tests
+ run: make test-unit-codecov
+ - name: Integration 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 }}
+ files: ./coverage-unit.out,./test/coverage.out
diff --git a/.gitignore b/.gitignore
index 3004869..783be10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,7 @@ cli/.DS_Store
.DS_Store
cli/.vscode
.vscode/launch.json
+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
new file mode 100644
index 0000000..d028f45
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,36 @@
+# 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$
+ additional_dependencies:
+ - golang.org/x/tools/cmd/goimports@v0.24.0
+ args:
+ - -w
+ require_serial: true
+- repo: https://github.com/golangci/golangci-lint
+ rev: v1.50.1
+ hooks:
+ - id: golangci-lint
+ exclude: ^pkg/
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..41e7209
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,10 @@
+{
+ "gopls": {
+ "ui.semanticTokens": true
+ },
+ "go.lintTool": "golangci-lint",
+ "go.lintFlags": [
+ "--fast"
+ ],
+ "go.testTimeout": "600s"
+}
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..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`
@@ -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 b39d711..4591f5f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,25 @@
GOPATH = $(shell go env GOPATH)
-# Test SDK
-test:
- cd test; go test -v
+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
+
+# 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 .
+
+test-codecov:
+ cd test; go mod tidy && go test -v -race -coverprofile=coverage.out -covermode=atomic .
# GO SDK
sdk: *.go
@@ -11,15 +28,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/go-sdk
-
+ open https://github.com/sqlitecloud/sqlitecloud-go
+
diff:
git difftool
-
+
# gosec
gosec:
@@ -40,15 +57,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/go-sdk/ ) &
+ @( 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 5f9ff29..4e7c9fd 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,43 @@
-# SQLite Cloud Client SDK for Go
+# 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.
+
+
+
-## 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
+[](https://github.com/sqlitecloud/sqlitecloud-go/actions/workflows/testing.yaml)
+[](https://codecov.io/gh/sqlitecloud/sqlitecloud-go)
+[](https://pkg.go.dev/github.com/sqlitecloud/sqlitecloud-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#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)
+
+## Example
+
+### Use SQLite Cloud in your Go code
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
+ ```bash
+ $ go mod tidy
+ 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 +61,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"
@@ -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/go-sdk
\ No newline at end of file
+
+## Setting up the IDE
+
+[](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.
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..8140367 100644
--- a/chunk.go
+++ b/chunk.go
@@ -22,7 +22,9 @@ import (
"fmt"
"io"
"net"
+ "reflect"
"strconv"
+ "strings"
"time"
"github.com/pierrec/lz4"
@@ -60,26 +62,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 +144,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) {
@@ -221,21 +222,48 @@ func (this *Value) readBufferAt(chunk *Chunk, offset uint64) (uint64, error) {
return 0, errors.New("Unsuported type")
}
-func protocolBufferFromValue(v interface{}) [][]byte {
- if v == 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))
+func protocolBufferFromValue(v interface{}) ([][]byte, error) {
+ switch v := v.(type) {
+ case nil:
+ 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), nil
+ case []byte:
+ return protocolBufferFromBytes(v), nil
+ default:
+ 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 make([][]byte, 0)
+ return nil, fmt.Errorf("unsupported parameter type %T", v)
}
}
}
@@ -257,7 +285,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 {
@@ -289,6 +317,8 @@ func (this *SQCloud) sendString(data string) (int, error) {
}
}
+ data = strings.Trim(data, " \t\r\n")
+
rawBuffer := protocolBufferFromString(data, false)[0]
bytesToSend = len(rawBuffer)
@@ -366,10 +396,16 @@ 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 {
- buffers = append(buffers, protocolBufferFromValue(v)...)
+ valueBuffers, err := protocolBufferFromValue(v)
+ if err != nil {
+ return 0, err
+ }
+ buffers = append(buffers, valueBuffers...)
}
// calculate the array header
@@ -404,8 +440,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/chunk_internal_test.go b/chunk_internal_test.go
new file mode 100644
index 0000000..e1a5021
--- /dev/null
+++ b/chunk_internal_test.go
@@ -0,0 +1,158 @@
+package sqlitecloud
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+type testStringEnum string
+type testIntEnum int
+
+func TestProtocolBufferFromValue(t *testing.T) {
+ type unsupported struct{}
+ intVal := 42
+ strVal := "hello"
+
+ tests := []struct {
+ name string
+ value interface{}
+ wantLen int
+ wantType byte
+ wantError bool
+ }{
+ {"nil", nil, 1, CMD_NULL, false},
+ {"string", "hello", 1, CMD_ZEROSTRING, false},
+ {"int", int(42), 1, CMD_INT, false},
+ {"int8", int8(8), 1, CMD_INT, false},
+ {"int16", int16(16), 1, CMD_INT, false},
+ {"int32", int32(32), 1, CMD_INT, false},
+ {"int64", int64(64), 1, CMD_INT, false},
+ {"uint", uint(1), 1, CMD_INT, false},
+ {"uint8", uint8(1), 1, CMD_INT, false},
+ {"uint16", uint16(1), 1, CMD_INT, false},
+ {"uint32", uint32(1), 1, CMD_INT, false},
+ {"uint64", uint64(1), 1, CMD_INT, false},
+ {"float32", float32(3.14), 1, CMD_FLOAT, false},
+ {"float64", float64(2.71), 1, CMD_FLOAT, false},
+ {"[]byte", []byte("blob"), 2, CMD_BLOB, false},
+ {"bool true", true, 1, CMD_INT, false},
+ {"bool false", false, 1, CMD_INT, false},
+ {"*int", &intVal, 1, CMD_INT, false},
+ {"*string", &strVal, 1, CMD_ZEROSTRING, false},
+ {"*int nil", (*int)(nil), 1, CMD_NULL, false},
+ {"*string nil", (*string)(nil), 1, CMD_NULL, false},
+ {"unsupported", unsupported{}, 0, 0, true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ buffers, err := protocolBufferFromValue(tt.value)
+ if tt.wantError {
+ if err == nil {
+ t.Fatalf("expected error, got nil")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(buffers) != tt.wantLen {
+ t.Fatalf("got %d buffers, want %d", len(buffers), tt.wantLen)
+ }
+ if tt.wantLen > 0 && buffers[0][0] != tt.wantType {
+ t.Fatalf("got first buffer type %q, want %q", buffers[0][0], tt.wantType)
+ }
+ })
+ }
+}
+
+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")
+ }
+}
+
+func TestProtocolBufferFromValueMixedArrayNoSilentDrops(t *testing.T) {
+ pInt := 99
+ values := []interface{}{
+ "hello",
+ int(42),
+ nil,
+ &pInt,
+ float64(3),
+ uint(7),
+ []byte("x"),
+ true,
+ }
+
+ buffers := [][]byte{}
+ for i, v := range values {
+ valueBuffers, err := protocolBufferFromValue(v)
+ if err != nil {
+ t.Fatalf("unexpected error at index %d (%T): %v", i, v, err)
+ }
+ if len(valueBuffers) == 0 {
+ t.Fatalf("value at index %d produced zero buffers", i)
+ }
+ buffers = append(buffers, valueBuffers...)
+ }
+
+ if len(buffers) < len(values) {
+ t.Fatalf("got %d total buffers, expected at least %d", len(buffers), len(values))
+ }
+}
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 1af66a3..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
@@ -17,6 +16,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/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/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 3d30302..74200ae 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"
@@ -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]
@@ -91,6 +91,16 @@ 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
+ -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
+ -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 +141,21 @@ 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"`
+ Token string `docopt:"--token"`
+ 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 +260,47 @@ 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})
+ p["--token"] = getFirstNoneEmptyString([]string{dropError(p.String("--token")), conf.Token})
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)})
@@ -281,9 +320,12 @@ 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"})
+ 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 {
@@ -302,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 != "" {
@@ -375,18 +420,24 @@ 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,
+ Token: parameter.Token,
+ Database: parameter.Database,
+ Timeout: time.Duration(parameter.Timeout) * time.Second,
+ Compression: parameter.Compress == sqlitecloud.CompressModeLZ4,
+ 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)
@@ -458,16 +509,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
@@ -514,7 +561,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 4ab63ed..8bc1070 100644
--- a/connection.go
+++ b/connection.go
@@ -34,19 +34,27 @@ import (
type SQCloudConfig struct {
Host string
Port int
+ ProjectID string // ProjectID to identify the user's node
Username string
Password string
Database string
- Timeout time.Duration
- CompressMode string
- Secure bool
- TlsInsecureSkipVerify bool
+ 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
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 {
@@ -69,13 +77,20 @@ type SQCloud struct {
ErrorMessage string
}
+const SQLiteDefaultPort = 8860
+
+const (
+ CompressModeNo = "NO"
+ CompressModeLZ4 = "LZ4"
+)
+
const SQLiteCloudCA = "SQLiteCloudCA"
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
@@ -99,20 +114,26 @@ 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, "/")
config.Timeout = 0
- config.CompressMode = "NO"
+ config.Compression = true // enabled by default
+ config.CompressMode = CompressModeLZ4
+ config.Zerotext = false
+ config.Memory = false
+ config.Create = false
config.Secure = true
+ config.NonLinearizable = false
config.TlsInsecureSkipVerify = false
config.Pem = ""
- config.ApiKey = ""
config.NoBlob = false
config.MaxData = 0
config.MaxRows = 0
config.MaxRowset = 0
+ config.ApiKey = ""
+ config.Token = ""
sPort := strings.TrimSpace(u.Port())
if len(sPort) > 0 {
@@ -121,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)) {
@@ -132,11 +159,53 @@ func ParseConnectionString(ConnectionString string) (config *SQCloudConfig, err
}
case "compress":
- config.CompressMode = strings.ToUpper(lastLiteral)
+ 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 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 {
+ 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 "apikey":
config.ApiKey = lastLiteral
+ case "token":
+ config.Token = lastLiteral
case "noblob":
if b, err := parseBool(lastLiteral, config.NoBlob); err == nil {
config.NoBlob = b
@@ -186,75 +255,11 @@ 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))
- }
-
- 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))
+ return fmt.Errorf("Invalid hostname (%s)", this.Host)
}
- // 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 strings.ToUpper(this.CompressMode) {
- case "NO", "LZ4":
- 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 fmt.Errorf("Invalid Timeout (%s)", this.Timeout.String())
}
return nil
@@ -299,12 +304,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.
@@ -315,9 +319,16 @@ 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
switch {
case this.cert != nil:
@@ -340,45 +351,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 +394,102 @@ 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{}{}
+
+ // it must be executed before authentication command
+ if config.NonLinearizable {
+ buffer += nonlinearizableCommand(config.NonLinearizable)
+ }
+
+ if config.ApiKey != "" {
+ c, a := authWithApiKeyCommand(config.ApiKey)
+ buffer += c
+ args = append(args, a...)
+ } 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...)
+ }
+
+ 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)
+ }
+
+ if config.MaxData > 0 {
+ buffer += maxdataCommand(config.MaxData)
+ }
+
+ if config.MaxRows > 0 {
+ buffer += maxrowsCommand(config.MaxRows)
+ }
+
+ if config.MaxRowset > 0 {
+ buffer += maxrowsetCommand(config.MaxRowset)
+ }
+
+ return buffer, args
+}
+
func noblobCommand(NoBlob bool) string {
if NoBlob {
return "SET CLIENT KEY NOBLOB TO 1;"
@@ -443,15 +512,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 {
@@ -459,7 +544,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/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 1926127..20aebad 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
@@ -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/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/server.go b/server.go
index 1813142..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
@@ -246,22 +248,35 @@ 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{}) {
+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
@@ -318,8 +333,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 +346,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/src b/src
deleted file mode 120000
index 945c9b4..0000000
--- a/src
+++ /dev/null
@@ -1 +0,0 @@
-.
\ No newline at end of file
diff --git a/test/commons.go b/test/commons.go
new file mode 100644
index 0000000..ef73b2b
--- /dev/null
+++ b/test/commons.go
@@ -0,0 +1,45 @@
+package test
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "testing"
+
+ "github.com/joho/godotenv"
+ sqlitecloud "github.com/sqlitecloud/sqlitecloud-go"
+)
+
+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
+}
+
+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(fmt.Sprintf("%s/%s?apikey=%s", connectionString, dbname, apikey))
+ if err != nil {
+ t.Fatal("Connection error: ", err.Error())
+ }
+
+ return db, func() {
+ db.Close()
+ }
+}
diff --git a/test/compress_test.go b/test/compress_test.go
index 01955a0..71adb43 100644
--- a/test/compress_test.go
+++ b/test/compress_test.go
@@ -21,19 +21,22 @@ import (
"fmt"
"math/rand"
"net/url"
+ "os"
"testing"
- sqlitecloud "github.com/sqlitecloud/go-sdk"
+ sqlitecloud "github.com/sqlitecloud/sqlitecloud-go"
)
const testDbnameCompress = "test-gosdk-compress-db.sqlite"
-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, _ := url.Parse(connectionString)
values := url.Query()
- values.Add(testCompressKey, testCompressValue)
+ values.Add("compress", sqlitecloud.CompressModeLZ4)
url.RawQuery = values.Encode()
connstring := url.String()
@@ -124,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/go.mod b/test/go.mod
index 071503e..8ff8e6d 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -1,16 +1,22 @@
-module github.com/sqlitecloud/go-sdk/test
+module github.com/sqlitecloud/sqlitecloud-go/test
go 1.18
-require github.com/sqlitecloud/go-sdk v0.0.0
+require (
+ github.com/joho/godotenv v1.5.1
+ github.com/sqlitecloud/sqlitecloud-go v0.0.0
+ github.com/stretchr/testify v1.9.0
+)
require (
- github.com/google/go-cmp v0.5.9 // indirect
+ github.com/davecgh/go-spew v1.1.1 // 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 => ../
+replace github.com/sqlitecloud/sqlitecloud-go v0.0.0 => ../
diff --git a/test/go.sum b/test/go.sum
index 9d229bd..8211dea 100644
--- a/test/go.sum
+++ b/test/go.sum
@@ -1,7 +1,10 @@
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/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=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -9,11 +12,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..51efed6 100644
--- a/test/literals_test.go
+++ b/test/literals_test.go
@@ -18,9 +18,10 @@
package test
import (
+ "os"
"testing"
- sqlitecloud "github.com/sqlitecloud/go-sdk"
+ sqlitecloud "github.com/sqlitecloud/sqlitecloud-go"
)
const testDbnameLiteral = "test-gosdk-literal-db.sqlite"
@@ -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..303ef95 100644
--- a/test/pubsub_test.go
+++ b/test/pubsub_test.go
@@ -20,11 +20,12 @@ package test
import (
"encoding/json"
"fmt"
+ "os"
"reflect"
"testing"
"time"
- sqlitecloud "github.com/sqlitecloud/go-sdk"
+ sqlitecloud "github.com/sqlitecloud/sqlitecloud-go"
)
const testPubsubChannelName = "TestPubsubChannel"
@@ -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/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))
+}
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())
+ }
+}
diff --git a/test/selectarray_test.go b/test/selectarray_test.go
index 144f6b5..50e8ac4 100644
--- a/test/selectarray_test.go
+++ b/test/selectarray_test.go
@@ -18,17 +18,21 @@
package test
import (
+ "os"
"testing"
- sqlitecloud "github.com/sqlitecloud/go-sdk"
+ sqlitecloud "github.com/sqlitecloud/sqlitecloud-go"
)
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())
}
@@ -134,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())
+ }
+}
diff --git a/test/server_test.go b/test/server_test.go
index f3994de..c3c8c54 100644
--- a/test/server_test.go
+++ b/test/server_test.go
@@ -19,28 +19,35 @@ package test
import (
"fmt"
+ "os"
"strings"
"testing"
- sqlitecloud "github.com/sqlitecloud/go-sdk"
+ sqlitecloud "github.com/sqlitecloud/sqlitecloud-go"
+ "github.com/stretchr/testify/assert"
)
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 +59,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())
}
@@ -207,3 +214,94 @@ 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)
+}
+
+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))
+}
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.
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..4e6ccc9
--- /dev/null
+++ b/test/unit/connection_test.go
@@ -0,0 +1,462 @@
+package test
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+ "time"
+
+ sqlitecloud "github.com/sqlitecloud/sqlitecloud-go"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseConnectionString(t *testing.T) {
+ connectionString := "sqlitecloud://myproject.sqlite.cloud:8860/mydatabase?timeout=11&compress=yes"
+
+ expectedConfig := &sqlitecloud.SQCloudConfig{
+ Host: "myproject.sqlite.cloud",
+ Port: 8860,
+ ProjectID: "myproject",
+ Username: "",
+ Password: "",
+ Database: "mydatabase",
+ Timeout: time.Duration(11) * time.Second,
+ Compression: true,
+ CompressMode: sqlitecloud.CompressModeLZ4,
+ 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,
+ Compression: true,
+ CompressMode: sqlitecloud.CompressModeLZ4,
+ 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 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)
+
+ 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: "false",
+ expectedValue: false,
+ },
+ {
+ 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: "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",
+ },
+ {
+ param: "token",
+ configParam: "Token",
+ value: "123|tok123",
+ expectedValue: "123|tok123",
+ },
+ }
+
+ 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 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
+ 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: "