diff --git a/.gitignore b/.gitignore index febba79..7f71e30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.idea /vendor /build +.phpunit.result.cache \ No newline at end of file diff --git a/README.md b/README.md index bee7f9b..35fa70a 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Loop::run(function () { foreach ($result as $row) { printf("The keyspace %s has a table called %s\n", $row['keyspace_name'], $row['columnfamily_name']); } + + yield $cluster->disconnect(); }); ``` diff --git a/benchmark/shared.php b/benchmark/shared.php new file mode 100644 index 0000000..e7e89e4 --- /dev/null +++ b/benchmark/shared.php @@ -0,0 +1,46 @@ +, + post_id timeuuid, + text text, + date timestamp, + tags set, + PRIMARY KEY ((author), post_id) + ) WITH CLUSTERING ORDER BY (post_id DESC);", +]; diff --git a/benchmark/simple.php b/benchmark/simple.php deleted file mode 100644 index 9004482..0000000 --- a/benchmark/simple.php +++ /dev/null @@ -1,19 +0,0 @@ -connect(); -}); diff --git a/benchmark/write.php b/benchmark/write.php new file mode 100644 index 0000000..0c9886d --- /dev/null +++ b/benchmark/write.php @@ -0,0 +1,69 @@ +connect(); + $setup = require __DIR__ . '/shared.php'; + + $watcher = Loop::onSignal(SIGTERM, function () use ($cluster) { + yield $cluster->disconnect(); + }); + + try { + foreach ($setup as $query) { + yield $session->query($query); + } + + $time = \microtime(true); + $count = $argv[1] ?? 1000; + $promises = []; + + for ($i = 1; $i <= $count; $i++) { + $author = new Value\UserDefined([ + 'id' => $i, + 'name' => "User $i", + 'enabled' => (bool) ($i % 2), + ]); + + $arguments = [ + 'author' => $author, + 'post_id' => Uuid::uuid1(), + 'text' => random_string(500), + 'date' => Value\Timestamp::fromDateTime(random_date()), + 'tags' => Value\Collection::set(random_tags(\rand(1, 10), 5)), + ]; + + $fields = \implode(',', \array_keys($arguments)); + $values = \implode(',', \array_fill(0, \count($arguments), '?')); + + $promises[] = $session->query("INSERT INTO posts_by_user ($fields) VALUES ($values)", $arguments); + } + + yield $promises; + + echo \sprintf("Done %d inserts in %f seconds.\n", $count, \microtime(true) - $time); + } catch (\Throwable $error) { + echo "Got error: {$error->getMessage()}.\n"; + } finally { + yield $session->query("DROP KEYSPACE IF EXISTS blogs;"); + } + + yield $cluster->disconnect(); + + Loop::cancel($watcher); +}); diff --git a/composer.json b/composer.json index fa9dc37..f15fd14 100644 --- a/composer.json +++ b/composer.json @@ -18,18 +18,26 @@ "require": { "php": "^7.2", "amphp/amp": "^2.0", - "amphp/socket": "^0.10.11", - "phpinnacle/buffer": "^0.1.1", - "ramsey/uuid": "^3.8" + "amphp/socket": "^0.10", + "phpinnacle/buffer": "^0.1" }, "require-dev": { - "phpunit/phpunit": "^6.0", - "amphp/phpunit-util": "^1.0" + "phpunit/phpunit": "^8.0", + "ramsey/uuid": "^3.8" + }, + "suggest": { + "ramsey/uuid": "For use Uuid and Timeuuid Cassandra types", + "ext-gmp": "For use Varint and Decimal Cassandra types", + "ext-snappy": "For use Google Snappy compression mechanism", + "ext-lz4": "For use LZ4 compression mechanism" }, "autoload": { "psr-4": { "PHPinnacle\\Cassis\\": "src" - } + }, + "files": [ + "src/functions.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/composer.lock b/composer.lock index e63f0c2..aefed34 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b77e94c24b9f1a89139f7a6a9ee2a560", + "content-hash": "05a0814d680da9cb94c07e240b86c38c", "packages": [ { "name": "amphp/amp", @@ -189,33 +189,31 @@ }, { "name": "amphp/dns", - "version": "v0.9.13", + "version": "v0.9.14", "source": { "type": "git", "url": "https://github.com/amphp/dns.git", - "reference": "4647e5f58263ffdeff7da5c269f517cb48cff84f" + "reference": "1ccd6337f72107ba422ae72a5812687739d214c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/dns/zipball/4647e5f58263ffdeff7da5c269f517cb48cff84f", - "reference": "4647e5f58263ffdeff7da5c269f517cb48cff84f", + "url": "https://api.github.com/repos/amphp/dns/zipball/1ccd6337f72107ba422ae72a5812687739d214c8", + "reference": "1ccd6337f72107ba422ae72a5812687739d214c8", "shasum": "" }, "require": { "amphp/amp": "^2", "amphp/byte-stream": "^1.1", "amphp/cache": "^1.2", - "amphp/file": "^0.2 || ^0.3", "amphp/parser": "^1", - "amphp/uri": "^0.1", "amphp/windows-registry": "^0.3", "daverandom/libdns": "^2.0.1", "ext-filter": "*", "php": ">=7.0" }, "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", "amphp/phpunit-util": "^1", - "friendsofphp/php-cs-fixer": "^2.3", "phpunit/phpunit": "^6" }, "type": "library", @@ -263,136 +261,7 @@ "dns", "resolve" ], - "time": "2018-05-01T18:08:54+00:00" - }, - { - "name": "amphp/file", - "version": "v0.3.3", - "source": { - "type": "git", - "url": "https://github.com/amphp/file.git", - "reference": "25d8ef6e67b95d5249e0af7a80adce77657d16bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/file/zipball/25d8ef6e67b95d5249e0af7a80adce77657d16bb", - "reference": "25d8ef6e67b95d5249e0af7a80adce77657d16bb", - "shasum": "" - }, - "require": { - "amphp/amp": "^2", - "amphp/byte-stream": "^1", - "amphp/parallel": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", - "phpunit/phpunit": "^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\File\\": "lib" - }, - "files": [ - "lib/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "Allows non-blocking access to the filesystem for Amp.", - "homepage": "https://github.com/amphp/file", - "keywords": [ - "amp", - "amphp", - "async", - "disk", - "file", - "filesystem", - "io", - "non-blocking", - "static" - ], - "time": "2018-10-28T14:38:35+00:00" - }, - { - "name": "amphp/parallel", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/parallel.git", - "reference": "687776dc6933af4c6009ac58f915faf45e8463ce" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/687776dc6933af4c6009ac58f915faf45e8463ce", - "reference": "687776dc6933af4c6009ac58f915faf45e8463ce", - "shasum": "" - }, - "require": { - "amphp/amp": "^2", - "amphp/byte-stream": "^1.5", - "amphp/parser": "^1", - "amphp/process": "^1", - "amphp/sync": "^1.0.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", - "phpunit/phpunit": "^6" - }, - "suggest": { - "ext-pthreads": "Required for thread contexts" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Parallel\\": "lib" - }, - "files": [ - "lib/Worker/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Stephen Coakley", - "email": "me@stephencoakley.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "Parallel processing component for Amp.", - "homepage": "https://github.com/amphp/parallel", - "keywords": [ - "async", - "asynchronous", - "concurrent", - "multi-processing", - "multi-threading" - ], - "time": "2019-01-09T21:31:46+00:00" + "time": "2019-01-25T04:12:31+00:00" }, { "name": "amphp/parser", @@ -567,62 +436,6 @@ ], "time": "2018-10-17T16:53:02+00:00" }, - { - "name": "amphp/sync", - "version": "v1.0.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/sync.git", - "reference": "a1d8f244eb19e3e2a96abc4686cebc80995bbc90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/sync/zipball/a1d8f244eb19e3e2a96abc4686cebc80995bbc90", - "reference": "a1d8f244eb19e3e2a96abc4686cebc80995bbc90", - "shasum": "" - }, - "require": { - "amphp/amp": "^2" - }, - "require-dev": { - "amphp/phpunit-util": "^1", - "friendsofphp/php-cs-fixer": "^2.3", - "phpunit/phpunit": "^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Sync\\": "lib" - }, - "files": [ - "lib/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Stephen Coakley", - "email": "me@stephencoakley.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "Mutex, Semaphore, and other synchronization tools for Amp.", - "homepage": "https://github.com/amphp/sync", - "keywords": [ - "async", - "asynchronous", - "mutex", - "semaphore", - "synchronization" - ], - "time": "2017-11-29T21:48:53+00:00" - }, { "name": "amphp/uri", "version": "v0.1.3", @@ -746,274 +559,37 @@ "keywords": [ "dns" ], - "time": "2018-01-10T15:56:17+00:00" - }, - { - "name": "paragonie/random_compat", - "version": "v9.99.99", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", - "shasum": "" - }, - "require": { - "php": "^7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "time": "2018-07-02T15:55:56+00:00" - }, - { - "name": "phpinnacle/buffer", - "version": "0.1.2", - "source": { - "type": "git", - "url": "https://github.com/phpinnacle/buffer.git", - "reference": "a3b662cc90ac9ca81219626f8d2e7a15f096bc60" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpinnacle/buffer/zipball/a3b662cc90ac9ca81219626f8d2e7a15f096bc60", - "reference": "a3b662cc90ac9ca81219626f8d2e7a15f096bc60", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "PHPinnacle\\Buffer\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHPinnacle", - "email": "dev@phpinnacle.com", - "homepage": "https://phpinnacle.com", - "role": "Developer" - } - ], - "description": "PHPinnacle binary buffer implementation", - "homepage": "https://github.com/phpinnacle/buffer", - "keywords": [ - "Buffer", - "binary", - "binary data", - "pack", - "phpinnacle", - "unpack" - ], - "time": "2019-01-16T13:38:21+00:00" - }, - { - "name": "ramsey/uuid", - "version": "3.8.0", - "source": { - "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/d09ea80159c1929d75b3f9c60504d613aeb4a1e3", - "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3", - "shasum": "" - }, - "require": { - "paragonie/random_compat": "^1.0|^2.0|9.99.99", - "php": "^5.4 || ^7.0", - "symfony/polyfill-ctype": "^1.8" - }, - "replace": { - "rhumsaa/uuid": "self.version" - }, - "require-dev": { - "codeception/aspect-mock": "^1.0 | ~2.0.0", - "doctrine/annotations": "~1.2.0", - "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ~2.1.0", - "ircmaxell/random-lib": "^1.1", - "jakub-onderka/php-parallel-lint": "^0.9.0", - "mockery/mockery": "^0.9.9", - "moontoast/math": "^1.1", - "php-mock/php-mock-phpunit": "^0.3|^1.1", - "phpunit/phpunit": "^4.7|^5.0|^6.5", - "squizlabs/php_codesniffer": "^2.3" - }, - "suggest": { - "ext-ctype": "Provides support for PHP Ctype functions", - "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", - "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", - "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", - "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", - "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", - "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Ramsey\\Uuid\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marijn Huizendveld", - "email": "marijn.huizendveld@gmail.com" - }, - { - "name": "Thibaud Fabre", - "email": "thibaud@aztech.io" - }, - { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" - } - ], - "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", - "homepage": "https://github.com/ramsey/uuid", - "keywords": [ - "guid", - "identifier", - "uuid" - ], - "time": "2018-07-19T23:38:55+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.10.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.9-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, - { - "name": "Gert de Pagter", - "email": "backendtea@gmail.com" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "time": "2018-08-06T14:22:27+00:00" - } - ], - "packages-dev": [ + "time": "2018-01-10T15:56:17+00:00" + }, { - "name": "amphp/phpunit-util", - "version": "v1.0.0", + "name": "phpinnacle/buffer", + "version": "0.1.2", "source": { "type": "git", - "url": "https://github.com/amphp/phpunit-util.git", - "reference": "1194636f5f1796d03e4e59c219cd3abfffe40eb8" + "url": "https://github.com/phpinnacle/buffer.git", + "reference": "a3b662cc90ac9ca81219626f8d2e7a15f096bc60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/phpunit-util/zipball/1194636f5f1796d03e4e59c219cd3abfffe40eb8", - "reference": "1194636f5f1796d03e4e59c219cd3abfffe40eb8", + "url": "https://api.github.com/repos/phpinnacle/buffer/zipball/a3b662cc90ac9ca81219626f8d2e7a15f096bc60", + "reference": "a3b662cc90ac9ca81219626f8d2e7a15f096bc60", "shasum": "" }, "require": { - "phpunit/phpunit": "^6" + "php": "^7.1" }, "require-dev": { - "amphp/amp": "^2" + "phpunit/phpunit": "^6.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, "autoload": { "psr-4": { - "Amp\\PHPUnit\\": "src" + "PHPinnacle\\Buffer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1022,14 +598,26 @@ ], "authors": [ { - "name": "Niklas Keller", - "email": "me@kelunik.com" + "name": "PHPinnacle", + "email": "dev@phpinnacle.com", + "homepage": "https://phpinnacle.com", + "role": "Developer" } ], - "description": "Helper package to ease testing with PHPUnit.", - "homepage": "http://amphp.org/phpunit-util", - "time": "2017-06-15T15:43:56+00:00" - }, + "description": "PHPinnacle binary buffer implementation", + "homepage": "https://github.com/phpinnacle/buffer", + "keywords": [ + "Buffer", + "binary", + "binary data", + "pack", + "phpinnacle", + "unpack" + ], + "time": "2019-01-16T13:38:21+00:00" + } + ], + "packages-dev": [ { "name": "doctrine/instantiator", "version": "1.1.0", @@ -1132,24 +720,69 @@ ], "time": "2018-06-11T23:09:50+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.99", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "shasum": "" + }, + "require": { + "php": "^7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "time": "2018-07-02T15:55:56+00:00" + }, { "name": "phar-io/manifest", - "version": "1.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", "shasum": "" }, "require": { "ext-dom": "*", "ext-phar": "*", - "phar-io/version": "^1.0.1", + "phar-io/version": "^2.0", "php": "^5.6 || ^7.0" }, "type": "library", @@ -1185,20 +818,20 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2017-03-05T18:14:27+00:00" + "time": "2018-07-08T19:23:20+00:00" }, { "name": "phar-io/version", - "version": "1.0.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", "shasum": "" }, "require": { @@ -1232,7 +865,7 @@ } ], "description": "Library for handling version information and constraints", - "time": "2017-03-05T17:38:23+00:00" + "time": "2018-07-08T19:19:57+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -1451,40 +1084,40 @@ }, { "name": "phpunit/php-code-coverage", - "version": "5.3.2", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac" + "reference": "cfca9c5f7f2694ca0c7749ffb142927d9f05250f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/cfca9c5f7f2694ca0c7749ffb142927d9f05250f", + "reference": "cfca9c5f7f2694ca0c7749ffb142927d9f05250f", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.0", - "phpunit/php-file-iterator": "^1.4.2", + "php": "^7.2", + "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^2.0.1", + "phpunit/php-token-stream": "^3.0.1", "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.0", + "sebastian/environment": "^4.1", "sebastian/version": "^2.0.1", "theseer/tokenizer": "^1.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^8.0" }, "suggest": { - "ext-xdebug": "^2.5.5" + "ext-xdebug": "^2.6.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.3.x-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -1510,29 +1143,32 @@ "testing", "xunit" ], - "time": "2018-04-06T15:36:58+00:00" + "time": "2019-02-15T13:40:27+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.5", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + "reference": "050bedf145a257b1ff02746c31894800e5122946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", + "reference": "050bedf145a257b1ff02746c31894800e5122946", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -1547,7 +1183,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -1557,7 +1193,7 @@ "filesystem", "iterator" ], - "time": "2017-11-27T13:52:08+00:00" + "time": "2018-09-13T20:33:42+00:00" }, { "name": "phpunit/php-text-template", @@ -1602,28 +1238,28 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.9", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + "reference": "8b389aebe1b8b0578430bda0c7c95a829608e059" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b389aebe1b8b0578430bda0c7c95a829608e059", + "reference": "8b389aebe1b8b0578430bda0c7c95a829608e059", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -1638,7 +1274,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -1647,33 +1283,33 @@ "keywords": [ "timer" ], - "time": "2017-02-26T11:10:40+00:00" + "time": "2019-02-20T10:12:59+00:00" }, { "name": "phpunit/php-token-stream", - "version": "2.0.2", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "791198a2c6254db10131eecfe8c06670700904db" + "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", - "reference": "791198a2c6254db10131eecfe8c06670700904db", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18", + "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2.4" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1696,57 +1332,55 @@ "keywords": [ "tokenizer" ], - "time": "2017-11-27T05:48:46+00:00" + "time": "2018-10-30T05:52:18+00:00" }, { "name": "phpunit/phpunit", - "version": "6.5.13", + "version": "8.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693" + "reference": "a7af0201285445c9c73c4bdf869c486e36b41604" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693", - "reference": "0973426fb012359b2f18d3bd1e90ef1172839693", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a7af0201285445c9c73c4bdf869c486e36b41604", + "reference": "a7af0201285445c9c73c4bdf869c486e36b41604", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "^1.6.1", - "phar-io/manifest": "^1.0.1", - "phar-io/version": "^1.0", - "php": "^7.0", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.2", "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.3", - "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-code-coverage": "^7.0", + "phpunit/php-file-iterator": "^2.0.1", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.9", - "sebastian/comparator": "^2.1", - "sebastian/diff": "^2.0", - "sebastian/environment": "^3.1", + "phpunit/php-timer": "^2.0", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.1", "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", + "sebastian/global-state": "^3.0", "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^1.0", + "sebastian/resource-operations": "^2.0", "sebastian/version": "^2.0.1" }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2", - "phpunit/dbunit": "<3.0" - }, "require-dev": { "ext-pdo": "*" }, "suggest": { + "ext-soap": "*", "ext-xdebug": "*", - "phpunit/php-invoker": "^1.1" + "phpunit/php-invoker": "^2.0" }, "bin": [ "phpunit" @@ -1754,7 +1388,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.5.x-dev" + "dev-master": "8.0-dev" } }, "autoload": { @@ -1780,66 +1414,89 @@ "testing", "xunit" ], - "time": "2018-09-08T15:10:43+00:00" + "time": "2019-02-18T09:23:05+00:00" }, { - "name": "phpunit/phpunit-mock-objects", - "version": "5.0.10", + "name": "ramsey/uuid", + "version": "3.8.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" + "url": "https://github.com/ramsey/uuid.git", + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", - "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/d09ea80159c1929d75b3f9c60504d613aeb4a1e3", + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.5", - "php": "^7.0", - "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.1" + "paragonie/random_compat": "^1.0|^2.0|9.99.99", + "php": "^5.4 || ^7.0", + "symfony/polyfill-ctype": "^1.8" }, - "conflict": { - "phpunit/phpunit": "<6.0" + "replace": { + "rhumsaa/uuid": "self.version" }, "require-dev": { - "phpunit/phpunit": "^6.5.11" + "codeception/aspect-mock": "^1.0 | ~2.0.0", + "doctrine/annotations": "~1.2.0", + "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ~2.1.0", + "ircmaxell/random-lib": "^1.1", + "jakub-onderka/php-parallel-lint": "^0.9.0", + "mockery/mockery": "^0.9.9", + "moontoast/math": "^1.1", + "php-mock/php-mock-phpunit": "^0.3|^1.1", + "phpunit/phpunit": "^4.7|^5.0|^6.5", + "squizlabs/php_codesniffer": "^2.3" }, "suggest": { - "ext-soap": "*" + "ext-ctype": "Provides support for PHP Ctype functions", + "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", + "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", + "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Marijn Huizendveld", + "email": "marijn.huizendveld@gmail.com" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io" + }, + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" } ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "homepage": "https://github.com/ramsey/uuid", "keywords": [ - "mock", - "xunit" + "guid", + "identifier", + "uuid" ], - "time": "2018-08-09T05:50:03+00:00" + "time": "2018-07-19T23:38:55+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1888,30 +1545,30 @@ }, { "name": "sebastian/comparator", - "version": "2.1.3", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", "shasum": "" }, "require": { - "php": "^7.0", - "sebastian/diff": "^2.0 || ^3.0", + "php": "^7.1", + "sebastian/diff": "^3.0", "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1948,32 +1605,33 @@ "compare", "equality" ], - "time": "2018-02-01T13:46:46+00:00" + "time": "2018-07-12T15:12:46+00:00" }, { "name": "sebastian/diff", - "version": "2.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.2" + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1998,34 +1656,40 @@ "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "diff" + "diff", + "udiff", + "unidiff", + "unified diff" ], - "time": "2017-08-03T08:09:46+00:00" + "time": "2019-02-04T06:01:07+00:00" }, { "name": "sebastian/environment", - "version": "3.1.0", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + "reference": "6fda8ce1974b62b14935adc02a9ed38252eca656" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6fda8ce1974b62b14935adc02a9ed38252eca656", + "reference": "6fda8ce1974b62b14935adc02a9ed38252eca656", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.1" + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -2050,7 +1714,7 @@ "environment", "hhvm" ], - "time": "2017-07-01T08:51:00+00:00" + "time": "2019-02-01T05:27:49+00:00" }, { "name": "sebastian/exporter", @@ -2121,23 +1785,26 @@ }, { "name": "sebastian/global-state", - "version": "2.0.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.2", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "ext-dom": "*", + "phpunit/phpunit": "^8.0" }, "suggest": { "ext-uopz": "*" @@ -2145,7 +1812,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2168,7 +1835,7 @@ "keywords": [ "global state" ], - "time": "2017-04-27T15:39:26+00:00" + "time": "2019-02-01T05:30:01+00:00" }, { "name": "sebastian/object-enumerator", @@ -2317,25 +1984,25 @@ }, { "name": "sebastian/resource-operations", - "version": "1.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", "shasum": "" }, "require": { - "php": ">=5.6.0" + "php": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2355,7 +2022,7 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28T20:34:47+00:00" + "time": "2018-10-04T04:07:39+00:00" }, { "name": "sebastian/version", @@ -2400,6 +2067,64 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2016-10-03T07:35:21+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "backendtea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" + }, { "name": "theseer/tokenizer", "version": "1.1.0", diff --git a/examples/basic.php b/examples/basic.php index bd192e2..fcac06e 100644 --- a/examples/basic.php +++ b/examples/basic.php @@ -15,10 +15,12 @@ $cluster = Cluster::build(\getenv('CASSIS_EXAMPLE_DSN')); /** @var Session $session */ - $session = yield $cluster->connect('system'); - $result = yield $session->query('SELECT keyspace_name, columnfamily_name FROM schema_columnfamilies'); + $session = yield $cluster->connect('system_schema'); + $result = yield $session->query('SELECT keyspace_name, table_name FROM tables'); foreach ($result as $row) { - printf("The keyspace %s has a table called %s\n", $row['keyspace_name'], $row['columnfamily_name']); + \printf("The keyspace %s has a table called %s\n", $row['keyspace_name'], $row['table_name']); } + + yield $cluster->disconnect(); }); diff --git a/phpunit.xml.dist b/phpunit.xml similarity index 65% rename from phpunit.xml.dist rename to phpunit.xml index 351de21..d1408ea 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml @@ -2,13 +2,19 @@ + + + + tests @@ -16,17 +22,14 @@ - src/ + src - - - - + - + diff --git a/src/Buffer.php b/src/Buffer.php index 02bc6c5..a37bb52 100644 --- a/src/Buffer.php +++ b/src/Buffer.php @@ -13,6 +13,7 @@ namespace PHPinnacle\Cassis; use PHPinnacle\Buffer\Binary; +use Ramsey\Uuid\UuidInterface; final class Buffer { @@ -119,6 +120,47 @@ public function consumeByte(): int return $this->data->consumeUint8(); } + /** + * @param array $bytes + * + * @return self + */ + public function appendBytes(array $bytes): self + { + $this->appendInt(\count($bytes)); + + foreach ($bytes as $byte) { + $this->appendByte($byte); + } + + return $this; + } + + /** + * @param int $n + * + * @return int[] + */ + public function consumeBytes(int $n): array + { + return \array_values(\unpack('C*', $this->consume($n))); + } + + /** + * @return int[] + */ + public function consumeBytesMap(): array + { + $count = $this->consumeShort(); + $bytes = []; + + for ($i = 0; $i < $count; ++$i) { + $bytes[$this->consumeString()] = $this->consumeBytes($this->consumeInt()); + } + + return $bytes; + } + /** * @param int $value * @@ -126,7 +168,7 @@ public function consumeByte(): int */ public function appendShort(int $value): self { - $this->data->appendUint16($value); + $this->data->appendUint16(abs($value)); return $this; } @@ -246,7 +288,7 @@ public function consumeInt(): int */ public function appendUint(int $value): self { - $this->data->appendUint32($value); + $this->data->appendUint32(abs($value)); return $this; } @@ -433,152 +475,39 @@ public function appendStringList(array $values): self foreach ($values as $value) { $this->appendString($value); } - - return $this; - } - - /** - * @param array $values - * - * @return self - */ - public function appendStringMap(array $values): self - { - $this->appendShort(\count($values)); - - foreach ($values as $key => $value) { - $this->appendString($key); - $this->appendString($value); - } - - return $this; - } - - /** - * @param array $values - * - * @return self - */ - public function appendStringMultiMap(array $values): self - { - $this->appendShort(\count($values)); - - foreach ($values as $key => $value) { - $this->appendString($key); - $this->appendStringList($value); - } return $this; } - - /** - * @param int $id - * @param mixed $value - * - * @return self - */ - public function appendOption(int $id, $value): self - { - return $this - ->appendShort($id) - ->append($value) - ; - } - - /** - * @param string[] $values - * - * @return self - */ - public function appendOptionList(array $values): self - { - $this->appendShort(\count($values)); - - foreach ($values as $id => $value) { - $this->appendOption($id, $value); - } - - return $this; - } - - /** - * @param string $value - * - * @return self - */ - public function appendBytes(string $value): self - { - return $this - ->appendInt(\strlen($value)) - ->append($value) - ; - } - - /** - * @param int $offset - * - * @return string - */ - public function readBytes(int $offset = 0): string - { - return $this->data->read($this->readInt($offset), $offset + 4); - } /** - * @return string + * @return string[] */ - public function consumeBytes(): ?string + public function consumeStringList(): array { - $n = $this->consumeInt(); - - return $n < 0 ? null : $this->data->consume($n); - } + $values = []; + $count = $this->consumeShort(); - /** - * @param mixed $value - * - * @return self - */ - public function appendShortBytes(string $value): self - { - return $this - ->appendShort(\strlen($value)) - ->append($value) - ; - } - - /** - * @param int $offset - * - * @return string - */ - public function readShortBytes(int $offset = 0): string - { - return $this->data->read($this->readShort($offset), $offset + 2); - } + for ($i = 0; $i < $count; ++$i) { + $values[] = $this->consumeString(); + } - /** - * @return string - */ - public function consumeShortBytes(): string - { - return $this->data->consume($this->consumeShort()); + return $values; } - + /** * @param array $values * * @return self */ - public function appendBytesMap(array $values): self + public function appendStringMap(array $values): self { $this->appendShort(\count($values)); - + foreach ($values as $key => $value) { $this->appendString($key); - $this->appendBytes($value); + $this->appendString($value); } - + return $this; } @@ -599,7 +528,10 @@ public function appendValue($value): self break; case \is_bool($value): - $this->appendByte($value ? 1 : 0); + $this + ->appendInt(1) + ->appendByte($value ? 1 : 0) + ; break; case \is_int($value): @@ -617,7 +549,27 @@ public function appendValue($value): self break; case \is_string($value): - $this->appendLongString($value); + $this + ->appendInt(\strlen($value)) + ->append($value) + ; + + break; + case \is_array($value): + if (\is_assoc($value)) { + $list = Value\Collection::assoc($value); + } else { + $list = Value\Collection::list($value); + } + + $list->write($this); + + break; + case $value instanceof UuidInterface: + $this + ->appendInt(16) + ->append($value->getBytes()) + ; break; case $value instanceof \DateTimeInterface: @@ -667,115 +619,6 @@ public function appendValuesMap(array $values): self return $this; } - /** - * @param Type $type - * - * @return null|bool|float|int|string|Value - */ - public function consumeValue(Type $type) - { - $value = null; - $length = $this->consumeInt(); - - if ($length < 0) { - return $value; - } - - switch ($type->code()) { - case Type::CUSTOM: - $value = $this->consumeString(); - break; - case Type::ASCII: - case Type::VARCHAR: - case Type::TEXT: - case Type::BLOB: - $value = $this->consume($length); - break; - case Type::BOOLEAN: - $value = (bool) $this->consumeByte(); - break; - case Type::SMALLINT: - $value = $this->consumeSmallInt(); - break; - case Type::TINYINT: - $value = $this->consumeTinyInt(); - break; - case Type::INT: - $value = $this->consumeInt(); - break; - case Type::BIGINT: - $value = $this->consumeLong(); - break; - case Type::FLOAT: - $value = $this->consumeFloat(); - break; - case Type::DOUBLE: - $value = Value\Double::read($this)->value(); - break; - case Type::DECIMAL: - $value = Value\Decimal::read($this)->value(); - break; - case Type::DATE: - $value = Value\Date::read($this); - break; - case Type::TIME: - $value = Value\Time::read($this); - break; - case Type::TIMESTAMP: - $value = Value\Timestamp::read($this); - break; - case Type::INET: - $value = $length === 4 ? Value\Inet::readV4($this) : Value\Inet::readV6($this); - break; - case Type::UUID: - $value = Value\Uuid::read($this); - break; - case Type::COUNTER: - $value = Value\Counter::read($this); - break; - case Type::TIMEUUID: - $value = Value\Timeuuid::read($this); - break; - case Type::COLLECTION_LIST: - case Type::COLLECTION_SET: - $value = []; - $count = $this->consumeInt(); - - for ($i = 0; $i < $count; ++$i) { - /** @var Type\Collection|Type\Set $type */ - $value[] = $this->consumeValue($type->value()); - } - - break; - case Type::COLLECTION_MAP: - $value = []; - $count = $this->consumeInt(); - - /** @var Type\Map $type */ - [$keyType, $valueType] = [$type->key(), $type->value()]; - - for ($i = 0; $i < $count; ++$i) { - $value[$this->consumeValue($keyType)] = $this->consumeValue($valueType); - } - - break; - case Type::UDT: - case Type::TUPLE: - $value = []; - - /** @var Type\Tuple|Type\UserDefined $type */ - foreach ($type->definitions() as $key => $type) { - $value[$key] = $this->consumeValue($type); - } - - break; - default: - throw new Exception\ClientException('Unknown type.'); - } - - return $value; - } - /** * @return Type */ @@ -786,12 +629,12 @@ public function consumeType(): Type switch ($type) { case Type::CUSTOM: return new Type\Custom($this->consumeString()); - case Type::COLLECTION_LIST: - return new Type\Collection($this->consumeType()); - case Type::COLLECTION_SET: - return new Type\Set($this->consumeType()); - case Type::COLLECTION_MAP: - return new Type\Map($this->consumeType(), $this->consumeType()); + case Type::LIST: + return Type\Collection::list($this->consumeType()); + case Type::SET: + return Type\Collection::set($this->consumeType()); + case Type::MAP: + return Type\Collection::map($this->consumeType(), $this->consumeType()); case Type::UDT: $keyspace = $this->consumeString(); $name = $this->consumeString(); diff --git a/src/Cluster.php b/src/Cluster.php index 41cfdc0..96d44af 100644 --- a/src/Cluster.php +++ b/src/Cluster.php @@ -14,6 +14,7 @@ use function Amp\call; use Amp\Promise; +use Amp\Socket; final class Cluster { @@ -30,14 +31,14 @@ final class Cluster private $config; /** - * @var int + * @var Streams */ - private $state = self::STATE_NOT_CONNECTED; + private $streams; /** - * @var Session[] + * @var int */ - private $sessions = []; + private $state = self::STATE_NOT_CONNECTED; /** * @var Connection @@ -49,7 +50,8 @@ final class Cluster */ public function __construct(Config $config) { - $this->config = $config; + $this->config = $config; + $this->streams = Streams::instance(); } /** @@ -62,6 +64,23 @@ public static function build(string $dsn): self return new self(Config::parse($dsn)); } + /** + * @return Promise + */ + public function options(): Promise + { + return call(function () { + if ($this->connection === null) { + $this->connection = yield $this->open(); + } + + /** @var Response\Supported $response */ + $response = yield $this->connection->send(new Request\Options); + + return $response->options; + }); + } + /** * @param string $keyspace * @@ -71,22 +90,16 @@ public function connect(string $keyspace = null): Promise { return call(function () use ($keyspace) { if ($this->state !== self::STATE_NOT_CONNECTED) { - throw new \RuntimeException('Client already connected/connecting'); + throw Exception\ClientException::alreadyConnected(); } $this->state = self::STATE_CONNECTING; - $this->connection = new Connection($this->config->uri()); - - yield $this->connection->open( - $this->config->tcpTimeout(), - $this->config->tcpAttempts(), - $this->config->tcpNoDelay() - ); - - yield $this->connection->send(new Request\Startup($this->config->options())); + if (null === $this->connection) { + $this->connection = yield $this->open(); + } - $frame = yield $this->connection->await(0); + $frame = yield $this->connection->send(new Request\Startup($this->config->options())); if ($frame instanceof Response\Authenticate) { yield $this->authenticate(); @@ -95,7 +108,7 @@ public function connect(string $keyspace = null): Promise $session = new Session($this->connection); if ($keyspace !== null) { - yield $session->execute(new Statement\Simple("USE {$keyspace}")); + yield $session->keyspace($keyspace); } $this->state = self::STATE_CONNECTED; @@ -113,28 +126,16 @@ public function connect(string $keyspace = null): Promise public function disconnect(int $code = 0, string $reason = ''): Promise { return call(function() use ($code, $reason) { - if (\in_array($this->state, [self::STATE_NOT_CONNECTED, self::STATE_DISCONNECTING])) { + if ($this->state === self::STATE_DISCONNECTING) { return; } - if ($this->state !== self::STATE_CONNECTED) { - throw new Exception\ClientException('Client is not connected'); - } - $this->state = self::STATE_DISCONNECTING; - if ($code === 0) { - $promises = []; - - foreach($this->sessions as $id => $session) { - $promises[] = $session->close($code, $reason); - } - - yield $promises; + if ($this->connection !== null) { + $this->connection->close(); } - $this->connection->close(); - $this->state = self::STATE_NOT_CONNECTED; }); } @@ -150,15 +151,53 @@ public function isConnected(): bool /** * @return Promise */ - private function authenticate(): Promise + private function open(): Promise { return call(function () { - yield $this->connection->send(new Request\AuthResponse( - $this->config->user(), - $this->config->password() - )); + $compressor = $this->detectCompressor(); + + foreach ($this->config->hosts() as $host) { + $connection = new Connection($host, $this->streams, $compressor); - yield $this->connection->await(0); + try { + yield $connection->open( + $this->config->tcpTimeout(), + $this->config->tcpAttempts(), + $this->config->tcpNoDelay() + ); + + return $connection; + } catch (Socket\ConnectException $error) { + continue; + } + } + + throw Exception\ClientException::couldNotConnect(); }); } + + /** + * @return Promise + */ + private function authenticate(): Promise + { + return $this->connection->send(new Request\AuthResponse($this->config->user(), $this->config->password())); + } + + /** + * @return Compressor + */ + private function detectCompressor(): Compressor + { + switch ($this->config->compression()) { + case Config::COMPRESSION_NONE: + return new Compressor\NoneCompressor; + case Config::COMPRESSION_LZ4: + return new Compressor\LzCompressor; + case Config::COMPRESSION_SNAPPY: + return new Compressor\SnappyCompressor; + default: + throw Exception\ConfigException::unknownCompressionMechanism($this->config->compression()); + } + } } diff --git a/src/Column.php b/src/Column.php index 4b49a0c..48f6dde 100644 --- a/src/Column.php +++ b/src/Column.php @@ -46,6 +46,33 @@ public function __construct(string $keyspace, string $table, string $name, Type $this->type = $type; } + /** + * @param Buffer $buffer + * + * @return self + */ + public static function full(Buffer $buffer): self + { + return new self( + $buffer->consumeString(), + $buffer->consumeString(), + $buffer->consumeString(), + $buffer->consumeType() + ); + } + + /** + * @param string $keyspace + * @param string $table + * @param Buffer $buffer + * + * @return self + */ + public static function partial(string $keyspace, string $table, Buffer $buffer): self + { + return new self($keyspace, $table, $buffer->consumeString(), $buffer->consumeType()); + } + /** * @return string */ @@ -77,4 +104,14 @@ public function type(): Type { return $this->type; } + + /** + * @param Buffer $buffer + * + * @return mixed + */ + public function value(Buffer $buffer) + { + return $this->type->read($buffer); + } } diff --git a/src/Compressor.php b/src/Compressor.php new file mode 100644 index 0000000..c46c9df --- /dev/null +++ b/src/Compressor.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis; + +interface Compressor +{ + /** + * @param string $data + * + * @return string + */ + public function compress(string $data): string; + + /** + * @param string $binary + * + * @return string + */ + public function decompress(string $binary): string; +} diff --git a/src/Compressor/LzCompressor.php b/src/Compressor/LzCompressor.php new file mode 100644 index 0000000..c65e46b --- /dev/null +++ b/src/Compressor/LzCompressor.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Compressor; + +use PHPinnacle\Cassis\Compressor; + +class LzCompressor implements Compressor +{ + /** + * {@inheritdoc} + */ + public function compress(string $data): string + { + return lz4_compress($data); + } + + /** + * {@inheritdoc} + */ + public function decompress(string $binary): string + { + return lz4_uncompress($binary); + } +} diff --git a/src/Compressor/NoneCompressor.php b/src/Compressor/NoneCompressor.php new file mode 100644 index 0000000..0aceeea --- /dev/null +++ b/src/Compressor/NoneCompressor.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Compressor; + +use PHPinnacle\Cassis\Compressor; + +class NoneCompressor implements Compressor +{ + /** + * {@inheritdoc} + */ + public function compress(string $data): string + { + return $data; + } + + /** + * {@inheritdoc} + */ + public function decompress(string $binary): string + { + return $binary; + } +} diff --git a/src/Compressor/SnappyCompressor.php b/src/Compressor/SnappyCompressor.php new file mode 100644 index 0000000..16f6f3d --- /dev/null +++ b/src/Compressor/SnappyCompressor.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Compressor; + +use PHPinnacle\Cassis\Compressor; + +class SnappyCompressor implements Compressor +{ + /** + * {@inheritdoc} + */ + public function compress(string $data): string + { + return snappy_compress($data); + } + + /** + * {@inheritdoc} + */ + public function decompress(string $binary): string + { + return snappy_uncompress($binary); + } +} diff --git a/src/Config.php b/src/Config.php index eec4fec..f014d48 100644 --- a/src/Config.php +++ b/src/Config.php @@ -15,37 +15,23 @@ final class Config { private const - DEFAULT_HOST = 'localhost', - DEFAULT_PORT = 9042, - DEFAULT_KEYSPACE = null, - DEFAULT_USER = null, - DEFAULT_PASS = null + DEFAULT_SCHEME = 'tcp', + DEFAULT_HOST = 'localhost', + DEFAULT_PORT = 9042 ; - private const OPTIONS = [ - 'compression' => null, - 'compatibility' => false, - 'tcp_timeout' => 1000, - 'tcp_nodelay' => false, - 'tcp_attempts' => 2, - ]; - - private const + private const CQL_VERSION = '3.0.0'; + + const + COMPRESSION_NONE = 'none', COMPRESSION_LZ4 = 'lz4', COMPRESSION_SNAPPY = 'snappy' ; - private const CQL_VERSION = '3.0.0'; - - /** - * @var string - */ - private $host; - /** - * @var int + * @var string[] */ - private $port; + private $hosts; /** * @var string @@ -57,15 +43,10 @@ final class Config */ private $pass; - /** - * @var string - */ - private $keyspace; - /** * @var int */ - private $tcpTimeout = 1; + private $tcpTimeout = 0; /** * @var bool @@ -75,32 +56,28 @@ final class Config /** * @var int */ - private $tcpAttempts = 2; + private $tcpAttempts = 1; /** * @var string */ - private $compression; + private $compression = self::COMPRESSION_NONE; /** * @var bool */ - private $compatibility; + private $compatibility = false; /** - * @param string $host - * @param int $port - * @param string $keyspace - * @param string $user - * @param string $pass + * @param string[] $hosts + * @param string $user + * @param string $pass */ - public function __construct(string $host, int $port, string $keyspace = null, string $user = null, string $pass = null) + public function __construct(array $hosts, string $user = null, string $pass = null) { - $this->host = $host; - $this->port = $port; - $this->keyspace = $keyspace ?: self::DEFAULT_KEYSPACE; - $this->user = $user ?: self::DEFAULT_USER; - $this->pass = $pass ?: self::DEFAULT_PASS; + $this->hosts = $hosts; + $this->user = $user; + $this->pass = $pass; } /** @@ -112,50 +89,59 @@ public static function parse(string $dsn): self { $parts = \parse_url($dsn); - \parse_str($parts['query'] ?? '', $query); + $scheme = $parts['scheme'] ?? self::DEFAULT_SCHEME; + $hosts = \explode(',', $parts['host'] ?? self::DEFAULT_HOST); + $uris = []; - $options = \array_replace(self::OPTIONS, $query); + if (isset($parts['port'])) { + \end($hosts); - $self = new self( - $parts['host'] ?? self::DEFAULT_HOST, - $parts['port'] ?? self::DEFAULT_PORT, - $parts['path'] ?? self::DEFAULT_KEYSPACE, - $parts['user'] ?? self::DEFAULT_USER, - $parts['pass'] ?? self::DEFAULT_PASS - ); + $key = \key($hosts); - $self->tcpAttempts = \filter_var($options['tcp_attempts'], FILTER_VALIDATE_INT); - $self->tcpNoDelay = \filter_var($options['tcp_nodelay'], FILTER_VALIDATE_BOOLEAN); - $self->tcpTimeout = \filter_var($options['tcp_timeout'], FILTER_VALIDATE_INT); + $hosts[$key] = $hosts[$key] . ':' . $parts['port']; - $self->compression = $options['compression'] ?? null; - $self->compatibility = $options['compatibility'] ?? null; + \reset($hosts); + } - return $self; - } + foreach ($hosts as $host) { + $hostAndPort = \explode(':', $host); - /** - * @return string - */ - public function uri(): string - { - return \sprintf('tcp://%s:%d', $this->host, $this->port); - } + $uris[] = \sprintf('%s://%s:%d', $scheme, $hostAndPort[0], (int) ($hostAndPort[1] ?? self::DEFAULT_PORT)); + } - /** - * @return string - */ - public function host(): string - { - return $this->host; + $self = new self($uris, $parts['user'] ?? null, $parts['pass'] ?? null); + + \parse_str($parts['query'] ?? '', $options); + + if (isset($options['tcp_nodelay'])) { + $self->tcpNoDelay = \filter_var($options['tcp_nodelay'], FILTER_VALIDATE_BOOLEAN); + } + + if (isset($options['tcp_attempts'])) { + $self->tcpAttempts = \filter_var($options['tcp_attempts'], FILTER_VALIDATE_INT) ?: 0; + } + + if (isset($options['tcp_timeout'])) { + $self->tcpTimeout = \filter_var($options['tcp_timeout'], FILTER_VALIDATE_INT) ?: 0; + } + + if (isset($options['compression']) && \is_string($options['compression'])) { + $self->compression($options['compression']); + } + + if (isset($options['compatibility'])) { + $self->compatibility(\filter_var($options['compatibility'], FILTER_VALIDATE_BOOLEAN)); + }; + + return $self; } /** - * @return int + * @return string[] */ - public function port(): int + public function hosts(): array { - return $this->port; + return $this->hosts; } /** @@ -163,7 +149,7 @@ public function port(): int * * @return string */ - public function user(string $value = null): string + public function user(string $value = null): ?string { return \is_null($value) ? $this->user : $this->user = $value; } @@ -173,21 +159,11 @@ public function user(string $value = null): string * * @return string */ - public function password(string $value = null): string + public function password(string $value = null): ?string { return \is_null($value) ? $this->pass : $this->pass = $value; } - /** - * @param string|null $value - * - * @return string - */ - public function keyspace(string $value = null): string - { - return \is_null($value) ? $this->keyspace : $this->keyspace = $value; - } - /** * @param int|null $value * @@ -219,19 +195,39 @@ public function tcpAttempts(int $value = null): int } /** - * @return bool|null + * @param bool|null $value + * + * @return bool */ - public function compatibility(): ?bool + public function compatibility(bool $value = null): bool { - return $this->compatibility; + return \is_null($value) ? $this->compatibility : $this->compatibility = $value; } /** - * @return null|string + * @param string|null $value + * + * @return string */ - public function compression(): ?string + public function compression(string $value = null): string { - return $this->compression; + if (\is_null($value)) { + return $this->compression; + } + + if (!\in_array($value, [ + self::COMPRESSION_NONE, + self::COMPRESSION_LZ4, + self::COMPRESSION_SNAPPY, + ])) { + throw Exception\ConfigException::unknownCompressionMechanism($value); + } + + if ($value !== self::COMPRESSION_NONE && !\extension_loaded($value)) { + throw Exception\ConfigException::compressionExtensionNotLoaded($value); + } + + return $this->compression = $value; } /** @@ -239,10 +235,18 @@ public function compression(): ?string */ public function options(): array { - return \array_filter([ + $options = [ 'CQL_VERSION' => self::CQL_VERSION, - //'COMPRESSION' => $this->compression, - //'NO_COMPACT' => \is_bool($this->compatibility) ? !$this->compatibility : null, - ]); + ]; + + if ($this->compression !== self::COMPRESSION_NONE) { + $options['COMPRESSION'] = $this->compression; + } + + if ($this->compatibility) { + $options['NO_COMPACT'] = false; + } + + return $options; } } diff --git a/src/Connection.php b/src/Connection.php index 0d147cb..2515582 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -13,8 +13,8 @@ namespace PHPinnacle\Cassis; use function Amp\asyncCall, Amp\call; -use Amp\Deferred; use function Amp\Socket\connect; +use Amp\Deferred; use Amp\Loop; use Amp\Promise; use Amp\Socket\ClientConnectContext; @@ -22,94 +22,109 @@ final class Connection { + const WRITE_ROUNDS = 64; + /** * @var string */ private $uri; + /** + * @var Streams + */ + private $streams; + + /** + * @var Buffer + */ + private $packer; + /** * @var Parser */ private $parser; /** - * @var Socket + * @var EventEmitter */ - private $socket; + private $emitter; /** - * @var callable[] + * @var \SplQueue */ - private $callbacks = []; + private $queue; /** - * @var int + * @var Socket */ - private $lastWrite = 0; + private $socket; /** - * @param string $uri + * @var bool */ - public function __construct(string $uri) - { - $this->uri = $uri; - $this->parser = new Parser; - } + private $processing = false; /** - * @param int $stream - * @param callable $callback + * @var Deferred[] */ - public function subscribe(int $stream, callable $callback): void - { - $this->callbacks[$stream][] = $callback; - } + private $defers = []; /** - * @param int $stream - * - * @return Promise + * @var int */ - public function await(int $stream): Promise - { - $deferred = new Deferred; - - $this->subscribe($stream, function (Frame $frame) use ($deferred) { - if ($frame instanceof Response\Error) { - $deferred->fail(new Exception\ServerException($frame->message, $frame->code)); - } else { - $deferred->resolve($frame); - } - - return true; - }); + private $lastWrite = 0; - return $deferred->promise(); + /** + * @param string $uri + * @param Streams $streams + * @param Compressor $compressor + */ + public function __construct(string $uri, Streams $streams, Compressor $compressor) + { + $this->uri = $uri; + $this->streams = $streams; + $this->packer = new Packer($compressor); + $this->parser = new Parser($compressor); + $this->emitter = new EventEmitter($this); + $this->queue = new \SplQueue; } /** - * @param int $stream + * @param string $event + * @param callable $listener * - * @return void + * @return Promise */ - public function cancel(int $stream): void + public function register(string $event, callable $listener): Promise { - unset($this->callbacks[$stream]); + return $this->emitter->register($event, $listener); } /** * @noinspection PhpDocMissingThrowsInspection * - * @param Frame $frame + * @param Request $request * * @return Promise */ - public function send(Frame $frame): Promise + public function send(Request $request): Promise { - $this->lastWrite = Loop::now(); + $stream = $this->streams->reserve(); + $deferred = new Deferred; + + $this->defers[$stream] = $deferred; + + $this->queue->enqueue($this->packer->pack($request, $stream)); + + if ($this->processing === false) { + $this->processing = true; - /** @noinspection PhpUnhandledExceptionInspection */ - return $this->socket->write($frame->pack()->flush()); + Loop::defer(function () { + $this->write(); + }); + } + + return $deferred->promise(); } /** @@ -122,36 +137,23 @@ public function send(Frame $frame): Promise public function open(int $timeout, int $attempts, bool $noDelay): Promise { return call(function () use ($timeout, $attempts, $noDelay) { - $context = (new ClientConnectContext) - ->withConnectTimeout($timeout) - ->withMaxAttempts($attempts) - ; + $context = new ClientConnectContext; - if ($noDelay) { - $context->withTcpNoDelay(); + if ($attempts > 0) { + $context = $context->withMaxAttempts($attempts); } - $this->socket = yield connect($this->uri, $context); - - asyncCall(function () { - while (null !== $chunk = yield $this->socket->read()) { - $this->parser->append($chunk); + if ($timeout > 0) { + $context = $context->withConnectTimeout($timeout); + } - while ($frame = $this->parser->parse()) { - if (!isset($this->callbacks[$frame->stream])) { - continue 2; - } + if ($noDelay) { + $context = $context->withTcpNoDelay(); + } - foreach ($this->callbacks[$frame->stream] as $i => $callback) { - if (yield call($callback, $frame)) { - unset($this->callbacks[$frame->stream][$i]); - } - } - } - } + $this->socket = yield connect($this->uri, $context); - unset($this->socket); - }); + $this->listen(); }); } @@ -164,6 +166,75 @@ public function close(): void $this->socket->close(); } - $this->callbacks = []; + $this->defers = []; + } + + /** + * @return void + */ + private function write(): void + { + asyncCall(function () { + $processed = 0; + $data = ''; + + while ($this->queue->isEmpty() === false) { + $data .= $this->queue->dequeue(); + + ++$processed; + + if ($processed % self::WRITE_ROUNDS === 0) { + Loop::defer(function () { + $this->write(); + }); + + break; + } + } + + yield $this->socket->write($data); + + $this->lastWrite = Loop::now(); + + $this->processing = false; + }); + } + + /** + * @return void + */ + private function listen(): void + { + asyncCall(function () { + while (null !== $chunk = yield $this->socket->read()) { + $this->parser->append($chunk); + + while ($frame = $this->parser->parse()) { + if ($frame->stream === -1) { + $this->emitter->emit($frame); + + continue 2; + } + + if (!isset($this->defers[$frame->stream])) { + continue 2; + } + + $deferred = $this->defers[$frame->stream]; + unset($this->defers[$frame->stream]); + + $this->streams->release($frame->stream); + + if ($frame->opcode === Frame::OPCODE_ERROR) { + /** @var Response\Error $frame */ + $deferred->fail($frame->exception); + } else { + $deferred->resolve($frame); + } + } + } + + $this->socket = null; + }); } } diff --git a/src/Context.php b/src/Context.php index 6d264ca..9658bca 100644 --- a/src/Context.php +++ b/src/Context.php @@ -46,11 +46,6 @@ final class Context */ private $flags = 0; - /** - * @var array - */ - private $values; - /** * @var int */ @@ -71,6 +66,11 @@ final class Context */ private $defaultTimestamp; + /** + * @var array + */ + private $values; + /** * @param int $consistency */ @@ -87,31 +87,6 @@ public function consistency(): int return $this->consistency; } - /** - * @return int - */ - public function flags(): int - { - return $this->flags; - } - - /** - * @param mixed[] $values - * - * @return self - */ - public function withValues(array $values): self - { - $this->flags |= self::FLAG_VALUES; - $this->values = $values; - - if (\array_keys($values) !== \range(0, \count($values) - 1)) { - $this->flags |= self::FLAG_NAMES_FOR_VALUES; - } - - return $this; - } - /** * @return self */ @@ -237,7 +212,7 @@ public function skipMetadata(): self * * @return self */ - public function pageSize(int $size): self + public function limit(int $size): self { $this->flags |= self::FLAG_PAGE_SIZE; $this->pageSize = $size; @@ -246,14 +221,14 @@ public function pageSize(int $size): self } /** - * @param string $state + * @param string $offset * * @return self */ - public function pagingState(string $state): self + public function offset(string $offset): self { $this->flags |= self::FLAG_PAGING_STATE; - $this->pagingState = $state; + $this->pagingState = $offset; return $this; } @@ -285,16 +260,35 @@ public function defaultTimestamp(int $timestamp): self } /** - * @param Buffer $buffer + * @param mixed[] $values * - * @return Buffer + * @return self */ - public function queryParameters(Buffer $buffer): string + public function arguments(array $values): self { - $flags = $this->flags; + if (empty($values)) { + return $this; + } + + $this->flags |= self::FLAG_VALUES; + $this->values = $values; + if (\is_assoc($values)) { + $this->flags |= self::FLAG_NAMES_FOR_VALUES; + } + + return $this; + } + + /** + * @param Buffer $buffer + * + * @return void + */ + public function writeParameters(Buffer $buffer): void + { $buffer->appendShort($this->consistency); - $buffer->appendByte($flags); + $buffer->appendByte($this->flags); if ($this->flags & self::FLAG_VALUES) { if ($this->flags & self::FLAG_NAMES_FOR_VALUES) { @@ -309,7 +303,7 @@ public function queryParameters(Buffer $buffer): string } if ($this->flags & self::FLAG_PAGING_STATE) { - $buffer->appendBytes($this->pagingState); + $buffer->appendLongString($this->pagingState); } if ($this->flags & self::FLAG_SERIAL_CONSISTENCY) { @@ -319,7 +313,5 @@ public function queryParameters(Buffer $buffer): string if ($this->flags & self::FLAG_DEFAULT_TIMESTAMP) { $buffer->appendLong($this->defaultTimestamp); } - - return $buffer->flush(); } } diff --git a/src/Event.php b/src/Event.php new file mode 100644 index 0000000..0cc650f --- /dev/null +++ b/src/Event.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis; + +interface Event +{ + const + TOPOLOGY_CHANGE = 'TOPOLOGY_CHANGE', + STATUS_CHANGE = 'STATUS_CHANGE', + SCHEMA_CHANGE = 'SCHEMA_CHANGE' + ; + + const + CHANGE_NODE_NEW = 'NEW_NODE', + CHANGE_NODE_REMOVED = 'REMOVED_NODE', + CHANGE_NODE_UP = 'UP', + CHANGE_NODE_DOWN = 'DOWN', + CHANGE_TARGET_CREATED = 'CREATED', + CHANGE_TARGET_UPDATED = 'UPDATED', + CHANGE_TARGET_DROPPED = 'DROPPED' + ; + + const + TARGET_KEYSPACE = 'KEYSPACE', + TARGET_TABLE = 'TABLE', + TARGET_TYPE = 'TYPE', + TARGET_FUNCTION = 'FUNCTION', + TARGET_AGGREGATE = 'AGGREGATE' + ; + + /** + * @return string + */ + public function type(): string; +} diff --git a/src/Event/SchemaChange.php b/src/Event/SchemaChange.php new file mode 100644 index 0000000..782f8ca --- /dev/null +++ b/src/Event/SchemaChange.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Event; + +use PHPinnacle\Cassis\Event; + +final class SchemaChange implements Event +{ + /** + * @var string + */ + private $change; + + /** + * @var string + */ + private $target; + + /** + * @var string + */ + private $keyspace; + + /** + * @var string + */ + private $name; + + /** + * @var array + */ + private $arguments; + + /** + * @param string $change + * @param string $target + * @param string $keyspace + * @param string $name + * @param array $arguments + */ + public function __construct( + string $change, + string $target, + string $keyspace, + string $name = '', + array $arguments = [] + ) { + $this->change = $change; + $this->target = $target; + $this->keyspace = $keyspace; + $this->name = $name; + $this->arguments = $arguments; + } + + /** + * {@inheritdoc} + */ + public function type(): string + { + return self::SCHEMA_CHANGE; + } + + /** + * @return string + */ + public function change(): string + { + return $this->change; + } + + /** + * @return string + */ + public function target(): string + { + return $this->target; + } + + /** + * @return string + */ + public function keyspace(): string + { + return $this->keyspace; + } + + /** + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * @return array + */ + public function arguments(): array + { + return $this->arguments; + } +} diff --git a/src/Event/StatusChange.php b/src/Event/StatusChange.php new file mode 100644 index 0000000..aa3058f --- /dev/null +++ b/src/Event/StatusChange.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Event; + +use PHPinnacle\Cassis\Event; + +final class StatusChange implements Event +{ + /** + * @var string + */ + private $change; + + /** + * @var string + */ + private $address; + + /** + * @param string $change + * @param string $address + */ + public function __construct(string $change, string $address) + { + $this->change = $change; + $this->address = $address; + } + + /** + * {@inheritdoc} + */ + public function type(): string + { + return self::STATUS_CHANGE; + } + + /** + * @return string + */ + public function change(): string + { + return $this->change; + } + + /** + * @return string + */ + public function address(): string + { + return $this->address; + } +} diff --git a/src/Event/TopologyChange.php b/src/Event/TopologyChange.php new file mode 100644 index 0000000..3aa8146 --- /dev/null +++ b/src/Event/TopologyChange.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Event; + +use PHPinnacle\Cassis\Event; + +final class TopologyChange implements Event +{ + /** + * @var string + */ + private $change; + + /** + * @var string + */ + private $address; + + /** + * @param string $change + * @param string $address + */ + public function __construct(string $change, string $address) + { + $this->change = $change; + $this->address = $address; + } + + /** + * {@inheritdoc} + */ + public function type(): string + { + return self::TOPOLOGY_CHANGE; + } + + /** + * @return string + */ + public function change(): string + { + return $this->change; + } + + /** + * @return string + */ + public function address(): string + { + return $this->address; + } +} diff --git a/src/EventEmitter.php b/src/EventEmitter.php new file mode 100644 index 0000000..2988eaf --- /dev/null +++ b/src/EventEmitter.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis; + +use function Amp\asyncCall; +use function Amp\call; +use Amp\Promise; + +final class EventEmitter +{ + /** + * @var Connection + */ + private $connection; + + /** + * @var callable[][] + */ + private $listeners = []; + + /** + * @param Connection $connection + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * @param string $event + * @param callable $listener + * + * @return Promise + */ + public function register(string $event, callable $listener): Promise + { + return call(function() use ($event, $listener) { + if (!isset($this->listeners[$event])) { + yield $this->connection->send(new Request\Register([$event])); + } + + $this->listeners[$event][] = $listener; + }); + } + + /** + * @param Frame $frame + */ + public function emit(Frame $frame): void + { + asyncCall(function () use ($frame) { + $event = null; + + switch (true) { + case $frame instanceof Response\TopologyChange: + $event = new Event\TopologyChange($frame->change, \inet_ntop($frame->address)); + + break; + case $frame instanceof Response\StatusChange: + $event = new Event\StatusChange($frame->change, \inet_ntop($frame->address)); + + break; + case $frame instanceof Response\SchemaChange: + $event = new Event\SchemaChange( + $frame->change, + $frame->target, + $frame->keyspace, + $frame->name, + $frame->arguments + ); + + break; + } + + if ($event === null) { + return; + } + + $class = \get_class($event); + $listeners = $this->listeners[$class] ?? []; + + foreach ($listeners as $listener) { + asyncCall($listener, $event); + } + }); + } +} diff --git a/src/Exception/AlreadyExistsException.php b/src/Exception/AlreadyExistsException.php new file mode 100644 index 0000000..f4ebe82 --- /dev/null +++ b/src/Exception/AlreadyExistsException.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class AlreadyExistsException extends ServerException +{ + /** + * @var string + */ + private $keyspace; + + /** + * @var string + */ + private $table; + + /** + * @param string $message + * @param string $keyspace + * @param string $table + */ + public function __construct(string $message, string $keyspace, string $table) + { + parent::__construct($message); + + $this->keyspace = $keyspace; + $this->table = $table; + } + + /** + * @return string + */ + public function keyspace(): string + { + return $this->keyspace; + } + + /** + * @return string + */ + public function table(): string + { + return $this->table; + } +} diff --git a/src/Exception/AuthenticationException.php b/src/Exception/AuthenticationException.php new file mode 100644 index 0000000..c40d9a5 --- /dev/null +++ b/src/Exception/AuthenticationException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class AuthenticationException extends ServerException +{ +} diff --git a/src/Exception/BootstrappingException.php b/src/Exception/BootstrappingException.php new file mode 100644 index 0000000..38a66f3 --- /dev/null +++ b/src/Exception/BootstrappingException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class BootstrappingException extends ServerException +{ +} diff --git a/src/Exception/ClientException.php b/src/Exception/ClientException.php index 524afb7..aaf4789 100644 --- a/src/Exception/ClientException.php +++ b/src/Exception/ClientException.php @@ -14,4 +14,18 @@ final class ClientException extends CassisException { + public static function alreadyConnected(): self + { + return new self("Client already connected/connecting."); + } + + public static function unknownType(int $code): self + { + return new self("Unknown type with code {$code}."); + } + + public static function couldNotConnect(): self + { + return new self("Could not connect to any providing host."); + } } diff --git a/src/Exception/ConfigException.php b/src/Exception/ConfigException.php new file mode 100644 index 0000000..3bf8b02 --- /dev/null +++ b/src/Exception/ConfigException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class ConfigException extends CassisException +{ + public static function unknownCompressionMechanism(string $compression): self + { + return new self("Unknown compression mechanism \"{$compression}\"."); + } + + public static function compressionExtensionNotLoaded(string $compression): self + { + return new self("Extension for compression mechanism \"{$compression}\" not loaded."); + } +} diff --git a/src/Exception/FunctionFailureException.php b/src/Exception/FunctionFailureException.php new file mode 100644 index 0000000..6fa7a84 --- /dev/null +++ b/src/Exception/FunctionFailureException.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class FunctionFailureException extends ServerException +{ + /** + * @var string + */ + private $keyspace; + + /** + * @var string + */ + private $function; + + /** + * @var array + */ + private $arguments; + + /** + * @param string $message + * @param string $keyspace + * @param string $function + * @param array $arguments + */ + public function __construct(string $message, string $keyspace, string $function, array $arguments) + { + parent::__construct($message); + + $this->keyspace = $keyspace; + $this->function = $function; + $this->arguments = $arguments; + } + + /** + * @return string + */ + public function keyspace(): string + { + return $this->keyspace; + } + + /** + * @return string + */ + public function function(): string + { + return $this->function; + } + + /** + * @return array + */ + public function arguments(): array + { + return $this->arguments; + } +} diff --git a/src/Exception/InvalidQueryException.php b/src/Exception/InvalidQueryException.php new file mode 100644 index 0000000..7ef0c76 --- /dev/null +++ b/src/Exception/InvalidQueryException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class InvalidQueryException extends ServerException +{ +} diff --git a/src/Exception/OverloadedException.php b/src/Exception/OverloadedException.php new file mode 100644 index 0000000..e953ef0 --- /dev/null +++ b/src/Exception/OverloadedException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class OverloadedException extends ServerException +{ +} diff --git a/src/Exception/ProtocolException.php b/src/Exception/ProtocolException.php new file mode 100644 index 0000000..1fc48e4 --- /dev/null +++ b/src/Exception/ProtocolException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class ProtocolException extends ServerException +{ +} diff --git a/src/Exception/ReadFailureException.php b/src/Exception/ReadFailureException.php new file mode 100644 index 0000000..ba70e9c --- /dev/null +++ b/src/Exception/ReadFailureException.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class ReadFailureException extends ServerException +{ + /** + * @var int + */ + private $consistency; + + /** + * @var int + */ + private $received; + + /** + * @var int + */ + private $blockfor; + + /** + * @var int + */ + private $failures; + + /** + * @var bool + */ + private $dataPresent; + + /** + * @param string $message + * @param int $consistency + * @param int $received + * @param int $blockfor + * @param int $failures + * @param bool $dataPresent + */ + public function __construct( + string $message, + int $consistency, + int $received, + int $blockfor, + int $failures, + bool $dataPresent + ) { + parent::__construct($message); + + $this->consistency = $consistency; + $this->received = $received; + $this->blockfor = $blockfor; + $this->failures = $failures; + $this->dataPresent = $dataPresent; + } + + /** + * @return int + */ + public function consistency(): int + { + return $this->consistency; + } + + /** + * @return int + */ + public function received(): int + { + return $this->received; + } + + /** + * @return int + */ + public function blockfor(): int + { + return $this->blockfor; + } + + /** + * @return int + */ + public function failures(): int + { + return $this->failures; + } + + /** + * @return bool + */ + public function dataPresent(): bool + { + return $this->dataPresent; + } +} diff --git a/src/Exception/ReadTimeoutException.php b/src/Exception/ReadTimeoutException.php new file mode 100644 index 0000000..030101e --- /dev/null +++ b/src/Exception/ReadTimeoutException.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class ReadTimeoutException extends TimeoutException +{ + /** + * @var int + */ + private $consistency; + + /** + * @var int + */ + private $received; + + /** + * @var int + */ + private $blockfor; + + /** + * @var bool + */ + private $dataPresent; + + /** + * @param string $message + * @param int $consistency + * @param int $received + * @param int $blockfor + * @param bool $dataPresent + */ + public function __construct(string $message, int $consistency, int $received, int $blockfor, bool $dataPresent) + { + parent::__construct($message); + + $this->consistency = $consistency; + $this->received = $received; + $this->blockfor = $blockfor; + $this->dataPresent = $dataPresent; + } + + /** + * @return int + */ + public function consistency(): int + { + return $this->consistency; + } + + /** + * @return int + */ + public function received(): int + { + return $this->received; + } + + /** + * @return int + */ + public function blockfor(): int + { + return $this->blockfor; + } + + /** + * @return bool + */ + public function dataPresent(): bool + { + return $this->dataPresent; + } +} diff --git a/src/Exception/ServerException.php b/src/Exception/ServerException.php index a326b5f..c71a2eb 100644 --- a/src/Exception/ServerException.php +++ b/src/Exception/ServerException.php @@ -12,6 +12,6 @@ namespace PHPinnacle\Cassis\Exception; -final class ServerException extends CassisException +class ServerException extends CassisException { } diff --git a/src/Exception/SyntaxException.php b/src/Exception/SyntaxException.php new file mode 100644 index 0000000..3cc4a2c --- /dev/null +++ b/src/Exception/SyntaxException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class SyntaxException extends ServerException +{ +} diff --git a/src/Exception/TimeoutException.php b/src/Exception/TimeoutException.php new file mode 100644 index 0000000..e2631c4 --- /dev/null +++ b/src/Exception/TimeoutException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +abstract class TimeoutException extends ServerException +{ +} diff --git a/src/Exception/TruncatingException.php b/src/Exception/TruncatingException.php new file mode 100644 index 0000000..8c4d801 --- /dev/null +++ b/src/Exception/TruncatingException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class TruncatingException extends ServerException +{ +} diff --git a/src/Exception/UnauthorizedException.php b/src/Exception/UnauthorizedException.php new file mode 100644 index 0000000..c7f2469 --- /dev/null +++ b/src/Exception/UnauthorizedException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class UnauthorizedException extends ServerException +{ +} diff --git a/src/Exception/UnavailableException.php b/src/Exception/UnavailableException.php new file mode 100644 index 0000000..b1f5230 --- /dev/null +++ b/src/Exception/UnavailableException.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class UnavailableException extends ServerException +{ + /** + * @var int + */ + private $consistency; + + /** + * @var int + */ + private $required; + + /** + * @var int + */ + private $alive; + + /** + * @param string $message + * @param int $consistency + * @param int $required + * @param int $alive + */ + public function __construct(string $message, int $consistency, int $required, int $alive) + { + parent::__construct($message); + + $this->consistency = $consistency; + $this->required = $required; + $this->alive = $alive; + } + + /** + * @return int + */ + public function consistency(): int + { + return $this->consistency; + } + + /** + * @return int + */ + public function required(): int + { + return $this->required; + } + + /** + * @return int + */ + public function alive(): int + { + return $this->alive; + } +} diff --git a/src/Exception/UnpreparedException.php b/src/Exception/UnpreparedException.php new file mode 100644 index 0000000..2641d71 --- /dev/null +++ b/src/Exception/UnpreparedException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class UnpreparedException extends ServerException +{ + /** + * @var string + */ + private $id; + + /** + * @param string $message + * @param string $id + */ + public function __construct(string $message, string $id) + { + parent::__construct($message); + + $this->id = $id; + } +} diff --git a/src/Exception/WriteFailureException.php b/src/Exception/WriteFailureException.php new file mode 100644 index 0000000..33d026b --- /dev/null +++ b/src/Exception/WriteFailureException.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class WriteFailureException extends ServerException +{ + /** + * @var int + */ + private $consistency; + + /** + * @var int + */ + private $received; + + /** + * @var int + */ + private $blockfor; + + /** + * @var int + */ + private $failures; + + /** + * @var string + */ + private $type; + + /** + * @param string $message + * @param int $consistency + * @param int $received + * @param int $blockfor + * @param int $failures + * @param string $type + */ + public function __construct( + string $message, + int $consistency, + int $received, + int $blockfor, + int $failures, + string $type + ) { + parent::__construct($message); + + $this->consistency = $consistency; + $this->received = $received; + $this->blockfor = $blockfor; + $this->failures = $failures; + $this->type = $type; + } + + /** + * @return int + */ + public function consistency(): int + { + return $this->consistency; + } + + /** + * @return int + */ + public function received(): int + { + return $this->received; + } + + /** + * @return int + */ + public function blockfor(): int + { + return $this->blockfor; + } + + /** + * @return int + */ + public function failures(): int + { + return $this->failures; + } + + /** + * @return string + */ + public function type(): string + { + return $this->type; + } +} diff --git a/src/Exception/WriteTimeoutException.php b/src/Exception/WriteTimeoutException.php new file mode 100644 index 0000000..767b50c --- /dev/null +++ b/src/Exception/WriteTimeoutException.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Exception; + +final class WriteTimeoutException extends TimeoutException +{ + /** + * @var int + */ + private $consistency; + + /** + * @var int + */ + private $received; + + /** + * @var int + */ + private $blockfor; + + /** + * @var string + */ + private $type; + + /** + * @param string $message + * @param int $consistency + * @param int $received + * @param int $blockfor + * @param string $type + */ + public function __construct(string $message, int $consistency, int $received, int $blockfor, string $type) + { + parent::__construct($message); + + $this->consistency = $consistency; + $this->received = $received; + $this->blockfor = $blockfor; + $this->type = $type; + } + + /** + * @return int + */ + public function consistency(): int + { + return $this->consistency; + } + + /** + * @return int + */ + public function received(): int + { + return $this->received; + } + + /** + * @return int + */ + public function blockfor(): int + { + return $this->blockfor; + } + + /** + * @return string + */ + public function type(): string + { + return $this->type; + } +} diff --git a/src/Frame.php b/src/Frame.php index 4eba719..f61d17e 100644 --- a/src/Frame.php +++ b/src/Frame.php @@ -21,6 +21,13 @@ abstract class Frame RESPONSE = 0x80 + self::VERSION ; + const + FLAG_COMPRESSION = 0x01, + FLAG_TRACING = 0x02, + FLAG_PAYLOAD = 0x04, + FLAG_WARNING = 0x08 + ; + const OPCODE_ERROR = 0x00, OPCODE_STARTUP = 0x01, @@ -46,9 +53,9 @@ abstract class Frame public $type; /** - * @var array + * @var int */ - public $flags = []; + public $flags = 0; /** * @var int @@ -59,41 +66,4 @@ abstract class Frame * @var int */ public $opcode; - - /** - * @var string - */ - public $body = ''; - - /** - * @return Buffer - */ - public function pack(): Buffer - { - $buffer = new Buffer; - $buffer - ->appendByte($this->type) - ->appendByte($this->convertFlags()) - ->appendShort($this->stream) - ->appendByte($this->opcode) - ->appendLongString($this->body) - ; - - return $buffer; - } - - /** - * @return int - */ - private function convertFlags(): int - { - $value = 0; - - foreach ($this->flags as $n => $bit) { - $bit = $bit ? 1 : 0; - $value |= $bit << $n; - } - - return $value; - } } diff --git a/src/Metadata.php b/src/Metadata.php index 7b2c1ff..d68a2fb 100644 --- a/src/Metadata.php +++ b/src/Metadata.php @@ -12,30 +12,30 @@ final class Metadata { - private const + const FLAG_GLOBAL_TABLES_SPEC = 0x0001, FLAG_HAS_MORE_PAGES = 0x0002, FLAG_NO_METADATA = 0x0004 ; - + /** - * @var string + * @var array */ - private $paging; + private $columns; /** - * @var array + * @var string */ - private $columns; + private $cursor; /** - * @param string $paging * @param Column[] $columns + * @param string $cursor */ - public function __construct(string $paging = null, array $columns = []) + public function __construct(array $columns, string $cursor = null) { - $this->paging = $paging; $this->columns = $columns; + $this->cursor = $cursor; } /** @@ -45,53 +45,42 @@ public function __construct(string $paging = null, array $columns = []) */ public static function create(Buffer $buffer): self { - $flags = $buffer->consumeInt(); - $count = $buffer->consumeInt(); - - $paging = null; - $keyspace = null; - $table = null; - $columns = []; + $flags = $buffer->consumeInt(); + $count = $buffer->consumeInt(); + $cursor = null; if ($flags & self::FLAG_HAS_MORE_PAGES) { - $paging = $buffer->consumeBytes(); + $cursor = $buffer->consumeLongString(); } if ($flags & self::FLAG_NO_METADATA) { - return new self($paging); + return new self([], $cursor); } + $columns = []; + if ($flags & self::FLAG_GLOBAL_TABLES_SPEC) { $keyspace = $buffer->consumeString(); $table = $buffer->consumeString(); for ($i = 0; $i < $count; ++$i) { - $columns[] = new Column( - $keyspace, - $table, - $buffer->consumeString(), - $buffer->consumeType()); + $columns[] = Column::partial($keyspace, $table, $buffer); } } else { for ($i = 0; $i < $count; ++$i) { - $columns[] = new Column( - $buffer->consumeString(), - $buffer->consumeString(), - $buffer->consumeString(), - $buffer->consumeType() - ); + $columns[] = Column::full($buffer); } } - return new self($paging, $columns); + return new self($columns, $cursor); } /** * @return string */ - public function paging(): ?string + public function cursor(): ?string { - return $this->paging; + return $this->cursor; } /** diff --git a/src/Packer.php b/src/Packer.php new file mode 100644 index 0000000..9fab68b --- /dev/null +++ b/src/Packer.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis; + +final class Packer +{ + /** + * @var Compressor + */ + private $compressor; + + /** + * @var Buffer + */ + private $writeBuffer; + + /** + * @var Buffer + */ + private $frameBuffer; + + /** + * @param Compressor $compressor + */ + public function __construct(Compressor $compressor) + { + $this->compressor = $compressor; + $this->writeBuffer = new Buffer; + $this->frameBuffer = new Buffer; + } + + /** + * @param Request $frame + * @param int $stream + * + * @return string + */ + public function pack(Request $frame, int $stream): string + { + $frame->write($this->frameBuffer); + + $body = $this->frameBuffer->flush(); + + $this->writeBuffer + ->appendByte($frame->type) + ->appendByte($frame->flags) + ->appendShort($stream) + ->appendByte($frame->opcode) + ->appendLongString($this->compressor->compress($body)) + ; + + return $this->writeBuffer->flush(); + } +} diff --git a/src/Parser.php b/src/Parser.php index 091dbbf..fa840eb 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -10,16 +10,54 @@ namespace PHPinnacle\Cassis; +use Ramsey\Uuid\Uuid; + final class Parser { + private const + ERROR_SERVER = 0x0000, + ERROR_PROTOCOL = 0x000A, + ERROR_AUTHENTICATION = 0x0100, + ERROR_UNAVAILABLE = 0x1000, + ERROR_OVERLOADED = 0x1001, + ERROR_BOOTSTRAPPING = 0x1002, + ERROR_TRUNCATING = 0x1003, + ERROR_WRITE_TIMEOUT = 0x1100, + ERROR_READ_TIMEOUT = 0x1200, + ERROR_READ_FAILURE = 0x1300, + ERROR_FUNC_FAILURE = 0x1400, + ERROR_WRITE_FAILURE = 0x1500, + ERROR_SYNTAX = 0x2000, + ERROR_UNAUTHORIZED = 0x2100, + ERROR_INVALID = 0x2200, + ERROR_CONFIG = 0x2300, + ERROR_ALREADY_EXISTS = 0x2400, + ERROR_UNPREPARED = 0x2500 + ; + /** - * @var Buffer + * @var Compressor */ - private $buffer; + private $compressor; - public function __construct() + /** + * @var Buffer + */ + private $readBuffer; + + /** + * @var Buffer + */ + private $frameBuffer; + + /** + * @param Compressor $compressor + */ + public function __construct(Compressor $compressor) { - $this->buffer = new Buffer; + $this->compressor = $compressor; + $this->readBuffer = new Buffer; + $this->frameBuffer = new Buffer; } /** @@ -29,7 +67,7 @@ public function __construct() */ public function append(string $chunk): void { - $this->buffer->append($chunk); + $this->readBuffer->append($chunk); } /** @@ -37,95 +75,296 @@ public function append(string $chunk): void */ public function parse(): ?Frame { - if ($this->buffer->size() < 9) { + if ($this->readBuffer->size() < 9) { return null; } - $type = $this->buffer->readByte(0); - $flags = $this->buffer->readByte(1); - $stream = $this->buffer->readShort(2); - $opcode = $this->buffer->readByte(4); - $length = $this->buffer->readInt(5); + $type = $this->readBuffer->readByte(0); + $flags = $this->readBuffer->readByte(1); + $stream = $this->readBuffer->readShort(2); + $opcode = $this->readBuffer->readByte(4); + $length = $this->readBuffer->readInt(5); - if ($this->buffer->size() < $length + 9) { + if ($type !== Frame::RESPONSE) { + throw new Exception\ServerException; + } + + if ($this->readBuffer->size() < $length + 9) { return null; } - $this->buffer->discard(9); + $this->readBuffer->discard(9); - switch ($type) { - case Frame::REQUEST: - $frame = $this->parseRequest($opcode); + $body = $this->readBuffer->consume($length); - break; - case Frame::RESPONSE: - $frame = $this->parseResponse($opcode); + if ($flags & Frame::FLAG_COMPRESSION) { + $body = $this->compressor->decompress($body); + } - break; - default: - throw new Exception\ServerException; + $this->frameBuffer->append($body); + + if ($flags & Frame::FLAG_TRACING) { + $tracing = Uuid::fromBytes($this->frameBuffer->consume(16)); + } + + if ($flags & Frame::FLAG_PAYLOAD) { + $payload = $this->frameBuffer->consumeBytesMap(); + } + + if ($flags & Frame::FLAG_WARNING) { + $warnings = $this->frameBuffer->consumeStringList(); } - $frame->flags = $this->convertFlags($flags); + $frame = $this->frame($opcode, $length); + $frame->flags = $flags; $frame->stream = $stream; $frame->opcode = $opcode; return $frame; } - + /** * @param int $opcode + * @param int $length * * @return Frame */ - private function parseRequest(int $opcode): Frame + private function frame(int $opcode, int $length): Frame { switch ($opcode) { + case Frame::OPCODE_READY: + return new Response\Ready; + case Frame::OPCODE_AUTHENTICATE: + return new Response\Authenticate($this->frameBuffer->consumeString()); + case Frame::OPCODE_AUTH_SUCCESS: + return new Response\AuthSuccess; + case Frame::OPCODE_RESULT: + return new Response\Result($this->frameBuffer->consumeInt(), $this->frameBuffer->consume($length - 4)); + case Frame::OPCODE_ERROR: + return $this->parseError($this->frameBuffer->consumeInt(), $this->frameBuffer->consumeString()); + case Frame::OPCODE_SUPPORTED: + return $this->parseSupported($this->frameBuffer->consumeShort()); case Frame::OPCODE_EVENT: - return new Request\Event; + return $this->parseEvent($this->frameBuffer->consumeString()); default: throw new Exception\ServerException; } } /** - * @param int $opcode + * @param int $code + * @param string $message * * @return Frame */ - private function parseResponse(int $opcode): Frame + private function parseError(int $code, string $message): Frame { - switch ($opcode) { - case Frame::OPCODE_ERROR: - return new Response\Error($this->buffer->consumeInt(), $this->buffer->consumeString()); - case Frame::OPCODE_READY: - return new Response\Ready; - case Frame::OPCODE_AUTHENTICATE: - return new Response\Authenticate($this->buffer->consumeString()); - case Frame::OPCODE_AUTH_SUCCESS: - return new Response\AuthSuccess; - case Frame::OPCODE_SUPPORTED: - return new Response\Supported; - case Frame::OPCODE_RESULT: - return new Response\Result($this->buffer->consumeInt(), new Buffer($this->buffer->flush())); + switch ($code) { + case self::ERROR_SERVER: + $exception = new Exception\ServerException($message); + + break; + case self::ERROR_PROTOCOL: + $exception = new Exception\ProtocolException($message); + + break; + case self::ERROR_AUTHENTICATION: + $exception = new Exception\AuthenticationException($message); + + break; + case self::ERROR_UNAVAILABLE: + $exception = new Exception\UnavailableException( + $message, + $this->frameBuffer->consumeShort(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeInt() + ); + + break; + case self::ERROR_OVERLOADED: + $exception = new Exception\OverloadedException($message); + + break; + case self::ERROR_BOOTSTRAPPING: + $exception = new Exception\BootstrappingException($message); + + break; + case self::ERROR_TRUNCATING: + $exception = new Exception\TruncatingException($message); + + break; + case self::ERROR_WRITE_TIMEOUT: + $exception = new Exception\WriteTimeoutException( + $message, + $this->frameBuffer->consumeShort(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeString() + ); + + break; + case self::ERROR_READ_TIMEOUT: + $exception = new Exception\ReadTimeoutException( + $message, + $this->frameBuffer->consumeShort(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeByte() > 0 + ); + + break; + case self::ERROR_READ_FAILURE: + $exception = new Exception\ReadFailureException( + $message, + $this->frameBuffer->consumeShort(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeByte() > 0 + ); + + break; + case self::ERROR_FUNC_FAILURE: + $exception = new Exception\FunctionFailureException( + $message, + $this->frameBuffer->consumeString(), + $this->frameBuffer->consumeString(), + $this->frameBuffer->consumeStringList() + ); + + break; + case self::ERROR_WRITE_FAILURE: + $exception = new Exception\WriteFailureException( + $message, + $this->frameBuffer->consumeShort(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeInt(), + $this->frameBuffer->consumeString() + ); + + break; + case self::ERROR_SYNTAX: + $exception = new Exception\SyntaxException($message); + + break; + case self::ERROR_UNAUTHORIZED: + $exception = new Exception\UnauthorizedException($message); + + break; + case self::ERROR_INVALID: + $exception = new Exception\InvalidQueryException($message); + + break; + case self::ERROR_CONFIG: + $exception = new Exception\ConfigException($message); + + break; + case self::ERROR_ALREADY_EXISTS: + $exception = new Exception\AlreadyExistsException( + $message, + $this->frameBuffer->consumeString(), + $this->frameBuffer->consumeString() + ); + + break; + case self::ERROR_UNPREPARED: + $count = $this->frameBuffer->consumeShort(); + + $exception = new Exception\UnpreparedException( + $message, + $this->frameBuffer->consume($count) + ); + + break; default: throw new Exception\ServerException; } + + return new Response\Error($exception); } /** - * @param int $value + * @param int $size * - * @return bool[] + * @return Frame */ - private function convertFlags(int $value): array + private function parseSupported(int $size): Frame { - $bits = []; + $options = []; + + for ($i = 0; $i < $size; ++$i) { + $option = \strtoupper($this->frameBuffer->consumeString()); + $length = $this->frameBuffer->consumeShort(); - for ($i = 0; $i < 4; ++$i) { - $bits[] = ($value & (1 << $i)) > 0; + for ($j = 0; $j < $length; ++$j) { + $options[$option][] = $this->frameBuffer->consumeString(); + } + } + + return new Response\Supported($options); + } + + /** + * @param string $type + * + * @return Frame + */ + private function parseEvent(string $type): Frame + { + $change = $this->frameBuffer->consumeString(); + + switch ($type) { + case Event::TOPOLOGY_CHANGE: + $length = $this->frameBuffer->consumeInt(); + $address = $this->frameBuffer->consume($length); + + $event = new Event\TopologyChange($change, $address); + + break; + case Event::STATUS_CHANGE: + $length = $this->frameBuffer->consumeInt(); + $address = $this->frameBuffer->consume($length); + + $event = new Event\StatusChange($change, $address); + + break; + case Event::SCHEMA_CHANGE: + $target = $this->frameBuffer->consumeString(); + + switch ($target) { + case Event::TARGET_KEYSPACE: + $keyspace = $this->frameBuffer->consumeString(); + + $event = new Event\SchemaChange($change, $target, $keyspace); + + break; + case Event::TARGET_TABLE: + case Event::TARGET_TYPE: + $keyspace = $this->frameBuffer->consumeString(); + $name = $this->frameBuffer->consumeString(); + + $event = new Event\SchemaChange($change, $target, $keyspace, $name); + + break; + case Event::TARGET_FUNCTION: + case Event::TARGET_AGGREGATE: + $keyspace = $this->frameBuffer->consumeString(); + $name = $this->frameBuffer->consumeString(); + $arguments = $this->frameBuffer->consumeStringList(); + + $event = new Event\SchemaChange($change, $target, $keyspace, $name, $arguments); + + break; + default: + throw new Exception\ServerException; + } + + break; + default: + throw new Exception\ServerException; } - return $bits; + return new Response\Event($event); } } diff --git a/src/Request/Event.php b/src/Request.php similarity index 58% rename from src/Request/Event.php rename to src/Request.php index 1be5404..44d1778 100644 --- a/src/Request/Event.php +++ b/src/Request.php @@ -8,15 +8,16 @@ * file that was distributed with this source code. */ -declare(strict_types = 1); +namespace PHPinnacle\Cassis; -namespace PHPinnacle\Cassis\Request; - -use PHPinnacle\Cassis\Frame; - -class Event extends Frame +abstract class Request extends Frame { - public $opcode = self::OPCODE_EVENT; public $type = self::REQUEST; - public $stream = -1; + + /** + * @param Buffer $buffer + * + * @return void + */ + abstract public function write(Buffer $buffer): void; } diff --git a/src/Request/AuthResponse.php b/src/Request/AuthResponse.php index edf5cc9..5b1dacd 100644 --- a/src/Request/AuthResponse.php +++ b/src/Request/AuthResponse.php @@ -13,12 +13,21 @@ namespace PHPinnacle\Cassis\Request; use PHPinnacle\Cassis\Buffer; -use PHPinnacle\Cassis\Frame; +use PHPinnacle\Cassis\Request; -class AuthResponse extends Frame +final class AuthResponse extends Request { public $opcode = self::OPCODE_AUTH_RESPONSE; - public $type = self::REQUEST; + + /** + * @var string + */ + public $user; + + /** + * @var string + */ + public $password; /** * @param string $user @@ -26,13 +35,18 @@ class AuthResponse extends Frame */ public function __construct(string $user, string $password) { - $buffer = new Buffer; + $this->user = $user; + $this->password = $password; + } + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { $buffer - ->appendString($user) - ->appendString($password) + ->appendString($this->user) + ->appendString($this->password) ; - - $this->stream = 0; - $this->body = $buffer->flush(); } } diff --git a/src/Request/Batch.php b/src/Request/Batch.php index dd5c911..873a5a3 100644 --- a/src/Request/Batch.php +++ b/src/Request/Batch.php @@ -13,30 +13,52 @@ namespace PHPinnacle\Cassis\Request; use PHPinnacle\Cassis\Buffer; -use PHPinnacle\Cassis\Frame; use PHPinnacle\Cassis\Context; +use PHPinnacle\Cassis\Request; use PHPinnacle\Cassis\Statement; -class Batch extends Frame +final class Batch extends Request { - public $opcode = self::OPCODE_QUERY; - public $type = self::REQUEST; - + public $opcode = self::OPCODE_BATCH; + + /** + * @var int + */ + public $type; + + /** + * @var string[] + */ + public $queries; + + /** + * @var Context + */ + public $context; + + /** + * @param int $type + * @param string[] $queries + * @param Context $context + */ + public function __construct(int $type, array $queries, Context $context) + { + $this->type = $type; + $this->queries = $queries; + $this->context = $context; + } + /** - * @param int $stream - * @param int $type - * @param array $queries - * @param Context $context + * {@inheritdoc} */ - public function __construct(int $stream, int $type, array $queries, Context $context) + public function write(Buffer $buffer): void { - $buffer = new Buffer; $buffer - ->appendByte($type) - ->appendShort(\count($queries)) + ->appendByte($this->type) + ->appendShort(\count($this->queries)) ; - foreach ($queries as $query) { + foreach ($this->queries as $query) { switch (true) { case $query instanceof Statement\Simple: $buffer @@ -49,7 +71,7 @@ public function __construct(int $stream, int $type, array $queries, Context $con case $query instanceof Statement\Prepared: $buffer ->appendByte(1) - ->appendShortBytes($query->id()) + ->appendString($query->id()) ->appendValuesList($query->values()) ; @@ -57,9 +79,6 @@ public function __construct(int $stream, int $type, array $queries, Context $con } } - $buffer->appendShort($context->consistency()); - - $this->stream = $stream; - $this->body = $buffer->flush(); + $buffer->appendShort($this->context->consistency()); } } diff --git a/src/Request/Execute.php b/src/Request/Execute.php index a76b117..2d0624f 100644 --- a/src/Request/Execute.php +++ b/src/Request/Execute.php @@ -14,29 +14,39 @@ use PHPinnacle\Cassis\Buffer; use PHPinnacle\Cassis\Context; -use PHPinnacle\Cassis\Frame; +use PHPinnacle\Cassis\Request; -class Execute extends Frame +final class Execute extends Request { public $opcode = self::OPCODE_EXECUTE; - public $type = self::REQUEST; /** - * @param int $stream + * @var string + */ + public $id; + + /** + * @var Context + */ + public $context; + + /** * @param string $id - * @param array $values * @param Context $context */ - public function __construct(int $stream, string $id, array $values, Context $context) + public function __construct(string $id, Context $context) { - if (!empty($values)) { - $context->withValues($values); - } - - $buffer = new Buffer; - $buffer->appendString($id); + $this->id = $id; + $this->context = $context; + } - $this->stream = $stream; - $this->body = $context->queryParameters($buffer); + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + $buffer->appendString($this->id); + + $this->context->writeParameters($buffer); } } diff --git a/src/Request/Options.php b/src/Request/Options.php new file mode 100644 index 0000000..ad055cf --- /dev/null +++ b/src/Request/Options.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Request; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Request; + +final class Options extends Request +{ + public $opcode = self::OPCODE_OPTIONS; + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + } +} diff --git a/src/Request/Prepare.php b/src/Request/Prepare.php index 7394bb7..e88d53a 100644 --- a/src/Request/Prepare.php +++ b/src/Request/Prepare.php @@ -13,23 +13,30 @@ namespace PHPinnacle\Cassis\Request; use PHPinnacle\Cassis\Buffer; -use PHPinnacle\Cassis\Frame; +use PHPinnacle\Cassis\Request; -class Prepare extends Frame +final class Prepare extends Request { public $opcode = self::OPCODE_PREPARE; - public $type = self::REQUEST; /** - * @param int $stream + * @var string + */ + public $cql; + + /** * @param string $cql */ - public function __construct(int $stream, string $cql) + public function __construct(string $cql) { - $buffer = new Buffer; - $buffer->appendLongString($cql); - - $this->stream = $stream; - $this->body = $buffer->flush(); + $this->cql = $cql; + } + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + $buffer->appendLongString($this->cql); } } diff --git a/src/Request/Query.php b/src/Request/Query.php index a4d8111..e1ad86e 100644 --- a/src/Request/Query.php +++ b/src/Request/Query.php @@ -13,30 +13,40 @@ namespace PHPinnacle\Cassis\Request; use PHPinnacle\Cassis\Buffer; -use PHPinnacle\Cassis\Frame; use PHPinnacle\Cassis\Context; +use PHPinnacle\Cassis\Request; -class Query extends Frame +final class Query extends Request { public $opcode = self::OPCODE_QUERY; - public $type = self::REQUEST; - + + /** + * @var string + */ + public $cql; + + /** + * @var Context + */ + public $context; + /** - * @param int $stream * @param string $cql - * @param array $values * @param Context $context */ - public function __construct(int $stream, string $cql, array $values, Context $context) + public function __construct(string $cql, Context $context) { - if (!empty($values)) { - $context->withValues($values); - } - - $buffer = new Buffer; - $buffer->appendLongString($cql); + $this->cql = $cql; + $this->context = $context; + } + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + $buffer->appendLongString($this->cql); - $this->stream = $stream; - $this->body = $context->queryParameters($buffer); + $this->context->writeParameters($buffer); } } diff --git a/src/Request/Register.php b/src/Request/Register.php new file mode 100644 index 0000000..1286934 --- /dev/null +++ b/src/Request/Register.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Request; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Request; + +final class Register extends Request +{ + public $opcode = self::OPCODE_REGISTER; + + /** + * @var array + */ + public $events; + + /** + * @param array $events + */ + public function __construct(array $events) + { + $this->events = $events; + } + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + $buffer->appendStringList($this->events); + } +} diff --git a/src/Request/Startup.php b/src/Request/Startup.php index 34508fc..49fa065 100644 --- a/src/Request/Startup.php +++ b/src/Request/Startup.php @@ -13,22 +13,30 @@ namespace PHPinnacle\Cassis\Request; use PHPinnacle\Cassis\Buffer; -use PHPinnacle\Cassis\Frame; +use PHPinnacle\Cassis\Request; -class Startup extends Frame +final class Startup extends Request { public $opcode = self::OPCODE_STARTUP; - public $type = self::REQUEST; + + /** + * @var array + */ + public $options; /** * @param array $options */ public function __construct(array $options) { - $buffer = new Buffer; - $buffer->appendStringMap($options); - - $this->stream = 0; - $this->body = $buffer->flush(); + $this->options = $options; + } + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + $buffer->appendStringMap($this->options); } } diff --git a/src/Response.php b/src/Response.php new file mode 100644 index 0000000..8b6e994 --- /dev/null +++ b/src/Response.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis; + +abstract class Response extends Frame +{ + public $type = self::RESPONSE; +} diff --git a/src/Response/AuthSuccess.php b/src/Response/AuthSuccess.php index 2e1923e..3360330 100644 --- a/src/Response/AuthSuccess.php +++ b/src/Response/AuthSuccess.php @@ -12,10 +12,9 @@ namespace PHPinnacle\Cassis\Response; -use PHPinnacle\Cassis\Frame; +use PHPinnacle\Cassis\Response; -class AuthSuccess extends Frame +final class AuthSuccess extends Response { public $opcode = self::OPCODE_AUTH_SUCCESS; - public $type = self::RESPONSE; } diff --git a/src/Response/Authenticate.php b/src/Response/Authenticate.php index 915b0c3..f9984ea 100644 --- a/src/Response/Authenticate.php +++ b/src/Response/Authenticate.php @@ -12,12 +12,11 @@ namespace PHPinnacle\Cassis\Response; -use PHPinnacle\Cassis\Frame; +use PHPinnacle\Cassis\Response; -class Authenticate extends Frame +final class Authenticate extends Response { public $opcode = self::OPCODE_AUTHENTICATE; - public $type = self::RESPONSE; /** * @var string diff --git a/src/Response/Error.php b/src/Response/Error.php index 0381a12..ae4cdf1 100644 --- a/src/Response/Error.php +++ b/src/Response/Error.php @@ -12,78 +12,22 @@ namespace PHPinnacle\Cassis\Response; -use PHPinnacle\Cassis\Buffer; -use PHPinnacle\Cassis\Frame; -use PHPinnacle\Cassis\Exception; +use PHPinnacle\Cassis\Response; -class Error extends Frame +final class Error extends Response { - const - SERVER = 0x0000, - PROTOCOL = 0x000A, - AUTHENTICATION = 0x0100, - UNAVAILABLE = 0x1000, - OVERLOADED = 0x1001, - BOOTSTRAPPING = 0x1002, - TRUNCATING = 0x1003, - WRITE_TIMEOUT = 0x1100, - READ_TIMEOUT = 0x1200, - READ_FAILURE = 0x1300, - FUNC_FAILURE = 0x1400, - WRITE_FAILURE = 0x1500, - SYNTAX = 0x2000, - UNAUTHORIZED = 0x2100, - INVALID = 0x2200, - CONFIG = 0x2300, - ALREADY_EXISTS = 0x2400, - UNPREPARED = 0x2500 - ; - public $opcode = self::OPCODE_ERROR; - public $type = self::RESPONSE; /** - * @var int + * @var \Throwable */ - public $code; + public $exception; /** - * @var string + * @param \Throwable $exception */ - public $message; - - /** - * @param int $code - * @param string $message - * @param Buffer $buffer - */ - public function __construct(int $code, string $message, Buffer $buffer = null) + public function __construct(\Throwable $exception) { - $this->code = $code; - $this->message = $message; - - switch ($code) { - case self::SERVER: - case self::PROTOCOL: - case self::AUTHENTICATION: - case self::UNAVAILABLE: - case self::OVERLOADED: - case self::BOOTSTRAPPING: - case self::TRUNCATING: - case self::WRITE_TIMEOUT: - case self::READ_TIMEOUT: - case self::READ_FAILURE: - case self::FUNC_FAILURE: - case self::WRITE_FAILURE: - case self::SYNTAX: - case self::UNAUTHORIZED: - case self::INVALID: - case self::CONFIG: - case self::ALREADY_EXISTS: - case self::UNPREPARED: - break; - default: - throw new Exception\ServerException; - } + $this->exception = $exception; } } diff --git a/src/Response/Event.php b/src/Response/Event.php new file mode 100644 index 0000000..4618bad --- /dev/null +++ b/src/Response/Event.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Response; + +use PHPinnacle\Cassis\Response; +use PHPinnacle\Cassis\Event as EventContract; + +final class Event extends Response +{ + public $opcode = self::OPCODE_EVENT; + + /** + * @var EventContract + */ + public $event; + + /** + * @param EventContract $event + */ + public function __construct(EventContract $event) + { + $this->event = $event; + } +} diff --git a/src/Response/Ready.php b/src/Response/Ready.php index 0f80172..eeda33b 100644 --- a/src/Response/Ready.php +++ b/src/Response/Ready.php @@ -12,10 +12,9 @@ namespace PHPinnacle\Cassis\Response; -use PHPinnacle\Cassis\Frame; +use PHPinnacle\Cassis\Response; -class Ready extends Frame +final class Ready extends Response { public $opcode = self::OPCODE_READY; - public $type = self::RESPONSE; } diff --git a/src/Response/Result.php b/src/Response/Result.php index eb8ee31..c909d69 100644 --- a/src/Response/Result.php +++ b/src/Response/Result.php @@ -12,13 +12,11 @@ namespace PHPinnacle\Cassis\Response; -use PHPinnacle\Cassis\Buffer; -use PHPinnacle\Cassis\Frame; +use PHPinnacle\Cassis\Response; -class Result extends Frame +final class Result extends Response { public $opcode = self::OPCODE_RESULT; - public $type = self::RESPONSE; /** * @var int @@ -26,15 +24,15 @@ class Result extends Frame public $kind; /** - * @var Buffer + * @var string */ public $data; /** * @param int $kind - * @param Buffer $data + * @param string $data */ - public function __construct(int $kind, Buffer $data) + public function __construct(int $kind, string $data) { $this->kind = $kind; $this->data = $data; diff --git a/src/Response/Supported.php b/src/Response/Supported.php index e2a9adc..e88a112 100644 --- a/src/Response/Supported.php +++ b/src/Response/Supported.php @@ -12,10 +12,22 @@ namespace PHPinnacle\Cassis\Response; -use PHPinnacle\Cassis\Frame; +use PHPinnacle\Cassis\Response; -class Supported extends Frame +final class Supported extends Response { public $opcode = self::OPCODE_SUPPORTED; - public $type = self::RESPONSE; + + /** + * @var string[][] + */ + public $options; + + /** + * @param string[][] $options + */ + public function __construct(array $options) + { + $this->options = $options; + } } diff --git a/src/Result.php b/src/Result.php index c9b02e1..d01c711 100644 --- a/src/Result.php +++ b/src/Result.php @@ -14,4 +14,11 @@ interface Result { + const + VOID = 0x0001, + ROWS = 0x0002, + USE = 0x0003, + PREPARE = 0x0004, + ALTER = 0x0005 + ; } diff --git a/src/Result/Prepared.php b/src/Result/Prepared.php index 0e85965..eff1029 100644 --- a/src/Result/Prepared.php +++ b/src/Result/Prepared.php @@ -12,6 +12,8 @@ namespace PHPinnacle\Cassis\Result; +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Column; use PHPinnacle\Cassis\Metadata; use PHPinnacle\Cassis\Response; use PHPinnacle\Cassis\Result; @@ -24,25 +26,32 @@ class Prepared implements Result private $id; /** - * @var Metadata + * @var array + */ + private $keys; + + /** + * @var array */ - private $prepared; + private $columns; /** * @var Metadata */ - private $result; + private $meta; /** * @param string $id - * @param Metadata $prepared - * @param Metadata $result + * @param array $keys + * @param array $columns + * @param Metadata $meta */ - public function __construct(string $id, Metadata $prepared, Metadata $result) + public function __construct(string $id, array $keys, array $columns, Metadata $meta) { - $this->id = $id; - $this->prepared = $prepared; - $this->result = $result; + $this->id = $id; + $this->keys = $keys; + $this->columns = $columns; + $this->meta = $meta; } /** @@ -52,10 +61,65 @@ public function __construct(string $id, Metadata $prepared, Metadata $result) */ public static function create(Response\Result $frame): self { - return new self( - $frame->data->consumeString(), - Metadata::create($frame->data), - Metadata::create($frame->data) - ); + $buffer = new Buffer($frame->data); + + $statementId = $buffer->consumeString(); + $flags = $buffer->consumeInt(); + $columnsCount = $buffer->consumeInt(); + $primaryCount = $buffer->consumeInt(); + + $keys = []; + $columns = []; + + for ($i = 0; $i < $primaryCount; ++$i) { + $keys[] = $buffer->consumeShort(); + } + + if ($flags & Metadata::FLAG_GLOBAL_TABLES_SPEC) { + $keyspace = $buffer->consumeString(); + $table = $buffer->consumeString(); + + for ($i = 0; $i < $columnsCount; ++$i) { + $columns[] = Column::partial($keyspace, $table, $buffer); + } + } else { + for ($i = 0; $i < $columnsCount; ++$i) { + $columns[] = Column::full($buffer); + } + } + + return new self($statementId, $keys, $columns, Metadata::create($buffer)); + } + + /** + * @return string + */ + public function id(): string + { + return $this->id; + } + + /** + * @return array + */ + public function keys(): array + { + return $this->keys; + } + + /** + * @return Column[] + */ + public function columns(): array + { + return $this->columns; + } + + /** + * @return Metadata + */ + public function meta(): Metadata + { + return $this->meta; } } diff --git a/src/Result/Rows.php b/src/Result/Rows.php index c2bc9d5..8dc3e8c 100644 --- a/src/Result/Rows.php +++ b/src/Result/Rows.php @@ -12,23 +12,32 @@ namespace PHPinnacle\Cassis\Result; +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Column; use PHPinnacle\Cassis\Metadata; use PHPinnacle\Cassis\Response; use PHPinnacle\Cassis\Result; -final class Rows implements Result, \IteratorAggregate, \Countable +final class Rows implements Result, \Iterator, \Countable, \ArrayAccess { /** * @var \SplFixedArray */ public $data; + /** + * @var Metadata + */ + private $meta; + /** * @param \SplFixedArray $data + * @param Metadata $meta */ - public function __construct(\SplFixedArray $data) + public function __construct(\SplFixedArray $data, Metadata $meta) { $this->data = $data; + $this->meta = $meta; } /** @@ -38,8 +47,8 @@ public function __construct(\SplFixedArray $data) */ public static function create(Response\Result $frame): self { - $buffer = $frame->data; - $metadata = Metadata::create($buffer); + $buffer = new Buffer($frame->data); + $meta = Metadata::create($buffer); $count = $buffer->consumeInt(); $rows = new \SplFixedArray($count); @@ -47,22 +56,54 @@ public static function create(Response\Result $frame): self for ($i = 0; $i < $count; ++$i) { $data = []; - foreach ($metadata->columns() as $column) { - $data[$column->name()] = $buffer->consumeValue($column->type()); + foreach ($meta->columns() as $column) { + $data[$column->name()] = $column->value($buffer); } $rows[$i] = $data; } - return new self($rows); + return new self($rows, $meta); } /** * {@inheritdoc} */ - public function getIterator() + public function current() { - return $this->data; + return $this->data->current(); + } + + /** + * {@inheritdoc} + */ + public function next() + { + $this->data->next(); + } + + /** + * {@inheritdoc} + */ + public function key() + { + return $this->data->key(); + } + + /** + * {@inheritdoc} + */ + public function valid() + { + return $this->data->valid(); + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->data->rewind(); } /** @@ -72,4 +113,60 @@ public function count() { return $this->data->count(); } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return $this->data->offsetExists($offset); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + return $this->data->offsetGet($offset); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + throw new \BadMethodCallException('Rows result are immutable.'); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + throw new \BadMethodCallException('Rows result are immutable.'); + } + + /** + * @return Metadata + */ + public function meta(): Metadata + { + return $this->meta; + } + + /** + * @return string|null + */ + public function cursor(): ?string + { + return $this->meta->cursor(); + } + + /** + * @return Column[] + */ + public function columns(): array + { + return $this->meta->columns(); + } } diff --git a/src/Result/SchemaChange.php b/src/Result/SchemaChange.php index aa16846..3342915 100644 --- a/src/Result/SchemaChange.php +++ b/src/Result/SchemaChange.php @@ -12,6 +12,7 @@ namespace PHPinnacle\Cassis\Result; +use PHPinnacle\Cassis\Buffer; use PHPinnacle\Cassis\Response; use PHPinnacle\Cassis\Result; @@ -51,10 +52,12 @@ public function __construct(string $type, string $target, string $options) */ public static function create(Response\Result $frame): self { + $buffer = new Buffer($frame->data); + return new self( - $frame->data->consumeString(), - $frame->data->consumeString(), - $frame->data->consumeString() + $buffer->consumeString(), + $buffer->consumeString(), + $buffer->consumeString() ); } diff --git a/src/Result/SetKeyspace.php b/src/Result/SetKeyspace.php index 759bde6..3cef37a 100644 --- a/src/Result/SetKeyspace.php +++ b/src/Result/SetKeyspace.php @@ -12,6 +12,7 @@ namespace PHPinnacle\Cassis\Result; +use PHPinnacle\Cassis\Buffer; use PHPinnacle\Cassis\Response; use PHPinnacle\Cassis\Result; @@ -37,6 +38,6 @@ public function __construct(string $name) */ public static function create(Response\Result $frame): self { - return new self($frame->data->consumeString()); + return new self((new Buffer($frame->data))->consumeString()); } } diff --git a/src/Session.php b/src/Session.php index 90b65be..a5ee9fb 100644 --- a/src/Session.php +++ b/src/Session.php @@ -17,26 +17,16 @@ final class Session { - private const - RESPONSE_VOID = 0x0001, - RESPONSE_ROWS = 0x0002, - RESPONSE_USE = 0x0003, - RESPONSE_PREPARE = 0x0004, - RESPONSE_ALTER = 0x0005 - ; - - private const MAX_STREAM = 32768; - /** * @var Connection */ private $connection; /** - * @var int + * @var string */ - private $stream = 0; - + private $keyspace; + /** * @param Connection $connection */ @@ -46,116 +36,109 @@ public function __construct(Connection $connection) } /** - * @param string $cql - * @param array $values - * @param Context $context + * @param string $keyspace * - * @return Promise + * @return Promise */ - public function query(string $cql, array $values = [], Context $context = null): Promise + public function keyspace(string $keyspace): Promise { - return $this->execute(new Statement\Simple($cql, $values), $context); + return call(function () use ($keyspace) { + if ($this->keyspace === $keyspace) { + return $this->keyspace; + } + + yield $this->execute(new Statement\Simple("USE {$keyspace}")); + + return $this->keyspace = $keyspace; + }); } /** - * @param Statement $statement - * @param Context $context + * @param string $event + * @param callable $listener * - * @return Promise + * @return Promise */ - public function execute(Statement $statement, Context $context = null): Promise + public function register(string $event, callable $listener): Promise { - return call(function () use ($statement, $context) { - $stream = $this->reserveStream(); - $context = $context ?: new Context; - - switch (true) { - case $statement instanceof Statement\Simple: - $query = new Request\Query($stream, $statement->cql(), $statement->values(), $context); - - break; - case $statement instanceof Statement\Prepared: - $query = new Request\Execute($stream, $statement->id(), $statement->values(), $context); - - break; - case $statement instanceof Statement\Batch: - $query = new Request\Batch($stream, $statement->type(), $statement->queries(), $context); - break; - default: - throw new Exception\ClientException; - } - - yield $this->connection->send($query); - - /** @var Response\Result $frame */ - $frame = yield $this->connection->await($stream); - - $this->releaseStream($stream); + return $this->connection->register($event, $listener); + } - switch ($frame->kind) { - case self::RESPONSE_VOID: - return null; - case self::RESPONSE_ROWS: - return Result\Rows::create($frame); - case self::RESPONSE_USE: - return Result\SetKeyspace::create($frame); - case self::RESPONSE_ALTER: - return Result\SchemaChange::create($frame); - default: - throw new Exception\ServerException; - } - }); + /** + * @param string $cql + * @param array $values + * @param Context $context + * + * @return Promise + */ + public function query(string $cql, array $values = [], Context $context = null): Promise + { + return $this->execute(new Statement\Simple($cql, $values), $context); } /** * @param string $cql * - * @return Promise + * @return Promise */ public function prepare(string $cql): Promise { return call(function () use ($cql) { - $stream = $this->reserveStream(); - $prepare = new Request\Prepare($stream, $cql); - - yield $this->connection->send($prepare); - - /** @var Response\Result $frame */ - $frame = yield $this->connection->await($stream); + $request = new Request\Prepare($cql); + $response = yield $this->connection->send($request); - $this->releaseStream($stream); - - if ($frame->kind !== self::RESPONSE_PREPARE) { + if ($response->kind !== Result::PREPARE) { throw new Exception\ServerException; } - return Result\Prepared::create($frame); + return Result\Prepared::create($response); }); } /** - * @return Promise + * @param Statement $statement + * @param Context $context + * + * @return Promise */ - public function close(): Promise + public function execute(Statement $statement, Context $context = null): Promise { - return call(function () { + return call(function () use ($statement, $context) { + $request = $this->request($statement, $context ?: new Context); + $response = yield $this->connection->send($request); + switch ($response->kind) { + case Result::VOID: + return null; + case Result::ROWS: + return Result\Rows::create($response); + case Result::USE: + return Result\SetKeyspace::create($response); + case Result::ALTER: + return Result\SchemaChange::create($response); + default: + throw new Exception\ServerException; + } }); } /** - * @return int - */ - private function reserveStream(): int - { - return $this->stream++; - } - - /** - * @param int $stream + * @param Statement $statement + * @param Context $context + * + * @return Request */ - private function releaseStream(int $stream): void + private function request(Statement $statement, Context $context): Request { - // TODO + switch (true) { + case $statement instanceof Statement\Simple: + return new Request\Query($statement->cql(), $context->arguments($statement->values())); + case $statement instanceof Statement\Prepared: + return new Request\Execute($statement->id(), $context->arguments($statement->values())); + case $statement instanceof Statement\Batch: + return new Request\Batch($statement->type(), $statement->queries(), $context); + default: + throw new Exception\ClientException; + } } } diff --git a/src/Statement/Batch.php b/src/Statement/Batch.php index 9d9838d..aed78ff 100644 --- a/src/Statement/Batch.php +++ b/src/Statement/Batch.php @@ -14,7 +14,7 @@ final class Batch implements Statement { - const + private const TYPE_LOGGED = 0, TYPE_UNLOGGED = 1, TYPE_COUNTER = 2 @@ -31,15 +31,45 @@ final class Batch implements Statement private $queries; /** - * @param int $type - * @param Statement ...$queries + * @param int $type + * @param Statement[] $queries */ - public function __construct(int $type, Statement ...$queries) + private function __construct(int $type, array $queries) { $this->type = $type; $this->queries = $queries; } + /** + * @param Statement ...$queries + * + * @return Batch + */ + public static function logged(Statement ...$queries): self + { + return new self(self::TYPE_LOGGED, $queries); + } + + /** + * @param Statement ...$queries + * + * @return Batch + */ + public static function unlogged(Statement ...$queries): self + { + return new self(self::TYPE_UNLOGGED, $queries); + } + + /** + * @param Statement ...$queries + * + * @return Batch + */ + public static function counter(Statement ...$queries): self + { + return new self(self::TYPE_COUNTER, $queries); + } + /** * @return int */ diff --git a/src/Streams.php b/src/Streams.php new file mode 100644 index 0000000..549a1a0 --- /dev/null +++ b/src/Streams.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis; + +final class Streams +{ + private const MAX_STREAM = 32768; + + /** + * @var self + */ + private static $instance; + + /** + * @var int + */ + private $next = 0; + + /** + * @var \SplStack + */ + private $stack; + + /** + * Closed constructor + */ + private function __construct() + { + $this->stack = new \SplStack; + } + + /** + * @return self + */ + public static function instance(): self + { + return self::$instance ?: self::$instance = new self; + } + + /** + * @return int + */ + public function reserve(): int + { + if (!$this->stack->isEmpty()) { + return $this->stack->pop(); + } + + $next = ++$this->next; + + return $next === self::MAX_STREAM ? $this->next = 0 : $next; + } + + /** + * @param int $id + */ + public function release(int $id): void + { + $this->stack->push($id); + } +} diff --git a/src/Type.php b/src/Type.php index 7551da2..77a33fd 100644 --- a/src/Type.php +++ b/src/Type.php @@ -15,36 +15,38 @@ interface Type { const - CUSTOM = 0x0000, - ASCII = 0x0001, - BIGINT = 0x0002, - BLOB = 0x0003, - BOOLEAN = 0x0004, - COUNTER = 0x0005, - DECIMAL = 0x0006, - DOUBLE = 0x0007, - FLOAT = 0x0008, - INT = 0x0009, - TEXT = 0x000A, - TIMESTAMP = 0x000B, - UUID = 0x000C, - VARCHAR = 0x000D, - VARINT = 0x000E, - TIMEUUID = 0x000F, - INET = 0x0010, - DATE = 0x0011, - TIME = 0x0012, - SMALLINT = 0x0013, - TINYINT = 0x0014, - COLLECTION_LIST = 0x0020, - COLLECTION_MAP = 0x0021, - COLLECTION_SET = 0x0022, - UDT = 0x0030, - TUPLE = 0x0031 + CUSTOM = 0x0000, + ASCII = 0x0001, + BIGINT = 0x0002, + BLOB = 0x0003, + BOOLEAN = 0x0004, + COUNTER = 0x0005, + DECIMAL = 0x0006, + DOUBLE = 0x0007, + FLOAT = 0x0008, + INT = 0x0009, + TEXT = 0x000A, + TIMESTAMP = 0x000B, + UUID = 0x000C, + VARCHAR = 0x000D, + VARINT = 0x000E, + TIMEUUID = 0x000F, + INET = 0x0010, + DATE = 0x0011, + TIME = 0x0012, + SMALLINT = 0x0013, + TINYINT = 0x0014, + LIST = 0x0020, + MAP = 0x0021, + SET = 0x0022, + UDT = 0x0030, + TUPLE = 0x0031 ; /** - * @return int + * @param Buffer $buffer + * + * @return mixed */ - public function code(): int; + public function read(Buffer $buffer); } diff --git a/src/Type/Base.php b/src/Type/Base.php index 2cd8ee8..01c0924 100644 --- a/src/Type/Base.php +++ b/src/Type/Base.php @@ -10,28 +10,81 @@ namespace PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Exception; use PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Value; +use Ramsey\Uuid\Uuid; final class Base implements Type { /** * @var int */ - private $type; + private $code; /** - * @param int $type + * @param int $code */ - public function __construct(int $type) + public function __construct(int $code) { - $this->type = $type; + $this->code = $code; } /** - * @return int + * {@inheritdoc} */ - public function code(): int + public function read(Buffer $buffer) { - return $this->type; + $length = $buffer->consumeInt(); + + if ($length < 0) { + return null; + } + + switch ($this->code) { + case Type::ASCII: + case Type::VARCHAR: + case Type::TEXT: + return $buffer->consume($length); + case Type::BLOB: + return Value\Blob::fromArray($buffer->consumeBytes($length)); + case Type::BOOLEAN: + return (bool) $buffer->consumeByte(); + case Type::TINYINT: + return $buffer->consumeTinyInt(); + case Type::SMALLINT: + return $buffer->consumeSmallInt(); + case Type::INT: + return $buffer->consumeInt(); + case Type::BIGINT: + return $buffer->consumeLong(); + case Type::VARINT: + return Value\Varint::fromBytes($buffer->consume($length)); + case Type::FLOAT: + return $buffer->consumeFloat(); + case Type::DOUBLE: + return $buffer->consumeDouble(); + case Type::DECIMAL: + $scale = $buffer->consumeUint(); + $bytes = $buffer->consume($length - 4); + + return Value\Decimal::fromBytes($bytes, $scale); + case Type::TIMESTAMP: + return Value\Timestamp::fromMicroSeconds($buffer->consumeLong()); + case Type::DATE: + return Value\Date::fromSeconds($buffer->consumeUint()); + case Type::TIME: + return Value\Time::fromNanoSeconds($buffer->consumeLong()); + case Type::UUID: + case Type::TIMEUUID: + return Uuid::fromBytes($buffer->consume(16)); + case Type::INET: + return Value\Inet::fromBytes($buffer->consume($length)); + case Type::COUNTER: + return new Value\Counter($buffer->consumeLong()); + default: + throw Exception\ClientException::unknownType($this->code); + } } } diff --git a/src/Type/Collection.php b/src/Type/Collection.php index 72ca0d7..de7bf97 100644 --- a/src/Type/Collection.php +++ b/src/Type/Collection.php @@ -10,36 +10,106 @@ namespace PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Buffer; use PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Value; final class Collection implements Type { + const + KIND_LIST = 0, + KIND_SET = 1, + KIND_MAP = 2 + ; + + /** + * @var int + */ + private $kind; + /** * @var Type */ - private $value; + private $type; /** - * @param Type $value + * @param int $kind + * @param Type $type + */ + private function __construct(int $kind, Type $type) + { + $this->kind = $kind; + $this->type = $type; + } + + /** + * @param Type $type + * + * @return self */ - public function __construct(Type $value) + public static function list(Type $type): self { - $this->value = $value; + return new self(self::KIND_LIST, $type); } /** - * @return int + * @param Type $type + * + * @return self */ - public function code(): int + public static function set(Type $type): self { - return self::COLLECTION_LIST; + return new self(self::KIND_SET, $type); } /** - * @return Type + * @param Type $key + * @param Type $value + * + * @return self */ - public function value(): Type + public static function map(Type $key, Type $value): self { - return $this->value; + return new self(self::KIND_MAP, new KeyValue($key, $value)); + } + + /** + * {@inheritdoc} + */ + public function read(Buffer $buffer): Value + { + $values = []; + + $slice = $buffer->slice($buffer->consumeInt()); + $count = $slice->consumeInt(); + + switch ($this->kind) { + case self::KIND_LIST: + for ($i = 0; $i < $count; ++$i) { + $values[] = $this->type->read($slice); + } + + return Value\Collection::list($values); + case self::KIND_SET: + for ($i = 0; $i < $count; ++$i) { + $values[] = $this->type->read($slice); + } + + return Value\Collection::set($values); + case self::KIND_MAP: + $keys = []; + + for ($i = 0; $i < $count; ++$i) { + [$key, $value] = $this->type->read($slice); + + $keys[] = $key; + $values[] = $value; + } + + return Value\Collection::map($keys, $values); + default: + // Newer goes here + throw new \InvalidArgumentException; + } } } diff --git a/src/Type/Custom.php b/src/Type/Custom.php index ce1af44..d40e6f3 100644 --- a/src/Type/Custom.php +++ b/src/Type/Custom.php @@ -10,7 +10,9 @@ namespace PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Buffer; use PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Value; final class Custom implements Type { @@ -28,18 +30,18 @@ public function __construct(string $class) } /** - * @return int + * @return string */ - public function code(): int + public function class(): string { - return self::CUSTOM; + return $this->class; } /** - * @return string + * {@inheritdoc} */ - public function class(): string + public function read(Buffer $buffer): Value { - return $this->class; + // TODO } } diff --git a/src/Type/Map.php b/src/Type/KeyValue.php similarity index 65% rename from src/Type/Map.php rename to src/Type/KeyValue.php index c6ace73..21e29e6 100644 --- a/src/Type/Map.php +++ b/src/Type/KeyValue.php @@ -10,9 +10,10 @@ namespace PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Buffer; use PHPinnacle\Cassis\Type; -final class Map implements Type +final class KeyValue implements Type { /** * @var Type @@ -35,26 +36,15 @@ public function __construct(Type $key, Type $value) } /** - * @return int + * @param Buffer $buffer + * + * @return mixed */ - public function code(): int + public function read(Buffer $buffer) { - return self::COLLECTION_MAP; - } - - /** - * @return Type - */ - public function key(): Type - { - return $this->key; - } - - /** - * @return Type - */ - public function value(): Type - { - return $this->value; + return [ + $this->key->read($buffer), + $this->value->read($buffer) + ]; } } diff --git a/src/Type/Set.php b/src/Type/Set.php deleted file mode 100644 index a93662c..0000000 --- a/src/Type/Set.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace PHPinnacle\Cassis\Type; - -use PHPinnacle\Cassis\Type; - -final class Set implements Type -{ - /** - * @var Type - */ - private $value; - - /** - * @param Type $value - */ - public function __construct(Type $value) - { - $this->value = $value; - } - - /** - * @return int - */ - public function code(): int - { - return self::COLLECTION_SET; - } - - /** - * @return Type - */ - public function value(): Type - { - return $this->value; - } -} diff --git a/src/Type/Tuple.php b/src/Type/Tuple.php index 88c091f..0d0ec85 100644 --- a/src/Type/Tuple.php +++ b/src/Type/Tuple.php @@ -10,7 +10,9 @@ namespace PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Buffer; use PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Value; final class Tuple implements Type { @@ -28,18 +30,16 @@ public function __construct(array $definitions) } /** - * @return int + * {@inheritdoc} */ - public function code(): int + public function read(Buffer $buffer): Value { - return self::TUPLE; - } + $values = []; - /** - * @return Type[] - */ - public function definitions(): array - { - return $this->definitions; + foreach ($this->definitions as $key => $type) { + $value[$key] = $type->read($buffer); + } + + return new Value\Tuple($values); } } diff --git a/src/Type/UserDefined.php b/src/Type/UserDefined.php index 02e722a..cb593ec 100644 --- a/src/Type/UserDefined.php +++ b/src/Type/UserDefined.php @@ -10,7 +10,9 @@ namespace PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Buffer; use PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Value; final class UserDefined implements Type { @@ -42,11 +44,18 @@ public function __construct(string $keyspace, string $name, array $definitions) } /** - * @return int + * {@inheritdoc} */ - public function code(): int + public function read(Buffer $buffer): Value { - return self::UDT; + $values = []; + $slice = $buffer->slice($buffer->consumeInt()); + + foreach ($this->definitions as $key => $type) { + $values[$key] = $type->read($slice); + } + + return new Value\UserDefined($values); } /** @@ -64,12 +73,4 @@ public function name(): string { return $this->name; } - - /** - * @return Type[] - */ - public function definitions(): array - { - return $this->definitions; - } } diff --git a/src/Value/Blob.php b/src/Value/Blob.php new file mode 100644 index 0000000..60c2ad6 --- /dev/null +++ b/src/Value/Blob.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Value; + +final class Blob implements Value +{ + /** + * @var array + */ + private $values; + + /** + * @param array $values + */ + public function __construct(array $values) + { + $this->values = $values; + } + + /** + * @param array $bytes + * + * @return self + */ + public static function fromArray(array $bytes): self + { + return new self($bytes); + } + + /** + * @param string $value + * + * @return self + */ + public static function fromString(string $value): self + { + $length = \strlen($value); + $values = []; + + for ($i = 0; $i < $length; ++$i) { + $values[] = \ord($value[$i]); + } + + return new self($values); + } + + /** + * @return array + */ + public function values(): array + { + return $this->values; + } + + /** + * @return string + */ + public function bytes(): string + { + return '0x' . implode('', array_map('dechex', $this->values)); + } + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + $buffer->appendInt(\count($this->values)); + + foreach ($this->values as $value) { + $buffer->appendByte($value); + } + } + + /** + * @return string + */ + public function __toString(): string + { + return \implode('', \array_map(function (int $value) { + return \chr($value); + }, $this->values)); + } +} diff --git a/src/Value/Collection.php b/src/Value/Collection.php new file mode 100644 index 0000000..3d251ac --- /dev/null +++ b/src/Value/Collection.php @@ -0,0 +1,221 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Value; +use PHPinnacle\Cassis\Type; + +final class Collection implements Value, \Iterator, \Countable, \ArrayAccess +{ + /** + * @var int + */ + private $kind; + + /** + * @var array + */ + private $keys; + + /** + * @var array + */ + private $values; + + /** + * @param int $kind + * @param array $keys + * @param array $values + */ + private function __construct(int $kind, array $keys, array $values) + { + $this->kind = $kind; + $this->keys = $keys; + $this->values = $values; + } + + /** + * @param array $values + * + * @return self + */ + public static function list(array $values): self + { + $values = \array_values($values); + + return new self(Type\Collection::KIND_LIST, \array_keys($values), $values); + } + + /** + * @param array $values + * + * @return self + */ + public static function set(array $values): self + { + $values = \array_unique($values); + + return new self(Type\Collection::KIND_SET, \array_keys($values), \array_values($values)); + } + + /** + * @param array $keys + * @param array $values + * + * @return self + */ + public static function map(array $keys, array $values): self + { + return new self(Type\Collection::KIND_MAP, $keys, $values); + } + + /** + * @param array $values + * + * @return self + */ + public static function assoc(array $values): self + { + return self::map(\array_keys($values), \array_values($values)); + } + + /** + * @return array + */ + public function keys(): array + { + return $this->keys; + } + + /** + * @return array + */ + public function values(): array + { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + $tmp = new Buffer; + $tmp->appendInt(\count($this->keys)); + + switch ($this->kind) { + case Type\Collection::KIND_LIST: + case Type\Collection::KIND_SET: + foreach ($this->values as $value) { + $tmp->appendValue($value); + } + + break; + case Type\Collection::KIND_MAP: + foreach ($this->keys as $i => $key) { + $tmp->appendValue($key); + $tmp->appendValue($this->values[$i]); + } + + break; + } + + $buffer + ->appendInt($tmp->size()) + ->append($tmp->flush()) + ; + } + + /** + * {@inheritdoc} + */ + public function current() + { + return $this->values[\key($this->keys)]; + } + + /** + * {@inheritdoc} + */ + public function next() + { + \next($this->keys); + } + + /** + * {@inheritdoc} + */ + public function key() + { + return \current($this->keys); + } + + /** + * {@inheritdoc} + */ + public function valid() + { + return isset($this->values[\key($this->keys)]); + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + \reset($this->keys); + } + + /** + * {@inheritdoc} + */ + public function count() + { + return \count($this->keys); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return \array_search($offset, $this->keys) !== false; + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + $key = \array_search($offset, $this->keys); + + return $this->values[$key]; + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + throw new \BadMethodCallException('Collection is immutable.'); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + throw new \BadMethodCallException('Collection is immutable.'); + } +} diff --git a/src/Value/Compound.php b/src/Value/Compound.php new file mode 100644 index 0000000..6c80d7d --- /dev/null +++ b/src/Value/Compound.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Value; + +abstract class Compound implements Value +{ + /** + * @var array + */ + private $values; + + /** + * @param array $values + */ + public function __construct(array $values) + { + $this->values = $values; + } + + /** + * @return array + */ + public function values(): array + { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + $tmp = new Buffer; + + foreach ($this->values as $value) { + $tmp->appendValue($value); + } + + $buffer + ->appendInt($tmp->size()) + ->append($tmp->flush()) + ; + } +} diff --git a/src/Value/Counter.php b/src/Value/Counter.php index 2450179..92c0db0 100644 --- a/src/Value/Counter.php +++ b/src/Value/Counter.php @@ -48,14 +48,4 @@ public function write(Buffer $buffer): void ->appendLong($this->value) ; } - - /** - * @param Buffer $buffer - * - * @return self - */ - public static function read(Buffer $buffer): self - { - return new self($buffer->consumeLong()); - } } diff --git a/src/Value/Date.php b/src/Value/Date.php index 9a9cf4e..7bb168a 100644 --- a/src/Value/Date.php +++ b/src/Value/Date.php @@ -30,14 +30,24 @@ public function __construct(int $value) $this->value = $value; } + /** + * @param int $timestamp + * + * @return self + */ + public static function fromSeconds(int $timestamp): self + { + return new self($timestamp - (2 ** 31)); + } + /** * @param \DateTimeInterface $dateTime * * @return self */ - public function fromDateTime(\DateTimeInterface $dateTime): self + public static function fromDateTime(\DateTimeInterface $dateTime): self { - return new self((int) floor($dateTime->getTimestamp() / 86400) + (2 ** 31) + 1); + return new self((int) floor($dateTime->getTimestamp() / 86400)); } /** @@ -47,10 +57,11 @@ public function fromDateTime(\DateTimeInterface $dateTime): self */ public function toDateTime(): \DateTimeInterface { - $value = $this->value - (2 ** 31) + 1; - /** @noinspection PhpUnhandledExceptionInspection */ - return (new \DateTimeImmutable())->setDate(1970, 1, $value); + return (new \DateTimeImmutable()) + ->setDate(1970, 1, $this->value + 1) + ->setTime(0, 0, 0) + ; } /** @@ -68,15 +79,7 @@ public function write(Buffer $buffer): void { $buffer ->appendInt(4) - ->appendUint($this->value) + ->appendUint($this->value + (2 ** 31)) ; } - - /** - * {@inheritdoc} - */ - public static function read(Buffer $buffer): self - { - return new self($buffer->consumeUint()); - } } diff --git a/src/Value/Decimal.php b/src/Value/Decimal.php index aa40b9d..bc360a0 100644 --- a/src/Value/Decimal.php +++ b/src/Value/Decimal.php @@ -18,24 +18,61 @@ final class Decimal implements Value { /** - * @var float + * @var \GMP */ private $value; /** - * @param float $value + * @var int */ - public function __construct(float $value) + private $scale; + + /** + * @param \GMP $value + * @param int $scale + */ + public function __construct(\GMP $value, int $scale) { $this->value = $value; + $this->scale = $scale; + } + + /** + * @param string $value + * @param int $scale + * + * @return self + */ + public static function fromString(string $value, int $scale): self + { + return new self(bigint_init($value), $scale); + } + + /** + * @param string $bytes + * @param int $scale + * + * @return self + */ + public static function fromBytes(string $bytes, int $scale): self + { + return new self(bigint_import($bytes), $scale); } /** - * @return float + * @return string + */ + public function value(): string + { + return \gmp_strval($this->value); + } + + /** + * @return int */ - public function value(): float + public function scale(): int { - return $this->value; + return $this->scale; } /** @@ -43,27 +80,35 @@ public function value(): float */ public function write(Buffer $buffer): void { - $pos = \strpos($this->value, '.'); - $scale = $pos === false ? 0 : \strlen($this->value) - $pos - 1; - $value = (($this->value * \pow(10, $scale)) & 0xffffffff00000000) >> 32; + $binary = \gmp_export($this->value); $buffer - ->appendInt(8) - ->appendUint($scale) - ->appendUint($value) + ->appendInt(4 + \strlen($binary)) + ->appendUint($this->scale) + ->append($binary) ; } - + /** - * @param Buffer $buffer - * - * @return self + * @return string */ - public static function read(Buffer $buffer): self + public function __toString(): string { - $scale = $buffer->consumeUint(); - $value = $buffer->consumeUint(); + $sign = ''; + $value = \gmp_strval($this->value); + $length = \strlen($value); + + if ($value[0] === '-') { + $sign = '-'; + $value = \substr($value, 1); + + --$length; + } + + if ($length <= $this->scale) { + $value = \str_pad($value, $this->scale + 1, '0', \STR_PAD_LEFT); + } - return new self((float) \substr_replace($value, '.', -1 * $scale, 0)); + return $sign . ($this->scale > 0 ? \substr_replace($value, '.', $this->scale * -1, 0) : $value); } } diff --git a/src/Value/Double.php b/src/Value/Double.php index 87a7018..bbecc83 100644 --- a/src/Value/Double.php +++ b/src/Value/Double.php @@ -30,6 +30,30 @@ public function __construct(float $value) $this->value = $value; } + /** + * @param float $value + * + * @return self + */ + public static function fromFloat(float $value): self + { + return new self($value); + } + + /** + * @param string $value + * + * @return self + */ + public static function fromString(string $value): self + { + if (false === \is_numeric($value)) { + throw new \InvalidArgumentException("Value \"{$value}\" not numeric"); + } + + return new self((float) $value); + } + /** * @return float */ @@ -48,14 +72,12 @@ public function write(Buffer $buffer): void ->appendDouble($this->value) ; } - + /** - * @param Buffer $buffer - * - * @return self + * @return string */ - public static function read(Buffer $buffer): self + public function __toString(): string { - return new self($buffer->consumeDouble()); + return (string) $this->value; } } diff --git a/src/Value/Inet.php b/src/Value/Inet.php index 8525c0a..5b8b899 100644 --- a/src/Value/Inet.php +++ b/src/Value/Inet.php @@ -25,25 +25,37 @@ final class Inet implements Value /** * @param string $value */ - public function __construct(string $value) + private function __construct(string $value) { $this->value = $value; } /** - * {@inheritdoc} + * @param string $value + * + * @return self */ - public static function readV4(Buffer $buffer): self + public static function fromString(string $value): self { - return new self(\inet_ntop($buffer->consume(4))); + if (\filter_var($value, FILTER_VALIDATE_IP) === false) { + throw new \InvalidArgumentException("Invalid ip address: \"{$value}\"."); + } + + return new self($value); } /** - * {@inheritdoc} + * @param string $bytes + * + * @return self */ - public static function readV6(Buffer $buffer): self + public static function fromBytes(string $bytes): self { - return new self(\inet_ntop($buffer->consume(16))); + if (!$value = @\inet_ntop($bytes)) { + throw new \InvalidArgumentException("Cant read ip address from bytes string."); + } + + return new self($value); } /** diff --git a/src/Value/Integer.php b/src/Value/Integer.php index d9532a2..ef853a4 100644 --- a/src/Value/Integer.php +++ b/src/Value/Integer.php @@ -31,7 +31,7 @@ final class Integer implements Value * @param int $value * @param int $size */ - public function __construct(int $value, int $size = 4) + private function __construct(int $value, int $size = 4) { $this->value = $value; $this->size = $size; @@ -57,6 +57,16 @@ public static function small(int $value): self return new self($value, 2); } + /** + * @param int $value + * + * @return self + */ + public static function int(int $value): self + { + return new self($value, 4); + } + /** * @param int $value * diff --git a/src/Value/Time.php b/src/Value/Time.php index c750020..83b385b 100644 --- a/src/Value/Time.php +++ b/src/Value/Time.php @@ -25,11 +25,44 @@ final class Time implements Value /** * @param int $value */ - public function __construct(int $value) + private function __construct(int $value = 0) { + if ($value < 0) { + throw new \InvalidArgumentException("Time must be nanoseconds since midnight, {$value} given"); + } + + if ($value > 86399999999999) { + throw new \InvalidArgumentException("Time must be nanoseconds since midnight, {$value} given"); + } + $this->value = $value; } + /** + * @param int $value + * + * @return self + */ + public static function fromNanoSeconds(int $value): self + { + return new self($value); + } + + /** + * @param \DateTimeInterface $time + * + * @return self + */ + public static function fromDateTime(\DateTimeInterface $time): self + { + $parts = \array_map('intval', \explode(':', $time->format('H:i:s:u'))); + + $seconds = $parts[0] * 3600 + $parts[1] * 60 + $parts[2]; + $micro = $seconds * 1000000 + $parts[3]; + + return new self($micro * 1000); + } + /** * @param \DateInterval $interval * @@ -38,9 +71,9 @@ public function __construct(int $value) public static function fromInterval(\DateInterval $interval): self { $seconds = $interval->h * 3600 + $interval->m * 60 + $interval->s; - $micro = $seconds * 100000 + $interval->f; + $micro = $seconds * 1000000 + $interval->f; - return new self($micro * 1000); + return new self((int) $micro * 1000); } /** @@ -50,13 +83,24 @@ public static function fromInterval(\DateInterval $interval): self */ public function toDateInterval(): \DateInterval { - $value = \floor($this->value / 1000); - /** @noinspection PhpUnhandledExceptionInspection */ $now = (new \DateTimeImmutable)->setTime(0, 0, 0, 0); - $new = $now->setTime(0, 0, 0, (int) $value); - return $now->diff($new, true); + return $this->toDateTime()->diff($now); + } + + /** + * @noinspection PhpDocMissingThrowsInspection + * + * @return \DateTimeInterface + */ + public function toDateTime(): \DateTimeInterface + { + $value = (int) \floor($this->value / 1000); + $seconds = (int) \floor($value / 1000000); + + /** @noinspection PhpUnhandledExceptionInspection */ + return (new \DateTimeImmutable)->setTime(0, 0, $seconds, $value - $seconds * 1000000); } /** @@ -77,12 +121,4 @@ public function write(Buffer $buffer): void ->appendLong($this->value) ; } - - /** - * {@inheritdoc} - */ - public static function read(Buffer $buffer): self - { - return new self($buffer->consumeLong()); - } } diff --git a/src/Value/Timestamp.php b/src/Value/Timestamp.php index 9d13239..36493f9 100644 --- a/src/Value/Timestamp.php +++ b/src/Value/Timestamp.php @@ -25,11 +25,25 @@ final class Timestamp implements Value /** * @param int $value */ - public function __construct(int $value) + private function __construct(int $value) { + if ($value < 0) { + throw new \InvalidArgumentException("Timestamp must be positive, {$value} given"); + } + $this->value = $value; } - + + /** + * @param int $value + * + * @return self + */ + public static function fromMicroSeconds(int $value): self + { + return new self($value); + } + /** * @param \DateTimeInterface $dateTime * @@ -47,7 +61,7 @@ public static function fromDateTime(\DateTimeInterface $dateTime): self */ public function toDateTime(): \DateTimeInterface { - $value = \substr_replace($this->value, '.', -5, 0); + $value = \substr_replace(\str_pad((string) $this->value, 7, '0', STR_PAD_LEFT), '.', -6, 0); return \DateTimeImmutable::createFromFormat('U.u', $value); } @@ -70,12 +84,4 @@ public function write(Buffer $buffer): void ->appendLong($this->value) ; } - - /** - * {@inheritdoc} - */ - public static function read(Buffer $buffer): self - { - return new self($buffer->consumeLong()); - } } diff --git a/src/Value/Timeuuid.php b/src/Value/Tuple.php similarity index 53% rename from src/Value/Timeuuid.php rename to src/Value/Tuple.php index 27983fe..c2c2b93 100644 --- a/src/Value/Timeuuid.php +++ b/src/Value/Tuple.php @@ -12,15 +12,6 @@ namespace PHPinnacle\Cassis\Value; -use Ramsey\Uuid\Uuid as Implementation; - -final class Timeuuid extends Uuid +final class Tuple extends Compound { - /** - * @return \DateTimeInterface - */ - public function toDateTime(): \DateTimeInterface - { - return Implementation::fromString($this->value)->getDateTime(); - } } diff --git a/src/Value/UserDefined.php b/src/Value/UserDefined.php new file mode 100644 index 0000000..9cb03da --- /dev/null +++ b/src/Value/UserDefined.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types = 1); + +namespace PHPinnacle\Cassis\Value; + +final class UserDefined extends Compound +{ +} diff --git a/src/Value/Uuid.php b/src/Value/Varint.php similarity index 51% rename from src/Value/Uuid.php rename to src/Value/Varint.php index d79d236..0731749 100644 --- a/src/Value/Uuid.php +++ b/src/Value/Varint.php @@ -14,42 +14,53 @@ use PHPinnacle\Cassis\Buffer; use PHPinnacle\Cassis\Value; -use Ramsey\Uuid\Uuid as Implementation; -class Uuid implements Value +final class Varint implements Value { /** - * @var Implementation + * @var \GMP */ - protected $value; + private $value; /** - * @param string $value + * @param \GMP $value */ - public function __construct(string $value) + public function __construct(\GMP $value) { - $this->value = Implementation::fromString($value); + $this->value = $value; } /** - * {@inheritdoc} + * @param string $value + * + * @return self */ - public function write(Buffer $buffer): void + public static function fromString(string $value): self { - $buffer - ->appendInt(16) - ->append($this->value->getBytes()) - ; + return new self(bigint_init($value)); } /** - * @param Buffer $buffer + * @param string $bytes * - * @return static + * @return self */ - public static function read(Buffer $buffer): self + public static function fromBytes(string $bytes): self { - return new static(Implementation::fromBytes($buffer->consume(16))->toString()); + return new self(bigint_import($bytes)); + } + + /** + * {@inheritdoc} + */ + public function write(Buffer $buffer): void + { + $binary = gmp_export($this->value); + + $buffer + ->appendInt(\strlen($binary)) + ->append($binary) + ; } /** @@ -57,6 +68,6 @@ public static function read(Buffer $buffer): self */ public function __toString(): string { - return $this->value->toString(); + return \gmp_strval($this->value); } } diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 0000000..659711f --- /dev/null +++ b/src/functions.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use Amp\Loop; +use function Amp\call; +use PHPinnacle\Cassis\Cluster; + +abstract class AsyncTest extends CassisTest +{ + /** + * @var string + */ + private $realTestName; + + /** + * @codeCoverageIgnore Invoked before code coverage data is being collected. + * + * @param string $name + */ + public function setName(string $name): void + { + parent::setName($name); + + $this->realTestName = $name; + } + + protected function runTest() + { + parent::setName('runTestAsync'); + + return parent::runTest(); + } + + protected function runTestAsync(...$args) + { + $return = null; + + try { + Loop::run(function () use (&$return, $args) { + $return = yield call([$this, $this->realTestName], ...$args); + + $info = Loop::getInfo(); + $count = $info['enabled_watchers']['referenced']; + + if ($count !== 0) { + $message = "Still have {$count} loop watchers."; + + foreach (['defer', 'delay', 'repeat', 'on_readable', 'on_writable'] as $key) { + $message .= " {$key} - {$info[$key]['enabled']}."; + } + + self::markTestIncomplete($message); + + Loop::stop(); + } + }); + } finally { + Loop::set((new Loop\DriverFactory)->create()); + + \gc_collect_cycles(); + } + + return $return; + } + + /** + * @return Cluster + */ + public static function cluster(): Cluster + { + if (!$dsn = \getenv('CASSIS_TEST_DSN')) { + self::markTestSkipped('No test dsn! Please set CASSIS_TEST_DSN environment variable.'); + } + + return Cluster::build($dsn); + } +} diff --git a/tests/BufferTest.php b/tests/BufferTest.php new file mode 100644 index 0000000..d5309e6 --- /dev/null +++ b/tests/BufferTest.php @@ -0,0 +1,276 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use PHPinnacle\Cassis\Buffer; +use Ramsey\Uuid\Uuid; + +class BufferTest extends CassisTest +{ + public function testAppend() + { + $buffer = new Buffer; + + self::assertSame($buffer, $buffer->append('abcd')); + self::assertSame('abcd', $buffer->consume(4)); + } + + public function testSlice() + { + $buffer = new Buffer('abcd'); + $slice = $buffer->slice(2); + + self::assertSame('ab', $slice->flush()); + self::assertSame('cd', $buffer->flush()); + } + + public function testDiscard() + { + $buffer = new Buffer('abcd'); + $buffer->discard(2); + + self::assertSame('cd', $buffer->flush()); + } + + public function testFlush() + { + $buffer = new Buffer('abcd'); + + self::assertSame(4, $buffer->size()); + self::assertSame('abcd', $buffer->flush()); + self::assertSame(0, $buffer->size()); + } + + public function testByte() + { + $buffer = new Buffer; + $buffer->appendByte(100); + + self::assertSame(100, $buffer->readByte()); + self::assertSame(100, $buffer->consumeByte()); + self::assertSame(0, $buffer->size()); + } + + public function testBytes() + { + $buffer = new Buffer; + $buffer->appendByte(100); + $buffer->appendByte(101); + $buffer->appendByte(42); + + self::assertSame([100, 101, 42], $buffer->consumeBytes(3)); + } + + public function testBytesMap() + { + $buffer = new Buffer; + $buffer->appendShort(2); + $buffer->appendString('a'); + $buffer->appendBytes([1, 2]); + $buffer->appendString('b'); + $buffer->appendBytes([3, 4]); + + self::assertSame([ + 'a' => [1,2], + 'b' => [3,4], + ], $buffer->consumeBytesMap()); + } + + public function testShort() + { + $buffer = new Buffer; + $buffer->appendShort(100); + + self::assertSame(100, $buffer->readShort()); + self::assertSame(100, $buffer->consumeShort()); + self::assertSame(0, $buffer->size()); + + $buffer->appendShort(-100); + + self::assertSame(100, $buffer->readShort()); + self::assertSame(100, $buffer->consumeShort()); + self::assertSame(0, $buffer->size()); + } + + public function testTinyInt() + { + $buffer = new Buffer; + $buffer->appendTinyInt(100); + + self::assertSame(100, $buffer->readTinyInt()); + self::assertSame(100, $buffer->consumeTinyInt()); + self::assertSame(0, $buffer->size()); + } + + public function testSmallInt() + { + $buffer = new Buffer; + $buffer->appendSmallInt(100); + + self::assertSame(100, $buffer->readSmallInt()); + self::assertSame(100, $buffer->consumeSmallInt()); + self::assertSame(0, $buffer->size()); + } + + public function testInt() + { + $buffer = new Buffer; + $buffer->appendInt(100); + + self::assertSame(100, $buffer->readInt()); + self::assertSame(100, $buffer->consumeInt()); + self::assertSame(0, $buffer->size()); + } + + public function testUint() + { + $buffer = new Buffer; + $buffer->appendUint(100); + + self::assertSame(100, $buffer->readUint()); + self::assertSame(100, $buffer->consumeUint()); + self::assertSame(0, $buffer->size()); + + $buffer->appendUint(-100); + + self::assertSame(100, $buffer->readUint()); + self::assertSame(100, $buffer->consumeUint()); + self::assertSame(0, $buffer->size()); + } + + public function testLong() + { + $buffer = new Buffer; + $buffer->appendLong(100); + + self::assertSame(100, $buffer->readLong()); + self::assertSame(100, $buffer->consumeLong()); + self::assertSame(0, $buffer->size()); + } + + public function testFloat() + { + $buffer = new Buffer; + $buffer->appendFloat(100.01); + + self::assertEquals(100.01, round($buffer->readFloat(), 2)); + self::assertEquals(100.01, round($buffer->consumeFloat(), 2)); + self::assertEquals(0, $buffer->size()); + } + + public function testDouble() + { + $buffer = new Buffer; + $buffer->appendDouble(100.01); + + self::assertEquals(100.01, $buffer->readDouble()); + self::assertEquals(100.01, $buffer->consumeDouble()); + self::assertEquals(0, $buffer->size()); + } + + public function testString() + { + $buffer = new Buffer; + $buffer->appendString('abcd'); + + self::assertSame(6, $buffer->size()); + self::assertSame('abcd', $buffer->readString()); + self::assertSame('abcd', $buffer->consumeString()); + self::assertSame(0, $buffer->size()); + } + + public function testLongString() + { + $buffer = new Buffer; + $buffer->appendLongString('abcd'); + + self::assertSame(8, $buffer->size()); + self::assertSame('abcd', $buffer->readLongString()); + self::assertSame('abcd', $buffer->consumeLongString()); + self::assertSame(0, $buffer->size()); + } + + public function testStringList() + { + $buffer = new Buffer; + $buffer->appendStringList([ + 'abcd', + 'fegh', + ]); + + self::assertSame(14, $buffer->size()); + self::assertSame([ + 'abcd', + 'fegh', + ], $buffer->consumeStringList()); + self::assertSame(0, $buffer->size()); + } + + public function testStringMap() + { + $buffer = new Buffer; + $buffer->appendStringMap([ + 'a' => 'abcd', + 'b' => 'fegh', + ]); + + self::assertSame(20, $buffer->size()); + self::assertSame(2, $buffer->consumeShort()); + self::assertSame('a', $buffer->consumeString()); + self::assertSame('abcd', $buffer->consumeString()); + self::assertSame('b', $buffer->consumeString()); + self::assertSame('fegh', $buffer->consumeString()); + self::assertSame(0, $buffer->size()); + } + + public function testSimpleValues() + { + $values = [ + null, + true, + false, + 0, + 10, + 0.2, + 'abcd', + [ + 'a', + 'b' + ], + [ + 'a' => 'b', + 'c' => 'd', + ], + new \DateTime, + Uuid::fromString('550e8400-e29b-41d4-a716-446655440000'), + ]; + + $buffer = new Buffer; + + foreach ($values as $value) { + $buffer->appendValue($value); + } + + self::assertSame(124, $buffer->size()); + } +// +// public function testCustomValues() +// { +// $values = [ +// ]; +// +// $buffer = new Buffer; +// +// foreach ($values as $value) { +// $buffer->appendValue($value); +// } +// } +} diff --git a/tests/CassisTest.php b/tests/CassisTest.php index 845874d..154a2da 100644 --- a/tests/CassisTest.php +++ b/tests/CassisTest.php @@ -10,40 +10,11 @@ namespace PHPinnacle\Cassis\Tests; -use function Amp\call; -use Amp\Loop; -use Amp\PHPUnit\TestCase; use Amp\Promise; +use PHPUnit\Framework\TestCase; abstract class CassisTest extends TestCase { - /** - * @param callable $callable - * - * @return void - */ - public static function loop(callable $callable): void - { - Loop::run(function () use ($callable) { - yield call($callable); - - $info = Loop::getInfo(); - $count = $info['enabled_watchers']['referenced']; - - if ($count !== 0) { - $message = "Still have {$count} loop watchers."; - - foreach (['defer', 'delay', 'repeat', 'on_readable', 'on_writable'] as $key) { - $message .= " {$key} - {$info[$key]['enabled']}."; - } - - self::markTestIncomplete($message); - - Loop::stop(); - } - }); - } - /** * @param mixed $value * @@ -53,24 +24,4 @@ public static function assertPromise($value): void { self::assertInstanceOf(Promise::class, $value); } - - /** - * @param mixed $value - * - * @return void - */ - public static function assertInteger($value): void - { - self::assertInternalType('int', $value); - } - - /** - * @param mixed $value - * - * @return void - */ - public static function assertArray($value): void - { - self::assertInternalType('array', $value); - } } diff --git a/tests/ClusterTest.php b/tests/ClusterTest.php new file mode 100644 index 0000000..c861e27 --- /dev/null +++ b/tests/ClusterTest.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use Amp\Loop; +use PHPinnacle\Cassis\Cluster; +use PHPinnacle\Cassis\Exception\ClientException; +use PHPinnacle\Cassis\Exception\ServerException; +use PHPinnacle\Cassis\Session; + +class ClusterTest extends AsyncTest +{ + public static function setUpBeforeClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + yield $session->query( + "CREATE KEYSPACE IF NOT EXISTS simplex + WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1 };" + ); + + yield $cluster->disconnect(); + }); + } + + public function testBuild() + { + $cluster = Cluster::build('tcp://localhost:9042'); + + self::assertInstanceOf(Cluster::class, $cluster); + } + + public function testOptions() + { + $cluster = self::cluster(); + $promise = $cluster->options(); + + self::assertPromise($promise); + + $options = yield $promise; + + self::assertIsArray($options); + + foreach ($options as $option => $values) { + self::assertIsArray($values); + } + + yield $cluster->disconnect(); + } + + public function testConnect() + { + $cluster = self::cluster(); + $promise = $cluster->connect(); + + self::assertPromise($promise); + self::assertFalse($cluster->isConnected()); + + $session = yield $promise; + + self::assertInstanceOf(Session::class, $session); + self::assertTrue($cluster->isConnected()); + + yield $cluster->disconnect(); + } + + public function testConnectWithKeyspace() + { + $cluster = self::cluster(); + $session = yield $cluster->connect('simplex'); + + $ref = new \ReflectionProperty(Session::class, 'keyspace'); + $ref->setAccessible(true); + + self::assertSame('simplex', $ref->getValue($session)); + + yield $cluster->disconnect(); + } + + public function testDisconnectTwice() + { + $cluster = self::cluster(); + + yield $cluster->connect(); + + yield $cluster->disconnect(); + yield $cluster->disconnect(); + + self::assertFalse($cluster->isConnected()); + } + + public function testConnectTwice() + { + $this->expectException(ClientException::class); + + try { + $cluster = self::cluster(); + + yield $cluster->connect(); + yield $cluster->connect(); + } finally { + yield $cluster->disconnect(); + } + } + + public function testConnectWithUnknownKeyspace() + { + $this->expectException(ServerException::class); + + $cluster = self::cluster(); + + yield $cluster->connect('unknown'); + } + + public function testConnectFailure() + { + $this->expectException(ClientException::class); + + $cluster = cluster::build('tcp://localhost:19042'); + + yield $cluster->connect(); + } + + public static function tearDownAfterClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + yield $session->query("DROP KEYSPACE IF EXISTS simplex;"); + + yield $cluster->disconnect(); + }); + } +} diff --git a/tests/CollectionsTest.php b/tests/CollectionsTest.php new file mode 100644 index 0000000..3be7743 --- /dev/null +++ b/tests/CollectionsTest.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use Amp\Loop; +use PHPinnacle\Cassis\Session; +use PHPinnacle\Cassis\Value\Collection; + +class CollectionsTest extends AsyncTest +{ + public static function setUpBeforeClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + $cql = " + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1 }; + USE simplex; + CREATE TABLE collections ( + id int PRIMARY KEY, + list_value list, + set_value set, + map_value map + )"; + + foreach (\explode(';', $cql) as $query) { + yield $session->query(\trim($query)); + } + + yield $cluster->disconnect(); + }); + } + + public function testDataTypes() + { + $cluster = self::cluster(); + + /** @var Session $session */ + $session = yield $cluster->connect("simplex"); + + $list = Collection::list(['a','b','c']); + $set = Collection::set(['a','b','c','c']); + $map = Collection::assoc(['a' => 1,'b' => 2,'c' => 3]); + + $arguments = [ + 'id' => 1, + 'list_value' => $list, + 'set_value' => $set, + 'map_value' => $map, + ]; + + $fields = \implode(',', \array_keys($arguments)); + $values = \implode(',', \array_fill(0, \count($arguments), '?')); + $result = yield $session->query("INSERT INTO collections ($fields) VALUES ($values)", $arguments); + + self::assertNull($result); + + $rows = yield $session->query("SELECT * FROM collections"); + $result = $rows[0]; + + self::assertEquals($list->values(), $result['list_value']->values()); + self::assertEquals($list->keys(), $result['list_value']->keys()); + self::assertEquals($set->values(), $result['set_value']->values()); + self::assertEquals($set->keys(), $result['set_value']->keys()); + self::assertEquals($map->values(), $result['map_value']->values()); + self::assertEquals($map->keys(), $result['map_value']->keys()); + + yield $cluster->disconnect(); + } + + public static function tearDownAfterClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + yield $session->query("DROP KEYSPACE simplex;"); + + yield $cluster->disconnect(); + }); + } +} diff --git a/tests/ColumnTest.php b/tests/ColumnTest.php new file mode 100644 index 0000000..ee657cb --- /dev/null +++ b/tests/ColumnTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Column; +use PHPinnacle\Cassis\Type; + +class ColumnTest extends CassisTest +{ + public function testCreate() + { + $type = new Type\Base(Type::CUSTOM); + $column = new Column('simplex', 'table', 'name', $type); + + self::assertEquals('simplex', $column->keyspace()); + self::assertEquals('table', $column->table()); + self::assertEquals('name', $column->name()); + self::assertEquals($type, $column->type()); + } + + public function testFullFromBuffer() + { + $buffer = new Buffer; + $buffer + ->appendString('simplex') + ->appendString('table') + ->appendString('name') + ->appendShort(Type::INT) + ; + + $column = Column::full($buffer); + + self::assertEquals('simplex', $column->keyspace()); + self::assertEquals('table', $column->table()); + self::assertEquals('name', $column->name()); + self::assertEquals(new Type\Base(Type::INT), $column->type()); + } + + public function testPartialFromBuffer() + { + $buffer = new Buffer; + $buffer + ->appendString('name') + ->appendShort(Type::INT) + ; + + $column = Column::partial('simplex', 'table', $buffer); + + self::assertEquals('simplex', $column->keyspace()); + self::assertEquals('table', $column->table()); + self::assertEquals('name', $column->name()); + self::assertEquals(new Type\Base(Type::INT), $column->type()); + } + + public function testValue() + { + $buffer = new Buffer; + $buffer + ->appendInt(4) + ->appendInt(123) + ; + + $type = new Type\Base(Type::INT); + $column = new Column('simplex', 'table', 'name', $type); + + self::assertEquals(123, $column->value($buffer)); + } +} diff --git a/tests/Compressor/LzCompressorTest.php b/tests/Compressor/LzCompressorTest.php new file mode 100644 index 0000000..5bb36f9 --- /dev/null +++ b/tests/Compressor/LzCompressorTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Compressor; + +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Compressor; + +class LzCompressorTest extends CassisTest +{ + public function testCompressDecompress() + { + if (\extension_loaded('lz4') === false) { + self::expectNotToPerformAssertions(); + + return; + } + + $compressor = new Compressor\LzCompressor; + + $binary = $compressor->compress('abc'); + + self::assertEquals('abc', $compressor->decompress($binary)); + } +} diff --git a/tests/Compressor/NoneCompressorTest.php b/tests/Compressor/NoneCompressorTest.php new file mode 100644 index 0000000..cd55d84 --- /dev/null +++ b/tests/Compressor/NoneCompressorTest.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Compressor; + +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Compressor; + +class NoneCompressorTest extends CassisTest +{ + public function testCompressDecompress() + { + $compressor = new Compressor\NoneCompressor; + + self::assertEquals('abc', $compressor->compress('abc')); + self::assertEquals('abc', $compressor->decompress('abc')); + } +} diff --git a/tests/Compressor/SnappyCompressorTest.php b/tests/Compressor/SnappyCompressorTest.php new file mode 100644 index 0000000..fca273f --- /dev/null +++ b/tests/Compressor/SnappyCompressorTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Compressor; + +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Compressor; + +class SnappyCompressorTest extends CassisTest +{ + public function testCompressDecompress() + { + if (\extension_loaded('snappy') === false) { + self::expectNotToPerformAssertions(); + + return; + } + + $compressor = new Compressor\SnappyCompressor; + + $binary = $compressor->compress('abc'); + + self::assertEquals('abc', $compressor->decompress($binary)); + } +} diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php new file mode 100644 index 0000000..c625ed4 --- /dev/null +++ b/tests/ConfigTest.php @@ -0,0 +1,140 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use PHPinnacle\Cassis\Config; +use PHPinnacle\Cassis\Exception\ConfigException; + +class ConfigTest extends CassisTest +{ + public function testCreate() + { + $config = new Config(['tcp://localhost:9042'], 'user', 'pass'); + + self::assertEquals(['tcp://localhost:9042'], $config->hosts()); + self::assertEquals('user', $config->user()); + self::assertEquals('pass', $config->password()); + } + + public function testNoUser() + { + $config = new Config(['tcp://localhost:9042']); + + self::assertNull($config->user()); + self::assertNull($config->password()); + } + + public function testTcpOptions() + { + $config = new Config(['tcp://localhost:9042']); + + self::assertEquals(1, $config->tcpAttempts()); + self::assertEquals(0, $config->tcpTimeout()); + self::assertEquals(false, $config->tcpNoDelay()); + + self::assertEquals(3, $config->tcpAttempts(3)); + self::assertEquals(10, $config->tcpTimeout(10)); + self::assertEquals(true, $config->tcpNoDelay(true)); + } + + public function testCassandraOptions() + { + $config = new Config(['tcp://localhost:9042']); + + self::assertEquals(false, $config->compatibility()); + self::assertEquals(Config::COMPRESSION_NONE, $config->compression()); + + self::assertEquals(true, $config->compatibility(true)); + self::assertEquals([ + 'CQL_VERSION' => '3.0.0', + 'NO_COMPACT' => false, + ], $config->options()); + + if (\extension_loaded('snappy')) { + self::assertEquals(Config::COMPRESSION_SNAPPY, $config->compression(Config::COMPRESSION_SNAPPY)); + self::assertEquals([ + 'CQL_VERSION' => '3.0.0', + 'COMPRESSION' => Config::COMPRESSION_SNAPPY, + 'NO_COMPACT' => false, + ], $config->options()); + } + + if (\extension_loaded('lz4')) { + self::assertEquals(Config::COMPRESSION_LZ4, $config->compression(Config::COMPRESSION_LZ4)); + self::assertEquals([ + 'CQL_VERSION' => '3.0.0', + 'COMPRESSION' => Config::COMPRESSION_LZ4, + 'NO_COMPACT' => false, + ], $config->options()); + } + } + + public function testParse() + { + $config = Config::parse('tcp://localhost:9042'); + + self::assertEquals(['tcp://localhost:9042'], $config->hosts()); + self::assertEquals(null, $config->user()); + self::assertEquals(null, $config->password()); + + $config = Config::parse('tcp://user:pass@localhost:9042'); + + self::assertEquals(['tcp://localhost:9042'], $config->hosts()); + self::assertEquals('user', $config->user()); + self::assertEquals('pass', $config->password()); + + $config = Config::parse('tcp://localhost:9042?tcp_nodelay=1&tcp_attempts=10&tcp_timeout=100'); + + self::assertEquals(true, $config->tcpNoDelay()); + self::assertEquals(10, $config->tcpAttempts()); + self::assertEquals(100, $config->tcpTimeout()); + + $config = Config::parse('tcp://localhost:9042?compression=none&compatibility=1'); + + self::assertEquals(Config::COMPRESSION_NONE, $config->compression()); + self::assertEquals(true, $config->compatibility()); + } + + public function testParseMultiHost() + { + $config = Config::parse('tcp://localhost:9041,127.0.0.2,127.0.0.3:9043'); + + self::assertEquals([ + 'tcp://localhost:9041', + 'tcp://127.0.0.2:9042', + 'tcp://127.0.0.3:9043', + ], $config->hosts()); + } + + public function testUnknownCompression() + { + $this->expectException(ConfigException::class); + $this->expectExceptionMessage('Unknown compression mechanism "gzip".'); + + $config = new Config(['tcp://localhost:9042']); + $config->compression('gzip'); + } + + public function testCompressionExtensionNotLoaded() + { + if (\extension_loaded('lz4')) { + self::expectNotToPerformAssertions(); + + return; + } + + $this->expectException(ConfigException::class); + $this->expectExceptionMessage('Extension for compression mechanism "lz4" not loaded.'); + + $config = new Config(['tcp://localhost:9042']); + $config->compression(Config::COMPRESSION_LZ4); + } +} diff --git a/tests/ContextTest.php b/tests/ContextTest.php new file mode 100644 index 0000000..30a2f42 --- /dev/null +++ b/tests/ContextTest.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Context; +use PHPinnacle\Cassis\Type; + +class ContextTest extends CassisTest +{ + public function testWriteNoParameters() + { + $context = new Context; + $buffer = new Buffer; + + $context->writeParameters($buffer); + + self::assertEquals(Context::CONSISTENCY_ALL, $context->consistency()); + self::assertEquals(Context::CONSISTENCY_ALL, $buffer->consumeShort()); + self::assertEquals(0, $buffer->consumeByte()); + } + + public function testConsistency() + { + $context = new Context; + + $context->consistencyAny(); + self::assertEquals(Context::CONSISTENCY_ANY, $context->consistency()); + + $context->consistencyOne(); + self::assertEquals(Context::CONSISTENCY_ONE, $context->consistency()); + + $context->consistencyTwo(); + self::assertEquals(Context::CONSISTENCY_TWO, $context->consistency()); + + $context->consistencyThree(); + self::assertEquals(Context::CONSISTENCY_THREE, $context->consistency()); + + $context->consistencyQuorum(); + self::assertEquals(Context::CONSISTENCY_QUORUM, $context->consistency()); + + $context->consistencyAll(); + self::assertEquals(Context::CONSISTENCY_ALL, $context->consistency()); + + $context->consistencyEachQuorum(); + self::assertEquals(Context::CONSISTENCY_EACH_QUORUM, $context->consistency()); + + $context->consistencyLocalQuorum(); + self::assertEquals(Context::CONSISTENCY_LOCAL_QUORUM, $context->consistency()); + + $context->consistencyLocalOne(); + self::assertEquals(Context::CONSISTENCY_LOCAL_ONE, $context->consistency()); + } + + public function testWriteParameters() + { + $context = new Context; + $buffer = new Buffer; + + $context + ->consistencyOne() + ->serialConsistency(Context::CONSISTENCY_LOCAL_SERIAL) + ->defaultTimestamp($time = time()) + ->limit(1000) + ->offset('state') + ->skipMetadata() + ->arguments([ + 'a' => 1, + 'b' => 2, + 'c' => 3, + ]) + ; + + $context->writeParameters($buffer); + + self::assertEquals(Context::CONSISTENCY_ONE, $buffer->consumeShort()); + + // Flags + self::assertEquals(127, $buffer->consumeByte()); + + // Values + self::assertEquals(3, $buffer->consumeShort()); + self::assertEquals('a', $buffer->consumeString()); + self::assertEquals(1, (new Type\Base(Type::INT))->read($buffer)); + self::assertEquals('b', $buffer->consumeString()); + self::assertEquals(2, (new Type\Base(Type::INT))->read($buffer)); + self::assertEquals('c', $buffer->consumeString()); + self::assertEquals(3, (new Type\Base(Type::INT))->read($buffer)); + + // Paging + self::assertEquals(1000, $buffer->consumeInt()); + self::assertEquals('state', $buffer->consumeLongString()); + + // Serial consistency + self::assertEquals(Context::CONSISTENCY_LOCAL_SERIAL, $buffer->consumeShort()); + + // Default timestamp + self::assertEquals($time, $buffer->consumeLong()); + } + + public function testArgumentsList() + { + $context = new Context; + $buffer = new Buffer; + + $context->arguments([1, true, 'yes']); + + $context->writeParameters($buffer); + + self::assertEquals(Context::CONSISTENCY_ALL, $buffer->consumeShort()); + + $flags = $buffer->consumeByte(); + + self::assertEquals(1, $flags); + self::assertEquals(1, $flags & Context::FLAG_VALUES); + self::assertEquals(0, $flags & Context::FLAG_NAMES_FOR_VALUES); + + // Values + self::assertEquals(3, $buffer->consumeShort()); + self::assertEquals(1, (new Type\Base(Type::INT))->read($buffer)); + self::assertEquals(true, (new Type\Base(Type::BOOLEAN))->read($buffer)); + self::assertEquals('yes', (new Type\Base(Type::TEXT))->read($buffer)); + } + + public function testEmptyArguments() + { + $context = new Context; + $buffer = new Buffer; + + $context->arguments([]); + + $context->writeParameters($buffer); + + self::assertEquals(Context::CONSISTENCY_ALL, $buffer->consumeShort()); + + $flags = $buffer->consumeByte(); + + self::assertEquals(0, $flags); + self::assertEquals(0, $flags & Context::FLAG_VALUES); + self::assertEquals(0, $flags & Context::FLAG_NAMES_FOR_VALUES); + } +} diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php new file mode 100644 index 0000000..89a553e --- /dev/null +++ b/tests/FunctionsTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +class FunctionsTest extends CassisTest +{ + /** + * @dataProvider assocProvider + * + * @param array $data + */ + public function testIsAssoc(array $data) + { + self::assertTrue(\is_assoc($data)); + } + + public function testBigintFromNotNumeric() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Value "qwe" not numeric'); + + bigint_init("qwe"); + } + + public function assocProvider(): array + { + return [ + [['a' => 1, 'b' => 0]], + [[1 => 1, 2 => 0]], + [[1 => 1, 'a' => 0]], + [[1 => 1, 0]], + ]; + } +} diff --git a/tests/MetadataTest.php b/tests/MetadataTest.php new file mode 100644 index 0000000..bbf8f72 --- /dev/null +++ b/tests/MetadataTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Column; +use PHPinnacle\Cassis\Metadata; +use PHPinnacle\Cassis\Type; + +class MetadataTest extends CassisTest +{ + public function testCreate() + { + $columns = [ + new Column('simplex', 'table', 'int', new Type\Base(Type::INT)), + new Column('simplex', 'table', 'bool', new Type\Base(Type::BOOLEAN)) + ]; + + $metadata = new Metadata($columns, 'cursor'); + + self::assertEquals($columns, $metadata->columns()); + self::assertEquals('cursor', $metadata->cursor()); + } + + public function testCreateFromBufferWithPaging() + { + $buffer = new Buffer; + $buffer + ->appendInt(Metadata::FLAG_HAS_MORE_PAGES) + ->appendInt(0) + ->appendLongString('cursor') + ; + + $metadata = Metadata::create($buffer); + + self::assertEquals([], $metadata->columns()); + self::assertEquals('cursor', $metadata->cursor()); + } + + public function testCreateFromBufferWithoutColumns() + { + $buffer = new Buffer; + $buffer + ->appendInt(Metadata::FLAG_NO_METADATA) + ->appendInt(0) + ; + + $metadata = Metadata::create($buffer); + + self::assertEquals([], $metadata->columns()); + self::assertEquals(null, $metadata->cursor()); + } + + public function testCreateFromBufferWithGlobalColumns() + { + $buffer = new Buffer; + $buffer + ->appendInt(Metadata::FLAG_GLOBAL_TABLES_SPEC) + ->appendInt(2) + ->appendString('simplex') + ->appendString('table') + ->appendString('int') + ->appendShort(Type::INT) + ->appendString('bool') + ->appendShort(Type::BOOLEAN) + ; + + $metadata = Metadata::create($buffer); + + self::assertEquals([ + new Column('simplex', 'table', 'int', new Type\Base(Type::INT)), + new Column('simplex', 'table', 'bool', new Type\Base(Type::BOOLEAN)) + ], $metadata->columns()); + } + + public function testCreateFromBufferWithLocalColumns() + { + $buffer = new Buffer; + $buffer + ->appendInt(0) + ->appendInt(2) + ->appendString('simplex') + ->appendString('table') + ->appendString('int') + ->appendShort(Type::INT) + ->appendString('simplex') + ->appendString('table') + ->appendString('bool') + ->appendShort(Type::BOOLEAN) + ; + + $metadata = Metadata::create($buffer); + + self::assertEquals([ + new Column('simplex', 'table', 'int', new Type\Base(Type::INT)), + new Column('simplex', 'table', 'bool', new Type\Base(Type::BOOLEAN)) + ], $metadata->columns()); + } +} diff --git a/tests/SessionTest.php b/tests/SessionTest.php new file mode 100644 index 0000000..3066ca2 --- /dev/null +++ b/tests/SessionTest.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use Amp\Loop; +use PHPinnacle\Cassis\Context; +use PHPinnacle\Cassis\Result; +use PHPinnacle\Cassis\Session; +use PHPinnacle\Cassis\Statement; + +class SessionTest extends AsyncTest +{ + public static function setUpBeforeClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + yield $session->query( + "CREATE KEYSPACE IF NOT EXISTS simplex + WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1 };" + ); + + yield $cluster->disconnect(); + }); + } + + public function testChangeKeyspace() + { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + yield $session->query( + "CREATE KEYSPACE IF NOT EXISTS simplex_2 + WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1 };" + ); + + self::assertSame('simplex_2', yield $session->keyspace('simplex_2')); + + yield $session->query("DROP KEYSPACE simplex_2;"); + + yield $cluster->disconnect(); + } + + public function testSimpleQueries() + { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect('simplex'); + + yield $session->query("CREATE TABLE simple (id int PRIMARY KEY, enabled boolean)"); + + $result1 = yield $session->query("INSERT INTO simple (id, enabled) VALUES (1, True)"); + $result2 = yield $session->query("INSERT INTO simple (id, enabled) VALUES (?, ?)", [2, false]); + $result3 = yield $session->query("INSERT INTO simple (id, enabled) VALUES (:id, :enabled)", [ + 'id' => 3, + 'enabled' => true, + ]); + + self::assertNull($result1); + self::assertNull($result2); + self::assertNull($result3); + + $rows = yield $session->query("SELECT * FROM simple"); + + self::assertCount(3, $rows); + + self::assertEquals(1, $rows[0]['id']); + self::assertEquals(true, $rows[0]['enabled']); + self::assertEquals(2, $rows[1]['id']); + self::assertEquals(false, $rows[1]['enabled']); + self::assertEquals(3, $rows[2]['id']); + self::assertEquals(true, $rows[2]['enabled']); + + yield $cluster->disconnect(); + } + + public function testPaging() + { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect('simplex'); + + yield $session->query( + "CREATE TABLE paging ( + id int, + ordering int, + enabled boolean, + PRIMARY KEY ((id), ordering) + ) WITH CLUSTERING ORDER BY (ordering ASC)" + ); + + for ($i = 1; $i <= 5; $i++) { + yield $session->query("INSERT INTO paging (id, ordering, enabled) VALUES (1, {$i}, True)"); + } + + $context = new Context; + $context->limit(3); + + /** @var Result\Rows $rows1 */ + $rows1 = yield $session->query("SELECT * FROM paging", [], $context); + + self::assertCount(3, $rows1); + self::assertEquals(1, $rows1[0]['ordering']); + self::assertEquals(2, $rows1[1]['ordering']); + self::assertEquals(3, $rows1[2]['ordering']); + + $context->offset($rows1->cursor()); + + /** @var Result\Rows $rows2 */ + $rows2 = yield $session->query("SELECT * FROM paging", [], $context); + + self::assertCount(2, $rows2); + self::assertEquals(4, $rows2[0]['ordering']); + self::assertEquals(5, $rows2[1]['ordering']); + self::assertNull($rows2->cursor()); + + yield $cluster->disconnect(); + } + + public function testPreparedQueries() + { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect('simplex'); + + yield $session->query("CREATE TABLE prepared (id int PRIMARY KEY, enabled boolean)"); + + /** @var Result\Prepared $prepared */ + $prepared = yield $session->prepare("INSERT INTO prepared (id, enabled) VALUES (?, ?)"); + + $result1 = yield $session->execute(new Statement\Prepared($prepared->id(), [1, true])); + $result2 = yield $session->execute(new Statement\Prepared($prepared->id(), [2, false])); + + self::assertNull($result1); + self::assertNull($result2); + + $rows = yield $session->query("SELECT * FROM prepared"); + + self::assertCount(2, $rows); + + self::assertEquals(1, $rows[0]['id']); + self::assertEquals(true, $rows[0]['enabled']); + self::assertEquals(2, $rows[1]['id']); + self::assertEquals(false, $rows[1]['enabled']); + + yield $cluster->disconnect(); + } +// +// public function testBatchQueries() +// { +// self::loop(function () { +// $cluster = self::cluster(); +// /** @var Session $session */ +// $session = yield $cluster->connect('simplex'); +// +// yield $session->query("CREATE TABLE batched (id int PRIMARY KEY, enabled boolean)"); +// +// $statement = Statement\Batch::logged( +// new Statement\Simple('INSERT INTO batched (id, enabled) VALUES (1, True)'), +// new Statement\Simple('INSERT INTO batched (id, enabled) VALUES (2, False)') +// ); +// +// $result = yield $session->execute($statement); +// +// self::assertNull($result); +// +// $rows = yield $session->query("SELECT * FROM batched"); +// +// self::assertCount(2, $rows); +// +// self::assertEquals(1, $rows[0]['id']); +// self::assertEquals(true, $rows[0]['enabled']); +// self::assertEquals(2, $rows[1]['id']); +// self::assertEquals(false, $rows[1]['enabled']); +// +// yield $cluster->disconnect(); +// }); +// } +// +// public function testRegisterEvent() +// { +// self::loop(function () { +// $cluster = self::cluster(); +// /** @var Session $session */ +// $session = yield $cluster->connect('simplex'); +// +// yield $session->query( +// "CREATE TABLE simple (id int PRIMARY KEY, enabled boolean);" +// ); +// +// yield $session->register(Event::SCHEMA_CHANGE, function (Event $event) use ($cluster) { +// var_dump($event); +// }); +// +// yield $session->query('ALTER TABLE simple ADD name text;'); +// +// yield $cluster->disconnect(); +// }); +// } + + public static function tearDownAfterClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + yield $session->query("DROP KEYSPACE IF EXISTS simplex;"); + + yield $cluster->disconnect(); + }); + } +} diff --git a/tests/Statement/BatchTest.php b/tests/Statement/BatchTest.php new file mode 100644 index 0000000..7a658c8 --- /dev/null +++ b/tests/Statement/BatchTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Statement; + +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Statement; + +class BatchTest extends CassisTest +{ + public function testCreate() + { + $queries = [ + new Statement\Simple("SELECT * FROM table WHERE id = ?", [1]), + new Statement\Simple("SELECT * FROM table WHERE id = ?", [2]) + ]; + + $statement = Statement\Batch::logged(...$queries); + + self::assertEquals(0, $statement->type()); + self::assertEquals($queries, $statement->queries()); + + $statement = Statement\Batch::unlogged(...$queries); + + self::assertEquals(1, $statement->type()); + self::assertEquals($queries, $statement->queries()); + + $statement = Statement\Batch::counter(...$queries); + + self::assertEquals(2, $statement->type()); + self::assertEquals($queries, $statement->queries()); + } +} diff --git a/tests/Statement/PreparedTest.php b/tests/Statement/PreparedTest.php new file mode 100644 index 0000000..5ae5f1d --- /dev/null +++ b/tests/Statement/PreparedTest.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Statement; + +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Statement; + +class PreparedTest extends CassisTest +{ + public function testCreate() + { + $id = "2b34b8b0-39b8-11e9-b210-d663bd873d93"; + $arguments = [1]; + + $statement = new Statement\Prepared($id, $arguments); + + self::assertEquals($id, $statement->id()); + self::assertEquals($arguments, $statement->values()); + } +} diff --git a/tests/Statement/SimpleTest.php b/tests/Statement/SimpleTest.php new file mode 100644 index 0000000..83fba36 --- /dev/null +++ b/tests/Statement/SimpleTest.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Statement; + +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Statement; + +class SimpleTest extends CassisTest +{ + public function testCreate() + { + $cql = "SELECT * FROM table WHERE id = ?"; + $arguments = [1]; + + $statement = new Statement\Simple($cql, $arguments); + + self::assertEquals($cql, $statement->cql()); + self::assertEquals($arguments, $statement->values()); + } +} diff --git a/tests/StreamsTest.php b/tests/StreamsTest.php new file mode 100644 index 0000000..510a3ed --- /dev/null +++ b/tests/StreamsTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use PHPinnacle\Cassis\Streams; + +class StreamsTest extends CassisTest +{ + public function testCreate() + { + $streams1 = Streams::instance(); + $streams2 = Streams::instance(); + + self::assertSame($streams1, $streams2); + } + + public function testReserveRelease() + { + $streams = Streams::instance(); + + self::assertSame(1, $streams->reserve()); + self::assertSame(2, $streams->reserve()); + self::assertSame(3, $streams->reserve()); + + $streams->release(3); + $streams->release(2); + $streams->release(1); + + self::assertSame(1, $streams->reserve()); + self::assertSame(2, $streams->reserve()); + self::assertSame(3, $streams->reserve()); + self::assertSame(4, $streams->reserve()); + } +} diff --git a/tests/TypesTest.php b/tests/TypesTest.php new file mode 100644 index 0000000..74294f1 --- /dev/null +++ b/tests/TypesTest.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use Amp\Loop; +use PHPinnacle\Cassis\Session; +use PHPinnacle\Cassis\Value\Blob; +use PHPinnacle\Cassis\Value\Date; +use PHPinnacle\Cassis\Value\Decimal; +use PHPinnacle\Cassis\Value\Double; +use PHPinnacle\Cassis\Value\Inet; +use PHPinnacle\Cassis\Value\Integer; +use PHPinnacle\Cassis\Value\Time; +use PHPinnacle\Cassis\Value\Timestamp; +use PHPinnacle\Cassis\Value\Varint; +use Ramsey\Uuid\Uuid; + +class TypesTest extends AsyncTest +{ + public static function setUpBeforeClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + $cql = " + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1 }; + USE simplex; + CREATE TABLE values ( + id int PRIMARY KEY, + boolean_value boolean, + tinyint_value tinyint, + smallint_value smallint, + int_value int, + bigint_value bigint, + varint_value varint, + float_value float, + double_value double, + decimal_value decimal, + timestamp_value timestamp, + date_value date, + time_value time, + uuid_value uuid, + timeuuid_value timeuuid, + inet_value inet, + blob_value blob + )"; + + foreach (\explode(';', $cql) as $query) { + yield $session->query(\trim($query)); + } + + yield $cluster->disconnect(); + }); + } + + public function testDataTypes() + { + $cluster = self::cluster(); + + /** @var Session $session */ + $session = yield $cluster->connect("simplex"); + + $arguments = [ + 'id' => 1, + 'boolean_value' => true, + 'tinyint_value' => Integer::tiny(127), + 'smallint_value' => Integer::small(512), + 'int_value' => 8193, + 'bigint_value' => Integer::big(-765438000), + 'varint_value' => Varint::fromString('67890656781923123918798273492834712837198237'), + 'float_value' => 3.14, + 'double_value' => Double::fromFloat(3.141592653589793), + 'decimal_value' => Decimal::fromString('1313123123234234234234234234123', 21), + 'timestamp_value' => Timestamp::fromMicroSeconds(1425691864001), + 'date_value' => Date::fromDateTime(new \DateTimeImmutable('2017-05-05')), + 'time_value' => Time::fromDateTime(new \DateTimeImmutable('13:29:01.050')), + 'uuid_value' => Uuid::fromString('ab3352d9-4f7f-4007-a35a-e62aa7ab0b19'), + 'timeuuid_value' => Uuid::fromString('7f0a920f-c7fd-11e4-7f7f-7f7f7f7f7f7f'), + 'inet_value' => Inet::fromString('200.199.198.197'), + 'blob_value' => Blob::fromString('0x000000'), + ]; + + $fields = \implode(',', \array_keys($arguments)); + $values = \implode(',', \array_fill(0, \count($arguments), '?')); + $result = yield $session->query("INSERT INTO values ($fields) VALUES ($values)", $arguments); + + self::assertNull($result); + + $rows = yield $session->query("SELECT * FROM values"); + $result = $rows[0]; + + self::assertEquals(true, $result['boolean_value']); + self::assertEquals(127, $result['tinyint_value']); + self::assertEquals(512, $result['smallint_value']); + self::assertEquals(8193, $result['int_value']); + self::assertEquals(-765438000, $result['bigint_value']); + self::assertEquals('67890656781923123918798273492834712837198237', (string) $result['varint_value']); + self::assertEquals(3.14, round($result['float_value'], 2)); + self::assertEquals(3.1415926535898, $result['double_value']); + self::assertEquals('1313123123.234234234234234234123', (string) $result['decimal_value']); + self::assertEquals(1425691864001, $result['timestamp_value']->value()); + self::assertEquals('2017-05-05', $result['date_value']->toDateTime()->format('Y-m-d')); + self::assertEquals('13:29:01.050000', $result['time_value']->toDateTime()->format('H:i:s.u')); + self::assertEquals('ab3352d9-4f7f-4007-a35a-e62aa7ab0b19', (string) $result['uuid_value']); + self::assertEquals('7f0a920f-c7fd-11e4-7f7f-7f7f7f7f7f7f', (string) $result['timeuuid_value']); + self::assertEquals('200.199.198.197', (string) $result['inet_value']); + self::assertEquals('0x3078303030303030', $result['blob_value']->bytes()); + + yield $cluster->disconnect(); + } + + public static function tearDownAfterClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + yield $session->query("DROP KEYSPACE simplex;"); + + yield $cluster->disconnect(); + }); + } +} diff --git a/tests/UserTypesTest.php b/tests/UserTypesTest.php new file mode 100644 index 0000000..71e69de --- /dev/null +++ b/tests/UserTypesTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests; + +use Amp\Loop; +use PHPinnacle\Cassis\Session; +use PHPinnacle\Cassis\Value; + +class UserTypesTest extends AsyncTest +{ + public static function setUpBeforeClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + $cql = " + CREATE KEYSPACE simplex WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1 }; + USE simplex; + CREATE TYPE IF NOT EXISTS user ( + id int, + name text + ); + CREATE TYPE IF NOT EXISTS comment ( + text text, + author frozen + ); + CREATE TABLE comments ( + id int PRIMARY KEY, + comment comment + )"; + + foreach (\explode(';', $cql) as $query) { + yield $session->query(\trim($query)); + } + + yield $cluster->disconnect(); + }); + } + + public function testDataTypes() + { + $cluster = self::cluster(); + + /** @var Session $session */ + $session = yield $cluster->connect("simplex"); + + $user = new Value\UserDefined([ + 'id' => 1, + 'name' => 'John Doe', + ]); + + $comment = new Value\UserDefined([ + 'text' => 'Cassandra is cool!', + 'author' => $user, + ]); + + $arguments = [ + 'id' => 1, + 'comment' => $comment, + ]; + + $fields = \implode(',', \array_keys($arguments)); + $values = \implode(',', \array_fill(0, \count($arguments), '?')); + $result = yield $session->query("INSERT INTO comments ($fields) VALUES ($values)", $arguments); + + self::assertNull($result); + + $rows = yield $session->query("SELECT * FROM comments"); + $result = $rows[0]; + + self::assertIsArray($result); + self::assertArrayHasKey('comment', $result); + self::assertEquals($comment, $result['comment']); + + yield $cluster->disconnect(); + } + + public static function tearDownAfterClass(): void + { + Loop::run(function () { + $cluster = self::cluster(); + /** @var Session $session */ + $session = yield $cluster->connect(); + + yield $session->query("DROP KEYSPACE simplex;"); + + yield $cluster->disconnect(); + }); + } +} diff --git a/tests/Value/BlobTest.php b/tests/Value/BlobTest.php new file mode 100644 index 0000000..998eda0 --- /dev/null +++ b/tests/Value/BlobTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Blob; + +class BlobTest extends CassisTest +{ + public function testHexEncodesString() + { + $blob = Blob::fromString("Hi"); + + $this->assertEquals("Hi", $blob->__toString()); + $this->assertEquals([72, 105], $blob->values()); + $this->assertEquals("0x4869", $blob->bytes()); + } + + public function testWrite() + { + $buffer = new Buffer; + $value = Blob::fromString("Hi"); + + $buffer->appendValue($value); + + self::assertEquals($value, (new Base(Base::BLOB))->read($buffer)); + } +} diff --git a/tests/Value/CollectionTest.php b/tests/Value/CollectionTest.php new file mode 100644 index 0000000..8132e28 --- /dev/null +++ b/tests/Value/CollectionTest.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Collection; + +class CollectionTest extends CassisTest +{ + public function testList() + { + $values = [1,2,3]; + $list = Collection::list($values); + + self::assertEquals($values, $list->values()); + self::assertEquals(\array_keys($values), $list->keys()); + + $values = [1,2,3,3]; + $set = Collection::set($values); + + self::assertEquals([1,2,3], $set->values()); + self::assertEquals([0,1,2], $set->keys()); + + $values = ['a' => 1, 'b' => 2]; + $set = Collection::assoc($values); + + self::assertEquals(\array_values($values), $set->values()); + self::assertEquals(\array_keys($values), $set->keys()); + } + + public function testIterate() + { + $values = [1,2,3]; + $list = Collection::list($values); + + foreach ($list as $key => $value) { + self::assertEquals($values[$key], $value); + } + } + + public function testCount() + { + $list1 = Collection::list([1,2,3]); + $list2 = Collection::list([]); + + self::assertCount(3, $list1); + self::assertCount(0, $list2); + } + + public function testExists() + { + $list = Collection::list([1,2,3]); + + self::assertTrue(isset($list[1])); + self::assertFalse(isset($list[3])); + } + + public function testGet() + { + $list = Collection::list([1,2,3]); + + self::assertEquals(1, $list[0]); + self::assertEquals(2, $list[1]); + self::assertEquals(3, $list[2]); + } + + public function testUnset() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Collection is immutable.'); + + $list = Collection::list([1,2,3]); + + unset($list[1]); + } + + public function testSet() + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Collection is immutable.'); + + $list = Collection::list([1,2,3]); + $list[1] = 5; + } + + public function testWrite() + { + $buffer = new Buffer; + + $list = Collection::list([1,2,3]); + $set = Collection::set([1,2,3]); + $map = Collection::assoc(['a' => 1, 'b' => 2]); + + $buffer + ->appendValue($list) + ->appendValue($set) + ->appendValue($map) + ; + + self::assertEquals($list, (Type\Collection::list(new Base(Base::INT)))->read($buffer)); + self::assertEquals($set, (Type\Collection::set(new Base(Base::INT)))->read($buffer)); + self::assertEquals($map, (Type\Collection::map(new Base(Base::TEXT), new Base(Base::INT))->read($buffer))); + self::assertEquals(0, $buffer->size()); + } +} diff --git a/tests/Value/CounterTest.php b/tests/Value/CounterTest.php new file mode 100644 index 0000000..f82536a --- /dev/null +++ b/tests/Value/CounterTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Counter; + +class CounterTest extends CassisTest +{ + public function testCreate() + { + $counter = new Counter(100); + + self::assertEquals(100, $counter->value()); + } + + public function testWrite() + { + $buffer = new Buffer; + $value = new Counter(100); + + $buffer->appendValue($value); + + self::assertEquals($value, (new Base(Base::COUNTER))->read($buffer)); + } +} diff --git a/tests/Value/DateTest.php b/tests/Value/DateTest.php new file mode 100644 index 0000000..9d8651b --- /dev/null +++ b/tests/Value/DateTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Value\Date; +use PHPinnacle\Cassis\Type\Base; + +class DateTest extends CassisTest +{ + public function testConstruct() + { + $datetime = new \DateTime("1970-01-01T00:00:00+0000"); + + $date1 = new Date(0); + $date2 = Date::fromSeconds(2 ** 31); + + self::assertEquals(0, $date1->value()); + self::assertEquals(0, $date2->value()); + self::assertEquals($datetime, $date1->toDateTime()); + self::assertEquals($datetime, $date2->toDateTime()); + } + + public function testFromDateTime() + { + $datetime = new \DateTime("1970-01-01T00:00:00+0000"); + $date = Date::fromDateTime($datetime); + self::assertEquals(0, $date->value()); + self::assertEquals($datetime, $date->toDateTime()); + + $datetime = new \DateTime("1970-01-02T00:00:00+0000"); + $date = Date::fromDateTime($datetime); + self::assertEquals(1, $date->value()); + self::assertEquals($datetime, $date->toDateTime()); + + $datetime = new \DateTime("1969-12-31T00:00:00+0000"); + $date = Date::fromDateTime($datetime); + self::assertEquals(-1, $date->value()); + self::assertEquals($datetime, $date->toDateTime()); + } + + public function testWrite() + { + $buffer = new Buffer; + + $datetime = new \DateTime("1970-01-02T00:00:00+0000"); + $value = Date::fromDateTime($datetime); + + $buffer->appendValue($value); + + self::assertEquals($value, (new Base(Base::DATE))->read($buffer)); + self::assertEquals(0, $buffer->size()); + } +} diff --git a/tests/Value/DecimalTest.php b/tests/Value/DecimalTest.php new file mode 100644 index 0000000..5bc7bbf --- /dev/null +++ b/tests/Value/DecimalTest.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Decimal; + +class DecimalTest extends CassisTest +{ + /** + * @dataProvider validStrings + * + * @param $input + * @param $value + * @param $scale + * @param $string + */ + public function testFromString($input, $value, $scale, $string) + { + $number = Decimal::fromString($input, $scale); + + self::assertEquals($value, $number->value()); + self::assertEquals($scale, $number->scale()); + self::assertEquals($string, (string) $number); + } + + public function testWrite() + { + $buffer = new Buffer; + $value = Decimal::fromString("123123", 2); + + $buffer->appendValue($value); + + self::assertEquals($value, (new Base(Base::DECIMAL))->read($buffer)); + } + + /** + * @return array + */ + public function validStrings(): array + { + return [ + ["123", "123", 0, "123"], + ["0123", "83", 0, "83"], + ["0x123", "291", 0, "291"], + ["0b1010101", "85", 0, "85"], + ["-123", "-123", 0, "-123"], + ["-0123", "-83", 0, "-83"], + ["-0x123", "-291", 0, "-291"], + ["-0b1010101", "-85", 0, "-85"], + ["1313123123234234234234234234123", "1313123123234234234234234234123", 21, "1313123123.234234234234234234123"], + ["1231", "1231", 1, "123.1"], + ["5555", "5555", 2, "55.55"], + ["-123123", "-123123", 3, "-123.123"], + ["5", "5", 1, "0.5"], + ["95", "95", 1, "9.5"], + ["-95", "-95", 1, "-9.5"], + ["1", "1", 5, "0.00001"], + ["-1", "-1", 5, "-0.00001"], + ["1", "1", 8, "0.00000001"], + ["-1", "-1", 8, "-0.00000001"], + ["95", "95", 9, "0.000000095"], + ["-95", "-95", 9, "-0.000000095"], + ["15", "15", 9, "0.000000015"], + ["-15", "-15", 9, "-0.000000015"], + ]; + } +} diff --git a/tests/Value/DoubleTest.php b/tests/Value/DoubleTest.php new file mode 100644 index 0000000..fb71768 --- /dev/null +++ b/tests/Value/DoubleTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Double; + +class DoubleTest extends CassisTest +{ + /** + * @dataProvider validStrings + * + * @param $input + * @param $value + */ + public function testConstruct($input, $value) + { + $number1 = Double::fromString($input); + $number2 = Double::fromFloat($value); + + self::assertEquals($value, $number1->value()); + self::assertEquals($input, (string) $number1); + self::assertEquals($value, $number2->value()); + self::assertEquals($input, (string) $number2); + } + + public function testThrowsWhenCreatingNotAnInteger() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Value "qwe" not numeric'); + + Double::fromString("qwe"); + } + + public function testWrite() + { + $buffer = new Buffer; + $value = Double::fromString("13.45"); + + $buffer->appendValue($value); + + self::assertEquals(13.45, (new Base(Base::DOUBLE))->read($buffer)); + } + + /** + * @return array + */ + public function validStrings(): array + { + return [ + ["13.45", 13.45], + ["3.14", 3.14], + ]; + } +} diff --git a/tests/Value/InetTest.php b/tests/Value/InetTest.php new file mode 100644 index 0000000..ce218fd --- /dev/null +++ b/tests/Value/InetTest.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Inet; + +class InetTest extends CassisTest +{ + public function testFromToString() + { + $value = Inet::fromString('192.168.1.1'); + + self::assertEquals('192.168.1.1', (string) $value); + } + + public function testInvalidString() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid ip address: "192.168.1".'); + + Inet::fromString('192.168.1'); + } + + public function testFromBytes() + { + $bytes = \inet_pton('192.168.1.1'); + $value = Inet::fromBytes($bytes); + + self::assertEquals('192.168.1.1', (string) $value); + } + + public function testFromInvalidBytes() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cant read ip address from bytes string.'); + + Inet::fromBytes('wrong'); + } + + public function testWrite() + { + $buffer = new Buffer; + $value = Inet::fromString('192.168.1.1'); + + $buffer->appendValue($value); + + self::assertEquals($value, (new Base(Base::INET))->read($buffer)); + } +} diff --git a/tests/Value/IntegerTest.php b/tests/Value/IntegerTest.php new file mode 100644 index 0000000..7e23104 --- /dev/null +++ b/tests/Value/IntegerTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Integer; + +class IntegerTest extends CassisTest +{ + public function testWrite() + { + $buffer = new Buffer; + + $tiny = Integer::tiny(1); + $small = Integer::small(500); + $int = Integer::int(8193); + $big = Integer::big(765438000); + + $buffer + ->appendValue($tiny) + ->appendValue($small) + ->appendValue($int) + ->appendValue($big) + ; + + self::assertEquals(1, (new Base(Base::TINYINT))->read($buffer)); + self::assertEquals(500, (new Base(Base::SMALLINT))->read($buffer)); + self::assertEquals(8193, (new Base(Base::INT))->read($buffer)); + self::assertEquals(765438000, (new Base(Base::BIGINT))->read($buffer)); + self::assertEquals(0, $buffer->size()); + } +} diff --git a/tests/Value/TimeTest.php b/tests/Value/TimeTest.php new file mode 100644 index 0000000..92b26d2 --- /dev/null +++ b/tests/Value/TimeTest.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Time; + +class TimeTest extends CassisTest +{ + public function testConstruct() + { + $time = Time::fromNanoSeconds(0); + self::assertEquals(0, $time->value()); + + $time = Time::fromNanoSeconds(42); + self::assertEquals(42, $time->value()); + + $time = Time::fromNanoSeconds(86399999999999); + self::assertEquals(86399999999999, $time->value()); + } + + public function testConstructNegative() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Time must be nanoseconds since midnight, -1 given'); + + Time::fromNanoSeconds(-1); + } + + public function testConstructTooBig() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Time must be nanoseconds since midnight, 86400000000000 given'); + + Time::fromNanoSeconds(86400000000000); + } + + public function testFromDateTime() + { + $datetime = new \DateTime("1970-01-01T00:00:00+0000"); + $time = Time::fromDateTime($datetime); + self::assertEquals(0, $time->value()); + + $datetime = new \DateTime("1970-01-01T00:00:01+0000"); + $time = Time::fromDateTime($datetime); + self::assertEquals(1000000000, $time->value()); + + $datetime = new \DateTime("1970-01-01T23:59:59+0000"); + $time = Time::fromDateTime($datetime); + self::assertEquals(86399000000000, $time->value()); + } + + public function testFromToInterval() + { + $datetime1 = new \DateTime("1970-01-01T00:00:00+0000"); + $datetime2 = new \DateTime("1970-01-01T00:00:02+0000"); + $interval = $datetime2->diff($datetime1); + + $time = Time::fromInterval($datetime2->diff($datetime1)); + + self::assertEquals(2000000000, $time->value()); + self::assertEquals($interval, $time->toDateInterval()); + } + + public function testWrite() + { + $buffer = new Buffer; + $value = Time::fromNanoSeconds(42); + + $buffer->appendValue($value); + + self::assertEquals($value, (new Base(Base::TIME))->read($buffer)); + } +} diff --git a/tests/Value/TimestampTest.php b/tests/Value/TimestampTest.php new file mode 100644 index 0000000..116c81d --- /dev/null +++ b/tests/Value/TimestampTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Timestamp; + +class TimestampTest extends CassisTest +{ + public function testConstruct() + { + $time = Timestamp::fromMicroSeconds(0); + self::assertEquals(0, $time->value()); + + $time = Timestamp::fromMicroSeconds(42); + self::assertEquals(42, $time->value()); + } + + public function testConstructNegative() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Timestamp must be positive, -1 given'); + + Timestamp::fromMicroSeconds(-1); + } + + public function testFromToDateTime() + { + $datetime = new \DateTime("1970-01-01T00:00:00+0000"); + $timestamp = Timestamp::fromDateTime($datetime); + self::assertEquals(0, $timestamp->value()); + self::assertEquals($datetime, $timestamp->toDateTime()); + + $datetime = new \DateTime("1970-01-01T00:00:01+0000"); + $timestamp = Timestamp::fromDateTime($datetime); + self::assertEquals(1000000, $timestamp->value()); + self::assertEquals($datetime, $timestamp->toDateTime()); + + $datetime = new \DateTime("1970-01-01T23:59:59.000003+0000"); + $timestamp = Timestamp::fromDateTime($datetime); + self::assertEquals(86399000003, $timestamp->value()); + self::assertEquals($datetime, $timestamp->toDateTime()); + } + + public function testWrite() + { + $buffer = new Buffer; + $value = Timestamp::fromMicroSeconds(42); + + $buffer->appendValue($value); + + self::assertEquals($value, (new Base(Base::TIMESTAMP))->read($buffer)); + } +} diff --git a/tests/Value/UserDefinedTest.php b/tests/Value/UserDefinedTest.php new file mode 100644 index 0000000..8f91f45 --- /dev/null +++ b/tests/Value/UserDefinedTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\UserDefined; + +class UserDefinedTest extends CassisTest +{ + public function testValues() + { + $args = [ + 'id' => 1, + 'name' => 'John Doe', + ]; + + $user = new UserDefined($args); + + self::assertEquals($args, $user->values()); + } + + public function testWrite() + { + $buffer = new Buffer; + $value = new UserDefined([ + 'id' => 1, + 'name' => 'John Doe', + ]); + + $buffer->appendValue($value); + + $type = new Type\UserDefined('test', 'user', [ + 'id' => new Base(Base::INT), + 'name' => new Base(Base::TEXT), + ]); + + self::assertEquals($value, $type->read($buffer)); + } +} diff --git a/tests/Value/VarintTest.php b/tests/Value/VarintTest.php new file mode 100644 index 0000000..296be29 --- /dev/null +++ b/tests/Value/VarintTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PHPinnacle\Cassis\Tests\Value; + +use PHPinnacle\Cassis\Buffer; +use PHPinnacle\Cassis\Tests\CassisTest; +use PHPinnacle\Cassis\Type\Base; +use PHPinnacle\Cassis\Value\Varint; + +class VarintTest extends CassisTest +{ + const VALUE = '67890656781923123918798273492834712837198237'; + + public function testConstruct() + { + $gmp = \gmp_init(self::VALUE); + $value = new Varint($gmp); + + self::assertEquals(self::VALUE, (string) $value); + } + + public function testFromToString() + { + $value = Varint::fromString(self::VALUE); + + self::assertEquals(self::VALUE, (string) $value); + } + + public function testFromBytes() + { + $bytes = \gmp_export(self::VALUE); + $value = Varint::fromBytes($bytes); + + self::assertEquals(self::VALUE, (string) $value); + } + + public function testWrite() + { + $buffer = new Buffer; + $value = Varint::fromString(self::VALUE); + + $buffer->appendValue($value); + + self::assertEquals($value, (new Base(Base::VARINT))->read($buffer)); + self::assertEquals(0, $buffer->size()); + } +}