├── .github ├── release.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── go.mod ├── go.sum └── trino ├── etc ├── catalog │ ├── memory.properties │ └── tpch.properties ├── config-pre-466version.properties ├── config.properties ├── jvm.config ├── node.properties ├── password-authenticator.properties ├── secrets │ ├── .gitignore │ └── password.db └── spooling-manager.properties ├── integration_test.go ├── serial.go ├── serial_test.go ├── trino.go └── trino_test.go /.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.23', '1.22'] 16 | trino: ['latest', '372'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go }} 22 | - run: go test -v -race -timeout 2m ./... -trino_image_tag=${{ matrix.trino }} 23 | -------------------------------------------------------------------------------- /.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@v4 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@v5 25 | with: 26 | go-version: "1.23" 27 | 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v5 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out 2 | .idea 3 | /dist 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - skip: true 3 | changelog: 4 | use: github-native 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.22 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: comma-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 | ##### `queryTimeout` 243 | 244 | ``` 245 | Type: time.Duration 246 | Valid values: duration string 247 | Default: nil 248 | ``` 249 | 250 | The `queryTimeout` 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 | #### Examples 253 | 254 | ``` 255 | http://user@localhost:8080?source=hello&catalog=default&schema=foobar 256 | ``` 257 | 258 | ``` 259 | https://user@localhost:8443?session_properties=query_max_run_time=10m,query_priority=2 260 | ``` 261 | 262 | ## Data types 263 | 264 | ### Query arguments 265 | 266 | When passing arguments to queries, the driver supports the following Go data 267 | types: 268 | * integers 269 | * `bool` 270 | * `string` 271 | * `[]byte` 272 | * slices 273 | * `trino.Numeric` - a string representation of a number 274 | * `time.Time` - passed to Trino as a timestamp with a time zone 275 | * the result of `trino.Date(year, month, day)` - passed to Trino as a date 276 | * the result of `trino.Time(hour, minute, second, nanosecond)` - passed to 277 | Trino as a time without a time zone 278 | * the result of `trino.TimeTz(hour, minute, second, nanosecond, location)` - 279 | passed to Trino as a time with a time zone 280 | * the result of `trino.Timestamp(year, month, day, hour, minute, second, 281 | nanosecond)` - passed to Trino as a timestamp without a time zone 282 | * `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. 283 | 284 | It's not yet possible to pass: 285 | * `float32` or `float64` 286 | * `byte` 287 | * `json.RawMessage` 288 | * maps 289 | 290 | To use the unsupported types, pass them as strings and use casts in the query, 291 | like so: 292 | ```sql 293 | SELECT * FROM table WHERE col_double = cast(? AS DOUBLE) OR col_timestamp = CAST(? AS TIMESTAMP) 294 | ``` 295 | 296 | ### Response rows 297 | 298 | When reading response rows, the driver supports most Trino data types, except: 299 | * time and timestamps with precision - all time types are returned as 300 | `time.Time`. All precisions up to nanoseconds (`TIMESTAMP(9)` or `TIME(9)`) 301 | are supported (since this is the maximum precision Golang's `time.Time` 302 | supports). If a query returns columns defined with a greater precision, 303 | values are trimmed to 9 decimal digits. Use `CAST` to reduce the returned 304 | precision, or convert the value to a string that then can be parsed manually. 305 | * `DECIMAL` - returned as string 306 | * `IPADDRESS` - returned as string 307 | * `INTERVAL YEAR TO MONTH` and `INTERVAL DAY TO SECOND` - returned as string 308 | * `UUID` - returned as string 309 | 310 | Data types like `HyperLogLog`, `SetDigest`, `QDigest`, and `TDigest` are not 311 | supported and cannot be returned from a query. 312 | 313 | For reading nullable columns, use: 314 | * `trino.NullTime` 315 | * `trino.NullMap` - which stores a map of `map[string]interface{}` 316 | or similar structs from the `database/sql` package, like `sql.NullInt64` 317 | 318 | To read query results containing arrays or maps, pass one of the following 319 | structs to the `Scan()` function: 320 | 321 | * `trino.NullSliceBool` 322 | * `trino.NullSliceString` 323 | * `trino.NullSliceInt64` 324 | * `trino.NullSliceFloat64` 325 | * `trino.NullSliceTime` 326 | * `trino.NullSliceMap` 327 | 328 | For two or three dimensional arrays, use `trino.NullSlice2Bool` and 329 | `trino.NullSlice3Bool` or equivalents for other data types. 330 | 331 | To read `ROW` values, implement the `sql.Scanner` interface in a struct. Its 332 | `Scan()` function receives a `[]interface{}` slice, with values of the 333 | following types: 334 | * `bool` 335 | * `json.Number` for any numeric Trino types 336 | * `[]interface{}` for Trino arrays 337 | * `map[string]interface{}` for Trino maps 338 | * `string` for other Trino types, as character, date, time, or timestamp. 339 | 340 | > [!NOTE] 341 | > `VARBINARY` columns are returned as base64-encoded strings when used within 342 | > `ROW`, `MAP`, or `ARRAY` values. 343 | 344 | ## Spooling Protocol 345 | 346 | 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. 347 | 348 | If the Trino server has the spooling protocol enabled, the client will use it by default with the `json` encoding. 349 | 350 | You can configure other encodings: 351 | 352 | - Supported encodings: `json`, `json+lz4`, `json+zstd` 353 | 354 | ```go 355 | rows, err := db.Query(query, sql.Named("encoding", "json+zstd")) 356 | ``` 357 | 358 | Or specify a list of supported encodings in order of preference: 359 | 360 | ```go 361 | rows, err := db.Query(query, sql.Named("encoding", "json+zstd, json+lz4, json")) 362 | ``` 363 | 364 | ### Configuration Options 365 | 366 | You can tune the spooling protocol using the following parameters, passed as `sql.Named` arguments to your query: 367 | 368 | - **Spooling Worker Count** 369 | `sql.Named("spooling_worker_count", "N")` 370 | Sets the number of parallel workers used to download spooled segments. 371 | **Default:** `5` 372 | **Considerations:** 373 | - Increasing this value can improve throughput for large result sets, especially on high-latency networks. 374 | - Higher values increase parallelism but may also increase memory usage. 375 | 376 | - **Max Out-of-Order Segments** 377 | `sql.Named("max_out_of_order_segments", "N")` 378 | Sets the maximum number of segments that can be downloaded and buffered out-of-order before blocking further downloads. 379 | **Default:** `10` 380 | **Considerations:** 381 | - Higher values increase the potential memory usage, but actual usage depends on download behavior and may be lower in practice. 382 | - Higher values reduce the chance that one slow or stalled segment will block the download of additional segments. 383 | - Lower values reduce memory usage but may limit parallelism and throughput. 384 | 385 | **Note:** 386 | It is **not allowed** to set `spooling_worker_count` higher than `max_out_of_order_segments` — doing so will result in an error. 387 | 388 | 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. 389 | 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. 390 | 391 | #### Example: Customizing Spooling Parameters 392 | 393 | ```go 394 | rows, err := db.Query( 395 | query, 396 | sql.Named("encoding", "json+zstd"), 397 | sql.Named("spooling_worker_count", "8"), 398 | sql.Named("max_out_of_order_segments", "20"), 399 | ) 400 | ``` 401 | 402 | ## License 403 | 404 | Apache License V2.0, as described in the [LICENSE](./LICENSE) file. 405 | 406 | ## Build 407 | 408 | You can build the client code locally and run tests with the following command: 409 | 410 | ``` 411 | go test -v -race -timeout 2m ./... 412 | ``` 413 | 414 | ## Contributing 415 | 416 | For contributing, development, and release guidelines, see 417 | [CONTRIBUTING.md](./CONTRIBUTING.md). 418 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trinodb/trino-go-client 2 | 3 | go 1.22 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26 9 | github.com/aws/aws-sdk-go v1.55.6 10 | github.com/aws/aws-sdk-go-v2/config v1.29.12 11 | github.com/aws/aws-sdk-go-v2/credentials v1.17.65 12 | github.com/aws/aws-sdk-go-v2/service/s3 v1.79.0 13 | github.com/aws/smithy-go v1.22.2 14 | github.com/golang-jwt/jwt/v5 v5.2.1 15 | github.com/jcmturner/gokrb5/v8 v8.4.4 16 | github.com/klauspost/compress v1.18.0 17 | github.com/ory/dockertest/v3 v3.11.0 18 | github.com/pierrec/lz4 v2.6.1+incompatible 19 | github.com/stretchr/testify v1.9.0 20 | ) 21 | 22 | require ( 23 | dario.cat/mergo v1.0.0 // indirect 24 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 25 | github.com/Microsoft/go-winio v0.6.2 // indirect 26 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 27 | github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26 // indirect 28 | github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect 29 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect 30 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 31 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 32 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 33 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 34 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect 35 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 36 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect 37 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 38 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect 40 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect 41 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect 42 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 43 | github.com/containerd/continuity v0.4.3 // indirect 44 | github.com/davecgh/go-spew v1.1.1 // indirect 45 | github.com/docker/cli v26.1.4+incompatible // indirect 46 | github.com/docker/docker v27.1.1+incompatible // indirect 47 | github.com/docker/go-connections v0.5.0 // indirect 48 | github.com/docker/go-units v0.5.0 // indirect 49 | github.com/frankban/quicktest v1.14.6 // indirect 50 | github.com/gogo/protobuf v1.3.2 // indirect 51 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 52 | github.com/hashicorp/go-uuid v1.0.3 // indirect 53 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 54 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 55 | github.com/jcmturner/gofork v1.7.6 // indirect 56 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 57 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 58 | github.com/jmespath/go-jmespath v0.4.0 // indirect 59 | github.com/mitchellh/mapstructure v1.5.0 // indirect 60 | github.com/moby/docker-image-spec v1.3.1 // indirect 61 | github.com/moby/term v0.5.0 // indirect 62 | github.com/opencontainers/go-digest v1.0.0 // indirect 63 | github.com/opencontainers/image-spec v1.1.0 // indirect 64 | github.com/opencontainers/runc v1.1.13 // indirect 65 | github.com/pkg/errors v0.9.1 // indirect 66 | github.com/pmezard/go-difflib v1.0.0 // indirect 67 | github.com/sirupsen/logrus v1.9.3 // indirect 68 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 69 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 70 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 71 | golang.org/x/crypto v0.27.0 // indirect 72 | golang.org/x/net v0.24.0 // indirect 73 | golang.org/x/sys v0.25.0 // indirect 74 | gopkg.in/yaml.v2 v2.4.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 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-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 6 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/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.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= 16 | github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 17 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 18 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 19 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= 20 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= 21 | github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo= 22 | github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI= 23 | github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM= 24 | github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ= 25 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 26 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 27 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 28 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 29 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 30 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 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.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= 34 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= 35 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 36 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 37 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= 38 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= 39 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 40 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 41 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= 42 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= 43 | github.com/aws/aws-sdk-go-v2/service/s3 v1.79.0 h1:OIw2nryEApESTYI5deCZGcq4Gvz8DBAt4tJlNyg3v5o= 44 | github.com/aws/aws-sdk-go-v2/service/s3 v1.79.0/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= 45 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E= 46 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 47 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo= 48 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 49 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= 50 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 51 | github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= 52 | github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 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.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= 56 | github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= 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 v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= 64 | github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 65 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= 66 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 67 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 68 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 76 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 77 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 78 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 79 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 80 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 81 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 82 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 83 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 84 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 85 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 86 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 87 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 88 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 89 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 90 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 91 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 92 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 93 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 94 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 95 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 96 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 97 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 98 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 99 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 100 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 101 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 102 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 103 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 104 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 105 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 106 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 107 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 108 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 109 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 110 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 111 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 112 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 113 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 114 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 115 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 116 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 117 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 118 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 119 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 120 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 121 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 122 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 123 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 124 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 125 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 126 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 127 | github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= 128 | github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= 129 | github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= 130 | github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= 131 | github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= 132 | github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 133 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 134 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 135 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 136 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 137 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 138 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 139 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 140 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 141 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 142 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 143 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 144 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 145 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 146 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 147 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 148 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 149 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 150 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 151 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 152 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 153 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 154 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 155 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 156 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 157 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 158 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 159 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 160 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 161 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 162 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 163 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 164 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 165 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 166 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 167 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 168 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= 169 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 170 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 171 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 172 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 173 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 174 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 176 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 177 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 178 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 179 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 180 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 181 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 182 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 183 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 184 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 185 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 186 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 187 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 188 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 189 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 190 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 191 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 192 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 193 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 194 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 195 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 196 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 197 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 198 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 199 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 200 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 201 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 202 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 203 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 204 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 205 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 206 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 207 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 208 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 209 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 210 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 211 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 212 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 213 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 214 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 215 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 216 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 217 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 218 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 219 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 220 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 221 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 222 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 223 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 224 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 225 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 226 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 227 | -------------------------------------------------------------------------------- /trino/etc/catalog/memory.properties: -------------------------------------------------------------------------------- 1 | connector.name=memory 2 | -------------------------------------------------------------------------------- /trino/etc/catalog/tpch.properties: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | connector.name=tpch 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /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/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/password-authenticator.properties: -------------------------------------------------------------------------------- 1 | password-authenticator.name=file 2 | file.password-file=/etc/trino/secrets/password.db 3 | -------------------------------------------------------------------------------- /trino/etc/secrets/.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | -------------------------------------------------------------------------------- /trino/etc/secrets/password.db: -------------------------------------------------------------------------------- 1 | admin:$2y$10$xLVM/FUuZaNEmGOU3y7fjOLzRJOGItnTWTiFVKwIFX.ZMBctxl8gq 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "os" 38 | "reflect" 39 | "strconv" 40 | "strings" 41 | "testing" 42 | "time" 43 | 44 | "github.com/ahmetb/dlog" 45 | "github.com/aws/aws-sdk-go-v2/config" 46 | "github.com/aws/aws-sdk-go-v2/credentials" 47 | "github.com/aws/aws-sdk-go-v2/service/s3" 48 | "github.com/aws/aws-sdk-go/aws" 49 | "github.com/golang-jwt/jwt/v5" 50 | dt "github.com/ory/dockertest/v3" 51 | docker "github.com/ory/dockertest/v3/docker" 52 | ) 53 | 54 | const ( 55 | DockerLocalStackName = "localstack" 56 | bucketName = "spooling" 57 | DockerTrinoName = "trino-go-client-tests" 58 | MAXRetries = 10 59 | TrinoNetwork = "trino-network" 60 | ) 61 | 62 | var ( 63 | pool *dt.Pool 64 | trinoResource *dt.Resource 65 | localStackResource *dt.Resource 66 | spoolingProtocolSupported bool 67 | 68 | trinoImageTagFlag = flag.String( 69 | "trino_image_tag", 70 | os.Getenv("TRINO_IMAGE_TAG"), 71 | "Docker image tag used for the Trino server container", 72 | ) 73 | integrationServerFlag = flag.String( 74 | "trino_server_dsn", 75 | os.Getenv("TRINO_SERVER_DSN"), 76 | "dsn of the Trino server used for integration tests instead of starting a Docker container", 77 | ) 78 | integrationServerQueryTimeout = flag.Duration( 79 | "trino_query_timeout", 80 | 5*time.Second, 81 | "max duration for Trino queries to run before giving up", 82 | ) 83 | noCleanup = flag.Bool( 84 | "no_cleanup", 85 | false, 86 | "do not delete containers on exit", 87 | ) 88 | tlsServer = "" 89 | ) 90 | 91 | func TestMain(m *testing.M) { 92 | flag.Parse() 93 | DefaultQueryTimeout = *integrationServerQueryTimeout 94 | DefaultCancelQueryTimeout = *integrationServerQueryTimeout 95 | if *trinoImageTagFlag == "" { 96 | *trinoImageTagFlag = "latest" 97 | } 98 | 99 | if *trinoImageTagFlag == "latest" { 100 | spoolingProtocolSupported = true 101 | } else { 102 | version, err := strconv.Atoi(*trinoImageTagFlag) 103 | if err != nil { 104 | log.Fatalf("Invalid trino_image_tag: %s", *trinoImageTagFlag) 105 | } 106 | spoolingProtocolSupported = version >= 466 107 | } 108 | 109 | var err error 110 | if *integrationServerFlag == "" && !testing.Short() { 111 | pool, err = dt.NewPool("") 112 | if err != nil { 113 | log.Fatalf("Could not connect to docker: %s", err) 114 | } 115 | pool.MaxWait = 1 * time.Minute 116 | 117 | networkID := getOrCreateNetwork(pool) 118 | 119 | wd, err := os.Getwd() 120 | if err != nil { 121 | log.Fatalf("Failed to get working directory: %s", err) 122 | } 123 | 124 | var ok bool 125 | if spoolingProtocolSupported { 126 | localStackResource = getOrCreateLocalStack(pool, networkID) 127 | } 128 | 129 | trinoResource, ok = pool.ContainerByName(DockerTrinoName) 130 | 131 | if !ok { 132 | err = generateCerts(wd + "/etc/secrets") 133 | if err != nil { 134 | log.Fatalf("Could not generate TLS certificates: %s", err) 135 | } 136 | 137 | mounts := []string{ 138 | wd + "/etc/catalog:/etc/trino/catalog", 139 | wd + "/etc/secrets:/etc/trino/secrets", 140 | wd + "/etc/jvm.config:/etc/trino/jvm.config", 141 | wd + "/etc/node.properties:/etc/trino/node.properties", 142 | wd + "/etc/password-authenticator.properties:/etc/trino/password-authenticator.properties", 143 | } 144 | 145 | if spoolingProtocolSupported { 146 | mounts = append(mounts, wd+"/etc/config.properties:/etc/trino/config.properties") 147 | mounts = append(mounts, wd+"/etc/spooling-manager.properties:/etc/trino/spooling-manager.properties") 148 | } else { 149 | mounts = append(mounts, wd+"/etc/config-pre-466version.properties:/etc/trino/config.properties") 150 | } 151 | trinoResource, err = pool.RunWithOptions(&dt.RunOptions{ 152 | Name: DockerTrinoName, 153 | Repository: "trinodb/trino", 154 | Tag: *trinoImageTagFlag, 155 | Mounts: mounts, 156 | ExposedPorts: []string{ 157 | "8080/tcp", 158 | "8443/tcp", 159 | }, 160 | NetworkID: networkID, 161 | }, func(hc *docker.HostConfig) { 162 | hc.Ulimits = []docker.ULimit{ 163 | { 164 | Name: "nofile", 165 | Hard: 4096, 166 | Soft: 4096, 167 | }, 168 | } 169 | }) 170 | if err != nil { 171 | log.Fatalf("Could not start resource: %s", err) 172 | } 173 | } else if !trinoResource.Container.State.Running { 174 | pool.Client.StartContainer(trinoResource.Container.ID, nil) 175 | } 176 | 177 | waitForContainerHealth(trinoResource.Container.ID, "trino") 178 | 179 | *integrationServerFlag = "http://test@localhost:" + trinoResource.GetPort("8080/tcp") 180 | tlsServer = "https://admin:admin@localhost:" + trinoResource.GetPort("8443/tcp") 181 | 182 | http.DefaultTransport.(*http.Transport).TLSClientConfig, err = getTLSConfig(wd + "/etc/secrets") 183 | if err != nil { 184 | log.Fatalf("Failed to set the default TLS config: %s", err) 185 | } 186 | } 187 | 188 | code := m.Run() 189 | 190 | if !*noCleanup && pool != nil { 191 | if trinoResource != nil { 192 | if err := pool.Purge(trinoResource); err != nil { 193 | log.Fatalf("Could not purge resource: %s", err) 194 | } 195 | } 196 | 197 | if localStackResource != nil { 198 | if err := pool.Purge(localStackResource); err != nil { 199 | log.Fatalf("Could not purge LocalStack resource: %s", err) 200 | } 201 | } 202 | 203 | networkExists, networkID, err := networkExists(pool, TrinoNetwork) 204 | if err == nil && networkExists { 205 | if err := pool.Client.RemoveNetwork(networkID); err != nil { 206 | log.Fatalf("Could not remove Docker network: %s", err) 207 | } 208 | } 209 | } 210 | 211 | os.Exit(code) 212 | } 213 | 214 | func getOrCreateLocalStack(pool *dt.Pool, networkID string) *dt.Resource { 215 | resource, ok := pool.ContainerByName(DockerLocalStackName) 216 | if ok { 217 | return resource 218 | } 219 | 220 | newResource, err := setupLocalStack(pool, networkID) 221 | if err != nil { 222 | log.Fatalf("Failed to start LocalStack: %s", err) 223 | } 224 | 225 | return newResource 226 | } 227 | 228 | func getOrCreateNetwork(pool *dt.Pool) string { 229 | networkExists, networkID, err := networkExists(pool, TrinoNetwork) 230 | if err != nil { 231 | log.Fatalf("Could not check if Docker network exists: %s", err) 232 | } 233 | 234 | if networkExists { 235 | return networkID 236 | } 237 | 238 | network, err := pool.Client.CreateNetwork(docker.CreateNetworkOptions{ 239 | Name: TrinoNetwork, 240 | }) 241 | if err != nil { 242 | log.Fatalf("Could not create Docker network: %s", err) 243 | } 244 | 245 | return network.ID 246 | } 247 | 248 | func networkExists(pool *dt.Pool, networkName string) (bool, string, error) { 249 | networks, err := pool.Client.ListNetworks() 250 | if err != nil { 251 | return false, "", fmt.Errorf("could not list Docker networks: %w", err) 252 | } 253 | for _, network := range networks { 254 | if network.Name == networkName { 255 | return true, network.ID, nil 256 | } 257 | } 258 | return false, "", nil 259 | } 260 | 261 | func setupLocalStack(pool *dt.Pool, networkID string) (*dt.Resource, error) { 262 | localstackResource, err := pool.RunWithOptions(&dt.RunOptions{ 263 | Name: DockerLocalStackName, 264 | Repository: "localstack/localstack", 265 | Tag: "latest", 266 | Env: []string{ 267 | "SERVICES=s3", 268 | "region_name=us-east-1", 269 | "AWS_ACCESS_KEY_ID=test", 270 | "AWS_SECRET_ACCESS_KEY=test", 271 | }, 272 | 273 | PortBindings: map[docker.Port][]docker.PortBinding{ 274 | "4566/tcp": {{HostIP: "0.0.0.0", HostPort: "4566"}}, 275 | "4571/tcp": {{HostIP: "0.0.0.0", HostPort: "4571"}}, 276 | }, 277 | 278 | NetworkID: networkID, 279 | }) 280 | if err != nil { 281 | return nil, fmt.Errorf("could not start LocalStack: %w", err) 282 | } 283 | 284 | localstackPort := localstackResource.GetPort("4566/tcp") 285 | s3Endpoint := "http://localhost:" + localstackPort 286 | 287 | log.Println("LocalStack started at:", s3Endpoint) 288 | 289 | waitForContainerHealth(localstackResource.Container.ID, "localstack") 290 | 291 | for retry := 0; retry < MAXRetries; retry++ { 292 | err := createS3Bucket(s3Endpoint, "test", "test", bucketName) 293 | if err == nil { 294 | log.Println("S3 bucket created successfully") 295 | return localstackResource, nil 296 | } 297 | log.Printf("Failed to create S3 bucket, retrying... (%d/%d)\n", retry+1, MAXRetries) 298 | time.Sleep(2 * time.Second) 299 | } 300 | 301 | return nil, fmt.Errorf("failed to create S3 bucket after multiple attempts: %w", err) 302 | } 303 | 304 | func createS3Bucket(endpoint, accessKey, secretKey, bucketName string) error { 305 | cfg, err := config.LoadDefaultConfig(context.TODO(), 306 | config.WithRegion("us-east-1"), 307 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")), 308 | ) 309 | if err != nil { 310 | return fmt.Errorf("failed to load AWS config: %w", err) 311 | } 312 | 313 | s3Client := s3.New(s3.Options{ 314 | Credentials: cfg.Credentials, 315 | Region: "us-east-1", 316 | BaseEndpoint: &endpoint, 317 | UsePathStyle: *aws.Bool(true), 318 | }) 319 | 320 | createBucketInput := &s3.CreateBucketInput{ 321 | Bucket: aws.String(bucketName), 322 | } 323 | 324 | _, err = s3Client.CreateBucket(context.TODO(), createBucketInput) 325 | if err != nil { 326 | return fmt.Errorf("failed to create S3 bucket: %w", err) 327 | } 328 | 329 | log.Printf("Bucket %s created successfully!", bucketName) 330 | return nil 331 | } 332 | 333 | func waitForContainerHealth(containerID, containerName string) { 334 | if err := pool.Retry(func() error { 335 | c, err := pool.Client.InspectContainer(containerID) 336 | if err != nil { 337 | log.Fatalf("Failed to inspect container %s: %s", containerID, err) 338 | } 339 | if !c.State.Running { 340 | log.Fatalf("Container %s is not running: %s\nContainer logs:\n%s", containerID, c.State.String(), getLogs(trinoResource.Container.ID)) 341 | } 342 | log.Printf("Waiting for %s container: %s\n", containerName, c.State.String()) 343 | if c.State.Health.Status != "healthy" { 344 | return errors.New("Not ready") 345 | } 346 | return nil 347 | }); err != nil { 348 | log.Fatalf("Timed out waiting for container %s to get ready: %s\nContainer logs:\n%s", containerName, err, getLogs(containerID)) 349 | } 350 | } 351 | 352 | func generateCerts(dir string) error { 353 | priv, err := rsa.GenerateKey(rand.Reader, 2048) 354 | if err != nil { 355 | return fmt.Errorf("failed to generate private key: %w", err) 356 | } 357 | 358 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 359 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 360 | if err != nil { 361 | return fmt.Errorf("failed to generate serial number: %w", err) 362 | } 363 | 364 | template := x509.Certificate{ 365 | SerialNumber: serialNumber, 366 | Subject: pkix.Name{ 367 | Organization: []string{"Trino Software Foundation"}, 368 | }, 369 | DNSNames: []string{"localhost"}, 370 | NotBefore: time.Now(), 371 | NotAfter: time.Now().Add(1 * time.Hour), 372 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 373 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 374 | BasicConstraintsValid: true, 375 | } 376 | 377 | privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 378 | if err != nil { 379 | return fmt.Errorf("unable to marshal private key: %w", err) 380 | } 381 | privBlock := &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes} 382 | err = writePEM(dir+"/private_key.pem", privBlock) 383 | if err != nil { 384 | return err 385 | } 386 | 387 | pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) 388 | if err != nil { 389 | return fmt.Errorf("unable to marshal public key: %w", err) 390 | } 391 | pubBlock := &pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes} 392 | err = writePEM(dir+"/public_key.pem", pubBlock) 393 | if err != nil { 394 | return err 395 | } 396 | 397 | certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) 398 | if err != nil { 399 | return fmt.Errorf("failed to create certificate: %w", err) 400 | } 401 | certBlock := &pem.Block{Type: "CERTIFICATE", Bytes: certBytes} 402 | err = writePEM(dir+"/certificate.pem", certBlock) 403 | if err != nil { 404 | return err 405 | } 406 | 407 | err = writePEM(dir+"/certificate_with_key.pem", certBlock, privBlock, pubBlock) 408 | if err != nil { 409 | return err 410 | } 411 | 412 | return nil 413 | } 414 | 415 | func writePEM(filename string, blocks ...*pem.Block) error { 416 | // all files are world-readable, so they can be read inside the Trino container 417 | out, err := os.Create(filename) 418 | if err != nil { 419 | return fmt.Errorf("failed to open %s for writing: %w", filename, err) 420 | } 421 | for _, block := range blocks { 422 | if err := pem.Encode(out, block); err != nil { 423 | return fmt.Errorf("failed to write %s data to %s: %w", block.Type, filename, err) 424 | } 425 | } 426 | if err := out.Close(); err != nil { 427 | return fmt.Errorf("error closing %s: %w", filename, err) 428 | } 429 | return nil 430 | } 431 | 432 | func getTLSConfig(dir string) (*tls.Config, error) { 433 | certPool, err := x509.SystemCertPool() 434 | if err != nil { 435 | return nil, fmt.Errorf("failed to read the system cert pool: %s", err) 436 | } 437 | caCertPEM, err := os.ReadFile(dir + "/certificate.pem") 438 | if err != nil { 439 | return nil, fmt.Errorf("failed to read the certificate: %s", err) 440 | } 441 | ok := certPool.AppendCertsFromPEM(caCertPEM) 442 | if !ok { 443 | return nil, fmt.Errorf("failed to parse the certificate: %s", err) 444 | } 445 | return &tls.Config{ 446 | RootCAs: certPool, 447 | }, nil 448 | } 449 | 450 | func getLogs(id string) []byte { 451 | var buf bytes.Buffer 452 | pool.Client.Logs(docker.LogsOptions{ 453 | Container: id, 454 | OutputStream: &buf, 455 | ErrorStream: &buf, 456 | Stdout: true, 457 | Stderr: true, 458 | RawTerminal: true, 459 | }) 460 | logs, _ := io.ReadAll(dlog.NewReader(&buf)) 461 | return logs 462 | } 463 | 464 | // integrationOpen opens a connection to the integration test server. 465 | func integrationOpen(t *testing.T, dsn ...string) *sql.DB { 466 | if testing.Short() { 467 | t.Skip("Skipping test in short mode.") 468 | } 469 | target := *integrationServerFlag 470 | if len(dsn) > 0 { 471 | target = dsn[0] 472 | } 473 | db, err := sql.Open("trino", target) 474 | if err != nil { 475 | t.Fatal(err) 476 | } 477 | return db 478 | } 479 | 480 | // integration tests based on python tests: 481 | // https://github.com/trinodb/trino-python-client/tree/master/integration_tests 482 | 483 | type nodesRow struct { 484 | NodeID string 485 | HTTPURI string 486 | NodeVersion string 487 | Coordinator bool 488 | State string 489 | } 490 | 491 | func TestIntegrationSelectQueryIterator(t *testing.T) { 492 | db := integrationOpen(t) 493 | defer db.Close() 494 | rows, err := db.Query("SELECT * FROM system.runtime.nodes") 495 | if err != nil { 496 | t.Fatal(err) 497 | } 498 | defer rows.Close() 499 | count := 0 500 | for rows.Next() { 501 | count++ 502 | var col nodesRow 503 | err = rows.Scan( 504 | &col.NodeID, 505 | &col.HTTPURI, 506 | &col.NodeVersion, 507 | &col.Coordinator, 508 | &col.State, 509 | ) 510 | if err != nil { 511 | t.Fatal(err) 512 | } 513 | if col.NodeID != "test" { 514 | t.Errorf("Expected node_id == test but got %s", col.NodeID) 515 | } 516 | } 517 | if err = rows.Err(); err != nil { 518 | t.Fatal(err) 519 | } 520 | if count < 1 { 521 | t.Error("no rows returned") 522 | } 523 | } 524 | 525 | func TestIntegrationSelectQueryNoResult(t *testing.T) { 526 | db := integrationOpen(t) 527 | defer db.Close() 528 | row := db.QueryRow("SELECT * FROM system.runtime.nodes where false") 529 | var col nodesRow 530 | err := row.Scan( 531 | &col.NodeID, 532 | &col.HTTPURI, 533 | &col.NodeVersion, 534 | &col.Coordinator, 535 | &col.State, 536 | ) 537 | if err == nil { 538 | t.Fatalf("unexpected query returning data: %+v", col) 539 | } 540 | } 541 | 542 | func TestIntegrationSelectFailedQuery(t *testing.T) { 543 | db := integrationOpen(t) 544 | defer db.Close() 545 | rows, err := db.Query("SELECT * FROM catalog.schema.do_not_exist") 546 | if err == nil { 547 | rows.Close() 548 | t.Fatal("query to invalid catalog succeeded") 549 | } 550 | queryFailed, ok := err.(*ErrQueryFailed) 551 | if !ok { 552 | t.Fatal("unexpected error:", err) 553 | } 554 | trinoErr, ok := errors.Unwrap(queryFailed).(*ErrTrino) 555 | if !ok { 556 | t.Fatal("unexpected error:", trinoErr) 557 | } 558 | expected := ErrTrino{ 559 | Message: "line 1:15: Catalog 'catalog'", 560 | SqlState: "", 561 | ErrorCode: 44, 562 | ErrorName: "CATALOG_NOT_FOUND", 563 | ErrorType: "USER_ERROR", 564 | ErrorLocation: ErrorLocation{ 565 | LineNumber: 1, 566 | ColumnNumber: 15, 567 | }, 568 | FailureInfo: FailureInfo{ 569 | Type: "io.trino.spi.TrinoException", 570 | Message: "line 1:15: Catalog 'catalog'", 571 | }, 572 | } 573 | if !strings.HasPrefix(trinoErr.Message, expected.Message) { 574 | t.Fatalf("expected ErrTrino.Message to start with `%s`, got: %s", expected.Message, trinoErr.Message) 575 | } 576 | if trinoErr.SqlState != expected.SqlState { 577 | t.Fatalf("expected ErrTrino.SqlState to be `%s`, got: %s", expected.SqlState, trinoErr.SqlState) 578 | } 579 | if trinoErr.ErrorCode != expected.ErrorCode { 580 | t.Fatalf("expected ErrTrino.ErrorCode to be `%d`, got: %d", expected.ErrorCode, trinoErr.ErrorCode) 581 | } 582 | if trinoErr.ErrorName != expected.ErrorName { 583 | t.Fatalf("expected ErrTrino.ErrorName to be `%s`, got: %s", expected.ErrorName, trinoErr.ErrorName) 584 | } 585 | if trinoErr.ErrorType != expected.ErrorType { 586 | t.Fatalf("expected ErrTrino.ErrorType to be `%s`, got: %s", expected.ErrorType, trinoErr.ErrorType) 587 | } 588 | if trinoErr.ErrorLocation.LineNumber != expected.ErrorLocation.LineNumber { 589 | t.Fatalf("expected ErrTrino.ErrorLocation.LineNumber to be `%d`, got: %d", expected.ErrorLocation.LineNumber, trinoErr.ErrorLocation.LineNumber) 590 | } 591 | if trinoErr.ErrorLocation.ColumnNumber != expected.ErrorLocation.ColumnNumber { 592 | t.Fatalf("expected ErrTrino.ErrorLocation.ColumnNumber to be `%d`, got: %d", expected.ErrorLocation.ColumnNumber, trinoErr.ErrorLocation.ColumnNumber) 593 | } 594 | if trinoErr.FailureInfo.Type != expected.FailureInfo.Type { 595 | t.Fatalf("expected ErrTrino.FailureInfo.Type to be `%s`, got: %s", expected.FailureInfo.Type, trinoErr.FailureInfo.Type) 596 | } 597 | if !strings.HasPrefix(trinoErr.FailureInfo.Message, expected.FailureInfo.Message) { 598 | t.Fatalf("expected ErrTrino.FailureInfo.Message to start with `%s`, got: %s", expected.FailureInfo.Message, trinoErr.FailureInfo.Message) 599 | } 600 | } 601 | 602 | type tpchRow struct { 603 | CustKey int 604 | Name string 605 | Address string 606 | NationKey int 607 | Phone string 608 | AcctBal float64 609 | MktSegment string 610 | Comment string 611 | } 612 | 613 | func TestIntegrationSelectTpch1000(t *testing.T) { 614 | db := integrationOpen(t) 615 | defer db.Close() 616 | rows, err := db.Query("SELECT * FROM tpch.sf1.customer LIMIT 1000") 617 | if err != nil { 618 | t.Fatal(err) 619 | } 620 | defer rows.Close() 621 | count := 0 622 | for rows.Next() { 623 | count++ 624 | var col tpchRow 625 | err = rows.Scan( 626 | &col.CustKey, 627 | &col.Name, 628 | &col.Address, 629 | &col.NationKey, 630 | &col.Phone, 631 | &col.AcctBal, 632 | &col.MktSegment, 633 | &col.Comment, 634 | ) 635 | if err != nil { 636 | t.Fatal(err) 637 | } 638 | /* 639 | if col.CustKey == 1 && col.AcctBal != 711.56 { 640 | t.Fatal("unexpected acctbal for custkey=1:", col.AcctBal) 641 | } 642 | */ 643 | } 644 | if rows.Err() != nil { 645 | t.Fatal(err) 646 | } 647 | if count != 1000 { 648 | t.Fatal("not enough rows returned:", count) 649 | } 650 | } 651 | 652 | func TestIntegrationSelectCancelQuery(t *testing.T) { 653 | db := integrationOpen(t) 654 | defer db.Close() 655 | deadline := time.Now().Add(200 * time.Millisecond) 656 | ctx, cancel := context.WithDeadline(context.Background(), deadline) 657 | defer cancel() 658 | rows, err := db.QueryContext(ctx, "SELECT * FROM tpch.sf1.customer") 659 | if err != nil { 660 | goto handleErr 661 | } 662 | defer rows.Close() 663 | for rows.Next() { 664 | var col tpchRow 665 | err = rows.Scan( 666 | &col.CustKey, 667 | &col.Name, 668 | &col.Address, 669 | &col.NationKey, 670 | &col.Phone, 671 | &col.AcctBal, 672 | &col.MktSegment, 673 | &col.Comment, 674 | ) 675 | if err != nil { 676 | break 677 | } 678 | } 679 | if err = rows.Err(); err == nil { 680 | t.Fatal("unexpected query with deadline succeeded") 681 | } 682 | handleErr: 683 | errmsg := err.Error() 684 | for _, msg := range []string{"cancel", "deadline"} { 685 | if strings.Contains(errmsg, msg) { 686 | return 687 | } 688 | } 689 | t.Fatal("unexpected error:", err) 690 | } 691 | 692 | func TestIntegrationSessionProperties(t *testing.T) { 693 | dsn := *integrationServerFlag 694 | dsn += "?session_properties=query_max_run_time%3A10m%3Bquery_priority%3A2" 695 | db := integrationOpen(t, dsn) 696 | defer db.Close() 697 | rows, err := db.Query("SHOW SESSION") 698 | if err != nil { 699 | t.Fatal(err) 700 | } 701 | for rows.Next() { 702 | col := struct { 703 | Name string 704 | Value string 705 | Default string 706 | Type string 707 | Description string 708 | }{} 709 | err = rows.Scan( 710 | &col.Name, 711 | &col.Value, 712 | &col.Default, 713 | &col.Type, 714 | &col.Description, 715 | ) 716 | if err != nil { 717 | t.Fatal(err) 718 | } 719 | switch { 720 | case col.Name == "query_max_run_time" && col.Value != "10m": 721 | t.Fatal("unexpected value for query_max_run_time:", col.Value) 722 | case col.Name == "query_priority" && col.Value != "2": 723 | t.Fatal("unexpected value for query_priority:", col.Value) 724 | } 725 | } 726 | if err = rows.Err(); err != nil { 727 | t.Fatal(err) 728 | } 729 | } 730 | 731 | func TestIntegrationTypeConversion(t *testing.T) { 732 | err := RegisterCustomClient("uncompressed", &http.Client{Transport: &http.Transport{DisableCompression: true}}) 733 | if err != nil { 734 | t.Fatal(err) 735 | } 736 | dsn := *integrationServerFlag 737 | dsn += "?custom_client=uncompressed" 738 | db := integrationOpen(t, dsn) 739 | var ( 740 | goTime time.Time 741 | nullTime NullTime 742 | goBytes []byte 743 | nullBytes []byte 744 | goString string 745 | nullString sql.NullString 746 | nullStringSlice NullSliceString 747 | nullStringSlice2 NullSlice2String 748 | nullStringSlice3 NullSlice3String 749 | nullInt64Slice NullSliceInt64 750 | nullInt64Slice2 NullSlice2Int64 751 | nullInt64Slice3 NullSlice3Int64 752 | nullFloat64Slice NullSliceFloat64 753 | nullFloat64Slice2 NullSlice2Float64 754 | nullFloat64Slice3 NullSlice3Float64 755 | goMap map[string]interface{} 756 | nullMap NullMap 757 | goRow []interface{} 758 | ) 759 | err = db.QueryRow(` 760 | SELECT 761 | TIMESTAMP '2017-07-10 01:02:03.004 UTC', 762 | CAST(NULL AS TIMESTAMP), 763 | CAST(X'FFFF0FFF3FFFFFFF' AS VARBINARY), 764 | CAST(NULL AS VARBINARY), 765 | CAST('string' AS VARCHAR), 766 | CAST(NULL AS VARCHAR), 767 | ARRAY['A', 'B', NULL], 768 | ARRAY[ARRAY['A'], NULL], 769 | ARRAY[ARRAY[ARRAY['A'], NULL], NULL], 770 | ARRAY[1, 2, NULL], 771 | ARRAY[ARRAY[1, 1, 1], NULL], 772 | ARRAY[ARRAY[ARRAY[1, 1, 1], NULL], NULL], 773 | ARRAY[1.0, 2.0, NULL], 774 | ARRAY[ARRAY[1.1, 1.1, 1.1], NULL], 775 | ARRAY[ARRAY[ARRAY[1.1, 1.1, 1.1], NULL], NULL], 776 | MAP(ARRAY['a', 'b'], ARRAY['c', 'd']), 777 | CAST(NULL AS MAP(ARRAY(INTEGER), ARRAY(INTEGER))), 778 | ROW(1, 'a', CAST('2017-07-10 01:02:03.004 UTC' AS TIMESTAMP(6) WITH TIME ZONE), ARRAY['c']) 779 | `).Scan( 780 | &goTime, 781 | &nullTime, 782 | &goBytes, 783 | &nullBytes, 784 | &goString, 785 | &nullString, 786 | &nullStringSlice, 787 | &nullStringSlice2, 788 | &nullStringSlice3, 789 | &nullInt64Slice, 790 | &nullInt64Slice2, 791 | &nullInt64Slice3, 792 | &nullFloat64Slice, 793 | &nullFloat64Slice2, 794 | &nullFloat64Slice3, 795 | &goMap, 796 | &nullMap, 797 | &goRow, 798 | ) 799 | if err != nil { 800 | t.Fatal(err) 801 | } 802 | 803 | // Compare the actual and expected values. 804 | expectedTime := time.Date(2017, 7, 10, 1, 2, 3, 4*1000000, time.UTC) 805 | if !goTime.Equal(expectedTime) { 806 | t.Errorf("expected GoTime to be %v, got %v", expectedTime, goTime) 807 | } 808 | 809 | expectedBytes := []byte{0xff, 0xff, 0x0f, 0xff, 0x3f, 0xff, 0xff, 0xff} 810 | if !bytes.Equal(goBytes, expectedBytes) { 811 | t.Errorf("expected GoBytes to be %v, got %v", expectedBytes, goBytes) 812 | } 813 | 814 | if nullBytes != nil { 815 | t.Errorf("expected NullBytes to be nil, got %v", nullBytes) 816 | } 817 | 818 | if goString != "string" { 819 | t.Errorf("expected GoString to be %q, got %q", "string", goString) 820 | } 821 | 822 | if nullString.Valid { 823 | t.Errorf("expected NullString.Valid to be false, got true") 824 | } 825 | 826 | if !reflect.DeepEqual(nullStringSlice.SliceString, []sql.NullString{{String: "A", Valid: true}, {String: "B", Valid: true}, {Valid: false}}) { 827 | t.Errorf("expected NullStringSlice.SliceString to be %v, got %v", 828 | []sql.NullString{{String: "A", Valid: true}, {String: "B", Valid: true}, {Valid: false}}, 829 | nullStringSlice.SliceString) 830 | } 831 | if !nullStringSlice.Valid { 832 | t.Errorf("expected NullStringSlice.Valid to be true, got false") 833 | } 834 | 835 | expectedSlice2String := [][]sql.NullString{{{String: "A", Valid: true}}, {}} 836 | if !reflect.DeepEqual(nullStringSlice2.Slice2String, expectedSlice2String) { 837 | t.Errorf("expected NullStringSlice2.Slice2String to be %v, got %v", expectedSlice2String, nullStringSlice2.Slice2String) 838 | } 839 | if !nullStringSlice2.Valid { 840 | t.Errorf("expected NullStringSlice2.Valid to be true, got false") 841 | } 842 | 843 | expectedSlice3String := [][][]sql.NullString{{{{String: "A", Valid: true}}, {}}, {}} 844 | if !reflect.DeepEqual(nullStringSlice3.Slice3String, expectedSlice3String) { 845 | t.Errorf("expected NullStringSlice3.Slice3String to be %v, got %v", expectedSlice3String, nullStringSlice3.Slice3String) 846 | } 847 | if !nullStringSlice3.Valid { 848 | t.Errorf("expected NullStringSlice3.Valid to be true, got false") 849 | } 850 | 851 | expectedSliceInt64 := []sql.NullInt64{{Int64: 1, Valid: true}, {Int64: 2, Valid: true}, {Valid: false}} 852 | if !reflect.DeepEqual(nullInt64Slice.SliceInt64, expectedSliceInt64) { 853 | t.Errorf("expected NullInt64Slice.SliceInt64 to be %v, got %v", expectedSliceInt64, nullInt64Slice.SliceInt64) 854 | } 855 | if !nullInt64Slice.Valid { 856 | t.Errorf("expected NullInt64Slice.Valid to be true, got false") 857 | } 858 | 859 | expectedSlice2Int64 := [][]sql.NullInt64{{{Int64: 1, Valid: true}, {Int64: 1, Valid: true}, {Int64: 1, Valid: true}}, {}} 860 | if !reflect.DeepEqual(nullInt64Slice2.Slice2Int64, expectedSlice2Int64) { 861 | t.Errorf("expected NullInt64Slice2.Slice2Int64 to be %v, got %v", expectedSlice2Int64, nullInt64Slice2.Slice2Int64) 862 | } 863 | if !nullInt64Slice2.Valid { 864 | t.Errorf("expected NullInt64Slice2.Valid to be true, got false") 865 | } 866 | 867 | expectedSlice3Int64 := [][][]sql.NullInt64{{{{Int64: 1, Valid: true}, {Int64: 1, Valid: true}, {Int64: 1, Valid: true}}, {}}, {}} 868 | if !reflect.DeepEqual(nullInt64Slice3.Slice3Int64, expectedSlice3Int64) { 869 | t.Errorf("expected NullInt64Slice3.Slice3Int64 to be %v, got %v", expectedSlice3Int64, nullInt64Slice3.Slice3Int64) 870 | } 871 | if !nullInt64Slice3.Valid { 872 | t.Errorf("expected NullInt64Slice3.Valid to be true, got false") 873 | } 874 | 875 | expectedSliceFloat64 := []sql.NullFloat64{{Float64: 1.0, Valid: true}, {Float64: 2.0, Valid: true}, {Valid: false}} 876 | if !reflect.DeepEqual(nullFloat64Slice.SliceFloat64, expectedSliceFloat64) { 877 | t.Errorf("expected NullFloat64Slice.SliceFloat64 to be %v, got %v", expectedSliceFloat64, nullFloat64Slice.SliceFloat64) 878 | } 879 | if !nullFloat64Slice.Valid { 880 | t.Errorf("expected NullFloat64Slice.Valid to be true, got false") 881 | } 882 | 883 | expectedSlice2Float64 := [][]sql.NullFloat64{{{Float64: 1.1, Valid: true}, {Float64: 1.1, Valid: true}, {Float64: 1.1, Valid: true}}, {}} 884 | if !reflect.DeepEqual(nullFloat64Slice2.Slice2Float64, expectedSlice2Float64) { 885 | t.Errorf("expected NullFloat64Slice2.Slice2Float64 to be %v, got %v", expectedSlice2Float64, nullFloat64Slice2.Slice2Float64) 886 | } 887 | if !nullFloat64Slice2.Valid { 888 | t.Errorf("expected NullFloat64Slice2.Valid to be true, got false") 889 | } 890 | 891 | expectedSlice3Float64 := [][][]sql.NullFloat64{{{{Float64: 1.1, Valid: true}, {Float64: 1.1, Valid: true}, {Float64: 1.1, Valid: true}}, {}}, {}} 892 | if !reflect.DeepEqual(nullFloat64Slice3.Slice3Float64, expectedSlice3Float64) { 893 | t.Errorf("expected NullFloat64Slice3.Slice3Float64 to be %v, got %v", expectedSlice3Float64, nullFloat64Slice3.Slice3Float64) 894 | } 895 | if !nullFloat64Slice3.Valid { 896 | t.Errorf("expected NullFloat64Slice3.Valid to be true, got false") 897 | } 898 | 899 | expectedMap := map[string]interface{}{"a": "c", "b": "d"} 900 | if !reflect.DeepEqual(goMap, expectedMap) { 901 | t.Errorf("expected GoMap to be %v, got %v", expectedMap, goMap) 902 | } 903 | 904 | if nullMap.Valid { 905 | t.Errorf("expected NullMap.Valid to be false, got true") 906 | } 907 | 908 | expectedRow := []interface{}{json.Number("1"), "a", "2017-07-10 01:02:03.004000 UTC", []interface{}{"c"}} 909 | if !reflect.DeepEqual(goRow, expectedRow) { 910 | t.Errorf("expected GoRow to be %v, got %v", expectedRow, goRow) 911 | } 912 | } 913 | 914 | func TestComplexTypes(t *testing.T) { 915 | // This test has been created to showcase some issues with parsing 916 | // complex types. It is not intended to be a comprehensive test of 917 | // the parsing logic, but rather to provide a reference for future 918 | // changes to the parsing logic. 919 | // 920 | // The current implementation of the parsing logic reads the value 921 | // in the same format as the JSON response from Trino. This means 922 | // that we don't go further to parse values as their structured types. 923 | // For example, a row like `ROW(1, X'0000')` is read as 924 | // a list of a `json.Number(1)` and a base64-encoded string. 925 | t.Skip("skipping failing test") 926 | 927 | dsn := *integrationServerFlag 928 | db := integrationOpen(t, dsn) 929 | 930 | for _, tt := range []struct { 931 | name string 932 | query string 933 | expected interface{} 934 | }{ 935 | { 936 | name: "row containing scalar values", 937 | query: `SELECT ROW(1, 'a', X'0000')`, 938 | expected: []interface{}{1, "a", []byte{0x00, 0x00}}, 939 | }, 940 | { 941 | name: "nested row", 942 | query: `SELECT ROW(ROW(1, 'a'), ROW(2, 'b'))`, 943 | expected: []interface{}{[]interface{}{1, "a"}, []interface{}{2, "b"}}, 944 | }, 945 | { 946 | name: "map with scalar values", 947 | query: `SELECT MAP(ARRAY['a', 'b'], ARRAY[1, 2])`, 948 | expected: map[string]interface{}{"a": 1, "b": 2}, 949 | }, 950 | { 951 | name: "map with nested row", 952 | query: `SELECT MAP(ARRAY['a', 'b'], ARRAY[ROW(1, 'a'), ROW(2, 'b')])`, 953 | expected: map[string]interface{}{"a": []interface{}{1, "a"}, "b": []interface{}{2, "b"}}, 954 | }, 955 | } { 956 | t.Run(tt.name, func(t *testing.T) { 957 | var result interface{} 958 | err := db.QueryRow(tt.query).Scan(&result) 959 | if err != nil { 960 | t.Fatal(err) 961 | } 962 | 963 | if !reflect.DeepEqual(result, tt.expected) { 964 | t.Errorf("expected %v, got %v", tt.expected, result) 965 | } 966 | }) 967 | } 968 | } 969 | 970 | func TestIntegrationArgsConversion(t *testing.T) { 971 | dsn := *integrationServerFlag 972 | db := integrationOpen(t, dsn) 973 | value := 0 974 | err := db.QueryRow(` 975 | SELECT 1 FROM (VALUES ( 976 | CAST(1 AS TINYINT), 977 | CAST(1 AS SMALLINT), 978 | CAST(1 AS INTEGER), 979 | CAST(1 AS BIGINT), 980 | CAST(1 AS REAL), 981 | CAST(1 AS DOUBLE), 982 | TIMESTAMP '2017-07-10 01:02:03.004 UTC', 983 | CAST('string' AS VARCHAR), 984 | CAST(X'FFFF0FFF3FFFFFFF' AS VARBINARY), 985 | ARRAY['A', 'B'] 986 | )) AS t(col_tiny, col_small, col_int, col_big, col_real, col_double, col_ts, col_varchar, col_varbinary, col_array ) 987 | WHERE 1=1 988 | AND col_tiny = ? 989 | AND col_small = ? 990 | AND col_int = ? 991 | AND col_big = ? 992 | AND col_real = cast(? as real) 993 | AND col_double = cast(? as double) 994 | AND col_ts = ? 995 | AND col_varchar = ? 996 | AND col_varbinary = ? 997 | AND col_array = ?`, 998 | int16(1), 999 | int16(1), 1000 | int32(1), 1001 | int64(1), 1002 | Numeric("1"), 1003 | Numeric("1"), 1004 | time.Date(2017, 7, 10, 1, 2, 3, 4*1000000, time.UTC), 1005 | "string", 1006 | []byte{0xff, 0xff, 0x0f, 0xff, 0x3f, 0xff, 0xff, 0xff}, 1007 | []string{"A", "B"}, 1008 | ).Scan(&value) 1009 | if err != nil { 1010 | t.Fatal(err) 1011 | } 1012 | } 1013 | 1014 | func TestIntegrationNoResults(t *testing.T) { 1015 | db := integrationOpen(t) 1016 | rows, err := db.Query("SELECT 1 LIMIT 0") 1017 | if err != nil { 1018 | t.Fatal(err) 1019 | } 1020 | for rows.Next() { 1021 | t.Fatal(errors.New("Rows returned")) 1022 | } 1023 | if err = rows.Err(); err != nil { 1024 | t.Fatal(err) 1025 | } 1026 | } 1027 | 1028 | func TestIntegrationQueryParametersSelect(t *testing.T) { 1029 | scenarios := []struct { 1030 | name string 1031 | query string 1032 | args []interface{} 1033 | expectedError error 1034 | expectedRows int 1035 | }{ 1036 | { 1037 | name: "valid string as varchar", 1038 | query: "SELECT * FROM system.runtime.nodes WHERE system.runtime.nodes.node_id=?", 1039 | args: []interface{}{"test"}, 1040 | expectedRows: 1, 1041 | }, 1042 | { 1043 | name: "valid int as bigint", 1044 | query: "SELECT * FROM tpch.sf1.customer WHERE custkey=? LIMIT 2", 1045 | args: []interface{}{int(1)}, 1046 | expectedRows: 1, 1047 | }, 1048 | { 1049 | name: "invalid string as bigint", 1050 | query: "SELECT * FROM tpch.sf1.customer WHERE custkey=? LIMIT 2", 1051 | args: []interface{}{"1"}, 1052 | expectedError: errors.New(`trino: query failed (200 OK): "USER_ERROR: line 1:46: Cannot apply operator: bigint = varchar(1)"`), 1053 | }, 1054 | { 1055 | name: "valid string as date", 1056 | query: "SELECT * FROM tpch.sf1.lineitem WHERE shipdate=? LIMIT 2", 1057 | args: []interface{}{"1995-01-27"}, 1058 | expectedError: errors.New(`trino: query failed (200 OK): "USER_ERROR: line 1:47: Cannot apply operator: date = varchar(10)"`), 1059 | }, 1060 | } 1061 | 1062 | for i := range scenarios { 1063 | scenario := scenarios[i] 1064 | 1065 | t.Run(scenario.name, func(t *testing.T) { 1066 | db := integrationOpen(t) 1067 | defer db.Close() 1068 | 1069 | rows, err := db.Query(scenario.query, scenario.args...) 1070 | if err != nil { 1071 | if scenario.expectedError == nil { 1072 | t.Errorf("Unexpected err: %s", err) 1073 | return 1074 | } 1075 | if err.Error() == scenario.expectedError.Error() { 1076 | return 1077 | } 1078 | t.Errorf("Expected err to be %s but got %s", scenario.expectedError, err) 1079 | } 1080 | 1081 | if scenario.expectedError != nil { 1082 | t.Error("missing expected error") 1083 | return 1084 | } 1085 | 1086 | defer rows.Close() 1087 | 1088 | var count int 1089 | for rows.Next() { 1090 | count++ 1091 | } 1092 | if err = rows.Err(); err != nil { 1093 | t.Fatal(err) 1094 | } 1095 | if count != scenario.expectedRows { 1096 | t.Errorf("expecting %d rows, got %d", scenario.expectedRows, count) 1097 | } 1098 | }) 1099 | } 1100 | } 1101 | 1102 | func TestIntegrationQueryNextAfterClose(t *testing.T) { 1103 | // NOTE: This is testing invalid behaviour. It ensures that we don't 1104 | // panic if we call driverRows.Next after we closed the driverStmt. 1105 | 1106 | ctx := context.Background() 1107 | conn, err := (&Driver{}).Open(*integrationServerFlag) 1108 | if err != nil { 1109 | t.Fatalf("Failed to open connection: %v", err) 1110 | } 1111 | defer conn.Close() 1112 | 1113 | stmt, err := conn.(driver.ConnPrepareContext).PrepareContext(ctx, "SELECT 1") 1114 | if err != nil { 1115 | t.Fatalf("Failed preparing query: %v", err) 1116 | } 1117 | 1118 | rows, err := stmt.(driver.StmtQueryContext).QueryContext(ctx, []driver.NamedValue{}) 1119 | if err != nil { 1120 | t.Fatalf("Failed running query: %v", err) 1121 | } 1122 | defer rows.Close() 1123 | 1124 | stmt.Close() // NOTE: the important bit. 1125 | 1126 | var result driver.Value 1127 | if err := rows.Next([]driver.Value{result}); err != nil && !spoolingProtocolSupported { 1128 | t.Fatalf("unexpected result: %+v, no error was expected", err) 1129 | } 1130 | if err := rows.Next([]driver.Value{result}); err != io.EOF { 1131 | t.Fatalf("unexpected result: %+v, expected io.EOF", err) 1132 | } 1133 | } 1134 | 1135 | func TestIntegrationExec(t *testing.T) { 1136 | db := integrationOpen(t) 1137 | defer db.Close() 1138 | 1139 | _, err := db.Query(`SELECT count(*) FROM nation`) 1140 | expected := "Schema must be specified when session schema is not set" 1141 | if err == nil || !strings.Contains(err.Error(), expected) { 1142 | t.Fatalf("Expected to fail to execute query with error: %v, got: %v", expected, err) 1143 | } 1144 | 1145 | result, err := db.Exec("USE tpch.sf100") 1146 | if err != nil { 1147 | t.Fatal("Failed executing query:", err.Error()) 1148 | } 1149 | if result == nil { 1150 | t.Fatal("Expected exec result to be not nil") 1151 | } 1152 | 1153 | a, err := result.RowsAffected() 1154 | if err != nil { 1155 | t.Fatal("Expected RowsAffected not to return any error, got:", err) 1156 | } 1157 | if a != 0 { 1158 | t.Fatal("Expected RowsAffected to be zero, got:", a) 1159 | } 1160 | rows, err := db.Query(`SELECT count(*) FROM nation`) 1161 | if err != nil { 1162 | t.Fatal("Failed executing query:", err.Error()) 1163 | } 1164 | if rows == nil || !rows.Next() { 1165 | t.Fatal("Failed fetching results") 1166 | } 1167 | } 1168 | 1169 | func TestIntegrationUnsupportedHeader(t *testing.T) { 1170 | dsn := *integrationServerFlag 1171 | dsn += "?catalog=tpch&schema=sf10" 1172 | db := integrationOpen(t, dsn) 1173 | defer db.Close() 1174 | cases := []struct { 1175 | query string 1176 | err error 1177 | }{ 1178 | { 1179 | query: "SET ROLE dummy", 1180 | err: errors.New(`trino: query failed (200 OK): "USER_ERROR: line 1:1: Role 'dummy' does not exist"`), 1181 | }, 1182 | { 1183 | query: "SET PATH dummy", 1184 | err: errors.New(`trino: query failed (200 OK): "USER_ERROR: SET PATH not supported by client"`), 1185 | }, 1186 | } 1187 | for _, c := range cases { 1188 | _, err := db.Query(c.query) 1189 | if err == nil || err.Error() != c.err.Error() { 1190 | t.Fatal("unexpected error:", err) 1191 | } 1192 | } 1193 | } 1194 | 1195 | func TestSpoolingWorkersHigherThenAllowedOutOfOrderSegments(t *testing.T) { 1196 | if !spoolingProtocolSupported { 1197 | t.Skip("Skipping test when spooling protocol is not supported.") 1198 | } 1199 | db := integrationOpen(t) 1200 | defer db.Close() 1201 | 1202 | expectedError := "spooling worker cannot be greater than max out of order segments allowed. spooling workers: 2, allowed out of order segments: 1" 1203 | _, err := db.Query("SELECT 1", 1204 | sql.Named(trinoEncoding, "json"), 1205 | sql.Named(trinoSpoolingWorkerCount, "2"), 1206 | sql.Named(trinoMaxOutOfOrdersSegments, "1")) 1207 | 1208 | if err == nil || err.Error() != expectedError { 1209 | t.Fatal("unexpected error:", err) 1210 | } 1211 | } 1212 | 1213 | func TestIntegrationQueryContext(t *testing.T) { 1214 | tests := []struct { 1215 | name string 1216 | timeout time.Duration 1217 | expectedErrMsg string 1218 | }{ 1219 | { 1220 | name: "Context Cancellation", 1221 | timeout: 0, 1222 | expectedErrMsg: "canceled", 1223 | }, 1224 | { 1225 | name: "Context Deadline Exceeded", 1226 | timeout: 3 * time.Second, 1227 | expectedErrMsg: "context deadline exceeded", 1228 | }, 1229 | } 1230 | 1231 | if err := RegisterCustomClient("uncompressed", &http.Client{Transport: &http.Transport{DisableCompression: true}}); err != nil { 1232 | t.Fatal(err) 1233 | } 1234 | 1235 | dsn := *integrationServerFlag + "?catalog=tpch&schema=sf100&source=cancel-test&custom_client=uncompressed" 1236 | db := integrationOpen(t, dsn) 1237 | defer db.Close() 1238 | 1239 | for _, tt := range tests { 1240 | t.Run(tt.name, func(t *testing.T) { 1241 | var ctx context.Context 1242 | var cancel context.CancelFunc 1243 | 1244 | if tt.timeout == 0 { 1245 | ctx, cancel = context.WithCancel(context.Background()) 1246 | } else { 1247 | ctx, cancel = context.WithTimeout(context.Background(), tt.timeout) 1248 | } 1249 | defer cancel() 1250 | 1251 | errCh := make(chan error, 1) 1252 | done := make(chan struct{}) 1253 | longQuery := "SELECT COUNT(*) FROM lineitem" 1254 | 1255 | go func() { 1256 | // query will complete in ~7s unless cancelled 1257 | rows, err := db.QueryContext(ctx, longQuery) 1258 | if err != nil { 1259 | errCh <- err 1260 | return 1261 | } 1262 | defer rows.Close() 1263 | 1264 | rows.Next() 1265 | if err = rows.Err(); err != nil { 1266 | errCh <- err 1267 | return 1268 | } 1269 | close(done) 1270 | }() 1271 | 1272 | // Poll system.runtime.queries to get the query ID 1273 | var queryID string 1274 | pollCtx, pollCancel := context.WithTimeout(context.Background(), 1*time.Second) 1275 | defer pollCancel() 1276 | 1277 | for { 1278 | row := db.QueryRowContext(pollCtx, "SELECT query_id FROM system.runtime.queries WHERE state = 'RUNNING' AND source = 'cancel-test' AND query = ?", longQuery) 1279 | err := row.Scan(&queryID) 1280 | if err == nil { 1281 | break 1282 | } 1283 | if err != sql.ErrNoRows { 1284 | t.Fatal("failed to read query ID:", err) 1285 | } 1286 | if err = contextSleep(pollCtx, 100*time.Millisecond); err != nil { 1287 | t.Fatal("query did not start in 1 second") 1288 | } 1289 | } 1290 | 1291 | if tt.timeout == 0 { 1292 | cancel() 1293 | } 1294 | 1295 | // Wait for the query to be canceled or completed 1296 | select { 1297 | case <-done: 1298 | t.Fatal("unexpected query succeeded despite cancellation or deadline") 1299 | case err := <-errCh: 1300 | if !strings.Contains(err.Error(), tt.expectedErrMsg) { 1301 | t.Fatalf("expected error containing %q, but got: %v", tt.expectedErrMsg, err) 1302 | } 1303 | } 1304 | 1305 | // Poll system.runtime.queries to verify the query was canceled 1306 | pollCtx, pollCancel = context.WithTimeout(context.Background(), 2*time.Second) 1307 | defer pollCancel() 1308 | 1309 | for { 1310 | row := db.QueryRowContext(pollCtx, "SELECT state, error_code FROM system.runtime.queries WHERE query_id = ?", queryID) 1311 | var state string 1312 | var code *string 1313 | err := row.Scan(&state, &code) 1314 | if err != nil { 1315 | t.Fatal("failed to read query state:", err) 1316 | } 1317 | if state == "FAILED" && code != nil && *code == "USER_CANCELED" { 1318 | return 1319 | } 1320 | if err = contextSleep(pollCtx, 100*time.Millisecond); err != nil { 1321 | t.Fatalf("query was not canceled in 2 seconds; state: %s, code: %v, err: %v", state, code, err) 1322 | } 1323 | } 1324 | }) 1325 | } 1326 | } 1327 | 1328 | func TestIntegrationAccessToken(t *testing.T) { 1329 | if tlsServer == "" { 1330 | t.Skip("Skipping access token test when using a custom integration server.") 1331 | } 1332 | 1333 | accessToken, err := generateToken() 1334 | if err != nil { 1335 | t.Fatal(err) 1336 | } 1337 | 1338 | dsn := tlsServer + "?accessToken=" + accessToken 1339 | 1340 | db := integrationOpen(t, dsn) 1341 | 1342 | defer db.Close() 1343 | rows, err := db.Query("SHOW CATALOGS") 1344 | if err != nil { 1345 | t.Fatal(err) 1346 | } 1347 | defer rows.Close() 1348 | count := 0 1349 | for rows.Next() { 1350 | count++ 1351 | } 1352 | if count < 1 { 1353 | t.Fatal("not enough rows returned:", count) 1354 | } 1355 | } 1356 | 1357 | func generateToken() (string, error) { 1358 | privateKeyPEM, err := os.ReadFile("etc/secrets/private_key.pem") 1359 | if err != nil { 1360 | return "", fmt.Errorf("error reading private key file: %w", err) 1361 | } 1362 | 1363 | privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEM) 1364 | if err != nil { 1365 | return "", fmt.Errorf("error parsing private key: %w", err) 1366 | } 1367 | 1368 | // Subject must be 'test' 1369 | claims := jwt.RegisteredClaims{ 1370 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * 365 * time.Hour)), 1371 | Issuer: "gotrino", 1372 | Subject: "test", 1373 | } 1374 | 1375 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 1376 | signedToken, err := token.SignedString(privateKey) 1377 | 1378 | if err != nil { 1379 | return "", fmt.Errorf("error generating token: %w", err) 1380 | } 1381 | 1382 | return signedToken, nil 1383 | } 1384 | 1385 | func TestIntegrationTLS(t *testing.T) { 1386 | if tlsServer == "" { 1387 | t.Skip("Skipping TLS test when using a custom integration server.") 1388 | } 1389 | 1390 | dsn := tlsServer 1391 | db := integrationOpen(t, dsn) 1392 | 1393 | defer db.Close() 1394 | row := db.QueryRow("SELECT 1") 1395 | var count int 1396 | if err := row.Scan(&count); err != nil { 1397 | t.Fatal(err) 1398 | } 1399 | if count != 1 { 1400 | t.Fatal("unexpected count=", count) 1401 | } 1402 | } 1403 | 1404 | func contextSleep(ctx context.Context, d time.Duration) error { 1405 | timer := time.NewTimer(100 * time.Millisecond) 1406 | select { 1407 | case <-timer.C: 1408 | return nil 1409 | case <-ctx.Done(): 1410 | if !timer.Stop() { 1411 | <-timer.C 1412 | } 1413 | return ctx.Err() 1414 | } 1415 | } 1416 | 1417 | func TestIntegrationDayToHourIntervalMilliPrecision(t *testing.T) { 1418 | db := integrationOpen(t) 1419 | defer db.Close() 1420 | tests := []struct { 1421 | name string 1422 | arg time.Duration 1423 | wantErr bool 1424 | }{ 1425 | { 1426 | name: "valid 1234567891s", 1427 | arg: time.Duration(1234567891) * time.Second, 1428 | wantErr: false, 1429 | }, 1430 | { 1431 | name: "valid 123456789.1s", 1432 | arg: time.Duration(123456789100) * time.Millisecond, 1433 | wantErr: false, 1434 | }, 1435 | { 1436 | name: "valid 12345678.91s", 1437 | arg: time.Duration(12345678910) * time.Millisecond, 1438 | wantErr: false, 1439 | }, 1440 | { 1441 | name: "valid 1234567.891s", 1442 | arg: time.Duration(1234567891) * time.Millisecond, 1443 | wantErr: false, 1444 | }, 1445 | { 1446 | name: "valid -1234567891s", 1447 | arg: time.Duration(-1234567891) * time.Second, 1448 | wantErr: false, 1449 | }, 1450 | { 1451 | name: "valid -123456789.1s", 1452 | arg: time.Duration(-123456789100) * time.Millisecond, 1453 | wantErr: false, 1454 | }, 1455 | { 1456 | name: "valid -12345678.91s", 1457 | arg: time.Duration(-12345678910) * time.Millisecond, 1458 | wantErr: false, 1459 | }, 1460 | { 1461 | name: "valid -1234567.891s", 1462 | arg: time.Duration(-1234567891) * time.Millisecond, 1463 | wantErr: false, 1464 | }, 1465 | { 1466 | name: "invalid 1234567891.2s", 1467 | arg: time.Duration(1234567891200) * time.Millisecond, 1468 | wantErr: true, 1469 | }, 1470 | { 1471 | name: "invalid 123456789.12s", 1472 | arg: time.Duration(123456789120) * time.Millisecond, 1473 | wantErr: true, 1474 | }, 1475 | { 1476 | name: "invalid 12345678.912s", 1477 | arg: time.Duration(12345678912) * time.Millisecond, 1478 | wantErr: true, 1479 | }, 1480 | { 1481 | name: "invalid -1234567891.2s", 1482 | arg: time.Duration(-1234567891200) * time.Millisecond, 1483 | wantErr: true, 1484 | }, 1485 | { 1486 | name: "invalid -123456789.12s", 1487 | arg: time.Duration(-123456789120) * time.Millisecond, 1488 | wantErr: true, 1489 | }, 1490 | { 1491 | name: "invalid -12345678.912s", 1492 | arg: time.Duration(-12345678912) * time.Millisecond, 1493 | wantErr: true, 1494 | }, 1495 | { 1496 | name: "invalid max seconds (9223372036)", 1497 | arg: time.Duration(math.MaxInt64) / time.Second * time.Second, 1498 | wantErr: true, 1499 | }, 1500 | { 1501 | name: "invalid min seconds (-9223372036)", 1502 | arg: time.Duration(math.MinInt64) / time.Second * time.Second, 1503 | wantErr: true, 1504 | }, 1505 | { 1506 | name: "valid max seconds (2147483647)", 1507 | arg: math.MaxInt32 * time.Second, 1508 | }, 1509 | { 1510 | name: "valid min seconds (-2147483647)", 1511 | arg: -math.MaxInt32 * time.Second, 1512 | }, 1513 | { 1514 | name: "valid max minutes (153722867)", 1515 | arg: time.Duration(math.MaxInt64) / time.Minute * time.Minute, 1516 | }, 1517 | { 1518 | name: "valid min minutes (-153722867)", 1519 | arg: time.Duration(math.MinInt64) / time.Minute * time.Minute, 1520 | }, 1521 | { 1522 | name: "valid max hours (2562047)", 1523 | arg: time.Duration(math.MaxInt64) / time.Hour * time.Hour, 1524 | }, 1525 | { 1526 | name: "valid min hours (-2562047)", 1527 | arg: time.Duration(math.MinInt64) / time.Hour * time.Hour, 1528 | }, 1529 | } 1530 | for _, test := range tests { 1531 | t.Run(test.name, func(t *testing.T) { 1532 | _, err := db.Exec("SELECT ?", test.arg) 1533 | if (err != nil) != test.wantErr { 1534 | t.Errorf("Exec() error = %v, wantErr %v", err, test.wantErr) 1535 | return 1536 | } 1537 | }) 1538 | } 1539 | } 1540 | 1541 | func TestIntegrationLargeQuery(t *testing.T) { 1542 | version, err := strconv.Atoi(*trinoImageTagFlag) 1543 | if (err != nil && *trinoImageTagFlag != "latest") || (err == nil && version < 418) { 1544 | t.Skip("Skipping test when not using Trino 418 or later.") 1545 | } 1546 | dsn := *integrationServerFlag 1547 | dsn += "?explicitPrepare=false" 1548 | db := integrationOpen(t, dsn) 1549 | defer db.Close() 1550 | rows, err := db.Query("SELECT ?, '"+strings.Repeat("a", 5000000)+"'", 42) 1551 | if err != nil { 1552 | t.Fatal(err) 1553 | } 1554 | defer rows.Close() 1555 | count := 0 1556 | for rows.Next() { 1557 | count++ 1558 | } 1559 | if rows.Err() != nil { 1560 | t.Fatal(err) 1561 | } 1562 | if count != 1 { 1563 | t.Fatal("not enough rows returned:", count) 1564 | } 1565 | } 1566 | 1567 | func TestIntegrationTypeConversionSpoolingProtocolInlineJsonEncoder(t *testing.T) { 1568 | err := RegisterCustomClient("uncompressed", &http.Client{Transport: &http.Transport{DisableCompression: true}}) 1569 | if err != nil { 1570 | t.Fatal(err) 1571 | } 1572 | dsn := *integrationServerFlag 1573 | dsn += "?custom_client=uncompressed" 1574 | db := integrationOpen(t, dsn) 1575 | var ( 1576 | goTime time.Time 1577 | nullTime NullTime 1578 | goString string 1579 | nullString sql.NullString 1580 | nullStringSlice NullSliceString 1581 | nullStringSlice2 NullSlice2String 1582 | nullStringSlice3 NullSlice3String 1583 | nullInt64Slice NullSliceInt64 1584 | nullInt64Slice2 NullSlice2Int64 1585 | nullInt64Slice3 NullSlice3Int64 1586 | nullFloat64Slice NullSliceFloat64 1587 | nullFloat64Slice2 NullSlice2Float64 1588 | nullFloat64Slice3 NullSlice3Float64 1589 | goMap map[string]interface{} 1590 | nullMap NullMap 1591 | goRow []interface{} 1592 | ) 1593 | err = db.QueryRow(` 1594 | SELECT 1595 | TIMESTAMP '2017-07-10 01:02:03.004 UTC', 1596 | CAST(NULL AS TIMESTAMP), 1597 | CAST('string' AS VARCHAR), 1598 | CAST(NULL AS VARCHAR), 1599 | ARRAY['A', 'B', NULL], 1600 | ARRAY[ARRAY['A'], NULL], 1601 | ARRAY[ARRAY[ARRAY['A'], NULL], NULL], 1602 | ARRAY[1, 2, NULL], 1603 | ARRAY[ARRAY[1, 1, 1], NULL], 1604 | ARRAY[ARRAY[ARRAY[1, 1, 1], NULL], NULL], 1605 | ARRAY[1.0, 2.0, NULL], 1606 | ARRAY[ARRAY[1.1, 1.1, 1.1], NULL], 1607 | ARRAY[ARRAY[ARRAY[1.1, 1.1, 1.1], NULL], NULL], 1608 | MAP(ARRAY['a', 'b'], ARRAY['c', 'd']), 1609 | CAST(NULL AS MAP(ARRAY(INTEGER), ARRAY(INTEGER))), 1610 | ROW(1, 'a', CAST('2017-07-10 01:02:03.004 UTC' AS TIMESTAMP(6) WITH TIME ZONE), ARRAY['c']) 1611 | `, sql.Named(trinoEncoding, "json")).Scan( 1612 | &goTime, 1613 | &nullTime, 1614 | &goString, 1615 | &nullString, 1616 | &nullStringSlice, 1617 | &nullStringSlice2, 1618 | &nullStringSlice3, 1619 | &nullInt64Slice, 1620 | &nullInt64Slice2, 1621 | &nullInt64Slice3, 1622 | &nullFloat64Slice, 1623 | &nullFloat64Slice2, 1624 | &nullFloat64Slice3, 1625 | &goMap, 1626 | &nullMap, 1627 | &goRow, 1628 | ) 1629 | if err != nil { 1630 | t.Fatal(err) 1631 | } 1632 | } 1633 | 1634 | func TestIntegrationSelectTpchSpoolingSegments(t *testing.T) { 1635 | tests := []struct { 1636 | name string 1637 | query string 1638 | encoding string 1639 | expected int 1640 | }{ 1641 | // Testing with a LIMIT of 1001 rows. 1642 | // Since we exceed the `protocol.spooling.inlining.max-rows` threshold (1000), 1643 | // this query trigger spooling protocol with spooled segments. 1644 | { 1645 | name: "Spooled Segment JSON+ZSTD Encoded", 1646 | query: "SELECT * FROM tpch.sf1.customer LIMIT 1001", 1647 | encoding: "json+zstd", 1648 | expected: 1001, 1649 | }, 1650 | { 1651 | name: "Spooled Segment JSON Encoded", 1652 | query: "SELECT * FROM tpch.sf1.customer LIMIT 1001", 1653 | encoding: "json", 1654 | expected: 1001, 1655 | }, 1656 | { 1657 | name: "Spooled Segment JSON+LZ4 Encoded", 1658 | query: "SELECT * FROM tpch.sf1.customer LIMIT 1001", 1659 | encoding: "json+lz4", 1660 | expected: 1001, 1661 | }, 1662 | // Testing with a LIMIT of 100 rows. 1663 | // This should remain inline as it is below the `protocol.spooling.inlining.max-rows` (1000) and bellow `protocol.spooling.inlining.max-size` 128kb 1664 | { 1665 | name: "Inline Segment JSON+ZSTD Encoded", 1666 | query: "SELECT * FROM tpch.sf1.customer LIMIT 100", 1667 | encoding: "json+zstd", 1668 | expected: 100, 1669 | }, 1670 | { 1671 | name: "Inline Segment JSON+LZ4 Encoded", 1672 | query: "SELECT * FROM tpch.sf1.customer LIMIT 100", 1673 | encoding: "json+lz4", 1674 | expected: 100, 1675 | }, 1676 | } 1677 | 1678 | for _, tt := range tests { 1679 | t.Run(tt.name, func(t *testing.T) { 1680 | db := integrationOpen(t) 1681 | defer db.Close() 1682 | 1683 | rows, err := db.Query(tt.query, sql.Named(trinoEncoding, tt.encoding)) 1684 | if err != nil { 1685 | t.Fatalf("Query failed: %v", err) 1686 | } 1687 | defer rows.Close() 1688 | 1689 | count := 0 1690 | for rows.Next() { 1691 | count++ 1692 | var col tpchRow 1693 | err = rows.Scan( 1694 | &col.CustKey, 1695 | &col.Name, 1696 | &col.Address, 1697 | &col.NationKey, 1698 | &col.Phone, 1699 | &col.AcctBal, 1700 | &col.MktSegment, 1701 | &col.Comment, 1702 | ) 1703 | if err != nil { 1704 | t.Fatalf("Row scan failed: %v", err) 1705 | } 1706 | } 1707 | 1708 | if rows.Err() != nil { 1709 | t.Fatalf("Rows iteration error: %v", rows.Err()) 1710 | } 1711 | 1712 | if count != tt.expected { 1713 | t.Fatalf("Expected %d rows, got %d", tt.expected, count) 1714 | } 1715 | }) 1716 | } 1717 | } 1718 | 1719 | func TestSpoolingIntegrationOrderedResults(t *testing.T) { 1720 | if !spoolingProtocolSupported { 1721 | t.Skip("Skipping test when spooling protocol is not supported.") 1722 | } 1723 | db := integrationOpen(t) 1724 | defer db.Close() 1725 | 1726 | query := ` 1727 | SELECT * 1728 | FROM TABLE(sequence( 1729 | start => 1, 1730 | stop => 5000000 1731 | )) 1732 | ORDER BY sequential_number 1733 | ` 1734 | 1735 | rows, err := db.Query(query, sql.Named(trinoEncoding, "json")) 1736 | if err != nil { 1737 | t.Fatalf("Query failed: %v", err) 1738 | } 1739 | defer rows.Close() 1740 | 1741 | expected := 1 1742 | var actual int 1743 | 1744 | for rows.Next() { 1745 | err = rows.Scan(&actual) 1746 | if err != nil { 1747 | t.Fatalf("Row scan failed: %v", err) 1748 | } 1749 | 1750 | if actual != expected { 1751 | t.Fatalf("Unexpected number at position %d: got %d, expected %d", expected, actual, expected) 1752 | } 1753 | expected++ 1754 | } 1755 | 1756 | if rows.Err() != nil { 1757 | t.Fatalf("Rows iteration error: %v", rows.Err()) 1758 | } 1759 | 1760 | if expected != 5_000_001 { 1761 | t.Fatalf("Expected 5,000,000 rows, got %d", expected-1) 1762 | } 1763 | } 1764 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------