├── trino ├── etc │ ├── secrets │ │ ├── .gitignore │ │ └── password.db │ ├── catalog │ │ ├── memory.properties │ │ ├── tpch.properties │ │ └── hive.properties │ ├── password-authenticator.properties │ ├── node.properties │ ├── jvm.config │ ├── spooling-manager.properties │ ├── config-pre-466version.properties │ ├── config.properties │ └── config-pre-477version.properties ├── serial.go ├── serial_test.go └── integration_test.go ├── .gitignore ├── .goreleaser.yml ├── .github ├── release.yml └── workflows │ ├── ci.yml │ └── release.yml ├── CONTRIBUTING.md ├── go.mod ├── LICENSE ├── README.md └── go.sum /trino/etc/secrets/.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out 2 | .idea 3 | /dist 4 | -------------------------------------------------------------------------------- /trino/etc/catalog/memory.properties: -------------------------------------------------------------------------------- 1 | connector.name=memory 2 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - skip: true 3 | changelog: 4 | use: github-native 5 | -------------------------------------------------------------------------------- /trino/etc/secrets/password.db: -------------------------------------------------------------------------------- 1 | admin:$2y$10$xLVM/FUuZaNEmGOU3y7fjOLzRJOGItnTWTiFVKwIFX.ZMBctxl8gq 2 | -------------------------------------------------------------------------------- /trino/etc/catalog/tpch.properties: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | connector.name=tpch 3 | -------------------------------------------------------------------------------- /trino/etc/password-authenticator.properties: -------------------------------------------------------------------------------- 1 | password-authenticator.name=file 2 | file.password-file=/etc/trino/secrets/password.db 3 | -------------------------------------------------------------------------------- /trino/etc/node.properties: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | node.environment=test 3 | node.id=test 4 | node.data-dir=/data/trino 5 | -------------------------------------------------------------------------------- /trino/etc/catalog/hive.properties: -------------------------------------------------------------------------------- 1 | connector.name=hive 2 | hive.metastore=file 3 | hive.metastore.catalog.dir=/tmp/metastore 4 | hive.security=sql-standard 5 | fs.hadoop.enabled=true -------------------------------------------------------------------------------- /trino/etc/jvm.config: -------------------------------------------------------------------------------- 1 | -Xmx4G 2 | -XX:+UseG1GC 3 | -XX:G1HeapRegionSize=32M 4 | -XX:+UseGCOverheadLimit 5 | -XX:+ExplicitGCInvokesConcurrent 6 | -XX:+ExitOnOutOfMemoryError 7 | -Djdk.attach.allowAttachSelf=true 8 | -Djdk.nio.maxCachedBufferSize=2000000 9 | -------------------------------------------------------------------------------- /trino/etc/spooling-manager.properties: -------------------------------------------------------------------------------- 1 | spooling-manager.name=filesystem 2 | fs.s3.enabled=true 3 | fs.location=s3://spooling/ 4 | s3.endpoint=http://localstack:4566/ 5 | s3.region=us-east-1 6 | s3.aws-access-key=test 7 | s3.aws-secret-key=test 8 | s3.path-style-access=true 9 | 10 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Breaking changes 7 | labels: 8 | - breaking-change 9 | - title: Features 10 | labels: 11 | - enhancement 12 | - title: Bug fixes 13 | labels: 14 | - bug 15 | - title: Other changes 16 | labels: 17 | - "*" 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | go: ['>=1.25','1.24.7'] 16 | trino: ['latest', '372'] 17 | steps: 18 | - uses: actions/checkout@v6 19 | - uses: actions/setup-go@v6 20 | with: 21 | go-version: ${{ matrix.go }} 22 | - run: go test -v -race -timeout 2m ./... -trino_image_tag=${{ matrix.trino }} 23 | -------------------------------------------------------------------------------- /trino/etc/config-pre-466version.properties: -------------------------------------------------------------------------------- 1 | coordinator=true 2 | node-scheduler.include-coordinator=true 3 | http-server.http.port=8080 4 | discovery-server.enabled=true 5 | discovery.uri=http://localhost:8080 6 | 7 | http-server.authentication.type=PASSWORD,JWT 8 | http-server.authentication.jwt.key-file=/etc/trino/secrets/public_key.pem 9 | http-server.https.enabled=true 10 | http-server.https.port=8443 11 | http-server.authentication.allow-insecure-over-http=true 12 | http-server.https.keystore.path=/etc/trino/secrets/certificate_with_key.pem 13 | internal-communication.shared-secret=gotrino 14 | 15 | query.max-length=5000043 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Trino 2 | 3 | ## Contributor License Agreement ("CLA") 4 | 5 | In order to accept your pull request, we need you to [submit a CLA](https://github.com/trinodb/cla). 6 | 7 | ## License 8 | 9 | By contributing to Trino, you agree that your contributions will be licensed under the [Apache License Version 2.0 (APLv2)](LICENSE). 10 | 11 | # Go Test 12 | 13 | Please Run [go test](https://pkg.go.dev/testing) before creating Pull Request 14 | 15 | ```bash 16 | go test -v -race -timeout 1m ./... 17 | ``` 18 | 19 | # Releases 20 | 21 | To create a new release, a maintainer with repository write permissions needs to create and push a new git tag. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | # run only against tags 6 | tags: 7 | - '*' 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Fetch all tags 21 | run: git fetch --force --tags 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v6 25 | with: 26 | go-version: "1.25" 27 | 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v6 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /trino/etc/config.properties: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | coordinator=true 3 | node-scheduler.include-coordinator=true 4 | http-server.http.port=8080 5 | 6 | http-server.authentication.type=PASSWORD,JWT 7 | http-server.authentication.jwt.key-file=/etc/trino/secrets/public_key.pem 8 | http-server.https.enabled=true 9 | http-server.https.port=8443 10 | http-server.authentication.allow-insecure-over-http=true 11 | http-server.https.keystore.path=/etc/trino/secrets/certificate_with_key.pem 12 | internal-communication.shared-secret=gotrino 13 | 14 | query.max-length=5000043 15 | 16 | ## spooling protocol settings 17 | protocol.spooling.enabled=true 18 | protocol.spooling.shared-secret-key=jxTKysfCBuMZtFqUf8UJDQ1w9ez8rynEJsJqgJf66u0= 19 | protocol.spooling.retrieval-mode=coordinator_proxy 20 | # Max number of rows to inline per worker 21 | # If the number of rows exceeds this threshold, spooled segments will be returned. 22 | # If the number of rows is within this threshold and the max size is below the max-size threshold, 23 | # inline segments will be returne 24 | protocol.spooling.inlining.max-rows=1000 25 | 26 | # Max size of rows to inline per worker 27 | # If the total size of the rows exceeds this threshold, spooled segments will be returned. 28 | # If the total size of the rows is within this threshold and the row count is below the max-rows threshold, 29 | # inline segments will be returned. 30 | protocol.spooling.inlining.max-size=128kB 31 | -------------------------------------------------------------------------------- /trino/etc/config-pre-477version.properties: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | coordinator=true 3 | node-scheduler.include-coordinator=true 4 | http-server.http.port=8080 5 | discovery-server.enabled=true 6 | discovery.uri=http://localhost:8080 7 | 8 | http-server.authentication.type=PASSWORD,JWT 9 | http-server.authentication.jwt.key-file=/etc/trino/secrets/public_key.pem 10 | http-server.https.enabled=true 11 | http-server.https.port=8443 12 | http-server.authentication.allow-insecure-over-http=true 13 | http-server.https.keystore.path=/etc/trino/secrets/certificate_with_key.pem 14 | internal-communication.shared-secret=gotrino 15 | 16 | query.max-length=5000043 17 | 18 | ## spooling protocol settings 19 | protocol.spooling.enabled=true 20 | protocol.spooling.shared-secret-key=jxTKysfCBuMZtFqUf8UJDQ1w9ez8rynEJsJqgJf66u0= 21 | protocol.spooling.retrieval-mode=coordinator_proxy 22 | # Max number of rows to inline per worker 23 | # If the number of rows exceeds this threshold, spooled segments will be returned. 24 | # If the number of rows is within this threshold and the max size is below the max-size threshold, 25 | # inline segments will be returne 26 | protocol.spooling.inlining.max-rows=1000 27 | 28 | # Max size of rows to inline per worker 29 | # If the total size of the rows exceeds this threshold, spooled segments will be returned. 30 | # If the total size of the rows is within this threshold and the row count is below the max-rows threshold, 31 | # inline segments will be returned. 32 | protocol.spooling.inlining.max-size=128kB 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trinodb/trino-go-client 2 | 3 | go 1.24.7 4 | 5 | require ( 6 | github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26 7 | github.com/aws/aws-sdk-go v1.55.8 8 | github.com/aws/aws-sdk-go-v2/config v1.31.8 9 | github.com/aws/aws-sdk-go-v2/credentials v1.18.12 10 | github.com/aws/aws-sdk-go-v2/service/s3 v1.88.1 11 | github.com/golang-jwt/jwt/v5 v5.3.0 12 | github.com/google/btree v1.1.3 13 | github.com/jcmturner/gokrb5/v8 v8.4.4 14 | github.com/klauspost/compress v1.18.1 15 | github.com/ory/dockertest/v3 v3.12.0 16 | github.com/pierrec/lz4 v2.6.1+incompatible 17 | github.com/stretchr/testify v1.11.1 18 | ) 19 | 20 | require ( 21 | dario.cat/mergo v1.0.2 // indirect 22 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 23 | github.com/Microsoft/go-winio v0.6.2 // indirect 24 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 25 | github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26 // indirect 26 | github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect 27 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect 28 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect 29 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect 30 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect 31 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 32 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.7 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect 34 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.7 // indirect 35 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect 36 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.7 // indirect 37 | github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect 38 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect 40 | github.com/aws/smithy-go v1.23.0 // indirect 41 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 42 | github.com/containerd/continuity v0.4.5 // indirect 43 | github.com/davecgh/go-spew v1.1.1 // indirect 44 | github.com/docker/cli v28.4.0+incompatible // indirect 45 | github.com/docker/docker v28.4.0+incompatible // indirect 46 | github.com/docker/go-connections v0.6.0 // indirect 47 | github.com/docker/go-units v0.5.0 // indirect 48 | github.com/frankban/quicktest v1.14.6 // indirect 49 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 50 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 51 | github.com/hashicorp/go-uuid v1.0.3 // indirect 52 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 53 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 54 | github.com/jcmturner/gofork v1.7.6 // indirect 55 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 56 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 57 | github.com/moby/docker-image-spec v1.3.1 // indirect 58 | github.com/moby/sys/user v0.4.0 // indirect 59 | github.com/moby/term v0.5.2 // indirect 60 | github.com/opencontainers/go-digest v1.0.0 // indirect 61 | github.com/opencontainers/image-spec v1.1.1 // indirect 62 | github.com/opencontainers/runc v1.3.1 // indirect 63 | github.com/pkg/errors v0.9.1 // indirect 64 | github.com/pmezard/go-difflib v1.0.0 // indirect 65 | github.com/sirupsen/logrus v1.9.3 // indirect 66 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 67 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 68 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 69 | golang.org/x/crypto v0.45.0 // indirect 70 | golang.org/x/net v0.47.0 // indirect 71 | golang.org/x/sys v0.38.0 // indirect 72 | gopkg.in/yaml.v3 v3.0.1 // indirect 73 | ) 74 | -------------------------------------------------------------------------------- /trino/serial.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package trino 16 | 17 | import ( 18 | "encoding/hex" 19 | "encoding/json" 20 | "fmt" 21 | "math" 22 | "reflect" 23 | "strconv" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | type UnsupportedArgError struct { 29 | t string 30 | } 31 | 32 | func (e UnsupportedArgError) Error() string { 33 | return fmt.Sprintf("trino: unsupported arg type: %s", e.t) 34 | } 35 | 36 | // Numeric is a string representation of a number, such as "10", "5.5" or in scientific form 37 | // If another string format is used it will error to serialise 38 | type Numeric string 39 | 40 | // trinoDate represents a Date type in Trino. 41 | type trinoDate struct { 42 | year int 43 | month time.Month 44 | day int 45 | } 46 | 47 | // Date creates a representation of a Trino Date type. 48 | func Date(year int, month time.Month, day int) trinoDate { 49 | return trinoDate{year, month, day} 50 | } 51 | 52 | // trinoTime represents a Time type in Trino. 53 | type trinoTime struct { 54 | hour int 55 | minute int 56 | second int 57 | nanosecond int 58 | } 59 | 60 | // Time creates a representation of a Trino Time type. To represent time with precision higher than nanoseconds, pass the value as a string and use a cast in the query. 61 | func Time(hour int, 62 | minute int, 63 | second int, 64 | nanosecond int) trinoTime { 65 | return trinoTime{hour, minute, second, nanosecond} 66 | } 67 | 68 | // trinoTimeTz represents a Time(9) With Timezone type in Trino. 69 | type trinoTimeTz time.Time 70 | 71 | // TimeTz creates a representation of a Trino Time(9) With Timezone type. 72 | func TimeTz(hour int, 73 | minute int, 74 | second int, 75 | nanosecond int, 76 | location *time.Location) trinoTimeTz { 77 | // When reading a time, a nil location indicates UTC. 78 | // However, passing nil to time.Date() panics. 79 | if location == nil { 80 | location = time.UTC 81 | } 82 | return trinoTimeTz(time.Date(0, 0, 0, hour, minute, second, nanosecond, location)) 83 | } 84 | 85 | // Timestamp indicates we want a TimeStamp type WITHOUT a time zone in Trino from a Golang time. 86 | type trinoTimestamp time.Time 87 | 88 | // Timestamp creates a representation of a Trino Timestamp(9) type. 89 | func Timestamp(year int, 90 | month time.Month, 91 | day int, 92 | hour int, 93 | minute int, 94 | second int, 95 | nanosecond int) trinoTimestamp { 96 | return trinoTimestamp(time.Date(year, month, day, hour, minute, second, nanosecond, time.UTC)) 97 | } 98 | 99 | // Serial converts any supported value to its equivalent string for as a Trino parameter 100 | // See https://trino.io/docs/current/language/types.html 101 | func Serial(v interface{}) (string, error) { 102 | switch x := v.(type) { 103 | case nil: 104 | return "NULL", nil 105 | 106 | // numbers convertible to int 107 | case int8: 108 | return strconv.Itoa(int(x)), nil 109 | case int16: 110 | return strconv.Itoa(int(x)), nil 111 | case int32: 112 | return strconv.Itoa(int(x)), nil 113 | case int: 114 | return strconv.Itoa(x), nil 115 | case uint16: 116 | return strconv.Itoa(int(x)), nil 117 | 118 | case int64: 119 | return strconv.FormatInt(x, 10), nil 120 | 121 | case uint32: 122 | return strconv.FormatUint(uint64(x), 10), nil 123 | case uint: 124 | return strconv.FormatUint(uint64(x), 10), nil 125 | case uint64: 126 | return strconv.FormatUint(x, 10), nil 127 | 128 | // float32, float64 not supported because digit precision will easily cause large problems 129 | case float32: 130 | return "", UnsupportedArgError{"float32"} 131 | case float64: 132 | return "", UnsupportedArgError{"float64"} 133 | 134 | case Numeric: 135 | if _, err := strconv.ParseFloat(string(x), 64); err != nil { 136 | return "", err 137 | } 138 | return string(x), nil 139 | 140 | // note byte and uint are not supported, this is because byte is an alias for uint8 141 | // if you were to use uint8 (as a number) it could be interpreted as a byte, so it is unsupported 142 | // use string instead of byte and any other uint/int type for uint8 143 | case byte: 144 | return "", UnsupportedArgError{"byte/uint8"} 145 | 146 | case bool: 147 | return strconv.FormatBool(x), nil 148 | 149 | case string: 150 | return "'" + strings.Replace(x, "'", "''", -1) + "'", nil 151 | 152 | case []byte: 153 | if x == nil { 154 | return "NULL", nil 155 | } 156 | return "X'" + hex.EncodeToString(x) + "'", nil 157 | 158 | case trinoDate: 159 | return fmt.Sprintf("DATE '%04d-%02d-%02d'", x.year, x.month, x.day), nil 160 | case trinoTime: 161 | return fmt.Sprintf("TIME '%02d:%02d:%02d.%09d'", x.hour, x.minute, x.second, x.nanosecond), nil 162 | case trinoTimeTz: 163 | return "TIME " + time.Time(x).Format("'15:04:05.999999999 Z07:00'"), nil 164 | case trinoTimestamp: 165 | return "TIMESTAMP " + time.Time(x).Format("'2006-01-02 15:04:05.999999999'"), nil 166 | case time.Time: 167 | return "TIMESTAMP " + time.Time(x).Format("'2006-01-02 15:04:05.999999999 Z07:00'"), nil 168 | 169 | case time.Duration: 170 | return serialDuration(x) 171 | 172 | // TODO - json.RawMesssage should probably be matched to 'JSON' in Trino 173 | case json.RawMessage: 174 | return "", UnsupportedArgError{"json.RawMessage"} 175 | } 176 | 177 | if reflect.TypeOf(v).Kind() == reflect.Slice { 178 | x := reflect.ValueOf(v) 179 | if x.IsNil() { 180 | return "", UnsupportedArgError{"[]"} 181 | } 182 | 183 | slice := make([]interface{}, x.Len()) 184 | 185 | for i := 0; i < x.Len(); i++ { 186 | slice[i] = x.Index(i).Interface() 187 | } 188 | 189 | return serialSlice(slice) 190 | } 191 | 192 | if reflect.TypeOf(v).Kind() == reflect.Map { 193 | // are Trino MAPs indifferent to order? Golang maps are, if Trino aren't then the two types can't be compatible 194 | return "", UnsupportedArgError{"map"} 195 | } 196 | 197 | // TODO - consider the remaining types in https://trino.io/docs/current/language/types.html (Row, IP, ...) 198 | 199 | return "", UnsupportedArgError{fmt.Sprintf("%T", v)} 200 | } 201 | 202 | func serialSlice(v []interface{}) (string, error) { 203 | ss := make([]string, len(v)) 204 | 205 | for i, x := range v { 206 | s, err := Serial(x) 207 | if err != nil { 208 | return "", err 209 | } 210 | ss[i] = s 211 | } 212 | 213 | return "ARRAY[" + strings.Join(ss, ", ") + "]", nil 214 | } 215 | 216 | const ( 217 | // For seconds with milliseconds there is a maximum length of 10 digits 218 | // or 11 characters with the dot and 12 characters with the minus sign and dot 219 | maxIntervalStrLenWithDot = 11 // 123456789.1 and 12345678.91 are valid 220 | ) 221 | 222 | func serialDuration(dur time.Duration) (string, error) { 223 | switch { 224 | case dur%time.Hour == 0: 225 | return serialHoursInterval(dur), nil 226 | case dur%time.Minute == 0: 227 | return serialMinutesInterval(dur), nil 228 | case dur%time.Second == 0: 229 | return serialSecondsInterval(dur) 230 | case dur%time.Millisecond == 0: 231 | return serialMillisecondsInterval(dur) 232 | default: 233 | return "", fmt.Errorf("trino: duration %v is not a multiple of hours, minutes, seconds or milliseconds", dur) 234 | } 235 | } 236 | 237 | func serialHoursInterval(dur time.Duration) string { 238 | return "INTERVAL '" + strconv.Itoa(int(dur/time.Hour)) + "' HOUR" 239 | } 240 | 241 | func serialMinutesInterval(dur time.Duration) string { 242 | return "INTERVAL '" + strconv.Itoa(int(dur/time.Minute)) + "' MINUTE" 243 | } 244 | 245 | func serialSecondsInterval(dur time.Duration) (string, error) { 246 | seconds := int64(dur / time.Second) 247 | if seconds <= math.MinInt32 || seconds > math.MaxInt32 { 248 | return "", fmt.Errorf("trino: duration %v is out of range for interval of seconds type", dur) 249 | } 250 | return "INTERVAL '" + strconv.FormatInt(seconds, 10) + "' SECOND", nil 251 | } 252 | 253 | func serialMillisecondsInterval(dur time.Duration) (string, error) { 254 | seconds := int64(dur / time.Second) 255 | millisInSecond := dur.Abs().Milliseconds() % 1000 256 | intervalNr := strings.TrimRight(fmt.Sprintf("%d.%03d", seconds, millisInSecond), "0") 257 | if seconds > 0 && len(intervalNr) > maxIntervalStrLenWithDot || 258 | seconds < 0 && len(intervalNr) > maxIntervalStrLenWithDot+1 { // +1 for the minus sign 259 | return "", fmt.Errorf("trino: duration %v is out of range for interval of seconds with millis type", dur) 260 | } 261 | return "INTERVAL '" + intervalNr + "' SECOND", nil 262 | } 263 | -------------------------------------------------------------------------------- /trino/serial_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package trino 16 | 17 | import ( 18 | "math" 19 | "testing" 20 | "time" 21 | 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestSerial(t *testing.T) { 26 | paris, err := time.LoadLocation("Europe/Paris") 27 | require.NoError(t, err) 28 | scenarios := []struct { 29 | name string 30 | value interface{} 31 | expectedError bool 32 | expectedSerial string 33 | }{ 34 | { 35 | name: "basic string", 36 | value: "hello world", 37 | expectedSerial: `'hello world'`, 38 | }, 39 | { 40 | name: "single quoted string", 41 | value: "hello world's", 42 | expectedSerial: `'hello world''s'`, 43 | }, 44 | { 45 | name: "double quoted string", 46 | value: `hello "world"`, 47 | expectedSerial: `'hello "world"'`, 48 | }, 49 | { 50 | name: "basic binary", 51 | value: []byte{0x01, 0x02, 0x03}, 52 | expectedSerial: `X'010203'`, 53 | }, 54 | { 55 | name: "empty binary", 56 | value: []byte{}, 57 | expectedSerial: `X''`, 58 | }, 59 | { 60 | name: "nil binary", 61 | value: []byte(nil), 62 | expectedSerial: `NULL`, 63 | }, 64 | { 65 | name: "int8", 66 | value: int8(100), 67 | expectedSerial: "100", 68 | }, 69 | { 70 | name: "int16", 71 | value: int16(100), 72 | expectedSerial: "100", 73 | }, 74 | { 75 | name: "int32", 76 | value: int32(100), 77 | expectedSerial: "100", 78 | }, 79 | { 80 | name: "int", 81 | value: int(100), 82 | expectedSerial: "100", 83 | }, 84 | { 85 | name: "int64", 86 | value: int64(100), 87 | expectedSerial: "100", 88 | }, 89 | { 90 | name: "uint8", 91 | value: uint8(100), 92 | expectedError: true, 93 | }, 94 | { 95 | name: "uint16", 96 | value: uint16(100), 97 | expectedSerial: "100", 98 | }, 99 | { 100 | name: "uint32", 101 | value: uint32(100), 102 | expectedSerial: "100", 103 | }, 104 | { 105 | name: "uint", 106 | value: uint(100), 107 | expectedSerial: "100", 108 | }, 109 | { 110 | name: "uint64", 111 | value: uint64(100), 112 | expectedSerial: "100", 113 | }, 114 | { 115 | name: "byte", 116 | value: byte('a'), 117 | expectedError: true, 118 | }, 119 | { 120 | name: "valid Numeric", 121 | value: Numeric("10"), 122 | expectedSerial: "10", 123 | }, 124 | { 125 | name: "invalid Numeric", 126 | value: Numeric("not-a-number"), 127 | expectedError: true, 128 | }, 129 | { 130 | name: "bool true", 131 | value: true, 132 | expectedSerial: "true", 133 | }, 134 | { 135 | name: "bool false", 136 | value: false, 137 | expectedSerial: "false", 138 | }, 139 | { 140 | name: "date", 141 | value: Date(2017, 7, 10), 142 | expectedSerial: "DATE '2017-07-10'", 143 | }, 144 | { 145 | name: "time without timezone", 146 | value: Time(11, 34, 25, 123456), 147 | expectedSerial: "TIME '11:34:25.000123456'", 148 | }, 149 | { 150 | name: "time with timezone", 151 | value: TimeTz(11, 34, 25, 123456, time.FixedZone("test zone", +2*3600)), 152 | expectedSerial: "TIME '11:34:25.000123456 +02:00'", 153 | }, 154 | { 155 | name: "time with timezone", 156 | value: TimeTz(11, 34, 25, 123456, nil), 157 | expectedSerial: "TIME '11:34:25.000123456 Z'", 158 | }, 159 | { 160 | name: "timestamp without timezone", 161 | value: Timestamp(2017, 7, 10, 11, 34, 25, 123456), 162 | expectedSerial: "TIMESTAMP '2017-07-10 11:34:25.000123456'", 163 | }, 164 | { 165 | name: "timestamp with time zone in Fixed Zone", 166 | value: time.Date(2017, 7, 10, 11, 34, 25, 123456, time.FixedZone("test zone", +2*3600)), 167 | expectedSerial: "TIMESTAMP '2017-07-10 11:34:25.000123456 +02:00'", 168 | }, 169 | { 170 | name: "timestamp with time zone in Named Zone", 171 | value: time.Date(2017, 7, 10, 11, 34, 25, 123456, paris), 172 | expectedSerial: "TIMESTAMP '2017-07-10 11:34:25.000123456 +02:00'", 173 | }, 174 | { 175 | name: "timestamp with time zone in UTC", 176 | value: time.Date(2017, 7, 10, 11, 34, 25, 123456, time.UTC), 177 | expectedSerial: "TIMESTAMP '2017-07-10 11:34:25.000123456 Z'", 178 | }, 179 | { 180 | name: "duration", 181 | value: 10*time.Second + 5*time.Millisecond, 182 | expectedSerial: "INTERVAL '10.005' SECOND", 183 | }, 184 | { 185 | name: "duration with negative value", 186 | value: -(10*time.Second + 5*time.Millisecond), 187 | expectedSerial: "INTERVAL '-10.005' SECOND", 188 | }, 189 | { 190 | name: "minute duration", 191 | value: 10 * time.Minute, 192 | expectedSerial: "INTERVAL '10' MINUTE", 193 | }, 194 | { 195 | name: "hour duration", 196 | value: 23 * time.Hour, 197 | expectedSerial: "INTERVAL '23' HOUR", 198 | }, 199 | { 200 | name: "max hour duration", 201 | value: (math.MaxInt64 / time.Hour) * time.Hour, 202 | expectedSerial: "INTERVAL '2562047' HOUR", 203 | }, 204 | { 205 | name: "min hour duration", 206 | value: (math.MinInt64 / time.Hour) * time.Hour, 207 | expectedSerial: "INTERVAL '-2562047' HOUR", 208 | }, 209 | { 210 | name: "max minute duration", 211 | value: (math.MaxInt64 / time.Minute) * time.Minute, 212 | expectedSerial: "INTERVAL '153722867' MINUTE", 213 | }, 214 | { 215 | name: "min minute duration", 216 | value: (math.MinInt64 / time.Minute) * time.Minute, 217 | expectedSerial: "INTERVAL '-153722867' MINUTE", 218 | }, 219 | { 220 | name: "too big second duration", 221 | value: (math.MaxInt64 / time.Second) * time.Second, 222 | expectedError: true, 223 | }, 224 | { 225 | name: "too small second duration", 226 | value: (math.MinInt64 / time.Second) * time.Second, 227 | expectedError: true, 228 | }, 229 | { 230 | name: "too big millisecond duration", 231 | value: time.Millisecond*912 + time.Second*12345678, 232 | expectedError: true, 233 | }, 234 | { 235 | name: "too small millisecond duration", 236 | value: -(time.Millisecond*910 + time.Second*123456789), 237 | expectedError: true, 238 | }, 239 | { 240 | name: "max allowed second duration", 241 | value: math.MaxInt32 * time.Second, 242 | expectedSerial: "INTERVAL '2147483647' SECOND", 243 | }, 244 | { 245 | name: "min allowed second duration", 246 | value: -math.MaxInt32 * time.Second, 247 | expectedSerial: "INTERVAL '-2147483647' SECOND", 248 | }, 249 | { 250 | name: "max allowed second with milliseconds duration", 251 | value: 999999999*time.Second + 900*time.Millisecond, 252 | expectedSerial: "INTERVAL '999999999.9' SECOND", 253 | }, 254 | { 255 | name: "min allowed second with milliseconds duration", 256 | value: -999999999*time.Second - 900*time.Millisecond, 257 | expectedSerial: "INTERVAL '-999999999.9' SECOND", 258 | }, 259 | { 260 | name: "nil", 261 | value: nil, 262 | expectedSerial: "NULL", 263 | }, 264 | { 265 | name: "slice typed nil", 266 | value: []interface{}(nil), 267 | expectedError: true, 268 | }, 269 | { 270 | name: "valid slice", 271 | value: []interface{}{1, 2}, 272 | expectedSerial: "ARRAY[1, 2]", 273 | }, 274 | { 275 | name: "valid empty", 276 | value: []interface{}{}, 277 | expectedSerial: "ARRAY[]", 278 | }, 279 | { 280 | name: "invalid slice contents", 281 | value: []interface{}{1, byte('a')}, 282 | expectedError: true, 283 | }, 284 | } 285 | 286 | for i := range scenarios { 287 | scenario := scenarios[i] 288 | 289 | t.Run(scenario.name, func(t *testing.T) { 290 | s, err := Serial(scenario.value) 291 | if err != nil { 292 | if scenario.expectedError { 293 | return 294 | } 295 | t.Fatal(err) 296 | } 297 | 298 | if scenario.expectedError { 299 | t.Fatal("missing an expected error") 300 | } 301 | 302 | if scenario.expectedSerial != s { 303 | t.Fatalf("mismatched serial, got %q expected %q", s, scenario.expectedSerial) 304 | } 305 | }) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trino Go client 2 | 3 | A [Trino](https://trino.io) client for the [Go](https://golang.org) programming 4 | language. It enables you to send SQL statements from your Go application to 5 | Trino, and receive the resulting data. 6 | 7 | [![Build Status](https://github.com/trinodb/trino-go-client/workflows/ci/badge.svg)](https://github.com/trinodb/trino-go-client/actions?query=workflow%3Aci+event%3Apush+branch%3Amaster) 8 | [![GoDoc](https://godoc.org/github.com/trinodb/trino-go-client?status.svg)](https://godoc.org/github.com/trinodb/trino-go-client) 9 | 10 | ## Features 11 | 12 | * Native Go implementation 13 | * Connections over HTTP or HTTPS 14 | * HTTP Basic, Kerberos, and JSON web token (JWT) authentication 15 | * Per-query user information for access control 16 | * Support custom HTTP client (tunable conn pools, timeouts, TLS) 17 | * Supports conversion from Trino to native Go data types 18 | * `string`, `sql.NullString` 19 | * `int64`, `sql.NullInt64` 20 | * `float64`, `sql.NullFloat64` 21 | * `map`, `trino.NullMap` 22 | * `time.Time`, `trino.NullTime` 23 | * Up to 3-dimensional arrays to Go slices, of any supported type 24 | 25 | ## Requirements 26 | 27 | * Go 1.24.7 or newer 28 | * Trino 372 or newer 29 | 30 | ## Installation 31 | 32 | You need a working environment with Go installed and $GOPATH set. 33 | 34 | Download and install Trino database/sql driver: 35 | 36 | ```bash 37 | go get github.com/trinodb/trino-go-client/trino 38 | ``` 39 | 40 | Make sure you have Git installed and in your $PATH. 41 | 42 | ## Usage 43 | 44 | This Trino client is an implementation of Go's `database/sql/driver` interface. 45 | In order to use it, you need to import the package and use the 46 | [`database/sql`](https://golang.org/pkg/database/sql/) API then. 47 | 48 | Use `trino` as `driverName` and a valid [DSN](#dsn-data-source-name) as the 49 | `dataSourceName`. 50 | 51 | Example: 52 | 53 | ```go 54 | import "database/sql" 55 | import _ "github.com/trinodb/trino-go-client/trino" 56 | 57 | dsn := "http://user@localhost:8080?catalog=default&schema=test" 58 | db, err := sql.Open("trino", dsn) 59 | ``` 60 | 61 | ### Authentication 62 | 63 | Both HTTP Basic, Kerberos, and JWT authentication are supported. 64 | 65 | #### HTTP Basic authentication 66 | 67 | If the DSN contains a password, the client enables HTTP Basic authentication by 68 | setting the `Authorization` header in every request to Trino. 69 | 70 | HTTP Basic authentication **is only supported on encrypted connections over 71 | HTTPS**. 72 | 73 | #### Kerberos authentication 74 | 75 | This driver supports Kerberos authentication by setting up the Kerberos fields 76 | in the 77 | [Config](https://godoc.org/github.com/trinodb/trino-go-client/trino#Config) 78 | struct. 79 | 80 | Please refer to the [Coordinator Kerberos 81 | Authentication](https://trino.io/docs/current/security/server.html) for 82 | server-side configuration. 83 | 84 | #### JSON web token authentication 85 | 86 | This driver supports JWT authentication by setting up the `AccessToken` field 87 | in the 88 | [Config](https://godoc.org/github.com/trinodb/trino-go-client/trino#Config) 89 | struct. 90 | 91 | Please refer to the [Coordinator JWT 92 | Authentication](https://trino.io/docs/current/security/jwt.html) for 93 | server-side configuration. 94 | 95 | #### Authorization header forwarding 96 | This driver supports forwarding authorization headers by adding a [NamedArg](https://godoc.org/database/sql#NamedArg) with the name `accessToken` (e.g., `accessToken=`) and setting the `ForwardAuthorizationHeader` field in the [Config](https://godoc.org/github.com/trinodb/trino-go-client/trino#Config) struct to `true`. 97 | 98 | When enabled, this configuration will override the `AccessToken` set in the `Config` struct. 99 | 100 | 101 | #### System access control and per-query user information 102 | 103 | It's possible to pass user information to Trino, different from the principal 104 | used to authenticate to the coordinator. See the [System Access 105 | Control](https://trino.io/docs/current/develop/system-access-control.html) 106 | documentation for details. 107 | 108 | In order to pass user information in queries to Trino, you have to add a 109 | [NamedArg](https://godoc.org/database/sql#NamedArg) to the query parameters 110 | where the key is X-Trino-User. This parameter is used by the driver to inform 111 | Trino about the user executing the query regardless of the authentication 112 | method for the actual connection, and its value is NOT passed to the query. 113 | 114 | Example: 115 | 116 | ```go 117 | db.Query("SELECT * FROM foobar WHERE id=?", 1, sql.Named("X-Trino-User", string("Alice"))) 118 | ``` 119 | 120 | The position of the X-Trino-User NamedArg is irrelevant and does not affect the 121 | query in any way. 122 | 123 | ### DSN (Data Source Name) 124 | 125 | The Data Source Name is a URL with a mandatory username, and optional query 126 | string parameters that are supported by this driver, in the following format: 127 | 128 | ``` 129 | http[s]://user[:pass]@host[:port][?parameters] 130 | ``` 131 | 132 | The easiest way to build your DSN is by using the 133 | [Config.FormatDSN](https://godoc.org/github.com/trinodb/trino-go-client/trino#Config.FormatDSN) 134 | helper function. 135 | 136 | The driver supports both HTTP and HTTPS. If you use HTTPS it's recommended that 137 | you also provide a custom `http.Client` that can validate (or skip) the 138 | security checks of the server certificate, and/or to configure TLS client 139 | authentication. 140 | 141 | #### Parameters 142 | 143 | *Parameters are case-sensitive* 144 | 145 | Refer to the [Trino 146 | Concepts](https://trino.io/docs/current/overview/concepts.html) documentation 147 | for more information. 148 | 149 | ##### `source` 150 | 151 | ``` 152 | Type: string 153 | Valid values: string describing the source of the connection to Trino 154 | Default: empty 155 | ``` 156 | 157 | The `source` parameter is optional, but if used, can help Trino admins 158 | troubleshoot queries and trace them back to the original client. 159 | 160 | ##### `catalog` 161 | 162 | ``` 163 | Type: string 164 | Valid values: the name of a catalog configured in the Trino server 165 | Default: empty 166 | ``` 167 | 168 | The `catalog` parameter defines the Trino catalog where schemas exist to 169 | organize tables. 170 | 171 | ##### `schema` 172 | 173 | ``` 174 | Type: string 175 | Valid values: the name of an existing schema in the catalog 176 | Default: empty 177 | ``` 178 | 179 | The `schema` parameter defines the Trino schema where tables exist. This is 180 | also known as namespace in some environments. 181 | 182 | ##### `session_properties` 183 | 184 | ``` 185 | Type: string 186 | Valid values: semicolon-separated list of key:value session properties 187 | Default: empty 188 | ``` 189 | 190 | The `session_properties` parameter must contain valid parameters accepted by 191 | the Trino server. Run `SHOW SESSION` in Trino to get the current list. 192 | 193 | ##### `custom_client` 194 | 195 | ``` 196 | Type: string 197 | Valid values: the name of a client previously registered to the driver 198 | Default: empty (defaults to http.DefaultClient) 199 | ``` 200 | 201 | The `custom_client` parameter allows the use of custom `http.Client` for the 202 | communication with Trino. 203 | 204 | Register your custom client in the driver, then refer to it by name in the DSN, 205 | on the call to `sql.Open`: 206 | 207 | ```go 208 | foobarClient := &http.Client{ 209 | Transport: &http.Transport{ 210 | Proxy: http.ProxyFromEnvironment, 211 | DialContext: (&net.Dialer{ 212 | Timeout: 30 * time.Second, 213 | KeepAlive: 30 * time.Second, 214 | DualStack: true, 215 | }).DialContext, 216 | MaxIdleConns: 100, 217 | IdleConnTimeout: 90 * time.Second, 218 | TLSHandshakeTimeout: 10 * time.Second, 219 | ExpectContinueTimeout: 1 * time.Second, 220 | TLSClientConfig: &tls.Config{ 221 | // your config here... 222 | }, 223 | }, 224 | } 225 | trino.RegisterCustomClient("foobar", foobarClient) 226 | db, err := sql.Open("trino", "https://user@localhost:8080?custom_client=foobar") 227 | ``` 228 | 229 | A custom client can also be used to add OpenTelemetry instrumentation. The 230 | [otelhttp](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp) 231 | package provides a transport wrapper that creates spans for HTTP requests and 232 | propagates the trace ID in HTTP headers: 233 | 234 | ```go 235 | otelClient := &http.Client{ 236 | Transport: otelhttp.NewTransport(http.DefaultTransport), 237 | } 238 | trino.RegisterCustomClient("otel", otelClient) 239 | db, err := sql.Open("trino", "https://user@localhost:8080?custom_client=otel") 240 | ``` 241 | 242 | ##### `query_timeout` 243 | 244 | ``` 245 | Type: time.Duration 246 | Valid values: duration string 247 | Default: nil 248 | ``` 249 | 250 | The `query_timeout` parameter sets a timeout for the query. If the query takes longer than the timeout, it will be cancelled. If it is not set the default context timeout will be used. 251 | 252 | ##### `explicitPrepare` 253 | 254 | ``` 255 | Type: string 256 | Valid values: "true", "false" 257 | Default: "true" 258 | ``` 259 | 260 | The `explicitPrepare` parameter controls how queries are sent to the Trino server. When set to `false`, the client uses `EXECUTE IMMEDIATE` which sends the query text in the HTTP request body instead of HTTP headers. This allows sending large query text that would otherwise exceed HTTP header size limits. When set to `true` (default), queries use explicit prepared statements sent via HTTP headers. 261 | 262 | ##### `clientTags` 263 | 264 | ``` 265 | Type: string 266 | Valid values: comma-separated list of tags (e.g. tag1,tag2) 267 | Default: empty 268 | ``` 269 | 270 | The `clientTags` parameter is optional and is used to identify Trino resource groups. 271 | This helps with query tracking and resource management in Trino clusters. 272 | 273 | **DSN parameter example:** 274 | ``` 275 | clientTags=tag1,tag2 276 | ``` 277 | 278 | **Config struct example:** 279 | ```go 280 | config := &Config{ 281 | ServerURI: "http://foobar@localhost:8080", 282 | ClientTags: []string{"tag1", "tag2", "tag3"}, 283 | } 284 | 285 | dsn, err := config.FormatDSN() 286 | ``` 287 | 288 | **Query parameter example (overrides DSN client tags):** 289 | ```go 290 | rows, err := db.Query(query, sql.Named("X-Trino-Client-Tags", "tag1,tag2,tag3")) 291 | ``` 292 | ======= 293 | 294 | #### `roles` 295 | 296 | ``` 297 | Type: string 298 | Format: roles=catalog1:role1;catalog2=role2 299 | Valid values: A semicolon-separated list of catalog-to-role assignments, where each assignment maps a catalog to a role. 300 | Default: empty 301 | ``` 302 | The roles parameter defines authorization roles to assume for one or more catalogs during the Trino session. 303 | 304 | ##### Example 305 | ``` go 306 | c := &Config{ 307 | ServerURI: "https://foobar@localhost:8090", 308 | SessionProperties: map[string]string{"query_priority": "1"}, 309 | Roles: map[string]string{"catalog1": "role1", "catalog2": "role2"}, 310 | } 311 | 312 | dsn, err := c.FormatDSN() 313 | ``` 314 | 315 | **Query parameter example (overrides DSN roles):** 316 | ```go 317 | rows, err := db.Query( 318 | query, 319 | sql.Named("X-Trino-Role", map[string]string{ 320 | "catalog1": "role1", 321 | "catalog2": "role2", 322 | }), 323 | ) 324 | ``` 325 | 326 | #### Examples 327 | 328 | ``` 329 | http://user@localhost:8080?source=hello&catalog=default&schema=foobar 330 | ``` 331 | 332 | ``` 333 | https://user@localhost:8443?session_properties=query_max_run_time:10m;query_priority:2 334 | ``` 335 | 336 | 337 | ``` 338 | http://user@localhost:8080?source=hello&catalog=default&schema=foobar&roles=catalog1:role1;catalog2:role2 339 | ``` 340 | 341 | ## Data types 342 | 343 | ### Query arguments 344 | 345 | When passing arguments to queries, the driver supports the following Go data 346 | types: 347 | * integers 348 | * `bool` 349 | * `string` 350 | * `[]byte` 351 | * slices 352 | * `trino.Numeric` - a string representation of a number 353 | * `time.Time` - passed to Trino as a timestamp with a time zone 354 | * the result of `trino.Date(year, month, day)` - passed to Trino as a date 355 | * the result of `trino.Time(hour, minute, second, nanosecond)` - passed to 356 | Trino as a time without a time zone 357 | * the result of `trino.TimeTz(hour, minute, second, nanosecond, location)` - 358 | passed to Trino as a time with a time zone 359 | * the result of `trino.Timestamp(year, month, day, hour, minute, second, 360 | nanosecond)` - passed to Trino as a timestamp without a time zone 361 | * `time.Duration` - passed to Trino as an interval day to second. Because Trino does not support nanosecond precision for intervals, if the nanosecond part of the value is not zero, an error will be returned. 362 | 363 | It's not yet possible to pass: 364 | * `float32` or `float64` 365 | * `byte` 366 | * `json.RawMessage` 367 | * maps 368 | 369 | To use the unsupported types, pass them as strings and use casts in the query, 370 | like so: 371 | ```sql 372 | SELECT * FROM table WHERE col_double = cast(? AS DOUBLE) OR col_timestamp = CAST(? AS TIMESTAMP) 373 | ``` 374 | 375 | ### Response rows 376 | 377 | When reading response rows, the driver supports most Trino data types, except: 378 | * time and timestamps with precision - all time types are returned as 379 | `time.Time`. All precisions up to nanoseconds (`TIMESTAMP(9)` or `TIME(9)`) 380 | are supported (since this is the maximum precision Golang's `time.Time` 381 | supports). If a query returns columns defined with a greater precision, 382 | values are trimmed to 9 decimal digits. Use `CAST` to reduce the returned 383 | precision, or convert the value to a string that then can be parsed manually. 384 | * `DECIMAL` - returned as string 385 | * `IPADDRESS` - returned as string 386 | * `INTERVAL YEAR TO MONTH` and `INTERVAL DAY TO SECOND` - returned as string 387 | * `UUID` - returned as string 388 | 389 | Data types like `HyperLogLog`, `SetDigest`, `QDigest`, and `TDigest` are not 390 | supported and cannot be returned from a query. 391 | 392 | For reading nullable columns, use: 393 | * `trino.NullTime` 394 | * `trino.NullMap` - which stores a map of `map[string]interface{}` 395 | or similar structs from the `database/sql` package, like `sql.NullInt64` 396 | 397 | To read query results containing arrays or maps, pass one of the following 398 | structs to the `Scan()` function: 399 | 400 | * `trino.NullSliceBool` 401 | * `trino.NullSliceString` 402 | * `trino.NullSliceInt64` 403 | * `trino.NullSliceFloat64` 404 | * `trino.NullSliceTime` 405 | * `trino.NullSliceMap` 406 | 407 | For two or three dimensional arrays, use `trino.NullSlice2Bool` and 408 | `trino.NullSlice3Bool` or equivalents for other data types. 409 | 410 | To read `ROW` values, implement the `sql.Scanner` interface in a struct. Its 411 | `Scan()` function receives a `[]interface{}` slice, with values of the 412 | following types: 413 | * `bool` 414 | * `json.Number` for any numeric Trino types 415 | * `[]interface{}` for Trino arrays 416 | * `map[string]interface{}` for Trino maps 417 | * `string` for other Trino types, as character, date, time, or timestamp. 418 | 419 | > [!NOTE] 420 | > `VARBINARY` columns are returned as base64-encoded strings when used within 421 | > `ROW`, `MAP`, or `ARRAY` values. 422 | 423 | ## Spooling Protocol 424 | 425 | The client supports the [Trino spooling protocol](https://trino.io/docs/current/client/client-protocol.html#spooling-protocol), which enables efficient retrieval of large result sets by downloading data in segments, optionally in parallel and out-of-order. 426 | 427 | If the Trino server has the spooling protocol enabled, the client will use it by default with the `json` encoding. 428 | 429 | You can configure other encodings: 430 | 431 | - Supported encodings: `json`, `json+lz4`, `json+zstd` 432 | 433 | ```go 434 | rows, err := db.Query(query, sql.Named("encoding", "json+zstd")) 435 | ``` 436 | 437 | Or specify a list of supported encodings in order of preference: 438 | 439 | ```go 440 | rows, err := db.Query(query, sql.Named("encoding", "json+zstd, json+lz4, json")) 441 | ``` 442 | 443 | ### Configuration Options 444 | 445 | You can tune the spooling protocol using the following parameters, passed as `sql.Named` arguments to your query: 446 | 447 | - **Spooling Worker Count** 448 | `sql.Named("spooling_worker_count", "N")` 449 | Sets the number of parallel workers used to download spooled segments. 450 | **Default:** `5` 451 | **Considerations:** 452 | - Increasing this value can improve throughput for large result sets, especially on high-latency networks. 453 | - Higher values increase parallelism but may also increase memory usage. 454 | 455 | - **Max Out-of-Order Segments** 456 | `sql.Named("max_out_of_order_segments", "N")` 457 | Sets the maximum number of segments that can be downloaded and buffered out-of-order before blocking further downloads. 458 | **Default:** `10` 459 | **Considerations:** 460 | - Higher values increase the potential memory usage, but actual usage depends on download behavior and may be lower in practice. 461 | - Higher values reduce the chance that one slow or stalled segment will block the download of additional segments. 462 | - Lower values reduce memory usage but may limit parallelism and throughput. 463 | 464 | **Note:** 465 | It is **not allowed** to set `spooling_worker_count` higher than `max_out_of_order_segments` — doing so will result in an error. 466 | 467 | Each download worker must reserve a slot for the segment it fetches, and a slot is only released when that segment can be processed in order. The total number of slots corresponds to max_out_of_order_segments. 468 | If you configure more workers than allowed out-of-order segments, the extra workers would immediately block while waiting for a slot — defeating the purpose of parallelism and potentially wasting resources. 469 | 470 | #### Example: Customizing Spooling Parameters 471 | 472 | ```go 473 | rows, err := db.Query( 474 | query, 475 | sql.Named("encoding", "json+zstd"), 476 | sql.Named("spooling_worker_count", "8"), 477 | sql.Named("max_out_of_order_segments", "20"), 478 | ) 479 | ``` 480 | 481 | ## License 482 | 483 | Apache License V2.0, as described in the [LICENSE](./LICENSE) file. 484 | 485 | ## Build 486 | 487 | You can build the client code locally and run tests with the following command: 488 | 489 | ``` 490 | go test -v -race -timeout 2m ./... 491 | ``` 492 | 493 | ## Contributing 494 | 495 | For contributing, development, and release guidelines, see 496 | [CONTRIBUTING.md](./CONTRIBUTING.md). 497 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 2 | dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 3 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 4 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 5 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= 6 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 7 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 8 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 9 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= 10 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= 11 | github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:pzStYMLAXM7CNQjS/Wn+zK9MUxDhSUNfVvnHsyQyjs0= 12 | github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:ilK+u7u1HoqaDk0mjhh27QJB7PyWMreGffEvOCoEKiY= 13 | github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:3YVZUqkoev4mL+aCwVOSWV4M7pN+NURHL38Z2zq5JKA= 14 | github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:ymXt5bw5uSNu4jveerFxE0vNYxF8ncqbptntMaFMg3k= 15 | github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= 16 | github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= 17 | github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4= 18 | github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= 19 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E= 20 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00= 21 | github.com/aws/aws-sdk-go-v2/config v1.31.8 h1:kQjtOLlTU4m4A64TsRcqwNChhGCwaPBt+zCQt/oWsHU= 22 | github.com/aws/aws-sdk-go-v2/config v1.31.8/go.mod h1:QPpc7IgljrKwH0+E6/KolCgr4WPLerURiU592AYzfSY= 23 | github.com/aws/aws-sdk-go-v2/credentials v1.18.12 h1:zmc9e1q90wMn8wQbjryy8IwA6Q4XlaL9Bx2zIqdNNbk= 24 | github.com/aws/aws-sdk-go-v2/credentials v1.18.12/go.mod h1:3VzdRDR5u3sSJRI4kYcOSIBbeYsgtVk7dG5R/U6qLWY= 25 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM= 26 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w= 27 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg= 28 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k= 29 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY= 30 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8= 31 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 32 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 33 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.7 h1:BszAktdUo2xlzmYHjWMq70DqJ7cROM8iBd3f6hrpuMQ= 34 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.7/go.mod h1:XJ1yHki/P7ZPuG4fd3f0Pg/dSGA2cTQBCLw82MH2H48= 35 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= 36 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= 37 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.7 h1:zmZ8qvtE9chfhBPuKB2aQFxW5F/rpwXUgmcVCgQzqRw= 38 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.7/go.mod h1:vVYfbpd2l+pKqlSIDIOgouxNsGu5il9uDp0ooWb0jys= 39 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8= 40 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA= 41 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.7 h1:u3VbDKUCWarWiU+aIUK4gjTr/wQFXV17y3hgNno9fcA= 42 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.7/go.mod h1:/OuMQwhSyRapYxq6ZNpPer8juGNrB4P5Oz8bZ2cgjQE= 43 | github.com/aws/aws-sdk-go-v2/service/s3 v1.88.1 h1:+RpGuaQ72qnU83qBKVwxkznewEdAGhIWo/PQCmkhhog= 44 | github.com/aws/aws-sdk-go-v2/service/s3 v1.88.1/go.mod h1:xajPTguLoeQMAOE44AAP2RQoUhF8ey1g5IFHARv71po= 45 | github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 h1:7PKX3VYsZ8LUWceVRuv0+PU+E7OtQb1lgmi5vmUE9CM= 46 | github.com/aws/aws-sdk-go-v2/service/sso v1.29.3/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY= 47 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 h1:e0XBRn3AptQotkyBFrHAxFB8mDhAIOfsG+7KyJ0dg98= 48 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I= 49 | github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 h1:PR00NXRYgY4FWHqOGx3fC3lhVKjsp1GdloDv2ynMSd8= 50 | github.com/aws/aws-sdk-go-v2/service/sts v1.38.4/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8= 51 | github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= 52 | github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 53 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 54 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 55 | github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= 56 | github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= 57 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 58 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 59 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 60 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 61 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 62 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 63 | github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY= 64 | github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 65 | github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= 66 | github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 67 | github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= 68 | github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 69 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 70 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 71 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 72 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 73 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 74 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 75 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 76 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 77 | github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 78 | github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 79 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 80 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 81 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 82 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 83 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 84 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 85 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 86 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 87 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 88 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 89 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 90 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 91 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 92 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 93 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 94 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 95 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 96 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 97 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 98 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 99 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 100 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 101 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 102 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 103 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 104 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 105 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 106 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 107 | github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= 108 | github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= 109 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 110 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 111 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 112 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 113 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 114 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 115 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 116 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 117 | github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= 118 | github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 119 | github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= 120 | github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= 121 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 122 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 123 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 124 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 125 | github.com/opencontainers/runc v1.3.1 h1:c/yY0oh2wK7tzDuD56REnSxyU8ubh8hoAIOLGLrm4SM= 126 | github.com/opencontainers/runc v1.3.1/go.mod h1:9wbWt42gV+KRxKRVVugNP6D5+PQciRbenB4fLVsqGPs= 127 | github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= 128 | github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= 129 | github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= 130 | github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 131 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 132 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 133 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 134 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 135 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 136 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 137 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 138 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 139 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 140 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 141 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 142 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 143 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 144 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 145 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 146 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 147 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 148 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 149 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 150 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 151 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 152 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 153 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 154 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 155 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 156 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 157 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 158 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 159 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 160 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 161 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 162 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 163 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 164 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 165 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 166 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 167 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 168 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 169 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 170 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 171 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 172 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 173 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 174 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 175 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 176 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 178 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 179 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 180 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 181 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 182 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 183 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 184 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 185 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 186 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 187 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 188 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 189 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 190 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 191 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 192 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 193 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 194 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 195 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 196 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 197 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 198 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 199 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 201 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 202 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 203 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 204 | -------------------------------------------------------------------------------- /trino/integration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package trino 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "crypto/rand" 21 | "crypto/rsa" 22 | "crypto/tls" 23 | "crypto/x509" 24 | "crypto/x509/pkix" 25 | "database/sql" 26 | "database/sql/driver" 27 | "encoding/json" 28 | "encoding/pem" 29 | "errors" 30 | "flag" 31 | "fmt" 32 | "io" 33 | "log" 34 | "math" 35 | "math/big" 36 | "net/http" 37 | "net/url" 38 | "os" 39 | "reflect" 40 | "strconv" 41 | "strings" 42 | "testing" 43 | "time" 44 | 45 | "github.com/ahmetb/dlog" 46 | "github.com/aws/aws-sdk-go-v2/config" 47 | "github.com/aws/aws-sdk-go-v2/credentials" 48 | "github.com/aws/aws-sdk-go-v2/service/s3" 49 | "github.com/aws/aws-sdk-go/aws" 50 | "github.com/golang-jwt/jwt/v5" 51 | dt "github.com/ory/dockertest/v3" 52 | docker "github.com/ory/dockertest/v3/docker" 53 | "github.com/stretchr/testify/require" 54 | ) 55 | 56 | const ( 57 | DockerLocalStackName = "localstack" 58 | bucketName = "spooling" 59 | DockerTrinoName = "trino-go-client-tests" 60 | MAXRetries = 10 61 | TrinoNetwork = "trino-network" 62 | ) 63 | 64 | var ( 65 | pool *dt.Pool 66 | trinoResource *dt.Resource 67 | localStackResource *dt.Resource 68 | spoolingProtocolSupported bool 69 | 70 | trinoImageTagFlag = flag.String( 71 | "trino_image_tag", 72 | os.Getenv("TRINO_IMAGE_TAG"), 73 | "Docker image tag used for the Trino server container", 74 | ) 75 | integrationServerFlag = flag.String( 76 | "trino_server_dsn", 77 | os.Getenv("TRINO_SERVER_DSN"), 78 | "dsn of the Trino server used for integration tests instead of starting a Docker container", 79 | ) 80 | integrationServerQueryTimeout = flag.Duration( 81 | "trino_query_timeout", 82 | 5*time.Second, 83 | "max duration for Trino queries to run before giving up", 84 | ) 85 | noCleanup = flag.Bool( 86 | "no_cleanup", 87 | false, 88 | "do not delete containers on exit", 89 | ) 90 | tlsServer = "" 91 | ) 92 | 93 | func TestMain(m *testing.M) { 94 | flag.Parse() 95 | DefaultQueryTimeout = *integrationServerQueryTimeout 96 | DefaultCancelQueryTimeout = *integrationServerQueryTimeout 97 | if *trinoImageTagFlag == "" { 98 | *trinoImageTagFlag = "latest" 99 | } 100 | 101 | if *trinoImageTagFlag == "latest" { 102 | spoolingProtocolSupported = true 103 | } else { 104 | version, err := strconv.Atoi(*trinoImageTagFlag) 105 | if err != nil { 106 | log.Fatalf("Invalid trino_image_tag: %s", *trinoImageTagFlag) 107 | } 108 | spoolingProtocolSupported = version >= 466 109 | } 110 | 111 | var err error 112 | if *integrationServerFlag == "" && !testing.Short() { 113 | pool, err = dt.NewPool("") 114 | if err != nil { 115 | log.Fatalf("Could not connect to docker: %s", err) 116 | } 117 | pool.MaxWait = 1 * time.Minute 118 | 119 | networkID := getOrCreateNetwork(pool) 120 | 121 | wd, err := os.Getwd() 122 | if err != nil { 123 | log.Fatalf("Failed to get working directory: %s", err) 124 | } 125 | 126 | var ok bool 127 | if spoolingProtocolSupported { 128 | localStackResource = getOrCreateLocalStack(pool, networkID) 129 | } 130 | 131 | trinoResource, ok = pool.ContainerByName(DockerTrinoName) 132 | 133 | if !ok { 134 | err = generateCerts(wd + "/etc/secrets") 135 | if err != nil { 136 | log.Fatalf("Could not generate TLS certificates: %s", err) 137 | } 138 | 139 | mounts := []string{ 140 | wd + "/etc/secrets:/etc/trino/secrets", 141 | wd + "/etc/jvm.config:/etc/trino/jvm.config", 142 | wd + "/etc/node.properties:/etc/trino/node.properties", 143 | wd + "/etc/password-authenticator.properties:/etc/trino/password-authenticator.properties", 144 | wd + "/etc/catalog/memory.properties:/etc/trino/catalog/memory.properties", 145 | wd + "/etc/catalog/tpch.properties:/etc/trino/catalog/tpch.properties", 146 | } 147 | version, err := strconv.Atoi(*trinoImageTagFlag) 148 | if (err != nil && *trinoImageTagFlag == "latest") || (err == nil && version >= 458) { 149 | mounts = append(mounts, wd+"/etc/catalog/hive.properties:/etc/trino/catalog/hive.properties") 150 | } 151 | 152 | if spoolingProtocolSupported { 153 | version, err := strconv.Atoi(*trinoImageTagFlag) 154 | if (err != nil && *trinoImageTagFlag != "latest") || (err == nil && version < 477) { 155 | mounts = append(mounts, wd+"/etc/config-pre-477version.properties:/etc/trino/config.properties") 156 | } else { 157 | mounts = append(mounts, wd+"/etc/config.properties:/etc/trino/config.properties") 158 | } 159 | mounts = append(mounts, wd+"/etc/spooling-manager.properties:/etc/trino/spooling-manager.properties") 160 | } else { 161 | mounts = append(mounts, wd+"/etc/config-pre-466version.properties:/etc/trino/config.properties") 162 | } 163 | trinoResource, err = pool.RunWithOptions(&dt.RunOptions{ 164 | Name: DockerTrinoName, 165 | Repository: "trinodb/trino", 166 | Tag: *trinoImageTagFlag, 167 | Mounts: mounts, 168 | ExposedPorts: []string{ 169 | "8080/tcp", 170 | "8443/tcp", 171 | }, 172 | NetworkID: networkID, 173 | }, func(hc *docker.HostConfig) { 174 | hc.Ulimits = []docker.ULimit{ 175 | { 176 | Name: "nofile", 177 | Hard: 4096, 178 | Soft: 4096, 179 | }, 180 | } 181 | }) 182 | if err != nil { 183 | log.Fatalf("Could not start resource: %s", err) 184 | } 185 | } else if !trinoResource.Container.State.Running { 186 | pool.Client.StartContainer(trinoResource.Container.ID, nil) 187 | } 188 | 189 | waitForContainerHealth(trinoResource.Container.ID, "trino") 190 | 191 | err = grantAdminRoleToTestUser() 192 | if err != nil { 193 | log.Fatalf("Warning: Failed to grant admin role to test user: %s", err) 194 | } 195 | 196 | *integrationServerFlag = "http://test@localhost:" + trinoResource.GetPort("8080/tcp") 197 | tlsServer = "https://admin:admin@localhost:" + trinoResource.GetPort("8443/tcp") 198 | 199 | http.DefaultTransport.(*http.Transport).TLSClientConfig, err = getTLSConfig(wd + "/etc/secrets") 200 | if err != nil { 201 | log.Fatalf("Failed to set the default TLS config: %s", err) 202 | } 203 | } 204 | 205 | code := m.Run() 206 | 207 | if !*noCleanup && pool != nil { 208 | if trinoResource != nil { 209 | if err := pool.Purge(trinoResource); err != nil { 210 | log.Fatalf("Could not purge resource: %s", err) 211 | } 212 | } 213 | 214 | if localStackResource != nil { 215 | if err := pool.Purge(localStackResource); err != nil { 216 | log.Fatalf("Could not purge LocalStack resource: %s", err) 217 | } 218 | } 219 | 220 | networkExists, networkID, err := networkExists(pool, TrinoNetwork) 221 | if err == nil && networkExists { 222 | if err := pool.Client.RemoveNetwork(networkID); err != nil { 223 | log.Fatalf("Could not remove Docker network: %s", err) 224 | } 225 | } 226 | } 227 | 228 | os.Exit(code) 229 | } 230 | 231 | func grantAdminRoleToTestUser() error { 232 | grantSQL := "SET ROLE admin IN hive; GRANT admin TO USER test IN hive;" 233 | 234 | execCmd := []string{ 235 | "trino", 236 | "--user", "admin", 237 | "--execute", grantSQL, 238 | } 239 | exec, err := pool.Client.CreateExec(docker.CreateExecOptions{ 240 | Container: trinoResource.Container.ID, 241 | Cmd: execCmd, 242 | }) 243 | if err != nil { 244 | log.Printf("Warning: Failed to create exec for GRANT: %s", err) 245 | } else { 246 | var stdout, stderr bytes.Buffer 247 | err = pool.Client.StartExec(exec.ID, docker.StartExecOptions{ 248 | Detach: false, 249 | OutputStream: &stdout, 250 | ErrorStream: &stderr, 251 | }) 252 | if err != nil { 253 | log.Printf("Warning: Failed to execute GRANT: %s", err) 254 | } 255 | } 256 | 257 | return err 258 | } 259 | 260 | func getOrCreateLocalStack(pool *dt.Pool, networkID string) *dt.Resource { 261 | resource, ok := pool.ContainerByName(DockerLocalStackName) 262 | if ok { 263 | return resource 264 | } 265 | 266 | newResource, err := setupLocalStack(pool, networkID) 267 | if err != nil { 268 | log.Fatalf("Failed to start LocalStack: %s", err) 269 | } 270 | 271 | return newResource 272 | } 273 | 274 | func getOrCreateNetwork(pool *dt.Pool) string { 275 | networkExists, networkID, err := networkExists(pool, TrinoNetwork) 276 | if err != nil { 277 | log.Fatalf("Could not check if Docker network exists: %s", err) 278 | } 279 | 280 | if networkExists { 281 | return networkID 282 | } 283 | 284 | network, err := pool.Client.CreateNetwork(docker.CreateNetworkOptions{ 285 | Name: TrinoNetwork, 286 | }) 287 | if err != nil { 288 | log.Fatalf("Could not create Docker network: %s", err) 289 | } 290 | 291 | return network.ID 292 | } 293 | 294 | func networkExists(pool *dt.Pool, networkName string) (bool, string, error) { 295 | networks, err := pool.Client.ListNetworks() 296 | if err != nil { 297 | return false, "", fmt.Errorf("could not list Docker networks: %w", err) 298 | } 299 | for _, network := range networks { 300 | if network.Name == networkName { 301 | return true, network.ID, nil 302 | } 303 | } 304 | return false, "", nil 305 | } 306 | 307 | func setupLocalStack(pool *dt.Pool, networkID string) (*dt.Resource, error) { 308 | localstackResource, err := pool.RunWithOptions(&dt.RunOptions{ 309 | Name: DockerLocalStackName, 310 | Repository: "localstack/localstack", 311 | Tag: "latest", 312 | Env: []string{ 313 | "SERVICES=s3", 314 | "region_name=us-east-1", 315 | "AWS_ACCESS_KEY_ID=test", 316 | "AWS_SECRET_ACCESS_KEY=test", 317 | }, 318 | 319 | PortBindings: map[docker.Port][]docker.PortBinding{ 320 | "4566/tcp": {{HostIP: "0.0.0.0", HostPort: "4566"}}, 321 | "4571/tcp": {{HostIP: "0.0.0.0", HostPort: "4571"}}, 322 | }, 323 | 324 | NetworkID: networkID, 325 | }) 326 | if err != nil { 327 | return nil, fmt.Errorf("could not start LocalStack: %w", err) 328 | } 329 | 330 | localstackPort := localstackResource.GetPort("4566/tcp") 331 | s3Endpoint := "http://localhost:" + localstackPort 332 | 333 | log.Println("LocalStack started at:", s3Endpoint) 334 | 335 | waitForContainerHealth(localstackResource.Container.ID, "localstack") 336 | 337 | for retry := 0; retry < MAXRetries; retry++ { 338 | err := createS3Bucket(s3Endpoint, "test", "test", bucketName) 339 | if err == nil { 340 | log.Println("S3 bucket created successfully") 341 | return localstackResource, nil 342 | } 343 | log.Printf("Failed to create S3 bucket, retrying... (%d/%d)\n", retry+1, MAXRetries) 344 | time.Sleep(2 * time.Second) 345 | } 346 | 347 | return nil, fmt.Errorf("failed to create S3 bucket after multiple attempts: %w", err) 348 | } 349 | 350 | func createS3Bucket(endpoint, accessKey, secretKey, bucketName string) error { 351 | cfg, err := config.LoadDefaultConfig(context.TODO(), 352 | config.WithRegion("us-east-1"), 353 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")), 354 | ) 355 | if err != nil { 356 | return fmt.Errorf("failed to load AWS config: %w", err) 357 | } 358 | 359 | s3Client := s3.New(s3.Options{ 360 | Credentials: cfg.Credentials, 361 | Region: "us-east-1", 362 | BaseEndpoint: &endpoint, 363 | UsePathStyle: *aws.Bool(true), 364 | }) 365 | 366 | createBucketInput := &s3.CreateBucketInput{ 367 | Bucket: aws.String(bucketName), 368 | } 369 | 370 | _, err = s3Client.CreateBucket(context.TODO(), createBucketInput) 371 | if err != nil { 372 | return fmt.Errorf("failed to create S3 bucket: %w", err) 373 | } 374 | 375 | log.Printf("Bucket %s created successfully!", bucketName) 376 | return nil 377 | } 378 | 379 | func waitForContainerHealth(containerID, containerName string) { 380 | if err := pool.Retry(func() error { 381 | c, err := pool.Client.InspectContainer(containerID) 382 | if err != nil { 383 | log.Fatalf("Failed to inspect container %s: %s", containerID, err) 384 | } 385 | if !c.State.Running { 386 | log.Fatalf("Container %s is not running: %s\nContainer logs:\n%s", containerID, c.State.String(), getLogs(trinoResource.Container.ID)) 387 | } 388 | log.Printf("Waiting for %s container: %s\n", containerName, c.State.String()) 389 | if c.State.Health.Status != "healthy" { 390 | return errors.New("Not ready") 391 | } 392 | return nil 393 | }); err != nil { 394 | log.Fatalf("Timed out waiting for container %s to get ready: %s\nContainer logs:\n%s", containerName, err, getLogs(containerID)) 395 | } 396 | } 397 | 398 | func generateCerts(dir string) error { 399 | priv, err := rsa.GenerateKey(rand.Reader, 2048) 400 | if err != nil { 401 | return fmt.Errorf("failed to generate private key: %w", err) 402 | } 403 | 404 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 405 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 406 | if err != nil { 407 | return fmt.Errorf("failed to generate serial number: %w", err) 408 | } 409 | 410 | template := x509.Certificate{ 411 | SerialNumber: serialNumber, 412 | Subject: pkix.Name{ 413 | Organization: []string{"Trino Software Foundation"}, 414 | }, 415 | DNSNames: []string{"localhost"}, 416 | NotBefore: time.Now(), 417 | NotAfter: time.Now().Add(1 * time.Hour), 418 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 419 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 420 | BasicConstraintsValid: true, 421 | } 422 | 423 | privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 424 | if err != nil { 425 | return fmt.Errorf("unable to marshal private key: %w", err) 426 | } 427 | privBlock := &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes} 428 | err = writePEM(dir+"/private_key.pem", privBlock) 429 | if err != nil { 430 | return err 431 | } 432 | 433 | pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) 434 | if err != nil { 435 | return fmt.Errorf("unable to marshal public key: %w", err) 436 | } 437 | pubBlock := &pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes} 438 | err = writePEM(dir+"/public_key.pem", pubBlock) 439 | if err != nil { 440 | return err 441 | } 442 | 443 | certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) 444 | if err != nil { 445 | return fmt.Errorf("failed to create certificate: %w", err) 446 | } 447 | certBlock := &pem.Block{Type: "CERTIFICATE", Bytes: certBytes} 448 | err = writePEM(dir+"/certificate.pem", certBlock) 449 | if err != nil { 450 | return err 451 | } 452 | 453 | err = writePEM(dir+"/certificate_with_key.pem", certBlock, privBlock, pubBlock) 454 | if err != nil { 455 | return err 456 | } 457 | 458 | return nil 459 | } 460 | 461 | func writePEM(filename string, blocks ...*pem.Block) error { 462 | // all files are world-readable, so they can be read inside the Trino container 463 | out, err := os.Create(filename) 464 | if err != nil { 465 | return fmt.Errorf("failed to open %s for writing: %w", filename, err) 466 | } 467 | for _, block := range blocks { 468 | if err := pem.Encode(out, block); err != nil { 469 | return fmt.Errorf("failed to write %s data to %s: %w", block.Type, filename, err) 470 | } 471 | } 472 | if err := out.Close(); err != nil { 473 | return fmt.Errorf("error closing %s: %w", filename, err) 474 | } 475 | return nil 476 | } 477 | 478 | func getTLSConfig(dir string) (*tls.Config, error) { 479 | certPool, err := x509.SystemCertPool() 480 | if err != nil { 481 | return nil, fmt.Errorf("failed to read the system cert pool: %s", err) 482 | } 483 | caCertPEM, err := os.ReadFile(dir + "/certificate.pem") 484 | if err != nil { 485 | return nil, fmt.Errorf("failed to read the certificate: %s", err) 486 | } 487 | ok := certPool.AppendCertsFromPEM(caCertPEM) 488 | if !ok { 489 | return nil, fmt.Errorf("failed to parse the certificate: %s", err) 490 | } 491 | return &tls.Config{ 492 | RootCAs: certPool, 493 | }, nil 494 | } 495 | 496 | func getLogs(id string) []byte { 497 | var buf bytes.Buffer 498 | pool.Client.Logs(docker.LogsOptions{ 499 | Container: id, 500 | OutputStream: &buf, 501 | ErrorStream: &buf, 502 | Stdout: true, 503 | Stderr: true, 504 | RawTerminal: true, 505 | }) 506 | logs, _ := io.ReadAll(dlog.NewReader(&buf)) 507 | return logs 508 | } 509 | 510 | // integrationOpen opens a connection to the integration test server. 511 | func integrationOpen(t *testing.T, dsn ...string) *sql.DB { 512 | if testing.Short() { 513 | t.Skip("Skipping test in short mode.") 514 | } 515 | target := *integrationServerFlag 516 | if len(dsn) > 0 { 517 | target = dsn[0] 518 | } 519 | db, err := sql.Open("trino", target) 520 | if err != nil { 521 | t.Fatal(err) 522 | } 523 | return db 524 | } 525 | 526 | // integration tests based on python tests: 527 | // https://github.com/trinodb/trino-python-client/tree/master/integration_tests 528 | 529 | type nodesRow struct { 530 | NodeID string 531 | HTTPURI string 532 | NodeVersion string 533 | Coordinator bool 534 | State string 535 | } 536 | 537 | func TestIntegrationSelectQueryIterator(t *testing.T) { 538 | db := integrationOpen(t) 539 | defer db.Close() 540 | rows, err := db.Query("SELECT * FROM system.runtime.nodes") 541 | if err != nil { 542 | t.Fatal(err) 543 | } 544 | defer rows.Close() 545 | count := 0 546 | for rows.Next() { 547 | count++ 548 | var col nodesRow 549 | err = rows.Scan( 550 | &col.NodeID, 551 | &col.HTTPURI, 552 | &col.NodeVersion, 553 | &col.Coordinator, 554 | &col.State, 555 | ) 556 | if err != nil { 557 | t.Fatal(err) 558 | } 559 | if col.NodeID != "test" { 560 | t.Errorf("Expected node_id == test but got %s", col.NodeID) 561 | } 562 | } 563 | if err = rows.Err(); err != nil { 564 | t.Fatal(err) 565 | } 566 | if count < 1 { 567 | t.Error("no rows returned") 568 | } 569 | } 570 | 571 | func TestIntegrationSelectQueryNoResult(t *testing.T) { 572 | db := integrationOpen(t) 573 | defer db.Close() 574 | row := db.QueryRow("SELECT * FROM system.runtime.nodes where false") 575 | var col nodesRow 576 | err := row.Scan( 577 | &col.NodeID, 578 | &col.HTTPURI, 579 | &col.NodeVersion, 580 | &col.Coordinator, 581 | &col.State, 582 | ) 583 | if err == nil { 584 | t.Fatalf("unexpected query returning data: %+v", col) 585 | } 586 | } 587 | 588 | func TestIntegrationSelectFailedQuery(t *testing.T) { 589 | db := integrationOpen(t) 590 | defer db.Close() 591 | rows, err := db.Query("SELECT * FROM catalog.schema.do_not_exist") 592 | if err == nil { 593 | rows.Close() 594 | t.Fatal("query to invalid catalog succeeded") 595 | } 596 | queryFailed, ok := err.(*ErrQueryFailed) 597 | if !ok { 598 | t.Fatal("unexpected error:", err) 599 | } 600 | trinoErr, ok := errors.Unwrap(queryFailed).(*ErrTrino) 601 | if !ok { 602 | t.Fatal("unexpected error:", trinoErr) 603 | } 604 | expected := ErrTrino{ 605 | Message: "line 1:15: Catalog 'catalog'", 606 | SqlState: "", 607 | ErrorCode: 44, 608 | ErrorName: "CATALOG_NOT_FOUND", 609 | ErrorType: "USER_ERROR", 610 | ErrorLocation: ErrorLocation{ 611 | LineNumber: 1, 612 | ColumnNumber: 15, 613 | }, 614 | FailureInfo: FailureInfo{ 615 | Type: "io.trino.spi.TrinoException", 616 | Message: "line 1:15: Catalog 'catalog'", 617 | }, 618 | } 619 | if !strings.HasPrefix(trinoErr.Message, expected.Message) { 620 | t.Fatalf("expected ErrTrino.Message to start with `%s`, got: %s", expected.Message, trinoErr.Message) 621 | } 622 | if trinoErr.SqlState != expected.SqlState { 623 | t.Fatalf("expected ErrTrino.SqlState to be `%s`, got: %s", expected.SqlState, trinoErr.SqlState) 624 | } 625 | if trinoErr.ErrorCode != expected.ErrorCode { 626 | t.Fatalf("expected ErrTrino.ErrorCode to be `%d`, got: %d", expected.ErrorCode, trinoErr.ErrorCode) 627 | } 628 | if trinoErr.ErrorName != expected.ErrorName { 629 | t.Fatalf("expected ErrTrino.ErrorName to be `%s`, got: %s", expected.ErrorName, trinoErr.ErrorName) 630 | } 631 | if trinoErr.ErrorType != expected.ErrorType { 632 | t.Fatalf("expected ErrTrino.ErrorType to be `%s`, got: %s", expected.ErrorType, trinoErr.ErrorType) 633 | } 634 | if trinoErr.ErrorLocation.LineNumber != expected.ErrorLocation.LineNumber { 635 | t.Fatalf("expected ErrTrino.ErrorLocation.LineNumber to be `%d`, got: %d", expected.ErrorLocation.LineNumber, trinoErr.ErrorLocation.LineNumber) 636 | } 637 | if trinoErr.ErrorLocation.ColumnNumber != expected.ErrorLocation.ColumnNumber { 638 | t.Fatalf("expected ErrTrino.ErrorLocation.ColumnNumber to be `%d`, got: %d", expected.ErrorLocation.ColumnNumber, trinoErr.ErrorLocation.ColumnNumber) 639 | } 640 | if trinoErr.FailureInfo.Type != expected.FailureInfo.Type { 641 | t.Fatalf("expected ErrTrino.FailureInfo.Type to be `%s`, got: %s", expected.FailureInfo.Type, trinoErr.FailureInfo.Type) 642 | } 643 | if !strings.HasPrefix(trinoErr.FailureInfo.Message, expected.FailureInfo.Message) { 644 | t.Fatalf("expected ErrTrino.FailureInfo.Message to start with `%s`, got: %s", expected.FailureInfo.Message, trinoErr.FailureInfo.Message) 645 | } 646 | } 647 | 648 | type tpchRow struct { 649 | CustKey int 650 | Name string 651 | Address string 652 | NationKey int 653 | Phone string 654 | AcctBal float64 655 | MktSegment string 656 | Comment string 657 | } 658 | 659 | func TestIntegrationSelectTpch1000(t *testing.T) { 660 | db := integrationOpen(t) 661 | defer db.Close() 662 | rows, err := db.Query("SELECT * FROM tpch.sf1.customer LIMIT 1000") 663 | if err != nil { 664 | t.Fatal(err) 665 | } 666 | defer rows.Close() 667 | count := 0 668 | for rows.Next() { 669 | count++ 670 | var col tpchRow 671 | err = rows.Scan( 672 | &col.CustKey, 673 | &col.Name, 674 | &col.Address, 675 | &col.NationKey, 676 | &col.Phone, 677 | &col.AcctBal, 678 | &col.MktSegment, 679 | &col.Comment, 680 | ) 681 | if err != nil { 682 | t.Fatal(err) 683 | } 684 | /* 685 | if col.CustKey == 1 && col.AcctBal != 711.56 { 686 | t.Fatal("unexpected acctbal for custkey=1:", col.AcctBal) 687 | } 688 | */ 689 | } 690 | if rows.Err() != nil { 691 | t.Fatal(err) 692 | } 693 | if count != 1000 { 694 | t.Fatal("not enough rows returned:", count) 695 | } 696 | } 697 | 698 | func TestIntegrationSelectCancelQuery(t *testing.T) { 699 | db := integrationOpen(t) 700 | defer db.Close() 701 | deadline := time.Now().Add(200 * time.Millisecond) 702 | ctx, cancel := context.WithDeadline(context.Background(), deadline) 703 | defer cancel() 704 | rows, err := db.QueryContext(ctx, "SELECT * FROM tpch.sf1.customer") 705 | if err != nil { 706 | goto handleErr 707 | } 708 | defer rows.Close() 709 | for rows.Next() { 710 | var col tpchRow 711 | err = rows.Scan( 712 | &col.CustKey, 713 | &col.Name, 714 | &col.Address, 715 | &col.NationKey, 716 | &col.Phone, 717 | &col.AcctBal, 718 | &col.MktSegment, 719 | &col.Comment, 720 | ) 721 | if err != nil { 722 | break 723 | } 724 | } 725 | if err = rows.Err(); err == nil { 726 | t.Fatal("unexpected query with deadline succeeded") 727 | } 728 | handleErr: 729 | errmsg := err.Error() 730 | for _, msg := range []string{"cancel", "deadline"} { 731 | if strings.Contains(errmsg, msg) { 732 | return 733 | } 734 | } 735 | t.Fatal("unexpected error:", err) 736 | } 737 | 738 | func TestIntegrationSessionProperties(t *testing.T) { 739 | dsn := *integrationServerFlag 740 | dsn += "?session_properties=query_max_run_time%3A10m%3Bquery_priority%3A2" 741 | db := integrationOpen(t, dsn) 742 | defer db.Close() 743 | rows, err := db.Query("SHOW SESSION") 744 | if err != nil { 745 | t.Fatal(err) 746 | } 747 | for rows.Next() { 748 | col := struct { 749 | Name string 750 | Value string 751 | Default string 752 | Type string 753 | Description string 754 | }{} 755 | err = rows.Scan( 756 | &col.Name, 757 | &col.Value, 758 | &col.Default, 759 | &col.Type, 760 | &col.Description, 761 | ) 762 | if err != nil { 763 | t.Fatal(err) 764 | } 765 | switch { 766 | case col.Name == "query_max_run_time" && col.Value != "10m": 767 | t.Fatal("unexpected value for query_max_run_time:", col.Value) 768 | case col.Name == "query_priority" && col.Value != "2": 769 | t.Fatal("unexpected value for query_priority:", col.Value) 770 | } 771 | } 772 | if err = rows.Err(); err != nil { 773 | t.Fatal(err) 774 | } 775 | } 776 | 777 | func TestIntegrationTypeConversion(t *testing.T) { 778 | err := RegisterCustomClient("uncompressed", &http.Client{Transport: &http.Transport{DisableCompression: true}}) 779 | if err != nil { 780 | t.Fatal(err) 781 | } 782 | dsn := *integrationServerFlag 783 | dsn += "?custom_client=uncompressed" 784 | db := integrationOpen(t, dsn) 785 | var ( 786 | goTime time.Time 787 | nullTime NullTime 788 | goBytes []byte 789 | nullBytes []byte 790 | goString string 791 | nullString sql.NullString 792 | nullStringSlice NullSliceString 793 | nullStringSlice2 NullSlice2String 794 | nullStringSlice3 NullSlice3String 795 | nullInt64Slice NullSliceInt64 796 | nullInt64Slice2 NullSlice2Int64 797 | nullInt64Slice3 NullSlice3Int64 798 | nullFloat64Slice NullSliceFloat64 799 | nullFloat64Slice2 NullSlice2Float64 800 | nullFloat64Slice3 NullSlice3Float64 801 | goMap map[string]interface{} 802 | nullMap NullMap 803 | goRow []interface{} 804 | ) 805 | err = db.QueryRow(` 806 | SELECT 807 | TIMESTAMP '2017-07-10 01:02:03.004 UTC', 808 | CAST(NULL AS TIMESTAMP), 809 | CAST(X'FFFF0FFF3FFFFFFF' AS VARBINARY), 810 | CAST(NULL AS VARBINARY), 811 | CAST('string' AS VARCHAR), 812 | CAST(NULL AS VARCHAR), 813 | ARRAY['A', 'B', NULL], 814 | ARRAY[ARRAY['A'], NULL], 815 | ARRAY[ARRAY[ARRAY['A'], NULL], NULL], 816 | ARRAY[1, 2, NULL], 817 | ARRAY[ARRAY[1, 1, 1], NULL], 818 | ARRAY[ARRAY[ARRAY[1, 1, 1], NULL], NULL], 819 | ARRAY[1.0, 2.0, NULL], 820 | ARRAY[ARRAY[1.1, 1.1, 1.1], NULL], 821 | ARRAY[ARRAY[ARRAY[1.1, 1.1, 1.1], NULL], NULL], 822 | MAP(ARRAY['a', 'b'], ARRAY['c', 'd']), 823 | CAST(NULL AS MAP(ARRAY(INTEGER), ARRAY(INTEGER))), 824 | ROW(1, 'a', CAST('2017-07-10 01:02:03.004 UTC' AS TIMESTAMP(6) WITH TIME ZONE), ARRAY['c']) 825 | `).Scan( 826 | &goTime, 827 | &nullTime, 828 | &goBytes, 829 | &nullBytes, 830 | &goString, 831 | &nullString, 832 | &nullStringSlice, 833 | &nullStringSlice2, 834 | &nullStringSlice3, 835 | &nullInt64Slice, 836 | &nullInt64Slice2, 837 | &nullInt64Slice3, 838 | &nullFloat64Slice, 839 | &nullFloat64Slice2, 840 | &nullFloat64Slice3, 841 | &goMap, 842 | &nullMap, 843 | &goRow, 844 | ) 845 | if err != nil { 846 | t.Fatal(err) 847 | } 848 | 849 | // Compare the actual and expected values. 850 | expectedTime := time.Date(2017, 7, 10, 1, 2, 3, 4*1000000, time.UTC) 851 | if !goTime.Equal(expectedTime) { 852 | t.Errorf("expected GoTime to be %v, got %v", expectedTime, goTime) 853 | } 854 | 855 | expectedBytes := []byte{0xff, 0xff, 0x0f, 0xff, 0x3f, 0xff, 0xff, 0xff} 856 | if !bytes.Equal(goBytes, expectedBytes) { 857 | t.Errorf("expected GoBytes to be %v, got %v", expectedBytes, goBytes) 858 | } 859 | 860 | if nullBytes != nil { 861 | t.Errorf("expected NullBytes to be nil, got %v", nullBytes) 862 | } 863 | 864 | if goString != "string" { 865 | t.Errorf("expected GoString to be %q, got %q", "string", goString) 866 | } 867 | 868 | if nullString.Valid { 869 | t.Errorf("expected NullString.Valid to be false, got true") 870 | } 871 | 872 | if !reflect.DeepEqual(nullStringSlice.SliceString, []sql.NullString{{String: "A", Valid: true}, {String: "B", Valid: true}, {Valid: false}}) { 873 | t.Errorf("expected NullStringSlice.SliceString to be %v, got %v", 874 | []sql.NullString{{String: "A", Valid: true}, {String: "B", Valid: true}, {Valid: false}}, 875 | nullStringSlice.SliceString) 876 | } 877 | if !nullStringSlice.Valid { 878 | t.Errorf("expected NullStringSlice.Valid to be true, got false") 879 | } 880 | 881 | expectedSlice2String := [][]sql.NullString{{{String: "A", Valid: true}}, {}} 882 | if !reflect.DeepEqual(nullStringSlice2.Slice2String, expectedSlice2String) { 883 | t.Errorf("expected NullStringSlice2.Slice2String to be %v, got %v", expectedSlice2String, nullStringSlice2.Slice2String) 884 | } 885 | if !nullStringSlice2.Valid { 886 | t.Errorf("expected NullStringSlice2.Valid to be true, got false") 887 | } 888 | 889 | expectedSlice3String := [][][]sql.NullString{{{{String: "A", Valid: true}}, {}}, {}} 890 | if !reflect.DeepEqual(nullStringSlice3.Slice3String, expectedSlice3String) { 891 | t.Errorf("expected NullStringSlice3.Slice3String to be %v, got %v", expectedSlice3String, nullStringSlice3.Slice3String) 892 | } 893 | if !nullStringSlice3.Valid { 894 | t.Errorf("expected NullStringSlice3.Valid to be true, got false") 895 | } 896 | 897 | expectedSliceInt64 := []sql.NullInt64{{Int64: 1, Valid: true}, {Int64: 2, Valid: true}, {Valid: false}} 898 | if !reflect.DeepEqual(nullInt64Slice.SliceInt64, expectedSliceInt64) { 899 | t.Errorf("expected NullInt64Slice.SliceInt64 to be %v, got %v", expectedSliceInt64, nullInt64Slice.SliceInt64) 900 | } 901 | if !nullInt64Slice.Valid { 902 | t.Errorf("expected NullInt64Slice.Valid to be true, got false") 903 | } 904 | 905 | expectedSlice2Int64 := [][]sql.NullInt64{{{Int64: 1, Valid: true}, {Int64: 1, Valid: true}, {Int64: 1, Valid: true}}, {}} 906 | if !reflect.DeepEqual(nullInt64Slice2.Slice2Int64, expectedSlice2Int64) { 907 | t.Errorf("expected NullInt64Slice2.Slice2Int64 to be %v, got %v", expectedSlice2Int64, nullInt64Slice2.Slice2Int64) 908 | } 909 | if !nullInt64Slice2.Valid { 910 | t.Errorf("expected NullInt64Slice2.Valid to be true, got false") 911 | } 912 | 913 | expectedSlice3Int64 := [][][]sql.NullInt64{{{{Int64: 1, Valid: true}, {Int64: 1, Valid: true}, {Int64: 1, Valid: true}}, {}}, {}} 914 | if !reflect.DeepEqual(nullInt64Slice3.Slice3Int64, expectedSlice3Int64) { 915 | t.Errorf("expected NullInt64Slice3.Slice3Int64 to be %v, got %v", expectedSlice3Int64, nullInt64Slice3.Slice3Int64) 916 | } 917 | if !nullInt64Slice3.Valid { 918 | t.Errorf("expected NullInt64Slice3.Valid to be true, got false") 919 | } 920 | 921 | expectedSliceFloat64 := []sql.NullFloat64{{Float64: 1.0, Valid: true}, {Float64: 2.0, Valid: true}, {Valid: false}} 922 | if !reflect.DeepEqual(nullFloat64Slice.SliceFloat64, expectedSliceFloat64) { 923 | t.Errorf("expected NullFloat64Slice.SliceFloat64 to be %v, got %v", expectedSliceFloat64, nullFloat64Slice.SliceFloat64) 924 | } 925 | if !nullFloat64Slice.Valid { 926 | t.Errorf("expected NullFloat64Slice.Valid to be true, got false") 927 | } 928 | 929 | expectedSlice2Float64 := [][]sql.NullFloat64{{{Float64: 1.1, Valid: true}, {Float64: 1.1, Valid: true}, {Float64: 1.1, Valid: true}}, {}} 930 | if !reflect.DeepEqual(nullFloat64Slice2.Slice2Float64, expectedSlice2Float64) { 931 | t.Errorf("expected NullFloat64Slice2.Slice2Float64 to be %v, got %v", expectedSlice2Float64, nullFloat64Slice2.Slice2Float64) 932 | } 933 | if !nullFloat64Slice2.Valid { 934 | t.Errorf("expected NullFloat64Slice2.Valid to be true, got false") 935 | } 936 | 937 | expectedSlice3Float64 := [][][]sql.NullFloat64{{{{Float64: 1.1, Valid: true}, {Float64: 1.1, Valid: true}, {Float64: 1.1, Valid: true}}, {}}, {}} 938 | if !reflect.DeepEqual(nullFloat64Slice3.Slice3Float64, expectedSlice3Float64) { 939 | t.Errorf("expected NullFloat64Slice3.Slice3Float64 to be %v, got %v", expectedSlice3Float64, nullFloat64Slice3.Slice3Float64) 940 | } 941 | if !nullFloat64Slice3.Valid { 942 | t.Errorf("expected NullFloat64Slice3.Valid to be true, got false") 943 | } 944 | 945 | expectedMap := map[string]interface{}{"a": "c", "b": "d"} 946 | if !reflect.DeepEqual(goMap, expectedMap) { 947 | t.Errorf("expected GoMap to be %v, got %v", expectedMap, goMap) 948 | } 949 | 950 | if nullMap.Valid { 951 | t.Errorf("expected NullMap.Valid to be false, got true") 952 | } 953 | 954 | expectedRow := []interface{}{json.Number("1"), "a", "2017-07-10 01:02:03.004000 UTC", []interface{}{"c"}} 955 | if !reflect.DeepEqual(goRow, expectedRow) { 956 | t.Errorf("expected GoRow to be %v, got %v", expectedRow, goRow) 957 | } 958 | } 959 | 960 | func TestComplexTypes(t *testing.T) { 961 | // This test has been created to showcase some issues with parsing 962 | // complex types. It is not intended to be a comprehensive test of 963 | // the parsing logic, but rather to provide a reference for future 964 | // changes to the parsing logic. 965 | // 966 | // The current implementation of the parsing logic reads the value 967 | // in the same format as the JSON response from Trino. This means 968 | // that we don't go further to parse values as their structured types. 969 | // For example, a row like `ROW(1, X'0000')` is read as 970 | // a list of a `json.Number(1)` and a base64-encoded string. 971 | t.Skip("skipping failing test") 972 | 973 | dsn := *integrationServerFlag 974 | db := integrationOpen(t, dsn) 975 | 976 | for _, tt := range []struct { 977 | name string 978 | query string 979 | expected interface{} 980 | }{ 981 | { 982 | name: "row containing scalar values", 983 | query: `SELECT ROW(1, 'a', X'0000')`, 984 | expected: []interface{}{1, "a", []byte{0x00, 0x00}}, 985 | }, 986 | { 987 | name: "nested row", 988 | query: `SELECT ROW(ROW(1, 'a'), ROW(2, 'b'))`, 989 | expected: []interface{}{[]interface{}{1, "a"}, []interface{}{2, "b"}}, 990 | }, 991 | { 992 | name: "map with scalar values", 993 | query: `SELECT MAP(ARRAY['a', 'b'], ARRAY[1, 2])`, 994 | expected: map[string]interface{}{"a": 1, "b": 2}, 995 | }, 996 | { 997 | name: "map with nested row", 998 | query: `SELECT MAP(ARRAY['a', 'b'], ARRAY[ROW(1, 'a'), ROW(2, 'b')])`, 999 | expected: map[string]interface{}{"a": []interface{}{1, "a"}, "b": []interface{}{2, "b"}}, 1000 | }, 1001 | } { 1002 | t.Run(tt.name, func(t *testing.T) { 1003 | var result interface{} 1004 | err := db.QueryRow(tt.query).Scan(&result) 1005 | if err != nil { 1006 | t.Fatal(err) 1007 | } 1008 | 1009 | if !reflect.DeepEqual(result, tt.expected) { 1010 | t.Errorf("expected %v, got %v", tt.expected, result) 1011 | } 1012 | }) 1013 | } 1014 | } 1015 | 1016 | func TestIntegrationArgsConversion(t *testing.T) { 1017 | dsn := *integrationServerFlag 1018 | db := integrationOpen(t, dsn) 1019 | value := 0 1020 | err := db.QueryRow(` 1021 | SELECT 1 FROM (VALUES ( 1022 | CAST(1 AS TINYINT), 1023 | CAST(1 AS SMALLINT), 1024 | CAST(1 AS INTEGER), 1025 | CAST(1 AS BIGINT), 1026 | CAST(1 AS REAL), 1027 | CAST(1 AS DOUBLE), 1028 | TIMESTAMP '2017-07-10 01:02:03.004 UTC', 1029 | CAST('string' AS VARCHAR), 1030 | CAST(X'FFFF0FFF3FFFFFFF' AS VARBINARY), 1031 | ARRAY['A', 'B'] 1032 | )) AS t(col_tiny, col_small, col_int, col_big, col_real, col_double, col_ts, col_varchar, col_varbinary, col_array ) 1033 | WHERE 1=1 1034 | AND col_tiny = ? 1035 | AND col_small = ? 1036 | AND col_int = ? 1037 | AND col_big = ? 1038 | AND col_real = cast(? as real) 1039 | AND col_double = cast(? as double) 1040 | AND col_ts = ? 1041 | AND col_varchar = ? 1042 | AND col_varbinary = ? 1043 | AND col_array = ?`, 1044 | int16(1), 1045 | int16(1), 1046 | int32(1), 1047 | int64(1), 1048 | Numeric("1"), 1049 | Numeric("1"), 1050 | time.Date(2017, 7, 10, 1, 2, 3, 4*1000000, time.UTC), 1051 | "string", 1052 | []byte{0xff, 0xff, 0x0f, 0xff, 0x3f, 0xff, 0xff, 0xff}, 1053 | []string{"A", "B"}, 1054 | ).Scan(&value) 1055 | if err != nil { 1056 | t.Fatal(err) 1057 | } 1058 | } 1059 | 1060 | func TestIntegrationNoResults(t *testing.T) { 1061 | db := integrationOpen(t) 1062 | rows, err := db.Query("SELECT 1 LIMIT 0") 1063 | if err != nil { 1064 | t.Fatal(err) 1065 | } 1066 | for rows.Next() { 1067 | t.Fatal(errors.New("Rows returned")) 1068 | } 1069 | if err = rows.Err(); err != nil { 1070 | t.Fatal(err) 1071 | } 1072 | } 1073 | func TestRoleHeaderSupport(t *testing.T) { 1074 | version, err := strconv.Atoi(*trinoImageTagFlag) 1075 | if (err != nil && *trinoImageTagFlag != "latest") || (err == nil && version < 458) { 1076 | t.Skip("Skipping test when not using Trino 458 or later.") 1077 | } 1078 | tests := []struct { 1079 | name string 1080 | config Config 1081 | rawDSN string 1082 | query string 1083 | expectError bool 1084 | errorSubstr string 1085 | validateRows func(t *testing.T, rows *sql.Rows) 1086 | }{ 1087 | { 1088 | name: "Valid hive admin role via Config", 1089 | config: Config{ 1090 | ServerURI: *integrationServerFlag, 1091 | Roles: map[string]string{"hive": "admin"}, 1092 | }, 1093 | query: "SHOW ROLES FROM hive", 1094 | expectError: false, 1095 | validateRows: func(t *testing.T, rows *sql.Rows) { 1096 | foundAdmin := false 1097 | for rows.Next() { 1098 | var roleName string 1099 | err := rows.Scan(&roleName) 1100 | require.NoError(t, err) 1101 | if roleName == "admin" { 1102 | foundAdmin = true 1103 | } 1104 | } 1105 | require.True(t, foundAdmin, "Expected to find 'admin' role in SHOW ROLES output") 1106 | }, 1107 | }, 1108 | { 1109 | config: Config{ 1110 | ServerURI: *integrationServerFlag, 1111 | Roles: map[string]string{"tpch": "NONE", "memory": "ALL"}, 1112 | }, 1113 | query: "SELECT 1", 1114 | expectError: false, 1115 | }, 1116 | { 1117 | name: "Valid special roles via Config", 1118 | config: Config{ 1119 | ServerURI: *integrationServerFlag, 1120 | Roles: map[string]string{"tpch": "NONE", "memory": "ALL"}, 1121 | }, 1122 | query: "SELECT 1", 1123 | expectError: false, 1124 | }, 1125 | { 1126 | name: "Valid hive admin role via DSN, not encoded url", 1127 | rawDSN: *integrationServerFlag + "?roles=hive:admin", 1128 | query: "SHOW ROLES FROM hive", 1129 | expectError: false, 1130 | validateRows: func(t *testing.T, rows *sql.Rows) { 1131 | foundAdmin := false 1132 | for rows.Next() { 1133 | var roleName string 1134 | err := rows.Scan(&roleName) 1135 | require.NoError(t, err) 1136 | if roleName == "admin" { 1137 | foundAdmin = true 1138 | } 1139 | } 1140 | require.True(t, foundAdmin, "Expected to find 'admin' role in SHOW ROLES output") 1141 | }, 1142 | }, 1143 | { 1144 | name: "Valid roles via DSN, url encoded", 1145 | rawDSN: *integrationServerFlag + "?roles=hive:admin", 1146 | query: "SHOW ROLES FROM hive", 1147 | expectError: false, 1148 | validateRows: func(t *testing.T, rows *sql.Rows) { 1149 | foundAdmin := false 1150 | for rows.Next() { 1151 | var roleName string 1152 | err := rows.Scan(&roleName) 1153 | require.NoError(t, err) 1154 | if roleName == "admin" { 1155 | foundAdmin = true 1156 | } 1157 | } 1158 | require.True(t, foundAdmin, "Expected to find 'admin' role in SHOW ROLES output") 1159 | }, 1160 | }, 1161 | { 1162 | name: "No role - should fail to show roles", 1163 | config: Config{ 1164 | ServerURI: *integrationServerFlag, 1165 | }, 1166 | query: "SHOW ROLES FROM hive", 1167 | expectError: true, 1168 | errorSubstr: "Access Denied", 1169 | }, 1170 | { 1171 | name: "Wrong role - should fail to show roles", 1172 | config: Config{ 1173 | ServerURI: *integrationServerFlag, 1174 | Roles: map[string]string{"hive": "ALL"}, 1175 | }, 1176 | query: "SHOW ROLES FROM hive", 1177 | expectError: true, 1178 | errorSubstr: "Access Denied", 1179 | }, 1180 | { 1181 | name: "Non-existent catalog role", 1182 | config: Config{ 1183 | ServerURI: *integrationServerFlag, 1184 | Roles: map[string]string{"not-exist-catalog": "role1"}, 1185 | }, 1186 | query: "SELECT 1", 1187 | expectError: true, 1188 | errorSubstr: "USER_ERROR: Catalog", 1189 | }, 1190 | } 1191 | 1192 | for _, tt := range tests { 1193 | t.Run(tt.name, func(t *testing.T) { 1194 | var dns string 1195 | var err error 1196 | 1197 | if tt.rawDSN != "" { 1198 | dns = tt.rawDSN 1199 | } else { 1200 | dns, err = tt.config.FormatDSN() 1201 | if err != nil { 1202 | t.Fatal(err) 1203 | } 1204 | } 1205 | 1206 | db := integrationOpen(t, dns) 1207 | defer db.Close() 1208 | 1209 | rows, err := db.Query(tt.query) 1210 | 1211 | if tt.expectError { 1212 | require.Error(t, err) 1213 | if tt.errorSubstr != "" { 1214 | require.Contains(t, err.Error(), tt.errorSubstr) 1215 | } 1216 | } else { 1217 | require.NoError(t, err) 1218 | if tt.validateRows != nil && rows != nil { 1219 | defer rows.Close() 1220 | tt.validateRows(t, rows) 1221 | } 1222 | } 1223 | }) 1224 | } 1225 | } 1226 | 1227 | func TestIntegrationQueryParametersSelect(t *testing.T) { 1228 | scenarios := []struct { 1229 | name string 1230 | query string 1231 | args []interface{} 1232 | expectedError error 1233 | expectedRows int 1234 | }{ 1235 | { 1236 | name: "valid string as varchar", 1237 | query: "SELECT * FROM system.runtime.nodes WHERE system.runtime.nodes.node_id=?", 1238 | args: []interface{}{"test"}, 1239 | expectedRows: 1, 1240 | }, 1241 | { 1242 | name: "valid int as bigint", 1243 | query: "SELECT * FROM tpch.sf1.customer WHERE custkey=? LIMIT 2", 1244 | args: []interface{}{int(1)}, 1245 | expectedRows: 1, 1246 | }, 1247 | { 1248 | name: "invalid string as bigint", 1249 | query: "SELECT * FROM tpch.sf1.customer WHERE custkey=? LIMIT 2", 1250 | args: []interface{}{"1"}, 1251 | expectedError: errors.New(`trino: query failed (200 OK): "USER_ERROR: line 1:46: Cannot apply operator: bigint = varchar(1)"`), 1252 | }, 1253 | { 1254 | name: "valid string as date", 1255 | query: "SELECT * FROM tpch.sf1.lineitem WHERE shipdate=? LIMIT 2", 1256 | args: []interface{}{"1995-01-27"}, 1257 | expectedError: errors.New(`trino: query failed (200 OK): "USER_ERROR: line 1:47: Cannot apply operator: date = varchar(10)"`), 1258 | }, 1259 | } 1260 | 1261 | for i := range scenarios { 1262 | scenario := scenarios[i] 1263 | 1264 | t.Run(scenario.name, func(t *testing.T) { 1265 | db := integrationOpen(t) 1266 | defer db.Close() 1267 | 1268 | rows, err := db.Query(scenario.query, scenario.args...) 1269 | if err != nil { 1270 | if scenario.expectedError == nil { 1271 | t.Errorf("Unexpected err: %s", err) 1272 | return 1273 | } 1274 | if err.Error() == scenario.expectedError.Error() { 1275 | return 1276 | } 1277 | t.Errorf("Expected err to be %s but got %s", scenario.expectedError, err) 1278 | } 1279 | 1280 | if scenario.expectedError != nil { 1281 | t.Error("missing expected error") 1282 | return 1283 | } 1284 | 1285 | defer rows.Close() 1286 | 1287 | var count int 1288 | for rows.Next() { 1289 | count++ 1290 | } 1291 | if err = rows.Err(); err != nil { 1292 | t.Fatal(err) 1293 | } 1294 | if count != scenario.expectedRows { 1295 | t.Errorf("expecting %d rows, got %d", scenario.expectedRows, count) 1296 | } 1297 | }) 1298 | } 1299 | } 1300 | 1301 | func TestIntegrationQueryNextAfterClose(t *testing.T) { 1302 | // NOTE: This is testing invalid behaviour. It ensures that we don't 1303 | // panic if we call driverRows.Next after we closed the driverStmt. 1304 | 1305 | ctx := context.Background() 1306 | conn, err := (&Driver{}).Open(*integrationServerFlag) 1307 | if err != nil { 1308 | t.Fatalf("Failed to open connection: %v", err) 1309 | } 1310 | defer conn.Close() 1311 | 1312 | stmt, err := conn.(driver.ConnPrepareContext).PrepareContext(ctx, "SELECT 1") 1313 | if err != nil { 1314 | t.Fatalf("Failed preparing query: %v", err) 1315 | } 1316 | 1317 | rows, err := stmt.(driver.StmtQueryContext).QueryContext(ctx, []driver.NamedValue{}) 1318 | if err != nil { 1319 | t.Fatalf("Failed running query: %v", err) 1320 | } 1321 | defer rows.Close() 1322 | 1323 | stmt.Close() // NOTE: the important bit. 1324 | 1325 | var result driver.Value 1326 | if err := rows.Next([]driver.Value{result}); err != nil && !spoolingProtocolSupported { 1327 | t.Fatalf("unexpected result: %+v, no error was expected", err) 1328 | } 1329 | if err := rows.Next([]driver.Value{result}); err != io.EOF { 1330 | t.Fatalf("unexpected result: %+v, expected io.EOF", err) 1331 | } 1332 | } 1333 | 1334 | func TestIntegrationExec(t *testing.T) { 1335 | db := integrationOpen(t) 1336 | defer db.Close() 1337 | 1338 | _, err := db.Query(`SELECT count(*) FROM nation`) 1339 | expected := "Schema must be specified when session schema is not set" 1340 | if err == nil || !strings.Contains(err.Error(), expected) { 1341 | t.Fatalf("Expected to fail to execute query with error: %v, got: %v", expected, err) 1342 | } 1343 | 1344 | result, err := db.Exec("USE tpch.sf100") 1345 | if err != nil { 1346 | t.Fatal("Failed executing query:", err.Error()) 1347 | } 1348 | if result == nil { 1349 | t.Fatal("Expected exec result to be not nil") 1350 | } 1351 | 1352 | a, err := result.RowsAffected() 1353 | if err != nil { 1354 | t.Fatal("Expected RowsAffected not to return any error, got:", err) 1355 | } 1356 | if a != 0 { 1357 | t.Fatal("Expected RowsAffected to be zero, got:", a) 1358 | } 1359 | rows, err := db.Query(`SELECT count(*) FROM nation`) 1360 | if err != nil { 1361 | t.Fatal("Failed executing query:", err.Error()) 1362 | } 1363 | if rows == nil || !rows.Next() { 1364 | t.Fatal("Failed fetching results") 1365 | } 1366 | } 1367 | 1368 | func TestIntegrationUnsupportedHeader(t *testing.T) { 1369 | dsn := *integrationServerFlag 1370 | dsn += "?catalog=tpch&schema=sf10" 1371 | db := integrationOpen(t, dsn) 1372 | defer db.Close() 1373 | cases := []struct { 1374 | query string 1375 | err error 1376 | }{ 1377 | { 1378 | query: "SET ROLE dummy", 1379 | err: errors.New(`trino: query failed (200 OK): "USER_ERROR: line 1:1: Role 'dummy' does not exist"`), 1380 | }, 1381 | { 1382 | query: "SET PATH dummy", 1383 | err: errors.New(`trino: query failed (200 OK): "USER_ERROR: SET PATH not supported by client"`), 1384 | }, 1385 | } 1386 | for _, c := range cases { 1387 | _, err := db.Query(c.query) 1388 | if err == nil || err.Error() != c.err.Error() { 1389 | t.Fatal("unexpected error:", err) 1390 | } 1391 | } 1392 | } 1393 | 1394 | func TestSpoolingWorkersHigherThenAllowedOutOfOrderSegments(t *testing.T) { 1395 | if !spoolingProtocolSupported { 1396 | t.Skip("Skipping test when spooling protocol is not supported.") 1397 | } 1398 | db := integrationOpen(t) 1399 | defer db.Close() 1400 | 1401 | expectedError := "spooling worker cannot be greater than max out of order segments allowed. spooling workers: 2, allowed out of order segments: 1" 1402 | _, err := db.Query("SELECT 1", 1403 | sql.Named(trinoEncoding, "json"), 1404 | sql.Named(trinoSpoolingWorkerCount, "2"), 1405 | sql.Named(trinoMaxOutOfOrdersSegments, "1")) 1406 | 1407 | if err == nil || err.Error() != expectedError { 1408 | t.Fatal("unexpected error:", err) 1409 | } 1410 | } 1411 | 1412 | func TestIntegrationQueryContext(t *testing.T) { 1413 | tests := []struct { 1414 | name string 1415 | timeout time.Duration 1416 | expectedErrMsg string 1417 | }{ 1418 | { 1419 | name: "Context Cancellation", 1420 | timeout: 0, 1421 | expectedErrMsg: "canceled", 1422 | }, 1423 | { 1424 | name: "Context Deadline Exceeded", 1425 | timeout: 3 * time.Second, 1426 | expectedErrMsg: "context deadline exceeded", 1427 | }, 1428 | } 1429 | 1430 | if err := RegisterCustomClient("uncompressed", &http.Client{Transport: &http.Transport{DisableCompression: true}}); err != nil { 1431 | t.Fatal(err) 1432 | } 1433 | 1434 | dsn := *integrationServerFlag + "?catalog=tpch&schema=sf100&source=cancel-test&custom_client=uncompressed" 1435 | db := integrationOpen(t, dsn) 1436 | defer db.Close() 1437 | 1438 | for _, tt := range tests { 1439 | t.Run(tt.name, func(t *testing.T) { 1440 | var ctx context.Context 1441 | var cancel context.CancelFunc 1442 | 1443 | if tt.timeout == 0 { 1444 | ctx, cancel = context.WithCancel(context.Background()) 1445 | } else { 1446 | ctx, cancel = context.WithTimeout(context.Background(), tt.timeout) 1447 | } 1448 | defer cancel() 1449 | 1450 | errCh := make(chan error, 1) 1451 | done := make(chan struct{}) 1452 | longQuery := "SELECT COUNT(*) FROM lineitem" 1453 | 1454 | go func() { 1455 | // query will complete in ~7s unless cancelled 1456 | rows, err := db.QueryContext(ctx, longQuery) 1457 | if err != nil { 1458 | errCh <- err 1459 | return 1460 | } 1461 | defer rows.Close() 1462 | 1463 | rows.Next() 1464 | if err = rows.Err(); err != nil { 1465 | errCh <- err 1466 | return 1467 | } 1468 | close(done) 1469 | }() 1470 | 1471 | // Poll system.runtime.queries to get the query ID 1472 | var queryID string 1473 | pollCtx, pollCancel := context.WithTimeout(context.Background(), 1*time.Second) 1474 | defer pollCancel() 1475 | 1476 | for { 1477 | row := db.QueryRowContext(pollCtx, "SELECT query_id FROM system.runtime.queries WHERE state = 'RUNNING' AND source = 'cancel-test' AND query = ?", longQuery) 1478 | err := row.Scan(&queryID) 1479 | if err == nil { 1480 | break 1481 | } 1482 | if err != sql.ErrNoRows { 1483 | t.Fatal("failed to read query ID:", err) 1484 | } 1485 | if err = contextSleep(pollCtx, 100*time.Millisecond); err != nil { 1486 | t.Fatal("query did not start in 1 second") 1487 | } 1488 | } 1489 | 1490 | if tt.timeout == 0 { 1491 | cancel() 1492 | } 1493 | 1494 | // Wait for the query to be canceled or completed 1495 | select { 1496 | case <-done: 1497 | t.Fatal("unexpected query succeeded despite cancellation or deadline") 1498 | case err := <-errCh: 1499 | if !strings.Contains(err.Error(), tt.expectedErrMsg) { 1500 | t.Fatalf("expected error containing %q, but got: %v", tt.expectedErrMsg, err) 1501 | } 1502 | } 1503 | 1504 | // Poll system.runtime.queries to verify the query was canceled 1505 | pollCtx, pollCancel = context.WithTimeout(context.Background(), 2*time.Second) 1506 | defer pollCancel() 1507 | 1508 | for { 1509 | row := db.QueryRowContext(pollCtx, "SELECT state, error_code FROM system.runtime.queries WHERE query_id = ?", queryID) 1510 | var state string 1511 | var code *string 1512 | err := row.Scan(&state, &code) 1513 | if err != nil { 1514 | t.Fatal("failed to read query state:", err) 1515 | } 1516 | if state == "FAILED" && code != nil && *code == "USER_CANCELED" { 1517 | return 1518 | } 1519 | if err = contextSleep(pollCtx, 100*time.Millisecond); err != nil { 1520 | t.Fatalf("query was not canceled in 2 seconds; state: %s, code: %v, err: %v", state, code, err) 1521 | } 1522 | } 1523 | }) 1524 | } 1525 | } 1526 | 1527 | func TestIntegrationAccessToken(t *testing.T) { 1528 | if tlsServer == "" { 1529 | t.Skip("Skipping access token test when using a custom integration server.") 1530 | } 1531 | 1532 | accessToken, err := generateToken() 1533 | if err != nil { 1534 | t.Fatal(err) 1535 | } 1536 | 1537 | dsn := tlsServer + "?accessToken=" + accessToken 1538 | 1539 | db := integrationOpen(t, dsn) 1540 | 1541 | defer db.Close() 1542 | rows, err := db.Query("SHOW CATALOGS") 1543 | if err != nil { 1544 | t.Fatal(err) 1545 | } 1546 | defer rows.Close() 1547 | count := 0 1548 | for rows.Next() { 1549 | count++ 1550 | } 1551 | if count < 1 { 1552 | t.Fatal("not enough rows returned:", count) 1553 | } 1554 | } 1555 | 1556 | func generateToken() (string, error) { 1557 | privateKeyPEM, err := os.ReadFile("etc/secrets/private_key.pem") 1558 | if err != nil { 1559 | return "", fmt.Errorf("error reading private key file: %w", err) 1560 | } 1561 | 1562 | privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEM) 1563 | if err != nil { 1564 | return "", fmt.Errorf("error parsing private key: %w", err) 1565 | } 1566 | 1567 | // Subject must be 'test' 1568 | claims := jwt.RegisteredClaims{ 1569 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * 365 * time.Hour)), 1570 | Issuer: "gotrino", 1571 | Subject: "test", 1572 | } 1573 | 1574 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 1575 | signedToken, err := token.SignedString(privateKey) 1576 | 1577 | if err != nil { 1578 | return "", fmt.Errorf("error generating token: %w", err) 1579 | } 1580 | 1581 | return signedToken, nil 1582 | } 1583 | 1584 | func TestIntegrationTLS(t *testing.T) { 1585 | if tlsServer == "" { 1586 | t.Skip("Skipping TLS test when using a custom integration server.") 1587 | } 1588 | 1589 | dsn := tlsServer 1590 | db := integrationOpen(t, dsn) 1591 | 1592 | defer db.Close() 1593 | row := db.QueryRow("SELECT 1") 1594 | var count int 1595 | if err := row.Scan(&count); err != nil { 1596 | t.Fatal(err) 1597 | } 1598 | if count != 1 { 1599 | t.Fatal("unexpected count=", count) 1600 | } 1601 | } 1602 | 1603 | func contextSleep(ctx context.Context, d time.Duration) error { 1604 | timer := time.NewTimer(100 * time.Millisecond) 1605 | select { 1606 | case <-timer.C: 1607 | return nil 1608 | case <-ctx.Done(): 1609 | if !timer.Stop() { 1610 | <-timer.C 1611 | } 1612 | return ctx.Err() 1613 | } 1614 | } 1615 | 1616 | func TestIntegrationDayToHourIntervalMilliPrecision(t *testing.T) { 1617 | db := integrationOpen(t) 1618 | defer db.Close() 1619 | tests := []struct { 1620 | name string 1621 | arg time.Duration 1622 | wantErr bool 1623 | }{ 1624 | { 1625 | name: "valid 1234567891s", 1626 | arg: time.Duration(1234567891) * time.Second, 1627 | wantErr: false, 1628 | }, 1629 | { 1630 | name: "valid 123456789.1s", 1631 | arg: time.Duration(123456789100) * time.Millisecond, 1632 | wantErr: false, 1633 | }, 1634 | { 1635 | name: "valid 12345678.91s", 1636 | arg: time.Duration(12345678910) * time.Millisecond, 1637 | wantErr: false, 1638 | }, 1639 | { 1640 | name: "valid 1234567.891s", 1641 | arg: time.Duration(1234567891) * time.Millisecond, 1642 | wantErr: false, 1643 | }, 1644 | { 1645 | name: "valid -1234567891s", 1646 | arg: time.Duration(-1234567891) * time.Second, 1647 | wantErr: false, 1648 | }, 1649 | { 1650 | name: "valid -123456789.1s", 1651 | arg: time.Duration(-123456789100) * time.Millisecond, 1652 | wantErr: false, 1653 | }, 1654 | { 1655 | name: "valid -12345678.91s", 1656 | arg: time.Duration(-12345678910) * time.Millisecond, 1657 | wantErr: false, 1658 | }, 1659 | { 1660 | name: "valid -1234567.891s", 1661 | arg: time.Duration(-1234567891) * time.Millisecond, 1662 | wantErr: false, 1663 | }, 1664 | { 1665 | name: "invalid 1234567891.2s", 1666 | arg: time.Duration(1234567891200) * time.Millisecond, 1667 | wantErr: true, 1668 | }, 1669 | { 1670 | name: "invalid 123456789.12s", 1671 | arg: time.Duration(123456789120) * time.Millisecond, 1672 | wantErr: true, 1673 | }, 1674 | { 1675 | name: "invalid 12345678.912s", 1676 | arg: time.Duration(12345678912) * time.Millisecond, 1677 | wantErr: true, 1678 | }, 1679 | { 1680 | name: "invalid -1234567891.2s", 1681 | arg: time.Duration(-1234567891200) * time.Millisecond, 1682 | wantErr: true, 1683 | }, 1684 | { 1685 | name: "invalid -123456789.12s", 1686 | arg: time.Duration(-123456789120) * time.Millisecond, 1687 | wantErr: true, 1688 | }, 1689 | { 1690 | name: "invalid -12345678.912s", 1691 | arg: time.Duration(-12345678912) * time.Millisecond, 1692 | wantErr: true, 1693 | }, 1694 | { 1695 | name: "invalid max seconds (9223372036)", 1696 | arg: time.Duration(math.MaxInt64) / time.Second * time.Second, 1697 | wantErr: true, 1698 | }, 1699 | { 1700 | name: "invalid min seconds (-9223372036)", 1701 | arg: time.Duration(math.MinInt64) / time.Second * time.Second, 1702 | wantErr: true, 1703 | }, 1704 | { 1705 | name: "valid max seconds (2147483647)", 1706 | arg: math.MaxInt32 * time.Second, 1707 | }, 1708 | { 1709 | name: "valid min seconds (-2147483647)", 1710 | arg: -math.MaxInt32 * time.Second, 1711 | }, 1712 | { 1713 | name: "valid max minutes (153722867)", 1714 | arg: time.Duration(math.MaxInt64) / time.Minute * time.Minute, 1715 | }, 1716 | { 1717 | name: "valid min minutes (-153722867)", 1718 | arg: time.Duration(math.MinInt64) / time.Minute * time.Minute, 1719 | }, 1720 | { 1721 | name: "valid max hours (2562047)", 1722 | arg: time.Duration(math.MaxInt64) / time.Hour * time.Hour, 1723 | }, 1724 | { 1725 | name: "valid min hours (-2562047)", 1726 | arg: time.Duration(math.MinInt64) / time.Hour * time.Hour, 1727 | }, 1728 | } 1729 | for _, test := range tests { 1730 | t.Run(test.name, func(t *testing.T) { 1731 | _, err := db.Exec("SELECT ?", test.arg) 1732 | if (err != nil) != test.wantErr { 1733 | t.Errorf("Exec() error = %v, wantErr %v", err, test.wantErr) 1734 | return 1735 | } 1736 | }) 1737 | } 1738 | } 1739 | 1740 | func TestIntegrationLargeQuery(t *testing.T) { 1741 | version, err := strconv.Atoi(*trinoImageTagFlag) 1742 | if (err != nil && *trinoImageTagFlag != "latest") || (err == nil && version < 418) { 1743 | t.Skip("Skipping test when not using Trino 418 or later.") 1744 | } 1745 | dsn := *integrationServerFlag 1746 | dsn += "?explicitPrepare=false" 1747 | db := integrationOpen(t, dsn) 1748 | defer db.Close() 1749 | rows, err := db.Query("SELECT ?, '"+strings.Repeat("a", 5000000)+"'", 42) 1750 | if err != nil { 1751 | t.Fatal(err) 1752 | } 1753 | defer rows.Close() 1754 | count := 0 1755 | for rows.Next() { 1756 | count++ 1757 | } 1758 | if rows.Err() != nil { 1759 | t.Fatal(err) 1760 | } 1761 | if count != 1 { 1762 | t.Fatal("not enough rows returned:", count) 1763 | } 1764 | } 1765 | 1766 | func TestIntegrationTypeConversionSpoolingProtocolInlineJsonEncoder(t *testing.T) { 1767 | err := RegisterCustomClient("uncompressed", &http.Client{Transport: &http.Transport{DisableCompression: true}}) 1768 | if err != nil { 1769 | t.Fatal(err) 1770 | } 1771 | dsn := *integrationServerFlag 1772 | dsn += "?custom_client=uncompressed" 1773 | db := integrationOpen(t, dsn) 1774 | var ( 1775 | goTime time.Time 1776 | nullTime NullTime 1777 | goString string 1778 | nullString sql.NullString 1779 | nullStringSlice NullSliceString 1780 | nullStringSlice2 NullSlice2String 1781 | nullStringSlice3 NullSlice3String 1782 | nullInt64Slice NullSliceInt64 1783 | nullInt64Slice2 NullSlice2Int64 1784 | nullInt64Slice3 NullSlice3Int64 1785 | nullFloat64Slice NullSliceFloat64 1786 | nullFloat64Slice2 NullSlice2Float64 1787 | nullFloat64Slice3 NullSlice3Float64 1788 | goMap map[string]interface{} 1789 | nullMap NullMap 1790 | goRow []interface{} 1791 | ) 1792 | err = db.QueryRow(` 1793 | SELECT 1794 | TIMESTAMP '2017-07-10 01:02:03.004 UTC', 1795 | CAST(NULL AS TIMESTAMP), 1796 | CAST('string' AS VARCHAR), 1797 | CAST(NULL AS VARCHAR), 1798 | ARRAY['A', 'B', NULL], 1799 | ARRAY[ARRAY['A'], NULL], 1800 | ARRAY[ARRAY[ARRAY['A'], NULL], NULL], 1801 | ARRAY[1, 2, NULL], 1802 | ARRAY[ARRAY[1, 1, 1], NULL], 1803 | ARRAY[ARRAY[ARRAY[1, 1, 1], NULL], NULL], 1804 | ARRAY[1.0, 2.0, NULL], 1805 | ARRAY[ARRAY[1.1, 1.1, 1.1], NULL], 1806 | ARRAY[ARRAY[ARRAY[1.1, 1.1, 1.1], NULL], NULL], 1807 | MAP(ARRAY['a', 'b'], ARRAY['c', 'd']), 1808 | CAST(NULL AS MAP(ARRAY(INTEGER), ARRAY(INTEGER))), 1809 | ROW(1, 'a', CAST('2017-07-10 01:02:03.004 UTC' AS TIMESTAMP(6) WITH TIME ZONE), ARRAY['c']) 1810 | `, sql.Named(trinoEncoding, "json")).Scan( 1811 | &goTime, 1812 | &nullTime, 1813 | &goString, 1814 | &nullString, 1815 | &nullStringSlice, 1816 | &nullStringSlice2, 1817 | &nullStringSlice3, 1818 | &nullInt64Slice, 1819 | &nullInt64Slice2, 1820 | &nullInt64Slice3, 1821 | &nullFloat64Slice, 1822 | &nullFloat64Slice2, 1823 | &nullFloat64Slice3, 1824 | &goMap, 1825 | &nullMap, 1826 | &goRow, 1827 | ) 1828 | if err != nil { 1829 | t.Fatal(err) 1830 | } 1831 | } 1832 | 1833 | func TestIntegrationSelectTpchSpoolingSegments(t *testing.T) { 1834 | tests := []struct { 1835 | name string 1836 | query string 1837 | encoding string 1838 | expected int 1839 | }{ 1840 | // Testing with a LIMIT of 1001 rows. 1841 | // Since we exceed the `protocol.spooling.inlining.max-rows` threshold (1000), 1842 | // this query trigger spooling protocol with spooled segments. 1843 | { 1844 | name: "Spooled Segment JSON+ZSTD Encoded", 1845 | query: "SELECT * FROM tpch.sf1.customer LIMIT 1001", 1846 | encoding: "json+zstd", 1847 | expected: 1001, 1848 | }, 1849 | { 1850 | name: "Spooled Segment JSON Encoded", 1851 | query: "SELECT * FROM tpch.sf1.customer LIMIT 1001", 1852 | encoding: "json", 1853 | expected: 1001, 1854 | }, 1855 | { 1856 | name: "Spooled Segment JSON+LZ4 Encoded", 1857 | query: "SELECT * FROM tpch.sf1.customer LIMIT 1001", 1858 | encoding: "json+lz4", 1859 | expected: 1001, 1860 | }, 1861 | // Testing with a LIMIT of 100 rows. 1862 | // This should remain inline as it is below the `protocol.spooling.inlining.max-rows` (1000) and bellow `protocol.spooling.inlining.max-size` 128kb 1863 | { 1864 | name: "Inline Segment JSON+ZSTD Encoded", 1865 | query: "SELECT * FROM tpch.sf1.customer LIMIT 100", 1866 | encoding: "json+zstd", 1867 | expected: 100, 1868 | }, 1869 | { 1870 | name: "Inline Segment JSON+LZ4 Encoded", 1871 | query: "SELECT * FROM tpch.sf1.customer LIMIT 100", 1872 | encoding: "json+lz4", 1873 | expected: 100, 1874 | }, 1875 | } 1876 | 1877 | for _, tt := range tests { 1878 | t.Run(tt.name, func(t *testing.T) { 1879 | db := integrationOpen(t) 1880 | defer db.Close() 1881 | 1882 | rows, err := db.Query(tt.query, sql.Named(trinoEncoding, tt.encoding)) 1883 | if err != nil { 1884 | t.Fatalf("Query failed: %v", err) 1885 | } 1886 | defer rows.Close() 1887 | 1888 | count := 0 1889 | for rows.Next() { 1890 | count++ 1891 | var col tpchRow 1892 | err = rows.Scan( 1893 | &col.CustKey, 1894 | &col.Name, 1895 | &col.Address, 1896 | &col.NationKey, 1897 | &col.Phone, 1898 | &col.AcctBal, 1899 | &col.MktSegment, 1900 | &col.Comment, 1901 | ) 1902 | if err != nil { 1903 | t.Fatalf("Row scan failed: %v", err) 1904 | } 1905 | } 1906 | 1907 | if rows.Err() != nil { 1908 | t.Fatalf("Rows iteration error: %v", rows.Err()) 1909 | } 1910 | 1911 | if count != tt.expected { 1912 | t.Fatalf("Expected %d rows, got %d", tt.expected, count) 1913 | } 1914 | }) 1915 | } 1916 | } 1917 | 1918 | func TestSpoolingIntegrationOrderedResults(t *testing.T) { 1919 | if !spoolingProtocolSupported { 1920 | t.Skip("Skipping test when spooling protocol is not supported.") 1921 | } 1922 | db := integrationOpen(t) 1923 | defer db.Close() 1924 | 1925 | query := ` 1926 | SELECT * 1927 | FROM TABLE(sequence( 1928 | start => 1, 1929 | stop => 5000000 1930 | )) 1931 | ORDER BY sequential_number 1932 | ` 1933 | 1934 | rows, err := db.Query(query, sql.Named(trinoEncoding, "json")) 1935 | if err != nil { 1936 | t.Fatalf("Query failed: %v", err) 1937 | } 1938 | defer rows.Close() 1939 | 1940 | expected := 1 1941 | var actual int 1942 | 1943 | for rows.Next() { 1944 | err = rows.Scan(&actual) 1945 | if err != nil { 1946 | t.Fatalf("Row scan failed: %v", err) 1947 | } 1948 | 1949 | if actual != expected { 1950 | t.Fatalf("Unexpected number at position %d: got %d, expected %d", expected, actual, expected) 1951 | } 1952 | expected++ 1953 | } 1954 | 1955 | if rows.Err() != nil { 1956 | t.Fatalf("Rows iteration error: %v", rows.Err()) 1957 | } 1958 | 1959 | if expected != 5_000_001 { 1960 | t.Fatalf("Expected 5,000,000 rows, got %d", expected-1) 1961 | } 1962 | } 1963 | 1964 | func TestDsnClientTags(t *testing.T) { 1965 | tests := []struct { 1966 | name string 1967 | dsnSuffix string 1968 | source string 1969 | expectedTags []string 1970 | }{ 1971 | { 1972 | name: "Single tag", 1973 | dsnSuffix: "?clientTags=test&source=single-tag-test", 1974 | source: "single-tag-test", 1975 | expectedTags: []string{"test"}, 1976 | }, 1977 | { 1978 | name: "Multiple tags with special characters", 1979 | dsnSuffix: "?clientTags=foo+%2520%2Cbar%3Dtest%2Cbaz%23tag&source=multiple-tags-test-special-characters", 1980 | source: "multiple-tags-test-special-characters", 1981 | expectedTags: []string{"foo %20", "bar=test", "baz#tag"}, 1982 | }, 1983 | } 1984 | 1985 | for _, tt := range tests { 1986 | t.Run(tt.name, func(t *testing.T) { 1987 | dsn := *integrationServerFlag + tt.dsnSuffix 1988 | db := integrationOpen(t, dsn) 1989 | defer db.Close() 1990 | 1991 | query := "SELECT 1" 1992 | rows, err := db.Query(query) 1993 | if err != nil { 1994 | t.Fatal(err) 1995 | } 1996 | defer rows.Close() 1997 | 1998 | if rows.Next() { 1999 | } 2000 | 2001 | if err := rows.Err(); err != nil { 2002 | t.Fatal(err) 2003 | } 2004 | 2005 | var queryID string 2006 | err = db.QueryRowContext(context.Background(), 2007 | "SELECT query_id FROM system.runtime.queries WHERE source = ? AND query = ?", tt.source, query, 2008 | ).Scan(&queryID) 2009 | if err != nil { 2010 | t.Fatal(err) 2011 | } 2012 | 2013 | queryInfo, err := getQueryInfo(dsn, queryID) 2014 | if err != nil { 2015 | t.Fatal(err) 2016 | } 2017 | 2018 | if !reflect.DeepEqual(queryInfo.Session.ClientTags, tt.expectedTags) { 2019 | t.Fatalf("Expected client tags %v, got %v", tt.expectedTags, queryInfo.Session.ClientTags) 2020 | } 2021 | }) 2022 | } 2023 | } 2024 | 2025 | func TestParametersClientTags(t *testing.T) { 2026 | tests := []struct { 2027 | name string 2028 | dsnSuffix string 2029 | Tags string 2030 | source string 2031 | expectedTags []string 2032 | }{ 2033 | { 2034 | name: "Single tag", 2035 | dsnSuffix: "?clientTags=query-parameter-single-tag-test&source=query-parameter-single-tag-test", 2036 | Tags: "single-tag", 2037 | source: "query-parameter-single-tag-test", 2038 | expectedTags: []string{"single-tag"}, 2039 | }, 2040 | { 2041 | name: "Multiple tags with special characters", 2042 | dsnSuffix: "?clientTags=query-parameter-multiple-tags-test&source=query-parameter-multiple-tags-test", 2043 | Tags: "foo %20,bar=test,baz#tag", 2044 | source: "query-parameter-multiple-tags-test", 2045 | expectedTags: []string{"foo %20", "bar=test", "baz#tag"}, 2046 | }, 2047 | { 2048 | name: "Override dsn tags", 2049 | dsnSuffix: "?clientTags=foo%2B%2520%3Bbar%3Dtest%3Bbaz%23tag&source=query-parameter-override-tags", 2050 | Tags: "query-parameter-override-tag-test", 2051 | source: "query-parameter-override-tags", 2052 | expectedTags: []string{"query-parameter-override-tag-test"}, 2053 | }, 2054 | } 2055 | 2056 | for _, tt := range tests { 2057 | t.Run(tt.name, func(t *testing.T) { 2058 | dsn := *integrationServerFlag + tt.dsnSuffix 2059 | db := integrationOpen(t, dsn) 2060 | defer db.Close() 2061 | 2062 | query := "SELECT 1" 2063 | rows, err := db.Query(query, sql.Named(trinoTagsHeader, tt.Tags)) 2064 | if err != nil { 2065 | t.Fatal(err) 2066 | } 2067 | defer rows.Close() 2068 | 2069 | if rows.Next() { 2070 | } 2071 | 2072 | if err := rows.Err(); err != nil { 2073 | t.Fatal(err) 2074 | } 2075 | 2076 | var queryID string 2077 | err = db.QueryRowContext(context.Background(), 2078 | "SELECT query_id FROM system.runtime.queries WHERE source = ? AND query = ?", tt.source, query, 2079 | ).Scan(&queryID) 2080 | if err != nil { 2081 | t.Fatal(err) 2082 | } 2083 | 2084 | queryInfo, err := getQueryInfo(dsn, queryID) 2085 | if err != nil { 2086 | t.Fatal(err) 2087 | } 2088 | 2089 | if !reflect.DeepEqual(queryInfo.Session.ClientTags, tt.expectedTags) { 2090 | t.Fatalf("Expected client tags %v, got %v", tt.expectedTags, queryInfo.Session.ClientTags) 2091 | } 2092 | }) 2093 | } 2094 | } 2095 | 2096 | type QuerySession struct { 2097 | ClientTags []string `json:"clientTags"` 2098 | } 2099 | type QueryInfo struct { 2100 | Session QuerySession `json:"session"` 2101 | } 2102 | 2103 | func getQueryInfo(dsn, queryId string) (QueryInfo, error) { 2104 | 2105 | serverURL, err := url.Parse(dsn) 2106 | if err != nil { 2107 | return QueryInfo{}, err 2108 | } 2109 | queryInfoURL := serverURL.Scheme + "://" + serverURL.Host + "/v1/query/" + url.PathEscape(queryId) 2110 | 2111 | req, err := http.NewRequest("GET", queryInfoURL, nil) 2112 | if err != nil { 2113 | return QueryInfo{}, err 2114 | } 2115 | req.Header.Set("X-Trino-User", serverURL.User.Username()) 2116 | 2117 | resp, err := http.DefaultClient.Do(req) 2118 | if err != nil { 2119 | return QueryInfo{}, err 2120 | } 2121 | 2122 | defer resp.Body.Close() 2123 | 2124 | var queryInfo QueryInfo 2125 | if err := json.NewDecoder(resp.Body).Decode(&queryInfo); err != nil { 2126 | return QueryInfo{}, err 2127 | } 2128 | 2129 | return queryInfo, nil 2130 | } 2131 | --------------------------------------------------------------------------------