diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..410d4a1
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+/.github export-ignore
+/tests export-ignore
+/.gitignore export-ignore
+/Makefile export-ignore
+/phpunit.xml.dist export-ignore
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..ef7eb61
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: [nyholm, jderusse]
diff --git a/.github/workflows/.editorconfig b/.github/workflows/.editorconfig
new file mode 100644
index 0000000..7bd3346
--- /dev/null
+++ b/.github/workflows/.editorconfig
@@ -0,0 +1,2 @@
+[*.yml]
+indent_size = 2
diff --git a/.github/workflows/branch_alias.yml b/.github/workflows/branch_alias.yml
new file mode 100644
index 0000000..a79ba44
--- /dev/null
+++ b/.github/workflows/branch_alias.yml
@@ -0,0 +1,76 @@
+name: Update branch alias
+
+on:
+ push:
+ tags: ['*']
+
+jobs:
+ branch-alias:
+ name: Update branch alias
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Set up PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.1
+ coverage: none
+
+ - name: Find branch alias
+ id: find_alias
+ run: |
+ TAG=$(echo $GITHUB_REF | cut -d'/' -f 3)
+ echo "Last tag was $TAG"
+ ARR=(${TAG//./ })
+ ARR[1]=$((${ARR[1]}+1))
+ echo ::set-output name=alias::${ARR[0]}.${ARR[1]}
+
+ - name: Checkout main repo
+ run: |
+ git clone --branch master https://${{ secrets.BOT_GITHUB_TOKEN }}:x-oauth-basic@github.com/async-aws/aws aws
+
+ - name: Update branch alias
+ run: |
+ cd aws/src/Service/TimestreamQuery
+ CURRENT_ALIAS=$(composer config extra.branch-alias.dev-master | cut -d'-' -f 1)
+
+ # If there is a current value on the branch alias
+ if [ ! -z $CURRENT_ALIAS ]; then
+ NEW_ALIAS=${{ steps.find_alias.outputs.alias }}
+ CURRENT_ARR=(${CURRENT_ALIAS//./ })
+ NEW_ARR=(${NEW_ALIAS//./ })
+
+ if [ ${CURRENT_ARR[0]} -gt ${NEW_ARR[0]} ]; then
+ echo "The current value for major version is larger"
+ exit 1;
+ fi
+
+ if [ ${CURRENT_ARR[0]} -eq ${NEW_ARR[0]} ] && [ ${CURRENT_ARR[1]} -gt ${NEW_ARR[1]} ]; then
+ echo "The current value for minor version is larger"
+ exit 1;
+ fi
+ fi
+
+ composer config extra.branch-alias.dev-master ${{ steps.find_alias.outputs.alias }}-dev
+
+ - name: Commit & push the new files
+ run: |
+ echo "::group::git status"
+ cd aws
+ git status
+ echo "::endgroup::"
+
+ git add -N .
+ if [[ $(git diff --numstat | wc -l) -eq 0 ]]; then
+ echo "No changes found. Exiting."
+ exit 0;
+ fi
+
+ git config --local user.email "github@async-aws.com"
+ git config --local user.name "AsyncAws Bot"
+
+ echo "::group::git push"
+ git add .
+ git commit -m "Update branch alias"
+ git push
+ echo "::endgroup::"
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
new file mode 100644
index 0000000..1a72276
--- /dev/null
+++ b/.github/workflows/checks.yml
@@ -0,0 +1,27 @@
+name: BC Check
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ roave-bc-check:
+ name: Roave BC Check
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Modify composer.json
+ run: |
+ sed -i -re 's/"require": \{/"minimum-stability": "dev","prefer-stable": true,"require": \{/' composer.json
+ cat composer.json
+
+ git config --local user.email "github@async-aws.com"
+ git config --local user.name "AsyncAws Bot"
+ git commit -am "Allow unstable dependencies"
+
+ - name: Roave BC Check
+ uses: docker://nyholm/roave-bc-check-ga
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4e80845
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,38 @@
+name: Tests
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ strategy:
+ max-parallel: 10
+ matrix:
+ php: ['7.2', '7.3', '7.4', '8.0', '8.1']
+
+ steps:
+ - name: Set up PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ coverage: none
+
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Initialize tests
+ run: make initialize
+
+ - name: Download dependencies
+ run: |
+ composer config minimum-stability dev
+ composer req symfony/phpunit-bridge --no-update
+ composer update --no-interaction --prefer-dist --optimize-autoloader --prefer-stable
+
+ - name: Run tests
+ run: ./vendor/bin/simple-phpunit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4ef8091
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/vendor/
+*.cache
+composer.lock
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..457d417
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Change Log
+
+## NOT RELEASED
+
+## 0.1.0
+
+First version
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..50402d9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2021 Jérémy Derussé, Tobias Nyholm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..850dffc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,12 @@
+.EXPORT_ALL_VARIABLES:
+
+initialize: start-docker
+start-docker:
+ echo "Noop"
+
+test: initialize
+ ./vendor/bin/simple-phpunit
+
+clean: stop-docker
+stop-docker:
+ echo "Noop"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b7ba35a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+# AsyncAws Timestream Query Client
+
+![](https://github.com/async-aws/timestream-query/workflows/Tests/badge.svg?branch=master)
+![](https://github.com/async-aws/timestream-query/workflows/BC%20Check/badge.svg?branch=master)
+
+An API client for Timestream Query.
+
+## Install
+
+```cli
+composer require async-aws/timestream-query
+```
+
+## Documentation
+
+See https://async-aws.com/clients/timestream-query.html for documentation.
+
+## Contribute
+
+Contributions are welcome and appreciated. Please read https://async-aws.com/contribute/
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..adbd8fa
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "async-aws/timestream-query",
+ "description": "Timestream Query client, part of the AWS SDK provided by AsyncAws.",
+ "license": "MIT",
+ "type": "library",
+ "keywords": [
+ "aws",
+ "amazon",
+ "sdk",
+ "async-aws",
+ "timestream-query"
+ ],
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "ext-filter": "*",
+ "ext-json": "*",
+ "async-aws/core": "^1.9"
+ },
+ "autoload": {
+ "psr-4": {
+ "AsyncAws\\TimestreamQuery\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "AsyncAws\\TimestreamQuery\\Tests\\": "tests/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.1-dev"
+ }
+ }
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..9894ce3
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,23 @@
+
+
+
+
+ ./src
+
+
+
+
+
+
+
+ ./tests/
+
+
+
diff --git a/src/Enum/ScalarType.php b/src/Enum/ScalarType.php
new file mode 100644
index 0000000..7bc0538
--- /dev/null
+++ b/src/Enum/ScalarType.php
@@ -0,0 +1,38 @@
+ true,
+ self::BOOLEAN => true,
+ self::DATE => true,
+ self::DOUBLE => true,
+ self::INTEGER => true,
+ self::INTERVAL_DAY_TO_SECOND => true,
+ self::INTERVAL_YEAR_TO_MONTH => true,
+ self::TIME => true,
+ self::TIMESTAMP => true,
+ self::UNKNOWN => true,
+ self::VARCHAR => true,
+ ][$value]);
+ }
+}
diff --git a/src/Exception/AccessDeniedException.php b/src/Exception/AccessDeniedException.php
new file mode 100644
index 0000000..ea10425
--- /dev/null
+++ b/src/Exception/AccessDeniedException.php
@@ -0,0 +1,21 @@
+toArray(false);
+
+ if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
+ $this->message = $v;
+ }
+ }
+}
diff --git a/src/Exception/ConflictException.php b/src/Exception/ConflictException.php
new file mode 100644
index 0000000..9393502
--- /dev/null
+++ b/src/Exception/ConflictException.php
@@ -0,0 +1,21 @@
+toArray(false);
+
+ if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
+ $this->message = $v;
+ }
+ }
+}
diff --git a/src/Exception/InternalServerException.php b/src/Exception/InternalServerException.php
new file mode 100644
index 0000000..a07da2f
--- /dev/null
+++ b/src/Exception/InternalServerException.php
@@ -0,0 +1,21 @@
+toArray(false);
+
+ if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
+ $this->message = $v;
+ }
+ }
+}
diff --git a/src/Exception/InvalidEndpointException.php b/src/Exception/InvalidEndpointException.php
new file mode 100644
index 0000000..1a46949
--- /dev/null
+++ b/src/Exception/InvalidEndpointException.php
@@ -0,0 +1,21 @@
+toArray(false);
+
+ if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
+ $this->message = $v;
+ }
+ }
+}
diff --git a/src/Exception/QueryExecutionException.php b/src/Exception/QueryExecutionException.php
new file mode 100644
index 0000000..bc86392
--- /dev/null
+++ b/src/Exception/QueryExecutionException.php
@@ -0,0 +1,21 @@
+toArray(false);
+
+ if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
+ $this->message = $v;
+ }
+ }
+}
diff --git a/src/Exception/ThrottlingException.php b/src/Exception/ThrottlingException.php
new file mode 100644
index 0000000..5ad23e6
--- /dev/null
+++ b/src/Exception/ThrottlingException.php
@@ -0,0 +1,21 @@
+toArray(false);
+
+ if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
+ $this->message = $v;
+ }
+ }
+}
diff --git a/src/Exception/ValidationException.php b/src/Exception/ValidationException.php
new file mode 100644
index 0000000..0d791f3
--- /dev/null
+++ b/src/Exception/ValidationException.php
@@ -0,0 +1,21 @@
+toArray(false);
+
+ if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
+ $this->message = $v;
+ }
+ }
+}
diff --git a/src/Input/CancelQueryRequest.php b/src/Input/CancelQueryRequest.php
new file mode 100644
index 0000000..318aeeb
--- /dev/null
+++ b/src/Input/CancelQueryRequest.php
@@ -0,0 +1,85 @@
+queryId = $input['QueryId'] ?? null;
+ parent::__construct($input);
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ public function getQueryId(): ?string
+ {
+ return $this->queryId;
+ }
+
+ /**
+ * @internal
+ */
+ public function request(): Request
+ {
+ // Prepare headers
+ $headers = [
+ 'Content-Type' => 'application/x-amz-json-1.0',
+ 'X-Amz-Target' => 'Timestream_20181101.CancelQuery',
+ ];
+
+ // Prepare query
+ $query = [];
+
+ // Prepare URI
+ $uriString = '/';
+
+ // Prepare Body
+ $bodyPayload = $this->requestBody();
+ $body = empty($bodyPayload) ? '{}' : json_encode($bodyPayload, 4194304);
+
+ // Return the Request
+ return new Request('POST', $uriString, $query, $headers, StreamFactory::create($body));
+ }
+
+ public function setQueryId(?string $value): self
+ {
+ $this->queryId = $value;
+
+ return $this;
+ }
+
+ private function requestBody(): array
+ {
+ $payload = [];
+ if (null === $v = $this->queryId) {
+ throw new InvalidArgument(sprintf('Missing parameter "QueryId" for "%s". The value cannot be null.', __CLASS__));
+ }
+ $payload['QueryId'] = $v;
+
+ return $payload;
+ }
+}
diff --git a/src/Input/PrepareQueryRequest.php b/src/Input/PrepareQueryRequest.php
new file mode 100644
index 0000000..674cf0b
--- /dev/null
+++ b/src/Input/PrepareQueryRequest.php
@@ -0,0 +1,111 @@
+queryString = $input['QueryString'] ?? null;
+ $this->validateOnly = $input['ValidateOnly'] ?? null;
+ parent::__construct($input);
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ public function getQueryString(): ?string
+ {
+ return $this->queryString;
+ }
+
+ public function getValidateOnly(): ?bool
+ {
+ return $this->validateOnly;
+ }
+
+ /**
+ * @internal
+ */
+ public function request(): Request
+ {
+ // Prepare headers
+ $headers = [
+ 'Content-Type' => 'application/x-amz-json-1.0',
+ 'X-Amz-Target' => 'Timestream_20181101.PrepareQuery',
+ ];
+
+ // Prepare query
+ $query = [];
+
+ // Prepare URI
+ $uriString = '/';
+
+ // Prepare Body
+ $bodyPayload = $this->requestBody();
+ $body = empty($bodyPayload) ? '{}' : json_encode($bodyPayload, 4194304);
+
+ // Return the Request
+ return new Request('POST', $uriString, $query, $headers, StreamFactory::create($body));
+ }
+
+ public function setQueryString(?string $value): self
+ {
+ $this->queryString = $value;
+
+ return $this;
+ }
+
+ public function setValidateOnly(?bool $value): self
+ {
+ $this->validateOnly = $value;
+
+ return $this;
+ }
+
+ private function requestBody(): array
+ {
+ $payload = [];
+ if (null === $v = $this->queryString) {
+ throw new InvalidArgument(sprintf('Missing parameter "QueryString" for "%s". The value cannot be null.', __CLASS__));
+ }
+ $payload['QueryString'] = $v;
+ if (null !== $v = $this->validateOnly) {
+ $payload['ValidateOnly'] = (bool) $v;
+ }
+
+ return $payload;
+ }
+}
diff --git a/src/Input/QueryRequest.php b/src/Input/QueryRequest.php
new file mode 100644
index 0000000..959549e
--- /dev/null
+++ b/src/Input/QueryRequest.php
@@ -0,0 +1,165 @@
+queryString = $input['QueryString'] ?? null;
+ $this->clientToken = $input['ClientToken'] ?? null;
+ $this->nextToken = $input['NextToken'] ?? null;
+ $this->maxRows = $input['MaxRows'] ?? null;
+ parent::__construct($input);
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ public function getClientToken(): ?string
+ {
+ return $this->clientToken;
+ }
+
+ public function getMaxRows(): ?int
+ {
+ return $this->maxRows;
+ }
+
+ public function getNextToken(): ?string
+ {
+ return $this->nextToken;
+ }
+
+ public function getQueryString(): ?string
+ {
+ return $this->queryString;
+ }
+
+ /**
+ * @internal
+ */
+ public function request(): Request
+ {
+ // Prepare headers
+ $headers = [
+ 'Content-Type' => 'application/x-amz-json-1.0',
+ 'X-Amz-Target' => 'Timestream_20181101.Query',
+ ];
+
+ // Prepare query
+ $query = [];
+
+ // Prepare URI
+ $uriString = '/';
+
+ // Prepare Body
+ $bodyPayload = $this->requestBody();
+ $body = empty($bodyPayload) ? '{}' : json_encode($bodyPayload, 4194304);
+
+ // Return the Request
+ return new Request('POST', $uriString, $query, $headers, StreamFactory::create($body));
+ }
+
+ public function setClientToken(?string $value): self
+ {
+ $this->clientToken = $value;
+
+ return $this;
+ }
+
+ public function setMaxRows(?int $value): self
+ {
+ $this->maxRows = $value;
+
+ return $this;
+ }
+
+ public function setNextToken(?string $value): self
+ {
+ $this->nextToken = $value;
+
+ return $this;
+ }
+
+ public function setQueryString(?string $value): self
+ {
+ $this->queryString = $value;
+
+ return $this;
+ }
+
+ private function requestBody(): array
+ {
+ $payload = [];
+ if (null === $v = $this->queryString) {
+ throw new InvalidArgument(sprintf('Missing parameter "QueryString" for "%s". The value cannot be null.', __CLASS__));
+ }
+ $payload['QueryString'] = $v;
+ if (null === $v = $this->clientToken) {
+ $v = uuid_create(\UUID_TYPE_RANDOM);
+ }
+ $payload['ClientToken'] = $v;
+ if (null !== $v = $this->nextToken) {
+ $payload['NextToken'] = $v;
+ }
+ if (null !== $v = $this->maxRows) {
+ $payload['MaxRows'] = $v;
+ }
+
+ return $payload;
+ }
+}
diff --git a/src/Result/CancelQueryResponse.php b/src/Result/CancelQueryResponse.php
new file mode 100644
index 0000000..2abce6c
--- /dev/null
+++ b/src/Result/CancelQueryResponse.php
@@ -0,0 +1,29 @@
+initialize();
+
+ return $this->cancellationMessage;
+ }
+
+ protected function populateResult(Response $response): void
+ {
+ $data = $response->toArray();
+
+ $this->cancellationMessage = isset($data['CancellationMessage']) ? (string) $data['CancellationMessage'] : null;
+ }
+}
diff --git a/src/Result/PrepareQueryResponse.php b/src/Result/PrepareQueryResponse.php
new file mode 100644
index 0000000..c75b9b7
--- /dev/null
+++ b/src/Result/PrepareQueryResponse.php
@@ -0,0 +1,140 @@
+initialize();
+
+ return $this->columns;
+ }
+
+ /**
+ * @return ParameterMapping[]
+ */
+ public function getParameters(): array
+ {
+ $this->initialize();
+
+ return $this->parameters;
+ }
+
+ public function getQueryString(): string
+ {
+ $this->initialize();
+
+ return $this->queryString;
+ }
+
+ protected function populateResult(Response $response): void
+ {
+ $data = $response->toArray();
+
+ $this->queryString = (string) $data['QueryString'];
+ $this->columns = $this->populateResultSelectColumnList($data['Columns']);
+ $this->parameters = $this->populateResultParameterMappingList($data['Parameters']);
+ }
+
+ private function populateResultColumnInfo(array $json): ColumnInfo
+ {
+ return new ColumnInfo([
+ 'Name' => isset($json['Name']) ? (string) $json['Name'] : null,
+ 'Type' => $this->populateResultType($json['Type']),
+ ]);
+ }
+
+ /**
+ * @return ColumnInfo[]
+ */
+ private function populateResultColumnInfoList(array $json): array
+ {
+ $items = [];
+ foreach ($json as $item) {
+ $items[] = $this->populateResultColumnInfo($item);
+ }
+
+ return $items;
+ }
+
+ private function populateResultParameterMapping(array $json): ParameterMapping
+ {
+ return new ParameterMapping([
+ 'Name' => (string) $json['Name'],
+ 'Type' => $this->populateResultType($json['Type']),
+ ]);
+ }
+
+ /**
+ * @return ParameterMapping[]
+ */
+ private function populateResultParameterMappingList(array $json): array
+ {
+ $items = [];
+ foreach ($json as $item) {
+ $items[] = $this->populateResultParameterMapping($item);
+ }
+
+ return $items;
+ }
+
+ private function populateResultSelectColumn(array $json): SelectColumn
+ {
+ return new SelectColumn([
+ 'Name' => isset($json['Name']) ? (string) $json['Name'] : null,
+ 'Type' => empty($json['Type']) ? null : $this->populateResultType($json['Type']),
+ 'DatabaseName' => isset($json['DatabaseName']) ? (string) $json['DatabaseName'] : null,
+ 'TableName' => isset($json['TableName']) ? (string) $json['TableName'] : null,
+ 'Aliased' => isset($json['Aliased']) ? filter_var($json['Aliased'], \FILTER_VALIDATE_BOOLEAN) : null,
+ ]);
+ }
+
+ /**
+ * @return SelectColumn[]
+ */
+ private function populateResultSelectColumnList(array $json): array
+ {
+ $items = [];
+ foreach ($json as $item) {
+ $items[] = $this->populateResultSelectColumn($item);
+ }
+
+ return $items;
+ }
+
+ private function populateResultType(array $json): Type
+ {
+ return new Type([
+ 'ScalarType' => isset($json['ScalarType']) ? (string) $json['ScalarType'] : null,
+ 'ArrayColumnInfo' => empty($json['ArrayColumnInfo']) ? null : $this->populateResultColumnInfo($json['ArrayColumnInfo']),
+ 'TimeSeriesMeasureValueColumnInfo' => empty($json['TimeSeriesMeasureValueColumnInfo']) ? null : $this->populateResultColumnInfo($json['TimeSeriesMeasureValueColumnInfo']),
+ 'RowColumnInfo' => !isset($json['RowColumnInfo']) ? null : $this->populateResultColumnInfoList($json['RowColumnInfo']),
+ ]);
+ }
+}
diff --git a/src/Result/QueryResponse.php b/src/Result/QueryResponse.php
new file mode 100644
index 0000000..f85ba75
--- /dev/null
+++ b/src/Result/QueryResponse.php
@@ -0,0 +1,247 @@
+
+ */
+class QueryResponse extends Result implements \IteratorAggregate
+{
+ /**
+ * A unique ID for the given query.
+ */
+ private $queryId;
+
+ /**
+ * A pagination token that can be used again on a `Query` call to get the next set of results.
+ */
+ private $nextToken;
+
+ /**
+ * The result set rows returned by the query.
+ */
+ private $rows;
+
+ /**
+ * The column data types of the returned result set.
+ */
+ private $columnInfo;
+
+ /**
+ * Information about the status of the query, including progress and bytes scanned.
+ */
+ private $queryStatus;
+
+ /**
+ * @return ColumnInfo[]
+ */
+ public function getColumnInfo(): array
+ {
+ $this->initialize();
+
+ return $this->columnInfo;
+ }
+
+ /**
+ * Iterates over Rows.
+ *
+ * @return \Traversable
+ */
+ public function getIterator(): \Traversable
+ {
+ yield from $this->getRows();
+ }
+
+ public function getNextToken(): ?string
+ {
+ $this->initialize();
+
+ return $this->nextToken;
+ }
+
+ public function getQueryId(): string
+ {
+ $this->initialize();
+
+ return $this->queryId;
+ }
+
+ public function getQueryStatus(): ?QueryStatus
+ {
+ $this->initialize();
+
+ return $this->queryStatus;
+ }
+
+ /**
+ * @param bool $currentPageOnly When true, iterates over items of the current page. Otherwise also fetch items in the next pages.
+ *
+ * @return iterable
+ */
+ public function getRows(bool $currentPageOnly = false): iterable
+ {
+ if ($currentPageOnly) {
+ $this->initialize();
+ yield from $this->rows;
+
+ return;
+ }
+
+ $client = $this->awsClient;
+ if (!$client instanceof TimestreamQueryClient) {
+ throw new InvalidArgument('missing client injected in paginated result');
+ }
+ if (!$this->input instanceof QueryRequest) {
+ throw new InvalidArgument('missing last request injected in paginated result');
+ }
+ $input = clone $this->input;
+ $page = $this;
+ while (true) {
+ $page->initialize();
+ if ($page->nextToken) {
+ $input->setNextToken($page->nextToken);
+
+ $this->registerPrefetch($nextPage = $client->query($input));
+ } else {
+ $nextPage = null;
+ }
+
+ yield from $page->rows;
+
+ if (null === $nextPage) {
+ break;
+ }
+
+ $this->unregisterPrefetch($nextPage);
+ $page = $nextPage;
+ }
+ }
+
+ protected function populateResult(Response $response): void
+ {
+ $data = $response->toArray();
+
+ $this->queryId = (string) $data['QueryId'];
+ $this->nextToken = isset($data['NextToken']) ? (string) $data['NextToken'] : null;
+ $this->rows = $this->populateResultRowList($data['Rows']);
+ $this->columnInfo = $this->populateResultColumnInfoList($data['ColumnInfo']);
+ $this->queryStatus = empty($data['QueryStatus']) ? null : $this->populateResultQueryStatus($data['QueryStatus']);
+ }
+
+ private function populateResultColumnInfo(array $json): ColumnInfo
+ {
+ return new ColumnInfo([
+ 'Name' => isset($json['Name']) ? (string) $json['Name'] : null,
+ 'Type' => $this->populateResultType($json['Type']),
+ ]);
+ }
+
+ /**
+ * @return ColumnInfo[]
+ */
+ private function populateResultColumnInfoList(array $json): array
+ {
+ $items = [];
+ foreach ($json as $item) {
+ $items[] = $this->populateResultColumnInfo($item);
+ }
+
+ return $items;
+ }
+
+ private function populateResultDatum(array $json): Datum
+ {
+ return new Datum([
+ 'ScalarValue' => isset($json['ScalarValue']) ? (string) $json['ScalarValue'] : null,
+ 'TimeSeriesValue' => !isset($json['TimeSeriesValue']) ? null : $this->populateResultTimeSeriesDataPointList($json['TimeSeriesValue']),
+ 'ArrayValue' => !isset($json['ArrayValue']) ? null : $this->populateResultDatumList($json['ArrayValue']),
+ 'RowValue' => empty($json['RowValue']) ? null : $this->populateResultRow($json['RowValue']),
+ 'NullValue' => isset($json['NullValue']) ? filter_var($json['NullValue'], \FILTER_VALIDATE_BOOLEAN) : null,
+ ]);
+ }
+
+ /**
+ * @return Datum[]
+ */
+ private function populateResultDatumList(array $json): array
+ {
+ $items = [];
+ foreach ($json as $item) {
+ $items[] = $this->populateResultDatum($item);
+ }
+
+ return $items;
+ }
+
+ private function populateResultQueryStatus(array $json): QueryStatus
+ {
+ return new QueryStatus([
+ 'ProgressPercentage' => isset($json['ProgressPercentage']) ? (float) $json['ProgressPercentage'] : null,
+ 'CumulativeBytesScanned' => isset($json['CumulativeBytesScanned']) ? (string) $json['CumulativeBytesScanned'] : null,
+ 'CumulativeBytesMetered' => isset($json['CumulativeBytesMetered']) ? (string) $json['CumulativeBytesMetered'] : null,
+ ]);
+ }
+
+ private function populateResultRow(array $json): Row
+ {
+ return new Row([
+ 'Data' => $this->populateResultDatumList($json['Data']),
+ ]);
+ }
+
+ /**
+ * @return Row[]
+ */
+ private function populateResultRowList(array $json): array
+ {
+ $items = [];
+ foreach ($json as $item) {
+ $items[] = $this->populateResultRow($item);
+ }
+
+ return $items;
+ }
+
+ private function populateResultTimeSeriesDataPoint(array $json): TimeSeriesDataPoint
+ {
+ return new TimeSeriesDataPoint([
+ 'Time' => (string) $json['Time'],
+ 'Value' => $this->populateResultDatum($json['Value']),
+ ]);
+ }
+
+ /**
+ * @return TimeSeriesDataPoint[]
+ */
+ private function populateResultTimeSeriesDataPointList(array $json): array
+ {
+ $items = [];
+ foreach ($json as $item) {
+ $items[] = $this->populateResultTimeSeriesDataPoint($item);
+ }
+
+ return $items;
+ }
+
+ private function populateResultType(array $json): Type
+ {
+ return new Type([
+ 'ScalarType' => isset($json['ScalarType']) ? (string) $json['ScalarType'] : null,
+ 'ArrayColumnInfo' => empty($json['ArrayColumnInfo']) ? null : $this->populateResultColumnInfo($json['ArrayColumnInfo']),
+ 'TimeSeriesMeasureValueColumnInfo' => empty($json['TimeSeriesMeasureValueColumnInfo']) ? null : $this->populateResultColumnInfo($json['TimeSeriesMeasureValueColumnInfo']),
+ 'RowColumnInfo' => !isset($json['RowColumnInfo']) ? null : $this->populateResultColumnInfoList($json['RowColumnInfo']),
+ ]);
+ }
+}
diff --git a/src/TimestreamQueryClient.php b/src/TimestreamQueryClient.php
new file mode 100644
index 0000000..b5e0057
--- /dev/null
+++ b/src/TimestreamQueryClient.php
@@ -0,0 +1,153 @@
+getResponse($input->request(), new RequestContext(['operation' => 'CancelQuery', 'region' => $input->getRegion(), 'exceptionMapping' => [
+ 'AccessDeniedException' => AccessDeniedException::class,
+ 'InternalServerException' => InternalServerException::class,
+ 'ThrottlingException' => ThrottlingException::class,
+ 'ValidationException' => ValidationException::class,
+ 'InvalidEndpointException' => InvalidEndpointException::class,
+ ]]));
+
+ return new CancelQueryResponse($response);
+ }
+
+ /**
+ * A synchronous operation that allows you to submit a query with parameters to be stored by Timestream for later
+ * running. Timestream only supports using this operation with the `PrepareQueryRequest$ValidateOnly` set to `true`.
+ *
+ * @see https://docs.aws.amazon.com/timestream/latest/developerguide/API_Operations_Amazon_Timestream_Query.html/API_PrepareQuery.html
+ * @see https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-query.timestream-2018-11-01.html#preparequery
+ *
+ * @param array{
+ * QueryString: string,
+ * ValidateOnly?: bool,
+ * @region?: string,
+ * }|PrepareQueryRequest $input
+ *
+ * @throws AccessDeniedException
+ * @throws InternalServerException
+ * @throws ThrottlingException
+ * @throws ValidationException
+ * @throws InvalidEndpointException
+ */
+ public function prepareQuery($input): PrepareQueryResponse
+ {
+ $input = PrepareQueryRequest::create($input);
+ $response = $this->getResponse($input->request(), new RequestContext(['operation' => 'PrepareQuery', 'region' => $input->getRegion(), 'exceptionMapping' => [
+ 'AccessDeniedException' => AccessDeniedException::class,
+ 'InternalServerException' => InternalServerException::class,
+ 'ThrottlingException' => ThrottlingException::class,
+ 'ValidationException' => ValidationException::class,
+ 'InvalidEndpointException' => InvalidEndpointException::class,
+ ]]));
+
+ return new PrepareQueryResponse($response);
+ }
+
+ /**
+ * `Query` is a synchronous operation that enables you to run a query against your Amazon Timestream data. `Query` will
+ * time out after 60 seconds. You must update the default timeout in the SDK to support a timeout of 60 seconds. See the
+ * code sample for details.
+ *
+ * @see https://docs.aws.amazon.com/timestream/latest/developerguide/code-samples.run-query.html
+ * @see https://docs.aws.amazon.com/timestream/latest/developerguide/API_Operations_Amazon_Timestream_Query.html/API_Query.html
+ * @see https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-query.timestream-2018-11-01.html#query
+ *
+ * @param array{
+ * QueryString: string,
+ * ClientToken?: string,
+ * NextToken?: string,
+ * MaxRows?: int,
+ * @region?: string,
+ * }|QueryRequest $input
+ *
+ * @throws AccessDeniedException
+ * @throws ConflictException
+ * @throws InternalServerException
+ * @throws QueryExecutionException
+ * @throws ThrottlingException
+ * @throws ValidationException
+ * @throws InvalidEndpointException
+ */
+ public function query($input): QueryResponse
+ {
+ $input = QueryRequest::create($input);
+ $response = $this->getResponse($input->request(), new RequestContext(['operation' => 'Query', 'region' => $input->getRegion(), 'exceptionMapping' => [
+ 'AccessDeniedException' => AccessDeniedException::class,
+ 'ConflictException' => ConflictException::class,
+ 'InternalServerException' => InternalServerException::class,
+ 'QueryExecutionException' => QueryExecutionException::class,
+ 'ThrottlingException' => ThrottlingException::class,
+ 'ValidationException' => ValidationException::class,
+ 'InvalidEndpointException' => InvalidEndpointException::class,
+ ]]));
+
+ return new QueryResponse($response, $this, $input);
+ }
+
+ protected function getAwsErrorFactory(): AwsErrorFactoryInterface
+ {
+ return new JsonRpcAwsErrorFactory();
+ }
+
+ protected function getEndpointMetadata(?string $region): array
+ {
+ if (null === $region) {
+ $region = Configuration::DEFAULT_REGION;
+ }
+
+ return [
+ 'endpoint' => "https://query.timestream.$region.amazonaws.com",
+ 'signRegion' => $region,
+ 'signService' => 'timestream',
+ 'signVersions' => ['v4'],
+ ];
+ }
+}
diff --git a/src/ValueObject/ColumnInfo.php b/src/ValueObject/ColumnInfo.php
new file mode 100644
index 0000000..2d87fd3
--- /dev/null
+++ b/src/ValueObject/ColumnInfo.php
@@ -0,0 +1,48 @@
+name = $input['Name'] ?? null;
+ $this->type = isset($input['Type']) ? Type::create($input['Type']) : null;
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function getType(): Type
+ {
+ return $this->type;
+ }
+}
diff --git a/src/ValueObject/Datum.php b/src/ValueObject/Datum.php
new file mode 100644
index 0000000..11e778d
--- /dev/null
+++ b/src/ValueObject/Datum.php
@@ -0,0 +1,88 @@
+scalarValue = $input['ScalarValue'] ?? null;
+ $this->timeSeriesValue = isset($input['TimeSeriesValue']) ? array_map([TimeSeriesDataPoint::class, 'create'], $input['TimeSeriesValue']) : null;
+ $this->arrayValue = isset($input['ArrayValue']) ? array_map([Datum::class, 'create'], $input['ArrayValue']) : null;
+ $this->rowValue = isset($input['RowValue']) ? Row::create($input['RowValue']) : null;
+ $this->nullValue = $input['NullValue'] ?? null;
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ /**
+ * @return Datum[]
+ */
+ public function getArrayValue(): array
+ {
+ return $this->arrayValue ?? [];
+ }
+
+ public function getNullValue(): ?bool
+ {
+ return $this->nullValue;
+ }
+
+ public function getRowValue(): ?Row
+ {
+ return $this->rowValue;
+ }
+
+ public function getScalarValue(): ?string
+ {
+ return $this->scalarValue;
+ }
+
+ /**
+ * @return TimeSeriesDataPoint[]
+ */
+ public function getTimeSeriesValue(): array
+ {
+ return $this->timeSeriesValue ?? [];
+ }
+}
diff --git a/src/ValueObject/ParameterMapping.php b/src/ValueObject/ParameterMapping.php
new file mode 100644
index 0000000..276ca33
--- /dev/null
+++ b/src/ValueObject/ParameterMapping.php
@@ -0,0 +1,43 @@
+name = $input['Name'] ?? null;
+ $this->type = isset($input['Type']) ? Type::create($input['Type']) : null;
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getType(): Type
+ {
+ return $this->type;
+ }
+}
diff --git a/src/ValueObject/QueryStatus.php b/src/ValueObject/QueryStatus.php
new file mode 100644
index 0000000..7356afa
--- /dev/null
+++ b/src/ValueObject/QueryStatus.php
@@ -0,0 +1,61 @@
+progressPercentage = $input['ProgressPercentage'] ?? null;
+ $this->cumulativeBytesScanned = $input['CumulativeBytesScanned'] ?? null;
+ $this->cumulativeBytesMetered = $input['CumulativeBytesMetered'] ?? null;
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ public function getCumulativeBytesMetered(): ?string
+ {
+ return $this->cumulativeBytesMetered;
+ }
+
+ public function getCumulativeBytesScanned(): ?string
+ {
+ return $this->cumulativeBytesScanned;
+ }
+
+ public function getProgressPercentage(): ?float
+ {
+ return $this->progressPercentage;
+ }
+}
diff --git a/src/ValueObject/Row.php b/src/ValueObject/Row.php
new file mode 100644
index 0000000..d6f661f
--- /dev/null
+++ b/src/ValueObject/Row.php
@@ -0,0 +1,37 @@
+data = isset($input['Data']) ? array_map([Datum::class, 'create'], $input['Data']) : null;
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ /**
+ * @return Datum[]
+ */
+ public function getData(): array
+ {
+ return $this->data ?? [];
+ }
+}
diff --git a/src/ValueObject/SelectColumn.php b/src/ValueObject/SelectColumn.php
new file mode 100644
index 0000000..b8f7f5a
--- /dev/null
+++ b/src/ValueObject/SelectColumn.php
@@ -0,0 +1,79 @@
+name = $input['Name'] ?? null;
+ $this->type = isset($input['Type']) ? Type::create($input['Type']) : null;
+ $this->databaseName = $input['DatabaseName'] ?? null;
+ $this->tableName = $input['TableName'] ?? null;
+ $this->aliased = $input['Aliased'] ?? null;
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ public function getAliased(): ?bool
+ {
+ return $this->aliased;
+ }
+
+ public function getDatabaseName(): ?string
+ {
+ return $this->databaseName;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function getTableName(): ?string
+ {
+ return $this->tableName;
+ }
+
+ public function getType(): ?Type
+ {
+ return $this->type;
+ }
+}
diff --git a/src/ValueObject/TimeSeriesDataPoint.php b/src/ValueObject/TimeSeriesDataPoint.php
new file mode 100644
index 0000000..404bd74
--- /dev/null
+++ b/src/ValueObject/TimeSeriesDataPoint.php
@@ -0,0 +1,48 @@
+time = $input['Time'] ?? null;
+ $this->value = isset($input['Value']) ? Datum::create($input['Value']) : null;
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ public function getTime(): string
+ {
+ return $this->time;
+ }
+
+ public function getValue(): Datum
+ {
+ return $this->value;
+ }
+}
diff --git a/src/ValueObject/Type.php b/src/ValueObject/Type.php
new file mode 100644
index 0000000..52088ad
--- /dev/null
+++ b/src/ValueObject/Type.php
@@ -0,0 +1,75 @@
+scalarType = $input['ScalarType'] ?? null;
+ $this->arrayColumnInfo = isset($input['ArrayColumnInfo']) ? ColumnInfo::create($input['ArrayColumnInfo']) : null;
+ $this->timeSeriesMeasureValueColumnInfo = isset($input['TimeSeriesMeasureValueColumnInfo']) ? ColumnInfo::create($input['TimeSeriesMeasureValueColumnInfo']) : null;
+ $this->rowColumnInfo = isset($input['RowColumnInfo']) ? array_map([ColumnInfo::class, 'create'], $input['RowColumnInfo']) : null;
+ }
+
+ public static function create($input): self
+ {
+ return $input instanceof self ? $input : new self($input);
+ }
+
+ public function getArrayColumnInfo(): ?ColumnInfo
+ {
+ return $this->arrayColumnInfo;
+ }
+
+ /**
+ * @return ColumnInfo[]
+ */
+ public function getRowColumnInfo(): array
+ {
+ return $this->rowColumnInfo ?? [];
+ }
+
+ /**
+ * @return ScalarType::*|null
+ */
+ public function getScalarType(): ?string
+ {
+ return $this->scalarType;
+ }
+
+ public function getTimeSeriesMeasureValueColumnInfo(): ?ColumnInfo
+ {
+ return $this->timeSeriesMeasureValueColumnInfo;
+ }
+}
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/tests/Integration/TimestreamQueryClientTest.php b/tests/Integration/TimestreamQueryClientTest.php
new file mode 100644
index 0000000..ddd1a58
--- /dev/null
+++ b/tests/Integration/TimestreamQueryClientTest.php
@@ -0,0 +1,80 @@
+getClient();
+
+ $input = new CancelQueryRequest([
+ 'QueryId' => 'change me',
+ ]);
+ $result = $client->cancelQuery($input);
+
+ $result->resolve();
+
+ self::assertSame('changeIt', $result->getCancellationMessage());
+ }
+
+ public function testPrepareQuery(): void
+ {
+ self::markTestIncomplete('Cannot test without support for timestream.');
+
+ $client = $this->getClient();
+
+ $input = new PrepareQueryRequest([
+ 'QueryString' => 'change me',
+ 'ValidateOnly' => false,
+ ]);
+ $result = $client->prepareQuery($input);
+
+ $result->resolve();
+
+ self::assertSame('changeIt', $result->getQueryString());
+ // self::assertTODO(expected, $result->getColumns());
+ // self::assertTODO(expected, $result->getParameters());
+ }
+
+ public function testQuery(): void
+ {
+ self::markTestIncomplete('Cannot test without support for timestream.');
+
+ $client = $this->getClient();
+
+ $input = new QueryRequest([
+ 'QueryString' => 'change me',
+ 'ClientToken' => 'change me',
+ 'NextToken' => 'change me',
+ 'MaxRows' => 1337,
+ ]);
+ $result = $client->query($input);
+
+ $result->resolve();
+
+ self::assertSame('changeIt', $result->getQueryId());
+ self::assertSame('changeIt', $result->getNextToken());
+ // self::assertTODO(expected, $result->getRows());
+ // self::assertTODO(expected, $result->getColumnInfo());
+ // self::assertTODO(expected, $result->getQueryStatus());
+ }
+
+ private function getClient(): TimestreamQueryClient
+ {
+ self::fail('Not implemented');
+
+ return new TimestreamQueryClient([
+ 'endpoint' => 'http://localhost',
+ ], new NullProvider());
+ }
+}
diff --git a/tests/Unit/Input/CancelQueryRequestTest.php b/tests/Unit/Input/CancelQueryRequestTest.php
new file mode 100644
index 0000000..4b87c8c
--- /dev/null
+++ b/tests/Unit/Input/CancelQueryRequestTest.php
@@ -0,0 +1,29 @@
+ 'qwertyuiop',
+ ]);
+
+ // see https://docs.aws.amazon.com/timestream/latest/developerguide/API_Operations_Amazon_Timestream_Query.html/API_CancelQuery.html
+ $expected = '
+ POST / HTTP/1.0
+ Content-Type: application/x-amz-json-1.0
+ x-amz-target: Timestream_20181101.CancelQuery
+
+ {
+ "QueryId": "qwertyuiop"
+ }
+ ';
+
+ self::assertRequestEqualsHttpRequest($expected, $input->request());
+ }
+}
diff --git a/tests/Unit/Input/PrepareQueryRequestTest.php b/tests/Unit/Input/PrepareQueryRequestTest.php
new file mode 100644
index 0000000..fcdd603
--- /dev/null
+++ b/tests/Unit/Input/PrepareQueryRequestTest.php
@@ -0,0 +1,31 @@
+ 'SELECT * FROM db.tbl ORDER BY time DESC LIMIT 10',
+ 'ValidateOnly' => true,
+ ]);
+
+ // see https://docs.aws.amazon.com/timestream/latest/developerguide/API_Operations_Amazon_Timestream_Query.html/API_PrepareQuery.html
+ $expected = '
+ POST / HTTP/1.0
+ Content-Type: application/x-amz-json-1.0
+ x-amz-target: Timestream_20181101.PrepareQuery
+
+ {
+ "QueryString": "SELECT * FROM db.tbl ORDER BY time DESC LIMIT 10",
+ "ValidateOnly": true
+ }
+ ';
+
+ self::assertRequestEqualsHttpRequest($expected, $input->request());
+ }
+}
diff --git a/tests/Unit/Input/QueryRequestTest.php b/tests/Unit/Input/QueryRequestTest.php
new file mode 100644
index 0000000..82b2cde
--- /dev/null
+++ b/tests/Unit/Input/QueryRequestTest.php
@@ -0,0 +1,31 @@
+ 'qwertyuiop',
+ 'QueryString' => 'SELECT * FROM db.tbl ORDER BY time DESC LIMIT 10',
+ ]);
+
+ // see https://docs.aws.amazon.com/timestream/latest/developerguide/API_Operations_Amazon_Timestream_Query.html/API_Query.html
+ $expected = '
+ POST / HTTP/1.0
+ Content-Type: application/x-amz-json-1.0
+ x-amz-target: Timestream_20181101.Query
+
+ {
+ "ClientToken": "qwertyuiop",
+ "QueryString": "SELECT * FROM db.tbl ORDER BY time DESC LIMIT 10"
+ }
+ ';
+
+ self::assertRequestEqualsHttpRequest($expected, $input->request());
+ }
+}
diff --git a/tests/Unit/Result/CancelQueryResponseTest.php b/tests/Unit/Result/CancelQueryResponseTest.php
new file mode 100644
index 0000000..177c48c
--- /dev/null
+++ b/tests/Unit/Result/CancelQueryResponseTest.php
@@ -0,0 +1,26 @@
+request('POST', 'http://localhost'), $client, new NullLogger()));
+
+ self::assertSame('Query cancelled.', $result->getCancellationMessage());
+ }
+}
diff --git a/tests/Unit/Result/PrepareQueryResponseTest.php b/tests/Unit/Result/PrepareQueryResponseTest.php
new file mode 100644
index 0000000..7924008
--- /dev/null
+++ b/tests/Unit/Result/PrepareQueryResponseTest.php
@@ -0,0 +1,50 @@
+request('POST', 'http://localhost'), $client, new NullLogger()));
+
+ self::assertCount(1, $result->getColumns());
+ self::assertInstanceOf(SelectColumn::class, $result->getColumns()[0]);
+ self::assertFalse($result->getColumns()[0]->getAliased());
+ self::assertSame('db', $result->getColumns()[0]->getDatabaseName());
+ self::assertSame('foo', $result->getColumns()[0]->getName());
+ self::assertSame('tbl', $result->getColumns()[0]->getTableName());
+ self::assertInstanceOf(Type::class, $result->getColumns()[0]->getType());
+ self::assertSame(ScalarType::VARCHAR, $result->getColumns()[0]->getType()->getScalarType());
+ self::assertSame([], $result->getParameters());
+ self::assertSame('query string', $result->getQueryString());
+ }
+}
diff --git a/tests/Unit/Result/QueryResponseTest.php b/tests/Unit/Result/QueryResponseTest.php
new file mode 100644
index 0000000..058bb20
--- /dev/null
+++ b/tests/Unit/Result/QueryResponseTest.php
@@ -0,0 +1,73 @@
+request('POST', 'http://localhost'), $client, new NullLogger()), new TimestreamQueryClient(), new QueryRequest([]));
+
+ self::assertCount(1, $result->getColumnInfo());
+ self::assertInstanceOf(ColumnInfo::class, $result->getColumnInfo()[0]);
+ self::assertSame('foo', $result->getColumnInfo()[0]->getName());
+ self::assertInstanceOf(Type::class, $result->getColumnInfo()[0]->getType());
+ self::assertSame(ScalarType::VARCHAR, $result->getColumnInfo()[0]->getType()->getScalarType());
+ self::assertSame('qwertyuiop', $result->getQueryId());
+ self::assertInstanceOf(QueryStatus::class, $result->getQueryStatus());
+ self::assertSame('1024', $result->getQueryStatus()->getCumulativeBytesMetered());
+ self::assertSame('800', $result->getQueryStatus()->getCumulativeBytesScanned());
+ self::assertSame(1.0, $result->getQueryStatus()->getProgressPercentage());
+
+ $rows = iterator_to_array($result->getRows(true));
+
+ self::assertCount(1, $rows);
+ self::assertInstanceOf(Row::class, $rows[0]);
+ self::assertCount(1, $rows[0]->getData());
+ self::assertInstanceOf(Datum::class, $rows[0]->getData()[0]);
+ self::assertSame('datum', $rows[0]->getData()[0]->getScalarValue());
+ }
+}
diff --git a/tests/Unit/TimestreamQueryClientTest.php b/tests/Unit/TimestreamQueryClientTest.php
new file mode 100644
index 0000000..b7a9877
--- /dev/null
+++ b/tests/Unit/TimestreamQueryClientTest.php
@@ -0,0 +1,60 @@
+ 'qwertyuiop',
+ ]);
+ $result = $client->cancelQuery($input);
+
+ self::assertInstanceOf(CancelQueryResponse::class, $result);
+ self::assertFalse($result->info()['resolved']);
+ }
+
+ public function testPrepareQuery(): void
+ {
+ $client = new TimestreamQueryClient([], new NullProvider(), new MockHttpClient());
+
+ $input = new PrepareQueryRequest([
+ 'QueryString' => 'SELECT * FROM db.tbl ORDER BY time DESC LIMIT 10',
+ 'ValidateOnly' => true,
+
+ ]);
+ $result = $client->prepareQuery($input);
+
+ self::assertInstanceOf(PrepareQueryResponse::class, $result);
+ self::assertFalse($result->info()['resolved']);
+ }
+
+ public function testQuery(): void
+ {
+ $client = new TimestreamQueryClient([], new NullProvider(), new MockHttpClient());
+
+ $input = new QueryRequest([
+ 'ClientToken' => 'qwertyuiop',
+ 'QueryString' => 'SELECT * FROM db.tbl ORDER BY time DESC LIMIT 10',
+
+ ]);
+ $result = $client->query($input);
+
+ self::assertInstanceOf(QueryResponse::class, $result);
+ self::assertFalse($result->info()['resolved']);
+ }
+}