├── NOTICE ├── .github ├── dependabot.yml └── workflows │ ├── go.yml │ └── codeql-analysis.yml ├── CODE_OF_CONDUCT.md ├── .gitignore ├── go.mod ├── qldbdriver ├── errors.go ├── retry.go ├── logger.go ├── qldbhash.go ├── qldbsessioniface │ └── Interface.go ├── session.go ├── session_management_test.go ├── transaction.go ├── communicator.go ├── integration_base_test.go ├── result.go ├── communicator_test.go ├── qldbdriver.go ├── result_test.go ├── transaction_test.go ├── session_test.go ├── qldbdriver_test.go └── statement_execution_test.go ├── CHANGELOG.md ├── README.md ├── go.sum └── LICENSE /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "tuesday" 8 | commit-message: 9 | prefix: "gomod" 10 | open-pull-requests-limit: 10 11 | target-branch: "master" 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/go 2 | # Edit at https://www.gitignore.io/?templates=go 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | ### Go Patch ### 22 | /vendor/ 23 | /Godeps/ 24 | 25 | # End of https://www.gitignore.io/api/go 26 | 27 | .idea -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/amazon-qldb-driver-go/v3 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/amzn/ion-go v1.1.3 7 | github.com/amzn/ion-hash-go v1.2.0 8 | github.com/aws/aws-sdk-go-v2 v1.22.1 9 | github.com/aws/aws-sdk-go-v2/config v1.22.1 10 | github.com/aws/aws-sdk-go-v2/service/qldb v1.18.0 11 | github.com/aws/aws-sdk-go-v2/service/qldbsession v1.18.0 12 | github.com/aws/smithy-go v1.16.0 13 | github.com/kr/text v0.2.0 // indirect 14 | github.com/stretchr/testify v1.8.4 15 | golang.org/x/sync v0.5.0 16 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /qldbdriver/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | // qldbDriverError is returned when an error caused by QLDBDriver has occurred. 17 | type qldbDriverError struct { 18 | errorMessage string 19 | } 20 | 21 | // Return the message denoting the cause of the error. 22 | func (e *qldbDriverError) Error() string { 23 | return e.errorMessage 24 | } 25 | 26 | type txnError struct { 27 | transactionID string 28 | message string 29 | err error 30 | canRetry bool 31 | abortSuccess bool 32 | isISE bool 33 | } 34 | 35 | func (e *txnError) unwrap() error { 36 | return e.err 37 | } 38 | -------------------------------------------------------------------------------- /qldbdriver/retry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "math" 18 | "math/rand" 19 | "time" 20 | ) 21 | 22 | // BackoffStrategy is an interface for implementing a delay before retrying the provided function with a new transaction. 23 | type BackoffStrategy interface { 24 | // Get the time to delay before retrying, using an exponential function on the retry attempt, and jitter. 25 | Delay(retryAttempt int) time.Duration 26 | } 27 | 28 | // RetryPolicy defines the policy to use to for retrying the provided function in the case of a non-fatal error. 29 | type RetryPolicy struct { 30 | // The maximum amount of times to retry. 31 | MaxRetryLimit int 32 | // The strategy to use for delaying before the retry attempt. 33 | Backoff BackoffStrategy 34 | } 35 | 36 | // ExponentialBackoffStrategy exponentially increases the delay per retry attempt given a base and a cap. 37 | // 38 | // This is the default strategy implementation. 39 | type ExponentialBackoffStrategy struct { 40 | // The time in milliseconds to use as the exponent base for the delay calculation. 41 | SleepBase time.Duration 42 | // The maximum delay time in milliseconds. 43 | SleepCap time.Duration 44 | } 45 | 46 | // Delay gets the time to delay before retrying, using an exponential function on the retry attempt, and jitter. 47 | func (s ExponentialBackoffStrategy) Delay(retryAttempt int) time.Duration { 48 | rand.Seed(time.Now().UTC().UnixNano()) 49 | jitter := rand.Float64()*0.5 + 0.5 50 | 51 | return time.Duration(jitter*math.Min(float64(s.SleepCap.Milliseconds()), float64(s.SleepBase.Milliseconds())*math.Pow(2, float64(retryAttempt)))) * time.Millisecond 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | id-token: write 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | name: Build and tests 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | max-parallel: 6 19 | matrix: 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | go_version: ['1.20', '1.21'] 22 | fail-fast: false 23 | 24 | steps: 25 | 26 | - name: Set up Go version 27 | uses: actions/setup-go@v4 28 | with: 29 | go-version: ${{ matrix.go_version }} 30 | 31 | - name: Configure AWS Credentials 32 | uses: aws-actions/configure-aws-credentials@v4 33 | with: 34 | aws-region: us-east-2 35 | role-to-assume: arn:aws:iam::264319671630:role/GitHubActionsOidc 36 | unset-current-credentials: true 37 | 38 | - name: Check out code into the Go module directory 39 | uses: actions/checkout@v4 40 | 41 | - name: Build 42 | run: go build -v ./... 43 | 44 | - name: Unit Tests 45 | run: go test -v -short ./... 46 | 47 | - name: Integration Tests 48 | run: | 49 | GITHUB_SHA_SHORT=$(git rev-parse --short $GITHUB_SHA) 50 | go test -v -timeout 30m -run Integration ./... -args -ledger_suffix ${{ strategy.job-index }}-$GITHUB_SHA_SHORT 51 | shell: bash 52 | 53 | check: 54 | name: Perform style and linter check 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Set up Go version 58 | uses: actions/setup-go@v4 59 | with: 60 | go-version: '1.18' 61 | - name: Install goimports 62 | run: go install golang.org/x/tools/cmd/goimports@latest 63 | - name: Check out code into the Go module directory 64 | uses: actions/checkout@v4 65 | - run: goimports -w . 66 | - run: go mod tidy 67 | # If there are any diffs from goimports or go mod tidy, fail. 68 | - name: Verify no changes from goimports and go mod tidy 69 | run: | 70 | if [ -n "$(git status --porcelain)" ]; then 71 | exit 1 72 | fi 73 | - run: go vet ./... 74 | - uses: dominikh/staticcheck-action@v1.3.0 75 | with: 76 | version: "2022.1" 77 | install-go: false 78 | -------------------------------------------------------------------------------- /qldbdriver/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | ) 20 | 21 | // Logger is an interface for a QLDBDriver logger. 22 | type Logger interface { 23 | // Log the message using the built-in Golang logging package. 24 | Log(message string, verbosity LogLevel) 25 | } 26 | 27 | // LogLevel represents the valid logging verbosity levels. 28 | type LogLevel uint8 29 | 30 | const ( 31 | // LogOff is for logging nothing. 32 | LogOff LogLevel = iota 33 | // LogInfo is for logging informative events. This is the default logging level. 34 | LogInfo 35 | // LogDebug is for logging information useful for closely tracing the operation of the QLDBDriver. 36 | LogDebug 37 | ) 38 | 39 | type qldbLogger struct { 40 | logger Logger 41 | verbosity LogLevel 42 | } 43 | 44 | func (qldbLogger *qldbLogger) log(verbosityLevel LogLevel, message string) { 45 | if verbosityLevel <= qldbLogger.verbosity { 46 | switch verbosityLevel { 47 | case LogInfo: 48 | qldbLogger.logger.Log("[INFO] "+message, verbosityLevel) 49 | case LogDebug: 50 | qldbLogger.logger.Log("[DEBUG] "+message, verbosityLevel) 51 | default: 52 | qldbLogger.logger.Log(message, verbosityLevel) 53 | } 54 | } 55 | } 56 | 57 | func (qldbLogger *qldbLogger) logf(verbosityLevel LogLevel, message string, args ...interface{}) { 58 | if verbosityLevel <= qldbLogger.verbosity { 59 | switch verbosityLevel { 60 | case LogInfo: 61 | qldbLogger.logger.Log(fmt.Sprintf("[INFO] "+message, args...), verbosityLevel) 62 | case LogDebug: 63 | qldbLogger.logger.Log(fmt.Sprintf("[DEBUG] "+message, args...), verbosityLevel) 64 | default: 65 | qldbLogger.logger.Log(fmt.Sprintf(message, args...), verbosityLevel) 66 | } 67 | } 68 | } 69 | 70 | type defaultLogger struct{} 71 | 72 | // Log the message using the built-in Golang logging package. 73 | func (logger defaultLogger) Log(message string, verbosity LogLevel) { 74 | log.Println(message) 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '16 1 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v2 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v2 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /qldbdriver/qldbhash.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "crypto/sha256" 18 | 19 | "github.com/amzn/ion-go/ion" 20 | ionhash "github.com/amzn/ion-hash-go" 21 | ) 22 | 23 | const hashSize = 32 24 | 25 | type qldbHash struct { 26 | hash []byte 27 | } 28 | 29 | func toQLDBHash(value interface{}) (*qldbHash, error) { 30 | ionValue, err := ion.MarshalBinary(value) 31 | if err != nil { 32 | return nil, err 33 | } 34 | ionReader := ion.NewReaderBytes(ionValue) 35 | hashReader, err := ionhash.NewHashReader(ionReader, ionhash.NewCryptoHasherProvider(ionhash.SHA256)) 36 | if err != nil { 37 | return nil, err 38 | } 39 | for hashReader.Next() { 40 | // Read over value 41 | } 42 | hash, err := hashReader.Sum(nil) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return &qldbHash{hash}, nil 47 | } 48 | 49 | func (thisHash *qldbHash) dot(thatHash *qldbHash) (*qldbHash, error) { 50 | concatenated, err := joinHashesPairwise(thisHash.hash, thatHash.hash) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | newHash := sha256.Sum256(concatenated) 56 | return &qldbHash{newHash[:]}, nil 57 | } 58 | 59 | func joinHashesPairwise(h1 []byte, h2 []byte) ([]byte, error) { 60 | if len(h1) == 0 { 61 | return h2, nil 62 | } 63 | if len(h2) == 0 { 64 | return h1, nil 65 | } 66 | 67 | compare, err := hashComparator(h1, h2) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | var concatenated []byte 73 | if compare < 0 { 74 | concatenated = append(h1, h2...) 75 | } else { 76 | concatenated = append(h2, h1...) 77 | } 78 | return concatenated, nil 79 | } 80 | 81 | func hashComparator(h1 []byte, h2 []byte) (int16, error) { 82 | if len(h1) != hashSize || len(h2) != hashSize { 83 | return 0, &qldbDriverError{"invalid hash"} 84 | } 85 | for i := range h1 { 86 | // Reverse index for little endianness 87 | index := hashSize - 1 - i 88 | 89 | // Handle byte being unsigned and overflow 90 | h1Int := int16(h1[index]) 91 | h2Int := int16(h2[index]) 92 | if h1Int > 127 { 93 | h1Int = 0 - (256 - h1Int) 94 | } 95 | if h2Int > 127 { 96 | h2Int = 0 - (256 - h2Int) 97 | } 98 | 99 | difference := h1Int - h2Int 100 | if difference != 0 { 101 | return difference, nil 102 | } 103 | } 104 | return 0, nil 105 | } 106 | -------------------------------------------------------------------------------- /qldbdriver/qldbsessioniface/Interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | // Package qldbsessioniface provides an interface to enable mocking the Amazon QLDB Session service client 15 | // for testing your code. 16 | // 17 | // It is important to note that this interface will have breaking changes 18 | // when the service model is updated and adds new API operations, paginators, 19 | // and waiters. 20 | package qldbsessioniface 21 | 22 | import ( 23 | "context" 24 | 25 | "github.com/aws/aws-sdk-go-v2/service/qldbsession" 26 | ) 27 | 28 | // ClientAPI provides an interface to enable mocking the 29 | // qldbsession.Client methods. This make unit testing your code that 30 | // calls out to the SDK's service client's calls easier. 31 | // 32 | // The best way to use this interface is so the SDK's service client's calls 33 | // can be stubbed out for unit testing your code with the SDK without needing 34 | // to inject custom request handlers into the SDK's request pipeline. 35 | // 36 | // // myFunc uses an SDK service client to make a request to 37 | // // QLDB Session. 38 | // func myFunc(svc qldbsessioniface.ClientAPI) bool { 39 | // // Make svc.SendCommand request 40 | // } 41 | // 42 | // func main() { 43 | // cfg, err := external.LoadDefaultAWSConfig() 44 | // if err != nil { 45 | // panic("failed to load config, " + err.Error()) 46 | // } 47 | // 48 | // svc := qldbsession.New(cfg) 49 | // 50 | // myFunc(svc) 51 | // } 52 | // 53 | // In your _test.go file: 54 | // 55 | // // Define a mock struct to be used in your unit tests of myFunc. 56 | // type mockClientClient struct { 57 | // qldbsessioniface.ClientAPI 58 | // } 59 | // func (m *mockClientClient) SendCommand(ctx context.Context, params *qldbsession.SendCommandInput, optFns ...func(*qldbsession.Options)) (*qldbsession.SendCommandOutput, error) { 60 | // // mock response/functionality 61 | // } 62 | // 63 | // func TestMyFunc(t *testing.T) { 64 | // // Setup Test 65 | // mockSvc := &mockClientClient{} 66 | // 67 | // myfunc(mockSvc) 68 | // 69 | // // Verify myFunc's functionality 70 | // } 71 | // 72 | // It is important to note that this interface will have breaking changes 73 | // when the service model is updated and adds new API operations, paginators, 74 | // and waiters. It's suggested to use the pattern above for testing, or using 75 | // tooling to generate mocks to satisfy the interfaces. 76 | type ClientAPI interface { 77 | SendCommand(ctx context.Context, params *qldbsession.SendCommandInput, optFns ...func(*qldbsession.Options)) (*qldbsession.SendCommandOutput, error) 78 | } 79 | 80 | var _ ClientAPI = (*qldbsession.Client)(nil) 81 | -------------------------------------------------------------------------------- /qldbdriver/session.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "regexp" 20 | 21 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 22 | "github.com/aws/smithy-go" 23 | ) 24 | 25 | var regex = regexp.MustCompile(`Transaction\s.*\shas\sexpired`) 26 | 27 | type session struct { 28 | communicator qldbService 29 | logger *qldbLogger 30 | } 31 | 32 | func (session *session) endSession(ctx context.Context) error { 33 | _, err := session.communicator.endSession(ctx) 34 | return err 35 | } 36 | 37 | func (session *session) execute(ctx context.Context, fn func(txn Transaction) (interface{}, error)) (interface{}, *txnError) { 38 | txn, err := session.startTransaction(ctx) 39 | if err != nil { 40 | return nil, session.wrapError(ctx, err, "") 41 | } 42 | 43 | result, err := fn(&transactionExecutor{ctx, txn}) 44 | if err != nil { 45 | return nil, session.wrapError(ctx, err, *txn.id) 46 | } 47 | 48 | err = txn.commit(ctx) 49 | if err != nil { 50 | return nil, session.wrapError(ctx, err, *txn.id) 51 | } 52 | 53 | return result, nil 54 | } 55 | 56 | func (session *session) wrapError(ctx context.Context, err error, transID string) *txnError { 57 | var ise *types.InvalidSessionException 58 | var occ *types.OccConflictException 59 | var apiErr smithy.APIError 60 | switch { 61 | case errors.As(err, &ise): 62 | match := regex.MatchString(ise.ErrorMessage()) 63 | return &txnError{ 64 | transactionID: transID, 65 | message: "Invalid Session Exception.", 66 | err: err, 67 | canRetry: !match, 68 | abortSuccess: false, 69 | isISE: true, 70 | } 71 | case errors.As(err, &occ): 72 | return &txnError{ 73 | transactionID: transID, 74 | message: "OCC Conflict Exception.", 75 | err: err, 76 | canRetry: true, 77 | abortSuccess: true, 78 | isISE: false, 79 | } 80 | case errors.As(err, &apiErr): 81 | code := apiErr.ErrorCode() 82 | if code == "InternalFailure" || code == "ServiceUnavailable" { 83 | return &txnError{ 84 | transactionID: transID, 85 | message: "Service unavailable or internal error.", 86 | err: err, 87 | canRetry: true, 88 | abortSuccess: session.tryAbort(ctx), 89 | isISE: false, 90 | } 91 | } 92 | } 93 | return &txnError{ 94 | transactionID: transID, 95 | message: "", 96 | err: err, 97 | canRetry: false, 98 | abortSuccess: session.tryAbort(ctx), 99 | isISE: false, 100 | } 101 | } 102 | 103 | func (session *session) startTransaction(ctx context.Context) (*transaction, error) { 104 | result, err := session.communicator.startTransaction(ctx) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | txnHash, err := toQLDBHash(*result.TransactionId) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return &transaction{session.communicator, result.TransactionId, session.logger, txnHash}, nil 115 | } 116 | 117 | func (session *session) tryAbort(ctx context.Context) bool { 118 | _, err := session.communicator.abortTransaction(ctx) 119 | if err != nil { 120 | session.logger.logf(LogDebug, "Failed to abort the transaction.\nCaused by '%v'", err.Error()) 121 | return false 122 | } 123 | return true 124 | } 125 | -------------------------------------------------------------------------------- /qldbdriver/session_management_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | A copy of the License is located at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | or in the "license" file accompanying this file. This file is distributed 11 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | express or implied. See the License for the specific language governing 13 | permissions and limitations under the License. 14 | */ 15 | 16 | package qldbdriver 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "testing" 22 | "time" 23 | 24 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | "golang.org/x/sync/errgroup" 28 | ) 29 | 30 | func TestSessionManagementIntegration(t *testing.T) { 31 | if testing.Short() { 32 | t.Skip("skipping integration test") 33 | } 34 | 35 | // setup 36 | testBase := createTestBase("Golang-SessionMgmt") 37 | testBase.deleteLedger(t) 38 | testBase.waitForDeletion() 39 | testBase.createLedger(t) 40 | defer testBase.deleteLedger(t) 41 | 42 | t.Run("Fail connecting to non existent ledger", func(t *testing.T) { 43 | driver, err := testBase.getDriver(&testDriverOptions{ 44 | ledgerName: "NoSuchLedger", 45 | maxConcTx: 10, 46 | retryLimit: 4, 47 | }) 48 | require.NoError(t, err) 49 | defer driver.Shutdown(context.Background()) 50 | 51 | _, err = driver.GetTableNames(context.Background()) 52 | require.Error(t, err) 53 | 54 | var bre *types.BadRequestException 55 | assert.True(t, errors.As(err, &bre)) 56 | }) 57 | 58 | t.Run("Get session when pool doesnt have session and has not hit limit", func(t *testing.T) { 59 | driver, err := testBase.getDefaultDriver() 60 | require.NoError(t, err) 61 | defer driver.Shutdown(context.Background()) 62 | 63 | result, err := driver.GetTableNames(context.Background()) 64 | assert.NoError(t, err) 65 | assert.NotNil(t, result) 66 | }) 67 | 68 | t.Run("Get session when pool has session and has not hit limit", func(t *testing.T) { 69 | driver, err := testBase.getDefaultDriver() 70 | require.NoError(t, err) 71 | defer driver.Shutdown(context.Background()) 72 | 73 | result, err := driver.GetTableNames(context.Background()) 74 | 75 | assert.NoError(t, err) 76 | assert.NotNil(t, result) 77 | 78 | result, err = driver.GetTableNames(context.Background()) 79 | 80 | assert.NoError(t, err) 81 | assert.NotNil(t, result) 82 | }) 83 | 84 | t.Run("Get session when pool doesnt have session and has hit limit", func(t *testing.T) { 85 | driver, err := testBase.getDriver(&testDriverOptions{ 86 | ledgerName: *testBase.ledgerName, 87 | maxConcTx: 1, 88 | retryLimit: 4, 89 | }) 90 | require.NoError(t, err) 91 | driver.Shutdown(context.Background()) 92 | 93 | errs, ctx := errgroup.WithContext(context.Background()) 94 | 95 | for i := 0; i < 3; i++ { 96 | i := i 97 | errs.Go(func() error { 98 | testBase.logger.Log("start "+string(rune(i)), LogInfo) 99 | _, err := driver.GetTableNames(ctx) 100 | time.Sleep(1 * time.Second) 101 | testBase.logger.Log("end "+string(rune(i)), LogInfo) 102 | return err 103 | }) 104 | } 105 | 106 | err = errs.Wait() 107 | assert.Error(t, err) 108 | driverErr, ok := err.(*qldbDriverError) 109 | assert.True(t, ok) 110 | assert.Error(t, driverErr) 111 | }) 112 | 113 | t.Run("Get session when driver is closed", func(t *testing.T) { 114 | driver, err := testBase.getDriver(&testDriverOptions{ 115 | ledgerName: *testBase.ledgerName, 116 | maxConcTx: 1, 117 | retryLimit: 4, 118 | }) 119 | require.NoError(t, err) 120 | driver.Shutdown(context.Background()) 121 | 122 | _, err = driver.GetTableNames(context.Background()) 123 | assert.Error(t, err) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /qldbdriver/transaction.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "reflect" 20 | 21 | "github.com/amzn/ion-go/ion" 22 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 23 | ) 24 | 25 | // Transaction represents an active QLDB transaction. 26 | type Transaction interface { 27 | // Execute a statement with any parameters within this transaction. 28 | Execute(statement string, parameters ...interface{}) (Result, error) 29 | // Buffer a Result into a BufferedResult to use outside the context of this transaction. 30 | BufferResult(res Result) (BufferedResult, error) 31 | // Abort the transaction, discarding any previous statement executions within this transaction. 32 | Abort() error 33 | // Return the automatically generated transaction ID. 34 | ID() string 35 | } 36 | 37 | type transaction struct { 38 | communicator qldbService 39 | id *string 40 | logger *qldbLogger 41 | commitHash *qldbHash 42 | } 43 | 44 | func (txn *transaction) execute(ctx context.Context, statement string, parameters ...interface{}) (*result, error) { 45 | executeHash, err := toQLDBHash(statement) 46 | if err != nil { 47 | return nil, err 48 | } 49 | valueHolders := make([]types.ValueHolder, len(parameters)) 50 | for i, parameter := range parameters { 51 | parameterHash, err := toQLDBHash(parameter) 52 | if err != nil { 53 | return nil, err 54 | } 55 | executeHash, err = executeHash.dot(parameterHash) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | // Can ignore error here since toQLDBHash calls MarshalBinary already 61 | ionBinary, _ := ion.MarshalBinary(parameter) 62 | valueHolder := types.ValueHolder{IonBinary: ionBinary} 63 | valueHolders[i] = valueHolder 64 | } 65 | commitHash, err := txn.commitHash.dot(executeHash) 66 | if err != nil { 67 | return nil, err 68 | } 69 | txn.commitHash = commitHash 70 | 71 | executeResult, err := txn.communicator.executeStatement(ctx, &statement, valueHolders, txn.id) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | // create IOUsage and copy the values returned in executeResult.ConsumedIOs 77 | var ioUsage = &IOUsage{new(int64), new(int64)} 78 | if executeResult.ConsumedIOs != nil { 79 | *ioUsage.readIOs += executeResult.ConsumedIOs.ReadIOs 80 | *ioUsage.writeIOs += executeResult.ConsumedIOs.WriteIOs 81 | } 82 | // create TimingInformation and copy the values returned in executeResult.TimingInformation 83 | var timingInfo = &TimingInformation{new(int64)} 84 | if executeResult.TimingInformation != nil { 85 | *timingInfo.processingTimeMilliseconds = executeResult.TimingInformation.ProcessingTimeMilliseconds 86 | } 87 | 88 | return &result{ctx, txn.communicator, txn.id, executeResult.FirstPage.Values, executeResult.FirstPage.NextPageToken, 0, txn.logger, nil, ioUsage, timingInfo, nil}, nil 89 | } 90 | 91 | func (txn *transaction) commit(ctx context.Context) error { 92 | commitResult, err := txn.communicator.commitTransaction(ctx, txn.id, txn.commitHash.hash) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | if !reflect.DeepEqual(commitResult.CommitDigest, txn.commitHash.hash) { 98 | return &qldbDriverError{ 99 | errorMessage: "Transaction's commit digest did not match returned value from QLDB. Please retry with a new transaction.", 100 | } 101 | } 102 | 103 | return nil 104 | } 105 | 106 | type transactionExecutor struct { 107 | ctx context.Context 108 | txn *transaction 109 | } 110 | 111 | // Execute a statement with any parameters within this transaction. 112 | func (executor *transactionExecutor) Execute(statement string, parameters ...interface{}) (Result, error) { 113 | return executor.txn.execute(executor.ctx, statement, parameters...) 114 | } 115 | 116 | // Buffer a Result into a BufferedResult to use outside the context of this transaction. 117 | func (executor *transactionExecutor) BufferResult(result Result) (BufferedResult, error) { 118 | bufferedResults := make([][]byte, 0) 119 | for result.Next(executor) { 120 | bufferedResults = append(bufferedResults, result.GetCurrentData()) 121 | } 122 | if result.Err() != nil { 123 | return nil, result.Err() 124 | } 125 | 126 | return &bufferedResult{bufferedResults, 0, nil, result.GetConsumedIOs(), result.GetTimingInformation()}, nil 127 | } 128 | 129 | // Abort the transaction, discarding any previous statement executions within this transaction. 130 | func (executor *transactionExecutor) Abort() error { 131 | return errors.New("transaction aborted") 132 | } 133 | 134 | // Return the automatically generated transaction ID. 135 | func (executor *transactionExecutor) ID() string { 136 | return *executor.txn.id 137 | } 138 | -------------------------------------------------------------------------------- /qldbdriver/communicator.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/aws/aws-sdk-go-v2/aws" 20 | "github.com/aws/aws-sdk-go-v2/aws/middleware" 21 | "github.com/aws/aws-sdk-go-v2/service/qldbsession" 22 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 23 | "github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver/qldbsessioniface" 24 | ) 25 | 26 | const version string = "3.0.1" 27 | const userAgentString string = "QLDB Driver for Golang v" + version 28 | 29 | type qldbService interface { 30 | abortTransaction(ctx context.Context) (*types.AbortTransactionResult, error) 31 | commitTransaction(ctx context.Context, txnID *string, commitDigest []byte) (*types.CommitTransactionResult, error) 32 | executeStatement(ctx context.Context, statement *string, parameters []types.ValueHolder, txnID *string) (*types.ExecuteStatementResult, error) 33 | endSession(context.Context) (*types.EndSessionResult, error) 34 | fetchPage(ctx context.Context, pageToken *string, txnID *string) (*types.FetchPageResult, error) 35 | startTransaction(ctx context.Context) (*types.StartTransactionResult, error) 36 | } 37 | 38 | type communicator struct { 39 | service qldbsessioniface.ClientAPI 40 | sessionToken *string 41 | logger *qldbLogger 42 | } 43 | 44 | func startSession(ctx context.Context, ledgerName string, service qldbsessioniface.ClientAPI, logger *qldbLogger) (*communicator, error) { 45 | startSession := &types.StartSessionRequest{LedgerName: &ledgerName} 46 | sendInput := &qldbsession.SendCommandInput{StartSession: startSession} 47 | result, err := service.SendCommand(ctx, sendInput, func(options *qldbsession.Options) { 48 | options.Retryer = aws.NopRetryer{} 49 | options.APIOptions = append(options.APIOptions, middleware.AddUserAgentKey(userAgentString)) 50 | }) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return &communicator{service, result.StartSession.SessionToken, logger}, nil 55 | } 56 | 57 | func (communicator *communicator) abortTransaction(ctx context.Context) (*types.AbortTransactionResult, error) { 58 | abortTransaction := &types.AbortTransactionRequest{} 59 | sendInput := &qldbsession.SendCommandInput{AbortTransaction: abortTransaction} 60 | result, err := communicator.sendCommand(ctx, sendInput) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return result.AbortTransaction, nil 65 | } 66 | 67 | func (communicator *communicator) commitTransaction(ctx context.Context, txnID *string, commitDigest []byte) (*types.CommitTransactionResult, error) { 68 | commitTransaction := &types.CommitTransactionRequest{TransactionId: txnID, CommitDigest: commitDigest} 69 | sendInput := &qldbsession.SendCommandInput{CommitTransaction: commitTransaction} 70 | result, err := communicator.sendCommand(ctx, sendInput) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return result.CommitTransaction, nil 75 | } 76 | 77 | func (communicator *communicator) executeStatement(ctx context.Context, statement *string, parameters []types.ValueHolder, txnID *string) (*types.ExecuteStatementResult, error) { 78 | executeStatement := &types.ExecuteStatementRequest{ 79 | Parameters: parameters, 80 | Statement: statement, 81 | TransactionId: txnID, 82 | } 83 | sendInput := &qldbsession.SendCommandInput{ExecuteStatement: executeStatement} 84 | result, err := communicator.sendCommand(ctx, sendInput) 85 | if err != nil { 86 | return nil, err 87 | } 88 | return result.ExecuteStatement, nil 89 | } 90 | 91 | func (communicator *communicator) endSession(ctx context.Context) (*types.EndSessionResult, error) { 92 | endSession := &types.EndSessionRequest{} 93 | sendInput := &qldbsession.SendCommandInput{EndSession: endSession} 94 | result, err := communicator.sendCommand(ctx, sendInput) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return result.EndSession, nil 99 | } 100 | 101 | func (communicator *communicator) fetchPage(ctx context.Context, pageToken *string, txnID *string) (*types.FetchPageResult, error) { 102 | fetchPage := &types.FetchPageRequest{NextPageToken: pageToken, TransactionId: txnID} 103 | sendInput := &qldbsession.SendCommandInput{FetchPage: fetchPage} 104 | result, err := communicator.sendCommand(ctx, sendInput) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return result.FetchPage, nil 109 | } 110 | 111 | func (communicator *communicator) startTransaction(ctx context.Context) (*types.StartTransactionResult, error) { 112 | startTransaction := &types.StartTransactionRequest{} 113 | sendInput := &qldbsession.SendCommandInput{StartTransaction: startTransaction} 114 | result, err := communicator.sendCommand(ctx, sendInput) 115 | if err != nil { 116 | return nil, err 117 | } 118 | return result.StartTransaction, nil 119 | } 120 | 121 | func (communicator *communicator) sendCommand(ctx context.Context, command *qldbsession.SendCommandInput) (*qldbsession.SendCommandOutput, error) { 122 | command.SessionToken = communicator.sessionToken 123 | communicator.logger.logf(LogDebug, "%v", command) 124 | return communicator.service.SendCommand(ctx, command, func(options *qldbsession.Options) { 125 | options.Retryer = aws.NopRetryer{} 126 | options.APIOptions = append(options.APIOptions, middleware.AddUserAgentKey(userAgentString)) 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.0.1 (2022-11-08) 2 | 3 | ## :bug: Fixed 4 | 5 | * Fix default backoff jitter taking too much time [PR 122](https://github.com/awslabs/amazon-qldb-driver-go/pull/122). 6 | 7 | # 3.0.0 (2022-08-11) 8 | 9 | ## :tada: Enhancements 10 | 11 | * Migrate to [AWS SDK for Go V2](https://github.com/aws/aws-sdk-go-v2). 12 | 13 | ## :boom: Breaking changes 14 | 15 | > All the breaking changes are introduced by SDK V2, please check [Migrating to the AWS SDK for Go V2](https://aws.github.io/aws-sdk-go-v2/docs/migrating/) to learn how to migrate to the AWS SDK for Go V2 from AWS SDK for Go V1. 16 | 17 | * Bumped minimum Go version from `1.14` to `1.15` as required by SDK V2. 18 | * Changed driver constructor to take a new type of `qldbSession` client. Application code needs to be modified for [qldbSession client]( https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/qldbsession) construction. 19 | For example, the following: 20 | ```go 21 | import "github.com/aws/aws-sdk-go/aws/session" 22 | import "github.com/aws/aws-sdk-go/service/qldbSession" 23 | 24 | // ... 25 | 26 | sess, err := session.NewSession() 27 | if err != nil { 28 | // handle error 29 | } 30 | 31 | client := s3.New(sess) 32 | ``` 33 | 34 | Should be changed to 35 | 36 | ```go 37 | import "context" 38 | import "github.com/aws/aws-sdk-go-v2/config" 39 | import "github.com/aws/aws-sdk-go-v2/service/qldbSession" 40 | 41 | // ... 42 | 43 | cfg, err := config.LoadDefaultConfig(context.TODO()) 44 | if err != nil { 45 | // handle error 46 | } 47 | qldbSession := qldbsession.NewFromConfig(cfg) 48 | ``` 49 | * The driver now returns modeled service errors that could be found in `qldbSession` client [types](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/qldbsession/types). Application code which uses a type assertion or type switch to check error types should be updated to use [errors.As](https://pkg.go.dev/errors#As) to test whether the returned operation error is a modeled service error. For more details about error type changes in the AWS SDK for Go V2, see [Error Types](https://aws.github.io/aws-sdk-go-v2/docs/migrating/#errors-types). 50 | 51 | 52 | # 2.0.2 (2021-07-21) 53 | 54 | Releases v2.0.0 and v2.0.1 were skipped due to instability issues. 55 | 56 | ## :tada: Enhancements 57 | 58 | * Bumped Ion Go dependency to `v1.1.3` allow support for unmarshalling Ion timestamps to Go time objects. 59 | 60 | ## :boom: Breaking changes 61 | 62 | * The `Logger` interface's `Log` method now takes in a `LogLevel` to specify the logging verbosity. Any instances of `Logger.Log()` will need to be updated accordingly. 63 | 64 | ie. 65 | ```go 66 | logger.Log("Log Message") 67 | ``` 68 | 69 | should be updated to 70 | 71 | ```go 72 | logger.Log("Log Message", qldbdriver.LogInfo) 73 | ``` 74 | 75 | * `Result` and `BufferedResult` have changed from struct types to interface types. As a consequence of this change, the `Transaction` interface's `Execute()` and `BufferResult()` methods respectively return `Result` and `BufferedResult` rather than `*Result` and `*BufferedResult`. Any logic dereferencing or casting to a `Result` or `BufferedResult` will need to be updated accordingly. 76 | 77 | ie. 78 | ```go 79 | result.(*BufferedResult) 80 | ``` 81 | 82 | should be updated to 83 | 84 | ```go 85 | result.(BufferedResult) 86 | ``` 87 | 88 | * The `Transaction` interface has a new `ID()` method for exposing the transaction ID. Any implementations of this interface will need a new `ID() string` method defined. 89 | 90 | # 1.1.1 (2021-06-16) 91 | 92 | ## :bug: Fixed 93 | 94 | * Bumped Ion Go to `v1.1.2` and Ion Hash Go to `v1.1.1` to fix a bug where inserting certain timestamps would throw an error. 95 | * Prevent mutation of `*qldbsession.QLDBSession` after passing into QLDBDriver constructor. 96 | * Allow users to wrap `awserr.RequestFailure` within a transaction lambda and support retryable errors. 97 | 98 | # 1.1.0 (2021-02-25) 99 | 100 | ## :tada: Enhancements 101 | 102 | * Updated the AWS SDK dependency to v1.37.8 to support [CapacityExceededException](https://docs.aws.amazon.com/qldb/latest/developerguide/driver-errors.html). This exception will better inform users that they are overloading their ledger. 103 | * Statements that return exceptions containing status code 500 and 503 will now be retried. 104 | * User agent string is now included in start session requests. 105 | * Added `IOUsage` and `TimingInformation` structs to provide server-side execution statistics 106 | * `IOUsage` provides `GetReadIOs` method 107 | * `TimingInformation` provides `GetProcessingTimeMilliseconds` method 108 | * Added `GetConsumedIOs` and `GetTimingInformation` methods in `BufferedResult` and `Result` structs 109 | * `GetConsumedIOs` and `GetTimingInformation` methods are stateful, meaning the statistics returned by them reflect the state at the time of method execution. 110 | 111 | # 1.0.1 (2020-10-27) 112 | 113 | * Fixed the dates in this CHANGELOG.md file 114 | * Updated the driver version in the user agent string to 1.0.1. 115 | 116 | # 1.0.0 (2020-10-20) 117 | 118 | The release candidate (v1.0.0-rc.1) has been selected as a final release of v1.0.0 with the following change: 119 | 120 | ## :boom: Breaking changes 121 | 122 | * `QLDBDriverError` struct is no longer exported and has been updated to `qldbDriverError`. 123 | 124 | # 1.0.0-rc.1 (2020-10-06) 125 | 126 | ## :tada: Enhancements 127 | 128 | * Improved the iterator pattern for `Result.Next`. More details can be found in the [release notes](https://github.com/awslabs/amazon-qldb-driver-go/releases/tag/v1.0.0-rc.1) 129 | * Removed panics in the driver. We can handle more errors gracefully now. 130 | 131 | ## :boom: Breaking changes 132 | 133 | * Updated `QldbDriver.New` function to return `(QLDBDriver, error)`. 134 | * Renamed `QldbDriver.Close` function to `QldbDriver.Shutdown`. 135 | * Removed `QLDBDriver.ExecuteWithRetryPolicy` function. 136 | * Removed `RetryPolicyContext` struct. `BackoffStrategy.Delay` function now takes in an `int` parameter as retry attempt. 137 | * The `SleepBase` and `SleepCap` fields in struct type `ExponentialBackoffStrategy` have been updated to type `time.Duration`. 138 | 139 | # 0.1.0 (2020-08-06) 140 | 141 | * Preview release of the driver. 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon QLDB Go Driver 2 | 3 | [![license](https://img.shields.io/badge/license-Apache%202.0-blue)](https://github.com/awslabs/amazon-qldb-driver-go/blob/master/LICENSE) 4 | [![AWS Provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900)](https://aws.amazon.com/qldb/) 5 | 6 | [![Go Build](https://github.com/awslabs/amazon-qldb-driver-go/actions/workflows/go.yml/badge.svg)](https://github.com/awslabs/amazon-qldb-driver-go/actions/workflows/go.yml) 7 | [![CodeQL](https://github.com/awslabs/amazon-qldb-driver-go/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/awslabs/amazon-qldb-driver-go/actions/workflows/codeql-analysis.yml) 8 | 9 | This is the Go driver for [Amazon Quantum Ledger Database (QLDB)](https://aws.amazon.com/qldb/), which allows Golang developers 10 | to write software that makes use of Amazon QLDB. 11 | 12 | For getting started with the driver, see [Go and Amazon QLDB](https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.golang.html). 13 | 14 | ## Requirements 15 | 16 | ### Basic Configuration 17 | 18 | See [Accessing Amazon QLDB](https://docs.aws.amazon.com/qldb/latest/developerguide/accessing.html) for information on connecting to AWS. 19 | 20 | ### Required Golang versions 21 | 22 | QldbDriver requires Golang 1.20 or later. 23 | 24 | Please see the link below for more detail to install Golang: 25 | 26 | * [Golang Download](https://golang.org/dl/) 27 | 28 | ## Getting Started 29 | 30 | Please see the [Quickstart guide for the Amazon QLDB Driver for Go](https://docs.aws.amazon.com/qldb/latest/developerguide/driver-quickstart-golang.html). 31 | 32 | First, ensure your project is using [Go modules](https://blog.golang.org/using-go-modules) to install dependencies in your project. 33 | 34 | In your project directory, install the driver using go get: 35 | 36 | ```go get github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver``` 37 | 38 | For more instructions on working with the golang driver, please refer to the instructions below. 39 | 40 | ### See Also 41 | 42 | 1. [Getting Started with Amazon QLDB Go Driver](https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.golang.html) A guide that gets you started with executing transactions with the QLDB Go driver. 43 | 2. For a quick start on how to interact with the driver, please refer to [Go Driver Quick Start](https://docs.aws.amazon.com/qldb/latest/developerguide/driver-quickstart-golang.html). 44 | 3. [QLDB Go Driver Cookbook](https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-golang.html) The cookbook provides code samples for some simple QLDB Go driver use cases. 45 | 4. QLDB Golang driver accepts and returns [Amazon ION](http://amzn.github.io/ion-docs/) Documents. Amazon Ion is a richly-typed, self-describing, hierarchical data serialization format offering interchangeable binary and text representations. For more information read the [ION docs](https://readthedocs.org/projects/ion-python/). 46 | 5. Amazon QLDB supports a subset of the [PartiQL](https://partiql.org/) query language. PartiQL provides SQL-compatible query access across multiple data stores containing structured data, semistructured data, and nested data. For more information read the [docs](https://docs.aws.amazon.com/qldb/latest/developerguide/ql-reference.html). 47 | 6. Refer the section [Common Errors while using the Amazon QLDB Drivers](https://docs.aws.amazon.com/qldb/latest/developerguide/driver-errors.html) which describes runtime errors that can be thrown by the Amazon QLDB Driver when calling the qldb-session APIs. 48 | 7. **Driver Recommendations** — Check them out in the [Best Practices](https://docs.aws.amazon.com/qldb/latest/developerguide/driver.best-practices.html) 49 | in the QLDB documentation. 50 | 8. **Exception handling when using QLDB Drivers** — Refer to the section [Common Errors while using the Amazon 51 | QLDB Drivers](https://docs.aws.amazon.com/qldb/latest/developerguide/driver-errors.html) 52 | which describes runtime exceptions that can be thrown by the Amazon QLDB Driver when calling the qldb-session APIs. 53 | 54 | ## Development 55 | 56 | ### Setup 57 | Assuming that you have Golang installed, use the below command to clone the driver repository. 58 | 59 | ``` 60 | $ git clone https://github.com/awslabs/amazon-qldb-driver-go.git 61 | $ cd amazon-qldb-driver-go 62 | ``` 63 | Changes can now be made in the repository. 64 | ### Running Tests 65 | 66 | All the tests can be run by running the following command in the qldbdriver folder. Please make sure to setup and configure an AWS account to run the integration tests. 67 | ``` 68 | go test -v 69 | ``` 70 | 71 | To only run the unit tests: 72 | 73 | ``` 74 | go test -v -short 75 | ``` 76 | 77 | To only run the integration tests: 78 | 79 | ``` 80 | go test -run Integration 81 | ``` 82 | 83 | ### Documentation 84 | 85 | The Amazon QLDB Go Driver adheres to GoDoc standards and the documentation can be found [here](https://pkg.go.dev/github.com/awslabs/amazon-qldb-driver-go/qldbdriver?tab=doc). 86 | 87 | You can generate the docstring HTML locally by running the following in the root directory of this repository: 88 | ```godoc -http=:6060``` 89 | 90 | ## Getting Help 91 | 92 | Please use these community resources for getting help. 93 | * Ask a question on StackOverflow and tag it with the [amazon-qldb](https://stackoverflow.com/questions/tagged/amazon-qldb) tag. 94 | * Open a support ticket with [AWS Support](http://docs.aws.amazon.com/awssupport/latest/user/getting-started.html). 95 | * Make a new thread at [AWS QLDB Forum](https://forums.aws.amazon.com/forum.jspa?forumID=353&start=0). 96 | * If you think you may have found a bug, please open an [issue](https://github.com/awslabs/amazon-qldb-driver-go/issues/new). 97 | 98 | ## Opening Issues 99 | 100 | If you encounter a bug with the Amazon QLDB Go Driver, we would like to hear about it. Please search the [existing issues](https://github.com/awslabs/amazon-qldb-driver-go/issues) and see if others are also experiencing the issue before opening a new issue. When opening a new issue, we will need the version of Amazon QLDB Go Driver, Go language version, and OS you’re using. Please also include reproduction case for the issue when appropriate. 101 | 102 | The GitHub issues are intended for bug reports and feature requests. For help and questions with using AWS QLDB GO Driver please make use of the resources listed in the [Getting Help](https://github.com/awslabs/amazon-qldb-driver-go#getting-help) section. Keeping the list of open issues lean will help us respond in a timely manner. 103 | 104 | ## License 105 | 106 | This library is licensed under the Apache 2.0 License. 107 | -------------------------------------------------------------------------------- /qldbdriver/integration_base_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | A copy of the License is located at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | or in the "license" file accompanying this file. This file is distributed 11 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | express or implied. See the License for the specific language governing 13 | permissions and limitations under the License. 14 | */ 15 | 16 | package qldbdriver 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "flag" 22 | "fmt" 23 | "strings" 24 | "testing" 25 | "time" 26 | 27 | "github.com/aws/aws-sdk-go-v2/config" 28 | "github.com/aws/aws-sdk-go-v2/service/qldb" 29 | "github.com/aws/aws-sdk-go-v2/service/qldb/types" 30 | "github.com/aws/aws-sdk-go-v2/service/qldbsession" 31 | "github.com/aws/smithy-go" 32 | "github.com/stretchr/testify/assert" 33 | ) 34 | 35 | type testBase struct { 36 | qldb *qldb.Client 37 | ledgerName *string 38 | regionName *string 39 | logger Logger 40 | } 41 | 42 | const ( 43 | region = "us-east-2" 44 | testTableName = "GoIntegrationTestTable" 45 | indexAttribute = "Name" 46 | columnName = "Name" 47 | singleDocumentValue = "SingleDocumentValue" 48 | multipleDocumentValue1 = "MultipleDocumentValue1" 49 | multipleDocumentValue2 = "MultipleDocumentValue2" 50 | ) 51 | 52 | var ledgerSuffix = flag.String("ledger_suffix", "", "Suffix to the ledger name") 53 | 54 | func createTestBase(ledgerNameBase string) *testBase { 55 | 56 | cfg, err := config.LoadDefaultConfig(context.TODO()) 57 | if err != nil { 58 | panic(err) 59 | } 60 | client := qldb.NewFromConfig(cfg, func(options *qldb.Options) { 61 | options.Region = region 62 | }) 63 | logger := defaultLogger{} 64 | ledgerName := ledgerNameBase + *ledgerSuffix 65 | regionName := region 66 | return &testBase{client, &ledgerName, ®ionName, logger} 67 | } 68 | 69 | func (testBase *testBase) createLedger(t *testing.T) { 70 | testBase.logger.Log(fmt.Sprint("Creating ledger named ", *testBase.ledgerName, " ..."), LogInfo) 71 | _, err := testBase.qldb.CreateLedger(context.TODO(), &qldb.CreateLedgerInput{ 72 | Name: testBase.ledgerName, 73 | DeletionProtection: newBool(false), 74 | PermissionsMode: types.PermissionsModeStandard, 75 | }) 76 | assert.NoError(t, err) 77 | testBase.waitForActive() 78 | } 79 | 80 | func (testBase *testBase) deleteLedger(t *testing.T) { 81 | testBase.logger.Log(fmt.Sprint("Deleting ledger ", *testBase.ledgerName), LogInfo) 82 | _, err := testBase.qldb.DeleteLedger(context.TODO(), &qldb.DeleteLedgerInput{Name: testBase.ledgerName}) 83 | if err != nil { 84 | var rnf *types.ResourceNotFoundException 85 | if errors.As(err, &rnf) { 86 | testBase.logger.Log("Encountered resource not found", LogInfo) 87 | return 88 | } 89 | var riu *types.ResourceInUseException 90 | if errors.As(err, &riu) { 91 | if strings.Contains(riu.ErrorMessage(), "Ledger is being created") { 92 | testBase.logger.Log("Encountered resource still being created", LogInfo) 93 | testBase.waitForActive() 94 | _, err = testBase.qldb.DeleteLedger(context.TODO(), &qldb.DeleteLedgerInput{Name: testBase.ledgerName}) 95 | } else if strings.Contains(riu.ErrorMessage(), "Ledger is being deleted") { 96 | err = nil 97 | } 98 | } 99 | if err != nil { 100 | testBase.logger.Log("Encountered error during deletion", LogInfo) 101 | testBase.logger.Log(err.Error(), LogInfo) 102 | t.Errorf("Failing test due to deletion failure") 103 | assert.NoError(t, err) 104 | return 105 | } 106 | } 107 | } 108 | 109 | func (testBase *testBase) waitForActive() { 110 | testBase.logger.Log("Waiting for ledger to become active...", LogInfo) 111 | for { 112 | output, _ := testBase.qldb.DescribeLedger(context.TODO(), &qldb.DescribeLedgerInput{Name: testBase.ledgerName}) 113 | if output.State == "ACTIVE" { 114 | testBase.logger.Log("Success. Ledger is active and ready to use.", LogInfo) 115 | return 116 | } 117 | testBase.logger.Log("The ledger is still creating. Please wait...", LogInfo) 118 | time.Sleep(5 * time.Second) 119 | } 120 | } 121 | 122 | func (testBase *testBase) waitForDeletion() { 123 | testBase.logger.Log("Waiting for ledger to be deleted...", LogInfo) 124 | for { 125 | _, err := testBase.qldb.DescribeLedger(context.TODO(), &qldb.DescribeLedgerInput{Name: testBase.ledgerName}) 126 | testBase.logger.Log("The ledger is still deleting. Please wait...", LogInfo) 127 | if err != nil { 128 | var rnf *types.ResourceNotFoundException 129 | if errors.As(err, &rnf) { 130 | testBase.logger.Log("The ledger is deleted", LogInfo) 131 | return 132 | } 133 | } 134 | time.Sleep(5 * time.Second) 135 | } 136 | } 137 | 138 | func (testBase *testBase) getDefaultDriver() (*QLDBDriver, error) { 139 | return testBase.getDriver(&testDriverOptions{ 140 | ledgerName: *testBase.ledgerName, 141 | maxConcTx: 10, 142 | retryLimit: 4, 143 | }) 144 | } 145 | 146 | type testDriverOptions struct { 147 | ledgerName string 148 | maxConcTx int 149 | retryLimit int 150 | } 151 | 152 | func (testBase *testBase) getDriver(tdo *testDriverOptions) (*QLDBDriver, error) { 153 | 154 | cfg, err := config.LoadDefaultConfig(context.TODO()) 155 | if err != nil { 156 | panic(err) 157 | } 158 | qldbSession := qldbsession.NewFromConfig(cfg, func(options *qldbsession.Options) { 159 | options.Region = region 160 | }) 161 | 162 | return New(tdo.ledgerName, qldbSession, func(options *DriverOptions) { 163 | options.LoggerVerbosity = LogInfo 164 | options.MaxConcurrentTransactions = tdo.maxConcTx 165 | options.RetryPolicy.MaxRetryLimit = tdo.retryLimit 166 | }) 167 | } 168 | 169 | var ErrCodeInvalidSessionException = "InvalidSessionException" 170 | var ErrMessageInvalidSessionException = "Invalid session" 171 | var ErrCodeInvalidSessionException2 = "Transaction 23EA3C089B23423D has expired" 172 | var ErrMessageOccConflictException = "OCC" 173 | var ErrCodeBadRequestException = "BadRequestException" 174 | var ErrMessageBadRequestException = "Bad request" 175 | var ErrCodeInternalFailure = "InternalFailure" 176 | var ErrMessageInternalFailure = "Five Hundred" 177 | var ErrMessageCapacityExceedException = "Capacity Exceeded" 178 | 179 | // InternalFailure is used to mock 500s exception in tests 180 | type InternalFailure struct { 181 | Message *string 182 | Code *string 183 | } 184 | 185 | func (e *InternalFailure) Error() string { 186 | return fmt.Sprintf("%s: %s", e.ErrorCode(), e.ErrorMessage()) 187 | } 188 | func (e *InternalFailure) ErrorMessage() string { 189 | if e.Message == nil { 190 | return "" 191 | } 192 | return *e.Message 193 | } 194 | func (e *InternalFailure) ErrorCode() string { return "InternalFailure" } 195 | func (e *InternalFailure) ErrorFault() smithy.ErrorFault { return smithy.FaultServer } 196 | 197 | func newBool(b bool) *bool { return &b } 198 | -------------------------------------------------------------------------------- /qldbdriver/result.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 20 | ) 21 | 22 | // Result is a cursor over a result set from a QLDB statement. 23 | type Result interface { 24 | Next(txn Transaction) bool 25 | GetCurrentData() []byte 26 | GetConsumedIOs() *IOUsage 27 | GetTimingInformation() *TimingInformation 28 | Err() error 29 | } 30 | 31 | type result struct { 32 | ctx context.Context 33 | communicator qldbService 34 | txnID *string 35 | pageValues []types.ValueHolder 36 | pageToken *string 37 | index int 38 | logger *qldbLogger 39 | ionBinary []byte 40 | ioUsage *IOUsage 41 | timingInfo *TimingInformation 42 | err error 43 | } 44 | 45 | // Next advances to the next row of data in the current result set. 46 | // Returns true if there was another row of data to advance. Returns false if there is no more data or if an error occurred. 47 | // After a successful call to Next, call GetCurrentData to retrieve the current row of data. 48 | // After an unsuccessful call to Next, check Err to see if Next returned false because an error happened or because there is no more data. 49 | func (result *result) Next(txn Transaction) bool { 50 | result.ionBinary = nil 51 | result.err = nil 52 | 53 | if result.index >= len(result.pageValues) { 54 | if result.pageToken == nil { 55 | // No more data left 56 | return false 57 | } 58 | result.err = result.getNextPage() 59 | if result.err != nil { 60 | return false 61 | } 62 | return result.Next(txn) 63 | } 64 | 65 | result.ionBinary = result.pageValues[result.index].IonBinary 66 | result.index++ 67 | 68 | return true 69 | } 70 | 71 | func (result *result) getNextPage() error { 72 | nextPage, err := result.communicator.fetchPage(result.ctx, result.pageToken, result.txnID) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | result.pageValues = nextPage.Page.Values 78 | result.pageToken = nextPage.Page.NextPageToken 79 | result.index = 0 80 | result.updateMetrics(nextPage) 81 | return nil 82 | } 83 | 84 | func (result *result) updateMetrics(fetchPageResult *types.FetchPageResult) { 85 | if fetchPageResult.ConsumedIOs != nil { 86 | *result.ioUsage.readIOs += fetchPageResult.ConsumedIOs.ReadIOs 87 | *result.ioUsage.writeIOs += fetchPageResult.ConsumedIOs.WriteIOs 88 | } 89 | 90 | if fetchPageResult.TimingInformation != nil { 91 | *result.timingInfo.processingTimeMilliseconds += fetchPageResult.TimingInformation.ProcessingTimeMilliseconds 92 | } 93 | } 94 | 95 | // GetConsumedIOs returns the statement statistics for the current number of read IO requests that were consumed. The statistics are stateful. 96 | func (result *result) GetConsumedIOs() *IOUsage { 97 | if result.ioUsage == nil { 98 | return nil 99 | } 100 | return newIOUsage(*result.ioUsage.readIOs, *result.ioUsage.writeIOs) 101 | } 102 | 103 | // GetTimingInformation returns the statement statistics for the current server-side processing time. The statistics are stateful. 104 | func (result *result) GetTimingInformation() *TimingInformation { 105 | if result.timingInfo == nil { 106 | return nil 107 | } 108 | return newTimingInformation(*result.timingInfo.processingTimeMilliseconds) 109 | } 110 | 111 | // GetCurrentData returns the current row of data in Ion format. Use ion.Unmarshal or other Ion library methods to handle parsing. 112 | // See https://github.com/amzn/ion-go for more information. 113 | func (result *result) GetCurrentData() []byte { 114 | return result.ionBinary 115 | } 116 | 117 | // Err returns an error if a previous call to Next has failed. 118 | // The returned error will be nil if the previous call to Next succeeded. 119 | func (result *result) Err() error { 120 | return result.err 121 | } 122 | 123 | // BufferedResult is a cursor over a result set from a QLDB statement that is valid outside the context of a transaction. 124 | type BufferedResult interface { 125 | Next() bool 126 | GetCurrentData() []byte 127 | GetConsumedIOs() *IOUsage 128 | GetTimingInformation() *TimingInformation 129 | } 130 | 131 | type bufferedResult struct { 132 | values [][]byte 133 | index int 134 | ionBinary []byte 135 | ioUsage *IOUsage 136 | timingInfo *TimingInformation 137 | } 138 | 139 | // Next advances to the next row of data in the current result set. 140 | // Returns true if there was another row of data to advance. Returns false if there is no more data. 141 | // After a successful call to Next, call GetCurrentData to retrieve the current row of data. 142 | func (result *bufferedResult) Next() bool { 143 | result.ionBinary = nil 144 | 145 | if result.index >= len(result.values) { 146 | return false 147 | } 148 | 149 | result.ionBinary = result.values[result.index] 150 | result.index++ 151 | return true 152 | } 153 | 154 | // GetCurrentData returns the current row of data in Ion format. Use ion.Unmarshal or other Ion library methods to handle parsing. 155 | // See https://github.com/amzn/ion-go for more information. 156 | func (result *bufferedResult) GetCurrentData() []byte { 157 | return result.ionBinary 158 | } 159 | 160 | // GetConsumedIOs returns the statement statistics for the total number of read IO requests that were consumed. 161 | func (result *bufferedResult) GetConsumedIOs() *IOUsage { 162 | if result.ioUsage == nil { 163 | return nil 164 | } 165 | return newIOUsage(*result.ioUsage.readIOs, *result.ioUsage.writeIOs) 166 | } 167 | 168 | // GetTimingInformation returns the statement statistics for the total server-side processing time. 169 | func (result *bufferedResult) GetTimingInformation() *TimingInformation { 170 | if result.timingInfo == nil { 171 | return nil 172 | } 173 | return newTimingInformation(*result.timingInfo.processingTimeMilliseconds) 174 | } 175 | 176 | // IOUsage contains metrics for the amount of IO requests that were consumed. 177 | type IOUsage struct { 178 | readIOs *int64 179 | writeIOs *int64 180 | } 181 | 182 | // newIOUsage creates a new instance of IOUsage. 183 | func newIOUsage(readIOs int64, writeIOs int64) *IOUsage { 184 | return &IOUsage{&readIOs, &writeIOs} 185 | } 186 | 187 | // GetReadIOs returns the number of read IO requests that were consumed for a statement execution. 188 | func (ioUsage *IOUsage) GetReadIOs() *int64 { 189 | return ioUsage.readIOs 190 | } 191 | 192 | // getWriteIOs returns the number of write IO requests that were consumed for a statement execution. 193 | func (ioUsage *IOUsage) getWriteIOs() *int64 { 194 | return ioUsage.writeIOs 195 | } 196 | 197 | // TimingInformation contains metrics for server-side processing time. 198 | type TimingInformation struct { 199 | processingTimeMilliseconds *int64 200 | } 201 | 202 | // newTimingInformation creates a new instance of TimingInformation. 203 | func newTimingInformation(processingTimeMilliseconds int64) *TimingInformation { 204 | return &TimingInformation{&processingTimeMilliseconds} 205 | } 206 | 207 | // GetProcessingTimeMilliseconds returns the server-side processing time in milliseconds for a statement execution. 208 | func (timingInfo *TimingInformation) GetProcessingTimeMilliseconds() *int64 { 209 | return timingInfo.processingTimeMilliseconds 210 | } 211 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/amzn/ion-go v1.1.3 h1:gGhjtLY0GUNQXej5N2qHhoVWQBkgtoPDt1feYYFMfOc= 2 | github.com/amzn/ion-go v1.1.3/go.mod h1:7wQBWQ7PhPpZCr9PL+mtuIyNmyLjuV8qt2mrfxmvkA8= 3 | github.com/amzn/ion-hash-go v1.2.0 h1:4pqJj2fUjhilWPmxMm+4tb4/OXicc6sqcrpfr8AtRRE= 4 | github.com/amzn/ion-hash-go v1.2.0/go.mod h1:2lu+vG/SVoiHK9uvZRZ1upMUx+kZwEu74IlkzsDVauM= 5 | github.com/aws/aws-sdk-go-v2 v1.22.1 h1:sjnni/AuoTXxHitsIdT0FwmqUuNUuHtufcVDErVFT9U= 6 | github.com/aws/aws-sdk-go-v2 v1.22.1/go.mod h1:Kd0OJtkW3Q0M0lUWGszapWjEvrXDzRW+D21JNsroB+c= 7 | github.com/aws/aws-sdk-go-v2/config v1.22.1 h1:UrRYnF7mXCGuKmZWlczOXeH0WUbQpi/gseQIPtrhme8= 8 | github.com/aws/aws-sdk-go-v2/config v1.22.1/go.mod h1:2eWgw5lps8fKI7LZVTrRTYP6HE6k/uEFUuTSHfXwqP0= 9 | github.com/aws/aws-sdk-go-v2/credentials v1.15.1 h1:hmf6lAm9hk7uLCfapZn/jL05lm6Uwdbn1B0fgjyuf4M= 10 | github.com/aws/aws-sdk-go-v2/credentials v1.15.1/go.mod h1:QTcHga3ZbQOneJuxmGBOCxiClxmp+TlvmjFexAnJ790= 11 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.2 h1:gIeH4+o1MN/caGBWjoGQTUTIu94xD6fI5B2+TcwBf70= 12 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.2/go.mod h1:wLyMIo/zPOhQhPXTddpfdkSleyigtFi8iMnC+2m/SK4= 13 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.1 h1:fi1ga6WysOyYb5PAf3Exd6B5GiSNpnZim4h1rhlBqx0= 14 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.1/go.mod h1:V5CY8wNurvPUibTi9mwqUqpiFZ5LnioKWIFUDtIzdI8= 15 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.1 h1:ZpaV/j48RlPc4AmOZuPv22pJliXjXq8/reL63YzyFnw= 16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.1/go.mod h1:R8aXraabD2e3qv1csxM14/X9WF4wFMIY0kH4YEtYD5M= 17 | github.com/aws/aws-sdk-go-v2/internal/ini v1.5.0 h1:DqOQvIfmGkXZUVJnl9VRk0AnxyS59tCtX9k1Pyss4Ak= 18 | github.com/aws/aws-sdk-go-v2/internal/ini v1.5.0/go.mod h1:VV/Kbw9Mg1GWJOT9WK+oTL3cWZiXtapnNvDSRqTZLsg= 19 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.1 h1:2OXw3ppu1XsB6rqKEMV4tnecTjIY3PRV2U6IP6KPJQo= 20 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.1/go.mod h1:FZB4AdakIqW/yERVdGJA6Z9jraax1beXfhBBnK2wwR8= 21 | github.com/aws/aws-sdk-go-v2/service/qldb v1.18.0 h1:kYYQaxxAYAvmbCRyDx02WNV6nLb5xF/16OrfLETg9J8= 22 | github.com/aws/aws-sdk-go-v2/service/qldb v1.18.0/go.mod h1:Nz2qg+oWxZXZ61RWfWk9yAJT5v8SYQQwqRfl8SNM4B4= 23 | github.com/aws/aws-sdk-go-v2/service/qldbsession v1.18.0 h1:j7B028akO6mmmDSma3Yw8IbPeA2iyH05qpRuN3ltZ2o= 24 | github.com/aws/aws-sdk-go-v2/service/qldbsession v1.18.0/go.mod h1:Rja9DfkuixcQTuC4GVFGWJjsRk0fr7PQWxnzdUNgzzA= 25 | github.com/aws/aws-sdk-go-v2/service/sso v1.17.0 h1:I/Oh3IxGPfHXiGnwM54TD6hNr/8TlUrBXAtTyGhR+zw= 26 | github.com/aws/aws-sdk-go-v2/service/sso v1.17.0/go.mod h1:H6NCMvDBqA+CvIaXzaSqM6LWtzv9BzZrqBOqz+PzRF8= 27 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.19.0 h1:irbXQkfVYIRaewYSXcu4yVk0m2T+JzZd0dkop7FjmO0= 28 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.19.0/go.mod h1:4wPNCkM22+oRe71oydP66K50ojDUC33XutSMi2pEF/M= 29 | github.com/aws/aws-sdk-go-v2/service/sts v1.25.0 h1:sYIFy8tm1xQwRvVQ4CRuBGXKIg9sHNuG6+3UAQuoujk= 30 | github.com/aws/aws-sdk-go-v2/service/sts v1.25.0/go.mod h1:S/LOQUeYDfJeJpFCIJDMjy7dwL4aA33HUdVi+i7uH8k= 31 | github.com/aws/smithy-go v1.16.0 h1:gJZEH/Fqh+RsvlJ1Zt4tVAtV6bKkp3cC+R6FCZMNzik= 32 | github.com/aws/smithy-go v1.16.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= 33 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 39 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 40 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 41 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 42 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 43 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 44 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 45 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 46 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 53 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 54 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 55 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 57 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 58 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 59 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 60 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 61 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 62 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= 63 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 64 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 66 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 67 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 68 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 69 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= 72 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 73 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 79 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 81 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 82 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 83 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 84 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 85 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 86 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 87 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 88 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 89 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 90 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 92 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 95 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 96 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 97 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 98 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 99 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 100 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 101 | -------------------------------------------------------------------------------- /qldbdriver/communicator_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "testing" 20 | 21 | "github.com/aws/aws-sdk-go-v2/service/qldbsession" 22 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 23 | "github.com/stretchr/testify/assert" 24 | "github.com/stretchr/testify/mock" 25 | ) 26 | 27 | func TestStartSession(t *testing.T) { 28 | t.Run("error", func(t *testing.T) { 29 | mockSession := new(mockQLDBSession) 30 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, errMock) 31 | communicator, err := startSession(context.Background(), "ledgerName", mockSession, mockLogger) 32 | 33 | assert.Equal(t, err, errMock) 34 | assert.Nil(t, communicator) 35 | }) 36 | 37 | t.Run("success", func(t *testing.T) { 38 | mockSession := new(mockQLDBSession) 39 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, nil) 40 | communicator, err := startSession(context.Background(), "ledgerName", mockSession, mockLogger) 41 | assert.NoError(t, err) 42 | 43 | assert.Equal(t, communicator.sessionToken, &mockSessionToken) 44 | assert.NoError(t, err) 45 | }) 46 | } 47 | 48 | func TestAbortTransaction(t *testing.T) { 49 | testCommunicator := communicator{ 50 | service: nil, 51 | sessionToken: &mockSessionToken, 52 | logger: mockLogger, 53 | } 54 | 55 | t.Run("error", func(t *testing.T) { 56 | mockSession := new(mockQLDBSession) 57 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, errMock) 58 | testCommunicator.service = mockSession 59 | result, err := testCommunicator.abortTransaction(context.Background()) 60 | 61 | assert.Equal(t, err, errMock) 62 | assert.Nil(t, result) 63 | }) 64 | 65 | t.Run("success", func(t *testing.T) { 66 | mockSession := new(mockQLDBSession) 67 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, nil) 68 | testCommunicator.service = mockSession 69 | result, err := testCommunicator.abortTransaction(context.Background()) 70 | 71 | assert.Equal(t, result, &mockAbortTransaction) 72 | assert.NoError(t, err) 73 | }) 74 | } 75 | 76 | func TestCommitTransaction(t *testing.T) { 77 | testCommunicator := communicator{ 78 | service: nil, 79 | sessionToken: &mockSessionToken, 80 | logger: mockLogger, 81 | } 82 | 83 | t.Run("error", func(t *testing.T) { 84 | mockSession := new(mockQLDBSession) 85 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, errMock) 86 | testCommunicator.service = mockSession 87 | result, err := testCommunicator.commitTransaction(context.Background(), nil, nil) 88 | 89 | assert.Equal(t, err, errMock) 90 | assert.Nil(t, result) 91 | }) 92 | 93 | t.Run("success", func(t *testing.T) { 94 | mockSession := new(mockQLDBSession) 95 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, nil) 96 | testCommunicator.service = mockSession 97 | result, err := testCommunicator.commitTransaction(context.Background(), nil, nil) 98 | 99 | assert.Equal(t, result, &mockCommitTransaction) 100 | assert.NoError(t, err) 101 | }) 102 | } 103 | 104 | func TestExecuteStatement(t *testing.T) { 105 | testCommunicator := communicator{ 106 | service: nil, 107 | sessionToken: &mockSessionToken, 108 | logger: mockLogger, 109 | } 110 | 111 | t.Run("error", func(t *testing.T) { 112 | mockSession := new(mockQLDBSession) 113 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, errMock) 114 | testCommunicator.service = mockSession 115 | result, err := testCommunicator.executeStatement(context.Background(), nil, nil, nil) 116 | 117 | assert.Equal(t, err, errMock) 118 | assert.Nil(t, result) 119 | }) 120 | 121 | t.Run("success", func(t *testing.T) { 122 | mockSession := new(mockQLDBSession) 123 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, nil) 124 | testCommunicator.service = mockSession 125 | result, err := testCommunicator.executeStatement(context.Background(), nil, nil, nil) 126 | 127 | assert.Equal(t, result, &mockExecuteStatement) 128 | assert.NoError(t, err) 129 | }) 130 | } 131 | 132 | func TestEndSession(t *testing.T) { 133 | testCommunicator := communicator{ 134 | service: nil, 135 | sessionToken: &mockSessionToken, 136 | logger: mockLogger, 137 | } 138 | 139 | t.Run("error", func(t *testing.T) { 140 | mockSession := new(mockQLDBSession) 141 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, errMock) 142 | testCommunicator.service = mockSession 143 | result, err := testCommunicator.endSession(context.Background()) 144 | 145 | assert.Equal(t, err, errMock) 146 | assert.Nil(t, result) 147 | }) 148 | 149 | t.Run("success", func(t *testing.T) { 150 | mockSession := new(mockQLDBSession) 151 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, nil) 152 | testCommunicator.service = mockSession 153 | result, err := testCommunicator.endSession(context.Background()) 154 | 155 | assert.Equal(t, result, &mockEndSession) 156 | assert.NoError(t, err) 157 | }) 158 | } 159 | 160 | func TestFetchPage(t *testing.T) { 161 | testCommunicator := communicator{ 162 | service: nil, 163 | sessionToken: &mockSessionToken, 164 | logger: mockLogger, 165 | } 166 | 167 | t.Run("error", func(t *testing.T) { 168 | mockSession := new(mockQLDBSession) 169 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, errMock) 170 | testCommunicator.service = mockSession 171 | result, err := testCommunicator.fetchPage(context.Background(), nil, nil) 172 | 173 | assert.Equal(t, err, errMock) 174 | assert.Nil(t, result) 175 | }) 176 | 177 | t.Run("success", func(t *testing.T) { 178 | mockSession := new(mockQLDBSession) 179 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, nil) 180 | testCommunicator.service = mockSession 181 | result, err := testCommunicator.fetchPage(context.Background(), nil, nil) 182 | 183 | assert.Equal(t, result, &mockFetchPage) 184 | assert.NoError(t, err) 185 | }) 186 | } 187 | 188 | func TestStartTransaction(t *testing.T) { 189 | testCommunicator := communicator{ 190 | service: nil, 191 | sessionToken: &mockSessionToken, 192 | logger: mockLogger, 193 | } 194 | 195 | t.Run("error", func(t *testing.T) { 196 | mockSession := new(mockQLDBSession) 197 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, errMock) 198 | testCommunicator.service = mockSession 199 | result, err := testCommunicator.startTransaction(context.Background()) 200 | 201 | assert.Equal(t, err, errMock) 202 | assert.Nil(t, result) 203 | }) 204 | 205 | t.Run("success", func(t *testing.T) { 206 | mockSession := new(mockQLDBSession) 207 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, nil) 208 | testCommunicator.service = mockSession 209 | result, err := testCommunicator.startTransaction(context.Background()) 210 | 211 | assert.Equal(t, result, &mockStartTransaction) 212 | assert.NoError(t, err) 213 | }) 214 | } 215 | 216 | func TestSendCommand(t *testing.T) { 217 | testCommunicator := communicator{ 218 | service: nil, 219 | sessionToken: &mockSessionToken, 220 | logger: mockLogger, 221 | } 222 | mockSession := new(mockQLDBSession) 223 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommand, errMock) 224 | testCommunicator.service = mockSession 225 | result, err := testCommunicator.sendCommand(context.Background(), &qldbsession.SendCommandInput{}) 226 | 227 | assert.Equal(t, result, &mockSendCommand) 228 | assert.Equal(t, err, errMock) 229 | } 230 | 231 | var mockLogger = &qldbLogger{defaultLogger{}, LogOff} 232 | var errMock = errors.New("mock") 233 | 234 | var mockSessionToken = "token" 235 | var mockStartSession = types.StartSessionResult{SessionToken: &mockSessionToken} 236 | var mockAbortTransaction = types.AbortTransactionResult{} 237 | var mockCommitTransaction = types.CommitTransactionResult{} 238 | var mockExecuteStatement = types.ExecuteStatementResult{} 239 | var mockEndSession = types.EndSessionResult{} 240 | var mockFetchPage = types.FetchPageResult{} 241 | var mockStartTransaction = types.StartTransactionResult{} 242 | var mockSendCommand = qldbsession.SendCommandOutput{ 243 | AbortTransaction: &mockAbortTransaction, 244 | CommitTransaction: &mockCommitTransaction, 245 | EndSession: &mockEndSession, 246 | ExecuteStatement: &mockExecuteStatement, 247 | FetchPage: &mockFetchPage, 248 | StartSession: &mockStartSession, 249 | StartTransaction: &mockStartTransaction, 250 | } 251 | 252 | type mockQLDBSession struct { 253 | mock.Mock 254 | } 255 | 256 | func (m *mockQLDBSession) SendCommand(ctx context.Context, params *qldbsession.SendCommandInput, optFns ...func(*qldbsession.Options)) (*qldbsession.SendCommandOutput, error) { 257 | args := m.Called(ctx, params, optFns) 258 | return args.Get(0).(*qldbsession.SendCommandOutput), args.Error(1) 259 | } 260 | -------------------------------------------------------------------------------- /qldbdriver/qldbdriver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | // Package qldbdriver is the Golang driver for working with Amazon Quantum Ledger Database. 15 | package qldbdriver 16 | 17 | import ( 18 | "context" 19 | "sync" 20 | "time" 21 | 22 | "github.com/amzn/ion-go/ion" 23 | "github.com/aws/aws-sdk-go-v2/service/qldbsession" 24 | "github.com/awslabs/amazon-qldb-driver-go/v3/qldbdriver/qldbsessioniface" 25 | ) 26 | 27 | // DriverOptions can be used to configure the driver during construction. 28 | type DriverOptions struct { 29 | // The policy guiding retry attempts upon a recoverable error. 30 | // Default: MaxRetryLimit: 4, ExponentialBackoff: SleepBase: 10ms, SleepCap: 5000ms. 31 | RetryPolicy RetryPolicy 32 | // The maximum amount of concurrent transactions this driver will permit. Default: 50. 33 | MaxConcurrentTransactions int 34 | // The logger that the driver will use for any logging messages. Default: "log" package. 35 | Logger Logger 36 | // The verbosity level of the logs that the logger should receive. Default: qldbdriver.LogInfo. 37 | LoggerVerbosity LogLevel 38 | } 39 | 40 | // QLDBDriver is used to execute statements against QLDB. Call constructor qldbdriver.New for a valid QLDBDriver. 41 | type QLDBDriver struct { 42 | ledgerName string 43 | qldbSession qldbsessioniface.ClientAPI 44 | maxConcurrentTransactions int 45 | logger *qldbLogger 46 | isClosed bool 47 | semaphore *semaphore 48 | sessionPool chan *session 49 | retryPolicy RetryPolicy 50 | lock sync.Mutex 51 | } 52 | 53 | type semaphore struct { 54 | values chan struct{} 55 | } 56 | 57 | // New creates a QLBDDriver using the parameters and options, and verifies the configuration. 58 | // 59 | // Note that qldbSession will disable all SDK retry attempts when calling service operations. 60 | // DriverOptions.RetryLimit is unrelated to SDK retries, but should be used if it is desired to modify the amount of retires for statement executions. 61 | func New(ledgerName string, qldbSession *qldbsession.Client, fns ...func(*DriverOptions)) (*QLDBDriver, error) { 62 | if qldbSession == nil { 63 | return nil, &qldbDriverError{"Provided QLDBSession is nil."} 64 | } 65 | 66 | retryPolicy := RetryPolicy{ 67 | MaxRetryLimit: 4, 68 | Backoff: ExponentialBackoffStrategy{SleepBase: time.Duration(10) * time.Millisecond, SleepCap: time.Duration(5000) * time.Millisecond}} 69 | options := &DriverOptions{RetryPolicy: retryPolicy, MaxConcurrentTransactions: 50, Logger: defaultLogger{}, LoggerVerbosity: LogInfo} 70 | 71 | for _, fn := range fns { 72 | fn(options) 73 | } 74 | 75 | if options.MaxConcurrentTransactions < 1 { 76 | return nil, &qldbDriverError{"MaxConcurrentTransactions must be 1 or greater."} 77 | } 78 | 79 | logger := &qldbLogger{options.Logger, options.LoggerVerbosity} 80 | 81 | driverQldbSession := *qldbSession 82 | 83 | semaphore := makeSemaphore(options.MaxConcurrentTransactions) 84 | sessionPool := make(chan *session, options.MaxConcurrentTransactions) 85 | isClosed := false 86 | 87 | return &QLDBDriver{ledgerName, &driverQldbSession, options.MaxConcurrentTransactions, logger, isClosed, 88 | semaphore, sessionPool, options.RetryPolicy, sync.Mutex{}}, nil 89 | } 90 | 91 | // SetRetryPolicy sets the driver's retry policy for Execute. 92 | func (driver *QLDBDriver) SetRetryPolicy(rp RetryPolicy) { 93 | driver.retryPolicy = rp 94 | } 95 | 96 | // Execute a provided function within the context of a new QLDB transaction. 97 | // 98 | // The provided function might be executed more than once and is not expected to run concurrently. 99 | // It is recommended for it to be idempotent, so that it doesn't have unintended side effects in the case of retries. 100 | func (driver *QLDBDriver) Execute(ctx context.Context, fn func(txn Transaction) (interface{}, error)) (interface{}, error) { 101 | if driver.isClosed { 102 | return nil, &qldbDriverError{"Cannot invoke methods on a closed QLDBDriver."} 103 | } 104 | 105 | retryAttempt := 0 106 | 107 | session, err := driver.getSession(ctx) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | var result interface{} 113 | var txnErr *txnError 114 | for { 115 | result, txnErr = session.execute(ctx, fn) 116 | if txnErr != nil { 117 | // If initial session is invalid, always retry once 118 | if txnErr.canRetry && txnErr.isISE && retryAttempt == 0 { 119 | driver.logger.log(LogDebug, "Initial session received from pool invalid. Retrying...") 120 | session, err = driver.createSession(ctx) 121 | if err != nil { 122 | return nil, err 123 | } 124 | retryAttempt++ 125 | continue 126 | } 127 | // Do not retry 128 | if !txnErr.canRetry || retryAttempt >= driver.retryPolicy.MaxRetryLimit { 129 | if txnErr.abortSuccess { 130 | driver.releaseSession(session) 131 | } else { 132 | driver.semaphore.release() 133 | } 134 | return nil, txnErr.unwrap() 135 | } 136 | // Retry 137 | retryAttempt++ 138 | driver.logger.logf(LogInfo, "A recoverable error has occurred. Attempting retry #%d.", retryAttempt) 139 | driver.logger.logf(LogDebug, "Errored Transaction ID: %s. Error cause: '%v'", txnErr.transactionID, txnErr) 140 | if txnErr.isISE { 141 | driver.logger.log(LogDebug, "Replacing expired session...") 142 | session, err = driver.createSession(ctx) 143 | if err != nil { 144 | return nil, err 145 | } 146 | } else { 147 | if !txnErr.abortSuccess { 148 | driver.logger.log(LogDebug, "Retrying with a different session...") 149 | driver.semaphore.release() 150 | session, err = driver.getSession(ctx) 151 | if err != nil { 152 | return nil, err 153 | } 154 | } 155 | } 156 | 157 | delay := driver.retryPolicy.Backoff.Delay(retryAttempt) 158 | sleepWithContext(ctx, delay) 159 | continue 160 | } 161 | driver.releaseSession(session) 162 | break 163 | } 164 | return result, nil 165 | } 166 | 167 | // GetTableNames returns a list of the names of active tables in the ledger. 168 | func (driver *QLDBDriver) GetTableNames(ctx context.Context) ([]string, error) { 169 | const tableNameQuery string = "SELECT name FROM information_schema.user_tables WHERE status = 'ACTIVE'" 170 | type tableName struct { 171 | Name string `ion:"name"` 172 | } 173 | 174 | executeResult, err := driver.Execute(ctx, func(txn Transaction) (interface{}, error) { 175 | result, err := txn.Execute(tableNameQuery) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | tableNames := make([]string, 0) 181 | for result.Next(txn) { 182 | nameStruct := new(tableName) 183 | err = ion.Unmarshal(result.GetCurrentData(), &nameStruct) 184 | if err != nil { 185 | return nil, err 186 | } 187 | tableNames = append(tableNames, nameStruct.Name) 188 | } 189 | if result.Err() != nil { 190 | return nil, result.Err() 191 | } 192 | return tableNames, nil 193 | }) 194 | if err != nil { 195 | return nil, err 196 | } 197 | return executeResult.([]string), nil 198 | } 199 | 200 | // Shutdown the driver, cleaning up allocated resources. 201 | func (driver *QLDBDriver) Shutdown(ctx context.Context) { 202 | driver.lock.Lock() 203 | defer driver.lock.Unlock() 204 | if !driver.isClosed { 205 | driver.isClosed = true 206 | for len(driver.sessionPool) > 0 { 207 | session := <-driver.sessionPool 208 | err := session.endSession(ctx) 209 | if err != nil { 210 | driver.logger.logf(LogDebug, "Encountered error trying to end session: '%v'", err.Error()) 211 | } 212 | } 213 | close(driver.sessionPool) 214 | } 215 | } 216 | 217 | func (driver *QLDBDriver) getSession(ctx context.Context) (*session, error) { 218 | driver.logger.logf(LogDebug, "Getting session. Existing sessions available: %v", len(driver.sessionPool)) 219 | isPermitAcquired := driver.semaphore.tryAcquire() 220 | if isPermitAcquired { 221 | if len(driver.sessionPool) > 0 { 222 | session := <-driver.sessionPool 223 | driver.logger.log(LogDebug, "Reusing session from pool.") 224 | return session, nil 225 | } 226 | return driver.createSession(ctx) 227 | } 228 | return nil, &qldbDriverError{"MaxConcurrentTransactions limit exceeded."} 229 | } 230 | 231 | func (driver *QLDBDriver) createSession(ctx context.Context) (*session, error) { 232 | driver.logger.log(LogDebug, "Creating a new session") 233 | communicator, err := startSession(ctx, driver.ledgerName, driver.qldbSession, driver.logger) 234 | if err != nil { 235 | driver.semaphore.release() 236 | return nil, err 237 | } 238 | return &session{communicator, driver.logger}, nil 239 | } 240 | 241 | func (driver *QLDBDriver) releaseSession(session *session) { 242 | driver.sessionPool <- session 243 | driver.semaphore.release() 244 | driver.logger.logf(LogDebug, "Session returned to pool; size of pool is now %v", len(driver.sessionPool)) 245 | } 246 | 247 | func sleepWithContext(ctx context.Context, delay time.Duration) { 248 | select { 249 | case <-ctx.Done(): 250 | case <-time.After(delay): 251 | } 252 | } 253 | 254 | func makeSemaphore(size int) *semaphore { 255 | smphr := &semaphore{make(chan struct{}, size)} 256 | for counter := 0; counter < size; counter++ { 257 | smphr.values <- struct{}{} 258 | } 259 | return smphr 260 | } 261 | 262 | func (smphr *semaphore) tryAcquire() bool { 263 | select { 264 | case _, ok := <-smphr.values: 265 | return ok 266 | default: 267 | return false 268 | } 269 | } 270 | 271 | func (smphr *semaphore) release() { 272 | smphr.values <- struct{}{} 273 | } 274 | -------------------------------------------------------------------------------- /qldbdriver/result_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/mock" 23 | ) 24 | 25 | func TestResult(t *testing.T) { 26 | mockIonBinary := make([]byte, 1) 27 | mockIonBinary[0] = 1 28 | mockValueHolder := types.ValueHolder{IonBinary: mockIonBinary} 29 | mockPageValues := make([]types.ValueHolder, 1) 30 | // Has only one value 31 | mockPageValues[0] = mockValueHolder 32 | 33 | mockNextIonBinary := make([]byte, 1) 34 | mockNextIonBinary[0] = 2 35 | mockNextValueHolder := types.ValueHolder{IonBinary: mockNextIonBinary} 36 | mockNextPageValues := make([]types.ValueHolder, 1) 37 | // Has only one value 38 | mockNextPageValues[0] = mockNextValueHolder 39 | 40 | readIOs := int64(1) 41 | writeIOs := int64(2) 42 | processingTimeMilliseconds := int64(3) 43 | qldbsessionTimingInformation := generateQldbsessionTimingInformation(processingTimeMilliseconds) 44 | qldbsessionConsumedIOs := generateQldbsessionIOUsage(readIOs, writeIOs) 45 | 46 | res := &result{ 47 | ctx: nil, 48 | communicator: nil, 49 | txnID: nil, 50 | pageValues: mockPageValues, 51 | pageToken: nil, 52 | index: 0, 53 | logger: nil, 54 | ioUsage: newIOUsage(0, 0), 55 | timingInfo: newTimingInformation(0), 56 | } 57 | 58 | fetchPageResult := types.FetchPageResult{Page: &types.Page{Values: mockNextPageValues}} 59 | fetchPageResultWithStats := fetchPageResult 60 | fetchPageResultWithStats.TimingInformation = qldbsessionTimingInformation 61 | fetchPageResultWithStats.ConsumedIOs = qldbsessionConsumedIOs 62 | 63 | t.Run("Next", func(t *testing.T) { 64 | t.Run("pageToken is nil", func(t *testing.T) { 65 | res.index = 0 66 | res.pageToken = nil 67 | 68 | assert.True(t, res.Next(&transactionExecutor{nil, nil})) 69 | assert.Equal(t, mockIonBinary, res.GetCurrentData()) 70 | 71 | // No more values 72 | assert.False(t, res.Next(&transactionExecutor{nil, nil})) 73 | assert.Nil(t, res.GetCurrentData()) 74 | assert.NoError(t, res.Err()) 75 | }) 76 | 77 | t.Run("pageToken present", func(t *testing.T) { 78 | mockToken := "mockToken" 79 | 80 | t.Run("success", func(t *testing.T) { 81 | res.index = 0 82 | res.pageToken = &mockToken 83 | mockService := new(mockResultService) 84 | mockService.On("fetchPage", mock.Anything, mock.Anything, mock.Anything).Return(&fetchPageResult, nil) 85 | res.communicator = mockService 86 | 87 | // Default page 88 | assert.True(t, res.Next(&transactionExecutor{nil, nil})) 89 | assert.Equal(t, mockIonBinary, res.GetCurrentData()) 90 | 91 | // Fetched page 92 | assert.True(t, res.Next(&transactionExecutor{nil, nil})) 93 | assert.Equal(t, mockNextIonBinary, res.GetCurrentData()) 94 | 95 | // No more results 96 | assert.False(t, res.Next(&transactionExecutor{nil, nil})) 97 | assert.Nil(t, res.GetCurrentData()) 98 | assert.NoError(t, res.Err()) 99 | }) 100 | 101 | t.Run("query stats are updated", func(t *testing.T) { 102 | res.index = 0 103 | res.pageToken = &mockToken 104 | mockService := new(mockResultService) 105 | mockService.On("fetchPage", mock.Anything, mock.Anything, mock.Anything).Return(&fetchPageResultWithStats, nil) 106 | res.communicator = mockService 107 | 108 | // Default page 109 | assert.True(t, res.Next(&transactionExecutor{nil, nil})) 110 | assert.Equal(t, int64(0), *res.ioUsage.GetReadIOs()) 111 | assert.Equal(t, int64(0), *res.ioUsage.getWriteIOs()) 112 | assert.Equal(t, int64(0), *res.timingInfo.GetProcessingTimeMilliseconds()) 113 | 114 | // Fetched page 115 | assert.True(t, res.Next(&transactionExecutor{nil, nil})) 116 | assert.Equal(t, readIOs, *res.ioUsage.GetReadIOs()) 117 | assert.Equal(t, writeIOs, *res.ioUsage.getWriteIOs()) 118 | assert.Equal(t, processingTimeMilliseconds, *res.timingInfo.GetProcessingTimeMilliseconds()) 119 | }) 120 | 121 | t.Run("fail", func(t *testing.T) { 122 | res.index = 0 123 | res.pageToken = &mockToken 124 | res.pageValues = mockPageValues 125 | mockService := new(mockResultService) 126 | mockService.On("fetchPage", mock.Anything, mock.Anything, mock.Anything).Return(&fetchPageResult, errMock) 127 | res.communicator = mockService 128 | 129 | // Default page 130 | assert.True(t, res.Next(&transactionExecutor{nil, nil})) 131 | assert.Equal(t, mockIonBinary, res.GetCurrentData()) 132 | 133 | // Fetched page 134 | assert.False(t, res.Next(&transactionExecutor{nil, nil})) 135 | assert.Nil(t, res.GetCurrentData()) 136 | assert.Equal(t, errMock, res.Err()) 137 | }) 138 | }) 139 | }) 140 | 141 | t.Run("updateMetrics", func(t *testing.T) { 142 | t.Run("res does not have metrics and fetch page does not have metrics", func(t *testing.T) { 143 | res := result{ioUsage: newIOUsage(0, 0), timingInfo: newTimingInformation(0)} 144 | res.updateMetrics(&fetchPageResult) 145 | 146 | assert.Equal(t, int64(0), *res.GetConsumedIOs().GetReadIOs()) 147 | assert.Equal(t, int64(0), *res.GetConsumedIOs().getWriteIOs()) 148 | assert.Equal(t, int64(0), *res.GetTimingInformation().GetProcessingTimeMilliseconds()) 149 | }) 150 | 151 | t.Run("res does not have metrics and fetch page has metrics", func(t *testing.T) { 152 | result := result{ioUsage: newIOUsage(0, 0), timingInfo: newTimingInformation(0)} 153 | result.updateMetrics(&fetchPageResultWithStats) 154 | 155 | assert.Equal(t, readIOs, *result.GetConsumedIOs().GetReadIOs()) 156 | assert.Equal(t, writeIOs, *result.GetConsumedIOs().getWriteIOs()) 157 | assert.Equal(t, processingTimeMilliseconds, *result.GetTimingInformation().GetProcessingTimeMilliseconds()) 158 | }) 159 | 160 | t.Run("res has metrics and fetch page does not have metrics", func(t *testing.T) { 161 | result := result{ioUsage: newIOUsage(readIOs, writeIOs), timingInfo: newTimingInformation(processingTimeMilliseconds)} 162 | result.updateMetrics(&fetchPageResult) 163 | 164 | assert.Equal(t, readIOs, *result.GetConsumedIOs().GetReadIOs()) 165 | assert.Equal(t, writeIOs, *result.GetConsumedIOs().getWriteIOs()) 166 | assert.Equal(t, processingTimeMilliseconds, *result.GetTimingInformation().GetProcessingTimeMilliseconds()) 167 | }) 168 | 169 | t.Run("res has metrics and fetch page has metrics", func(t *testing.T) { 170 | result := result{ioUsage: newIOUsage(readIOs, writeIOs), timingInfo: newTimingInformation(processingTimeMilliseconds)} 171 | 172 | readIOsBeforeUpdate := result.GetConsumedIOs().GetReadIOs() 173 | writeIOsBeforeUpdate := result.GetConsumedIOs().getWriteIOs() 174 | processingTimeMillisecondsBeforeUpdate := result.GetTimingInformation().GetProcessingTimeMilliseconds() 175 | 176 | result.updateMetrics(&fetchPageResultWithStats) 177 | 178 | assert.Equal(t, int64(1), *readIOsBeforeUpdate) 179 | assert.Equal(t, int64(2), *writeIOsBeforeUpdate) 180 | assert.Equal(t, int64(3), *processingTimeMillisecondsBeforeUpdate) 181 | 182 | assert.Equal(t, int64(2), *result.GetConsumedIOs().GetReadIOs()) 183 | assert.Equal(t, int64(4), *result.GetConsumedIOs().getWriteIOs()) 184 | assert.Equal(t, int64(6), *result.GetTimingInformation().GetProcessingTimeMilliseconds()) 185 | }) 186 | }) 187 | } 188 | 189 | func TestBufferedResult(t *testing.T) { 190 | byteSlice1 := make([]byte, 1) 191 | byteSlice1[0] = 1 192 | byteSlice2 := make([]byte, 1) 193 | byteSlice2[0] = 2 194 | byteSliceSlice := make([][]byte, 2) 195 | byteSliceSlice[0] = byteSlice1 196 | byteSliceSlice[1] = byteSlice2 197 | 198 | readIOs := int64(1) 199 | writeIOs := int64(2) 200 | processingTimeMilliseconds := int64(3) 201 | result := bufferedResult{ 202 | values: byteSliceSlice, 203 | index: 0, 204 | ioUsage: newIOUsage(readIOs, writeIOs), 205 | timingInfo: newTimingInformation(processingTimeMilliseconds)} 206 | 207 | t.Run("Next", func(t *testing.T) { 208 | result.index = 0 209 | 210 | assert.True(t, result.Next()) 211 | assert.Equal(t, byteSlice1, result.GetCurrentData()) 212 | 213 | assert.True(t, result.Next()) 214 | assert.Equal(t, byteSlice2, result.GetCurrentData()) 215 | 216 | // End of slice 217 | assert.False(t, result.Next()) 218 | assert.Nil(t, result.GetCurrentData()) 219 | 220 | assert.Equal(t, processingTimeMilliseconds, *result.GetTimingInformation().GetProcessingTimeMilliseconds()) 221 | assert.Equal(t, readIOs, *result.GetConsumedIOs().GetReadIOs()) 222 | assert.Equal(t, writeIOs, *result.GetConsumedIOs().getWriteIOs()) 223 | }) 224 | } 225 | 226 | type mockResultService struct { 227 | mock.Mock 228 | } 229 | 230 | func (m *mockResultService) abortTransaction(ctx context.Context) (*types.AbortTransactionResult, error) { 231 | panic("not used") 232 | } 233 | 234 | func (m *mockResultService) commitTransaction(ctx context.Context, txnID *string, commitDigest []byte) (*types.CommitTransactionResult, error) { 235 | panic("not used") 236 | } 237 | 238 | func (m *mockResultService) executeStatement(ctx context.Context, statement *string, parameters []types.ValueHolder, txnID *string) (*types.ExecuteStatementResult, error) { 239 | panic("not used") 240 | } 241 | 242 | func (m *mockResultService) endSession(ctx context.Context) (*types.EndSessionResult, error) { 243 | panic("not used") 244 | } 245 | 246 | func (m *mockResultService) fetchPage(ctx context.Context, pageToken *string, txnID *string) (*types.FetchPageResult, error) { 247 | args := m.Called(ctx, pageToken, txnID) 248 | return args.Get(0).(*types.FetchPageResult), args.Error(1) 249 | } 250 | 251 | func (m *mockResultService) startTransaction(ctx context.Context) (*types.StartTransactionResult, error) { 252 | panic("not used") 253 | } 254 | 255 | func generateQldbsessionIOUsage(readIOs int64, writeIOs int64) *types.IOUsage { 256 | return &types.IOUsage{ 257 | ReadIOs: readIOs, 258 | WriteIOs: writeIOs, 259 | } 260 | } 261 | 262 | func generateQldbsessionTimingInformation(processingTimeMilliseconds int64) *types.TimingInformation { 263 | return &types.TimingInformation{ 264 | ProcessingTimeMilliseconds: processingTimeMilliseconds, 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /qldbdriver/transaction_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/mock" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | func TestTransaction(t *testing.T) { 27 | t.Run("execute", func(t *testing.T) { 28 | mockHash, _ := toQLDBHash(mockTxnID) 29 | mockNextPageToken := "mockToken" 30 | var mockPageValues []types.ValueHolder 31 | mockFirstPage := types.Page{ 32 | NextPageToken: &mockNextPageToken, 33 | Values: mockPageValues, 34 | } 35 | 36 | readIOs := int64(1) 37 | writeIOs := int64(2) 38 | processingTimeMilliseconds := int64(3) 39 | qldbsessionTimingInformation := generateQldbsessionTimingInformation(processingTimeMilliseconds) 40 | qldbsessionConsumedIOs := generateQldbsessionIOUsage(readIOs, writeIOs) 41 | 42 | executeResult := types.ExecuteStatementResult{ 43 | FirstPage: &mockFirstPage, 44 | } 45 | 46 | executeResultWithQueryStats := executeResult 47 | executeResultWithQueryStats.TimingInformation = qldbsessionTimingInformation 48 | executeResultWithQueryStats.ConsumedIOs = qldbsessionConsumedIOs 49 | 50 | testTransaction := &transaction{ 51 | communicator: nil, 52 | id: &mockTxnID, 53 | logger: nil, 54 | commitHash: mockHash, 55 | } 56 | 57 | t.Run("success", func(t *testing.T) { 58 | mockService := new(mockTransactionService) 59 | mockService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&executeResult, nil) 60 | testTransaction.communicator = mockService 61 | 62 | result, err := testTransaction.execute(context.Background(), "mockStatement", "mockParam1", "mockParam2") 63 | assert.NoError(t, err) 64 | assert.NotNil(t, result) 65 | 66 | assert.Equal(t, testTransaction.communicator, result.communicator) 67 | assert.Equal(t, testTransaction.id, result.txnID) 68 | assert.Equal(t, &mockNextPageToken, result.pageToken) 69 | assert.Equal(t, mockPageValues, result.pageValues) 70 | assert.Equal(t, int64(0), *result.GetConsumedIOs().GetReadIOs()) 71 | assert.Equal(t, int64(0), *result.GetConsumedIOs().getWriteIOs()) 72 | assert.Equal(t, int64(0), *result.GetTimingInformation().GetProcessingTimeMilliseconds()) 73 | }) 74 | 75 | t.Run("success and execute statement result contains query stats", func(t *testing.T) { 76 | mockService := new(mockTransactionService) 77 | mockService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&executeResultWithQueryStats, nil) 78 | testTransaction.communicator = mockService 79 | 80 | result, err := testTransaction.execute(context.Background(), "mockStatement", "mockParam1", "mockParam2") 81 | assert.NoError(t, err) 82 | assert.NotNil(t, result) 83 | 84 | assert.Equal(t, testTransaction.communicator, result.communicator) 85 | assert.Equal(t, testTransaction.id, result.txnID) 86 | assert.Equal(t, &mockNextPageToken, result.pageToken) 87 | assert.Equal(t, mockPageValues, result.pageValues) 88 | assert.Equal(t, readIOs, *result.GetConsumedIOs().GetReadIOs()) 89 | assert.Equal(t, writeIOs, *result.GetConsumedIOs().getWriteIOs()) 90 | assert.Equal(t, processingTimeMilliseconds, *result.GetTimingInformation().GetProcessingTimeMilliseconds()) 91 | }) 92 | 93 | t.Run("error", func(t *testing.T) { 94 | mockService := new(mockTransactionService) 95 | mockService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&mockExecuteResult, errMock) 96 | testTransaction.communicator = mockService 97 | 98 | result, err := testTransaction.execute(context.Background(), "mockStatement", "mockParam1", "mockParam2") 99 | assert.Error(t, err) 100 | assert.Nil(t, result) 101 | assert.Equal(t, errMock, err) 102 | }) 103 | }) 104 | 105 | t.Run("commit", func(t *testing.T) { 106 | mockTxnID := "mockId" 107 | 108 | mockHash1 := make([]byte, 1) 109 | mockHash1[0] = 0 110 | mockHash2 := make([]byte, 1) 111 | mockHash2[0] = 1 112 | mockCommitTransactionResult := types.CommitTransactionResult{ 113 | CommitDigest: mockHash1, 114 | } 115 | 116 | testTransaction := &transaction{ 117 | communicator: nil, 118 | id: &mockTxnID, 119 | logger: nil, 120 | commitHash: &qldbHash{hash: mockHash1}, 121 | } 122 | 123 | t.Run("success", func(t *testing.T) { 124 | mockService := new(mockTransactionService) 125 | mockService.On("commitTransaction", mock.Anything, mock.Anything, mock.Anything).Return(&mockCommitTransactionResult, nil) 126 | testTransaction.communicator = mockService 127 | 128 | assert.NoError(t, testTransaction.commit(context.Background())) 129 | }) 130 | 131 | t.Run("error", func(t *testing.T) { 132 | mockService := new(mockTransactionService) 133 | mockService.On("commitTransaction", mock.Anything, mock.Anything, mock.Anything).Return(&mockCommitTransactionResult, errMock) 134 | testTransaction.communicator = mockService 135 | 136 | assert.Equal(t, errMock, testTransaction.commit(context.Background())) 137 | }) 138 | 139 | t.Run("digest mismatch", func(t *testing.T) { 140 | mockService := new(mockTransactionService) 141 | mockService.On("commitTransaction", mock.Anything, mock.Anything, mock.Anything).Return(&mockCommitTransactionResult, nil) 142 | testTransaction.communicator = mockService 143 | mockCommitTransactionResult.CommitDigest = mockHash2 144 | 145 | assert.Error(t, testTransaction.commit(context.Background())) 146 | }) 147 | }) 148 | } 149 | 150 | func TestTransactionExecutor(t *testing.T) { 151 | mockID := "txnID" 152 | mockHash, _ := toQLDBHash(mockTxnID) 153 | 154 | mockTransaction := transaction{ 155 | communicator: nil, 156 | id: &mockID, 157 | logger: mockLogger, 158 | commitHash: mockHash, 159 | } 160 | 161 | testExecutor := transactionExecutor{ 162 | ctx: context.Background(), 163 | txn: &mockTransaction, 164 | } 165 | 166 | t.Run("execute", func(t *testing.T) { 167 | mockNextPageToken := "mockToken" 168 | var mockPageValues []types.ValueHolder 169 | mockFirstPage := types.Page{ 170 | NextPageToken: &mockNextPageToken, 171 | Values: mockPageValues, 172 | } 173 | mockExecuteResult := types.ExecuteStatementResult{ 174 | FirstPage: &mockFirstPage, 175 | } 176 | 177 | t.Run("success", func(t *testing.T) { 178 | mockService := new(mockTransactionService) 179 | mockService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&mockExecuteResult, nil) 180 | mockTransaction.communicator = mockService 181 | 182 | res, err := testExecutor.Execute("mockStatement", "mockParam1", "mockParam2") 183 | assert.NoError(t, err) 184 | assert.NotNil(t, res) 185 | 186 | result, ok := res.(*result) 187 | require.True(t, ok) 188 | 189 | assert.Equal(t, mockTransaction.communicator, result.communicator) 190 | assert.Equal(t, mockTransaction.id, result.txnID) 191 | assert.Equal(t, &mockNextPageToken, result.pageToken) 192 | assert.Equal(t, mockPageValues, result.pageValues) 193 | }) 194 | 195 | t.Run("error", func(t *testing.T) { 196 | mockService := new(mockTransactionService) 197 | mockService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&mockExecuteResult, errMock) 198 | mockTransaction.communicator = mockService 199 | 200 | result, err := testExecutor.Execute("mockStatement", "mockParam1", "mockParam2") 201 | assert.Error(t, err) 202 | assert.Nil(t, result) 203 | assert.Equal(t, errMock, err) 204 | }) 205 | 206 | t.Run("execute result does not contain IOUsage and TimingInformation", func(t *testing.T) { 207 | mockService := new(mockTransactionService) 208 | mockService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&mockExecuteResult, nil) 209 | mockTransaction.communicator = mockService 210 | 211 | res, err := testExecutor.Execute("mockStatement", "mockParam1", "mockParam2") 212 | assert.NoError(t, err) 213 | assert.NotNil(t, res) 214 | 215 | result, ok := res.(*result) 216 | require.True(t, ok) 217 | 218 | assert.Equal(t, int64(0), *result.ioUsage.GetReadIOs()) 219 | assert.Equal(t, int64(0), *result.ioUsage.getWriteIOs()) 220 | assert.Equal(t, int64(0), *result.timingInfo.GetProcessingTimeMilliseconds()) 221 | }) 222 | 223 | t.Run("execute result contains IOUsage and TimingInformation", func(t *testing.T) { 224 | mockService := new(mockTransactionService) 225 | 226 | readIOs := int64(1) 227 | writeIOs := int64(2) 228 | timingInfo := int64(3) 229 | 230 | timingInformation := generateQldbsessionTimingInformation(timingInfo) 231 | consumedIOs := generateQldbsessionIOUsage(readIOs, writeIOs) 232 | 233 | mockExecuteResultWithQueryStats := mockExecuteResult 234 | mockExecuteResultWithQueryStats.TimingInformation = timingInformation 235 | mockExecuteResultWithQueryStats.ConsumedIOs = consumedIOs 236 | 237 | mockService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&mockExecuteResultWithQueryStats, nil) 238 | mockTransaction.communicator = mockService 239 | 240 | res, err := testExecutor.Execute("mockStatement", "mockParam1", "mockParam2") 241 | assert.NoError(t, err) 242 | assert.NotNil(t, res) 243 | 244 | result, ok := res.(*result) 245 | require.True(t, ok) 246 | 247 | assert.Equal(t, &readIOs, result.ioUsage.readIOs) 248 | assert.Equal(t, &writeIOs, result.ioUsage.writeIOs) 249 | assert.Equal(t, &timingInfo, result.timingInfo.processingTimeMilliseconds) 250 | }) 251 | }) 252 | 253 | t.Run("BufferResult", func(t *testing.T) { 254 | mockIonBinary := make([]byte, 1) 255 | mockIonBinary[0] = 1 256 | mockValueHolder := types.ValueHolder{IonBinary: mockIonBinary} 257 | mockPageValues := make([]types.ValueHolder, 1) 258 | // Has only one value 259 | mockPageValues[0] = mockValueHolder 260 | 261 | mockNextIonBinary := make([]byte, 1) 262 | mockNextIonBinary[0] = 2 263 | mockNextValueHolder := types.ValueHolder{IonBinary: mockNextIonBinary} 264 | mockNextPageValues := make([]types.ValueHolder, 1) 265 | // Has only one value 266 | mockNextPageValues[0] = mockNextValueHolder 267 | mockFetchPageResult := types.FetchPageResult{Page: &types.Page{Values: mockNextPageValues}} 268 | 269 | mockPageToken := "mockToken" 270 | readIOs := int64(1) 271 | writeIOs := int64(2) 272 | processingTime := int64(3) 273 | 274 | testResult := result{ 275 | ctx: context.Background(), 276 | communicator: nil, 277 | txnID: &mockID, 278 | pageValues: mockPageValues, 279 | pageToken: &mockPageToken, 280 | index: 0, 281 | logger: mockLogger, 282 | ioUsage: newIOUsage(readIOs, writeIOs), 283 | timingInfo: newTimingInformation(processingTime), 284 | } 285 | 286 | t.Run("success", func(t *testing.T) { 287 | mockService := new(mockTransactionService) 288 | mockService.On("fetchPage", mock.Anything, mock.Anything, mock.Anything).Return(&mockFetchPageResult, nil) 289 | testResult.communicator = mockService 290 | 291 | bufferedResult, err := testExecutor.BufferResult(&testResult) 292 | assert.Nil(t, err) 293 | assert.True(t, bufferedResult.Next()) 294 | assert.Equal(t, mockIonBinary, bufferedResult.GetCurrentData()) 295 | assert.True(t, bufferedResult.Next()) 296 | assert.Equal(t, mockNextIonBinary, bufferedResult.GetCurrentData()) 297 | assert.Equal(t, processingTime, *bufferedResult.GetTimingInformation().GetProcessingTimeMilliseconds()) 298 | assert.Equal(t, readIOs, *bufferedResult.GetConsumedIOs().GetReadIOs()) 299 | assert.Equal(t, writeIOs, *bufferedResult.GetConsumedIOs().getWriteIOs()) 300 | }) 301 | 302 | t.Run("error", func(t *testing.T) { 303 | mockService := new(mockTransactionService) 304 | mockService.On("fetchPage", mock.Anything, mock.Anything, mock.Anything).Return(&mockFetchPageResult, errMock) 305 | testResult.communicator = mockService 306 | // Reset result state 307 | testResult.pageValues = mockPageValues 308 | testResult.pageToken = &mockPageToken 309 | testResult.index = 0 310 | 311 | bufferedResult, err := testExecutor.BufferResult(&testResult) 312 | assert.Nil(t, bufferedResult) 313 | assert.Equal(t, errMock, err) 314 | }) 315 | }) 316 | 317 | t.Run("Abort", func(t *testing.T) { 318 | abort := testExecutor.Abort() 319 | assert.Error(t, abort) 320 | }) 321 | 322 | t.Run("Transaction ID", func(t *testing.T) { 323 | id := testExecutor.ID() 324 | assert.Equal(t, mockID, id) 325 | }) 326 | } 327 | 328 | type mockTransactionService struct { 329 | mock.Mock 330 | } 331 | 332 | func (m *mockTransactionService) abortTransaction(ctx context.Context) (*types.AbortTransactionResult, error) { 333 | args := m.Called(ctx) 334 | return args.Get(0).(*types.AbortTransactionResult), args.Error(1) 335 | } 336 | 337 | func (m *mockTransactionService) commitTransaction(ctx context.Context, txnID *string, commitDigest []byte) (*types.CommitTransactionResult, error) { 338 | args := m.Called(ctx, txnID, commitDigest) 339 | return args.Get(0).(*types.CommitTransactionResult), args.Error(1) 340 | } 341 | 342 | func (m *mockTransactionService) executeStatement(ctx context.Context, statement *string, parameters []types.ValueHolder, txnID *string) (*types.ExecuteStatementResult, error) { 343 | args := m.Called(ctx, statement, parameters, txnID) 344 | return args.Get(0).(*types.ExecuteStatementResult), args.Error(1) 345 | } 346 | 347 | func (m *mockTransactionService) endSession(ctx context.Context) (*types.EndSessionResult, error) { 348 | panic("not used") 349 | } 350 | 351 | func (m *mockTransactionService) fetchPage(ctx context.Context, pageToken *string, txnID *string) (*types.FetchPageResult, error) { 352 | args := m.Called(ctx, pageToken, txnID) 353 | return args.Get(0).(*types.FetchPageResult), args.Error(1) 354 | } 355 | 356 | func (m *mockTransactionService) startTransaction(ctx context.Context) (*types.StartTransactionResult, error) { 357 | panic("not used") 358 | } 359 | -------------------------------------------------------------------------------- /qldbdriver/session_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | 14 | package qldbdriver 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/mock" 24 | ) 25 | 26 | func TestSessionStartTransaction(t *testing.T) { 27 | t.Run("error", func(t *testing.T) { 28 | mockSessionService := new(mockSessionService) 29 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, errMock) 30 | session := session{mockSessionService, mockLogger} 31 | 32 | result, err := session.startTransaction(context.Background()) 33 | 34 | assert.Equal(t, errMock, err) 35 | assert.Nil(t, result) 36 | }) 37 | 38 | t.Run("success", func(t *testing.T) { 39 | mockSessionService := new(mockSessionService) 40 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 41 | session := session{mockSessionService, mockLogger} 42 | 43 | result, err := session.startTransaction(context.Background()) 44 | 45 | assert.NoError(t, err) 46 | assert.Equal(t, mockTransactionID, *result.id) 47 | }) 48 | } 49 | 50 | func TestSessionEndSession(t *testing.T) { 51 | t.Run("error", func(t *testing.T) { 52 | mockSessionService := new(mockSessionService) 53 | mockSessionService.On("endSession", mock.Anything).Return(&mockEndSessionResult, errMock) 54 | session := session{mockSessionService, mockLogger} 55 | 56 | err := session.endSession(context.Background()) 57 | 58 | assert.Equal(t, errMock, err) 59 | }) 60 | 61 | t.Run("success", func(t *testing.T) { 62 | mockSessionService := new(mockSessionService) 63 | mockSessionService.On("endSession", mock.Anything).Return(&mockEndSessionResult, nil) 64 | session := session{mockSessionService, mockLogger} 65 | 66 | err := session.endSession(context.Background()) 67 | assert.NoError(t, err) 68 | }) 69 | } 70 | 71 | func TestSessionExecute(t *testing.T) { 72 | t.Run("success", func(t *testing.T) { 73 | mockSessionService := new(mockSessionService) 74 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 75 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 76 | Return(&mockExecuteResult, nil) 77 | mockSessionService.On("commitTransaction", mock.Anything, mock.Anything, mock.Anything). 78 | Return(&mockCommitTransactionResult, nil) 79 | session := session{mockSessionService, mockLogger} 80 | 81 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 82 | _, err := txn.Execute("SELECT v FROM table") 83 | if err != nil { 84 | return nil, err 85 | } 86 | return 3, nil 87 | }) 88 | assert.Nil(t, err) 89 | assert.Equal(t, 3, result) 90 | }) 91 | 92 | t.Run("startTxnUnknownErrorAbortSuccess", func(t *testing.T) { 93 | mockSessionService := new(mockSessionService) 94 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, errMock) 95 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, nil) 96 | session := session{mockSessionService, mockLogger} 97 | 98 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 99 | _, err := txn.Execute("SELECT v FROM table") 100 | if err != nil { 101 | return nil, err 102 | } 103 | return 3, nil 104 | }) 105 | 106 | assert.Nil(t, result) 107 | assert.Equal(t, errMock, err.err) 108 | assert.False(t, err.isISE) 109 | assert.False(t, err.canRetry) 110 | assert.True(t, err.abortSuccess) 111 | }) 112 | 113 | t.Run("startTxnUnknownErrorAbortErr", func(t *testing.T) { 114 | mockSessionService := new(mockSessionService) 115 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, errMock) 116 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, errMock) 117 | session := session{mockSessionService, mockLogger} 118 | 119 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 120 | _, err := txn.Execute("SELECT v FROM table") 121 | if err != nil { 122 | return nil, err 123 | } 124 | return 3, nil 125 | }) 126 | 127 | assert.Nil(t, result) 128 | assert.Equal(t, errMock, err.err) 129 | assert.False(t, err.isISE) 130 | assert.False(t, err.canRetry) 131 | assert.False(t, err.abortSuccess) 132 | }) 133 | 134 | t.Run("startTxnISE", func(t *testing.T) { 135 | mockSessionService := new(mockSessionService) 136 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, testISE) 137 | session := session{mockSessionService, mockLogger} 138 | 139 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 140 | _, err := txn.Execute("SELECT * FROM table") 141 | if err != nil { 142 | return nil, err 143 | } 144 | return 3, nil 145 | }) 146 | 147 | assert.Nil(t, result) 148 | assert.Equal(t, testISE, err.err) 149 | assert.True(t, err.isISE) 150 | assert.True(t, err.canRetry) 151 | assert.False(t, err.abortSuccess) 152 | }) 153 | 154 | t.Run("startTxn500AbortSuccess", func(t *testing.T) { 155 | mockSessionService := new(mockSessionService) 156 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, test500) 157 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, nil) 158 | session := session{mockSessionService, mockLogger} 159 | 160 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 161 | _, err := txn.Execute("SELECT v FROM table") 162 | if err != nil { 163 | return nil, err 164 | } 165 | return 3, nil 166 | }) 167 | 168 | assert.Nil(t, result) 169 | assert.Equal(t, test500, err.err) 170 | assert.Equal(t, "", err.transactionID) 171 | assert.False(t, err.isISE) 172 | assert.True(t, err.canRetry) 173 | assert.True(t, err.abortSuccess) 174 | }) 175 | 176 | t.Run("startTxn500AbortError", func(t *testing.T) { 177 | mockSessionService := new(mockSessionService) 178 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, test500) 179 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, errMock) 180 | session := session{mockSessionService, mockLogger} 181 | 182 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 183 | _, err := txn.Execute("SELECT v FROM table") 184 | if err != nil { 185 | return nil, err 186 | } 187 | return 3, nil 188 | }) 189 | 190 | assert.Nil(t, result) 191 | assert.Equal(t, test500, err.err) 192 | assert.Equal(t, "", err.transactionID) 193 | assert.False(t, err.isISE) 194 | assert.True(t, err.canRetry) 195 | assert.False(t, err.abortSuccess) 196 | }) 197 | 198 | t.Run("executeUnknownErrorAbortSuccess", func(t *testing.T) { 199 | mockSessionService := new(mockSessionService) 200 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 201 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 202 | Return(&mockExecuteResult, errMock) 203 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, nil) 204 | session := session{mockSessionService, mockLogger} 205 | 206 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 207 | _, err := txn.Execute("SELECT v FROM table") 208 | if err != nil { 209 | return nil, err 210 | } 211 | return 3, nil 212 | }) 213 | 214 | assert.Nil(t, result) 215 | assert.Equal(t, errMock, err.err) 216 | assert.False(t, err.isISE) 217 | assert.False(t, err.canRetry) 218 | assert.True(t, err.abortSuccess) 219 | }) 220 | 221 | t.Run("executeUnknownErrorAbortError", func(t *testing.T) { 222 | mockSessionService := new(mockSessionService) 223 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 224 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 225 | Return(&mockExecuteResult, errMock) 226 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, errMock) 227 | session := session{mockSessionService, mockLogger} 228 | 229 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 230 | _, err := txn.Execute("SELECT v FROM table") 231 | if err != nil { 232 | return nil, err 233 | } 234 | return 3, nil 235 | }) 236 | 237 | assert.Nil(t, result) 238 | assert.Equal(t, errMock, err.err) 239 | assert.False(t, err.isISE) 240 | assert.False(t, err.canRetry) 241 | assert.False(t, err.abortSuccess) 242 | }) 243 | 244 | t.Run("executeISE", func(t *testing.T) { 245 | mockSessionService := new(mockSessionService) 246 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 247 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 248 | Return(&mockExecuteResult, testISE) 249 | session := session{mockSessionService, mockLogger} 250 | 251 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 252 | _, err := txn.Execute("SELECT v FROM table") 253 | if err != nil { 254 | return nil, err 255 | } 256 | return 3, nil 257 | }) 258 | 259 | assert.Nil(t, result) 260 | assert.Equal(t, testISE, err.err) 261 | assert.True(t, err.isISE) 262 | assert.True(t, err.canRetry) 263 | assert.False(t, err.abortSuccess) 264 | }) 265 | 266 | t.Run("execute500AbortSuccess", func(t *testing.T) { 267 | mockSessionService := new(mockSessionService) 268 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 269 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 270 | Return(&mockExecuteResult, test500) 271 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, nil) 272 | session := session{mockSessionService, mockLogger} 273 | 274 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 275 | _, err := txn.Execute("SELECT v FROM table") 276 | if err != nil { 277 | return nil, err 278 | } 279 | return 3, nil 280 | }) 281 | 282 | assert.Nil(t, result) 283 | assert.IsType(t, &txnError{}, err) 284 | assert.Equal(t, test500, err.err) 285 | assert.Equal(t, mockTransactionID, err.transactionID) 286 | assert.False(t, err.isISE) 287 | assert.True(t, err.canRetry) 288 | assert.True(t, err.abortSuccess) 289 | }) 290 | 291 | t.Run("execute500AbortError", func(t *testing.T) { 292 | mockSessionService := new(mockSessionService) 293 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 294 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 295 | Return(&mockExecuteResult, test500) 296 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, errMock) 297 | session := session{mockSessionService, mockLogger} 298 | 299 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 300 | _, err := txn.Execute("SELECT v FROM table") 301 | if err != nil { 302 | return nil, err 303 | } 304 | return 3, nil 305 | }) 306 | 307 | assert.Nil(t, result) 308 | assert.IsType(t, &txnError{}, err) 309 | assert.Equal(t, test500, err.err) 310 | assert.Equal(t, mockTransactionID, err.transactionID) 311 | assert.False(t, err.isISE) 312 | assert.True(t, err.canRetry) 313 | assert.False(t, err.abortSuccess) 314 | }) 315 | 316 | t.Run("executeBadReqAbortSuccess", func(t *testing.T) { 317 | mockSessionService := new(mockSessionService) 318 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 319 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 320 | Return(&mockExecuteResult, testBadReq) 321 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, nil) 322 | session := session{mockSessionService, mockLogger} 323 | 324 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 325 | _, err := txn.Execute("SELECT v FROM table") 326 | if err != nil { 327 | return nil, err 328 | } 329 | return 3, nil 330 | }) 331 | 332 | assert.Nil(t, result) 333 | assert.Equal(t, testBadReq, err.err) 334 | assert.False(t, err.isISE) 335 | assert.False(t, err.canRetry) 336 | assert.True(t, err.abortSuccess) 337 | }) 338 | 339 | t.Run("executeBadReqAbortError", func(t *testing.T) { 340 | mockSessionService := new(mockSessionService) 341 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 342 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 343 | Return(&mockExecuteResult, testBadReq) 344 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, errMock) 345 | session := session{mockSessionService, mockLogger} 346 | 347 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 348 | _, err := txn.Execute("SELECT v FROM table") 349 | if err != nil { 350 | return nil, err 351 | } 352 | return 3, nil 353 | }) 354 | 355 | assert.Nil(t, result) 356 | assert.Equal(t, testBadReq, err.err) 357 | assert.False(t, err.isISE) 358 | assert.False(t, err.canRetry) 359 | assert.False(t, err.abortSuccess) 360 | }) 361 | 362 | t.Run("commitUnknownErrorAbortSuccess", func(t *testing.T) { 363 | mockSessionService := new(mockSessionService) 364 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 365 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 366 | Return(&mockExecuteResult, nil) 367 | mockSessionService.On("commitTransaction", mock.Anything, mock.Anything, mock.Anything). 368 | Return(&mockCommitTransactionResult, errMock) 369 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, nil) 370 | session := session{mockSessionService, mockLogger} 371 | 372 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 373 | _, err := txn.Execute("SELECT v FROM table") 374 | if err != nil { 375 | return nil, err 376 | } 377 | return 3, nil 378 | }) 379 | 380 | assert.Nil(t, result) 381 | assert.Equal(t, errMock, err.err) 382 | assert.False(t, err.isISE) 383 | assert.False(t, err.canRetry) 384 | assert.True(t, err.abortSuccess) 385 | }) 386 | 387 | t.Run("commitUnknownErrorAbortError", func(t *testing.T) { 388 | mockSessionService := new(mockSessionService) 389 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 390 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 391 | Return(&mockExecuteResult, nil) 392 | mockSessionService.On("commitTransaction", mock.Anything, mock.Anything, mock.Anything). 393 | Return(&mockCommitTransactionResult, errMock) 394 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, errMock) 395 | session := session{mockSessionService, mockLogger} 396 | 397 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 398 | _, err := txn.Execute("SELECT v FROM table") 399 | if err != nil { 400 | return nil, err 401 | } 402 | return 3, nil 403 | }) 404 | 405 | assert.Nil(t, result) 406 | assert.Equal(t, errMock, err.err) 407 | assert.False(t, err.isISE) 408 | assert.False(t, err.canRetry) 409 | assert.False(t, err.abortSuccess) 410 | }) 411 | 412 | t.Run("commit500AbortSuccess", func(t *testing.T) { 413 | mockSessionService := new(mockSessionService) 414 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 415 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 416 | Return(&mockExecuteResult, nil) 417 | mockSessionService.On("commitTransaction", mock.Anything, mock.Anything, mock.Anything). 418 | Return(&mockCommitTransactionResult, test500) 419 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, nil) 420 | session := session{mockSessionService, mockLogger} 421 | 422 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 423 | _, err := txn.Execute("SELECT v FROM table") 424 | if err != nil { 425 | return nil, err 426 | } 427 | return 3, nil 428 | }) 429 | 430 | assert.Nil(t, result) 431 | assert.Equal(t, test500, err.err) 432 | assert.Equal(t, mockTransactionID, err.transactionID) 433 | assert.False(t, err.isISE) 434 | assert.True(t, err.canRetry) 435 | assert.True(t, err.abortSuccess) 436 | }) 437 | 438 | t.Run("commit500AbortError", func(t *testing.T) { 439 | mockSessionService := new(mockSessionService) 440 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 441 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 442 | Return(&mockExecuteResult, nil) 443 | mockSessionService.On("commitTransaction", mock.Anything, mock.Anything, mock.Anything). 444 | Return(&mockCommitTransactionResult, test500) 445 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, errMock) 446 | session := session{mockSessionService, mockLogger} 447 | 448 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 449 | _, err := txn.Execute("SELECT v FROM table") 450 | if err != nil { 451 | return nil, err 452 | } 453 | return 3, nil 454 | }) 455 | 456 | assert.Nil(t, result) 457 | assert.Equal(t, test500, err.err) 458 | assert.Equal(t, mockTransactionID, err.transactionID) 459 | assert.False(t, err.isISE) 460 | assert.True(t, err.canRetry) 461 | assert.False(t, err.abortSuccess) 462 | }) 463 | 464 | t.Run("commitOCC", func(t *testing.T) { 465 | mockSessionService := new(mockSessionService) 466 | mockSessionService.On("startTransaction", mock.Anything).Return(&mockStartTransactionResult, nil) 467 | mockSessionService.On("executeStatement", mock.Anything, mock.Anything, mock.Anything, mock.Anything). 468 | Return(&mockExecuteResult, nil) 469 | mockSessionService.On("commitTransaction", mock.Anything, mock.Anything, mock.Anything). 470 | Return(&mockCommitTransactionResult, testOCC) 471 | session := session{mockSessionService, mockLogger} 472 | 473 | result, err := session.execute(context.Background(), func(txn Transaction) (interface{}, error) { 474 | _, err := txn.Execute("SELECT v FROM table") 475 | if err != nil { 476 | return nil, err 477 | } 478 | return 3, nil 479 | }) 480 | 481 | assert.Nil(t, result) 482 | assert.Equal(t, testOCC, err.err) 483 | assert.False(t, err.isISE) 484 | assert.True(t, err.canRetry) 485 | assert.True(t, err.abortSuccess) 486 | }) 487 | 488 | t.Run("wrappedAWSErrorHandling", func(t *testing.T) { 489 | mockSessionService := new(mockSessionService) 490 | mockSessionService.On("abortTransaction", mock.Anything).Return(&mockAbortTransactionResult, errMock) 491 | 492 | session := session{mockSessionService, mockLogger} 493 | 494 | err := session.wrapError(context.Background(), fmt.Errorf("ordinary error"), mockTransactionID) 495 | assert.Equal(t, "", err.message) 496 | 497 | err = session.wrapError(context.Background(), testOCC, mockTransactionID) 498 | assert.Equal(t, testOCC, err.err) 499 | assert.True(t, err.canRetry) 500 | }) 501 | } 502 | 503 | var mockTransactionID = "testTransactionIdddddd" 504 | var mockAbortTransactionResult = types.AbortTransactionResult{} 505 | var mockStartTransactionResult = types.StartTransactionResult{TransactionId: &mockTransactionID} 506 | var mockEndSessionResult = types.EndSessionResult{} 507 | var mockExecuteResult = types.ExecuteStatementResult{ 508 | FirstPage: &types.Page{}, 509 | } 510 | var mockHash = []byte{73, 10, 104, 87, 43, 252, 182, 60, 142, 193, 0, 77, 158, 129, 52, 84, 126, 196, 120, 55, 241, 253, 113, 114, 114, 53, 233, 223, 234, 227, 191, 172} 511 | var mockCommitTransactionResult = types.CommitTransactionResult{ 512 | TransactionId: &mockTransactionID, 513 | CommitDigest: mockHash, 514 | } 515 | 516 | var testISE = &types.InvalidSessionException{Code: &ErrCodeInvalidSessionException, Message: &ErrMessageInvalidSessionException} 517 | var testOCC = &types.OccConflictException{Message: &ErrMessageOccConflictException} 518 | var testBadReq = &types.BadRequestException{Code: &ErrCodeBadRequestException, Message: &ErrMessageBadRequestException} 519 | var test500 = &InternalFailure{Code: &ErrCodeInternalFailure, Message: &ErrMessageInternalFailure} 520 | 521 | type mockSessionService struct { 522 | mock.Mock 523 | } 524 | 525 | func (m *mockSessionService) abortTransaction(ctx context.Context) (*types.AbortTransactionResult, error) { 526 | args := m.Called(ctx) 527 | return args.Get(0).(*types.AbortTransactionResult), args.Error(1) 528 | } 529 | 530 | func (m *mockSessionService) commitTransaction(ctx context.Context, txnID *string, commitDigest []byte) (*types.CommitTransactionResult, error) { 531 | args := m.Called(ctx, txnID, commitDigest) 532 | return args.Get(0).(*types.CommitTransactionResult), args.Error(1) 533 | } 534 | 535 | func (m *mockSessionService) executeStatement(ctx context.Context, statement *string, parameters []types.ValueHolder, txnID *string) (*types.ExecuteStatementResult, error) { 536 | args := m.Called(ctx, statement, parameters, txnID) 537 | return args.Get(0).(*types.ExecuteStatementResult), args.Error(1) 538 | } 539 | 540 | func (m *mockSessionService) endSession(ctx context.Context) (*types.EndSessionResult, error) { 541 | args := m.Called(ctx) 542 | return args.Get(0).(*types.EndSessionResult), args.Error(1) 543 | } 544 | 545 | func (m *mockSessionService) fetchPage(ctx context.Context, pageToken *string, txnID *string) (*types.FetchPageResult, error) { 546 | panic("not used") 547 | } 548 | 549 | func (m *mockSessionService) startTransaction(ctx context.Context) (*types.StartTransactionResult, error) { 550 | args := m.Called(ctx) 551 | return args.Get(0).(*types.StartTransactionResult), args.Error(1) 552 | } 553 | -------------------------------------------------------------------------------- /qldbdriver/qldbdriver_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | the License. A copy of the License is located at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | and limitations under the License. 12 | */ 13 | package qldbdriver 14 | 15 | import ( 16 | "context" 17 | "errors" 18 | "testing" 19 | "time" 20 | 21 | "github.com/amzn/ion-go/ion" 22 | "github.com/aws/aws-sdk-go-v2/config" 23 | "github.com/aws/aws-sdk-go-v2/service/qldbsession" 24 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/mock" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestNew(t *testing.T) { 31 | t.Run("0 max transactions error", func(t *testing.T) { 32 | cfg, err := config.LoadDefaultConfig(context.TODO()) 33 | require.NoError(t, err) 34 | qldbSession := qldbsession.NewFromConfig(cfg) 35 | 36 | _, err = New(mockLedgerName, 37 | qldbSession, 38 | func(options *DriverOptions) { 39 | options.LoggerVerbosity = LogOff 40 | options.MaxConcurrentTransactions = 0 41 | }) 42 | assert.Error(t, err) 43 | }) 44 | 45 | t.Run("Invalid QLDBSession error", func(t *testing.T) { 46 | _, err := New(mockLedgerName, 47 | nil, 48 | func(options *DriverOptions) { 49 | options.LoggerVerbosity = LogOff 50 | }) 51 | assert.Error(t, err) 52 | }) 53 | 54 | t.Run("New default success", func(t *testing.T) { 55 | cfg, err := config.LoadDefaultConfig(context.TODO()) 56 | require.NoError(t, err) 57 | qldbSession := qldbsession.NewFromConfig(cfg) 58 | 59 | createdDriver, err := New(mockLedgerName, 60 | qldbSession, 61 | func(options *DriverOptions) { 62 | options.LoggerVerbosity = LogOff 63 | }) 64 | require.NoError(t, err) 65 | 66 | assert.Equal(t, createdDriver.ledgerName, mockLedgerName) 67 | assert.Equal(t, createdDriver.maxConcurrentTransactions, defaultMaxConcurrentTransactions) 68 | assert.Equal(t, createdDriver.retryPolicy.MaxRetryLimit, defaultRetry) 69 | assert.Equal(t, createdDriver.isClosed, false) 70 | assert.Equal(t, cap(createdDriver.sessionPool), defaultMaxConcurrentTransactions) 71 | 72 | driverQldbSession := createdDriver.qldbSession 73 | 74 | assert.Equal(t, qldbSession, driverQldbSession) 75 | }) 76 | 77 | t.Run("Retry limit overflow handled", func(t *testing.T) { 78 | cfg, err := config.LoadDefaultConfig(context.TODO()) 79 | require.NoError(t, err) 80 | qldbSession := qldbsession.NewFromConfig(cfg) 81 | 82 | createdDriver, err := New(mockLedgerName, 83 | qldbSession, 84 | func(options *DriverOptions) { 85 | options.LoggerVerbosity = LogOff 86 | options.MaxConcurrentTransactions = 65534 87 | }) 88 | require.NoError(t, err) 89 | assert.Equal(t, 65534, createdDriver.maxConcurrentTransactions) 90 | }) 91 | 92 | t.Run("Protected against QLDBSession mutation", func(t *testing.T) { 93 | cfg, err := config.LoadDefaultConfig(context.TODO()) 94 | require.NoError(t, err) 95 | qldbSession := qldbsession.NewFromConfig(cfg) 96 | 97 | createdDriver, err := New(mockLedgerName, 98 | qldbSession, 99 | func(options *DriverOptions) { 100 | options.LoggerVerbosity = LogOff 101 | }) 102 | require.NoError(t, err) 103 | 104 | driverQldbSession := createdDriver.qldbSession 105 | 106 | qldbSession = nil 107 | assert.NotNil(t, driverQldbSession) 108 | }) 109 | } 110 | 111 | func TestExecute(t *testing.T) { 112 | testDriver := QLDBDriver{ 113 | ledgerName: mockLedgerName, 114 | qldbSession: nil, 115 | maxConcurrentTransactions: 10, 116 | logger: mockLogger, 117 | isClosed: false, 118 | semaphore: makeSemaphore(10), 119 | sessionPool: make(chan *session, 10), 120 | retryPolicy: RetryPolicy{ 121 | MaxRetryLimit: 4, 122 | Backoff: ExponentialBackoffStrategy{ 123 | SleepBase: time.Duration(10) * time.Millisecond, 124 | SleepCap: time.Duration(5000) * time.Millisecond}}, 125 | } 126 | 127 | t.Run("Execute with closed driver error", func(t *testing.T) { 128 | testDriver.isClosed = true 129 | 130 | _, err := testDriver.Execute(context.Background(), nil) 131 | assert.Error(t, err) 132 | 133 | testDriver.isClosed = false 134 | }) 135 | 136 | t.Run("error", func(t *testing.T) { 137 | mockSession := new(mockQLDBSession) 138 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockDriverSendCommand, errMock) 139 | testDriver.qldbSession = mockSession 140 | 141 | result, err := testDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 142 | // Note : We are using a select * without specifying a where condition for the purpose of this test. 143 | // However, we do not recommend using such a query in a normal/production context. 144 | innerResult, innerErr := txn.Execute("SELECT * FROM someTable") 145 | if innerErr != nil { 146 | return nil, innerErr 147 | } 148 | return innerResult, innerErr 149 | }) 150 | assert.Equal(t, err, errMock) 151 | assert.Nil(t, result) 152 | }) 153 | 154 | t.Run("success", func(t *testing.T) { 155 | mockTables := make([]string, 1) 156 | mockTables = append(mockTables, "table1") 157 | mockSession := new(mockQLDBSession) 158 | 159 | mockSendCommandWithTxID.CommitTransaction.CommitDigest = []byte{167, 123, 231, 255, 170, 172, 35, 142, 73, 31, 239, 199, 252, 120, 175, 217, 235, 220, 184, 200, 85, 203, 140, 230, 151, 221, 131, 255, 163, 151, 170, 210} 160 | 161 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommandWithTxID, nil) 162 | testDriver.qldbSession = mockSession 163 | 164 | executeResult, err := testDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 165 | tableNames := make([]string, 1) 166 | tableNames = append(tableNames, "table1") 167 | return tableNames, nil 168 | }) 169 | 170 | assert.Equal(t, mockTables, executeResult.([]string)) 171 | assert.Nil(t, err) 172 | }) 173 | 174 | t.Run("error get session", func(t *testing.T) { 175 | mockSession := new(mockQLDBSession) 176 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockDriverSendCommand, errMock) 177 | testDriver.qldbSession = mockSession 178 | testDriver.sessionPool = make(chan *session, 10) 179 | 180 | result, err := testDriver.Execute(context.Background(), nil) 181 | 182 | assert.Nil(t, result) 183 | assert.Equal(t, err, errMock) 184 | }) 185 | 186 | t.Run("error session execute", func(t *testing.T) { 187 | mockSendCommandForSession := qldbsession.SendCommandOutput{ 188 | AbortTransaction: &mockAbortTransaction, 189 | CommitTransaction: &mockCommitTransaction, 190 | EndSession: &mockEndSession, 191 | ExecuteStatement: &mockExecuteStatement, 192 | FetchPage: &mockFetchPage, 193 | StartSession: &mockStartSession, 194 | StartTransaction: &mockStartTransactionWithID, 195 | } 196 | 197 | startSession := &types.StartSessionRequest{LedgerName: &mockLedgerName} 198 | startSessionRequest := &qldbsession.SendCommandInput{StartSession: startSession} 199 | 200 | startTransaction := &types.StartTransactionRequest{} 201 | startTransactionRequest := &qldbsession.SendCommandInput{StartTransaction: startTransaction} 202 | startTransactionRequest.SessionToken = &mockDriverSessionToken 203 | 204 | abortTransaction := &types.AbortTransactionRequest{} 205 | abortTransactionRequest := &qldbsession.SendCommandInput{AbortTransaction: abortTransaction} 206 | abortTransactionRequest.SessionToken = &mockDriverSessionToken 207 | 208 | testOCCError := &types.OccConflictException{Message: &ErrMessageOccConflictException} 209 | 210 | mockSession := new(mockQLDBSession) 211 | mockSession.On("SendCommand", mock.Anything, startSessionRequest, mock.Anything).Return(&mockSendCommandForSession, nil) 212 | mockSession.On("SendCommand", mock.Anything, startTransactionRequest, mock.Anything).Return(&mockSendCommandForSession, testOCCError) 213 | mockSession.On("SendCommand", mock.Anything, abortTransactionRequest, mock.Anything).Return(&mockSendCommandForSession, nil) 214 | testDriver.qldbSession = mockSession 215 | 216 | testDriver.sessionPool = make(chan *session, 10) 217 | testDriver.semaphore = makeSemaphore(10) 218 | 219 | result, err := testDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 220 | tableNames := make([]string, 1) 221 | tableNames = append(tableNames, "table1") 222 | return tableNames, nil 223 | }) 224 | 225 | assert.Nil(t, result) 226 | 227 | var occ *types.OccConflictException 228 | assert.True(t, errors.As(err, &occ)) 229 | assert.Equal(t, testOCCError, err) 230 | mockSession.AssertNumberOfCalls(t, "SendCommand", 6) 231 | }) 232 | 233 | t.Run("success execute without retry", func(t *testing.T) { 234 | mockSendCommandWithTxID.CommitTransaction.CommitDigest = []byte{167, 123, 231, 255, 170, 172, 35, 142, 73, 31, 239, 199, 252, 120, 175, 217, 235, 220, 184, 200, 85, 203, 140, 230, 151, 221, 131, 255, 163, 151, 170, 210} 235 | 236 | mockSession := new(mockQLDBSession) 237 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommandWithTxID, nil) 238 | testDriver.qldbSession = mockSession 239 | 240 | testDriver.sessionPool = make(chan *session, 10) 241 | testDriver.semaphore = makeSemaphore(10) 242 | 243 | result, err := testDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 244 | tableNames := make([]string, 1) 245 | tableNames = append(tableNames, "table1") 246 | return tableNames, nil 247 | }) 248 | 249 | expectedTables := make([]string, 1) 250 | expectedTables = append(expectedTables, "table1") 251 | 252 | assert.Equal(t, expectedTables, result.([]string)) 253 | assert.NoError(t, err) 254 | }) 255 | 256 | t.Run("success execute with retry on ISE", func(t *testing.T) { 257 | hash := []byte{167, 123, 231, 255, 170, 172, 35, 142, 73, 31, 239, 199, 252, 120, 175, 217, 235, 220, 184, 200, 85, 203, 140, 230, 151, 221, 131, 255, 163, 151, 170, 210} 258 | mockSendCommandWithTxID.CommitTransaction.CommitDigest = hash 259 | 260 | startSession := &types.StartSessionRequest{LedgerName: &mockLedgerName} 261 | startSessionRequest := &qldbsession.SendCommandInput{StartSession: startSession} 262 | 263 | startTransaction := &types.StartTransactionRequest{} 264 | startTransactionRequest := &qldbsession.SendCommandInput{StartTransaction: startTransaction} 265 | startTransactionRequest.SessionToken = &mockDriverSessionToken 266 | 267 | commitTransaction := &types.CommitTransactionRequest{TransactionId: &mockTxnID, CommitDigest: hash} 268 | commitTransactionRequest := &qldbsession.SendCommandInput{CommitTransaction: commitTransaction} 269 | commitTransactionRequest.SessionToken = &mockDriverSessionToken 270 | 271 | testISE := &types.InvalidSessionException{Code: &ErrCodeInvalidSessionException, Message: &ErrMessageInvalidSessionException} 272 | 273 | mockSession := new(mockQLDBSession) 274 | mockSession.On("SendCommand", mock.Anything, startSessionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 275 | mockSession.On("SendCommand", mock.Anything, startTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 276 | mockSession.On("SendCommand", mock.Anything, commitTransactionRequest, mock.Anything). 277 | Return(&mockSendCommandWithTxID, testISE).Times(4) 278 | mockSession.On("SendCommand", mock.Anything, commitTransactionRequest, mock.Anything). 279 | Return(&mockSendCommandWithTxID, nil).Once() 280 | 281 | testDriver.qldbSession = mockSession 282 | 283 | testDriver.sessionPool = make(chan *session, 10) 284 | testDriver.semaphore = makeSemaphore(10) 285 | 286 | result, err := testDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 287 | tableNames := make([]string, 1) 288 | tableNames = append(tableNames, "table1") 289 | return tableNames, nil 290 | }) 291 | 292 | expectedTables := make([]string, 1) 293 | expectedTables = append(expectedTables, "table1") 294 | 295 | assert.Equal(t, expectedTables, result.([]string)) 296 | assert.NoError(t, err) 297 | }) 298 | 299 | t.Run("ISE returned when exceed ISE retry limit", func(t *testing.T) { 300 | hash := []byte{167, 123, 231, 255, 170, 172, 35, 142, 73, 31, 239, 199, 252, 120, 175, 217, 235, 220, 184, 200, 85, 203, 140, 230, 151, 221, 131, 255, 163, 151, 170, 210} 301 | mockSendCommandWithTxID.CommitTransaction.CommitDigest = hash 302 | 303 | startSession := &types.StartSessionRequest{LedgerName: &mockLedgerName} 304 | startSessionRequest := &qldbsession.SendCommandInput{StartSession: startSession} 305 | 306 | startTransaction := &types.StartTransactionRequest{} 307 | startTransactionRequest := &qldbsession.SendCommandInput{StartTransaction: startTransaction} 308 | startTransactionRequest.SessionToken = &mockDriverSessionToken 309 | 310 | commitTransaction := &types.CommitTransactionRequest{TransactionId: &mockTxnID, CommitDigest: hash} 311 | commitTransactionRequest := &qldbsession.SendCommandInput{CommitTransaction: commitTransaction} 312 | commitTransactionRequest.SessionToken = &mockDriverSessionToken 313 | 314 | testISE := &types.InvalidSessionException{Code: &ErrCodeInvalidSessionException, Message: &ErrMessageInvalidSessionException} 315 | 316 | mockSession := new(mockQLDBSession) 317 | mockSession.On("SendCommand", mock.Anything, startSessionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 318 | mockSession.On("SendCommand", mock.Anything, startTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 319 | mockSession.On("SendCommand", mock.Anything, commitTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, testISE) 320 | 321 | testDriver.qldbSession = mockSession 322 | 323 | testDriver.sessionPool = make(chan *session, 10) 324 | testDriver.semaphore = makeSemaphore(10) 325 | 326 | result, err := testDriver.Execute(context.Background(), 327 | func(txn Transaction) (interface{}, error) { 328 | tableNames := make([]string, 1) 329 | tableNames = append(tableNames, "table1") 330 | return tableNames, nil 331 | }) 332 | assert.Error(t, err) 333 | assert.Nil(t, result) 334 | 335 | var ise *types.InvalidSessionException 336 | assert.True(t, errors.As(err, &ise)) 337 | assert.Equal(t, testISE, err) 338 | }) 339 | 340 | t.Run("CapacityExceededException returned when exceed CapacityExceededException retry limit", func(t *testing.T) { 341 | hash := []byte{167, 123, 231, 255, 170, 172, 35, 142, 73, 31, 239, 199, 252, 120, 175, 217, 235, 220, 184, 200, 85, 203, 140, 230, 151, 221, 131, 255, 163, 151, 170, 210} 342 | mockSendCommandWithTxID.CommitTransaction.CommitDigest = hash 343 | 344 | startSession := &types.StartSessionRequest{LedgerName: &mockLedgerName} 345 | startSessionRequest := &qldbsession.SendCommandInput{StartSession: startSession} 346 | 347 | startTransaction := &types.StartTransactionRequest{} 348 | startTransactionRequest := &qldbsession.SendCommandInput{StartTransaction: startTransaction} 349 | startTransactionRequest.SessionToken = &mockDriverSessionToken 350 | 351 | abortTransaction := &types.AbortTransactionRequest{} 352 | abortTransactionRequest := &qldbsession.SendCommandInput{AbortTransaction: abortTransaction} 353 | abortTransactionRequest.SessionToken = &mockDriverSessionToken 354 | 355 | commitTransaction := &types.CommitTransactionRequest{TransactionId: &mockTxnID, CommitDigest: hash} 356 | commitTransactionRequest := &qldbsession.SendCommandInput{CommitTransaction: commitTransaction} 357 | commitTransactionRequest.SessionToken = &mockDriverSessionToken 358 | 359 | testCEE := &types.CapacityExceededException{Message: &ErrMessageCapacityExceedException} 360 | 361 | mockSession := new(mockQLDBSession) 362 | mockSession.On("SendCommand", mock.Anything, startSessionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 363 | mockSession.On("SendCommand", mock.Anything, startTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 364 | mockSession.On("SendCommand", mock.Anything, commitTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, testCEE) 365 | mockSession.On("SendCommand", mock.Anything, abortTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil).Times(5) 366 | 367 | testDriver.qldbSession = mockSession 368 | 369 | result, err := testDriver.Execute(context.Background(), 370 | func(txn Transaction) (interface{}, error) { 371 | return "tableNames", nil 372 | }) 373 | assert.Error(t, err) 374 | assert.Nil(t, result) 375 | 376 | var cee *types.CapacityExceededException 377 | assert.True(t, errors.As(err, &cee)) 378 | assert.Equal(t, testCEE, err) 379 | }) 380 | 381 | t.Run("error on transaction expiry.", func(t *testing.T) { 382 | hash := []byte{167, 123, 231, 255, 170, 172, 35, 142, 73, 31, 239, 199, 252, 120, 175, 217, 235, 220, 184, 200, 85, 203, 140, 230, 151, 221, 131, 255, 163, 151, 170, 210} 383 | mockSendCommandWithTxID.CommitTransaction.CommitDigest = hash 384 | 385 | startSession := &types.StartSessionRequest{LedgerName: &mockLedgerName} 386 | startSessionRequest := &qldbsession.SendCommandInput{StartSession: startSession} 387 | 388 | startTransaction := &types.StartTransactionRequest{} 389 | startTransactionRequest := &qldbsession.SendCommandInput{StartTransaction: startTransaction} 390 | startTransactionRequest.SessionToken = &mockDriverSessionToken 391 | 392 | commitTransaction := &types.CommitTransactionRequest{TransactionId: &mockTxnID, CommitDigest: hash} 393 | commitTransactionRequest := &qldbsession.SendCommandInput{CommitTransaction: commitTransaction} 394 | commitTransactionRequest.SessionToken = &mockDriverSessionToken 395 | 396 | testTxnExpire := &types.InvalidSessionException{Code: &ErrCodeInvalidSessionException, Message: &ErrCodeInvalidSessionException2} 397 | 398 | mockSession := new(mockQLDBSession) 399 | mockSession.On("SendCommand", mock.Anything, startSessionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 400 | mockSession.On("SendCommand", mock.Anything, startTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 401 | mockSession.On("SendCommand", mock.Anything, commitTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, testTxnExpire).Once() 402 | 403 | testDriver.qldbSession = mockSession 404 | 405 | testDriver.sessionPool = make(chan *session, 10) 406 | testDriver.semaphore = makeSemaphore(10) 407 | 408 | result, err := testDriver.Execute(context.Background(), 409 | func(txn Transaction) (interface{}, error) { 410 | tableNames := make([]string, 1) 411 | tableNames = append(tableNames, "table1") 412 | return tableNames, nil 413 | }) 414 | assert.Error(t, err) 415 | assert.Nil(t, result) 416 | 417 | var ise *types.InvalidSessionException 418 | assert.True(t, errors.As(err, &ise)) 419 | assert.Equal(t, testTxnExpire, err) 420 | }) 421 | 422 | t.Run("abort transaction on customer error", func(t *testing.T) { 423 | hash := []byte{167, 123, 231, 255, 170, 172, 35, 142, 73, 31, 239, 199, 252, 120, 175, 217, 235, 220, 184, 200, 85, 203, 140, 230, 151, 221, 131, 255, 163, 151, 170, 210} 424 | mockSendCommandWithTxID.CommitTransaction.CommitDigest = hash 425 | 426 | startSession := &types.StartSessionRequest{LedgerName: &mockLedgerName} 427 | startSessionRequest := &qldbsession.SendCommandInput{StartSession: startSession} 428 | 429 | startTransaction := &types.StartTransactionRequest{} 430 | startTransactionRequest := &qldbsession.SendCommandInput{StartTransaction: startTransaction} 431 | startTransactionRequest.SessionToken = &mockDriverSessionToken 432 | 433 | commitTransaction := &types.CommitTransactionRequest{TransactionId: &mockTxnID, CommitDigest: hash} 434 | commitTransactionRequest := &qldbsession.SendCommandInput{CommitTransaction: commitTransaction} 435 | commitTransactionRequest.SessionToken = &mockDriverSessionToken 436 | 437 | abortTransaction := &types.AbortTransactionRequest{} 438 | abortTransactionRequest := &qldbsession.SendCommandInput{AbortTransaction: abortTransaction} 439 | abortTransactionRequest.SessionToken = &mockDriverSessionToken 440 | 441 | customerErr := errors.New("customer error") 442 | 443 | mockSession := new(mockQLDBSession) 444 | mockSession.On("SendCommand", mock.Anything, startSessionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 445 | mockSession.On("SendCommand", mock.Anything, startTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil) 446 | mockSession.On("SendCommand", mock.Anything, abortTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil).Once() 447 | 448 | testDriver.qldbSession = mockSession 449 | 450 | testDriver.sessionPool = make(chan *session, 10) 451 | testDriver.semaphore = makeSemaphore(10) 452 | 453 | result, err := testDriver.Execute(context.Background(), 454 | func(txn Transaction) (interface{}, error) { 455 | return nil, customerErr 456 | }) 457 | assert.Error(t, err) 458 | assert.Nil(t, result) 459 | assert.Equal(t, customerErr, err) 460 | 461 | mockSession.AssertNumberOfCalls(t, "SendCommand", 3) 462 | }) 463 | 464 | t.Run("success execute with retry on ISE and 500", func(t *testing.T) { 465 | hash := []byte{167, 123, 231, 255, 170, 172, 35, 142, 73, 31, 239, 199, 252, 120, 175, 217, 235, 220, 184, 200, 85, 203, 140, 230, 151, 221, 131, 255, 163, 151, 170, 210} 466 | mockSendCommandWithTxID.CommitTransaction.CommitDigest = hash 467 | 468 | startSession := &types.StartSessionRequest{LedgerName: &mockLedgerName} 469 | startSessionRequest := &qldbsession.SendCommandInput{StartSession: startSession} 470 | 471 | startTransaction := &types.StartTransactionRequest{} 472 | startTransactionRequest := &qldbsession.SendCommandInput{StartTransaction: startTransaction} 473 | startTransactionRequest.SessionToken = &mockDriverSessionToken 474 | 475 | commitTransaction := &types.CommitTransactionRequest{TransactionId: &mockTxnID, CommitDigest: hash} 476 | commitTransactionRequest := &qldbsession.SendCommandInput{CommitTransaction: commitTransaction} 477 | 478 | testISE := &types.InvalidSessionException{Code: &ErrCodeInvalidSessionException, Message: &ErrMessageInvalidSessionException} 479 | test500error := &InternalFailure{Code: &ErrCodeInternalFailure, Message: &ErrMessageInternalFailure} 480 | 481 | mockSession := new(mockQLDBSession) 482 | mockSession.On("SendCommand", mock.Anything, startSessionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil).Once() 483 | mockSession.On("SendCommand", mock.Anything, startTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil).Once() 484 | mockSession.On("SendCommand", mock.Anything, commitTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, testISE).Once() 485 | 486 | mockSession.On("SendCommand", mock.Anything, startSessionRequest, mock.Anything).Return(&mockSendCommandWithTxID, nil).Once() 487 | mockSession.On("SendCommand", mock.Anything, startTransactionRequest, mock.Anything).Return(&mockSendCommandWithTxID, test500error).Once() 488 | 489 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommandWithTxID, nil).Once() 490 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommandWithTxID, nil).Once() 491 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommandWithTxID, nil).Once() 492 | 493 | testDriver.qldbSession = mockSession 494 | 495 | testDriver.sessionPool = make(chan *session, 10) 496 | testDriver.semaphore = makeSemaphore(10) 497 | 498 | result, err := testDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 499 | tableNames := make([]string, 1) 500 | tableNames = append(tableNames, "table1") 501 | return tableNames, nil 502 | }) 503 | 504 | expectedTables := make([]string, 1) 505 | expectedTables = append(expectedTables, "table1") 506 | 507 | assert.Equal(t, expectedTables, result.([]string)) 508 | assert.NoError(t, err) 509 | }) 510 | } 511 | 512 | func TestGetTableNames(t *testing.T) { 513 | testDriver := QLDBDriver{ 514 | ledgerName: mockLedgerName, 515 | qldbSession: nil, 516 | maxConcurrentTransactions: 10, 517 | logger: mockLogger, 518 | isClosed: false, 519 | semaphore: makeSemaphore(10), 520 | sessionPool: make(chan *session, 10), 521 | retryPolicy: RetryPolicy{ 522 | MaxRetryLimit: 10, 523 | Backoff: ExponentialBackoffStrategy{ 524 | SleepBase: time.Duration(10) * time.Millisecond, 525 | SleepCap: time.Duration(5000) * time.Millisecond}}, 526 | } 527 | 528 | t.Run("GetTableNames from closed driver error", func(t *testing.T) { 529 | testDriver.isClosed = true 530 | 531 | _, err := testDriver.GetTableNames(context.Background()) 532 | assert.Error(t, err) 533 | 534 | testDriver.isClosed = false 535 | }) 536 | 537 | t.Run("error on Execute", func(t *testing.T) { 538 | mockSession := new(mockQLDBSession) 539 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockDriverSendCommand, errMock) 540 | testDriver.qldbSession = mockSession 541 | 542 | result, err := testDriver.GetTableNames(context.Background()) 543 | 544 | assert.Nil(t, result) 545 | assert.Equal(t, err, errMock) 546 | }) 547 | 548 | t.Run("success", func(t *testing.T) { 549 | type tableName struct { 550 | Name string `ion:"name"` 551 | } 552 | 553 | ionStruct := &tableName{"table1"} 554 | tableBinary, _ := ion.MarshalBinary(&ionStruct) 555 | 556 | mockValueHolder := types.ValueHolder{IonBinary: tableBinary} 557 | mockPageValues := make([]types.ValueHolder, 1) 558 | 559 | mockPageValues[0] = mockValueHolder 560 | mockExecuteForTable := types.ExecuteStatementResult{} 561 | mockExecuteForTable.FirstPage = &types.Page{Values: mockPageValues} 562 | 563 | mockSendCommandWithTxID.ExecuteStatement = &mockExecuteForTable 564 | mockSendCommandWithTxID.CommitTransaction.CommitDigest = []byte{46, 176, 81, 229, 236, 60, 17, 188, 81, 216, 217, 0, 89, 228, 233, 134, 252, 90, 165, 63, 143, 66, 127, 173, 131, 13, 134, 159, 14, 198, 19, 73} 565 | 566 | expectedTables := make([]string, 0) 567 | expectedTables = append(expectedTables, "table1") 568 | 569 | mockSession := new(mockQLDBSession) 570 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockSendCommandWithTxID, nil) 571 | testDriver.qldbSession = mockSession 572 | 573 | result, err := testDriver.GetTableNames(context.Background()) 574 | assert.NoError(t, err) 575 | assert.Equal(t, expectedTables, result) 576 | }) 577 | } 578 | 579 | func TestShutdownDriver(t *testing.T) { 580 | testDriver := QLDBDriver{ 581 | ledgerName: mockLedgerName, 582 | qldbSession: nil, 583 | maxConcurrentTransactions: 10, 584 | logger: mockLogger, 585 | isClosed: false, 586 | semaphore: nil, 587 | sessionPool: make(chan *session, 10), 588 | retryPolicy: RetryPolicy{ 589 | MaxRetryLimit: 10, 590 | Backoff: ExponentialBackoffStrategy{ 591 | SleepBase: time.Duration(10) * time.Millisecond, 592 | SleepCap: time.Duration(5000) * time.Millisecond}}, 593 | } 594 | 595 | t.Run("success", func(t *testing.T) { 596 | testDriver.Shutdown(context.Background()) 597 | assert.Equal(t, testDriver.isClosed, true) 598 | _, ok := <-testDriver.sessionPool 599 | assert.Equal(t, ok, false) 600 | }) 601 | 602 | } 603 | 604 | func TestGetSession(t *testing.T) { 605 | testDriver := QLDBDriver{ 606 | ledgerName: mockLedgerName, 607 | qldbSession: nil, 608 | maxConcurrentTransactions: 10, 609 | logger: mockLogger, 610 | isClosed: false, 611 | semaphore: makeSemaphore(10), 612 | sessionPool: make(chan *session, 10), 613 | retryPolicy: RetryPolicy{ 614 | MaxRetryLimit: 10, 615 | Backoff: ExponentialBackoffStrategy{ 616 | SleepBase: time.Duration(10) * time.Millisecond, 617 | SleepCap: time.Duration(5000) * time.Millisecond}}, 618 | } 619 | defer testDriver.Shutdown(context.Background()) 620 | 621 | t.Run("error", func(t *testing.T) { 622 | mockSession := new(mockQLDBSession) 623 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockDriverSendCommand, errMock) 624 | testDriver.qldbSession = mockSession 625 | 626 | session, err := testDriver.getSession(context.Background()) 627 | 628 | assert.Equal(t, err, errMock) 629 | assert.Nil(t, session) 630 | }) 631 | 632 | t.Run("success through createSession while empty pool", func(t *testing.T) { 633 | mockSession := new(mockQLDBSession) 634 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockDriverSendCommand, nil) 635 | testDriver.qldbSession = mockSession 636 | 637 | session, err := testDriver.getSession(context.Background()) 638 | 639 | assert.NoError(t, err) 640 | assert.Equal(t, &mockSessionToken, session.communicator.(*communicator).sessionToken) 641 | }) 642 | 643 | t.Run("success through existing session", func(t *testing.T) { 644 | mockSession := new(mockQLDBSession) 645 | 646 | testCommunicator := communicator{ 647 | service: mockSession, 648 | sessionToken: &mockDriverSessionToken, 649 | logger: mockLogger, 650 | } 651 | 652 | session1 := &session{&testCommunicator, mockLogger} 653 | session2 := &session{&testCommunicator, mockLogger} 654 | 655 | testDriver.sessionPool <- session1 656 | testDriver.sessionPool <- session2 657 | 658 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockDriverSendCommand, errMock) 659 | 660 | testDriver.qldbSession = mockSession 661 | 662 | session, err := testDriver.getSession(context.Background()) 663 | assert.NoError(t, err) 664 | assert.Equal(t, &mockSessionToken, session.communicator.(*communicator).sessionToken) 665 | }) 666 | } 667 | 668 | func TestSessionPoolCapacity(t *testing.T) { 669 | t.Run("error when exceed pool limit but succeed after release one session", func(t *testing.T) { 670 | testDriver := QLDBDriver{ 671 | ledgerName: mockLedgerName, 672 | qldbSession: nil, 673 | maxConcurrentTransactions: 2, 674 | logger: mockLogger, 675 | isClosed: false, 676 | semaphore: makeSemaphore(2), 677 | sessionPool: make(chan *session, 2), 678 | retryPolicy: RetryPolicy{ 679 | MaxRetryLimit: 10, 680 | Backoff: ExponentialBackoffStrategy{ 681 | SleepBase: time.Duration(10) * time.Millisecond, 682 | SleepCap: time.Duration(5000) * time.Millisecond}}, 683 | } 684 | defer testDriver.Shutdown(context.Background()) 685 | 686 | mockSession := new(mockQLDBSession) 687 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockDriverSendCommand, nil) 688 | testDriver.qldbSession = mockSession 689 | 690 | session1, err := testDriver.getSession(context.Background()) 691 | assert.NoError(t, err) 692 | assert.NotNil(t, session1) 693 | 694 | session2, err := testDriver.getSession(context.Background()) 695 | assert.NoError(t, err) 696 | assert.NotNil(t, session2) 697 | 698 | session3, err := testDriver.getSession(context.Background()) 699 | assert.Error(t, err) 700 | assert.Nil(t, session3) 701 | qldbErr := err.(*qldbDriverError) 702 | assert.Error(t, qldbErr) 703 | 704 | testDriver.releaseSession(session1) 705 | 706 | session4, err := testDriver.getSession(context.Background()) 707 | assert.NoError(t, err) 708 | assert.NotNil(t, session4) 709 | }) 710 | } 711 | 712 | func TestCreateSession(t *testing.T) { 713 | 714 | testDriver := QLDBDriver{ 715 | ledgerName: mockLedgerName, 716 | qldbSession: nil, 717 | maxConcurrentTransactions: 10, 718 | logger: mockLogger, 719 | isClosed: false, 720 | semaphore: makeSemaphore(10), 721 | sessionPool: make(chan *session, 10), 722 | retryPolicy: RetryPolicy{ 723 | MaxRetryLimit: 10, 724 | Backoff: ExponentialBackoffStrategy{ 725 | SleepBase: time.Duration(10) * time.Millisecond, 726 | SleepCap: time.Duration(5000) * time.Millisecond}}, 727 | } 728 | 729 | t.Run("error", func(t *testing.T) { 730 | mockSession := new(mockQLDBSession) 731 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockDriverSendCommand, errMock) 732 | testDriver.qldbSession = mockSession 733 | 734 | testDriver.semaphore.tryAcquire() 735 | session, err := testDriver.createSession(context.Background()) 736 | 737 | assert.Nil(t, session) 738 | assert.Equal(t, errMock, err) 739 | }) 740 | 741 | t.Run("success", func(t *testing.T) { 742 | mockSession := new(mockQLDBSession) 743 | mockSession.On("SendCommand", mock.Anything, mock.Anything, mock.Anything).Return(&mockDriverSendCommand, nil) 744 | testDriver.qldbSession = mockSession 745 | 746 | session, err := testDriver.createSession(context.Background()) 747 | 748 | assert.NoError(t, err) 749 | assert.Equal(t, &mockSessionToken, session.communicator.(*communicator).sessionToken) 750 | }) 751 | } 752 | 753 | var mockLedgerName = "someLedgerName" 754 | var defaultMaxConcurrentTransactions = 50 755 | var defaultRetry = 4 756 | var mockTxnID = "12341" 757 | var mockStartTransactionWithID = types.StartTransactionResult{TransactionId: &mockTxnID} 758 | 759 | var mockSendCommandWithTxID = qldbsession.SendCommandOutput{ 760 | AbortTransaction: &mockAbortTransaction, 761 | CommitTransaction: &mockCommitTransaction, 762 | EndSession: &mockEndSession, 763 | ExecuteStatement: &mockExecuteStatement, 764 | FetchPage: &mockFetchPage, 765 | StartSession: &mockStartSession, 766 | StartTransaction: &mockStartTransactionWithID, 767 | } 768 | 769 | var mockDriverSessionToken = "token" 770 | var mockDriverStartSession = types.StartSessionResult{SessionToken: &mockDriverSessionToken} 771 | var mockDriverAbortTransaction = types.AbortTransactionResult{} 772 | var mockDriverCommitTransaction = types.CommitTransactionResult{} 773 | var mockDriverExecuteStatement = types.ExecuteStatementResult{} 774 | var mockDriverEndSession = types.EndSessionResult{} 775 | var mockDriverFetchPage = types.FetchPageResult{} 776 | var mockDriverStartTransaction = types.StartTransactionResult{} 777 | var mockDriverSendCommand = qldbsession.SendCommandOutput{ 778 | AbortTransaction: &mockDriverAbortTransaction, 779 | CommitTransaction: &mockDriverCommitTransaction, 780 | EndSession: &mockDriverEndSession, 781 | ExecuteStatement: &mockDriverExecuteStatement, 782 | FetchPage: &mockDriverFetchPage, 783 | StartSession: &mockDriverStartSession, 784 | StartTransaction: &mockDriverStartTransaction, 785 | } 786 | -------------------------------------------------------------------------------- /qldbdriver/statement_execution_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | A copy of the License is located at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | or in the "license" file accompanying this file. This file is distributed 11 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | express or implied. See the License for the specific language governing 13 | permissions and limitations under the License. 14 | */ 15 | 16 | package qldbdriver 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "fmt" 22 | "math" 23 | "reflect" 24 | "testing" 25 | "time" 26 | 27 | "github.com/amzn/ion-go/ion" 28 | "github.com/aws/aws-sdk-go-v2/service/qldbsession/types" 29 | "github.com/aws/smithy-go" 30 | "github.com/stretchr/testify/assert" 31 | "github.com/stretchr/testify/require" 32 | ) 33 | 34 | func contains(slice []string, val string) bool { 35 | for _, item := range slice { 36 | if item == val { 37 | return true 38 | } 39 | } 40 | return false 41 | } 42 | 43 | func cleanup(driver *QLDBDriver, testTableName string) { 44 | _, _ = driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 45 | return txn.Execute(fmt.Sprintf("DELETE FROM %s", testTableName)) 46 | }) 47 | } 48 | 49 | func TestStatementExecutionIntegration(t *testing.T) { 50 | if testing.Short() { 51 | t.Skip("skipping integration test") 52 | } 53 | 54 | // setup 55 | testBase := createTestBase("Golang-StatementExec") 56 | testBase.deleteLedger(t) 57 | testBase.waitForDeletion() 58 | testBase.createLedger(t) 59 | defer testBase.deleteLedger(t) 60 | 61 | qldbDriver, err := testBase.getDefaultDriver() 62 | require.NoError(t, err) 63 | defer qldbDriver.Shutdown(context.Background()) 64 | 65 | _, err = qldbDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 66 | return txn.Execute(fmt.Sprintf("CREATE TABLE %s", testTableName)) 67 | }) 68 | require.NoError(t, err) 69 | 70 | executeWithParam := func(ctx context.Context, query string, txn Transaction, parameters ...interface{}) (interface{}, error) { 71 | result, err := txn.Execute(query, parameters...) 72 | if err != nil { 73 | return nil, err 74 | } 75 | count := 0 76 | for result.Next(txn) { 77 | count++ 78 | } 79 | if result.Err() != nil { 80 | return nil, result.Err() 81 | } 82 | return count, nil 83 | } 84 | 85 | t.Run("Drop existing table", func(t *testing.T) { 86 | driver, err := testBase.getDefaultDriver() 87 | require.NoError(t, err) 88 | defer driver.Shutdown(context.Background()) 89 | 90 | createTableName := "GoIntegrationTestCreateTable" 91 | createTableQuery := fmt.Sprintf("CREATE TABLE %s", createTableName) 92 | 93 | executeResult, err := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 94 | return executeWithParam(context.Background(), createTableQuery, txn) 95 | }) 96 | assert.NoError(t, err) 97 | actualResult := executeResult.(int) 98 | assert.Equal(t, 1, actualResult) 99 | 100 | tables, err := driver.GetTableNames(context.Background()) 101 | assert.NoError(t, err) 102 | assert.True(t, contains(tables, createTableName)) 103 | 104 | dropTableQuery := fmt.Sprintf("DROP TABLE %s", createTableName) 105 | dropResult, droperr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 106 | return executeWithParam(context.Background(), dropTableQuery, txn) 107 | }) 108 | assert.NoError(t, droperr) 109 | assert.Equal(t, 1, dropResult.(int)) 110 | }) 111 | 112 | t.Run("List tables", func(t *testing.T) { 113 | driver, err := testBase.getDefaultDriver() 114 | require.NoError(t, err) 115 | defer driver.Shutdown(context.Background()) 116 | defer cleanup(driver, testTableName) 117 | 118 | tables, err := driver.GetTableNames(context.Background()) 119 | assert.NoError(t, err) 120 | assert.True(t, contains(tables, testTableName)) 121 | }) 122 | 123 | t.Run("Create table that exists", func(t *testing.T) { 124 | driver, err := testBase.getDefaultDriver() 125 | require.NoError(t, err) 126 | defer driver.Shutdown(context.Background()) 127 | defer cleanup(driver, testTableName) 128 | 129 | query := fmt.Sprintf("CREATE TABLE %s", testTableName) 130 | 131 | result, err := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 132 | return txn.Execute(query) 133 | }) 134 | assert.Error(t, err) 135 | assert.Nil(t, result) 136 | // The exception was wrapped as un-modeled service error responses by SDK V2, not types.BadRequestException 137 | // Example response as APIError: code: 412, message: Table with name: USER.GoIntegrationTestTable already exists, fault: unknown 138 | var ae smithy.APIError 139 | assert.True(t, errors.As(err, &ae)) 140 | }) 141 | 142 | t.Run("Create index", func(t *testing.T) { 143 | driver, err := testBase.getDefaultDriver() 144 | require.NoError(t, err) 145 | defer driver.Shutdown(context.Background()) 146 | defer cleanup(driver, testTableName) 147 | 148 | indexQuery := fmt.Sprintf("CREATE INDEX ON %s (%s)", testTableName, indexAttribute) 149 | indexResult, indexErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 150 | return executeWithParam(context.Background(), indexQuery, txn) 151 | }) 152 | assert.NoError(t, indexErr) 153 | assert.Equal(t, 1, indexResult.(int)) 154 | 155 | // Wait for above index to be created before querying index 156 | time.Sleep(5 * time.Second) 157 | 158 | searchQuery := "SELECT VALUE indexes[0] FROM information_schema.user_tables WHERE status = 'ACTIVE' " 159 | searchQuery += fmt.Sprintf("AND name = '%s'", testTableName) 160 | 161 | type exprName struct { 162 | Expr string `ion:"expr"` 163 | } 164 | 165 | searchRes, searchErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 166 | result, err := txn.Execute(searchQuery) 167 | if err != nil { 168 | return nil, err 169 | } 170 | if !result.Next(txn) { 171 | return nil, result.Err() 172 | } 173 | exprStruct := new(exprName) 174 | err = ion.Unmarshal(result.GetCurrentData(), &exprStruct) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return exprStruct.Expr, nil 179 | }) 180 | assert.NoError(t, searchErr) 181 | assert.Equal(t, fmt.Sprint("[", indexAttribute, "]"), searchRes.(string)) 182 | }) 183 | 184 | t.Run("Return empty when no records found", func(t *testing.T) { 185 | driver, err := testBase.getDefaultDriver() 186 | require.NoError(t, err) 187 | defer driver.Shutdown(context.Background()) 188 | defer cleanup(driver, testTableName) 189 | 190 | // Note : We are using a select * without specifying a where condition for the purpose of this test. 191 | // However, we do not recommend using such a query in a normal/production context. 192 | query := fmt.Sprintf("SELECT * from %s", testTableName) 193 | selectRes, selectErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 194 | return executeWithParam(context.Background(), query, txn) 195 | }) 196 | assert.NoError(t, selectErr) 197 | assert.Equal(t, 0, selectRes.(int)) 198 | }) 199 | 200 | t.Run("Insert document", func(t *testing.T) { 201 | driver, err := testBase.getDefaultDriver() 202 | require.NoError(t, err) 203 | defer driver.Shutdown(context.Background()) 204 | defer cleanup(driver, testTableName) 205 | 206 | type TestTable struct { 207 | Name string `ion:"Name"` 208 | } 209 | 210 | record := TestTable{singleDocumentValue} 211 | query := fmt.Sprintf("INSERT INTO %s ?", testTableName) 212 | 213 | insertResult, insertErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 214 | return executeWithParam(context.Background(), query, txn, record) 215 | }) 216 | 217 | assert.NoError(t, insertErr) 218 | assert.Equal(t, 1, insertResult.(int)) 219 | 220 | searchQuery := fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName) 221 | searchResult, searchErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 222 | result, err := txn.Execute(searchQuery, singleDocumentValue) 223 | if err != nil { 224 | return nil, err 225 | } 226 | if result.Next(txn) { 227 | decodedResult := "" 228 | decodedErr := ion.Unmarshal(result.GetCurrentData(), &decodedResult) 229 | if decodedErr != nil { 230 | return nil, decodedErr 231 | } 232 | return decodedResult, nil 233 | } 234 | return nil, result.Err() 235 | }) 236 | assert.NoError(t, searchErr) 237 | assert.Equal(t, singleDocumentValue, searchResult.(string)) 238 | }) 239 | 240 | t.Run("Query table enclosed in quotes", func(t *testing.T) { 241 | driver, err := testBase.getDefaultDriver() 242 | require.NoError(t, err) 243 | defer driver.Shutdown(context.Background()) 244 | defer cleanup(driver, testTableName) 245 | 246 | type TestTable struct { 247 | Name string `ion:"Name"` 248 | } 249 | 250 | record := TestTable{singleDocumentValue} 251 | query := fmt.Sprintf("INSERT INTO %s ?", testTableName) 252 | 253 | insertResult, insertErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 254 | return executeWithParam(context.Background(), query, txn, record) 255 | }) 256 | 257 | assert.NoError(t, insertErr) 258 | assert.Equal(t, 1, insertResult.(int)) 259 | 260 | searchQuery := fmt.Sprintf("SELECT VALUE %s FROM \"%s\" WHERE %s = ?", columnName, testTableName, columnName) 261 | searchResult, searchErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 262 | result, err := txn.Execute(searchQuery, singleDocumentValue) 263 | if err != nil { 264 | return nil, err 265 | } 266 | if result.Next(txn) { 267 | decodedResult := "" 268 | decodedErr := ion.Unmarshal(result.GetCurrentData(), &decodedResult) 269 | if decodedErr != nil { 270 | return nil, decodedErr 271 | } 272 | return decodedResult, nil 273 | } 274 | return nil, result.Err() 275 | }) 276 | assert.NoError(t, searchErr) 277 | assert.Equal(t, singleDocumentValue, searchResult.(string)) 278 | }) 279 | 280 | t.Run("Insert multiple documents", func(t *testing.T) { 281 | driver, err := testBase.getDefaultDriver() 282 | require.NoError(t, err) 283 | defer driver.Shutdown(context.Background()) 284 | defer cleanup(driver, testTableName) 285 | 286 | type TestTable struct { 287 | Name string `ion:"Name"` 288 | } 289 | 290 | record1 := TestTable{multipleDocumentValue1} 291 | record2 := TestTable{multipleDocumentValue2} 292 | 293 | query := fmt.Sprintf("INSERT INTO %s <>", testTableName) 294 | insertResult, insertErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 295 | return executeWithParam(context.Background(), query, txn, record1, record2) 296 | }) 297 | 298 | assert.NoError(t, insertErr) 299 | assert.Equal(t, 2, insertResult.(int)) 300 | 301 | searchQuery := fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s IN (?,?)", columnName, testTableName, columnName) 302 | searchResult, searchErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 303 | result, err := txn.Execute(searchQuery, multipleDocumentValue1, multipleDocumentValue2) 304 | if err != nil { 305 | return nil, err 306 | } 307 | results := make([]string, 0) 308 | for result.Next(txn) { 309 | decodedResult := "temp" 310 | decodedErr := ion.Unmarshal(result.GetCurrentData(), &decodedResult) 311 | if decodedErr != nil { 312 | return nil, decodedErr 313 | } 314 | results = append(results, decodedResult) 315 | } 316 | if result.Err() != nil { 317 | return nil, result.Err() 318 | } 319 | return results, nil 320 | }) 321 | 322 | obtainedResults := searchResult.([]string) 323 | assert.NoError(t, searchErr) 324 | assert.True(t, contains(obtainedResults, multipleDocumentValue1)) 325 | assert.True(t, contains(obtainedResults, multipleDocumentValue2)) 326 | }) 327 | 328 | t.Run("Delete single document", func(t *testing.T) { 329 | driver, err := testBase.getDefaultDriver() 330 | require.NoError(t, err) 331 | defer driver.Shutdown(context.Background()) 332 | defer cleanup(driver, testTableName) 333 | 334 | type TestTable struct { 335 | Name string `ion:"Name"` 336 | } 337 | 338 | query := fmt.Sprintf("INSERT INTO %s ?", testTableName) 339 | record := TestTable{singleDocumentValue} 340 | insertResult, insertErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 341 | return executeWithParam(context.Background(), query, txn, record) 342 | }) 343 | assert.NoError(t, insertErr) 344 | assert.Equal(t, 1, insertResult.(int)) 345 | 346 | deleteQuery := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", testTableName, columnName) 347 | deleteResult, deleteErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 348 | return executeWithParam(context.Background(), deleteQuery, txn, singleDocumentValue) 349 | }) 350 | assert.NoError(t, deleteErr) 351 | assert.Equal(t, 1, deleteResult.(int)) 352 | 353 | countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", testTableName) 354 | type rowCount struct { 355 | Count int `ion:"_1"` 356 | } 357 | countResult, countErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 358 | result, err := txn.Execute(countQuery) 359 | if err != nil { 360 | return nil, err 361 | } 362 | if !result.Next(txn) { 363 | return nil, result.Err() 364 | } 365 | countStruct := new(rowCount) 366 | err = ion.Unmarshal(result.GetCurrentData(), &countStruct) 367 | if err != nil { 368 | return nil, err 369 | } 370 | return countStruct.Count, nil 371 | }) 372 | assert.NoError(t, countErr) 373 | assert.Equal(t, 0, countResult.(int)) 374 | }) 375 | 376 | t.Run("Delete all documents", func(t *testing.T) { 377 | driver, err := testBase.getDefaultDriver() 378 | require.NoError(t, err) 379 | defer driver.Shutdown(context.Background()) 380 | defer cleanup(driver, testTableName) 381 | 382 | type TestTable struct { 383 | Name string `ion:"Name"` 384 | } 385 | 386 | record1 := TestTable{multipleDocumentValue1} 387 | record2 := TestTable{multipleDocumentValue2} 388 | 389 | query := fmt.Sprintf("INSERT INTO %s <>", testTableName) 390 | insertResult, insertErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 391 | return executeWithParam(context.Background(), query, txn, record1, record2) 392 | }) 393 | 394 | assert.NoError(t, insertErr) 395 | assert.Equal(t, 2, insertResult.(int)) 396 | 397 | deleteQuery := fmt.Sprintf("DELETE FROM %s", testTableName) 398 | deleteResult, deleteErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 399 | return executeWithParam(context.Background(), deleteQuery, txn) 400 | }) 401 | assert.NoError(t, deleteErr) 402 | assert.Equal(t, 2, deleteResult.(int)) 403 | 404 | countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", testTableName) 405 | type rowCount struct { 406 | Count int `ion:"_1"` 407 | } 408 | countResult, countErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 409 | result, err := txn.Execute(countQuery) 410 | if err != nil { 411 | return nil, err 412 | } 413 | if !result.Next(txn) { 414 | return nil, result.Err() 415 | } 416 | countStruct := new(rowCount) 417 | err = ion.Unmarshal(result.GetCurrentData(), &countStruct) 418 | if err != nil { 419 | return nil, err 420 | } 421 | return countStruct.Count, nil 422 | }) 423 | assert.NoError(t, countErr) 424 | assert.Equal(t, 0, countResult.(int)) 425 | }) 426 | 427 | t.Run("Test OCC Exception", func(t *testing.T) { 428 | type TestTable struct { 429 | Name string `ion:"Name"` 430 | } 431 | driverWithoutRetry, err := testBase.getDriver(&testDriverOptions{ 432 | ledgerName: *testBase.ledgerName, 433 | maxConcTx: 10, 434 | retryLimit: 0, 435 | }) 436 | require.NoError(t, err) 437 | defer driverWithoutRetry.Shutdown(context.Background()) 438 | defer cleanup(driverWithoutRetry, testTableName) 439 | 440 | record := TestTable{"dummy"} 441 | 442 | insertQuery := fmt.Sprintf("INSERT INTO %s ?", testTableName) 443 | insertResult, insertErr := driverWithoutRetry.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 444 | return executeWithParam(context.Background(), insertQuery, txn, record) 445 | }) 446 | assert.NoError(t, insertErr) 447 | assert.Equal(t, insertResult.(int), 1) 448 | 449 | executeResult, err := driverWithoutRetry.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 450 | _, err = txn.Execute(fmt.Sprintf("SELECT VALUE %s FROM %s", columnName, testTableName)) 451 | assert.NoError(t, err) 452 | 453 | return driverWithoutRetry.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 454 | return txn.Execute(fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName), 5) 455 | }) 456 | 457 | }) 458 | assert.Nil(t, executeResult) 459 | 460 | var occ *types.OccConflictException 461 | assert.True(t, errors.As(err, &occ)) 462 | }) 463 | 464 | t.Run("Execution metrics", func(t *testing.T) { 465 | driver, err := testBase.getDefaultDriver() 466 | require.NoError(t, err) 467 | defer driver.Shutdown(context.Background()) 468 | 469 | type TestTable struct { 470 | Name string `ion:"Name"` 471 | } 472 | record := TestTable{singleDocumentValue} 473 | 474 | selectQuery := fmt.Sprintf("SELECT * FROM %s as a, %s as b, %s as c, %s as d, %s as e, %s as f", 475 | testTableName, testTableName, testTableName, testTableName, testTableName, testTableName) 476 | 477 | insertDocuments := func(driver *QLDBDriver) { 478 | _, err = driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 479 | return txn.Execute(fmt.Sprintf("INSERT INTO %s <>", testTableName), record, record, record) 480 | 481 | }) 482 | require.NoError(t, err) 483 | } 484 | 485 | t.Run("Execution metrics for stream result", func(t *testing.T) { 486 | defer cleanup(driver, testTableName) 487 | 488 | // Insert docs 489 | insertDocuments(driver) 490 | 491 | _, err = driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 492 | result, err := txn.Execute(selectQuery) 493 | require.NoError(t, err) 494 | 495 | totalReadIOs := int64(0) 496 | totalProcessingTimeMilliseconds := int64(0) 497 | for result.Next(txn) { 498 | // IOUsage test 499 | ioUsage := result.GetConsumedIOs() 500 | require.NotNil(t, ioUsage) 501 | assert.True(t, *ioUsage.GetReadIOs() > 0) 502 | totalReadIOs = *ioUsage.GetReadIOs() 503 | 504 | // TimingInformation test 505 | timingInfo := result.GetTimingInformation() 506 | require.NotNil(t, timingInfo) 507 | assert.True(t, *timingInfo.GetProcessingTimeMilliseconds() > 0) 508 | totalProcessingTimeMilliseconds = *timingInfo.GetProcessingTimeMilliseconds() 509 | } 510 | 511 | // This value comes from the statement that performs self joins on a table. 512 | assert.Equal(t, int64(1092), totalReadIOs) 513 | assert.True(t, totalProcessingTimeMilliseconds > 0) 514 | return nil, nil 515 | }) 516 | assert.NoError(t, err) 517 | }) 518 | 519 | t.Run("Execution metrics for buffered result", func(t *testing.T) { 520 | defer cleanup(driver, testTableName) 521 | 522 | // Insert docs 523 | insertDocuments(driver) 524 | 525 | result, err := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 526 | result, err := txn.Execute(selectQuery) 527 | if err != nil { 528 | return nil, err 529 | } 530 | 531 | return txn.BufferResult(result) 532 | }) 533 | require.NoError(t, err) 534 | 535 | bufferedResult := result.(BufferedResult) 536 | 537 | // IOUsage test 538 | ioUsage := bufferedResult.GetConsumedIOs() 539 | require.NotNil(t, ioUsage) 540 | // This value comes from the statement that performs self joins on a table. 541 | assert.Equal(t, int64(1092), *ioUsage.GetReadIOs()) 542 | 543 | // TimingInformation test 544 | timingInfo := bufferedResult.GetTimingInformation() 545 | require.NotNil(t, timingInfo) 546 | assert.True(t, *timingInfo.GetProcessingTimeMilliseconds() > 0) 547 | }) 548 | }) 549 | 550 | t.Run("Insert and read Ion types", func(t *testing.T) { 551 | t.Run("struct", func(t *testing.T) { 552 | driver, err := testBase.getDefaultDriver() 553 | require.NoError(t, err) 554 | defer driver.Shutdown(context.Background()) 555 | defer cleanup(driver, testTableName) 556 | 557 | type Anon struct { 558 | A, B int 559 | } 560 | parameterValue := Anon{42, 2} 561 | 562 | type TestTable struct { 563 | Name Anon `ion:"Name"` 564 | } 565 | parameter := TestTable{parameterValue} 566 | 567 | query := fmt.Sprintf("INSERT INTO %s ?", testTableName) 568 | executeResult, executeErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 569 | return executeWithParam(context.Background(), query, txn, parameter) 570 | }) 571 | assert.NoError(t, executeErr) 572 | assert.Equal(t, 1, executeResult.(int)) 573 | 574 | searchQuery := fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName) 575 | searchResult, searchErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 576 | result, err := txn.Execute(searchQuery, parameterValue) 577 | if err != nil { 578 | return nil, err 579 | } 580 | if !result.Next(txn) { 581 | return nil, result.Err() 582 | } 583 | ionReceiver := new(Anon) 584 | err = ion.Unmarshal(result.GetCurrentData(), &ionReceiver) 585 | if err != nil { 586 | return nil, err 587 | } 588 | return ionReceiver, nil 589 | }) 590 | assert.NoError(t, searchErr) 591 | assert.Equal(t, ¶meterValue, searchResult.(*Anon)) 592 | }) 593 | 594 | testInsertCommon := func(testName, inputQuery, searchQuery string, parameterValue, ionReceiver, parameter interface{}) { 595 | t.Run(testName, func(t *testing.T) { 596 | driver, err := testBase.getDefaultDriver() 597 | require.NoError(t, err) 598 | defer driver.Shutdown(context.Background()) 599 | defer cleanup(driver, testTableName) 600 | 601 | executeResult, executeErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 602 | return executeWithParam(context.Background(), inputQuery, txn, parameter) 603 | }) 604 | assert.NoError(t, executeErr) 605 | assert.Equal(t, 1, executeResult.(int)) 606 | searchResult, searchErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 607 | result, err := txn.Execute(searchQuery, parameterValue) 608 | if err != nil { 609 | return nil, err 610 | } 611 | if !result.Next(txn) { 612 | return nil, result.Err() 613 | } 614 | err = ion.Unmarshal(result.GetCurrentData(), ionReceiver) 615 | if err != nil { 616 | return nil, err 617 | } 618 | return ionReceiver, nil 619 | }) 620 | assert.NoError(t, searchErr) 621 | switch actualVal := searchResult.(type) { 622 | case *bool: 623 | if !reflect.DeepEqual(parameterValue, *actualVal) { 624 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 625 | } 626 | case *int: 627 | if !reflect.DeepEqual(parameterValue, *actualVal) { 628 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 629 | } 630 | case *float32: 631 | if !reflect.DeepEqual(parameterValue, *actualVal) { 632 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 633 | } 634 | case *float64: 635 | if !reflect.DeepEqual(parameterValue, *actualVal) { 636 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 637 | } 638 | case *[]int: 639 | if !reflect.DeepEqual(parameterValue, *actualVal) { 640 | fmt.Println(*actualVal) 641 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 642 | } 643 | case *[]string: 644 | if !reflect.DeepEqual(parameterValue, *actualVal) { 645 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 646 | } 647 | case *ion.Timestamp: 648 | _, ok := parameterValue.(time.Time) 649 | if ok && !reflect.DeepEqual(parameterValue, (*actualVal).GetDateTime()) { 650 | t.Errorf("expected %v, got %v", parameterValue, (*actualVal).GetDateTime()) 651 | } else if !ok && !reflect.DeepEqual(parameterValue, *actualVal) { 652 | t.Errorf("expected %v, got %v", parameterValue, *actualVal) 653 | } 654 | default: 655 | t.Errorf("Could not find type") 656 | } 657 | 658 | }) 659 | } 660 | 661 | // Time.time 662 | type TestTableTime struct { 663 | Name time.Time `ion:"Name"` 664 | } 665 | timeParam := time.Now().UTC() 666 | testInsertCommon("Time.time", 667 | fmt.Sprintf("INSERT INTO %s ?", testTableName), 668 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 669 | timeParam, 670 | new(ion.Timestamp), 671 | TestTableTime{timeParam}, 672 | ) 673 | 674 | // ion.Timestamp 675 | type TestTableTimeStamp struct { 676 | Name ion.Timestamp `ion:"Name"` 677 | } 678 | timestampParam := ion.NewTimestampWithFractionalSeconds(time.Now().UTC(), ion.TimestampPrecisionNanosecond, ion.TimezoneUTC, 9) 679 | testInsertCommon("ion.Timestamp", 680 | fmt.Sprintf("INSERT INTO %s ?", testTableName), 681 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 682 | timestampParam, 683 | new(ion.Timestamp), 684 | TestTableTimeStamp{timestampParam}, 685 | ) 686 | 687 | // boolean 688 | type TestTableBoolean struct { 689 | Name bool `ion:"Name"` 690 | } 691 | boolParam := true 692 | testInsertCommon("boolean", 693 | fmt.Sprintf("INSERT INTO %s ?", testTableName), 694 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 695 | boolParam, 696 | new(bool), 697 | TestTableBoolean{boolParam}, 698 | ) 699 | 700 | // integer 701 | type TestTableInt struct { 702 | Name int `ion:"Name"` 703 | } 704 | intParam := 5 705 | testInsertCommon("integer", 706 | fmt.Sprintf("INSERT INTO %s ?", testTableName), 707 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 708 | intParam, 709 | new(int), 710 | TestTableInt{intParam}, 711 | ) 712 | 713 | // float32 714 | type TestTableFloat32 struct { 715 | Name float32 `ion:"Name"` 716 | } 717 | var float32Param float32 = math.MaxFloat32 718 | testInsertCommon("float32", 719 | fmt.Sprintf("INSERT INTO %s ?", testTableName), 720 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 721 | float32Param, 722 | new(float32), 723 | TestTableFloat32{float32(float32Param)}, 724 | ) 725 | 726 | // float64 727 | type TestTableFloat64 struct { 728 | Name float64 `ion:"Name"` 729 | } 730 | var float64Param float64 = math.MaxFloat64 731 | testInsertCommon("float64", 732 | fmt.Sprintf("INSERT INTO %s ?", testTableName), 733 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 734 | float64Param, 735 | new(float64), 736 | TestTableFloat64{float64Param}, 737 | ) 738 | 739 | // int slice 740 | type TestTableSlice struct { 741 | Name []int `ion:"Name"` 742 | } 743 | parameterValue := []int{2, 3, 4} 744 | testInsertCommon("slice int", 745 | fmt.Sprintf("INSERT INTO %s ?", testTableName), 746 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 747 | parameterValue, 748 | &[]int{}, 749 | TestTableSlice{parameterValue}, 750 | ) 751 | 752 | // string slice 753 | type TestTableSliceString struct { 754 | Name []string `ion:"Name"` 755 | } 756 | stringParam := []string{"Hello", "How", "Are"} 757 | testInsertCommon("slice string", 758 | fmt.Sprintf("INSERT INTO %s ?", testTableName), 759 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 760 | stringParam, 761 | &[]string{}, 762 | TestTableSliceString{stringParam}, 763 | ) 764 | 765 | }) 766 | 767 | t.Run("Update Ion types", func(t *testing.T) { 768 | updateDriver, err := testBase.getDefaultDriver() 769 | require.NoError(t, err) 770 | 771 | type TestTable struct { 772 | Name int `ion:"Name"` 773 | } 774 | parameter := TestTable{1} 775 | 776 | insertQuery := fmt.Sprintf("INSERT INTO %s ?", testTableName) 777 | _, err = updateDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 778 | return executeWithParam(context.Background(), insertQuery, txn, parameter) 779 | }) 780 | require.NoError(t, err) 781 | 782 | testUpdateCommon := func(testName, inputQuery, searchQuery string, parameterValue, ionReceiver, parameter interface{}) { 783 | t.Run(testName, func(t *testing.T) { 784 | driver, err := testBase.getDefaultDriver() 785 | require.NoError(t, err) 786 | defer driver.Shutdown(context.Background()) 787 | 788 | executeResult, executeErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 789 | return executeWithParam(context.Background(), inputQuery, txn, parameter) 790 | }) 791 | assert.NoError(t, executeErr) 792 | assert.Equal(t, 1, executeResult.(int)) 793 | searchResult, searchErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 794 | result, err := txn.Execute(searchQuery, parameterValue) 795 | if err != nil { 796 | return nil, err 797 | } 798 | if !result.Next(txn) { 799 | return nil, result.Err() 800 | } 801 | err = ion.Unmarshal(result.GetCurrentData(), ionReceiver) 802 | if err != nil { 803 | return nil, err 804 | } 805 | return ionReceiver, nil 806 | }) 807 | assert.NoError(t, searchErr) 808 | switch actualVal := searchResult.(type) { 809 | case *bool: 810 | if !reflect.DeepEqual(parameterValue, *actualVal) { 811 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 812 | } 813 | case *int: 814 | if !reflect.DeepEqual(parameterValue, *actualVal) { 815 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 816 | } 817 | case *float32: 818 | if !reflect.DeepEqual(parameterValue, *actualVal) { 819 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 820 | } 821 | case *float64: 822 | if !reflect.DeepEqual(parameterValue, *actualVal) { 823 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 824 | } 825 | case *[]int: 826 | if !reflect.DeepEqual(parameterValue, *actualVal) { 827 | fmt.Println(*actualVal) 828 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 829 | } 830 | case *[]string: 831 | if !reflect.DeepEqual(parameterValue, *actualVal) { 832 | t.Errorf("expected %v, got %v", parameterValue, reflect.ValueOf(*actualVal)) 833 | } 834 | case *ion.Timestamp: 835 | _, ok := parameterValue.(time.Time) 836 | if ok && !reflect.DeepEqual(parameterValue, (*actualVal).GetDateTime()) { 837 | t.Errorf("expected %v, got %v", parameterValue, (*actualVal).GetDateTime()) 838 | } else if !ok && !reflect.DeepEqual(parameterValue, *actualVal) { 839 | t.Errorf("expected %v, got %v", parameterValue, *actualVal) 840 | } 841 | default: 842 | t.Errorf("Could not find type") 843 | } 844 | }) 845 | } 846 | 847 | // Time.time 848 | timeParam := time.Now().UTC() 849 | testUpdateCommon("Time.time", 850 | fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName), 851 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 852 | timeParam, 853 | new(ion.Timestamp), 854 | timeParam, 855 | ) 856 | 857 | // ion.Timestamp 858 | timestampParam := ion.NewTimestampWithFractionalSeconds(time.Now().UTC(), ion.TimestampPrecisionNanosecond, ion.TimezoneUTC, 9) 859 | testUpdateCommon("ion.Timestamp", 860 | fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName), 861 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 862 | timestampParam, 863 | new(ion.Timestamp), 864 | timestampParam, 865 | ) 866 | 867 | // boolean 868 | boolParam := true 869 | testUpdateCommon("boolean", 870 | fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName), 871 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 872 | boolParam, 873 | new(bool), 874 | boolParam, 875 | ) 876 | 877 | // integer 878 | intParam := 5 879 | testUpdateCommon("integer", 880 | fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName), 881 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 882 | intParam, 883 | new(int), 884 | intParam, 885 | ) 886 | 887 | // float32 888 | var float32Param float32 = math.MaxFloat32 889 | testUpdateCommon("float32", 890 | fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName), 891 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 892 | float32Param, 893 | new(float32), 894 | float32Param, 895 | ) 896 | 897 | // float64 898 | var float64Param float64 = math.MaxFloat64 899 | testUpdateCommon("float64", 900 | fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName), 901 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 902 | float64Param, 903 | new(float64), 904 | float64Param, 905 | ) 906 | 907 | // int slice 908 | parameterValue := []int{2, 3, 4} 909 | testUpdateCommon("slice int", 910 | fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName), 911 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 912 | parameterValue, 913 | &[]int{}, 914 | parameterValue, 915 | ) 916 | 917 | // string slice 918 | stringParam := []string{"Hello", "How", "Are"} 919 | testUpdateCommon("slice string", 920 | fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName), 921 | fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName), 922 | stringParam, 923 | &[]string{}, 924 | stringParam, 925 | ) 926 | 927 | t.Run("nil", func(t *testing.T) { 928 | driver, err := testBase.getDefaultDriver() 929 | require.NoError(t, err) 930 | defer driver.Shutdown(context.Background()) 931 | 932 | query := fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName) 933 | executeResult, executeErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 934 | return executeWithParam(context.Background(), query, txn, nil) 935 | }) 936 | assert.NoError(t, executeErr) 937 | assert.Equal(t, 1, executeResult.(int)) 938 | 939 | searchQuery := fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s IS NULL", columnName, testTableName, columnName) 940 | searchResult, searchErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 941 | result, err := txn.Execute(searchQuery) 942 | if err != nil { 943 | return nil, err 944 | } 945 | if !result.Next(txn) { 946 | return nil, result.Err() 947 | } 948 | ionReceiver := "" 949 | err = ion.Unmarshal(result.GetCurrentData(), &ionReceiver) 950 | if err != nil { 951 | return nil, err 952 | } 953 | return ionReceiver, nil 954 | }) 955 | assert.NoError(t, searchErr) 956 | assert.Equal(t, searchResult.(string), "") 957 | 958 | }) 959 | 960 | t.Run("struct", func(t *testing.T) { 961 | driver, err := testBase.getDefaultDriver() 962 | require.NoError(t, err) 963 | defer driver.Shutdown(context.Background()) 964 | defer cleanup(driver, testTableName) 965 | 966 | type Anon struct { 967 | A, B int 968 | } 969 | parameterValue := Anon{42, 2} 970 | 971 | query := fmt.Sprintf("UPDATE %s SET %s = ?", testTableName, columnName) 972 | executeResult, executeErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 973 | return executeWithParam(context.Background(), query, txn, parameterValue) 974 | }) 975 | assert.NoError(t, executeErr) 976 | assert.Equal(t, 1, executeResult.(int)) 977 | 978 | searchQuery := fmt.Sprintf("SELECT VALUE %s FROM %s WHERE %s = ?", columnName, testTableName, columnName) 979 | searchResult, searchErr := driver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 980 | result, err := txn.Execute(searchQuery, parameterValue) 981 | if err != nil { 982 | return nil, err 983 | } 984 | if !result.Next(txn) { 985 | return nil, result.Err() 986 | } 987 | ionReceiver := new(Anon) 988 | err = ion.Unmarshal(result.GetCurrentData(), &ionReceiver) 989 | if err != nil { 990 | return nil, err 991 | } 992 | return ionReceiver, nil 993 | }) 994 | assert.NoError(t, searchErr) 995 | assert.Equal(t, ¶meterValue, searchResult.(*Anon)) 996 | }) 997 | 998 | }) 999 | 1000 | t.Run("Delete Table that does not exist", func(t *testing.T) { 1001 | query := "DELETE FROM NonExistentTable" 1002 | result, err := qldbDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 1003 | return txn.Execute(query) 1004 | }) 1005 | assert.Nil(t, result) 1006 | // The exception was wrapped as un-modeled service error responses by SDK V2, not types.BadRequestException 1007 | // Example response as APIError: code: 432, message: Semantic Error: at line 1, column 13: No such variable named 'NonExistentTable'; No such variable named 'NonExistentTable', fault: unknown 1008 | var ae smithy.APIError 1009 | assert.True(t, errors.As(err, &ae)) 1010 | }) 1011 | 1012 | t.Run("Return Transaction ID after executing statement", func(t *testing.T) { 1013 | query := fmt.Sprintf("SELECT * FROM %s", testTableName) 1014 | txnID, err := qldbDriver.Execute(context.Background(), func(txn Transaction) (interface{}, error) { 1015 | _, err := txn.Execute(query) 1016 | if err != nil { 1017 | return nil, err 1018 | } 1019 | 1020 | return txn.ID(), nil 1021 | }) 1022 | require.NoError(t, err) 1023 | assert.NotEmpty(t, txnID) 1024 | }) 1025 | } 1026 | --------------------------------------------------------------------------------