From 394742122f3a9d1a860884984069fb7178c51b41 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Tue, 29 Dec 2015 08:23:48 +0700 Subject: [PATCH 01/23] Installing PHPCS. --- build.xml | 8 +- composer.json | 4 +- composer.lock | 412 +++++++++++++++++++++++++------------ src/ActiveDataProvider.php | 46 +++++ src/ActiveQuery.php | 30 +-- src/ActiveRecord.php | 55 +++-- src/Command.php | 7 +- src/Connection.php | 26 ++- src/DynamoDataProvider.php | 30 --- src/Marshaler.php | 138 +++++++++++-- src/Query.php | 59 +++--- src/TableSchema.php | 6 - 12 files changed, 559 insertions(+), 262 deletions(-) create mode 100644 src/ActiveDataProvider.php delete mode 100644 src/DynamoDataProvider.php delete mode 100644 src/TableSchema.php diff --git a/build.xml b/build.xml index 30bff8c..7179ec7 100644 --- a/build.xml +++ b/build.xml @@ -34,7 +34,7 @@ - + Running phpunit @@ -49,6 +49,12 @@ + + + + + + diff --git a/composer.json b/composer.json index 626d676..0198b50 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,9 @@ "phpunit/phpunit": "4.6.*", "phpunit/dbunit": ">=1.2", "fzaninotto/faker": "dev-master", - "flow/jsonpath": "dev-master" + "flow/jsonpath": "dev-master", + "yiisoft/yii2-coding-standards": "*", + "squizlabs/php_codesniffer": "2.*" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 9935b6d..85a75f0 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,11 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "19061a283051d394d93b4a6ae07a581c", + "hash": "5e3149f538a8e48a8bb768144e23540b", + "content-hash": "230ffee254155b6fe9937e5fd1df8fe7", "packages": [ { "name": "aws/aws-sdk-php", @@ -12,12 +13,12 @@ "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "282aa6e96275c49fcc363429297ddc1ba006c42a" + "reference": "54b67f902bb2c5bbab481bc3c46537752a018830" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/282aa6e96275c49fcc363429297ddc1ba006c42a", - "reference": "282aa6e96275c49fcc363429297ddc1ba006c42a", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/54b67f902bb2c5bbab481bc3c46537752a018830", + "reference": "54b67f902bb2c5bbab481bc3c46537752a018830", "shasum": "" }, "require": { @@ -67,7 +68,7 @@ "s3", "sdk" ], - "time": "2015-08-20 23:04:59" + "time": "2015-11-16 22:34:08" }, { "name": "bower-asset/jquery", @@ -115,16 +116,16 @@ }, { "name": "bower-asset/jquery.inputmask", - "version": "3.1.63", + "version": "3.2.5", "source": { "type": "git", "url": "https://github.com/RobinHerbots/jquery.inputmask.git", - "reference": "c40c7287eadc31e341ebbf0c02352eb55b9cbc48" + "reference": "d08264a678865849c808359d126e3bddb9ec87a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/c40c7287eadc31e341ebbf0c02352eb55b9cbc48", - "reference": "c40c7287eadc31e341ebbf0c02352eb55b9cbc48", + "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/d08264a678865849c808359d126e3bddb9ec87a6", + "reference": "d08264a678865849c808359d126e3bddb9ec87a6", "shasum": "" }, "require": { @@ -133,23 +134,17 @@ "type": "bower-asset-library", "extra": { "bower-asset-main": [ - "./dist/inputmask/jquery.inputmask.js", - "./dist/inputmask/jquery.inputmask.extensions.js", - "./dist/inputmask/jquery.inputmask.date.extensions.js", - "./dist/inputmask/jquery.inputmask.numeric.extensions.js", - "./dist/inputmask/jquery.inputmask.phone.extensions.js", - "./dist/inputmask/jquery.inputmask.regex.extensions.js" + "./dist/inputmask/inputmask.js" ], "bower-asset-ignore": [ - "**/.*", - "qunit/", - "nuget/", - "tools/", - "js/", - "*.md", - "build.properties", - "build.xml", - "jquery.inputmask.jquery.json" + "**/*", + "!dist/*", + "!dist/inputmask/*", + "!dist/min/*", + "!dist/min/inputmask/*", + "!extra/bindings/*", + "!extra/dependencyLibs/*", + "!extra/phone-codes/*" ] }, "license": [ @@ -434,13 +429,13 @@ "version": "2.8.x-dev", "source": { "type": "git", - "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "d7246885b7fe4cb5a2786bda34362d2f0e40b730" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "a5eb815363c0388e83247e7e9853e5dbc14999cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/d7246885b7fe4cb5a2786bda34362d2f0e40b730", - "reference": "d7246885b7fe4cb5a2786bda34362d2f0e40b730", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a5eb815363c0388e83247e7e9853e5dbc14999cc", + "reference": "a5eb815363c0388e83247e7e9853e5dbc14999cc", "shasum": "" }, "require": { @@ -451,7 +446,6 @@ "symfony/config": "~2.0,>=2.0.5|~3.0.0", "symfony/dependency-injection": "~2.6|~3.0.0", "symfony/expression-language": "~2.6|~3.0.0", - "symfony/phpunit-bridge": "~2.7|~3.0.0", "symfony/stopwatch": "~2.3|~3.0.0" }, "suggest": { @@ -467,7 +461,10 @@ "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" - } + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -485,7 +482,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2015-06-24 15:32:32" + "time": "2015-10-30 20:15:42" }, { "name": "yiisoft/yii2", @@ -493,20 +490,21 @@ "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "38a7f24c04df98aaf250bd9e6a44a087fde41881" + "reference": "0267c425bf6f4500b08c3921cf442f0f3bd46ae4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/38a7f24c04df98aaf250bd9e6a44a087fde41881", - "reference": "38a7f24c04df98aaf250bd9e6a44a087fde41881", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/0267c425bf6f4500b08c3921cf442f0f3bd46ae4", + "reference": "0267c425bf6f4500b08c3921cf442f0f3bd46ae4", "shasum": "" }, "require": { "bower-asset/jquery": "2.1.*@stable | 1.11.*@stable", - "bower-asset/jquery.inputmask": "3.1.*", + "bower-asset/jquery.inputmask": "~3.2.2", "bower-asset/punycode": "1.3.*", "bower-asset/yii2-pjax": ">=2.0.1", "cebe/markdown": "~1.0.0 | ~1.1.0", + "ext-ctype": "*", "ext-mbstring": "*", "ezyang/htmlpurifier": "4.6.*", "lib-pcre": "*", @@ -565,6 +563,11 @@ "name": "Paul Klimov", "email": "klimov.paul@gmail.com", "role": "Core framework development" + }, + { + "name": "Dmitry Naumenko", + "email": "d.naumenko.a@gmail.com", + "role": "Core framework development" } ], "description": "Yii PHP Framework Version 2", @@ -573,7 +576,7 @@ "framework", "yii2" ], - "time": "2015-08-25 03:54:18" + "time": "2015-12-27 21:07:36" }, { "name": "yiisoft/yii2-composer", @@ -581,12 +584,12 @@ "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-composer.git", - "reference": "6b2a2b3bb83d4b3dd791f76e64c48973ce01bac6" + "reference": "574dcb1d101ae55be230e0c00a2428af6ec4c5c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/6b2a2b3bb83d4b3dd791f76e64c48973ce01bac6", - "reference": "6b2a2b3bb83d4b3dd791f76e64c48973ce01bac6", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/574dcb1d101ae55be230e0c00a2428af6ec4c5c1", + "reference": "574dcb1d101ae55be230e0c00a2428af6ec4c5c1", "shasum": "" }, "require": { @@ -620,7 +623,7 @@ "extension installer", "yii2" ], - "time": "2015-06-11 19:55:58" + "time": "2015-12-01 20:06:03" } ], "packages-dev": [ @@ -684,12 +687,12 @@ "source": { "type": "git", "url": "https://github.com/FlowCommunications/JSONPath.git", - "reference": "0b8d3719951ae710804feb01bb55bc7168e4e2f5" + "reference": "e064398010089f9efb46044f85ee63c458ae89e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FlowCommunications/JSONPath/zipball/0b8d3719951ae710804feb01bb55bc7168e4e2f5", - "reference": "0b8d3719951ae710804feb01bb55bc7168e4e2f5", + "url": "https://api.github.com/repos/FlowCommunications/JSONPath/zipball/e064398010089f9efb46044f85ee63c458ae89e1", + "reference": "e064398010089f9efb46044f85ee63c458ae89e1", "shasum": "" }, "require-dev": { @@ -714,7 +717,7 @@ } ], "description": "JSONPath implementation for parsing, searching and flattening arrays", - "time": "2015-06-25 12:42:18" + "time": "2015-10-12 07:39:47" }, { "name": "fzaninotto/faker", @@ -722,24 +725,22 @@ "source": { "type": "git", "url": "https://github.com/fzaninotto/Faker.git", - "reference": "6615c01b7b2a9c46f4cc426f860866a122eae0af" + "reference": "3037df47eca2e534f62f061415a071f9b5d1a1a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/6615c01b7b2a9c46f4cc426f860866a122eae0af", - "reference": "6615c01b7b2a9c46f4cc426f860866a122eae0af", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/3037df47eca2e534f62f061415a071f9b5d1a1a4", + "reference": "3037df47eca2e534f62f061415a071f9b5d1a1a4", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { + "ext-intl": "*", "phpunit/phpunit": "~4.0", "squizlabs/php_codesniffer": "~1.5" }, - "suggest": { - "ext-intl": "*" - }, "type": "library", "extra": { "branch-alias": { @@ -766,7 +767,7 @@ "faker", "fixtures" ], - "time": "2015-06-23 08:30:36" + "time": "2015-11-30 17:35:42" }, { "name": "phpdocumentor/reflection-docblock", @@ -823,18 +824,20 @@ "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "5700f75b23b0dd3495c0f495fe33a5e6717ee160" + "reference": "e55e3e32a870bd4f05425fa4f717b52bd40e5659" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/5700f75b23b0dd3495c0f495fe33a5e6717ee160", - "reference": "5700f75b23b0dd3495c0f495fe33a5e6717ee160", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e55e3e32a870bd4f05425fa4f717b52bd40e5659", + "reference": "e55e3e32a870bd4f05425fa4f717b52bd40e5659", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", "phpdocumentor/reflection-docblock": "~2.0", - "sebastian/comparator": "~1.1" + "sebastian/comparator": "~1.1", + "sebastian/recursion-context": "~1.0" }, "require-dev": { "phpspec/phpspec": "~2.0" @@ -842,7 +845,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "1.5.x-dev" } }, "autoload": { @@ -875,7 +878,7 @@ "spy", "stub" ], - "time": "2015-06-30 10:26:46" + "time": "2015-12-28 13:26:33" }, { "name": "phpunit/dbunit", @@ -883,40 +886,36 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/dbunit.git", - "reference": "a0b24d7b98844377d6915381e956e3a6573b8844" + "reference": "76adbf67d800f9e275e4bb70f1106d02f152c7a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/a0b24d7b98844377d6915381e956e3a6573b8844", - "reference": "a0b24d7b98844377d6915381e956e3a6573b8844", + "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/76adbf67d800f9e275e4bb70f1106d02f152c7a4", + "reference": "76adbf67d800f9e275e4bb70f1106d02f152c7a4", "shasum": "" }, "require": { "ext-pdo": "*", "ext-simplexml": "*", - "php": ">=5.3.3", - "phpunit/phpunit": "~4.0", - "symfony/yaml": "~2.1" + "php": ">=5.4", + "phpunit/phpunit": "~4|~5", + "symfony/yaml": "~2.1|~3.0" }, "bin": [ - "composer/bin/dbunit" + "dbunit" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "classmap": [ - "PHPUnit/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "", - "../../symfony/yaml/" - ], "license": [ "BSD-3-Clause" ], @@ -934,7 +933,7 @@ "testing", "xunit" ], - "time": "2015-06-21 13:05:35" + "time": "2015-11-03 14:01:44" }, { "name": "phpunit/php-code-coverage", @@ -942,25 +941,25 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "df88fdf48a61af29cc309545f9c8d425c63b8852" + "reference": "6aa886e7de7d04dc4ac7b799063a806e36c03ce3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/df88fdf48a61af29cc309545f9c8d425c63b8852", - "reference": "df88fdf48a61af29cc309545f9c8d425c63b8852", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6aa886e7de7d04dc4ac7b799063a806e36c03ce3", + "reference": "6aa886e7de7d04dc4ac7b799063a806e36c03ce3", "shasum": "" }, "require": { - "php": ">=5.3.3", + "php": ">=5.6", "phpunit/php-file-iterator": "~1.3", "phpunit/php-text-template": "~1.2", "phpunit/php-token-stream": "~1.3", - "sebastian/environment": "~1.0", + "sebastian/environment": "^1.3.2", "sebastian/version": "~1.0" }, "require-dev": { "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~4" + "phpunit/phpunit": "~5" }, "suggest": { "ext-dom": "*", @@ -970,7 +969,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -996,7 +995,7 @@ "testing", "xunit" ], - "time": "2015-06-21 07:51:27" + "time": "2015-11-12 21:11:35" }, { "name": "phpunit/php-file-iterator", @@ -1133,12 +1132,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9" + "reference": "cab6c6fefee93d7b7c3a01292a0fe0884ea66644" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/7a9b0969488c3c54fd62b4d504b3ec758fd005d9", - "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/cab6c6fefee93d7b7c3a01292a0fe0884ea66644", + "reference": "cab6c6fefee93d7b7c3a01292a0fe0884ea66644", "shasum": "" }, "require": { @@ -1174,7 +1173,7 @@ "keywords": [ "tokenizer" ], - "time": "2015-06-19 03:43:16" + "time": "2015-09-23 14:46:55" }, { "name": "phpunit/phpunit", @@ -1254,22 +1253,22 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "e4862226480f86ea18ef773212ec1e2acd42ba64" + "reference": "c4dd0c9e7fcbf53c6aa48012902be692318c7566" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/e4862226480f86ea18ef773212ec1e2acd42ba64", - "reference": "e4862226480f86ea18ef773212ec1e2acd42ba64", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/c4dd0c9e7fcbf53c6aa48012902be692318c7566", + "reference": "c4dd0c9e7fcbf53c6aa48012902be692318c7566", "shasum": "" }, "require": { - "doctrine/instantiator": "~1.0,>=1.0.2", - "php": ">=5.3.3", + "doctrine/instantiator": "^1.0.2", + "php": ">=5.6", "phpunit/php-text-template": "~1.2", "sebastian/exporter": "~1.2" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "~5" }, "suggest": { "ext-soap": "*" @@ -1277,7 +1276,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -1302,7 +1301,7 @@ "mock", "xunit" ], - "time": "2015-07-04 15:35:39" + "time": "2015-12-08 08:54:19" }, { "name": "sebastian/comparator", @@ -1310,12 +1309,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "abd05e77d364138c0532a1b78a13c4d78311f546" + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/abd05e77d364138c0532a1b78a13c4d78311f546", - "reference": "abd05e77d364138c0532a1b78a13c4d78311f546", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", "shasum": "" }, "require": { @@ -1329,7 +1328,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -1366,7 +1365,7 @@ "compare", "equality" ], - "time": "2015-06-21 07:54:35" + "time": "2015-07-26 15:48:44" }, { "name": "sebastian/diff", @@ -1374,24 +1373,24 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "6899b3e33bfbd386d88b5eea5f65f563e8793051" + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/6899b3e33bfbd386d88b5eea5f65f563e8793051", - "reference": "6899b3e33bfbd386d88b5eea5f65f563e8793051", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "~4.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -1414,11 +1413,11 @@ } ], "description": "Diff implementation", - "homepage": "http://www.github.com/sebastianbergmann/diff", + "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ "diff" ], - "time": "2015-06-22 14:15:55" + "time": "2015-12-08 07:14:41" }, { "name": "sebastian/environment", @@ -1426,12 +1425,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "bc66c3bbe6e5822c34395049e110566491cafa3b" + "reference": "6e7133793a8e5a5714a551a8324337374be209df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/bc66c3bbe6e5822c34395049e110566491cafa3b", - "reference": "bc66c3bbe6e5822c34395049e110566491cafa3b", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6e7133793a8e5a5714a551a8324337374be209df", + "reference": "6e7133793a8e5a5714a551a8324337374be209df", "shasum": "" }, "require": { @@ -1468,7 +1467,7 @@ "environment", "hhvm" ], - "time": "2015-06-21 13:07:52" + "time": "2015-12-02 08:37:27" }, { "name": "sebastian/exporter", @@ -1476,12 +1475,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "7ae5513327cb536431847bcc0c10edba2701064e" + "reference": "f88f8936517d54ae6d589166810877fb2015d0a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", - "reference": "7ae5513327cb536431847bcc0c10edba2701064e", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/f88f8936517d54ae6d589166810877fb2015d0a2", + "reference": "f88f8936517d54ae6d589166810877fb2015d0a2", "shasum": "" }, "require": { @@ -1489,12 +1488,13 @@ "sebastian/recursion-context": "~1.0" }, "require-dev": { + "ext-mbstring": "*", "phpunit/phpunit": "~4.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -1534,20 +1534,20 @@ "export", "exporter" ], - "time": "2015-06-21 07:55:53" + "time": "2015-08-09 04:23:41" }, { "name": "sebastian/global-state", - "version": "dev-master", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "23af31f402993cfd94e99cbc4b782e9a78eb0e97" + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/23af31f402993cfd94e99cbc4b782e9a78eb0e97", - "reference": "23af31f402993cfd94e99cbc4b782e9a78eb0e97", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", "shasum": "" }, "require": { @@ -1585,7 +1585,7 @@ "keywords": [ "global state" ], - "time": "2015-06-21 15:11:22" + "time": "2015-10-12 03:26:01" }, { "name": "sebastian/recursion-context", @@ -1593,12 +1593,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "994d4a811bafe801fb06dccbee797863ba2792ba" + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/994d4a811bafe801fb06dccbee797863ba2792ba", - "reference": "994d4a811bafe801fb06dccbee797863ba2792ba", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", "shasum": "" }, "require": { @@ -1638,7 +1638,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2015-06-21 08:04:50" + "time": "2015-11-11 19:50:13" }, { "name": "sebastian/version", @@ -1676,35 +1676,112 @@ "time": "2015-06-21 13:59:46" }, { - "name": "symfony/yaml", - "version": "2.8.x-dev", + "name": "squizlabs/php_codesniffer", + "version": "2.5.0", "source": { "type": "git", - "url": "https://github.com/symfony/Yaml.git", - "reference": "000e7fc2653335cd42c6d21405dac1c74224a387" + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "e4fb41d5d0387d556e2c25534d630b3cce90ea67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/000e7fc2653335cd42c6d21405dac1c74224a387", - "reference": "000e7fc2653335cd42c6d21405dac1c74224a387", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e4fb41d5d0387d556e2c25534d630b3cce90ea67", + "reference": "e4fb41d5d0387d556e2c25534d630b3cce90ea67", "shasum": "" }, "require": { - "php": ">=5.3.9" + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.1.2" }, "require-dev": { - "symfony/phpunit-bridge": "~2.7|~3.0.0" + "phpunit/phpunit": "~4.0" }, + "bin": [ + "scripts/phpcs", + "scripts/phpcbf" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "CodeSniffer.php", + "CodeSniffer/CLI.php", + "CodeSniffer/Exception.php", + "CodeSniffer/File.php", + "CodeSniffer/Fixer.php", + "CodeSniffer/Report.php", + "CodeSniffer/Reporting.php", + "CodeSniffer/Sniff.php", + "CodeSniffer/Tokens.php", + "CodeSniffer/Reports/", + "CodeSniffer/Tokenizers/", + "CodeSniffer/DocGenerators/", + "CodeSniffer/Standards/AbstractPatternSniff.php", + "CodeSniffer/Standards/AbstractScopeSniff.php", + "CodeSniffer/Standards/AbstractVariableSniff.php", + "CodeSniffer/Standards/IncorrectPatternException.php", + "CodeSniffer/Standards/Generic/Sniffs/", + "CodeSniffer/Standards/MySource/Sniffs/", + "CodeSniffer/Standards/PEAR/Sniffs/", + "CodeSniffer/Standards/PSR1/Sniffs/", + "CodeSniffer/Standards/PSR2/Sniffs/", + "CodeSniffer/Standards/Squiz/Sniffs/", + "CodeSniffer/Standards/Zend/Sniffs/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2015-12-11 00:12:46" + }, + { + "name": "symfony/yaml", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "816bd36756bc86351de54aed8fce14c77b353a30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/816bd36756bc86351de54aed8fce14c77b353a30", + "reference": "816bd36756bc86351de54aed8fce14c77b353a30", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" } }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" - } + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1722,7 +1799,75 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-07-01 14:16:54" + "time": "2015-12-28 13:15:29" + }, + { + "name": "yiisoft/yii2-coding-standards", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-coding-standards.git", + "reference": "5c4fa33f645927f705b5f1fab7ce836f80eb71fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-coding-standards/zipball/5c4fa33f645927f705b5f1fab7ce836f80eb71fc", + "reference": "5c4fa33f645927f705b5f1fab7ce836f80eb71fc", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com", + "homepage": "http://www.yiiframework.com/", + "role": "Founder and project lead" + }, + { + "name": "Alexander Makarov", + "email": "sam@rmcreative.ru", + "homepage": "http://rmcreative.ru/", + "role": "Core framework development" + }, + { + "name": "Maurizio Domba", + "homepage": "http://mdomba.info/", + "role": "Core framework development" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Core framework development" + }, + { + "name": "Timur Ruziev", + "email": "resurtm@gmail.com", + "homepage": "http://resurtm.com/", + "role": "Core framework development" + }, + { + "name": "Paul Klimov", + "email": "klimov.paul@gmail.com", + "role": "Core framework development" + } + ], + "description": "Yii PHP Framework Version 2 - Coding standard tools", + "homepage": "http://www.yiiframework.com/", + "keywords": [ + "codesniffer", + "framework", + "yii" + ], + "time": "2015-04-03 06:38:48" } ], "aliases": [], @@ -1732,6 +1877,7 @@ "flow/jsonpath": 20 }, "prefer-stable": false, + "prefer-lowest": false, "platform": { "php": ">=5.4.0" }, diff --git a/src/ActiveDataProvider.php b/src/ActiveDataProvider.php new file mode 100644 index 0000000..c53a3b6 --- /dev/null +++ b/src/ActiveDataProvider.php @@ -0,0 +1,46 @@ + + */ +namespace UrbanIndo\Yii2\DynamoDb; + +/** + * ActiveDataProvider implements a data provider based on DynamoDB Query and ActiveQuery. + * + * ActiveDataProvider provides data by performing DB queries using [[query]]. + * + * The following is an example of using ActiveDataProvider to provide ActiveRecord instances: + * + * ```php + * $provider = new ActiveDataProvider([ + * 'query' => Post::find(), + * 'pagination' => [ + * 'pageSize' => 20, + * ], + * ]); + * + * // get the posts in the current page + * $posts = $provider->getModels(); + * ``` + * + * @author Petra Barus + */ +class ActiveDataProvider extends \yii\data\BaseDataProvider +{ + + protected function prepareKeys($models) + { + + } + + protected function prepareModels() + { + + } + + protected function prepareTotalCount() + { + + } + +} diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index 77524b9..dfcb4bd 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -14,7 +14,7 @@ * ActiveQuery represents a [[Query]] associated with an [[ActiveRecord]] class. * * An ActiveQuery can be a normal query or be used in a relational context. - * + * * ActiveQuery instances are usually created by [[ActiveRecord::find()]]. * Relational queries are created by [[ActiveRecord::hasOne()]] and [[ActiveRecord::hasMany()]]. * @@ -29,10 +29,11 @@ * - [[scalar()]]: returns the value of the first column in the first row of the query result. * - [[column()]]: returns the value of the first column in the query result. * - [[exists()]]: returns a value indicating whether the query result has data or not. - * + * * @author Petra Barus */ -class ActiveQuery extends Query implements ActiveQueryInterface { +class ActiveQuery extends Query implements ActiveQueryInterface +{ use ActiveQueryTrait; use ActiveRelationTrait; @@ -47,7 +48,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface { * @param array $modelClass the model class associated with this query * @param array $config configurations to be applied to the newly created query object */ - public function __construct($modelClass, $config = []) { + public function __construct($modelClass, $config = []) + { $this->modelClass = $modelClass; parent::__construct($config); } @@ -58,7 +60,8 @@ public function __construct($modelClass, $config = []) { * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end * to ensure triggering of the event. */ - public function init() { + public function init() + { parent::init(); $this->trigger(self::EVENT_INIT); } @@ -67,23 +70,25 @@ public function init() { * @param Connection $db * @return ActiveRecord */ - public function one($db = null) { + public function one($db = null) + { /* @var $response \Guzzle\Service\Resource\Model */ $response = parent::one($db); $value = $response->get('Item'); $marshaller = new \Aws\DynamoDb\Marshaler(); - return $this->createModel($value, $marshaller); + return $this->createModel($value, $marshaller); } /** * @param Connection $db * @return ActiveRecord[] */ - public function all($db = null) { + public function all($db = null) + { $responses = parent::all($db); $modelClass = $this->modelClass; $marshaller = new \Aws\DynamoDb\Marshaler(); - return array_map(function($value) use ($marshaller) { + return array_map(function ($value) use ($marshaller) { return $this->createModel($value, $marshaller); }, $responses[$modelClass::tableName()]); } @@ -94,7 +99,8 @@ public function all($db = null) { * @param Aws\DynamoDb\Marshaler $marshaller * @return \UrbanIndo\Yii2\DynamoDb\modelClass */ - private function createModel($value, \Aws\DynamoDb\Marshaler $marshaller = null) { + private function createModel($value, \Aws\DynamoDb\Marshaler $marshaller = null) + { $model = new $this->modelClass; if (!isset($marshaller)) { $marshaller = new \Aws\DynamoDb\Marshaler(); @@ -102,8 +108,4 @@ private function createModel($value, \Aws\DynamoDb\Marshaler $marshaller = null) $model->setAttributes($marshaller->unmarshalItem($value), false); return $model; } - -// public function asArray($value = true) {} - -// public function batch($batchSize = 100, $db = null) {} } diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 9b374f3..9d0145b 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -14,13 +14,13 @@ /** * ActiveRecord is the base class for classes representing relational data in terms of objects. - * + * * Active Record implements the [Active Record design pattern](http://en.wikipedia.org/wiki/Active_record) for * [DynamoDB] (https://aws.amazon.com/dynamodb/). - * + * * For defining a record a subclass should at least implement the [[attributes()]] method to define * attributes and the [[tableName()]] methods to define the table name that the class represents. - * + * * The following is an example model called `Customer`: * * ```php @@ -30,16 +30,17 @@ * { * return ['id', 'name', 'address', 'registration_date']; * } - * + * * public static function tableName() { * return 'Customers'; * } * } * ``` - * + * * @author Petra Barus */ -class ActiveRecord extends BaseActiveRecord { +class ActiveRecord extends BaseActiveRecord +{ protected static $_primaryKeys = []; @@ -49,7 +50,8 @@ class ActiveRecord extends BaseActiveRecord { * You may override this method if you want to use a different database connection. * @return Connection the database connection used by this AR class. */ - public static function getDb() { + public static function getDb() + { return Yii::$app->get('dynamodb'); } @@ -58,7 +60,8 @@ public static function getDb() { * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]. * @return string the table name */ - public static function tableName() { + public static function tableName() + { return Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); } @@ -66,7 +69,8 @@ public static function tableName() { * @inheritdoc * @return ActiveQuery the newly created [[ActiveQuery]] instance. */ - public static function find() { + public static function find() + { return Yii::createObject(ActiveQuery::className(), [get_called_class()]); } @@ -105,7 +109,8 @@ public static function find() { * * @return boolean whether the attributes are valid and the record is inserted successfully. */ - public function insert($runValidation = true, $attributes = null) { + public function insert($runValidation = true, $attributes = null) + { if ($runValidation && !$this->validate($attributes)) { Yii::info('Model not inserted due to validation error.', __METHOD__); return false; @@ -123,20 +128,22 @@ public function insert($runValidation = true, $attributes = null) { return $ret; } + /** * Returns the primary key **name(s)** for this AR class. * * Note that an array should be returned even when the record only has a single primary key. * * For the primary key **value** see [[getPrimaryKey()]] instead. - * + * * The array returned will consist of either one or two string. The first one * will be the name of the HASH key. The second one will be the name of the RANGE * key if exists. * * @return string[] the primary key name(s) for this AR class. */ - public static function primaryKey() { + public static function primaryKey() + { if (!isset(self::$_primaryKeys[get_called_class()])) { $client = self::getDb()->getClient(); $command = $client->getCommand('DescribeTable', [ @@ -154,7 +161,8 @@ public static function primaryKey() { return self::$_primaryKeys[get_called_class()]; } - public static function batchInsert($values){ + public static function batchInsert($values) + { self::getDb()->createCommand()->putItems(static::tableName(), $values); } @@ -164,7 +172,8 @@ public static function batchInsert($values){ * @param array $options addition attribute. * @return ActiveRecord */ - public static function findOne($condition, $options = null) { + public static function findOne($condition, $options = null) + { return self::createQueryWithParameter($options)->where($condition)->one(); } @@ -174,7 +183,8 @@ public static function findOne($condition, $options = null) { * @param array $options addition attribute. * @return ActiveRecord[] */ - public static function findAll($condition, $options = null) { + public static function findAll($condition, $options = null) + { if ($options == null) { $options = ['using' => Query::TYPE_BATCH_GET]; } @@ -186,10 +196,11 @@ public static function findAll($condition, $options = null) { * @param array $options * @return ActiveQuery the query. */ - private static function createQueryWithParameter($options = null) { + private static function createQueryWithParameter($options = null) + { $query = self::find(); if ($options !== null) { - foreach($options as $attribute => $value) { + foreach ($options as $attribute => $value) { $query->{$attribute} = $value; } } @@ -200,7 +211,8 @@ private static function createQueryWithParameter($options = null) { * @inheritdoc * @todo */ - public static function updateAll($attributes, $condition = '') { + public static function updateAll($attributes, $condition = '') + { parent::updateAll($attributes, $condition); } @@ -208,7 +220,8 @@ public static function updateAll($attributes, $condition = '') { * @inheritdoc * @todo */ - public static function updateAllCounters($counters, $condition = '') { + public static function updateAllCounters($counters, $condition = '') + { parent::updateAllCounters($counters, $condition); } @@ -216,8 +229,8 @@ public static function updateAllCounters($counters, $condition = '') { * @inheritdoc * @todo */ - public static function deleteAll($condition = '', $params = []) { + public static function deleteAll($condition = '', $params = []) + { parent::deleteAll($condition, $params); } - } diff --git a/src/Command.php b/src/Command.php index 1aa48fd..b8d8001 100644 --- a/src/Command.php +++ b/src/Command.php @@ -13,7 +13,8 @@ /** * @author Petra Barus */ -class Command extends Object { +class Command extends Object +{ /** * @var Connection @@ -22,8 +23,12 @@ class Command extends Object { public $method; public $request; + /** + * Initializes the object. + */ public function init() { } + /** * @return DynamoDbClient */ diff --git a/src/Connection.php b/src/Connection.php index 03aa094..78057c0 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -3,6 +3,7 @@ * Connection class file. * @author Petra Barus */ + namespace UrbanIndo\Yii2\DynamoDb; use Yii; @@ -10,15 +11,16 @@ /** * Connection wraps DynamoDB connection for Aws PHP SDK. - * + * * To use the connection puts this in the config. - * + * * ``` * ``` - * + * * @author Petra Barus */ -class Connection extends \yii\base\Component { +class Connection extends \yii\base\Component +{ /** * @var array the configuration for DynamoDB client. @@ -30,10 +32,12 @@ class Connection extends \yii\base\Component { */ protected $_client; protected $_builder; + /** * Initialize the dynamodb client. */ - public function init() { + public function init() + { parent::init(); //For v2 compatibility. //TODO: remove deprecated. @@ -43,7 +47,8 @@ public function init() { /** * @return DynamoDbClient */ - public function getClient() { + public function getClient() + { return $this->_client; } @@ -52,19 +57,20 @@ public function getClient() { * @param array $config the configuration for the Command class * @return Command the DB command */ - public function createCommand($config = []) { + public function createCommand($config = []) + { $command = Yii::createObject(array_merge($config, [ - 'class' => 'UrbanIndo\Yii2\DynamoDb\Command', + 'class' => Command::className(), 'db' => $this ])); return $command; } /** - * * @return QueryBuilder */ - public function getQueryBuilder() { + public function getQueryBuilder() + { if ($this->_builder === null) { $this->_builder = new QueryBuilder($this); } diff --git a/src/DynamoDataProvider.php b/src/DynamoDataProvider.php deleted file mode 100644 index fa71f3b..0000000 --- a/src/DynamoDataProvider.php +++ /dev/null @@ -1,30 +0,0 @@ - */ namespace UrbanIndo\Yii2\DynamoDb; /** - * Description of Marshaller + * Marshaller wraps AWS DynamoDB Marshaller. * * @author adinata */ -class Marshaler { - /* @var $singleton Marshaler */ +class Marshaler +{ + /** + * @var static + */ private static $singleton = null; - + /** - * + * Singleton method. * @return Marshaler */ - private static function marshaler() { + private static function marshaler() + { if (self::$singleton == null) { self::$singleton = new \Aws\DynamoDb\Marshaler(); } return self::$singleton; } - public static function marshal($item) { + + /** + * Marshal object either Yii2 model or basic PHP native array. + * @param mixed $item Item to be marshalled. + * @return mixed + */ + public static function marshal($item) + { if ($item instanceof \yii\base\Model) { $val = self::marshalModel($item); } else { @@ -35,22 +44,86 @@ public static function marshal($item) { } return $val; } - public static function marshalItem($item) { + + /** + * Marshal a native PHP array of data to a new array that is formatted in + * the proper parameter structure required by DynamoDB operations. + * + * @param array|\stdClass $item An associative array of data. + * + * @return array + */ + public static function marshalItem($item) + { return self::marshaler()->marshalItem($item); } - public static function marshalModel(\yii\base\Model $item) { + + /** + * Marshal a Yii2 model object to a new array that is formatted in + * the proper parameter structure required by DynamoDB operations. + * + * @param \yii\base\Model $item + * @return mixed + */ + public static function marshalModel(\yii\base\Model $item) + { return self::marshaler()->marshalItem($item->getAttributes()); } - public static function marshalJson($json) { + + /** + * Marshal a JSON document from a string to an array that is formatted in + * the proper parameter structure required by DynamoDB operations. + * + * @param string $json A valid JSON document. + * @return array + * @throws \InvalidArgumentException if the JSON is invalid. + */ + public static function marshalJson($json) + { return self::marshaler()->marshalJson($json); } - public static function marshalValue($value) { + + /** + * Marshal a native PHP value into an array that is formatted in the proper + * parameter structure required by DynamoDB operations. + * + * @param mixed $value A scalar, array, or stdClass value. + * + * @return array Formatted like `array(TYPE => VALUE)`. + * @throws \UnexpectedValueException if the value cannot be marshaled. + */ + public static function marshalValue($value) + { return self::marshaler()->marshalValue($value); } - public static function unmarshalItem(array $data) { + + /** + * Unmarshal an item from a DynamoDB operation result into a native PHP + * array. If you set $mapAsObject to true, then a stdClass value will be + * returned instead. + * + * @param array $data Item from a DynamoDB result. + * + * @return array|\stdClass + */ + public static function unmarshalItem(array $data) + { return self::marshaler()->unmarshalItem($data); } - public static function unmarshalModel(array $data, $class) { + + /** + * Unmarshal a value from a DynamoDB operation result into a native Yii2 mode. + * Will return a scalar, array, or (if you set $mapAsObject to true) + * stdClass value. + * + * @param array $data Value from a DynamoDB result. + * @param string $class The name of the class. + * + * @return \yii\base\Model + * @throws \UnexpectedValueException + */ + public static function unmarshalModel(array $data, $class) + { $object = new $class(); if (!($object instanceof \yii\base\Model)) { throw new \InvalidArgumentException("Class to unmarshal must an instance of \yii\base\Model"); @@ -58,11 +131,34 @@ public static function unmarshalModel(array $data, $class) { $object->setAttributes(self::marshaler()->unmarshalItem($data)); return $object; } - public static function unmarshalJson($json) { + + /** + * Unmarshal a document (item) from a DynamoDB operation result into a JSON + * document string. + * + * @param array $data Item/document from a DynamoDB result. + * @param int $jsonEncodeFlags Flags to use with `json_encode()`. + * + * @return string + */ + public static function unmarshalJson($json) + { return self::marshaler()->unmarshalJson($json); } - public static function unmarshalValue($value) { + + /** + * Unmarshal a value from a DynamoDB operation result into a native PHP + * value. Will return a scalar, array, or (if you set $mapAsObject to true) + * stdClass value. + * + * @param array $value Value from a DynamoDB result. + * @param bool $mapAsObject Whether maps should be represented as stdClass. + * + * @return mixed + * @throws \UnexpectedValueException + */ + public static function unmarshalValue($value) + { return self::marshaler()->unmarshalValue($value); } - } diff --git a/src/Query.php b/src/Query.php index a8db84e..286aaf9 100644 --- a/src/Query.php +++ b/src/Query.php @@ -1,8 +1,8 @@ */ + namespace UrbanIndo\Yii2\DynamoDb; use Yii; @@ -18,14 +18,14 @@ */ class Query extends Component implements QueryInterface { + use QueryTrait; - + const TYPE_BATCH_GET = 'BatchGetItem'; const TYPE_GET = 'GetItem'; const TYPE_QUERY = 'Query'; const TYPE_SCAN = 'Scan'; - /** * Array of attributes being selected. It will be used to build Projection Expression. * @var array @@ -43,36 +43,36 @@ class Query extends Component implements QueryInterface * For example, `[':name' => 'Dan', ':age' => 31]`. */ public $expressionAttributesNames = []; - + /** * */ public $expressionAttributesValues = []; - + /** * * @var type */ public $consistentRead; - + /** * * @var type */ public $returnConsumedCapacity; - + /** * * @var type */ public $from; - + /** * * @var type */ public $keys = []; - + /** * Executes the query and returns all results as an array. * @param Connection $db the database connection used to execute the query. @@ -88,7 +88,7 @@ public function createCommand($db = null) return $db->createCommand($config); } - + /** * Identifies one or more attributes to retrieve from the table. * These attributes can include scalars, sets, or elements of a JSON document. @@ -100,7 +100,8 @@ public function createCommand($db = null) public function select($attributes, $expression = []) { if (!is_array($attributes)) { - $attributes = preg_split('/\s*,\s*/', trim($attributes), -1, PREG_SPLIT_NO_EMPTY); + $attributes = preg_split('/\s*,\s*/', trim($attributes), -1, + PREG_SPLIT_NO_EMPTY); } if (empty($this->select)) { $this->select = $attributes; @@ -112,11 +113,12 @@ public function select($attributes, $expression = []) $this->withExpressionAttributesName($exp, $alias); } } - + return $this; } - public function from($tableName) { + public function from($tableName) + { $this->from = $tableName; } @@ -139,7 +141,8 @@ public function withExpressionAttributesName($attributes, $alias) * Whether to use consistent read in the query. * @return static */ - public function withConsistentRead() { + public function withConsistentRead() + { $this->consistentRead = true; return $this; } @@ -148,25 +151,28 @@ public function withConsistentRead() { * Whether to not use consistent read in the query. * @return static */ - public function withoutConsistentRead() { + public function withoutConsistentRead() + { $this->consistentRead = false; return $this; } - + /** * Whether to return the consumed capacity. * @return static */ - public function withConsumedCapacity() { + public function withConsumedCapacity() + { $this->returnConsumedCapacity = true; return $this; } - + /** * Whether not to return the consumed capacity. * @return static */ - public function withoutConsumedCapacity() { + public function withoutConsumedCapacity() + { $this->returnConsumedCapacity = false; return $this; } @@ -176,20 +182,24 @@ public function withoutConsumedCapacity() { * @param Connection $db the dynamodb connection. * @return */ - public function all($db = null) { + public function all($db = null) + { return $this->createCommand($db)->queryAll(); } - public function count($q = '*', $db = null) { + public function count($q = '*', $db = null) + { // TODO: only if query and scan operations. // batch get assumes results equal to number of id and hash } - - public function exists($db = null) { + + public function exists($db = null) + { return !empty($this->createCommand($db)->queryOne()); } - public function one($db = null) { + public function one($db = null) + { $this->using = self::TYPE_GET; return $this->createCommand($db)->queryOne(); } @@ -203,4 +213,5 @@ public function batch($batchSize = 100, $db = null) { // todo } + } diff --git a/src/TableSchema.php b/src/TableSchema.php deleted file mode 100644 index b66ed2d..0000000 --- a/src/TableSchema.php +++ /dev/null @@ -1,6 +0,0 @@ - Date: Tue, 29 Dec 2015 13:54:14 +0700 Subject: [PATCH 02/23] [WIP] DynamoDB query. --- build.xml | 2 +- composer.json | 1 + composer.lock | 36 ++--- ruleset.xml | 59 +++++++ src/ActiveDataProvider.php | 25 ++- src/ActiveQuery.php | 39 ++--- src/ActiveRecord.php | 132 ++++++++------- src/Command.php | 214 ++++++++++++------------- src/Connection.php | 36 +++-- src/Marshaler.php | 24 ++- src/Pagination.php | 15 ++ src/Query.php | 168 +++++++++++--------- src/QueryBuilder.php | 318 +++++++++---------------------------- src/TableSchema.php | 21 +++ test/CommandTest.php | 201 +++++++++++++++++------ test/QueryBuilderTest.php | 26 +++ test/TestCase.php | 32 +--- 17 files changed, 717 insertions(+), 632 deletions(-) create mode 100644 ruleset.xml create mode 100644 src/Pagination.php create mode 100644 src/TableSchema.php create mode 100644 test/QueryBuilderTest.php diff --git a/build.xml b/build.xml index 7179ec7..0577870 100644 --- a/build.xml +++ b/build.xml @@ -34,7 +34,7 @@ - + Running phpunit diff --git a/composer.json b/composer.json index 0198b50..3ded615 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "require-dev": { "phpunit/phpunit": "4.6.*", "phpunit/dbunit": ">=1.2", + "phpunit/php-code-coverage": "2.2.4", "fzaninotto/faker": "dev-master", "flow/jsonpath": "dev-master", "yiisoft/yii2-coding-standards": "*", diff --git a/composer.lock b/composer.lock index 85a75f0..d08de94 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "5e3149f538a8e48a8bb768144e23540b", - "content-hash": "230ffee254155b6fe9937e5fd1df8fe7", + "hash": "9292aa348f0da9aaf3454c934fcd7234", + "content-hash": "f210641db2be6c0ef1b4e5295d41b60a", "packages": [ { "name": "aws/aws-sdk-php", @@ -937,20 +937,20 @@ }, { "name": "phpunit/php-code-coverage", - "version": "dev-master", + "version": "2.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6aa886e7de7d04dc4ac7b799063a806e36c03ce3" + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6aa886e7de7d04dc4ac7b799063a806e36c03ce3", - "reference": "6aa886e7de7d04dc4ac7b799063a806e36c03ce3", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", "shasum": "" }, "require": { - "php": ">=5.6", + "php": ">=5.3.3", "phpunit/php-file-iterator": "~1.3", "phpunit/php-text-template": "~1.2", "phpunit/php-token-stream": "~1.3", @@ -959,7 +959,7 @@ }, "require-dev": { "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~5" + "phpunit/phpunit": "~4" }, "suggest": { "ext-dom": "*", @@ -969,7 +969,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-master": "2.2.x-dev" } }, "autoload": { @@ -995,7 +995,7 @@ "testing", "xunit" ], - "time": "2015-11-12 21:11:35" + "time": "2015-10-06 15:47:00" }, { "name": "phpunit/php-file-iterator", @@ -1249,26 +1249,26 @@ }, { "name": "phpunit/phpunit-mock-objects", - "version": "dev-master", + "version": "2.3.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "c4dd0c9e7fcbf53c6aa48012902be692318c7566" + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/c4dd0c9e7fcbf53c6aa48012902be692318c7566", - "reference": "c4dd0c9e7fcbf53c6aa48012902be692318c7566", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", - "php": ">=5.6", + "php": ">=5.3.3", "phpunit/php-text-template": "~1.2", "sebastian/exporter": "~1.2" }, "require-dev": { - "phpunit/phpunit": "~5" + "phpunit/phpunit": "~4.4" }, "suggest": { "ext-soap": "*" @@ -1276,7 +1276,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-master": "2.3.x-dev" } }, "autoload": { @@ -1301,7 +1301,7 @@ "mock", "xunit" ], - "time": "2015-12-08 08:54:19" + "time": "2015-10-02 06:51:40" }, { "name": "sebastian/comparator", diff --git a/ruleset.xml b/ruleset.xml new file mode 100644 index 0000000..330bf57 --- /dev/null +++ b/ruleset.xml @@ -0,0 +1,59 @@ + + + UrbanIndo Coding Standard + + */tests/* + */data/* + */config/* + */views/* + */migrations/* + */messages/id/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + */migrations/* + + + */migrations/* + + + \ No newline at end of file diff --git a/src/ActiveDataProvider.php b/src/ActiveDataProvider.php index c53a3b6..3d022a5 100644 --- a/src/ActiveDataProvider.php +++ b/src/ActiveDataProvider.php @@ -1,7 +1,9 @@ */ + namespace UrbanIndo\Yii2\DynamoDb; /** @@ -28,19 +30,32 @@ class ActiveDataProvider extends \yii\data\BaseDataProvider { - protected function prepareKeys($models) + /** + * Prepares the keys associated with the currently available data models. + * @param array $models The available data models. + * @return array the keys. + */ + protected function prepareKeys(array $models) { - + $models; + return []; } + /** + * Prepares the data models that will be made available in the current page. + * @return array the available data models + */ protected function prepareModels() { - + return []; } + /** + * Returns a value indicating the total number of data models in this data provider. + * @return integer total number of data models in this data provider. + */ protected function prepareTotalCount() { - + return 0; } - } diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index dfcb4bd..876eaf9 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -1,6 +1,6 @@ */ @@ -34,7 +34,7 @@ */ class ActiveQuery extends Query implements ActiveQueryInterface { - + use ActiveQueryTrait; use ActiveRelationTrait; @@ -45,10 +45,10 @@ class ActiveQuery extends Query implements ActiveQueryInterface /** * Constructor. - * @param array $modelClass the model class associated with this query - * @param array $config configurations to be applied to the newly created query object + * @param mixed $modelClass The model class associated with this query. + * @param array $config Configurations to be applied to the newly created query object. */ - public function __construct($modelClass, $config = []) + public function __construct($modelClass, array $config = []) { $this->modelClass = $modelClass; parent::__construct($config); @@ -59,18 +59,19 @@ public function __construct($modelClass, $config = []) * This method is called at the end of the constructor. The default implementation will trigger * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end * to ensure triggering of the event. + * @return void */ public function init() { parent::init(); $this->trigger(self::EVENT_INIT); } - + /** - * @param Connection $db + * @param Connection $db The DB connection used to create the DB command. * @return ActiveRecord */ - public function one($db = null) + public function one(Connection $db = null) { /* @var $response \Guzzle\Service\Resource\Model */ $response = parent::one($db); @@ -78,12 +79,12 @@ public function one($db = null) $marshaller = new \Aws\DynamoDb\Marshaler(); return $this->createModel($value, $marshaller); } - + /** - * @param Connection $db + * @param Connection $db The DB connection used to create the DB command. * @return ActiveRecord[] */ - public function all($db = null) + public function all(Connection $db = null) { $responses = parent::all($db); $modelClass = $this->modelClass; @@ -92,15 +93,17 @@ public function all($db = null) return $this->createModel($value, $marshaller); }, $responses[$modelClass::tableName()]); } - + /** - * Create model base on return. - * @param type $value - * @param Aws\DynamoDb\Marshaler $marshaller - * @return \UrbanIndo\Yii2\DynamoDb\modelClass + * Create model based on dynamodb return value. + * @param mixed $value The return value from dynamodb. + * @param \Aws\DynamoDb\Marshaler $marshaller The marshaller. + * @return ActiveRecord */ - private function createModel($value, \Aws\DynamoDb\Marshaler $marshaller = null) - { + private function createModel( + $value, + \Aws\DynamoDb\Marshaler $marshaller = null + ) { $model = new $this->modelClass; if (!isset($marshaller)) { $marshaller = new \Aws\DynamoDb\Marshaler(); diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 9d0145b..759f214 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -1,5 +1,4 @@ @@ -41,9 +40,12 @@ */ class ActiveRecord extends BaseActiveRecord { - + /** + * Stores the primary keys loaded from table schema. + * @var array + */ protected static $_primaryKeys = []; - + /** * Returns the database connection used by this AR class. * By default, the "dynamodb" application component is used as the database connection. @@ -67,11 +69,14 @@ public static function tableName() /** * @inheritdoc + * @param array $options Additional options for the query class. * @return ActiveQuery the newly created [[ActiveQuery]] instance. */ - public static function find() + public static function find(array $options = []) { - return Yii::createObject(ActiveQuery::className(), [get_called_class()]); + return Yii::createObject(ActiveQuery::className(), array_merge($options, [ + 'class' => get_called_class() + ])); } /** @@ -102,14 +107,14 @@ public static function find() * $customer->insert(); * ~~~ * - * @param boolean $runValidation whether to perform validation before saving the record. + * @param boolean $runValidation Whether to perform validation before saving the record. * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, + * @param array $attributes List of attributes that need to be saved. Defaults to null, * meaning all attributes will be saved. * * @return boolean whether the attributes are valid and the record is inserted successfully. */ - public function insert($runValidation = true, $attributes = null) + public function insert($runValidation = true, array $attributes = null) { if ($runValidation && !$this->validate($attributes)) { Yii::info('Model not inserted due to validation error.', __METHOD__); @@ -125,7 +130,7 @@ public function insert($runValidation = true, $attributes = null) $changedAttributes = array_fill_keys(array_keys($values), null); $this->setOldAttributes($values); $this->afterSave(true, $changedAttributes); - + return $ret; } @@ -160,76 +165,95 @@ public static function primaryKey() } return self::$_primaryKeys[get_called_class()]; } - - public static function batchInsert($values) + + /** + * Batch insert values into the table. + * @param array $values The values to be inserted. + * @return mixed + */ + public static function batchInsert(array $values) { - self::getDb()->createCommand()->putItems(static::tableName(), $values); + return self::getDb()->createCommand()->putItems(static::tableName(), $values); } - + /** * Search for one object. - * @param array $condition the condition for search. - * @param array $options addition attribute. - * @return ActiveRecord + * @param mixed $condition The condition for search. + * @param array $options Additional attribute. + * @return static */ - public static function findOne($condition, $options = null) + public static function findOne($condition, array $options = null) { - return self::createQueryWithParameter($options)->where($condition)->one(); + return self::find($options)->where($condition)->one(); } - + /** * Search for all object that matches condition. - * @param array $condition the condition for search. - * @param array $options addition attribute. - * @return ActiveRecord[] + * @param mixed $condition The condition for search. + * @param array $options Additional attribute for the query class. + * @return static[] */ - public static function findAll($condition, $options = null) + public static function findAll($condition, array $options = null) { if ($options == null) { - $options = ['using' => Query::TYPE_BATCH_GET]; - } - return self::createQueryWithParameter($options)->where($condition)->all(); - } - - /** - * Create query and assign options if exists. - * @param array $options - * @return ActiveQuery the query. - */ - private static function createQueryWithParameter($options = null) - { - $query = self::find(); - if ($options !== null) { - foreach ($options as $attribute => $value) { - $query->{$attribute} = $value; - } + $options = ['using' => Query::USING_BATCH_GET_ITEM]; } - return $query; + return self::find($options)->where($condition)->all(); } /** - * @inheritdoc - * @todo + * Updates the whole table using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ```php + * Customer::updateAll(['status' => 1], 'status = 2'); + * ``` + * + * @param array $attributes Attribute values (name-value pairs) to be saved into the table. + * @param string|array $condition The conditions of the rows. + * @return void + * @throws \yii\base\NotSupportedException Not implemented yet. */ - public static function updateAll($attributes, $condition = '') + public static function updateAll(array $attributes, $condition = '') { - parent::updateAll($attributes, $condition); + throw new \yii\base\NotSupportedException(__METHOD__ . ' is not supported.'); } - + /** - * @inheritdoc - * @todo + * Updates the whole table using the provided counter changes and conditions. + * For example, to increment all customers' age by 1, + * + * ```php + * Customer::updateAllCounters(['age' => 1]); + * ``` + * + * @param array $counters The counters to be updated (attribute name => increment value). + * Use negative values if you want to decrement the counters. + * @param string|array $condition The conditions to select the rows to be updated. + * @return void + * @throws \yii\base\NotSupportedException Not implemented yet. */ - public static function updateAllCounters($counters, $condition = '') + public static function updateAllCounters(array $counters, $condition = '') { - parent::updateAllCounters($counters, $condition); + throw new \yii\base\NotSupportedException(__METHOD__ . ' is not supported.'); } - + /** - * @inheritdoc - * @todo + * Deletes rows in the table using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. + * + * For example, to delete all customers whose status is 3: + * + * ```php + * Customer::deleteAll('status = 3'); + * ``` + * + * @param string|array $condition The conditions that will select the rows. + * @param array $params The parameters (name => value) to be bound to the query. + * @return void + * @throws \yii\base\NotSupportedException Not implemented yet. */ - public static function deleteAll($condition = '', $params = []) + public static function deleteAll($condition = '', array $params = []) { parent::deleteAll($condition, $params); } diff --git a/src/Command.php b/src/Command.php index b8d8001..ab5b84a 100644 --- a/src/Command.php +++ b/src/Command.php @@ -1,7 +1,9 @@ */ + namespace UrbanIndo\Yii2\DynamoDb; use Aws\DynamoDb\DynamoDbClient; @@ -15,124 +17,140 @@ */ class Command extends Object { - + /** * @var Connection */ public $db; - public $method; - public $request; - + /** - * Initializes the object. + * The name of the DynamoDB request. For example `CreateTable`, `GetItem`. + * @var string */ - public function init() { - } + public $name; + /** + * The argument of the DynamoDB. This contains, for example `KeySchema`, + * `AttributeDefinitions`, etc. + * @var array + */ + public $argument; + /** * @return DynamoDbClient */ - protected function getClient() { + protected function getClient() + { return $this->db->getClient(); } /** - * - * @return Model + * Execute the command. + * @return array The array result of the command execution. */ - public function execute() { - Yii::info("{$this->method}: " . json_encode($this->request) , 'yii\db\Command::query'); - $command = $this->getClient()->getCommand($this->method, $this->request); - return $this->getClient()->execute($command); + public function execute() + { + Yii::info("{$this->name}: " . json_encode($this->argument), '\UrbanIndo\Yii2\DynamoDb::execute'); + $command = $this->getClient()->getCommand($this->name, $this->argument); + $result = $this->getClient()->execute($command); + /* @var $result \Guzzle\Service\Resource\Model */ + return $result->toArray(); } + /** + * Specifies the command and the argument to be requested to DynamoDB. + * @param string $name The command name. + * @param array $argument The command argument. + * @return static + */ + public function setCommand($name, array $argument) { + $this->name = $name; + $this->argument = $argument; + return $this; + } + /** * Create new table. - * @param string $tableName the name of the table. + * @param string $table the name of the table. * @param array $options valid options for `CreateTable` command. + * @return static */ - public function createTable($tableName, $options) { - $command = $this->getClient()->getCommand('CreateTable', array_merge([ - 'TableName' => $tableName, - ], - $options)); - return $this->getClient()->execute($command); + public function createTable($table, array $options) + { + list($name, $argument) = $this->db->getQueryBuilder()->createTable($table, $options); + return $this->setCommand($name, $argument); } /** - * @param string $tableName - * @param array $values + * Delete an existing table. + * @param string $table the name of the table. + * @return static */ - public function insert($tableName, $values) { - return $this->putItem($tableName, $values); + public function deleteTable($table) + { + list($name, $argument) = $this->db->getQueryBuilder()->deleteTable($table); + return $this->setCommand($name, $argument); } /** - * @param string $tableName - * @param array $values + * Describe a table. + * @param string $table the name of the table. + * @return static + */ + public function describeTable($table) + { + list($name, $argument) = $this->db->getQueryBuilder()->describeTable($table); + return $this->setCommand($name, $argument); + } + + /** + * Return whether a table exists or not. + * @param string $table The name of the table. + * @return boolean */ - public function putItem($tableName, $values) { - $marshaler = new Marshaler(); - $command = $this->getClient()->getCommand('PutItem', [ - 'TableName' => $tableName, - 'Item' => $marshaler->marshalItem($values), - ]); - $marshaler->marshalItem($values); + public function tableExists($table) + { try { - $this->getClient()->execute($command); + $this->describeTable($table)->execute(); return true; - } catch (\Exception $exc) { + } catch (\Aws\DynamoDb\Exception\ResourceNotFoundException $exc) { return false; } } + + /** + * Put a single item in the table. + * @param string $table The name of the table. + * @param array $value The values to input. + * @param array $options Additional options to the request argument. + */ + public function putItem($table, array $value, array $options = []) + { + list($name, $argument) = $this->db->getQueryBuilder()->putItem($table, $value, $options); + return $this->setCommand($name, $argument); + } + + /** + * Put a single item in the table. + * @param string $table The name of the table. + * @param mixed $key The values to input. + * @param array $options Additional options to the request argument. + */ + public function getItem($table, $key, $options = []) + { + list($name, $argument) = $this->db->getQueryBuilder()->getItem($table, $key, $options); + return $this->setCommand($name, $argument); + } + /** * @param string $tableName * @param array $values */ - public function putItems($tableName, $values) { - $unprocessedItems = []; - foreach ($values as $item) { - $unprocessedItems[] = [ - 'PutRequest' => [ - 'Item' => Marshaler::marshal($item) - ] - ]; - - } - while (!empty($unprocessedItems)) { - $chunks = array_chunk($unprocessedItems, 25); - $unprocessedItems = []; - foreach ($chunks as $chunk) { - $request = [ - 'RequestItems' => [ - $tableName => $chunk - ] - ]; - $command = $this->getClient()->getCommand('BatchWriteItem', $request); - $response = $this->getClient()->execute($command); - if (isset($response->get("UnprocessedItems")[$tableName])) { - $unprocessedItems = $unprocessedItems + $response->get("UnprocessedItems")[$tableName]; - } - } - } - return $response; - } - public function count() { - if ($this->method == Query::TYPE_GET) { - return 1; - } else if ($this->method == Query::TYPE_BATCH_GET) { - $tables = array_keys($this->request['RequestItems']); - $count = 0; - foreach ($tables as $keys) { - $count += count(reset($keys)); - } - return $count; - } else { - // TODO use query Select => COUNT - throw new NotSupportedException('Not implemented yet'); - } + public function insert($tableName, $values) + { + return $this->putItem($tableName, $values); } - + /** * Increase can be done unlimited time, decrease max 4 times a day * @param string $tablename @@ -140,7 +158,9 @@ public function count() { * @param int $writeThroughput * @return array @see http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TableDescription.html */ - public function updateThroughput($tablename, $readThroughput, $writeThroughput) { + public function updateThroughput($tablename, $readThroughput, + $writeThroughput) + { $request = [ 'TableName' => $tablename, 'ProvisionedThroughput' => [ @@ -151,41 +171,21 @@ public function updateThroughput($tablename, $readThroughput, $writeThroughput) $command = $this->getClient()->getCommand('UpdateTable', $request); return $this->getClient()->execute($command); } - /** - * @param type $tableName - * @return array @see http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TableDescription.html - */ - public function describeTable($tableName) { - $command = $this->getClient()->getCommand('DescribeTable', [ - 'TableName' => $tableName, - ]); - return $this->getClient()->execute($command)->get('Table'); - } - - /** - * Return whether a table exists or not. - * @param string $tableName the name of the table. - * @return boolean - */ - public function tableExists($tableName) { - try { - $this->describeTable($tableName); - return true; - } catch (\Aws\DynamoDb\Exception\ResourceNotFoundException $exc) { - return false; - } - } - + /** * * @return type */ - public function queryOne() { + public function queryOne() + { $response = $this->execute(); return $response; } - public function queryAll() { + + public function queryAll() + { $response = $this->execute(); return $response->get('Responses'); } + } diff --git a/src/Connection.php b/src/Connection.php index 78057c0..3483c5c 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -12,29 +12,33 @@ /** * Connection wraps DynamoDB connection for Aws PHP SDK. * - * To use the connection puts this in the config. - * - * ``` - * ``` - * * @author Petra Barus + * @see DynamoDbClient */ class Connection extends \yii\base\Component { - + /** - * @var array the configuration for DynamoDB client. + * The configuration for DynamoDB client. + * @var array */ public $config; - + /** - * @var DynamoDbClient the DynamoDB client. + * The DynamoDB client. + * @var DynamoDbClient */ protected $_client; - protected $_builder; /** - * Initialize the dynamodb client. + * The query builder. + * @var QueryBuilder + */ + protected $_builder; + + /** + * Initialize the DynamoDB client. + * @return void */ public function init() { @@ -43,7 +47,7 @@ public function init() //TODO: remove deprecated. $this->_client = DynamoDbClient::factory($this->config); } - + /** * @return DynamoDbClient */ @@ -51,13 +55,13 @@ public function getClient() { return $this->_client; } - + /** * Creates a command for execution. - * @param array $config the configuration for the Command class + * @param array $config The configuration for the Command class. * @return Command the DB command */ - public function createCommand($config = []) + public function createCommand(array $config = []) { $command = Yii::createObject(array_merge($config, [ 'class' => Command::className(), @@ -67,6 +71,7 @@ public function createCommand($config = []) } /** + * Returns the query builder for this connection. * @return QueryBuilder */ public function getQueryBuilder() @@ -75,6 +80,5 @@ public function getQueryBuilder() $this->_builder = new QueryBuilder($this); } return $this->_builder; - } } diff --git a/src/Marshaler.php b/src/Marshaler.php index 57a1326..2d76d2d 100644 --- a/src/Marshaler.php +++ b/src/Marshaler.php @@ -8,11 +8,11 @@ /** * Marshaller wraps AWS DynamoDB Marshaller. - * - * @author adinata + * @author Muhammad Adinata */ class Marshaler { + /** * @var static */ @@ -62,7 +62,7 @@ public static function marshalItem($item) * Marshal a Yii2 model object to a new array that is formatted in * the proper parameter structure required by DynamoDB operations. * - * @param \yii\base\Model $item + * @param \yii\base\Model $item The model to be marshalled. * @return mixed */ public static function marshalModel(\yii\base\Model $item) @@ -76,7 +76,7 @@ public static function marshalModel(\yii\base\Model $item) * * @param string $json A valid JSON document. * @return array - * @throws \InvalidArgumentException if the JSON is invalid. + * @throws \InvalidArgumentException If the JSON is invalid. */ public static function marshalJson($json) { @@ -89,8 +89,8 @@ public static function marshalJson($json) * * @param mixed $value A scalar, array, or stdClass value. * - * @return array Formatted like `array(TYPE => VALUE)`. - * @throws \UnexpectedValueException if the value cannot be marshaled. + * @return array Formatted like `(TYPE => VALUE)`. + * @throws \UnexpectedValueException If the value cannot be marshaled. */ public static function marshalValue($value) { @@ -120,7 +120,7 @@ public static function unmarshalItem(array $data) * @param string $class The name of the class. * * @return \yii\base\Model - * @throws \UnexpectedValueException + * @throws \InvalidArgumentException If the class is not type of \yii\base\Model. */ public static function unmarshalModel(array $data, $class) { @@ -136,12 +136,10 @@ public static function unmarshalModel(array $data, $class) * Unmarshal a document (item) from a DynamoDB operation result into a JSON * document string. * - * @param array $data Item/document from a DynamoDB result. - * @param int $jsonEncodeFlags Flags to use with `json_encode()`. - * + * @param array $json Item/document from a DynamoDB result. * @return string */ - public static function unmarshalJson($json) + public static function unmarshalJson(array $json) { return self::marshaler()->unmarshalJson($json); } @@ -151,11 +149,9 @@ public static function unmarshalJson($json) * value. Will return a scalar, array, or (if you set $mapAsObject to true) * stdClass value. * - * @param array $value Value from a DynamoDB result. - * @param bool $mapAsObject Whether maps should be represented as stdClass. + * @param mixed $value Value from a DynamoDB result. * * @return mixed - * @throws \UnexpectedValueException */ public static function unmarshalValue($value) { diff --git a/src/Pagination.php b/src/Pagination.php new file mode 100644 index 0000000..002d859 --- /dev/null +++ b/src/Pagination.php @@ -0,0 +1,15 @@ + */ @@ -12,7 +13,7 @@ use yii\base\NotSupportedException; /** - * Description of Query + * Query represents item fetching operation from DynamoDB table. * * @author Petra Barus */ @@ -21,65 +22,73 @@ class Query extends Component implements QueryInterface use QueryTrait; - const TYPE_BATCH_GET = 'BatchGetItem'; - const TYPE_GET = 'GetItem'; - const TYPE_QUERY = 'Query'; - const TYPE_SCAN = 'Scan'; - /** - * Array of attributes being selected. It will be used to build Projection Expression. - * @var array + * If the query is BatchGetItem operation, meaning the query is for multiple item using keys. + * @link http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html */ - public $select = []; - + const USING_BATCH_GET_ITEM = 'BatchGetItem'; + /** - * @var string Type of query that will be executed, 'Get', 'BatchGet', 'Query', or 'Scan'. Defaults to 'BatchGet' - * @see from() + * If the query is GetItem operation, meaning the query is for a single item using the key. + * @link http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html */ - public $using = self::TYPE_SCAN; - + const USING_GET_ITEM = 'GetItem'; + /** - * @var array list of query parameter values indexed by parameter placeholders. - * For example, `[':name' => 'Dan', ':age' => 31]`. + * If the query is Query operation, meaning it uses primary key or secondary key from the table. + * @link http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html */ - public $expressionAttributesNames = []; + const USING_QUERY = 'Query'; + + /** + * If the query is Scan operation, meaning it will access every item in the table. + * @link http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html + */ + const USING_SCAN = 'Scan'; + + /** + * This will try to detect the auto. + */ + const USING_AUTO = 'Auto'; /** - * + * Array of attributes being selected. It will be used to build Projection Expression. + * @var array */ - public $expressionAttributesValues = []; + public $select = []; /** - * - * @var type + * Type of query that will be executed, 'Get', 'BatchGet', 'Query', or 'Scan'. Defaults to 'BatchGet'. + * @var string + * @see from() + */ + public $using = self::USING_AUTO; + + /** + * Whether to use consistent read or not. + * @var boolean */ public $consistentRead; /** - * - * @var type + * Whether to return consumed capacity or not. + * @var boolean */ public $returnConsumedCapacity; /** - * - * @var type + * The table to query on. + * @var string */ public $from; - /** - * - * @var type - */ - public $keys = []; - /** * Executes the query and returns all results as an array. - * @param Connection $db the database connection used to execute the query. + * @param Connection $db The database connection used to execute the query. * If this parameter is not given, the `dynamodb` application component will be used. * @return Command */ - public function createCommand($db = null) + public function createCommand(Connection $db = null) { if ($db === null) { $db = Yii::$app->get('dynamodb'); @@ -90,18 +99,27 @@ public function createCommand($db = null) } /** - * Identifies one or more attributes to retrieve from the table. - * These attributes can include scalars, sets, or elements of a JSON document. - * - * @param string|array $attributes the attributes to be selected. Attributes can be specified in either a string separated with comma (e.g. "id, name") or an array (e.g. ['id', 'name']). see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html - * @param array $expression expression attributes name. see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ExpressionPlaceholders.html#ExpressionAttributeNames (e.g. [''MyKey' => '#mk'] - * @return Query + * Identifies one or more attributes to retrieve from the table. + * These attributes can include scalars, sets, or elements of a JSON document. + * + * @param string|array $attributes The attributes to be selected. + * Attributes can be specified in either a string separated with comma (e.g. "id, name") + * or an array (e.g. ['id', 'name']). + * See http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html . + * @param array $expression Expression attributes name. + * See http://amzn.to/1JFZP1n + * (e.g. [''MyKey' => '#mk']. + * @return static */ - public function select($attributes, $expression = []) + public function select($attributes, array $expression = []) { if (!is_array($attributes)) { - $attributes = preg_split('/\s*,\s*/', trim($attributes), -1, - PREG_SPLIT_NO_EMPTY); + $attributes = preg_split( + '/\s*,\s*/', + trim($attributes), + -1, + PREG_SPLIT_NO_EMPTY + ); } if (empty($this->select)) { $this->select = $attributes; @@ -117,23 +135,14 @@ public function select($attributes, $expression = []) return $this; } - public function from($tableName) - { - $this->from = $tableName; - } - - public function using($queryType) - { - if (!in_array($queryType, [self::TYPE_BATCH_GET, self::TYPE_GET])) { - throw new NotSupportedException('only batch get and get that is currently supported'); - } - $this->using = $queryType; - return $this; - } - - public function withExpressionAttributesName($attributes, $alias) + /** + * Sets the table name for the query. + * @param string|array $table The table(s) to be selected from. + * @return static the query object itself. + */ + public function from($table) { - $this->expressionAttributesNames[$attributes] = $alias; + $this->from = $table; return $this; } @@ -179,39 +188,40 @@ public function withoutConsumedCapacity() /** * Returns all object that matches the query. - * @param Connection $db the dynamodb connection. - * @return + * @param Connection $db The dynamodb connection. + * @return array */ - public function all($db = null) + public function all(Connection $db = null) { return $this->createCommand($db)->queryAll(); } - public function count($q = '*', $db = null) - { - // TODO: only if query and scan operations. - // batch get assumes results equal to number of id and hash - } - - public function exists($db = null) - { - return !empty($this->createCommand($db)->queryOne()); - } - - public function one($db = null) + /** + * Returns one object that matches the query. + * @param Connection $db The dynamodb connection. + * @return array + */ + public function one(Connection $db = null) { - $this->using = self::TYPE_GET; + $this->using = self::USING_GET_ITEM; return $this->createCommand($db)->queryOne(); } /** - * Starts a batch query. Doesn't necessarily have $batchSize size. - * Will call one requests for each batch instead of calling until empty like all. - * + * Returns the number of records. + * @param string $q the COUNT expression. This parameter is ignored by this implementation. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return integer number of records */ - public function batch($batchSize = 100, $db = null) + public function count($q = '*', Connection $db = null) + { + + } + + public function exists($db = null) { - // todo + } } diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 71a46f8..ff09ade 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -2,24 +2,23 @@ /** * QueryBuilder class file. + * * @author Petra Barus */ namespace UrbanIndo\Yii2\DynamoDb; -use yii\base\InvalidParamException; -use yii\base\NotSupportedException; use yii\base\Object; +use Aws\DynamoDb\Marshaler; /** - * QueryBuilder builds an elasticsearch query based on the specification given + * QueryBuilder builds an elasticsearch query based on the specification given * as a [[Query]] object. + * * @author Petra Barus */ -class QueryBuilder extends Object { - - const PARAM_PREFIX = ':var'; - +class QueryBuilder extends Object +{ /** * @var Connection the database connection. */ @@ -30,273 +29,100 @@ class QueryBuilder extends Object { * @param Connection $connection the database connection. * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct(Connection $connection, $config = []) { + public function __construct(Connection $connection, $config = []) + { $this->db = $connection; parent::__construct($config); } - + /** * Generates DynamoDB Query from a [[Query]] object. * @param Query $query object from which the query will be generated. * @return array the generated DynamoDB command configuration */ - public function build(Query $query) { - if ($query->using == Query::TYPE_BATCH_GET) { - $request = $this->buildBatchGetItem($query); - } else if ($query->using == Query::TYPE_GET) { - $request = $this->buildGetItem($query); - } - return [ - 'method'=> $query->using, - 'request' => $request - ]; - } - - private function buildGetItem(Query $query) { - $request = []; - // required - $request['Key'] = $this->buildKey($query); - $request['TableName'] = $this->buildTableName($query); - - // optional - $this->setCommonRequest($request, $query); - $this->buildConsumedCapacity($request, $query); - return $request; - } - private function buildConsumedCapacity(&$request, &$query) { - if (isset($query->returnConsumedCapacity)) { - $request['ReturnConsumedCapacity'] = $query->returnConsumedCapacity; - } - } - - private function buildKey(Query& $query) { - $condition = $query->where; - if (!is_array($condition)) { - throw new NotSupportedException('String conditions in where() are not supported by this extension. Please use attribute map ["key" => "value"].'); - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - throw new InvalidParamException('`Query::TYPE_GET` only supports hash format in condition (e.g. "key"=>"value")'); - } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - $parts = Marshaler::marshal($condition); - return $parts; - } - } - - private function buildBatchGetItem(Query $query) { - $condition = $query->where; - - if (!is_array($condition)) { - throw new NotSupportedException('String conditions in where() are not supported by dynamodb.'); - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - throw new InvalidParamException('`Query::TYPE_GET` only supports hash format in condition (e.g. "key"=>"value")'); - } - // check size - $size = null; - foreach ($condition as $key => $value) { - if ($size == null) { - $size = count($value); - } else if ($size != count($value)) { - throw new NotSupportedException('Number of comparison between hash and range query must be equals'); - } - } - $tablename = $this->buildTableName($query); - - $request = [ - 'RequestItems' => [ - $tablename => [ - 'Keys' => [] - ] - ] - ]; - $items = &$request['RequestItems'][$tablename]; + public function build(Query $query) + { - foreach (range(0, $size-1) as $i) { - $item = []; - foreach ($condition as $key => $value) { - $item[$key] = $value[$i]; - } - $items['Keys'][] = Marshaler::marshal($item); - } - - $this->setCommonRequest($items, $query); - $this->buildConsumedCapacity($request, $query); - return $request; - } - - private function buildTableName(&$query) { - return isset($query->from) ? - $query->from : // get table name from active record incase of from not called - call_user_func([$query->modelClass, 'tableName']); } /** - * - * @param type $item - * @param type $query + * Builds a DynamoDB command to create table. + * + * @param string $table The name of the table to be created. + * @param array $options Additional options for the argument. + * @return array The create table request syntax. The first element is the name of the command, + * the second is the argument. */ - private function setCommonRequest(&$item, &$query) { - if (isset($query->consistentRead)) { - $item['ConsistentRead'] = $query->consistentRead; - } - if (!empty($query->expressionAttributesNames)) { - $item['ExpressionAttributeNames'] = []; - foreach ($query->expressionAttributesNames as $key => $expression) { - $item['ExpressionAttributeNames'][$key] = $expression; - } - } - if (!empty($query->select)) { - $item['ProjectionExpression'] = $this->buildSelect($query->select); - } + public function createTable($table, array $options = []) { + $name = 'CreateTable'; + $argument = array_merge(['TableName' => $table], $options); + return [$name, $argument]; } /** - * - * @param type $select - * @return type + * Builds a DynamoDB command to describe table. + * + * @param string $table The name of the table to be created. + * @return array The create table request syntax. The first element is the name of the command, + * the second is the argument. */ - private function buildSelect($select) { - return implode(',', $select); + public function describeTable($table) { + $name = 'DescribeTable'; + $argument = ['TableName' => $table]; + return [$name, $argument]; } /** - * where -> - * key condition expression + expression attribute value, - * - if query type is batch get or get: - * - required. check whether hash and range key both are used - * - only `=` operator can be used - * - * filter expression + expression attribute value, - * - in scan type, all uses this - * - in query type, only that's not included in query filter here - * - * query filter + expression attribute value, - * - only hash and range key attribute can be filtered, other attributes goes to filter expression - * - in range key, only function with following operator allowed - * - =, <, <=, >, >=, BETWEEN, begins_with(). Other goes to filter expression - * select -> - * projection expression - * - * - * @staticvar array $builders - * @param type $condition - * @return type - * @throws NotSupportedException - * @throws InvalidParamException + * Builds a DynamoDB command to delete table. + * + * @param string $table The name of the table to be deleted. + * @return array The create table request syntax. The first element is the name of the command, + * the second is the argument. */ - private function buildCondition($condition, Query &$query) - { - static $builders = [ - 'not' => 'buildNotCondition', - 'and' => 'buildAndCondition', - 'or' => 'buildAndCondition', - 'between' => 'buildBetweenCondition', - 'not between' => 'buildBetweenCondition', - 'in' => 'buildInCondition', - 'not in' => 'buildInCondition', - 'like' => 'buildLikeCondition', - 'not like' => 'buildLikeCondition', - 'or like' => 'buildLikeCondition', - 'or not like' => 'buildLikeCondition', - ]; - if (empty($condition)) { - return []; - } - if (!is_array($condition)) { - throw new NotSupportedException('String conditions in where() are not supported by dynamodb.'); - } - - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtolower($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - - } else { - $method = 'buildSimpleCondition'; - } - array_shift($condition); - return $this->$method($operator, $condition, $query); - } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - return $this->buildHashCondition($condition, $query); - } - } - private function buildNotCondition($operator, $operands, &$query) { - if (count($operands) != 1) { - throw new InvalidParamException("Operator '$operator' requires exactly one operand."); - } - $operand = reset($operands); - if (is_array($operand)) { - $operand = $this->buildCondition($operand); - } - return [$operator => $operand]; - } - - private function buildAndCondition($operator, $operands, &$query) { - $parts = []; - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand); - } - if (!empty($operand)) { - $parts[] = $operand; - } - } - if (!empty($parts)) { - return [$operator => $parts]; - } else { - return []; - } - } - - private function buildBetweenCondition($operator, $operands, &$query) { - throw new NotSupportedException('not ready'); - } - private function buildInCondition($operator, $operands, &$query) { - throw new NotSupportedException('not ready'); - } - private function buildLikeCondition($operator, $operands) { - throw new NotSupportedException('like conditions are not supported by dynamodb.'); + public function deleteTable($table) { + $name = 'DeleteTable'; + $argument = ['TableName' => $table]; + return [$name, $argument]; } /** - * Creates an SQL expressions like `"column" operator value`. - * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. - * @param array $operands contains two column names. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if wrong number of operands have been given. + * Builds a DynamoDB command to put item. + * + * @param string $table The name of the table to be created. + * @param array $value The value to put into the table. + * @param array $options The value to put into the table. + * @return array The create table request syntax. The first element is the name of the command, + * the second is the argument. */ - public function buildSimpleCondition($operator, $operands, Query &$query) - { - if (count($operands) !== 2) { - throw new InvalidParamException("Operator '$operator' requires two operands."); - } - - list($column, $value) = $operands; - - $varname = self::PARAM_PREFIX . count($query->expressionAttributesValues); - $query->expressionAttributesValues[$varname] = Marshaler::marshal($value); - return "$column $operator $varname"; + public function putItem($table, array $value, array $options = []) { + $marshaler = new Marshaler(); + $name = 'PutItem'; + $argument = array_merge([ + 'TableName' => $table, + 'Item' => $marshaler->marshalItem($value) + ], $options); + return [$name, $argument]; } + /** - * Creates a condition based on column-value pairs. - * @param array $condition the condition specification. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression + * Builds a DynamoDB command to get item. + * + * @param string $table The name of the table to be created. + * @param mixed $key The value to put into the table. + * @param array $options The value to put into the table. + * @return array The create table request syntax. The first element is the name of the command, + * the second is the argument. */ - public function buildHashCondition($condition, Query &$query) - { - $parts = []; - foreach ($condition as $column => $value) { - if (is_array($value) || $value instanceof Query) { - // IN condition - $parts[] = $this->buildInCondition('IN', [$column, $value], $query); - } else { - $varname = self::PARAM_PREFIX . count($query->expressionAttributesValues); - $query->expressionAttributesValues[$varname] = Marshaler::marshal($value); - $parts[] = "$column=$varname"; - } + public function getItem($table, $key, array $options = []) { + $marshaler = new Marshaler(); + $name = 'GetItem'; + + //TODO build the argument base don key. + if (is_string($key) || is_numeric($key)) { + + } else { + } - return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; + return [$name, $argument]; } } diff --git a/src/TableSchema.php b/src/TableSchema.php new file mode 100644 index 0000000..babb236 --- /dev/null +++ b/src/TableSchema.php @@ -0,0 +1,21 @@ + + */ + +namespace UrbanIndo\Yii2\DynamoDb; + +use yii\base\Object; + +/** + * TableSchema represents the metadata of a DynamoDB table. + * + * @author Petra Barus + */ +class TableSchema extends Object +{ + + +} diff --git a/test/CommandTest.php b/test/CommandTest.php index aec0edd..f4b440d 100644 --- a/test/CommandTest.php +++ b/test/CommandTest.php @@ -1,21 +1,28 @@ dynamodb->getClient(); - $command = $client->getCommand('CreateTable', [ - 'TableName' => 'Testing', + public function testCreateTable() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->firstNameMale; + $fieldName1 = $faker->firstNameMale; + + $this->assertFalse($command->tableExists($tableName)); + + $command->createTable($tableName, [ 'KeySchema' => [ [ - 'AttributeName' => 'Test1', + 'AttributeName' => $fieldName1, 'KeyType' => 'HASH', ] ], 'AttributeDefinitions' => [ [ - 'AttributeName' => 'Test1', + 'AttributeName' => $fieldName1, 'AttributeType' => 'S', ] ], @@ -23,48 +30,150 @@ public function testCreate() { 'ReadCapacityUnits' => 5, 'WriteCapacityUnits' => 5, ] - ]); - $result = $client->execute($command); - /* @var $result Aws\Result */ - $description = $result->get('TableDescription'); - $this->assertNotEmpty($description); - } - public function testPut() { - /* @var $client \Aws\DynamoDb\DynamoDbClient */ - $client = Yii::$app->dynamodb->getClient(); - - $marshaler = new \UrbanIndo\Yii2\DynamoDb\Marshaler(); - $command = $client->getCommand('PutItem', [ - 'TableName' => 'Testing', - "Item" => $marshaler->marshalItem([ - 'Test1' => 'key', - "testobj1" => [ - 'ada' => [ - 'arr' => ["a", "b"], - 'p' => 'x' + ])->execute(); + + $this->assertTrue($command->tableExists($tableName)); + + $result = $command->describeTable($tableName)->execute(); + $this->assertArraySubset([ + 'Table' => [ + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', ] ], - ]) - ]); - /* @var $result \Guzzle\Service\Resource\Model */ - $result = $command->execute(); - $this->assertNotNull($result); - } - public function testGet() { - /* @var $client \Aws\DynamoDb\DynamoDbClient */ - $client = Yii::$app->dynamodb->getClient(); - $command = $client->getCommand('GetItem', [ - 'TableName' => 'Testing', - "Key" => [ - 'Test1' => [ - 'S' => 'key' + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] ], + 'TableName' => $tableName, + ] + ], $result); + } + + public function testDeleteTable() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->firstNameMale; + $fieldName1 = $faker->firstNameMale; + + $this->assertFalse($command->tableExists($tableName)); + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $this->assertTrue($command->tableExists($tableName)); + + $command->deleteTable($tableName)->execute(); + + $this->assertFalse($command->tableExists($tableName)); + } + + public function testPutItem() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->firstNameMale; + $fieldName1 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $this->assertNotFalse($command->tableExists($tableName)); + + $this->assertEquals(0, $this->getTableItemCount($tableName)); + + $command->putItem($tableName, [ + $fieldName1 => $faker->firstNameFemale, + ])->execute(); + + $this->assertEquals(1, $this->getTableItemCount($tableName)); + + $command->putItem($tableName, [ + $fieldName1 => $faker->firstNameFemale, + 'Field2' => 'Hello', + ])->execute(); + + $this->assertEquals(2, $this->getTableItemCount($tableName)); + } + + private function getTableItemCount($tableName) { + $tableDescription = $this->getConnection()->createCommand()->describeTable($tableName)->execute(); + return $tableDescription['Table']['ItemCount']; + } + + public function testGetItemUsingScalarKey() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->firstNameMale; + $fieldName1 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, ] - ]); - /* @var $result \Guzzle\Service\Resource\Model */ - $result = $command->execute(); - $result = (new \UrbanIndo\Yii2\DynamoDb\Marshaler)->unmarshalItem($result->get('Item')); - $this->assertArraySubset(['Test1' => 'key'], $result); + ])->execute(); + + + + } + + public function testGetItemUsingCompositeKey() + { + } } diff --git a/test/QueryBuilderTest.php b/test/QueryBuilderTest.php new file mode 100644 index 0000000..413df8f --- /dev/null +++ b/test/QueryBuilderTest.php @@ -0,0 +1,26 @@ +getConnection(); + return $connection->getQueryBuilder(); + } + + public function testCreateTable() + { + $qb = $this->getQueryBuilder(); + list($name, $options) = $qb->createTable('Test'); + $this->assertEquals('CreateTable', $name); + $this->assertEquals([ + 'TableName' => 'Test' + ], $options); + } +} diff --git a/test/TestCase.php b/test/TestCase.php index 3d392a6..dac53a2 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -1,37 +1,13 @@ getConnection()->createCommand(); - /* @var $command ClassName */ - if (!$command->tableExists(\test\data\Customer::tableName())) { - $command->createTable(\test\data\Customer::tableName(), [ - 'AttributeDefinitions' => [ - [ - 'AttributeName' => 'id', - 'AttributeType' => 'N' - ] - ], - 'KeySchema' => [ - [ - 'AttributeName' => 'id', - 'KeyType' => 'HASH', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 10, - 'WriteCapacityUnits' => 10 - ] - ]); - } - } +abstract class TestCase extends \PHPUnit_Framework_TestCase +{ /** * @return \UrbanIndo\Yii2\DynamoDb\Connection */ - public function getConnection() { + public function getConnection() + { return Yii::$app->dynamodb; } } From 8aa4415c29d95e623eeaef2c7c0d3c2f774bc185 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Tue, 29 Dec 2015 14:19:01 +0700 Subject: [PATCH 03/23] Add getItem query. --- src/Command.php | 17 ++-- src/QueryBuilder.php | 38 +++++++-- test/CommandTest.php | 184 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 224 insertions(+), 15 deletions(-) diff --git a/src/Command.php b/src/Command.php index ab5b84a..2040ebe 100644 --- a/src/Command.php +++ b/src/Command.php @@ -7,6 +7,7 @@ namespace UrbanIndo\Yii2\DynamoDb; use Aws\DynamoDb\DynamoDbClient; +use Aws\DynamoDb\Marshaler; use Guzzle\Service\Resource\Model; use Yii; use yii\base\NotSupportedException; @@ -123,6 +124,7 @@ public function tableExists($table) * @param string $table The name of the table. * @param array $value The values to input. * @param array $options Additional options to the request argument. + * @return static */ public function putItem($table, array $value, array $options = []) { @@ -135,6 +137,7 @@ public function putItem($table, array $value, array $options = []) * @param string $table The name of the table. * @param mixed $key The values to input. * @param array $options Additional options to the request argument. + * @return static */ public function getItem($table, $key, $options = []) { @@ -173,19 +176,17 @@ public function updateThroughput($tablename, $readThroughput, } /** - * - * @return type + * Executes the query and returns the first item of the result. + * @return array The items that are already marshaled. */ public function queryOne() { $response = $this->execute(); + if ($this->name == 'GetItem') { + $marshaller = new Marshaler(); + return $marshaller->unmarshalItem($response['Item']); + } return $response; } - public function queryAll() - { - $response = $this->execute(); - return $response->get('Responses'); - } - } diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index ff09ade..75c931a 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -10,6 +10,7 @@ use yii\base\Object; use Aws\DynamoDb\Marshaler; +use yii\helpers\ArrayHelper; /** * QueryBuilder builds an elasticsearch query based on the specification given @@ -108,21 +109,46 @@ public function putItem($table, array $value, array $options = []) { * Builds a DynamoDB command to get item. * * @param string $table The name of the table to be created. - * @param mixed $key The value to put into the table. + * @param mixed $key The key of the item to get. This can be a scalar + * (numeric or string) or an indexed array or an associative array. + * If the key is indexed array, the first element will be the primary key, + * and the second element will be the secondary key. * @param array $options The value to put into the table. * @return array The create table request syntax. The first element is the name of the command, * the second is the argument. */ public function getItem($table, $key, array $options = []) { - $marshaler = new Marshaler(); $name = 'GetItem'; - - //TODO build the argument base don key. + //TODO refactor this. + $tableDescription = $this->db->createCommand()->describeTable($table)->execute(); + $keySchema = $tableDescription['Table']['KeySchema']; + $marshaler = new Marshaler(); if (is_string($key) || is_numeric($key)) { - + if (count($keySchema) > 1) { + throw new \InvalidArgumentException('Can not use scalar key argument on table with multiple key'); + } + $keyName = $keySchema[0]['AttributeName']; + $keyArgument = [ + $keyName => $marshaler->marshalValue($key), + ]; } else { - + $keyArgument = []; + if (ArrayHelper::isIndexed($key)) { + foreach ($key as $i => $value) { + $keyArgument[$keySchema[$i]['AttributeName']] = $marshaler->marshalValue($value); + } + } else { + foreach ($key as $i => $value) { + $keyArgument[$i] = $marshaler->marshalValue($value); + } + } } + + $argument = array_merge( + ['TableName' => $table], + ['Key' => $keyArgument], + $options + ); return [$name, $argument]; } } diff --git a/test/CommandTest.php b/test/CommandTest.php index f4b440d..0cac794 100644 --- a/test/CommandTest.php +++ b/test/CommandTest.php @@ -167,13 +167,195 @@ public function testGetItemUsingScalarKey() ] ])->execute(); + $id = $faker->firstNameFemale; + $value = [ + $fieldName1 => $id, + 'Field2' => 'Hello', + ]; + $command->putItem($tableName, $value)->execute(); + + $result = $command->getItem($tableName, $id)->queryOne(); + + $this->assertEquals($value, $result); + + } + + public function testGetItemUsingCompositeIndexedArrayKeyWithOneElement() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->firstNameMale; + $fieldName1 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $id = $faker->firstNameFemale; + $value = [ + $fieldName1 => $id, + 'Field2' => 'Hello', + ]; + $command->putItem($tableName, $value)->execute(); + + $result = $command->getItem($tableName, [$id])->queryOne(); + + $this->assertEquals($value, $result); + } + + public function testGetItemUsingCompositeIndexedArrayKeyWithTwoElement() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->firstNameMale; + $fieldName1 = $faker->firstNameFemale; + $fieldName2 = $faker->firstNameFemale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ], + [ + 'AttributeName' => $fieldName2, + 'KeyType' => 'RANGE', + ], + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ], + [ + 'AttributeName' => $fieldName2, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $id1 = $faker->firstNameFemale; + $id2 = $faker->firstNameFemale; + $value = [ + $fieldName1 => $id1, + $fieldName2 => $id2, + 'Field2' => 'Hello', + ]; + $command->putItem($tableName, $value)->execute(); + $result = $command->getItem($tableName, [$id1, $id2])->queryOne(); + $this->assertEquals($value, $result); } - public function testGetItemUsingCompositeKey() + public function testGetItemUsingCompositeAssociativeArrayKeyWithOneElement() { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->firstNameMale; + $fieldName1 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $id = $faker->firstNameFemale; + $value = [ + $fieldName1 => $id, + 'Field2' => 'Hello', + ]; + $command->putItem($tableName, $value)->execute(); + + $result = $command->getItem($tableName, [$fieldName1 => $id])->queryOne(); + + $this->assertEquals($value, $result); + } + + public function testGetItemUsingCompositeAssociativeArrayKeyWithTwoElement() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->firstNameMale; + $fieldName1 = $faker->firstNameFemale; + $fieldName2 = $faker->firstNameFemale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ], + [ + 'AttributeName' => $fieldName2, + 'KeyType' => 'RANGE', + ], + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ], + [ + 'AttributeName' => $fieldName2, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $id1 = $faker->firstNameFemale; + $id2 = $faker->firstNameFemale; + $value = [ + $fieldName1 => $id1, + $fieldName2 => $id2, + 'Field2' => 'Hello', + ]; + $command->putItem($tableName, $value)->execute(); + + $result = $command->getItem($tableName, [$fieldName1 => $id1, $fieldName2 => $id2])->queryOne(); + $this->assertEquals($value, $result); } } From 01a6ff650b25724c406d8ea037f172d3f95700b0 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Tue, 29 Dec 2015 16:15:24 +0700 Subject: [PATCH 04/23] Batch Get Item. --- src/Command.php | 69 +++------ src/QueryBuilder.php | 255 ++++++++++++++++++++++++++----- test/CommandTest.php | 353 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 577 insertions(+), 100 deletions(-) diff --git a/src/Command.php b/src/Command.php index 2040ebe..7e70c8b 100644 --- a/src/Command.php +++ b/src/Command.php @@ -64,7 +64,8 @@ public function execute() * @param array $argument The command argument. * @return static */ - public function setCommand($name, array $argument) { + public function setCommand($name, array $argument) + { $this->name = $name; $this->argument = $argument; return $this; @@ -72,8 +73,8 @@ public function setCommand($name, array $argument) { /** * Create new table. - * @param string $table the name of the table. - * @param array $options valid options for `CreateTable` command. + * @param string $table The name of the table. + * @param array $options Valid options for `CreateTable` command. * @return static */ public function createTable($table, array $options) @@ -84,7 +85,7 @@ public function createTable($table, array $options) /** * Delete an existing table. - * @param string $table the name of the table. + * @param string $table The name of the table. * @return static */ public function deleteTable($table) @@ -95,7 +96,7 @@ public function deleteTable($table) /** * Describe a table. - * @param string $table the name of the table. + * @param string $table The name of the table. * @return static */ public function describeTable($table) @@ -133,60 +134,32 @@ public function putItem($table, array $value, array $options = []) } /** - * Put a single item in the table. + * Get a single item from table. * @param string $table The name of the table. - * @param mixed $key The values to input. + * @param mixed $key The key of the row. * @param array $options Additional options to the request argument. * @return static */ - public function getItem($table, $key, $options = []) + public function getItem($table, $key, array $options = []) { list($name, $argument) = $this->db->getQueryBuilder()->getItem($table, $key, $options); return $this->setCommand($name, $argument); } /** - * @param string $tableName - * @param array $values - */ - public function insert($tableName, $values) - { - return $this->putItem($tableName, $values); - } - - /** - * Increase can be done unlimited time, decrease max 4 times a day - * @param string $tablename - * @param int $readThroughput - * @param int $writeThroughput - * @return array @see http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TableDescription.html - */ - public function updateThroughput($tablename, $readThroughput, - $writeThroughput) - { - $request = [ - 'TableName' => $tablename, - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => $readThroughput, - 'WriteCapacityUnits' => $writeThroughput - ] - ]; - $command = $this->getClient()->getCommand('UpdateTable', $request); - return $this->getClient()->execute($command); - } - - /** - * Executes the query and returns the first item of the result. - * @return array The items that are already marshaled. + * Get multiple items from table using keys. + * + * @param string $table The name of the table. + * @param array $keys The keys of the row. This can be indexed array of + * scalar value, indexed array of array of scalar value, indexed array of + * associative array. + * @param array $options Additional options to the request argument. + * @return static + * @see QueryBuilder::batchGetItem */ - public function queryOne() + public function batchGetItem($table, array $keys, array $options = []) { - $response = $this->execute(); - if ($this->name == 'GetItem') { - $marshaller = new Marshaler(); - return $marshaller->unmarshalItem($response['Item']); - } - return $response; + list($name, $argument) = $this->db->getQueryBuilder()->batchGetItem($table, $keys, $options); + return $this->setCommand($name, $argument); } - } diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 75c931a..b488da6 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -1,5 +1,4 @@ db = $connection; parent::__construct($config); @@ -38,12 +37,13 @@ public function __construct(Connection $connection, $config = []) /** * Generates DynamoDB Query from a [[Query]] object. - * @param Query $query object from which the query will be generated. - * @return array the generated DynamoDB command configuration + * @param Query $query Object from which the query will be generated. + * @return array The generated DynamoDB command configuration. */ public function build(Query $query) { - + $query; + return []; } /** @@ -54,7 +54,8 @@ public function build(Query $query) * @return array The create table request syntax. The first element is the name of the command, * the second is the argument. */ - public function createTable($table, array $options = []) { + public function createTable($table, array $options = []) + { $name = 'CreateTable'; $argument = array_merge(['TableName' => $table], $options); return [$name, $argument]; @@ -63,11 +64,12 @@ public function createTable($table, array $options = []) { /** * Builds a DynamoDB command to describe table. * - * @param string $table The name of the table to be created. + * @param string $table The name of the table to be created. * @return array The create table request syntax. The first element is the name of the command, * the second is the argument. */ - public function describeTable($table) { + public function describeTable($table) + { $name = 'DescribeTable'; $argument = ['TableName' => $table]; return [$name, $argument]; @@ -76,11 +78,12 @@ public function describeTable($table) { /** * Builds a DynamoDB command to delete table. * - * @param string $table The name of the table to be deleted. + * @param string $table The name of the table to be deleted. * @return array The create table request syntax. The first element is the name of the command, * the second is the argument. */ - public function deleteTable($table) { + public function deleteTable($table) + { $name = 'DeleteTable'; $argument = ['TableName' => $table]; return [$name, $argument]; @@ -95,7 +98,8 @@ public function deleteTable($table) { * @return array The create table request syntax. The first element is the name of the command, * the second is the argument. */ - public function putItem($table, array $value, array $options = []) { + public function putItem($table, array $value, array $options = []) + { $marshaler = new Marshaler(); $name = 'PutItem'; $argument = array_merge([ @@ -113,35 +117,21 @@ public function putItem($table, array $value, array $options = []) { * (numeric or string) or an indexed array or an associative array. * If the key is indexed array, the first element will be the primary key, * and the second element will be the secondary key. - * @param array $options The value to put into the table. + * @param array $options The additional options for the request. * @return array The create table request syntax. The first element is the name of the command, * the second is the argument. */ - public function getItem($table, $key, array $options = []) { + public function getItem($table, $key, array $options = []) + { $name = 'GetItem'; - //TODO refactor this. + $tableDescription = $this->db->createCommand()->describeTable($table)->execute(); $keySchema = $tableDescription['Table']['KeySchema']; - $marshaler = new Marshaler(); + if (is_string($key) || is_numeric($key)) { - if (count($keySchema) > 1) { - throw new \InvalidArgumentException('Can not use scalar key argument on table with multiple key'); - } - $keyName = $keySchema[0]['AttributeName']; - $keyArgument = [ - $keyName => $marshaler->marshalValue($key), - ]; + $keyArgument = $this->buildGetItemScalarKey($keySchema, $key); } else { - $keyArgument = []; - if (ArrayHelper::isIndexed($key)) { - foreach ($key as $i => $value) { - $keyArgument[$keySchema[$i]['AttributeName']] = $marshaler->marshalValue($value); - } - } else { - foreach ($key as $i => $value) { - $keyArgument[$i] = $marshaler->marshalValue($value); - } - } + $keyArgument = $this->buildGetItemCompositeKey($keySchema, $key); } $argument = array_merge( @@ -151,4 +141,201 @@ public function getItem($table, $key, array $options = []) { ); return [$name, $argument]; } + + /** + * @param array $keySchema The schema of the key in the table. + * @param mixed $key The key either string or integer. + * @return array + * @throws \InvalidArgumentException When the key in argument is scalar but + * the table has multiple keys. + */ + public function buildGetItemScalarKey(array $keySchema, $key) + { + $marshaler = new Marshaler(); + if (count($keySchema) > 1) { + throw new \InvalidArgumentException('Can not use scalar key argument on table with multiple key'); + } + $keyName = $keySchema[0]['AttributeName']; + return [ + $keyName => $marshaler->marshalValue($key), + ]; + } + + /** + * @param array $keySchema The schema of the key in the table. + * @param array $keys The key as indexed key or associative key. + * @return array + */ + public function buildGetItemCompositeKey(array $keySchema, array $keys) + { + $marshaler = new Marshaler(); + + $keyArgument = []; + if (ArrayHelper::isIndexed($keys)) { + foreach ($keys as $i => $value) { + $keyArgument[$keySchema[$i]['AttributeName']] = $marshaler->marshalValue($value); + } + } else { + foreach ($keys as $i => $value) { + $keyArgument[$i] = $marshaler->marshalValue($value); + } + } + return $keyArgument; + } + + /** + * Builds a DynamoDB command for batch get item. + * + * @param string $table The name of the table to be created. + * @param array $keys The keys of the row to get. + * This can be + * 1) indexed array of scalar value for table with single key, + * + * e.g. ['value1', 'value2', 'value3', 'value4'] + * + * 2) indexed array of array of scalar value for table with multiple key, + * + * e.g. [ + * ['value11', 'value12'], + * ['value21', 'value22'], + * ['value31', 'value32'], + * ['value41', 'value42'], + * ] + * + * The first scalar will be the primary (or hash) key, the second will be the + * secondary (or range) key. + * + * 3) indexed array of associative array + * + * e.g. [ + * ['attribute1' => 'value11', 'attribute2' => 'value12'], + * ['attribute1' => 'value21', 'attribute2' => 'value22'], + * ['attribute1' => 'value31', 'attribute2' => 'value32'], + * ['attribute1' => 'value41', 'attribute2' => 'value42'], + * ] + * + * 4) or associative of scalar values. + * + * e.g. [ + * 'attribute1' => ['value11', 'value21', 'value31', 'value41'] + * 'attribute2' => ['value12', 'value22', 'value32', 'value42'] + * ]. + * + * @param array $options Additional options for the final argument. + * @param array $requestItemOptions Additional options for the request item. + * @return array The create table request syntax. The first element is the name of the command, + * the second is the argument. + */ + public function batchGetItem($table, array $keys, array $options = [], array $requestItemOptions = []) + { + $name = 'BatchGetItem'; + + $tableDescription = $this->db->createCommand()->describeTable($table)->execute(); + $keySchema = $tableDescription['Table']['KeySchema']; + + if (ArrayHelper::isIndexed($keys)) { + $isScalar = is_string($keys[0]) || is_numeric($keys[0]); + if ($isScalar) { + $keyArgument = $this->buildBatchGetItemFromIndexedArrayOfScalar($keySchema, $keys); + } elseif (ArrayHelper::isIndexed($keys[0])) { + $keyArgument = $this->buildBatchGetItemFromIndexedArrayOfIndexedArray($keySchema, $keys); + } else { + $keyArgument = $this->buildBatchGetItemFromIndexedArrayOfAssociativeArray($keySchema, $keys); + } + } else { + $keyArgument = $this->buildBatchGetItemFromAssociativeArray($keySchema, $keys); + } + + $tableArgument = array_merge([ + $table => [ + 'Keys' => $keyArgument + ] + ], $requestItemOptions); + + $argument = array_merge(['RequestItems' => $tableArgument], $options); + return [$name, $argument]; + } + + /** + * @param array $keySchema The KeySchema of the table. + * @param array $keys Indexed array of scalar element. + * @return array + * @throws \InvalidArgumentException When the table has multiple key. + */ + public function buildBatchGetItemFromIndexedArrayOfScalar(array $keySchema, array $keys) + { + $marshaler = new Marshaler(); + if (count($keySchema) > 1) { + throw new \InvalidArgumentException('Can not use scalar key argument on table with multiple key'); + } + $attribute = $keySchema[0]['AttributeName']; + return array_map(function ($key) use ($attribute, $marshaler) { + return [ + $attribute => $marshaler->marshalValue($key), + ]; + }, $keys); + } + + /** + * @param array $keySchema The KeySchema of the table. + * @param array $keys Indexed array of indexed array. + * @return array + * @throws \InvalidArgumentException When the table has multiple key. + */ + public function buildBatchGetItemFromIndexedArrayOfIndexedArray(array $keySchema, array $keys) + { + $marshaler = new Marshaler(); + return array_map(function ($key) use ($keySchema, $marshaler) { + $return = []; + foreach ($key as $i => $value) { + $return[$keySchema[$i]['AttributeName']] = $marshaler->marshalValue($value); + } + return $return; + }, $keys); + } + + /** + * @param array $keySchema The KeySchema of the table. + * @param array $keys Indexed array of associative array. + * @return array + * @throws \InvalidArgumentException When the table has multiple key. + */ + public function buildBatchGetItemFromIndexedArrayOfAssociativeArray(array $keySchema, array $keys) + { + $marshaler = new Marshaler(); + $keySchema; + return array_map(function ($key) use ($marshaler) { + $return = []; + foreach ($key as $i => $value) { + $return[$i] = $marshaler->marshalValue($value); + } + return $return; + }, $keys); + } + + /** + * @param array $keySchema The KeySchema of the table. + * @param array $keys Associative array of indexed scalar. + * @return array + * @throws \InvalidArgumentException When the table has multiple key. + */ + public function buildBatchGetItemFromAssociativeArray(array $keySchema, array $keys) + { + $attributes = array_keys($keys); + $countKeyInEachAttributes = array_values(array_map(function ($key) { + return count($key); + }, $keys)); + if (count(array_unique($countKeyInEachAttributes)) != 1) { + throw new \InvalidArgumentException('The number of keys is not the same'); + } + $countKey = $countKeyInEachAttributes[0]; + $indexedKey = []; + foreach (range(1, $countKey) as $i) { + $k = $i - 1; + foreach ($attributes as $attribute) { + $indexedKey[$k][$attribute] = $keys[$attribute][$k]; + } + } + return $this->buildBatchGetItemFromIndexedArrayOfAssociativeArray($keySchema, $indexedKey); + } } diff --git a/test/CommandTest.php b/test/CommandTest.php index 0cac794..ff7909b 100644 --- a/test/CommandTest.php +++ b/test/CommandTest.php @@ -3,12 +3,15 @@ class CommandTest extends TestCase { + /** + * @group createTable + */ public function testCreateTable() { $db = $this->getConnection(); $command = $db->createCommand(); $faker = \Faker\Factory::create(); - $tableName = $faker->firstNameMale; + $tableName = $faker->uuid; $fieldName1 = $faker->firstNameMale; $this->assertFalse($command->tableExists($tableName)); @@ -54,12 +57,15 @@ public function testCreateTable() ], $result); } + /** + * @group deleteTable + */ public function testDeleteTable() { $db = $this->getConnection(); $command = $db->createCommand(); $faker = \Faker\Factory::create(); - $tableName = $faker->firstNameMale; + $tableName = $faker->uuid; $fieldName1 = $faker->firstNameMale; $this->assertFalse($command->tableExists($tableName)); @@ -90,12 +96,15 @@ public function testDeleteTable() $this->assertFalse($command->tableExists($tableName)); } + /** + * @group putItem + */ public function testPutItem() { $db = $this->getConnection(); $command = $db->createCommand(); $faker = \Faker\Factory::create(); - $tableName = $faker->firstNameMale; + $tableName = $faker->uuid; $fieldName1 = $faker->firstNameMale; $command->createTable($tableName, [ @@ -140,12 +149,15 @@ private function getTableItemCount($tableName) { return $tableDescription['Table']['ItemCount']; } + /** + * @group getItem + */ public function testGetItemUsingScalarKey() { $db = $this->getConnection(); $command = $db->createCommand(); $faker = \Faker\Factory::create(); - $tableName = $faker->firstNameMale; + $tableName = $faker->uuid; $fieldName1 = $faker->firstNameMale; $command->createTable($tableName, [ @@ -174,18 +186,21 @@ public function testGetItemUsingScalarKey() ]; $command->putItem($tableName, $value)->execute(); - $result = $command->getItem($tableName, $id)->queryOne(); + $result = $command->getItem($tableName, $id)->execute(); - $this->assertEquals($value, $result); + $this->assertNotEmpty($result); } + /** + * @group getItem + */ public function testGetItemUsingCompositeIndexedArrayKeyWithOneElement() { $db = $this->getConnection(); $command = $db->createCommand(); $faker = \Faker\Factory::create(); - $tableName = $faker->firstNameMale; + $tableName = $faker->uuid; $fieldName1 = $faker->firstNameMale; $command->createTable($tableName, [ @@ -214,17 +229,20 @@ public function testGetItemUsingCompositeIndexedArrayKeyWithOneElement() ]; $command->putItem($tableName, $value)->execute(); - $result = $command->getItem($tableName, [$id])->queryOne(); + $result = $command->getItem($tableName, [$id])->execute(); - $this->assertEquals($value, $result); + $this->assertNotEmpty($result); } + /** + * @group getItem + */ public function testGetItemUsingCompositeIndexedArrayKeyWithTwoElement() { $db = $this->getConnection(); $command = $db->createCommand(); $faker = \Faker\Factory::create(); - $tableName = $faker->firstNameMale; + $tableName = $faker->uuid; $fieldName1 = $faker->firstNameFemale; $fieldName2 = $faker->firstNameFemale; @@ -264,17 +282,20 @@ public function testGetItemUsingCompositeIndexedArrayKeyWithTwoElement() ]; $command->putItem($tableName, $value)->execute(); - $result = $command->getItem($tableName, [$id1, $id2])->queryOne(); + $result = $command->getItem($tableName, [$id1, $id2])->execute(); - $this->assertEquals($value, $result); + $this->assertNotEmpty($result); } + /** + * @group getItem + */ public function testGetItemUsingCompositeAssociativeArrayKeyWithOneElement() { $db = $this->getConnection(); $command = $db->createCommand(); $faker = \Faker\Factory::create(); - $tableName = $faker->firstNameMale; + $tableName = $faker->uuid; $fieldName1 = $faker->firstNameMale; $command->createTable($tableName, [ @@ -303,17 +324,20 @@ public function testGetItemUsingCompositeAssociativeArrayKeyWithOneElement() ]; $command->putItem($tableName, $value)->execute(); - $result = $command->getItem($tableName, [$fieldName1 => $id])->queryOne(); + $result = $command->getItem($tableName, [$fieldName1 => $id])->execute(); - $this->assertEquals($value, $result); + $this->assertNotEmpty($result); } + /** + * @group getItem + */ public function testGetItemUsingCompositeAssociativeArrayKeyWithTwoElement() { $db = $this->getConnection(); $command = $db->createCommand(); $faker = \Faker\Factory::create(); - $tableName = $faker->firstNameMale; + $tableName = $faker->uuid; $fieldName1 = $faker->firstNameFemale; $fieldName2 = $faker->firstNameFemale; @@ -353,9 +377,302 @@ public function testGetItemUsingCompositeAssociativeArrayKeyWithTwoElement() ]; $command->putItem($tableName, $value)->execute(); - $result = $command->getItem($tableName, [$fieldName1 => $id1, $fieldName2 => $id2])->queryOne(); + $result = $command->getItem($tableName, [$fieldName1 => $id1, $fieldName2 => $id2])->execute(); - $this->assertEquals($value, $result); + $this->assertNotEmpty($result); + } + + /** + * @group batchGetItem + */ + public function testBatchGetItemUsingIndexedArrayOfScalarElement() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->uuid; + $fieldName1 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $ids = array_map(function() use ($faker) { + return $faker->uuid; + }, range(1, 10)); + + + $values = array_map(function($id) use ($fieldName1, $faker) { + return [ + $fieldName1 => $id, + 'Field2' => $faker->firstName, + ]; + }, $ids); + foreach ($values as $value) { + $command->putItem($tableName, $value)->execute(); + } + $this->assertEquals(10, $this->getTableItemCount($tableName)); + + $result = $command->batchGetItem($tableName, $ids)->execute(); + + $this->assertNotEmpty($result); + $this->assertEmpty($result['UnprocessedKeys']); + } + + /** + * @group batchGetItem + */ + public function testBatchGetItemUsingIndexedArrayOfIndexedArrayOnOneKey() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->uuid; + $fieldName1 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $ids = array_map(function() use ($faker) { + return [ + $faker->uuid, + ]; + }, range(1, 10)); + + $values = array_map(function($id) use ($fieldName1, $faker) { + return [ + $fieldName1 => $id[0], + 'Field2' => $faker->firstName, + ]; + }, $ids); + foreach ($values as $value) { + $command->putItem($tableName, $value)->execute(); + } + $this->assertEquals(10, $this->getTableItemCount($tableName)); + + $result = $command->batchGetItem($tableName, $ids)->execute(); + + $this->assertNotEmpty($result); + $this->assertEmpty($result['UnprocessedKeys']); + } + + /** + * @group batchGetItem + */ + public function testBatchGetItemUsingIndexedArrayOfIndexedArrayOnTwoKey() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->uuid; + $fieldName1 = $faker->firstNameMale; + $fieldName2 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ], + [ + 'AttributeName' => $fieldName2, + 'KeyType' => 'RANGE', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ], + [ + 'AttributeName' => $fieldName2, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $ids = array_map(function() use ($faker) { + return [ + $faker->uuid, + $faker->uuid, + ]; + }, range(1, 10)); + + $values = array_map(function($id) use ($fieldName1, $fieldName2, $faker) { + return [ + $fieldName1 => $id[0], + $fieldName2 => $id[1], + 'Field2' => $faker->firstName, + ]; + }, $ids); + foreach ($values as $value) { + $command->putItem($tableName, $value)->execute(); + } + $this->assertEquals(10, $this->getTableItemCount($tableName)); + + $result = $command->batchGetItem($tableName, $ids)->execute(); + + $this->assertNotEmpty($result); + $this->assertEmpty($result['UnprocessedKeys']); + } + + /** + * @group batchGetItem + */ + public function testBatchGetItemUsingIndexedArrayOfAssociativeArray() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->uuid; + $fieldName1 = $faker->firstNameMale; + $fieldName2 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ], + [ + 'AttributeName' => $fieldName2, + 'KeyType' => 'RANGE', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ], + [ + 'AttributeName' => $fieldName2, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $ids = array_map(function() use ($fieldName1, $fieldName2, $faker) { + return [ + $fieldName1 => $faker->uuid, + $fieldName2 => $faker->uuid, + ]; + }, range(1, 10)); + + $values = array_map(function($id) use ($faker) { + return array_merge($id, [ + 'Field2' => $faker->firstName, + ]); + }, $ids); + foreach ($values as $value) { + $command->putItem($tableName, $value)->execute(); + } + $this->assertEquals(10, $this->getTableItemCount($tableName)); + + $result = $command->batchGetItem($tableName, $ids)->execute(); + $this->assertNotEmpty($result); + $this->assertEmpty($result['UnprocessedKeys']); + } + + /** + * @group batchGetItem + */ + public function testBatchGetItemUsingAssociativeArrayOnTwoKeys() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->uuid; + $fieldName1 = $faker->firstNameMale; + $fieldName2 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ], + [ + 'AttributeName' => $fieldName2, + 'KeyType' => 'RANGE', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ], + [ + 'AttributeName' => $fieldName2, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + $ids = []; + $values = []; + foreach (range(1, 10) as $i) { + $field1 = $faker->uuid; + $field2 = $faker->uuid; + $ids[$fieldName1][] = $field1; + $ids[$fieldName2][] = $field2; + $values[] = [ + $fieldName1 => $field1, + $fieldName2 => $field2, + ]; + } + + foreach ($values as $value) { + $command->putItem($tableName, $value)->execute(); + } + + $this->assertEquals(10, $this->getTableItemCount($tableName)); + + $result = $command->batchGetItem($tableName, $ids)->execute(); + $this->assertNotEmpty($result); + $this->assertEmpty($result['UnprocessedKeys']); } } From 0060d49f5272522d750a5e8fe3f2fab440d76f3a Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Tue, 29 Dec 2015 17:11:13 +0700 Subject: [PATCH 05/23] Scan --- src/ActiveRecord.php | 12 +++--- src/Command.php | 12 ++++++ src/QueryBuilder.php | 17 ++++++++ test/CommandTest.php | 95 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 7 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 759f214..1f223a3 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -150,12 +150,10 @@ public function insert($runValidation = true, array $attributes = null) public static function primaryKey() { if (!isset(self::$_primaryKeys[get_called_class()])) { - $client = self::getDb()->getClient(); - $command = $client->getCommand('DescribeTable', [ - 'TableName' => self::tableName() - ]); - $result = $client->execute($command); - $keySchema = $result['KeySchema']; + $description = self::getDb()->createCommand() + ->describeTable(self::tableName()) + ->execute(); + $keySchema = $description['KeySchema']; $keys = []; foreach ($keySchema as $key) { $idx = $key['KeyType'] == 'HASH' ? 0 : 1; @@ -255,6 +253,6 @@ public static function updateAllCounters(array $counters, $condition = '') */ public static function deleteAll($condition = '', array $params = []) { - parent::deleteAll($condition, $params); + throw new \yii\base\NotSupportedException(__METHOD__ . ' is not supported.'); } } diff --git a/src/Command.php b/src/Command.php index 7e70c8b..263deb9 100644 --- a/src/Command.php +++ b/src/Command.php @@ -146,6 +146,18 @@ public function getItem($table, $key, array $options = []) return $this->setCommand($name, $argument); } + /** + * Scan table. + * @param string $table The name of the table. + * @param array $options Options to the request argument. + * @return static + */ + public function scan($table, array $options = []) + { + list($name, $argument) = $this->db->getQueryBuilder()->scan($table, $options); + return $this->setCommand($name, $argument); + } + /** * Get multiple items from table using keys. * diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index b488da6..5262cc7 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -338,4 +338,21 @@ public function buildBatchGetItemFromAssociativeArray(array $keySchema, array $k } return $this->buildBatchGetItemFromIndexedArrayOfAssociativeArray($keySchema, $indexedKey); } + + /** + * Builds a DynamoDB command to scan table. + * + * @param string $table The name of the table to scan. + * @param array $options The scan options. + * @return array The create table request syntax. The first element is the name of the command, + * the second is the argument. + */ + public function scan($table, array $options = []) + { + $name = 'Scan'; + $argument = array_merge([ + 'TableName' => $table, + ], $options); + return [$name, $argument]; + } } diff --git a/test/CommandTest.php b/test/CommandTest.php index ff7909b..3ba8f9d 100644 --- a/test/CommandTest.php +++ b/test/CommandTest.php @@ -674,5 +674,100 @@ public function testBatchGetItemUsingAssociativeArrayOnTwoKeys() $this->assertNotEmpty($result); $this->assertEmpty($result['UnprocessedKeys']); } + + /** + * @group scan + */ + public function testScan() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->uuid; + $fieldName1 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + foreach (range(1, 10) as $i) { + $command->putItem($tableName, [ + $fieldName1 => $faker->uuid, + 'field2' => $i, + 'field3' => $i, + 'field4' => $i % 3, + ])->execute(); + } + + $result1 = $command->scan($tableName, [ + 'FilterExpression' => 'field2 > :val1', + 'ExpressionAttributeValues' => [ + ':val1' => ['N' => '3'] + ], + ])->execute(); + + $this->assertNotEmpty($result1); + $this->assertEquals(7, $result1['Count']); + + $result2 = $command->scan($tableName, [ + 'FilterExpression' => 'field2 > :val1 AND field2 < :val2', + 'ExpressionAttributeValues' => [ + ':val1' => ['N' => '3'], + ':val2' => ['N' => '6'] + ], + ])->execute(); + + $this->assertNotEmpty($result2); + $this->assertEquals(2, $result2['Count']); + + $result3 = $command->scan($tableName, [ + 'FilterExpression' => 'field2 > :val1 AND field4 = :val2', + 'ExpressionAttributeValues' => [ + ':val1' => ['N' => '3'], + ':val2' => ['N' => '2'] + ], + ])->execute(); + + $this->assertNotEmpty($result3); + $this->assertEquals(2, $result3['Count']); + + $result4 = $command->scan($tableName, [ + 'FilterExpression' => 'field4 = :val1 OR field1 = :val2', + 'ExpressionAttributeValues' => [ + ':val1' => ['N' => '2'], + ':val2' => ['N' => '4'] + ], + ])->execute(); + + $this->assertNotEmpty($result4); + $this->assertEquals(3, $result4['Count']); + + $result5 = $command->scan($tableName, [ + 'FilterExpression' => 'field4 = :val1 AND (field1 = :val2 OR field2 = :val3)', + 'ExpressionAttributeValues' => [ + ':val1' => ['N' => '2'], + ':val2' => ['N' => '4'], + ':val3' => ['N' => '5'] + ], + ])->execute(); + + $this->assertNotEmpty($result5); + $this->assertEquals(1, $result5['Count']); + } } From b43e02d90ee1ce462b2e22e1fa112a8e2f6927a7 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Wed, 30 Dec 2015 06:03:13 +0700 Subject: [PATCH 06/23] Pagination class. --- ruleset.xml | 1 + src/ActiveDataProvider.php | 98 +++++++++++++++++- src/ActiveQuery.php | 8 +- src/ActiveRecord.php | 48 +++++++++ src/Pagination.php | 170 ++++++++++++++++++++++++++++++++ test/ActiveDataProviderTest.php | 6 ++ test/PaginationTest.php | 115 +++++++++++++++++++++ test/TestCase.php | 27 +++++ 8 files changed, 470 insertions(+), 3 deletions(-) create mode 100644 test/ActiveDataProviderTest.php create mode 100644 test/PaginationTest.php diff --git a/ruleset.xml b/ruleset.xml index 330bf57..ad32c5b 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -3,6 +3,7 @@ UrbanIndo Coding Standard */tests/* + */test/* */data/* */config/* */views/* diff --git a/src/ActiveDataProvider.php b/src/ActiveDataProvider.php index 3d022a5..11c116b 100644 --- a/src/ActiveDataProvider.php +++ b/src/ActiveDataProvider.php @@ -6,6 +6,11 @@ namespace UrbanIndo\Yii2\DynamoDb; +use Yii; +use yii\base\InvalidParamException; +use yii\di\Instance; +use yii\helpers\ArrayHelper; + /** * ActiveDataProvider implements a data provider based on DynamoDB Query and ActiveQuery. * @@ -29,6 +34,31 @@ */ class ActiveDataProvider extends \yii\data\BaseDataProvider { + /** + * @var Query the query that is used to fetch data models and [[totalCount]] + * if it is not explicitly set. + */ + public $query; + + /** + * @var Connection|array|string the DB connection object or the application component ID of the DB connection. + * If not set, the default DB connection will be used. + */ + public $db = 'dynamodb'; + + /** + * Initializes the DB connection component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @return void + * @throws InvalidConfigException If [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Instance::ensure($this->db, Connection::className()); + } + } /** * Prepares the keys associated with the currently available data models. @@ -44,10 +74,28 @@ protected function prepareKeys(array $models) /** * Prepares the data models that will be made available in the current page. * @return array the available data models + * @throws \yii\base\InvalidConfigException If the query is not class of UrbanIndo\Yii2\DynamoDb\Query */ protected function prepareModels() { - return []; + if (!$this->query instanceof Query) { + throw new InvalidConfigException('The "query" property must be an instance of a class that implements the UrbanIndo\Yii2\DynamoDb\Query or its subclasses.'); + } + $query = clone $this->query; + if (($pagination = $this->getPagination()) !== false) { + $pagination->totalCount = $this->getTotalCount(); + $query->limit($pagination->getLimit()); + $query->offset($pagination->getOffset()); + } + + $models = $query->all($this->db); + if ($pagination !== false) { + $peek = current(array_slice($models, -1)); + /* @var $peek ActiveRecord */ + $nextLastKey = ArrayHelper::getValue($peek->getResponseData(), 'LastEvaluatedKey'); + $pagination->setNextLastKey($nextLastKey); + } + return $models; } /** @@ -56,6 +104,52 @@ protected function prepareModels() */ protected function prepareTotalCount() { - return 0; + if (!$this->query instanceof Query) { + throw new InvalidConfigException('The "query" property must be an instance of a class that implements the UrbanIndo\Yii2\DynamoDb\Query or its subclasses.'); + } + $query = clone $this->query; + return (int) $query->limit(-1)->orderBy([])->count('*', $this->db); + } + + /** + * Returns the pagination object used by this data provider. + * Note that you should call [[prepare()]] or [[getModels()]] first to get correct values + * of [[Pagination::totalCount]] and [[Pagination::pageCount]]. + * @return Pagination|boolean the pagination object. If this is false, it means the pagination is disabled. + */ + public function getPagination() + { + if ($this->_pagination === null) { + $this->setPagination([]); + } + + return $this->_pagination; + } + + /** + * Sets the pagination for this data provider. + * @param array|Pagination|boolean $value the pagination to be used by this data provider. + * This can be one of the following: + * + * - a configuration array for creating the pagination object. The "class" element defaults + * to 'UrbanIndo\Yii2\DynamoDb\Pagination' + * - an instance of [[Pagination]] or its subclass + * - false, if pagination needs to be disabled. + * + * @throws InvalidParamException + */ + public function setPagination($value) + { + if (is_array($value)) { + $config = ['class' => Pagination::className()]; + if ($this->id !== null) { + $config['pageSizeParam'] = $this->id . '-per-page'; + } + parent::setPagination(Yii::createObject(array_merge($config, $value))); + } elseif ($value instanceof Pagination || $value === false) { + parent::setPagination($value); + } else { + throw new InvalidParamException('Only Pagination instance, configuration array or false is allowed.'); + } } } diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index 876eaf9..055fd78 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -16,7 +16,6 @@ * An ActiveQuery can be a normal query or be used in a relational context. * * ActiveQuery instances are usually created by [[ActiveRecord::find()]]. - * Relational queries are created by [[ActiveRecord::hasOne()]] and [[ActiveRecord::hasMany()]]. * * Normal Query * ------------ @@ -42,6 +41,13 @@ class ActiveQuery extends Query implements ActiveQueryInterface * @event Event an event that is triggered when the query is initialized via [[init()]]. */ const EVENT_INIT = 'init'; + + /** + * Whether to store response data in ActiveRecord model returned. This can be either boolean + * false if not to store response data or the key of the response to store. + * @var array|boolean + */ + public $storeResponseData = ['ConsumedCapacity', 'LastEvaluatedKey', 'ScannedCount']; /** * Constructor. diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 1f223a3..0830ef4 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -45,6 +45,29 @@ class ActiveRecord extends BaseActiveRecord * @var array */ protected static $_primaryKeys = []; + + /** + * Stores the response metadata either from retrieval operation such as + *
    + *
  • `ConsumedCapacity` from GetItem, BatchGetItem, Query, and Scan
  • + *
  • `UnprocessedKeys` from BatchGetItem
  • + *
  • `Count`, `LastEvaluatedKey`, `ScannedCount` from Query or Scan
  • + *
+ * @var array + */ + protected $_responseData = []; + + /** + * Stores the operation type that retrieves this model. Eligible values are. + *
    + *
  • Query::USING_BATCH_GET_ITEM
  • + *
  • Query::USING_GET_ITEM
  • + *
  • Query::USING_QUERY
  • + *
  • Query::USING_SCAN
  • + *
+ * @var string + */ + protected $_findType; /** * Returns the database connection used by this AR class. @@ -255,4 +278,29 @@ public static function deleteAll($condition = '', array $params = []) { throw new \yii\base\NotSupportedException(__METHOD__ . ' is not supported.'); } + + /** + * Populate active records from query response. + * @param ActiveQuery $query The query that produces the records. + * @param array $response The operation response. + * @return static[] + */ + public static function populateRecords(ActiveQuery $query, array $response) + { + $this->_findType = $query->using; + } + + /** + * Returns the response meta data from BatchGetItem, GetItem, Scan, or Query + * operation. This can contains `ConsumedCapacity`, `UnprocessedKeys`, `Count`, + * `LastEvaluatedKey`, `ScannedCount` depends on whether the query enables + * storing the meta data. + * @return array + */ + public function getResponseData() + { + return $this->_responseData; + } + + } diff --git a/src/Pagination.php b/src/Pagination.php index 002d859..0df8695 100644 --- a/src/Pagination.php +++ b/src/Pagination.php @@ -1,9 +1,43 @@ + */ namespace UrbanIndo\Yii2\DynamoDb; +use Yii; +use yii\web\Link; + +/** + * Pagination represents information relevant to pagination of data items. + * @author Petra Barus + */ class Pagination extends \yii\data\Pagination { + /** + * @var string name of the parameter storing the current page index. + * @see params + */ + public $lastKeyParam = 'last-key'; + + /** + * @var boolean whether to always have the last-key parameter in the URL created by [[createUrl()]]. + * If false and [[lastKey]] is null, the lastKey parameter will not be put in the URL. + */ + public $forceLastKeyParam = true; + + /** + * @var array parameters (name => value) that should be used to obtain the current page number + * and to create new pagination URLs. If not set, all parameters from $_GET will be used instead. + * + * In order to add hash to all links use `array_merge($_GET, ['#' => 'my-hash'])`. + * + * The array element indexed by [[lastKeyParam]] is considered to be the current last key param (defaults to null). + * while the element indexed by [[pageSizeParam]] is treated as the page size (defaults to [[defaultPageSize]]). + */ + public $params; + /** * This will only return 1. * @return integer number of pages @@ -12,4 +46,140 @@ public function getPageCount() { return 1; } + + /** + * Creates the URL suitable for pagination with the specified page number. + * This method is mainly called by pagers when creating URLs used to perform pagination. + * @param string $lastKey The key of the last evaluated item. + * @param integer $pageSize The number of items on each page. If not set, the value of [[pageSize]] will be used. + * @param boolean $absolute Whether to create an absolute URL. Defaults to `false`. + * @return string the created URL + * @see params + * @see forcePageParam + */ + public function createUrl($lastKey, $pageSize = null, $absolute = false) + { + $pageSize = (int) $pageSize; + if (($params = $this->params) === null) { + $request = Yii::$app->getRequest(); + $params = $request instanceof Request ? $request->getQueryParams() : []; + } + + if ($lastKey !== null || $this->forcePageParam) { + $params[$this->lastKeyParam] = $lastKey; + } else { + unset($params[$this->lastKeyParam]); + } + if ($pageSize <= 0) { + $pageSize = $this->getPageSize(); + } + if ($pageSize != $this->defaultPageSize) { + $params[$this->pageSizeParam] = $pageSize; + } else { + unset($params[$this->pageSizeParam]); + } + + $params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; + $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; + if ($absolute) { + return $urlManager->createAbsoluteUrl($params); + } else { + return $urlManager->createUrl($params); + } + } + + /** + * Returns just one link to the next page. + * @param boolean $absolute Whether the generated URLs should be absolute. + * @return array array containing links to the next page. + */ + public function getLinks($absolute = false) + { + $pageSize = $this->getPageSize(); + $links = [ + Link::REL_SELF => $this->createUrl( + $this->getLastKey(), + $pageSize, + $absolute + ) + ]; + if (($nextLastKey = $this->getNextLastKey()) !== null) { + $links[self::LINK_NEXT] = $this->createUrl($nextLastKey, $pageSize, $absolute); + } + return $links; + } + + /** + * @return integer the limit of the data. This may be used to set the + * LIMIT value for Query or Scan operation. + * Note that if the page size is infinite, a value -1 will be returned. + */ + public function getLimit() + { + $pageSize = $this->getPageSize(); + + return $pageSize < 1 ? -1 : $pageSize; + } + + /** + * Stores the current last key. + * @var string + */ + private $_lastKey; + + /** + * Returns the last key evaluated in the DynamoDB. + * @return string + */ + public function getLastKey() + { + if ($this->_lastKey === null) { + $this->setLastKey($this->getQueryParam($this->lastKeyParam)); + } + return $this->_lastKey; + } + + /** + * Sets the current last key. + * @param string $value The last key that was evaluated by DynamoDB. + * @return void + */ + public function setLastKey($value) + { + $this->_lastKey = $value; + } + + /** + * Stores the next last key. + * @var string + */ + private $_nextLastKey; + + /** + * Returns the next last key. + * @return string + */ + public function getNextLastKey() + { + return $this->_nextLastKey; + } + + /** + * Sets the next last key. This has to be set manually in the data provider. + * @param string $value The last key that was evaluated by DynamoDB. + * @return void + */ + public function setNextLastKey($value) + { + $this->_nextLastKey = $value; + } + + /** + * This is shorthand for `getLastKey()`. + * @return string + */ + public function getOffset() + { + return $this->getLastKey(); + } } diff --git a/test/ActiveDataProviderTest.php b/test/ActiveDataProviderTest.php new file mode 100644 index 0000000..58996f9 --- /dev/null +++ b/test/ActiveDataProviderTest.php @@ -0,0 +1,6 @@ +mockWebApplication([ + 'components' => [ + 'urlManager' => [ + 'scriptUrl' => '/index.php' + ], + ], + ]); + } + + /** + * Data provider for [[testCreateUrl()]] + * @return array test data + */ + public function dataProviderCreateUrl() + { + return [ + [ + null, + null, + '/index.php?r=item%2Flist', + ], + [ + 2, + null, + '/index.php?r=item%2Flist&last-key=2', + ], + [ + 2, + 5, + '/index.php?r=item%2Flist&last-key=2&per-page=5', + ], + ]; + } + + /** + * @dataProvider dataProviderCreateUrl + * + * @param string $lastKey + * @param integer $pageSize + * @param string $expectedUrl + */ + public function testCreateUrl($lastKey, $pageSize, $expectedUrl) + { + $pagination = new Pagination(); + $pagination->route = 'item/list'; + $this->assertEquals($expectedUrl, $pagination->createUrl($lastKey, $pageSize)); + } + + /** + * Data provider for [[testCreateUrl()]] + * @return array test data + */ + public function dataProviderGetLinks() + { + return [ + [ + null, + 5, + null, + [ + 'self' => '/index.php?r=item%2Flist', + 'next' => '/index.php?r=item%2Flist&last-key=5', + ] + ], + [ + 5, + 10, + null, + [ + 'self' => '/index.php?r=item%2Flist&last-key=5', + 'next' => '/index.php?r=item%2Flist&last-key=10', + ] + ], + [ + 5, + 10, + 10, + [ + 'self' => '/index.php?r=item%2Flist&last-key=5&per-page=10', + 'next' => '/index.php?r=item%2Flist&last-key=10&per-page=10', + ] + ], + ]; + } + + /** + * @dataProvider dataProviderGetLinks + * + * @param string $currentLastKey The current last key. + * @param string $nextLastKey The next last key. + * @param integer $pageSize The page size to show. + * @param array $links The links resulted + */ + public function testGetLinks($currentLastKey, $nextLastKey, $pageSize, $links) + { + $pagination = new Pagination([ + 'route' => 'item/list', + 'lastKey' => $currentLastKey, + 'nextLastKey' => $nextLastKey, + 'pageSize' => $pageSize, + ]); + + $this->assertEquals($links, $pagination->getLinks()); + } +} diff --git a/test/TestCase.php b/test/TestCase.php index dac53a2..5ae9996 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -1,5 +1,7 @@ dynamodb; } + + protected function mockWebApplication($config = [], $appClass = '\yii\web\Application') + { + new $appClass(ArrayHelper::merge([ + 'id' => 'testapp', + 'basePath' => __DIR__, + 'vendorPath' => $this->getVendorPath(), + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ .'/index.php', + 'scriptUrl' => '/index.php', + ], + ] + ], $config)); + } + + protected function getVendorPath() + { + $vendor = dirname(dirname(__DIR__)) . '/vendor'; + if (!is_dir($vendor)) { + $vendor = dirname(dirname(dirname(dirname(__DIR__)))); + } + return $vendor; + } } From 3ef49b58c59be0d12984e84e9ef04fd2f31920a1 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Wed, 30 Dec 2015 10:35:40 +0700 Subject: [PATCH 07/23] Query Populate --- ruleset.xml | 5 +- src/ActiveQuery.php | 7 - src/Query.php | 119 +++++++++++- test/CommandTest.php | 4 +- test/QueryTest.php | 417 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 531 insertions(+), 21 deletions(-) diff --git a/ruleset.xml b/ruleset.xml index ad32c5b..58316ed 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -38,7 +38,10 @@ - + + + + diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index 055fd78..f9ab0be 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -41,13 +41,6 @@ class ActiveQuery extends Query implements ActiveQueryInterface * @event Event an event that is triggered when the query is initialized via [[init()]]. */ const EVENT_INIT = 'init'; - - /** - * Whether to store response data in ActiveRecord model returned. This can be either boolean - * false if not to store response data or the key of the response to store. - * @var array|boolean - */ - public $storeResponseData = ['ConsumedCapacity', 'LastEvaluatedKey', 'ScannedCount']; /** * Constructor. diff --git a/src/Query.php b/src/Query.php index 21621b0..e1c9ed5 100644 --- a/src/Query.php +++ b/src/Query.php @@ -11,6 +11,7 @@ use yii\db\QueryInterface; use yii\db\QueryTrait; use yii\base\NotSupportedException; +use yii\helpers\ArrayHelper; /** * Query represents item fetching operation from DynamoDB table. @@ -81,6 +82,21 @@ class Query extends Component implements QueryInterface * @var string */ public $from; + + /** + * Whether to store response data in the data returned. This can be either boolean + * false if not to store response data or the key of the response to store. + * @var array|boolean + */ + public $storeResponseData = ['ConsumedCapacity', 'LastEvaluatedKey', 'ScannedCount', 'Count']; + + /** + * The key to store the response data. When populating the values of the response data for + * keys listed in [[storeResponseData]] will be stored on each row returned on the key + * stated in this variable. + * @var string + */ + public $responseDataKeyParam = '_response'; /** * Executes the query and returns all results as an array. @@ -185,13 +201,90 @@ public function withoutConsumedCapacity() $this->returnConsumedCapacity = false; return $this; } + + /** + * @param array $response The raw response result from operation. + * @return array array of values. + */ + public function getItemsFromResponse($response) + { + if (in_array($this->using, [self::USING_QUERY, self::USING_SCAN])) { + $rows = array_map(function ($item) { + return Marshaler::unmarshalItem($item); + }, $response['Items']); + } else if ($this->using == self::USING_BATCH_GET_ITEM) { + $rows = array_map(function ($item) { + return Marshaler::unmarshalItem($item); + }, $response['Responses'][$this->from]); + } else if ($this->using == self::USING_GET_ITEM) { + $row = Marshaler::unmarshalItem($response['Item']); + $rows = [$row]; + } + + $storedResponse = self::extractStoredResponseData($this->storeResponseData, $response); + if (!empty($storedResponse)) { + $rows = array_map(function ($row) use ($storedResponse) { + $row[$this->responseDataKeyParam] = $storedResponse; + return $row; + }, $rows); + } + + return $rows; + } + + /** + * @param mixed $responseKeys List of keys to store from operation response, false if don't want to store. + * @param array $response The raw response from operation. + * @return array Stored response data. + */ + private static function extractStoredResponseData($responseKeys, $response) + { + if ($responseKeys == false) { + return []; + } + $return = []; + foreach ($responseKeys as $key) { + if (empty($value = ArrayHelper::getValue($response, $key))) { + continue; + } + $return[$key] = $value; + } + return $return; + } + + /** + * Converts the raw query results into the format as specified by this query. + * This method is internally used to convert the data fetched from database + * into the format as required by this query. + * @param array $response The raw response result from operation. + * @return array the converted query result + */ + public function populate($response) + { + $rows = $this->getItemsFromResponse($response); + if ($this->indexBy === null) { + return $rows; + } + $result = []; + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else if (is_array($this->indexBy)) { + $key = $row[$this->indexBy[0]] . $row[$this->indexBy[1]]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $result[$key] = $row; + } + return $result; + } /** * Returns all object that matches the query. * @param Connection $db The dynamodb connection. * @return array */ - public function all(Connection $db = null) + public function all($db = null) { return $this->createCommand($db)->queryAll(); } @@ -201,7 +294,7 @@ public function all(Connection $db = null) * @param Connection $db The dynamodb connection. * @return array */ - public function one(Connection $db = null) + public function one($db = null) { $this->using = self::USING_GET_ITEM; return $this->createCommand($db)->queryOne(); @@ -209,19 +302,29 @@ public function one(Connection $db = null) /** * Returns the number of records. - * @param string $q the COUNT expression. This parameter is ignored by this implementation. - * @param Connection $db the database connection used to execute the query. + * @param string $q The COUNT expression. This parameter is ignored by this implementation. + * @param Connection $db The database connection used to execute the query. * If this parameter is not given, the `elasticsearch` application component will be used. * @return integer number of records */ - public function count($q = '*', Connection $db = null) + public function count($q = '*', $db = null) { - + //WIP + $q; + $db; + return $i; } + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db The database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return boolean whether the query result contains any row of data. + */ public function exists($db = null) { - + //WIP + $db; + return false; } - } diff --git a/test/CommandTest.php b/test/CommandTest.php index 3ba8f9d..3663156 100644 --- a/test/CommandTest.php +++ b/test/CommandTest.php @@ -187,7 +187,7 @@ public function testGetItemUsingScalarKey() $command->putItem($tableName, $value)->execute(); $result = $command->getItem($tableName, $id)->execute(); - + $this->assertNotEmpty($result); } @@ -429,7 +429,7 @@ public function testBatchGetItemUsingIndexedArrayOfScalarElement() $this->assertEquals(10, $this->getTableItemCount($tableName)); $result = $command->batchGetItem($tableName, $ids)->execute(); - + $this->assertNotEmpty($result); $this->assertEmpty($result['UnprocessedKeys']); } diff --git a/test/QueryTest.php b/test/QueryTest.php index be56604..c117cdc 100644 --- a/test/QueryTest.php +++ b/test/QueryTest.php @@ -2,11 +2,422 @@ use UrbanIndo\Yii2\DynamoDb\Query; -class QueryTest extends TestCase { - - public function testOne() +class QueryTest extends TestCase +{ + + /** + * @group getItemsFromResponse + */ + public function testGetItemsFromResponseOnBatchGetItemResult() + { + $query = new Query(); + $query->using = Query::USING_BATCH_GET_ITEM; + $query->from = 'b3cbe4b0-0540-327c-92f1-82dc68be9af5'; + $response = [ + 'ConsumedCapacity' => [ + [ + 'CapacityUnits' => 10, + 'Table' => [ + 'CapacityUnits' => 10, + ], + 'TableName' => 'b3cbe4b0-0540-327c-92f1-82dc68be9af5' + ] + ], + 'Responses' => [ + 'b3cbe4b0-0540-327c-92f1-82dc68be9af5' => [ + [ + 'Field2' => [ + 'S' => 'Richard', + ], + 'Brendon' => [ + 'S' => '2ce0cac8-ba40-376f-b34b-2b67db56f82f', + ], + ], + [ + 'Field2' => [ + 'S' => 'Louisa', + ], + 'Brendon' => [ + 'S' => '46c1196a-adbe-37eb-8459-35af1fae97c5', + ], + ], + [ + 'Field2' => [ + 'S' => 'Onie', + ], + 'Brendon' => [ + 'S' => '3c9136f5-3c59-3636-b295-980a63681218', + ], + ], + [ + 'Field2' => [ + 'S' => 'Andreane', + ], + 'Brendon' => [ + 'S' => 'bc401cc3-2e77-34ec-8b68-6ed7eb62636d', + ], + ], + [ + 'Field2' => [ + 'S' => 'Dandre', + ], + 'Brendon' => [ + 'S' => '53def5fd-ac60-33d9-b5c2-81c02c4e792b', + ], + ], + [ + 'Field2' => [ + 'S' => 'Marilou', + ], + 'Brendon' => [ + 'S' => '9c1c5652-1fe9-30d3-8af2-74cc730834a5', + ], + ], + [ + 'Field2' => [ + 'S' => 'Isabell', + ], + 'Brendon' => [ + 'S' => 'dd1a115e-2552-39b8-b0ff-5a831f5f8366', + ], + ], + [ + 'Field2' => [ + 'S' => 'Carmen', + ], + 'Brendon' => [ + 'S' => '0a05b57f-a29f-3b00-bb4a-1002ba2ac4c7', + ], + ], + [ + 'Field2' => [ + 'S' => 'Joaquin', + ], + 'Brendon' => [ + 'S' => 'd55dfce4-6238-3f19-a740-8c8278a38c94', + ], + ], + [ + 'Field2' => [ + 'S' => 'Eleanora', + ], + 'Brendon' => [ + 'S' => '7b74a73b-dde5-352a-a87b-c98cb4514e61', + ], + ], + ], + ], + 'UnprocessedKeys' => [ + ], + ]; + $items = $query->getItemsFromResponse($response); + $this->assertCount(10, $items); + $peek = array_slice($items, -1)[0]; + $this->assertArraySubset([ + '_response' => [ + 'ConsumedCapacity' => [] + ] + ], $peek); + } + + /** + * @group getItemsFromResponse + */ + public function testGetItemsFromResponseOnScanResult() + { + $query = new Query(); + $query->using = Query::USING_SCAN; + $response = [ + 'ConsumedCapacity' => [ + [ + 'CapacityUnits' => 10, + 'Table' => [ + 'CapacityUnits' => 10, + ], + 'TableName' => 'b3cbe4b0-0540-327c-92f1-82dc68be9af5' + ] + ], + 'Items' => [ + [ + 'Theo' => [ + 'S' => 'f9dac574-ce4c-352a-8aed-8eeb9f5a06a8', + ], + 'field4' => [ + 'N' => '0', + ], + 'field3' => [ + 'N' => '9', + ], + 'field2' => [ + 'N' => '9', + ], + ], + [ + 'Theo' => [ + 'S' => '0b04d16f-306b-346d-8d36-dac2adfac959', + ], + 'field4' => [ + 'N' => '2', + ], + 'field3' => [ + 'N' => '5', + ], + 'field2' => [ + 'N' => '5', + ], + ], + [ + 'Theo' => [ + 'S' => '2c71f148-20e4-3077-9364-00279c3dd5de', + ], + 'field4' => [ + 'N' => '1', + ], + 'field3' => [ + 'N' => '7', + ], + 'field2' => [ + 'N' => '7', + ], + ], + [ + 'Theo' => [ + 'S' => 'b575fd82-c7f6-308d-8a4c-e932ce676052', + ], + 'field4' => [ + 'N' => '2', + ], + 'field3' => [ + 'N' => '8', + ], + 'field2' => [ + 'N' => '8', + ], + ], + [ + 'Theo' => [ + 'S' => 'b3aa0dcc-052b-32a5-afde-41ae2f7c182e', + ], + 'field4' => [ + 'N' => '1', + ], + 'field3' => [ + 'N' => '10', + ], + 'field2' => [ + 'N' => '10', + ], + ], + [ + 'Theo' => [ + 'S' => 'ffce0c31-7343-3933-9d98-dcb4790220d9', + ], + 'field4' => [ + 'N' => '1', + ], + 'field3' => [ + 'N' => '4', + ], + 'field2' => [ + 'N' => '4', + ], + ], + [ + 'Theo' => [ + 'S' => 'ee866873-b3f7-3804-b0db-163fa47dbe71', + ], + 'field4' => [ + 'N' => '0', + ], + 'field3' => [ + 'N' => '6', + ], + 'field2' => [ + 'N' => '6', + ], + ], + ], + 'Count' => 7, + 'ScannedCount' => 10, + ]; + $items = $query->getItemsFromResponse($response); + $this->assertCount(7, $items); + $peek = array_slice($items, -1)[0]; + $this->assertArraySubset([ + '_response' => [ + 'ConsumedCapacity' => [], + 'Count' => 7, + 'ScannedCount' => 10, + ] + ], $peek); + } + + /** + * @group getItemsFromResponse + */ + public function testGetItemsFromResponseOnQueryResult() { $query = new Query(); + $query->using = Query::USING_QUERY; + $response = [ + 'ConsumedCapacity' => [ + [ + 'CapacityUnits' => 10, + 'Table' => [ + 'CapacityUnits' => 10, + ], + 'TableName' => 'b3cbe4b0-0540-327c-92f1-82dc68be9af5' + ] + ], + 'Items' => [ + [ + 'Theo' => [ + 'S' => 'f9dac574-ce4c-352a-8aed-8eeb9f5a06a8', + ], + 'field4' => [ + 'N' => '0', + ], + 'field3' => [ + 'N' => '9', + ], + 'field2' => [ + 'N' => '9', + ], + ], + [ + 'Theo' => [ + 'S' => '0b04d16f-306b-346d-8d36-dac2adfac959', + ], + 'field4' => [ + 'N' => '2', + ], + 'field3' => [ + 'N' => '5', + ], + 'field2' => [ + 'N' => '5', + ], + ], + [ + 'Theo' => [ + 'S' => '2c71f148-20e4-3077-9364-00279c3dd5de', + ], + 'field4' => [ + 'N' => '1', + ], + 'field3' => [ + 'N' => '7', + ], + 'field2' => [ + 'N' => '7', + ], + ], + [ + 'Theo' => [ + 'S' => 'b575fd82-c7f6-308d-8a4c-e932ce676052', + ], + 'field4' => [ + 'N' => '2', + ], + 'field3' => [ + 'N' => '8', + ], + 'field2' => [ + 'N' => '8', + ], + ], + [ + 'Theo' => [ + 'S' => 'b3aa0dcc-052b-32a5-afde-41ae2f7c182e', + ], + 'field4' => [ + 'N' => '1', + ], + 'field3' => [ + 'N' => '10', + ], + 'field2' => [ + 'N' => '10', + ], + ], + [ + 'Theo' => [ + 'S' => 'ffce0c31-7343-3933-9d98-dcb4790220d9', + ], + 'field4' => [ + 'N' => '1', + ], + 'field3' => [ + 'N' => '4', + ], + 'field2' => [ + 'N' => '4', + ], + ], + [ + 'Theo' => [ + 'S' => 'ee866873-b3f7-3804-b0db-163fa47dbe71', + ], + 'field4' => [ + 'N' => '0', + ], + 'field3' => [ + 'N' => '6', + ], + 'field2' => [ + 'N' => '6', + ], + ], + ], + 'Count' => 7, + 'ScannedCount' => 10, + ]; + $items = $query->getItemsFromResponse($response); + $this->assertCount(7, $items); + $peek = array_slice($items, -1)[0]; + $this->assertArraySubset([ + '_response' => [ + 'ConsumedCapacity' => [], + 'Count' => 7, + 'ScannedCount' => 10, + ] + ], $peek); } + + /** + * @group getItemsFromResponse + */ + public function testGetItemsFromResponseOnGetItemResult() + { + $query = new Query(); + $query->using = Query::USING_GET_ITEM; + $response = [ + 'ConsumedCapacity' => [ + [ + 'CapacityUnits' => 10, + 'Table' => [ + 'CapacityUnits' => 10, + ], + 'TableName' => 'b3cbe4b0-0540-327c-92f1-82dc68be9af5' + ] + ], + 'Item' => [ + 'Barry' => [ + 'S' => 'Sydni', + ], + 'Field2' => [ + 'S' => 'Hello', + ], + ], + ]; + $items = $query->getItemsFromResponse($response); + $this->assertCount(1, $items); + $peek = array_slice($items, -1)[0]; + $this->assertArraySubset([ + '_response' => [ + 'ConsumedCapacity' => [], + ] + ], $peek); + } } From f553115bdc28085b9a293a591d76fe9fd8437e85 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Wed, 30 Dec 2015 11:26:37 +0700 Subject: [PATCH 08/23] Active query. --- src/ActiveQuery.php | 70 +++++++++++++++++++++------------------- src/Query.php | 78 ++++++++++++++++++++++++++------------------- 2 files changed, 84 insertions(+), 64 deletions(-) diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index f9ab0be..d5a1a92 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -24,7 +24,6 @@ * * - [[one()]]: returns a single record populated with the first row of data. * - [[all()]]: returns all records based on the query results. - * - [[count()]]: returns the number of records. * - [[scalar()]]: returns the value of the first column in the first row of the query result. * - [[column()]]: returns the value of the first column in the query result. * - [[exists()]]: returns a value indicating whether the query result has data or not. @@ -65,49 +64,56 @@ public function init() parent::init(); $this->trigger(self::EVENT_INIT); } - + /** - * @param Connection $db The DB connection used to create the DB command. - * @return ActiveRecord + * Converts the raw query results into the format as specified by this query. + * This method is internally used to convert the data fetched from database + * into the format as required by this query. + * @param array $rows The rows from response parsing. + * @return array the converted query result */ - public function one(Connection $db = null) + public function populate($rows) { - /* @var $response \Guzzle\Service\Resource\Model */ - $response = parent::one($db); - $value = $response->get('Item'); - $marshaller = new \Aws\DynamoDb\Marshaler(); - return $this->createModel($value, $marshaller); + if (empty($rows)) { + return []; + } + + $models = $this->createModels($rows); + if (!$this->asArray) { + foreach ($models as $model) { + $model->afterFind(); + } + } + return $models; } /** + * Executes query and returns a single row of result. * @param Connection $db The DB connection used to create the DB command. - * @return ActiveRecord[] + * If null, the DB connection returned by [[modelClass]] will be used. + * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. */ - public function all(Connection $db = null) + public function one($db = null) { - $responses = parent::all($db); - $modelClass = $this->modelClass; - $marshaller = new \Aws\DynamoDb\Marshaler(); - return array_map(function ($value) use ($marshaller) { - return $this->createModel($value, $marshaller); - }, $responses[$modelClass::tableName()]); + $row = parent::one($db); + if ($row !== false) { + $models = $this->populate([$row]); + return reset($models) ?: null; + } else { + return null; + } } /** - * Create model based on dynamodb return value. - * @param mixed $value The return value from dynamodb. - * @param \Aws\DynamoDb\Marshaler $marshaller The marshaller. - * @return ActiveRecord + * Executes query and returns all results as an array. + * @param Connection $db The DB connection used to create the DB command. + * If null, the DB connection returned by [[modelClass]] will be used. + * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. */ - private function createModel( - $value, - \Aws\DynamoDb\Marshaler $marshaller = null - ) { - $model = new $this->modelClass; - if (!isset($marshaller)) { - $marshaller = new \Aws\DynamoDb\Marshaler(); - } - $model->setAttributes($marshaller->unmarshalItem($value), false); - return $model; + public function all($db = null) + { + return parent::all($db); } } diff --git a/src/Query.php b/src/Query.php index e1c9ed5..8a9a97f 100644 --- a/src/Query.php +++ b/src/Query.php @@ -99,10 +99,10 @@ class Query extends Component implements QueryInterface public $responseDataKeyParam = '_response'; /** - * Executes the query and returns all results as an array. - * @param Connection $db The database connection used to execute the query. - * If this parameter is not given, the `dynamodb` application component will be used. - * @return Command + * Creates a DB command that can be used to execute this query. + * @param Connection $db The database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return Command the created DB command instance. */ public function createCommand(Connection $db = null) { @@ -113,6 +113,18 @@ public function createCommand(Connection $db = null) return $db->createCommand($config); } + + + /** + * Creates command and execute the query. + * @param Connection $db The database connection used. + * @return array The raw response. + */ + public function execute(Connection $db = null) + { + $command = $this->createCommand($db); + return $command->execute(); + } /** * Identifies one or more attributes to retrieve from the table. @@ -256,12 +268,11 @@ private static function extractStoredResponseData($responseKeys, $response) * Converts the raw query results into the format as specified by this query. * This method is internally used to convert the data fetched from database * into the format as required by this query. - * @param array $response The raw response result from operation. + * @param array $rows The rows resulted from response parsing. * @return array the converted query result */ - public function populate($response) + public function populate($rows) { - $rows = $this->getItemsFromResponse($response); if ($this->indexBy === null) { return $rows; } @@ -282,37 +293,27 @@ public function populate($response) /** * Returns all object that matches the query. * @param Connection $db The dynamodb connection. - * @return array + * @return array The query results. If the query results in nothing, an empty array will be returned. */ public function all($db = null) { - return $this->createCommand($db)->queryAll(); + $response = $this->execute($db); + $rows = $this->getItemsFromResponse($response); + return $rows; } /** - * Returns one object that matches the query. - * @param Connection $db The dynamodb connection. - * @return array + * Executes the query and returns a single row of result. + * @param Connection $db The database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. */ public function one($db = null) { - $this->using = self::USING_GET_ITEM; - return $this->createCommand($db)->queryOne(); - } - - /** - * Returns the number of records. - * @param string $q The COUNT expression. This parameter is ignored by this implementation. - * @param Connection $db The database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return integer number of records - */ - public function count($q = '*', $db = null) - { - //WIP - $q; - $db; - return $i; + $response = $this->execute($db); + $rows = $this->getItemsFromResponse($response); + return $rows; } /** @@ -323,8 +324,21 @@ public function count($q = '*', $db = null) */ public function exists($db = null) { - //WIP - $db; - return false; + $response = $this->execute($db); + $rows = $this->getItemsFromResponse($response); + return !empty($rows); + } + + /** + * Returns the number of records. + * @param string $q The COUNT expression. This parameter is ignored by this implementation. + * @param Connection $db The database connection used to execute the query. + * If this parameter is not given, the `elasticsearch` application component will be used. + * @return void + * @throws NotSupportedException The count operation is not supported. + */ + public function count($q = '*', $db = null) + { + throw new NotSupportedException('Count operation is not suppported.'); } } From 41fb48d48ea24406db85682678d1c517e3a483ac Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Wed, 30 Dec 2015 13:12:04 +0700 Subject: [PATCH 09/23] Query table query builder. --- src/QueryBuilder.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 5262cc7..caa4252 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -344,7 +344,7 @@ public function buildBatchGetItemFromAssociativeArray(array $keySchema, array $k * * @param string $table The name of the table to scan. * @param array $options The scan options. - * @return array The create table request syntax. The first element is the name of the command, + * @return array The scan table request syntax. The first element is the name of the command, * the second is the argument. */ public function scan($table, array $options = []) @@ -355,4 +355,21 @@ public function scan($table, array $options = []) ], $options); return [$name, $argument]; } + + /** + * Builds a DynamoDB command to query table. + * + * @param string $table The name of the table to query. + * @param array $options The scan options. + * @return array The query table request syntax. The first element is the name of the command, + * the second is the argument. + */ + public function query($table, array $options = []) + { + $name = 'Query'; + $argument = array_merge([ + 'TableName' => $table, + ], $options); + return [$name, $argument]; + } } From 1a9af95655d8574e96b4fddbb1718007f89dafed Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Wed, 30 Dec 2015 15:00:42 +0700 Subject: [PATCH 10/23] Batch put item and batch delete item. --- src/Command.php | 28 ++ src/QueryBuilder.php | 161 ++++++++--- test/ActiveDataProviderTest.php | 14 + test/CommandTest.php | 495 ++++++++------------------------ test/TestCase.php | 76 +++++ 5 files changed, 367 insertions(+), 407 deletions(-) diff --git a/src/Command.php b/src/Command.php index 263deb9..0c77c9d 100644 --- a/src/Command.php +++ b/src/Command.php @@ -133,6 +133,34 @@ public function putItem($table, array $value, array $options = []) return $this->setCommand($name, $argument); } + /** + * Put multiple items in the table. + * @param string $table The name of the table. + * @param array $values The values to input. + * @param array $options Additional options to the request argument. + * @return static + */ + public function batchPutItem($table, array $values, array $options = []) + { + list($name, $argument) = $this->db->getQueryBuilder()->batchPutItem($table, $values, $options); + return $this->setCommand($name, $argument); + } + + /** + * Put multiple items in the table. + * @param string $table The name of the table. + * @param array $keys The keys of the row. This can be indexed array of + * scalar value, indexed array of array of scalar value, indexed array of + * associative array. + * @param array $options Additional options to the request argument. + * @return static + */ + public function batchDeleteItem($table, array $keys, array $options = []) + { + list($name, $argument) = $this->db->getQueryBuilder()->batchDeleteItem($table, $keys, $options); + return $this->setCommand($name, $argument); + } + /** * Get a single item from table. * @param string $table The name of the table. diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index caa4252..cd10aa0 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -8,7 +8,6 @@ namespace UrbanIndo\Yii2\DynamoDb; use yii\base\Object; -use Aws\DynamoDb\Marshaler; use yii\helpers\ArrayHelper; /** @@ -100,11 +99,10 @@ public function deleteTable($table) */ public function putItem($table, array $value, array $options = []) { - $marshaler = new Marshaler(); $name = 'PutItem'; $argument = array_merge([ 'TableName' => $table, - 'Item' => $marshaler->marshalItem($value) + 'Item' => Marshaler::marshalItem($value), ], $options); return [$name, $argument]; } @@ -151,13 +149,12 @@ public function getItem($table, $key, array $options = []) */ public function buildGetItemScalarKey(array $keySchema, $key) { - $marshaler = new Marshaler(); if (count($keySchema) > 1) { throw new \InvalidArgumentException('Can not use scalar key argument on table with multiple key'); } $keyName = $keySchema[0]['AttributeName']; return [ - $keyName => $marshaler->marshalValue($key), + $keyName => Marshaler::marshalValue($key), ]; } @@ -168,16 +165,14 @@ public function buildGetItemScalarKey(array $keySchema, $key) */ public function buildGetItemCompositeKey(array $keySchema, array $keys) { - $marshaler = new Marshaler(); - $keyArgument = []; if (ArrayHelper::isIndexed($keys)) { foreach ($keys as $i => $value) { - $keyArgument[$keySchema[$i]['AttributeName']] = $marshaler->marshalValue($value); + $keyArgument[$keySchema[$i]['AttributeName']] = Marshaler::marshalValue($value); } } else { foreach ($keys as $i => $value) { - $keyArgument[$i] = $marshaler->marshalValue($value); + $keyArgument[$i] = Marshaler::marshalValue($value); } } return $keyArgument; @@ -230,30 +225,39 @@ public function batchGetItem($table, array $keys, array $options = [], array $re { $name = 'BatchGetItem'; + $tableArgument = array_merge([ + $table => [ + 'Keys' => $this->buildBatchKeyArgument($table, $keys), + ] + ], $requestItemOptions); + + $argument = array_merge(['RequestItems' => $tableArgument], $options); + return [$name, $argument]; + } + + /** + * Resolve the keys into `BatchGetItem` eligible argument. + * @param string $table The name of the table. + * @param array $keys The keys. + * @return array + */ + private function buildBatchKeyArgument($table, $keys) + { $tableDescription = $this->db->createCommand()->describeTable($table)->execute(); $keySchema = $tableDescription['Table']['KeySchema']; if (ArrayHelper::isIndexed($keys)) { $isScalar = is_string($keys[0]) || is_numeric($keys[0]); if ($isScalar) { - $keyArgument = $this->buildBatchGetItemFromIndexedArrayOfScalar($keySchema, $keys); + return $this->buildBatchKeyArgumentFromIndexedArrayOfScalar($keySchema, $keys); } elseif (ArrayHelper::isIndexed($keys[0])) { - $keyArgument = $this->buildBatchGetItemFromIndexedArrayOfIndexedArray($keySchema, $keys); + return $this->buildBatchKeyArgumentFromIndexedArrayOfIndexedArray($keySchema, $keys); } else { - $keyArgument = $this->buildBatchGetItemFromIndexedArrayOfAssociativeArray($keySchema, $keys); + return $this->buildBatchKeyArgumentFromIndexedArrayOfAssociativeArray($keySchema, $keys); } } else { - $keyArgument = $this->buildBatchGetItemFromAssociativeArray($keySchema, $keys); + return $this->buildBatchGetItemFromAssociativeArray($keySchema, $keys); } - - $tableArgument = array_merge([ - $table => [ - 'Keys' => $keyArgument - ] - ], $requestItemOptions); - - $argument = array_merge(['RequestItems' => $tableArgument], $options); - return [$name, $argument]; } /** @@ -262,16 +266,15 @@ public function batchGetItem($table, array $keys, array $options = [], array $re * @return array * @throws \InvalidArgumentException When the table has multiple key. */ - public function buildBatchGetItemFromIndexedArrayOfScalar(array $keySchema, array $keys) + private function buildBatchKeyArgumentFromIndexedArrayOfScalar(array $keySchema, array $keys) { - $marshaler = new Marshaler(); if (count($keySchema) > 1) { throw new \InvalidArgumentException('Can not use scalar key argument on table with multiple key'); } $attribute = $keySchema[0]['AttributeName']; - return array_map(function ($key) use ($attribute, $marshaler) { + return array_map(function ($key) use ($attribute) { return [ - $attribute => $marshaler->marshalValue($key), + $attribute => Marshaler::marshalValue($key), ]; }, $keys); } @@ -282,13 +285,12 @@ public function buildBatchGetItemFromIndexedArrayOfScalar(array $keySchema, arra * @return array * @throws \InvalidArgumentException When the table has multiple key. */ - public function buildBatchGetItemFromIndexedArrayOfIndexedArray(array $keySchema, array $keys) + private function buildBatchKeyArgumentFromIndexedArrayOfIndexedArray(array $keySchema, array $keys) { - $marshaler = new Marshaler(); - return array_map(function ($key) use ($keySchema, $marshaler) { + return array_map(function ($key) use ($keySchema) { $return = []; foreach ($key as $i => $value) { - $return[$keySchema[$i]['AttributeName']] = $marshaler->marshalValue($value); + $return[$keySchema[$i]['AttributeName']] = Marshaler::marshalValue($value); } return $return; }, $keys); @@ -300,14 +302,13 @@ public function buildBatchGetItemFromIndexedArrayOfIndexedArray(array $keySchema * @return array * @throws \InvalidArgumentException When the table has multiple key. */ - public function buildBatchGetItemFromIndexedArrayOfAssociativeArray(array $keySchema, array $keys) + private function buildBatchKeyArgumentFromIndexedArrayOfAssociativeArray(array $keySchema, array $keys) { - $marshaler = new Marshaler(); $keySchema; - return array_map(function ($key) use ($marshaler) { + return array_map(function ($key) { $return = []; foreach ($key as $i => $value) { - $return[$i] = $marshaler->marshalValue($value); + $return[$i] = Marshaler::marshalValue($value); } return $return; }, $keys); @@ -336,7 +337,7 @@ public function buildBatchGetItemFromAssociativeArray(array $keySchema, array $k $indexedKey[$k][$attribute] = $keys[$attribute][$k]; } } - return $this->buildBatchGetItemFromIndexedArrayOfAssociativeArray($keySchema, $indexedKey); + return $this->buildBatchKeyArgumentFromIndexedArrayOfAssociativeArray($keySchema, $indexedKey); } /** @@ -372,4 +373,94 @@ public function query($table, array $options = []) ], $options); return [$name, $argument]; } + + /** + * Builds a DynamoDB command to put multiple items. + * + * @param string $table The name of the table to be created. + * @param array $values The value to put into the table. + * @param array $options The value to put into the table. + * @return array The create table request syntax. The first element is the name of the command, + * the second is the argument. + */ + public function batchPutItem($table, array $values, array $options = []) + { + $name = 'BatchWriteItem'; + $requests = array_map(function ($value) { + return [ + 'PutRequest' => [ + 'Item' => Marshaler::marshalItem($value), + ] + ]; + }, $values); + $argument = array_merge([ + 'RequestItems' => [ + $table => $requests, + ] + ], $options); + return [$name, $argument]; + } + + /** + * Builds a DynamoDB command for batch delete item. + * + * @param string $table The name of the table to be created. + * @param array $keys The keys of the row to get. + * This can be + * 1) indexed array of scalar value for table with single key, + * + * e.g. ['value1', 'value2', 'value3', 'value4'] + * + * 2) indexed array of array of scalar value for table with multiple key, + * + * e.g. [ + * ['value11', 'value12'], + * ['value21', 'value22'], + * ['value31', 'value32'], + * ['value41', 'value42'], + * ] + * + * The first scalar will be the primary (or hash) key, the second will be the + * secondary (or range) key. + * + * 3) indexed array of associative array + * + * e.g. [ + * ['attribute1' => 'value11', 'attribute2' => 'value12'], + * ['attribute1' => 'value21', 'attribute2' => 'value22'], + * ['attribute1' => 'value31', 'attribute2' => 'value32'], + * ['attribute1' => 'value41', 'attribute2' => 'value42'], + * ] + * + * 4) or associative of scalar values. + * + * e.g. [ + * 'attribute1' => ['value11', 'value21', 'value31', 'value41'] + * 'attribute2' => ['value12', 'value22', 'value32', 'value42'] + * ]. + * + * @param array $options Additional options for the final argument. + * @return array The create table request syntax. The first element is the name of the command, + * the second is the argument. + */ + public function batchDeleteItem($table, array $keys, array $options = []) + { + $name = 'BatchWriteItem'; + $keyArgument = $this->buildBatchKeyArgument($table, $keys); + $deleteRequests = array_map(function ($key) { + return [ + 'DeleteRequest' => [ + 'Key' => $key, + ] + ]; + }, $keyArgument); + + $argument = array_merge([ + 'RequestItems' => [ + $table => $deleteRequests, + ] + ], $options); + + return [$name, $argument]; + } } diff --git a/test/ActiveDataProviderTest.php b/test/ActiveDataProviderTest.php index 58996f9..b046318 100644 --- a/test/ActiveDataProviderTest.php +++ b/test/ActiveDataProviderTest.php @@ -3,4 +3,18 @@ class ActiveDataProviderTest extends TestCase { + public function testPagination() + { + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + + $values = array_map(function ($id) use ($fieldName1, $faker) { + return [ + $fieldName1 => $faker->uuid, + ]; + }, range(1, 50)); + + + } } diff --git a/test/CommandTest.php b/test/CommandTest.php index 3663156..b84963e 100644 --- a/test/CommandTest.php +++ b/test/CommandTest.php @@ -2,41 +2,26 @@ class CommandTest extends TestCase { - /** * @group createTable */ - public function testCreateTable() + public function testTableExists() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); $faker = \Faker\Factory::create(); $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; $this->assertFalse($command->tableExists($tableName)); - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); - + } + + /** + * @group createTable + */ + public function testCreateTable() + { + $command = $this->createCommand(); + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); $this->assertTrue($command->tableExists($tableName)); - $result = $command->describeTable($tableName)->execute(); $this->assertArraySubset([ 'Table' => [ @@ -62,32 +47,9 @@ public function testCreateTable() */ public function testDeleteTable() { - $db = $this->getConnection(); - $command = $db->createCommand(); - $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; + $command = $this->createCommand(); - $this->assertFalse($command->tableExists($tableName)); - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); $this->assertTrue($command->tableExists($tableName)); @@ -101,30 +63,11 @@ public function testDeleteTable() */ public function testPutItem() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $this->assertNotFalse($command->tableExists($tableName)); @@ -154,30 +97,11 @@ private function getTableItemCount($tableName) { */ public function testGetItemUsingScalarKey() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $id = $faker->firstNameFemale; $value = [ @@ -197,30 +121,11 @@ public function testGetItemUsingScalarKey() */ public function testGetItemUsingCompositeIndexedArrayKeyWithOneElement() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $id = $faker->firstNameFemale; $value = [ @@ -239,39 +144,11 @@ public function testGetItemUsingCompositeIndexedArrayKeyWithOneElement() */ public function testGetItemUsingCompositeIndexedArrayKeyWithTwoElement() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1, $fieldName2) = $this->createSimpleTableWithHashKeyAndRangeKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameFemale; - $fieldName2 = $faker->firstNameFemale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ], - [ - 'AttributeName' => $fieldName2, - 'KeyType' => 'RANGE', - ], - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ], - [ - 'AttributeName' => $fieldName2, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $id1 = $faker->firstNameFemale; $id2 = $faker->firstNameFemale; @@ -292,30 +169,11 @@ public function testGetItemUsingCompositeIndexedArrayKeyWithTwoElement() */ public function testGetItemUsingCompositeAssociativeArrayKeyWithOneElement() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $id = $faker->firstNameFemale; $value = [ @@ -334,39 +192,11 @@ public function testGetItemUsingCompositeAssociativeArrayKeyWithOneElement() */ public function testGetItemUsingCompositeAssociativeArrayKeyWithTwoElement() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1, $fieldName2) = $this->createSimpleTableWithHashKeyAndRangeKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameFemale; - $fieldName2 = $faker->firstNameFemale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ], - [ - 'AttributeName' => $fieldName2, - 'KeyType' => 'RANGE', - ], - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ], - [ - 'AttributeName' => $fieldName2, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $id1 = $faker->firstNameFemale; $id2 = $faker->firstNameFemale; @@ -387,30 +217,11 @@ public function testGetItemUsingCompositeAssociativeArrayKeyWithTwoElement() */ public function testBatchGetItemUsingIndexedArrayOfScalarElement() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $ids = array_map(function() use ($faker) { return $faker->uuid; @@ -439,30 +250,11 @@ public function testBatchGetItemUsingIndexedArrayOfScalarElement() */ public function testBatchGetItemUsingIndexedArrayOfIndexedArrayOnOneKey() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $ids = array_map(function() use ($faker) { return [ @@ -492,39 +284,11 @@ public function testBatchGetItemUsingIndexedArrayOfIndexedArrayOnOneKey() */ public function testBatchGetItemUsingIndexedArrayOfIndexedArrayOnTwoKey() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1, $fieldName2) = $this->createSimpleTableWithHashKeyAndRangeKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - $fieldName2 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ], - [ - 'AttributeName' => $fieldName2, - 'KeyType' => 'RANGE', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ], - [ - 'AttributeName' => $fieldName2, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $ids = array_map(function() use ($faker) { return [ @@ -556,39 +320,11 @@ public function testBatchGetItemUsingIndexedArrayOfIndexedArrayOnTwoKey() */ public function testBatchGetItemUsingIndexedArrayOfAssociativeArray() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1, $fieldName2) = $this->createSimpleTableWithHashKeyAndRangeKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - $fieldName2 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ], - [ - 'AttributeName' => $fieldName2, - 'KeyType' => 'RANGE', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ], - [ - 'AttributeName' => $fieldName2, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $ids = array_map(function() use ($fieldName1, $fieldName2, $faker) { return [ @@ -617,39 +353,11 @@ public function testBatchGetItemUsingIndexedArrayOfAssociativeArray() */ public function testBatchGetItemUsingAssociativeArrayOnTwoKeys() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1, $fieldName2) = $this->createSimpleTableWithHashKeyAndRangeKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - $fieldName2 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ], - [ - 'AttributeName' => $fieldName2, - 'KeyType' => 'RANGE', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ], - [ - 'AttributeName' => $fieldName2, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); $ids = []; $values = []; @@ -680,30 +388,11 @@ public function testBatchGetItemUsingAssociativeArrayOnTwoKeys() */ public function testScan() { - $db = $this->getConnection(); - $command = $db->createCommand(); + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + $faker = \Faker\Factory::create(); - $tableName = $faker->uuid; - $fieldName1 = $faker->firstNameMale; - - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ] - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ] - ])->execute(); foreach (range(1, 10) as $i) { $command->putItem($tableName, [ @@ -769,5 +458,67 @@ public function testScan() $this->assertNotEmpty($result5); $this->assertEquals(1, $result5['Count']); } + + /** + * @group batchPutItem + */ + public function testBatchPutItem() + { + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + + $faker = \Faker\Factory::create(); + + $ids = array_map(function() use ($faker) { + return $faker->uuid; + }, range(1, 10)); + + $values = array_map(function($id) use ($fieldName1, $faker) { + return [ + $fieldName1 => $id, + 'Field2' => $faker->firstName, + ]; + }, $ids); + + $command->batchPutItem($tableName, $values)->execute(); + + $this->assertEquals(10, $this->getTableItemCount($tableName)); + } + + /** + * @group batchDeleteItem + */ + public function testBatchDeleteItem() + { + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + + $faker = \Faker\Factory::create(); + + $ids = array_map(function() use ($faker) { + return $faker->uuid; + }, range(1, 10)); + + $values = array_map(function($id) use ($fieldName1, $faker) { + return [ + $fieldName1 => $id, + 'Field2' => $faker->firstName, + ]; + }, $ids); + + $command->batchPutItem($tableName, $values)->execute(); + + $this->assertEquals(10, $this->getTableItemCount($tableName)); + + $keys = array_slice(array_map(function($value) use ($fieldName1) { + return $value[$fieldName1]; + }, $values), 0, 5); + + $command->batchDeleteItem($tableName, $keys)->execute(); + + $this->assertEquals(5, $this->getTableItemCount($tableName)); + } } diff --git a/test/TestCase.php b/test/TestCase.php index 5ae9996..0cfc5b2 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -13,6 +13,82 @@ public function getConnection() return Yii::$app->dynamodb; } + /** + * @return \UrbanIndo\Yii2\DynamoDb\Command + */ + public function createCommand() + { + return $this->getConnection()->createCommand(); + } + + + public function createSimpleTableWithHashKey() + { + $command = $this->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->uuid; + $fieldName1 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + return [$tableName, $fieldName1]; + } + + public function createSimpleTableWithHashKeyAndRangeKey() + { + $command = $this->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = $faker->uuid; + $fieldName1 = $faker->firstNameMale; + $fieldName2 = $faker->firstNameMale; + + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ], + [ + 'AttributeName' => $fieldName2, + 'KeyType' => 'RANGE', + ] + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ], + [ + 'AttributeName' => $fieldName2, + 'AttributeType' => 'S', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ] + ])->execute(); + + return [$tableName, $fieldName1, $fieldName2]; + } + protected function mockWebApplication($config = [], $appClass = '\yii\web\Application') { new $appClass(ArrayHelper::merge([ From 6a0a808f4052b9be7b9faeef26c4cc659a83e53f Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Wed, 30 Dec 2015 16:22:55 +0700 Subject: [PATCH 11/23] - Add populateRecord in ActiveRecord - Fix insert in ActiveRecord - Set the responsedata assignment in populateRecord --- src/ActiveDataProvider.php | 14 ++++-- src/ActiveQuery.php | 2 + src/ActiveRecord.php | 88 ++++++++++++++++++++++++--------- src/Command.php | 10 ++++ src/Query.php | 15 +++--- test/ActiveDataProviderTest.php | 8 +-- test/ActiveRecordTest.php | 39 ++++++++++++--- test/CommandTest.php | 5 -- test/TestCase.php | 5 ++ 9 files changed, 129 insertions(+), 57 deletions(-) diff --git a/src/ActiveDataProvider.php b/src/ActiveDataProvider.php index 11c116b..b950d52 100644 --- a/src/ActiveDataProvider.php +++ b/src/ActiveDataProvider.php @@ -74,12 +74,13 @@ protected function prepareKeys(array $models) /** * Prepares the data models that will be made available in the current page. * @return array the available data models - * @throws \yii\base\InvalidConfigException If the query is not class of UrbanIndo\Yii2\DynamoDb\Query + * @throws InvalidConfigException If the query is not class of UrbanIndo\Yii2\DynamoDb\Query. */ protected function prepareModels() { if (!$this->query instanceof Query) { - throw new InvalidConfigException('The "query" property must be an instance of a class that implements the UrbanIndo\Yii2\DynamoDb\Query or its subclasses.'); + throw new InvalidConfigException('The "query" property must be an instance of a class that '. + 'implements the UrbanIndo\Yii2\DynamoDb\Query or its subclasses.'); } $query = clone $this->query; if (($pagination = $this->getPagination()) !== false) { @@ -101,11 +102,13 @@ protected function prepareModels() /** * Returns a value indicating the total number of data models in this data provider. * @return integer total number of data models in this data provider. + * @throws InvalidConfigException When the query is not istance of UrbanIndo\Yii2\DynamoDb\Query. */ protected function prepareTotalCount() { if (!$this->query instanceof Query) { - throw new InvalidConfigException('The "query" property must be an instance of a class that implements the UrbanIndo\Yii2\DynamoDb\Query or its subclasses.'); + throw new InvalidConfigException('The "query" property must be an instance of a class'. + ' that implements the UrbanIndo\Yii2\DynamoDb\Query or its subclasses.'); } $query = clone $this->query; return (int) $query->limit(-1)->orderBy([])->count('*', $this->db); @@ -128,7 +131,7 @@ public function getPagination() /** * Sets the pagination for this data provider. - * @param array|Pagination|boolean $value the pagination to be used by this data provider. + * @param array|Pagination|boolean $value The pagination to be used by this data provider. * This can be one of the following: * * - a configuration array for creating the pagination object. The "class" element defaults @@ -136,7 +139,8 @@ public function getPagination() * - an instance of [[Pagination]] or its subclass * - false, if pagination needs to be disabled. * - * @throws InvalidParamException + * @throws InvalidParamException When the value is not Pagination instance. + * @return void */ public function setPagination($value) { diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index d5a1a92..fc12daf 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -81,6 +81,8 @@ public function populate($rows) $models = $this->createModels($rows); if (!$this->asArray) { foreach ($models as $model) { + /* @var $model ActiveRecord */ + $model->setFindType($this->using); $model->afterFind(); } } diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 0830ef4..db958f0 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -10,6 +10,7 @@ use yii\db\BaseActiveRecord; use yii\helpers\Inflector; use yii\helpers\StringHelper; +use yii\helpers\ArrayHelper; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. @@ -97,9 +98,9 @@ public static function tableName() */ public static function find(array $options = []) { - return Yii::createObject(ActiveQuery::className(), array_merge($options, [ - 'class' => get_called_class() - ])); + return Yii::createObject(ActiveQuery::className(), array_merge([ + get_called_class(), + ], $options)); } /** @@ -137,7 +138,7 @@ public static function find(array $options = []) * * @return boolean whether the attributes are valid and the record is inserted successfully. */ - public function insert($runValidation = true, array $attributes = null) + public function insert($runValidation = true, $attributes = null) { if ($runValidation && !$this->validate($attributes)) { Yii::info('Model not inserted due to validation error.', __METHOD__); @@ -148,13 +149,13 @@ public function insert($runValidation = true, array $attributes = null) } $values = $this->getDirtyAttributes($attributes); - $ret = $this->getDb()->createCommand()->insert($this->tableName(), $values); + $ret = $this->getDb()->createCommand()->putItem($this->tableName(), $values)->execute(); $changedAttributes = array_fill_keys(array_keys($values), null); $this->setOldAttributes($values); $this->afterSave(true, $changedAttributes); - return $ret; + return true; } /** @@ -203,7 +204,7 @@ public static function batchInsert(array $values) * @param array $options Additional attribute. * @return static */ - public static function findOne($condition, array $options = null) + public static function findOne($condition, array $options = []) { return self::find($options)->where($condition)->one(); } @@ -214,11 +215,8 @@ public static function findOne($condition, array $options = null) * @param array $options Additional attribute for the query class. * @return static[] */ - public static function findAll($condition, array $options = null) + public static function findAll($condition, array $options = []) { - if ($options == null) { - $options = ['using' => Query::USING_BATCH_GET_ITEM]; - } return self::find($options)->where($condition)->all(); } @@ -235,7 +233,7 @@ public static function findAll($condition, array $options = null) * @return void * @throws \yii\base\NotSupportedException Not implemented yet. */ - public static function updateAll(array $attributes, $condition = '') + public static function updateAll($attributes, $condition = '') { throw new \yii\base\NotSupportedException(__METHOD__ . ' is not supported.'); } @@ -254,7 +252,7 @@ public static function updateAll(array $attributes, $condition = '') * @return void * @throws \yii\base\NotSupportedException Not implemented yet. */ - public static function updateAllCounters(array $counters, $condition = '') + public static function updateAllCounters($counters, $condition = '') { throw new \yii\base\NotSupportedException(__METHOD__ . ' is not supported.'); } @@ -274,26 +272,41 @@ public static function updateAllCounters(array $counters, $condition = '') * @return void * @throws \yii\base\NotSupportedException Not implemented yet. */ - public static function deleteAll($condition = '', array $params = []) + public static function deleteAll($condition = '', $params = []) { throw new \yii\base\NotSupportedException(__METHOD__ . ' is not supported.'); } /** - * Populate active records from query response. - * @param ActiveQuery $query The query that produces the records. - * @param array $response The operation response. - * @return static[] + * Populates an active record object using a row of data from the database/storage. + * + * This is an internal method meant to be called to create active record objects after + * fetching data from the database. It is mainly used by [[ActiveQuery]] to populate + * the query results into active records. + * + * When calling this method manually you should call [[afterFind()]] on the created + * record to trigger the [[EVENT_AFTER_FIND|afterFind Event]]. + * + * @param static $record The record to be populated. In most cases this will be an instance + * created by [[instantiate()]] beforehand. + * @param array $row Attribute values (name => value). + * @return void */ - public static function populateRecords(ActiveQuery $query, array $response) + public static function populateRecord($record, $row) { - $this->_findType = $query->using; + $responseData = ArrayHelper::getValue($row, Query::RESPONSE_KEY_PARAM); + unset($row[Query::RESPONSE_KEY_PARAM]); + parent::populateRecord($record, $row); + + if (!empty($responseData)) { + $record->_responseData = $responseData; + } } /** - * Returns the response meta data from BatchGetItem, GetItem, Scan, or Query - * operation. This can contains `ConsumedCapacity`, `UnprocessedKeys`, `Count`, - * `LastEvaluatedKey`, `ScannedCount` depends on whether the query enables + * Returns the response meta data from BatchGetItem, GetItem, Scan, or Query + * operation. This can contains `ConsumedCapacity`, `UnprocessedKeys`, `Count`, + * `LastEvaluatedKey`, `ScannedCount` depends on whether the query enables * storing the meta data. * @return array */ @@ -302,5 +315,34 @@ public function getResponseData() return $this->_responseData; } + /** + * Sets the method how the active record was retrieved. Valid values are + *
    + *
  • Query::USING_BATCH_GET_ITEM
  • + *
  • Query::USING_GET_ITEM
  • + *
  • Query::USING_QUERY
  • + *
  • Query::USING_SCAN
  • + *
+ * @param string $type The type. + * @return void + */ + public function setFindType($type) + { + $this->_findType = $type; + } + /** + * Returns the method how the active record was retrieved. Valid values are + *
    + *
  • Query::USING_BATCH_GET_ITEM
  • + *
  • Query::USING_GET_ITEM
  • + *
  • Query::USING_QUERY
  • + *
  • Query::USING_SCAN
  • + *
. + * @return string + */ + public function getFindType() + { + return $this->_findType; + } } diff --git a/src/Command.php b/src/Command.php index 0c77c9d..3018480 100644 --- a/src/Command.php +++ b/src/Command.php @@ -104,6 +104,16 @@ public function describeTable($table) list($name, $argument) = $this->db->getQueryBuilder()->describeTable($table); return $this->setCommand($name, $argument); } + + /** + * @param string $table The name of the table. + * @return integer + */ + public function getTableItemCount($table) + { + $description = $this->describeTable($table)->execute(); + return $description['Table']['ItemCount']; + } /** * Return whether a table exists or not. diff --git a/src/Query.php b/src/Query.php index 8a9a97f..583c74c 100644 --- a/src/Query.php +++ b/src/Query.php @@ -51,6 +51,11 @@ class Query extends Component implements QueryInterface * This will try to detect the auto. */ const USING_AUTO = 'Auto'; + + /** + * The name of the key of the row to store the response data. + */ + const RESPONSE_KEY_PARAM = '_response'; /** * Array of attributes being selected. It will be used to build Projection Expression. @@ -89,14 +94,6 @@ class Query extends Component implements QueryInterface * @var array|boolean */ public $storeResponseData = ['ConsumedCapacity', 'LastEvaluatedKey', 'ScannedCount', 'Count']; - - /** - * The key to store the response data. When populating the values of the response data for - * keys listed in [[storeResponseData]] will be stored on each row returned on the key - * stated in this variable. - * @var string - */ - public $responseDataKeyParam = '_response'; /** * Creates a DB command that can be used to execute this query. @@ -236,7 +233,7 @@ public function getItemsFromResponse($response) $storedResponse = self::extractStoredResponseData($this->storeResponseData, $response); if (!empty($storedResponse)) { $rows = array_map(function ($row) use ($storedResponse) { - $row[$this->responseDataKeyParam] = $storedResponse; + $row[self::RESPONSE_KEY_PARAM] = $storedResponse; return $row; }, $rows); } diff --git a/test/ActiveDataProviderTest.php b/test/ActiveDataProviderTest.php index b046318..af4c2db 100644 --- a/test/ActiveDataProviderTest.php +++ b/test/ActiveDataProviderTest.php @@ -9,12 +9,6 @@ public function testPagination() list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); - $values = array_map(function ($id) use ($fieldName1, $faker) { - return [ - $fieldName1 => $faker->uuid, - ]; - }, range(1, 50)); - - + $faker = Faker\Factory::create(); } } diff --git a/test/ActiveRecordTest.php b/test/ActiveRecordTest.php index 37a157d..ef0b48a 100644 --- a/test/ActiveRecordTest.php +++ b/test/ActiveRecordTest.php @@ -2,8 +2,37 @@ class ActiveRecordTest extends TestCase { + protected function setUp() { + parent::setUp(); + $command = $this->getConnection()->createCommand(); + /* @var $command \UrbanIndo\Yii2\DynamoDb\Command */ + $table = \test\data\Customer::tableName(); + if ($command->tableExists($table)) { + $command->deleteTable($table)->execute(); + } + $command->createTable($table, [ + 'AttributeDefinitions' => [ + [ + 'AttributeName' => 'id', + 'AttributeType' => 'N' + ] + ], + 'KeySchema' => [ + [ + 'AttributeName' => 'id', + 'KeyType' => 'HASH', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 10, + 'WriteCapacityUnits' => 10 + ] + ])->execute(); + } + public function testInsertAndFindOne() { + $this->assertEquals(0, $this->getTableItemCount(\test\data\Customer::tableName())); $objectToInsert = new \test\data\Customer(); $id = (int) \Faker\Provider\Base::randomNumber(5); $faker = \Faker\Factory::create(); @@ -27,13 +56,7 @@ public function testInsertAndFindOne() { ]; $this->assertTrue($objectToInsert->save(false)); - $objectFromFind = \test\data\Customer::findOne(['id' => $id]); - - /* @var $objectFromFind data\Customer */ - $this->assertNotNull($objectFromFind); - $this->assertEquals($id, $objectFromFind->id); - $this->assertEquals($objectToInsert->name, $objectFromFind->name); - $this->assertEquals($objectToInsert->kids, $objectFromFind->kids); + $this->assertEquals(1, $this->getTableItemCount(\test\data\Customer::tableName())); } public function testInsertAndFindAll() { @@ -90,7 +113,7 @@ public function testInsertAndFindAll() { $this->assertTrue($objectToInsert2->save(false)); - $objectsFromFind2 = \test\data\Customer::findAll(['id' => [$id1, $id2]]); + $objectsFromFind2 = \test\data\Customer::findAll(['id' => [$id1, $id2]]); /* @var $objectFromFind data\Customer */ $this->assertEquals(2, count($objectsFromFind2)); diff --git a/test/CommandTest.php b/test/CommandTest.php index b84963e..17f6be0 100644 --- a/test/CommandTest.php +++ b/test/CommandTest.php @@ -87,11 +87,6 @@ public function testPutItem() $this->assertEquals(2, $this->getTableItemCount($tableName)); } - private function getTableItemCount($tableName) { - $tableDescription = $this->getConnection()->createCommand()->describeTable($tableName)->execute(); - return $tableDescription['Table']['ItemCount']; - } - /** * @group getItem */ diff --git a/test/TestCase.php b/test/TestCase.php index 0cfc5b2..9ee525c 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -21,6 +21,11 @@ public function createCommand() return $this->getConnection()->createCommand(); } + public function getTableItemCount($tableName) { + $tableDescription = $this->getConnection()->createCommand()->describeTable($tableName)->execute(); + return $tableDescription['Table']['ItemCount']; + } + public function createSimpleTableWithHashKey() { From fa475481c2d0c59928fe15db4403622b0e0e6a74 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Wed, 30 Dec 2015 17:18:11 +0700 Subject: [PATCH 12/23] Add tests for active data provider and active record. --- CHANGELOG.md | 9 ++++ README.md | 57 ++++++++++++++++++++++++- src/ActiveDataProvider.php | 74 +++++++++++++++++++++++++-------- src/Connection.php | 1 + test/ActiveDataProviderTest.php | 65 +++++++++++++++++++++++++++++ test/ActiveRecordTest.php | 25 +---------- test/TestCase.php | 28 +++++++++++++ 7 files changed, 216 insertions(+), 43 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5b97918 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [1.0.0] - 2015-12-30 +### Added +- Support `ActiveDataProvider` and `Pagination` +- More unit tests. +### Changed +- A lot of changes from last release. _Sorry for that_ :) \ No newline at end of file diff --git a/README.md b/README.md index da1c54e..4f52ece 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,59 @@ This is a DynamoDB extension for Yii2 -[![Build Status](https://travis-ci.org/urbanindo/yii2-dynamodb.svg)](https://travis-ci.org/urbanindo/yii2-dynamodb) + +[![Latest Stable Version](https://poser.pugx.org/urbanindo/yii2-dynamodb/v/stable.svg)](https://packagist.org/packages/urbanindo/yii2-queue) +[![Total Downloads](https://poser.pugx.org/urbanindo/yii2-dynamodb/downloads.svg)](https://packagist.org/packages/urbanindo/yii2-queue) +[![Latest Unstable Version](https://poser.pugx.org/urbanindo/yii2-dynamodb/v/unstable.svg)](https://packagist.org/packages/urbanindo/yii2-queue) +[![Build Status](https://travis-ci.org/urbanindo/yii2-dynamodb.svg)](https://travis-ci.org/urbanindo/yii2-queue) + +## Requirement + +This extension requires +- PHP 5.4 +- Yii2 +- AWS PHP SDK 2.8 + +## Installation + +The preferred way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run + +``` +php composer.phar require --prefer-dist urbanindo/yii2-dynamodb "*" +``` + +or add + +``` +"urbanindo/yii2-dynamodb": "*" +``` + +to the require section of your `composer.json` file. + +## Setting Up + +After the installation, sets the `dynamodb` component in the config. + +```php +return [ + // ... + 'components' => [ + // + 'dynamodb' => [ + 'class' => 'UrbanIndo\Yii2\DynamoDb\Connection', + 'config' => [ + //This is the config used for Aws\DynamoDb\DynamoDbClient::factory() + //See http://docs.aws.amazon.com/aws-sdk-php/v2/guide/service-dynamodb.html#factory-method + 'credentials' => [ + 'key' => 'YOUR_AWS_ACCESS_KEY_ID', + 'secret' => 'YOUR_AWS_SECRET_ACCESS_KEY', + ], + 'region' => 'ap-southeast-1', + ] + ] + ], +]; +``` + diff --git a/src/ActiveDataProvider.php b/src/ActiveDataProvider.php index b950d52..be8eb10 100644 --- a/src/ActiveDataProvider.php +++ b/src/ActiveDataProvider.php @@ -46,6 +46,20 @@ class ActiveDataProvider extends \yii\data\BaseDataProvider */ public $db = 'dynamodb'; + /** + * @var string|callable the column that is used as the key of the data models. + * This can be either a column name, or a callable that returns the key value of a given data model. + * + * If this is not set, the following rules will be used to determine the keys of the data models: + * + * - If [[query]] is an [[\yii\db\ActiveQuery]] instance, + * the primary keys of [[\yii\db\ActiveQuery::modelClass]] will be used. + * - Otherwise, the keys of the [[models]] array will be used. + * + * @see getKeys() + */ + public $key; + /** * Initializes the DB connection component. * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. @@ -65,10 +79,44 @@ public function init() * @param array $models The available data models. * @return array the keys. */ - protected function prepareKeys(array $models) + protected function prepareKeys($models) { - $models; - return []; + $keys = []; + if ($this->key !== null) { + foreach ($models as $model) { + if (is_string($this->key)) { + $keys[] = $model[$this->key]; + } else { + $keys[] = call_user_func($this->key, $model); + } + } + + return $keys; + } elseif ($this->query instanceof \yii\db\ActiveQueryInterface) { + /* @var $class ActiveRecord */ + $query = $this->query; + /* @var $query ActiveQuery */ + $class = $query->modelClass; + $pks = $class::primaryKey(); + if (count($pks) === 1) { + $pk = $pks[0]; + foreach ($models as $model) { + $keys[] = $model[$pk]; + } + } else { + foreach ($models as $model) { + $kk = []; + foreach ($pks as $pk) { + $kk[$pk] = $model[$pk]; + } + $keys[] = $kk; + } + } + + return $keys; + } else { + return array_keys($models); + } } /** @@ -82,9 +130,9 @@ protected function prepareModels() throw new InvalidConfigException('The "query" property must be an instance of a class that '. 'implements the UrbanIndo\Yii2\DynamoDb\Query or its subclasses.'); } + $query = clone $this->query; if (($pagination = $this->getPagination()) !== false) { - $pagination->totalCount = $this->getTotalCount(); $query->limit($pagination->getLimit()); $query->offset($pagination->getOffset()); } @@ -100,18 +148,12 @@ protected function prepareModels() } /** - * Returns a value indicating the total number of data models in this data provider. - * @return integer total number of data models in this data provider. - * @throws InvalidConfigException When the query is not istance of UrbanIndo\Yii2\DynamoDb\Query. + * This will always return 0. + * @return integer Value 0. */ protected function prepareTotalCount() { - if (!$this->query instanceof Query) { - throw new InvalidConfigException('The "query" property must be an instance of a class'. - ' that implements the UrbanIndo\Yii2\DynamoDb\Query or its subclasses.'); - } - $query = clone $this->query; - return (int) $query->limit(-1)->orderBy([])->count('*', $this->db); + return 0; } /** @@ -122,11 +164,7 @@ protected function prepareTotalCount() */ public function getPagination() { - if ($this->_pagination === null) { - $this->setPagination([]); - } - - return $this->_pagination; + return parent::getPagination(); } /** diff --git a/src/Connection.php b/src/Connection.php index 3483c5c..17e3807 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -21,6 +21,7 @@ class Connection extends \yii\base\Component /** * The configuration for DynamoDB client. * @var array + * @see http://docs.aws.amazon.com/aws-sdk-php/v2/guide/service-dynamodb.html#factory-method */ public $config; diff --git a/test/ActiveDataProviderTest.php b/test/ActiveDataProviderTest.php index af4c2db..60bf62a 100644 --- a/test/ActiveDataProviderTest.php +++ b/test/ActiveDataProviderTest.php @@ -1,7 +1,41 @@ createCustomersTable(); + } + + public function testWithoutPagination() + { + $command = $this->createCommand(); + + list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); + + $faker = Faker\Factory::create(); + + $values = array_map(function ($id) use ($faker, $fieldName1) { + return [ + $fieldName1 => $faker->uuid, + 'Field2' => $id, + ]; + }, range(1, 25)); + + $command->batchPutItem($tableName, $values)->execute(); + + $dataProvider = new ActiveDataProvider([ + 'query' => test\data\Customer::find(), + 'pagination' => false, + ]); + + $this->assertCount(25, $dataProvider->getModels()); + + $this->assertFalse($dataProvider->getPagination()); + } public function testPagination() { @@ -10,5 +44,36 @@ public function testPagination() list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); $faker = Faker\Factory::create(); + + $values = array_map(function ($id) use ($faker, $fieldName1) { + return [ + $fieldName1 => $faker->uuid, + 'Field2' => $id, + ]; + }, range(1, 25)); + + $command->batchPutItem($tableName, $values)->execute(); + + $dataProvider1 = new ActiveDataProvider([ + 'query' => test\data\Customer::find(), + 'pagination' => [ + 'pageSize' => 5, + ] + ]); + + $this->assertCount(5, $dataProvider1->getModels()); + + $pagination1 = $dataProvider1->getPagination(); + $this->assertNotNull($pagination1->getNextLastKey()); + + $dataProvider2 = new ActiveDataProvider([ + 'query' => test\data\Customer::find(), + 'pagination' => [ + 'lastKey' => $pagination1->getNextLastKey(), + 'pageSize' => 5, + ] + ]); + + $this->assertCount(5, $dataProvider2->getModels()); } } diff --git a/test/ActiveRecordTest.php b/test/ActiveRecordTest.php index ef0b48a..0a5d3eb 100644 --- a/test/ActiveRecordTest.php +++ b/test/ActiveRecordTest.php @@ -4,30 +4,7 @@ class ActiveRecordTest extends TestCase { protected function setUp() { parent::setUp(); - $command = $this->getConnection()->createCommand(); - /* @var $command \UrbanIndo\Yii2\DynamoDb\Command */ - $table = \test\data\Customer::tableName(); - if ($command->tableExists($table)) { - $command->deleteTable($table)->execute(); - } - $command->createTable($table, [ - 'AttributeDefinitions' => [ - [ - 'AttributeName' => 'id', - 'AttributeType' => 'N' - ] - ], - 'KeySchema' => [ - [ - 'AttributeName' => 'id', - 'KeyType' => 'HASH', - ] - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 10, - 'WriteCapacityUnits' => 10 - ] - ])->execute(); + $this->createCustomersTable(); } public function testInsertAndFindOne() { diff --git a/test/TestCase.php b/test/TestCase.php index 9ee525c..0fe833b 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -118,4 +118,32 @@ protected function getVendorPath() } return $vendor; } + + protected function createCustomersTable() + { + $command = $this->createCommand(); + /* @var $command \UrbanIndo\Yii2\DynamoDb\Command */ + $table = \test\data\Customer::tableName(); + if ($command->tableExists($table)) { + $command->deleteTable($table)->execute(); + } + $command->createTable($table, [ + 'AttributeDefinitions' => [ + [ + 'AttributeName' => 'id', + 'AttributeType' => 'N' + ] + ], + 'KeySchema' => [ + [ + 'AttributeName' => 'id', + 'KeyType' => 'HASH', + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 10, + 'WriteCapacityUnits' => 10 + ] + ])->execute(); + } } From 78fbcb0aa0605b6fa7b81b9b74b3ea60b01419a7 Mon Sep 17 00:00:00 2001 From: Setyo Legowo Date: Tue, 29 Dec 2015 19:38:03 +0700 Subject: [PATCH 13/23] [WIP] QueryBuilder from Query - Support method: GetItem, BatchGetItem, Scan, Query --- src/QueryBuilder.php | 605 ++++++++++++++++++++++++++++++++++++-- test/QueryBuilderTest.php | 490 ++++++++++++++++++++++++++++-- test/data/Customer.php | 14 +- 3 files changed, 1051 insertions(+), 58 deletions(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index cd10aa0..6d813d5 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -18,11 +18,34 @@ */ class QueryBuilder extends Object { + /** + * The prefix for automatically generated query binding parameters. + */ + const PARAM_PREFIX = ':dqp'; + /** * @var Connection the database connection. */ public $db; + /** + * @var array map of query condition to builder methods. + * These methods are used by [[buildCondition]] to build SQL conditions from array syntax. + */ + protected $conditionBuilders = [ + 'NOT' => 'buildNotCondition', + 'AND' => 'buildAndOrCondition', + 'OR' => 'buildAndOrCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'ATTRIBUTE_EXISTS' => 'buildFunctionCondition', + 'ATTRIBUTE_NOT_EXISTS' => 'buildFunctionCondition', + 'ATTRIBUTE_TYPE' => 'buildFunctionCondition2Param', + 'BEGINS_WITH' => 'buildFunctionCondition2Param', + 'CONTAINS' => 'buildFunctionCondition2Param', + 'SIZE' => 'buildFunctionCondition', + ]; + /** * Constructor. * @param Connection $connection The database connection. @@ -41,10 +64,532 @@ public function __construct(Connection $connection, array $config = []) */ public function build(Query $query) { - $query; - return []; + // Validate query + if (empty($query->from)) { + throw new \InvalidArgumentException('Table name not set'); + } + if ($query->using == Query::USING_AUTO) { + if(empty($query->where) || !empty($query->indexBy) || !empty($query->limit) + || !empty($query->offset) || !empty($query->orderBy)) { + // TODO Choose either SCAN OR QUERY + throw new \Exception('[WIP]'); + // $query->using = Query::USING_SCAN; + } else { + $query->using = Query::USING_BATCH_GET_ITEM; + } + } + + $call = 'build' . $query->using; + + // Call builder + return $this->{$call}($query); + } + + /** + * Generates DynamoDB Query from a [[Query]] object use GetItem method + * @param Query $query Object from which the query will be generated. + * @return array The generated DynamoDB command configuration. + */ + public function buildGetItem(Query $query) + { + if (empty($query->where)) { + throw new \InvalidArgumentException('WHERE clause must not be empty.'); + } + if (!empty($query->indexBy) || !empty($query->limit) || !empty($query->offset) || !empty($query->orderBy)) { + throw new \InvalidArgumentException($query->using . ' is not support parameter beside where and select clause.'); + } + + return $this->getItem( + $query->from, + $this->buildWhereGetItem($query), + $this->buildOptions($query) + ); + } + + /** + * Generates DynamoDB Query from a [[Query]] object use BatchGetItem method + * @param Query $query Object from which the query will be generated. + * @return array The generated DynamoDB command configuration. + */ + public function buildBatchGetItem(Query $query) + { + if (empty($query->where)) { + throw new \InvalidArgumentException('WHERE clause must not be empty.'); + } + if (!empty($query->indexBy) || !empty($query->limit) || !empty($query->offset) || !empty($query->orderBy)) { + throw new \InvalidArgumentException($query->using . ' is not support parameter beside where and select clause.'); + } + + return $this->batchGetItem( + $query->from, + $this->buildWhereGetItem($query), + [], + $this->buildOptions($query) + ); + } + + /** + * Generates DynamoDB Query from a [[Query]] object use Scan method + * @param Query $query Object from which the query will be generated. + * @return array The generated DynamoDB command configuration. + * @throws Exception IndexName is string type, can not callable. + */ + public function buildScan(Query $query) + { + $options = $this->buildOptions($query); + if (!empty($query->indexBy)) { + if (is_callable($query->indexBy)) { + throw new \Exception("Cannot combine callable parameter."); + } + $options = array_merge( + $options, + [ + 'IndexName' => $query->indexBy + ] + ); + } + if (!empty($query->where)) { + $options = array_merge( + $options, + $this->buildWhereQueryScan($query->where) + ); + } + return $this->scan($query->from, $options); + } + + /** + * Generates DynamoDB Query from a [[Query]] object use Query method + * @param Query $query Object from which the query will be generated. + * @return array The generated DynamoDB command configuration. + * @throws Exception IndexName is string type, can not callable. + */ + public function buildQuery(Query $query) + { + $options = $this->buildOptions($query); + if (!empty($query->indexBy)) { + if (is_callable($query->indexBy)) { + throw new \Exception("Cannot combine callable parameter."); + } + $options = array_merge( + $options, + [ + 'IndexName' => $query->indexBy + ] + ); + } + if (!empty($query->where)) { + $options = array_merge( + $options, + $this->buildWhereQueryScan($query->where) + ); + // TODO Seperate FilterExpression and KeyConditionExpression + // For now, change all condition to KeyConditionExpression + $options['KeyConditionExpression'] = $options['FilterExpression']; + unset($options['FilterExpression']); + } + return $this->query($query->from, $options); + } + + /** + * Generate projection or selection of attribute for DynamoDB query + * @param Query $query Object from which the query will be generated. + * @return array Array of projection options + */ + public function buildProjection(Query $query) + { + if(!empty($query->select)) { + return is_array($query->select)? + ['ProjectionExpression' => implode(', ', $query->select)] : + ['ProjectionExpression' => $query->select]; + } else + return []; + } + + /** + * Generate $key parameter associate with query method + * @param Query $query Object from which the parameter will be generated. + * @return array Array of parameter + */ + public function buildWhereGetItem(Query $query) + { + if (!in_array($query->using, [Query::USING_BATCH_GET_ITEM])) + return $query->where; + + if (is_string($query->where) || is_numeric($query->where)) { + return [$query->where]; + } + // remain array type + // supported example: ['a' => 'b'], ['IN', 'a', 'b'], [['IN', 'a', 'b']] + // and combination of it like [['IN', 'a', 'b'], 'c' => 'd'] + + $new_where = []; + foreach ($query->where as $key => $value) { + if (is_string($value) || is_numeric($value)) { + if (ArrayHelper::isIndexed($query->where)) { + if ($query->where[0] != '=' && $query->where[0] != 'IN') + throw new \InvalidParamException($query->using . " not support operator '".$query->where[0]."'."); + if (sizeof($query->where) != 3) + throw new \InvalidParamException("The WHERE element require 3 elements."); + + if (is_array($query->where[2])) { + $new_where[$query->where[1]] = $query->where[2]; + } else { + $new_where[$query->where[1]] = [$query->where[2]]; + } + break; + } else { + if (isset($new_where[$key])) { + $new_where[$key] = array_merge($new_where[$key], [$value]); + } else { + $new_where[$key] = [$value]; + } + } + } else { // else just array type, perhaps + if (ArrayHelper::isIndexed($value)) { + if (isset($new_where[$key])) { + $new_where[$key] = array_merge($new_where[$key], $value); + } else { + $new_where[$key] = $value; + } + } + } + } + + return $new_where; + } + + /** + * @param string|array $condition + * @return array the WHERE clause built from [[Query::$where]]. + * @throws Exception throw when $condition is non array type + */ + public function buildWhereQueryScan($condition) + { + if (is_string($condition) || is_numeric($condition)) { + throw new \Exception("Condition just accept array type."); + } + $params = []; + $where = $this->buildCondition($condition, $params); + + return $where === '' ? [] : [ + 'FilterExpression' => $where, + 'ExpressionAttributeValues' => $this->paramToExpressionAttributeValues($params), + ]; + } + + public function paramToExpressionAttributeValues($params) + { + foreach ($params as $i => $value) { + if (is_int($value)) { + $params[$i] = ['N' => $value]; + } elseif (is_string($value)) { + $params[$i] = ['S' => $value]; + } else { + throw new \Exception("Unsupported value type."); + } + } + return $params; + } + + /** + * Parses the condition specification and generates the corresponding filter expression. + * @param string|array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @param array $params the binding parameters to be populated + * @return string the generated filter expression + */ + public function buildCondition($condition, &$params) + { + if (!is_array($condition)) { + return (string) $condition; + } elseif (empty($condition)) { + return ''; + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($this->conditionBuilders[$operator])) { + $method = $this->conditionBuilders[$operator]; + } else { + $method = 'buildSimpleCondition'; + } + array_shift($condition); + return $this->{$method}($operator, $condition, $params); + } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... + return $this->buildHashCondition($condition, $params); + } + } + + /** + * Creates a condition based on column-value pairs. + * @param array $condition the condition specification. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws Exception No NULL value in DynamoDB + */ + public function buildHashCondition($condition, &$params) + { + $parts = []; + foreach ($condition as $attribute => $value) { + if (is_array($value)) { + // IN condition + $parts[] = $this->buildInCondition('IN', [$attribute, $value], $params); + } else { + if ($value === null) { + throw new \Exception(__METHOD__ . " cannot include NULL value."); + } else { + $phName = self::PARAM_PREFIX . count($params); + $parts[] = "$attribute=$phName"; + $params[$phName] = $value; + } + } + } + return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; + } + + /** + * Connects two or more filter expressions with the `AND` or `OR` operator. + * @param string $operator the operator to use for connecting the given operands + * @param array $operands the SQL expressions to connect. + * @param array $params the binding parameters to be populated + * @return string the generated filter expression + */ + public function buildAndOrCondition($operator, $operands, &$params) + { + $parts = []; + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if (!empty($parts)) { + return '(' . implode(") $operator (", $parts) . ')'; + } else { + return ''; + } + } + + /** + * Inverts an SQL expressions with `NOT` operator. + * @param string $operator the operator to use for connecting the given operands + * @param array $operands the SQL expressions to connect. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildNotCondition($operator, $operands, &$params) + { + if (count($operands) !== 1) { + throw new \InvalidParamException("Operator '$operator' requires exactly one operand."); + } + $operand = reset($operands); + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand === '') { + return ''; + } + return "$operator ($operand)"; + } + + /** + * Creates an SQL expressions with the `BETWEEN` operator. + * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) + * @param array $operands the first operand is the column name. The second and third operands + * describe the interval that column value should be in. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildBetweenCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new \InvalidParamException("Operator '$operator' requires three operands."); + } + list($column, $value1, $value2) = $operands; + $phName1 = self::PARAM_PREFIX . count($params); + $params[$phName1] = $value1; + $phName2 = self::PARAM_PREFIX . count($params); + $params[$phName2] = $value2; + return "$column $operator $phName1 AND $phName2"; + } + + /** + * Creates an filter expressions with the `IN` operator. + * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) + * @param array $operands the first operand is the column name. If it is an array + * a composite IN condition will be generated. + * The second operand is an array of values that column value should be among. + * If it is an empty array the generated expression will be a `false` value if + * operator is `IN` and empty if operator is `NOT IN`. + * @param array $params the binding parameters to be populated + * @return string the generated filter expression + * @throws Exception if wrong number of operands have been given or no NULL + * value in DynamoDB. + */ + public function buildInCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new \Exception("Operator '$operator' requires two operands."); + } + list($column, $values) = $operands; + if ($values === [] || $column === []) { + return $operator === 'IN' ? '0=1' : ''; + } + $values = (array) $values; + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $params); + } + if (is_array($column)) { + $column = reset($column); + } + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + throw new \Exception(__METHOD__ . " cannot include NULL value."); + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $values[$i] = $phName; + } + } + if (count($values) > 1) { + return "$column $operator (" . implode(', ', $values) . ')'; + } else { + $operator = $operator === 'IN' ? '=' : '<>'; + return $column . $operator . reset($values); + } + } + + /** + * Builds filter expression for IN condition + * + * @param string $operator + * @param array $columns + * @param array $values + * @param array $params + * @return string filter expression + * @throws Exception No NULL value in DynamoDB + */ + protected function buildCompositeInCondition($operator, $columns, $values, &$params) + { + $vss = []; + foreach ($values as $value) { + $vs = []; + foreach ($columns as $column) { + if (isset($value[$column])) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value[$column]; + $vs[] = $phName; + } else { + throw new \Exception(__METHOD__ . " cannot include NULL value."); + } + } + $vss[] = '(' . implode(', ', $vs) . ')'; + } + foreach ($columns as $i => $column) { + if (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; } - + + /** + * Creates an filter expressions for function. + * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. + * @param array $operands contains two column names. + * @param array $params the binding parameters to be populated + * @return string the generated filter expression + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildFunctionCondition($func, $operands, &$params) + { + if (count($operands) !== 1) { + throw new \InvalidParamException("Function '$func' requires exactly one operand."); + } + $operand = reset($operands); + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand === '') { + return ''; + } + $func = strtolower($func); + return "$func ($operand)"; + } + + /** + * Creates an filter expressions like `"column" operator value`. + * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. + * @param array $operands contains two column names. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws InvalidParamException if wrong number of operands have been given. + */ + public function buildFunctionCondition2Param($func, $operands, &$params) + { + if (count($operands) !== 2) { + throw new \InvalidParamException("Function '$func' requires exactly two operands."); + } + $phName1 = self::PARAM_PREFIX . count($params); + $params[$phName1] = $operands[0]; + $phName2 = self::PARAM_PREFIX . count($params); + $params[$phName2] = $operands[1]; + $func = strtolower($func); + return "$func ($operand)"; + } + + /** + * Creates an filter expressions like `"column" operator value`. + * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. + * @param array $operands contains two column names. + * @param array $params the binding parameters to be populated + * @return string the generated filter expression + * @throws InvalidParamException if wrong number of operands have been given. + * @throws Exception No NULL value in DynamoDB + */ + public function buildSimpleCondition($operator, $operands, &$params) + { + if (count($operands) !== 2) { + throw new \InvalidParamException("Operator '$operator' requires two operands."); + } + list($column, $value) = $operands; + if ($value === null) { + throw new \Exception(__METHOD__ . " cannot include NULL value."); + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + return "$column $operator $phName"; + } + } + + /** + * Generate options or addition information for DynamoDB query + * @param Query $query Object from which the query will be generated. + * @return array Another options which used in the query + */ + public function buildOptions(Query $query) + { + $options = []; + + if (empty($query->consistentRead)) { + $query->consistentRead = false; + } + $options['ConsistentRead'] = $query->consistentRead; + + if (empty($query->returnConsumedCapacity)) { + $query->returnConsumedCapacity = false; + } + $options['ReturnConsumedCapacity'] = $query->returnConsumedCapacity; + + return array_merge($options, $this->buildProjection($query)); + } + /** * Builds a DynamoDB command to create table. * @@ -59,7 +604,7 @@ public function createTable($table, array $options = []) $argument = array_merge(['TableName' => $table], $options); return [$name, $argument]; } - + /** * Builds a DynamoDB command to describe table. * @@ -73,7 +618,7 @@ public function describeTable($table) $argument = ['TableName' => $table]; return [$name, $argument]; } - + /** * Builds a DynamoDB command to delete table. * @@ -106,7 +651,7 @@ public function putItem($table, array $value, array $options = []) ], $options); return [$name, $argument]; } - + /** * Builds a DynamoDB command to get item. * @@ -122,10 +667,10 @@ public function putItem($table, array $value, array $options = []) public function getItem($table, $key, array $options = []) { $name = 'GetItem'; - + $tableDescription = $this->db->createCommand()->describeTable($table)->execute(); $keySchema = $tableDescription['Table']['KeySchema']; - + if (is_string($key) || is_numeric($key)) { $keyArgument = $this->buildGetItemScalarKey($keySchema, $key); } else { @@ -139,7 +684,7 @@ public function getItem($table, $key, array $options = []) ); return [$name, $argument]; } - + /** * @param array $keySchema The schema of the key in the table. * @param mixed $key The key either string or integer. @@ -157,7 +702,7 @@ public function buildGetItemScalarKey(array $keySchema, $key) $keyName => Marshaler::marshalValue($key), ]; } - + /** * @param array $keySchema The schema of the key in the table. * @param array $keys The key as indexed key or associative key. @@ -177,7 +722,7 @@ public function buildGetItemCompositeKey(array $keySchema, array $keys) } return $keyArgument; } - + /** * Builds a DynamoDB command for batch get item. * @@ -224,17 +769,19 @@ public function buildGetItemCompositeKey(array $keySchema, array $keys) public function batchGetItem($table, array $keys, array $options = [], array $requestItemOptions = []) { $name = 'BatchGetItem'; - - $tableArgument = array_merge([ - $table => [ + + $tableArgument = [ + $table => array_merge([ 'Keys' => $this->buildBatchKeyArgument($table, $keys), - ] - ], $requestItemOptions); - + ], + $requestItemOptions + ) + ]; + $argument = array_merge(['RequestItems' => $tableArgument], $options); return [$name, $argument]; } - + /** * Resolve the keys into `BatchGetItem` eligible argument. * @param string $table The name of the table. @@ -245,7 +792,7 @@ private function buildBatchKeyArgument($table, $keys) { $tableDescription = $this->db->createCommand()->describeTable($table)->execute(); $keySchema = $tableDescription['Table']['KeySchema']; - + if (ArrayHelper::isIndexed($keys)) { $isScalar = is_string($keys[0]) || is_numeric($keys[0]); if ($isScalar) { @@ -259,7 +806,7 @@ private function buildBatchKeyArgument($table, $keys) return $this->buildBatchGetItemFromAssociativeArray($keySchema, $keys); } } - + /** * @param array $keySchema The KeySchema of the table. * @param array $keys Indexed array of scalar element. @@ -278,7 +825,7 @@ private function buildBatchKeyArgumentFromIndexedArrayOfScalar(array $keySchema, ]; }, $keys); } - + /** * @param array $keySchema The KeySchema of the table. * @param array $keys Indexed array of indexed array. @@ -295,7 +842,7 @@ private function buildBatchKeyArgumentFromIndexedArrayOfIndexedArray(array $keyS return $return; }, $keys); } - + /** * @param array $keySchema The KeySchema of the table. * @param array $keys Indexed array of associative array. @@ -313,7 +860,7 @@ private function buildBatchKeyArgumentFromIndexedArrayOfAssociativeArray(array $ return $return; }, $keys); } - + /** * @param array $keySchema The KeySchema of the table. * @param array $keys Associative array of indexed scalar. @@ -339,7 +886,7 @@ public function buildBatchGetItemFromAssociativeArray(array $keySchema, array $k } return $this->buildBatchKeyArgumentFromIndexedArrayOfAssociativeArray($keySchema, $indexedKey); } - + /** * Builds a DynamoDB command to scan table. * @@ -356,7 +903,7 @@ public function scan($table, array $options = []) ], $options); return [$name, $argument]; } - + /** * Builds a DynamoDB command to query table. * @@ -373,7 +920,7 @@ public function query($table, array $options = []) ], $options); return [$name, $argument]; } - + /** * Builds a DynamoDB command to put multiple items. * @@ -400,7 +947,7 @@ public function batchPutItem($table, array $values, array $options = []) ], $options); return [$name, $argument]; } - + /** * Builds a DynamoDB command for batch delete item. * @@ -454,13 +1001,13 @@ public function batchDeleteItem($table, array $keys, array $options = []) ] ]; }, $keyArgument); - + $argument = array_merge([ 'RequestItems' => [ $table => $deleteRequests, ] ], $options); - + return [$name, $argument]; } } diff --git a/test/QueryBuilderTest.php b/test/QueryBuilderTest.php index 413df8f..930ec9f 100644 --- a/test/QueryBuilderTest.php +++ b/test/QueryBuilderTest.php @@ -1,26 +1,464 @@ -getConnection(); - return $connection->getQueryBuilder(); - } - - public function testCreateTable() - { - $qb = $this->getQueryBuilder(); - list($name, $options) = $qb->createTable('Test'); - $this->assertEquals('CreateTable', $name); - $this->assertEquals([ - 'TableName' => 'Test' - ], $options); - } -} + + */ + +use UrbanIndo\Yii2\DynamoDb\Query; +use UrbanIndo\Yii2\DynamoDb\QueryBuilder; +use test\data\Customer; + +/** + * PHP Unit Test Class for Query Builder + * + * @author Setyo Legowo + */ +class QueryBuilderTest extends TestCase +{ + + /** + * @var Connection the database connection. + */ + public $db; + + /** + * Initiate testing + * @return void + */ + public function setUp() + { + $this->db = $this->getConnection(); + $command = $this->db->createCommand(); + $faker = \Faker\Factory::create(); + $tableName = Customer::tableName(); + $fieldName1 = Customer::primaryKey()[0]; + $index1 = Customer::secondaryIndex()[0]; + $indexFieldName1 = Customer::keySecondayIndex()[$index1][0]; + + if (!$command->tableExists($tableName)) { + $command->createTable($tableName, [ + 'KeySchema' => [ + [ + 'AttributeName' => $fieldName1, + 'KeyType' => 'HASH', + ], + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $fieldName1, + 'AttributeType' => 'S', + ], + [ + 'AttributeName' => $indexFieldName1, + 'AttributeType' => 'S', + ], + ], + 'LocalSecondaryIndexes' => [ + [ + 'IndexName' => $index1, + 'KeySchema' => [ + [ 'AttributeName' => $index1, 'KeyType' => 'HASH' ] + ], + 'Projection' => [ + 'ProjectionType' => 'KEYS_ONLY' + ], + ], + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 5, + 'WriteCapacityUnits' => 5, + ], + ])->execute(); + } + } + + /** + * Get Query Builder + * @return QueryBuilder + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Get Query Get Item method + * @return Query + */ + public function createQueryGetItem() + { + $query = new Query(); + $query->using = Query::USING_GET_ITEM; + $query->from(Customer::tableName()); + + return $query; + } + + /** + * Get Query Get Batch Item method + * @return Query + */ + public function createQueryGetBatchItem() + { + $query = new Query(); + $query->using = Query::USING_BATCH_GET_ITEM; + $query->from(Customer::tableName()); + + return $query; + } + + /** + * Get Query Scan method + * @return Query + */ + public function createQueryScan() + { + $query = new Query(); + $query->using = Query::USING_SCAN; + $query->from(Customer::tableName()); + + return $query; + } + + /** + * Get Query method + * @return Query + */ + public function createQuery() + { + $query = new Query(); + $query->using = Query::USING_QUERY; + $query->from(Customer::tableName()); + + return $query; + } + + /** + * Test build simple GetItem method + * @return void + */ + public function testBuildSimpleGetItem() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id = $faker->firstNameFemale; + $query1 = $this->createQueryGetItem()->where(['id' => $id]); + $query2 = $this->createQueryGetItem()->where($id); + + $expected = [ + 'TableName' => Customer::tableName(), + 'Key' => [ + 'id' => ['S' => $id] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + $this->assertEquals($expected, $qb->build($query2)[1]); + } + + /** + * Test build GetItem method with simple select + * @return void + */ + public function testBuildGetItemWithSimpleSelect() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id = $faker->firstNameFemale; + $query1 = $this->createQueryGetItem()->select(['id', 'name', 'contacts']) + ->where(['id' => $id]); + + $expected = [ + 'TableName' => Customer::tableName(), + 'Key' => [ + 'id' => ['S' => $id] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + 'ProjectionExpression' => 'id, name, contacts', + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } + + /** + * Test build simple GetBatchItem method + * @return void + */ + public function testBuildSimpleGetBatchItem() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id = $faker->firstNameFemale; + $query1 = $this->createQueryGetBatchItem()->where(['id' => $id]); + $query2 = $this->createQueryGetBatchItem()->where($id); + + $expected = [ + 'RequestItems' => [ + Customer::tableName() => [ + 'Keys' => [ + [ + 'id' => ['S' => $id] + ] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ] + ] + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + $this->assertEquals($expected, $qb->build($query2)[1]); + } + + /** + * Test build simple GetBatchItem method + * @return void + */ + public function testBuildSimpleGetBatchItem2() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id1 = $faker->firstNameFemale; + $id2 = $faker->firstNameFemale; + $id3 = $faker->firstNameFemale; + $query1 = $this->createQueryGetBatchItem()->where(['IN', 'id', [$id1, $id2, $id3]]); + $query2 = $this->createQueryGetBatchItem()->where(['id' => [$id1, $id2, $id3]]); + + $expected = [ + 'RequestItems' => [ + Customer::tableName() => [ + 'Keys' => [ + [ + 'id' => ['S' => $id1] + ], + [ + 'id' => ['S' => $id2] + ], + [ + 'id' => ['S' => $id3] + ] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ] + ] + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + $this->assertEquals($expected, $qb->build($query2)[1]); + } + + /** + * Test build Get Batch Item method with simple select + * @return void + */ + public function testBuildGetBatchItemWithSimpleSelect() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id1 = $faker->firstNameFemale; + $id2 = $faker->firstNameFemale; + $id3 = $faker->firstNameFemale; + $query1 = $this->createQueryGetBatchItem()->select(['id', 'name', 'contacts']) + ->where(['IN', 'id', [$id1, $id2, $id3]]); + + $expected = [ + 'RequestItems' => [ + Customer::tableName() => [ + 'Keys' => [ + [ + 'id' => ['S' => $id1] + ], + [ + 'id' => ['S' => $id2] + ], + [ + 'id' => ['S' => $id3] + ] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + 'ProjectionExpression' => 'id, name, contacts', + ] + ] + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } + + /** + * Test build Simple Scan method with no parameter + * @return void + */ + public function testBuildSimpleScanNoParameter() + { + $qb = $this->createQueryBuilder(); + $query1 = $this->createQueryScan(); + + $expected = [ + 'TableName' => Customer::tableName(), + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } + + /** + * Test build Simple Scan method + * @return void + */ + public function testBuildSimpleScan() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id = $faker->firstNameFemale; + $query1 = $this->createQueryScan()->where(['id' => $id]); + + $expected = [ + 'TableName' => Customer::tableName(), + 'FilterExpression' => 'id=:dqp0', + 'ExpressionAttributeValues' => [ + ':dqp0' => ['S' => $id] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } + + /** + * Test build Scan method with simple select + * @return void + */ + public function testBuildScanWithSimpleSelect() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id = $faker->firstNameFemale; + $query1 = $this->createQueryScan()->select(['id', 'name', 'contacts']) + ->where(['id' => $id]); + + $expected = [ + 'TableName' => Customer::tableName(), + 'FilterExpression' => 'id=:dqp0', + 'ProjectionExpression' => 'id, name, contacts', + 'ExpressionAttributeValues' => [ + ':dqp0' => ['S' => $id] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } + + /** + * Test build Scan method with simple select + * @return void + */ + public function testBuildScanWithIndex() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id = $faker->firstNameFemale; + $query1 = $this->createQueryScan()->select(['id', 'name', 'contacts']) + ->where(['name' => $id])->indexBy(Customer::secondaryIndex()[0]); + + $expected = [ + 'TableName' => Customer::tableName(), + 'IndexName' => Customer::secondaryIndex()[0], + 'FilterExpression' => 'name=:dqp0', + 'ProjectionExpression' => 'id, name, contacts', + 'ExpressionAttributeValues' => [ + ':dqp0' => ['S' => $id] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } + + /** + * Test build Simple Query method + * @return void + */ + public function testBuildSimpleQuery() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id = $faker->firstNameFemale; + $query1 = $this->createQuery()->where(['id' => $id]); + + $expected = [ + 'TableName' => Customer::tableName(), + 'KeyConditionExpression' => 'id=:dqp0', + 'ExpressionAttributeValues' => [ + ':dqp0' => ['S' => $id] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } + + /** + * Test build Query method with simple select + * @return void + */ + public function testBuildQueryWithSimpleSelect() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id = $faker->firstNameFemale; + $query1 = $this->createQuery()->select(['id', 'name', 'contacts']) + ->where(['id' => $id]); + + $expected = [ + 'TableName' => Customer::tableName(), + 'KeyConditionExpression' => 'id=:dqp0', + 'ProjectionExpression' => 'id, name, contacts', + 'ExpressionAttributeValues' => [ + ':dqp0' => ['S' => $id] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } + + /** + * Test build Query method with index parameter + * @return void + */ + public function testBuildQueryWithIndex() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $id = $faker->firstNameFemale; + $query1 = $this->createQuery()->select(['id', 'name', 'contacts']) + ->where(['name' => $id])->indexBy(Customer::secondaryIndex()[0]); + + $expected = [ + 'TableName' => Customer::tableName(), + 'IndexName' => Customer::secondaryIndex()[0], + 'KeyConditionExpression' => 'name=:dqp0', + 'ProjectionExpression' => 'id, name, contacts', + 'ExpressionAttributeValues' => [ + ':dqp0' => ['S' => $id] + ], + 'ConsistentRead' => false, + 'ReturnConsumedCapacity' => false, + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } +} diff --git a/test/data/Customer.php b/test/data/Customer.php index a1e1982..92e4387 100644 --- a/test/data/Customer.php +++ b/test/data/Customer.php @@ -7,15 +7,23 @@ * @property integer $name */ class Customer extends \UrbanIndo\Yii2\DynamoDb\ActiveRecord { - + public static function tableName() { return 'Customers'; } - + public static function primaryKey() { return ['id']; } - + + public static function secondaryIndex() { + return ['index1']; + } + + public static function keySecondayIndex() { + return ['index1' => ['name']]; + } + public function attributes() { return [ 'id', From f44f27c21acfb305637a35f4c60be6fabfdcb76c Mon Sep 17 00:00:00 2001 From: Setyo Legowo Date: Wed, 30 Dec 2015 17:36:30 +0700 Subject: [PATCH 14/23] [WIP] Offset and key lookup - Comply with PHPCS --- src/ActiveRecord.php | 22 +-- src/Query.php | 45 +++--- src/QueryBuilder.php | 324 ++++++++++++++++++++++---------------- test/QueryBuilderTest.php | 178 ++++++++++++++++++--- 4 files changed, 367 insertions(+), 202 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index db958f0..875f96d 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -46,7 +46,7 @@ class ActiveRecord extends BaseActiveRecord * @var array */ protected static $_primaryKeys = []; - + /** * Stores the response metadata either from retrieval operation such as *
    @@ -57,7 +57,7 @@ class ActiveRecord extends BaseActiveRecord * @var array */ protected $_responseData = []; - + /** * Stores the operation type that retrieves this model. Eligible values are. *
      @@ -96,7 +96,7 @@ public static function tableName() * @param array $options Additional options for the query class. * @return ActiveQuery the newly created [[ActiveQuery]] instance. */ - public static function find(array $options = []) + public static function find($options = []) { return Yii::createObject(ActiveQuery::className(), array_merge([ get_called_class(), @@ -193,7 +193,7 @@ public static function primaryKey() * @param array $values The values to be inserted. * @return mixed */ - public static function batchInsert(array $values) + public static function batchInsert($values) { return self::getDb()->createCommand()->putItems(static::tableName(), $values); } @@ -204,7 +204,7 @@ public static function batchInsert(array $values) * @param array $options Additional attribute. * @return static */ - public static function findOne($condition, array $options = []) + public static function findOne($condition, $options = null) { return self::find($options)->where($condition)->one(); } @@ -215,7 +215,7 @@ public static function findOne($condition, array $options = []) * @param array $options Additional attribute for the query class. * @return static[] */ - public static function findAll($condition, array $options = []) + public static function findAll($condition, $options = null) { return self::find($options)->where($condition)->all(); } @@ -276,7 +276,7 @@ public static function deleteAll($condition = '', $params = []) { throw new \yii\base\NotSupportedException(__METHOD__ . ' is not supported.'); } - + /** * Populates an active record object using a row of data from the database/storage. * @@ -297,12 +297,12 @@ public static function populateRecord($record, $row) $responseData = ArrayHelper::getValue($row, Query::RESPONSE_KEY_PARAM); unset($row[Query::RESPONSE_KEY_PARAM]); parent::populateRecord($record, $row); - + if (!empty($responseData)) { $record->_responseData = $responseData; } } - + /** * Returns the response meta data from BatchGetItem, GetItem, Scan, or Query * operation. This can contains `ConsumedCapacity`, `UnprocessedKeys`, `Count`, @@ -314,7 +314,7 @@ public function getResponseData() { return $this->_responseData; } - + /** * Sets the method how the active record was retrieved. Valid values are *
        @@ -330,7 +330,7 @@ public function setFindType($type) { $this->_findType = $type; } - + /** * Returns the method how the active record was retrieved. Valid values are *
          diff --git a/src/Query.php b/src/Query.php index 583c74c..af87f97 100644 --- a/src/Query.php +++ b/src/Query.php @@ -28,30 +28,30 @@ class Query extends Component implements QueryInterface * @link http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html */ const USING_BATCH_GET_ITEM = 'BatchGetItem'; - + /** * If the query is GetItem operation, meaning the query is for a single item using the key. * @link http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html */ const USING_GET_ITEM = 'GetItem'; - + /** * If the query is Query operation, meaning it uses primary key or secondary key from the table. * @link http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html */ const USING_QUERY = 'Query'; - + /** * If the query is Scan operation, meaning it will access every item in the table. * @link http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html */ const USING_SCAN = 'Scan'; - + /** * This will try to detect the auto. */ const USING_AUTO = 'Auto'; - + /** * The name of the key of the row to store the response data. */ @@ -69,7 +69,7 @@ class Query extends Component implements QueryInterface * @see from() */ public $using = self::USING_AUTO; - + /** * Whether to use consistent read or not. * @var boolean @@ -87,7 +87,7 @@ class Query extends Component implements QueryInterface * @var string */ public $from; - + /** * Whether to store response data in the data returned. This can be either boolean * false if not to store response data or the key of the response to store. @@ -110,8 +110,8 @@ public function createCommand(Connection $db = null) return $db->createCommand($config); } - - + + /** * Creates command and execute the query. * @param Connection $db The database connection used. @@ -192,25 +192,16 @@ public function withoutConsistentRead() } /** - * Whether to return the consumed capacity. + * Whether to use the consumed capacity. + * @param string $setConsumedCapacity String about consumed capacity. * @return static */ - public function withConsumedCapacity() + public function setConsumedCapacity($setConsumedCapacity) { - $this->returnConsumedCapacity = true; + $this->returnConsumedCapacity = $setConsumedCapacity; return $this; } - /** - * Whether not to return the consumed capacity. - * @return static - */ - public function withoutConsumedCapacity() - { - $this->returnConsumedCapacity = false; - return $this; - } - /** * @param array $response The raw response result from operation. * @return array array of values. @@ -229,7 +220,7 @@ public function getItemsFromResponse($response) $row = Marshaler::unmarshalItem($response['Item']); $rows = [$row]; } - + $storedResponse = self::extractStoredResponseData($this->storeResponseData, $response); if (!empty($storedResponse)) { $rows = array_map(function ($row) use ($storedResponse) { @@ -237,10 +228,10 @@ public function getItemsFromResponse($response) return $row; }, $rows); } - + return $rows; } - + /** * @param mixed $responseKeys List of keys to store from operation response, false if don't want to store. * @param array $response The raw response from operation. @@ -260,7 +251,7 @@ private static function extractStoredResponseData($responseKeys, $response) } return $return; } - + /** * Converts the raw query results into the format as specified by this query. * This method is internally used to convert the data fetched from database @@ -325,7 +316,7 @@ public function exists($db = null) $rows = $this->getItemsFromResponse($response); return !empty($rows); } - + /** * Returns the number of records. * @param string $q The COUNT expression. This parameter is ignored by this implementation. diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 6d813d5..11b02fb 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -9,6 +9,7 @@ use yii\base\Object; use yii\helpers\ArrayHelper; +use yii\base\InvalidParamException; /** * QueryBuilder builds an elasticsearch query based on the specification given @@ -61,19 +62,24 @@ public function __construct(Connection $connection, array $config = []) * Generates DynamoDB Query from a [[Query]] object. * @param Query $query Object from which the query will be generated. * @return array The generated DynamoDB command configuration. + * @throws InvalidArgumentException Table name should be exist. */ public function build(Query $query) { // Validate query if (empty($query->from)) { - throw new \InvalidArgumentException('Table name not set'); + throw new InvalidArgumentException('Table name not set'); } + if ($query->using == Query::USING_AUTO) { - if(empty($query->where) || !empty($query->indexBy) || !empty($query->limit) + if (empty($query->where) || !empty($query->indexBy) || !empty($query->limit) || !empty($query->offset) || !empty($query->orderBy)) { - // TODO Choose either SCAN OR QUERY - throw new \Exception('[WIP]'); - // $query->using = Query::USING_SCAN; + // TODO Choose appropiate method + if (empty($query->orderBy)) { + $query->using = Query::USING_QUERY; + } else { + $query->using = Query::USING_SCAN; + } } else { $query->using = Query::USING_BATCH_GET_ITEM; } @@ -89,14 +95,16 @@ public function build(Query $query) * Generates DynamoDB Query from a [[Query]] object use GetItem method * @param Query $query Object from which the query will be generated. * @return array The generated DynamoDB command configuration. + * @throws InvalidArgumentException Parameters should be comply with method. */ public function buildGetItem(Query $query) { if (empty($query->where)) { - throw new \InvalidArgumentException('WHERE clause must not be empty.'); + throw new InvalidArgumentException('WHERE clause must not be empty.'); } if (!empty($query->indexBy) || !empty($query->limit) || !empty($query->offset) || !empty($query->orderBy)) { - throw new \InvalidArgumentException($query->using . ' is not support parameter beside where and select clause.'); + throw new InvalidArgumentException($query->using . + ' is not support parameter beside where and select clause.'); } return $this->getItem( @@ -110,14 +118,16 @@ public function buildGetItem(Query $query) * Generates DynamoDB Query from a [[Query]] object use BatchGetItem method * @param Query $query Object from which the query will be generated. * @return array The generated DynamoDB command configuration. + * @throws InvalidArgumentException Should comply with BatchGetItem condition. */ public function buildBatchGetItem(Query $query) { if (empty($query->where)) { - throw new \InvalidArgumentException('WHERE clause must not be empty.'); + throw new InvalidArgumentException('WHERE clause must not be empty.'); } if (!empty($query->indexBy) || !empty($query->limit) || !empty($query->offset) || !empty($query->orderBy)) { - throw new \InvalidArgumentException($query->using . ' is not support parameter beside where and select clause.'); + throw new InvalidArgumentException($query->using . + ' is not support parameter beside where and select clause.'); } return $this->batchGetItem( @@ -129,25 +139,18 @@ public function buildBatchGetItem(Query $query) } /** - * Generates DynamoDB Query from a [[Query]] object use Scan method + * Generates DynamoDB Query from a [[Query]] object use Scan method. * @param Query $query Object from which the query will be generated. * @return array The generated DynamoDB command configuration. - * @throws Exception IndexName is string type, can not callable. + * @throws Exception Scan method do not support sorting. */ public function buildScan(Query $query) { - $options = $this->buildOptions($query); - if (!empty($query->indexBy)) { - if (is_callable($query->indexBy)) { - throw new \Exception("Cannot combine callable parameter."); - } - $options = array_merge( - $options, - [ - 'IndexName' => $query->indexBy - ] - ); + if (!empty($query->orderBy)) { + throw new Exception($query->using . ' method cannot set in order.'); } + + $options = $this->buildOptions($query); if (!empty($query->where)) { $options = array_merge( $options, @@ -161,29 +164,18 @@ public function buildScan(Query $query) * Generates DynamoDB Query from a [[Query]] object use Query method * @param Query $query Object from which the query will be generated. * @return array The generated DynamoDB command configuration. - * @throws Exception IndexName is string type, can not callable. */ public function buildQuery(Query $query) { $options = $this->buildOptions($query); - if (!empty($query->indexBy)) { - if (is_callable($query->indexBy)) { - throw new \Exception("Cannot combine callable parameter."); - } - $options = array_merge( - $options, - [ - 'IndexName' => $query->indexBy - ] - ); - } if (!empty($query->where)) { $options = array_merge( $options, $this->buildWhereQueryScan($query->where) ); // TODO Seperate FilterExpression and KeyConditionExpression - // For now, change all condition to KeyConditionExpression + // For now, change all condition to KeyConditionExpression (assumed + // all where condition use key attributes) $options['KeyConditionExpression'] = $options['FilterExpression']; unset($options['FilterExpression']); } @@ -191,30 +183,34 @@ public function buildQuery(Query $query) } /** - * Generate projection or selection of attribute for DynamoDB query + * Generate projection or selection of attribute for DynamoDB query. * @param Query $query Object from which the query will be generated. - * @return array Array of projection options + * @return array Array of projection options. */ public function buildProjection(Query $query) { - if(!empty($query->select)) { - return is_array($query->select)? - ['ProjectionExpression' => implode(', ', $query->select)] : - ['ProjectionExpression' => $query->select]; - } else + if (!empty($query->select)) { + return is_array($query->select) ? [ + 'ProjectionExpression' => implode(', ', $query->select) + ] : [ + 'ProjectionExpression' => $query->select + ]; + } else { return []; + } } /** - * Generate $key parameter associate with query method + * Generate $key parameter associate with query method. * @param Query $query Object from which the parameter will be generated. * @return array Array of parameter + * @throws InvalidParamException Param query has to comply. */ public function buildWhereGetItem(Query $query) { - if (!in_array($query->using, [Query::USING_BATCH_GET_ITEM])) + if (!in_array($query->using, [Query::USING_BATCH_GET_ITEM])) { return $query->where; - + } if (is_string($query->where) || is_numeric($query->where)) { return [$query->where]; } @@ -226,10 +222,13 @@ public function buildWhereGetItem(Query $query) foreach ($query->where as $key => $value) { if (is_string($value) || is_numeric($value)) { if (ArrayHelper::isIndexed($query->where)) { - if ($query->where[0] != '=' && $query->where[0] != 'IN') - throw new \InvalidParamException($query->using . " not support operator '".$query->where[0]."'."); - if (sizeof($query->where) != 3) - throw new \InvalidParamException("The WHERE element require 3 elements."); + if ($query->where[0] != '=' && $query->where[0] != 'IN') { + throw new InvalidParamException($query->using . + " not support operator '" . $query->where[0] . "'."); + } + if (sizeof($query->where) != 3) { + throw new InvalidParamException('The WHERE element require 3 elements.'); + } if (is_array($query->where[2])) { $new_where[$query->where[1]] = $query->where[2]; @@ -259,14 +258,15 @@ public function buildWhereGetItem(Query $query) } /** - * @param string|array $condition - * @return array the WHERE clause built from [[Query::$where]]. - * @throws Exception throw when $condition is non array type - */ + * Build where condition comply with DynamoDB method Query and Scan. + * @param string|array $condition Condition or where value. + * @return array The WHERE clause built from [[Query::$where]]. + * @throws Exception Throw when $condition is non array type. + */ public function buildWhereQueryScan($condition) { if (is_string($condition) || is_numeric($condition)) { - throw new \Exception("Condition just accept array type."); + throw new Exception('Condition just accept array type.'); } $params = []; $where = $this->buildCondition($condition, $params); @@ -277,25 +277,31 @@ public function buildWhereQueryScan($condition) ]; } - public function paramToExpressionAttributeValues($params) - { - foreach ($params as $i => $value) { - if (is_int($value)) { - $params[$i] = ['N' => $value]; - } elseif (is_string($value)) { - $params[$i] = ['S' => $value]; - } else { - throw new \Exception("Unsupported value type."); - } - } - return $params; - } + /** + * Parses the condition specification and generates the corresponding filter expression. + * @param array $params The binding parameters to be populated. + * @return string The generated array for expression attribute values. + * @throws Exception Value type just support basic value, temporary. + */ + public function paramToExpressionAttributeValues($params) + { + foreach ($params as $i => $value) { + if (is_int($value)) { + $params[$i] = ['N' => $value]; + } elseif (is_string($value)) { + $params[$i] = ['S' => $value]; + } else { + throw new Exception('Unsupported value type.'); + } + } + return $params; + } /** * Parses the condition specification and generates the corresponding filter expression. - * @param string|array $condition the condition specification. Please refer to [[Query::where()]] - * on how to specify a condition. - * @param array $params the binding parameters to be populated + * @param string|array $condition The condition specification. Please refer + * to [[Query::where()]] on how to specify a condition. + * @param array $params The binding parameters to be populated. * @return string the generated filter expression */ public function buildCondition($condition, &$params) @@ -321,10 +327,10 @@ public function buildCondition($condition, &$params) /** * Creates a condition based on column-value pairs. - * @param array $condition the condition specification. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws Exception No NULL value in DynamoDB + * @param array $condition The condition specification. + * @param array $params The binding parameters to be populated. + * @return string The generated SQL expression + * @throws Exception No NULL value in DynamoDB. */ public function buildHashCondition($condition, &$params) { @@ -335,7 +341,7 @@ public function buildHashCondition($condition, &$params) $parts[] = $this->buildInCondition('IN', [$attribute, $value], $params); } else { if ($value === null) { - throw new \Exception(__METHOD__ . " cannot include NULL value."); + throw new Exception(__METHOD__ . ' cannot include NULL value.'); } else { $phName = self::PARAM_PREFIX . count($params); $parts[] = "$attribute=$phName"; @@ -348,10 +354,10 @@ public function buildHashCondition($condition, &$params) /** * Connects two or more filter expressions with the `AND` or `OR` operator. - * @param string $operator the operator to use for connecting the given operands - * @param array $operands the SQL expressions to connect. - * @param array $params the binding parameters to be populated - * @return string the generated filter expression + * @param string $operator The operator to use for connecting the given operands. + * @param array $operands The SQL expressions to connect. + * @param array $params The binding parameters to be populated. + * @return string The generated filter expression. */ public function buildAndOrCondition($operator, $operands, &$params) { @@ -373,16 +379,16 @@ public function buildAndOrCondition($operator, $operands, &$params) /** * Inverts an SQL expressions with `NOT` operator. - * @param string $operator the operator to use for connecting the given operands - * @param array $operands the SQL expressions to connect. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if wrong number of operands have been given. + * @param string $operator The operator to use for connecting the given operands. + * @param array $operands The SQL expressions to connect. + * @param array $params The binding parameters to be populated. + * @return string The generated SQL expression + * @throws InvalidParamException If wrong number of operands have been given. */ public function buildNotCondition($operator, $operands, &$params) { if (count($operands) !== 1) { - throw new \InvalidParamException("Operator '$operator' requires exactly one operand."); + throw new InvalidParamException("Operator '$operator' requires exactly one operand."); } $operand = reset($operands); if (is_array($operand)) { @@ -396,17 +402,17 @@ public function buildNotCondition($operator, $operands, &$params) /** * Creates an SQL expressions with the `BETWEEN` operator. - * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) - * @param array $operands the first operand is the column name. The second and third operands - * describe the interval that column value should be in. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if wrong number of operands have been given. + * @param string $operator The operator to use (e.g. `BETWEEN` or `NOT BETWEEN`). + * @param array $operands The first operand is the column name. The second and + * third operands describe the interval that column value should be in. + * @param array $params The binding parameters to be populated. + * @return string The generated SQL expression. + * @throws InvalidParamException If wrong number of operands have been given. */ public function buildBetweenCondition($operator, $operands, &$params) { if (!isset($operands[0], $operands[1], $operands[2])) { - throw new \InvalidParamException("Operator '$operator' requires three operands."); + throw new InvalidParamException("Operator '$operator' requires three operands."); } list($column, $value1, $value2) = $operands; $phName1 = self::PARAM_PREFIX . count($params); @@ -418,21 +424,21 @@ public function buildBetweenCondition($operator, $operands, &$params) /** * Creates an filter expressions with the `IN` operator. - * @param string $operator the operator to use (e.g. `IN` or `NOT IN`) - * @param array $operands the first operand is the column name. If it is an array + * @param string $operator The operator to use (e.g. `IN` or `NOT IN`). + * @param array $operands The first operand is the column name. If it is an array * a composite IN condition will be generated. * The second operand is an array of values that column value should be among. * If it is an empty array the generated expression will be a `false` value if * operator is `IN` and empty if operator is `NOT IN`. - * @param array $params the binding parameters to be populated - * @return string the generated filter expression - * @throws Exception if wrong number of operands have been given or no NULL + * @param array $params The binding parameters to be populated. + * @return string The generated filter expression. + * @throws Exception If wrong number of operands have been given or no NULL * value in DynamoDB. */ public function buildInCondition($operator, $operands, &$params) { if (!isset($operands[0], $operands[1])) { - throw new \Exception("Operator '$operator' requires two operands."); + throw new Exception("Operator '$operator' requires two operands."); } list($column, $values) = $operands; if ($values === [] || $column === []) { @@ -450,7 +456,7 @@ public function buildInCondition($operator, $operands, &$params) $value = isset($value[$column]) ? $value[$column] : null; } if ($value === null) { - throw new \Exception(__METHOD__ . " cannot include NULL value."); + throw new Exception(__METHOD__ . ' cannot include NULL value.'); } else { $phName = self::PARAM_PREFIX . count($params); $params[$phName] = $value; @@ -468,12 +474,12 @@ public function buildInCondition($operator, $operands, &$params) /** * Builds filter expression for IN condition * - * @param string $operator - * @param array $columns - * @param array $values - * @param array $params - * @return string filter expression - * @throws Exception No NULL value in DynamoDB + * @param string $operator The operator to use. Anything could be used e.g. '`IN`'. + * @param array $columns The columns of composite IN condition. + * @param array $values The values of composite IN condition. + * @param array $params The binding parameters to be populated. + * @return string Filter expression. + * @throws Exception No NULL value in DynamoDB. */ protected function buildCompositeInCondition($operator, $columns, $values, &$params) { @@ -486,7 +492,7 @@ protected function buildCompositeInCondition($operator, $columns, $values, &$par $params[$phName] = $value[$column]; $vs[] = $phName; } else { - throw new \Exception(__METHOD__ . " cannot include NULL value."); + throw new Exception(__METHOD__ . ' cannot include NULL value.'); } } $vss[] = '(' . implode(', ', $vs) . ')'; @@ -501,20 +507,24 @@ protected function buildCompositeInCondition($operator, $columns, $values, &$par /** * Creates an filter expressions for function. - * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. - * @param array $operands contains two column names. - * @param array $params the binding parameters to be populated - * @return string the generated filter expression - * @throws InvalidParamException if wrong number of operands have been given. + * @param string $func The operator to use. Anything could be used e.g. `>`, `<=`, etc. + * @param array $operands Contains two column names. + * @param array $params The binding parameters to be populated. + * @return string The generated filter expression. + * @throws InvalidParamException If wrong number of operands have been given. */ public function buildFunctionCondition($func, $operands, &$params) { if (count($operands) !== 1) { - throw new \InvalidParamException("Function '$func' requires exactly one operand."); + throw new InvalidParamException("Function '$func' requires exactly one operand."); } $operand = reset($operands); if (is_array($operand)) { $operand = $this->buildCondition($operand, $params); + } else { + $phName1 = self::PARAM_PREFIX . count($params); + $params[$phName1] = $operand; + $operand = $phName1; } if ($operand === '') { return ''; @@ -525,42 +535,42 @@ public function buildFunctionCondition($func, $operands, &$params) /** * Creates an filter expressions like `"column" operator value`. - * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. - * @param array $operands contains two column names. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws InvalidParamException if wrong number of operands have been given. + * @param string $func The operator to use. Anything could be used e.g. `>`, `<=`, etc. + * @param array $operands Contains two column names. + * @param array $params The binding parameters to be populated. + * @return string The generated SQL expression. + * @throws InvalidParamException If wrong number of operands have been given. */ public function buildFunctionCondition2Param($func, $operands, &$params) { if (count($operands) !== 2) { - throw new \InvalidParamException("Function '$func' requires exactly two operands."); + throw new InvalidParamException("Function '$func' requires exactly two operands."); } $phName1 = self::PARAM_PREFIX . count($params); $params[$phName1] = $operands[0]; $phName2 = self::PARAM_PREFIX . count($params); $params[$phName2] = $operands[1]; $func = strtolower($func); - return "$func ($operand)"; + return "$func ($phName1, $phName2)"; } /** * Creates an filter expressions like `"column" operator value`. - * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. - * @param array $operands contains two column names. - * @param array $params the binding parameters to be populated - * @return string the generated filter expression - * @throws InvalidParamException if wrong number of operands have been given. - * @throws Exception No NULL value in DynamoDB + * @param string $operator The operator to use. Anything could be used e.g. `>`, `<=`, etc. + * @param array $operands Contains two column names. + * @param array $params The binding parameters to be populated. + * @return string the generated filter expression. + * @throws InvalidParamException If wrong number of operands have been given. + * @throws Exception No NULL value in DynamoDB. */ public function buildSimpleCondition($operator, $operands, &$params) { if (count($operands) !== 2) { - throw new \InvalidParamException("Operator '$operator' requires two operands."); + throw new InvalidParamException("Operator '$operator' requires two operands."); } list($column, $value) = $operands; if ($value === null) { - throw new \Exception(__METHOD__ . " cannot include NULL value."); + throw new Exception(__METHOD__ . ' cannot include NULL value.'); } else { $phName = self::PARAM_PREFIX . count($params); $params[$phName] = $value; @@ -572,20 +582,60 @@ public function buildSimpleCondition($operator, $operands, &$params) * Generate options or addition information for DynamoDB query * @param Query $query Object from which the query will be generated. * @return array Another options which used in the query + * @throws InvalidArgumentException Table name should be exist and IndexName + * is string type, can not callable. */ public function buildOptions(Query $query) { $options = []; - if (empty($query->consistentRead)) { - $query->consistentRead = false; + if (!empty($query->orderBy)) { + $sort = ''; + if (is_array($query->orderBy)) { + if (ArrayHelper::isIndexed($query->orderBy)) { + $query->indexBy = $query->orderBy[0]; + $sort = $query->orderBy[1]; + } else { + $query->indexBy = key($query->orderBy); + $sort = current($query->orderBy); + } + } else { + list($index, $sort) = explode(' ', $query->orderBy, 2); + $query->indexBy = $index; + } + $sort = strtoupper($sort); + if (!in_array($sort, ['ASC', 'DESC'])) { + throw new InvalidArgumentException('Sort key unknown: ' . reset($query->orderBy)); + } + $options['ScanIndexForward'] = ($sort == 'ASC'); } - $options['ConsistentRead'] = $query->consistentRead; - - if (empty($query->returnConsumedCapacity)) { - $query->returnConsumedCapacity = false; + if (!empty($query->indexBy)) { + if (is_callable($query->indexBy)) { + throw new InvalidArgumentException('Cannot combine callable parameter.'); + } + $options = array_merge($options, ['IndexName' => $query->indexBy]); + } + if (!empty($query->limit)) { + $options = array_merge($options, ['Limit' => (int) $query->limit]); + } + if (!is_null($query->consistentRead)) { + if (!is_bool($query->consistentRead)) { + throw new InvalidArgumentException( + 'Unsupported consistent read value. Accept boolean type.' + ); + } + $options['ConsistentRead'] = $query->consistentRead; + } + if (!empty($query->returnConsumedCapacity)) { + $query->returnConsumedCapacity = strtoupper($query->returnConsumedCapacity); + if (!in_array($query->returnConsumedCapacity, ['INDEXES', 'TOTAL', 'NONE'])) { + throw new InvalidArgumentException( + 'Unsupported return consumed capacity value:' . + $query->returnConsumedCapacity + ); + } + $options['ReturnConsumedCapacity'] = $query->returnConsumedCapacity; } - $options['ReturnConsumedCapacity'] = $query->returnConsumedCapacity; return array_merge($options, $this->buildProjection($query)); } @@ -772,10 +822,8 @@ public function batchGetItem($table, array $keys, array $options = [], array $re $tableArgument = [ $table => array_merge([ - 'Keys' => $this->buildBatchKeyArgument($table, $keys), - ], - $requestItemOptions - ) + 'Keys' => $this->buildBatchKeyArgument($table, $keys), + ], $requestItemOptions) ]; $argument = array_merge(['RequestItems' => $tableArgument], $options); diff --git a/test/QueryBuilderTest.php b/test/QueryBuilderTest.php index 930ec9f..e49748c 100644 --- a/test/QueryBuilderTest.php +++ b/test/QueryBuilderTest.php @@ -145,18 +145,27 @@ public function testBuildSimpleGetItem() $id = $faker->firstNameFemale; $query1 = $this->createQueryGetItem()->where(['id' => $id]); $query2 = $this->createQueryGetItem()->where($id); + $query3 = $this->createQueryGetItem()->where($id)->withConsistentRead(); + $query4 = $this->createQueryGetItem()->where($id)->withoutConsistentRead(); + $query5 = $this->createQueryGetItem()->where($id)->setConsumedCapacity('NONE'); $expected = [ 'TableName' => Customer::tableName(), 'Key' => [ 'id' => ['S' => $id] - ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, + ] ]; $this->assertEquals($expected, $qb->build($query1)[1]); $this->assertEquals($expected, $qb->build($query2)[1]); + + $expected['ConsistentRead'] = true; + $this->assertEquals($expected, $qb->build($query3)[1]); + $expected['ConsistentRead'] = false; + $this->assertEquals($expected, $qb->build($query4)[1]); + unset($expected['ConsistentRead']); + $expected['ReturnConsumedCapacity'] = 'NONE'; + $this->assertEquals($expected, $qb->build($query5)[1]); } /** @@ -176,8 +185,6 @@ public function testBuildGetItemWithSimpleSelect() 'Key' => [ 'id' => ['S' => $id] ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, 'ProjectionExpression' => 'id, name, contacts', ]; @@ -195,6 +202,9 @@ public function testBuildSimpleGetBatchItem() $id = $faker->firstNameFemale; $query1 = $this->createQueryGetBatchItem()->where(['id' => $id]); $query2 = $this->createQueryGetBatchItem()->where($id); + $query3 = $this->createQueryGetBatchItem()->where($id)->withConsistentRead(); + $query4 = $this->createQueryGetBatchItem()->where($id)->withoutConsistentRead(); + $query5 = $this->createQueryGetBatchItem()->where($id)->setConsumedCapacity('NONE'); $expected = [ 'RequestItems' => [ @@ -204,14 +214,20 @@ public function testBuildSimpleGetBatchItem() 'id' => ['S' => $id] ] ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, ] ] ]; $this->assertEquals($expected, $qb->build($query1)[1]); $this->assertEquals($expected, $qb->build($query2)[1]); + + $expected['RequestItems'][Customer::tableName()]['ConsistentRead'] = true; + $this->assertEquals($expected, $qb->build($query3)[1]); + $expected['RequestItems'][Customer::tableName()]['ConsistentRead'] = false; + $this->assertEquals($expected, $qb->build($query4)[1]); + unset($expected['RequestItems'][Customer::tableName()]['ConsistentRead']); + $expected['RequestItems'][Customer::tableName()]['ReturnConsumedCapacity'] = 'NONE'; + $this->assertEquals($expected, $qb->build($query5)[1]); } /** @@ -241,9 +257,7 @@ public function testBuildSimpleGetBatchItem2() [ 'id' => ['S' => $id3] ] - ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, + ] ] ] ]; @@ -280,8 +294,6 @@ public function testBuildGetBatchItemWithSimpleSelect() 'id' => ['S' => $id3] ] ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, 'ProjectionExpression' => 'id, name, contacts', ] ] @@ -301,8 +313,6 @@ public function testBuildSimpleScanNoParameter() $expected = [ 'TableName' => Customer::tableName(), - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, ]; $this->assertEquals($expected, $qb->build($query1)[1]); @@ -318,6 +328,9 @@ public function testBuildSimpleScan() $faker = \Faker\Factory::create(); $id = $faker->firstNameFemale; $query1 = $this->createQueryScan()->where(['id' => $id]); + $query2 = $this->createQueryScan()->where(['id' => $id])->withConsistentRead(); + $query3 = $this->createQueryScan()->where(['id' => $id])->withoutConsistentRead(); + $query4 = $this->createQueryScan()->where(['id' => $id])->setConsumedCapacity('NONE'); $expected = [ 'TableName' => Customer::tableName(), @@ -325,11 +338,17 @@ public function testBuildSimpleScan() 'ExpressionAttributeValues' => [ ':dqp0' => ['S' => $id] ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, ]; $this->assertEquals($expected, $qb->build($query1)[1]); + + $expected['ConsistentRead'] = true; + $this->assertEquals($expected, $qb->build($query2)[1]); + $expected['ConsistentRead'] = false; + $this->assertEquals($expected, $qb->build($query3)[1]); + unset($expected['ConsistentRead']); + $expected['ReturnConsumedCapacity'] = 'NONE'; + $this->assertEquals($expected, $qb->build($query4)[1]); } /** @@ -351,8 +370,6 @@ public function testBuildScanWithSimpleSelect() 'ExpressionAttributeValues' => [ ':dqp0' => ['S' => $id] ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, ]; $this->assertEquals($expected, $qb->build($query1)[1]); @@ -378,8 +395,67 @@ public function testBuildScanWithIndex() 'ExpressionAttributeValues' => [ ':dqp0' => ['S' => $id] ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, + ]; + + $this->assertEquals($expected, $qb->build($query1)[1]); + } + + /** + * Test build Scan method with complicated condition + * @return void + */ + public function testBuildScanWithComplicatedCondition() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $name = $faker->firstNameFemale; + $id1 = $faker->firstNameFemale; + $id2 = $faker->firstNameFemale; + $query1 = $this->createQueryScan()->select(['id', 'name', 'contacts']) + ->where(['name' => $name])->andWhere(['id' => $id1]); + $query2 = $this->createQueryScan()->select(['id', 'name', 'contacts']) + ->where(['name' => $name])->andWhere(['id' => [$id1, $id2]]); + $query3 = $this->createQueryScan()->select(['id', 'name', 'contacts']) + ->where(['name' => $name])->andWhere(['attribute_exists', 'id']); + $query4 = $this->createQueryScan()->select(['id', 'name', 'contacts']) + ->where(['name' => $name])->andWhere(['begins_with', 'id', $id1]); + + $expected = [ + 'TableName' => Customer::tableName(), + 'FilterExpression' => '(name=:dqp0) AND (id=:dqp1)', + 'ProjectionExpression' => 'id, name, contacts', + 'ExpressionAttributeValues' => [ + ':dqp0' => ['S' => $name], + ':dqp1' => ['S' => $id1] + ], + ]; + $this->assertEquals($expected, $qb->build($query1)[1]); + + $expected['FilterExpression'] = '(name=:dqp0) AND (id IN (:dqp1, :dqp2))'; + $expected['ExpressionAttributeValues'][':dqp2'] = ['S' => $id2]; + $this->assertEquals($expected, $qb->build($query2)[1]); + + $expected['FilterExpression'] = '(name=:dqp0) AND (attribute_exists (:dqp1))'; + $expected['ExpressionAttributeValues'][':dqp1'] = ['S' => 'id']; + unset($expected['ExpressionAttributeValues'][':dqp2']); + $this->assertEquals($expected, $qb->build($query3)[1]); + + $expected['FilterExpression'] = '(name=:dqp0) AND (begins_with (:dqp1, :dqp2))'; + $expected['ExpressionAttributeValues'][':dqp2'] = ['S' => $id1]; + $this->assertEquals($expected, $qb->build($query4)[1]); + } + + /** + * Test build Simple Scan method with no parameter + * @return void + */ + public function testBuildSimpleQueryNoParameter() + { + $qb = $this->createQueryBuilder(); + $query1 = $this->createQuery(); + + $expected = [ + 'TableName' => Customer::tableName(), ]; $this->assertEquals($expected, $qb->build($query1)[1]); @@ -395,6 +471,9 @@ public function testBuildSimpleQuery() $faker = \Faker\Factory::create(); $id = $faker->firstNameFemale; $query1 = $this->createQuery()->where(['id' => $id]); + $query2 = $this->createQuery()->where(['id' => $id])->withConsistentRead(); + $query3 = $this->createQuery()->where(['id' => $id])->withoutConsistentRead(); + $query4 = $this->createQuery()->where(['id' => $id])->setConsumedCapacity('NONE'); $expected = [ 'TableName' => Customer::tableName(), @@ -402,11 +481,17 @@ public function testBuildSimpleQuery() 'ExpressionAttributeValues' => [ ':dqp0' => ['S' => $id] ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, ]; $this->assertEquals($expected, $qb->build($query1)[1]); + + $expected['ConsistentRead'] = true; + $this->assertEquals($expected, $qb->build($query2)[1]); + $expected['ConsistentRead'] = false; + $this->assertEquals($expected, $qb->build($query3)[1]); + unset($expected['ConsistentRead']); + $expected['ReturnConsumedCapacity'] = 'NONE'; + $this->assertEquals($expected, $qb->build($query4)[1]); } /** @@ -428,8 +513,6 @@ public function testBuildQueryWithSimpleSelect() 'ExpressionAttributeValues' => [ ':dqp0' => ['S' => $id] ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, ]; $this->assertEquals($expected, $qb->build($query1)[1]); @@ -455,10 +538,53 @@ public function testBuildQueryWithIndex() 'ExpressionAttributeValues' => [ ':dqp0' => ['S' => $id] ], - 'ConsistentRead' => false, - 'ReturnConsumedCapacity' => false, ]; $this->assertEquals($expected, $qb->build($query1)[1]); } + + /** + * Test build Query method with complicated condition + * @return void + */ + public function testBuildQueryWithComplicatedCondition() + { + $qb = $this->createQueryBuilder(); + $faker = \Faker\Factory::create(); + $name = $faker->firstNameFemale; + $id1 = $faker->firstNameFemale; + $id2 = $faker->firstNameFemale; + $query1 = $this->createQuery()->select(['id', 'name', 'contacts']) + ->where(['name' => $name])->andWhere(['id' => $id1]); + $query2 = $this->createQuery()->select(['id', 'name', 'contacts']) + ->where(['name' => $name])->andWhere(['id' => [$id1, $id2]]); + $query3 = $this->createQuery()->select(['id', 'name', 'contacts']) + ->where(['name' => $name])->andWhere(['attribute_exists', 'id']); + $query4 = $this->createQuery()->select(['id', 'name', 'contacts']) + ->where(['name' => $name])->andWhere(['begins_with', 'id', $id1]); + + $expected = [ + 'TableName' => Customer::tableName(), + 'KeyConditionExpression' => '(name=:dqp0) AND (id=:dqp1)', + 'ProjectionExpression' => 'id, name, contacts', + 'ExpressionAttributeValues' => [ + ':dqp0' => ['S' => $name], + ':dqp1' => ['S' => $id1] + ], + ]; + $this->assertEquals($expected, $qb->build($query1)[1]); + + $expected['KeyConditionExpression'] = '(name=:dqp0) AND (id IN (:dqp1, :dqp2))'; + $expected['ExpressionAttributeValues'][':dqp2'] = ['S' => $id2]; + $this->assertEquals($expected, $qb->build($query2)[1]); + + $expected['KeyConditionExpression'] = '(name=:dqp0) AND (attribute_exists (:dqp1))'; + $expected['ExpressionAttributeValues'][':dqp1'] = ['S' => 'id']; + unset($expected['ExpressionAttributeValues'][':dqp2']); + $this->assertEquals($expected, $qb->build($query3)[1]); + + $expected['KeyConditionExpression'] = '(name=:dqp0) AND (begins_with (:dqp1, :dqp2))'; + $expected['ExpressionAttributeValues'][':dqp2'] = ['S' => $id1]; + $this->assertEquals($expected, $qb->build($query4)[1]); + } } From 23d9950ca4237fedddce7ee22f51e0d4eac64c7f Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Wed, 30 Dec 2015 23:51:53 +0700 Subject: [PATCH 15/23] Fix Query Builder, Active Query, and ActiveRecordTest. --- src/ActiveQuery.php | 22 +++++++++++++- src/ActiveRecord.php | 4 +-- src/Query.php | 39 +++++++++++++++++++++---- src/QueryBuilder.php | 41 ++++++++++++++------------ test/ActiveDataProviderTest.php | 52 +++++++++++++-------------------- test/ActiveRecordTest.php | 8 +++++ test/ConnectionTest.php | 3 +- test/TestCase.php | 17 +++++++++++ test/bootstrap.php | 15 ---------- test/data/Customer.php | 1 + 10 files changed, 127 insertions(+), 75 deletions(-) diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index fc12daf..091b00d 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -77,7 +77,7 @@ public function populate($rows) if (empty($rows)) { return []; } - + $models = $this->createModels($rows); if (!$this->asArray) { foreach ($models as $model) { @@ -88,6 +88,26 @@ public function populate($rows) } return $models; } + + /** + * Prepares for building SQL. + * This method is called by [[QueryBuilder]] when it starts to build SQL from a query object. + * You may override this method to do some final preparation work when converting a query into a SQL statement. + * @param QueryBuilder $builder The query builder. + * @return $this a prepared query instance which will be used by [[QueryBuilder]] to build the SQL + */ + public function prepare(QueryBuilder $builder) + { + $builder; + if (empty($this->from)) { + /* @var $modelClass ActiveRecord */ + $modelClass = $this->modelClass; + $tableName = $modelClass::tableName(); + $this->from = $tableName; + } + return $this; + } + /** * Executes query and returns a single row of result. diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 875f96d..7d5903b 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -204,7 +204,7 @@ public static function batchInsert($values) * @param array $options Additional attribute. * @return static */ - public static function findOne($condition, $options = null) + public static function findOne($condition, $options = []) { return self::find($options)->where($condition)->one(); } @@ -215,7 +215,7 @@ public static function findOne($condition, $options = null) * @param array $options Additional attribute for the query class. * @return static[] */ - public static function findAll($condition, $options = null) + public static function findAll($condition, $options = []) { return self::find($options)->where($condition)->all(); } diff --git a/src/Query.php b/src/Query.php index af87f97..6b5eb8f 100644 --- a/src/Query.php +++ b/src/Query.php @@ -106,9 +106,12 @@ public function createCommand(Connection $db = null) if ($db === null) { $db = Yii::$app->get('dynamodb'); } - $config = $db->getQueryBuilder()->build($this); - - return $db->createCommand($config); + list($name, $argument) = $db->getQueryBuilder()->build($this); + + return $db->createCommand([ + 'name' => $name, + 'argument' => $argument, + ]); } @@ -194,6 +197,12 @@ public function withoutConsistentRead() /** * Whether to use the consumed capacity. * @param string $setConsumedCapacity String about consumed capacity. + * Available values are + *
            + *
          • INDEXES
          • + *
          • TOTAL
          • + *
          • NONE
          • + *
          * @return static */ public function setConsumedCapacity($setConsumedCapacity) @@ -220,7 +229,7 @@ public function getItemsFromResponse($response) $row = Marshaler::unmarshalItem($response['Item']); $rows = [$row]; } - + $storedResponse = self::extractStoredResponseData($this->storeResponseData, $response); if (!empty($storedResponse)) { $rows = array_map(function ($row) use ($storedResponse) { @@ -251,6 +260,18 @@ private static function extractStoredResponseData($responseKeys, $response) } return $return; } + + /** + * Prepares for building SQL. + * This method is called by [[QueryBuilder]] when it starts to build SQL from a query object. + * You may override this method to do some final preparation work when converting a query into a SQL statement. + * @param QueryBuilder $builder The query builder. + * @return $this a prepared query instance which will be used by [[QueryBuilder]] to build the SQL + */ + public function prepare(QueryBuilder $builder) + { + return $this; + } /** * Converts the raw query results into the format as specified by this query. @@ -287,7 +308,7 @@ public function all($db = null) { $response = $this->execute($db); $rows = $this->getItemsFromResponse($response); - return $rows; + return $this->populate($rows); } /** @@ -299,9 +320,15 @@ public function all($db = null) */ public function one($db = null) { + if (in_array($this->using, [self::USING_QUERY, self::USING_SCAN])) { + $this->limit(1); + } $response = $this->execute($db); $rows = $this->getItemsFromResponse($response); - return $rows; + if (empty($rows)) { + return false; + } + return reset($rows) ?: null; } /** diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 11b02fb..e6de1cc 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -10,6 +10,7 @@ use yii\base\Object; use yii\helpers\ArrayHelper; use yii\base\InvalidParamException; +use InvalidArgumentException; /** * QueryBuilder builds an elasticsearch query based on the specification given @@ -66,6 +67,8 @@ public function __construct(Connection $connection, array $config = []) */ public function build(Query $query) { + $query = $query->prepare($this); + // Validate query if (empty($query->from)) { throw new InvalidArgumentException('Table name not set'); @@ -84,7 +87,7 @@ public function build(Query $query) $query->using = Query::USING_BATCH_GET_ITEM; } } - + $call = 'build' . $query->using; // Call builder @@ -129,7 +132,7 @@ public function buildBatchGetItem(Query $query) throw new InvalidArgumentException($query->using . ' is not support parameter beside where and select clause.'); } - + return $this->batchGetItem( $query->from, $this->buildWhereGetItem($query), @@ -188,7 +191,7 @@ public function buildQuery(Query $query) * @return array Array of projection options. */ public function buildProjection(Query $query) - { + { if (!empty($query->select)) { return is_array($query->select) ? [ 'ProjectionExpression' => implode(', ', $query->select) @@ -217,8 +220,8 @@ public function buildWhereGetItem(Query $query) // remain array type // supported example: ['a' => 'b'], ['IN', 'a', 'b'], [['IN', 'a', 'b']] // and combination of it like [['IN', 'a', 'b'], 'c' => 'd'] - - $new_where = []; + + $newWhere = []; foreach ($query->where as $key => $value) { if (is_string($value) || is_numeric($value)) { if (ArrayHelper::isIndexed($query->where)) { @@ -231,30 +234,30 @@ public function buildWhereGetItem(Query $query) } if (is_array($query->where[2])) { - $new_where[$query->where[1]] = $query->where[2]; + $newWhere[$query->where[1]] = $query->where[2]; } else { - $new_where[$query->where[1]] = [$query->where[2]]; + $newWhere[$query->where[1]] = [$query->where[2]]; } break; } else { - if (isset($new_where[$key])) { - $new_where[$key] = array_merge($new_where[$key], [$value]); + if (isset($newWhere[$key])) { + $newWhere[$key] = array_merge($newWhere[$key], [$value]); } else { - $new_where[$key] = [$value]; + $newWhere[$key] = [$value]; } } } else { // else just array type, perhaps if (ArrayHelper::isIndexed($value)) { - if (isset($new_where[$key])) { - $new_where[$key] = array_merge($new_where[$key], $value); + if (isset($newWhere[$key])) { + $newWhere[$key] = array_merge($newWhere[$key], $value); } else { - $new_where[$key] = $value; + $newWhere[$key] = $value; } } } } - - return $new_where; + + return $newWhere; } /** @@ -636,7 +639,7 @@ public function buildOptions(Query $query) } $options['ReturnConsumedCapacity'] = $query->returnConsumedCapacity; } - + return array_merge($options, $this->buildProjection($query)); } @@ -819,13 +822,13 @@ public function buildGetItemCompositeKey(array $keySchema, array $keys) public function batchGetItem($table, array $keys, array $options = [], array $requestItemOptions = []) { $name = 'BatchGetItem'; - + $tableArgument = [ $table => array_merge([ 'Keys' => $this->buildBatchKeyArgument($table, $keys), ], $requestItemOptions) ]; - + $argument = array_merge(['RequestItems' => $tableArgument], $options); return [$name, $argument]; } @@ -840,7 +843,7 @@ private function buildBatchKeyArgument($table, $keys) { $tableDescription = $this->db->createCommand()->describeTable($table)->execute(); $keySchema = $tableDescription['Table']['KeySchema']; - + if (ArrayHelper::isIndexed($keys)) { $isScalar = is_string($keys[0]) || is_numeric($keys[0]); if ($isScalar) { diff --git a/test/ActiveDataProviderTest.php b/test/ActiveDataProviderTest.php index 60bf62a..45c56ec 100644 --- a/test/ActiveDataProviderTest.php +++ b/test/ActiveDataProviderTest.php @@ -12,50 +12,40 @@ protected function setUp() public function testWithoutPagination() { - $command = $this->createCommand(); - - list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); - - $faker = Faker\Factory::create(); - - $values = array_map(function ($id) use ($faker, $fieldName1) { - return [ - $fieldName1 => $faker->uuid, - 'Field2' => $id, - ]; - }, range(1, 25)); - - $command->batchPutItem($tableName, $values)->execute(); + $faker = \Faker\Factory::create(); + foreach (range(1, 10) as $id) { + $model = new test\data\Customer([ + 'id' => $id, + 'name' => $faker->firstNameMale, + 'age' => ($id % 2) + 1, + ]); + $model->save(false); + } $dataProvider = new ActiveDataProvider([ - 'query' => test\data\Customer::find(), + 'query' => test\data\Customer::find()->where(['age' => 2]), 'pagination' => false, ]); - $this->assertCount(25, $dataProvider->getModels()); + $this->assertCount(5, $dataProvider->getModels()); $this->assertFalse($dataProvider->getPagination()); } public function testPagination() { - $command = $this->createCommand(); - - list($tableName, $fieldName1) = $this->createSimpleTableWithHashKey(); - - $faker = Faker\Factory::create(); - - $values = array_map(function ($id) use ($faker, $fieldName1) { - return [ - $fieldName1 => $faker->uuid, - 'Field2' => $id, - ]; - }, range(1, 25)); - - $command->batchPutItem($tableName, $values)->execute(); + $faker = \Faker\Factory::create(); + foreach (range(1, 50) as $id) { + $model = new test\data\Customer([ + 'id' => $id, + 'name' => $faker->firstNameMale, + 'age' => $id, + ]); + $model->save(false); + } $dataProvider1 = new ActiveDataProvider([ - 'query' => test\data\Customer::find(), + 'query' => test\data\Customer::find()->where(['>', 'age', 15]), 'pagination' => [ 'pageSize' => 5, ] diff --git a/test/ActiveRecordTest.php b/test/ActiveRecordTest.php index 0a5d3eb..f11a789 100644 --- a/test/ActiveRecordTest.php +++ b/test/ActiveRecordTest.php @@ -34,6 +34,13 @@ public function testInsertAndFindOne() { $this->assertTrue($objectToInsert->save(false)); $this->assertEquals(1, $this->getTableItemCount(\test\data\Customer::tableName())); + $objectFromFind = \test\data\Customer::findOne(['id' => $id]); + + /* @var $objectFromFind data\Customer */ + $this->assertNotNull($objectFromFind); + $this->assertEquals($id, $objectFromFind->id); + $this->assertEquals($objectToInsert->name, $objectFromFind->name); + $this->assertEquals($objectToInsert->kids, $objectFromFind->kids); } public function testInsertAndFindAll() { @@ -61,6 +68,7 @@ public function testInsertAndFindAll() { ]; $this->assertTrue($objectToInsert1->save(false)); + $this->assertEquals(1, $this->getTableItemCount(\test\data\Customer::tableName())); $objectsFromFind = \test\data\Customer::findAll(['id' => [$id1]]); diff --git a/test/ConnectionTest.php b/test/ConnectionTest.php index 3840bb0..81a8154 100644 --- a/test/ConnectionTest.php +++ b/test/ConnectionTest.php @@ -3,6 +3,7 @@ class ConnectionTest extends TestCase { public function testConnectionClient() { + $faker = Faker\Factory::create(); $component = new \UrbanIndo\Yii2\DynamoDb\Connection([ 'config' => [ 'credentials' => [ @@ -16,7 +17,7 @@ public function testConnectionClient() { ]); $client = $component->getClient(); $command = $client->getCommand('CreateTable', [ - 'TableName' => 'Testing', + 'TableName' => $faker->uuid, 'KeySchema' => [ [ 'AttributeName' => 'Test1', diff --git a/test/TestCase.php b/test/TestCase.php index 0fe833b..64be437 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -10,6 +10,23 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase */ public function getConnection() { + $this->mockWebApplication([ + 'components' => [ + 'dynamodb' => [ + /* @var $dynamodb \UrbanIndo\Yii2\DynamoDb\Connection */ + 'class' => '\UrbanIndo\Yii2\DynamoDb\Connection', + 'config' => [ + 'credentials' => [ + 'key' => 'AKIA', + 'secret' => '1234567890', + ], + 'region' => 'ap-southeast-1', + 'version' => 'latest', + 'endpoint' => DYNAMODB_URL, + ] + ] + ] + ]); return Yii::$app->dynamodb; } diff --git a/test/bootstrap.php b/test/bootstrap.php index d61c5dd..fe97bf1 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -17,19 +17,4 @@ $application = new \yii\console\Application([ 'id' => 'Yii2 DynamoDB Test', 'basePath' => dirname(__FILE__), - 'components' => [ - 'dynamodb' => [ - /* @var $dynamodb \UrbanIndo\Yii2\DynamoDb\Connection */ - 'class' => '\UrbanIndo\Yii2\DynamoDb\Connection', - 'config' => [ - 'credentials' => [ - 'key' => 'AKIA', - 'secret' => '1234567890', - ], - 'region' => 'ap-southeast-1', - 'version' => 'latest', - 'endpoint' => DYNAMODB_URL, - ] - ] - ] ]); diff --git a/test/data/Customer.php b/test/data/Customer.php index 92e4387..4b4159f 100644 --- a/test/data/Customer.php +++ b/test/data/Customer.php @@ -31,6 +31,7 @@ public function attributes() { 'contacts', 'prices', 'kids', + 'age', ]; } } From 47366f8a7e3cf9c40d5e510c255d25018bc41911 Mon Sep 17 00:00:00 2001 From: Setyo Legowo Date: Thu, 31 Dec 2015 13:13:52 +0700 Subject: [PATCH 16/23] Update QueryBuilder and add some new testcases. --- README.md | 21 +++- src/Query.php | 6 +- src/QueryBuilder.php | 216 ++++++++++++++++++++++++++++---- test/ActiveDataProviderTest.php | 26 ++-- test/ActiveRecordTest.php | 118 ++++++++++++++--- test/QueryBuilderTest.php | 8 +- 6 files changed, 341 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 4f52ece..7ad05bc 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ After the installation, sets the `dynamodb` component in the config. return [ // ... 'components' => [ - // + // ... 'dynamodb' => [ 'class' => 'UrbanIndo\Yii2\DynamoDb\Connection', 'config' => [ @@ -58,3 +58,22 @@ return [ ]; ``` +## Limitation + +Because DynamoDB have different behavior with MySQL in general, there are several +limitations or behavior change applied. There are several method to get data from +DynamoDB: __GetItem__, __BatchGetItem__, __Scan__, and __Query__. + +1. We have tried to implement automatic method to acquire model from Query. You have +to assign method explicitly when you want to force method in use. +2. Not yet support attribute name aliasing (In MySQL known as field aliasing). +3. When using __Query__ method, in where condition should using just key attributes. +In next roll out will add filtering with non key attributes. +4. To make pagination, we recommend using Query method when want to filter result. +If you use filtering with non key attribute, it is possible result the model(s) less +than desired limit value. +5. `indexBy` and `orderBy` cannot use by attribute string value or callable parameter. +This will use as string value and assign to `IndexName` parameter in DynamoDB. To +use sorting, it will use __QUERY__ method and `orderBy` parameter should be either +`['myIndex' => 'ASC']` or `['myIndex', 'ASC']`. +6. Not support NULL type attribute. diff --git a/src/Query.php b/src/Query.php index 6b5eb8f..26d2a82 100644 --- a/src/Query.php +++ b/src/Query.php @@ -202,7 +202,7 @@ public function withoutConsistentRead() *
        • INDEXES
        • *
        • TOTAL
        • *
        • NONE
        • - *
        + *
      . * @return static */ public function setConsumedCapacity($setConsumedCapacity) @@ -229,7 +229,7 @@ public function getItemsFromResponse($response) $row = Marshaler::unmarshalItem($response['Item']); $rows = [$row]; } - + $storedResponse = self::extractStoredResponseData($this->storeResponseData, $response); if (!empty($storedResponse)) { $rows = array_map(function ($row) use ($storedResponse) { @@ -260,7 +260,7 @@ private static function extractStoredResponseData($responseKeys, $response) } return $return; } - + /** * Prepares for building SQL. * This method is called by [[QueryBuilder]] when it starts to build SQL from a query object. diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index e6de1cc..ee59127 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -45,7 +45,7 @@ class QueryBuilder extends Object 'ATTRIBUTE_TYPE' => 'buildFunctionCondition2Param', 'BEGINS_WITH' => 'buildFunctionCondition2Param', 'CONTAINS' => 'buildFunctionCondition2Param', - 'SIZE' => 'buildFunctionCondition', + // 'SIZE' => 'buildFunctionCondition', ]; /** @@ -68,17 +68,20 @@ public function __construct(Connection $connection, array $config = []) public function build(Query $query) { $query = $query->prepare($this); - + // Validate query if (empty($query->from)) { throw new InvalidArgumentException('Table name not set'); } if ($query->using == Query::USING_AUTO) { + $keyCondition = $this->isConditionMatchKeySchema($query); + $supportBatchGetItem = $this->isOperatorSupportBatchGetItem($query->where); if (empty($query->where) || !empty($query->indexBy) || !empty($query->limit) - || !empty($query->offset) || !empty($query->orderBy)) { - // TODO Choose appropiate method - if (empty($query->orderBy)) { + || !empty($query->offset) || !empty($query->orderBy) + || $keyCondition != 1 || !$supportBatchGetItem) { + // TODO WARNING AWS SDK not support operator beside '=' for key + if (empty($query->orderBy) && ($keyCondition > 0) && $supportBatchGetItem) { $query->using = Query::USING_QUERY; } else { $query->using = Query::USING_SCAN; @@ -87,13 +90,153 @@ public function build(Query $query) $query->using = Query::USING_BATCH_GET_ITEM; } } - + $call = 'build' . $query->using; // Call builder return $this->{$call}($query); } + /** + * Find out where all operator condition is able to support BatchGetItem or not. + * @param array $where Where condition string. + * @return boolean True when operator support BatchGetItem. + */ + public function isOperatorSupportBatchGetItem($where) + { + if (empty($where)) { + return false; + } + if (is_string($where) || is_numeric($where)) { + return true; + } + // Array type remaining + if (ArrayHelper::isIndexed($where)) { + foreach ($where as $whereIndexedElement) { + if (is_array($whereIndexedElement)) { + if (!$this->isOperatorSupportBatchGetItem($whereIndexedElement)) { + return false; + } + } else { + if (in_array($whereIndexedElement, ['IN', '='])) { + return true; + } + } + } + } else { // associative array remaining + foreach ($where as $key => $whereElement) { + if (is_array($whereElement)) { + if (!$this->isOperatorSupportBatchGetItem($whereElement)) { + return false; + } + } + } + } + + return false; + } + + /** + * To check conditoin which contain any key. + * @param Query $query Object from which the query will be generated. + * @return integer Return 0 if no match with key, 1 if match only key, + * 2 if contain both key and non key. + */ + public function isConditionMatchKeySchema(Query $query) + { + if (is_array($query->where)) { + foreach ($this->getKeySchema($query) as $keySchemaElement) { + $keySchema[] = $keySchemaElement['AttributeName']; + } + $dummy = false; + if (!is_array(current($query->where))) { + return $this->searchAttrInArray([$query->where], $keySchema, $dummy); + } else { + return $this->searchAttrInArray($query->where, $keySchema, $dummy); + } + } + + return 0; + } + + /** + * Search keys in where schema compare within key schema. + * @param array $where Where schema. + * @param array $keySchema Key schema. + * @param boolean $hasFoundKey Has found key or did not. + * @return integer Return 0 if no match with key, 1 if match only key, + * 2 if contain both key and non key. + */ + public function searchAttrInArray($where, $keySchema, &$hasFoundKey) + { + foreach ($where as $whereElement) { + if (ArrayHelper::isIndexed($whereElement)) { + if (sizeof($whereElement) < 2) { + continue; + } + if (!in_array($whereElement[1], $keySchema)) { + if ($hasFoundKey) { + return 2; + } + } else { + $hasFoundKey = true; + } + } else { + foreach ($whereElement as $attr => $val) { + if (is_array($val)) { + $tmp = $this->searchAttrInArray($val, $keySchema, $hasFoundKey); + if ($hasFoundKey) { + return $tmp; + } + } + if (!in_array($attr, $keySchema)) { + if ($hasFoundKey) { + return 2; + } + } else { + $hasFoundKey = true; + } + } + } + } + + return $hasFoundKey ? 1 : 0; + } + + /** + * Gather key schema + * @param Query $query Object from which the query will be generated. + * @return array Key schema + * @throws InvalidArgumentException IndexName not exist in table description. + */ + public function getKeySchema(Query $query) + { + $tableDescription = $this->db->createCommand()->describeTable($query->from)->execute(); + $options = $this->buildOptions($query); + + if (isset($options['IndexName'])) { + if (isset($tableDescription['Table']['LocalSecondaryIndexes'])) { + foreach ($tableDescription['Table']['LocalSecondaryIndexes'] as $row) { + if ($row['IndexName'] == $options['IndexName']) { + return $row['KeySchema']; + } + } + } + if (isset($tableDescription['Table']['GlobalSecondaryIndexes'])) { + foreach ($tableDescription['Table']['GlobalSecondaryIndexes'] as $row) { + if ($row['IndexName'] == $options['IndexName']) { + return $row['KeySchema']; + } + } + } + // Throw because not found in both indexes + throw new InvalidArgumentException('Index is set but not found in table description.'); + } + + // Use global key schema instead + return $tableDescription['Table']['KeySchema']; + } + /** * Generates DynamoDB Query from a [[Query]] object use GetItem method * @param Query $query Object from which the query will be generated. @@ -132,7 +275,7 @@ public function buildBatchGetItem(Query $query) throw new InvalidArgumentException($query->using . ' is not support parameter beside where and select clause.'); } - + return $this->batchGetItem( $query->from, $this->buildWhereGetItem($query), @@ -150,7 +293,7 @@ public function buildBatchGetItem(Query $query) public function buildScan(Query $query) { if (!empty($query->orderBy)) { - throw new Exception($query->using . ' method cannot set in order.'); + throw new Exception($query->using . ' method cannot use ORDER clause.'); } $options = $this->buildOptions($query); @@ -179,6 +322,7 @@ public function buildQuery(Query $query) // TODO Seperate FilterExpression and KeyConditionExpression // For now, change all condition to KeyConditionExpression (assumed // all where condition use key attributes) + $options['KeyConditionExpression'] = $options['FilterExpression']; unset($options['FilterExpression']); } @@ -191,7 +335,7 @@ public function buildQuery(Query $query) * @return array Array of projection options. */ public function buildProjection(Query $query) - { + { if (!empty($query->select)) { return is_array($query->select) ? [ 'ProjectionExpression' => implode(', ', $query->select) @@ -220,7 +364,7 @@ public function buildWhereGetItem(Query $query) // remain array type // supported example: ['a' => 'b'], ['IN', 'a', 'b'], [['IN', 'a', 'b']] // and combination of it like [['IN', 'a', 'b'], 'c' => 'd'] - + $newWhere = []; foreach ($query->where as $key => $value) { if (is_string($value) || is_numeric($value)) { @@ -229,8 +373,11 @@ public function buildWhereGetItem(Query $query) throw new InvalidParamException($query->using . " not support operator '" . $query->where[0] . "'."); } - if (sizeof($query->where) != 3) { - throw new InvalidParamException('The WHERE element require 3 elements.'); + if (sizeof($query->where) == 2) { + $newWhere[$query->where[0]] = $query->where[1]; + break; + } elseif (sizeof($query->where) != 3) { + throw new InvalidParamException('The WHERE element require 2 or 3 elements.'); } if (is_array($query->where[2])) { @@ -256,7 +403,7 @@ public function buildWhereGetItem(Query $query) } } } - + return $newWhere; } @@ -293,8 +440,26 @@ public function paramToExpressionAttributeValues($params) $params[$i] = ['N' => $value]; } elseif (is_string($value)) { $params[$i] = ['S' => $value]; + } elseif (is_bool($value)) { + $params[$i] = ['BOOL' => $value]; + } elseif (is_array($value)) { + $subValue = current($value); + if (is_array($subValue)) { + if (ArrayHelper::isIndexed($value)) { + $params[$i] = ['L' => $value]; + } else { + $params[$i] = ['M' => $value]; + } + continue; + } elseif (is_int($subValue)) { + $params[$i] = ['NS' => $value]; + } elseif (is_string($subValue)) { + $params[$i] = ['SS' => $value]; + } else { + $params[$i] = ['BS' => $value]; + } } else { - throw new Exception('Unsupported value type.'); + $params[$i] = ['B' => $value]; } } return $params; @@ -309,6 +474,8 @@ public function paramToExpressionAttributeValues($params) */ public function buildCondition($condition, &$params) { + // TODO Convert key name which conflict with reserved key word + // http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html if (!is_array($condition)) { return (string) $condition; } elseif (empty($condition)) { @@ -549,8 +716,7 @@ public function buildFunctionCondition2Param($func, $operands, &$params) if (count($operands) !== 2) { throw new InvalidParamException("Function '$func' requires exactly two operands."); } - $phName1 = self::PARAM_PREFIX . count($params); - $params[$phName1] = $operands[0]; + $phName1 = $operands[0]; $phName2 = self::PARAM_PREFIX . count($params); $params[$phName2] = $operands[1]; $func = strtolower($func); @@ -614,13 +780,21 @@ public function buildOptions(Query $query) } if (!empty($query->indexBy)) { if (is_callable($query->indexBy)) { - throw new InvalidArgumentException('Cannot combine callable parameter.'); + throw new InvalidArgumentException('Cannot using callable parameter.'); } $options = array_merge($options, ['IndexName' => $query->indexBy]); } if (!empty($query->limit)) { $options = array_merge($options, ['Limit' => (int) $query->limit]); } + if (!empty($query->offset)) { + if (!is_array($query->offset)) { + throw new InvalidArgumentException( + 'Missed associative array of keys mapping.' + ); + } + $options = array_merge($options, ['ExclusiveStartKey' => $query->offset]); + } if (!is_null($query->consistentRead)) { if (!is_bool($query->consistentRead)) { throw new InvalidArgumentException( @@ -639,7 +813,7 @@ public function buildOptions(Query $query) } $options['ReturnConsumedCapacity'] = $query->returnConsumedCapacity; } - + return array_merge($options, $this->buildProjection($query)); } @@ -822,13 +996,13 @@ public function buildGetItemCompositeKey(array $keySchema, array $keys) public function batchGetItem($table, array $keys, array $options = [], array $requestItemOptions = []) { $name = 'BatchGetItem'; - + $tableArgument = [ $table => array_merge([ 'Keys' => $this->buildBatchKeyArgument($table, $keys), ], $requestItemOptions) ]; - + $argument = array_merge(['RequestItems' => $tableArgument], $options); return [$name, $argument]; } @@ -843,7 +1017,7 @@ private function buildBatchKeyArgument($table, $keys) { $tableDescription = $this->db->createCommand()->describeTable($table)->execute(); $keySchema = $tableDescription['Table']['KeySchema']; - + if (ArrayHelper::isIndexed($keys)) { $isScalar = is_string($keys[0]) || is_numeric($keys[0]); if ($isScalar) { diff --git a/test/ActiveDataProviderTest.php b/test/ActiveDataProviderTest.php index 45c56ec..cbc98fe 100644 --- a/test/ActiveDataProviderTest.php +++ b/test/ActiveDataProviderTest.php @@ -9,7 +9,7 @@ protected function setUp() parent::setUp(); $this->createCustomersTable(); } - + public function testWithoutPagination() { $faker = \Faker\Factory::create(); @@ -21,17 +21,17 @@ public function testWithoutPagination() ]); $model->save(false); } - + $dataProvider = new ActiveDataProvider([ 'query' => test\data\Customer::find()->where(['age' => 2]), 'pagination' => false, ]); - + $this->assertCount(5, $dataProvider->getModels()); - + $this->assertFalse($dataProvider->getPagination()); } - + public function testPagination() { $faker = \Faker\Factory::create(); @@ -43,19 +43,23 @@ public function testPagination() ]); $model->save(false); } - + + // Pagination using filter non key attribute would return less than + // desired limit. $dataProvider1 = new ActiveDataProvider([ - 'query' => test\data\Customer::find()->where(['>', 'age', 15]), + 'query' => test\data\Customer::find(), 'pagination' => [ 'pageSize' => 5, ] ]); - + $this->assertCount(5, $dataProvider1->getModels()); - + $pagination1 = $dataProvider1->getPagination(); $this->assertNotNull($pagination1->getNextLastKey()); - + + // Pagination using filter non key attribute would return less than + // desired limit. $dataProvider2 = new ActiveDataProvider([ 'query' => test\data\Customer::find(), 'pagination' => [ @@ -63,7 +67,7 @@ public function testPagination() 'pageSize' => 5, ] ]); - + $this->assertCount(5, $dataProvider2->getModels()); } } diff --git a/test/ActiveRecordTest.php b/test/ActiveRecordTest.php index f11a789..d2878df 100644 --- a/test/ActiveRecordTest.php +++ b/test/ActiveRecordTest.php @@ -1,14 +1,14 @@ createCustomersTable(); } - + public function testInsertAndFindOne() { - + $this->assertEquals(0, $this->getTableItemCount(\test\data\Customer::tableName())); $objectToInsert = new \test\data\Customer(); $id = (int) \Faker\Provider\Base::randomNumber(5); @@ -31,18 +31,108 @@ public function testInsertAndFindOne() { 'Billy', 'Charlie', ]; - + $this->assertTrue($objectToInsert->save(false)); $this->assertEquals(1, $this->getTableItemCount(\test\data\Customer::tableName())); $objectFromFind = \test\data\Customer::findOne(['id' => $id]); - + /* @var $objectFromFind data\Customer */ $this->assertNotNull($objectFromFind); $this->assertEquals($id, $objectFromFind->id); $this->assertEquals($objectToInsert->name, $objectFromFind->name); $this->assertEquals($objectToInsert->kids, $objectFromFind->kids); } - + + public function testCondition() + { + $objectToInsert = new \test\data\Customer(); + $id1 = (int) \Faker\Provider\Base::randomNumber(5); + $faker = \Faker\Factory::create(); + $objectToInsert->id = $id1; + $objectToInsert->name = $faker->name; + $objectToInsert->contacts = [ + 'telephone1' => 123456, + 'telephone2' => 345678, + 'telephone3' => 345678, + ]; + $objectToInsert->prices = [ + 1000000, + 1000000, + 1000000, + 1000000 + ]; + $objectToInsert->kids = [ + 'Alice', + 'Billy', + 'Charlie', + ]; + + $this->assertTrue($objectToInsert->save(false)); + + $objectToInsert2 = new \test\data\Customer(); + $id2 = $id1 + 1; + $faker = \Faker\Factory::create(); + $objectToInsert2->id = $id2; + $objectToInsert2->name = $faker->name; + $objectToInsert2->contacts = [ + 'telephone1' => 123456, + 'telephone2' => 345678, + 'telephone3' => 345678, + ]; + $objectToInsert2->prices = [ + 1000000, + 1000000, + 1000000, + 1000000 + ]; + $objectToInsert2->kids = [ + 'Alice', + 'Ari', + 'Charlie', + ]; + + $this->assertTrue($objectToInsert2->save(false)); + + $objectToInsert3 = new \test\data\Customer(); + $id3 = $id2 + 1; + $faker = \Faker\Factory::create(); + $objectToInsert3->id = $id3; + $objectToInsert3->name = $faker->name; + $objectToInsert3->contacts = [ + 'telephone1' => 123456, + 'telephone2' => 345678, + 'telephone3' => 345678, + ]; + $objectToInsert3->prices = [ + 1000000, + 1000000, + 1000000, + 1000000 + ]; + $objectToInsert3->kids = [ + 'Alice', + 'Ari', + 'Angle', + ]; + + $this->assertTrue($objectToInsert3->save(false)); + + $objectsFromFind = \test\data\Customer::find()->where(['id' => [$id1]])->all(); + $this->assertEquals(1, count($objectsFromFind)); + $objectsFromFind = \test\data\Customer::find()->where(['id' => $id1]) + ->orWhere(['id' => $id2])->all(); + $this->assertEquals(2, count($objectsFromFind)); + $objectsFromFind = \test\data\Customer::find()->where(['>=', 'id', $id1])->all(); + $this->assertEquals(3, count($objectsFromFind)); + $objectsFromFind = \test\data\Customer::find()->where(['IN', 'id', [$id1, $id2]])->all(); + $this->assertEquals(2, count($objectsFromFind)); + $objectsFromFind = \test\data\Customer::find()->limit(2)->all(); + $this->assertEquals(2, count($objectsFromFind)); + $objectsFromFind = \test\data\Customer::find()->where(['>', 'id', $id1])->all(); + $objectsFromFind = \test\data\Customer::find()->where(['CONTAINS', 'kids', 'Angle'])->all(); + $this->assertEquals(1, count($objectsFromFind)); + } + public function testInsertAndFindAll() { $id1 = (int) \Faker\Provider\Base::randomNumber(5); @@ -66,15 +156,15 @@ public function testInsertAndFindAll() { 'Billy', 'Charlie', ]; - + $this->assertTrue($objectToInsert1->save(false)); $this->assertEquals(1, $this->getTableItemCount(\test\data\Customer::tableName())); - + $objectsFromFind = \test\data\Customer::findAll(['id' => [$id1]]); - + /* @var $objectFromFind data\Customer */ $this->assertEquals(1, count($objectsFromFind)); - + $id2 = (int) \Faker\Provider\Base::randomNumber(5); $objectToInsert2 = new \test\data\Customer(); $objectToInsert2->id = $id2; @@ -95,14 +185,14 @@ public function testInsertAndFindAll() { 'Billy', 'Charlie', ]; - + $this->assertTrue($objectToInsert2->save(false)); - + $objectsFromFind2 = \test\data\Customer::findAll(['id' => [$id1, $id2]]); - + /* @var $objectFromFind data\Customer */ $this->assertEquals(2, count($objectsFromFind2)); - + $this->assertTrue($objectsFromFind2[0]->id = $id1 || $objectsFromFind2[0]->id = $id2); } } diff --git a/test/QueryBuilderTest.php b/test/QueryBuilderTest.php index e49748c..9592828 100644 --- a/test/QueryBuilderTest.php +++ b/test/QueryBuilderTest.php @@ -440,8 +440,8 @@ public function testBuildScanWithComplicatedCondition() unset($expected['ExpressionAttributeValues'][':dqp2']); $this->assertEquals($expected, $qb->build($query3)[1]); - $expected['FilterExpression'] = '(name=:dqp0) AND (begins_with (:dqp1, :dqp2))'; - $expected['ExpressionAttributeValues'][':dqp2'] = ['S' => $id1]; + $expected['FilterExpression'] = '(name=:dqp0) AND (begins_with (id, :dqp1))'; + $expected['ExpressionAttributeValues'][':dqp1'] = ['S' => $id1]; $this->assertEquals($expected, $qb->build($query4)[1]); } @@ -583,8 +583,8 @@ public function testBuildQueryWithComplicatedCondition() unset($expected['ExpressionAttributeValues'][':dqp2']); $this->assertEquals($expected, $qb->build($query3)[1]); - $expected['KeyConditionExpression'] = '(name=:dqp0) AND (begins_with (:dqp1, :dqp2))'; - $expected['ExpressionAttributeValues'][':dqp2'] = ['S' => $id1]; + $expected['KeyConditionExpression'] = '(name=:dqp0) AND (begins_with (id, :dqp1))'; + $expected['ExpressionAttributeValues'][':dqp1'] = ['S' => $id1]; $this->assertEquals($expected, $qb->build($query4)[1]); } } From 29fe059736942f7ee41b0f74f913d00122661ad1 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Sat, 2 Jan 2016 00:10:53 +0700 Subject: [PATCH 17/23] Added tests for Pagination with multiple key. --- build.xml | 2 +- src/Pagination.php | 6 +++--- test/PaginationTest.php | 29 ++++++++++++++++++++++++----- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/build.xml b/build.xml index 0577870..eeb427f 100644 --- a/build.xml +++ b/build.xml @@ -51,7 +51,7 @@ - + diff --git a/src/Pagination.php b/src/Pagination.php index 0df8695..631943f 100644 --- a/src/Pagination.php +++ b/src/Pagination.php @@ -123,13 +123,13 @@ public function getLimit() /** * Stores the current last key. - * @var string + * @var string|string[] */ private $_lastKey; /** * Returns the last key evaluated in the DynamoDB. - * @return string + * @return string|string[] */ public function getLastKey() { @@ -141,7 +141,7 @@ public function getLastKey() /** * Sets the current last key. - * @param string $value The last key that was evaluated by DynamoDB. + * @param string|string[] $value The last key that was evaluated by DynamoDB. * @return void */ public function setLastKey($value) diff --git a/test/PaginationTest.php b/test/PaginationTest.php index ea6f6f7..e003ea7 100644 --- a/test/PaginationTest.php +++ b/test/PaginationTest.php @@ -39,13 +39,23 @@ public function dataProviderCreateUrl() 5, '/index.php?r=item%2Flist&last-key=2&per-page=5', ], + [ + ['a', 'f'], + null, + '/index.php?r=item%2Flist&last-key%5B0%5D=a&last-key%5B1%5D=f', + ], + [ + ['a', 'f'], + 5, + '/index.php?r=item%2Flist&last-key%5B0%5D=a&last-key%5B1%5D=f&per-page=5', + ], ]; } /** * @dataProvider dataProviderCreateUrl * - * @param string $lastKey + * @param string|string[] $lastKey * @param integer $pageSize * @param string $expectedUrl */ @@ -90,16 +100,25 @@ public function dataProviderGetLinks() 'next' => '/index.php?r=item%2Flist&last-key=10&per-page=10', ] ], + [ + ['a', 'b'], + ['a', 'f'], + 10, + [ + 'self' => '/index.php?r=item%2Flist&last-key%5B0%5D=a&last-key%5B1%5D=b&per-page=10', + 'next' => '/index.php?r=item%2Flist&last-key%5B0%5D=a&last-key%5B1%5D=f&per-page=10', + ] + ] ]; } /** * @dataProvider dataProviderGetLinks * - * @param string $currentLastKey The current last key. - * @param string $nextLastKey The next last key. - * @param integer $pageSize The page size to show. - * @param array $links The links resulted + * @param string|string[] $currentLastKey The current last key. + * @param string|string[] $nextLastKey The next last key. + * @param integer $pageSize The page size to show. + * @param array $links The links resulted */ public function testGetLinks($currentLastKey, $nextLastKey, $pageSize, $links) { From 7e14b3e9077186837f8b94ac9b727220b59ebab4 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Sat, 2 Jan 2016 10:22:01 +0700 Subject: [PATCH 18/23] Add LinkPager class. --- src/LinkPager.php | 121 ++++++++++++++++++++++++++++++++++++++++ src/Pagination.php | 9 ++- src/TableSchema.php | 21 ------- test/LinkPagerTest.php | 39 +++++++++++++ test/PaginationTest.php | 4 ++ 5 files changed, 170 insertions(+), 24 deletions(-) create mode 100644 src/LinkPager.php delete mode 100644 src/TableSchema.php create mode 100644 test/LinkPagerTest.php diff --git a/src/LinkPager.php b/src/LinkPager.php new file mode 100644 index 0000000..8b91ed5 --- /dev/null +++ b/src/LinkPager.php @@ -0,0 +1,121 @@ + + */ + +namespace UrbanIndo\Yii2\DynamoDb; + +use yii\base\InvalidConfigException; +use yii\helpers\Html; +use yii\helpers\ArrayHelper; +use \yii\web\Link; + +/** + * LinkPager extends \yii\widgets\LinkPager to displays a list of hyperlinks for + * DynamoDB pagination. + * + * LinkPager should only be used with \UrbanIndo\Yii2\DynamoDb\Pagination object + * that provides the current DynamoDb pagination key and the next pagination key. + * + * @author Petra Barus + */ +class LinkPager extends \yii\widgets\LinkPager +{ + /** + * @var Pagination the pagination object that this pager is associated with. + * You must set this property in order to make LinkPager work. This has to + * be instance of \UrbanIndo\Yii2\DynamoDb\Pagination class. + */ + public $pagination; + + /** + * @var string|boolean the text label for the "first" page button. Note that this will NOT be HTML-encoded. + * If it's specified as true, page number will be used as label. + * Default is false that means the "first" page button will not be displayed. + */ + public $firstPageLabel = true; + + /** + * @var string|boolean the text label for the "current" page button. Note that this will NOT be HTML-encoded. + * If it's specified as true, page number will be used as label. + * Default is false that means the "current" page button will not be displayed. + */ + public $currentPageLabel = false; + + /** + * Initializes the pager. + * @return void + * @throws InvalidConfigException When the pagination is not instance of + * \UrbanIndo\Yii2\DynamoDb\Pagination class. + */ + public function init() + { + if (!$this->pagination instanceof Pagination) { + throw new InvalidConfigException( + 'The "pagination" has to be instance of \UrbanIndo\Yii2\DynamoDb\Pagination.' + ); + } + } + + /** + * Renders the page buttons. + * @return string the rendering result + */ + protected function renderPageButtons() + { + $buttons = []; + $pagination = $this->pagination; + + $links = $pagination->getLinks(); + + $currentUrl = ArrayHelper::getValue($links, Link::REL_SELF); + + if ($this->firstPageLabel !== false && + !empty($url = ArrayHelper::getValue($links, Pagination::LINK_FIRST)) && + $url != $currentUrl) { + $buttons[] = $this->renderUrlPageButton($this->firstPageLabel, $url, $this->firstPageCssClass, false); + } + + if ($this->prevPageLabel !== false) { + $buttons[] = $this->renderUrlPageButton( + $this->prevPageLabel, + 'javascript:history.back()', + $this->prevPageCssClass, + false + ); + } + + if ($this->currentPageLabel !== false) { + $buttons[] = $this->renderUrlPageButton($this->currentPageLabel, $currentUrl, null, true); + } + + if ($this->nextPageLabel !== false && !empty($url = ArrayHelper::getValue($links, Pagination::LINK_NEXT))) { + $buttons[] = $this->renderUrlPageButton($this->nextPageLabel, $url, $this->nextPageCssClass, false); + } + + return Html::tag('ul', implode("\n", $buttons), $this->options); + } + + /** + * Renders a page button using URL. + * You may override this method to customize the generation of page buttons. + * @param string $label The text label for the button. + * @param string $url The URL of the button. + * @param string $class The CSS class for the page button. + * @param boolean $disabled Whether to disable the button. + * @return string the rendering result + */ + protected function renderUrlPageButton($label, $url, $class, $disabled) + { + $options = ['class' => empty($class) ? $this->pageCssClass : $class]; + if ($disabled) { + Html::addCssClass($options, $this->disabledPageCssClass); + $options['data-href'] = $url; + return Html::tag('li', Html::tag('span', $label), $options); + } + $linkOptions = $this->linkOptions; + + return Html::tag('li', Html::a($label, $url, $linkOptions), $options); + } +} diff --git a/src/Pagination.php b/src/Pagination.php index 631943f..c3e01f6 100644 --- a/src/Pagination.php +++ b/src/Pagination.php @@ -103,6 +103,9 @@ public function getLinks($absolute = false) $absolute ) ]; + + $links[self::LINK_FIRST] = $this->createUrl(null, $pageSize, $absolute); + if (($nextLastKey = $this->getNextLastKey()) !== null) { $links[self::LINK_NEXT] = $this->createUrl($nextLastKey, $pageSize, $absolute); } @@ -151,13 +154,13 @@ public function setLastKey($value) /** * Stores the next last key. - * @var string + * @var string|string[] */ private $_nextLastKey; /** * Returns the next last key. - * @return string + * @return string|string[] */ public function getNextLastKey() { @@ -166,7 +169,7 @@ public function getNextLastKey() /** * Sets the next last key. This has to be set manually in the data provider. - * @param string $value The last key that was evaluated by DynamoDB. + * @param string|string[] $value The last key that was evaluated by DynamoDB. * @return void */ public function setNextLastKey($value) diff --git a/src/TableSchema.php b/src/TableSchema.php deleted file mode 100644 index babb236..0000000 --- a/src/TableSchema.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - -namespace UrbanIndo\Yii2\DynamoDb; - -use yii\base\Object; - -/** - * TableSchema represents the metadata of a DynamoDB table. - * - * @author Petra Barus - */ -class TableSchema extends Object -{ - - -} diff --git a/test/LinkPagerTest.php b/test/LinkPagerTest.php new file mode 100644 index 0000000..9db4610 --- /dev/null +++ b/test/LinkPagerTest.php @@ -0,0 +1,39 @@ + + */ +class LinkPagerTest extends TestCase +{ + protected function setUp() + { + parent::setUp(); + $this->mockWebApplication([ + 'components' => [ + 'urlManager' => [ + 'scriptUrl' => '/' + ] + ] + ]); + } + + public function testButtons() + { + $pagination = new Pagination([ + 'route' => 'item/list', + 'lastKey' => 5, + 'nextLastKey' => 10, + 'pageSize' => 10, + ]); + + $linkPagerOutput = LinkPager::widget([ + 'pagination' => $pagination, + ]); + + $this->assertContains('
    • 1
    • ', $linkPagerOutput); + $this->assertContains('', $linkPagerOutput); + } +} diff --git a/test/PaginationTest.php b/test/PaginationTest.php index e003ea7..83a9718 100644 --- a/test/PaginationTest.php +++ b/test/PaginationTest.php @@ -78,6 +78,7 @@ public function dataProviderGetLinks() 5, null, [ + 'first' => '/index.php?r=item%2Flist', 'self' => '/index.php?r=item%2Flist', 'next' => '/index.php?r=item%2Flist&last-key=5', ] @@ -87,6 +88,7 @@ public function dataProviderGetLinks() 10, null, [ + 'first' => '/index.php?r=item%2Flist', 'self' => '/index.php?r=item%2Flist&last-key=5', 'next' => '/index.php?r=item%2Flist&last-key=10', ] @@ -96,6 +98,7 @@ public function dataProviderGetLinks() 10, 10, [ + 'first' => '/index.php?r=item%2Flist&per-page=10', 'self' => '/index.php?r=item%2Flist&last-key=5&per-page=10', 'next' => '/index.php?r=item%2Flist&last-key=10&per-page=10', ] @@ -105,6 +108,7 @@ public function dataProviderGetLinks() ['a', 'f'], 10, [ + 'first' => '/index.php?r=item%2Flist&per-page=10', 'self' => '/index.php?r=item%2Flist&last-key%5B0%5D=a&last-key%5B1%5D=b&per-page=10', 'next' => '/index.php?r=item%2Flist&last-key%5B0%5D=a&last-key%5B1%5D=f&per-page=10', ] From a7e4186586b3fe487ee77b52b641fa72b0ead941 Mon Sep 17 00:00:00 2001 From: Petra Barus Date: Sun, 3 Jan 2016 13:28:39 +0700 Subject: [PATCH 19/23] Added associative array as key in Pagination test. --- test/PaginationTest.php | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/test/PaginationTest.php b/test/PaginationTest.php index 83a9718..b4dd13c 100644 --- a/test/PaginationTest.php +++ b/test/PaginationTest.php @@ -4,7 +4,7 @@ class PaginationTest extends TestCase { - + protected function setUp() { parent::setUp(); @@ -16,7 +16,7 @@ protected function setUp() ], ]); } - + /** * Data provider for [[testCreateUrl()]] * @return array test data @@ -49,12 +49,17 @@ public function dataProviderCreateUrl() 5, '/index.php?r=item%2Flist&last-key%5B0%5D=a&last-key%5B1%5D=f&per-page=5', ], + [ + ['attr1' => 'a', 'attr2' => 'f'], + 5, + '/index.php?r=item%2Flist&last-key%5Battr1%5D=a&last-key%5Battr2%5D=f&per-page=5', + ], ]; } - + /** * @dataProvider dataProviderCreateUrl - * + * * @param string|string[] $lastKey * @param integer $pageSize * @param string $expectedUrl @@ -65,7 +70,7 @@ public function testCreateUrl($lastKey, $pageSize, $expectedUrl) $pagination->route = 'item/list'; $this->assertEquals($expectedUrl, $pagination->createUrl($lastKey, $pageSize)); } - + /** * Data provider for [[testCreateUrl()]] * @return array test data @@ -112,13 +117,23 @@ public function dataProviderGetLinks() 'self' => '/index.php?r=item%2Flist&last-key%5B0%5D=a&last-key%5B1%5D=b&per-page=10', 'next' => '/index.php?r=item%2Flist&last-key%5B0%5D=a&last-key%5B1%5D=f&per-page=10', ] + ], + [ + ['attr1' => 'a', 'attr2' => 'b'], + ['attr1' => 'a', 'attr2' => 'f'], + 10, + [ + 'first' => '/index.php?r=item%2Flist&per-page=10', + 'self' => '/index.php?r=item%2Flist&last-key%5Battr1%5D=a&last-key%5Battr2%5D=b&per-page=10', + 'next' => '/index.php?r=item%2Flist&last-key%5Battr1%5D=a&last-key%5Battr2%5D=f&per-page=10', + ] ] ]; } - + /** * @dataProvider dataProviderGetLinks - * + * * @param string|string[] $currentLastKey The current last key. * @param string|string[] $nextLastKey The next last key. * @param integer $pageSize The page size to show. @@ -132,7 +147,7 @@ public function testGetLinks($currentLastKey, $nextLastKey, $pageSize, $links) 'nextLastKey' => $nextLastKey, 'pageSize' => $pageSize, ]); - + $this->assertEquals($links, $pagination->getLinks()); } } From a88d88a0fe1b50d8dc6c70aab97c96a84e1f9fbd Mon Sep 17 00:00:00 2001 From: Setyo Legowo Date: Sun, 3 Jan 2016 16:29:15 +0700 Subject: [PATCH 20/23] Fix query one item and order method in Query Builder --- README.md | 32 ++++++++++++++++---------------- src/Query.php | 6 ++---- src/QueryBuilder.php | 10 +++++++--- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7ad05bc..62f058f 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ This is a DynamoDB extension for Yii2 -[![Latest Stable Version](https://poser.pugx.org/urbanindo/yii2-dynamodb/v/stable.svg)](https://packagist.org/packages/urbanindo/yii2-queue) -[![Total Downloads](https://poser.pugx.org/urbanindo/yii2-dynamodb/downloads.svg)](https://packagist.org/packages/urbanindo/yii2-queue) -[![Latest Unstable Version](https://poser.pugx.org/urbanindo/yii2-dynamodb/v/unstable.svg)](https://packagist.org/packages/urbanindo/yii2-queue) -[![Build Status](https://travis-ci.org/urbanindo/yii2-dynamodb.svg)](https://travis-ci.org/urbanindo/yii2-queue) +[![Latest Stable Version](https://poser.pugx.org/urbanindo/yii2-dynamodb/v/stable.svg)](https://packagist.org/packages/urbanindo/yii2-dynamodb) +[![Total Downloads](https://poser.pugx.org/urbanindo/yii2-dynamodb/downloads.svg)](https://packagist.org/packages/urbanindo/yii2-dynamodb) +[![Latest Unstable Version](https://poser.pugx.org/urbanindo/yii2-dynamodb/v/unstable.svg)](https://packagist.org/packages/urbanindo/yii2-dynamodb) +[![Build Status](https://travis-ci.org/urbanindo/yii2-dynamodb.svg)](https://travis-ci.org/urbanindo/yii2-dynamodb) ## Requirement @@ -60,20 +60,20 @@ return [ ## Limitation -Because DynamoDB have different behavior with MySQL in general, there are several +Because DynamoDB have different behavior from MySQL, there are several limitations or behavior change applied. There are several method to get data from DynamoDB: __GetItem__, __BatchGetItem__, __Scan__, and __Query__. -1. We have tried to implement automatic method to acquire model from Query. You have -to assign method explicitly when you want to force method in use. +1. We have tried to implement automatic method to acquire model from Query. You should +assign method explicitly if you want to force the method to use. 2. Not yet support attribute name aliasing (In MySQL known as field aliasing). -3. When using __Query__ method, in where condition should using just key attributes. -In next roll out will add filtering with non key attributes. -4. To make pagination, we recommend using Query method when want to filter result. -If you use filtering with non key attribute, it is possible result the model(s) less -than desired limit value. -5. `indexBy` and `orderBy` cannot use by attribute string value or callable parameter. +3. When using __Query__ method, where condition just support filter by key attributes. +In next roll out we will add filtering with non key attributes. +4. To make pagination, we forcedly using __Query__ method when WHERE condition is set. +Because if you use filtering with non key attribute, it is possible the model result(s) +will less than desired limit value. +5. `indexBy` and `orderBy` cannot use with attribute string value or callable parameter. This will use as string value and assign to `IndexName` parameter in DynamoDB. To -use sorting, it will use __QUERY__ method and `orderBy` parameter should be either -`['myIndex' => 'ASC']` or `['myIndex', 'ASC']`. -6. Not support NULL type attribute. +use sorting, this will forcedly use __QUERY__ method and `orderBy` parameter should be +either `['myIndex' => 'ASC']` or `['myIndex', 'DESC']`. +6. Not support NULL and any kind of set attribute type. diff --git a/src/Query.php b/src/Query.php index 26d2a82..1b2955c 100644 --- a/src/Query.php +++ b/src/Query.php @@ -107,7 +107,7 @@ public function createCommand(Connection $db = null) $db = Yii::$app->get('dynamodb'); } list($name, $argument) = $db->getQueryBuilder()->build($this); - + return $db->createCommand([ 'name' => $name, 'argument' => $argument, @@ -320,9 +320,7 @@ public function all($db = null) */ public function one($db = null) { - if (in_array($this->using, [self::USING_QUERY, self::USING_SCAN])) { - $this->limit(1); - } + $this->limit(1); $response = $this->execute($db); $rows = $this->getItemsFromResponse($response); if (empty($rows)) { diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index ee59127..7de0e7f 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -81,7 +81,7 @@ public function build(Query $query) || !empty($query->offset) || !empty($query->orderBy) || $keyCondition != 1 || !$supportBatchGetItem) { // TODO WARNING AWS SDK not support operator beside '=' for key - if (empty($query->orderBy) && ($keyCondition > 0) && $supportBatchGetItem) { + if (!empty($query->orderBy) && ($keyCondition == 1) && $supportBatchGetItem) { $query->using = Query::USING_QUERY; } else { $query->using = Query::USING_SCAN; @@ -769,8 +769,12 @@ public function buildOptions(Query $query) $sort = current($query->orderBy); } } else { - list($index, $sort) = explode(' ', $query->orderBy, 2); - $query->indexBy = $index; + if (in_array(strtoupper($query->orderBy), ['ASC', 'DESC'])) { + $sort = $query->orderBy; + } else { + list($index, $sort) = explode(' ', $query->orderBy, 2); + $query->indexBy = $index; + } } $sort = strtoupper($sort); if (!in_array($sort, ['ASC', 'DESC'])) { From 486a0f4084fd7058ca3567c15d0bbd08126c8509 Mon Sep 17 00:00:00 2001 From: Setyo Legowo Date: Mon, 4 Jan 2016 15:11:15 +0700 Subject: [PATCH 21/23] Fix LinkPager pagination and unknown index in AR --- src/ActiveRecord.php | 6 +++--- src/Pagination.php | 42 ++++++++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 7d5903b..a708062 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -174,10 +174,10 @@ public function insert($runValidation = true, $attributes = null) public static function primaryKey() { if (!isset(self::$_primaryKeys[get_called_class()])) { - $description = self::getDb()->createCommand() - ->describeTable(self::tableName()) + $description = static::getDb()->createCommand() + ->describeTable(static::tableName()) ->execute(); - $keySchema = $description['KeySchema']; + $keySchema = $description['Table']['KeySchema']; $keys = []; foreach ($keySchema as $key) { $idx = $key['KeyType'] == 'HASH' ? 0 : 1; diff --git a/src/Pagination.php b/src/Pagination.php index c3e01f6..32a292c 100644 --- a/src/Pagination.php +++ b/src/Pagination.php @@ -20,13 +20,13 @@ class Pagination extends \yii\data\Pagination * @see params */ public $lastKeyParam = 'last-key'; - + /** * @var boolean whether to always have the last-key parameter in the URL created by [[createUrl()]]. * If false and [[lastKey]] is null, the lastKey parameter will not be put in the URL. */ public $forceLastKeyParam = true; - + /** * @var array parameters (name => value) that should be used to obtain the current page number * and to create new pagination URLs. If not set, all parameters from $_GET will be used instead. @@ -37,7 +37,7 @@ class Pagination extends \yii\data\Pagination * while the element indexed by [[pageSizeParam]] is treated as the page size (defaults to [[defaultPageSize]]). */ public $params; - + /** * This will only return 1. * @return integer number of pages @@ -46,7 +46,7 @@ public function getPageCount() { return 1; } - + /** * Creates the URL suitable for pagination with the specified page number. * This method is mainly called by pagers when creating URLs used to perform pagination. @@ -64,7 +64,7 @@ public function createUrl($lastKey, $pageSize = null, $absolute = false) $request = Yii::$app->getRequest(); $params = $request instanceof Request ? $request->getQueryParams() : []; } - + if ($lastKey !== null || $this->forcePageParam) { $params[$this->lastKeyParam] = $lastKey; } else { @@ -78,7 +78,7 @@ public function createUrl($lastKey, $pageSize = null, $absolute = false) } else { unset($params[$this->pageSizeParam]); } - + $params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; if ($absolute) { @@ -87,7 +87,7 @@ public function createUrl($lastKey, $pageSize = null, $absolute = false) return $urlManager->createUrl($params); } } - + /** * Returns just one link to the next page. * @param boolean $absolute Whether the generated URLs should be absolute. @@ -103,15 +103,15 @@ public function getLinks($absolute = false) $absolute ) ]; - + $links[self::LINK_FIRST] = $this->createUrl(null, $pageSize, $absolute); - + if (($nextLastKey = $this->getNextLastKey()) !== null) { $links[self::LINK_NEXT] = $this->createUrl($nextLastKey, $pageSize, $absolute); } return $links; } - + /** * @return integer the limit of the data. This may be used to set the * LIMIT value for Query or Scan operation. @@ -123,13 +123,13 @@ public function getLimit() return $pageSize < 1 ? -1 : $pageSize; } - + /** * Stores the current last key. * @var string|string[] */ private $_lastKey; - + /** * Returns the last key evaluated in the DynamoDB. * @return string|string[] @@ -137,11 +137,17 @@ public function getLimit() public function getLastKey() { if ($this->_lastKey === null) { - $this->setLastKey($this->getQueryParam($this->lastKeyParam)); + if (($params = $this->params) === null) { + $request = Yii::$app->getRequest(); + $params = $request instanceof \yii\web\Request ? $request->getQueryParams() : []; + } + if (isset($params[$this->lastKeyParam])) { + $this->setLastKey($params[$this->lastKeyParam]); + } } return $this->_lastKey; } - + /** * Sets the current last key. * @param string|string[] $value The last key that was evaluated by DynamoDB. @@ -151,13 +157,13 @@ public function setLastKey($value) { $this->_lastKey = $value; } - + /** * Stores the next last key. * @var string|string[] */ private $_nextLastKey; - + /** * Returns the next last key. * @return string|string[] @@ -166,7 +172,7 @@ public function getNextLastKey() { return $this->_nextLastKey; } - + /** * Sets the next last key. This has to be set manually in the data provider. * @param string|string[] $value The last key that was evaluated by DynamoDB. @@ -176,7 +182,7 @@ public function setNextLastKey($value) { $this->_nextLastKey = $value; } - + /** * This is shorthand for `getLastKey()`. * @return string From d49833908d6e61add9552e15f4f9cd8d7c34691c Mon Sep 17 00:00:00 2001 From: Setyo Legowo Date: Mon, 4 Jan 2016 15:56:30 +0700 Subject: [PATCH 22/23] Fix order parameter value in Query Builder - And Fix problem in parsing orderBy and indexBy --- README.md | 3 ++- src/QueryBuilder.php | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 62f058f..84bfb96 100644 --- a/README.md +++ b/README.md @@ -75,5 +75,6 @@ will less than desired limit value. 5. `indexBy` and `orderBy` cannot use with attribute string value or callable parameter. This will use as string value and assign to `IndexName` parameter in DynamoDB. To use sorting, this will forcedly use __QUERY__ method and `orderBy` parameter should be -either `['myIndex' => 'ASC']` or `['myIndex', 'DESC']`. +either `['myIndex' => 'ASC']` or `['myIndex', 'DESC']` and key condition expression +should be defined. 6. Not support NULL and any kind of set attribute type. diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 7de0e7f..9986cef 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -11,6 +11,7 @@ use yii\helpers\ArrayHelper; use yii\base\InvalidParamException; use InvalidArgumentException; +use Exception; /** * QueryBuilder builds an elasticsearch query based on the specification given @@ -80,7 +81,8 @@ public function build(Query $query) if (empty($query->where) || !empty($query->indexBy) || !empty($query->limit) || !empty($query->offset) || !empty($query->orderBy) || $keyCondition != 1 || !$supportBatchGetItem) { - // TODO WARNING AWS SDK not support operator beside '=' for key + // WARNING AWS SDK not support operator beside '=' if use Query method + // TODO Slice where clause query if (!empty($query->orderBy) && ($keyCondition == 1) && $supportBatchGetItem) { $query->using = Query::USING_QUERY; } else { @@ -131,6 +133,7 @@ public function isOperatorSupportBatchGetItem($where) } } } + return true; } return false; @@ -435,6 +438,8 @@ public function buildWhereQueryScan($condition) */ public function paramToExpressionAttributeValues($params) { + // TODO Supporting attribute type detection beside single numeric, single + // string, and single boolean foreach ($params as $i => $value) { if (is_int($value)) { $params[$i] = ['N' => $value]; @@ -762,8 +767,12 @@ public function buildOptions(Query $query) $sort = ''; if (is_array($query->orderBy)) { if (ArrayHelper::isIndexed($query->orderBy)) { - $query->indexBy = $query->orderBy[0]; - $sort = $query->orderBy[1]; + if (sizeof($query->orderBy) > 1) { + $query->indexBy = $query->orderBy[0]; + $sort = $query->orderBy[1]; + } else { + $sort = $query->orderBy[0]; + } } else { $query->indexBy = key($query->orderBy); $sort = current($query->orderBy); @@ -780,13 +789,14 @@ public function buildOptions(Query $query) if (!in_array($sort, ['ASC', 'DESC'])) { throw new InvalidArgumentException('Sort key unknown: ' . reset($query->orderBy)); } - $options['ScanIndexForward'] = ($sort == 'ASC'); + $options['ScanIndexForward'] = ($sort != 'ASC'); } if (!empty($query->indexBy)) { if (is_callable($query->indexBy)) { throw new InvalidArgumentException('Cannot using callable parameter.'); } $options = array_merge($options, ['IndexName' => $query->indexBy]); + $query->indexBy = null; } if (!empty($query->limit)) { $options = array_merge($options, ['Limit' => (int) $query->limit]); From 62d1cae1e1f094a3930a7f89358372635118cb39 Mon Sep 17 00:00:00 2001 From: Setyo Legowo Date: Tue, 5 Jan 2016 17:26:00 +0700 Subject: [PATCH 23/23] Fix Auto select method problem. --- README.md | 5 ++ src/QueryBuilder.php | 129 +++++++++++++++++++++++------------- test/ActiveRecordTest.php | 102 ++++++++++++++++++++++++++++ test/QueryBuilderTest.php | 88 +++++++++--------------- test/TestCase.php | 85 ++++++++++++++++++++---- test/data/CustomerRange.php | 38 +++++++++++ 6 files changed, 331 insertions(+), 116 deletions(-) create mode 100644 test/data/CustomerRange.php diff --git a/README.md b/README.md index 84bfb96..c3eb410 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,8 @@ use sorting, this will forcedly use __QUERY__ method and `orderBy` parameter sho either `['myIndex' => 'ASC']` or `['myIndex', 'DESC']` and key condition expression should be defined. 6. Not support NULL and any kind of set attribute type. +7. Not support attribute aliasing belong to Reserve Keywords, which means all attributes +do not using any [Reserve Keywords](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html). +8. When use LinkPager, do not forget use ActiveDataProvider from this package. When +the pagination pass into any kind of Widget View, several components maybe unsupported +like _SerialColumn_, unnecessary total items in summary, and sorting. diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 9986cef..85190cc 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -89,7 +89,11 @@ public function build(Query $query) $query->using = Query::USING_SCAN; } } else { - $query->using = Query::USING_BATCH_GET_ITEM; + if ($this->isOperatorSupportOnlyGetItem($query->where)) { + $query->using = Query::USING_GET_ITEM; + } else { + $query->using = Query::USING_BATCH_GET_ITEM; + } } } @@ -116,27 +120,54 @@ public function isOperatorSupportBatchGetItem($where) if (ArrayHelper::isIndexed($where)) { foreach ($where as $whereIndexedElement) { if (is_array($whereIndexedElement)) { - if (!$this->isOperatorSupportBatchGetItem($whereIndexedElement)) { - return false; - } + return !$this->isOperatorSupportBatchGetItem($whereIndexedElement); } else { - if (in_array($whereIndexedElement, ['IN', '='])) { - return true; - } + + return in_array($whereIndexedElement, ['IN', '=']); } } } else { // associative array remaining foreach ($where as $key => $whereElement) { if (is_array($whereElement)) { - if (!$this->isOperatorSupportBatchGetItem($whereElement)) { - return false; - } - } + return !$this->isOperatorSupportBatchGetItem($whereElement); + } // else scalar value } + } + + return true; + } + + /** + * Find out where all operator condition is able to support BatchGetItem or not. + * @param array $where Where condition string. + * @return boolean True when operator support GetItem. + */ + public function isOperatorSupportOnlyGetItem($where) + { + if (empty($where)) { + return false; + } + if (is_string($where) || is_numeric($where)) { return true; } + // Array type remaining + if (ArrayHelper::isIndexed($where)) { + foreach ($where as $whereIndexedElement) { + if (is_array($whereIndexedElement)) { + return false; + } else { + return in_array($whereIndexedElement, ['=']); + } + } + } else { // associative array remaining + foreach ($where as $key => $whereElement) { + if (is_array($whereElement)) { + return false; + } + } + } - return false; + return true; } /** @@ -150,12 +181,12 @@ public function isConditionMatchKeySchema(Query $query) if (is_array($query->where)) { foreach ($this->getKeySchema($query) as $keySchemaElement) { $keySchema[] = $keySchemaElement['AttributeName']; + $allKeyTrue[$keySchemaElement['AttributeName']] = 0; } - $dummy = false; if (!is_array(current($query->where))) { - return $this->searchAttrInArray([$query->where], $keySchema, $dummy); + return $this->searchAttrInArray([$query->where], $keySchema, $allKeyTrue); } else { - return $this->searchAttrInArray($query->where, $keySchema, $dummy); + return $this->searchAttrInArray($query->where, $keySchema, $allKeyTrue); } } @@ -164,46 +195,48 @@ public function isConditionMatchKeySchema(Query $query) /** * Search keys in where schema compare within key schema. - * @param array $where Where schema. - * @param array $keySchema Key schema. - * @param boolean $hasFoundKey Has found key or did not. + * @param array $where Where schema. + * @param array $keySchema Key schema. + * @param array $allKeyTrue Finding all key in KeySchema exist in where clause. * @return integer Return 0 if no match with key, 1 if match only key, * 2 if contain both key and non key. */ - public function searchAttrInArray($where, $keySchema, &$hasFoundKey) + public function searchAttrInArray($where, $keySchema, &$allKeyTrue) { - foreach ($where as $whereElement) { - if (ArrayHelper::isIndexed($whereElement)) { - if (sizeof($whereElement) < 2) { + foreach ($where as $key => $whereElement) { + if (ArrayHelper::isIndexed($where)) { + if (is_string($whereElement) || is_numeric($whereElement)) { continue; } - if (!in_array($whereElement[1], $keySchema)) { - if ($hasFoundKey) { - return 2; - } - } else { - $hasFoundKey = true; - } - } else { - foreach ($whereElement as $attr => $val) { - if (is_array($val)) { - $tmp = $this->searchAttrInArray($val, $keySchema, $hasFoundKey); - if ($hasFoundKey) { - return $tmp; + if (ArrayHelper::isIndexed($whereElement)) { + $this->searchAttrInArray($whereElement, $keySchema, $allKeyTrue); + } else { // inner element is associative + foreach ($whereElement as $attr => $val) { + if (is_array($val)) { + $this->searchAttrInArray($val, $keySchema, $allKeyTrue); } - } - if (!in_array($attr, $keySchema)) { - if ($hasFoundKey) { - return 2; + if (!in_array($attr, $keySchema)) { + if (in_array(true, $allKeyTrue)) { + return 2; + } + } else { + $allKeyTrue[$attr] = 1; } - } else { - $hasFoundKey = true; } } + } else { // associative + $this->searchAttrInArray($whereElement, $keySchema, $allKeyTrue); + if (!in_array($key, $keySchema)) { + if (in_array(true, $allKeyTrue)) { + return 2; + } + } else { + $allKeyTrue[$key] = 1; + } } } - return $hasFoundKey ? 1 : 0; + return !in_array(0, $allKeyTrue) ? 1 : in_array(1, $allKeyTrue) ? 2 : 0; } /** @@ -215,7 +248,7 @@ public function searchAttrInArray($where, $keySchema, &$hasFoundKey) public function getKeySchema(Query $query) { $tableDescription = $this->db->createCommand()->describeTable($query->from)->execute(); - $options = $this->buildOptions($query); + $options = $this->buildOptions($query, false); if (isset($options['IndexName'])) { if (isset($tableDescription['Table']['LocalSecondaryIndexes'])) { @@ -754,12 +787,14 @@ public function buildSimpleCondition($operator, $operands, &$params) /** * Generate options or addition information for DynamoDB query - * @param Query $query Object from which the query will be generated. + * @param Query $query Object from which the query will be generated. + * @param boolean $clear Index by should clear after usage, this param + * give programmer options to clear or not. * @return array Another options which used in the query * @throws InvalidArgumentException Table name should be exist and IndexName * is string type, can not callable. */ - public function buildOptions(Query $query) + public function buildOptions(Query $query, $clear = true) { $options = []; @@ -796,7 +831,9 @@ public function buildOptions(Query $query) throw new InvalidArgumentException('Cannot using callable parameter.'); } $options = array_merge($options, ['IndexName' => $query->indexBy]); - $query->indexBy = null; + if ($clear) { + $query->indexBy = null; + } } if (!empty($query->limit)) { $options = array_merge($options, ['Limit' => (int) $query->limit]); diff --git a/test/ActiveRecordTest.php b/test/ActiveRecordTest.php index d2878df..fb2ee62 100644 --- a/test/ActiveRecordTest.php +++ b/test/ActiveRecordTest.php @@ -5,6 +5,7 @@ class ActiveRecordTest extends TestCase { protected function setUp() { parent::setUp(); $this->createCustomersTable(); + $this->createCustomersRangeTable(); } public function testInsertAndFindOne() { @@ -133,6 +134,107 @@ public function testCondition() $this->assertEquals(1, count($objectsFromFind)); } + public function testCondition2() + { + $objectToInsert = new \test\data\CustomerRange(); + $id1 = (int) \Faker\Provider\Base::randomNumber(5); + $faker = \Faker\Factory::create(); + $objectToInsert->id = $id1; + $objectToInsert->name = $faker->name; + $objectToInsert->phone = $faker->phoneNumber; + $objectToInsert->contacts = [ + 'telephone1' => 123456, + 'telephone2' => 345678, + 'telephone3' => 345678, + ]; + $objectToInsert->prices = [ + 1000000, + 1000000, + 1000000, + 1000000 + ]; + $objectToInsert->kids = [ + 'Alice', + 'Billy', + 'Charlie', + ]; + + $this->assertTrue($objectToInsert->save(false)); + + $objectToInsert2 = new \test\data\CustomerRange(); + $id2 = $id1 + 1; + $faker = \Faker\Factory::create(); + $objectToInsert2->id = $id2; + $objectToInsert2->name = $faker->name; + $objectToInsert2->phone = $faker->phoneNumber; + $objectToInsert2->contacts = [ + 'telephone1' => 123456, + 'telephone2' => 345678, + 'telephone3' => 345678, + ]; + $objectToInsert2->prices = [ + 1000000, + 1000000, + 1000000, + 1000000 + ]; + $objectToInsert2->kids = [ + 'Alice', + 'Ari', + 'Charlie', + ]; + + $this->assertTrue($objectToInsert2->save(false)); + + $objectToInsert3 = new \test\data\CustomerRange(); + $id3 = $id2 + 1; + $faker = \Faker\Factory::create(); + $objectToInsert3->id = $id3; + $objectToInsert3->name = $faker->name; + $objectToInsert3->phone = $faker->phoneNumber; + $objectToInsert3->contacts = [ + 'telephone1' => 123456, + 'telephone2' => 345678, + 'telephone3' => 345678, + ]; + $objectToInsert3->prices = [ + 1000000, + 1000000, + 1000000, + 1000000 + ]; + $objectToInsert3->kids = [ + 'Alice', + 'Ari', + 'Angle', + ]; + + $this->assertTrue($objectToInsert3->save(false)); + + $objectsFromFind = \test\data\CustomerRange::find()->where(['id' => [$id1]])->all(); + $this->assertEquals(1, count($objectsFromFind)); + $objectsFromFind = \test\data\CustomerRange::find()->where(['id' => $id1])->all(); + $this->assertEquals(1, count($objectsFromFind)); + $objectsFromFind = \test\data\CustomerRange::find()->where(['id' => $objectToInsert3->id]) + ->andWhere(['phone' => $objectToInsert3->phone]) + ->indexBy(\test\data\CustomerRange::secondaryIndex()[0])->all(); + $this->assertEquals(1, count($objectsFromFind)); + $objectsFromFind = \test\data\CustomerRange::find()->where(['id' => [$id1, $id2]])->all(); + $this->assertEquals(2, count($objectsFromFind)); + $objectsFromFind = \test\data\CustomerRange::find()->where(['IN', 'id', [$id1, $id2]])->all(); + $this->assertEquals(2, count($objectsFromFind)); + $objectsFromFind = \test\data\CustomerRange::find()->where(['id' => $id1]) + ->orWhere(['id' => $id2])->all(); + $this->assertEquals(2, count($objectsFromFind)); + $objectsFromFind = \test\data\CustomerRange::find()->where(['>=', 'id', $id1])->all(); + $this->assertEquals(3, count($objectsFromFind)); + $objectsFromFind = \test\data\CustomerRange::find()->limit(2)->all(); + $this->assertEquals(2, count($objectsFromFind)); + $objectsFromFind = \test\data\CustomerRange::find()->where(['>', 'id', $id1])->all(); + $objectsFromFind = \test\data\CustomerRange::find()->where(['CONTAINS', 'kids', 'Angle'])->all(); + $this->assertEquals(1, count($objectsFromFind)); + } + public function testInsertAndFindAll() { $id1 = (int) \Faker\Provider\Base::randomNumber(5); diff --git a/test/QueryBuilderTest.php b/test/QueryBuilderTest.php index 9592828..9ff076d 100644 --- a/test/QueryBuilderTest.php +++ b/test/QueryBuilderTest.php @@ -7,7 +7,7 @@ use UrbanIndo\Yii2\DynamoDb\Query; use UrbanIndo\Yii2\DynamoDb\QueryBuilder; -use test\data\Customer; +use test\data\CustomerRange as Customer; /** * PHP Unit Test Class for Query Builder @@ -28,49 +28,9 @@ class QueryBuilderTest extends TestCase */ public function setUp() { + parent::setUp(); $this->db = $this->getConnection(); - $command = $this->db->createCommand(); - $faker = \Faker\Factory::create(); - $tableName = Customer::tableName(); - $fieldName1 = Customer::primaryKey()[0]; - $index1 = Customer::secondaryIndex()[0]; - $indexFieldName1 = Customer::keySecondayIndex()[$index1][0]; - - if (!$command->tableExists($tableName)) { - $command->createTable($tableName, [ - 'KeySchema' => [ - [ - 'AttributeName' => $fieldName1, - 'KeyType' => 'HASH', - ], - ], - 'AttributeDefinitions' => [ - [ - 'AttributeName' => $fieldName1, - 'AttributeType' => 'S', - ], - [ - 'AttributeName' => $indexFieldName1, - 'AttributeType' => 'S', - ], - ], - 'LocalSecondaryIndexes' => [ - [ - 'IndexName' => $index1, - 'KeySchema' => [ - [ 'AttributeName' => $index1, 'KeyType' => 'HASH' ] - ], - 'Projection' => [ - 'ProjectionType' => 'KEYS_ONLY' - ], - ], - ], - 'ProvisionedThroughput' => [ - 'ReadCapacityUnits' => 5, - 'WriteCapacityUnits' => 5, - ], - ])->execute(); - } + parent::createCustomersRangeTable(); } /** @@ -143,16 +103,17 @@ public function testBuildSimpleGetItem() $qb = $this->createQueryBuilder(); $faker = \Faker\Factory::create(); $id = $faker->firstNameFemale; - $query1 = $this->createQueryGetItem()->where(['id' => $id]); - $query2 = $this->createQueryGetItem()->where($id); - $query3 = $this->createQueryGetItem()->where($id)->withConsistentRead(); - $query4 = $this->createQueryGetItem()->where($id)->withoutConsistentRead(); - $query5 = $this->createQueryGetItem()->where($id)->setConsumedCapacity('NONE'); + $query1 = $this->createQueryGetItem()->where(['id' => $id, 'name' => $id]); + $query2 = $this->createQueryGetItem()->where(['id' => $id, 'name' => $id]); + $query3 = $this->createQueryGetItem()->where(['id' => $id, 'name' => $id])->withConsistentRead(); + $query4 = $this->createQueryGetItem()->where(['id' => $id, 'name' => $id])->withoutConsistentRead(); + $query5 = $this->createQueryGetItem()->where(['id' => $id, 'name' => $id])->setConsumedCapacity('NONE'); $expected = [ 'TableName' => Customer::tableName(), 'Key' => [ - 'id' => ['S' => $id] + 'id' => ['S' => $id], + 'name' => ['S' => $id], ] ]; @@ -178,12 +139,13 @@ public function testBuildGetItemWithSimpleSelect() $faker = \Faker\Factory::create(); $id = $faker->firstNameFemale; $query1 = $this->createQueryGetItem()->select(['id', 'name', 'contacts']) - ->where(['id' => $id]); + ->where(['id' => $id, 'name' => $id]); $expected = [ 'TableName' => Customer::tableName(), 'Key' => [ - 'id' => ['S' => $id] + 'id' => ['S' => $id], + 'name' => ['S' => $id], ], 'ProjectionExpression' => 'id, name, contacts', ]; @@ -200,18 +162,19 @@ public function testBuildSimpleGetBatchItem() $qb = $this->createQueryBuilder(); $faker = \Faker\Factory::create(); $id = $faker->firstNameFemale; - $query1 = $this->createQueryGetBatchItem()->where(['id' => $id]); - $query2 = $this->createQueryGetBatchItem()->where($id); - $query3 = $this->createQueryGetBatchItem()->where($id)->withConsistentRead(); - $query4 = $this->createQueryGetBatchItem()->where($id)->withoutConsistentRead(); - $query5 = $this->createQueryGetBatchItem()->where($id)->setConsumedCapacity('NONE'); + $query1 = $this->createQueryGetBatchItem()->where(['id' => $id, 'name' => $id]); + $query2 = $this->createQueryGetBatchItem()->where(['id' => $id, 'name' => $id]); + $query3 = $this->createQueryGetBatchItem()->where(['id' => $id, 'name' => $id])->withConsistentRead(); + $query4 = $this->createQueryGetBatchItem()->where(['id' => $id, 'name' => $id])->withoutConsistentRead(); + $query5 = $this->createQueryGetBatchItem()->where(['id' => $id, 'name' => $id])->setConsumedCapacity('NONE'); $expected = [ 'RequestItems' => [ Customer::tableName() => [ 'Keys' => [ [ - 'id' => ['S' => $id] + 'id' => ['S' => $id], + 'name' => ['S' => $id], ] ], ] @@ -529,6 +492,12 @@ public function testBuildQueryWithIndex() $id = $faker->firstNameFemale; $query1 = $this->createQuery()->select(['id', 'name', 'contacts']) ->where(['name' => $id])->indexBy(Customer::secondaryIndex()[0]); + $query2 = $this->createQuery()->select(['id', 'name', 'contacts']) + ->where(['name' => $id])->orderBy(['ASC'])->indexBy(Customer::secondaryIndex()[0]); + $query3 = $this->createQuery()->select(['id', 'name', 'contacts']) + ->where(['name' => $id])->orderBy([Customer::secondaryIndex()[0] => 'ASC']); + $query4 = $this->createQuery()->select(['id', 'name', 'contacts']) + ->where(['name' => $id])->orderBy([Customer::secondaryIndex()[0] => 'DESC']); $expected = [ 'TableName' => Customer::tableName(), @@ -541,6 +510,11 @@ public function testBuildQueryWithIndex() ]; $this->assertEquals($expected, $qb->build($query1)[1]); + $expected['ScanIndexForward'] = false; + $this->assertEquals($expected, $qb->build($query2)[1]); + $this->assertEquals($expected, $qb->build($query3)[1]); + $expected['ScanIndexForward'] = true; + $this->assertEquals($expected, $qb->build($query4)[1]); } /** diff --git a/test/TestCase.php b/test/TestCase.php index 64be437..450770a 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -4,7 +4,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase { - + /** * @return \UrbanIndo\Yii2\DynamoDb\Connection */ @@ -29,7 +29,7 @@ public function getConnection() ]); return Yii::$app->dynamodb; } - + /** * @return \UrbanIndo\Yii2\DynamoDb\Command */ @@ -37,20 +37,20 @@ public function createCommand() { return $this->getConnection()->createCommand(); } - + public function getTableItemCount($tableName) { $tableDescription = $this->getConnection()->createCommand()->describeTable($tableName)->execute(); return $tableDescription['Table']['ItemCount']; } - - + + public function createSimpleTableWithHashKey() { $command = $this->createCommand(); $faker = \Faker\Factory::create(); $tableName = $faker->uuid; $fieldName1 = $faker->firstNameMale; - + $command->createTable($tableName, [ 'KeySchema' => [ [ @@ -69,10 +69,10 @@ public function createSimpleTableWithHashKey() 'WriteCapacityUnits' => 5, ] ])->execute(); - + return [$tableName, $fieldName1]; } - + public function createSimpleTableWithHashKeyAndRangeKey() { $command = $this->createCommand(); @@ -80,7 +80,7 @@ public function createSimpleTableWithHashKeyAndRangeKey() $tableName = $faker->uuid; $fieldName1 = $faker->firstNameMale; $fieldName2 = $faker->firstNameMale; - + $command->createTable($tableName, [ 'KeySchema' => [ [ @@ -107,10 +107,10 @@ public function createSimpleTableWithHashKeyAndRangeKey() 'WriteCapacityUnits' => 5, ] ])->execute(); - + return [$tableName, $fieldName1, $fieldName2]; } - + protected function mockWebApplication($config = [], $appClass = '\yii\web\Application') { new $appClass(ArrayHelper::merge([ @@ -126,7 +126,7 @@ protected function mockWebApplication($config = [], $appClass = '\yii\web\Applic ] ], $config)); } - + protected function getVendorPath() { $vendor = dirname(dirname(__DIR__)) . '/vendor'; @@ -135,7 +135,7 @@ protected function getVendorPath() } return $vendor; } - + protected function createCustomersTable() { $command = $this->createCommand(); @@ -163,4 +163,63 @@ protected function createCustomersTable() ] ])->execute(); } + + protected function createCustomersRangeTable() + { + $command = $this->createCommand(); + /* @var $command \UrbanIndo\Yii2\DynamoDb\Command */ + $table = \test\data\CustomerRange::tableName(); + if ($command->tableExists($table)) { + $command->deleteTable($table)->execute(); + } + $index = \test\data\CustomerRange::secondaryIndex()[0]; + $command->createTable($table, [ + 'AttributeDefinitions' => [ + [ + 'AttributeName' => \test\data\CustomerRange::primaryKey()[0], + 'AttributeType' => 'N' + ], + [ + 'AttributeName' => \test\data\CustomerRange::primaryKey()[1], + 'AttributeType' => 'S' + ], + [ + 'AttributeName' => \test\data\CustomerRange::keySecondayIndex()[$index][1], + 'AttributeType' => 'S' + ] + ], + 'KeySchema' => [ + [ + 'AttributeName' => \test\data\CustomerRange::primaryKey()[0], + 'KeyType' => 'HASH', + ], + [ + 'AttributeName' => \test\data\CustomerRange::primaryKey()[1], + 'KeyType' => 'RANGE', + ] + ], + 'LocalSecondaryIndexes' => [ + [ + 'IndexName' => $index, + 'KeySchema' => [ + [ + 'AttributeName' => \test\data\CustomerRange::keySecondayIndex()[$index][0], + 'KeyType' => 'HASH', + ], + [ + 'AttributeName' => \test\data\CustomerRange::keySecondayIndex()[$index][1], + 'KeyType' => 'RANGE', + ] + ], + 'Projection' => [ + 'ProjectionType' => 'ALL', + ] + ] + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 10, + 'WriteCapacityUnits' => 10 + ] + ])->execute(); + } } diff --git a/test/data/CustomerRange.php b/test/data/CustomerRange.php new file mode 100644 index 0000000..5aa4450 --- /dev/null +++ b/test/data/CustomerRange.php @@ -0,0 +1,38 @@ + ['id', 'phone']]; + } + + public function attributes() { + return [ + 'id', + 'name', + 'phone', + 'contacts', + 'prices', + 'kids', + 'age', + ]; + } +}