diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..fe7f9578 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +/.github export-ignore +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-cs-fixer.dist.php export-ignore +/infection.json.dist export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore + +*.php diff=php \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..93c7da49 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,18 @@ +--- +name: Bug report 🐛 +about: If something isn't working as expected. +--- + +# Bug report + + + + + +### Code snippet that reproduces the problem + + + +### Expected output + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..59002b55 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Support question + url: https://github.com/neo4j-php/php-cypher-dsl/discussions/new + about: Please open a new discussion instead. Thank you. diff --git a/.github/ISSUE_TEMPLATE/missing_feature_request.md b/.github/ISSUE_TEMPLATE/missing_feature_request.md new file mode 100644 index 00000000..e897ae71 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/missing_feature_request.md @@ -0,0 +1,8 @@ +--- +name: Missing feature request 🚀 +about: Some feature from Cypher has not been implemented +--- + +# Feature request + + diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..75fde29f --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,70 @@ +# Security policy + +**Please do not disclose security-related issues publicly, [see +below](#reporting-a-vulnerability).** + +## Reporting a vulnerability + +If you discover a security vulnerability within php-cypher-dsl, please send an +e-mail to Marijn van Wezel at marijnvanwezel@gmail.com. All security +vulnerabilities will be promptly addressed. + +You may optionally encrypt your report with PGP, using the following key +(fingerprint: `98ED96AA2260D7E3`, proof: +[keybase.io](https://keybase.io/01101101)): + +```text +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGHDX78BEACdBJTxzeBZqXprVACYX6z3G+CMWFCoer4eeJbPRHafme3pMnP2 +y6rtEA8MYBPeP7MV54HghNbXWKjr7fNaaVA9/FBNdyHUo/HfKx+HeqdeVpLND+8Q +0zCMJjrsQ9YpUwra3CgwVee5D/J7tQFU+HjwO8fB7LG1P8TgWWxqxjhosjZPaf6K +lUfmEvZI6MXKc+nZfFdlYAQIexT+HTuyfiXSGsQd8c9kvMLb3GDxLTOeIG/Ge4g7 +S9RaBFI5wZdf20djrLqtqBkiHiecFf9mKLIHS1wb2i2QLQQNhjKGOf6aqUd5zMu2 +SBWvhK5vr5i9RZ2HfxVQJlEEX7jpMRINCb2hSWIoqs1UISJNtC6GK3Wu7X9U5WB1 +q95ffU9Ppc57WxuawI4lKdH/+NKy+iuQspeebKsBQ6uUylK9Z/PQQMVNCfIqrdpW +Lqj7fHZI+MX4YnwmdnZ4cgAQYvLryMha9Ruzz+ek8IbHjzLXPQp19YgQnWZK11xk +3A7bkWbKslqMrdDz08EX3tHafNa8qsPL+21/+OeRNKqzaZGVppFidUOr7Rp3aQCG +UoNAdKEWo5VW1gEnNinH1DX45/NpDJUFU93drThBrOjn4288/PEwgEy0A4buScH0 +5BYbbkSdKttn0YERTYO9p1DLaPg7G2fWNxZdNkSpiwj6Qu52+bXypO3aEQARAQAB +tCtNYXJpam4gdmFuIFdlemVsIDxtYXJpam52YW53ZXplbEBnbWFpbC5jb20+iQJO +BBMBCgA4FiEE32OnYKaJzJteJXzimO2WqiJg1+MFAmHDX78CGwMFCwkIBwIGFQoJ +CAsCBBYCAwECHgECF4AACgkQmO2WqiJg1+PqHQ//YjnKZBo64PIDxdZQYMJIS+zo +1vjkO/+woCAJU7G/r0rQ08BDBLrkxqeGWcPDLYHIvW/CdOMXzSrF83s4BcIAbxvY +d4l4jU3UFt31HEGd96MhvnZRnhPKWsOBSNUgmKYNYmHhKoVKVY9T4HMUzb8Fek27 +VR/CQ1udsjjS2DTudHpO7evAIvqKm//QzLOyVZOwcFW06OG2J0IyOisB34E2qjQv +tFXPuPZjsJWRJhEdV/C9HCAndaXIzEWDn/AIIFstoN+6kTSBfxubAZ700BNGeopC +iBdcPJo4Njw9DpD1pRzGlcUpmGUEBz03slw8kvy+730/mZE6D1B67M/DJ+NL+pmT +Qpk+ow7qZRqOXCgDFXJChAj4gCTgctZgknOKeLm8g8pxEE9ZMBmXIONuLdtmmAeq +579HrtCP4tgoiJu3Wj/I+WWv5l6LFGfPx40BsOqIByaK6mjqFIytD87lXan3SkMu +MoMtHMjeRQLeulBHFrDo4unFDrd/85+VujlV6FK8nVJrVGgOb+R2CQ0TJvOplo0j +2Q1gMTERHcZ9p8X1nRbOt0J1vHfE7fAvL0fhonKkHgNi5IPL5K3QrDkTj4jhNx/R +S9OPYcQdpts6lOYnHsU3zf/9NOtxfVNdpOiGzjX18MFN1xMEt3Gpn3d8jHw+I8uC +EakUnKJ04dSIoySFlTC5Ag0EYcNfvwEQANvgHsf1C0N/ZxBqq7gpbsS/OKNX4V5C +3uWsl+08nJoKxgOGNH+QPjOI7NMO8GrhJv5bvLdLDqzSrJVsqPWfwM15yr5dssZJ +mMJhfSQxWfQjfxtIYugIPec5+NAkbPdcUB6wHHQoiT9sa47xi7uK6yS+/7BsL6A0 +uwOgNsPNUc9bD7vxIiggk+3oT5SlEUDiN6sJo8r7j0oj80mHjpah6Az2VtTTiJUp +wWTYbrGlMzQj8ZFv+KGlXwRnOoq10viacSDCGJCKgIi/yPwaZWAQz6TZHNzCwiHv +Tzc3A9lyxTauYIWsu0gAzpZGThbJOhOcgMMWL5Uet25nRrMvNz2VkNvRM+7z35Sm +CFOqNCnwC2ZviaIo6wB+nj6Z97pGsgP+8ugujsuU9fA//67REbX4Z4k0K+ZuLPB8 +ytaU8sAW7COvdBh7PYBM5v2d2WcWDpSa7hjzjULX71rCs1brbNhpciuZLXGIeqcV +yLEjbSclJSCjO+X0snhwnDlwIIYgqS4LbEO8fdNFnSG6IlRt9LDGBsYYY/rxjTtL +CfHdbIs9JFy5oCNk1m7Jmpsjd/G6F4ILlr/mduSXFzhpiDBKvloUjLD14YzuD593 +L4mx2Ab2j/mY0CPPbudziqrqZlmi1aK54a3KN7sk2DBcfI3tuDbyBiclmYZB8ifC +tMRtx6B9n+zjABEBAAGJAjYEGAEKACAWIQTfY6dgponMm14lfOKY7ZaqImDX4wUC +YcNfvwIbDAAKCRCY7ZaqImDX4zaQD/98rUqsjLI1DDEp4SxohrObMfb2/BRrTQlj +W0+fmWLabfhFwbc6k1akq9LLV+fowuRenRX0K17mt0wzHxW+9F/lJejBHo/MOpUM +jk0g1IhrYAa8osQXqNtzTfBb9e8d5h1YvbMUINEhP/UWyE8hKSwBkDo5OPcsrxD+ +t1sMaaLEzgn+8xF57p9jFIuyXT3iT9NFU6hjZNnDSfEgbqoN2H2teh2GakmoyKg/ +3VIjmyWp3wkWawoRLfsMiTZYOyz2lKqLn+Wf4Q2HpmHIXThJvTyMUD1pV2NUnfI+ +9r3E8IS/OsRT2dHY/qMXvfxlbJurd5HzrLlpAdepslgRRzqO47u3r2ORvmAp8sv9 +gPIDS94Xyr5JHE6gg5D3cgUTUoskwrVvLt7xvwzn52Qk7amrt5spP/48FVJ1AoS4 +0P+Z/WJNjn3iTly+vQTuchgfpRq+IJywOXkEbqYmrqZd6rgyRQ+ug6jmIHQ3pcan +sR3J70nPMUZqVQYiY9OZbdd428WDrMG5zUIPeEwQqj9uOrQqN7duQWKHGtAUqoWz +V7meIw2vG+CrZEYIdWfMoCvjqlYj3P59UqkbzipRrMiWB92qcNIR9QbmAQ8BlE9c +W7zVNgUTK0kk3MRvF2tul8tKlK7pl0/sOPl/jbLujyXwsKu6OxcEOKkLTWyn0Whr +1t3QWAzGmg== +=Yjmm +-----END PGP PUBLIC KEY BLOCK----- +``` + diff --git a/.github/workflows/analyse.yml b/.github/workflows/analyse.yml new file mode 100644 index 00000000..21e49091 --- /dev/null +++ b/.github/workflows/analyse.yml @@ -0,0 +1,38 @@ +name: analyse + +on: + push: + branches: [ 'main' ] + pull_request: + workflow_dispatch: + +jobs: + analyse: + name: "Static analysis (PHP ${{ matrix.php-version }})" + runs-on: "ubuntu-22.04" + continue-on-error: true + + strategy: + matrix: + php-version: [ "8.1" ] + + env: + COMPOSER_VERSION: 2 + COVERAGE_DRIVER: none + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: ${{ env.COVERAGE_DRIVER }} + tools: composer:${{ env.COMPOSER_VERSION }} + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist --no-progress + + - name: Execute type checking + run: php vendor/bin/phpstan diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..229835af --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,59 @@ +name: lint + +on: + push: + branches: [ 'main' ] + pull_request: + workflow_dispatch: + +jobs: + lint: + name: "Linting (PHP ${{ matrix.php-version }})" + runs-on: "ubuntu-22.04" + continue-on-error: true + + strategy: + matrix: + php-version: [ "8.1" ] + + env: + COMPOSER_VERSION: 2 + COVERAGE_DRIVER: none + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: ${{ env.COVERAGE_DRIVER }} + tools: composer:${{ env.COMPOSER_VERSION }} + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist --no-progress + + - name: Run PHP-CS-Fixer + run: php vendor/bin/php-cs-fixer fix --config .php-cs-fixer.dist.php --verbose --dry-run + + + validate: + name: "Validating composer.json" + runs-on: "ubuntu-22.04" + continue-on-error: true + + env: + COMPOSER_VERSION: 2 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + tools: composer:${{ env.COMPOSER_VERSION }} + + - name: Run composer validate + run: composer validate diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 45696ec0..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: build - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-20.04 - continue-on-error: true - - strategy: - matrix: - include: - - php_version: 7.4 - - php_version: 8.0 - - php_version: 8.1 - - php_version: latest - - container: - image: php:${{ matrix.php_version }} - - env: - COMPOSER_VERSION: 2 - INSTALL_PATH: php-cypher-dsl - REPOSITORY_NAME: php-cypher-dsl - - steps: - # https://getcomposer.org/download/ - - name: Install Composer - run: | - apt update - apt install -y unzip - php -r "copy('https://getcomposer.org/installer', 'installer');" - php -r "copy('https://composer.github.io/installer.sig', 'expected');" - echo `cat expected` " installer" | sha384sum -c - - php installer --${{ env.COMPOSER_VERSION }} - rm -f installer expected - mv composer.phar /usr/local/bin/composer - - - name: Install Xdebug - run: | - pecl install xdebug - docker-php-ext-enable xdebug - - - name: Checkout repository - uses: actions/checkout@v2 - with: - repository: WikibaseSolutions/${{ env.REPOSITORY_NAME }} - path: ${{ env.REPOSITORY_NAME }} - - - name: Install dependencies - run: composer update --working-dir ${{ env.INSTALL_PATH }} - - - name: Run php-cs-fixer - run: | - composer php-cs-fixer --working-dir ${{ env.INSTALL_PATH }} -- --dry-run ./src - composer php-cs-fixer --working-dir ${{ env.INSTALL_PATH }} -- --dry-run ./tests - - - name: Run unit tests - run: composer test --working-dir ${{ env.INSTALL_PATH }} - - - name: Run mutation tests - run: composer infect --working-dir ${{ env.INSTALL_PATH }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..ebca4dba --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: test + +on: + push: + branches: ['main'] + pull_request: + workflow_dispatch: + +jobs: + unit: + name: "Unit and mutation testing (PHP ${{ matrix.php-version }})" + runs-on: "ubuntu-22.04" + continue-on-error: true + + strategy: + matrix: + php-version: [ "7.4", "8.0", "8.1", "8.2" ] + + env: + COMPOSER_VERSION: 2 + COVERAGE_DRIVER: xdebug + MINIMUM_COVERAGE_PERCENTAGE: 90 + MINIMUM_MSI_PERCENTAGE: 80 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: ${{ env.COVERAGE_DRIVER }} + ini-values: memory_limit=512M, xdebug.mode=off + tools: composer:${{ env.COMPOSER_VERSION }} + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist --no-progress + + - name: Run unit tests + run: XDEBUG_MODE=coverage php vendor/bin/phpunit --testsuite unit + + - name: Check coverage + run: php vendor/bin/coverage-check coverage/clover.xml ${{ env.MINIMUM_COVERAGE_PERCENTAGE }} + + - name: Run end-to-end tests + run: php vendor/bin/phpunit --testsuite end-to-end --no-coverage + + - name: Run mutation tests + run: XDEBUG_MODE=coverage php vendor/bin/infection --show-mutations --min-msi=${{ env.MINIMUM_MSI_PERCENTAGE }} --threads=4 diff --git a/.gitignore b/.gitignore index 76201998..3b64f24a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .DS_Store composer.lock +coverage/ +doxygen/ phpunit.xml vendor/ *.idea diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 00000000..b94af5ce --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,289 @@ +files() + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/tests/unit') + ->in(__DIR__ . '/tests/integration') + ->in(__DIR__ . '/tests/end-to-end') + ->name('*.php'); + +$config = new Config(); +$config->setFinder($finder) + ->setRiskyAllowed(true) + ->setRules([ + 'align_multiline_comment' => true, + 'array_indentation' => true, + 'array_push' => true, + 'backtick_to_shell_exec' => true, + 'blank_line_after_namespace' => true, + 'blank_line_after_opening_tag' => false, + 'blank_line_before_statement' => [ + 'statements' => [ + 'break', + 'continue', + 'declare', + 'default', + 'do', + 'exit', + 'for', + 'foreach', + 'goto', + 'if', + 'include', + 'include_once', + 'require', + 'require_once', + 'return', + 'switch', + 'throw', + 'try', + 'while', + 'yield', + ], + ], + 'braces' => [ + 'position_after_anonymous_constructs' => 'next', + ], + 'cast_spaces' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'const' => 'one', + 'method' => 'one', + 'property' => 'only_if_meta', + ], + ], + 'class_definition' => true, + 'clean_namespace' => true, + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'combine_nested_dirname' => true, + 'compact_nullable_typehint' => true, + 'concat_space' => ['spacing' => 'one'], + 'constant_case' => true, + 'declare_equal_normalize' => ['space' => 'none'], + 'declare_strict_types' => true, + 'dir_constant' => true, + 'echo_tag_syntax' => true, + 'elseif' => true, + 'encoding' => true, + 'ereg_to_preg' => true, + 'explicit_indirect_variable' => true, + 'explicit_string_variable' => true, + 'fopen_flag_order' => true, + 'full_opening_tag' => true, + 'fully_qualified_strict_types' => true, + 'global_namespace_import' => [ + 'import_classes' => true, + 'import_constants' => false, + 'import_functions' => false, + ], + 'header_comment' => [ + 'header' => $header, + 'separate' => 'none', + ], + 'heredoc_to_nowdoc' => true, + 'implode_call' => true, + 'include' => true, + 'indentation_type' => true, + 'is_null' => true, + 'lambda_not_used_import' => true, + 'line_ending' => true, + 'list_syntax' => [ + 'syntax' => 'short', + ], + 'logical_operators' => true, + 'lowercase_cast' => true, + 'lowercase_keywords' => true, + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'magic_method_casing' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + ], + 'modernize_types_casting' => true, + 'multiline_comment_opening_closing' => true, + 'multiline_whitespace_before_semicolons' => true, + 'native_constant_invocation' => false, + 'native_function_casing' => false, + 'native_function_invocation' => false, + 'native_function_type_declaration_casing' => true, + 'no_alias_functions' => true, + 'no_alias_language_construct_call' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_blank_lines_before_namespace' => true, + 'no_break_comment' => true, + 'no_closing_tag' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_blank_lines' => true, + 'no_homoglyph_names' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_php4_constructor' => true, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_around_offset' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_superfluous_elseif' => true, + 'no_superfluous_phpdoc_tags' => [ + 'allow_mixed' => true, + ], + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_trailing_whitespace_in_string' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unneeded_curly_braces' => true, + 'no_unneeded_final_method' => true, + 'no_unreachable_default_argument_value' => true, + 'no_unset_cast' => true, + 'no_unset_on_property' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_useless_sprintf' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'nullable_type_declaration_for_default_null_value' => true, + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public_static', + 'property_protected_static', + 'property_private_static', + 'property_public', + 'property_protected', + 'property_private', + 'method_public_static', + 'construct', + 'destruct', + 'magic', + 'phpunit', + 'method_public', + 'method_protected', + 'method_private', + 'method_protected_static', + 'method_private_static', + ], + ], + 'ordered_imports' => true, + 'ordered_interfaces' => true, + 'ordered_traits' => true, + 'php_unit_dedicate_assert' => true, + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_method_casing' => [ + 'case' => 'camel_case', + ], + 'php_unit_test_annotation' => [ + 'style' => 'prefix', + ], + 'php_unit_test_case_static_method_calls' => [ + 'call_type' => 'this', + ], + 'phpdoc_align' => true, + 'phpdoc_annotation_without_dot' => true, + 'phpdoc_indent' => true, + 'phpdoc_inline_tag_normalizer' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_order' => true, + 'phpdoc_order_by_value' => [ + 'annotations' => [ + 'covers', + 'dataProvider', + 'throws', + 'uses', + ], + ], + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_tag_casing' => true, + 'phpdoc_tag_type' => true, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'phpdoc_types' => ['groups' => ['simple', 'meta']], + 'phpdoc_types_order' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_var_without_name' => true, + 'pow_to_exponentiation' => true, + 'protected_to_private' => true, + 'return_assignment' => true, + 'self_accessor' => true, + 'self_static_accessor' => true, + 'semicolon_after_instruction' => true, + 'set_type_to_cast' => true, + 'short_scalar_cast' => true, + 'simple_to_complex_string_variable' => true, + 'simplified_if_return' => true, + 'single_blank_line_at_eof' => true, + 'single_class_element_per_statement' => true, + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'single_line_throw' => true, + 'space_after_semicolon' => true, + 'standardize_increment' => true, + 'standardize_not_equals' => true, + 'static_lambda' => true, + 'strict_comparison' => true, + 'strict_param' => true, + 'string_line_ending' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'switch_continue_to_break' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_elvis_operator' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'visibility_required' => [ + 'elements' => [ + 'const', + 'method', + 'property', + ], + ], + 'void_return' => true, + 'whitespace_after_comma_in_array' => true, + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + 'less_and_greater' => false, + ], + ]); + +return $config; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ec38adf8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to the php-cypher-dsl project will be documented in this +file. A changelog has been kept from version 5.0.0 onwards. + +The format is based on [Keep a Changelog], and this project adheres to +[Semantic Versioning]. + +## 5.0.0 - To be determined + +### Added + +- Add a blank changelog. + +[keep a changelog]: https://keepachangelog.com/en/1.0.0/ +[semantic versioning]: https://semver.org/spec/v2.0.0.html diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..ed97c812 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,49 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating +documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to +fairly and consistently applying these principles to every aspect of managing +this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project maintainer at marijnvanwezel@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. Maintainers are +obligated to maintain confidentiality with regard to the reporter of an +incident. + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.3.0, available at +[https://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/3/0/ diff --git a/LIFECYCLE.md b/LIFECYCLE.md new file mode 100644 index 00000000..3249eabb --- /dev/null +++ b/LIFECYCLE.md @@ -0,0 +1,21 @@ +# Version lifecycle + +Since php-cypher-dsl version 5.0.0, support is governed by this version +lifecycle. + +**Bugfix support** means that issues will be fixed for a version of +php-cypher-dsl. Bugfix support for every major release `X` ends **six months** +after the release of the next major version (`X+1`). Bugfix support for minor +release `X.Y` will end with the release of the next minor version (`X.(Y+1)`). + +It is strongly recommended to use a version of php-cypher-dsl that has active +bugfix support. Versions that are no longer receiving bugfix support will not +receive any security or bug fixes. They may contain known critical +vulnerabilities and other major bugs, including the threat of possible data +loss and/or corruption. + +## Supported versions + +| Major version | PHP version | Initial release | End of bugfix support | +|------------------|-------------|------------------|-----------------------| +| php-cypher-dsl 5 | >=7.4 | To be determined | To be determined | diff --git a/README.md b/README.md index b7b37028..bb246a37 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,27 @@ # php-cypher-dsl -![Build status](https://github.com/WikibaseSolutions/php-cypher-dsl/actions/workflows/main.yml/badge.svg) +![Build +status](https://github.com/WikibaseSolutions/php-cypher-dsl/actions/workflows/main.yml/badge.svg) -The `php-cypher-dsl` library provides a way to construct advanced Cypher queries in an object-oriented and type-safe manner. +The `php-cypher-dsl` library provides a way to construct advanced Cypher +queries in an object-oriented and type-safe manner. ## Documentation -[The documentation can be found on the wiki here.](https://github.com/WikibaseSolutions/php-cypher-dsl/wiki) +[The documentation can be found on the wiki +here.](https://github.com/WikibaseSolutions/php-cypher-dsl/wiki) ## Installation ### Requirements -`php-cypher-dsl` requires PHP 7.4 or greater; using the latest version of PHP is highly recommended. +`php-cypher-dsl` requires PHP 7.4 or greater; using the latest version of PHP +is highly recommended. ### Installation through Composer -You can install `php-cypher-dsl` through composer by running the following command: +You can install `php-cypher-dsl` through composer by running the following +command: ``` composer require "wikibase-solutions/php-cypher-dsl" @@ -24,16 +29,18 @@ composer require "wikibase-solutions/php-cypher-dsl" ## Example -To construct a query to find all of Tom Hanks' co-actors, you can use the following code: +To construct a query to find all of Tom Hanks' co-actors, you can use the +following code: ```php -$tom = Query::node("Person")->withProperties(["name" => Query::literal("Tom Hanks")]); -$coActors = Query::node(); +use function WikibaseSolutions\CypherDSL\node; +use function WikibaseSolutions\CypherDSL\query; -$statement = Query::new() +$tom = node("Person")->withProperties(["name" => "Tom Hanks"]); +$coActors = node(); + +$statement = query() ->match($tom->relationshipTo(Query::node(), "ACTED_IN")->relationshipFrom($coActors, "ACTED_IN")) ->returning($coActors->property("name")) ->build(); - -$this->assertStringMatchesFormat("MATCH (:Person {name: 'Tom Hanks'})-[:`ACTED_IN`]->()<-[:`ACTED_IN`]-(%s) RETURN %s.name", $statement); ``` diff --git a/composer.json b/composer.json index 5cae1a87..4b82e6ae 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "name": "Marijn van Wezel", "email": "marijn@wikibase.nl", "homepage": "https://wikibase-solutions.com/author/marijn", - "role": "Developer" + "role": "Maintainer" }, { "name": "Wout Gevaert", @@ -48,14 +48,21 @@ "require": { "php": ">=7.4", "ext-ctype": "*", - "ext-openssl": "*" + "ext-openssl": "*", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php81": "^1.25" }, "require-dev": { "phpunit/phpunit": "~9.0", "infection/infection": "^0.25.5", - "friendsofphp/php-cs-fixer": "^3.0" + "friendsofphp/php-cs-fixer": "^3.0", + "rregeer/phpunit-coverage-check": "^0.3.1", + "phpstan/phpstan": "^1.8" }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { "WikibaseSolutions\\CypherDSL\\": "src/" } @@ -65,11 +72,13 @@ "WikibaseSolutions\\CypherDSL\\Tests\\": "tests/" } }, + "config": { + "allow-plugins": { + "infection/extension-installer": true + } + }, "scripts": { - "test": "phpunit tests/", - "infect": "XDEBUG_MODE=coverage infection --show-mutations", - "php-cs-fixer": - "php-cs-fixer fix --verbose --rules=@PSR12,align_multiline_comment,array_indentation,blank_line_before_statement,no_unused_imports,no_useless_else,no_useless_return,ordered_imports,phpdoc_scalar,return_assignment,simplified_if_return,trailing_comma_in_multiline" + "php-cs-fixer": "php-cs-fixer fix --config .php-cs-fixer.dist.php --verbose" }, "minimum-stability": "stable", "prefer-stable": true diff --git a/infection.json b/infection.json.dist similarity index 77% rename from infection.json rename to infection.json.dist index ca97ca21..4de978d7 100644 --- a/infection.json +++ b/infection.json.dist @@ -7,5 +7,6 @@ }, "mutators": { "@default": true - } + }, + "testFrameworkOptions": "--testsuite=unit" } \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..90f61d32 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + paths: + - src + level: 7 + treatPhpDocTypesAsCertain: false \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9ea10f48..d9399d2b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,30 @@ - + - - ./tests/Unit/ + + tests/unit - - ./tests/ + + tests/end-to-end + + + tests/integration + + + + + + + src + + + src/functions.php + + \ No newline at end of file diff --git a/src/Addition.php b/src/Addition.php deleted file mode 100644 index eb48ed33..00000000 --- a/src/Addition.php +++ /dev/null @@ -1,51 +0,0 @@ -original = $original; - $this->variable = $variable; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return sprintf("%s AS %s", $this->original->toQuery(), $this->variable->toQuery()); - } - - /** - * Gets the original item of the alias. - * - * @return AnyType - */ - public function getOriginal(): AnyType - { - return $this->original; - } - - /** - * Gets the variable from the alias. - * - * @return Variable - */ - public function getVariable(): Variable - { - return $this->variable; - } -} diff --git a/src/AndOperator.php b/src/AndOperator.php deleted file mode 100644 index 7c130386..00000000 --- a/src/AndOperator.php +++ /dev/null @@ -1,51 +0,0 @@ -mutate = $mutate; - - return $this; - } - - /** - * Returns whether the assignment uses property mutation instead of replacement. - * - * @return bool - */ - public function mutates(): bool - { - return $this->mutate; - } - - /** - * @inheritDoc - */ - protected function getOperator(): string - { - return $this->mutate ? "+=" : "="; - } -} diff --git a/src/BinaryOperator.php b/src/BinaryOperator.php deleted file mode 100644 index 78113892..00000000 --- a/src/BinaryOperator.php +++ /dev/null @@ -1,112 +0,0 @@ -left = $left; - $this->right = $right; - $this->insertParentheses = $insertParentheses; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return sprintf( - $this->insertParentheses ? "(%s %s %s)" : "%s %s %s", - $this->left->toQuery(), - $this->getOperator(), - $this->right->toQuery() - ); - } - - /** - * Returns the operator. For instance, this function would return "+" for the addition operator. - * - * @return string - */ - abstract protected function getOperator(): string; - - /** - * Gets the left-hand of the expression. - * - * @return AnyType - */ - public function getLeft(): AnyType - { - return $this->left; - } - - /** - * Gets the right-hand of the expression. - * - * @return AnyType - */ - public function getRight(): AnyType - { - return $this->right; - } - - /** - * Returns whether or not the operator inserts parenthesis. - * - * @return bool - */ - public function insertsParentheses(): bool - { - return $this->insertParentheses; - } -} diff --git a/src/Clauses/CallClause.php b/src/Clauses/CallClause.php index ce64cb57..cc8b11c8 100644 --- a/src/Clauses/CallClause.php +++ b/src/Clauses/CallClause.php @@ -1,48 +1,97 @@ -subQuery = $subQuery; + + return $this; } /** - * Returns the query that is being called. + * Add one or more variables to include in the WITH clause. + * + * @param Pattern|string|Variable ...$variables + * + * @return $this * - * @return Query + * @see https://neo4j.com/docs/cypher-manual/current/clauses/call-subquery/#subquery-correlated-importing */ - public function getSubQuery(): Query + public function addWithVariable(...$variables): self { - return $this->subQuery; + $res = []; + + foreach ($variables as $variable) { + $res[] = self::toVariable($variable); + } + + $this->withVariables = array_merge($this->withVariables, $res); + + return $this; } - public function toQuery(): string + /** + * Returns the query that is being called. This query does not include the WITH clause that is inserted + * if there are any correlated variables. + */ + public function getSubQuery(): ?Query { - $subQuery = trim($this->subQuery->toQuery()); - - if ($subQuery === '') { - return ''; - } + return $this->subQuery; + } - return sprintf('CALL { %s }', $subQuery); + /** + * Returns the variables that will be included in the WITH clause. + * + * @return Variable[] + */ + public function getWithVariables(): array + { + return $this->withVariables; } /** @@ -50,7 +99,21 @@ public function toQuery(): string */ protected function getSubject(): string { - return '{ ' . $this->subQuery->toQuery() . ' }'; + if (!isset($this->subQuery)) { + return ''; + } + + $subQuery = $this->subQuery->build(); + + if ($subQuery === '') { + return ''; + } + + if ($this->withVariables !== []) { + $subQuery = Query::new()->with($this->withVariables)->toQuery() . ' ' . $subQuery; + } + + return sprintf('{ %s }', $subQuery); } /** diff --git a/src/Clauses/CallProcedureClause.php b/src/Clauses/CallProcedureClause.php index e29b2c21..788909b5 100644 --- a/src/Clauses/CallProcedureClause.php +++ b/src/Clauses/CallProcedureClause.php @@ -1,65 +1,50 @@ -procedure = $procedure; @@ -67,82 +52,43 @@ public function setProcedure(string $procedure): self } /** - * Returns the variables to yield. - * - * @return Variable[] - */ - public function getYieldVariables(): array - { - return $this->yieldVariables; - } - - /** - * Returns the procedure to call. + * Adds a variable to yield. * - * @return string|null - */ - public function getProcedure(): ?string - { - return $this->procedure; - } - - /** - * Returns the arguments of the procedure. + * @param Alias|string|Variable $yields The variable to yield * - * @return AnyType[] + * @return $this */ - public function getArguments(): array + public function addYield(...$yields): self { - return $this->arguments; - } + $res = []; - /** - * Sets the arguments to pass to this procedure call. This overwrites any previously passed - * arguments. - * - * @param AnyType[] $arguments - * @return CallProcedureClause - */ - public function withArguments(array $arguments): self - { - foreach ($arguments as $argument) { - $this->assertClass('argument', AnyType::class, $argument); + foreach ($yields as $yield) { + $res[] = $yield instanceof Alias ? $yield : self::toName($yield); } - $this->arguments = $arguments; + $this->yields = array_merge($this->yields, $res); return $this; } /** - * Add an argument to pass to this procedure call. + * Returns the procedure to call. * - * @param AnyType $argument - * @return CallProcedureClause + * @return Procedure */ - public function addArgument(AnyType $argument): self + public function getProcedure(): ?Procedure { - $this->arguments[] = $argument; - - return $this; + return $this->procedure; } /** - * Used to explicitly select which available result fields are returned as newly-bound - * variables. + * Returns the variables to yield. * - * @param Variable[] $variables - * @return CallProcedureClause + * @return Alias[]|Variable[]|(Alias|Variable)[] */ - public function yields(array $variables): self + public function getYields(): array { - foreach ($variables as $variable) { - $this->assertClass('variable', Variable::class, $variable); - } - - $this->yieldVariables = $variables; - - return $this; + return $this->yields; } /** @@ -162,20 +108,13 @@ protected function getSubject(): string return ""; } - $arguments = implode( - ", ", - array_map(fn (AnyType $pattern): string => $pattern->toQuery(), $this->arguments) - ); - - if (count($this->yieldVariables) > 0) { - $yieldParameters = implode( - ", ", - array_map(fn (Variable $variable): string => $variable->toQuery(), $this->yieldVariables) - ); + $subject = $this->procedure->toQuery(); - return sprintf("%s(%s) YIELD %s", $this->procedure, $arguments, $yieldParameters); + if (!empty($this->yields)) { + $yields = array_map(static fn ($variableOrAlias): string => $variableOrAlias->toQuery(), $this->yields); + $subject .= sprintf(" YIELD %s", implode(", ", $yields)); } - return sprintf("%s(%s)", $this->procedure, $arguments); + return $subject; } } diff --git a/src/Clauses/Clause.php b/src/Clauses/Clause.php index 9c2678c5..3aeceeef 100644 --- a/src/Clauses/Clause.php +++ b/src/Clauses/Clause.php @@ -1,40 +1,41 @@ -getClause() === "") { + // If we have an empty clause (for example, for RAW queries), return nothing at all + return ""; + } + if ($this->getSubject() === "") { // If we have an empty subject, either return the empty clause, or nothing at all return $this->canBeEmpty() ? $this->getClause() : ""; @@ -47,25 +48,11 @@ public function toQuery(): string * Returns the subject of this object. The subject is anything after * the clause. For example, in the partial query "MATCH (a)", the subject * would be "(a)". - * - * @return string */ abstract protected function getSubject(): string; - /** - * Returns whether this clause is still valid if it has an empty subject. - * - * @return bool - */ - public function canBeEmpty(): bool - { - return false; - } - /** * Returns the clause this object describes. For instance "MATCH". - * - * @return string */ abstract protected function getClause(): string; } diff --git a/src/Clauses/CreateClause.php b/src/Clauses/CreateClause.php index 1cd294a5..bb4f460f 100644 --- a/src/Clauses/CreateClause.php +++ b/src/Clauses/CreateClause.php @@ -1,62 +1,54 @@ -assertClass('pattern', [PathType::class, NodeType::class], $pattern); - $this->patterns[] = $pattern; + $this->patterns = array_merge($this->patterns, $pattern); return $this; } /** - * Returns the patterns of the create clause. + * Returns the patterns of the CREATE clause. * - * @return (PathType|NodeType)[] + * @return CompletePattern[] */ public function getPatterns(): array { @@ -78,7 +70,7 @@ protected function getSubject(): string { return implode( ", ", - array_map(fn ($pattern): string => $pattern->toQuery(), $this->patterns) + array_map(static fn (CompletePattern $pattern): string => $pattern->toQuery(), $this->patterns) ); } } diff --git a/src/Clauses/DeleteClause.php b/src/Clauses/DeleteClause.php index 9ab48bf7..ecf8c759 100644 --- a/src/Clauses/DeleteClause.php +++ b/src/Clauses/DeleteClause.php @@ -1,55 +1,49 @@ -variables[] = $variable; + $res = []; + + foreach ($structures as $structure) { + $res[] = self::toStructuralType($structure); + } + + $this->structures = array_merge($this->structures, $res); return $this; } /** * Returns whether the deletion detaches the relationships. - * - * @return bool */ public function detachesDeletion(): bool { @@ -82,13 +81,13 @@ public function detachesDeletion(): bool } /** - * Returns the variables to delete. + * Returns the structures to delete. * - * @return Variable[] + * @return StructuralType[] */ - public function getVariables(): array + public function getStructural(): array { - return $this->variables; + return $this->structures; } /** @@ -110,7 +109,7 @@ protected function getSubject(): string { return implode( ", ", - array_map(fn (Variable $variable) => $variable->toQuery(), $this->variables) + array_map(static fn (StructuralType $structure) => $structure->toQuery(), $this->structures) ); } } diff --git a/src/Clauses/LimitClause.php b/src/Clauses/LimitClause.php index db747c4b..2819eda2 100644 --- a/src/Clauses/LimitClause.php +++ b/src/Clauses/LimitClause.php @@ -1,63 +1,56 @@ -limit; + $this->limit = self::toIntegerType($limit); + + return $this; } /** - * Sets the expression that returns the limit. - * - * @param NumeralType $limit The limit - * @return LimitClause + * Returns the limit of the clause. */ - public function setLimit(NumeralType $limit): self + public function getLimit(): ?IntegerType { - $this->limit = $limit; - - return $this; + return $this->limit; } /** diff --git a/src/Clauses/MatchClause.php b/src/Clauses/MatchClause.php index b87021a8..48cfc80d 100644 --- a/src/Clauses/MatchClause.php +++ b/src/Clauses/MatchClause.php @@ -1,66 +1,54 @@ -patterns; + $this->patterns = array_merge($this->patterns, $pattern); + + return $this; } /** - * Add a pattern to the match clause. + * Returns the patterns to match. * - * @param PathType|NodeType $pattern - * @return MatchClause + * @return CompletePattern[] */ - public function addPattern($pattern): self + public function getPatterns(): array { - $this->assertClass('pattern', [PathType::class, NodeType::class], $pattern); - $this->patterns[] = $pattern; - - return $this; + return $this->patterns; } /** @@ -78,7 +66,7 @@ protected function getSubject(): string { return implode( ", ", - array_map(fn ($pattern): string => $pattern->toQuery(), $this->patterns) + array_map(static fn (CompletePattern $pattern): string => $pattern->toQuery(), $this->patterns) ); } } diff --git a/src/Clauses/MergeClause.php b/src/Clauses/MergeClause.php index 6ed75d29..6eb4dd07 100644 --- a/src/Clauses/MergeClause.php +++ b/src/Clauses/MergeClause.php @@ -1,94 +1,51 @@ -createClause; - } + private ?SetClause $createClause = null; /** - * Returns the clause to execute when the pattern is matched. - * - * @return Clause|null + * @var null|SetClause The clause to execute when the pattern is matched */ - public function getOnMatchClause(): ?Clause - { - return $this->matchClause; - } - - /** - * Returns the pattern to MERGE. - * - * @return PathType|NodeType|null - */ - public function getPattern(): ?StructuralType - { - return $this->pattern; - } + private ?SetClause $matchClause = null; /** * Sets the pattern to merge. * - * @param PathType|NodeType $pattern The pattern to merge - * @return MergeClause + * @param CompletePattern $pattern The pattern to merge + * + * @return $this */ - public function setPattern($pattern): self + public function setPattern(CompletePattern $pattern): self { - $this->assertClass('pattern', [PathType::class, NodeType::class], $pattern); $this->pattern = $pattern; return $this; @@ -99,10 +56,9 @@ public function setPattern($pattern): self * * @see https://neo4j.com/docs/cypher-manual/current/clauses/merge/#merge-merge-with-on-create * - * @param Clause $createClause - * @return MergeClause + * @return $this */ - public function setOnCreate(Clause $createClause): self + public function setOnCreate(?SetClause $createClause): self { $this->createClause = $createClause; @@ -114,16 +70,39 @@ public function setOnCreate(Clause $createClause): self * * @see https://neo4j.com/docs/cypher-manual/current/clauses/merge/#merge-merge-with-on-match * - * @param Clause $matchClause - * @return MergeClause + * @return $this */ - public function setOnMatch(Clause $matchClause): self + public function setOnMatch(?SetClause $matchClause): self { $this->matchClause = $matchClause; return $this; } + /** + * Returns the pattern to MERGE. + */ + public function getPattern(): ?CompletePattern + { + return $this->pattern; + } + + /** + * Returns the clause to execute when the pattern is matched. + */ + public function getOnCreateClause(): ?SetClause + { + return $this->createClause; + } + + /** + * Returns the clause to execute when the pattern is matched. + */ + public function getOnMatchClause(): ?SetClause + { + return $this->matchClause; + } + /** * @inheritDoc */ diff --git a/src/Clauses/OptionalMatchClause.php b/src/Clauses/OptionalMatchClause.php index a8b4fc9e..fb297ac9 100644 --- a/src/Clauses/OptionalMatchClause.php +++ b/src/Clauses/OptionalMatchClause.php @@ -1,66 +1,54 @@ -patterns; + $this->patterns = array_merge($this->patterns, $pattern); + + return $this; } /** - * Add a pattern to the optional match clause. + * Returns the patterns to optionally match. * - * @param PathType|NodeType $pattern - * @return OptionalMatchClause + * @return CompletePattern[] */ - public function addPattern($pattern): self + public function getPatterns(): array { - $this->assertClass('pattern', [NodeType::class, PathType::class], $pattern); - $this->patterns[] = $pattern; - - return $this; + return $this->patterns; } /** @@ -78,7 +66,7 @@ protected function getSubject(): string { return implode( ", ", - array_map(fn ($pattern): string => $pattern->toQuery(), $this->patterns) + array_map(static fn (CompletePattern $pattern): string => $pattern->toQuery(), $this->patterns) ); } } diff --git a/src/Clauses/OrderByClause.php b/src/Clauses/OrderByClause.php index 2e23176a..d36f3c74 100644 --- a/src/Clauses/OrderByClause.php +++ b/src/Clauses/OrderByClause.php @@ -1,58 +1,63 @@ -properties[] = $property; + $this->properties = array_merge($this->properties, $property); + + return $this; + } + + /** + * Set to sort in a DESCENDING order. + * + * @return OrderByClause + */ + public function setDescending(bool $descending = true): self + { + $this->descending = $descending; return $this; } @@ -69,27 +74,12 @@ public function getProperties(): array /** * Returns whether the ordering is in descending order. - * - * @return bool */ public function isDescending(): bool { return $this->descending; } - /** - * Set to sort in a DESCENDING order. - * - * @param bool $descending - * @return OrderByClause - */ - public function setDescending(bool $descending = true): self - { - $this->descending = $descending; - - return $this; - } - /** * @inheritDoc */ @@ -103,9 +93,13 @@ protected function getClause(): string */ protected function getSubject(): string { - $properties = array_map(fn (Property $property): string => $property->toQuery(), $this->properties); + $properties = array_map(static fn (Property $property): string => $property->toQuery(), $this->properties); $subject = implode(", ", $properties); - return $this->descending ? sprintf("%s DESCENDING", $subject) : $subject; + if ($this->descending) { + $subject .= ' DESCENDING'; + } + + return $subject; } } diff --git a/src/Clauses/RawClause.php b/src/Clauses/RawClause.php index bb038511..d40d79b3 100644 --- a/src/Clauses/RawClause.php +++ b/src/Clauses/RawClause.php @@ -1,30 +1,21 @@ -clause = $clause; $this->subject = $subject; diff --git a/src/Clauses/RemoveClause.php b/src/Clauses/RemoveClause.php index b17176ba..af9850ab 100644 --- a/src/Clauses/RemoveClause.php +++ b/src/Clauses/RemoveClause.php @@ -1,68 +1,58 @@ -expressions; + self::assertClassArray('expressions', [Property::class, Label::class], $expressions); + $this->expressions = array_merge($this->expressions, $expressions); + + return $this; } /** - * Add an expression to the REMOVE clause. This expression usually returns a property (a.b) or a label (a:b). + * Returns the expressions in the REMOVE clause. * - * @param Property|Label $expression The expression to add - * @return RemoveClause + * @return Label[]|Property[]|(Label|Property)[] */ - public function addExpression($expression): self + public function getExpressions(): array { - $this->assertClass('expression', [Property::class, Label::class], $expression); - - $this->expressions[] = $expression; - - return $this; + return $this->expressions; } /** @@ -80,7 +70,7 @@ protected function getSubject(): string { return implode( ", ", - array_map(fn (QueryConvertable $expression) => $expression->toQuery(), $this->expressions) + array_map(static fn (QueryConvertible $expression) => $expression->toQuery(), $this->expressions) ); } } diff --git a/src/Clauses/ReturnClause.php b/src/Clauses/ReturnClause.php index c2dc6cbe..aec1ce24 100644 --- a/src/Clauses/ReturnClause.php +++ b/src/Clauses/ReturnClause.php @@ -1,37 +1,32 @@ -columns = array_merge($this->columns, $res); + + return $this; + } + + /** + * Sets this query to only return unique rows. + * + * @return $this + * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/return/#return-unique-results */ public function setDistinct(bool $distinct = true): self { @@ -60,7 +77,7 @@ public function setDistinct(bool $distinct = true): self /** * Returns the columns to return. Aliased columns have string keys instead of integers. * - * @return AnyType[] + * @return Alias[]|AnyType[]|(Alias|AnyType)[] */ public function getColumns(): array { @@ -69,34 +86,12 @@ public function getColumns(): array /** * Returns whether the returned results are distinct. - * - * @return bool */ public function isDistinct(): bool { return $this->distinct; } - /** - * Add a new column to this RETURN clause. - * - * @param AnyType $column The expression to return - * @param string $alias The alias of this column - * - * @return ReturnClause - * @see https://neo4j.com/docs/cypher-manual/current/clauses/return/#return-column-alias - */ - public function addColumn(AnyType $column, string $alias = ""): self - { - if ($alias !== "") { - $this->columns[$alias] = $column; - } else { - $this->columns[] = $column; - } - - return $this; - } - /** * @inheritDoc */ @@ -112,15 +107,9 @@ protected function getClause(): string */ protected function getSubject(): string { - $expressions = []; - - foreach ($this->columns as $alias => $expression) { - $expressionQuery = $expression->toQuery(); - $expressions[] = is_int($alias) ? - $expressionQuery : - sprintf("%s AS %s", $expressionQuery, $this->escape($alias)); - } - - return implode(", ", $expressions); + return implode( + ", ", + array_map(static fn (QueryConvertible $column) => $column->toQuery(), $this->columns) + ); } } diff --git a/src/Clauses/SetClause.php b/src/Clauses/SetClause.php index 3975e01c..6c88d014 100644 --- a/src/Clauses/SetClause.php +++ b/src/Clauses/SetClause.php @@ -1,68 +1,58 @@ -expressions; + $this->assertClassArray('expression', [PropertyReplacement::class, Label::class], $expressions); + $this->expressions = array_merge($this->expressions, $expressions); + + return $this; } /** - * Add an assignment. + * Returns the expressions to SET. * - * @param Assignment|Label $expression The assignment to execute - * @return SetClause + * @return Label[]|PropertyReplacement[]|(Label|PropertyReplacement)[] */ - public function addAssignment($expression): self + public function getExpressions(): array { - $this->assertClass('expression', [Assignment::class, Label::class], $expression); - - $this->expressions[] = $expression; - - return $this; + return $this->expressions; } /** @@ -80,7 +70,7 @@ protected function getSubject(): string { return implode( ", ", - array_map(fn (QueryConvertable $expression): string => $expression->toQuery(), $this->expressions) + array_map(static fn (QueryConvertible $expression): string => $expression->toQuery(), $this->expressions) ); } } diff --git a/src/Clauses/SkipClause.php b/src/Clauses/SkipClause.php index 88a7035b..4e544fe7 100644 --- a/src/Clauses/SkipClause.php +++ b/src/Clauses/SkipClause.php @@ -1,61 +1,51 @@ -skip = $skip; + $this->skip = self::toIntegerType($skip); return $this; } /** * Returns the amount to skip. - * - * @return NumeralType|null */ - public function getSkip(): ?NumeralType + public function getSkip(): ?IntegerType { return $this->skip; } diff --git a/src/Clauses/UnionClause.php b/src/Clauses/UnionClause.php index 73090e02..79b99317 100644 --- a/src/Clauses/UnionClause.php +++ b/src/Clauses/UnionClause.php @@ -1,55 +1,48 @@ -all = $all; - } + private bool $all = false; /** - * Combines two queries with a union. + * Sets that the union should include all results, instead of removing duplicates. * - * @param Query $left The query preceding the union clause. - * @param Query $right The query after the union clause. - * @param bool $all Whether the union should include all results or remove the duplicates instead. + * @param bool $all Whether the union should include all results or remove the duplicates instead + * + * @return static */ - public static function union(Query $left, Query $right, bool $all = false): Query + public function setAll(bool $all = true): self { - $tbr = Query::new(); - - foreach ($left->getClauses() as $clause) { - $tbr->addClause($clause); - } - - $tbr->addClause(new self($all)); - - foreach ($right->getClauses() as $clause) { - $tbr->addClause($clause); - } + $this->all = $all; - return $tbr; + return $this; } /** * Returns whether the union includes all results or removes the duplicates instead. - * - * @return bool */ public function includesAll(): bool { @@ -67,16 +60,16 @@ public function canBeEmpty(): bool /** * @inheritDoc */ - protected function getSubject(): string + protected function getClause(): string { - return $this->all ? 'ALL' : ''; + return 'UNION'; } /** * @inheritDoc */ - protected function getClause(): string + protected function getSubject(): string { - return 'UNION'; + return $this->all ? 'ALL' : ''; } } diff --git a/src/Clauses/WhereClause.php b/src/Clauses/WhereClause.php index b6524753..818d7a76 100644 --- a/src/Clauses/WhereClause.php +++ b/src/Clauses/WhereClause.php @@ -1,61 +1,76 @@ -expression = $expression; + $expression = self::toBooleanType($expression); + + if ($operator !== self::AND && $operator !== self::OR && $operator !== self::XOR) { + throw new InvalidArgumentException('$operator must either be "and", "xor" or "or"'); + } + + if ($this->expression === null) { + $this->expression = $expression; + } elseif ($operator === self::AND) { + $this->expression = $this->expression->and($expression); + } elseif ($operator === self::OR) { + $this->expression = $this->expression->or($expression); + } elseif ($operator === self::XOR) { + $this->expression = $this->expression->xor($expression); + } return $this; } /** * Returns the expression to match. - * - * @return Label|BooleanType|null */ - public function getExpression() + public function getExpression(): ?BooleanType { return $this->expression; } diff --git a/src/Clauses/WithClause.php b/src/Clauses/WithClause.php index f26c4cb4..5783c91d 100644 --- a/src/Clauses/WithClause.php +++ b/src/Clauses/WithClause.php @@ -1,70 +1,68 @@ -expressions[$alias] = $expression; - } else { - $this->expressions[] = $expression; + $res = []; + + foreach ($entries as $entry) { + $res[] = $entry instanceof Alias ? $entry : self::toVariable($entry); } + $this->entries = array_merge($this->entries, $res); + return $this; } /** * Returns the expression to include in the clause. * - * @return array + * @return Alias[]|Variable[]|(Alias|Variable)[] */ - public function getExpressions(): array + public function getEntries(): array { - return $this->expressions; + return $this->entries; } /** @@ -80,15 +78,9 @@ protected function getClause(): string */ protected function getSubject(): string { - $expressions = []; - - foreach ($this->expressions as $alias => $expression) { - $expressionQuery = $expression->toQuery(); - $expressions[] = is_int($alias) ? - $expressionQuery : - sprintf("%s AS %s", $expressionQuery, $this->escape($alias)); - } - - return implode(", ", $expressions); + return implode( + ", ", + array_map(static fn (QueryConvertible $expression) => $expression->toQuery(), $this->entries) + ); } } diff --git a/src/Contains.php b/src/Contains.php deleted file mode 100644 index 82f1dc16..00000000 --- a/src/Contains.php +++ /dev/null @@ -1,52 +0,0 @@ -match = $match; - $this->where = $where; - } - - /** - * Returns the MATCH part of the EXISTS expression. - * - * @return MatchClause - */ - public function getMatch(): MatchClause - { - return $this->match; - } - - /** - * Returns the WHERE part of the expression. - * - * @return WhereClause|null - */ - public function getWhere(): ?WhereClause - { - return $this->where; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - if (isset($this->where)) { - return sprintf("EXISTS { %s %s }", $this->match->toQuery(), $this->where->toQuery()); - } - - return sprintf("EXISTS { %s }", $this->match->toQuery()); - } -} diff --git a/src/Exponentiation.php b/src/Exponentiation.php deleted file mode 100644 index aea88f84..00000000 --- a/src/Exponentiation.php +++ /dev/null @@ -1,51 +0,0 @@ -assertClass('expression', AnyType::class, $expression); - } - - $this->expressions = $expressions; - } - - /** - * The homogeneous list of expressions. - * - * @return AnyType[] - */ - public function getExpressions(): array - { - return $this->expressions; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - $expressions = array_map( - fn (AnyType $expression): string => $expression->toQuery(), - $this->expressions - ); - - return sprintf("[%s]", implode(", ", $expressions)); - } -} diff --git a/src/Expressions/Exists.php b/src/Expressions/Exists.php new file mode 100644 index 00000000..72153975 --- /dev/null +++ b/src/Expressions/Exists.php @@ -0,0 +1,96 @@ +match = $match; + $this->where = $where; + $this->insertParentheses = $insertParentheses; + } + + /** + * Returns the MATCH part of the EXISTS expression. + */ + public function getMatch(): MatchClause + { + return $this->match; + } + + /** + * Returns the WHERE part of the expression. + */ + public function getWhere(): ?WhereClause + { + return $this->where; + } + + /** + * Returns whether it inserts parentheses around the expression. + */ + public function insertsParentheses(): bool + { + return $this->insertParentheses; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + if (isset($this->where)) { + return sprintf( + $this->insertParentheses ? "(EXISTS { %s %s })" : "EXISTS { %s %s }", + $this->match->toQuery(), + $this->where->toQuery() + ); + } + + return sprintf($this->insertParentheses ? "(EXISTS { %s })" : "EXISTS { %s }", $this->match->toQuery()); + } +} diff --git a/src/Expressions/Label.php b/src/Expressions/Label.php new file mode 100644 index 00000000..d3d11d92 --- /dev/null +++ b/src/Expressions/Label.php @@ -0,0 +1,95 @@ +variable = $variable; + $this->labels = array_map([self::class, 'escape'], $labels); + } + + /** + * Adds one or more labels to this class. + * + * @param string ...$labels One or more labels to add + * + * @return $this + */ + public function addLabels(string ...$labels): self + { + $this->labels = array_merge($this->labels, array_map([self::class, 'escape'], $labels)); + + return $this; + } + + /** + * Returns the escaped labels in this class. + * + * @return string[] + */ + public function getLabels(): array + { + return $this->labels; + } + + /** + * Returns the variable to which the labels are attached. + */ + public function getVariable(): Variable + { + return $this->variable; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + $query = $this->variable->toQuery(); + + foreach ($this->labels as $label) { + $query = sprintf("%s:%s", $query, $label); + } + + return $query; + } +} diff --git a/src/Expressions/Literals/Boolean.php b/src/Expressions/Literals/Boolean.php new file mode 100644 index 00000000..08523b9d --- /dev/null +++ b/src/Expressions/Literals/Boolean.php @@ -0,0 +1,52 @@ +value = $value; + } + + /** + * Returns the boolean value. + */ + public function getValue(): bool + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + return $this->value ? "true" : "false"; + } +} diff --git a/src/Expressions/Literals/Float_.php b/src/Expressions/Literals/Float_.php new file mode 100644 index 00000000..3c579f9f --- /dev/null +++ b/src/Expressions/Literals/Float_.php @@ -0,0 +1,58 @@ +value = $value; + } + + /** + * Returns the integer value. + */ + public function getValue(): float + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + $value = (string) $this->value; + + if (ctype_digit($value) || ($value[0] === '-' && ctype_digit(substr($value, 1)))) { + $value .= '.0'; + } + + return $value; + } +} diff --git a/src/Expressions/Literals/Integer.php b/src/Expressions/Literals/Integer.php new file mode 100644 index 00000000..f8f8ad37 --- /dev/null +++ b/src/Expressions/Literals/Integer.php @@ -0,0 +1,67 @@ +value = $parsedValue; + } + + /** + * Returns a string representation of the integer value. + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + return $this->value; + } +} diff --git a/src/Expressions/Literals/List_.php b/src/Expressions/Literals/List_.php new file mode 100644 index 00000000..949bb9dc --- /dev/null +++ b/src/Expressions/Literals/List_.php @@ -0,0 +1,90 @@ +expressions = array_map([self::class, 'toAnyType'], $expressions); + } + + /** + * Add one or more expressions to the list. + * + * @param AnyType|bool|float|int|mixed[]|Pattern|string ...$expressions + * + * @return $this + */ + public function addExpression(...$expressions): self + { + $this->expressions = array_merge($this->expressions, array_map([self::class, 'toAnyType'], $expressions)); + + return $this; + } + + /** + * The homogeneous list of expressions. + * + * @return AnyType[] + */ + public function getExpressions(): array + { + return $this->expressions; + } + + /** + * Returns whether this list is empty. + */ + public function isEmpty(): bool + { + return empty($this->expressions); + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + $expressions = array_map( + static fn (AnyType $expression): string => $expression->toQuery(), + $this->expressions + ); + + return sprintf("[%s]", implode(", ", $expressions)); + } +} diff --git a/src/Expressions/Literals/Literal.php b/src/Expressions/Literals/Literal.php new file mode 100644 index 00000000..bea5576d --- /dev/null +++ b/src/Expressions/Literals/Literal.php @@ -0,0 +1,889 @@ +__toString()); + } + + throw new TypeError("The literal type " . get_debug_type($literal) . " is not supported by Cypher"); + } + + /** + * Creates a new numeral literal. + * + * @param float|int $value + * + * @return Float_|Integer + * + * @see Literal::number() + * @deprecated + */ + public static function decimal($value) + { + return self::number($value); + } + + /** + * Creates a new numeral literal. + * + * @param float|int $value + * + * @return Float_|Integer + */ + public static function number($value) + { + self::assertClass('value', ['int', 'float'], $value); + + // @phpstan-ignore-next-line PHPStan does not work well with classes named "Integer" + return is_int($value) ? self::integer($value) : self::float($value); + } + + /** + * Creates a new boolean. + */ + public static function boolean(bool $value): Boolean + { + // PhpStorm warns about a type error here, this is a bug. + // @see https://youtrack.jetbrains.com/issue/WI-39239 + return new Boolean($value); + } + + /** + * Creates a new string. + */ + public static function string(string $value): String_ + { + return new String_($value); + } + + /** + * Creates a new integer. + */ + public static function integer(int $value): Integer + { + // PhpStorm warns about a type error here, this is a bug. + // @see https://youtrack.jetbrains.com/issue/WI-39239 + return new Integer($value); + } + + /** + * Creates a new float. + */ + public static function float(float $value): Float_ + { + return new Float_($value); + } + + /** + * Creates a new list literal. + * + * @param mixed[] $value + */ + public static function list(iterable $value): List_ + { + if ($value instanceof Traversable) { + $value = iterator_to_array($value); + } + + return new List_($value); + } + + /** + * Creates a new map literal. + * + * @param mixed[] $value + */ + public static function map(array $value): Map + { + return new Map($value); + } + + /** + * Retrieves the current Date value, optionally for a different time zone. In reality, this function just returns + * a call to the "date()" function. + * + * @param null|string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-date-current Corresponding documentation on Neo4j.com + */ + public static function date($timezone = null): Date + { + if ($timezone === null) { + return Procedure::date(); + } + + return Procedure::date(["timezone" => $timezone]); + } + + /** + * Creates a date from the given year, month and day. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $month + * @param null|int|NumeralType $day + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-date-calendar Corresponding documentation on Neo4j.com + */ + public static function dateYMD($year, $month = null, $day = null): Date + { + return Procedure::date( + self::makeTemporalMap( + [ + "year" => $year, + "month" => $month, + "day" => $day, + ] + ) + ); + } + + /** + * Creates a date from the given year, week and weekday. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $week + * @param null|int|NumeralType $weekday + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-date-week Corresponding documentation on Neo4j.com + */ + public static function dateYWD($year, $week = null, $weekday = null): Date + { + return Procedure::date( + self::makeTemporalMap( + [ + "year" => $year, + "week" => $week, + "dayOfWeek" => $weekday, + ] + ) + ); + } + + /** + * Creates a date from the given string. + * + * @param string|StringType $date + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-date-create-string Corresponding documentation on Neo4j.com + */ + public static function dateString($date): Date + { + return Procedure::date($date); + } + + /** + * Retrieves the current DateTime value, optionally for a different time zone. In reality, this + * function just returns a call to the "datetime()" function. + * + * @param string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-current Corresponding documentation on Neo4j.com + */ + public static function dateTime($timezone = null): DateTime + { + if ($timezone === null) { + return Procedure::datetime(); + } + + return Procedure::datetime(["timezone" => $timezone]); + } + + /** + * Creates a date from the given year, month, day and time values. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $month + * @param null|int|NumeralType $day + * @param null|int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * @param null|string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-calendar Corresponding documentation on Neo4j.com + */ + public static function dateTimeYMD( + $year, + $month = null, + $day = null, + $hour = null, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null, + $timezone = null + ): DateTime { + return Procedure::datetime( + self::makeTemporalMap( + [ + "year" => $year, + "month" => $month, + "day" => $day, + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + "timezone" => $timezone, + ] + ) + ); + } + + /** + * Creates a datetime with the specified year, week, dayOfWeek, hour, minute, second, millisecond, microsecond, nanosecond and timezone component values. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $week + * @param null|int|NumeralType $dayOfWeek + * @param null|int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * @param null|string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-week Corresponding documentation on Neo4j.com + */ + public static function dateTimeYWD( + $year, + $week = null, + $dayOfWeek = null, + $hour = null, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null, + $timezone = null + ): DateTime { + return Procedure::datetime( + self::makeTemporalMap( + [ + "year" => $year, + "week" => $week, + "dayOfWeek" => $dayOfWeek, + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + "timezone" => $timezone, + ] + ) + ); + } + + /** + * Creates a datetime with the specified year, quarter, dayOfQuarter, hour, minute, second, millisecond, microsecond, nanosecond and timezone component values. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $quarter + * @param null|int|NumeralType $dayOfQuarter + * @param null|int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * @param null|string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-quarter Corresponding documentation on Neo4j.com + */ + public static function dateTimeYQD( + $year, + $quarter = null, + $dayOfQuarter = null, + $hour = null, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null, + $timezone = null + ): DateTime { + return Procedure::datetime( + self::makeTemporalMap( + [ + "year" => $year, + "quarter" => $quarter, + "dayOfQuarter" => $dayOfQuarter, + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + "timezone" => $timezone, + ] + ) + ); + } + + /** + * Creates a datetime with the specified year, ordinalDay, hour, minute, second, millisecond, microsecond, nanosecond and timezone component values. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $ordinalDay + * @param null|int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * @param null|string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-ordinal Corresponding documentation on Neo4j.com + */ + public static function dateTimeYD( + $year, + $ordinalDay = null, + $hour = null, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null, + $timezone = null + ): DateTime { + return Procedure::datetime( + self::makeTemporalMap( + [ + "year" => $year, + "ordinalDay" => $ordinalDay, + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + "timezone" => $timezone, + ] + ) + ); + } + + /** + * Creates a datetime by parsing a string representation of a temporal value. + * + * @param string|StringType $dateString + */ + public static function dateTimeString($dateString): DateTime + { + return Procedure::datetime($dateString); + } + + /** + * Creates the current localDateTime value. + * + * @param null|string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-current Corresponding documentation on Neo4j.com + */ + public static function localDateTime($timezone = null): LocalDateTime + { + if ($timezone === null) { + return Procedure::localdatetime(); + } + + return Procedure::localdatetime(["timezone" => $timezone]); + } + + /** + * Creates a LocalDateTime value with specified year, month, day and time props. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $month + * @param null|int|NumeralType $day + * @param null|int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-calendar Corresponding documentation on Neo4j.com + */ + public static function localDateTimeYMD( + $year, + $month = null, + $day = null, + $hour = null, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null + ): LocalDateTime { + return Procedure::localdatetime( + self::makeTemporalMap( + [ + "year" => $year, + "month" => $month, + "day" => $day, + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + ] + ) + ); + } + + /** + * Creates a LocalDateTime value with the specified year, week, dayOfWeek, hour, minute, + * second, millisecond, microsecond and nanosecond component value. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $week + * @param null|int|NumeralType $dayOfWeek + * @param null|int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-week Corresponding documentation on Neo4j.com + */ + public static function localDateTimeYWD( + $year, + $week = null, + $dayOfWeek = null, + $hour = null, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null + ): LocalDateTime { + return Procedure::localdatetime( + self::makeTemporalMap( + [ + "year" => $year, + "week" => $week, + "dayOfWeek" => $dayOfWeek, + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + ] + ) + ); + } + + /** + * Creates a LocalDateTime value with the specified year, quarter, dayOfQuarter, hour, minute, second, millisecond, microsecond and nanosecond component values. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $quarter + * @param null|int|NumeralType $dayOfQuarter + * @param null|int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-quarter Corresponding documentation on Neo4j.com + */ + public static function localDateTimeYQD( + $year, + $quarter = null, + $dayOfQuarter = null, + $hour = null, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null + ): LocalDateTime { + return Procedure::localdatetime( + self::MakeTemporalMap( + [ + "year" => $year, + "quarter" => $quarter, + "dayOfQuarter" => $dayOfQuarter, + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + ] + ) + ); + } + + /** + * Creates a LocalDateTime value with the specified year, ordinalDay, hour, minute, second, millisecond, microsecond and nanosecond component values. + * + * @param int|NumeralType $year + * @param null|int|NumeralType $ordinalDay + * @param null|int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-ordinal Corresponding documentation on Neo4j.com + */ + public static function localDateTimeYD( + $year, + $ordinalDay = null, + $hour = null, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null + ): LocalDateTime { + return Procedure::localdatetime( + self::makeTemporalMap( + [ + "year" => $year, + "ordinalDay" => $ordinalDay, + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + ] + ) + ); + } + + /** + * Creates the LocalDateTime value obtained by parsing a string representation of a temporal value. + * + * @param string|StringType $localDateTimeString + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-create-string Corresponding documentation on Neo4j.com + */ + public static function localDateTimeString($localDateTimeString): LocalDateTime + { + return Procedure::localdatetime($localDateTimeString); + } + + /** + * Creates the current LocalTime value. + * + * @param null|string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localtime-current Corresponding documentation on Neo4j.com + */ + public static function localTimeCurrent($timezone = null): LocalTime + { + if ($timezone === null) { + return Procedure::localtime(); + } + + return Procedure::localtime(["timezone" => $timezone]); + } + + /** + * Creates a LocalTime value with the specified hour, minute, second, millisecond, microsecond and nanosecond component values. + * + * @param int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localtime-create Corresponding documentation on Neo4j.com + */ + public static function localTime( + $hour, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null + ): LocalTime { + return Procedure::localtime( + self::makeTemporalMap( + [ + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + ] + ) + ); + } + + /** + * Creates the LocalTime value obtained by parsing a string representation of a temporal value. + * + * @param string|StringType $localTimeString + */ + public static function localTimeString($localTimeString): LocalTime + { + return Procedure::localtime($localTimeString); + } + + /** + * Creates the current Time value. + * + * @param null|string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-time-current Corresponding documentation on Neo4j.com + */ + public static function time($timezone = null): Time + { + if ($timezone === null) { + return Procedure::time(); + } + + return Procedure::time(["timezone" => $timezone]); + } + + /** + * Creates a Time value with the specified hour, minute, second, millisecond, microsecond, nanosecond and timezone component values. + * + * @param int|NumeralType $hour + * @param null|int|NumeralType $minute + * @param null|int|NumeralType $second + * @param null|int|NumeralType $millisecond + * @param null|int|NumeralType $microsecond + * @param null|int|NumeralType $nanosecond + * @param null|string|StringType $timezone + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-time-create Corresponding documentation on Neo4j.com + */ + public static function timeHMS( + $hour, + $minute = null, + $second = null, + $millisecond = null, + $microsecond = null, + $nanosecond = null, + $timezone = null + ): Time { + return Procedure::time( + self::makeTemporalMap( + [ + "hour" => $hour, + "minute" => $minute, + "second" => $second, + "millisecond" => $millisecond, + "microsecond" => $microsecond, + "nanosecond" => $nanosecond, + "timezone" => $timezone, + ] + ) + ); + } + + /** + * Creates the Time value obtained by parsing a string representation of a temporal value. + * + * @param string|StringType $timeString + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-time-create-string Corresponding documentation on Neo4j.com + */ + public static function timeString($timeString): Time + { + return Procedure::time($timeString); + } + + /** + * Creates a 2d cartesian point. + * + * @param float|int|NumeralType $x + * @param float|int|NumeralType $y + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-cartesian-2d Corresponding documentation on Neo4j.com + */ + public static function point2d($x, $y): Point + { + return Procedure::point([ + "x" => $x, + "y" => $y, + "crs" => "cartesian", + ]); + } + + /** + * Creates a 3d cartesian point. + * + * @param float|int|NumeralType $x + * @param float|int|NumeralType $y + * @param float|int|NumeralType $z + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-cartesian-3d Corresponding documentation on Neo4j.com + */ + public static function point3d($x, $y, $z): Point + { + return Procedure::point([ + "x" => $x, + "y" => $y, + "z" => $z, + "crs" => "cartesian-3D", + ]); + } + + /** + * Creates a WGS 84 2D point. + * + * @param float|int|NumeralType $longitude + * @param float|int|NumeralType $latitude + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-wgs84-2d Corresponding documentation on Neo4j.com + */ + public static function point2dWGS84($longitude, $latitude): Point + { + return Procedure::point([ + "longitude" => $longitude, + "latitude" => $latitude, + "crs" => "WGS-84", + ]); + } + + /** + * Creates a WGS 84 2D point. + * + * @param float|int|NumeralType $longitude + * @param float|int|NumeralType $latitude + * @param float|int|NumeralType $height + * + * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-wgs84-2d Corresponding documentation on Neo4j.com + */ + public static function point3dWGS84($longitude, $latitude, $height): Point + { + return Procedure::point([ + "longitude" => $longitude, + "latitude" => $latitude, + "height" => $height, + "crs" => "WGS-84-3D", + ]); + } + + /** + * Prevent the construction of this class by making the constructor private. + */ + private function __construct() + { + } + + /** + * Prepares the variables to be used by temporal (i.e. time-like) CYPHER-functions. + * + * @param mixed[] $variables + * + * @return mixed[] + */ + private static function makeTemporalMap(array $variables): array + { + $map = []; + + $nullEncountered = false; + $secondsFound = false; + + foreach ($variables as $key => $variable) { + if ($variable === null) { + $nullEncountered = true; + + continue; + } + + if ($key === 'timezone') { + // Timezone can always be added, and is a string. + $map[$key] = $variable; + } else { + if (!$secondsFound && $nullEncountered) { + // Check if none of the previous, i.e. more important components, are null. + // sub-second values are not interdependent, but seconds must then be provided. + throw new LogicException("The key {$key} can only be provided when all more significant components are provided as well."); + } + + if ($key === 'second') { + $secondsFound = true; + } + + $map[$key] = $variable; + } + } + + return $map; + } +} diff --git a/src/Expressions/Literals/Map.php b/src/Expressions/Literals/Map.php new file mode 100644 index 00000000..4966ce94 --- /dev/null +++ b/src/Expressions/Literals/Map.php @@ -0,0 +1,108 @@ +elements = array_map([self::class, 'toAnyType'], $elements); + } + + /** + * Adds an element for the given name with the given value. Overrides the element if the $key already exists. + * + * @param string $key The name/label for the element + * @param mixed $value The value of the element + * + * @return $this + */ + public function add(string $key, $value): self + { + $this->elements[$key] = self::toAnyType($value); + + return $this; + } + + /** + * Merges the given map with this map. + * + * @param Map $map The map to merge + * + * @return $this + */ + public function mergeWith(self $map): self + { + $this->elements = array_merge($this->elements, $map->getElements()); + + return $this; + } + + /** + * Returns the elements of this map as an associative array with key-value pairs. + * + * @return AnyType[] + */ + public function getElements(): array + { + return $this->elements; + } + + /** + * Checks if this map is empty. + */ + public function isEmpty(): bool + { + return empty($this->elements); + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + $pairs = []; + + foreach ($this->elements as $key => $value) { + $pairs[] = sprintf("%s: %s", self::escape((string) $key), $value->toQuery()); + } + + return sprintf("{%s}", implode(", ", $pairs)); + } +} diff --git a/src/Expressions/Literals/String_.php b/src/Expressions/Literals/String_.php new file mode 100644 index 00000000..170b063e --- /dev/null +++ b/src/Expressions/Literals/String_.php @@ -0,0 +1,99 @@ +value = $value; + } + + /** + * Whether to use double quotes or not. + */ + public function useDoubleQuotes(bool $useDoubleQuotes = true): void + { + $this->useDoubleQuotes = $useDoubleQuotes; + } + + /** + * Returns the string value. + */ + public function getValue(): string + { + return $this->value; + } + + /** + * Returns whether the string uses double quotes. Single quotes are used if false. + */ + public function usesDoubleQuotes(): bool + { + return $this->useDoubleQuotes; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + // Encodes both "'" and '"', so returning in either context is safe + $value = $this->encodeSpecials($this->value); + + if ($this->useDoubleQuotes) { + return sprintf('"%s"', $value); + } + + return sprintf("'%s'", $value); + } + + /** + * Encodes special string characters in Cypher. + * + * @param string $value The string to encode + * + * @return string The encoded string + */ + private static function encodeSpecials(string $value): string + { + // See https://s3.amazonaws.com/artifacts.opencypher.org/openCypher9.pdf (Note on string literals) + return str_replace( + ["\\", "\t", "\u{0008}", "\n", "\r", "\f", "'", "\""], + ["\\\\", "\\t", "\\b", "\\n", "\\r", "\\f", "\\'", "\\\""], + $value + ); + } +} diff --git a/src/Expressions/Operators/Addition.php b/src/Expressions/Operators/Addition.php new file mode 100644 index 00000000..ee015866 --- /dev/null +++ b/src/Expressions/Operators/Addition.php @@ -0,0 +1,26 @@ +left = $left; + $this->right = $right; + } + + /** + * Gets the left-hand of the expression. + */ + public function getLeft(): AnyType + { + return $this->left; + } + + /** + * Gets the right-hand of the expression. + */ + public function getRight(): AnyType + { + return $this->right; + } + + /** + * @inheritDoc + */ + protected function toInner(): string + { + return sprintf("%s %s %s", $this->left->toQuery(), $this->getOperator(), $this->right->toQuery()); + } + + /** + * Returns the operator. For instance, this function would return "-" for the minus operator. + */ + abstract protected function getOperator(): string; +} diff --git a/src/Expressions/Operators/BooleanBinaryOperator.php b/src/Expressions/Operators/BooleanBinaryOperator.php new file mode 100644 index 00000000..4928e115 --- /dev/null +++ b/src/Expressions/Operators/BooleanBinaryOperator.php @@ -0,0 +1,38 @@ +" + * - less than: "<" + * - greater than: ">" + * - less than or equal to: "<=" + * - greater than or equal to: ">=" + * + * In additional, there are some string-specific comparison operators: + * + * - case-sensitive prefix search on strings: "STARTS WITH" + * - case-sensitive suffix search on strings: "ENDS WITH" + * - case-sensitive inclusion search in strings: "CONTAINS" + * + * @see https://neo4j.com/docs/cypher-manual/current/syntax/operators/#query-operators-comparison Corresponding documentation on Neo4j.com + */ +abstract class ComparisonBinaryOperator extends BinaryOperator implements BooleanType +{ + use BooleanTypeTrait; + + /** + * @inheritDoc + * + * @param AnyType $left The left-hand of the comparison operator + * @param AnyType $right The right-hand of the comparison operator + */ + public function __construct(AnyType $left, AnyType $right, bool $insertParentheses = true) + { + parent::__construct($left, $right, $insertParentheses); + } +} diff --git a/src/Expressions/Operators/ComparisonUnaryOperator.php b/src/Expressions/Operators/ComparisonUnaryOperator.php new file mode 100644 index 00000000..a0891069 --- /dev/null +++ b/src/Expressions/Operators/ComparisonUnaryOperator.php @@ -0,0 +1,37 @@ +) operator. + * + * @see https://neo4j.com/docs/cypher-manual/current/syntax/operators/#query-operators-comparison Corresponding documentation on Neo4j.com + */ +final class GreaterThan extends ComparisonBinaryOperator +{ + /** + * @inheritDoc + */ + protected function getOperator(): string + { + return ">"; + } +} diff --git a/src/Expressions/Operators/GreaterThanOrEqual.php b/src/Expressions/Operators/GreaterThanOrEqual.php new file mode 100644 index 00000000..3948fd28 --- /dev/null +++ b/src/Expressions/Operators/GreaterThanOrEqual.php @@ -0,0 +1,26 @@ +=) operator. + * + * @see https://neo4j.com/docs/cypher-manual/current/syntax/operators/#query-operators-comparison Corresponding documentation on Neo4j.com + */ +final class GreaterThanOrEqual extends ComparisonBinaryOperator +{ + /** + * @inheritDoc + */ + protected function getOperator(): string + { + return ">="; + } +} diff --git a/src/Expressions/Operators/In.php b/src/Expressions/Operators/In.php new file mode 100644 index 00000000..3cb70de9 --- /dev/null +++ b/src/Expressions/Operators/In.php @@ -0,0 +1,45 @@ +) operator. + * + * @see https://neo4j.com/docs/cypher-manual/current/syntax/operators/#query-operators-comparison Corresponding documentation on Neo4j.com + */ +final class Inequality extends ComparisonBinaryOperator +{ + /** + * @inheritDoc + */ + protected function getOperator(): string + { + return "<>"; + } +} diff --git a/src/Expressions/Operators/IsNotNull.php b/src/Expressions/Operators/IsNotNull.php new file mode 100644 index 00000000..f82787b3 --- /dev/null +++ b/src/Expressions/Operators/IsNotNull.php @@ -0,0 +1,34 @@ +insertParentheses = $insertParentheses; + } + + /** + * Returns whether the operator inserts parenthesis. + */ + public function insertsParentheses(): bool + { + return $this->insertParentheses; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + $format = $this->insertParentheses ? "(%s)" : "%s"; + $inner = $this->toInner(); + + return sprintf($format, $inner); + } + + /** + * Returns the inner part of the application of the operator, that is, without any parentheses. + */ + abstract protected function toInner(): string; +} diff --git a/src/Expressions/Operators/Regex.php b/src/Expressions/Operators/Regex.php new file mode 100644 index 00000000..70ad893f --- /dev/null +++ b/src/Expressions/Operators/Regex.php @@ -0,0 +1,28 @@ +expression = $expression; + } + + /** + * Returns whether this is a postfix operator or not. + */ + public function isPostfix(): bool + { + return false; + } + + /** + * Returns the expression to negate. + */ + public function getExpression(): AnyType + { + return $this->expression; + } + + /** + * @inheritDoc + */ + protected function toInner(): string + { + $expression = $this->expression->toQuery(); + $operator = $this->getOperator(); + + return $this->isPostfix() ? + sprintf("%s %s", $expression, $operator) : + sprintf("%s %s", $operator, $expression); + } + + /** + * Returns the operator. For instance, this function would return "-" for the minus operator. + */ + abstract protected function getOperator(): string; +} diff --git a/src/Expressions/Parameter.php b/src/Expressions/Parameter.php new file mode 100644 index 00000000..cbeb4bf9 --- /dev/null +++ b/src/Expressions/Parameter.php @@ -0,0 +1,107 @@ +parameter = self::escape($parameter); + } + + /** + * Returns the escaped parameter name. + */ + public function getParameter(): string + { + return $this->parameter; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + return sprintf('$%s', $this->parameter); + } +} diff --git a/src/Expressions/Procedures/All.php b/src/Expressions/Procedures/All.php new file mode 100644 index 00000000..f78a336b --- /dev/null +++ b/src/Expressions/Procedures/All.php @@ -0,0 +1,74 @@ +variable = $variable; + $this->list = $list; + $this->predicate = $predicate; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return "all(%s IN %s WHERE %s)"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return [$this->variable, $this->list, $this->predicate]; + } +} diff --git a/src/Expressions/Procedures/Any.php b/src/Expressions/Procedures/Any.php new file mode 100644 index 00000000..7dad6909 --- /dev/null +++ b/src/Expressions/Procedures/Any.php @@ -0,0 +1,74 @@ +variable = $variable; + $this->list = $list; + $this->predicate = $predicate; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return "any(%s IN %s WHERE %s)"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return [$this->variable, $this->list, $this->predicate]; + } +} diff --git a/src/Expressions/Procedures/Date.php b/src/Expressions/Procedures/Date.php new file mode 100644 index 00000000..6e63afc3 --- /dev/null +++ b/src/Expressions/Procedures/Date.php @@ -0,0 +1,58 @@ +value = $value; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return $this->value ? "date(%s)" : "date()"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return $this->value ? [$this->value] : []; + } +} diff --git a/src/Expressions/Procedures/DateTime.php b/src/Expressions/Procedures/DateTime.php new file mode 100644 index 00000000..fe596102 --- /dev/null +++ b/src/Expressions/Procedures/DateTime.php @@ -0,0 +1,58 @@ +value = $value; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return $this->value ? "datetime(%s)" : "datetime()"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return $this->value ? [$this->value] : []; + } +} diff --git a/src/Expressions/Procedures/Exists.php b/src/Expressions/Procedures/Exists.php new file mode 100644 index 00000000..541d681f --- /dev/null +++ b/src/Expressions/Procedures/Exists.php @@ -0,0 +1,60 @@ +expression = $expression; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return "exists(%s)"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return [$this->expression]; + } +} diff --git a/src/Expressions/Procedures/IsEmpty.php b/src/Expressions/Procedures/IsEmpty.php new file mode 100644 index 00000000..264492a3 --- /dev/null +++ b/src/Expressions/Procedures/IsEmpty.php @@ -0,0 +1,68 @@ +list = $list; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return "isEmpty(%s)"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return [$this->list]; + } +} diff --git a/src/Expressions/Procedures/LocalDateTime.php b/src/Expressions/Procedures/LocalDateTime.php new file mode 100644 index 00000000..8485c959 --- /dev/null +++ b/src/Expressions/Procedures/LocalDateTime.php @@ -0,0 +1,58 @@ +value = $value; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return $this->value ? "localdatetime(%s)" : "localdatetime()"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return $this->value ? [$this->value] : []; + } +} diff --git a/src/Expressions/Procedures/LocalTime.php b/src/Expressions/Procedures/LocalTime.php new file mode 100644 index 00000000..da625d56 --- /dev/null +++ b/src/Expressions/Procedures/LocalTime.php @@ -0,0 +1,58 @@ +value = $value; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return $this->value ? "localtime(%s)" : "localtime()"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return $this->value ? [$this->value] : []; + } +} diff --git a/src/Expressions/Procedures/None.php b/src/Expressions/Procedures/None.php new file mode 100644 index 00000000..34d1fcc1 --- /dev/null +++ b/src/Expressions/Procedures/None.php @@ -0,0 +1,74 @@ +variable = $variable; + $this->list = $list; + $this->predicate = $predicate; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return "none(%s IN %s WHERE %s)"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return [$this->variable, $this->list, $this->predicate]; + } +} diff --git a/src/Expressions/Procedures/Point.php b/src/Expressions/Procedures/Point.php new file mode 100644 index 00000000..6706b811 --- /dev/null +++ b/src/Expressions/Procedures/Point.php @@ -0,0 +1,58 @@ +map = $map; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return "point(%s)"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return [$this->map]; + } +} diff --git a/src/Expressions/Procedures/Procedure.php b/src/Expressions/Procedures/Procedure.php new file mode 100644 index 00000000..c858e20e --- /dev/null +++ b/src/Expressions/Procedures/Procedure.php @@ -0,0 +1,254 @@ +getSignature(); + + $parameters = array_map( + static fn (AnyType $value): string => $value->toQuery(), + $this->getParameters() + ); + + return sprintf($signature, ...$parameters); + } + + /** + * Returns the signature of this function as a format string. For example for the "all()" function, + * the signature would be this: "all(%s IN %s WHERE %s)". + */ + abstract protected function getSignature(): string; + + /** + * The parameters for this function as QueryConvertible objects. These parameters are inserted, in order, into + * the signature string retrieved from $this->getSignature(). + * + * @return AnyType[] + */ + abstract protected function getParameters(): array; +} diff --git a/src/Expressions/Procedures/Raw.php b/src/Expressions/Procedures/Raw.php new file mode 100644 index 00000000..a04744c4 --- /dev/null +++ b/src/Expressions/Procedures/Raw.php @@ -0,0 +1,122 @@ +functionName = self::escape($functionName); + $this->parameters = $parameters; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + // A string of the form '%s, %s, %s' with count($this->parameters) occurences of '%s' + $percentSString = + count($this->parameters) === 0 ? + '' : + str_repeat('%s, ', count($this->parameters) - 1) . '%s'; + + return $this->functionName . '(' . $percentSString . ')'; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return $this->parameters; + } +} diff --git a/src/Expressions/Procedures/Single.php b/src/Expressions/Procedures/Single.php new file mode 100644 index 00000000..90923d8d --- /dev/null +++ b/src/Expressions/Procedures/Single.php @@ -0,0 +1,74 @@ +variable = $variable; + $this->list = $list; + $this->predicate = $predicate; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return "single(%s IN %s WHERE %s)"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return [$this->variable, $this->list, $this->predicate]; + } +} diff --git a/src/Expressions/Procedures/Time.php b/src/Expressions/Procedures/Time.php new file mode 100644 index 00000000..f4873a34 --- /dev/null +++ b/src/Expressions/Procedures/Time.php @@ -0,0 +1,58 @@ +value = $value; + } + + /** + * @inheritDoc + */ + protected function getSignature(): string + { + return $this->value ? "time(%s)" : "time()"; + } + + /** + * @inheritDoc + */ + protected function getParameters(): array + { + return $this->value ? [$this->value] : []; + } +} diff --git a/src/Expressions/Property.php b/src/Expressions/Property.php new file mode 100644 index 00000000..32c50735 --- /dev/null +++ b/src/Expressions/Property.php @@ -0,0 +1,146 @@ +expression = $expression; + $this->property = $property; + } + + /** + * Replace the value of this property with something else. + * + * TODO: Disallow this function to be used outside the context of a SET + * + * @note This function only makes sense when used in the context of a SET + * + * @param bool|float|int|PropertyType|string $value The new value to give to this property + * + * @return PropertyReplacement + * + * TODO: Allow this function to take arrays of property types + */ + public function replaceWith($value): PropertyReplacement + { + return new PropertyReplacement($this, self::toPropertyType($value)); + } + + /** + * Returns the property name. + */ + public function getProperty(): string + { + return $this->property; + } + + /** + * Returns the expression to which the property belongs. + * + * @return MapType|NodeType|RelationshipType + */ + public function getExpression(): AnyType + { + return $this->expression; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + return sprintf("%s.%s", $this->expression->toQuery(), self::escape($this->property)); + } +} diff --git a/src/Expressions/RawExpression.php b/src/Expressions/RawExpression.php new file mode 100644 index 00000000..137dac24 --- /dev/null +++ b/src/Expressions/RawExpression.php @@ -0,0 +1,112 @@ +expression = $expression; + } + + /** + * Returns the RAW expression. + */ + public function getExpression(): string + { + return $this->expression; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + return $this->expression; + } +} diff --git a/src/Expressions/Variable.php b/src/Expressions/Variable.php new file mode 100644 index 00000000..28bc200f --- /dev/null +++ b/src/Expressions/Variable.php @@ -0,0 +1,146 @@ +name = self::escape($name); + } + + /** + * Returns a label with this variable. + * + * @param string ...$labels The labels to attach to this variable + */ + public function labeled(string ...$labels): Label + { + return new Label($this, ...$labels); + } + + /** + * Assign a value to this property. + * + * @param Map|mixed[] $value The value to assign + */ + public function assign($value): PropertyReplacement + { + return new PropertyReplacement($this, self::toMapType($value)); + } + + /** + * Returns the escaped name of this variable. + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + return $this->name; + } +} diff --git a/src/Functions/All.php b/src/Functions/All.php deleted file mode 100644 index dca9475f..00000000 --- a/src/Functions/All.php +++ /dev/null @@ -1,85 +0,0 @@ -variable = $variable; - $this->list = $list; - $this->predicate = $predicate; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return "all(%s IN %s WHERE %s)"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return [$this->variable, $this->list, $this->predicate]; - } -} diff --git a/src/Functions/Any.php b/src/Functions/Any.php deleted file mode 100644 index 581fdc54..00000000 --- a/src/Functions/Any.php +++ /dev/null @@ -1,86 +0,0 @@ -variable = $variable; - $this->list = $list; - $this->predicate = $predicate; - } - - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return "any(%s IN %s WHERE %s)"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return [$this->variable, $this->list, $this->predicate]; - } -} diff --git a/src/Functions/Date.php b/src/Functions/Date.php deleted file mode 100644 index de83e3c2..00000000 --- a/src/Functions/Date.php +++ /dev/null @@ -1,72 +0,0 @@ -value = $value; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return $this->value ? "date(%s)" : "date()"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return $this->value ? [$this->value] : []; - } -} diff --git a/src/Functions/DateTime.php b/src/Functions/DateTime.php deleted file mode 100644 index 3c726cb5..00000000 --- a/src/Functions/DateTime.php +++ /dev/null @@ -1,72 +0,0 @@ -value = $value; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return $this->value ? "datetime(%s)" : "datetime()"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return $this->value ? [$this->value] : []; - } -} diff --git a/src/Functions/Exists.php b/src/Functions/Exists.php deleted file mode 100644 index 8da18347..00000000 --- a/src/Functions/Exists.php +++ /dev/null @@ -1,69 +0,0 @@ -expression = $expression; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return "exists(%s)"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return [$this->expression]; - } -} diff --git a/src/Functions/FunctionCall.php b/src/Functions/FunctionCall.php deleted file mode 100644 index 2a021d49..00000000 --- a/src/Functions/FunctionCall.php +++ /dev/null @@ -1,270 +0,0 @@ -getSignature(); - $parameters = array_map( - fn (QueryConvertable $convertable): string => $convertable->toQuery(), - $this->getParameters() - ); - - return sprintf($signature, ...$parameters); - } - - /** - * Returns the signature of this function as a format string. For example for the "all()" function, - * the signature would be this: - * - * "all(%s IN %s WHERE %s)" - * - * @return string - */ - abstract protected function getSignature(): string; - - /** - * The parameters for this function as QueryConvertable objects. These parameters are inserted, in order, into - * the signature string retrieved from ::getSignature(). - * - * @return AnyType[] - */ - abstract protected function getParameters(): array; -} diff --git a/src/Functions/IsEmpty.php b/src/Functions/IsEmpty.php deleted file mode 100644 index 01bfd78b..00000000 --- a/src/Functions/IsEmpty.php +++ /dev/null @@ -1,78 +0,0 @@ -assertClass('list', [ListType::class, MapType::class, StringType::class], $list); - - $this->list = $list; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return "isEmpty(%s)"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return [$this->list]; - } -} diff --git a/src/Functions/LocalDateTime.php b/src/Functions/LocalDateTime.php deleted file mode 100644 index 1f0d44ce..00000000 --- a/src/Functions/LocalDateTime.php +++ /dev/null @@ -1,72 +0,0 @@ -value = $value; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return $this->value ? "localdatetime(%s)" : "localdatetime()"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return $this->value ? [$this->value] : []; - } -} diff --git a/src/Functions/LocalTime.php b/src/Functions/LocalTime.php deleted file mode 100644 index 61f56115..00000000 --- a/src/Functions/LocalTime.php +++ /dev/null @@ -1,72 +0,0 @@ -value = $value; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return $this->value ? "localtime(%s)" : "localtime()"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return $this->value ? [$this->value] : []; - } -} diff --git a/src/Functions/None.php b/src/Functions/None.php deleted file mode 100644 index dd6bd489..00000000 --- a/src/Functions/None.php +++ /dev/null @@ -1,85 +0,0 @@ -variable = $variable; - $this->list = $list; - $this->predicate = $predicate; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return "none(%s IN %s WHERE %s)"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return [$this->variable, $this->list, $this->predicate]; - } -} diff --git a/src/Functions/Point.php b/src/Functions/Point.php deleted file mode 100644 index 5fe3a792..00000000 --- a/src/Functions/Point.php +++ /dev/null @@ -1,72 +0,0 @@ -map = $map; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return "point(%s)"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return [$this->map]; - } -} diff --git a/src/Functions/RawFunction.php b/src/Functions/RawFunction.php deleted file mode 100644 index 06fb08eb..00000000 --- a/src/Functions/RawFunction.php +++ /dev/null @@ -1,106 +0,0 @@ -assertClass('parameter', AnyType::class, $parameter); - } - - if (!preg_match("/^[a-zA-Z0-9_]+$/", $functionName)) { - throw new InvalidArgumentException( - "\$functionName should only consist of alphanumeric characters and underscores" - ); - } - - $this->functionName = $functionName; - $this->parameters = $parameters; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return sprintf( - "%s(%s)", - $this->functionName, - implode(", ", array_fill(0, count($this->parameters), "%s")) - ); - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return $this->parameters; - } -} diff --git a/src/Functions/Single.php b/src/Functions/Single.php deleted file mode 100644 index 070ce995..00000000 --- a/src/Functions/Single.php +++ /dev/null @@ -1,85 +0,0 @@ -variable = $variable; - $this->list = $list; - $this->predicate = $predicate; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return "single(%s IN %s WHERE %s)"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return [$this->variable, $this->list, $this->predicate]; - } -} diff --git a/src/Functions/Time.php b/src/Functions/Time.php deleted file mode 100644 index 742fcb52..00000000 --- a/src/Functions/Time.php +++ /dev/null @@ -1,72 +0,0 @@ -value = $value; - } - - /** - * @inheritDoc - */ - protected function getSignature(): string - { - return $this->value ? "time(%s)" : "time()"; - } - - /** - * @inheritDoc - */ - protected function getParameters(): array - { - return $this->value ? [$this->value] : []; - } -} diff --git a/src/GreaterThan.php b/src/GreaterThan.php deleted file mode 100644 index ba9fc9d7..00000000 --- a/src/GreaterThan.php +++ /dev/null @@ -1,52 +0,0 @@ -) operator. - * - * @see https://neo4j.com/docs/cypher-manual/current/syntax/operators/#query-operators-comparison - */ -class GreaterThan extends BinaryOperator implements BooleanType -{ - use BooleanTypeTrait; - - /** - * @inheritDoc - */ - public function __construct(ComparableType $left, ComparableType $right, bool $insertParentheses = true) - { - parent::__construct($left, $right, $insertParentheses); - } - - /** - * @inheritDoc - */ - protected function getOperator(): string - { - return ">"; - } -} diff --git a/src/GreaterThanOrEqual.php b/src/GreaterThanOrEqual.php deleted file mode 100644 index 268afadd..00000000 --- a/src/GreaterThanOrEqual.php +++ /dev/null @@ -1,52 +0,0 @@ -=) operator. - * - * @see https://neo4j.com/docs/cypher-manual/current/syntax/operators/#query-operators-comparison - */ -class GreaterThanOrEqual extends BinaryOperator implements BooleanType -{ - use BooleanTypeTrait; - - /** - * @inheritDoc - */ - public function __construct(ComparableType $left, ComparableType $right, bool $insertParentheses = true) - { - parent::__construct($left, $right, $insertParentheses); - } - - /** - * @inheritDoc - */ - protected function getOperator(): string - { - return ">="; - } -} diff --git a/src/In.php b/src/In.php deleted file mode 100644 index b12ee812..00000000 --- a/src/In.php +++ /dev/null @@ -1,53 +0,0 @@ -) operator. - * - * @see https://neo4j.com/docs/cypher-manual/current/syntax/operators/#query-operators-comparison - */ -class Inequality extends BinaryOperator implements BooleanType -{ - use BooleanTypeTrait; - - /** - * @inheritDoc - */ - public function __construct(PropertyType $left, PropertyType $right, bool $insertParentheses = true) - { - parent::__construct($left, $right, $insertParentheses); - } - - /** - * @inheritDoc - */ - protected function getOperator(): string - { - return "<>"; - } -} diff --git a/src/IsNotNull.php b/src/IsNotNull.php deleted file mode 100644 index 3c476580..00000000 --- a/src/IsNotNull.php +++ /dev/null @@ -1,82 +0,0 @@ -expression = $expression; - $this->insertParentheses = $insertParentheses; - } - - /** - * Returns the expression to test against null. - * - * @return AnyType - */ - public function getExpression(): AnyType - { - return $this->expression; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return sprintf($this->insertParentheses ? "(%s IS NOT NULL)" : "%s IS NOT NULL", $this->expression->toQuery()); - } - - /** - * Returns whether or not the operator inserts parenthesis. - * - * @return bool - */ - public function insertsParentheses(): bool - { - return $this->insertParentheses; - } -} diff --git a/src/IsNull.php b/src/IsNull.php deleted file mode 100644 index 28f14be2..00000000 --- a/src/IsNull.php +++ /dev/null @@ -1,82 +0,0 @@ -expression = $expression; - $this->insertParentheses = $insertParentheses; - } - - /** - * Returns the expression to test against null. - * - * @return AnyType - */ - public function getExpression(): AnyType - { - return $this->expression; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return sprintf($this->insertParentheses ? "(%s IS NULL)" : "%s IS NULL", $this->expression->toQuery()); - } - - /** - * Returns whether or not the operator inserts parenthesis. - * - * @return bool - */ - public function insertsParentheses(): bool - { - return $this->insertParentheses; - } -} diff --git a/src/Label.php b/src/Label.php deleted file mode 100644 index 4a8a3a13..00000000 --- a/src/Label.php +++ /dev/null @@ -1,102 +0,0 @@ -variable = $variable; - $this->labels = $labels; - } - - /** - * Returns the labels. - * - * @return string[] - */ - public function getLabels(): array - { - return $this->labels; - } - - /** - * Returns the variable of the label. - * - * @return Variable - */ - public function getVariable(): Variable - { - return $this->variable; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - $query = $this->variable->toQuery(); - - foreach ($this->labels as $label) { - $query = sprintf("%s:%s", $query, $this->escape($label)); - } - - return $query; - } -} diff --git a/src/LessThan.php b/src/LessThan.php deleted file mode 100644 index ebe273b3..00000000 --- a/src/LessThan.php +++ /dev/null @@ -1,52 +0,0 @@ -value; - } - - /** - * Boolean constructor. - * - * @param bool $value - */ - public function __construct(bool $value) - { - $this->value = $value; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return $this->value ? "true" : "false"; - } -} diff --git a/src/Literals/Decimal.php b/src/Literals/Decimal.php deleted file mode 100644 index 7b1dcc45..00000000 --- a/src/Literals/Decimal.php +++ /dev/null @@ -1,66 +0,0 @@ -value; - } - - /** - * Decimal constructor. - * - * @param int|float $value - */ - public function __construct($value) - { - $this->value = $value; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return (string)$this->value; - } -} diff --git a/src/Literals/Literal.php b/src/Literals/Literal.php deleted file mode 100644 index 8ae27ae8..00000000 --- a/src/Literals/Literal.php +++ /dev/null @@ -1,915 +0,0 @@ - $timezone])); - } - - /** - * Creates a date from the given year, month and day. - * - * @param int|NumeralType $year - * @param null|int|NumeralType $month - * @param null|int|NumeralType $day - * @return DateType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-date-calendar - */ - public static function dateYMD($year, $month = null, $day = null): DateType - { - return FunctionCall::date(self::makeTemporalMap([ - "year" => $year, - "month" => $month, - "day" => $day, - ])); - } - - /** - * Creates a date from the given year, week and weekday. - * - * @param int|NumeralType $year - * @param null|int|NumeralType $week - * @param null|int|NumeralType $weekday - * @return DateType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-date-week - */ - public static function dateYWD($year, $week = null, $weekday = null): DateType - { - return FunctionCall::date(self::makeTemporalMap([ - "year" => $year, - "week" => $week, - "dayOfWeek" => $weekday, - ])); - } - - /** - * Creates a date from the given string. - * - * @param string|StringLiteral $date - * @return DateType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-date-create-string - */ - public static function dateString($date): DateType - { - if (!($date instanceof StringLiteral)) { - $date = self::string($date); - } - - return FunctionCall::date($date); - } - - /** - * Retrieves the current DateTime value, optionally for a different time zone. In reality, this - * function just returns a call to the "datetime()" function. - * - * @param string|StringType $timezone - * @return DateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-current - */ - public static function dateTime($timezone = null): DateTimeType - { - if ($timezone === null) { - return FunctionCall::datetime(); - } - - if (!($timezone instanceof StringType)) { - $timezone = self::string($timezone); - } - - return FunctionCall::datetime(Query::map(["timezone" => $timezone])); - } - - /** - * Creates a date from the given year, month, day and time values. - * - * @param int|NumeralType $year - * @param null|int|NumeralType $month - * @param null|int|NumeralType $day - * @param null|int|NumeralType $hour - * @param null|int|NumeralType $minute - * @param null|int|NumeralType $second - * @param null|int|NumeralType $millisecond - * @param null|int|NumeralType $microsecond - * @param null|int|NumeralType $nanosecond - * @param null|string|StringType $timezone - * @return DateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-calendar - */ - public static function dateTimeYMD( - $year, - $month = null, - $day = null, - $hour = null, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null, - $timezone = null - ): DateTimeType { - return FunctionCall::datetime(self::makeTemporalMap([ - "year" => $year, - "month" => $month, - "day" => $day, - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - "timezone" => $timezone, - ])); - } - - /** - * Creates a datetime with the specified year, week, dayOfWeek, hour, minute, second, millisecond, microsecond, nanosecond and timezone component values. - * - * @param int|NumeralType $year - * @param null|int|NumeralType $week - * @param null|int|NumeralType $dayOfWeek - * @param null|int|NumeralType $hour - * @param null|int|NumeralType $minute - * @param null|int|NumeralType $second - * @param null|int|NumeralType $millisecond - * @param null|int|NumeralType $microsecond - * @param null|int|NumeralType $nanosecond - * @param null|string|StringType $timezone - * @return DateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-week - */ - public static function datetimeYWD( - $year, - $week = null, - $dayOfWeek = null, - $hour = null, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null, - $timezone = null - ): DateTimeType { - return FunctionCall::datetime(self::makeTemporalMap([ - "year" => $year, - "week" => $week, - "dayOfWeek" => $dayOfWeek, - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - "timezone" => $timezone, - ])); - } - - /** - * Creates a datetime with the specified year, quarter, dayOfQuarter, hour, minute, second, millisecond, microsecond, nanosecond and timezone component values. - * - * @param int|NumeralType $year - * @param null|int|NumeralType $quarter - * @param null|int|NumeralType $dayOfQuarter - * @param null|int|NumeralType $hour - * @param null|int|NumeralType $minute - * @param null|int|NumeralType $second - * @param null|int|NumeralType $millisecond - * @param null|int|NumeralType $microsecond - * @param null|int|NumeralType $nanosecond - * @param null|string|StringType $timezone - * @return DateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-quarter - */ - public static function datetimeYQD( - $year, - $quarter = null, - $dayOfQuarter = null, - $hour = null, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null, - $timezone = null - ): DateTimeType { - return FunctionCall::datetime(self::makeTemporalMap([ - "year" => $year, - "quarter" => $quarter, - "dayOfQuarter" => $dayOfQuarter, - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - "timezone" => $timezone, - ])); - } - - /** - * Creates a datetime with the specified year, ordinalDay, hour, minute, second, millisecond, microsecond, nanosecond and timezone component values. - * - * @param int|NumeralType $year - * @param null|int|NumeralType $ordinalDay - * @param null|int|NumeralType $hour - * @param null|int|NumeralType $minute - * @param null|int|NumeralType $second - * @param null|int|NumeralType $millisecond - * @param null|int|NumeralType $microsecond - * @param null|int|NumeralType $nanosecond - * @param null|string|StringType $timezone - * @return DateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime-ordinal - */ - public static function datetimeYD( - $year, - $ordinalDay = null, - $hour = null, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null, - $timezone = null - ): DateTimeType { - return FunctionCall::datetime(self::makeTemporalMap([ - "year" => $year, - "ordinalDay" => $ordinalDay, - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - "timezone" => $timezone, - ])); - } - - /** - * Creates a datetime by parsing a string representation of a temporal value - * - * @param string|StringType $dateString - * @return DateTimeType - */ - public static function datetimeString($dateString): DateTimeType - { - if (!($dateString instanceof StringType)) { - $dateString = self::string($dateString); - } - - return FunctionCall::datetime($dateString); - } - - /** - * Creates the current localDateTime value - * - * @param null|string|StringType $timezone - * @return LocalDateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-current - */ - public static function localDatetime($timezone = null): LocalDateTimeType - { - if ($timezone === null) { - return FunctionCall::localdatetime(); - } - - if (!($timezone instanceof StringType)) { - $timezone = self::string($timezone); - } - - return FunctionCall::localdatetime(Query::map(["timezone" => $timezone])); - } - - /** - * Creates a LocalDateTime value with specified year, month, day and time props - * - * @param int|NumeralType $year - * @param null|int|NumeralType $month - * @param null|int|NumeralType $day - * @param null|int|NumeralType $hour - * @param null|int|NumeralType $minute - * @param null|int|NumeralType $second - * @param null|int|NumeralType $millisecond - * @param null|int|NumeralType $microsecond - * @param null|int|NumeralType $nanosecond - * @return LocalDateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-calendar - */ - public static function localDatetimeYMD( - $year, - $month = null, - $day = null, - $hour = null, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null - ): LocalDateTimeType { - return FunctionCall::localdatetime(self::makeTemporalMap([ - "year" => $year, - "month" => $month, - "day" => $day, - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - ])); - } - - /** - * Creates a LocalDateTime value with the specified year, week, dayOfWeek, hour, minute, - * second, millisecond, microsecond and nanosecond component value - * - * @param int|NumeralType $year - * @param null|int|NumeralType $week - * @param null|int|NumeralType $dayOfWeek - * @param null|int|NumeralType $hour - * @param null|int|NumeralType $minute - * @param null|int|NumeralType $second - * @param null|int|NumeralType $millisecond - * @param null|int|NumeralType $microsecond - * @param null|int|NumeralType $nanosecond - * @return LocalDateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-week - */ - public static function localDatetimeYWD( - $year, - $week = null, - $dayOfWeek = null, - $hour = null, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null - ): LocalDateTimeType { - return FunctionCall::localdatetime(self::makeTemporalMap([ - "year" => $year, - "week" => $week, - "dayOfWeek" => $dayOfWeek, - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - ])); - } - - /** - * Creates a LocalDateTime value with the specified year, quarter, dayOfQuarter, hour, minute, second, millisecond, microsecond and nanosecond component values - * - * @param $year - * @param null $quarter - * @param null $dayOfQuarter - * @param null $hour - * @param null $minute - * @param null $second - * @param null $millisecond - * @param null $microsecond - * @param null $nanosecond - * @return LocalDateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-quarter - */ - public static function localDatetimeYQD( - $year, - $quarter = null, - $dayOfQuarter = null, - $hour = null, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null - ): LocalDateTimeType { - return FunctionCall::localdatetime(self::MakeTemporalMap([ - "year" => $year, - "quarter" => $quarter, - "dayOfQuarter" => $dayOfQuarter, - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - ])); - } - - /** - * Creates a LocalDateTime value with the specified year, ordinalDay, hour, minute, second, millisecond, microsecond and nanosecond component values - * - * @param int|NumeralType $year - * @param null|int|NumeralType $ordinalDay - * @param null|int|NumeralType $hour - * @param null|int|NumeralType $minute - * @param null|int|NumeralType $second - * @param null|int|NumeralType $millisecond - * @param null|int|NumeralType $microsecond - * @param null|int|NumeralType $nanosecond - * @return LocalDateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-ordinal - */ - public static function localDatetimeYD( - $year, - $ordinalDay = null, - $hour = null, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null - ): LocalDateTimeType { - return FunctionCall::localdatetime(self::makeTemporalMap([ - "year" => $year, - "ordinalDay" => $ordinalDay, - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - ])); - } - - /** - * Creates the LocalDateTime value obtained by parsing a string representation of a temporal value - * - * @param string|StringType $localDateTimeString - * @return LocalDateTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localdatetime-create-string - */ - public static function localDatetimeString($localDateTimeString): LocalDateTimeType - { - if (!($localDateTimeString instanceof StringType)) { - $localDateTimeString = self::string($localDateTimeString); - } - - return FunctionCall::localdatetime($localDateTimeString); - } - - /** - * Creates the current LocalTime value - * - * @param null|string|StringType $timezone - * @return LocalTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localtime-current - */ - public static function localTimeCurrent($timezone = null): LocalTimeType - { - if ($timezone === null) { - return FunctionCall::localtime(); - } - - if (!($timezone instanceof StringType)) { - $timezone = self::string($timezone); - } - - return FunctionCall::localtime(Query::map(["timezone" => $timezone])); - } - - /** - * Creates a LocalTime value with the specified hour, minute, second, millisecond, microsecond and nanosecond component values - * - * @param int|NumeralType $hour - * @param null|int|NumeralType $minute - * @param null|int|NumeralType $second - * @param null|int|NumeralType $millisecond - * @param null|int|NumeralType $microsecond - * @param null|int|NumeralType $nanosecond - * @return LocalTimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-localtime-create - */ - public static function localTime( - $hour, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null - ): LocalTimeType { - return FunctionCall::localtime(self::makeTemporalMap([ - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - ])); - } - - /** - * Creates the LocalTime value obtained by parsing a string representation of a temporal value - * - * @param string|StringType $localTimeString - * @return LocalTimeType - */ - public static function localTimeString($localTimeString): LocalTimeType - { - if (!($localTimeString instanceof StringType)) { - $localTimeString = self::string($localTimeString); - } - - return FunctionCall::localtime($localTimeString); - } - - /** - * Creates the current Time value - * - * @param null|string|StringType $timezone - * @return TimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-time-current - */ - public static function time($timezone = null): TimeType - { - if ($timezone === null) { - return FunctionCall::time(); - } - - if (!($timezone instanceof StringType)) { - $timezone = self::string($timezone); - } - - return FunctionCall::time(Query::map(["timezone" => $timezone])); - } - - /** - * Creates a Time value with the specified hour, minute, second, millisecond, microsecond, nanosecond and timezone component values - * - * @param int|NumeralType $hour - * @param null|int|NumeralType $minute - * @param null|int|NumeralType $second - * @param null|int|NumeralType $millisecond - * @param null|int|NumeralType $microsecond - * @param null|int|NumeralType $nanosecond - * @param null|string|StringType $timezone - * @return TimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-time-create - */ - public static function timeHMS( - $hour, - $minute = null, - $second = null, - $millisecond = null, - $microsecond = null, - $nanosecond = null, - $timezone = null - ): TimeType { - return FunctionCall::time(self::makeTemporalMap([ - "hour" => $hour, - "minute" => $minute, - "second" => $second, - "millisecond" => $millisecond, - "microsecond" => $microsecond, - "nanosecond" => $nanosecond, - "timezone" => $timezone, - ])); - } - - /** - * Creates the Time value obtained by parsing a string representation of a temporal value - * - * @param string|StringType $timeString - * @return TimeType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-time-create-string - */ - public static function timeString($timeString): TimeType - { - if (!($timeString instanceof StringType)) { - $timeString = self::string($timeString); - } - - return FunctionCall::time($timeString); - } - - /** - * Creates a 2d cartesian point. - * - * @param float|int|NumeralType $x - * @param float|int|NumeralType $y - * @return PointType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-cartesian-2d - */ - public static function point2d($x, $y): PointType - { - if (!($x instanceof NumeralType)) { - $x = self::decimal($x); - } - - if (!($y instanceof NumeralType)) { - $y = self::decimal($y); - } - - $map = [ - "x" => $x, - "y" => $y, - ]; - - $map["crs"] = self::string("cartesian"); - - return FunctionCall::point(Query::map($map)); - } - - /** - * Creates a 3d cartesian point. - * - * @param float|int|NumeralType $x - * @param float|int|NumeralType $y - * @param float|int|NumeralType $z - * @return PointType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-cartesian-3d - */ - public static function point3d($x, $y, $z): PointType - { - if (!($x instanceof NumeralType)) { - $x = self::decimal($x); - } - - if (!($y instanceof NumeralType)) { - $y = self::decimal($y); - } - - if (!($z instanceof NumeralType)) { - $z = self::decimal($z); - } - - $map = [ - "x" => $x, - "y" => $y, - "z" => $z, - ]; - - $map["crs"] = self::string("cartesian-3D"); - - return FunctionCall::point(Query::map($map)); - } - - /** - * Creates a WGS 84 2D point. - * - * @param float|int|NumeralType $longitude - * @param float|int|NumeralType $latitude - * @return PointType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-wgs84-2d - */ - public static function point2dWGS84($longitude, $latitude): PointType - { - if (!($longitude instanceof NumeralType)) { - $longitude = self::decimal($longitude); - } - - if (!($latitude instanceof NumeralType)) { - $latitude = self::decimal($latitude); - } - - $map = [ - "longitude" => $longitude, - "latitude" => $latitude, - ]; - - $map["crs"] = self::string("WGS-84"); - - return FunctionCall::point(Query::map($map)); - } - - /** - * Creates a WGS 84 2D point. - * - * @param float|int|NumeralType $longitude - * @param float|int|NumeralType $latitude - * @param float|int|NumeralType $height - * @return PointType - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-wgs84-2d - */ - public static function point3dWGS84($longitude, $latitude, $height): PointType - { - if (!($longitude instanceof NumeralType)) { - $longitude = self::decimal($longitude); - } - - if (!($latitude instanceof NumeralType)) { - $latitude = self::decimal($latitude); - } - - if (!($height instanceof NumeralType)) { - $height = self::decimal($height); - } - - $map = [ - "longitude" => $longitude, - "latitude" => $latitude, - "height" => $height, - ]; - - $map["crs"] = self::string("WGS-84-3D"); - - return FunctionCall::point(Query::map($map)); - } - - /** - * Prepares the variables to be used by temporal (i.e. time-like) CYPHER-functions. - * - * The following are done: - * - For all $variables except for the 'timezone' it is checked if any one of them exists without the previous variable existing, up to & including the 'second' variable. - * - If a 'second' variable is encountered, it is checked if 'seconds' is not-null when milliseconds/microseconds/nanoseconds are provided. - * - All variables except 'timezone' are made into NumeralType. - * - 'timezone' is made into StringLiteral. - * - * @param array $variables - * @return Map - */ - private static function makeTemporalMap(array $variables): PropertyMap - { - $map = []; - - $nullEncountered = false; - $secondsFound = false; - - foreach ($variables as $key => $variable) { - if ($variable === null) { - $nullEncountered = true; - - continue; - } - - if ($key === 'timezone') { - // Timezone can always be added, and is a string. - $map[$key] = self::convertToString($variable); - } else { - if (!$secondsFound && $nullEncountered) { - // Check if none of the previous, i.e. more important components, are null. - // sub-second values are not interdependend, but seconds must then be provided. - throw new \LogicException("The key $key can only be provided when all more significant components are provided as well."); - } - - if ($key === 'second') { - $secondsFound = true; - } - - $map[$key] = self::convertToNumeral($variable); - } - } - - return Query::map($map); - } - - private static function convertToNumeral($var): NumeralType - { - if ($var instanceof NumeralType) { - return $var; - } - - return self::decimal($var); - } - - private static function convertToString($var): StringLiteral - { - if ($var instanceof StringLiteral) { - return $var; - } - - return self::string($var); - } -} diff --git a/src/Literals/StringLiteral.php b/src/Literals/StringLiteral.php deleted file mode 100644 index 29ddb226..00000000 --- a/src/Literals/StringLiteral.php +++ /dev/null @@ -1,101 +0,0 @@ -value = $value; - } - - /** - * Returns the string value. - * - * @return string - */ - public function getValue(): string - { - return $this->value; - } - - /** - * Returns whether the string uses double quotes. Single quotes are used if false. - * - * @return bool - */ - public function usesDoubleQuotes(): bool - { - return $this->useDoubleQuotes; - } - - /** - * Whether to use double quotes or not. - * - * @param bool $useDoubleQuotes - */ - public function useDoubleQuotes(bool $useDoubleQuotes = true): void - { - $this->useDoubleQuotes = $useDoubleQuotes; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - // Encode tabs, newlines, carriage returns and form feeds - $value = str_replace(["\t", "\n", "\r", "\f"], ["\\t", "\\n", "\\r", "\\f"], $this->value); - - if ($this->useDoubleQuotes) { - return sprintf('"%s"', str_replace('"', '\"', $value)); - } - - return sprintf("'%s'", str_replace("'", "\'", $value)); - } -} diff --git a/src/Minus.php b/src/Minus.php deleted file mode 100644 index 825e56df..00000000 --- a/src/Minus.php +++ /dev/null @@ -1,68 +0,0 @@ -expression = $expression; - } - - /** - * Returns the expression to negate. - * - * @return NumeralType - */ - public function getExpression(): NumeralType - { - return $this->expression; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return sprintf("-%s", $this->expression->toQuery()); - } -} diff --git a/src/Modulo.php b/src/Modulo.php deleted file mode 100644 index c6266e9c..00000000 --- a/src/Modulo.php +++ /dev/null @@ -1,51 +0,0 @@ -expression = $expression; - } - - /** - * Returns the expression to negate. - * - * @return BooleanType - */ - public function getExpression(): BooleanType - { - return $this->expression; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return sprintf("(NOT %s)", $this->expression->toQuery()); - } -} diff --git a/src/OrOperator.php b/src/OrOperator.php deleted file mode 100644 index fd178957..00000000 --- a/src/OrOperator.php +++ /dev/null @@ -1,51 +0,0 @@ -configureName($parameter, 'param'); - } - - /** - * Returns the parameter name. - * - * @return string - */ - public function getParameter(): string - { - return $this->getName(); - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return sprintf('$%s', $this->getName()); - } -} diff --git a/src/Patterns/CompletePattern.php b/src/Patterns/CompletePattern.php new file mode 100644 index 00000000..5a968d7b --- /dev/null +++ b/src/Patterns/CompletePattern.php @@ -0,0 +1,23 @@ +labels[] = $label; } } + /** + * Sets the labels of this node. This overwrites any previously set labels. + * + * @param string[] $labels + * + * @return $this + */ + public function withLabels(array $labels): self + { + $this->labels = $labels; + + return $this; + } + + /** + * Adds one or more labels to this node. + * + * @param string ...$label + * + * @return $this + */ + public function addLabel(string ...$label): self + { + $this->labels = array_merge($this->labels, $label); + + return $this; + } + /** * Returns the labels of the node. * @@ -64,25 +83,60 @@ public function getLabels(): array } /** - * Adds a label to the node. + * Returns a label with the variable in this node. * - * @param string $label - * @return Node + * @param string ...$labels The labels to attach to the variable in this node */ - public function labeled(string $label): self + public function labeled(string ...$labels): Label { - $this->labels[] = $label; + return new Label($this->getVariable(), ...$labels); + } - return $this; + /** + * @inheritDoc + */ + public function relationship(Relationship $relationship, Pattern $pattern): Path + { + return (new Path($this))->relationship($relationship, $pattern); + } + + /** + * @inheritDoc + */ + public function relationshipTo(Pattern $pattern, ?string $type = null, $properties = null, $name = null): Path + { + return (new Path($this))->relationshipTo($pattern, $type, $properties, $name); + } + + /** + * @inheritDoc + */ + public function relationshipFrom(Pattern $pattern, ?string $type = null, $properties = null, $name = null): Path + { + return (new Path($this))->relationshipFrom($pattern, $type, $properties, $name); + } + + /** + * @inheritDoc + */ + public function relationshipUni(Pattern $pattern, ?string $type = null, $properties = null, $name = null): Path + { + return (new Path($this))->relationshipUni($pattern, $type, $properties, $name); } /** * Returns the string representation of this relationship that can be used directly * in a query. - * - * @return string */ public function toQuery(): string + { + return sprintf("(%s)", $this->nodeInnerToString()); + } + + /** + * Returns the string representation of the inner part of a node. + */ + private function nodeInnerToString(): string { $nodeInner = ""; @@ -92,7 +146,7 @@ public function toQuery(): string if ($this->labels !== []) { foreach ($this->labels as $label) { - $nodeInner .= ":{$this->escape($label)}"; + $nodeInner .= ":" . self::escape($label); } } @@ -101,21 +155,11 @@ public function toQuery(): string $nodeInner .= " "; } - $nodeInner .= $this->properties->toQuery(); + if (!$this->properties instanceof Map || !$this->properties->isEmpty()) { + $nodeInner .= $this->properties->toQuery(); + } } - return "($nodeInner)"; - } - - /** - * Returns a property on the node. - * - * @param string $property The name of the property. - * - * @return Property - */ - public function property(string $property): Property - { - return new Property($this->getName(), $property); + return $nodeInner; } } diff --git a/src/Patterns/Path.php b/src/Patterns/Path.php index 73d002d3..ca838e30 100644 --- a/src/Patterns/Path.php +++ b/src/Patterns/Path.php @@ -1,109 +1,63 @@ -nodes = is_array($nodes) ? array_values($nodes) : [$nodes]; - $this->relationships = is_array($relationships) ? array_values($relationships) : [$relationships]; - } + /** + * @var Node[] + */ + private array $nodes; /** - * @inheritDoc + * @param Node|Node[] $nodes + * @param Relationship|Relationship[] $relationships + * + * @internal This method is not covered by the backwards compatibility guarantee of php-cypher-dsl */ - public function toQuery(): string + public function __construct($nodes = [], $relationships = []) { - // If there are no nodes in the path, it must be empty. - if (count($this->nodes) === 0) { - return ''; - } - - $cql = ''; - // If a variable exists, we need to assign following the expression to it - if ($this->getVariable() !== null) { - $cql = $this->getName()->toQuery() . ' = '; - } - - // We use the relationships as a reference point to iterate over. - // The nodes position will be calculated using the index of the current relationship. - // If R is the position of the relationship, N is the position of te node, and x the amount of relationships - // in the path, then the positional structure is like this: - // N0 - R0 - N1 - R1 - N2 - ... - Nx - Rx - N(x + 1) - foreach ($this->relationships as $i => $relationship) { - // To avoid a future crash, we already look for the node at the end of the relationship. - // If the following node does not exist, we must break the loop early. - // This case will be triggered if the amount of nodes is equal or less than the amount of relationships - // and is thus very unlikely. - if (!array_key_exists($i + 1, $this->nodes)) { - --$i; + $nodes = is_array($nodes) ? $nodes : [$nodes]; + $relationships = is_array($relationships) ? $relationships : [$relationships]; - break; - } - $cql .= $this->nodes[$i]->toQuery(); - $cql .= $relationship->toQuery(); - } + self::assertClassArray('nodes', Node::class, $nodes); + self::assertClassArray('relationships', Relationship::class, $relationships); - // Since the iteration leaves us at the final relationship, we still need to add the final node. - // If the path is simply a single node, $i won't be defined, hence the null coalescing operator with -1. By - // coalescing with -1 instead of 0, we remove the need of a separate if check, making both cases valid when they - // are incremented by 1. - $cql .= $this->nodes[($i ?? -1) + 1]->toQuery(); - - return $cql; + $this->nodes = $nodes; + $this->relationships = $relationships; } /** - * Returns the nodes in the path in sequential order. + * Returns the relatables in the path in sequential order. * * @return Node[] */ @@ -125,17 +79,20 @@ public function getRelationships(): array /** * @inheritDoc */ - public function relationship(RelationshipType $relationship, HasRelationshipsType $nodeOrPath): Path + public function relationship(Relationship $relationship, Pattern $pattern): self { - self::assertClass('nodeOrPath', [__CLASS__, Node::class], $nodeOrPath); - $this->relationships[] = $relationship; - if ($nodeOrPath instanceof self) { - $this->relationships = array_merge($this->relationships, $nodeOrPath->getRelationships()); - $this->nodes = array_merge($this->nodes, $nodeOrPath->getNodes()); - } else { - $this->nodes []= $nodeOrPath; + + if ($pattern instanceof self) { + // If the given relatable is also a path, we can merge their relatables and relationships + $this->relationships = array_merge($this->relationships, $pattern->getRelationships()); + $this->nodes = array_merge($this->nodes, $pattern->getNodes()); + + return $this; } + // Otherwise, add the relatable to the list of nodes + // @phpstan-ignore-next-line + $this->nodes[] = $pattern; return $this; } @@ -143,50 +100,96 @@ public function relationship(RelationshipType $relationship, HasRelationshipsTyp /** * @inheritDoc */ - public function relationshipTo(HasRelationshipsType $nodeOrPath, ?string $type = null, $properties = null, $name = null): Path + public function relationshipTo(Pattern $pattern, ?string $type = null, $properties = null, $name = null): self { - $relationship = $this->buildRelationship(Relationship::DIR_RIGHT, $type, $properties, $name); - - return $this->relationship($relationship, $nodeOrPath); + return $this->relationship( + self::buildRelationship(Relationship::DIR_RIGHT, $type, $properties, $name), + $pattern + ); } /** * @inheritDoc */ - public function relationshipFrom(HasRelationshipsType $nodeOrPath, ?string $type = null, $properties = null, $name = null): Path + public function relationshipFrom(Pattern $pattern, ?string $type = null, $properties = null, $name = null): self { - $relationship = $this->buildRelationship(Relationship::DIR_LEFT, $type, $properties, $name); + return $this->relationship( + self::buildRelationship(Relationship::DIR_LEFT, $type, $properties, $name), + $pattern + ); + } - return $this->relationship($relationship, $nodeOrPath); + /** + * @inheritDoc + */ + public function relationshipUni(Pattern $pattern, ?string $type = null, $properties = null, $name = null): self + { + return $this->relationship( + self::buildRelationship(Relationship::DIR_UNI, $type, $properties, $name), + $pattern + ); } /** * @inheritDoc */ - public function relationshipUni(HasRelationshipsType $nodeOrPath, ?string $type = null, $properties = null, $name = null): Path + public function toQuery(): string { - $relationship = $this->buildRelationship(Relationship::DIR_UNI, $type, $properties, $name); + // If there are no nodes in the path, it must be empty. + if (count($this->nodes) === 0) { + return ''; + } + + $cql = ''; - return $this->relationship($relationship, $nodeOrPath); + // If a variable exists, we need to assign following the expression to it; this results in a named + // path as described in page 66 of the openCypher reference (version 9). + if (isset($this->variable)) { + $cql = $this->variable->toQuery() . ' = '; + } + + // We use the relationships as a reference point to iterate over. + // The nodes position will be calculated using the index of the current relationship. + // If R is the position of the relationship, N is the position of te node, and x the amount of relationships + // in the path, then the positional structure is like this: + // N0 - R0 - N1 - R1 - N2 - ... - Nx - Rx - N(x + 1) + foreach ($this->relationships as $i => $relationship) { + // To avoid a future crash, we already look for the node at the end of the relationship. + // If the following node does not exist, we must break the loop early. + // This case will be triggered if the amount of nodes is equal or less than the amount of relationships + // and is thus very unlikely. + if (!array_key_exists($i + 1, $this->nodes)) { + --$i; + + break; + } + $cql .= $this->nodes[$i]->toQuery(); + $cql .= $relationship->toQuery(); + } + + // Since the iteration leaves us at the final relationship, we still need to add the final node. + // If the path is simply a single node, $i won't be defined, hence the null coalescing operator with -1. By + // coalescing with -1 instead of 0, we remove the need of a separate if check, making both cases valid when they + // are incremented by 1. + $cql .= $this->nodes[($i ?? -1) + 1]->toQuery(); + + return $cql; } /** - * @param array $direction - * @param string|null $type - * @param mixed $properties - * @param mixed $name + * Construct a new relationship from the given parameters. * - * @return Relationship + * @param string[] $direction The direction of the relationship (should be a Relationship::DIR_* constant) + * @param null|string $type The type of the relationship + * @param null|MapType|mixed[] $properties The properties to add to the relationship + * @param null|string|Variable $name The name of the variable to which to assign this relationship */ - private function buildRelationship(array $direction, ?string $type, $properties, $name): Relationship + private static function buildRelationship(array $direction, ?string $type = null, $properties = null, $name = null): Relationship { - self::assertClass('properties', ['array', PropertyMap::class, 'null'], $properties); - self::assertClass('name', ['string', Variable::class, 'null'], $name); - $relationship = new Relationship($direction); if ($type !== null) { - $relationship->withType($type); + $relationship->addType($type); } if ($properties !== null) { @@ -194,7 +197,7 @@ private function buildRelationship(array $direction, ?string $type, $properties, } if ($name !== null) { - $relationship->named($name); + $relationship->withVariable($name); } return $relationship; diff --git a/src/Patterns/Pattern.php b/src/Patterns/Pattern.php new file mode 100644 index 00000000..38806119 --- /dev/null +++ b/src/Patterns/Pattern.php @@ -0,0 +1,51 @@ +) + * relationship. + * + * @param RelatablePattern $pattern The pattern to attach to the end of this pattern + * @param null|string $type The type of the relationship + * @param null|MapType|mixed[] $properties The properties to attach to the relationship + * @param null|string|Variable $name The name fo the relationship + */ + public function relationshipTo(self $pattern, ?string $type = null, $properties = null, $name = null): Path; + + /** + * Forms a new path by adding the given relatable pattern to the end of this pattern using a left (<--) + * relationship. + * + * @param RelatablePattern $pattern The pattern to attach to the end of this pattern + * @param null|string $type The type of the relationship + * @param null|MapType|mixed[] $properties The properties to attach to the relationship + * @param null|string|Variable $name The name fo the relationship + */ + public function relationshipFrom(self $pattern, ?string $type = null, $properties = null, $name = null): Path; + + /** + * Forms a new path by adding the given relatable pattern to the end of this pattern using a unidirectional + * (--/<-->) relationship. + * + * @param RelatablePattern $pattern The pattern to attach to the end of this pattern + * @param null|string $type The type of the relationship + * @param null|MapType|mixed[] $properties The properties to attach to the relationship + * @param null|string|Variable $name The name fo the relationship + */ + public function relationshipUni(self $pattern, ?string $type = null, $properties = null, $name = null): Path; +} diff --git a/src/Patterns/Relationship.php b/src/Patterns/Relationship.php index f2268c1e..2d0ee726 100644 --- a/src/Patterns/Relationship.php +++ b/src/Patterns/Relationship.php @@ -1,82 +1,75 @@ -"]; + public const DIR_LEFT = ["<-", "-"]; + public const DIR_UNI = ["-", "-"]; /** - * @var string[] The direction of the relationship + * @var string[] The direction of the relationship (one of the DIR_* constants) */ private array $direction; /** - * @var string[] + * @var string[] A list of relationship condition types */ private array $types = []; /** - * @var int|null The minimum number of `relationship->node` hops away to search + * @var null|int The minimum number of `relationship->node` hops away to search */ private ?int $minHops = null; /** - * @var int|null The maximum number of `relationship->node` hops away to search + * @var null|int The maximum number of `relationship->node` hops away to search */ private ?int $maxHops = null; /** - * @var int|null The exact number of `relationship->node` hops away to search + * @var null|int The exact number of `relationship->node` hops away to search */ private ?int $exactHops = null; /** - * Path constructor. + * @var bool Whether to allow arbitrary hops between nodes + */ + private bool $arbitraryHops = false; + + /** + * @param string[] $direction The direction of the relationship, should be either: + * - Relationship::DIR_RIGHT (for a relation of (a)-->(b)) + * - Relationship::DIR_LEFT (for a relation of (a)<--(b)) + * - Relationship::DIR_UNI (for a relation of (a)--(b)) * - * @param array $direction The direction of the relationship, should be either: - * - Path::DIR_RIGHT (for a relation of - * (a)-->(b)) - Path::DIR_LEFT (for a relation - * of (a)<--(b)) - Path::DIR_UNI (for a - * relation of (a)--(b)) + * @internal This method is not covered by the backwards compatibility guarantee of php-cypher-dsl */ public function __construct(array $direction) { @@ -87,21 +80,12 @@ public function __construct(array $direction) $this->direction = $direction; } - /** - * @return string[] - */ - public function getDirection(): array - { - return $this->direction; - } - /** * Set the minimum number of `relationship->node` hops away to search. * - * @see https://neo4j.com/docs/cypher-manual/current/clauses/match/#varlength-rels + * @see https://neo4j.com/docs/cypher-manual/current/clauses/match/#varlength-rels Corresponding documentation on Neo4j.com * - * @param int $minHops - * @return Relationship + * @return $this */ public function withMinHops(int $minHops): self { @@ -113,8 +97,8 @@ public function withMinHops(int $minHops): self throw new DomainException("minHops cannot be greater than maxHops"); } - if (isset($this->exactHops)) { - throw new LogicException("Cannot use minHops in combination with exactHops"); + if (isset($this->exactHops) || $this->arbitraryHops) { + throw new LogicException("minHops cannot be used in combination with exactHops or arbitraryHops"); } $this->minHops = $minHops; @@ -125,10 +109,9 @@ public function withMinHops(int $minHops): self /** * Set the maximum number of `relationship->node` hops away to search. * - * @see https://neo4j.com/docs/cypher-manual/current/clauses/match/#varlength-rels + * @see https://neo4j.com/docs/cypher-manual/current/clauses/match/#varlength-rels Corresponding documentation on Neo4j.com * - * @param int $maxHops - * @return Relationship + * @return $this */ public function withMaxHops(int $maxHops): self { @@ -140,8 +123,8 @@ public function withMaxHops(int $maxHops): self throw new DomainException("maxHops cannot be less than minHops"); } - if (isset($this->exactHops)) { - throw new LogicException("Cannot use maxHops in combination with exactHops"); + if (isset($this->exactHops) || $this->arbitraryHops) { + throw new LogicException("maxHops cannot be used in combination with exactHops or arbitraryHops"); } $this->maxHops = $maxHops; @@ -152,10 +135,9 @@ public function withMaxHops(int $maxHops): self /** * Set the exact number of `relationship->node` hops away to search. * - * @see https://neo4j.com/docs/cypher-manual/current/clauses/match/#varlength-rels + * @see https://neo4j.com/docs/cypher-manual/current/clauses/match/#varlength-rels Corresponding documentation on Neo4j.com * - * @param int $exactHops - * @return Relationship + * @return $this */ public function withExactHops(int $exactHops): self { @@ -163,8 +145,8 @@ public function withExactHops(int $exactHops): self throw new DomainException("exactHops cannot be less than 1"); } - if (isset($this->minHops) || isset($this->maxHops)) { - throw new LogicException("Cannot use exactHops in combination with minHops or maxHops"); + if (isset($this->minHops) || isset($this->maxHops) || $this->arbitraryHops) { + throw new LogicException("exactHops cannot be used in combination with minHops, maxHops or arbitraryHops"); } $this->exactHops = $exactHops; @@ -173,80 +155,61 @@ public function withExactHops(int $exactHops): self } /** - * @param string $type - * @return Relationship + * Set the number of hops to be an arbitrary number (wildcard). + * + * @return $this */ - public function withType(string $type): self + public function withArbitraryHops(bool $arbitraryHops = true): self { - $this->types[] = $type; + if (isset($this->minHops) || isset($this->maxHops) || isset($this->exactHops)) { + throw new LogicException("arbitraryHops cannot be used in combination with minHops, maxHops or exactHops"); + } + + $this->arbitraryHops = $arbitraryHops; return $this; } /** - * Returns the string representation of this relationship that can be used directly - * in a query. + * The types to require for this relationship. Will overwrite any previously set types. * - * @return string + * @param string[] $types + * + * @return $this */ - public function toQuery(): string + public function withTypes(array $types): self { - return $this->direction[0] . $this->conditionToString() . $this->direction[1]; + $this->types = $types; + + return $this; } /** - * @return string + * Add one or more types to require for this relationship. + * + * @param string ...$type + * + * @return $this */ - private function conditionToString(): string + public function addType(string ...$type): self { - $conditionInner = ""; - - // The condition always starts with the variable - if (isset($this->variable)) { - $conditionInner .= $this->variable->toQuery(); - } - - $types = array_filter($this->types); - - if (count($types) !== 0) { - // If we have at least one condition type, escape them and insert them into the query - $escapedTypes = array_map(fn (string $type): string => $this->escape($type), $types); - $conditionInner .= sprintf(":%s", implode("|", $escapedTypes)); - } - - if (isset($this->minHops) || isset($this->maxHops)) { - // We have either a minHop, maxHop or both - $conditionInner .= "*"; - - if (isset($this->minHops)) { - $conditionInner .= $this->minHops; - } - - $conditionInner .= '..'; + $this->types = array_merge($this->types, $type); - if (isset($this->maxHops)) { - $conditionInner .= $this->maxHops; - } - } elseif (isset($this->exactHops)) { - $conditionInner .= '*' . $this->exactHops; - } - - if (isset($this->properties)) { - if ($conditionInner !== "") { - // Add some padding between the property list and the preceding structure - $conditionInner .= " "; - } - - $conditionInner .= $this->properties->toQuery(); - } + return $this; + } - return sprintf("[%s]", $conditionInner); + /** + * Returns the direction of this relationship (one of the Relationship::DIR_* constants). + * + * @return string[] + */ + public function getDirection(): array + { + return $this->direction; } /** * Returns the exact amount of hops configured. - * - * @return int|null */ public function getExactHops(): ?int { @@ -254,9 +217,7 @@ public function getExactHops(): ?int } /** - * Returns the maximum amount of hops configured - * - * @return int|null + * Returns the maximum amount of hops configured. */ public function getMaxHops(): ?int { @@ -265,8 +226,6 @@ public function getMaxHops(): ?int /** * Returns the minimum amount of hops configured. - * - * @return int|null */ public function getMinHops(): ?int { @@ -283,8 +242,74 @@ public function getTypes(): array return $this->types; } - public function property(string $property): Property + /** + * Returns the string representation of this relationship that can be used directly + * in a query. + */ + public function toQuery(): string + { + return $this->direction[0] . $this->relationshipDetailToString() . $this->direction[1]; + } + + /** + * Converts the relationship details (the inner part of the relationship) to Cypher syntax. + */ + private function relationshipDetailToString(): string { - return new Property($this->getName(), $property); + if (isset($this->variable)) { + $conditionInner = $this->variable->toQuery(); + } else { + $conditionInner = ""; + } + + $types = array_filter($this->types); + + if (count($types) !== 0) { + // If we have at least one condition type, escape them and insert them into the query + $escapedTypes = array_map(static fn (string $type): string => self::escape($type), $types); + $conditionInner .= sprintf(":%s", implode("|", $escapedTypes)); + } + + if (isset($this->minHops) || isset($this->maxHops)) { + // We have either a minHop, maxHop or both + $conditionInner .= "*"; + + if (isset($this->minHops)) { + $conditionInner .= $this->minHops; + } + + $conditionInner .= '..'; + + if (isset($this->maxHops)) { + $conditionInner .= $this->maxHops; + } + } elseif (isset($this->exactHops)) { + // We have an exact number of hops + $conditionInner .= '*' . $this->exactHops; + } elseif ($this->arbitraryHops) { + // We have an arbitrary number of hops + $conditionInner .= '*'; + } + + if (isset($this->properties)) { + // Only add the properties if they're not empty + if (!$this->properties instanceof Map || $this->properties->getElements() !== []) { + $map = $this->properties->toQuery(); + + if ($conditionInner !== "") { + // Add some padding between the property map and the preceding structure + $conditionInner .= " "; + } + + $conditionInner .= $map; + } + } + + if ($conditionInner === '') { + // If there is no condition, we can also omit the square brackets + return ''; + } + + return sprintf("[%s]", $conditionInner); } } diff --git a/src/Property.php b/src/Property.php deleted file mode 100644 index 66480560..00000000 --- a/src/Property.php +++ /dev/null @@ -1,138 +0,0 @@ -expression = $expression; - $this->property = $property; - } - - /** - * Assign a value to this property. - * - * @param AnyType $value The value to assign - * @return Assignment - */ - public function assign(AnyType $value): Assignment - { - return new Assignment($this, $value); - } - - /** - * Returns the property name. - * - * @return string - */ - public function getProperty(): string - { - return $this->property; - } - - /** - * Returns the map type of the property. - * - * @return MapType - */ - public function getExpression(): MapType - { - return $this->expression; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return sprintf("%s.%s", $this->expression->toQuery(), $this->escape($this->property)); - } -} diff --git a/src/PropertyMap.php b/src/PropertyMap.php deleted file mode 100644 index 9896e727..00000000 --- a/src/PropertyMap.php +++ /dev/null @@ -1,113 +0,0 @@ -properties = $properties; - } - - /** - * Adds a property for the given name with the given value. Overrides the property if it already exists. - * - * @param string $key The name of the property - * @param AnyType $value The value of the property - * @return PropertyMap - */ - public function addProperty(string $key, AnyType $value): self - { - $this->properties[$key] = $value; - - return $this; - } - - /** - * Merges the given PropertyMap with this PropertyMap. - * - * @param PropertyMap $propertyMap - * @return PropertyMap - */ - public function mergeWith(PropertyMap $propertyMap): self - { - $this->properties = array_merge($this->properties, $propertyMap->properties); - - return $this; - } - - /** - * Returns the map of properties as a number of key-expression pairs. - * - * @return AnyType[] - */ - public function getProperties(): array - { - return $this->properties; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - $pairs = []; - - foreach ($this->properties as $key => $value) { - $pairs[] = sprintf("%s: %s", $this->escape(strval($key)), $value->toQuery()); - } - - return sprintf("{%s}", implode(", ", $pairs)); - } -} diff --git a/src/Query.php b/src/Query.php index d61cb58c..6e43318b 100644 --- a/src/Query.php +++ b/src/Query.php @@ -1,27 +1,15 @@ -(b)) - * - Path::DIR_LEFT (for a relation of (a)<--(b)) - * - Path::DIR_UNI (for a relation of (a)--(b)) + * @param string[] $direction The direction of the relationship (optional, default: unidirectional), should be either: + * - Relationship::DIR_RIGHT (for a relation of (a)-->(b)) + * - Relationship::DIR_LEFT (for a relation of (a)<--(b)) + * - Relationship::DIR_UNI (for a relation of (a)--(b)) * - * @return Relationship - * @see https://neo4j.com/docs/cypher-manual/current/syntax/patterns/#cypher-pattern-relationship + * @see https://neo4j.com/docs/cypher-manual/current/syntax/patterns/#cypher-pattern-relationship Corresponding documentation on Neo4j.com */ - public static function relationship(array $direction): Relationship + public static function relationship(array $direction = Relationship::DIR_UNI): Relationship { return new Relationship($direction); } /** - * Creates a variable. + * Creates a unidirectional relationship. + * + * @see https://neo4j.com/docs/cypher-manual/current/syntax/patterns/#cypher-pattern-relationship Corresponding documentation on Neo4j.com + */ + public static function relationshipUni(): Relationship + { + return new Relationship(Relationship::DIR_UNI); + } + + /** + * Creates a right relationship. + * + * @see https://neo4j.com/docs/cypher-manual/current/syntax/patterns/#cypher-pattern-relationship Corresponding documentation on Neo4j.com + */ + public static function relationshipTo(): Relationship + { + return new Relationship(Relationship::DIR_RIGHT); + } + + /** + * Creates a left relationship. * - * @param string|null $variable The name of the variable; leave empty to automatically generate a variable name. - * @return Variable + * @see https://neo4j.com/docs/cypher-manual/current/syntax/patterns/#cypher-pattern-relationship Corresponding documentation on Neo4j.com + */ + public static function relationshipFrom(): Relationship + { + return new Relationship(Relationship::DIR_LEFT); + } + + /** + * Creates a new variable with the given name, or generates a new variable with a random unique name. + * + * @param null|string $variable the name of the variable, or null to automatically generate a name\ + * + * @see https://neo4j.com/docs/cypher-manual/current/syntax/variables/ Corresponding documentation on Neo4j.com */ public static function variable(?string $variable = null): Variable { @@ -132,17 +161,53 @@ public static function variable(?string $variable = null): Variable * Creates a new literal from the given value. This function automatically constructs the appropriate * class based on the type of the value given. * - * This function cannot be used directly to construct Point or Date types. - * - * You can create a Point literal by using any of the following functions: - * - * Query::literal()::point2d(...) - For a 2D cartesian point - * Query::literal()::point3d(...) - For a 3D cartesian point - * Query::literal()::point2dWGS84(...) - For a 2D WGS 84 point - * Query::literal()::point3dWGS84(...) - For a 3D WGS 84 point - * - * @param mixed $literal The literal to construct - * @return StringLiteral|Boolean|Decimal|Literal|string + * This function cannot be used directly to construct Point or Date types. Instead, you can create a Point literal + * by using any of the following functions: + * + * - Query::literal()::point2d(...) - For a 2D cartesian point + * - Query::literal()::point3d(...) - For a 3D cartesian point + * - Query::literal()::point2dWGS84(...) - For a 2D WGS 84 point + * - Query::literal()::point3dWGS84(...) - For a 3D WGS 84 point + * + * And a Date literal by using one of the following functions: + * + * - Query::literal()::date(...) - For the current date + * - Query::literal()::dateYMD(...) - For a date from the given year, month and day + * - Query::literal()::dateYWD(...) - For a date from the given year, week and day + * - Query::literal()::dateString(...) - For a date from the given date string + * - Query::literal()::dateTime(...) - For the current datetime + * - Query::literal()::dateTimeYMD(...) - For a datetime from the given parameters (see function definition) + * - Query::literal()::dateTimeYWD(...) - For a datetime from the given parameters (see function definition) + * - Query::literal()::dateTimeYQD(...) - For a datetime from the given parameters (see function definition) + * - Query::literal()::dateTimeYD(...) - For a datetime from the given parameters (see function definition) + * - Query::literal()::dateTimeString(...) - For a datetime from the given datetime string + * - Query::literal()::localDateTime(...) - For the current local datetime + * - Query::literal()::localDateTimeYMD(...) - For a local datetime from the given parameters (see function definition) + * - Query::literal()::localDateTimeYWD(...) - For a local datetime from the given parameters (see function definition) + * - Query::literal()::localDateTimeYQD(...) - For a local datetime from the given parameters (see function definition) + * - Query::literal()::localDateTimeYD(...) - For a local datetime from the given parameters (see function definition) + * - Query::literal()::localDateTimeString(...) - For a local datetime from the given datetime string + * - Query::literal()::localTimeCurrent(...) - For the current LocalTime + * - Query::literal()::localTime(...) - For a local time from the given parameters (see function definition) + * - Query::literal()::localTimeString(...) - For a local time from the given time string + * - Query::literal()::time(...) - For the curren time + * - Query::literal()::timeHMS(...) - For a time from the given hour, minute and second + * - Query::literal()::timeString(...) - For a time from the given time string + * + * When no arguments are given to this function, the function will return a reference to the Literal class. + * + * You can directly call the constructors of the most basic types: + * + * - Query::boolean() - For a boolean + * - Query::string() - For a string + * - Query::integer() - For an integer + * - Query::float() - For a float + * - Query::list() - For a list + * - Query::map() - For a map + * + * @param null|bool|float|int|mixed[]|string $literal The literal to construct + * + * @return Boolean|class-string|Float_|Integer|List_|Map|String_ */ public static function literal($literal = null) { @@ -154,90 +219,228 @@ public static function literal($literal = null) } /** - * Creates a list of expressions. - * - * @param iterable $values - * @return ExpressionList + * Creates a new boolean. */ - public static function list(iterable $values): ExpressionList + public static function boolean(bool $value): Boolean { - $expressions = []; - foreach ($values as $value) { - $expressions[] = $value instanceof AnyType ? - $value : self::literal($value); - } + // @phpstan-ignore-next-line + return self::literal()::boolean($value); + } + + /** + * Creates a new string. + */ + public static function string(string $value): String_ + { + // @phpstan-ignore-next-line + return self::literal()::string($value); + } - return new ExpressionList($expressions); + /** + * Creates a new integer. + */ + public static function integer(int $value): Integer + { + // @phpstan-ignore-next-line + return self::literal()::integer($value); + } + + /** + * Creates a new float. + */ + public static function float(float $value): Float_ + { + // @phpstan-ignore-next-line + return self::literal()::float($value); } /** - * Creates a property map. + * Creates a new list literal. * - * @param AnyType[] $properties The map of properties as a number of key-expression pairs - * @return PropertyMap + * @param mixed[] $value */ - public static function map(array $properties): PropertyMap + public static function list(iterable $value): List_ { - return new PropertyMap($properties); + // @phpstan-ignore-next-line + return self::literal()::list($value); } /** - * Creates a parameter. + * Creates a new map literal. * - * @param string $parameter The name of the parameter; may only consist of alphanumeric characters and - * underscores - * @return Parameter + * @param mixed[] $value */ - public static function parameter(string $parameter): Parameter + public static function map(array $value): Map { - return new Parameter($parameter); + // @phpstan-ignore-next-line + return self::literal()::map($value); } /** - * Returns the name of the FunctionCall class. This can be used to more easily create new functions calls, like so: + * Creates a new parameter. * - * Query::function()::raw(...) + * @param null|string $parameter The name of the parameter, or null to automatically generate a name + */ + public static function parameter(?string $parameter = null): Parameter + { + return new Parameter($parameter); + } + + /** + * Returns the class string of the "Procedure" class. This can be used to more easily create new functions calls. * - * @return FunctionCall + * @return class-string */ public static function function(): string { - return self::function; + return self::procedure(); + } + + /** + * Returns the class string of the "Procedure" class. This can be used to more easily create new functions calls. + * + * @return class-string + */ + public static function procedure(): string + { + return self::procedure; } /** - * Creates a raw expression. + * Creates a new raw expression. + * + * @note This should be used only for features that are not implemented by the DSL. * * @param string $expression The raw expression - * @return ListType|MapType|BooleanType|NumeralType|StringType|NodeType|PathType */ - public static function rawExpression(string $expression): AnyType + public static function rawExpression(string $expression): RawExpression { return new RawExpression($expression); } + /** + * Creates an EXISTS expression. + * + * @param CompletePattern|CompletePattern[]|MatchClause $match + * @param null|BooleanType|WhereClause $where + */ + public static function exists($match, $where = null, bool $insertParentheses = false): Exists + { + if (!$match instanceof MatchClause) { + $match = is_array($match) ? $match : [$match]; + $match = (new MatchClause())->addPattern(...$match); + } + + if (!$where instanceof WhereClause && $where !== null) { + $where = (new WhereClause())->addExpression($where); + } + + return new Exists($match, $where, $insertParentheses); + } + + /** + * @see Query::new() + * + * @internal This method is not covered by the backwards compatibility guarantee of php-cypher-dsl + */ + public function __construct() + { + // This constructor currently does nothing, but we still define it, so we can mark it as internal. We could + // make it private, but people might assume the class should not be constructed at all. + } + + /** + * Automatically build the query if this object is used as a string somewhere. + */ + public function __toString(): string + { + return $this->build(); + } + + /** + * Creates a CALL sub query clause and adds it to the query. + * + * @note This feature is not part of the openCypher standard. + * + * @param callable|Query $query A callable decorating a Query, or an instance of Query + * @param Pattern|Pattern[]|string|string[]|Variable|Variable[]|(Pattern|string|Variable)[] $variables The variables to include in the WITH clause for correlation (optional) + * + * @return $this + * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/call-subquery/ Corresponding documentation on Neo4j.com + */ + public function call($query, $variables = []): self + { + $this->assertClass('query', [self::class, Closure::class, 'callable'], $query); + + if (is_callable($query)) { + $subQuery = self::new(); + $query($subQuery); + } else { + $subQuery = $query; + } + + if (!is_array($variables)) { + $variables = [$variables]; + } + + $callClause = new CallClause(); + $callClause->withSubQuery($subQuery); + $callClause->addWithVariable(...$variables); + + $this->clauses[] = $callClause; + + return $this; + } + + /** + * Creates the CALL procedure clause. + * + * @param Procedure|string $procedure The procedure to call + * @param AnyType|AnyType[]|bool|bool[]|float|float[]|int|int[]|mixed[]|mixed[][]|Pattern|Pattern[]|string|string[]|(AnyType|bool|float|int|mixed[]|Pattern|string)[] $yields The result fields that should be returned (optional) + * + * @return $this + * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/call/ Corresponding documentation on Neo4j.com + */ + public function callProcedure($procedure, $yields = []): self + { + if (is_string($procedure)) { + $procedure = Procedure::raw($procedure); + } + + if (!is_array($yields)) { + $yields = [$yields]; + } + + $yields = $this->makeAliasArray($yields, fn ($value) => $this->toName($value)); + + $callProcedureClause = new CallProcedureClause(); + $callProcedureClause->setProcedure($procedure); + $callProcedureClause->addYield(...$yields); + + $this->clauses[] = $callProcedureClause; + + return $this; + } + /** * Creates the MATCH clause. * - * @param PathType|NodeType|(PathType|NodeType)[] $patterns A single pattern or a list of patterns + * @param CompletePattern|CompletePattern[] $patterns A single pattern or a non-empty list of patterns * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/match/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/match/ Corresponding documentation on Neo4j.com */ public function match($patterns): self { - $matchClause = new MatchClause(); - if (!is_array($patterns)) { $patterns = [$patterns]; } - foreach ($patterns as $pattern) { - $this->assertClass('pattern', [PathType::class, NodeType::class], $pattern); - - $matchClause->addPattern($pattern); - } + $matchClause = new MatchClause(); + $matchClause->addPattern(...$patterns); $this->clauses[] = $matchClause; @@ -247,34 +450,23 @@ public function match($patterns): self /** * Creates the RETURN clause. * - * @param AnyType[]|AnyType $expressions The expressions to return; if the array-key is - * non-numerical, it is used as the alias - * @param bool $distinct + * @param Alias|Alias[]|AnyType|AnyType[]|bool|bool[]|float|float[]|int|int[]|Pattern|Pattern[]|string|string[]|(Alias|AnyType|mixed[]|bool|float|int|Pattern|string)[] $expressions A single expression to return, or a non-empty list of expressions to return + * @param bool $distinct Whether to be a RETURN DISTINCT clause (optional, default: false) * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/return/#return-column-alias * - * @see https://neo4j.com/docs/cypher-manual/current/clauses/return/ + * @see https://neo4j.com/docs/cypher-manual/current/clauses/return/ Corresponding documentation on Neo4j.com */ public function returning($expressions, bool $distinct = false): self { - $returnClause = new ReturnClause(); - if (!is_array($expressions)) { $expressions = [$expressions]; } - foreach ($expressions as $maybeAlias => $expression) { - $this->assertClass('expression', AnyType::class, $expression); - - if ($expression instanceof Node) { - $expression = $expression->getName(); - } - - $alias = is_int($maybeAlias) ? "" : $maybeAlias; - $returnClause->addColumn($expression, $alias); - } + $expressions = $this->makeAliasArray($expressions, fn ($value) => $this->toAnyType($value)); + $returnClause = new ReturnClause(); + $returnClause->addColumn(...$expressions); $returnClause->setDistinct($distinct); $this->clauses[] = $returnClause; @@ -285,25 +477,20 @@ public function returning($expressions, bool $distinct = false): self /** * Creates the CREATE clause. * - * @param PathType|NodeType|(PathType|NodeType)[] $patterns A single pattern or a list of patterns + * @param CompletePattern|CompletePattern[] $patterns A single pattern or a non-empty list of patterns * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/create/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/create/ Corresponding documentation on Neo4j.com */ public function create($patterns): self { - $createClause = new CreateClause(); - if (!is_array($patterns)) { $patterns = [$patterns]; } - foreach ($patterns as $pattern) { - $this->assertClass('pattern', [PathType::class, NodeType::class], $pattern); - - $createClause->addPattern($pattern); - } + $createClause = new CreateClause(); + $createClause->addPattern(...$patterns); $this->clauses[] = $createClause; @@ -313,25 +500,22 @@ public function create($patterns): self /** * Creates the DELETE clause. * - * @param Variable|Variable[] $variables The nodes to delete + * @param Pattern|Pattern[]|StructuralType|StructuralType[]|(Pattern|StructuralType)[] $structures A single structure to delete, or a non-empty list of structures to delete + * @param bool $detach Whether to DETACH DELETE (optional, default: false) * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/delete/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/delete/ Corresponding documentation on Neo4j.com */ - public function delete($variables): self + public function delete($structures, bool $detach = false): self { - $deleteClause = new DeleteClause(); - - if (!is_array($variables)) { - $variables = [$variables]; + if (!is_array($structures)) { + $structures = [$structures]; } - foreach ($variables as $variable) { - $this->assertClass('variable', Variable::class, $variable); - - $deleteClause->addVariable($variable); - } + $deleteClause = new DeleteClause(); + $deleteClause->setDetach($detach); + $deleteClause->addStructure(...$structures); $this->clauses[] = $deleteClause; @@ -341,42 +525,27 @@ public function delete($variables): self /** * Creates the DETACH DELETE clause. * - * @param Variable|Variable[] $variables The variables to delete, including relationships + * @param Pattern|Pattern[]|StructuralType|StructuralType[]|(Pattern|StructuralType)[] $structures A single structure to delete, or a non-empty list of structures to delete * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/delete/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/delete/ Corresponding documentation on Neo4j.com */ - public function detachDelete($variables): self + public function detachDelete($structures): self { - $deleteClause = new DeleteClause(); - $deleteClause->setDetach(true); - - if (!is_array($variables)) { - $variables = [$variables]; - } - - foreach ($variables as $variable) { - $this->assertClass('variable', Variable::class, $variable); - - $deleteClause->addVariable($variable); - } - - $this->clauses[] = $deleteClause; - - return $this; + return $this->delete($structures, true); } /** - * Creates the LIMIT clause. + * Adds a LIMIT clause. * - * @param NumeralType $limit The amount to use as the limit + * @param int|IntegerType $limit The amount to use as the limit * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/limit/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/limit/ Corresponding documentation on Neo4j.com */ - public function limit(NumeralType $limit): self + public function limit($limit): self { $limitClause = new LimitClause(); $limitClause->setLimit($limit); @@ -389,15 +558,16 @@ public function limit(NumeralType $limit): self /** * Creates the SKIP clause. * - * @param NumeralType $limit The amount to use as the limit + * @param int|IntegerType $amount The amount to skip * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/skip/ + * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/skip/ Corresponding documentation on Neo4j.com */ - public function skip(NumeralType $limit): self + public function skip($amount): self { $skipClause = new SkipClause(); - $skipClause->setSkip($limit); + $skipClause->setSkip($amount); $this->clauses[] = $skipClause; @@ -407,27 +577,20 @@ public function skip(NumeralType $limit): self /** * Creates the MERGE clause. * - * @param PathType|NodeType $pattern The pattern to merge - * @param Clause|null $createClause The clause to execute when the pattern is created - * @param Clause|null $matchClause The clause to execute when the pattern is matched + * @param CompletePattern $pattern The pattern to merge + * @param null|SetClause $createClause The clause to execute when the pattern is created (optional) + * @param null|SetClause $matchClause The clause to execute when the pattern is matched (optional) * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/merge/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/merge/ Corresponding documentation on Neo4j.com */ - public function merge($pattern, Clause $createClause = null, Clause $matchClause = null): self + public function merge(CompletePattern $pattern, ?SetClause $createClause = null, ?SetClause $matchClause = null): self { - $this->assertClass('pattern', [PathType::class, NodeType::class], $pattern); $mergeClause = new MergeClause(); $mergeClause->setPattern($pattern); - - if (isset($createClause)) { - $mergeClause->setOnCreate($createClause); - } - - if (isset($matchClause)) { - $mergeClause->setOnMatch($matchClause); - } + $mergeClause->setOnCreate($createClause); + $mergeClause->setOnMatch($matchClause); $this->clauses[] = $mergeClause; @@ -437,25 +600,20 @@ public function merge($pattern, Clause $createClause = null, Clause $matchClause /** * Creates the OPTIONAL MATCH clause. * - * @param PathType|NodeType|(PathType|NodeType)[] $patterns A single pattern or a list of patterns + * @param CompletePattern|CompletePattern[] $patterns A single pattern to match, or a non-empty list of patterns to match * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/optional-match/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/optional-match/ Corresponding documentation on Neo4j.com */ public function optionalMatch($patterns): self { - $optionalMatchClause = new OptionalMatchClause(); - if (!is_array($patterns)) { $patterns = [$patterns]; } - foreach ($patterns as $pattern) { - $this->assertClass('pattern', [PathType::class, NodeType::class], $pattern); - - $optionalMatchClause->addPattern($pattern); - } + $optionalMatchClause = new OptionalMatchClause(); + $optionalMatchClause->addPattern(...$patterns); $this->clauses[] = $optionalMatchClause; @@ -465,27 +623,22 @@ public function optionalMatch($patterns): self /** * Creates the ORDER BY clause. * - * @param Property|Property[] $properties A single property or a list of properties - * @param bool $descending Whether or not to order in a descending order + * @param Property|Property[] $properties A single property to order by, or a non-empty list of properties to order by + * @param bool $descending Whether to order in descending order (optional, default: false) * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/order-by/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/order-by/ Corresponding documentation on Neo4j.com */ public function orderBy($properties, bool $descending = false): self { - $orderByClause = new OrderByClause(); - $orderByClause->setDescending($descending); - if (!is_array($properties)) { $properties = [$properties]; } - foreach ($properties as $property) { - $this->assertClass('property', Property::class, $property); - - $orderByClause->addProperty($property); - } + $orderByClause = new OrderByClause(); + $orderByClause->setDescending($descending); + $orderByClause->addProperty(...$properties); $this->clauses[] = $orderByClause; @@ -495,25 +648,20 @@ public function orderBy($properties, bool $descending = false): self /** * Creates the REMOVE clause. * - * @param Property|Label|Property[]|Label[] $expressions The expressions to remove (should either be a Node or a Property) + * @param Label|Label[]|Property|Property[]|(Label|Property)[] $expressions A single expression to remove, or a non-empty list of expressions to remove * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/remove/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/remove/ Corresponding documentation on Neo4j.com */ public function remove($expressions): self { - $removeClause = new RemoveClause(); - if (!is_array($expressions)) { $expressions = [$expressions]; } - foreach ($expressions as $expression) { - $this->assertClass('expression', [Property::class, Label::class], $expression); - - $removeClause->addExpression($expression); - } + $removeClause = new RemoveClause(); + $removeClause->addExpression(...$expressions); $this->clauses[] = $removeClause; @@ -523,25 +671,20 @@ public function remove($expressions): self /** * Create the SET clause. * - * @param Assignment|Label|(Assignment|Label)[] $expressions A single expression or a list of expressions + * @param Label|Label[]|PropertyReplacement|PropertyReplacement[]|(Label|PropertyReplacement)[] $expressions A single expression to set, or a non-empty list of expressions to set * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/set/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/set/ Corresponding documentation on Neo4j.com */ public function set($expressions): self { - $setClause = new SetClause(); - if (!is_array($expressions)) { $expressions = [$expressions]; } - foreach ($expressions as $expression) { - $this->assertClass('expression', [Assignment::class, Label::class], $expression); - - $setClause->addAssignment($expression); - } + $setClause = new SetClause(); + $setClause->add(...$expressions); $this->clauses[] = $setClause; @@ -551,16 +694,25 @@ public function set($expressions): self /** * Creates the WHERE clause. * - * @param AnyType $expression The expression to match + * @param bool|bool[]|BooleanType|BooleanType[]|(bool|BooleanType)[] $expressions A boolean expression to evaluate, or a non-empty list of boolean expression to evaluate + * @param string $operator The operator with which to unify the given expressions, should be either WhereClause::OR, + * WhereClause::AND or WhereClause::XOR (optional, default: 'and') * * @return $this - * @see https://neo4j.com/docs/cypher-manual/current/clauses/where/ * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/where/ Corresponding documentation on Neo4j.com */ - public function where(AnyType $expression): self + public function where($expressions, string $operator = WhereClause::AND): self { + if (!is_array($expressions)) { + $expressions = [$expressions]; + } + $whereClause = new WhereClause(); - $whereClause->setExpression($expression); + + foreach ($expressions as $expression) { + $whereClause->addExpression($expression, $operator); + } $this->clauses[] = $whereClause; @@ -570,31 +722,22 @@ public function where(AnyType $expression): self /** * Creates the WITH clause. * - * @param AnyType[]|AnyType $expressions The entries to add; if the array-key is non-numerical, it is used as the alias + * @param Alias|Alias[]|AnyType|AnyType[]|bool|bool[]|float|float[]|int|int[]|mixed[][]|Pattern|Pattern[]|string|string[]|(Alias|AnyType|bool|float|int|mixed[]|Pattern|string)[] $expressions An entry to add, or a non-empty list of entries to add; if the array-key is non-numerical, it is used as the alias * + * @return $this * - * @return Query - * @see https://neo4j.com/docs/cypher-manual/current/clauses/with/ - * + * @see https://neo4j.com/docs/cypher-manual/current/clauses/with/ Corresponding documentation on Neo4j.com */ public function with($expressions): self { - $withClause = new WithClause(); - if (!is_array($expressions)) { $expressions = [$expressions]; } - foreach ($expressions as $maybeAlias => $expression) { - $this->assertClass('expression', AnyType::class, $expression); - - if ($expression instanceof Node) { - $expression = $expression->getName(); - } + $expressions = $this->makeAliasArray($expressions, fn ($value) => $this->toAnyType($value)); - $alias = is_int($maybeAlias) ? "" : $maybeAlias; - $withClause->addEntry($expression, $alias); - } + $withClause = new WithClause(); + $withClause->addEntry(...$expressions); $this->clauses[] = $withClause; @@ -602,99 +745,59 @@ public function with($expressions): self } /** - * Creates a "RAW" query. + * Creates a raw clause. * - * @param string $clause The name of the clause; for instance "MATCH" - * @param string $subject The subject/body of the clause - * @return Query - */ - public function raw(string $clause, string $subject): self - { - $this->clauses[] = new RawClause($clause, $subject); - - return $this; - } - - /** - * Creates the CALL procedure clause. - * - * @param string $procedure The procedure to call - * @param AnyType[] $arguments The arguments passed to the procedure - * @param Variable[] $yields The results field that will be returned + * @note This should only be used for features that are not implemented by the DSL. * - * @return Query - * @see https://neo4j.com/docs/cypher-manual/current/clauses/call/ + * @param string $clause The name of the clause, for instance "MATCH" + * @param string $subject The subject (body) of the clause * + * @return $this */ - public function callProcedure(string $procedure, array $arguments = [], array $yields = []): self + public function raw(string $clause, string $subject): self { - $callProcedureClause = new CallProcedureClause(); - $callProcedureClause->setProcedure($procedure); - $callProcedureClause->withArguments($arguments); - $callProcedureClause->yields($yields); - - $this->clauses[] = $callProcedureClause; + $this->clauses[] = new RawClause($clause, $subject); return $this; } - /** * Combines the result of this query with another one via a UNION clause. * - * @param callable(Query):void|Query $queryOrCallable The callable decorating a fresh query instance or the query instance to be attached after the union clause. - * @param bool $all Whether the union should include all results or remove the duplicates instead. + * @param callable|Query $queryOrCallable The callable decorating a fresh query instance or the query instance to be attached after the union clause + * @param bool $all Whether the union should include all results or remove the duplicates instead (optional, default: false) * - * @return Query + * @return $this * - * @see https://neo4j.com/docs/cypher-manual/current/clauses/union/ + * @see https://neo4j.com/docs/cypher-manual/current/clauses/union/ Corresponding documentation on Neo4j.com */ public function union($queryOrCallable, bool $all = false): self { - $this->clauses[] = new UnionClause($all); - if (is_callable($queryOrCallable)) { - $query = Query::new(); + $query = self::new(); $queryOrCallable($query); } else { $query = $queryOrCallable; } - foreach ($query->getClauses() as $clause) { - $this->clauses[] = $clause; - } + $unionClause = new UnionClause(); + $unionClause->setAll($all); - return $this; - } + $this->clauses[] = $unionClause; - /** - * Creates a CALL sub query clause. - * - * @param callable(Query)|Query $decoratorOrClause The callable decorating the pattern, or the actual CALL clause. - * - * @return Query - * - * @see https://neo4j.com/docs/cypher-manual/current/clauses/call-subquery/ - */ - public function call($decoratorOrClause): self - { - if (is_callable($decoratorOrClause)) { - $subQuery = self::new(); - $decoratorOrClause($subQuery); - } else { - $subQuery = $decoratorOrClause; + foreach ($query->getClauses() as $clause) { + $this->clauses[] = $clause; } - $this->clauses[] = new CallClause($subQuery); - return $this; } /** * Add a clause to the query. * - * @param Clause $clause - * @return Query + * @param Clause $clause The clause to add to the query + * + * @return $this */ public function addClause(Clause $clause): self { @@ -714,9 +817,25 @@ public function getClauses(): array } /** - * Converts the object into a (partial) query. + * Builds the query. * - * @return string + * @return string The fully constructed query + */ + public function build(): string + { + $builtClauses = array_map( + static fn (Clause $clause): string => $clause->toQuery(), + $this->clauses + ); + + // Filter any empty clauses to prevent double spaces + return implode(" ", array_filter($builtClauses, static fn ($clause) => !empty($clause))); + } + + /** + * Alias of $this->build(). + * + * @inheritDoc */ public function toQuery(): string { @@ -724,20 +843,30 @@ public function toQuery(): string } /** - * Builds the query. + * Changes an associative array into an array of aliases. * - * @return string The fully constructed query + * @param mixed[] $values The array to change into an array of aliases + * @param callable $castFunc Function to use to cast the elements of $array + * + * @return mixed[] A sequential array, possibly consisting of aliases */ - public function build(): string + private static function makeAliasArray(array $values, callable $castFunc): array { - $builtClauses = array_map( - fn (Clause $clause): string => $clause->toQuery(), - $this->clauses - ); + $res = []; - return implode( - " ", - array_filter($builtClauses, fn ($clause) => !empty($clause)) - ); + foreach ($values as $key => $value) { + if (is_string($key)) { + /** + * @var AnyType $value + */ + $value = $castFunc($value); + $res[] = $value->alias($key); + } else { + // If key is numeric, keep value unchanged. + $res[]= $value; + } + } + + return $res; } } diff --git a/src/QueryConvertable.php b/src/QueryConvertable.php deleted file mode 100644 index bb05c767..00000000 --- a/src/QueryConvertable.php +++ /dev/null @@ -1,37 +0,0 @@ -expression = $expression; - } - - /** - * Returns the RAW expression. - * - * @return string - */ - public function getExpression(): string - { - return $this->expression; - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return $this->expression; - } -} diff --git a/src/Regex.php b/src/Regex.php deleted file mode 100644 index 21c38641..00000000 --- a/src/Regex.php +++ /dev/null @@ -1,52 +0,0 @@ -original = $original; + $this->variable = $variable; + } + + /** + * Gets the original item of the alias. + */ + public function getOriginal(): AnyType + { + return $this->original; + } + + /** + * Gets the variable from the alias. + */ + public function getVariable(): Variable + { + return $this->variable; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + return \sprintf("%s AS %s", $this->original->toQuery(), $this->variable->toQuery()); + } +} diff --git a/src/Syntax/PropertyReplacement.php b/src/Syntax/PropertyReplacement.php new file mode 100644 index 00000000..7d006a29 --- /dev/null +++ b/src/Syntax/PropertyReplacement.php @@ -0,0 +1,110 @@ +property = $property; + $this->value = $value; + } + + /** + * Whether to use the property mutation instead of the property replacement + * operator. + * + * @return $this + */ + public function setMutate(bool $mutate = true): self + { + $this->mutate = $mutate; + + return $this; + } + + /** + * Returns whether the assignment uses property mutation instead of replacement. + */ + public function mutates(): bool + { + return $this->mutate; + } + + /** + * Returns the name of the property to which we assign a (new) value. + * + * @return Property|Variable + */ + public function getProperty() + { + return $this->property; + } + + /** + * Returns value to assign to the property. + */ + public function getValue(): AnyType + { + return $this->value; + } + + /** + * @inheritDoc + */ + public function toQuery(): string + { + return sprintf( + "%s %s %s", + $this->property->toQuery(), + $this->mutate ? "+=" : "=", + $this->value->toQuery() + ); + } +} diff --git a/src/Traits/AliasableTrait.php b/src/Traits/AliasableTrait.php deleted file mode 100644 index 3e6d8e1f..00000000 --- a/src/Traits/AliasableTrait.php +++ /dev/null @@ -1,46 +0,0 @@ -getVariable(); + } + + /** + * Casts the given value to a Variable. + * + * @param Pattern|string|Variable $variable + * + * @see CastTrait::toName() for a function that does not accept Pattern + */ + private static function toVariable($variable): Variable + { + self::assertClass('variable', [Variable::class, Pattern::class, 'string'], $variable); + + if ($variable instanceof Variable) { + return $variable; + } + + if ($variable instanceof Pattern) { + return $variable->getVariable(); + } + + return new Variable($variable); + } + + /** + * Casts the given value to a name (as a variable). + * + * @param string|Variable $name + * + * @see CastTrait::toVariable() for a function that accepts Pattern + */ + private static function toName($name): Variable + { + self::assertClass('name', [Variable::class, 'string'], $name); + + return $name instanceof Variable ? $name : new Variable($name); + } + + /** + * Casts the given value to an AnyType. + * + * @param AnyType|bool|float|int|mixed[]|Pattern|string $value + */ + private static function toAnyType($value): AnyType + { + self::assertClass('value', [AnyType::class, Pattern::class, 'int', 'float', 'string', 'bool', 'array'], $value); + + if ($value instanceof Pattern) { + return $value->getVariable(); + } + + if ($value instanceof AnyType) { + return $value; + } + + // @phpstan-ignore-next-line + return Literal::literal($value); + } +} diff --git a/src/Traits/ComparableTypeTrait.php b/src/Traits/ComparableTypeTrait.php deleted file mode 100644 index da63c287..00000000 --- a/src/Traits/ComparableTypeTrait.php +++ /dev/null @@ -1,84 +0,0 @@ -= 65535) { - // Remark: Actual limit depends on Neo4j version, but we just take the lower bound. - throw new InvalidArgumentException('A name cannot be longer than 65534 characters'); - } - } - - /** - * @param string $varName * @param string[] $classNames - * @param mixed $userInput - * @return TypeError + * @param mixed $userInput */ private static function typeError(string $varName, array $classNames, $userInput): TypeError { - return new TypeError(self::getTypeErrorText($varName, $classNames, $userInput)); - } - - /** - * @param string $varName - * @param string[] $classNames - * @param mixed $userInput - * @return string - */ - private static function getTypeErrorText(string $varName, array $classNames, $userInput): string - { - return sprintf( - '$%s should be a %s object, %s given.', + $errorText = sprintf( + '$%s should be a %s, %s given.', $varName, implode(' or ', $classNames), self::getDebugType($userInput) ); + + return new TypeError($errorText); } /** * Returns the name of the scalar type of the value if it is one. * * @param mixed $value - * @return string|null */ private static function detectScalar($value): ?string { @@ -181,8 +140,6 @@ private static function detectScalar($value): ?string * Returns the name of the class of the value if it is one. * * @param mixed $value - * - * @return string|null */ private static function detectClass($value): ?string { @@ -193,11 +150,12 @@ private static function detectClass($value): ?string if (is_object($value)) { $class = get_class($value); - if (false === strpos($class, '@')) { + if (strpos($class, '@') === false) { return $class; } - return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous'; + // @phpstan-ignore-next-line + return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class') . '@anonymous'; } return null; @@ -207,22 +165,22 @@ private static function detectClass($value): ?string * Returns the name of the resource of the value if it is one. * * @param mixed $value - * - * @return string|null */ private static function detectResource($value): ?string { if (is_resource($value)) { $type = @get_resource_type($value); - if (null === $type) { + + // @phpstan-ignore-next-line + if ($type === null) { return 'unknown'; } - if ('Unknown' === $type) { + if ($type === 'Unknown') { $type = 'closed'; } - return "resource ($type)"; + return "resource ({$type})"; } return null; diff --git a/src/Traits/EscapeTrait.php b/src/Traits/EscapeTrait.php index 5c3f1a9b..f23bda80 100644 --- a/src/Traits/EscapeTrait.php +++ b/src/Traits/EscapeTrait.php @@ -1,63 +1,56 @@ - 65534) { + // Remark: Some versions of Neo4j support names up to 65535 characters, but we just take the lower bound + throw new InvalidArgumentException("A name cannot be longer than 65534 (2^16 - 2) characters"); + } + + if (\preg_match('/^\p{L}[\p{L}\d_]*$/u', $name)) { + // The name is already valid and does not need to be escaped + return $name; + } - /** - * Escapes the given $name to be used directly in a CYPHER query. - * Note: according to https://github.com/neo4j/neo4j/issues/12901 backslashes might give problems in some Neo4j versions. - */ - public static function escapeRaw($name) - { // Escape backticks that are included in $name by doubling them. - $name = str_replace('`', '``', $name); + $name = \str_replace('`', '``', $name); - return sprintf("`%s`", $name); + return \sprintf("`%s`", $name); } } diff --git a/src/Traits/HasNameTrait.php b/src/Traits/HasNameTrait.php deleted file mode 100644 index d367ee43..00000000 --- a/src/Traits/HasNameTrait.php +++ /dev/null @@ -1,80 +0,0 @@ -name)) { - $this->configureName(null, 'var'); - } - - return $this->name; - } - - /** - * Generates a unique random identifier. - * - * @note It is not entirely guaranteed that this function gives a truly unique identifier. However, because the - * number of possible IDs is so huge, it should not be a problem. - * - * @param string $prefix The prefix to put before the name. Must start with a letter to adhere to cypher namings. - * - * @param int|null $length The length of the generated name in bytes. - * - * @return string - */ - private function generateName(string $prefix = 'var', int $length = null): string - { - $length ??= self::$automaticVariableLength; - - $random = ''; - for ($i = 0; $i < $length; ++$i) { - $random .= dechex(mt_rand(0, 15)); - } - - return $prefix . $random; - } - - private function configureName(?string $givenName, string $prefix, int $length = null): void - { - $name = $givenName ?? $this->generateName($prefix, $length); - - self::assertValidName($name); - - $this->name = $name; - } -} diff --git a/src/Traits/HasPropertiesTrait.php b/src/Traits/HasPropertiesTrait.php deleted file mode 100644 index 5d640715..00000000 --- a/src/Traits/HasPropertiesTrait.php +++ /dev/null @@ -1,85 +0,0 @@ -initialiseProperties(); - - $this->properties->addProperty($key, $value); - - return $this; - } - - /** - * Add the given properties to the properties of this node. - * - * @param PropertyMap|array $properties - * - * @return static - */ - public function withProperties($properties): self - { - self::assertClass('properties', [PropertyMap::class, 'array'], $properties); - - $this->initialiseProperties(); - - $properties = is_array($properties) ? new PropertyMap($properties) : $properties; - - $this->properties->mergeWith($properties); - - return $this; - } - - public function getProperties(): ?PropertyMap - { - return $this->properties; - } - - /** - * @return void - */ - private function initialiseProperties(): void - { - if ($this->properties === null) { - $this->properties = new PropertyMap(); - } - } -} diff --git a/src/Traits/HasVariableTrait.php b/src/Traits/HasVariableTrait.php deleted file mode 100644 index b267fbd2..00000000 --- a/src/Traits/HasVariableTrait.php +++ /dev/null @@ -1,99 +0,0 @@ -variable = $variable; - - return $this; - } - - /** - * Alias of Node::named(). - * - * @param $variable - * @return $this - * @see Node::named() - */ - public function setName($variable): self - { - return $this->named($variable); - } - - /** - * Returns the variable of the path. - * - * @return Variable|null - */ - public function getVariable(): ?Variable - { - return $this->variable; - } - - /** - * Returns the name of this Structural Type. This function automatically generates a name if the node does not have a - * name yet. - * - * @return Variable The name of this node - */ - public function getName(): Variable - { - if (!isset($this->variable)) { - $this->named(new Variable()); - } - - return $this->variable; - } -} diff --git a/src/Traits/ListTypeTrait.php b/src/Traits/ListTypeTrait.php deleted file mode 100644 index 068d02d9..00000000 --- a/src/Traits/ListTypeTrait.php +++ /dev/null @@ -1,43 +0,0 @@ -property("bar") would yield "foo.bar". - * - * @param string $property - * @return Property - */ - public function property(string $property): Property - { - return new Property($this, $property); - } -} diff --git a/src/Traits/NameGenerationTrait.php b/src/Traits/NameGenerationTrait.php new file mode 100644 index 00000000..5e1ca86f --- /dev/null +++ b/src/Traits/NameGenerationTrait.php @@ -0,0 +1,43 @@ +relationship($relationship, $nodeOrPath); - } - - /** - * @inheritDoc - */ - public function relationshipTo(HasRelationshipsType $nodeOrPath, ?string $type = null, $properties = null, $name = null): Path - { - return (new Path($this))->relationshipTo($nodeOrPath, $type, $properties, $name); - } - - /** - * @inheritDoc - */ - public function relationshipFrom(HasRelationshipsType $nodeOrPath, ?string $type = null, $properties = null, $name = null): Path - { - return (new Path($this))->relationshipFrom($nodeOrPath, $type, $properties, $name); - } - - /** - * @inheritDoc - */ - public function relationshipUni(HasRelationshipsType $nodeOrPath, ?string $type = null, $properties = null, $name = null): Path - { - return (new Path($this))->relationshipUni($nodeOrPath, $type, $properties, $name); - } -} diff --git a/src/Traits/NumeralTypeTrait.php b/src/Traits/NumeralTypeTrait.php deleted file mode 100644 index e9640a8d..00000000 --- a/src/Traits/NumeralTypeTrait.php +++ /dev/null @@ -1,123 +0,0 @@ -variable = self::toName($variable); + + return $this; + } + + /** + * Returns the variable of this object. This function generates a variable if none has been set. + */ + public function getVariable(): Variable + { + if (!isset($this->variable)) { + $this->variable = new Variable(); + } + + return $this->variable; + } +} diff --git a/src/Traits/PatternTraits/PropertyPatternTrait.php b/src/Traits/PatternTraits/PropertyPatternTrait.php new file mode 100644 index 00000000..e3ad55b3 --- /dev/null +++ b/src/Traits/PatternTraits/PropertyPatternTrait.php @@ -0,0 +1,111 @@ +getVariable(), $property); + } + + /** + * @inheritDoc + */ + public function withProperties($properties): self + { + $this->properties = self::toMapType($properties); + + return $this; + } + + /** + * @inheritDoc + */ + public function addProperty(string $key, $property): self + { + $this->makeMap()->add($key, $property); + + return $this; + } + + /** + * @inheritDoc + */ + public function addProperties($properties): self + { + self::assertClass('properties', [Map::class, 'array'], $properties); + + $map = $this->makeMap(); + + if (is_array($properties)) { + $res = []; + + foreach ($properties as $key => $property) { + $res[$key] = self::toAnyType($property); + } + + // Cast the array to a Map + $properties = new Map($res); + } + + $map->mergeWith($properties); + + return $this; + } + + /** + * @inheritDoc + */ + public function getProperties(): ?MapType + { + return $this->properties; + } + + /** + * Initialises the properties in this pattern. + */ + private function makeMap(): Map + { + if (!isset($this->properties)) { + $this->properties = new Map(); + } elseif (!$this->properties instanceof Map) { + // Adding to a map is not natively supported by the MapType, but it is supported by Map. Syntactically, it + // is not possible to add new items to, for instance, a Variable, even though it implements MapType. It is + // however still useful to be able to add items to objects where a Map is used (that is, an object of + // MapType with the {} syntax). + throw new TypeError('$this->properties must be of type Map to support "addProperty"'); + } + + return $this->properties; + } +} diff --git a/src/Traits/PointTrait.php b/src/Traits/PointTrait.php deleted file mode 100644 index 305d37ec..00000000 --- a/src/Traits/PointTrait.php +++ /dev/null @@ -1,30 +0,0 @@ -property("bar") would yield "foo.bar". - * - * @param string $property - * @return Property - */ - public function property(string $property): Property; } diff --git a/src/Types/Methods/PropertyMethod.php b/src/Types/Methods/PropertyMethod.php new file mode 100644 index 00000000..407aa0c6 --- /dev/null +++ b/src/Types/Methods/PropertyMethod.php @@ -0,0 +1,26 @@ +=, < and >. - */ -interface ComparableType extends PropertyType -{ - /** - * Perform a greater than comparison against the given expression. - * - * @param NumeralType $right - * @param bool $insertParentheses - * @return GreaterThan - */ - public function gt(ComparableType $right, bool $insertParentheses = true): GreaterThan; - - /** - * Perform a greater than or equal comparison against the given expression. - * - * @param NumeralType $right - * @param bool $insertParentheses - * @return GreaterThanOrEqual - */ - public function gte(ComparableType $right, bool $insertParentheses = true): GreaterThanOrEqual; - - /** - * Perform a less than comparison against the given expression. - * - * @param NumeralType $right - * @param bool $insertParentheses - * @return LessThan - */ - public function lt(ComparableType $right, bool $insertParentheses = true): LessThan; - - /** - * Perform a less than or equal comparison against the given expression. - * - * @param NumeralType $right - * @param bool $insertParentheses - * @return LessThanOrEqual - */ - public function lte(ComparableType $right, bool $insertParentheses = true): LessThanOrEqual; -} diff --git a/src/Types/PropertyTypes/DateTimeType.php b/src/Types/PropertyTypes/DateTimeType.php index e6883014..69557bc1 100644 --- a/src/Types/PropertyTypes/DateTimeType.php +++ b/src/Types/PropertyTypes/DateTimeType.php @@ -1,29 +1,21 @@ -configureName($variable, 'var'); - } - - /** - * Adds the given labels to this variable. - * - * @param string[] $labels - * @return Label - * @deprecated Use Variable::labeled() instead - */ - public function withLabels(array $labels): Label - { - return $this->labeled($labels); - } - - /** - * Adds the given labels to this variable. - * - * @param array $labels - * @return Label - */ - public function labeled(array $labels): Label - { - return new Label($this, $labels); - } - - /** - * Assign a value to this variable. - * - * @param AnyType $value The value to assign - * @return Assignment - */ - public function assign(AnyType $value): Assignment - { - return new Assignment($this, $value); - } - - public function getVariable(): string - { - return $this->getName(); - } - - /** - * @inheritDoc - */ - public function toQuery(): string - { - return self::escape($this->getName()); - } - - private function toNode(): NodeType - { - return Query::node()->named($this); - } - - /** - * @inheritdoc - */ - public function relationship(RelationshipType $relationship, HasRelationshipsType $nodeOrPath): Path - { - return $this->toNode()->relationship($relationship, $nodeOrPath); - } - - /** - * @inheritdoc - */ - public function relationshipTo(HasRelationshipsType $nodeOrPath, ?string $type = null, $properties = null, $name = null): Path - { - return $this->toNode()->relationshipTo($nodeOrPath, $type, $properties, $name); - } - - /** - * @inheritdoc - */ - public function relationshipFrom(HasRelationshipsType $nodeOrPath, ?string $type = null, $properties = null, $name = null): Path - { - return $this->toNode()->relationshipFrom($nodeOrPath, $type, $properties, $name); - } - - /** - * @inheritdoc - */ - public function relationshipUni(HasRelationshipsType $nodeOrPath, ?string $type = null, $properties = null, $name = null): Path - { - return $this->toNode()->relationshipUni($nodeOrPath, $type, $properties, $name); - } - - /** - * @inheritdoc - */ - public function withProperty(string $key, PropertyType $value): HasPropertiesType - { - return $this->toNode()->withProperty($key, $value); - } - - /** - * @inheritdoc - */ - public function withProperties($properties): HasPropertiesType - { - return $this->toNode()->withProperties($properties); - } -} diff --git a/src/XorOperator.php b/src/XorOperator.php deleted file mode 100644 index d502de7f..00000000 --- a/src/XorOperator.php +++ /dev/null @@ -1,51 +0,0 @@ -(b)) + * - Relationship::DIR_LEFT (for a relation of (a)<--(b)) + * - Relationship::DIR_UNI (for a relation of (a)--(b)) + * + * @see Query::relationship() + */ +function relationship(array $direction = Relationship::DIR_UNI): Relationship +{ + return Query::relationship($direction); +} + +/** + * Creates a unidirectional relationship. + * + * @see Query::relationshipUni() + */ +function relationshipUni(): Relationship +{ + return Query::relationshipUni(); +} + +/** + * Creates a right relationship. + * + * @see Query::relationshipTo() + */ +function relationshipTo(): Relationship +{ + return Query::relationshipTo(); +} + +/** + * Creates a left relationship. + * + * @see Query::relationshipFrom() + */ +function relationshipFrom(): Relationship +{ + return Query::relationshipFrom(); +} + +/** + * Creates a new variable with the given name, or generates a new variable with a random unique name. + * + * @param null|string $variable the name of the variable, or null to automatically generate a name + * + * @see Query::variable() + */ +function variable(?string $variable = null): Variable +{ + return Query::variable($variable); +} + +/** + * Creates a new literal from the given value. This function automatically constructs the appropriate + * class based on the type of the value given. + * + * This function cannot be used directly to construct Point or Date types. Instead, you can create a Point literal + * by using any of the following functions: + * + * - literal()::point2d(...) - For a 2D cartesian point + * - literal()::point3d(...) - For a 3D cartesian point + * - literal()::point2dWGS84(...) - For a 2D WGS 84 point + * - literal()::point3dWGS84(...) - For a 3D WGS 84 point + * + * And a Date literal by using one of the following functions: + * + * - literal()::date(...) - For the current date + * - literal()::dateYMD(...) - For a date from the given year, month and day + * - literal()::dateYWD(...) - For a date from the given year, week and day + * - literal()::dateString(...) - For a date from the given date string + * - literal()::dateTime(...) - For the current datetime + * - literal()::dateTimeYMD(...) - For a datetime from the given parameters (see function definition) + * - literal()::dateTimeYWD(...) - For a datetime from the given parameters (see function definition) + * - literal()::dateTimeYQD(...) - For a datetime from the given parameters (see function definition) + * - literal()::dateTimeYD(...) - For a datetime from the given parameters (see function definition) + * - literal()::dateTimeString(...) - For a datetime from the given datetime string + * - literal()::localDateTime(...) - For the current local datetime + * - literal()::localDateTimeYMD(...) - For a local datetime from the given parameters (see function definition) + * - literal()::localDateTimeYWD(...) - For a local datetime from the given parameters (see function definition) + * - literal()::localDateTimeYQD(...) - For a local datetime from the given parameters (see function definition) + * - literal()::localDateTimeYD(...) - For a local datetime from the given parameters (see function definition) + * - literal()::localDateTimeString(...) - For a local datetime from the given datetime string + * - literal()::localTimeCurrent(...) - For the current LocalTime + * - literal()::localTime(...) - For a local time from the given parameters (see function definition) + * - literal()::localTimeString(...) - For a local time from the given time string + * - literal()::time(...) - For the curren time + * - literal()::timeHMS(...) - For a time from the given hour, minute and second + * - literal()::timeString(...) - For a time from the given time string + * + * When no arguments are given to this function, the function will return a reference to the Literal class. + * + * You can directly call the constructors of the most basic types: + * + * - boolean() - For a boolean + * - string() - For a string + * - integer() - For an integer + * - float() - For a float + * - list_() - For a list + * - map() - For a map + * + * @param null|bool|float|int|mixed[]|string $literal The literal to construct + * + * @return Boolean|class-string|Float_|Integer|List_|Map|String_ + * + * @see Query::literal() + */ +function literal($literal = null) +{ + return Query::literal($literal); +} + +/** + * Creates a new boolean. + * + * @see Query::boolean() + */ +function boolean(bool $value): Boolean +{ + return Query::boolean($value); +} + +/** + * Creates a new string. + * + * @see Query::string() + */ +function string(string $value): String_ +{ + return Query::string($value); +} + +/** + * Creates a new integer. + * + * @see Query::integer() + */ +function integer(int $value): Integer +{ + return Query::integer($value); +} + +/** + * Creates a new float. + * + * @see Query::float() + */ +function float(float $value): Float_ +{ + return Query::float($value); +} + +/** + * Creates a new list literal. + * + * @param mixed[] $value + * + * @see Query::list() + */ +function list_(array $value): List_ +{ + return Query::list($value); +} + +/** + * Creates a new map literal. + * + * @param mixed[] $value + * + * @see Query::map() + */ +function map(array $value): Map +{ + return Query::map($value); +} + +/** + * Creates a new parameter. + * + * @param null|string $parameter The name of the parameter, or null to automatically generate a name + * + * @see Query::parameter() + */ +function parameter(?string $parameter = null): Parameter +{ + return Query::parameter($parameter); +} + +/** + * Returns the class string of the "Procedure" class. + * + * @return class-string + * + * @see Query::procedure() + */ +function function_(): string +{ + return Query::procedure(); +} + +/** + * Returns the class string of the "Procedure" class. + * + * @return class-string + * + * @see Query::procedure() + */ +function procedure(): string +{ + return Query::procedure(); +} + +/** + * Creates a new raw expression. + * + * @note This should be used only for features that are not implemented by the DSL. + * + * @param string $expression The raw expression + * + * @see Query::rawExpression() + */ +function raw(string $expression): RawExpression +{ + return Query::rawExpression($expression); +} diff --git a/tests/Unit/AdditionTest.php b/tests/Unit/AdditionTest.php deleted file mode 100644 index 54e421f9..00000000 --- a/tests/Unit/AdditionTest.php +++ /dev/null @@ -1,73 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"); - $right = $this->getQueryConvertableMock(NumeralType::class, "15"); - $addition = new Addition($left, $right); - - $this->assertSame("(10 + 15)", $addition->toQuery()); - - $this->assertEquals($left, $addition->getLeft()); - $this->assertEquals($right, $addition->getRight()); - - $newAddition = new Addition($addition, $addition); - - $this->assertSame("((10 + 15) + (10 + 15))", $newAddition->toQuery()); - - $this->assertTrue($newAddition->insertsParentheses()); - $this->assertEquals($addition, $newAddition->getLeft()); - $this->assertEquals($addition, $newAddition->getRight()); - - $newAddition = new Addition($addition, $addition, false); - - $this->assertSame("(10 + 15) + (10 + 15)", $newAddition->toQuery()); - - $this->assertFalse($newAddition->insertsParentheses()); - $this->assertEquals($addition, $newAddition->getLeft()); - $this->assertEquals($addition, $newAddition->getRight()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $addition = new Addition($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $addition->toQuery(); - } -} diff --git a/tests/Unit/AliasTest.php b/tests/Unit/AliasTest.php deleted file mode 100644 index 6801642a..00000000 --- a/tests/Unit/AliasTest.php +++ /dev/null @@ -1,42 +0,0 @@ -alias = new Alias( - $this->getQueryConvertableMock(Variable::class, "a"), - $this->getQueryConvertableMock(Variable::class, "b") - ); - } - - public function testToQuery(): void - { - $this->assertSame("a AS b", $this->alias->toQuery()); - } - - public function testGetOriginal(): void - { - $this->assertEquals($this->getQueryConvertableMock(Variable::class, "a"), $this->alias->getOriginal()); - } - - public function testGetVariable(): void - { - $this->assertEquals($this->getQueryConvertableMock(Variable::class, "b"), $this->alias->getVariable()); - } -} diff --git a/tests/Unit/AndOperatorTest.php b/tests/Unit/AndOperatorTest.php deleted file mode 100644 index 8b46d039..00000000 --- a/tests/Unit/AndOperatorTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(BooleanType::class, "true"), $this->getQueryConvertableMock(BooleanType::class, "false")); - - $this->assertSame("(true AND false)", $and->toQuery()); - - $and = new AndOperator($and, $and); - - $this->assertSame("((true AND false) AND (true AND false))", $and->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $and = new AndOperator($this->getQueryConvertableMock(BooleanType::class, "true"), $this->getQueryConvertableMock(BooleanType::class, "false"), false); - - $this->assertSame("true AND false", $and->toQuery()); - - $and = new AndOperator($and, $and); - - $this->assertSame("(true AND false AND true AND false)", $and->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $and = new AndOperator($this->getQueryConvertableMock(AnyType::class, "true"), $this->getQueryConvertableMock(AnyType::class, "false")); - - $and->toQuery(); - } -} diff --git a/tests/Unit/AssignmentTest.php b/tests/Unit/AssignmentTest.php deleted file mode 100644 index 32087d38..00000000 --- a/tests/Unit/AssignmentTest.php +++ /dev/null @@ -1,71 +0,0 @@ -getQueryConvertableMock(Property::class, "foo.bar"), $this->getQueryConvertableMock(AnyType::class, "true")); - - $this->assertSame("foo.bar = true", $assignment->toQuery()); - - $assignment->setMutate(); - - $this->assertSame("foo.bar += true", $assignment->toQuery()); - } - - public function testLeftDoesNotAcceptAnyType() - { - $this->expectException(TypeError::class); - - $assignment = new Assignment($this->getQueryConvertableMock(AnyType::class, "foo.bar"), $this->getQueryConvertableMock(AnyType::class, "true")); - - $assignment->toQuery(); - } - - public function testLeftAcceptsProperty() - { - $assignment = new Assignment($this->getQueryConvertableMock(Property::class, "foo.bar"), $this->getQueryConvertableMock(AnyType::class, "true")); - - $this->assertSame("foo.bar = true", $assignment->toQuery()); - } - - public function testLeftAcceptsVariable() - { - $assignment = new Assignment($this->getQueryConvertableMock(Variable::class, "foo.bar"), $this->getQueryConvertableMock(AnyType::class, "true")); - - $this->assertSame("foo.bar = true", $assignment->toQuery()); - } -} diff --git a/tests/Unit/Clauses/CallClauseTest.php b/tests/Unit/Clauses/CallClauseTest.php deleted file mode 100644 index 8f2db79e..00000000 --- a/tests/Unit/Clauses/CallClauseTest.php +++ /dev/null @@ -1,30 +0,0 @@ -assertEquals('', $clause->toQuery()); - $this->assertEquals(Query::new(), $clause->getSubQuery()); - } - - public function testCallClauseFilled(): void - { - $query = Query::new()->match(Query::node('X')->named('x'))->returning(Query::rawExpression('*')); - - $clause = new CallClause($query); - - $this->assertEquals('CALL { MATCH (x:X) RETURN * }', $clause->toQuery()); - $this->assertEquals($query, $clause->getSubQuery()); - } -} diff --git a/tests/Unit/Clauses/CallProcedureClauseTest.php b/tests/Unit/Clauses/CallProcedureClauseTest.php deleted file mode 100644 index 561b0463..00000000 --- a/tests/Unit/Clauses/CallProcedureClauseTest.php +++ /dev/null @@ -1,152 +0,0 @@ -assertSame("", $callProcedureClause->toQuery()); - $this->assertEquals([], $callProcedureClause->getArguments()); - $this->assertNull($callProcedureClause->getProcedure()); - $this->assertEquals([], $callProcedureClause->getYieldVariables()); - } - - public function testZeroArguments(): void - { - $callProcedureClause = new CallProcedureClause(); - $callProcedureClause->setProcedure("apoc.json"); - - $this->assertSame("CALL apoc.json()", $callProcedureClause->toQuery()); - $this->assertEquals([], $callProcedureClause->getArguments()); - $this->assertEquals('apoc.json', $callProcedureClause->getProcedure()); - $this->assertEquals([], $callProcedureClause->getYieldVariables()); - } - - public function testOneArgument(): void - { - $callProcedureClause = new CallProcedureClause(); - $callProcedureClause->setProcedure("apoc.json"); - - $param = $this->getQueryConvertableMock(AnyType::class, "'text'"); - $callProcedureClause->addArgument($param); - - $this->assertSame("CALL apoc.json('text')", $callProcedureClause->toQuery()); - $this->assertEquals('apoc.json', $callProcedureClause->getProcedure()); - $this->assertEquals([$param], $callProcedureClause->getArguments()); - $this->assertEquals([], $callProcedureClause->getYieldVariables()); - } - - public function testMultipleArgument(): void - { - $callProcedureClause = new CallProcedureClause(); - $callProcedureClause->setProcedure("apoc.json"); - - $expression = $this->getQueryConvertableMock(AnyType::class, "'text'"); - - $callProcedureClause->addArgument($expression); - $callProcedureClause->addArgument($expression); - $callProcedureClause->addArgument($expression); - - $this->assertSame("CALL apoc.json('text', 'text', 'text')", $callProcedureClause->toQuery()); - $this->assertEquals([$expression, $expression, $expression], $callProcedureClause->getArguments()); - $this->assertEquals('apoc.json', $callProcedureClause->getProcedure()); - $this->assertEquals([], $callProcedureClause->getYieldVariables()); - } - - public function testWithArguments(): void - { - $callProcedureClause = new CallProcedureClause(); - $callProcedureClause->setProcedure("apoc.json"); - - $expression = $this->getQueryConvertableMock(AnyType::class, "'text'"); - - $callProcedureClause->addArgument($expression); - $callProcedureClause->addArgument($expression); - $callProcedureClause->addArgument($expression); - - // This should overwrite the previous calls to addArgument - $callProcedureClause->withArguments([$expression]); - - $this->assertSame("CALL apoc.json('text')", $callProcedureClause->toQuery()); - $this->assertEquals([$expression], $callProcedureClause->getArguments()); - $this->assertEquals('apoc.json', $callProcedureClause->getProcedure()); - $this->assertEquals([], $callProcedureClause->getYieldVariables()); - } - - public function testWithYield(): void - { - $callProcedureClause = new CallProcedureClause(); - - $callProcedureClause->setProcedure("apoc.json"); - - $a = $this->getQueryConvertableMock(Variable::class, "a"); - $b = $this->getQueryConvertableMock(Variable::class, "b"); - $c = $this->getQueryConvertableMock(Variable::class, "c"); - - // This should overwrite the previous calls to addArgument - $callProcedureClause->yields([$a, $b, $c]); - - $this->assertSame("CALL apoc.json() YIELD a, b, c", $callProcedureClause->toQuery()); - $this->assertEquals([], $callProcedureClause->getArguments()); - $this->assertEquals('apoc.json', $callProcedureClause->getProcedure()); - $this->assertEquals([$a, $b, $c], $callProcedureClause->getYieldVariables()); - } - - public function testYieldDoesNotAcceptAnyType(): void - { - $callProcedureClause = new CallProcedureClause(); - - $a = $this->getQueryConvertableMock(AnyType::class, "a"); - - $this->expectException(TypeError::class); - - // callProcedureClause->yields requires a Variable - $callProcedureClause->yields([$a]); - } - - public function testArgumentsOnlyAcceptsAnyType(): void - { - $callProcedureClause = new CallProcedureClause(); - - $a = new class () {}; - - $this->expectException(TypeError::class); - - // $callProcedureClause->withArguments() requires Anytype - $callProcedureClause->withArguments([$a]); - } -} diff --git a/tests/Unit/Clauses/CreateClauseTest.php b/tests/Unit/Clauses/CreateClauseTest.php deleted file mode 100644 index 9a678d12..00000000 --- a/tests/Unit/Clauses/CreateClauseTest.php +++ /dev/null @@ -1,106 +0,0 @@ -assertSame("", $createClause->toQuery()); - $this->assertEquals([], $createClause->getPatterns()); - } - - public function testSinglePattern(): void - { - $createClause = new CreateClause(); - $pattern = $this->getQueryConvertableMock(NodeType::class, "(a)"); - - $createClause->addPattern($pattern); - - $this->assertSame("CREATE (a)", $createClause->toQuery()); - $this->assertEquals([$pattern], $createClause->getPatterns()); - } - - public function testMultiplePatterns(): void - { - $createClause = new CreateClause(); - - $patternA = $this->getQueryConvertableMock(NodeType::class, "(a)"); - $patternB = $this->getQueryConvertableMock(PathType::class, "(b)-->(c)"); - - $createClause->addPattern($patternA); - $createClause->addPattern($patternB); - - $this->assertSame("CREATE (a), (b)-->(c)", $createClause->toQuery()); - $this->assertEquals([$patternA, $patternB], $createClause->getPatterns()); - } - - public function testAcceptsNodeType(): void - { - $createClause = new CreateClause(); - - $patternA = $this->getQueryConvertableMock(NodeType::class, "(a)"); - - $createClause->addPattern($patternA); - $createClause->toQuery(); - - $this->assertEquals([$patternA], $createClause->getPatterns()); - } - - public function testAcceptsPathType(): void - { - $createClause = new CreateClause(); - - $patternA = $this->getQueryConvertableMock(PathType::class, "(a)"); - - $createClause->addPattern($patternA); - $createClause->toQuery(); - $this->assertEquals([$patternA], $createClause->getPatterns()); - } - - public function testDoesNotAcceptAnyType(): void - { - $createClause = new CreateClause(); - - $patternA = $this->getQueryConvertableMock(AnyType::class, "(a)"); - - $this->expectException(TypeError::class); - - $createClause->addPattern($patternA); - $createClause->toQuery(); - } -} diff --git a/tests/Unit/Clauses/DeleteClauseTest.php b/tests/Unit/Clauses/DeleteClauseTest.php deleted file mode 100644 index 03a538ea..00000000 --- a/tests/Unit/Clauses/DeleteClauseTest.php +++ /dev/null @@ -1,108 +0,0 @@ -assertSame("", $delete->toQuery()); - $this->assertEquals([], $delete->getVariables()); - $this->assertFalse($delete->detachesDeletion()); - } - - public function testSingleVariable(): void - { - $delete = new DeleteClause(); - $variable = $this->getQueryConvertableMock(Variable::class, "a"); - - $delete->addVariable($variable); - - $this->assertSame("DELETE a", $delete->toQuery()); - $this->assertEquals([$variable], $delete->getVariables()); - $this->assertFalse($delete->detachesDeletion()); - } - - public function testMultipleVariables(): void - { - $delete = new DeleteClause(); - - $a = $this->getQueryConvertableMock(Variable::class, "a"); - $b = $this->getQueryConvertableMock(Variable::class, "b"); - - $delete->addVariable($a); - $delete->addVariable($b); - - $this->assertSame("DELETE a, b", $delete->toQuery()); - $this->assertEquals([$a, $b], $delete->getVariables()); - $this->assertFalse($delete->detachesDeletion()); - } - - public function testDetachDelete(): void - { - $delete = new DeleteClause(); - $variable = $this->getQueryConvertableMock(Variable::class, "a"); - - $delete->addVariable($variable); - $delete->setDetach(true); - - $this->assertSame("DETACH DELETE a", $delete->toQuery()); - $this->assertEquals([$variable], $delete->getVariables()); - $this->assertTrue($delete->detachesDeletion()); - } - - public function testAcceptsVariable(): void - { - $delete = new DeleteClause(); - $variable = $this->getQueryConvertableMock(Variable::class, "a"); - - $delete->addVariable($variable); - $delete->toQuery(); - $this->assertEquals([$variable], $delete->getVariables()); - $this->assertFalse($delete->detachesDeletion()); - } - - public function testDoesNotAcceptAnyType(): void - { - $delete = new DeleteClause(); - $variable = $this->getQueryConvertableMock(AnyType::class, "a"); - - $this->expectException(TypeError::class); - - $delete->addVariable($variable); - $delete->toQuery(); - } -} diff --git a/tests/Unit/Clauses/LimitClauseTest.php b/tests/Unit/Clauses/LimitClauseTest.php deleted file mode 100644 index 3f631de1..00000000 --- a/tests/Unit/Clauses/LimitClauseTest.php +++ /dev/null @@ -1,79 +0,0 @@ -assertSame("", $limit->toQuery()); - $this->assertNull($limit->getLimit()); - } - - public function testPattern(): void - { - $limit = new LimitClause(); - $expression = $this->getQueryConvertableMock(NumeralType::class, "10"); - - $limit->setLimit($expression); - - $this->assertSame("LIMIT 10", $limit->toQuery()); - $this->assertEquals($expression, $limit->getLimit()); - } - - public function testAcceptsNumeralType(): void - { - $limit = new LimitClause(); - $expression = $this->getQueryConvertableMock(NumeralType::class, "10"); - - $limit->setLimit($expression); - - $limit->toQuery(); - $this->assertEquals($expression, $limit->getLimit()); - } - - public function testDoesNotAcceptAnyType(): void - { - $limit = new LimitClause(); - $expression = $this->getQueryConvertableMock(AnyType::class, "10"); - - $this->expectException(TypeError::class); - - $limit->setLimit($expression); - - $limit->toQuery(); - } -} diff --git a/tests/Unit/Clauses/MatchClauseTest.php b/tests/Unit/Clauses/MatchClauseTest.php deleted file mode 100644 index 1baf2b00..00000000 --- a/tests/Unit/Clauses/MatchClauseTest.php +++ /dev/null @@ -1,101 +0,0 @@ -assertSame("", $match->toQuery()); - $this->assertEquals([], $match->getPatterns()); - } - - public function testSinglePattern(): void - { - $match = new MatchClause(); - $pattern = $this->getQueryConvertableMock(NodeType::class, "(a)"); - $match->addPattern($pattern); - - $this->assertSame("MATCH (a)", $match->toQuery()); - $this->assertEquals([$pattern], $match->getPatterns()); - } - - public function testMultiplePatterns(): void - { - $match = new MatchClause(); - $patternA = $this->getQueryConvertableMock(NodeType::class, "(a)"); - $patternB = $this->getQueryConvertableMock(PathType::class, "(b)-->(c)"); - - $match->addPattern($patternA); - $match->addPattern($patternB); - - $this->assertSame("MATCH (a), (b)-->(c)", $match->toQuery()); - $this->assertEquals([$patternA, $patternB], $match->getPatterns()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsNodeType(): void - { - $match = new MatchClause(); - $match->addPattern($this->getQueryConvertableMock(NodeType::class, "(a)")); - - $match->toQuery(); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsPathType(): void - { - $match = new MatchClause(); - $match->addPattern($this->getQueryConvertableMock(PathType::class, "(a)")); - - $match->toQuery(); - } - - public function testDoesNotAcceptAnyType(): void - { - $this->expectException(TypeError::class); - - $match = new MatchClause(); - $match->addPattern($this->getQueryConvertableMock(AnyType::class, "(a)")); - - $match->toQuery(); - } -} diff --git a/tests/Unit/Clauses/MergeClauseTest.php b/tests/Unit/Clauses/MergeClauseTest.php deleted file mode 100644 index 091531dc..00000000 --- a/tests/Unit/Clauses/MergeClauseTest.php +++ /dev/null @@ -1,149 +0,0 @@ -assertSame("", $merge->toQuery()); - $this->assertNull($merge->getOnCreateClause()); - $this->assertNull($merge->getPattern()); - $this->assertNull($merge->getOnMatchClause()); - } - - public function testPattern(): void - { - $merge = new MergeClause(); - $pattern = $this->getQueryConvertableMock(NodeType::class, "(a)"); - - $merge->setPattern($pattern); - - $this->assertSame("MERGE (a)", $merge->toQuery()); - $this->assertNull($merge->getOnCreateClause()); - $this->assertEquals($pattern, $merge->getPattern()); - $this->assertNull($merge->getOnMatchClause()); - } - - public function testSetOnCreate(): void - { - $merge = new MergeClause(); - - $pattern = $this->getQueryConvertableMock(PathType::class, "(a)-->(b)"); - $clause = $this->getQueryConvertableMock(Clause::class, "SET a = 10"); - - $merge->setPattern($pattern); - $merge->setOnCreate($clause); - - $this->assertSame("MERGE (a)-->(b) ON CREATE SET a = 10", $merge->toQuery()); - $this->assertEquals($clause, $merge->getOnCreateClause()); - $this->assertEquals($pattern, $merge->getPattern()); - $this->assertNull($merge->getOnMatchClause()); - } - - public function testSetOnMatch(): void - { - $merge = new MergeClause(); - - $pattern = $this->getQueryConvertableMock(NodeType::class, "(a)"); - $clause = $this->getQueryConvertableMock(Clause::class, "SET a = 10"); - - $merge->setPattern($pattern); - $merge->setOnMatch($clause); - - $this->assertSame("MERGE (a) ON MATCH SET a = 10", $merge->toQuery()); - $this->assertNull($merge->getOnCreateClause()); - $this->assertEquals($pattern, $merge->getPattern()); - $this->assertEquals($clause, $merge->getOnMatchClause()); - } - - public function testSetOnBoth(): void - { - $merge = new MergeClause(); - - $pattern = $this->getQueryConvertableMock(PathType::class, "(a)-->(b)"); - $clause = $this->getQueryConvertableMock(Clause::class, "SET a = 10"); - - $merge->setPattern($pattern); - $merge->setOnCreate($clause); - $merge->setOnMatch($clause); - - $this->assertSame("MERGE (a)-->(b) ON CREATE SET a = 10 ON MATCH SET a = 10", $merge->toQuery()); - $this->assertEquals($clause, $merge->getOnCreateClause()); - $this->assertEquals($pattern, $merge->getPattern()); - $this->assertEquals($clause, $merge->getOnMatchClause()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsNodeType(): void - { - $merge = new MergeClause(); - $pattern = $this->getQueryConvertableMock(NodeType::class, "(a)"); - - $merge->setPattern($pattern); - - $merge->toQuery(); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsPathType(): void - { - $merge = new MergeClause(); - $pattern = $this->getQueryConvertableMock(PathType::class, "(a)"); - - $merge->setPattern($pattern); - - $merge->toQuery(); - } - - public function testDoesNotAcceptAnyType(): void - { - $merge = new MergeClause(); - $pattern = $this->getQueryConvertableMock(AnyType::class, "(a)"); - - $this->expectException(TypeError::class); - - $merge->setPattern($pattern); - - $merge->toQuery(); - } -} diff --git a/tests/Unit/Clauses/OptionalMatchTest.php b/tests/Unit/Clauses/OptionalMatchTest.php deleted file mode 100644 index c1aa7040..00000000 --- a/tests/Unit/Clauses/OptionalMatchTest.php +++ /dev/null @@ -1,102 +0,0 @@ -assertSame("", $match->toQuery()); - $this->assertEquals([], $match->getPatterns()); - } - - public function testSinglePattern(): void - { - $match = new OptionalMatchClause(); - $pattern = $this->getQueryConvertableMock(NodeType::class, "(a)"); - $match->addPattern($pattern); - - $this->assertSame("OPTIONAL MATCH (a)", $match->toQuery()); - $this->assertEquals([$pattern], $match->getPatterns()); - } - - public function testMultiplePatterns(): void - { - $match = new OptionalMatchClause(); - $patternA = $this->getQueryConvertableMock(NodeType::class, "(a)"); - $patternB = $this->getQueryConvertableMock(PathType::class, "(b)-->(c)"); - - $match->addPattern($patternA); - $match->addPattern($patternB); - - $this->assertSame("OPTIONAL MATCH (a), (b)-->(c)", $match->toQuery()); - $this->assertEquals([$patternA, $patternB], $match->getPatterns()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsNodeType(): void - { - $match = new OptionalMatchClause(); - $match->addPattern($this->getQueryConvertableMock(NodeType::class, "(a)")); - - $match->toQuery(); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsPathType(): void - { - $match = new OptionalMatchClause(); - $match->addPattern($this->getQueryConvertableMock(PathType::class, "(a)")); - - $match->toQuery(); - } - - public function testDoesNotAcceptAnyType(): void - { - $match = new OptionalMatchClause(); - - $this->expectException(TypeError::class); - - $match->addPattern($this->getQueryConvertableMock(AnyType::class, "(a)")); - - $match->toQuery(); - } -} diff --git a/tests/Unit/Clauses/OrderByClauseTest.php b/tests/Unit/Clauses/OrderByClauseTest.php deleted file mode 100644 index 7d0030f7..00000000 --- a/tests/Unit/Clauses/OrderByClauseTest.php +++ /dev/null @@ -1,120 +0,0 @@ -assertSame("", $orderBy->toQuery()); - $this->assertEquals([], $orderBy->getProperties()); - $this->assertFalse($orderBy->isDescending()); - } - - public function testSingleProperty(): void - { - $orderBy = new OrderByClause(); - $property = $this->getQueryConvertableMock(Property::class, "a.a"); - $orderBy->addProperty($property); - - $this->assertSame("ORDER BY a.a", $orderBy->toQuery()); - $this->assertEquals([$property], $orderBy->getProperties()); - $this->assertFalse($orderBy->isDescending()); - } - - public function testMultipleProperties(): void - { - $orderBy = new OrderByClause(); - $propertyA = $this->getQueryConvertableMock(Property::class, "a.a"); - $propertyB = $this->getQueryConvertableMock(Property::class, "a.b"); - - $orderBy->addProperty($propertyA); - $orderBy->addProperty($propertyB); - - $this->assertSame("ORDER BY a.a, a.b", $orderBy->toQuery()); - $this->assertEquals([$propertyA, $propertyB], $orderBy->getProperties()); - $this->assertFalse($orderBy->isDescending()); - } - - public function testSinglePropertyDesc(): void - { - $orderBy = new OrderByClause(); - $property = $this->getQueryConvertableMock(Property::class, "a.a"); - $orderBy->addProperty($property); - $orderBy->setDescending(); - - $this->assertSame("ORDER BY a.a DESCENDING", $orderBy->toQuery()); - $this->assertEquals([$property], $orderBy->getProperties()); - $this->assertTrue($orderBy->isDescending()); - } - - public function testMultiplePropertiesDesc(): void - { - $orderBy = new OrderByClause(); - $propertyA = $this->getQueryConvertableMock(Property::class, "a.a"); - $propertyB = $this->getQueryConvertableMock(Property::class, "a.b"); - - $orderBy->addProperty($propertyA); - $orderBy->addProperty($propertyB); - $orderBy->setDescending(); - - $this->assertSame("ORDER BY a.a, a.b DESCENDING", $orderBy->toQuery()); - $this->assertEquals([$propertyA, $propertyB], $orderBy->getProperties()); - $this->assertTrue($orderBy->isDescending()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsProperty(): void - { - $orderBy = new OrderByClause(); - $orderBy->addProperty($this->getQueryConvertableMock(Property::class, "a.a")); - - $orderBy->toQuery(); - } - - public function testDoesNotAcceptAnyType(): void - { - $orderBy = new OrderByClause(); - - $this->expectException(TypeError::class); - - $orderBy->addProperty($this->getQueryConvertableMock(AnyType::class, "a.a")); - - $orderBy->toQuery(); - } -} diff --git a/tests/Unit/Clauses/RawClauseTest.php b/tests/Unit/Clauses/RawClauseTest.php deleted file mode 100644 index 207e627c..00000000 --- a/tests/Unit/Clauses/RawClauseTest.php +++ /dev/null @@ -1,41 +0,0 @@ -assertSame("UNIMPLEMENTED clause body", $raw->toQuery()); - } -} diff --git a/tests/Unit/Clauses/RemoveClauseTest.php b/tests/Unit/Clauses/RemoveClauseTest.php deleted file mode 100644 index af632b39..00000000 --- a/tests/Unit/Clauses/RemoveClauseTest.php +++ /dev/null @@ -1,109 +0,0 @@ -assertSame("", $remove->toQuery()); - $this->assertEquals([], $remove->getExpressions()); - } - - public function testSingleExpression(): void - { - $remove = new RemoveClause(); - $expression = $this->getQueryConvertableMock(Property::class, "(a)"); - - $remove->addExpression($expression); - - $this->assertSame("REMOVE (a)", $remove->toQuery()); - $this->assertEquals([$expression], $remove->getExpressions()); - } - - public function testMultipleExpressions(): void - { - $remove = new RemoveClause(); - - $a = $this->getQueryConvertableMock(Property::class, "(a)"); - $b = $this->getQueryConvertableMock(Property::class, "(b)"); - - $remove->addExpression($a); - $remove->addExpression($b); - - $this->assertSame("REMOVE (a), (b)", $remove->toQuery()); - $this->assertEquals([$a, $b], $remove->getExpressions()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsProperty(): void - { - $remove = new RemoveClause(); - $expression = $this->getQueryConvertableMock(Property::class, "(a)"); - - $remove->addExpression($expression); - - $remove->toQuery(); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsLabel(): void - { - $remove = new RemoveClause(); - $expression = $this->getQueryConvertableMock(Label::class, "(a)"); - - $remove->addExpression($expression); - - $remove->toQuery(); - } - - public function testDoesNotAcceptAnyType(): void - { - $remove = new RemoveClause(); - $expression = $this->getQueryConvertableMock(AnyType::class, "(a)"); - - $this->expectException(TypeError::class); - - $remove->addExpression($expression); - - $remove->toQuery(); - } -} diff --git a/tests/Unit/Clauses/ReturnClauseTest.php b/tests/Unit/Clauses/ReturnClauseTest.php deleted file mode 100644 index d394727e..00000000 --- a/tests/Unit/Clauses/ReturnClauseTest.php +++ /dev/null @@ -1,146 +0,0 @@ -assertSame("", $return->toQuery()); - $this->assertSame([], $return->getColumns()); - $this->assertFalse($return->isDistinct()); - } - - public function testSingleColumn(): void - { - $return = new ReturnClause(); - $column = $this->getQueryConvertableMock(AnyType::class, "a"); - $return->addColumn($column); - - $this->assertSame("RETURN a", $return->toQuery()); - $this->assertSame([$column], $return->getColumns()); - $this->assertFalse($return->isDistinct()); - } - - public function testMultipleColumns(): void - { - $return = new ReturnClause(); - - $columnA = $this->getQueryConvertableMock(AnyType::class, "a"); - $columnB = $this->getQueryConvertableMock(AnyType::class, "b"); - $columnC = $this->getQueryConvertableMock(AnyType::class, "c"); - - $return->addColumn($columnA); - $return->addColumn($columnB); - $return->addColumn($columnC); - - $this->assertSame("RETURN a, b, c", $return->toQuery()); - $this->assertSame([$columnA, $columnB, $columnC], $return->getColumns()); - $this->assertFalse($return->isDistinct()); - } - - public function testSingleAlias(): void - { - $return = new ReturnClause(); - $column = $this->getQueryConvertableMock(AnyType::class, "a"); - $return->addColumn($column, "b"); - - $this->assertSame("RETURN a AS b", $return->toQuery()); - $this->assertSame(['b' => $column], $return->getColumns()); - $this->assertFalse($return->isDistinct()); - } - - public function testMultipleAliases(): void - { - $return = new ReturnClause(); - $columnA = $this->getQueryConvertableMock(AnyType::class, "a"); - $columnB = $this->getQueryConvertableMock(AnyType::class, "b"); - $return->addColumn($columnA, "b"); - $return->addColumn($columnB, "c"); - - $this->assertSame("RETURN a AS b, b AS c", $return->toQuery()); - $this->assertSame(['b' => $columnA, 'c' => $columnB], $return->getColumns()); - $this->assertFalse($return->isDistinct()); - } - - public function testMixedAliases(): void - { - $return = new ReturnClause(); - $columnA = $this->getQueryConvertableMock(AnyType::class, "a"); - $columnB = $this->getQueryConvertableMock(AnyType::class, "c"); - $columnC = $this->getQueryConvertableMock(AnyType::class, "b"); - $return->addColumn($columnA, "b"); - $return->addColumn($columnB); - $return->addColumn($columnC, "c"); - - $this->assertSame("RETURN a AS b, c, b AS c", $return->toQuery()); - $this->assertEquals(['b' => $columnA, $columnB, 'c' => $columnA], $return->getColumns()); - $this->assertFalse($return->isDistinct()); - } - - public function testAliasIsEscaped(): void - { - $return = new ReturnClause(); - $column = $this->getQueryConvertableMock(AnyType::class, "a"); - $return->addColumn($column, ":"); - - $this->assertSame("RETURN a AS `:`", $return->toQuery()); - $this->assertSame([':' => $column], $return->getColumns()); - $this->assertFalse($return->isDistinct()); - } - - public function testReturnDistinct(): void - { - $return = new ReturnClause(); - $column = $this->getQueryConvertableMock(AnyType::class, "a"); - $return->addColumn($column); - $return->setDistinct(); - - $this->assertSame("RETURN DISTINCT a", $return->toQuery()); - $this->assertSame([$column], $return->getColumns()); - $this->assertTrue($return->isDistinct()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAnyType(): void - { - $return = new ReturnClause(); - $return->addColumn($this->getQueryConvertableMock(AnyType::class, "a")); - $return->setDistinct(); - - $return->toQuery(); - } -} diff --git a/tests/Unit/Clauses/SetClauseTest.php b/tests/Unit/Clauses/SetClauseTest.php deleted file mode 100644 index ed4cc397..00000000 --- a/tests/Unit/Clauses/SetClauseTest.php +++ /dev/null @@ -1,108 +0,0 @@ -assertSame("", $set->toQuery()); - $this->assertEquals([], $set->getExpressions()); - } - - public function testSinglePattern(): void - { - $set = new SetClause(); - $expression = $this->getQueryConvertableMock(Assignment::class, "(a)"); - - $set->addAssignment($expression); - - $this->assertSame("SET (a)", $set->toQuery()); - $this->assertEquals([$expression], $set->getExpressions()); - } - - public function testMultiplePattern(): void - { - $set = new SetClause(); - $expressionA = $this->getQueryConvertableMock(Assignment::class, "(a)"); - $expressionB = $this->getQueryConvertableMock(Assignment::class, "(b)"); - - $set->addAssignment($expressionA); - $set->addAssignment($expressionB); - - $this->assertSame("SET (a), (b)", $set->toQuery()); - $this->assertEquals([$expressionA, $expressionB], $set->getExpressions()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAssignment(): void - { - $set = new SetClause(); - $expression = $this->getQueryConvertableMock(Assignment::class, "(a)"); - - $set->addAssignment($expression); - - $set->toQuery(); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsLabel(): void - { - $set = new SetClause(); - $expression = $this->getQueryConvertableMock(Label::class, "(a)"); - - $set->addAssignment($expression); - - $set->toQuery(); - } - - public function testDoesNotAcceptAnyType(): void - { - $set = new SetClause(); - $expression = $this->getQueryConvertableMock(AnyType::class, "(a)"); - - $this->expectException(TypeError::class); - - $set->addAssignment($expression); - - $set->toQuery(); - } -} diff --git a/tests/Unit/Clauses/SkipClauseTest.php b/tests/Unit/Clauses/SkipClauseTest.php deleted file mode 100644 index 1ff832b4..00000000 --- a/tests/Unit/Clauses/SkipClauseTest.php +++ /dev/null @@ -1,81 +0,0 @@ -assertSame("", $skip->toQuery()); - $this->assertNull($skip->getSkip()); - } - - public function testPattern(): void - { - $skip = new SkipClause(); - $expression = $this->getQueryConvertableMock(NumeralType::class, "10"); - - $skip->setSkip($expression); - - $this->assertSame("SKIP 10", $skip->toQuery()); - $this->assertEquals($expression, $skip->getSkip()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsNumeralType(): void - { - $skip = new SkipClause(); - $expression = $this->getQueryConvertableMock(NumeralType::class, "10"); - - $skip->setSkip($expression); - - $skip->toQuery(); - } - - public function testDoesNotAcceptAnyType(): void - { - $skip = new SkipClause(); - $expression = $this->getQueryConvertableMock(AnyType::class, "10"); - - $this->expectException(TypeError::class); - - $skip->setSkip($expression); - - $skip->toQuery(); - } -} diff --git a/tests/Unit/Clauses/UnionClauseTest.php b/tests/Unit/Clauses/UnionClauseTest.php deleted file mode 100644 index e6865eac..00000000 --- a/tests/Unit/Clauses/UnionClauseTest.php +++ /dev/null @@ -1,54 +0,0 @@ -assertFalse($union->includesAll()); - - $this->assertEquals('UNION', $union->toQuery()); - } - - public function testAll(): void - { - $union = new UnionClause(true); - - $this->assertTrue($union->includesAll()); - - $this->assertEquals('UNION ALL', $union->toQuery()); - } - - public function testUnionFactory(): void - { - $nodeX = Query::node('X')->named('x'); - $nodeY = Query::node('Y')->named('y'); - - $left = Query::new()->match($nodeX)->returning($nodeX->getVariable()); - $right = Query::new()->match($nodeY)->returning($nodeY->getVariable()); - - $query = UnionClause::union($left, $right, false); - - $this->assertEquals('MATCH (x:X) RETURN x UNION MATCH (y:Y) RETURN y', $query->toQuery()); - } - - public function testUnionFactoryAll(): void - { - $nodeX = Query::node('X')->named('x'); - $nodeY = Query::node('Y')->named('y'); - - $left = Query::new()->match($nodeX)->returning($nodeX->getVariable()); - $right = Query::new()->match($nodeY)->returning($nodeY->getVariable()); - - $query = UnionClause::union($left, $right, true); - - $this->assertEquals('MATCH (x:X) RETURN x UNION ALL MATCH (y:Y) RETURN y', $query->toQuery()); - } -} diff --git a/tests/Unit/Clauses/WhereClauseTest.php b/tests/Unit/Clauses/WhereClauseTest.php deleted file mode 100644 index 11e0e904..00000000 --- a/tests/Unit/Clauses/WhereClauseTest.php +++ /dev/null @@ -1,67 +0,0 @@ -assertSame("", $where->toQuery()); - $this->assertNull($where->getExpression()); - } - - public function testExpression(): void - { - $where = new WhereClause(); - $expression = $this->getQueryConvertableMock(AnyType::class, "(a)"); - - $where->setExpression($expression); - - $this->assertSame("WHERE (a)", $where->toQuery()); - $this->assertEquals($expression, $where->getExpression()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAnyType(): void - { - $where = new WhereClause(); - $expression = $this->getQueryConvertableMock(AnyType::class, "(a)"); - - $where->setExpression($expression); - - $where->toQuery(); - } -} diff --git a/tests/Unit/Clauses/WithClauseTest.php b/tests/Unit/Clauses/WithClauseTest.php deleted file mode 100644 index f02795ec..00000000 --- a/tests/Unit/Clauses/WithClauseTest.php +++ /dev/null @@ -1,127 +0,0 @@ -assertSame("", $return->toQuery()); - $this->assertEquals([], $return->getExpressions()); - } - - public function testSingleEntry(): void - { - $return = new WithClause(); - $entry = $this->getQueryConvertableMock(AnyType::class, "a"); - $return->addEntry($entry); - - $this->assertSame("WITH a", $return->toQuery()); - $this->assertEquals([$entry], $return->getExpressions()); - } - - public function testMultipleEntries(): void - { - $return = new WithClause(); - $entryA = $this->getQueryConvertableMock(AnyType::class, "a"); - $entryB = $this->getQueryConvertableMock(AnyType::class, "b"); - $entryC = $this->getQueryConvertableMock(AnyType::class, "c"); - - $return->addEntry($entryA); - $return->addEntry($entryB); - $return->addEntry($entryC); - - $this->assertSame("WITH a, b, c", $return->toQuery()); - $this->assertEquals([$entryA, $entryB, $entryC], $return->getExpressions()); - } - - public function testSingleAlias(): void - { - $return = new WithClause(); - $entry = $this->getQueryConvertableMock(AnyType::class, "a"); - $return->addEntry($entry, "b"); - - $this->assertSame("WITH a AS b", $return->toQuery()); - $this->assertEquals(['b' => $entry], $return->getExpressions()); - } - - public function testMultipleAliases(): void - { - $return = new WithClause(); - $entryA = $this->getQueryConvertableMock(AnyType::class, "a"); - $entryB = $this->getQueryConvertableMock(AnyType::class, "b"); - - $return->addEntry($entryA, "b"); - $return->addEntry($entryB, "c"); - - $this->assertSame("WITH a AS b, b AS c", $return->toQuery()); - $this->assertEquals(['b' => $entryA, 'c' => $entryB], $return->getExpressions()); - } - - public function testMixedAliases(): void - { - $return = new WithClause(); - $entryA = $this->getQueryConvertableMock(AnyType::class, "a"); - $entryB = $this->getQueryConvertableMock(AnyType::class, "c"); - $entryC = $this->getQueryConvertableMock(AnyType::class, "b"); - - $return->addEntry($entryA, "b"); - $return->addEntry($entryB); - $return->addEntry($entryC, "c"); - - $this->assertSame("WITH a AS b, c, b AS c", $return->toQuery()); - $this->assertEquals(['b' => $entryA, $entryB, 'c' => $entryC], $return->getExpressions()); - } - - public function testAliasIsEscaped(): void - { - $return = new WithClause(); - $entry = $this->getQueryConvertableMock(AnyType::class, "a"); - $return->addEntry($entry, ":"); - - $this->assertSame("WITH a AS `:`", $return->toQuery()); - $this->assertEquals([':' => $entry], $return->getExpressions()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAnyType(): void - { - $return = new WithClause(); - $return->addEntry($this->getQueryConvertableMock(AnyType::class, "a"), ":"); - - $return->toQuery(); - } -} diff --git a/tests/Unit/ContainsTest.php b/tests/Unit/ContainsTest.php deleted file mode 100644 index ca34a61d..00000000 --- a/tests/Unit/ContainsTest.php +++ /dev/null @@ -1,70 +0,0 @@ -getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b")); - - $this->assertSame("(a CONTAINS b)", $contains->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $contains = new Contains($this->getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b"), false); - - $this->assertSame("a CONTAINS b", $contains->toQuery()); - } - - public function testCannotBeNested(): void - { - $contains = new Contains($this->getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b")); - - $this->expectException(TypeError::class); - - $contains = new Contains($contains, $contains); - - $contains->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $contains = new Contains($this->getQueryConvertableMock(AnyType::class, "a"), $this->getQueryConvertableMock(AnyType::class, "b")); - - $contains->toQuery(); - } -} diff --git a/tests/Unit/DivisionTest.php b/tests/Unit/DivisionTest.php deleted file mode 100644 index 73519b50..00000000 --- a/tests/Unit/DivisionTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->assertSame("(10 / 15)", $division->toQuery()); - - $division = new Division($division, $division); - - $this->assertSame("((10 / 15) / (10 / 15))", $division->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $division = new Division($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15"), false); - - $this->assertSame("10 / 15", $division->toQuery()); - - $division = new Division($division, $division); - - $this->assertSame("(10 / 15 / 10 / 15)", $division->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $division = new Division($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $division->toQuery(); - } -} diff --git a/tests/Unit/EndsWithTest.php b/tests/Unit/EndsWithTest.php deleted file mode 100644 index ea351b7b..00000000 --- a/tests/Unit/EndsWithTest.php +++ /dev/null @@ -1,70 +0,0 @@ -getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b")); - - $this->assertSame("(a ENDS WITH b)", $endsWith->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $endsWith = new EndsWith($this->getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b"), false); - - $this->assertSame("a ENDS WITH b", $endsWith->toQuery()); - } - - public function testCannotBeNested(): void - { - $endsWith = new EndsWith($this->getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b")); - - $this->expectException(TypeError::class); - - $endsWith = new EndsWith($endsWith, $endsWith); - - $endsWith->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $endsWith = new EndsWith($this->getQueryConvertableMock(AnyType::class, "a"), $this->getQueryConvertableMock(AnyType::class, "b")); - - $endsWith->toQuery(); - } -} diff --git a/tests/Unit/EqualityTest.php b/tests/Unit/EqualityTest.php deleted file mode 100644 index e52ec48f..00000000 --- a/tests/Unit/EqualityTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(PropertyType::class, "10"), $this->getQueryConvertableMock(PropertyType::class, "15")); - - $this->assertSame("(10 = 15)", $equality->toQuery()); - - $equality = new Equality($equality, $equality); - - $this->assertSame("((10 = 15) = (10 = 15))", $equality->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $equality = new Equality($this->getQueryConvertableMock(PropertyType::class, "10"), $this->getQueryConvertableMock(PropertyType::class, "15"), false); - - $this->assertSame("10 = 15", $equality->toQuery()); - - $equality = new Equality($equality, $equality); - - $this->assertSame("(10 = 15 = 10 = 15)", $equality->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $equality = new Equality($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $equality->toQuery(); - } -} diff --git a/tests/Unit/ExistsTest.php b/tests/Unit/ExistsTest.php deleted file mode 100644 index 07ec4826..00000000 --- a/tests/Unit/ExistsTest.php +++ /dev/null @@ -1,46 +0,0 @@ -getQueryConvertableMock(MatchClause::class, "MATCH (person)-[:HAS_DOG]->(dog:Dog)")); - - $this->assertSame("EXISTS { MATCH (person)-[:HAS_DOG]->(dog:Dog) }", $exists->toQuery()); - - $exists = new Exists($this->getQueryConvertableMock(MatchClause::class, "MATCH (person)-[:HAS_DOG]->(dog:Dog)"), $this->getQueryConvertableMock(WhereClause::class, "WHERE toy.name = 'Banana'")); - - $this->assertSame("EXISTS { MATCH (person)-[:HAS_DOG]->(dog:Dog) WHERE toy.name = 'Banana' }", $exists->toQuery()); - } -} diff --git a/tests/Unit/ExponentiationTest.php b/tests/Unit/ExponentiationTest.php deleted file mode 100644 index d9002775..00000000 --- a/tests/Unit/ExponentiationTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->assertSame("(10 ^ 15)", $exponentiation->toQuery()); - - $exponentiation = new Exponentiation($exponentiation, $exponentiation); - - $this->assertSame("((10 ^ 15) ^ (10 ^ 15))", $exponentiation->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $exponentiation = new Exponentiation($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15"), false); - - $this->assertSame("10 ^ 15", $exponentiation->toQuery()); - - $exponentiation = new Exponentiation($exponentiation, $exponentiation); - - $this->assertSame("(10 ^ 15 ^ 10 ^ 15)", $exponentiation->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $exponentiation = new Exponentiation($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $exponentiation->toQuery(); - } -} diff --git a/tests/Unit/ExpressionListTest.php b/tests/Unit/ExpressionListTest.php deleted file mode 100644 index 5e253776..00000000 --- a/tests/Unit/ExpressionListTest.php +++ /dev/null @@ -1,93 +0,0 @@ -assertSame("[]", $expressionList->toQuery()); - } - - /** - * @dataProvider provideOneDimensionalData - * @param array $expressions - * @param string $expected - */ - public function testOneDimensional(array $expressions, string $expected) - { - $expressionList = new ExpressionList($expressions); - - $this->assertSame($expected, $expressionList->toQuery()); - } - - /** - * @dataProvider provideMultidimensionalData - * @param array $expressions - * @param string $expected - */ - public function testMultidimensional(array $expressions, string $expected) - { - $expressionList = new ExpressionList($expressions); - - $this->assertSame($expected, $expressionList->toQuery()); - } - - public function provideOneDimensionalData(): array - { - return [ - [[Query::literal(12)], "[12]"], - [[Query::literal('12')], "['12']"], - [[Query::literal('12'), Query::literal('13')], "['12', '13']"], - ]; - } - - public function provideMultidimensionalData(): array - { - return [ - [[new ExpressionList([Query::literal(12)])], "[[12]]"], - [[new ExpressionList([Query::literal('12')])], "[['12']]"], - [[new ExpressionList([Query::literal('12'), Query::literal('14')]), new ExpressionList([Query::literal('13')])], "[['12', '14'], ['13']]"], - ]; - } - - public function testRequiresAnyType() - { - $a = new class () {}; - - $this->expectException(TypeError::class); - - new ExpressionList([$a]); - } -} diff --git a/tests/Unit/Functions/AllTest.php b/tests/Unit/Functions/AllTest.php deleted file mode 100644 index 7e78381c..00000000 --- a/tests/Unit/Functions/AllTest.php +++ /dev/null @@ -1,75 +0,0 @@ -getQueryConvertableMock(Variable::class, "variable"); - $list = $this->getQueryConvertableMock(ListType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $all = new All($variable, $list, $predicate); - - $this->assertSame("all(variable IN list WHERE predicate)", $all->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsVariable() - { - $variable = $this->getQueryConvertableMock(AnyType::class, "variable"); - $list = $this->getQueryConvertableMock(ListType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $this->expectException(TypeError::class); - - $all = new All($variable, $list, $predicate); - - $all->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsList() - { - $variable = $this->getQueryConvertableMock(Variable::class, "variable"); - $list = $this->getQueryConvertableMock(AnyType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $this->expectException(TypeError::class); - - $all = new All($variable, $list, $predicate); - - $all->toQuery(); - } -} diff --git a/tests/Unit/Functions/AnyTest.php b/tests/Unit/Functions/AnyTest.php deleted file mode 100644 index 92e6d070..00000000 --- a/tests/Unit/Functions/AnyTest.php +++ /dev/null @@ -1,75 +0,0 @@ -getQueryConvertableMock(Variable::class, "variable"); - $list = $this->getQueryConvertableMock(ListType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $all = new Any($variable, $list, $predicate); - - $this->assertSame("any(variable IN list WHERE predicate)", $all->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsVariable() - { - $variable = $this->getQueryConvertableMock(AnyType::class, "variable"); - $list = $this->getQueryConvertableMock(ListType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $this->expectException(TypeError::class); - - $all = new Any($variable, $list, $predicate); - - $all->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsList() - { - $variable = $this->getQueryConvertableMock(Variable::class, "variable"); - $list = $this->getQueryConvertableMock(AnyType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $this->expectException(TypeError::class); - - $all = new Any($variable, $list, $predicate); - - $all->toQuery(); - } -} diff --git a/tests/Unit/Functions/DateTest.php b/tests/Unit/Functions/DateTest.php deleted file mode 100644 index 729e97fa..00000000 --- a/tests/Unit/Functions/DateTest.php +++ /dev/null @@ -1,64 +0,0 @@ -getQueryConvertableMock(MapType::class, "map"); - - $date = new Date($map); - - $this->assertSame("date(map)", $date->toQuery()); - } - - public function testEmpty() - { - $date = new Date(); - - $this->assertSame("date()", $date->toQuery()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAnyType() - { - $map = $this->getQueryConvertableMock(AnyType::class, "map"); - - $date = new Date($map); - - $date->toQuery(); - } -} diff --git a/tests/Unit/Functions/DateTimeTest.php b/tests/Unit/Functions/DateTimeTest.php deleted file mode 100644 index 6cf38fea..00000000 --- a/tests/Unit/Functions/DateTimeTest.php +++ /dev/null @@ -1,64 +0,0 @@ -getQueryConvertableMock(MapType::class, "map"); - - $date = new DateTime($map); - - $this->assertSame("datetime(map)", $date->toQuery()); - } - - public function testEmpty() - { - $date = new DateTime(); - - $this->assertSame("datetime()", $date->toQuery()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAnyType() - { - $map = $this->getQueryConvertableMock(AnyType::class, "map"); - - $date = new DateTime($map); - - $date->toQuery(); - } -} diff --git a/tests/Unit/Functions/ExistsTest.php b/tests/Unit/Functions/ExistsTest.php deleted file mode 100644 index 3468ab02..00000000 --- a/tests/Unit/Functions/ExistsTest.php +++ /dev/null @@ -1,56 +0,0 @@ -getQueryConvertableMock(AnyType::class, "expression"); - - $exists = new Exists($expression); - - $this->assertSame("exists(expression)", $exists->toQuery()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAnyType() - { - $expression = $this->getQueryConvertableMock(AnyType::class, "expression"); - - $exists = new Exists($expression); - - $exists->toQuery(); - } -} diff --git a/tests/Unit/Functions/FunctionCallTest.php b/tests/Unit/Functions/FunctionCallTest.php deleted file mode 100644 index a6a7afe5..00000000 --- a/tests/Unit/Functions/FunctionCallTest.php +++ /dev/null @@ -1,194 +0,0 @@ -assertInstanceOf(RawFunction::class, $raw); - } - - public function testAll() - { - $variable = $this->getQueryConvertableMock(Variable::class, "a"); - $list = $this->getQueryConvertableMock(ListType::class, "[]"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "b"); - - $all = FunctionCall::all($variable, $list, $predicate); - - $this->assertInstanceOf(All::class, $all); - } - - public function testAny() - { - $variable = $this->getQueryConvertableMock(Variable::class, "a"); - $list = $this->getQueryConvertableMock(ListType::class, "[]"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "b"); - - $any = FunctionCall::any($variable, $list, $predicate); - - $this->assertInstanceOf(Any::class, $any); - } - - public function testExists() - { - $expression = $this->getQueryConvertableMock(AnyType::class, "a"); - - $exists = FunctionCall::exists($expression); - - $this->assertInstanceOf(Exists::class, $exists); - } - - public function testIsEmpty() - { - $list = $this->getQueryConvertableMock(ListType::class, "[]"); - - $isEmpty = FunctionCall::isEmpty($list); - - $this->assertInstanceOf(IsEmpty::class, $isEmpty); - } - - public function testNone() - { - $variable = $this->getQueryConvertableMock(Variable::class, "a"); - $list = $this->getQueryConvertableMock(ListType::class, "[]"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "b"); - - $none = FunctionCall::none($variable, $list, $predicate); - - $this->assertInstanceOf(None::class, $none); - } - - public function testSingle() - { - $variable = $this->getQueryConvertableMock(Variable::class, "a"); - $list = $this->getQueryConvertableMock(ListType::class, "[]"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "b"); - - $single = FunctionCall::single($variable, $list, $predicate); - - $this->assertInstanceOf(Single::class, $single); - } - - public function testPoint() - { - $map = $this->getQueryConvertableMock(MapType::class, "map"); - - $point = FunctionCall::point($map); - - $this->assertInstanceOf(Point::class, $point); - } - - public function testDate() - { - $value = $this->getQueryConvertableMock(AnyType::class, "value"); - - $date = FunctionCall::date($value); - - $this->assertInstanceOf(Date::class, $date); - - $date = FunctionCall::date(); - - $this->assertInstanceOf(Date::class, $date); - } - - public function testDateTime() - { - $value = $this->getQueryConvertableMock(AnyType::class, "value"); - - $date = FunctionCall::datetime($value); - - $this->assertInstanceOf(DateTime::class, $date); - - $date = FunctionCall::datetime(); - - $this->assertInstanceOf(DateTime::class, $date); - } - - public function testLocalDateTime() - { - $value = $this->getQueryConvertableMock(AnyType::class, "value"); - - $date = FunctionCall::localdatetime($value); - - $this->assertInstanceOf(LocalDateTime::class, $date); - - $date = FunctionCall::localdatetime(); - - $this->assertInstanceOf(LocalDateTime::class, $date); - } - - public function testLocalTime() - { - $value = $this->getQueryConvertableMock(AnyType::class, "value"); - - $date = FunctionCall::localtime($value); - - $this->assertInstanceOf(LocalTime::class, $date); - - $date = FunctionCall::localtime(); - - $this->assertInstanceOf(LocalTime::class, $date); - } - - public function testTime() - { - $value = $this->getQueryConvertableMock(AnyType::class, "value"); - - $date = FunctionCall::time($value); - - $this->assertInstanceOf(Time::class, $date); - - $date = FunctionCall::time(); - - $this->assertInstanceOf(Time::class, $date); - } -} diff --git a/tests/Unit/Functions/IsEmptyTest.php b/tests/Unit/Functions/IsEmptyTest.php deleted file mode 100644 index 0516e8c1..00000000 --- a/tests/Unit/Functions/IsEmptyTest.php +++ /dev/null @@ -1,95 +0,0 @@ -getQueryConvertableMock(ListType::class, "list"); - - $isEmpty = new IsEmpty($list); - - $this->assertSame("isEmpty(list)", $isEmpty->toQuery()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsListType() - { - $list = $this->getQueryConvertableMock(ListType::class, "list"); - - $isEmpty = new IsEmpty($list); - - $isEmpty->toQuery(); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsMapType() - { - $list = $this->getQueryConvertableMock(MapType::class, "list"); - - $isEmpty = new IsEmpty($list); - - $isEmpty->toQuery(); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsStringType() - { - $list = $this->getQueryConvertableMock(StringType::class, "list"); - - $isEmpty = new IsEmpty($list); - - $isEmpty->toQuery(); - } - - public function testDoestNotAcceptAnyType() - { - $list = $this->getQueryConvertableMock(AnyType::class, "list"); - - $this->expectException(TypeError::class); - - $isEmpty = new IsEmpty($list); - - $isEmpty->toQuery(); - } -} diff --git a/tests/Unit/Functions/LocalDateTimeTest.php b/tests/Unit/Functions/LocalDateTimeTest.php deleted file mode 100644 index 507b2656..00000000 --- a/tests/Unit/Functions/LocalDateTimeTest.php +++ /dev/null @@ -1,64 +0,0 @@ -getQueryConvertableMock(MapType::class, "map"); - - $date = new LocalDateTime($map); - - $this->assertSame("localdatetime(map)", $date->toQuery()); - } - - public function testEmpty() - { - $date = new LocalDateTime(); - - $this->assertSame("localdatetime()", $date->toQuery()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAnyType() - { - $map = $this->getQueryConvertableMock(AnyType::class, "map"); - - $date = new LocalDateTime($map); - - $date->toQuery(); - } -} diff --git a/tests/Unit/Functions/LocalTimeTest.php b/tests/Unit/Functions/LocalTimeTest.php deleted file mode 100644 index e03bc1ed..00000000 --- a/tests/Unit/Functions/LocalTimeTest.php +++ /dev/null @@ -1,64 +0,0 @@ -getQueryConvertableMock(MapType::class, "map"); - - $date = new LocalTime($map); - - $this->assertSame("localtime(map)", $date->toQuery()); - } - - public function testEmpty() - { - $date = new LocalTime(); - - $this->assertSame("localtime()", $date->toQuery()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAnyType() - { - $map = $this->getQueryConvertableMock(AnyType::class, "map"); - - $date = new LocalTime($map); - - $date->toQuery(); - } -} diff --git a/tests/Unit/Functions/NoneTest.php b/tests/Unit/Functions/NoneTest.php deleted file mode 100644 index 3b292e98..00000000 --- a/tests/Unit/Functions/NoneTest.php +++ /dev/null @@ -1,75 +0,0 @@ -getQueryConvertableMock(Variable::class, "variable"); - $list = $this->getQueryConvertableMock(ListType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $all = new None($variable, $list, $predicate); - - $this->assertSame("none(variable IN list WHERE predicate)", $all->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsVariable() - { - $variable = $this->getQueryConvertableMock(AnyType::class, "variable"); - $list = $this->getQueryConvertableMock(ListType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $this->expectException(TypeError::class); - - $all = new None($variable, $list, $predicate); - - $all->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsList() - { - $variable = $this->getQueryConvertableMock(Variable::class, "variable"); - $list = $this->getQueryConvertableMock(AnyType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $this->expectException(TypeError::class); - - $all = new None($variable, $list, $predicate); - - $all->toQuery(); - } -} diff --git a/tests/Unit/Functions/PointTest.php b/tests/Unit/Functions/PointTest.php deleted file mode 100644 index 7ad7976d..00000000 --- a/tests/Unit/Functions/PointTest.php +++ /dev/null @@ -1,57 +0,0 @@ -getQueryConvertableMock(MapType::class, "{latitude: toInteger('1'), longitude: toInteger('1')}"); - - $point = new Point($map); - - $this->assertSame("point({latitude: toInteger('1'), longitude: toInteger('1')})", $point->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsVariable() - { - $map = $this->getQueryConvertableMock(AnyType::class, "map"); - - $this->expectException(TypeError::class); - - $point = new Point($map); - - $point->toQuery(); - } -} diff --git a/tests/Unit/Functions/RawFunctionTest.php b/tests/Unit/Functions/RawFunctionTest.php deleted file mode 100644 index 2b736adb..00000000 --- a/tests/Unit/Functions/RawFunctionTest.php +++ /dev/null @@ -1,56 +0,0 @@ -getQueryConvertableMock(AnyType::class, "a"); - $b = $this->getQueryConvertableMock(AnyType::class, "b"); - $c = $this->getQueryConvertableMock(AnyType::class, "c"); - - $raw = new RawFunction("foobar", [$a, $b, $c]); - - $this->assertSame("foobar(a, b, c)", $raw->toQuery()); - } - - public function testRequiresAnyTypeParameters() - { - $a = new class () {}; - - $this->expectException(TypeError::class); - - new RawFunction('foobar', [$a]); - } -} diff --git a/tests/Unit/Functions/SingleTest.php b/tests/Unit/Functions/SingleTest.php deleted file mode 100644 index b57112ca..00000000 --- a/tests/Unit/Functions/SingleTest.php +++ /dev/null @@ -1,75 +0,0 @@ -getQueryConvertableMock(Variable::class, "variable"); - $list = $this->getQueryConvertableMock(ListType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $all = new Single($variable, $list, $predicate); - - $this->assertSame("single(variable IN list WHERE predicate)", $all->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsVariable() - { - $variable = $this->getQueryConvertableMock(AnyType::class, "variable"); - $list = $this->getQueryConvertableMock(ListType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $this->expectException(TypeError::class); - - $all = new Single($variable, $list, $predicate); - - $all->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsList() - { - $variable = $this->getQueryConvertableMock(Variable::class, "variable"); - $list = $this->getQueryConvertableMock(AnyType::class, "list"); - $predicate = $this->getQueryConvertableMock(AnyType::class, "predicate"); - - $this->expectException(TypeError::class); - - $all = new Single($variable, $list, $predicate); - - $all->toQuery(); - } -} diff --git a/tests/Unit/Functions/TimeTest.php b/tests/Unit/Functions/TimeTest.php deleted file mode 100644 index 82ab9bd9..00000000 --- a/tests/Unit/Functions/TimeTest.php +++ /dev/null @@ -1,64 +0,0 @@ -getQueryConvertableMock(MapType::class, "map"); - - $date = new Time($map); - - $this->assertSame("time(map)", $date->toQuery()); - } - - public function testEmpty() - { - $date = new Time(); - - $this->assertSame("time()", $date->toQuery()); - } - - /** - * @doesNotPerformAssertions - */ - public function testAcceptsAnyType() - { - $map = $this->getQueryConvertableMock(AnyType::class, "map"); - - $date = new Time($map); - - $date->toQuery(); - } -} diff --git a/tests/Unit/GreaterThanOrEqualTest.php b/tests/Unit/GreaterThanOrEqualTest.php deleted file mode 100644 index e08afda0..00000000 --- a/tests/Unit/GreaterThanOrEqualTest.php +++ /dev/null @@ -1,70 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->assertSame("(10 >= 15)", $greaterThanOrEqual->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $greaterThanOrEqual = new GreaterThanOrEqual($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15"), false); - - $this->assertSame("10 >= 15", $greaterThanOrEqual->toQuery()); - } - - public function testCannotBeNested(): void - { - $greaterThanOrEqual = new GreaterThanOrEqual($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->expectException(TypeError::class); - - $greaterThanOrEqual = new GreaterThanOrEqual($greaterThanOrEqual, $greaterThanOrEqual); - - $greaterThanOrEqual->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $greaterThanOrEqual = new GreaterThanOrEqual($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $greaterThanOrEqual->toQuery(); - } -} diff --git a/tests/Unit/GreaterThanTest.php b/tests/Unit/GreaterThanTest.php deleted file mode 100644 index 97fd00a7..00000000 --- a/tests/Unit/GreaterThanTest.php +++ /dev/null @@ -1,70 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->assertSame("(10 > 15)", $greaterThan->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $greaterThan = new GreaterThan($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15"), false); - - $this->assertSame("10 > 15", $greaterThan->toQuery()); - } - - public function testCannotBeNested(): void - { - $greaterThan = new GreaterThan($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->expectException(TypeError::class); - - $greaterThan = new GreaterThan($greaterThan, $greaterThan); - - $greaterThan->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $greaterThan = new GreaterThan($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $greaterThan->toQuery(); - } -} diff --git a/tests/Unit/InTest.php b/tests/Unit/InTest.php deleted file mode 100644 index 676d75b6..00000000 --- a/tests/Unit/InTest.php +++ /dev/null @@ -1,81 +0,0 @@ -getQueryConvertableMock(PropertyType::class, "a"), $this->getQueryConvertableMock(ListType::class, "b")); - - $this->assertSame("(a IN b)", $inequality->toQuery()); - - $inequality = new In($inequality, $this->getQueryConvertableMock(ListType::class, "c")); - - $this->assertSame("((a IN b) IN c)", $inequality->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $inequality = new In($this->getQueryConvertableMock(PropertyType::class, "a"), $this->getQueryConvertableMock(ListType::class, "b"), false); - - $this->assertSame("a IN b", $inequality->toQuery()); - - $inequality = new In($inequality, $this->getQueryConvertableMock(ListType::class, "c")); - - $this->assertSame("(a IN b IN c)", $inequality->toQuery()); - } - - public function testInExpressionList(): void - { - $inequality = new In($this->getQueryConvertableMock(PropertyType::class, "a"), new ExpressionList([new StringLiteral('a'), new StringLiteral('b')])); - - $this->assertSame("(a IN ['a', 'b'])", $inequality->toQuery()); - - $inequality = new In($inequality, $this->getQueryConvertableMock(ListType::class, "c")); - - $this->assertSame("((a IN ['a', 'b']) IN c)", $inequality->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $inequality = new In($this->getQueryConvertableMock(AnyType::class, "a"), $this->getQueryConvertableMock(AnyType::class, "b")); - - $inequality->toQuery(); - } -} diff --git a/tests/Unit/InequalityTest.php b/tests/Unit/InequalityTest.php deleted file mode 100644 index 84228a4a..00000000 --- a/tests/Unit/InequalityTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(PropertyType::class, "a"), $this->getQueryConvertableMock(PropertyType::class, "b")); - - $this->assertSame("(a <> b)", $inequality->toQuery()); - - $inequality = new Inequality($inequality, $inequality); - - $this->assertSame("((a <> b) <> (a <> b))", $inequality->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $inequality = new Inequality($this->getQueryConvertableMock(PropertyType::class, "a"), $this->getQueryConvertableMock(PropertyType::class, "b"), false); - - $this->assertSame("a <> b", $inequality->toQuery()); - - $inequality = new Inequality($inequality, $inequality); - - $this->assertSame("(a <> b <> a <> b)", $inequality->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $inequality = new Inequality($this->getQueryConvertableMock(AnyType::class, "a"), $this->getQueryConvertableMock(AnyType::class, "b")); - - $inequality->toQuery(); - } -} diff --git a/tests/Unit/IsNotNullTest.php b/tests/Unit/IsNotNullTest.php deleted file mode 100644 index bccf8776..00000000 --- a/tests/Unit/IsNotNullTest.php +++ /dev/null @@ -1,49 +0,0 @@ -getQueryConvertableMock(BooleanType::class, "true"), false); - - $this->assertFalse($not->insertsParentheses()); - - $this->assertSame("true IS NOT NULL", $not->toQuery()); - - $not = new IsNotNull($not); - - $this->assertSame("(true IS NOT NULL IS NOT NULL)", $not->toQuery()); - - $this->assertTrue($not->insertsParentheses()); - } -} diff --git a/tests/Unit/IsNullTest.php b/tests/Unit/IsNullTest.php deleted file mode 100644 index 1c812cd8..00000000 --- a/tests/Unit/IsNullTest.php +++ /dev/null @@ -1,49 +0,0 @@ -getQueryConvertableMock(BooleanType::class, "true"), false); - - $this->assertFalse($not->insertsParentheses()); - - $this->assertSame("true IS NULL", $not->toQuery()); - - $not = new IsNull($not); - - $this->assertSame("(true IS NULL IS NULL)", $not->toQuery()); - - $this->assertTrue($not->insertsParentheses()); - } -} diff --git a/tests/Unit/LabelTest.php b/tests/Unit/LabelTest.php deleted file mode 100644 index ab0fcc58..00000000 --- a/tests/Unit/LabelTest.php +++ /dev/null @@ -1,64 +0,0 @@ -getQueryConvertableMock(Variable::class, "foo"); - $label = ["Bar"]; - - $label = new Label($expression, $label); - - $this->assertSame("foo:Bar", $label->toQuery()); - } - - public function testMultiple() - { - $expression = $this->getQueryConvertableMock(Variable::class, "foo"); - $label = ["Bar", "Baz"]; - - $label = new Label($expression, $label); - - $this->assertSame("foo:Bar:Baz", $label->toQuery()); - } - - public function testLabelIsEscaped() - { - $expression = $this->getQueryConvertableMock(Variable::class, "foo"); - $label = ["{}"]; - - $label = new Label($expression, $label); - - $this->assertSame("foo:`{}`", $label->toQuery()); - } -} diff --git a/tests/Unit/LessThanOrEqualTest.php b/tests/Unit/LessThanOrEqualTest.php deleted file mode 100644 index 986f222e..00000000 --- a/tests/Unit/LessThanOrEqualTest.php +++ /dev/null @@ -1,71 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->assertSame("(10 <= 15)", $lessThanOrEqual->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $lessThanOrEqual = new LessThanOrEqual($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15"), false); - - $this->assertSame("10 <= 15", $lessThanOrEqual->toQuery()); - } - - public function testCannotBeNested(): void - { - $lessThanOrEqual = new LessThanOrEqual($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->expectException(TypeError::class); - - $lessThanOrEqual = new GreaterThanOrEqual($lessThanOrEqual, $lessThanOrEqual); - - $this->assertSame("((10 <= 15) <= (10 <= 15))", $lessThanOrEqual->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $lessThanOrEqual = new LessThanOrEqual($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $lessThanOrEqual->toQuery(); - } -} diff --git a/tests/Unit/LessThanTest.php b/tests/Unit/LessThanTest.php deleted file mode 100644 index c2ce8c1d..00000000 --- a/tests/Unit/LessThanTest.php +++ /dev/null @@ -1,70 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->assertSame("(10 < 15)", $lessThan->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $lessThan = new LessThan($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15"), false); - - $this->assertSame("10 < 15", $lessThan->toQuery()); - } - - public function testCannotBeNested(): void - { - $lessThan = new LessThan($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->expectException(TypeError::class); - - $lessThan = new LessThan($lessThan, $lessThan); - - $this->assertSame("((10 < 15) < (10 < 15))", $lessThan->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $lessThan = new LessThan($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $lessThan->toQuery(); - } -} diff --git a/tests/Unit/Literals/BooleanTest.php b/tests/Unit/Literals/BooleanTest.php deleted file mode 100644 index 318e4e10..00000000 --- a/tests/Unit/Literals/BooleanTest.php +++ /dev/null @@ -1,53 +0,0 @@ -assertSame("true", $boolean->toQuery()); - $this->assertTrue($boolean->getValue()); - } - - public function testFalse(): void - { - $boolean = new Boolean(false); - - $this->assertSame("false", $boolean->toQuery()); - $this->assertFalse($boolean->getValue()); - } - - public function testInstanceOfBooleanType(): void - { - $this->assertInstanceOf(BooleanType::class, new Boolean(false)); - } -} diff --git a/tests/Unit/Literals/DecimalTest.php b/tests/Unit/Literals/DecimalTest.php deleted file mode 100644 index 490545c2..00000000 --- a/tests/Unit/Literals/DecimalTest.php +++ /dev/null @@ -1,69 +0,0 @@ -assertSame("0", $decimal->toQuery()); - $this->assertEquals('0', $decimal->getValue()); - } - - public function testInstanceOfNumeralType(): void - { - $this->assertInstanceOf(NumeralType::class, new Decimal(0)); - } - - /** - * @dataProvider provideToQueryData - * @param $number - * @param string $expected - */ - public function testToQuery($number, string $expected): void - { - $decimal = new Decimal($number); - - $this->assertSame($expected, $decimal->toQuery()); - $this->assertEquals($expected, $decimal->getValue()); - } - - public function provideToQueryData(): array - { - return [ - [1, "1"], - [2, "2"], - [3.14, "3.14"], - [-12, "-12"], - [69, "69"], - ]; - } -} diff --git a/tests/Unit/Literals/LiteralTest.php b/tests/Unit/Literals/LiteralTest.php deleted file mode 100644 index 1206e5a6..00000000 --- a/tests/Unit/Literals/LiteralTest.php +++ /dev/null @@ -1,894 +0,0 @@ -assertInstanceOf(StringLiteral::class, $string); - } - - public function testLiteralBoolean(): void - { - $boolean = Literal::literal(true); - - $this->assertInstanceOf(Boolean::class, $boolean); - } - - public function testLiteralInteger(): void - { - $integer = Literal::literal(1); - - $this->assertInstanceOf(Decimal::class, $integer); - } - - public function testLiteralFloat(): void - { - $float = Literal::literal(1.0); - - $this->assertInstanceOf(Decimal::class, $float); - } - - public function testStringable(): void - { - $stringable = Literal::literal(new class () { - public function __toString() - { - return 'Testing is a virtue!'; - } - }); - - $this->assertInstanceOf(StringLiteral::class, $stringable); - } - - public function testBoolean(): void - { - $boolean = Literal::boolean(true); - - $this->assertInstanceOf(Boolean::class, $boolean); - - $boolean = Literal::boolean(false); - - $this->assertInstanceOf(Boolean::class, $boolean); - } - - public function testString(): void - { - $string = Literal::string('Testing is a virtue!'); - - $this->assertInstanceOf(StringLiteral::class, $string); - } - - public function testDecimal(): void - { - $decimal = Literal::decimal(1); - - $this->assertInstanceOf(Decimal::class, $decimal); - - $decimal = Literal::decimal(1.0); - - $this->assertInstanceOf(Decimal::class, $decimal); - } - - public function testPoint2d(): void - { - $point = Literal::point2d(1, 2); - - $this->assertEquals(new Point(new PropertyMap(["x" => new Decimal(1), "y" => new Decimal(2), "crs" => new StringLiteral("cartesian")])), $point); - - $point = Literal::point2d( - new Decimal(1), - new Decimal(2) - ); - - $this->assertEquals(new Point(new PropertyMap(["x" => new Decimal(1), "y" => new Decimal(2), "crs" => new StringLiteral("cartesian")])), $point); - } - - public function testPoint3d(): void - { - $point = Literal::point3d(1, 2, 3); - - $this->assertEquals(new Point(new PropertyMap(["x" => new Decimal(1), "y" => new Decimal(2), "z" => new Decimal(3), "crs" => new StringLiteral("cartesian-3D")])), $point); - - $point = Literal::point3d( - new Decimal(1), - new Decimal(2), - new Decimal(3) - ); - - $this->assertEquals(new Point(new PropertyMap(["x" => new Decimal(1), "y" => new Decimal(2), "z" => new Decimal(3), "crs" => new StringLiteral("cartesian-3D")])), $point); - } - - public function testPoint2dWGS84(): void - { - $point = Literal::point2dWGS84(1, 2); - - $this->assertEquals(new Point(new PropertyMap(["longitude" => new Decimal(1), "latitude" => new Decimal(2), "crs" => new StringLiteral("WGS-84")])), $point); - - $point = Literal::point2dWGS84( - new Decimal(1), - new Decimal(2) - ); - - $this->assertEquals(new Point(new PropertyMap(["longitude" => new Decimal(1), "latitude" => new Decimal(2), "crs" => new StringLiteral("WGS-84")])), $point); - } - - public function testPoint3dWGS84(): void - { - $point = Literal::point3dWGS84(1, 2, 3); - - $this->assertEquals(new Point(new PropertyMap(["longitude" => new Decimal(1), "latitude" => new Decimal(2), "height" => new Decimal(3), "crs" => new StringLiteral("WGS-84-3D")])), $point); - - $point = Literal::point3dWGS84( - new Decimal(1), - new Decimal(2), - new Decimal(3) - ); - - $this->assertEquals(new Point(new PropertyMap(["longitude" => new Decimal(1), "latitude" => new Decimal(2), "height" => new Decimal(3), "crs" => new StringLiteral("WGS-84-3D")])), $point); - } - - public function testDate(): void - { - $date = Literal::date(); - - $this->assertEquals(new Date(), $date); - } - - public function testDateTimezone(): void - { - $date = Literal::date("Europe/Amsterdam"); - - $this->assertEquals(new Date(new PropertyMap(["timezone" => new StringLiteral("Europe/Amsterdam")])), $date); - - $date = Literal::date(new StringLiteral("Europe/Amsterdam")); - - $this->assertEquals(new Date(new PropertyMap(["timezone" => new StringLiteral("Europe/Amsterdam")])), $date); - } - - /** - * @dataProvider provideDateYMDData - * @param $year - * @param $month - * @param $day - * @param $expected - */ - public function testDateYMD($year, $month, $day, $expected): void - { - $date = Literal::dateYMD($year, $month, $day); - - $this->assertEquals($expected, $date); - } - - public function testDateYMDMissingMonth(): void - { - $this->expectException(LogicException::class); - - $date = Literal::dateYMD(2000, null, 17); - - $date->toQuery(); - } - - /** - * @dataProvider provideDateYWDData - * @param $year - * @param $week - * @param $weekday - * @param $expected - */ - public function testDateYWD($year, $week, $weekday, $expected): void - { - $date = Literal::dateYWD($year, $week, $weekday); - - $this->assertEquals($expected, $date); - } - - public function testDateYWDMissingWeek(): void - { - $this->expectException(LogicException::class); - - $date = Literal::dateYWD(2000, null, 17); - - $date->toQuery(); - } - - public function testDateString(): void - { - $date = Literal::dateString('2000-17-12'); - - $this->assertEquals(new Date(new StringLiteral('2000-17-12')), $date); - } - - public function testDateTimeWithoutTimeZone(): void - { - $datetime = Literal::dateTime(); - - $this->assertEquals(new DateTime(), $datetime); - } - - public function testDateTimeWithTimeZone(): void - { - $datetime = Literal::dateTime("America/Los Angeles"); - - $this->assertEquals(new DateTime(new PropertyMap(["timezone" => new StringLiteral("America/Los Angeles")])), $datetime); - } - - /** - * @dataProvider provideDatetimeYMDData - * @param $year - * @param $month - * @param $day - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $timezone - * @param $expected - */ - public function testDatetimeYMD($year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected): void - { - $datetime = Literal::dateTimeYMD($year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone); - - $this->assertEquals($expected, $datetime); - } - - public function testDatetimeYMDMissingMonth(): void - { - $this->expectException(LogicException::class); - - $datetime = Literal::dateTimeYMD(2000, null, 17); - - $datetime->toQuery(); - } - - /** - * @dataProvider provideDatetimeYWDData - * @param $year - * @param $week - * @param $dayOfWeek - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $timezone - * @param $expected - */ - public function testDatetimeYWD($year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected): void - { - $datetime = Literal::datetimeYWD($year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone); - - $this->assertEquals($expected, $datetime); - } - - public function testDatetimeYWDMissingWeek(): void - { - $this->expectException(LogicException::class); - - $datetime = Literal::dateTimeYWD(2000, null, 17); - - $datetime->toQuery(); - } - - /** - * @dataProvider provideDatetimeYQDData - * @param $year - * @param $quarter - * @param $dayOfQuarter - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $timezone - * @param $expected - */ - public function testDatetimeYQD($year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected): void - { - $datetime = Literal::datetimeYQD($year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone); - - $this->assertEquals($expected, $datetime); - } - - public function testDatetimeYQDMissingQuarter(): void - { - $this->expectException(LogicException::class); - - $datetime = Literal::dateTimeYQD(2000, null, 17); - - $datetime->toQuery(); - } - - /** - * @dataProvider provideDatetimeYQData - * @param $year - * @param $ordinalDay - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $timezone - * @param $expected - */ - public function testDatetimeYD($year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected): void - { - $datetime = Literal::datetimeYD($year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone); - - $this->assertEquals($expected, $datetime); - } - - public function testDatetimeYDMissingOrdinalDay(): void - { - $this->expectException(LogicException::class); - - $datetime = Literal::dateTimeYD(2000, null, 17); - - $datetime->toQuery(); - } - - public function testDatetimeString(): void - { - $datetime = Literal::datetimeString("2015-07-21T21:40:32.142+01:00"); - - $this->assertEquals(new DateTime(new StringLiteral("2015-07-21T21:40:32.142+01:00")), $datetime); - } - - public function testLocalDateTimeWithoutTimezone(): void - { - $localDateTime = Literal::localDatetime(); - - $this->assertEquals(new LocalDateTime(), $localDateTime); - } - - public function testLocalDateTimeWithTimezone(): void - { - $localDateTime = Literal::localDatetime("America/Los Angeles"); - - $this->assertEquals(new LocalDateTime(new PropertyMap(["timezone" => new StringLiteral("America/Los Angeles")])), $localDateTime); - } - - /** - * @dataProvider provideLocalDatetimeYMDData - * @param $year - * @param $month - * @param $day - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $expected - */ - public function testLocalDateTimeYMD($year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void - { - $localDatetime = Literal::localDatetimeYMD($year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond); - - $this->assertEquals($expected, $localDatetime); - } - - public function testLocalDateTimeYMDMissingMonth(): void - { - $this->expectException(LogicException::class); - - $datetime = Literal::localDatetimeYMD(2000, null, 17); - - $datetime->toQuery(); - } - - /** - * @dataProvider provideLocalDatetimeYWDData - * @param $year - * @param $week - * @param $dayOfWeek - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $expected - */ - public function testLocalDateTimeYWD($year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void - { - $localDatetime = Literal::localDatetimeYWD($year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond); - - $this->assertEquals($expected, $localDatetime); - } - - public function testLocalDateTimeYWDMissingWeek(): void - { - $this->expectException(LogicException::class); - - $datetime = Literal::localDatetimeYWD(2000, null, 17); - - $datetime->toQuery(); - } - - /** - * @dataProvider provideLocalDatetimeYQDData - * @param $year - * @param $quarter - * @param $dayOfQuarter - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $expected - */ - public function testLocalDatetimeYQD($year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void - { - $localDatetime = Literal::localDatetimeYQD($year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond); - - $this->assertEquals($expected, $localDatetime); - } - - public function testLocalDateTimeYQDMissingQuarter(): void - { - $this->expectException(LogicException::class); - - $datetime = Literal::localDatetimeYQD(2000, null, 17); - - $datetime->toQuery(); - } - - /** - * @dataProvider provideLocalDatetimeYQData - * @param $year - * @param $ordinalDay - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $expected - */ - public function testLocalDatetimeYD($year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void - { - $localDatetime = Literal::localDatetimeYD($year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond); - - $this->assertEquals($expected, $localDatetime); - } - - public function testLocalDateTimeYDMissingOrdinalDay(): void - { - $this->expectException(LogicException::class); - - $datetime = Literal::localDatetimeYD(2000, null, 17); - - $datetime->toQuery(); - } - - public function testLocalDatetimeString(): void - { - $localDatetime = Literal::localDatetimeString("2015-W30-2T214032.142"); - - $this->assertEquals(new LocalDateTime(new StringLiteral("2015-W30-2T214032.142")), $localDatetime); - } - - public function testLocalTimeCurrentWithoutTimezone(): void - { - $localTime = Literal::localTimeCurrent(); - $this->assertEquals(new LocalTime(), $localTime); - } - - public function testLocalTimeCurrentWithTimezone(): void - { - $localTime = Literal::localTimeCurrent("America/Los Angeles"); - $this->assertEquals(new LocalTime(new PropertyMap(["timezone" => new StringLiteral("America/Los Angeles")])), $localTime); - } - - /** - * @dataProvider provideLocalTimeData - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $expected - */ - public function testLocalTime($hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void - { - $localTime = Literal::localTime($hour, $minute, $second, $millisecond, $microsecond, $nanosecond); - $this->assertEquals($localTime, $expected); - } - - public function testLocalTimeMissingMinute(): void - { - $this->expectException(LogicException::class); - - $localTime = Literal::localTime(9, null, 17); - - $localTime->toQuery(); - } - - public function testLocalTimeString(): void - { - $localTime = Literal::localTimeString("21:40:32.142"); - $this->assertEquals(new LocalTime(new StringLiteral("21:40:32.142")), $localTime); - } - - public function testTimeCurrentWithoutTimezone(): void - { - $time = Literal::time(); - $this->assertEquals($time, new Time()); - } - - public function testTimeCurrentWithTimezone(): void - { - $time = Literal::time("America/Los Angeles"); - $this->assertEquals($time, new Time(new PropertyMap(["timezone" => new StringLiteral("America/Los Angeles")]))); - } - - /** - * @dataProvider provideTimeData - * @param $hour - * @param $minute - * @param $second - * @param $millisecond - * @param $microsecond - * @param $nanosecond - * @param $expected - */ - public function testTime($hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void - { - $time = Literal::timeHMS($hour, $minute, $second, $millisecond, $microsecond, $nanosecond); - $this->assertEquals($time, $expected); - } - - public function testTimeMissingMinute(): void - { - $this->expectException(LogicException::class); - - $time = Literal::timeHMS(9, null, 17); - - $time->toQuery(); - } - - public function testTimeString(): void - { - $time = Literal::timeString("21:40:32.142+0100"); - $this->assertEquals($time, new Time(new StringLiteral("21:40:32.142+0100"))); - } - - public function provideDateYMDData(): array - { - return [ - [2000, null, null, new Date(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 12, null, new Date(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12)]))], - [2000, 12, 17, new Date(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(17)]))], - [new Decimal(2000), null, null, new Date(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(12), null, new Date(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12)]))], - [new Decimal(2000), new Decimal(12), new Decimal(17), new Date(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(17)]))], - - ]; - } - - public function provideDateYWDData(): array - { - return [ - [2000, null, null, new Date(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 12, null, new Date(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(12)]))], - [2000, 12, 17, new Date(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(12), "dayOfWeek" => new Decimal(17)]))], - [new Decimal(2000), null, null, new Date(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(12), null, new Date(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(12)]))], - [new Decimal(2000), new Decimal(12), new Decimal(17), new Date(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(12), "dayOfWeek" => new Decimal(17)]))], - - ]; - } - - public function provideDatetimeYMDData(): array - { - // [$year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected] - return [ - [2000, null, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 12, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12)]))], - [2000, 12, 15, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15)]))], - [2000, 12, 15, 8, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8)]))], - [2000, 12, 15, 8, 25, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [2000, 12, 15, 8, 25, 44, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [2000, 12, 15, 8, 25, 44, 18, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [2000, 12, 15, 8, 25, 44, 18, 6, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [2000, 12, 15, 8, 25, 44, 18, 6, 31, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - [2000, 12, 15, 8, 25, 44, 18, 6, 31, "America/Los Angeles", new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31), "timezone" => new StringLiteral("America/Los Angeles")]))], - - // types - [new Decimal(2000), null, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(12), null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), new Decimal(44), null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), new StringLiteral("America/Los Angeles"), new DateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31), "timezone" => new StringLiteral("America/Los Angeles")]))], - ]; - } - - public function provideDatetimeYWDData(): array - { - // [$year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected] - return [ - [2000, null, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 9, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9)]))], - [2000, 9, 4, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4)]))], - [2000, 9, 4, 8, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8)]))], - [2000, 9, 4, 8, 25, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [2000, 9, 4, 8, 25, 44, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [2000, 9, 4, 8, 25, 44, 18, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [2000, 9, 4, 8, 25, 44, 18, 6, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [2000, 9, 4, 8, 25, 44, 18, 6, 31, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - [2000, 9, 4, 8, 25, 44, 18, 6, 31, "America/Los Angeles", new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31), "timezone" => new StringLiteral("America/Los Angeles")]))], - - // types - [new Decimal(2000), null, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(9), null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), new StringLiteral("America/Los Angeles"), new DateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31), "timezone" => new StringLiteral("America/Los Angeles")]))], - ]; - } - - public function provideDatetimeYQDData(): array - { - // [$year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected] - return [ - [2000, null, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 3, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3)]))], - [2000, 3, 4, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4)]))], - [2000, 3, 4, 8, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8)]))], - [2000, 3, 4, 8, 25, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [2000, 3, 4, 8, 25, 44, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [2000, 3, 4, 8, 25, 44, 18, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [2000, 3, 4, 8, 25, 44, 18, 6, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [2000, 3, 4, 8, 25, 44, 18, 6, 31, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - [2000, 3, 4, 8, 25, 44, 18, 6, 31, "America/Los Angeles", new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31), "timezone" => new StringLiteral("America/Los Angeles")]))], - - // types - [new Decimal(2000), null, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(3), null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), new StringLiteral("America/Los Angeles"), new DateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31), "timezone" => new StringLiteral("America/Los Angeles")]))], - ]; - } - - public function provideDatetimeYQData(): array - { - // [$year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected] - return [ - [2000, null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 3, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3)]))], - [2000, 3, 8, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8)]))], - [2000, 3, 8, 25, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [2000, 3, 8, 25, 44, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [2000, 3, 8, 25, 44, 18, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [2000, 3, 8, 25, 44, 18, 6, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [2000, 3, 8, 25, 44, 18, 6, 31, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - [2000, 3, 8, 25, 44, 18, 6, 31, "America/Los Angeles", new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31), "timezone" => new StringLiteral("America/Los Angeles")]))], - - // types - [new Decimal(2000), null, null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(3), null, null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), null, null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), null, null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), new Decimal(44), null, null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), null, null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), null, null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), null, new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), new StringLiteral("America/Los Angeles"), new DateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31), "timezone" => new StringLiteral("America/Los Angeles")]))], - ]; - } - - public function provideLocalDatetimeYMDData(): array - { - // [$year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] - return [ - [2000, null, null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 12, null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12)]))], - [2000, 12, 15, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15)]))], - [2000, 12, 15, 8, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8)]))], - [2000, 12, 15, 8, 25, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [2000, 12, 15, 8, 25, 44, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [2000, 12, 15, 8, 25, 44, 18, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [2000, 12, 15, 8, 25, 44, 18, 6, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [2000, 12, 15, 8, 25, 44, 18, 6, 31, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - - // types - [new Decimal(2000), null, null, null, null, null, null, null, null,new LocalDateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(12), null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), new Decimal(44), null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [new Decimal(2000), new Decimal(12), new Decimal(15), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "month" => new Decimal(12), "day" => new Decimal(15), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - ]; - } - - public function provideLocalDatetimeYWDData(): array - { - // [$year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] - return [ - [2000, null, null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 9, null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9)]))], - [2000, 9, 4, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4)]))], - [2000, 9, 4, 8, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8)]))], - [2000, 9, 4, 8, 25, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [2000, 9, 4, 8, 25, 44, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [2000, 9, 4, 8, 25, 44, 18, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [2000, 9, 4, 8, 25, 44, 18, 6, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [2000, 9, 4, 8, 25, 44, 18, 6, 31, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - - // types - [new Decimal(2000), null, null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(9), null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [new Decimal(2000), new Decimal(9), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "week" => new Decimal(9), "dayOfWeek" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - ]; - } - - - public function provideLocalDatetimeYQDData(): array - { - // [$year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] - return [ - [2000, null, null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 3, null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3)]))], - [2000, 3, 4, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4)]))], - [2000, 3, 4, 8, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8)]))], - [2000, 3, 4, 8, 25, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [2000, 3, 4, 8, 25, 44, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [2000, 3, 4, 8, 25, 44, 18, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [2000, 3, 4, 8, 25, 44, 18, 6, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [2000, 3, 4, 8, 25, 44, 18, 6, 31, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - - // types - [new Decimal(2000), null, null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(3), null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [new Decimal(2000), new Decimal(3), new Decimal(4), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "quarter" => new Decimal(3), "dayOfQuarter" => new Decimal(4), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - ]; - } - - public function provideLocalDatetimeYQData(): array - { - // [$year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] - return [ - [2000, null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [2000, 3, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3)]))], - [2000, 3, 8, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8)]))], - [2000, 3, 8, 25, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [2000, 3, 8, 25, 44, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [2000, 3, 8, 25, 44, 18, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [2000, 3, 8, 25, 44, 18, 6, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [2000, 3, 8, 25, 44, 18, 6, 31, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - - // types - [new Decimal(2000), null, null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000)]))], - [new Decimal(2000), new Decimal(3), null, null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), null, null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), null, null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), new Decimal(44), null, null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), null, null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), null, new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6)]))], - [new Decimal(2000), new Decimal(3), new Decimal(8), new Decimal(25), new Decimal(44), new Decimal(18), new Decimal(6), new Decimal(31), new LocalDateTime(new PropertyMap(["year" => new Decimal(2000), "ordinalDay" => new Decimal(3), "hour" => new Decimal(8), "minute" => new Decimal(25), "second" => new Decimal(44), "millisecond" => new Decimal(18), "microsecond" => new Decimal(6), "nanosecond" => new Decimal(31)]))], - ]; - } - - public function provideLocalTimeData(): array - { - // [$hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] - return [ - [11, null, null, null, null, null, new LocalTime(new PropertyMap(["hour" => new Decimal(11)]))], - [11, 23, null, null, null, null, new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23)]))], - [11, 23, 2, null, null, null, new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2)]))], - [11, 23, 2, 54, null, null, new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54)]))], - [11, 23, 2, 54, 8, null, new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54), "microsecond" => new Decimal(8)]))], - [11, 23, 2, 54, 8, 29, new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54), "microsecond" => new Decimal(8), "nanosecond" => new Decimal(29)]))], - - // types - [new Decimal(11), null, null, null, null, null, new LocalTime(new PropertyMap(["hour" => new Decimal(11)]))], - [new Decimal(11), new Decimal(23), null, null, null, null, new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23)]))], - [new Decimal(11), new Decimal(23), new Decimal(2), null, null, null, new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2)]))], - [new Decimal(11), new Decimal(23), new Decimal(2), new Decimal(54), null, null, new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54)]))], - [new Decimal(11), new Decimal(23), new Decimal(2), new Decimal(54), new Decimal(8), null, new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54), "microsecond" => new Decimal(8)]))], - [new Decimal(11), new Decimal(23), new Decimal(2), new Decimal(54), new Decimal(8), new Decimal(29), new LocalTime(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54), "microsecond" => new Decimal(8), "nanosecond" => new Decimal(29)]))], - ]; - } - - public function provideTimeData(): array - { - // [$hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] - return [ - [11, null, null, null, null, null, new Time(new PropertyMap(["hour" => new Decimal(11)]))], - [11, 23, null, null, null, null, new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23)]))], - [11, 23, 2, null, null, null, new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2)]))], - [11, 23, 2, 54, null, null, new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54)]))], - [11, 23, 2, 54, 8, null, new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54), "microsecond" => new Decimal(8)]))], - [11, 23, 2, 54, 8, 29, new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54), "microsecond" => new Decimal(8), "nanosecond" => new Decimal(29)]))], - - // types - [new Decimal(11), null, null, null, null, null, new Time(new PropertyMap(["hour" => new Decimal(11)]))], - [new Decimal(11), new Decimal(23), null, null, null, null, new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23)]))], - [new Decimal(11), new Decimal(23), new Decimal(2), null, null, null, new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2)]))], - [new Decimal(11), new Decimal(23), new Decimal(2), new Decimal(54), null, null, new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54)]))], - [new Decimal(11), new Decimal(23), new Decimal(2), new Decimal(54), new Decimal(8), null, new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54), "microsecond" => new Decimal(8)]))], - [new Decimal(11), new Decimal(23), new Decimal(2), new Decimal(54), new Decimal(8), new Decimal(29), new Time(new PropertyMap(["hour" => new Decimal(11), "minute" => new Decimal(23), "second" => new Decimal(2), "millisecond" => new Decimal(54), "microsecond" => new Decimal(8), "nanosecond" => new Decimal(29)]))], - ]; - } -} diff --git a/tests/Unit/Literals/StringLiteralTest.php b/tests/Unit/Literals/StringLiteralTest.php deleted file mode 100644 index 4afeeac2..00000000 --- a/tests/Unit/Literals/StringLiteralTest.php +++ /dev/null @@ -1,133 +0,0 @@ -useDoubleQuotes(false); - - $this->assertSame("''", $string->toQuery()); - $this->assertEquals('', $string->getValue()); - $this->assertFalse($string->usesDoubleQuotes()); - } - - public function testEmptyDoubleQuotes(): void - { - $string = new StringLiteral(""); - $string->useDoubleQuotes(true); - - $this->assertSame('""', $string->toQuery()); - $this->assertEquals('', $string->getValue()); - $this->assertTrue($string->usesDoubleQuotes()); - } - - public function testInstanceOfStringType(): void - { - $this->assertInstanceOf(StringType::class, new StringLiteral("")); - } - - /** - * @dataProvider provideSingleQuotesData - * @param string $string - * @param string $expected - */ - public function testSingleQuotes(string $string, string $expected): void - { - $literal = new StringLiteral($string); - $literal->useDoubleQuotes(false); - - $this->assertSame($expected, $literal->toQuery()); - $this->assertEquals($string, $literal->getValue()); - $this->assertFalse($literal->usesDoubleQuotes()); - } - - /** - * @dataProvider provideDoubleQuotesData - * @param string $string - * @param string $expected - */ - public function testDoubleQuotes(string $string, string $expected): void - { - $literal = new StringLiteral($string); - $literal->useDoubleQuotes(true); - - $this->assertSame($expected, $literal->toQuery()); - $this->assertEquals($string, $literal->getValue()); - $this->assertTrue($literal->usesDoubleQuotes()); - } - - public function provideSingleQuotesData(): array - { - return [ - ["a", "'a'"], - ["b", "'b'"], - ["\t", "'\\t'"], - ["\b", "'\\b'"], - ["\n", "'\\n'"], - ["\r", "'\\r'"], - ["\f", "'\\f'"], - ["'", "'\\''"], - ["\"", "'\"'"], - ["\\\\", "'\\\\'"], - ["\u1234", "'\\u1234'"], - ["\U12345678", "'\\U12345678'"], - ["\u0000", "'\\u0000'"], - ["\uffff", "'\\uffff'"], - ["\U00000000", "'\\U00000000'"], - ["\Uffffffff", "'\\Uffffffff'"], - ["\\\\b", "'\\\\b'"], - ]; - } - - public function provideDoubleQuotesData(): array - { - return [ - ["a", "\"a\""], - ["b", "\"b\""], - ["\t", "\"\\t\""], - ["\b", "\"\\b\""], - ["\n", "\"\\n\""], - ["\r", "\"\\r\""], - ["\f", "\"\\f\""], - ["'", "\"'\""], - ["\"", "\"\\\"\""], - ["\\\\", "\"\\\\\""], - ["\u1234", "\"\\u1234\""], - ["\U12345678", "\"\\U12345678\""], - ["\u0000", "\"\\u0000\""], - ["\uffff", "\"\\uffff\""], - ["\U00000000", "\"\\U00000000\""], - ["\Uffffffff", "\"\\Uffffffff\""], - ["\\\\b", "\"\\\\b\""], - ]; - } -} diff --git a/tests/Unit/MinusTest.php b/tests/Unit/MinusTest.php deleted file mode 100644 index 34551a17..00000000 --- a/tests/Unit/MinusTest.php +++ /dev/null @@ -1,56 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10")); - - $this->assertSame("-10", $minus->toQuery()); - - $minus = new Minus($minus); - - $this->assertSame("--10", $minus->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperand() - { - $this->expectException(TypeError::class); - - $minus = new Minus($this->getQueryConvertableMock(AnyType::class, "10")); - - $minus->toQuery(); - } -} diff --git a/tests/Unit/ModuloTest.php b/tests/Unit/ModuloTest.php deleted file mode 100644 index 109ff844..00000000 --- a/tests/Unit/ModuloTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->assertSame("(10 % 15)", $modulo->toQuery()); - - $modulo = new Modulo($modulo, $modulo); - - $this->assertSame("((10 % 15) % (10 % 15))", $modulo->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $modulo = new Modulo($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15"), false); - - $this->assertSame("10 % 15", $modulo->toQuery()); - - $modulo = new Modulo($modulo, $modulo); - - $this->assertSame("(10 % 15 % 10 % 15)", $modulo->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $modulo = new Modulo($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $modulo->toQuery(); - } -} diff --git a/tests/Unit/MultiplicationTest.php b/tests/Unit/MultiplicationTest.php deleted file mode 100644 index d200c316..00000000 --- a/tests/Unit/MultiplicationTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->assertSame("(10 * 15)", $multiplication->toQuery()); - - $multiplication = new Multiplication($multiplication, $multiplication); - - $this->assertSame("((10 * 15) * (10 * 15))", $multiplication->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $multiplication = new Multiplication($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15"), false); - - $this->assertSame("10 * 15", $multiplication->toQuery()); - - $multiplication = new Multiplication($multiplication, $multiplication); - - $this->assertSame("(10 * 15 * 10 * 15)", $multiplication->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $multiplication = new Multiplication($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $multiplication->toQuery(); - } -} diff --git a/tests/Unit/NotTest.php b/tests/Unit/NotTest.php deleted file mode 100644 index 01495798..00000000 --- a/tests/Unit/NotTest.php +++ /dev/null @@ -1,56 +0,0 @@ -getQueryConvertableMock(BooleanType::class, "true")); - - $this->assertSame("(NOT true)", $not->toQuery()); - - $not = new Not($not); - - $this->assertSame("(NOT (NOT true))", $not->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands() - { - $this->expectException(TypeError::class); - - $and = new Not($this->getQueryConvertableMock(AnyType::class, "true")); - - $and->toQuery(); - } -} diff --git a/tests/Unit/OrOperatorTest.php b/tests/Unit/OrOperatorTest.php deleted file mode 100644 index 002b7983..00000000 --- a/tests/Unit/OrOperatorTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(BooleanType::class, "true"), $this->getQueryConvertableMock(BooleanType::class, "false")); - - $this->assertSame("(true OR false)", $or->toQuery()); - - $or = new OrOperator($or, $or); - - $this->assertSame("((true OR false) OR (true OR false))", $or->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $or = new OrOperator($this->getQueryConvertableMock(BooleanType::class, "true"), $this->getQueryConvertableMock(BooleanType::class, "false"), false); - - $this->assertSame("true OR false", $or->toQuery()); - - $or = new OrOperator($or, $or); - - $this->assertSame("(true OR false OR true OR false)", $or->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $or = new OrOperator($this->getQueryConvertableMock(AnyType::class, "true"), $this->getQueryConvertableMock(AnyType::class, "false")); - - $or->toQuery(); - } -} diff --git a/tests/Unit/ParameterTest.php b/tests/Unit/ParameterTest.php deleted file mode 100644 index b552d16b..00000000 --- a/tests/Unit/ParameterTest.php +++ /dev/null @@ -1,74 +0,0 @@ -assertSame($expected, $parameter->toQuery()); - $this->assertSame(str_replace('$', '', $expected), $parameter->getParameter()); - } - - /** - * @dataProvider provideThrowsExceptionOnInvalidData - * @param string $parameter - */ - public function testThrowsExceptionOnInvalid(string $parameter): void - { - $this->expectException(InvalidArgumentException::class); - - new Parameter($parameter); - } - - public function provideToQueryData(): array - { - return [ - ["a", '$a'], - ["b", '$b'], - ["foo_bar", '$foo_bar'], - ["A", '$A'], - ]; - } - - public function provideThrowsExceptionOnInvalidData(): array - { - return [ - [""], - [str_repeat('a', 65535)], - ]; - } -} diff --git a/tests/Unit/Patterns/NodeTest.php b/tests/Unit/Patterns/NodeTest.php deleted file mode 100644 index 1aadeb02..00000000 --- a/tests/Unit/Patterns/NodeTest.php +++ /dev/null @@ -1,357 +0,0 @@ -assertSame("()", $node->toQuery()); - - $this->assertNull($node->getProperties()); - $this->assertEquals([], $node->getLabels()); - $this->assertNull($node->getVariable()); - - $name = $node->getName(); - $this->assertNotNull($name); - $this->assertSame($name, $node->getVariable()); - } - - // Further tests can be found in Traits/EscapeTraitTest - public function testBacktickIsEscaped(): void - { - $node = new Node(); - - $node->named('abcdr`eer'); - $this->assertEquals('(`abcdr``eer`)', $node->toQuery()); - } - - /** - * @dataProvider provideOnlyLabelData - * @param string $label - * @param string $expected - */ - public function testOnlyLabel(string $label, string $expected): void - { - $node = new Node(); - $node->labeled($label); - - $this->assertSame($expected, $node->toQuery()); - - $this->assertNull($node->getProperties()); - $this->assertEquals([$label], $node->getLabels()); - $this->assertNull($node->getVariable()); - - $name = $node->getName(); - $this->assertNotNull($name); - $this->assertSame($name, $node->getVariable()); - } - - /** - * @dataProvider provideOnlyNameData - * @param string $name - * @param string $expected - */ - public function testOnlyName(string $name, string $expected): void - { - $node = new Node(); - $node->named($name); - - $this->assertSame($expected, $node->toQuery()); - - $this->assertNull($node->getProperties()); - $this->assertEquals([], $node->getLabels()); - $this->assertNotNull($node->getVariable()); - - $variable = $node->getName(); - $this->assertNotNull($variable); - $this->assertEquals($name, $variable->getVariable()); - $this->assertSame($variable, $node->getVariable()); - } - - /** - * @dataProvider provideOnlyPropertiesData - * @param array $properties - * @param string $expected - */ - public function testOnlyProperties(array $properties, string $expected): void - { - $node = new Node(); - $node->withProperties($properties); - - $this->assertSame($expected, $node->toQuery()); - - $this->assertEquals(new PropertyMap($properties), $node->getProperties()); - $this->assertEquals([], $node->getLabels()); - $this->assertNull($node->getVariable()); - - $name = $node->getName(); - $this->assertNotNull($name); - $this->assertSame($name, $node->getVariable()); - } - - /** - * @dataProvider provideWithNameAndLabelData - * @param string $name - * @param string $label - * @param string $expected - */ - public function testWithNameAndLabel(string $name, string $label, string $expected): void - { - $node = new Node(); - $node->labeled($label)->named($name); - - $this->assertSame($expected, $node->toQuery()); - - $this->assertNull($node->getProperties()); - $this->assertEquals([$label], $node->getLabels()); - $this->assertNotNull($node->getVariable()); - - $variable = $node->getName(); - $this->assertNotNull($variable); - $this->assertEquals($name, $variable->getVariable()); - $this->assertSame($variable, $node->getVariable()); - } - - /** - * @dataProvider provideWithNameAndPropertiesData - * @param string $name - * @param array $properties - * @param string $expected - */ - public function testWithNameAndProperties(string $name, array $properties, string $expected): void - { - $node = new Node(); - $node->named($name)->withProperties($properties); - - $this->assertSame($expected, $node->toQuery()); - - $this->assertEquals(new PropertyMap($properties), $node->getProperties()); - $this->assertEquals([], $node->getLabels()); - $this->assertNotNull($node->getVariable()); - - $variable = $node->getName(); - $this->assertNotNull($variable); - $this->assertEquals($name, $variable->getVariable()); - $this->assertSame($variable, $node->getVariable()); - } - - /** - * @dataProvider provideWithLabelAndPropertiesData - * @param string $label - * @param array $properties - * @param string $expected - */ - public function testWithLabelAndProperties(string $label, array $properties, string $expected): void - { - $node = new Node(); - $node->labeled($label)->withProperties($properties); - - $this->assertSame($expected, $node->toQuery()); - - $this->assertEquals(new PropertyMap($properties), $node->getProperties()); - $this->assertEquals([$label], $node->getLabels()); - $this->assertNull($node->getVariable()); - - $name = $node->getName(); - $this->assertNotNull($name); - $this->assertSame($name, $node->getVariable()); - } - - /** - * @dataProvider provideWithNameAndLabelAndPropertiesData - * @param string $name - * @param string $label - * @param array $properties - * @param string $expected - */ - public function testWithNameAndLabelAndProperties(string $name, string $label, array $properties, string $expected): void - { - $node = new Node(); - $node->named($name)->labeled($label)->withProperties($properties); - - $this->assertSame($expected, $node->toQuery()); - - $this->assertEquals(new PropertyMap($properties), $node->getProperties()); - $this->assertEquals([$label], $node->getLabels()); - $this->assertNotNull($node->getVariable()); - - $variable = $node->getName(); - $this->assertNotNull($variable); - $this->assertEquals($name, $variable->getVariable()); - $this->assertSame($variable, $node->getVariable()); - } - - /** - * @dataProvider provideMultipleLabelsData - * @param array $labels - * @param string $expected - */ - public function testMultipleLabels(array $labels, string $expected): void - { - $node = new Node(); - - foreach ($labels as $label) { - $node->labeled($label); - } - - $this->assertSame($expected, $node->toQuery()); - - $this->assertNull($node->getProperties()); - $this->assertEquals($labels, $node->getLabels()); - $this->assertNull($node->getVariable()); - - $name = $node->getName(); - $this->assertNotNull($name); - $this->assertSame($name, $node->getVariable()); - } - - public function testSetterSameAsConstructor(): void - { - $label = "__test__"; - $viaConstructor = new Node($label); - $viaSetter = (new Node())->labeled($label); - - $this->assertSame($viaConstructor->toQuery(), $viaSetter->toQuery(), "Setting label via setter has different effect than using constructor"); - } - - public function testAddingProperties(): void - { - $node = new Node(); - - $node->withProperty("foo", new StringLiteral("bar")); - - $this->assertSame("({foo: 'bar'})", $node->toQuery()); - - $node->withProperty("foo", new StringLiteral("bar")); - - $this->assertSame("({foo: 'bar'})", $node->toQuery()); - - $node->withProperty("baz", new StringLiteral("bar")); - - $this->assertSame("({foo: 'bar', baz: 'bar'})", $node->toQuery()); - - $node->withProperties(["foo" => new StringLiteral("baz"), "qux" => new StringLiteral("baz")]); - - $this->assertSame("({foo: 'baz', baz: 'bar', qux: 'baz'})", $node->toQuery()); - } - - public function testPropertyWithName(): void - { - $node = new Node(); - $node->named('example'); - - $this->assertSame('example.foo', $node->property('foo')->toQuery()); - } - - public function testPropertyWithoutName(): void - { - $node = new Node(); - - $this->assertMatchesRegularExpression("/^var[0-9a-f]+\.foo$/", $node->property('foo')->toQuery()); - } - - public function provideOnlyLabelData(): array - { - return [ - ['a', '(:a)'], - ['A', '(:A)'], - [':', '(:`:`)'], - ]; - } - - public function provideOnlyNameData(): array - { - return [ - ['a', '(a)'], - ['A', '(A)'], - ]; - } - - public function provideWithNameAndLabelData(): array - { - return [ - ['a', 'a', '(a:a)'], - ['A', ':', '(A:`:`)'], - ]; - } - - public function provideWithNameAndPropertiesData(): array - { - return [ - ['a', ['a' => new StringLiteral('b'), 'b' => new StringLiteral('c')], "(a {a: 'b', b: 'c'})"], - ['b', ['a' => new Decimal(0), 'b' => new Decimal(1)], "(b {a: 0, b: 1})"], - ['c', [':' => new ExpressionList([new Decimal(1), new StringLiteral('a')])], "(c {`:`: [1, 'a']})"], - ]; - } - - public function provideWithLabelAndPropertiesData(): array - { - return [ - ['a', ['a' => new StringLiteral('b'), 'b' => new StringLiteral('c')], "(:a {a: 'b', b: 'c'})"], - ['b', ['a' => new Decimal(0), 'b' => new Decimal(1)], "(:b {a: 0, b: 1})"], - ['c', [':' => new ExpressionList([new Decimal(1), new StringLiteral('a')])], "(:c {`:`: [1, 'a']})"], - ]; - } - - public function provideOnlyPropertiesData(): array - { - return [ - [['a' => new StringLiteral('b'), 'b' => new StringLiteral('c')], "({a: 'b', b: 'c'})"], - [['a' => new Decimal(0), 'b' => new Decimal(1)], "({a: 0, b: 1})"], - [[':' => new ExpressionList([new Decimal(1), new StringLiteral('a')])], "({`:`: [1, 'a']})"], - ]; - } - - public function provideWithNameAndLabelAndPropertiesData(): array - { - return [ - ['a', 'd', ['a' => new StringLiteral('b'), 'b' => new StringLiteral('c')], "(a:d {a: 'b', b: 'c'})"], - ['b', 'e', ['a' => new Decimal(0), 'b' => new Decimal(1)], "(b:e {a: 0, b: 1})"], - ['c', 'f', [':' => new ExpressionList([new Decimal(1), new StringLiteral('a')])], "(c:f {`:`: [1, 'a']})"], - ]; - } - - public function provideMultipleLabelsData(): array - { - return [ - [['a'], '(:a)'], - [['A'], '(:A)'], - [[':'], '(:`:`)'], - [['a', 'b'], '(:a:b)'], - [['A', 'B'], '(:A:B)'], - [[':', 'a'], '(:`:`:a)'], - ]; - } -} diff --git a/tests/Unit/Patterns/PathTest.php b/tests/Unit/Patterns/PathTest.php deleted file mode 100644 index 9690891e..00000000 --- a/tests/Unit/Patterns/PathTest.php +++ /dev/null @@ -1,89 +0,0 @@ -toQuery()); - self::assertEquals('', (new Path(null, []))->toQuery()); - self::assertEquals('', (new Path([], []))->toQuery()); - self::assertEquals('', (new Path([], [new Relationship(Relationship::DIR_UNI)]))->toQuery()); - } - - public function testSingleNode(): void - { - $path = new Path(new Node()); - - self::assertEquals('()', $path->toQuery()); - } - - public function testSingleNodeNamed(): void - { - $path = new Path(new Node()); - $path->named('a'); - - self::assertEquals('a = ()', $path->toQuery()); - } - - public function testSingle(): void - { - $path = new Path(new Node(), new Relationship(Relationship::DIR_UNI)); - - self::assertEquals('()', $path->toQuery()); - } - - public function testSingleNoRel(): void - { - $path = new Path([new Node(), new Node()]); - - self::assertEquals('()', $path->toQuery()); - } - - public function testPathMerge(): void - { - $pathX = new Path([new Node(), new Node()], [new Relationship(Relationship::DIR_UNI)]); - $pathY = new Path([new Node(), new Node()], [new Relationship(Relationship::DIR_UNI)]); - - $pathX->named('x')->relationshipTo($pathY->named('y')); - - self::assertEquals('x = ()-[]-()-[]->()-[]-()', $pathX->toQuery()); - } - - public function testRelationshipLong(): void - { - $path = new Path(new Node()); - $path->relationshipUni(new Node('Label')) - ->relationshipTo((new Node())->named('b')) - ->relationshipFrom(new Node(), 'TYPE', ['x' => Query::literal('y')], 'c') - ->relationship(new Relationship(Relationship::DIR_UNI), (new Node())->named('d')) - ->named('a'); - - self::assertEquals('a = ()-[]-(:Label)-[]->(b)<-[c:TYPE {x: \'y\'}]-()-[]-(d)', $path->toQuery()); - - self::assertEquals([ - new Node(), - new Node('Label'), - (new Node())->named('b'), - new Node(), - (new Node())->named('d'), - ], $path->getNodes()); - - self::assertEquals([ - new Relationship(Relationship::DIR_UNI), - new Relationship(Relationship::DIR_RIGHT), - (new Relationship(Relationship::DIR_LEFT)) - ->withType('TYPE') - ->withProperties(['x' => Query::literal('y')]) - ->named('c'), - new Relationship(Relationship::DIR_UNI), - ], $path->getRelationships()); - } -} diff --git a/tests/Unit/Patterns/RelationshipTest.php b/tests/Unit/Patterns/RelationshipTest.php deleted file mode 100644 index 49b1266e..00000000 --- a/tests/Unit/Patterns/RelationshipTest.php +++ /dev/null @@ -1,676 +0,0 @@ -assertSame("-[]->", $r->toQuery()); - - $this->assertEquals(Relationship::DIR_RIGHT, $r->getDirection()); - $this->assertEquals([], $r->getTypes()); - $this->assertNull($r->getProperties()); - $this->assertNull($r->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - public function testDirLeft(): void - { - $r = new Relationship(Relationship::DIR_LEFT); - - $this->assertSame("<-[]-", $r->toQuery()); - - $this->assertEquals(Relationship::DIR_LEFT, $r->getDirection()); - $this->assertEquals([], $r->getTypes()); - $this->assertNull($r->getProperties()); - $this->assertNull($r->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - public function testDirUni(): void - { - $r = new Relationship(Relationship::DIR_UNI); - - $this->assertSame("-[]-", $r->toQuery()); - - $this->assertEquals(Relationship::DIR_UNI, $r->getDirection()); - $this->assertEquals([], $r->getTypes()); - $this->assertNull($r->getProperties()); - $this->assertNull($r->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - /** - * @dataProvider provideWithNameData - * @param string $name - * @param array $direction - * @param string $expected - */ - public function testWithName(string $name, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->named($name); - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([], $r->getTypes()); - $this->assertNull($r->getProperties()); - $this->assertNotNull($r->getVariable()); - $this->assertEquals($name, $r->getVariable()->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - /** - * @dataProvider provideWithTypeData - * @param string $type - * @param array $direction - * @param string $expected - */ - public function testWithType(string $type, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->withType($type); - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([$type], $r->getTypes()); - $this->assertNull($r->getProperties()); - $this->assertNull($r->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - /** - * @dataProvider provideWithPropertiesData - * @param array $properties - * @param array $direction - * @param string $expected - */ - public function testWithProperties(array $properties, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->withProperties($properties); - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([], $r->getTypes()); - $this->assertEquals(new PropertyMap($properties), $r->getProperties()); - $this->assertNull($r->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - /** - * @dataProvider provideWithNameAndTypeData - * @param string $name - * @param string $type - * @param array $direction - * @param string $expected - */ - public function testWithNameAndType(string $name, string $type, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->named($name)->withType($type); - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([$type], $r->getTypes()); - $this->assertNull($r->getProperties()); - $this->assertNotNull($r->getVariable()); - $this->assertEquals($name, $r->getVariable()->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - /** - * @dataProvider provideWithNameAndPropertiesData - * @param string $name - * @param array $properties - * @param array $direction - * @param string $expected - */ - public function testWithNameAndProperties(string $name, array $properties, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->named($name)->withProperties($properties); - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([], $r->getTypes()); - $this->assertEquals(new PropertyMap($properties), $r->getProperties()); - $this->assertNotNull($r->getVariable()); - $this->assertEquals($name, $r->getVariable()->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - /** - * @dataProvider provideWithTypeAndPropertiesData - * @param string $type - * @param array $properties - * @param array $direction - * @param string $expected - */ - public function testWithTypeAndProperties(string $type, array $properties, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->withType($type)->withProperties($properties); - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([$type], $r->getTypes()); - $this->assertEquals(new PropertyMap($properties), $r->getProperties()); - $this->assertNull($r->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - /** - * @dataProvider provideWithNameAndTypeAndPropertiesData - * @param string $name - * @param string $type - * @param array $properties - * @param array $direction - * @param string $expected - */ - public function testWithNameAndTypeAndProperties(string $name, string $type, array $properties, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->named($name)->withType($type)->withProperties($properties); - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([$type], $r->getTypes()); - $this->assertEquals(new PropertyMap($properties), $r->getProperties()); - $this->assertNotNull($r->getVariable()); - $this->assertEquals($name, $r->getVariable()->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - /** - * @dataProvider provideWithMultipleTypesData - * @param string $name - * @param array $types - * @param array $properties - * @param array $direction - * @param string $expected - */ - public function testWithMultipleTypes(string $name, array $types, array $properties, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->named($name)->withProperties($properties); - - foreach ($types as $type) { - $r->withType($type); - } - - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals($types, $r->getTypes()); - $this->assertEquals(new PropertyMap($properties), $r->getProperties()); - $this->assertNotNull($r->getVariable()); - $this->assertEquals($name, $r->getVariable()->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - /** - * @dataProvider provideVariableLengthRelationshipsWithNameData - * @param string $name - * @param int|null $minHops - * @param int|null $maxHops - * @param array $direction - * @param string $expected - */ - public function testVariableLengthRelationshipsWithName(string $name, ?int $minHops, ?int $maxHops, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->named($name); - - if (isset($minHops)) { - $r->withMinHops($minHops); - } - - if (isset($maxHops)) { - $r->withMaxHops($maxHops); - } - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([], $r->getTypes()); - $this->assertNull($r->getProperties()); - $this->assertNotNull($r->getVariable()); - $this->assertEquals($name, $r->getVariable()->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertEquals($maxHops, $r->getMaxHops()); - $this->assertEquals($minHops, $r->getMinHops()); - } - - /** - * @dataProvider provideVariableLengthRelationshipsWithTypeData - * @param string $type - * @param int|null $minHops - * @param int|null $maxHops - * @param array $direction - * @param string $expected - */ - public function testVariableLengthRelationshipsWithType(string $type, ?int $minHops, ?int $maxHops, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->withType($type); - - if (isset($minHops)) { - $r->withMinHops($minHops); - } - - if (isset($maxHops)) { - $r->withMaxHops($maxHops); - } - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([$type], $r->getTypes()); - $this->assertNull($r->getProperties()); - $this->assertNull($r->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertEquals($maxHops, $r->getMaxHops()); - $this->assertEquals($minHops, $r->getMinHops()); - } - - /** - * @dataProvider provideVariableLengthRelationshipsWithPropertiesData - * @param array $properties - * @param int|null $minHops - * @param int|null $maxHops - * @param array $direction - * @param string $expected - */ - public function testVariableLengthRelationshipsWithProperties(array $properties, ?int $minHops, ?int $maxHops, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->withProperties($properties); - - if (isset($minHops)) { - $r->withMinHops($minHops); - } - - if (isset($maxHops)) { - $r->withMaxHops($maxHops); - } - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([], $r->getTypes()); - $this->assertEquals(new PropertyMap($properties), $r->getProperties()); - $this->assertNull($r->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertEquals($maxHops, $r->getMaxHops()); - $this->assertEquals($minHops, $r->getMinHops()); - } - - /** - * @dataProvider provideVariableLengthRelationshipsWithNameAndTypeAndPropertiesData - * @param string $name - * @param string $type - * @param array $properties - * @param int|null $minHops - * @param int|null $maxHops - * @param array $direction - * @param string $expected - */ - public function testVariableLengthRelationshipsWithNameAndTypeAndProperties(string $name, string $type, array $properties, ?int $minHops, ?int $maxHops, array $direction, string $expected): void - { - $r = new Relationship($direction); - $r->named($name)->withType($type)->withProperties($properties); - - if (isset($minHops)) { - $r->withMinHops($minHops); - } - - if (isset($maxHops)) { - $r->withMaxHops($maxHops); - } - - $this->assertSame($expected, $r->toQuery()); - - $this->assertEquals($direction, $r->getDirection()); - $this->assertEquals([$type], $r->getTypes()); - $this->assertEquals(new PropertyMap($properties), $r->getProperties()); - $this->assertNotNull($r->getVariable()); - $this->assertEquals($name, $r->getVariable()->getVariable()); - - $this->assertNull($r->getExactHops()); - $this->assertEquals($maxHops, $r->getMaxHops()); - $this->assertEquals($minHops, $r->getMinHops()); - } - - public function testExactLengthRelationships(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - $r->named("tom") - ->withType("Person") - ->withProperties(['name' => Query::literal('Tom Hanks')]); - - $r->withExactHops(10); - - $this->assertSame("-[tom:Person*10 {name: 'Tom Hanks'}]->", $r->toQuery()); - - $this->assertEquals(Relationship::DIR_RIGHT, $r->getDirection()); - $this->assertEquals(['Person'], $r->getTypes()); - $this->assertEquals(new PropertyMap(['name' => Query::literal('Tom Hanks')]), $r->getProperties()); - $this->assertNotNull($r->getVariable()); - $this->assertEquals('tom', $r->getVariable()->getVariable()); - - $this->assertEquals(10, $r->getExactHops()); - $this->assertNull($r->getMaxHops()); - $this->assertNull($r->getMinHops()); - } - - public function testMinAndExactHops(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - $r->withMinHops(1); - - $this->expectException(LogicException::class); - - $r->withExactHops(1); - } - - public function testMaxAndExactHops(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - $r->withMaxHops(1); - - $this->expectException(LogicException::class); - - $r->withExactHops(1); - } - - public function testMinMaxAndExactHops(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - $r->withMinHops(1); - $r->withMaxHops(1); - - $this->expectException(LogicException::class); - - $r->withExactHops(1); - } - - public function testExactAndMinHops(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - $r->withExactHops(1); - - $this->expectException(LogicException::class); - - $r->withMinHops(1); - } - - public function testExactAndMaxHops(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - $r->withExactHops(1); - - $this->expectException(LogicException::class); - - $r->withMaxHops(1); - } - - public function testMaxHopsLessThanMinHops(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - $r->withMinHops(100); - - $this->expectException(DomainException::class); - - $r->withMaxHops(1); - } - - public function testMinHopsGreaterThanMaxHops(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - $r->withMaxHops(1); - - $this->expectException(DomainException::class); - - $r->withMinHops(100); - } - - public function testMinHopsLessThanZero(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - - $this->expectException(DomainException::class); - - $r->withMinHops(-1); - } - - public function testMaxHopsLessThanOne(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - - $this->expectException(DomainException::class); - - $r->withMaxHops(0); - } - - public function testMaxHopsLessThanZero(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - - $this->expectException(DomainException::class); - - $r->withMaxHops(-1); - } - - public function testExactHopsLessThanOne(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - - $this->expectException(DomainException::class); - - $r->withExactHops(0); - } - - public function testExactHopsLessThanZero(): void - { - $r = new Relationship(Relationship::DIR_RIGHT); - - $this->expectException(DomainException::class); - - $r->withExactHops(-1); - } - - public function provideVariableLengthRelationshipsWithNameData(): array - { - return [ - ['b', 1, 100, Relationship::DIR_UNI, '-[b*1..100]-'], - ['a', 10, null, Relationship::DIR_UNI, '-[a*10..]-'], - ['a', null, 10, Relationship::DIR_LEFT, '<-[a*..10]-'], - ]; - } - - public function provideVariableLengthRelationshipsWithTypeData(): array - { - return [ - ['', 1, 100, Relationship::DIR_LEFT, '<-[*1..100]-'], - ['a', 10, null, Relationship::DIR_LEFT, '<-[:a*10..]-'], - [':', null, 10, Relationship::DIR_LEFT, '<-[:`:`*..10]-'], - ]; - } - - public function provideVariableLengthRelationshipsWithPropertiesData(): array - { - return [ - [[], 10, 100, Relationship::DIR_LEFT, "<-[*10..100 {}]-"], - [[new StringLiteral('a')], 10, null, Relationship::DIR_LEFT, "<-[*10.. {`0`: 'a'}]-"], - [['a' => new StringLiteral('b')], null, 10, Relationship::DIR_LEFT, "<-[*..10 {a: 'b'}]-"], - ]; - } - - public function provideVariableLengthRelationshipsWithNameAndTypeAndPropertiesData(): array - { - return [ - ['a', 'a', [], 10, 100, Relationship::DIR_LEFT, "<-[a:a*10..100 {}]-"], - ['b', 'a', [new StringLiteral('a')], null, 10, Relationship::DIR_LEFT, "<-[b:a*..10 {`0`: 'a'}]-"], - ['a', 'b', [new StringLiteral('a')], 10, 100, Relationship::DIR_LEFT, "<-[a:b*10..100 {`0`: 'a'}]-"], - ['a', '', ['a' => new StringLiteral('b')], null, 10, Relationship::DIR_LEFT, "<-[a*..10 {a: 'b'}]-"], - ['a', ':', ['a' => new StringLiteral('b'), new StringLiteral('c')], 10, null, Relationship::DIR_LEFT, "<-[a:`:`*10.. {a: 'b', `0`: 'c'}]-"], - ]; - } - - public function provideWithNameData(): array - { - return [ - ['a', Relationship::DIR_UNI, '-[a]-'], - ['a', Relationship::DIR_LEFT, '<-[a]-'], - ]; - } - - public function provideWithTypeData(): array - { - return [ - ['', Relationship::DIR_LEFT, '<-[]-'], - ['a', Relationship::DIR_LEFT, '<-[:a]-'], - [':', Relationship::DIR_LEFT, '<-[:`:`]-'], - ]; - } - - public function provideWithPropertiesData(): array - { - return [ - [[], Relationship::DIR_LEFT, "<-[{}]-"], - [[new StringLiteral('a')], Relationship::DIR_LEFT, "<-[{`0`: 'a'}]-"], - [['a' => new StringLiteral('b')], Relationship::DIR_LEFT, "<-[{a: 'b'}]-"], - [['a' => new StringLiteral('b'), new StringLiteral('c')], Relationship::DIR_LEFT, "<-[{a: 'b', `0`: 'c'}]-"], - [[':' => new Decimal(12)], Relationship::DIR_LEFT, "<-[{`:`: 12}]-"], - ]; - } - - public function provideWithNameAndTypeData(): array - { - return [ - ['a', '', Relationship::DIR_LEFT, '<-[a]-'], - ['a', 'b', Relationship::DIR_LEFT, '<-[a:b]-'], - ]; - } - - public function provideWithNameAndPropertiesData(): array - { - return [ - ['a', [], Relationship::DIR_LEFT, "<-[a {}]-"], - ['b', [new StringLiteral('a')], Relationship::DIR_LEFT, "<-[b {`0`: 'a'}]-"], - ]; - } - - public function provideWithTypeAndPropertiesData(): array - { - return [ - ['a', [], Relationship::DIR_LEFT, "<-[:a {}]-"], - ['b', [new StringLiteral('a')], Relationship::DIR_LEFT, "<-[:b {`0`: 'a'}]-"], - ['', ['a' => new StringLiteral('b')], Relationship::DIR_LEFT, "<-[{a: 'b'}]-"], - [':', ['a' => new StringLiteral('b'), new StringLiteral('c')], Relationship::DIR_LEFT, "<-[:`:` {a: 'b', `0`: 'c'}]-"], - ]; - } - - public function provideWithNameAndTypeAndPropertiesData(): array - { - return [ - ['a', 'a', [], Relationship::DIR_LEFT, "<-[a:a {}]-"], - ['b', 'a', [new StringLiteral('a')], Relationship::DIR_LEFT, "<-[b:a {`0`: 'a'}]-"], - ['a', 'b', [new StringLiteral('a')], Relationship::DIR_LEFT, "<-[a:b {`0`: 'a'}]-"], - ['a', '', ['a' => new StringLiteral('b')], Relationship::DIR_LEFT, "<-[a {a: 'b'}]-"], - ['a', ':', ['a' => new StringLiteral('b'), new StringLiteral('c')], Relationship::DIR_LEFT, "<-[a:`:` {a: 'b', `0`: 'c'}]-"], - ]; - } - - public function provideWithMultipleTypesData(): array - { - return [ - ['a', [], [], Relationship::DIR_LEFT, "<-[a {}]-"], - ['b', ['a'], [new StringLiteral('a')], Relationship::DIR_LEFT, "<-[b:a {`0`: 'a'}]-"], - ['a', ['a', 'b', 'c'], [new StringLiteral('a')], Relationship::DIR_LEFT, "<-[a:a|b|c {`0`: 'a'}]-"], - ['a', ['a', 'b'], [], Relationship::DIR_LEFT, "<-[a:a|b {}]-"], - ]; - } -} diff --git a/tests/Unit/PropertyMapTest.php b/tests/Unit/PropertyMapTest.php deleted file mode 100644 index ef13f5bc..00000000 --- a/tests/Unit/PropertyMapTest.php +++ /dev/null @@ -1,139 +0,0 @@ -assertSame("{}", $propertyMap->toQuery()); - } - - /** - * @dataProvider provideNumericalKeysData - * @param array $properties - * @param string $expected - */ - public function testNumericalKeys(array $properties, string $expected) - { - $propertyMap = new PropertyMap($properties); - - $this->assertSame($expected, $propertyMap->toQuery()); - } - - /** - * @dataProvider provideStringKeysData - * @param array $properties - * @param string $expected - */ - public function testStringKeys(array $properties, string $expected) - { - $propertyMap = new PropertyMap($properties); - - $this->assertSame($expected, $propertyMap->toQuery()); - } - - /** - * @dataProvider provideNestedPropertyMapsData - * @param array $properties - * @param string $expected - */ - public function testNestedPropertyMaps(array $properties, string $expected) - { - $propertyMap = new PropertyMap($properties); - - $this->assertSame($expected, $propertyMap->toQuery()); - } - - public function testMergeWith() - { - $propertyMap = new PropertyMap(["foo" => new StringLiteral("bar")]); - $propertyMap->mergeWith(new PropertyMap(["boo" => new StringLiteral("far")])); - - $this->assertSame("{foo: 'bar', boo: 'far'}", $propertyMap->toQuery()); - - $propertyMap->mergeWith($propertyMap); - - $this->assertSame("{foo: 'bar', boo: 'far'}", $propertyMap->toQuery()); - } - - public function testAddProperty() - { - $propertyMap = new PropertyMap(["foo" => new StringLiteral("bar")]); - $propertyMap->addProperty('foo', new StringLiteral("baz")); - - $this->assertSame("{foo: 'baz'}", $propertyMap->toQuery()); - - $propertyMap->addProperty('boo', new StringLiteral("far")); - - $this->assertSame("{foo: 'baz', boo: 'far'}", $propertyMap->toQuery()); - } - - public function provideNumericalKeysData(): array - { - return [ - [[$this->getQueryConvertableMock(AnyType::class, "'a'")], "{`0`: 'a'}"], - [[$this->getQueryConvertableMock(AnyType::class, "'a'"), $this->getQueryConvertableMock(AnyType::class, "'b'")], "{`0`: 'a', `1`: 'b'}"], - ]; - } - - public function provideStringKeysData(): array - { - return [ - [['a' => $this->getQueryConvertableMock(AnyType::class, "'a'")], "{a: 'a'}"], - [['a' => $this->getQueryConvertableMock(AnyType::class, "'a'"), 'b' => $this->getQueryConvertableMock(AnyType::class, "'b'")], "{a: 'a', b: 'b'}"], - [['a' => $this->getQueryConvertableMock(AnyType::class, "'b'")], "{a: 'b'}"], - [[':' => $this->getQueryConvertableMock(AnyType::class, "'a'")], "{`:`: 'a'}"], - ]; - } - - public function provideNestedPropertyMapsData() - { - return [ - [['a' => new PropertyMap([])], "{a: {}}"], - [['a' => new PropertyMap(['a' => new PropertyMap(['a' => $this->getQueryConvertableMock(AnyType::class, "'b'")])])], "{a: {a: {a: 'b'}}}"], - [['a' => new PropertyMap(['b' => $this->getQueryConvertableMock(AnyType::class, "'c'")]), 'b' => $this->getQueryConvertableMock(AnyType::class, "'d'")], "{a: {b: 'c'}, b: 'd'}"], - ]; - } - - public function testRequiresAnyTypeProperties() - { - $a = new class () {}; - - $this->expectException(TypeError::class); - - new PropertyMap([$a]); - } -} diff --git a/tests/Unit/PropertyTest.php b/tests/Unit/PropertyTest.php deleted file mode 100644 index 73afa6a9..00000000 --- a/tests/Unit/PropertyTest.php +++ /dev/null @@ -1,58 +0,0 @@ -assertSame($expected, $property->toQuery()); - } - - public function provideToQueryData(): array - { - return [ - [$this->getQueryConvertableMock(Variable::class, "a"), "a", "a.a"], - [$this->getQueryConvertableMock(Variable::class, "a"), "b", "a.b"], - [$this->getQueryConvertableMock(Variable::class, "b"), "a", "b.a"], - [$this->getQueryConvertableMock(Variable::class, "a"), ":", "a.`:`"], - [$this->getQueryConvertableMock(Variable::class, "b"), ":", "b.`:`"], - ]; - } -} diff --git a/tests/Unit/QueryTest.php b/tests/Unit/QueryTest.php deleted file mode 100644 index 582d2a80..00000000 --- a/tests/Unit/QueryTest.php +++ /dev/null @@ -1,1155 +0,0 @@ -assertEquals($expected, $actual); - } - - public function testNodeWithLabel(): void - { - $label = "m"; - - $actual = Query::node($label); - $expected = (new Node())->labeled($label); - - $this->assertEquals($expected, $actual); - } - - public function testRelationship(): void - { - $directions = [Relationship::DIR_UNI, Relationship::DIR_LEFT, Relationship::DIR_RIGHT]; - - foreach ($directions as $direction) { - $expected = new Relationship($direction); - $actual = Query::relationship($direction); - - $this->assertEquals($expected, $actual); - } - } - - public function testVariable(): void - { - $this->assertInstanceOf(Variable::class, Query::variable("foo")); - } - - public function testVariableEmpty(): void - { - $this->assertInstanceOf(Variable::class, Query::variable()); - - $this->assertMatchesRegularExpression('/var[0-9a-f]+/', Query::variable()->toQuery()); - } - - public function testParameter(): void - { - $this->assertInstanceOf(Parameter::class, Query::parameter("foo")); - } - - /** - * @dataProvider provideLiteralData - * @param $literal - * @param PropertyType $expected - */ - public function testLiteral($literal, PropertyType $expected): void - { - $actual = Query::literal($literal); - - $this->assertEquals($expected, $actual); - } - - public function testList(): void - { - $list = Query::list([]); - - $this->assertInstanceOf(ExpressionList::class, $list); - } - - public function testListOfLiterals(): void - { - $list = Query::list(["hello", "world", 1.0, 1, 2, 3, true]); - - $this->assertInstanceOf(ExpressionList::class, $list); - } - - public function testListOfMixed(): void - { - $list = Query::list([$this->getQueryConvertableMock(AnyType::class, "hello"), "world"]); - - $this->assertInstanceOf(ExpressionList::class, $list); - } - - public function testListOfAnyType(): void - { - $list = Query::list([ - $this->getQueryConvertableMock(AnyType::class, "hello"), - $this->getQueryConvertableMock(AnyType::class, "world"), - ]); - - $this->assertInstanceOf(ExpressionList::class, $list); - } - - public function testNestedList(): void - { - $list = Query::list([Query::list([])]); - - $this->assertInstanceOf(ExpressionList::class, $list); - } - - public function testIteratorList(): void - { - $iterator = new class () implements \Iterator { - private int $count = 0; - - public function current() - { - return 1; - } - - public function next() - { - $this->count++; - - return 1; - } - - public function key() - { - return 0; - } - - public function valid() - { - // In order to avoid an infinite loop - return $this->count < 10; - } - - public function rewind() - { - } - }; - - $list = Query::list($iterator); - - $this->assertInstanceOf(ExpressionList::class, $list); - } - - public function testInvalidList(): void - { - $this->expectException(InvalidArgumentException::class); - Query::list([new class () {}]); - } - - public function testMap(): void - { - $map = Query::map([]); - - $this->assertInstanceOf(PropertyMap::class, $map); - } - - /** - * @doesNotPerformAssertions - */ - public function testFunction(): void - { - Query::function()::raw("test", []); - } - - public function testMatch(): void - { - $m = $this->getQueryConvertableMock(NodeType::class, "(m:Movie)"); - - $statement = (new Query())->match($m)->build(); - - $this->assertSame("MATCH (m:Movie)", $statement); - - $statement = (new Query())->match([$m, $m])->build(); - - $this->assertSame("MATCH (m:Movie), (m:Movie)", $statement); - } - - /** - * @doesNotPerformAssertions - */ - public function testMatchTypeAcceptance(): void - { - $path = $this->getQueryConvertableMock(PathType::class, '(a)-->(b)'); - $node = $this->getQueryConvertableMock(NodeType::class, '(a)'); - - (new Query())->match([$path, $node]); - } - - public function testMatchRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->match($m); - } - - public function testReturning(): void - { - $m = $this->getQueryConvertableMock(StructuralType::class, "(m:Movie)"); - - $statement = (new Query())->returning($m)->build(); - - $this->assertSame("RETURN (m:Movie)", $statement); - - $statement = (new Query())->returning(["n" => $m])->build(); - - $this->assertSame("RETURN (m:Movie) AS n", $statement); - } - - public function testReturningRejectsNotAnyType(): void - { - $m = new class () {}; - - $this->expectException(TypeError::class); - - (new Query())->returning([$m]); - } - - public function testReturningWithNode(): void - { - $node = Query::node("m"); - - $statement = (new Query())->returning($node)->build(); - - $this->assertMatchesRegularExpression("/(RETURN var[0-9a-f]+)/", $statement); - - $node = Query::node("m"); - $node->named('example'); - - $statement = (new Query())->returning($node)->build(); - - $this->assertSame('RETURN example', $statement); - } - - public function testCreate(): void - { - $m = $this->getQueryConvertableMock(PathType::class, "(m:Movie)-[:RELATED]->(b)"); - - $statement = (new Query())->create($m)->build(); - - $this->assertSame("CREATE (m:Movie)-[:RELATED]->(b)", $statement); - - $statement = (new Query())->create([$m, $m])->build(); - - $this->assertSame("CREATE (m:Movie)-[:RELATED]->(b), (m:Movie)-[:RELATED]->(b)", $statement); - } - - /** - * @doesNotPerformAssertions - */ - public function testCreateTypeAcceptance(): void - { - $path = $this->getQueryConvertableMock(PathType::class, '(a)-->(b)'); - $node = $this->getQueryConvertableMock(NodeType::class, '(a)'); - - (new Query())->create([$path, $node]); - } - - public function testCreateRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->create([$m, $m]); - } - - public function testDelete(): void - { - $m = $this->getQueryConvertableMock(Variable::class, "m"); - - $statement = (new Query())->delete($m)->build(); - - $this->assertSame("DELETE m", $statement); - - $statement = (new Query())->delete([$m, $m])->build(); - - $this->assertSame("DELETE m, m", $statement); - } - - public function testDeleteRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->delete([$m, $m]); - } - - public function testDetachDelete(): void - { - $m = $this->getQueryConvertableMock(Variable::class, "m"); - - $statement = (new Query())->detachDelete($m)->build(); - - $this->assertSame("DETACH DELETE m", $statement); - - $statement = (new Query())->detachDelete([$m, $m])->build(); - - $this->assertSame("DETACH DELETE m, m", $statement); - } - - public function testDetachDeleteRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->detachDelete([$m, $m]); - } - - public function testLimit(): void - { - $expression = $this->getQueryConvertableMock(NumeralType::class, "12"); - - $statement = (new Query())->limit($expression)->build(); - - $this->assertSame("LIMIT 12", $statement); - } - - public function testMerge(): void - { - $pattern = $this->getQueryConvertableMock(PathType::class, "(m)->(b)"); - - $statement = (new Query())->merge($pattern)->build(); - - $this->assertSame("MERGE (m)->(b)", $statement); - - $onCreate = $this->getQueryConvertableMock(Clause::class, "DELETE (m:Movie)"); - $onMatch = $this->getQueryConvertableMock(Clause::class, "CREATE (m:Movie)"); - - $statement = (new Query())->merge($pattern, $onCreate, $onMatch)->build(); - - $this->assertSame("MERGE (m)->(b) ON CREATE DELETE (m:Movie) ON MATCH CREATE (m:Movie)", $statement); - } - - /** - * @doesNotPerformAssertions - */ - public function testMergeTypeAcceptance(): void - { - $path = $this->getQueryConvertableMock(PathType::class, '(a)-->(b)'); - $node = $this->getQueryConvertableMock(NodeType::class, '(a)'); - - (new Query())->merge($path); - (new Query())->merge($node); - } - - public function testMergeRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->optionalMatch([$m, $m]); - } - - public function testOptionalMatch(): void - { - $pattern = $this->getQueryConvertableMock(NodeType::class, "(m)"); - - $statement = (new Query())->optionalMatch($pattern)->build(); - - $this->assertSame("OPTIONAL MATCH (m)", $statement); - - $statement = (new Query())->optionalMatch([$pattern, $pattern])->build(); - - $this->assertSame("OPTIONAL MATCH (m), (m)", $statement); - } - - /** - * @doesNotPerformAssertions - */ - public function testOptionalMatchTypeAcceptance(): void - { - $path = $this->getQueryConvertableMock(PathType::class, '(a)-->(b)'); - $node = $this->getQueryConvertableMock(NodeType::class, '(a)'); - - (new Query())->optionalMatch([$path, $node]); - } - - public function testOptionalMatchRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->optionalMatch([$m, $m]); - } - - public function testOrderBy(): void - { - $property = $this->getQueryConvertableMock(Property::class, "a.foo"); - - $statement = (new Query())->orderBy($property)->build(); - - $this->assertSame("ORDER BY a.foo", $statement); - - $statement = (new Query())->orderBy([$property, $property])->build(); - - $this->assertSame("ORDER BY a.foo, a.foo", $statement); - - $statement = (new Query())->orderBy([$property, $property], false)->build(); - - $this->assertSame("ORDER BY a.foo, a.foo", $statement); - - $statement = (new Query())->orderBy([$property, $property], true)->build(); - - $this->assertSame("ORDER BY a.foo, a.foo DESCENDING", $statement); - } - - public function testOrderByRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->orderBy([$m, $m]); - } - - public function testRemove(): void - { - $expression = $this->getQueryConvertableMock(Property::class, "a.age"); - - $statement = (new Query())->remove($expression)->build(); - - $this->assertSame("REMOVE a.age", $statement); - } - - public function testRemoveRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->remove($m); - } - - public function testSet(): void - { - $expression = $this->getQueryConvertableMock(Assignment::class, "a.age"); - - $statement = (new Query())->set($expression)->build(); - - $this->assertSame("SET a.age", $statement); - - $statement = (new Query())->set([$expression, $expression])->build(); - - $this->assertSame("SET a.age, a.age", $statement); - } - - public function testSetRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->set([$m, $m]); - } - - public function testSetWithLabel(): void - { - $label = Query::variable("n")->labeled(["LABEL1", "LABEL2", "LABEL3"]); - - $statement = (new Query())->set($label)->build(); - - $this->assertSame("SET n:LABEL1:LABEL2:LABEL3", $statement); - } - - public function testWhere(): void - { - $expression = $this->getQueryConvertableMock(BooleanType::class, "a.age"); - - $statement = (new Query())->where($expression)->build(); - - $this->assertSame("WHERE a.age", $statement); - } - - public function testWith(): void - { - $expression = $this->getQueryConvertableMock(AnyType::class, "a < b"); - - $statement = (new Query())->with($expression)->build(); - - $this->assertSame("WITH a < b", $statement); - - $statement = (new Query())->with(["foobar" => $expression])->build(); - - $this->assertSame("WITH a < b AS foobar", $statement); - } - - public function testWithRejectsAnyType(): void - { - $m = $this->getQueryConvertableMock(AnyType::class, 'foo'); - - $this->expectException(TypeError::class); - - (new Query())->delete([$m, $m]); - } - - public function testWithWithNode(): void - { - $node = Query::node('m'); - - $statement = (new Query())->with($node)->build(); - - $this->assertMatchesRegularExpression("/(WITH var[0-9a-f]+)/", $statement); - - $node = Query::node("m"); - $node->named('example'); - - $statement = (new Query())->with($node)->build(); - - $this->assertSame('WITH example', $statement); - } - - public function testCallProcedure(): void - { - $procedure = "apoc.json"; - - $statement = (new Query())->callProcedure($procedure)->build(); - - $this->assertSame("CALL apoc.json()", $statement); - - $expression = $this->getQueryConvertableMock(AnyType::class, "a < b"); - - $statement = (new Query())->callProcedure($procedure, [$expression])->build(); - - $this->assertSame("CALL apoc.json(a < b)", $statement); - - $variable = $this->getQueryConvertableMock(Variable::class, "a"); - - $statement = (new Query())->callProcedure($procedure, [$expression], [$variable])->build(); - - $this->assertSame("CALL apoc.json(a < b) YIELD a", $statement); - } - - public function testAddClause(): void - { - $clauseMockText = "FOOBAR foobar"; - $clauseMock = $this->getQueryConvertableMock(Clause::class, $clauseMockText); - $statement = (new Query())->addClause($clauseMock)->build(); - - $this->assertSame($clauseMockText, $statement); - } - - public function testBuild(): void - { - $withClause = $this->getQueryConvertableMock(Clause::class, "WITH foobar"); - $whereClause = $this->getQueryConvertableMock(Clause::class, "WHERE foobar"); - - $query = new Query(); - $query->clauses = [$withClause, $whereClause]; - - $statement = $query->build(); - - $this->assertSame("WITH foobar WHERE foobar", $statement); - - $nodeMock = $this->getQueryConvertableMock(Node::class, "(a)"); - $nodeMock->method('getName')->willReturn($this->getQueryConvertableMock(Variable::class, 'a')); - - $variableMock = $this->getQueryConvertableMock(Variable::class, "a"); - $pathMock = $this->getQueryConvertableMock(Path::class, "(a)->(b)"); - $numeralMock = $this->getQueryConvertableMock(NumeralType::class, "12"); - $booleanMock = $this->getQueryConvertableMock(BooleanType::class, "a > b"); - $propertyMock = $this->getQueryConvertableMock(Property::class, "a.b"); - - $query = new Query(); - $statement = $query->match([$pathMock, $nodeMock]) - ->returning(["#" => $nodeMock]) - ->create([$pathMock, $nodeMock]) - ->create($pathMock) - ->delete([$variableMock, $variableMock]) - ->detachDelete([$variableMock, $variableMock]) - ->limit($numeralMock) - ->merge($nodeMock) - ->optionalMatch([$nodeMock, $nodeMock]) - ->orderBy([$propertyMock, $propertyMock], true) - ->remove($propertyMock) - ->set([]) - ->where($booleanMock) - ->with(["#" => $nodeMock]) - ->build(); - - $this->assertSame("MATCH (a)->(b), (a) RETURN a AS `#` CREATE (a)->(b), (a) CREATE (a)->(b) DELETE a, a DETACH DELETE a, a LIMIT 12 MERGE (a) OPTIONAL MATCH (a), (a) ORDER BY a.b, a.b DESCENDING REMOVE a.b WHERE a > b WITH a AS `#`", $statement); - } - - public function testBuildEmpty(): void - { - $query = new Query(); - - $this->assertSame("", $query->build()); - } - - public function testInt(): void - { - $literal = Query::literal(1); - self::assertInstanceOf(Decimal::class, $literal); - self::assertEquals('1', $literal->toQuery()); - } - - public function testFloat(): void - { - $literal = Query::literal(1.2); - self::assertInstanceOf(Decimal::class, $literal); - self::assertEquals('1.2', $literal->toQuery()); - } - - public function testString(): void - { - $literal = Query::literal('abc'); - self::assertInstanceOf(StringLiteral::class, $literal); - self::assertEquals("'abc'", $literal->toQuery()); - } - - public function testStringAble(): void - { - $literal = Query::literal(new class () { - public function __toString(): string - { - return 'stringable abc'; - } - }); - self::assertInstanceOf(StringLiteral::class, $literal); - self::assertEquals("'stringable abc'", $literal->toQuery()); - } - - public function testBool(): void - { - $literal = Query::literal(true); - self::assertInstanceOf(Boolean::class, $literal); - self::assertEquals("true", $literal->toQuery()); - } - - public function testInvalidLiteral(): void - { - $literal = Query::literal(true); - $this->expectException(InvalidArgumentException::class); - Query::literal($literal); - } - - public function testLiteralReference(): void - { - $value = Query::literal(); - - $this->assertSame(Literal::class, $value); - } - - public function testWikiExamples(): void - { - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Examples - */ - - $m = Query::variable("m"); - $movie = Query::node("Movie")->named($m); - - $query = new Query(); - $statement = $query->match($movie) - ->returning($m) - ->build(); - - $this->assertSame("MATCH (m:Movie) RETURN m", $statement); - - $tom = Query::variable("tom"); - $tomHanks = Query::node()->named($tom)->withProperties([ - "name" => Query::literal("Tom Hanks"), - ]); - - $query = new Query(); - $statement = $query->match($tomHanks) - ->returning($tom) - ->build(); - - $this->assertSame("MATCH (tom {name: 'Tom Hanks'}) RETURN tom", $statement); - - $people = Query::variable("people"); - $person = Query::node("Person")->named($people); - - $statement = Query::new() - ->match($person) - ->returning($people->property("name")) - ->limit(Query::literal(10)) - ->build(); - - $this->assertSame("MATCH (people:Person) RETURN people.name LIMIT 10", $statement); - - $nineties = Query::variable("nineties"); - $released = $nineties->property("released"); - $movie = Query::node("Movie")->named($nineties); - $expression = $released->gte(Query::literal(1990))->and($released->lt(Query::literal(2000))); - - $statement = Query::new() - ->match($movie) - ->where($expression) - ->returning($nineties->property("title")) - ->build(); - - $this->assertSame("MATCH (nineties:Movie) WHERE ((nineties.released >= 1990) AND (nineties.released < 2000)) RETURN nineties.title", $statement); - - $tom = Query::variable("tom"); - $person = Query::node("Person")->withProperties([ - "name" => Query::literal("Tom Hanks"), - ])->named($tom); - - $tomHanksMovies = Query::variable("tomHanksMovies"); - $tomHanksMoviesNode = Query::node()->named($tomHanksMovies); - - $statement = Query::new() - ->match($person->relationshipTo($tomHanksMoviesNode, 'ACTED_IN')) - ->returning([$tom, $tomHanksMovies]) - ->build(); - - $this->assertSame("MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(tomHanksMovies) RETURN tom, tomHanksMovies", $statement); - - $cloudAtlas = Query::variable("cloudAtlas"); - $cloudAtlasNode = Query::node() - ->named($cloudAtlas) - ->withProperties(["title" => Query::literal("Cloud Atlas")]); - - $directors = Query::variable("directors"); - $directorsNode = Query::node()->named($directors); - - $statement = Query::new() - ->match($cloudAtlasNode->relationshipFrom($directorsNode, 'DIRECTED')) - ->returning($directors->property("name")) - ->build(); - - $this->assertSame("MATCH (cloudAtlas {title: 'Cloud Atlas'})<-[:DIRECTED]-(directors) RETURN directors.name", $statement); - - $tom = Query::variable("tom"); - $tomNode = Query::node("Person")->withProperties([ - "name" => Query::literal("Tom Hanks"), - ])->named($tom); - - $movie = Query::variable("m"); - $movieNode = Query::node()->named($movie); - - $coActors = Query::variable("coActors"); - $coActorsNode = Query::node()->named($coActors); - - $statement = Query::new() - ->match($tomNode->relationshipTo($movieNode, "ACTED_IN")->relationshipFrom($coActorsNode, "ACTED_IN")) - ->returning($coActors->property("name")) - ->build(); - - $this->assertSame("MATCH (tom:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors) RETURN coActors.name", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/CALL-procedure-clause - */ - - $statement = Query::new() - ->callProcedure("apoc.json") - ->build(); - - $this->assertSame("CALL apoc.json()", $statement); - - $name = Query::variable("name"); - $signature = Query::variable("signature"); - - $statement = Query::new() - ->callProcedure("dbms.procedures", [], [$name, $signature]) - ->build(); - - $this->assertSame("CALL dbms.procedures() YIELD name, signature", $statement); - - $username = Query::literal("example_username"); - $password = Query::literal("example_password"); - - $statement = Query::new() - ->callProcedure("dbms.security.createUser", [$username, $password, Query::literal(false)]) - ->build(); - - $this->assertSame("CALL dbms.security.createUser('example_username', 'example_password', false)", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/CREATE-clause - */ - - $tom = Query::node("Person")->named("tom"); - - $statement = Query::new() - ->create($tom) - ->build(); - - $this->assertSame("CREATE (tom:Person)", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/DELETE-clause - */ - - $tomVar = Query::variable('tom'); - $tomNode = Query::node("Person")->named($tomVar)->withProperty('name', Query::literal('tom')); - - - $statement = Query::new()->match($tomNode) - ->delete($tomVar) - ->build(); - - $this->assertSame("MATCH (tom:Person {name: 'tom'}) DELETE tom", $statement); - - $tomVar = Query::variable('tom'); - $tomNode = Query::node("Person")->named($tomVar)->withProperty('name', Query::literal('tom')); - - $statement = Query::new()->match($tomNode) - ->detachDelete($tomVar) - ->build(); - - $this->assertSame("MATCH (tom:Person {name: 'tom'}) DETACH DELETE tom", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/LIMIT-clause - */ - - $statement = Query::new() - ->limit(Query::literal(10)) - ->build(); - - $this->assertSame("LIMIT 10", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/MATCH-clause - */ - - $a = Query::node("a"); - $b = Query::node("b"); - $c = Query::node("c"); - - $statement = Query::new() - ->match([$a, $b, $c]) - ->build(); - - $this->assertSame("MATCH (:a), (:b), (:c)", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/MERGE-clause - */ - - $a = Query::node("a"); - - $statement = Query::new() - ->merge($a) - ->build(); - - $this->assertSame("MERGE (:a)", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/OPTIONAL-MATCH-clause - */ - - $a = Query::node("a"); - $b = Query::node("b"); - - $statement = Query::new() - ->optionalMatch([$a, $b]) - ->build(); - - $this->assertSame("OPTIONAL MATCH (:a), (:b)", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/ORDER-BY-clause - */ - - $n = Query::variable("n"); - - $name = $n->property("name"); - $age = $n->property("age"); - - $statement = Query::new() - ->orderBy([$name, $age], true) - ->build(); - - $this->assertSame("ORDER BY n.name, n.age DESCENDING", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/RAW-clause - */ - - $nested = Query::variable("nested"); - - $statement = Query::new() - ->raw("UNWIND", sprintf("%s AS x", $nested->toQuery())) - ->build(); - - $this->assertSame("UNWIND nested AS x", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/REMOVE-clause - */ - - $n = Query::variable("n")->withLabels(["foo"]); - - $statement = Query::new() - ->remove($n) - ->build(); - - $this->assertSame("REMOVE n:foo", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/RETURN-clause - */ - - $a = Query::variable("a"); - $b = Query::variable("b"); - - $statement = Query::new() - ->returning([$a, $b], true) - ->build(); - - $this->assertSame("RETURN DISTINCT a, b", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/SET-clause - */ - - $a = Query::variable("a")->assign(Query::literal(10)); - $b = Query::variable("b")->assign(Query::literal(15)); - - $statement = Query::new() - ->set([$a, $b]) - ->build(); - - $this->assertSame("SET a = 10, b = 15", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/WHERE-clause - */ - - $expression = Query::variable("n")->gte(Query::literal(10)); - - $statement = Query::new() - ->where($expression) - ->build(); - - $this->assertSame("WHERE (n >= 10)", $statement); - - $variable = Query::variable("n"); - - $exists = new Exists((new MatchClause())->addPattern(Query::node()->named($variable))); - $expression = $exists->and($variable->gte(Query::literal(10))); - - $statement = Query::new() - ->where($expression) - ->build(); - - $this->assertSame("WHERE (EXISTS { MATCH (n) } AND (n >= 10))", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Clauses/WITH-clause - */ - - $statement = Query::new() - ->with(["n" => Query::variable("a")]) - ->build(); - - $this->assertSame("WITH a AS n", $statement); - - /* - * @see https://gitlab.wikibase.nl/community/libraries/php-cypher-dsl/-/wikis/Usage/Expressions - */ - - $nineties = Query::variable("nineties"); - $released = $nineties->property("released"); - - $expression = $released->gte(Query::literal(1990))->and($released->lt(Query::literal(2000))); - - $this->assertSame("((nineties.released >= 1990) AND (nineties.released < 2000))", $expression->toQuery()); - - $actor = Query::variable("actor"); - $name = $actor->property("name"); - - $expression = $name->notEquals(Query::literal("Tom Hanks")); - - $this->assertSame("(actor.name <> 'Tom Hanks')", $expression->toQuery()); - - $nineties = Query::variable("nineties"); - $released = $nineties->property("released"); - - $expression = $released->gte(Query::literal(1990))->and(Query::rawExpression("(nineties IS NOT NULL)")); - - $this->assertSame("((nineties.released >= 1990) AND (nineties IS NOT NULL))", $expression->toQuery()); - } - - public function testUnionQueryAll(): void - { - $nodeX = Query::node('X')->named('x'); - $nodeY = Query::node('Y')->named('y'); - - $query = Query::new()->match($nodeX)->returning($nodeX->getVariable()); - $right = Query::new()->match($nodeY)->returning($nodeY->getVariable()); - - $query = $query->union($right, true); - - $this->assertEquals('MATCH (x:X) RETURN x UNION ALL MATCH (y:Y) RETURN y', $query->toQuery()); - } - - public function testUnionQuery(): void - { - $nodeX = Query::node('X')->named('x'); - $nodeY = Query::node('Y')->named('y'); - - $query = Query::new()->match($nodeX)->returning($nodeX->getVariable()); - $right = Query::new()->match($nodeY)->returning($nodeY->getVariable()); - - $query = $query->union($right, false); - - $this->assertEquals('MATCH (x:X) RETURN x UNION MATCH (y:Y) RETURN y', $query->toQuery()); - } - - public function testUnionDecorator(): void - { - $nodeX = Query::node('X')->named('x'); - - $query = Query::new()->match($nodeX)->returning($nodeX->getVariable()); - - $query = $query->union(function (Query $query) { - $nodeY = Query::node('Y')->named('y'); - $query->match($nodeY)->returning($nodeY->getVariable()); - }); - - $this->assertEquals('MATCH (x:X) RETURN x UNION MATCH (y:Y) RETURN y', $query->toQuery()); - } - - public function testUnionDecoratorAll(): void - { - $nodeX = Query::node('X')->named('x'); - - $query = Query::new()->match($nodeX)->returning($nodeX->getVariable()); - - $query = $query->union(function (Query $query) { - $nodeY = Query::node('Y')->named('y'); - $query->match($nodeY)->returning($nodeY->getVariable()); - }, true); - - $this->assertEquals('MATCH (x:X) RETURN x UNION ALL MATCH (y:Y) RETURN y', $query->toQuery()); - } - - public function testAutomaticIdentifierGeneration(): void - { - $node = Query::node(); - - $this->assertMatchesRegularExpression('/var[0-9a-f]+\.foo/', $node->property('foo')->toQuery()); - - $node->named('foo'); - - $this->assertSame('foo.bar', $node->property('bar')->toQuery()); - - $node = Query::node(); - $statement = Query::new()->match($node)->returning($node)->build(); - - $this->assertMatchesRegularExpression('/MATCH \(var[0-9a-f]+\) RETURN var[0-9a-f]+/', $statement); - - $node = Query::node(); - - $this->assertInstanceOf(Variable::class, $node->getName()); - } - - public function testCallCallable(): void - { - $node = Query::node('X')->named('y'); - $query = Query::new()->match($node) - ->call(function (Query $subQuery) use ($node) { - $subQuery->with($node->getVariable()) - ->where($node->property('z')->equals(Query::literal('foo'), false)) - ->returning($node->property('z')->alias('foo')); - }) - ->returning(Query::variable('foo')); - - $this->assertEquals("MATCH (y:X) CALL { WITH y WHERE y.z = 'foo' RETURN y.z AS foo } RETURN foo", $query->toQuery()); - } - - public function testCallClause(): void - { - $node = Query::node('X')->named('y'); - - $sub = Query::new()->with($node->getVariable()) - ->where($node->property('z')->equals(Query::literal('foo'), false)) - ->returning($node->property('z')->alias('foo')); - - $query = Query::new() - ->match($node) - ->call($sub) - ->returning(Query::variable('foo')); - - $this->assertEquals("MATCH (y:X) CALL { WITH y WHERE y.z = 'foo' RETURN y.z AS foo } RETURN foo", $query->toQuery()); - } - - public function provideLiteralData(): array - { - return [ - ['foobar', new StringLiteral('foobar')], - ['0', new StringLiteral('0')], - ['100', new StringLiteral('100')], - [0, new Decimal(0)], - [100, new Decimal(100)], - [10.0, new Decimal(10.0)], - [69420, new Decimal(69420)], - [10.0000000000000000000000000000001, new Decimal(10.0000000000000000000000000000001)], - [false, new Boolean(false)], - [true, new Boolean(true)], - ]; - } -} diff --git a/tests/Unit/RawExpressionTest.php b/tests/Unit/RawExpressionTest.php deleted file mode 100644 index a6acab05..00000000 --- a/tests/Unit/RawExpressionTest.php +++ /dev/null @@ -1,40 +0,0 @@ - 4)"); - - $this->assertSame("foobar(3 > 4)", $rawExpression->toQuery()); - } -} diff --git a/tests/Unit/RegexTest.php b/tests/Unit/RegexTest.php deleted file mode 100644 index bfa31c86..00000000 --- a/tests/Unit/RegexTest.php +++ /dev/null @@ -1,70 +0,0 @@ -getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b")); - - $this->assertSame("(a =~ b)", $regex->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $regex = new Regex($this->getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b"), false); - - $this->assertSame("a =~ b", $regex->toQuery()); - } - - public function testCannotBeNested(): void - { - $regex = new Regex($this->getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b")); - - $this->expectException(TypeError::class); - - $regex = new Regex($regex, $regex); - - $regex->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $regex = new Regex($this->getQueryConvertableMock(AnyType::class, "a"), $this->getQueryConvertableMock(AnyType::class, "b")); - - $regex->toQuery(); - } -} diff --git a/tests/Unit/StartsWithTest.php b/tests/Unit/StartsWithTest.php deleted file mode 100644 index e755c96b..00000000 --- a/tests/Unit/StartsWithTest.php +++ /dev/null @@ -1,70 +0,0 @@ -getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b")); - - $this->assertSame("(a STARTS WITH b)", $startsWith->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $startsWith = new StartsWith($this->getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b"), false); - - $this->assertSame("a STARTS WITH b", $startsWith->toQuery()); - } - - public function testCannotBeNested(): void - { - $startsWith = new StartsWith($this->getQueryConvertableMock(StringType::class, "a"), $this->getQueryConvertableMock(StringType::class, "b")); - - $this->expectException(TypeError::class); - - $startsWith = new StartsWith($startsWith, $startsWith); - - $startsWith->toQuery(); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $startsWith = new StartsWith($this->getQueryConvertableMock(AnyType::class, "a"), $this->getQueryConvertableMock(AnyType::class, "b")); - - $startsWith->toQuery(); - } -} diff --git a/tests/Unit/SubtractionTest.php b/tests/Unit/SubtractionTest.php deleted file mode 100644 index 7ad74e8c..00000000 --- a/tests/Unit/SubtractionTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15")); - - $this->assertSame("(10 - 15)", $subtraction->toQuery()); - - $subtraction = new Subtraction($subtraction, $subtraction); - - $this->assertSame("((10 - 15) - (10 - 15))", $subtraction->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $subtraction = new Subtraction($this->getQueryConvertableMock(NumeralType::class, "10"), $this->getQueryConvertableMock(NumeralType::class, "15"), false); - - $this->assertSame("10 - 15", $subtraction->toQuery()); - - $subtraction = new Subtraction($subtraction, $subtraction); - - $this->assertSame("(10 - 15 - 10 - 15)", $subtraction->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $subtraction = new Subtraction($this->getQueryConvertableMock(AnyType::class, "10"), $this->getQueryConvertableMock(AnyType::class, "15")); - - $subtraction->toQuery(); - } -} diff --git a/tests/Unit/TestHelper.php b/tests/Unit/TestHelper.php deleted file mode 100644 index 2c7fbfc8..00000000 --- a/tests/Unit/TestHelper.php +++ /dev/null @@ -1,40 +0,0 @@ -getMockBuilder($class)->disableOriginalConstructor()->getMock(); - $mock->method('toQuery')->willReturn($value); - - return $mock; - } -} diff --git a/tests/Unit/Traits/BooleanTypeTraitTest.php b/tests/Unit/Traits/BooleanTypeTraitTest.php deleted file mode 100644 index a78bcf59..00000000 --- a/tests/Unit/Traits/BooleanTypeTraitTest.php +++ /dev/null @@ -1,136 +0,0 @@ -a = new class () implements BooleanType { - use BooleanTypeTrait; - - public function toQuery(): string - { - return ''; - } - }; - $this->b = $this->getQueryConvertableMock(BooleanType::class, "false"); - } - - public function testAnd(): void - { - $and = $this->a->and($this->b); - - $this->assertInstanceOf(AndOperator::class, $and); - - $this->assertTrue($and->insertsParentheses()); - $this->assertEquals($this->a, $and->getLeft()); - $this->assertEquals($this->b, $and->getRight()); - } - - public function testAndNoParentheses(): void - { - $and = $this->a->and($this->b, false); - - $this->assertInstanceOf(AndOperator::class, $and); - - $this->assertFalse($and->insertsParentheses()); - $this->assertEquals($this->a, $and->getLeft()); - $this->assertEquals($this->b, $and->getRight()); - } - - public function testOr(): void - { - $or = $this->a->or($this->b); - - $this->assertInstanceOf(OrOperator::class, $or); - - $this->assertTrue($or->insertsParentheses()); - $this->assertEquals($this->a, $or->getLeft()); - $this->assertEquals($this->b, $or->getRight()); - } - - public function testOrNoParentheses(): void - { - $or = $this->a->or($this->b, false); - - $this->assertInstanceOf(OrOperator::class, $or); - - $this->assertFalse($or->insertsParentheses()); - $this->assertEquals($this->a, $or->getLeft()); - $this->assertEquals($this->b, $or->getRight()); - } - - public function testXor(): void - { - $xor = $this->a->xor($this->b); - - $this->assertInstanceOf(XorOperator::class, $xor); - - $this->assertTrue($xor->insertsParentheses()); - $this->assertEquals($this->a, $xor->getLeft()); - $this->assertEquals($this->b, $xor->getRight()); - } - - public function testXorNoParentheses(): void - { - $xor = $this->a->xor($this->b, false); - - $this->assertInstanceOf(XorOperator::class, $xor); - - $this->assertFalse($xor->insertsParentheses()); - $this->assertEquals($this->a, $xor->getLeft()); - $this->assertEquals($this->b, $xor->getRight()); - } - - public function testNot(): void - { - $not = $this->a->not(); - - $this->assertInstanceOf(Not::class, $not); - } -} diff --git a/tests/Unit/Traits/ComparableTypeTraitTest.php b/tests/Unit/Traits/ComparableTypeTraitTest.php deleted file mode 100644 index 64426bb4..00000000 --- a/tests/Unit/Traits/ComparableTypeTraitTest.php +++ /dev/null @@ -1,151 +0,0 @@ -a = new class () implements ComparableType { - use ComparableTypeTrait; - - public function toQuery(): string - { - return '10'; - } - }; - $this->b = $this->getQueryConvertableMock(ComparableType::class, "date({year: 2020, month: 12, day: 5})"); - } - - public function testGt(): void - { - $gt = $this->a->gt($this->b); - - $this->assertInstanceOf(GreaterThan::class, $gt); - - $this->assertTrue($gt->insertsParentheses()); - $this->assertEquals($this->a, $gt->getLeft()); - $this->assertEquals($this->b, $gt->getRight()); - } - - public function testGtNoParentheses(): void - { - $gt = $this->a->gt($this->b, false); - - $this->assertInstanceOf(GreaterThan::class, $gt); - - $this->assertFalse($gt->insertsParentheses()); - $this->assertEquals($this->a, $gt->getLeft()); - $this->assertEquals($this->b, $gt->getRight()); - } - - public function testGte(): void - { - $gte = $this->a->gte($this->b); - - $this->assertInstanceOf(GreaterThanOrEqual::class, $gte); - - $this->assertTrue($gte->insertsParentheses()); - $this->assertEquals($this->a, $gte->getLeft()); - $this->assertEquals($this->b, $gte->getRight()); - } - - public function testGteNoParentheses(): void - { - $gte = $this->a->gte($this->b, false); - - $this->assertInstanceOf(GreaterThanOrEqual::class, $gte); - - $this->assertFalse($gte->insertsParentheses()); - $this->assertEquals($this->a, $gte->getLeft()); - $this->assertEquals($this->b, $gte->getRight()); - } - - public function testLt(): void - { - $lt = $this->a->lt($this->b); - - $this->assertInstanceOf(LessThan::class, $lt); - - $this->assertTrue($lt->insertsParentheses()); - $this->assertEquals($this->a, $lt->getLeft()); - $this->assertEquals($this->b, $lt->getRight()); - } - - public function testLtNoParentheses(): void - { - $lt = $this->a->lt($this->b, false); - - $this->assertInstanceOf(LessThan::class, $lt); - - $this->assertFalse($lt->insertsParentheses()); - $this->assertEquals($this->a, $lt->getLeft()); - $this->assertEquals($this->b, $lt->getRight()); - } - - public function testLte(): void - { - $lte = $this->a->lte($this->b); - - $this->assertInstanceOf(LessThanOrEqual::class, $lte); - - $this->assertTrue($lte->insertsParentheses()); - $this->assertEquals($this->a, $lte->getLeft()); - $this->assertEquals($this->b, $lte->getRight()); - } - - public function testLteNoParentheses(): void - { - $lte = $this->a->lte($this->b, false); - - $this->assertInstanceOf(LessThanOrEqual::class, $lte); - - $this->assertFalse($lte->insertsParentheses()); - $this->assertEquals($this->a, $lte->getLeft()); - $this->assertEquals($this->b, $lte->getRight()); - } -} diff --git a/tests/Unit/Traits/HasNameTraitTest.php b/tests/Unit/Traits/HasNameTraitTest.php deleted file mode 100644 index e3152148..00000000 --- a/tests/Unit/Traits/HasNameTraitTest.php +++ /dev/null @@ -1,39 +0,0 @@ -hasName = new class () { - use HasNameTrait { - configureName as public; - generateName as public; - } - }; - } - - public function testHasName(): void - { - $this->assertMatchesRegularExpression('/var\w{32}/', $this->hasName->getName()); - } - - public function testGenerateName(): void - { - $this->assertMatchesRegularExpression('/var\w{32}/', $this->hasName->generateName()); - $this->assertMatchesRegularExpression('/x\w{16}/', $this->hasName->generateName('x', 16)); - } - - public function testConfigureName(): void - { - $this->hasName->configureName(null, 'y', 16); - - $this->assertMatchesRegularExpression('/y\w{16}/', $this->hasName->getName()); - } -} diff --git a/tests/Unit/Traits/HasPropertiesTraitTest.php b/tests/Unit/Traits/HasPropertiesTraitTest.php deleted file mode 100644 index 4bae3c3a..00000000 --- a/tests/Unit/Traits/HasPropertiesTraitTest.php +++ /dev/null @@ -1,71 +0,0 @@ -propertyTrait = new class () implements HasPropertiesType { - use HasPropertiesTrait { - initialiseProperties as public; - } - - public function toQuery(): string - { - return ''; - } - }; - } - - public function testGetProperties(): void - { - self::assertNull($this->propertyTrait->getProperties()); - } - - public function testWithPropertiesArray(): void - { - $this->propertyTrait->withProperties(['x' => Query::literal('y')]); - - self::assertEquals( - new PropertyMap(['x' => Query::literal('y')]), - $this->propertyTrait->getProperties() - ); - } - - public function testWithPropertiesMap(): void - { - $this->propertyTrait->withProperties(new PropertyMap(['x' => Query::literal('y')])); - - self::assertEquals( - new PropertyMap(['x' => Query::literal('y')]), - $this->propertyTrait->getProperties() - ); - } - - public function testWithProperty(): void - { - $this->propertyTrait->withProperty('x', Query::literal('y')); - $this->propertyTrait->withProperty('z', Query::literal('z')); - - self::assertEquals( - new PropertyMap(['x' => Query::literal('y'), 'z' => Query::literal('z')]), - $this->propertyTrait->getProperties() - ); - } - - public function testInitialise(): void - { - $this->propertyTrait->initialiseProperties(); - - self::assertEquals(new PropertyMap(), $this->propertyTrait->getProperties()); - } -} diff --git a/tests/Unit/Traits/HasVariableTraitTest.php b/tests/Unit/Traits/HasVariableTraitTest.php deleted file mode 100644 index cd7dc764..00000000 --- a/tests/Unit/Traits/HasVariableTraitTest.php +++ /dev/null @@ -1,42 +0,0 @@ -hasVariable = new class () { - use HasVariableTrait; - }; - } - - public function testDefaultGeneration(): void - { - self::assertNull($this->hasVariable->getVariable()); - self::assertNotNull($this->hasVariable->getName()); - - self::assertMatchesRegularExpression('/var\w{32}/', $this->hasVariable->getVariable()->getName()); - } - - public function testNamed(): void - { - $this->hasVariable->named('x'); - - self::assertSame($this->hasVariable->getVariable(), $this->hasVariable->getName()); - self::assertEquals('x', $this->hasVariable->getVariable()->getName()); - } - - public function testSetName(): void - { - $this->hasVariable->setName('x'); - - self::assertSame($this->hasVariable->getVariable(), $this->hasVariable->getName()); - self::assertEquals('x', $this->hasVariable->getVariable()->getName()); - } -} diff --git a/tests/Unit/Traits/ListTypeTraitTest.php b/tests/Unit/Traits/ListTypeTraitTest.php deleted file mode 100644 index db09eb7f..00000000 --- a/tests/Unit/Traits/ListTypeTraitTest.php +++ /dev/null @@ -1,60 +0,0 @@ -a = $this->getQueryConvertableMock(PropertyType::class, "a"); - $this->b = $this->getQueryConvertableMock(ListType::class, "[]"); - } - - public function testHas() - { - $has = $this->b->has($this->a); - - $this->assertInstanceOf(In::class, $has); - } -} diff --git a/tests/Unit/Traits/MapTypeTraitTest.php b/tests/Unit/Traits/MapTypeTraitTest.php deleted file mode 100644 index 992d9051..00000000 --- a/tests/Unit/Traits/MapTypeTraitTest.php +++ /dev/null @@ -1,53 +0,0 @@ -a = $this->getQueryConvertableMock(MapType::class, "{}"); - } - - public function testProperty() - { - $property = $this->a->property("foo"); - - $this->assertInstanceOf(Property::class, $property); - } -} diff --git a/tests/Unit/Traits/PropertyTypeTraitTest.php b/tests/Unit/Traits/PropertyTypeTraitTest.php deleted file mode 100644 index a25959a4..00000000 --- a/tests/Unit/Traits/PropertyTypeTraitTest.php +++ /dev/null @@ -1,155 +0,0 @@ -a = new class () implements PropertyType { - use PropertyTypeTrait; - - public function toQuery(): string - { - return '10'; - } - }; - $this->b = $this->getQueryConvertableMock(PropertyType::class, "15"); - $this->list = $this->getQueryConvertableMock(ListType::class, "['foobar']"); - } - - public function testEquals(): void - { - $equals = $this->a->equals($this->b); - - $this->assertInstanceOf(Equality::class, $equals); - - $this->assertTrue($equals->insertsParentheses()); - $this->assertEquals($this->a, $equals->getLeft()); - $this->assertEquals($this->b, $equals->getRight()); - } - - public function testEqualsNoParentheses(): void - { - $equals = $this->a->equals($this->b, false); - - $this->assertInstanceOf(Equality::class, $equals); - - $this->assertFalse($equals->insertsParentheses()); - $this->assertEquals($this->a, $equals->getLeft()); - $this->assertEquals($this->b, $equals->getRight()); - } - - public function testNotEquals(): void - { - $notEquals = $this->a->notEquals($this->b); - - $this->assertInstanceOf(Inequality::class, $notEquals); - - $this->assertTrue($notEquals->insertsParentheses()); - $this->assertEquals($this->a, $notEquals->getLeft()); - $this->assertEquals($this->b, $notEquals->getRight()); - } - - public function testNotEqualsNoParentheses(): void - { - $notEquals = $this->a->notEquals($this->b, false); - - $this->assertInstanceOf(Inequality::class, $notEquals); - - $this->assertFalse($notEquals->insertsParentheses()); - $this->assertEquals($this->a, $notEquals->getLeft()); - $this->assertEquals($this->b, $notEquals->getRight()); - } - - public function testIn(): void - { - $in = $this->a->in($this->list); - - $this->assertInstanceOf(In::class, $in); - - $this->assertTrue($in->insertsParentheses()); - $this->assertEquals($this->a, $in->getLeft()); - $this->assertEquals($this->list, $in->getRight()); - } - - public function testInNoParentheses(): void - { - $in = $this->a->in($this->list, false); - - $this->assertInstanceOf(In::class, $in); - - $this->assertFalse($in->insertsParentheses()); - $this->assertEquals($this->a, $in->getLeft()); - $this->assertEquals($this->list, $in->getRight()); - } - - public function testIsNull(): void - { - $test = $this->a->isNull(); - - $this->assertInstanceOf(IsNull::class, $test); - - $this->assertEquals($this->a, $test->getExpression()); - } - - public function testIsNotNull(): void - { - $test = $this->a->isNotNull(); - - $this->assertInstanceOf(IsNotNull::class, $test); - - $this->assertEquals($this->a, $test->getExpression()); - } -} diff --git a/tests/Unit/VariableTest.php b/tests/Unit/VariableTest.php deleted file mode 100644 index 9cc78dc3..00000000 --- a/tests/Unit/VariableTest.php +++ /dev/null @@ -1,85 +0,0 @@ -assertSame($expected, $variable->toQuery()); - } - - public function testEmptyConstructor() - { - $variable = new Variable(); - - $this->assertMatchesRegularExpression('/var[0-9a-f]+/', $variable->toQuery()); - - $variable = new Variable(null); - - $this->assertMatchesRegularExpression('/var[0-9a-f]+/', $variable->toQuery()); - } - - /** - * @dataProvider providePropertyData - * @param string $variable - * @param string $property - * @param Property $expected - */ - public function testProperty(string $variable, string $property, Property $expected) - { - $variable = new Variable($variable); - $property = $variable->property($property); - - $this->assertEquals($expected, $property); - } - - public function provideToQueryData(): array - { - return [ - ["a", "a"], - ["b", "b"], - ]; - } - - public function providePropertyData(): array - { - return [ - ["a", "a", new Property(new Variable("a"), "a")], - ["a", "b", new Property(new Variable("a"), "b")], - ]; - } -} diff --git a/tests/Unit/XorOperatorTest.php b/tests/Unit/XorOperatorTest.php deleted file mode 100644 index edafb36f..00000000 --- a/tests/Unit/XorOperatorTest.php +++ /dev/null @@ -1,67 +0,0 @@ -getQueryConvertableMock(BooleanType::class, "true"), $this->getQueryConvertableMock(BooleanType::class, "false")); - - $this->assertSame("(true XOR false)", $xor->toQuery()); - - $xor = new XorOperator($xor, $xor); - - $this->assertSame("((true XOR false) XOR (true XOR false))", $xor->toQuery()); - } - - public function testToQueryNoParentheses(): void - { - $xor = new XorOperator($this->getQueryConvertableMock(BooleanType::class, "true"), $this->getQueryConvertableMock(BooleanType::class, "false"), false); - - $this->assertSame("true XOR false", $xor->toQuery()); - - $xor = new XorOperator($xor, $xor); - - $this->assertSame("(true XOR false XOR true XOR false)", $xor->toQuery()); - } - - public function testDoesNotAcceptAnyTypeAsOperands(): void - { - $this->expectException(TypeError::class); - - $and = new XorOperator($this->getQueryConvertableMock(AnyType::class, "true"), $this->getQueryConvertableMock(AnyType::class, "false")); - - $and->toQuery(); - } -} diff --git a/tests/end-to-end/ExamplesTest.php b/tests/end-to-end/ExamplesTest.php new file mode 100644 index 00000000..fa75a78d --- /dev/null +++ b/tests/end-to-end/ExamplesTest.php @@ -0,0 +1,755 @@ +withProperties(["name" => "Tom Hanks"]); + $coActors = node(); + + $statement = query() + ->match($tom->relationshipTo(node(), "ACTED_IN")->relationshipFrom($coActors, "ACTED_IN")) + ->returning($coActors->property("name")) + ->build(); + + $this->assertStringMatchesFormat("MATCH (:Person {name: 'Tom Hanks'})-[:ACTED_IN]->()<-[:ACTED_IN]-(%s) RETURN %s.name", $statement); + } + + public function testCallSubqueryClauseExample1(): void + { + $query = query() + ->call(static function (Query $query): void + { + $query->create(node("Person")); + }) + ->build(); + + $this->assertSame("CALL { CREATE (:Person) }", $query); + } + + public function testCallSubqueryClauseExample2(): void + { + $subQuery = query()->create(node("Person")); + $query = query() + ->call($subQuery) + ->build(); + + $this->assertSame("CALL { CREATE (:Person) }", $query); + } + + public function testCallSubqueryClauseExample3(): void + { + $person = variable(); + $query = query() + ->match(node('Person')->withVariable($person)) + ->call(static function (Query $query) use ($person): void + { + $query->remove($person->labeled('Person')); + }, [$person]) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Person) CALL { WITH %s REMOVE %s:Person }", $query); + } + + public function testCallProcedureClauseExample1(): void + { + $statement = query() + ->callProcedure("apoc.json") + ->build(); + + $this->assertSame("CALL `apoc.json`()", $statement); + } + + public function testCallProcedureClauseExample2(): void + { + $statement = query() + ->callProcedure("dbms.procedures", [ + variable('name'), + variable('signature'), + ]) + ->build(); + + $this->assertSame("CALL `dbms.procedures`() YIELD name, signature", $statement); + } + + public function testCallProcedureClauseExample3(): void + { + $procedure = Procedure::raw("dbms.security.createUser", ['example_username', 'example_password', false]); + $statement = query() + ->callProcedure($procedure) + ->build(); + + $this->assertSame("CALL `dbms.security.createUser`('example_username', 'example_password', false)", $statement); + } + + public function testCreateClauseExample1(): void + { + $query = query() + ->create(node("Person")) + ->build(); + + $this->assertSame("CREATE (:Person)", $query); + } + + public function testCreateClauseExample2(): void + { + $query = query() + ->create(node("Person")->withVariable('n')->withProperties([ + 'name' => 'Marijn', + 'title' => 'Maintainer', + ])) + ->build(); + + $this->assertSame("CREATE (n:Person {name: 'Marijn', title: 'Maintainer'})", $query); + } + + public function testCreateClauseExample3(): void + { + $query = query() + ->create([node("Person"), node("Animal")]) + ->build(); + + $this->assertSame("CREATE (:Person), (:Animal)", $query); + } + + public function testDeleteClauseExample1(): void + { + $unknown = node('Person')->withProperties([ + 'name' => 'UNKNOWN', + ]); + + $query = query() + ->match($unknown) + ->delete($unknown) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Person {name: 'UNKNOWN'}) DELETE %s", $query); + } + + public function testDeleteClauseExample2(): void + { + $everything = node(); + + $query = query() + ->match($everything) + ->delete($everything) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) DELETE %s", $query); + } + + public function testDeleteClauseExample3(): void + { + $everything = node(); + + $query = query() + ->match($everything) + ->delete($everything, true) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) DETACH DELETE %s", $query); + } + + public function testDeleteClauseExample4(): void + { + $everything = node(); + + $query = query() + ->match($everything) + ->detachDelete($everything) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) DETACH DELETE %s", $query); + } + + public function testDeleteClauseExample5(): void + { + $persons = node('Person'); + $animals = node('Animal'); + + $query = query() + ->match([$persons, $animals]) + ->delete([$persons, $animals]) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Person), (%s:Animal) DELETE %s, %s", $query); + } + + public function testLimitClauseExample1(): void + { + $persons = node('Person'); + $query = query() + ->match($persons) + ->returning($persons->property('name')) + ->limit(3) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Person) RETURN %s.name LIMIT 3", $query); + } + + public function testMatchClauseExample1(): void + { + $n = node(); + $query = query() + ->match($n) + ->returning($n) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) RETURN %s", $query); + } + + public function testMatchClauseExample2(): void + { + $movie = node("Movie"); + $query = query() + ->match($movie) + ->returning($movie->property("title")) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Movie) RETURN %s.title", $query); + } + + public function testMatchClauseExample3(): void + { + $movie = node(); + $query = query() + ->match(node()->withProperties(['name' => 'Oliver Stone'])->relationshipUni($movie)) + ->returning($movie->property("title")) + ->build(); + + $this->assertStringMatchesFormat("MATCH ({name: 'Oliver Stone'})--(%s) RETURN %s.title", $query); + } + + public function testMatchClauseExample4(): void + { + $movie = node('Movie'); + $query = query() + ->match(node('Person')->withProperties(['name' => 'Oliver Stone'])->relationshipUni($movie)) + ->returning($movie->property("title")) + ->build(); + + $this->assertStringMatchesFormat("MATCH (:Person {name: 'Oliver Stone'})--(%s:Movie) RETURN %s.title", $query); + } + + public function testMatchClauseExample5(): void + { + $n = node()->addLabel('Movie', 'Person'); + $query = query() + ->match($n) + ->returning(['name' => $n->property("name"), 'title' => $n->property("title")]) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Movie:Person) RETURN %s.name AS name, %s.title AS title", $query); + } + + public function testMatchClauseExample6(): void + { + $movie = node(); + $query = query() + ->match( + node('Person')->withProperties(['name' => 'Oliver Stone'])->relationshipTo($movie) + ) + ->returning($movie->property('title')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (:Person {name: 'Oliver Stone'})-->(%s) RETURN %s.title", $query); + } + + public function testMatchClauseExample7(): void + { + $r = relationshipTo(); + $query = query() + ->match(node('Person')->withProperties(['name' => 'Oliver Stone'])->relationship($r, node())) + ->returning($r) + ->build(); + + $this->assertStringMatchesFormat("MATCH (:Person {name: 'Oliver Stone'})-[%s]->() RETURN %s", $query); + } + + public function testMatchClauseExample8(): void + { + $actor = node(); + $query = query() + ->match( + node('Movie') + ->withProperties(['title' => 'Wall Street']) + ->relationshipFrom($actor, 'ACTED_IN') + ) + ->returning($actor->property('name')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (:Movie {title: 'Wall Street'})<-[:ACTED_IN]-(%s) RETURN %s.name", $query); + } + + public function testMatchClauseExample9(): void + { + $person = node(); + $relationship = relationshipFrom()->withTypes(['ACTED_IN', 'DIRECTED']); + $query = query() + ->match( + node('Movie') + ->withProperties(['title' => 'Wall Street']) + ->relationship($relationship, $person) + ) + ->returning($person->property('name')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (:Movie {title: 'Wall Street'})<-[:ACTED_IN|DIRECTED]-(%s) RETURN %s.name", $query); + } + + public function testMatchClauseExample10(): void + { + $actor = node(); + $relationship = relationshipFrom()->withTypes(['ACTED_IN']); + $query = query() + ->match( + node('Movie') + ->withProperties(['title' => 'Wall Street']) + ->relationship($relationship, $actor) + ) + ->returning($relationship->property('role')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (:Movie {title: 'Wall Street'})<-[%s:ACTED_IN]-() RETURN %s.role", $query); + } + + public function testMatchClauseExample11(): void + { + $charlie = node('Person')->withProperties(['name' => 'Charlie Sheen']); + $rob = node('Person')->withProperties(['name' => 'Rob Reiner']); + + $query = query() + ->match([$charlie, $rob]) + ->create( + node() + ->withVariable($rob->getVariable()) + ->relationshipTo( + node()->withVariable($charlie->getVariable()), + 'TYPE INCLUDING A SPACE' + ) + ) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Person {name: 'Charlie Sheen'}), (%s:Person {name: 'Rob Reiner'}) CREATE (%s)-[:`TYPE INCLUDING A SPACE`]->(%s)", $query); + } + + public function testMatchClauseExample12(): void + { + $movie = node(); + $director = node(); + + $query = query() + ->match( + node() + ->withProperties(['name' => 'Charlie Sheen']) + ->relationshipTo($movie, 'ACTED_IN') + ->relationshipFrom($director, 'DIRECTED') + ) + ->returning([$movie->property('title'), $director->property('name')]) + ->build(); + + $this->assertStringMatchesFormat("MATCH ({name: 'Charlie Sheen'})-[:ACTED_IN]->(%s)<-[:DIRECTED]-(%s) RETURN %s.title, %s.name", $query); + } + + public function testMatchClauseExample13(): void + { + $movie = node('Movie'); + $r = relationshipUni()->addType('ACTED_IN')->withMinHops(1)->withMaxHops(3); + + $query = query() + ->match(node()->withProperties(['name' => 'Charlie Sheen'])->relationship($r, $movie)) + ->returning($movie->property('title')) + ->build(); + + $this->assertStringMatchesFormat("MATCH ({name: 'Charlie Sheen'})-[:ACTED_IN*1..3]-(%s:Movie) RETURN %s.title", $query); + } + + public function testMatchClauseExample14(): void + { + $p = node()->withProperties(['name' => 'Michael Douglas'])->relationshipTo(node()); + + $query = query() + ->match($p) + ->returning($p) + ->build(); + + $this->assertStringMatchesFormat("MATCH %s = ({name: 'Michael Douglas'})-->() RETURN %s", $query); + } + + public function testMergeClauseExample1(): void + { + $robert = node('Critic'); + + $query = query() + ->merge($robert) + ->returning($robert) + ->build(); + + $this->assertStringMatchesFormat("MERGE (%s:Critic) RETURN %s", $query); + } + + public function testMergeClauseExample2(): void + { + $keanu = node('Person')->withProperties(['name' => 'Keanu Reeves']); + + $query = query() + ->merge($keanu, (new SetClause())->add($keanu->property('created')->replaceWith(procedure()::raw('timestamp')))) + ->returning([$keanu->property('name'), $keanu->property('created')]) + ->build(); + + $this->assertStringMatchesFormat("MERGE (%s:Person {name: 'Keanu Reeves'}) ON CREATE SET %s.created = timestamp() RETURN %s.name, %s.created", $query); + } + + public function testMergeClauseExample3(): void + { + $keanu = node('Person')->withProperties(['name' => 'Keanu Reeves']); + + $query = query() + ->merge($keanu, null, (new SetClause())->add($keanu->property('created')->replaceWith(procedure()::raw('timestamp')))) + ->returning([$keanu->property('name'), $keanu->property('created')]) + ->build(); + + $this->assertStringMatchesFormat("MERGE (%s:Person {name: 'Keanu Reeves'}) ON MATCH SET %s.created = timestamp() RETURN %s.name, %s.created", $query); + } + + public function testOptionalMatchClauseExample1(): void + { + $movies = node("Movie"); + $query = query() + ->optionalMatch($movies) + ->build(); + + $this->assertSame("OPTIONAL MATCH (:Movie)", $query); + } + + public function testOrderByClauseExample1(): void + { + $n = node(); + $query = query() + ->match($n) + ->returning([$n->property('name'), $n->property('age')]) + ->orderBy($n->property('name')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) RETURN %s.name, %s.age ORDER BY %s.name", $query); + } + + public function testOrderByClauseExample2(): void + { + $n = node(); + $query = query() + ->match($n) + ->returning([$n->property('name'), $n->property('age')]) + ->orderBy([$n->property('age'), $n->property('name')]) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) RETURN %s.name, %s.age ORDER BY %s.age, %s.name", $query); + } + + public function testOrderByClauseExample3(): void + { + $n = node(); + $query = query() + ->match($n) + ->returning([$n->property('name'), $n->property('age')]) + ->orderBy([$n->property('name')], true) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) RETURN %s.name, %s.age ORDER BY %s.name DESCENDING", $query); + } + + public function testRemoveClauseExample1(): void + { + $a = node()->withProperties(['name' => 'Andy']); + $query = query() + ->match($a) + ->remove($a->property('age')) + ->returning([$a->property('name'), $a->property('age')]) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {name: 'Andy'}) REMOVE %s.age RETURN %s.name, %s.age", $query); + } + + public function testRemoveClauseExample2(): void + { + $n = node()->withProperties(['name' => 'Peter']); + $query = query() + ->match($n) + ->remove($n->labeled('German')) + ->returning($n->property('name')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {name: 'Peter'}) REMOVE %s:German RETURN %s.name", $query); + } + + public function testRemoveClauseExample3(): void + { + $n = node()->withProperties(['name' => 'Peter']); + $query = query() + ->match($n) + ->remove($n->labeled('German', 'Swedish')) + ->returning($n->property('name')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {name: 'Peter'}) REMOVE %s:German:Swedish RETURN %s.name", $query); + } + + public function testReturnClauseExample1(): void + { + $n = node()->withProperties(['name' => 'B']); + $query = query() + ->match($n) + ->returning($n) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {name: 'B'}) RETURN %s", $query); + } + + public function testReturnClauseExample2(): void + { + $r = relationshipTo()->addType('KNOWS'); + $query = query() + ->match(node()->withProperties(['name' => 'A'])->relationship($r, node())) + ->returning($r) + ->build(); + + $this->assertStringMatchesFormat("MATCH ({name: 'A'})-[%s:KNOWS]->() RETURN %s", $query); + } + + public function testReturnClauseExample3(): void + { + $n = node()->withProperties(['name' => 'A']); + $query = query() + ->match($n) + ->returning($n->property('name')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {name: 'A'}) RETURN %s.name", $query); + } + + public function testReturnClauseExample4(): void + { + $n = node()->withVariable('This isn\'t a common variable name'); + $query = query() + ->match($n) + ->where($n->property('name')->equals('A')) + ->returning($n->property('happy')) + ->build(); + + $this->assertSame("MATCH (`This isn't a common variable name`) WHERE (`This isn't a common variable name`.name = 'A') RETURN `This isn't a common variable name`.happy", $query); + } + + public function testReturnClauseExample5(): void + { + $a = node()->withProperties(['name' => 'A']); + $query = query() + ->match($a) + ->returning($a->property('age')->alias('SomethingTotallyDifferent')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {name: 'A'}) RETURN %s.age AS SomethingTotallyDifferent", $query); + } + + public function testReturnClauseExample6(): void + { + $a = node()->withProperties(['name' => 'A']); + $query = query() + ->match($a) + ->returning([$a->property('age')->gt(30), "I'm a literal"]) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {name: 'A'}) RETURN (%s.age > 30), 'I\\'m a literal'", $query); + } + + public function testReturnClauseExample7(): void + { + $b = node(); + $query = query() + ->match(node()->withProperties(['name' => 'A'])->relationshipTo($b)) + ->returning($b, true) + ->build(); + + $this->assertStringMatchesFormat("MATCH ({name: 'A'})-->(%s) RETURN DISTINCT %s", $query); + } + + public function testSetClauseExample1(): void + { + $n = node()->withProperties(['name' => 'Andy']); + $query = query() + ->match($n) + ->set($n->property('surname')->replaceWith('Taylor')) + ->returning([$n->property('name'), $n->property('surname')]) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {name: 'Andy'}) SET %s.surname = 'Taylor' RETURN %s.name, %s.surname", $query); + } + + public function testUnionClauseExample1(): void + { + $actor = node('Actor'); + $movie = node('Movie'); + + $query = query() + ->match($actor) + ->returning($actor->property('name')->alias('name')) + ->union( + query() + ->match($movie) + ->returning($movie->property('title')->alias('name')), + true + ) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Actor) RETURN %s.name AS name UNION ALL MATCH (%s:Movie) RETURN %s.title AS name", $query); + } + + public function testUnionClauseExample2(): void + { + $actor = node('Actor'); + $movie = node('Movie'); + + $query = query() + ->match($actor) + ->returning($actor->property('name')->alias('name')) + ->union( + query() + ->match($movie) + ->returning($movie->property('title')->alias('name')) + ) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Actor) RETURN %s.name AS name UNION MATCH (%s:Movie) RETURN %s.title AS name", $query); + } + + public function testSkipClauseExample1(): void + { + $n = node(); + $query = query() + ->match($n) + ->returning($n->property('name')) + ->orderBy($n->property('name')) + ->skip(3) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) RETURN %s.name ORDER BY %s.name SKIP 3", $query); + } + + public function testSkipClauseExample2(): void + { + $n = node(); + $query = query() + ->match($n) + ->returning($n->property('name')) + ->orderBy($n->property('name')) + ->skip(1) + ->limit(2) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) RETURN %s.name ORDER BY %s.name SKIP 1 LIMIT 2", $query); + } + + public function testSkipClauseExample3(): void + { + $n = node(); + $query = query() + ->match($n) + ->returning($n->property('name')) + ->orderBy($n->property('name')) + ->skip(integer(5)->exponentiate(2)) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) RETURN %s.name ORDER BY %s.name SKIP (5 ^ 2)", $query); + } + + public function testWhereClauseExample1(): void + { + $n = node('Person'); + $query = query() + ->match($n) + ->where($n->property('name')->equals('Peter')) + ->returning($n) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Person) WHERE (%s.name = 'Peter') RETURN %s", $query); + } + + public function testWhereClauseExample2(): void + { + $n = node(); + $query = query() + ->match($n) + ->where($n->labeled('Person')) + ->returning($n) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s) WHERE %s:Person RETURN %s", $query); + } + + public function testCombiningClausesExample1(): void + { + $nineties = node("Movie"); + $expression = $nineties->property('released')->gte(1990)->and($nineties->property('released')->lt(2000)); + + $statement = query() + ->match($nineties) + ->where($expression) + ->returning($nineties->property("title")) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Movie) WHERE ((%s.released >= 1990) AND (%s.released < 2000)) RETURN %s.title", $statement); + } + + public function testExpressions1(): void + { + $released = variable("nineties")->property("released"); + $expression = $released->gte(1990)->and($released->lt(2000)); + + $this->assertSame("((nineties.released >= 1990) AND (nineties.released < 2000))", $expression->toQuery()); + } + + public function testExpressions2(): void + { + $name = variable("actor")->property("name"); + $expression = $name->notEquals("Tom Hanks"); + + $this->assertSame("(actor.name <> 'Tom Hanks')", $expression->toQuery()); + } + + public function testExpressions3(): void + { + $released = variable("nineties")->property("released"); + $expression = $released->gte(1990)->and(raw("(nineties IS NOT NULL)")); + + $this->assertSame("((nineties.released >= 1990) AND (nineties IS NOT NULL))", $expression->toQuery()); + } +} diff --git a/tests/end-to-end/MoviesTest.php b/tests/end-to-end/MoviesTest.php new file mode 100644 index 00000000..9ffde5da --- /dev/null +++ b/tests/end-to-end/MoviesTest.php @@ -0,0 +1,196 @@ +match($movie) + ->returning($movie) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Movie) RETURN %s", $query); + } + + public function testFindActorNamedTomHanks(): void + { + $tom = node()->withProperties([ + 'name' => 'Tom Hanks', + ]); + + $query = query() + ->match($tom) + ->returning($tom) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {name: 'Tom Hanks'}) RETURN %s", $query); + } + + public function testFindTheMovieWithTitleCloudAtlas(): void + { + $cloudAtlas = node()->withProperties([ + 'title' => 'Cloud Atlas', + ]); + + $query = query() + ->match($cloudAtlas) + ->returning($cloudAtlas) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s {title: 'Cloud Atlas'}) RETURN %s", $query); + } + + public function testFind10People(): void + { + $people = node('Person'); + + $query = query() + ->match($people) + ->returning($people->property('name')) + ->limit(10) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Person) RETURN %s.name LIMIT 10", $query); + } + + public function testFindMoviesReleasedInThe1990s(): void + { + $nineties = node('Movie'); + $query = query() + ->match($nineties) + ->where([ + $nineties->property('released')->gte(1990), + $nineties->property('released')->lt(2000), + ]) + ->returning($nineties->property('title')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Movie) WHERE ((%s.released >= 1990) AND (%s.released < 2000)) RETURN %s.title", $query); + } + + public function testListAllTomHanksMovies(): void + { + $movies = node(); + $tom = node('Person')->withProperties(['name' => 'Tom Hanks']); + + $query = query() + ->match($tom->relationshipTo($movies, 'ACTED_IN')) + ->returning([$tom, $movies]) + ->build(); + + $this->assertStringMatchesFormat("MATCH (%s:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(%s) RETURN %s, %s", $query); + } + + public function testWhoDirectedCloudAtlas(): void + { + $directors = node(); + $cloudAtlas = node()->withProperties(['title' => 'Cloud Atlas']); + + $query = query() + ->match($cloudAtlas->relationshipFrom($directors, 'DIRECTED')) + ->returning($directors->property('name')) + ->build(); + + $this->assertStringMatchesFormat("MATCH ({title: 'Cloud Atlas'})<-[:DIRECTED]-(%s) RETURN %s.name", $query); + } + + public function testTomHanksCoActors(): void + { + $coActors = node(); + $tom = node('Person')->withProperties(['name' => 'Tom Hanks']); + + $query = query() + ->match($tom->relationshipTo(node(), 'ACTED_IN')->relationshipFrom($coActors, 'ACTED_IN')) + ->returning($coActors->property('name')) + ->build(); + + $this->assertStringMatchesFormat("MATCH (:Person {name: 'Tom Hanks'})-[:ACTED_IN]->()<-[:ACTED_IN]-(%s) RETURN %s.name", $query); + } + + public function testMoviesAndActorsUpTo4HopsAwayFromKevinBacon(): void + { + $hollywood = node(); + $bacon = node('Person')->withProperties([ + 'name' => 'Kevin Bacon', + ]); + + $relation = relationshipUni() + ->withMinHops(1) + ->withMaxHops(4); + + $query = query() + ->match($bacon->relationship($relation, $hollywood)) + ->returning($hollywood, true); + + $this->assertStringMatchesFormat('MATCH (:Person {name: \'Kevin Bacon\'})-[*1..4]-(%s) RETURN DISTINCT %s', $query->toQuery()); + } + + public function testFindSomeoneToIntroduceTomHanksToTomCruise(): void + { + $tom = node('Person')->withProperties([ + 'name' => 'Tom Hanks', + ]); + + $cruise = node('Person')->withProperties([ + 'name' => 'Tom Cruise', + ]); + + $m = node(); + $m2 = node(); + $coActors = node(); + + $query = query() + ->match([ + $tom->relationshipTo($m, 'ACTED_IN')->relationshipFrom($coActors, 'ACTED_IN'), + $coActors->relationshipTo($m2, 'ACTED_IN')->relationshipFrom($cruise, 'ACTED_IN'), + ]) + ->returning([$tom, $m, $coActors, $m2, $cruise]); + + $this->assertStringMatchesFormat('MATCH (%s:Person {name: \'Tom Hanks\'})-[:ACTED_IN]->(%s)<-[:ACTED_IN]-(%s), (%s)-[:ACTED_IN]->(%s)<-[:ACTED_IN]-(%s:Person {name: \'Tom Cruise\'}) RETURN %s, %s, %s, %s', $query->toQuery()); + } + + public function testDeleteAllMovieAndPersonNodes(): void + { + $n = node(); + + $query = query() + ->match($n) + ->delete($n, true); + + $this->assertStringMatchesFormat('MATCH (%s) DETACH DELETE %s', $query->toQuery()); + } + + public function testProveThatMovieGraphIsGone(): void + { + $n = node(); + + $query = query() + ->match($n) + ->returning($n); + + $this->assertStringMatchesFormat('MATCH (%s) RETURN %s', $query->toQuery()); + } +} diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/Clauses/CallClauseTest.php b/tests/unit/Clauses/CallClauseTest.php new file mode 100644 index 00000000..15bd27b2 --- /dev/null +++ b/tests/unit/Clauses/CallClauseTest.php @@ -0,0 +1,147 @@ +assertEquals('', $clause->toQuery()); + } + + public function testCallClauseWithEmptySubqueryIsEmpty(): void + { + $query = Query::new(); + + $clause = new CallClause(); + $clause->withSubQuery($query); + + $this->assertSame('', $clause->toQuery()); + + $clause->addWithVariable(Query::variable('x')); + + $this->assertSame('', $clause->toQuery()); + } + + public function testCallClauseWithoutWithDoesNotHaveWithStatement(): void + { + $query = Query::new()->match(Query::node('testing')); + + $clause = new CallClause(); + $clause->withSubQuery($query); + + $this->assertSame('CALL { ' . $query->toQuery() . ' }', $clause->toQuery()); + } + + public function testCallClauseFilled(): void + { + $query = Query::new()->match(Query::node('X')->withVariable('x'))->returning(Query::rawExpression('*')); + + $clause = new CallClause(); + $clause->withSubQuery($query); + + $this->assertSame('CALL { MATCH (x:X) RETURN * }', $clause->toQuery()); + } + + public function testCallClauseWithVariables(): void + { + $query = Query::new()->match(Query::node('X')->withVariable('x'))->returning(Query::rawExpression('*')); + + $clause = new CallClause(); + + $clause->withSubQuery($query); + $clause->addWithVariable(Query::variable('x')); + + $this->assertSame('CALL { WITH x MATCH (x:X) RETURN * }', $clause->toQuery()); + + $clause->addWithVariable(Query::variable('y')); + + $this->assertSame('CALL { WITH x, y MATCH (x:X) RETURN * }', $clause->toQuery()); + } + + public function testAddWithVariableSingleCall(): void + { + $clause = new CallClause(); + $clause->withSubQuery(Query::new()->match(Query::node('x'))); + + $clause->addWithVariable(Query::variable('a'), Query::variable('b')); + + $this->assertSame('CALL { WITH a, b MATCH (:x) }', $clause->toQuery()); + } + + public function testAddWithVariableSting(): void + { + $clause = new CallClause(); + $clause->withSubQuery(Query::new()->match(Query::node('x'))); + + $clause->addWithVariable('a'); + + $this->assertSame('CALL { WITH a MATCH (:x) }', $clause->toQuery()); + } + + public function testGetSubQuery(): void + { + $clause = new CallClause(); + $subQuery = Query::new()->match(Query::node('x')); + + $clause->withSubQuery($subQuery); + + $this->assertSame($subQuery, $clause->getSubQuery()); + } + + public function testGetWithVariables(): void + { + $clause = new CallClause(); + + $a = Query::variable('a'); + $b = Query::variable('b'); + $c = Query::variable('c'); + + $clause->addWithVariable($a, $b); + $clause->addWithVariable($c); + + $this->assertSame([$a, $b, $c], $clause->getWithVariables()); + } + + public function testWithSubQueryReturnsSameInstance(): void + { + $expected = new CallClause(); + $actual = $expected->withSubQuery(Query::new()); + + $this->assertSame($expected, $actual); + } + + public function testAddWithVariableReturnsSameInstance(): void + { + $expected = new CallClause(); + $actual = $expected->addWithVariable('a'); + + $this->assertSame($expected, $actual); + + $actual = $expected->addWithVariable('a', 'b'); + + $this->assertSame($expected, $actual); + } + + public function testCanBeEmpty(): void + { + $clause = new CallClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/CallProcedureClauseTest.php b/tests/unit/Clauses/CallProcedureClauseTest.php new file mode 100644 index 00000000..38920c33 --- /dev/null +++ b/tests/unit/Clauses/CallProcedureClauseTest.php @@ -0,0 +1,163 @@ +assertSame("", $callProcedureClause->toQuery()); + } + + public function testSetProcedure(): void + { + $procedure = Procedure::localtime(); + + $callProcedureClause = new CallProcedureClause(); + $callProcedureClause->setProcedure($procedure); + + $this->assertSame("CALL localtime()", $callProcedureClause->toQuery()); + } + + public function testAddSingleYield(): void + { + $procedure = Procedure::localtime(); + + $callProcedureClause = new CallProcedureClause(); + $callProcedureClause->addYield(Query::variable('a')); + $callProcedureClause->setProcedure($procedure); + + $this->assertSame("CALL localtime() YIELD a", $callProcedureClause->toQuery()); + } + + public function testAddMultipleYields(): void + { + $procedure = Procedure::localtime(); + + $callProcedureClause = new CallProcedureClause(); + $callProcedureClause->addYield(Query::variable('a')); + $callProcedureClause->addYield(Query::variable('b')); + $callProcedureClause->setProcedure($procedure); + + $this->assertSame("CALL localtime() YIELD a, b", $callProcedureClause->toQuery()); + } + + public function testAddMultipleYieldsSingleCall(): void + { + $procedure = Procedure::localtime(); + + $callProcedureClause = new CallProcedureClause(); + $callProcedureClause->addYield(Query::variable('a'), Query::variable('b')); + $callProcedureClause->setProcedure($procedure); + + $this->assertSame("CALL localtime() YIELD a, b", $callProcedureClause->toQuery()); + } + + public function testAddYieldString(): void + { + $procedure = Procedure::localtime(); + + $callProcedureClause = new CallProcedureClause(); + $callProcedureClause->addYield('a'); + $callProcedureClause->setProcedure($procedure); + + $this->assertSame("CALL localtime() YIELD a", $callProcedureClause->toQuery()); + } + + public function testAddYieldAlias(): void + { + $procedure = Procedure::localtime(); + + $callProcedureClause = new CallProcedureClause(); + $callProcedureClause->addYield(new Alias(new Variable('a'), new Variable('b'))); + $callProcedureClause->setProcedure($procedure); + + $this->assertSame("CALL localtime() YIELD a AS b", $callProcedureClause->toQuery()); + } + + public function testAddYieldWithoutProcedure(): void + { + $callProcedureClause = new CallProcedureClause(); + $callProcedureClause->addYield('a'); + + $this->assertSame("", $callProcedureClause->toQuery()); + } + + public function testSetProcedureReturnsSameInstance(): void + { + $expected = new CallProcedureClause(); + $actual = $expected->setProcedure(Procedure::localtime()); + + $this->assertSame($expected, $actual); + } + + public function testAddYieldReturnsSameInstance(): void + { + $expected = new CallProcedureClause(); + $actual = $expected->addYield('a'); + + $this->assertSame($expected, $actual); + + $actual = $expected->addYield('b', 'c'); + + $this->assertSame($expected, $actual); + } + + public function testGetProcedure(): void + { + $procedure = Procedure::localtime(); + + $callProcedureClause = new CallProcedureClause(); + + $this->assertNull($callProcedureClause->getProcedure()); + + $callProcedureClause->setProcedure($procedure); + + $this->assertSame($procedure, $callProcedureClause->getProcedure()); + } + + public function testGetYields(): void + { + $callProcedureClause = new CallProcedureClause(); + + $this->assertEmpty($callProcedureClause->getYields()); + + $a = Query::variable('a'); + + $callProcedureClause->addYield($a); + + $this->assertSame([$a], $callProcedureClause->getYields()); + + $b = Query::variable('b'); + $c = Query::variable('c'); + + $callProcedureClause->addYield($b, $c); + + $this->assertSame([$a, $b, $c], $callProcedureClause->getYields()); + } + + public function testCanBeEmpty(): void + { + $clause = new CallProcedureClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/ClauseTest.php b/tests/unit/Clauses/ClauseTest.php new file mode 100644 index 00000000..0e17b7e1 --- /dev/null +++ b/tests/unit/Clauses/ClauseTest.php @@ -0,0 +1,53 @@ +clause = $this->getMockForAbstractClass(Clause::class); + } + + public function testCanBeEmptyDefaultFalse(): void + { + $this->assertFalse($this->clause->canBeEmpty()); + } + + public function testEmptyClauseReturnsEmptyQuery(): void + { + $this->clause->method('getClause')->willReturn(''); + $this->clause->method('getSubject')->willReturn('body'); + + $this->assertSame('', $this->clause->toQuery()); + } + + public function testEmptySubjectReturnsEmptyQueryIfCannotBeEmpty(): void + { + $this->clause->method('getClause')->willReturn('CLAUSE'); + $this->clause->method('getSubject')->willReturn(''); + + $this->assertSame('', $this->clause->toQuery()); + } + + public function testToQuery(): void + { + $this->clause->method('getClause')->willReturn('CLAUSE'); + $this->clause->method('getSubject')->willReturn('body'); + + $this->assertSame('CLAUSE body', $this->clause->toQuery()); + } +} diff --git a/tests/unit/Clauses/CreateClauseTest.php b/tests/unit/Clauses/CreateClauseTest.php new file mode 100644 index 00000000..6f09d5e7 --- /dev/null +++ b/tests/unit/Clauses/CreateClauseTest.php @@ -0,0 +1,110 @@ +assertSame("", $createClause->toQuery()); + $this->assertEquals([], $createClause->getPatterns()); + } + + public function testSinglePattern(): void + { + $createClause = new CreateClause(); + $pattern = Query::node()->withVariable('a'); + + $createClause->addPattern($pattern); + + $this->assertSame("CREATE (a)", $createClause->toQuery()); + $this->assertEquals([$pattern], $createClause->getPatterns()); + } + + public function testMultiplePatterns(): void + { + $createClause = new CreateClause(); + + $patternA = Query::node()->withVariable('a'); + $patternB = Query::node()->withVariable('b')->relationshipTo(Query::node()->withVariable('c'), 'Foo'); + + $createClause->addPattern($patternA); + $createClause->addPattern($patternB); + + $this->assertSame("CREATE (a), (b)-[:Foo]->(c)", $createClause->toQuery()); + $this->assertEquals([$patternA, $patternB], $createClause->getPatterns()); + } + + public function testAddPattern(): void + { + $createClause = new CreateClause(); + + $createClause->addPattern(Query::node()->withVariable('a')); + $createClause->addPattern(Query::node()->withVariable('b')); + + $this->assertSame("CREATE (a), (b)", $createClause->toQuery()); + } + + public function testAddPatternDoesNotAcceptAnyType(): void + { + $createClause = new CreateClause(); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $createClause->addPattern(Query::function()::date()); + $createClause->toQuery(); + } + + public function testGetPatterns(): void + { + $createClause = new CreateClause(); + + $patterns = [ + Query::node('a'), + Query::node('b'), + ]; + + $createClause->addPattern(...$patterns); + + $this->assertSame($patterns, $createClause->getPatterns()); + + $patternC = Query::node('c'); + $patterns[] = $patternC; + + $createClause->addPattern($patternC); + + $this->assertSame($patterns, $createClause->getPatterns()); + } + + public function testAddPatternReturnsSameInstance(): void + { + $expected = new CreateClause(); + $actual = $expected->addPattern(Query::node()); + + $this->assertSame($expected, $actual); + } + + public function testCanBeEmpty(): void + { + $clause = new CreateClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/DeleteClauseTest.php b/tests/unit/Clauses/DeleteClauseTest.php new file mode 100644 index 00000000..115915c1 --- /dev/null +++ b/tests/unit/Clauses/DeleteClauseTest.php @@ -0,0 +1,150 @@ +assertSame("", $delete->toQuery()); + $this->assertEquals([], $delete->getStructural()); + $this->assertFalse($delete->detachesDeletion()); + } + + public function testSingleVariable(): void + { + $delete = new DeleteClause(); + $variable = new Variable('a'); + + $delete->addStructure($variable); + + $this->assertSame("DELETE a", $delete->toQuery()); + $this->assertEquals([$variable], $delete->getStructural()); + $this->assertFalse($delete->detachesDeletion()); + } + + public function testMultipleVariables(): void + { + $delete = new DeleteClause(); + + $a = new Variable('a'); + $b = new Variable('b'); + + $delete->addStructure($a); + $delete->addStructure($b); + + $this->assertSame("DELETE a, b", $delete->toQuery()); + $this->assertEquals([$a, $b], $delete->getStructural()); + $this->assertFalse($delete->detachesDeletion()); + } + + public function testDetachDelete(): void + { + $delete = new DeleteClause(); + $variable = new Variable('a'); + + $delete->addStructure($variable); + $delete->setDetach(true); + + $this->assertSame("DETACH DELETE a", $delete->toQuery()); + $this->assertEquals([$variable], $delete->getStructural()); + $this->assertTrue($delete->detachesDeletion()); + } + + public function testAcceptsVariable(): void + { + $delete = new DeleteClause(); + $variable = new Variable('a'); + + $delete->addStructure($variable); + $delete->toQuery(); + $this->assertEquals([$variable], $delete->getStructural()); + $this->assertFalse($delete->detachesDeletion()); + } + + public function testDoesNotAcceptAnyType(): void + { + $delete = new DeleteClause(); + $variable = $this->createMock(AnyType::class); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $delete->addStructure($variable); + $delete->toQuery(); + } + + public function testAddStructure(): void + { + $delete = new DeleteClause(); + + $variableA = new Variable('a'); + $variableB = new Variable('b'); + + $delete->addStructure($variableA, $variableB); + + $this->assertSame("DELETE a, b", $delete->toQuery()); + $this->assertSame([$variableA, $variableB], $delete->getStructural()); + } + + public function testAddStructureDoesNotAcceptAnyType(): void + { + $delete = new DeleteClause(); + + $variableA = new Variable('a'); + $variableB = $this->createMock(AnyType::class); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $delete->addStructure($variableA, $variableB); + $delete->toQuery(); + } + + public function testGetStructural(): void + { + $delete = new DeleteClause(); + + $this->assertSame([], $delete->getStructural()); + + $variable = new Variable('a'); + $delete->addStructure($variable); + + $this->assertSame([$variable], $delete->getStructural()); + } + + public function testDetachesDeletion(): void + { + $delete = new DeleteClause(); + + $this->assertFalse($delete->detachesDeletion()); + + $delete->setDetach(); + + $this->assertTrue($delete->detachesDeletion()); + } + + public function testCanBeEmpty(): void + { + $clause = new DeleteClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/LimitClauseTest.php b/tests/unit/Clauses/LimitClauseTest.php new file mode 100644 index 00000000..a1a5b392 --- /dev/null +++ b/tests/unit/Clauses/LimitClauseTest.php @@ -0,0 +1,84 @@ +assertSame("", $limit->toQuery()); + } + + public function testPattern(): void + { + $expression = Query::integer(10); + + $limit = new LimitClause(); + $limit->setLimit($expression); + + $this->assertSame("LIMIT 10", $limit->toQuery()); + } + + public function testDoesNotAcceptAnyType(): void + { + $expression = Query::string("10"); + $limit = new LimitClause(); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $limit->setLimit($expression); + } + + public function testSetLimitAcceptsPHPInteger(): void + { + $limit = new LimitClause(); + $limit->setLimit(10); + + $this->assertSame("LIMIT 10", $limit->toQuery()); + } + + public function testGetLimit(): void + { + $limit = new LimitClause(); + + $this->assertNull($limit->getLimit()); + + $expression = Query::integer(10); + + $limit->setLimit($expression); + + $this->assertSame($expression, $limit->getLimit()); + } + + public function testSetLimitReturnsSameInstance(): void + { + $limit = new LimitClause(); + $limit2 = $limit->setLimit(10); + + $this->assertSame($limit, $limit2); + } + + public function testCanBeEmpty(): void + { + $clause = new LimitClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/MatchClauseTest.php b/tests/unit/Clauses/MatchClauseTest.php new file mode 100644 index 00000000..bd0caa0e --- /dev/null +++ b/tests/unit/Clauses/MatchClauseTest.php @@ -0,0 +1,141 @@ +assertSame("", $match->toQuery()); + } + + public function testSinglePattern(): void + { + $pattern = Query::node()->withVariable('a'); + + $match = new MatchClause(); + $match->addPattern($pattern); + + $this->assertSame("MATCH (a)", $match->toQuery()); + } + + public function testMultiplePatterns(): void + { + $patternA = Query::node()->withVariable('a'); + $patternB = Query::node()->withVariable('b'); + + $match = new MatchClause(); + $match->addPattern($patternA, $patternB); + + $this->assertSame("MATCH (a), (b)", $match->toQuery()); + } + + public function testMultiplePatternsSeparateFunctionCalls(): void + { + $patternA = Query::node()->withVariable('a'); + $patternB = Query::node()->withVariable('b'); + + $match = new MatchClause(); + $match->addPattern($patternA); + $match->addPattern($patternB); + + $this->assertSame("MATCH (a), (b)", $match->toQuery()); + } + + public function testMultiplePatternsMerge(): void + { + $patternA = Query::node()->withVariable('a'); + $patternB = Query::node()->withVariable('b'); + $patternC = Query::node()->withVariable('c'); + $patternD = Query::node()->withVariable('d'); + + $match = new MatchClause(); + $match->addPattern($patternA, $patternB); + $match->addPattern($patternC, $patternD); + + $this->assertSame("MATCH (a), (b), (c), (d)", $match->toQuery()); + } + + public function testAddPatternAcceptsAnyMatchablePattern(): void + { + $node = Query::node(); + + $match = new MatchClause(); + $match->addPattern($node); + + $path = Query::node()->relationshipTo(Query::node()); + + $match->addPattern($path); + + $this->assertSame('MATCH (), ()-->()', $match->toQuery()); + } + + public function testAddPatternDoesNotAcceptRelationship(): void + { + $rel = Query::relationship(Relationship::DIR_LEFT); + + $match = new MatchClause(); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $match->addPattern($rel); + } + + public function testAddPatternArrayUnpacking(): void + { + $patternA = Query::node()->withVariable('a'); + $patternB = Query::node()->withVariable('b'); + + $patterns = [$patternA, $patternB]; + + $match = new MatchClause(); + $match->addPattern(...$patterns); + + $this->assertSame('MATCH (a), (b)', $match->toQuery()); + } + + public function testGetPatterns(): void + { + $pattern1 = Query::node(); + $pattern2 = Query::node(); + + $match = new MatchClause(); + $match->addPattern($pattern1); + $match->addPattern($pattern2); + + $this->assertSame([$pattern1, $pattern2], $match->getPatterns()); + } + + public function testAddPatternReturnsSameInstance(): void + { + $expected = new MatchClause(); + $actual = $expected->addPattern(Query::node()); + + $this->assertSame($expected, $actual); + } + + public function testCanBeEmpty(): void + { + $clause = new MatchClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/MergeClauseTest.php b/tests/unit/Clauses/MergeClauseTest.php new file mode 100644 index 00000000..3bb6f306 --- /dev/null +++ b/tests/unit/Clauses/MergeClauseTest.php @@ -0,0 +1,156 @@ +assertSame("", $merge->toQuery()); + } + + public function testSetPattern(): void + { + $pattern = Query::node()->withVariable('a'); + + $merge = new MergeClause(); + $merge->setPattern($pattern); + + $this->assertSame("MERGE (a)", $merge->toQuery()); + } + + public function testSetPatternReturnsSameInstance(): void + { + $pattern = Query::node()->withVariable('a'); + + $expected = new MergeClause(); + $actual = $expected->setPattern($pattern); + + $this->assertSame($expected, $actual); + } + + public function testSetOnCreate(): void + { + $pattern = Query::node()->withVariable('a'); + $onCreate = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $merge = new MergeClause(); + $merge->setPattern($pattern); + $merge->setOnCreate($onCreate); + + $this->assertSame("MERGE (a) ON CREATE SET a.a = 'b'", $merge->toQuery()); + } + + public function testSetOnCreateReturnsSameInstance(): void + { + $onCreate = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $expected = new MergeClause(); + $actual = $expected->setOnCreate($onCreate); + + $this->assertSame($expected, $actual); + } + + public function testSetOnMatch(): void + { + $pattern = Query::node()->withVariable('a'); + $onCreate = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $merge = new MergeClause(); + $merge->setPattern($pattern); + $merge->setOnMatch($onCreate); + + $this->assertSame("MERGE (a) ON MATCH SET a.a = 'b'", $merge->toQuery()); + } + + public function testSetOnMatchReturnsSameInstance(): void + { + $onCreate = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $expected = new MergeClause(); + $actual = $expected->setOnMatch($onCreate); + + $this->assertSame($expected, $actual); + } + + public function testSetOnBoth(): void + { + $pattern = Query::node()->withVariable('a'); + $clause = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $merge = new MergeClause(); + + $merge->setPattern($pattern); + $merge->setOnCreate($clause); + $merge->setOnMatch($clause); + + $this->assertSame("MERGE (a) ON CREATE SET a.a = 'b' ON MATCH SET a.a = 'b'", $merge->toQuery()); + } + + public function testSetPatternDoesNotAcceptRelationship(): void + { + $relationship = Query::relationship(Relationship::DIR_RIGHT); + + $merge = new MergeClause(); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $merge->setPattern($relationship); + } + + public function testGetPattern(): void + { + $pattern = Query::node()->withVariable('a'); + + $merge = new MergeClause(); + $merge->setPattern($pattern); + + $this->assertSame($pattern, $merge->getPattern()); + } + + public function testGetOnCreateClause(): void + { + $clause = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $merge = new MergeClause(); + $merge->setOnCreate($clause); + + $this->assertSame($clause, $merge->getOnCreateClause()); + } + + public function testGetOnMatch(): void + { + $clause = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $merge = new MergeClause(); + $merge->setOnMatch($clause); + + $this->assertSame($clause, $merge->getOnMatchClause()); + } + + public function testCanBeEmpty(): void + { + $clause = new MergeClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/OptionalMatchTest.php b/tests/unit/Clauses/OptionalMatchTest.php new file mode 100644 index 00000000..9656a86c --- /dev/null +++ b/tests/unit/Clauses/OptionalMatchTest.php @@ -0,0 +1,142 @@ +assertSame("", $match->toQuery()); + } + + public function testSinglePattern(): void + { + $pattern = Query::node()->withVariable('a'); + + $match = new OptionalMatchClause(); + $match->addPattern($pattern); + + $this->assertSame("OPTIONAL MATCH (a)", $match->toQuery()); + } + + public function testMultiplePatterns(): void + { + $patternA = Query::node()->withVariable('a'); + $patternB = Query::node()->withVariable('b'); + + $match = new OptionalMatchClause(); + $match->addPattern($patternA, $patternB); + + $this->assertSame("OPTIONAL MATCH (a), (b)", $match->toQuery()); + } + + public function testMultiplePatternsSeparateFunctionCalls(): void + { + $patternA = Query::node()->withVariable('a'); + $patternB = Query::node()->withVariable('b'); + + $match = new OptionalMatchClause(); + $match->addPattern($patternA); + $match->addPattern($patternB); + + $this->assertSame("OPTIONAL MATCH (a), (b)", $match->toQuery()); + } + + public function testMultiplePatternsMerge(): void + { + $patternA = Query::node()->withVariable('a'); + $patternB = Query::node()->withVariable('b'); + $patternC = Query::node()->withVariable('c'); + $patternD = Query::node()->withVariable('d'); + + $match = new OptionalMatchClause(); + $match->addPattern($patternA, $patternB); + $match->addPattern($patternC, $patternD); + + $this->assertSame("OPTIONAL MATCH (a), (b), (c), (d)", $match->toQuery()); + } + + public function testAddPatternAcceptsAnyMatchablePattern(): void + { + $node = Query::node(); + + $match = new OptionalMatchClause(); + $match->addPattern($node); + + $path = Query::node()->relationshipTo(Query::node()); + + $match->addPattern($path); + + $this->assertSame('OPTIONAL MATCH (), ()-->()', $match->toQuery()); + } + + public function testAddPatternDoesNotAcceptRelationship(): void + { + $rel = Query::relationship(Relationship::DIR_LEFT); + + $match = new OptionalMatchClause(); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $match->addPattern($rel); + } + + public function testAddPatternArrayUnpacking(): void + { + $patternA = Query::node()->withVariable('a'); + $patternB = Query::node()->withVariable('b'); + + $patterns = [$patternA, $patternB]; + + $match = new OptionalMatchClause(); + $match->addPattern(...$patterns); + + $this->assertSame('OPTIONAL MATCH (a), (b)', $match->toQuery()); + } + + public function testGetPatterns(): void + { + $pattern1 = Query::node(); + $pattern2 = Query::node(); + + $match = new OptionalMatchClause(); + $match->addPattern($pattern1); + $match->addPattern($pattern2); + + $this->assertSame([$pattern1, $pattern2], $match->getPatterns()); + } + + public function testAddPatternReturnsSameInstance(): void + { + $expected = new OptionalMatchClause(); + $actual = $expected->addPattern(Query::node()); + + $this->assertSame($expected, $actual); + } + + public function testCanBeEmpty(): void + { + $clause = new MatchClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/OrderByClauseTest.php b/tests/unit/Clauses/OrderByClauseTest.php new file mode 100644 index 00000000..20565d8f --- /dev/null +++ b/tests/unit/Clauses/OrderByClauseTest.php @@ -0,0 +1,167 @@ +assertSame("", $orderBy->toQuery()); + } + + public function testSingleProperty(): void + { + $property = Query::variable('a')->property('a'); + + $orderBy = new OrderByClause(); + $orderBy->addProperty($property); + + $this->assertSame("ORDER BY a.a", $orderBy->toQuery()); + } + + public function testMultipleProperties(): void + { + $a = Query::variable('a')->property('a'); + $b = Query::variable('a')->property('b'); + + $orderBy = new OrderByClause(); + + $orderBy->addProperty($a); + $orderBy->addProperty($b); + + $this->assertSame("ORDER BY a.a, a.b", $orderBy->toQuery()); + } + + public function testMultiplePropertiesSingleCall(): void + { + $a = Query::variable('a')->property('a'); + $b = Query::variable('a')->property('b'); + + $orderBy = new OrderByClause(); + + $orderBy->addProperty($a, $b); + + $this->assertSame("ORDER BY a.a, a.b", $orderBy->toQuery()); + } + + public function testMultiplePropertiesSingleCallArrayUnpacking(): void + { + $a = Query::variable('a')->property('a'); + $b = Query::variable('a')->property('b'); + + $orderBy = new OrderByClause(); + + $orderBy->addProperty(...[$a, $b]); + + $this->assertSame("ORDER BY a.a, a.b", $orderBy->toQuery()); + } + + public function testSinglePropertyDesc(): void + { + $a = Query::variable('a')->property('a'); + + $orderBy = new OrderByClause(); + $orderBy->addProperty($a); + $orderBy->setDescending(); + + $this->assertSame("ORDER BY a.a DESCENDING", $orderBy->toQuery()); + } + + public function testMultiplePropertiesDesc(): void + { + $a = Query::variable('a')->property('a'); + $b = Query::variable('a')->property('b'); + + $orderBy = new OrderByClause(); + + $orderBy->addProperty($a); + $orderBy->addProperty($b); + $orderBy->setDescending(); + + $this->assertSame("ORDER BY a.a, a.b DESCENDING", $orderBy->toQuery()); + } + + public function testSetDescendingTakesBoolean(): void + { + $a = Query::variable('a')->property('a'); + + $orderBy = new OrderByClause(); + + $orderBy->addProperty($a); + $orderBy->setDescending(); + + $this->assertSame("ORDER BY a.a DESCENDING", $orderBy->toQuery()); + + $orderBy->setDescending(true); + + $this->assertSame("ORDER BY a.a DESCENDING", $orderBy->toQuery()); + + $orderBy->setDescending(false); + + $this->assertSame("ORDER BY a.a", $orderBy->toQuery()); + } + + public function testDoesNotAcceptAnyType(): void + { + $orderBy = new OrderByClause(); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $orderBy->addProperty('a'); + } + + public function testGetProperties(): void + { + $orderBy = new OrderByClause(); + + $a = Query::variable()->property('a'); + $b = Query::variable()->property('b'); + + $orderBy->addProperty($a, $b); + + $this->assertSame([$a, $b], $orderBy->getProperties()); + } + + public function testIsDescending(): void + { + $orderBy = new OrderByClause(); + + $this->assertFalse($orderBy->isDescending()); + + $orderBy->setDescending(); + + $this->assertTrue($orderBy->isDescending()); + } + + public function testAddPropertyReturnsSameInstance(): void + { + $original = new OrderByClause(); + $returned = $original->addProperty(Query::variable()->property('a')); + + $this->assertSame($original, $returned); + } + + public function testCanBeEmpty(): void + { + $clause = new OrderByClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/RawClauseTest.php b/tests/unit/Clauses/RawClauseTest.php new file mode 100644 index 00000000..008dc6f6 --- /dev/null +++ b/tests/unit/Clauses/RawClauseTest.php @@ -0,0 +1,40 @@ +assertSame("UNIMPLEMENTED clause body", $raw->toQuery()); + } + + public function testEmptyClauseIsEmpty(): void + { + $raw = new RawClause('', 'body'); + + $this->assertSame('', $raw->toQuery()); + } + + public function testCanBeEmpty(): void + { + $clause = new RawClause(); + + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/RemoveClauseTest.php b/tests/unit/Clauses/RemoveClauseTest.php new file mode 100644 index 00000000..5e407948 --- /dev/null +++ b/tests/unit/Clauses/RemoveClauseTest.php @@ -0,0 +1,109 @@ +assertSame("", $remove->toQuery()); + } + + public function testSingleExpression(): void + { + $remove = new RemoveClause(); + $expression = new Property(new Variable('Foo'), 'Bar'); + + $remove->addExpression($expression); + + $this->assertSame("REMOVE Foo.Bar", $remove->toQuery()); + } + + public function testMultipleExpressionsSingleCall(): void + { + $remove = new RemoveClause(); + + $a = new Property(new Variable('Foo'), 'Bar'); + $b = new Label(new Variable('a'), 'B'); + + $remove->addExpression($a, $b); + + $this->assertSame("REMOVE Foo.Bar, a:B", $remove->toQuery()); + } + + public function testMultipleExpressionsMultipleCalls(): void + { + $remove = new RemoveClause(); + + $a = new Property(new Variable('Foo'), 'Bar'); + $b = new Label(new Variable('a'), 'B'); + + $remove->addExpression($a); + $remove->addExpression($b); + + $this->assertSame("REMOVE Foo.Bar, a:B", $remove->toQuery()); + } + + public function testAddExpressionReturnsSameInstance(): void + { + $expected = new RemoveClause(); + $actual = $expected->addExpression(Query::variable('a')->property('b')); + + $this->assertSame($expected, $actual); + } + + public function testGetExpressions(): void + { + $remove = new RemoveClause(); + + $this->assertSame([], $remove->getExpressions()); + + $a = Query::variable('a')->property('a'); + + $remove->addExpression($a); + + $this->assertSame([$a], $remove->getExpressions()); + + $b = Query::variable('a')->property('b'); + + $remove->addExpression($b); + + $this->assertSame([$a, $b], $remove->getExpressions()); + } + + public function testAddExpressionThrowsExceptionOnInvalidType(): void + { + $remove = new RemoveClause(); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $remove->addExpression(10); + } + + public function testCanBeEmpty(): void + { + $clause = new RemoveClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/ReturnClauseTest.php b/tests/unit/Clauses/ReturnClauseTest.php new file mode 100644 index 00000000..7f8f515f --- /dev/null +++ b/tests/unit/Clauses/ReturnClauseTest.php @@ -0,0 +1,145 @@ +assertSame("", $return->toQuery()); + $this->assertSame([], $return->getColumns()); + $this->assertFalse($return->isDistinct()); + } + + public function testSingleColumn(): void + { + $return = new ReturnClause(); + $column = $this->createMock(AnyType::class); + $column->method('toQuery')->willReturn('a'); + $return->addColumn($column); + + $this->assertSame("RETURN a", $return->toQuery()); + $this->assertSame([$column], $return->getColumns()); + $this->assertFalse($return->isDistinct()); + } + + public function testMultipleColumns(): void + { + $return = new ReturnClause(); + + $columnA = new Variable('a'); + $columnB = (new Path)->withVariable('b'); + $columnC = (new Node)->withVariable('c'); + + $return->addColumn($columnA); + $return->addColumn($columnB); + $return->addColumn($columnC); + + $this->assertSame("RETURN a, b, c", $return->toQuery()); + $this->assertSame([$columnA, $columnB->getVariable(), $columnC->getVariable()], $return->getColumns()); + $this->assertFalse($return->isDistinct()); + } + + public function testSingleAlias(): void + { + $return = new ReturnClause(); + $column = new Alias(new Variable('a'), new Variable('b')); + $return->addColumn($column); + + $this->assertSame("RETURN a AS b", $return->toQuery()); + $this->assertSame([$column], $return->getColumns()); + $this->assertFalse($return->isDistinct()); + } + + public function testMultipleAliases(): void + { + $return = new ReturnClause(); + $aliasA = new Alias(new Variable('a'), new Variable('b')); + $aliasB = new Alias(new Variable('b'), new Variable('c')); + $return->addColumn($aliasA); + $return->addColumn($aliasB); + + $this->assertSame("RETURN a AS b, b AS c", $return->toQuery()); + $this->assertSame([$aliasA, $aliasB], $return->getColumns()); + $this->assertFalse($return->isDistinct()); + } + + public function testMixedAliases(): void + { + $return = new ReturnClause(); + $columnA = new Alias(new Variable('a'), new Variable('b')); + $columnB = new Variable('c'); + $columnC = new Alias(new Variable('b'), new Variable('c')); + $return->addColumn($columnA); + $return->addColumn($columnB); + $return->addColumn($columnC); + + $this->assertSame("RETURN a AS b, c, b AS c", $return->toQuery()); + $this->assertEquals([$columnA, $columnB, $columnC], $return->getColumns()); + $this->assertFalse($return->isDistinct()); + } + + public function testReturnDistinct(): void + { + $return = new ReturnClause(); + $column = new Variable('a'); + $return->addColumn($column); + $return->setDistinct(); + + $this->assertSame("RETURN DISTINCT a", $return->toQuery()); + $this->assertSame([$column], $return->getColumns()); + $this->assertTrue($return->isDistinct()); + } + + public function testAddColumnLiteral(): void + { + $return = new ReturnClause(); + + $return->addColumn(100); + $return->addColumn("hello world"); + + $this->assertSame("RETURN 100, 'hello world'", $return->toQuery()); + } + + public function testAddColumnReturnsSameInstance(): void + { + $expected = new ReturnClause(); + $actual = $expected->addColumn(100); + + $this->assertSame($expected, $actual); + } + + public function testSetDistinctReturnsSameInstance(): void + { + $expected = new ReturnClause(); + $actual = $expected->setDistinct(); + + $this->assertSame($expected, $actual); + } + + public function testCanBeEmpty(): void + { + $clause = new ReturnClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/SetClauseTest.php b/tests/unit/Clauses/SetClauseTest.php new file mode 100644 index 00000000..cb012305 --- /dev/null +++ b/tests/unit/Clauses/SetClauseTest.php @@ -0,0 +1,150 @@ +assertSame("", $set->toQuery()); + } + + public function testAddSinglePropertyReplacement(): void + { + $map = Query::variable('a')->assign(['boo' => 'foo', 'far' => 'bar']); + + $set = new SetClause(); + $set->add($map); + + $this->assertSame("SET a = {boo: 'foo', far: 'bar'}", $set->toQuery()); + } + + public function testAddSingleLabel(): void + { + $label = Query::variable('a')->labeled('foo'); + + $set = new SetClause(); + $set->add($label); + + $this->assertSame("SET a:foo", $set->toQuery()); + } + + public function testAddMultiplePropertyReplacementSingleCall(): void + { + $mapA = Query::variable('a')->assign(['boo' => 'foo', 'far' => 'bar']); + $mapB = Query::variable('b')->assign(['boo' => 'foo', 'far' => 'bar']); + + $set = new SetClause(); + $set->add($mapA, $mapB); + + $this->assertSame("SET a = {boo: 'foo', far: 'bar'}, b = {boo: 'foo', far: 'bar'}", $set->toQuery()); + } + + public function testAddMultiplePropertyReplacementMultipleCalls(): void + { + $mapA = Query::variable('a')->assign(['boo' => 'foo', 'far' => 'bar']); + $mapB = Query::variable('b')->assign(['boo' => 'foo', 'far' => 'bar']); + + $set = new SetClause(); + $set->add($mapA); + $set->add($mapB); + + $this->assertSame("SET a = {boo: 'foo', far: 'bar'}, b = {boo: 'foo', far: 'bar'}", $set->toQuery()); + } + + public function testAddMultipleLabelsSingleCall(): void + { + $labelA = Query::variable('a')->labeled('foo'); + $labelB = Query::variable('b')->labeled('foo'); + + $set = new SetClause(); + $set->add($labelA, $labelB); + + $this->assertSame("SET a:foo, b:foo", $set->toQuery()); + } + + public function testAddMultipleLabelsMultipleCalls(): void + { + $labelA = Query::variable('a')->labeled('foo'); + $labelB = Query::variable('b')->labeled('foo'); + + $set = new SetClause(); + $set->add($labelA); + $set->add($labelB); + + $this->assertSame("SET a:foo, b:foo", $set->toQuery()); + } + + public function testAddMultipleMixedSingleCall(): void + { + $labelA = Query::variable('a')->labeled('foo'); + $mapB = Query::variable('a')->assign(['boo' => 'foo', 'far' => 'bar']); + + $set = new SetClause(); + $set->add($labelA, $mapB); + + $this->assertSame("SET a:foo, a = {boo: 'foo', far: 'bar'}", $set->toQuery()); + } + + public function testAddMultipleMixedMultipleCalls(): void + { + $labelA = Query::variable('a')->labeled('foo'); + $mapB = Query::variable('a')->assign(['boo' => 'foo', 'far' => 'bar']); + + $set = new SetClause(); + $set->add($labelA); + $set->add($mapB); + + $this->assertSame("SET a:foo, a = {boo: 'foo', far: 'bar'}", $set->toQuery()); + } + + public function testAddReturnsSameInstance(): void + { + $expected = new SetClause(); + $actual = $expected->add(Query::variable('a')->assign([])); + + $this->assertSame($expected, $actual); + } + + public function testGetExpressions(): void + { + $set = new SetClause(); + + $this->assertSame([], $set->getExpressions()); + + $labelA = Query::variable('a')->labeled('foo'); + + $set->add($labelA); + + $this->assertSame([$labelA], $set->getExpressions()); + + $labelB = Query::variable('b')->labeled('foo'); + + $set->add($labelB); + + $this->assertSame([$labelA, $labelB], $set->getExpressions()); + } + + public function testCanBeEmpty(): void + { + $clause = new SetClause(); + + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/SkipClauseTest.php b/tests/unit/Clauses/SkipClauseTest.php new file mode 100644 index 00000000..8b64ae82 --- /dev/null +++ b/tests/unit/Clauses/SkipClauseTest.php @@ -0,0 +1,82 @@ +assertSame("", $skip->toQuery()); + $this->assertNull($skip->getSkip()); + } + + public function testAcceptsInteger(): void + { + $skip = new SkipClause(); + $expression = Query::integer(10); + + $skip->setSkip($expression); + + $this->assertSame("SKIP 10", $skip->toQuery()); + } + + public function testDoesNotAcceptFloat(): void + { + $skip = new SkipClause(); + $expression = Query::float(1.0); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + $skip->setSkip($expression); + } + + public function testGetSkip(): void + { + $skip = new SkipClause(); + $expression = Query::integer(10); + + $skip->setSkip($expression); + + $this->assertSame($expression, $skip->getSkip()); + } + + public function testAcceptsLiteralInteger(): void + { + $skip = new SkipClause(); + $skip->setSkip(10); + + $this->assertSame("SKIP 10", $skip->toQuery()); + } + + public function testSetSkipReturnsSameInstance(): void + { + $expected = new SkipClause(); + $actual = $expected->setSkip(10); + + $this->assertSame($expected, $actual); + } + + public function testCanBeEmpty(): void + { + $clause = new SkipClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/UnionClauseTest.php b/tests/unit/Clauses/UnionClauseTest.php new file mode 100644 index 00000000..ae9fe310 --- /dev/null +++ b/tests/unit/Clauses/UnionClauseTest.php @@ -0,0 +1,80 @@ +assertEquals('UNION', $union->toQuery()); + } + + public function testAll(): void + { + $union = new UnionClause(); + $union->setAll(); + + $this->assertEquals('UNION ALL', $union->toQuery()); + } + + public function testSetAllDefaultIsTrue(): void + { + $union = new UnionClause(); + $union->setAll(); + + $this->assertSame("UNION ALL", $union->toQuery()); + } + + public function testSetAllCanBeUnset(): void + { + $union = new UnionClause(); + $union->setAll(); + + $this->assertSame("UNION ALL", $union->toQuery()); + + $union->setAll(false); + + $this->assertSame("UNION", $union->toQuery()); + } + + public function testIncludesAll(): void + { + $union = new UnionClause(); + + $this->assertFalse($union->includesAll()); + + $union->setAll(); + + $this->assertTrue($union->includesAll()); + } + + public function testSetAllReturnsSameInstance(): void + { + $expected = new UnionClause(); + $actual = $expected->setAll(); + + $this->assertSame($expected, $actual); + } + + public function testCanBeEmpty(): void + { + $clause = new UnionClause(); + + $this->assertTrue($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/WhereClauseTest.php b/tests/unit/Clauses/WhereClauseTest.php new file mode 100644 index 00000000..97519ffa --- /dev/null +++ b/tests/unit/Clauses/WhereClauseTest.php @@ -0,0 +1,124 @@ +assertSame("", $where->toQuery()); + $this->assertNull($where->getExpression()); + } + + public function testAddBooleanType(): void + { + $where = new WhereClause(); + + $where->addExpression(Literal::boolean(true)); + + $this->assertSame("WHERE true", $where->toQuery()); + } + + public function testAddBooleanLiteral(): void + { + $where = new WhereClause(); + + $where->addExpression(true); + + $this->assertSame("WHERE true", $where->toQuery()); + } + + public function testExpressionChainingDefaultAnd(): void + { + $where = new WhereClause(); + + $where->addExpression(true); + $where->addExpression(true); + $where->addExpression(false); + + $this->assertSame("WHERE ((true AND true) AND false)", $where->toQuery()); + } + + public function testExpressionChaining(): void + { + $where = new WhereClause(); + + $where->addExpression(true); + $where->addExpression(true, WhereClause::OR); + $where->addExpression(true, WhereClause::AND); + $where->addExpression(true, WhereClause::XOR); + + $this->assertSame('WHERE (((true OR true) AND true) XOR true)', $where->toQuery()); + } + + public function testExpressionChainingFirstArgumentDoesNothing(): void + { + $where = new WhereClause(); + + $where->addExpression(true, WhereClause::OR); + + $this->assertSame('WHERE true', $where->toQuery()); + } + + public function testExpressionChainingInvalidOperator1(): void + { + $where = new WhereClause(); + + $this->expectException(InvalidArgumentException::class); + + $where->addExpression(true, 'bad'); + } + + public function testExpressionChainingInvalidOperator2(): void + { + $where = new WhereClause(); + + $where->addExpression(true); + + $this->expectException(InvalidArgumentException::class); + + $where->addExpression(false, 'bad'); + } + + public function testGetExpression(): void + { + $expression = Literal::boolean(true); + + $where = new WhereClause(); + + $where->addExpression($expression); + + $this->assertSame($expression, $where->getExpression()); + } + + public function testAddExpressionReturnsSameInstance(): void + { + $expected = new WhereClause(); + $actual = $expected->addExpression(Literal::boolean(true)); + + $this->assertSame($expected, $actual); + } + + public function testCanBeEmpty(): void + { + $clause = new WhereClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Clauses/WithClauseTest.php b/tests/unit/Clauses/WithClauseTest.php new file mode 100644 index 00000000..29f3ab44 --- /dev/null +++ b/tests/unit/Clauses/WithClauseTest.php @@ -0,0 +1,118 @@ +assertSame("", $with->toQuery()); + } + + public function testSingleEntry(): void + { + $with = new WithClause(); + $with->addEntry(Query::variable('a')); + + $this->assertSame("WITH a", $with->toQuery()); + } + + public function testMultipleEntries(): void + { + $with = new WithClause(); + + $with->addEntry(Query::variable('a')); + $with->addEntry(Query::variable('b')); + $with->addEntry(Query::variable('c')); + + $this->assertSame("WITH a, b, c", $with->toQuery()); + } + + public function testSingleAlias(): void + { + $with = new WithClause(); + $with->addEntry(Query::variable('a')->alias('b')); + + $this->assertSame("WITH a AS b", $with->toQuery()); + } + + public function testMultipleAliases(): void + { + $with = new WithClause(); + $entryA = Query::variable('a')->alias('b'); + $entryB = Query::variable('b')->alias('c'); + + $with->addEntry($entryA, $entryB); + + $this->assertSame("WITH a AS b, b AS c", $with->toQuery()); + } + + public function testMixedAliases(): void + { + $entryA = Query::variable('a')->alias('b'); + $entryB = Query::variable('c'); + $entryC = Query::variable('b')->alias('c'); + + $with = new WithClause(); + $with->addEntry($entryA, $entryB, $entryC); + + $this->assertSame("WITH a AS b, c, b AS c", $with->toQuery()); + } + + public function testAcceptsStringAsVariableName(): void + { + $with = new WithClause(); + $with->addEntry('a', 'b', 'c'); + + $this->assertSame('WITH a, b, c', $with->toQuery()); + } + + public function testAcceptsPatternAsEntry(): void + { + $with = new WithClause(); + $with->addEntry(Query::node()); + + $this->assertStringMatchesFormat('WITH %s', $with->toQuery()); + } + + public function testGetEntries(): void + { + $entryA = Query::variable('a'); + $entryB = Query::variable('b'); + + $with = new WithClause(); + $with->addEntry($entryA, $entryB); + + $this->assertSame([$entryA, $entryB], $with->getEntries()); + } + + public function testAddEntryReturnsSameInstance(): void + { + $expected = new WithClause(); + $actual = $expected->addEntry('a'); + + $this->assertSame($expected, $actual); + } + + public function testCanBeEmpty(): void + { + $clause = new WithClause(); + $this->assertFalse($clause->canBeEmpty()); + } +} diff --git a/tests/unit/Expressions/AliasTest.php b/tests/unit/Expressions/AliasTest.php new file mode 100644 index 00000000..fafbc739 --- /dev/null +++ b/tests/unit/Expressions/AliasTest.php @@ -0,0 +1,47 @@ +alias = new Alias( + new Variable("a"), + new Variable("b") + ); + } + + public function testToQuery(): void + { + $this->assertSame("a AS b", $this->alias->toQuery()); + } + + public function testGetOriginal(): void + { + $this->assertEquals(new Variable("a"), $this->alias->getOriginal()); + } + + public function testGetVariable(): void + { + $this->assertEquals(new Variable("b"), $this->alias->getVariable()); + } +} diff --git a/tests/unit/Expressions/ExistsTest.php b/tests/unit/Expressions/ExistsTest.php new file mode 100644 index 00000000..5e70f891 --- /dev/null +++ b/tests/unit/Expressions/ExistsTest.php @@ -0,0 +1,56 @@ +addPattern( + new Path( + [(new Node)->withVariable('person'), (new Node('Dog'))->withVariable('dog')], + (new Relationship(Relationship::DIR_RIGHT))->addType('HAS_DOG') + ) + ) + ); + + $this->assertSame("EXISTS { MATCH (person)-[:HAS_DOG]->(dog:Dog) }", $exists->toQuery()); + + $exists = new Exists( + (new MatchClause)->addPattern( + new Path( + [(new Node)->withVariable('person'), (new Node('Dog'))->withVariable('dog')], + [(new Relationship(Relationship::DIR_RIGHT))->addType('HAS_DOG')] + ) + ), + (new WhereClause)->addExpression( + new Equality(new Property(new Variable('toy'), 'name'), new String_('Banana'), false) + ) + ); + + $this->assertSame("EXISTS { MATCH (person)-[:HAS_DOG]->(dog:Dog) WHERE toy.name = 'Banana' }", $exists->toQuery()); + } +} diff --git a/tests/unit/Expressions/LabelTest.php b/tests/unit/Expressions/LabelTest.php new file mode 100644 index 00000000..244717af --- /dev/null +++ b/tests/unit/Expressions/LabelTest.php @@ -0,0 +1,50 @@ +assertSame("foo:Bar", $label->toQuery()); + } + + public function testMultiple(): void + { + $expression = new Variable("foo"); + $label = ["Bar", "Baz"]; + + $label = new Label($expression, ...$label); + + $this->assertSame("foo:Bar:Baz", $label->toQuery()); + } + + public function testLabelIsEscaped(): void + { + $expression = new Variable("foo"); + $label = "{}"; + + $label = new Label($expression, $label); + + $this->assertSame("foo:`{}`", $label->toQuery()); + } +} diff --git a/tests/unit/Expressions/Literals/BooleanTest.php b/tests/unit/Expressions/Literals/BooleanTest.php new file mode 100644 index 00000000..26b23b5e --- /dev/null +++ b/tests/unit/Expressions/Literals/BooleanTest.php @@ -0,0 +1,41 @@ +assertSame("true", $boolean->toQuery()); + $this->assertTrue($boolean->getValue()); + } + + public function testFalse(): void + { + $boolean = new Boolean(false); + + $this->assertSame("false", $boolean->toQuery()); + $this->assertFalse($boolean->getValue()); + } + + public function testInstanceOfBooleanType(): void + { + $this->assertInstanceOf(BooleanType::class, new Boolean(false)); + } +} diff --git a/tests/unit/Expressions/Literals/FloatTest.php b/tests/unit/Expressions/Literals/FloatTest.php new file mode 100644 index 00000000..202c650d --- /dev/null +++ b/tests/unit/Expressions/Literals/FloatTest.php @@ -0,0 +1,61 @@ +assertSame("0.0", $float->toQuery()); + } + + /** + * @dataProvider provideToQueryData + */ + public function testToQuery(float $value, string $expected): void + { + $float = new Float_($value); + + $this->assertSame($expected, $float->toQuery()); + } + + public function testGetValue(): void + { + $value = 1.01239; + + $float = new Float_($value); + + $this->assertSame($value, $float->getValue()); + } + + public function testInstanceOfFloatType(): void + { + $this->assertInstanceOf(FloatType::class, new Float_(1.0)); + } + + public function provideToQueryData(): array + { + return [ + [1, '1.0'], + [1.0, '1.0'], + [111111111111111, '1.1111111111111E+14'], + [1337.1337, '1337.1337'], + ]; + } +} diff --git a/tests/unit/Expressions/Literals/IntegerTest.php b/tests/unit/Expressions/Literals/IntegerTest.php new file mode 100644 index 00000000..d2c2e6cc --- /dev/null +++ b/tests/unit/Expressions/Literals/IntegerTest.php @@ -0,0 +1,94 @@ +assertSame("0", $decimal->toQuery()); + $this->assertEquals('0', $decimal->getValue()); + } + + public function testInstanceOfIntegerType(): void + { + $this->assertInstanceOf(IntegerType::class, new Integer(0)); + } + + public function testThrowsTypeErrorOnInvalidType(): void + { + $this->expectException(TypeError::class); + + new Integer([]); + } + + public function testThrowsTypeErrorOnInvalidString(): void + { + $this->expectException(TypeError::class); + + new Integer('not an integer'); + } + + /** + * @dataProvider provideToQueryData + * + * @param $number + */ + public function testToQuery($number, string $expected): void + { + $decimal = new Integer($number); + + $this->assertSame($expected, $decimal->toQuery()); + $this->assertEquals($expected, $decimal->getValue()); + } + + /** + * @dataProvider provideInvalidInputData + */ + public function testInvalidInput($input): void + { + $this->expectException(TypeError::class); + new Integer($input); + } + + public function provideToQueryData(): array + { + return [ + [1, "1"], + [2, "2"], + ["2147483649", "2147483649"], + ["9223372036854775816", "9223372036854775816"], + [-12, "-12"], + [69, "69"], + ["292922929312312831203129382304823043284234729847294324724982749274294729427429471230918457", "292922929312312831203129382304823043284234729847294324724982749274294729427429471230918457"], + ["-1238109438204130457284308235720483205", "-1238109438204130457284308235720483205"], + ]; + } + + public function provideInvalidInputData(): array + { + return [ + ['nonumber'], + ['12.3E36'], + ['0x86'], + ['5.5'], + ]; + } +} diff --git a/tests/unit/Expressions/Literals/ListTest.php b/tests/unit/Expressions/Literals/ListTest.php new file mode 100644 index 00000000..c3440858 --- /dev/null +++ b/tests/unit/Expressions/Literals/ListTest.php @@ -0,0 +1,166 @@ +assertSame("[]", $list->toQuery()); + } + + public function testRequiresAnyTypeInConstructor(): void + { + $a = new class() + { + }; + + $this->expectException(TypeError::class); + + new List_([$a]); + } + + public function testDefaultListIsEmpty(): void + { + $list = new List_(); + + $this->assertSame('[]', $list->toQuery()); + } + + public function testAddExpression(): void + { + $list = new List_(); + + $list->addExpression(Query::float(1.0)); + + $this->assertSame('[1.0]', $list->toQuery()); + } + + public function testAddExpressionLiteral(): void + { + $list = new List_(); + + $list->addExpression(1.0); + + $this->assertSame('[1.0]', $list->toQuery()); + } + + public function testAddPatternIsCastedToVariable(): void + { + $list = new List_(); + $pattern = Query::node()->withVariable('foobar'); + + $list->addExpression($pattern); + + $this->assertSame('[foobar]', $list->toQuery()); + + $pattern = Query::node(); + + $list->addExpression($pattern); + + $this->assertStringMatchesFormat('[foobar, var%s]', $list->toQuery()); + } + + public function testGetExpressions(): void + { + $list = new List_(); + + $a = Query::literal(10); + + $list->addExpression($a); + + $this->assertSame([$a], $list->getExpressions()); + + $b = Query::literal(11); + + $list->addExpression($b); + + $this->assertSame([$a, $b], $list->getExpressions()); + } + + public function testAddExpressionMultipleSingleCall(): void + { + $list = new List_(); + + $a = Query::literal(10); + $b = Query::literal(11); + + $list->addExpression($a, $b); + + $this->assertSame('[10, 11]', $list->toQuery()); + } + + public function testIsEmpty(): void + { + $list = new List_(); + + $this->assertTrue($list->isEmpty()); + + $list->addExpression(10); + + $this->assertFalse($list->isEmpty()); + } + + public function testInstanceOfListType(): void + { + $list = new List_(); + + $this->assertInstanceOf(ListType::class, $list); + } + + /** + * @dataProvider provideOneDimensionalData + */ + public function testOneDimensional(array $expressions, string $expected): void + { + $list = new List_($expressions); + + $this->assertSame($expected, $list->toQuery()); + } + + /** + * @dataProvider provideMultidimensionalData + */ + public function testMultidimensional(array $expressions, string $expected): void + { + $list = new List_($expressions); + + $this->assertSame($expected, $list->toQuery()); + } + + public function provideOneDimensionalData(): array + { + return [ + [[Query::literal(12)], "[12]"], + [[Query::literal('12')], "['12']"], + [[Query::literal('12'), Query::literal('13')], "['12', '13']"], + ]; + } + + public function provideMultidimensionalData(): array + { + return [ + [[new List_([Query::literal(12)])], "[[12]]"], + [[new List_([Query::literal('12')])], "[['12']]"], + [[new List_([Query::literal('12'), Query::literal('14')]), new List_([Query::literal('13')])], "[['12', '14'], ['13']]"], + ]; + } +} diff --git a/tests/unit/Expressions/Literals/LiteralTest.php b/tests/unit/Expressions/Literals/LiteralTest.php new file mode 100644 index 00000000..916aa89c --- /dev/null +++ b/tests/unit/Expressions/Literals/LiteralTest.php @@ -0,0 +1,1032 @@ +assertInstanceOf(String_::class, $string); + } + + public function testLiteralBoolean(): void + { + $boolean = Literal::literal(true); + + $this->assertInstanceOf(Boolean::class, $boolean); + } + + public function testLiteralInteger(): void + { + $integer = Literal::literal(1); + + $this->assertInstanceOf(Integer::class, $integer); + } + + public function testLiteralFloat(): void + { + $float = Literal::literal(1.0); + + $this->assertInstanceOf(Float_::class, $float); + } + + public function testLiteralList(): void + { + $list = Literal::literal(['a', 'b', 'c']); + + $this->assertInstanceOf(List_::class, $list); + } + + public function testLiteralMap(): void + { + $map = Literal::literal(['a' => 'b']); + + $this->assertInstanceOf(Map::class, $map); + } + + public function testInvalidTypeThrowsException(): void + { + $this->expectException(TypeError::class); + + Literal::literal(new stdClass()); + } + + public function testStringable(): void + { + $stringable = Literal::literal(new class() + { + public function __toString() + { + return 'Testing is a virtue!'; + } + }); + + $this->assertInstanceOf(String_::class, $stringable); + } + + /** + * @dataProvider literalDeductionFailureProvider + */ + public function testLiteralDeductionFailure($value): void + { + $this->expectException(TypeError::class); + Literal::literal($value); + } + + public function literalDeductionFailureProvider(): array + { + return [ + [new class() + { + }, ], + [null], + ]; + } + + public function testNumberInteger(): void + { + $integer = Literal::number(1); + + $this->assertInstanceOf(Integer::class, $integer); + } + + public function testNumberFloat(): void + { + $float = Literal::number(1.0); + + $this->assertInstanceOf(Float_::class, $float); + } + + public function testNumberNoString(): void + { + $this->expectException(TypeError::class); + + Literal::number('55'); + } + + public function testBoolean(): void + { + $boolean = Literal::boolean(true); + + $this->assertInstanceOf(Boolean::class, $boolean); + + $boolean = Literal::boolean(false); + + $this->assertInstanceOf(Boolean::class, $boolean); + } + + public function testString(): void + { + $string = Literal::string('Testing is a virtue!'); + + $this->assertInstanceOf(String_::class, $string); + } + + public function testDecimal(): void + { + $decimal = Literal::decimal(1); + + $this->assertInstanceOf(Integer::class, $decimal); + + $decimal = Literal::decimal(1.0); + + $this->assertInstanceOf(Float_::class, $decimal); + } + + public function testInteger(): void + { + $integer = Literal::integer(1); + + $this->assertInstanceOf(Integer::class, $integer); + } + + public function testFloat(): void + { + $float = Literal::float(1.0); + + $this->assertInstanceOf(Float_::class, $float); + } + + public function testList(): void + { + $list = Literal::list(['a', 'b', 'c']); + + $this->assertInstanceOf(List_::class, $list); + } + + public function testListAcceptsIterable(): void + { + $list = Literal::list(new class implements Iterator + { + public function current() + { + return 1; + } + + public function next() + { + return 1; + } + + public function key(): void + { + } + + public function valid() + { + static $i = 0; + + return $i++ < 10; + } + + public function rewind(): void + { + } + }); + + $this->assertInstanceOf(List_::class, $list); + } + + public function testListCastsToAnyType(): void + { + $list = Literal::list(['a', 1, true]); + + $this->assertSame("['a', 1, true]", $list->toQuery()); + } + + public function testMapCastsToAnyType(): void + { + $map = Literal::map(['a' => 'a', 'b' => 1, 'c' => true]); + + $this->assertSame("{a: 'a', b: 1, c: true}", $map->toQuery()); + } + + public function testMap(): void + { + $map = Literal::map(['a' => 'b']); + + $this->assertInstanceOf(Map::class, $map); + } + + public function testPoint2d(): void + { + $point = Literal::point2d(1, 2); + + $this->assertEquals(new Point(Literal::map(["x" => 1, "y" => 2, "crs" => "cartesian"])), $point); + + $point = Literal::point2d( + new Integer(1), + new Integer(2) + ); + + $this->assertEquals(new Point(Literal::map(["x" => 1, "y" => 2, "crs" => "cartesian"])), $point); + } + + public function testPoint3d(): void + { + $point = Literal::point3d(1, 2, 3); + + $this->assertEquals(new Point(Literal::map(["x" => 1, "y" => 2, "z" => 3, "crs" => "cartesian-3D"])), $point); + + $point = Literal::point3d( + new Integer(1), + new Integer(2), + new Integer(3) + ); + + $this->assertEquals(new Point(Literal::map(["x" => 1, "y" => 2, "z" => 3, "crs" => "cartesian-3D"])), $point); + } + + public function testPoint2dWGS84(): void + { + $point = Literal::point2dWGS84(1, 2); + + $this->assertEquals(new Point(Literal::map(["longitude" => 1, "latitude" => 2, "crs" => "WGS-84"])), $point); + + $point = Literal::point2dWGS84( + new Integer(1), + new Integer(2) + ); + + $this->assertEquals(new Point(Literal::map(["longitude" => 1, "latitude" => 2, "crs" => "WGS-84"])), $point); + } + + public function testPoint3dWGS84(): void + { + $point = Literal::point3dWGS84(1, 2, 3); + + $this->assertEquals(new Point(Literal::map(["longitude" => 1, "latitude" => 2, "height" => 3, "crs" => "WGS-84-3D"])), $point); + + $point = Literal::point3dWGS84( + new Integer(1), + new Integer(2), + new Integer(3) + ); + + $this->assertEquals(new Point(Literal::map(["longitude" => 1, "latitude" => 2, "height" => 3, "crs" => "WGS-84-3D"])), $point); + } + + public function testDate(): void + { + $date = Literal::date(); + + $this->assertEquals(new Date(), $date); + } + + public function testDateTimezone(): void + { + $date = Literal::date("Europe/Amsterdam"); + + $this->assertEquals(new Date(Literal::map(["timezone" => "Europe/Amsterdam"])), $date); + + $date = Literal::date(new String_("Europe/Amsterdam")); + + $this->assertEquals(new Date(Literal::map(["timezone" => "Europe/Amsterdam"])), $date); + } + + /** + * @dataProvider provideDateYMDData + * + * @param $year + * @param $month + * @param $day + * @param $expected + */ + public function testDateYMD($year, $month, $day, $expected): void + { + $date = Literal::dateYMD($year, $month, $day); + + $this->assertEquals($expected, $date); + } + + public function testDateYMDMissingMonth(): void + { + $this->expectException(LogicException::class); + + $date = Literal::dateYMD(2000, null, 17); + + $date->toQuery(); + } + + /** + * @dataProvider provideDateYWDData + * + * @param $year + * @param $week + * @param $weekday + * @param $expected + */ + public function testDateYWD($year, $week, $weekday, $expected): void + { + $date = Literal::dateYWD($year, $week, $weekday); + + $this->assertEquals($expected, $date); + } + + public function testDateYWDMissingWeek(): void + { + $this->expectException(LogicException::class); + + $date = Literal::dateYWD(2000, null, 17); + + $date->toQuery(); + } + + public function testDateString(): void + { + $date = Literal::dateString('2000-17-12'); + + $this->assertEquals(new Date(new String_('2000-17-12')), $date); + } + + public function testDateTimeWithoutTimeZone(): void + { + $datetime = Literal::dateTime(); + + $this->assertEquals(new DateTime(), $datetime); + } + + public function testDateTimeWithTimeZone(): void + { + $datetime = Literal::dateTime("America/Los Angeles"); + + $this->assertEquals(new DateTime(Literal::map(["timezone" => "America/Los Angeles"])), $datetime); + } + + /** + * @dataProvider provideDatetimeYMDData + * + * @param $year + * @param $month + * @param $day + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $timezone + * @param $expected + */ + public function testDatetimeYMD($year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected): void + { + $datetime = Literal::dateTimeYMD($year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone); + + $this->assertEquals($expected, $datetime); + } + + public function testDatetimeYMDMissingMonth(): void + { + $this->expectException(LogicException::class); + + $datetime = Literal::dateTimeYMD(2000, null, 17); + + $datetime->toQuery(); + } + + /** + * @dataProvider provideDatetimeYWDData + * + * @param $year + * @param $week + * @param $dayOfWeek + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $timezone + * @param $expected + */ + public function testDatetimeYWD($year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected): void + { + $datetime = Literal::dateTimeYWD($year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone); + + $this->assertEquals($expected, $datetime); + } + + public function testDatetimeYWDMissingWeek(): void + { + $this->expectException(LogicException::class); + + $datetime = Literal::dateTimeYWD(2000, null, 17); + + $datetime->toQuery(); + } + + /** + * @dataProvider provideDatetimeYQDData + * + * @param $year + * @param $quarter + * @param $dayOfQuarter + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $timezone + * @param $expected + */ + public function testDatetimeYQD($year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected): void + { + $datetime = Literal::dateTimeYQD($year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone); + + $this->assertEquals($expected, $datetime); + } + + public function testDatetimeYQDMissingQuarter(): void + { + $this->expectException(LogicException::class); + + $datetime = Literal::dateTimeYQD(2000, null, 17); + + $datetime->toQuery(); + } + + /** + * @dataProvider provideDatetimeYQData + * + * @param $year + * @param $ordinalDay + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $timezone + * @param $expected + */ + public function testDatetimeYD($year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected): void + { + $datetime = Literal::dateTimeYD($year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone); + + $this->assertEquals($expected, $datetime); + } + + public function testDatetimeYDMissingOrdinalDay(): void + { + $this->expectException(LogicException::class); + + $datetime = Literal::dateTimeYD(2000, null, 17); + + $datetime->toQuery(); + } + + public function testDatetimeString(): void + { + $datetime = Literal::dateTimeString("2015-07-21T21:40:32.142+01:00"); + + $this->assertEquals(new DateTime(new String_("2015-07-21T21:40:32.142+01:00")), $datetime); + } + + public function testLocalDateTimeWithoutTimezone(): void + { + $localDateTime = Literal::localDateTime(); + + $this->assertEquals(new LocalDateTime(), $localDateTime); + } + + public function testLocalDateTimeWithTimezone(): void + { + $localDateTime = Literal::localDateTime("America/Los Angeles"); + + $this->assertEquals(new LocalDateTime(Literal::map(["timezone" => "America/Los Angeles"])), $localDateTime); + } + + /** + * @dataProvider provideLocalDatetimeYMDData + * + * @param $year + * @param $month + * @param $day + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $expected + */ + public function testLocalDateTimeYMD($year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void + { + $localDatetime = Literal::localDateTimeYMD($year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond); + + $this->assertEquals($expected, $localDatetime); + } + + public function testLocalDateTimeYMDMissingMonth(): void + { + $this->expectException(LogicException::class); + + $datetime = Literal::localDateTimeYMD(2000, null, 17); + + $datetime->toQuery(); + } + + /** + * @dataProvider provideLocalDatetimeYWDData + * + * @param $year + * @param $week + * @param $dayOfWeek + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $expected + */ + public function testLocalDateTimeYWD($year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void + { + $localDatetime = Literal::localDateTimeYWD($year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond); + + $this->assertEquals($expected, $localDatetime); + } + + public function testLocalDateTimeYWDMissingWeek(): void + { + $this->expectException(LogicException::class); + + $datetime = Literal::localDateTimeYWD(2000, null, 17); + + $datetime->toQuery(); + } + + /** + * @dataProvider provideLocalDatetimeYQDData + * + * @param $year + * @param $quarter + * @param $dayOfQuarter + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $expected + */ + public function testLocalDatetimeYQD($year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void + { + $localDatetime = Literal::localDateTimeYQD($year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond); + + $this->assertEquals($expected, $localDatetime); + } + + public function testLocalDateTimeYQDMissingQuarter(): void + { + $this->expectException(LogicException::class); + + $datetime = Literal::localDateTimeYQD(2000, null, 17); + + $datetime->toQuery(); + } + + /** + * @dataProvider provideLocalDatetimeYQData + * + * @param $year + * @param $ordinalDay + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $expected + */ + public function testLocalDatetimeYD($year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void + { + $localDatetime = Literal::localDateTimeYD($year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond); + + $this->assertEquals($expected, $localDatetime); + } + + public function testLocalDateTimeYDMissingOrdinalDay(): void + { + $this->expectException(LogicException::class); + + $datetime = Literal::localDateTimeYD(2000, null, 17); + + $datetime->toQuery(); + } + + public function testLocalDatetimeString(): void + { + $localDatetime = Literal::localDateTimeString("2015-W30-2T214032.142"); + + $this->assertEquals(new LocalDateTime(new String_("2015-W30-2T214032.142")), $localDatetime); + } + + public function testLocalTimeCurrentWithoutTimezone(): void + { + $localTime = Literal::localTimeCurrent(); + $this->assertEquals(new LocalTime(), $localTime); + } + + public function testLocalTimeCurrentWithTimezone(): void + { + $localTime = Literal::localTimeCurrent("America/Los Angeles"); + $this->assertEquals(new LocalTime(Literal::map(["timezone" => "America/Los Angeles"])), $localTime); + } + + /** + * @dataProvider provideLocalTimeData + * + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $expected + */ + public function testLocalTime($hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void + { + $localTime = Literal::localTime($hour, $minute, $second, $millisecond, $microsecond, $nanosecond); + $this->assertEquals($localTime, $expected); + } + + public function testLocalTimeMissingMinute(): void + { + $this->expectException(LogicException::class); + + $localTime = Literal::localTime(9, null, 17); + + $localTime->toQuery(); + } + + public function testLocalTimeString(): void + { + $localTime = Literal::localTimeString("21:40:32.142"); + $this->assertEquals(new LocalTime(new String_("21:40:32.142")), $localTime); + } + + public function testTimeCurrentWithoutTimezone(): void + { + $time = Literal::time(); + $this->assertEquals($time, new Time()); + } + + public function testTimeCurrentWithTimezone(): void + { + $time = Literal::time("America/Los Angeles"); + $this->assertEquals($time, new Time(Literal::map(["timezone" => "America/Los Angeles"]))); + } + + /** + * @dataProvider provideTimeData + * + * @param $hour + * @param $minute + * @param $second + * @param $millisecond + * @param $microsecond + * @param $nanosecond + * @param $expected + */ + public function testTime($hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected): void + { + $time = Literal::timeHMS($hour, $minute, $second, $millisecond, $microsecond, $nanosecond); + $this->assertEquals($time, $expected); + } + + public function testTimeMissingMinute(): void + { + $this->expectException(LogicException::class); + + $time = Literal::timeHMS(9, null, 17); + + $time->toQuery(); + } + + public function testTimeString(): void + { + $time = Literal::timeString("21:40:32.142+0100"); + $this->assertEquals($time, new Time(new String_("21:40:32.142+0100"))); + } + + public function provideDateYMDData(): array + { + return [ + [2000, null, null, new Date(Literal::map(["year" => 2000]))], + [2000, 12, null, new Date(Literal::map(["year" => 2000, "month" => 12]))], + [2000, 12, 17, new Date(Literal::map(["year" => 2000, "month" => 12, "day" => 17]))], + [new Integer(2000), null, null, new Date(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(12), null, new Date(Literal::map(["year" => 2000, "month" => 12]))], + [new Integer(2000), new Integer(12), new Integer(17), new Date(Literal::map(["year" => 2000, "month" => 12, "day" => 17]))], + + ]; + } + + public function provideDateYWDData(): array + { + return [ + [2000, null, null, new Date(Literal::map(["year" => 2000]))], + [2000, 12, null, new Date(Literal::map(["year" => 2000, "week" => 12]))], + [2000, 12, 17, new Date(Literal::map(["year" => 2000, "week" => 12, "dayOfWeek" => 17]))], + [new Integer(2000), null, null, new Date(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(12), null, new Date(Literal::map(["year" => 2000, "week" => 12]))], + [new Integer(2000), new Integer(12), new Integer(17), new Date(Literal::map(["year" => 2000, "week" => 12, "dayOfWeek" => 17]))], + + ]; + } + + public function provideDatetimeYMDData(): array + { + // [$year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected] + return [ + [2000, null, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000]))], + [2000, 12, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12]))], + [2000, 12, 15, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15]))], + [2000, 12, 15, 8, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8]))], + [2000, 12, 15, 8, 25, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25]))], + [2000, 12, 15, 8, 25, 44, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44]))], + [2000, 12, 15, 8, 25, 44, 18, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [2000, 12, 15, 8, 25, 44, 18, 6, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [2000, 12, 15, 8, 25, 44, 18, 6, 31, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + [2000, 12, 15, 8, 25, 44, 18, 6, 31, "America/Los Angeles", new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31, "timezone" => "America/Los Angeles"]))], + + // types + [new Integer(2000), null, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(12), null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12]))], + [new Integer(2000), new Integer(12), new Integer(15), null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), new Integer(44), null, null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), new Integer(44), new Integer(18), null, null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), null, null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), null, new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), new String_("America/Los Angeles"), new DateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31, "timezone" => "America/Los Angeles"]))], + ]; + } + + public function provideDatetimeYWDData(): array + { + // [$year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected] + return [ + [2000, null, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000]))], + [2000, 9, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9]))], + [2000, 9, 4, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4]))], + [2000, 9, 4, 8, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8]))], + [2000, 9, 4, 8, 25, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25]))], + [2000, 9, 4, 8, 25, 44, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44]))], + [2000, 9, 4, 8, 25, 44, 18, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [2000, 9, 4, 8, 25, 44, 18, 6, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [2000, 9, 4, 8, 25, 44, 18, 6, 31, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + [2000, 9, 4, 8, 25, 44, 18, 6, 31, "America/Los Angeles", new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31, "timezone" => "America/Los Angeles"]))], + + // types + [new Integer(2000), null, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(9), null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9]))], + [new Integer(2000), new Integer(9), new Integer(4), null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), new Integer(44), null, null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), null, null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), null, null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), null, new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), new String_("America/Los Angeles"), new DateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31, "timezone" => "America/Los Angeles"]))], + ]; + } + + public function provideDatetimeYQDData(): array + { + // [$year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected] + return [ + [2000, null, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000]))], + [2000, 3, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3]))], + [2000, 3, 4, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4]))], + [2000, 3, 4, 8, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8]))], + [2000, 3, 4, 8, 25, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25]))], + [2000, 3, 4, 8, 25, 44, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44]))], + [2000, 3, 4, 8, 25, 44, 18, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [2000, 3, 4, 8, 25, 44, 18, 6, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [2000, 3, 4, 8, 25, 44, 18, 6, 31, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + [2000, 3, 4, 8, 25, 44, 18, 6, 31, "America/Los Angeles", new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31, "timezone" => "America/Los Angeles"]))], + + // types + [new Integer(2000), null, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(3), null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3]))], + [new Integer(2000), new Integer(3), new Integer(4), null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), new Integer(44), null, null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), null, null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), null, null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), null, new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), new String_("America/Los Angeles"), new DateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31, "timezone" => "America/Los Angeles"]))], + ]; + } + + public function provideDatetimeYQData(): array + { + // [$year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $timezone, $expected] + return [ + [2000, null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000]))], + [2000, 3, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3]))], + [2000, 3, 8, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8]))], + [2000, 3, 8, 25, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25]))], + [2000, 3, 8, 25, 44, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44]))], + [2000, 3, 8, 25, 44, 18, null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [2000, 3, 8, 25, 44, 18, 6, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [2000, 3, 8, 25, 44, 18, 6, 31, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + [2000, 3, 8, 25, 44, 18, 6, 31, "America/Los Angeles", new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31, "timezone" => "America/Los Angeles"]))], + + // types + [new Integer(2000), null, null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(3), null, null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3]))], + [new Integer(2000), new Integer(3), new Integer(8), null, null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), null, null, null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), new Integer(44), null, null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), new Integer(44), new Integer(18), null, null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), null, null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), null, new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), new String_("America/Los Angeles"), new DateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31, "timezone" => "America/Los Angeles"]))], + ]; + } + + public function provideLocalDatetimeYMDData(): array + { + // [$year, $month, $day, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] + return [ + [2000, null, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000]))], + [2000, 12, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12]))], + [2000, 12, 15, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15]))], + [2000, 12, 15, 8, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8]))], + [2000, 12, 15, 8, 25, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25]))], + [2000, 12, 15, 8, 25, 44, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44]))], + [2000, 12, 15, 8, 25, 44, 18, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [2000, 12, 15, 8, 25, 44, 18, 6, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [2000, 12, 15, 8, 25, 44, 18, 6, 31, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + + // types + [new Integer(2000), null, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(12), null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12]))], + [new Integer(2000), new Integer(12), new Integer(15), null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), new Integer(44), null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), new Integer(44), new Integer(18), null, null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), null, new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [new Integer(2000), new Integer(12), new Integer(15), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), new LocalDateTime(Literal::map(["year" => 2000, "month" => 12, "day" => 15, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + ]; + } + + public function provideLocalDatetimeYWDData(): array + { + // [$year, $week, $dayOfWeek, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] + return [ + [2000, null, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000]))], + [2000, 9, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9]))], + [2000, 9, 4, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4]))], + [2000, 9, 4, 8, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8]))], + [2000, 9, 4, 8, 25, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25]))], + [2000, 9, 4, 8, 25, 44, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44]))], + [2000, 9, 4, 8, 25, 44, 18, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [2000, 9, 4, 8, 25, 44, 18, 6, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [2000, 9, 4, 8, 25, 44, 18, 6, 31, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + + // types + [new Integer(2000), null, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(9), null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9]))], + [new Integer(2000), new Integer(9), new Integer(4), null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), new Integer(44), null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), null, null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), null, new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [new Integer(2000), new Integer(9), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), new LocalDateTime(Literal::map(["year" => 2000, "week" => 9, "dayOfWeek" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + ]; + } + + public function provideLocalDatetimeYQDData(): array + { + // [$year, $quarter, $dayOfQuarter, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] + return [ + [2000, null, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000]))], + [2000, 3, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3]))], + [2000, 3, 4, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4]))], + [2000, 3, 4, 8, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8]))], + [2000, 3, 4, 8, 25, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25]))], + [2000, 3, 4, 8, 25, 44, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44]))], + [2000, 3, 4, 8, 25, 44, 18, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [2000, 3, 4, 8, 25, 44, 18, 6, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [2000, 3, 4, 8, 25, 44, 18, 6, 31, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + + // types + [new Integer(2000), null, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(3), null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3]))], + [new Integer(2000), new Integer(3), new Integer(4), null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), new Integer(44), null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), null, null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), null, new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [new Integer(2000), new Integer(3), new Integer(4), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), new LocalDateTime(Literal::map(["year" => 2000, "quarter" => 3, "dayOfQuarter" => 4, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + ]; + } + + public function provideLocalDatetimeYQData(): array + { + // [$year, $ordinalDay, $hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] + return [ + [2000, null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000]))], + [2000, 3, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3]))], + [2000, 3, 8, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8]))], + [2000, 3, 8, 25, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25]))], + [2000, 3, 8, 25, 44, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44]))], + [2000, 3, 8, 25, 44, 18, null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [2000, 3, 8, 25, 44, 18, 6, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [2000, 3, 8, 25, 44, 18, 6, 31, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + + // types + [new Integer(2000), null, null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000]))], + [new Integer(2000), new Integer(3), null, null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3]))], + [new Integer(2000), new Integer(3), new Integer(8), null, null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), null, null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), new Integer(44), null, null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), new Integer(44), new Integer(18), null, null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), null, new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6]))], + [new Integer(2000), new Integer(3), new Integer(8), new Integer(25), new Integer(44), new Integer(18), new Integer(6), new Integer(31), new LocalDateTime(Literal::map(["year" => 2000, "ordinalDay" => 3, "hour" => 8, "minute" => 25, "second" => 44, "millisecond" => 18, "microsecond" => 6, "nanosecond" => 31]))], + ]; + } + + public function provideLocalTimeData(): array + { + // [$hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] + return [ + [11, null, null, null, null, null, new LocalTime(Literal::map(["hour" => 11]))], + [11, 23, null, null, null, null, new LocalTime(Literal::map(["hour" => 11, "minute" => 23]))], + [11, 23, 2, null, null, null, new LocalTime(Literal::map(["hour" => 11, "minute" => 23, "second" => 2]))], + [11, 23, 2, 54, null, null, new LocalTime(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54]))], + [11, 23, 2, 54, 8, null, new LocalTime(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54, "microsecond" => 8]))], + [11, 23, 2, 54, 8, 29, new LocalTime(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54, "microsecond" => 8, "nanosecond" => 29]))], + + // types + [new Integer(11), null, null, null, null, null, new LocalTime(Literal::map(["hour" => 11]))], + [new Integer(11), new Integer(23), null, null, null, null, new LocalTime(Literal::map(["hour" => 11, "minute" => 23]))], + [new Integer(11), new Integer(23), new Integer(2), null, null, null, new LocalTime(Literal::map(["hour" => 11, "minute" => 23, "second" => 2]))], + [new Integer(11), new Integer(23), new Integer(2), new Integer(54), null, null, new LocalTime(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54]))], + [new Integer(11), new Integer(23), new Integer(2), new Integer(54), new Integer(8), null, new LocalTime(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54, "microsecond" => 8]))], + [new Integer(11), new Integer(23), new Integer(2), new Integer(54), new Integer(8), new Integer(29), new LocalTime(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54, "microsecond" => 8, "nanosecond" => 29]))], + ]; + } + + public function provideTimeData(): array + { + // [$hour, $minute, $second, $millisecond, $microsecond, $nanosecond, $expected] + return [ + [11, null, null, null, null, null, new Time(Literal::map(["hour" => 11]))], + [11, 23, null, null, null, null, new Time(Literal::map(["hour" => 11, "minute" => 23]))], + [11, 23, 2, null, null, null, new Time(Literal::map(["hour" => 11, "minute" => 23, "second" => 2]))], + [11, 23, 2, 54, null, null, new Time(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54]))], + [11, 23, 2, 54, 8, null, new Time(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54, "microsecond" => 8]))], + [11, 23, 2, 54, 8, 29, new Time(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54, "microsecond" => 8, "nanosecond" => 29]))], + + // types + [new Integer(11), null, null, null, null, null, new Time(Literal::map(["hour" => 11]))], + [new Integer(11), new Integer(23), null, null, null, null, new Time(Literal::map(["hour" => 11, "minute" => 23]))], + [new Integer(11), new Integer(23), new Integer(2), null, null, null, new Time(Literal::map(["hour" => 11, "minute" => 23, "second" => 2]))], + [new Integer(11), new Integer(23), new Integer(2), new Integer(54), null, null, new Time(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54]))], + [new Integer(11), new Integer(23), new Integer(2), new Integer(54), new Integer(8), null, new Time(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54, "microsecond" => 8]))], + [new Integer(11), new Integer(23), new Integer(2), new Integer(54), new Integer(8), new Integer(29), new Time(Literal::map(["hour" => 11, "minute" => 23, "second" => 2, "millisecond" => 54, "microsecond" => 8, "nanosecond" => 29]))], + ]; + } +} diff --git a/tests/unit/Expressions/Literals/MapTest.php b/tests/unit/Expressions/Literals/MapTest.php new file mode 100644 index 00000000..9738f440 --- /dev/null +++ b/tests/unit/Expressions/Literals/MapTest.php @@ -0,0 +1,131 @@ +assertSame("{}", $map->toQuery()); + } + + /** + * @dataProvider provideNumericalKeysData + */ + public function testNumericalKeys(array $properties, string $expected): void + { + $map = new Map($properties); + + $this->assertSame($expected, $map->toQuery()); + } + + /** + * @dataProvider provideStringKeysData + */ + public function testStringKeys(array $properties, string $expected): void + { + $map = new Map($properties); + + $this->assertSame($expected, $map->toQuery()); + } + + /** + * @dataProvider provideNestedMapsData + */ + public function testNestedMaps(array $properties, string $expected): void + { + $map = new Map($properties); + + $this->assertSame($expected, $map->toQuery()); + } + + public function testMergeWith(): void + { + $map = new Map(["foo" => new String_("bar")]); + $map->mergeWith(new Map(["boo" => new String_("far")])); + + $this->assertSame("{foo: 'bar', boo: 'far'}", $map->toQuery()); + + $map->mergeWith($map); + + $this->assertSame("{foo: 'bar', boo: 'far'}", $map->toQuery()); + } + + public function testAdd(): void + { + $map = new Map(["foo" => new String_("bar")]); + $map->add('foo', new String_("baz")); + + $this->assertSame("{foo: 'baz'}", $map->toQuery()); + + $map->add('boo', new String_("far")); + + $this->assertSame("{foo: 'baz', boo: 'far'}", $map->toQuery()); + + $map->add('boo', false); + + $this->assertSame("{foo: 'baz', boo: false}", $map->toQuery()); + } + + public function testIsEmpty(): void + { + $map = new Map(); + + $this->assertTrue($map->isEmpty()); + + $map->add('boo', 'far'); + + $this->assertFalse($map->isEmpty()); + } + + public function testIsInstanceOfMapType(): void + { + $map = new Map(); + + $this->assertInstanceOf(MapType::class, $map); + } + + public function provideNumericalKeysData(): array + { + return [ + [[0 => new String_('a')], "{`0`: 'a'}"], + [[0 => new String_('a'), 1 => new String_('b')], "{`0`: 'a', `1`: 'b'}"], + ]; + } + + public function provideStringKeysData(): array + { + return [ + [['a' => new String_('a')], "{a: 'a'}"], + [['a' => new String_('a'), 'b' => new String_('b')], "{a: 'a', b: 'b'}"], + [['a' => new String_('b')], "{a: 'b'}"], + [[':' => new String_('a')], "{`:`: 'a'}"], + ]; + } + + public function provideNestedMapsData() + { + return [ + [['a' => new Map([])], "{a: {}}"], + [['a' => new Map(['a' => new Map(['a' => new String_('b')])])], "{a: {a: {a: 'b'}}}"], + [['a' => new Map(['b' => new String_('c')]), 'b' => new String_('d')], "{a: {b: 'c'}, b: 'd'}"], + ]; + } +} diff --git a/tests/unit/Expressions/Literals/StringTest.php b/tests/unit/Expressions/Literals/StringTest.php new file mode 100644 index 00000000..1be239ae --- /dev/null +++ b/tests/unit/Expressions/Literals/StringTest.php @@ -0,0 +1,119 @@ +useDoubleQuotes(false); + + $this->assertSame("''", $string->toQuery()); + $this->assertEquals('', $string->getValue()); + $this->assertFalse($string->usesDoubleQuotes()); + } + + public function testEmptyDoubleQuotes(): void + { + $string = new String_(""); + $string->useDoubleQuotes(true); + + $this->assertSame('""', $string->toQuery()); + $this->assertEquals('', $string->getValue()); + $this->assertTrue($string->usesDoubleQuotes()); + } + + public function testInstanceOfStringType(): void + { + $this->assertInstanceOf(StringType::class, new String_("")); + } + + /** + * @dataProvider provideSingleQuotesData + */ + public function testSingleQuotes(string $string, string $expected): void + { + $literal = new String_($string); + $literal->useDoubleQuotes(false); + + $this->assertSame($expected, $literal->toQuery()); + $this->assertEquals($string, $literal->getValue()); + $this->assertFalse($literal->usesDoubleQuotes()); + } + + /** + * @dataProvider provideDoubleQuotesData + */ + public function testDoubleQuotes(string $string, string $expected): void + { + $literal = new String_($string); + $literal->useDoubleQuotes(); + + $this->assertSame($expected, $literal->toQuery()); + $this->assertEquals($string, $literal->getValue()); + $this->assertTrue($literal->usesDoubleQuotes()); + } + + public function provideSingleQuotesData(): array + { + return [ + ["a", "'a'"], + ["b", "'b'"], + ["\t", "'\\t'"], + ["\b", "'\\\\b'"], + ["\n", "'\\n'"], + ["\r", "'\\r'"], + ["\f", "'\\f'"], + ["'", "'\\''"], + ["\"", "'\\\"'"], + ["\\\\", "'\\\\\\\\'"], + ["\u1234", "'\\\\u1234'"], + ["\U12345678", "'\\\\U12345678'"], + ["\u0000", "'\\\\u0000'"], + ["\uffff", "'\\\\uffff'"], + ["\U00000000", "'\\\\U00000000'"], + ["\Uffffffff", "'\\\\Uffffffff'"], + ["\\\\b", "'\\\\\\\\b'"], + ["\t\n\\", "'\\t\\n\\\\'"], + ]; + } + + public function provideDoubleQuotesData(): array + { + return [ + ["a", "\"a\""], + ["b", "\"b\""], + ["\t", "\"\\t\""], + ["\b", "\"\\\\b\""], + ["\n", "\"\\n\""], + ["\r", "\"\\r\""], + ["\f", "\"\\f\""], + ["'", "\"\\'\""], + ["\"", "\"\\\"\""], + ["\\\\", "\"\\\\\\\\\""], + ["\u1234", "\"\\\\u1234\""], + ["\U12345678", "\"\\\\U12345678\""], + ["\u0000", "\"\\\\u0000\""], + ["\uffff", "\"\\\\uffff\""], + ["\U00000000", "\"\\\\U00000000\""], + ["\Uffffffff", "\"\\\\Uffffffff\""], + ["\\\\b", "\"\\\\\\\\b\""], + ["\t\n\\", "\"\\t\\n\\\\\""], + ]; + } +} diff --git a/tests/unit/Expressions/Operators/AdditionTest.php b/tests/unit/Expressions/Operators/AdditionTest.php new file mode 100644 index 00000000..93565fb2 --- /dev/null +++ b/tests/unit/Expressions/Operators/AdditionTest.php @@ -0,0 +1,66 @@ +assertSame("(10 + 15.0)", $addition->toQuery()); + + $this->assertSame($left, $addition->getLeft()); + $this->assertSame($right, $addition->getRight()); + + $newAddition = new Addition($addition, $addition); + + $this->assertSame("((10 + 15.0) + (10 + 15.0))", $newAddition->toQuery()); + + $this->assertTrue($newAddition->insertsParentheses()); + $this->assertEquals($addition, $newAddition->getLeft()); + $this->assertEquals($addition, $newAddition->getRight()); + + $newAddition = new Addition($addition, $addition, false); + + $this->assertSame("(10 + 15.0) + (10 + 15.0)", $newAddition->toQuery()); + + $this->assertFalse($newAddition->insertsParentheses()); + $this->assertEquals($addition, $newAddition->getLeft()); + $this->assertEquals($addition, $newAddition->getRight()); + } + + public function testInstanceOfFloatType(): void + { + $addition = new Addition(Literal::float(1), Literal::float(1)); + + $this->assertInstanceOf(FloatType::class, $addition); + } + + public function testInstanceOfIntegerType(): void + { + $addition = new Addition(Literal::float(1), Literal::float(1)); + + $this->assertInstanceOf(IntegerType::class, $addition); + } +} diff --git a/tests/unit/Expressions/Operators/ConjunctionTest.php b/tests/unit/Expressions/Operators/ConjunctionTest.php new file mode 100644 index 00000000..c20317b6 --- /dev/null +++ b/tests/unit/Expressions/Operators/ConjunctionTest.php @@ -0,0 +1,50 @@ +assertSame("(true AND false)", $and->toQuery()); + + $and = new Conjunction($and, $and); + + $this->assertSame("((true AND false) AND (true AND false))", $and->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $and = new Conjunction(new Boolean(true), new Boolean(false), false); + + $this->assertSame("true AND false", $and->toQuery()); + + $and = new Conjunction($and, $and); + + $this->assertSame("(true AND false AND true AND false)", $and->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $and = new Conjunction(new Boolean(true), new Boolean(false)); + + $this->assertInstanceOf(BooleanType::class, $and); + } +} diff --git a/tests/unit/Expressions/Operators/ContainsTest.php b/tests/unit/Expressions/Operators/ContainsTest.php new file mode 100644 index 00000000..f5b7899a --- /dev/null +++ b/tests/unit/Expressions/Operators/ContainsTest.php @@ -0,0 +1,58 @@ +assertSame("(a CONTAINS 'b')", $contains->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $contains = new Contains(new Variable("a"), new String_("b"), false); + + $this->assertSame("a CONTAINS 'b'", $contains->toQuery()); + } + + public function testCannotBeNested(): void + { + $contains = new Contains(new Variable("a"), new String_("b")); + + $this->expectException(TypeError::class); + + new Contains($contains, $contains); + } + + public function testInstanceOfBooleanType(): void + { + $a = Query::variable('a'); + $b = Literal::string('foo'); + + $contains = new Contains($a, $b); + + $this->assertInstanceOf(BooleanType::class, $contains); + } +} diff --git a/tests/unit/Expressions/Operators/DisjunctionTest.php b/tests/unit/Expressions/Operators/DisjunctionTest.php new file mode 100644 index 00000000..4a3c5be1 --- /dev/null +++ b/tests/unit/Expressions/Operators/DisjunctionTest.php @@ -0,0 +1,51 @@ +assertSame("(true OR false)", $or->toQuery()); + + $or = new Disjunction($or, $or); + + $this->assertSame("((true OR false) OR (true OR false))", $or->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $or = new Disjunction(new Boolean(true), new Boolean(false), false); + + $this->assertSame("true OR false", $or->toQuery()); + + $or = new Disjunction($or, $or); + + $this->assertSame("(true OR false OR true OR false)", $or->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $or = new Disjunction(Literal::boolean(true), Literal::boolean(true)); + + $this->assertInstanceOf(BooleanType::class, $or); + } +} diff --git a/tests/unit/Expressions/Operators/DivisionTest.php b/tests/unit/Expressions/Operators/DivisionTest.php new file mode 100644 index 00000000..f9ace69e --- /dev/null +++ b/tests/unit/Expressions/Operators/DivisionTest.php @@ -0,0 +1,59 @@ +assertSame("(10 / 15)", $division->toQuery()); + + $division = new Division($division, $division); + + $this->assertSame("((10 / 15) / (10 / 15))", $division->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $division = new Division(new Integer(10), new Integer(15), false); + + $this->assertSame("10 / 15", $division->toQuery()); + + $division = new Division($division, $division); + + $this->assertSame("(10 / 15 / 10 / 15)", $division->toQuery()); + } + + public function testInstanceOfFloatType(): void + { + $division = new Division(Literal::integer(10), Literal::integer(10)); + + $this->assertInstanceOf(FloatType::class, $division); + } + + public function testInstanceOfIntegerType(): void + { + $division = new Division(Literal::integer(10), Literal::integer(10)); + + $this->assertInstanceOf(IntegerType::class, $division); + } +} diff --git a/tests/unit/Expressions/Operators/EndsWithTest.php b/tests/unit/Expressions/Operators/EndsWithTest.php new file mode 100644 index 00000000..9de59c79 --- /dev/null +++ b/tests/unit/Expressions/Operators/EndsWithTest.php @@ -0,0 +1,57 @@ +assertSame("(a ENDS WITH 'b')", $endsWith->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $endsWith = new EndsWith(new Variable("a"), new String_("b"), false); + + $this->assertSame("a ENDS WITH 'b'", $endsWith->toQuery()); + } + + public function testCannotBeNested(): void + { + $endsWith = new EndsWith(new Variable("a"), new String_("b")); + + $this->expectException(TypeError::class); + + $endsWith = new EndsWith($endsWith, $endsWith); + + $endsWith->toQuery(); + } + + public function testInstanceOfBooleanType(): void + { + $endsWith = new EndsWith(Query::variable('a'), Literal::string('a')); + + $this->assertInstanceOf(BooleanType::class, $endsWith); + } +} diff --git a/tests/unit/Expressions/Operators/EqualityTest.php b/tests/unit/Expressions/Operators/EqualityTest.php new file mode 100644 index 00000000..1d497bef --- /dev/null +++ b/tests/unit/Expressions/Operators/EqualityTest.php @@ -0,0 +1,51 @@ +assertSame("(10 = 15)", $equality->toQuery()); + + $equality = new Equality($equality, $equality); + + $this->assertSame("((10 = 15) = (10 = 15))", $equality->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $equality = new Equality(new Integer(10), new Integer(15), false); + + $this->assertSame("10 = 15", $equality->toQuery()); + + $equality = new Equality($equality, $equality); + + $this->assertSame("(10 = 15 = 10 = 15)", $equality->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $equality = new Equality(Literal::string('foo'), Literal::string('bar')); + + $this->assertInstanceOf(BooleanType::class, $equality); + } +} diff --git a/tests/unit/Expressions/Operators/ExclusiveDisjunctionTest.php b/tests/unit/Expressions/Operators/ExclusiveDisjunctionTest.php new file mode 100644 index 00000000..efb2f9e4 --- /dev/null +++ b/tests/unit/Expressions/Operators/ExclusiveDisjunctionTest.php @@ -0,0 +1,50 @@ +assertSame("(true XOR false)", $xor->toQuery()); + + $xor = new ExclusiveDisjunction($xor, $xor); + + $this->assertSame("((true XOR false) XOR (true XOR false))", $xor->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $xor = new ExclusiveDisjunction(new Boolean(true), new Boolean(false), false); + + $this->assertSame("true XOR false", $xor->toQuery()); + + $xor = new ExclusiveDisjunction($xor, $xor); + + $this->assertSame("(true XOR false XOR true XOR false)", $xor->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $and = new ExclusiveDisjunction(new Boolean(true), new Boolean(false)); + + $this->assertInstanceOf(BooleanType::class, $and); + } +} diff --git a/tests/unit/Expressions/Operators/ExponentiationTest.php b/tests/unit/Expressions/Operators/ExponentiationTest.php new file mode 100644 index 00000000..db176b2d --- /dev/null +++ b/tests/unit/Expressions/Operators/ExponentiationTest.php @@ -0,0 +1,52 @@ +assertSame("(10 ^ 15)", $exponentiation->toQuery()); + + $exponentiation = new Exponentiation($exponentiation, $exponentiation); + + $this->assertSame("((10 ^ 15) ^ (10 ^ 15))", $exponentiation->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $exponentiation = new Exponentiation(new Integer(10), new Integer(15), false); + + $this->assertSame("10 ^ 15", $exponentiation->toQuery()); + + $exponentiation = new Exponentiation($exponentiation, $exponentiation); + + $this->assertSame("(10 ^ 15 ^ 10 ^ 15)", $exponentiation->toQuery()); + } + + public function testInstanceOfNumeralType(): void + { + $and = new Exponentiation(new Integer(1), new Integer(1)); + + $this->assertInstanceOf(FloatType::class, $and); + $this->assertInstanceOf(IntegerType::class, $and); + } +} diff --git a/tests/unit/Expressions/Operators/GreaterThanOrEqualTest.php b/tests/unit/Expressions/Operators/GreaterThanOrEqualTest.php new file mode 100644 index 00000000..733c93dd --- /dev/null +++ b/tests/unit/Expressions/Operators/GreaterThanOrEqualTest.php @@ -0,0 +1,46 @@ +assertSame("(10 >= 15)", $greaterThanOrEqual->toQuery()); + + $greaterThanOrEqual = new GreaterThanOrEqual($greaterThanOrEqual, $greaterThanOrEqual); + + $this->assertSame("((10 >= 15) >= (10 >= 15))", $greaterThanOrEqual->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $greaterThanOrEqual = new GreaterThanOrEqual(new Integer(10), new Integer(15), false); + + $this->assertSame("10 >= 15", $greaterThanOrEqual->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $greaterThanOrEqual = new GreaterThanOrEqual(new Integer(1), new Integer(1)); + + $this->assertInstanceOf(BooleanType::class, $greaterThanOrEqual); + } +} diff --git a/tests/unit/Expressions/Operators/GreaterThanTest.php b/tests/unit/Expressions/Operators/GreaterThanTest.php new file mode 100644 index 00000000..d89a50a7 --- /dev/null +++ b/tests/unit/Expressions/Operators/GreaterThanTest.php @@ -0,0 +1,46 @@ +assertSame("(10 > 15)", $greaterThan->toQuery()); + + $greaterThan = new GreaterThan($greaterThan, $greaterThan, false); + + $this->assertSame("(10 > 15) > (10 > 15)", $greaterThan->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $greaterThan = new GreaterThan(new Integer(10), new Integer(15), false); + + $this->assertSame("10 > 15", $greaterThan->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $greaterThan = new GreaterThan(new Integer(1), new Integer(1)); + + $this->assertInstanceOf(BooleanType::class, $greaterThan); + } +} diff --git a/tests/unit/Expressions/Operators/InTest.php b/tests/unit/Expressions/Operators/InTest.php new file mode 100644 index 00000000..fefbbae2 --- /dev/null +++ b/tests/unit/Expressions/Operators/InTest.php @@ -0,0 +1,53 @@ +assertSame("(v.a IN b)", $in->toQuery()); + + $in = new In($in, new List_([new Boolean(true), new Boolean(false)])); + + $this->assertSame("((v.a IN b) IN [true, false])", $in->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $in = new In(new Property(new Variable('v'), "a"), new Variable('b'), false); + + $this->assertSame("v.a IN b", $in->toQuery()); + + $in = new In($in, new List_([new Boolean(true), new Boolean(false)])); + + $this->assertSame("(v.a IN b IN [true, false])", $in->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $in = new In(new Property(new Variable('a'), 'a'), new Variable('b')); + + $this->assertInstanceOf(BooleanType::class, $in); + } +} diff --git a/tests/unit/Expressions/Operators/InequalityTest.php b/tests/unit/Expressions/Operators/InequalityTest.php new file mode 100644 index 00000000..cb0c5fff --- /dev/null +++ b/tests/unit/Expressions/Operators/InequalityTest.php @@ -0,0 +1,51 @@ +assertSame("(v.a <> v.b)", $inequality->toQuery()); + + $inequality = new Inequality($inequality, $inequality); + + $this->assertSame("((v.a <> v.b) <> (v.a <> v.b))", $inequality->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $inequality = new Inequality(new Property(new Variable('v'), "a"), new Property(new Variable('v'), "b"), false); + + $this->assertSame("v.a <> v.b", $inequality->toQuery()); + + $inequality = new Inequality($inequality, $inequality); + + $this->assertSame("(v.a <> v.b <> v.a <> v.b)", $inequality->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $inequality = new Inequality(new Variable('a'), new Variable('b')); + + $this->assertInstanceOf(BooleanType::class, $inequality); + } +} diff --git a/tests/unit/Expressions/Operators/IsNotNullTest.php b/tests/unit/Expressions/Operators/IsNotNullTest.php new file mode 100644 index 00000000..f0d3c33b --- /dev/null +++ b/tests/unit/Expressions/Operators/IsNotNullTest.php @@ -0,0 +1,43 @@ +assertFalse($isNotNull->insertsParentheses()); + + $this->assertSame("true IS NOT NULL", $isNotNull->toQuery()); + + $isNotNull = new IsNotNull($isNotNull); + + $this->assertSame("(true IS NOT NULL IS NOT NULL)", $isNotNull->toQuery()); + + $this->assertTrue($isNotNull->insertsParentheses()); + } + + public function testInstanceOfBooleanType(): void + { + $isNotNull = new IsNotNull(new Boolean(true)); + + $this->assertInstanceOf(BooleanType::class, $isNotNull); + } +} diff --git a/tests/unit/Expressions/Operators/IsNullTest.php b/tests/unit/Expressions/Operators/IsNullTest.php new file mode 100644 index 00000000..744bc79d --- /dev/null +++ b/tests/unit/Expressions/Operators/IsNullTest.php @@ -0,0 +1,43 @@ +assertFalse($isNull->insertsParentheses()); + + $this->assertSame("true IS NULL", $isNull->toQuery()); + + $isNull = new IsNull($isNull); + + $this->assertSame("(true IS NULL IS NULL)", $isNull->toQuery()); + + $this->assertTrue($isNull->insertsParentheses()); + } + + public function testInstanceOfBooleanType(): void + { + $isNull = new IsNull(new Boolean(true)); + + $this->assertInstanceOf(BooleanType::class, $isNull); + } +} diff --git a/tests/unit/Expressions/Operators/LessThanOrEqualTest.php b/tests/unit/Expressions/Operators/LessThanOrEqualTest.php new file mode 100644 index 00000000..dba00794 --- /dev/null +++ b/tests/unit/Expressions/Operators/LessThanOrEqualTest.php @@ -0,0 +1,46 @@ +assertSame("(10 <= 15)", $lessThanOrEqual->toQuery()); + + $lessThanOrEqual = new LessThanOrEqual($lessThanOrEqual, $lessThanOrEqual, false); + + $this->assertSame("(10 <= 15) <= (10 <= 15)", $lessThanOrEqual->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $lessThanOrEqual = new LessThanOrEqual(new Integer(10), new Integer(15), false); + + $this->assertSame("10 <= 15", $lessThanOrEqual->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $lessThanOrEqual = new LessThanOrEqual(new Integer(1), new Integer(2)); + + $this->assertInstanceOf(BooleanType::class, $lessThanOrEqual); + } +} diff --git a/tests/unit/Expressions/Operators/LessThanTest.php b/tests/unit/Expressions/Operators/LessThanTest.php new file mode 100644 index 00000000..e19ec7db --- /dev/null +++ b/tests/unit/Expressions/Operators/LessThanTest.php @@ -0,0 +1,46 @@ +assertSame("(10 < 15)", $lessThan->toQuery()); + + $lessThan = new LessThan($lessThan, $lessThan, false); + + $this->assertSame("(10 < 15) < (10 < 15)", $lessThan->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $lessThan = new LessThan(new Integer(10), new Integer(15), false); + + $this->assertSame("10 < 15", $lessThan->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $lessThan = new LessThan(new Integer(1), new Integer(2)); + + $this->assertInstanceOf(BooleanType::class, $lessThan); + } +} diff --git a/tests/unit/Expressions/Operators/ModuloDivisionTest.php b/tests/unit/Expressions/Operators/ModuloDivisionTest.php new file mode 100644 index 00000000..9f9533e2 --- /dev/null +++ b/tests/unit/Expressions/Operators/ModuloDivisionTest.php @@ -0,0 +1,59 @@ +assertSame("(10 % 15)", $moduloDivision->toQuery()); + + $moduloDivision = new ModuloDivision($moduloDivision, $moduloDivision); + + $this->assertSame("((10 % 15) % (10 % 15))", $moduloDivision->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $moduloDivision = new ModuloDivision(new Integer(10), new Integer(15), false); + + $this->assertSame("10 % 15", $moduloDivision->toQuery()); + + $moduloDivision = new ModuloDivision($moduloDivision, $moduloDivision); + + $this->assertSame("(10 % 15 % 10 % 15)", $moduloDivision->toQuery()); + } + + public function testInstanceOfIntegerType(): void + { + $moduloDivision = new ModuloDivision(new Integer(10), new Integer(15)); + + $this->assertInstanceOf(IntegerType::class, $moduloDivision); + } + + public function testInstanceOfFloatType(): void + { + $moduloDivision = new ModuloDivision(new Float_(10.0), new Float_(15.0)); + + $this->assertInstanceOf(FloatType::class, $moduloDivision); + } +} diff --git a/tests/unit/Expressions/Operators/MultiplicationTest.php b/tests/unit/Expressions/Operators/MultiplicationTest.php new file mode 100644 index 00000000..7bcadab0 --- /dev/null +++ b/tests/unit/Expressions/Operators/MultiplicationTest.php @@ -0,0 +1,59 @@ +assertSame("(10 * 15)", $multiplication->toQuery()); + + $multiplication = new Multiplication($multiplication, $multiplication); + + $this->assertSame("((10 * 15) * (10 * 15))", $multiplication->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $multiplication = new Multiplication(new Integer(10), new Integer(15), false); + + $this->assertSame("10 * 15", $multiplication->toQuery()); + + $multiplication = new Multiplication($multiplication, $multiplication); + + $this->assertSame("(10 * 15 * 10 * 15)", $multiplication->toQuery()); + } + + public function testInstanceOfFloatType(): void + { + $multiplication = new Multiplication(new Float_(10.0), new Float_(15.0), false); + + $this->assertInstanceOf(FloatType::class, $multiplication); + } + + public function testInstanceOfIntegerType(): void + { + $multiplication = new Multiplication(new Integer(10), new Integer(15), false); + + $this->assertInstanceOf(IntegerType::class, $multiplication); + } +} diff --git a/tests/unit/Expressions/Operators/NegationTest.php b/tests/unit/Expressions/Operators/NegationTest.php new file mode 100644 index 00000000..e2bad538 --- /dev/null +++ b/tests/unit/Expressions/Operators/NegationTest.php @@ -0,0 +1,46 @@ +assertSame("(NOT true)", $not->toQuery()); + + $not = new Negation($not); + + $this->assertSame("(NOT (NOT true))", $not->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $not = new Negation(new Boolean(true), false); + + $this->assertSame("NOT true", $not->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $not = new Negation(new Boolean(true)); + + $this->assertInstanceOf(BooleanType::class, $not); + } +} diff --git a/tests/unit/Expressions/Operators/RegexTest.php b/tests/unit/Expressions/Operators/RegexTest.php new file mode 100644 index 00000000..b45d5ded --- /dev/null +++ b/tests/unit/Expressions/Operators/RegexTest.php @@ -0,0 +1,43 @@ +assertSame("(a =~ 'b')", $regex->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $regex = new Regex(new Variable("a"), new String_("b"), false); + + $this->assertSame("a =~ 'b'", $regex->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $regex = new Regex(new Variable("a"), new String_("b")); + + $this->assertInstanceOf(BooleanType::class, $regex); + } +} diff --git a/tests/unit/Expressions/Operators/StartsWithTest.php b/tests/unit/Expressions/Operators/StartsWithTest.php new file mode 100644 index 00000000..70f479fc --- /dev/null +++ b/tests/unit/Expressions/Operators/StartsWithTest.php @@ -0,0 +1,43 @@ +assertSame("(a STARTS WITH 'b')", $startsWith->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $startsWith = new StartsWith(new Variable("a"), new String_("b"), false); + + $this->assertSame("a STARTS WITH 'b'", $startsWith->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $startsWith = new StartsWith(new Variable("a"), new String_("b")); + + $this->assertInstanceOf(BooleanType::class, $startsWith); + } +} diff --git a/tests/unit/Expressions/Operators/SubtractionTest.php b/tests/unit/Expressions/Operators/SubtractionTest.php new file mode 100644 index 00000000..43f73577 --- /dev/null +++ b/tests/unit/Expressions/Operators/SubtractionTest.php @@ -0,0 +1,58 @@ +assertSame("(10 - 15)", $subtraction->toQuery()); + + $subtraction = new Subtraction($subtraction, $subtraction); + + $this->assertSame("((10 - 15) - (10 - 15))", $subtraction->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $subtraction = new Subtraction(new Integer(10), new Integer(15), false); + + $this->assertSame("10 - 15", $subtraction->toQuery()); + + $subtraction = new Subtraction($subtraction, $subtraction); + + $this->assertSame("(10 - 15 - 10 - 15)", $subtraction->toQuery()); + } + + public function testInstanceOfIntegerType(): void + { + $subtraction = new Subtraction(new Integer(10), new Integer(15)); + + $this->assertInstanceOf(IntegerType::class, $subtraction); + } + + public function testInstanceOfFloatType(): void + { + $subtraction = new Subtraction(new Integer(10), new Integer(15)); + + $this->assertInstanceOf(FloatType::class, $subtraction); + } +} diff --git a/tests/unit/Expressions/Operators/UnaryMinusTest.php b/tests/unit/Expressions/Operators/UnaryMinusTest.php new file mode 100644 index 00000000..98d7da9c --- /dev/null +++ b/tests/unit/Expressions/Operators/UnaryMinusTest.php @@ -0,0 +1,58 @@ +assertSame("(- -10)", $minus->toQuery()); + + $minus = new UnaryMinus($minus); + + $this->assertSame("(- (- -10))", $minus->toQuery()); + + $minus = new UnaryMinus(new Integer(10)); + + $this->assertSame("(- 10)", $minus->toQuery()); + } + + public function testToQueryNoParentheses(): void + { + $minus = new UnaryMinus(new Integer(10), false); + + $this->assertSame("- 10", $minus->toQuery()); + } + + public function testInstanceOfFloatType(): void + { + $minus = new UnaryMinus(new Integer(-10)); + + $this->assertInstanceOf(FloatType::class, $minus); + } + + public function testInstanceOfIntegerType(): void + { + $minus = new UnaryMinus(new Integer(-10)); + + $this->assertInstanceOf(IntegerType::class, $minus); + } +} diff --git a/tests/unit/Expressions/ParameterTest.php b/tests/unit/Expressions/ParameterTest.php new file mode 100644 index 00000000..77f7afab --- /dev/null +++ b/tests/unit/Expressions/ParameterTest.php @@ -0,0 +1,59 @@ +assertSame($expected, $parameter->toQuery()); + $this->assertSame(str_replace('$', '', $expected), $parameter->getParameter()); + } + + /** + * @dataProvider provideThrowsExceptionOnInvalidData + */ + public function testThrowsExceptionOnInvalid(string $parameter): void + { + $this->expectException(InvalidArgumentException::class); + + new Parameter($parameter); + } + + public function provideToQueryData(): array + { + return [ + ["a", '$a'], + ["b", '$b'], + ["foo_bar", '$foo_bar'], + ["A", '$A'], + ]; + } + + public function provideThrowsExceptionOnInvalidData(): array + { + return [ + [""], + [str_repeat('a', 65535)], + ]; + } +} diff --git a/tests/unit/Expressions/Procedures/AllTest.php b/tests/unit/Expressions/Procedures/AllTest.php new file mode 100644 index 00000000..f16826e5 --- /dev/null +++ b/tests/unit/Expressions/Procedures/AllTest.php @@ -0,0 +1,42 @@ +assertSame("all(variable IN ['a', 'b'] WHERE true)", $all->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $variable = Query::variable('variable'); + $list = Literal::list(['a', 'b']); + + $all = new All($variable, $list, Literal::boolean(true)); + + $this->assertInstanceOf(BooleanType::class, $all); + } +} diff --git a/tests/unit/Expressions/Procedures/AnyTest.php b/tests/unit/Expressions/Procedures/AnyTest.php new file mode 100644 index 00000000..814f8ac1 --- /dev/null +++ b/tests/unit/Expressions/Procedures/AnyTest.php @@ -0,0 +1,41 @@ +assertSame("any(variable IN ['a', 'b'] WHERE true)", $any->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $variable = Query::variable('variable'); + $list = Query::list(['a', 'b']); + + $any = new Any($variable, $list, Query::boolean(true)); + + $this->assertInstanceOf(BooleanType::class, $any); + } +} diff --git a/tests/unit/Expressions/Procedures/DateTest.php b/tests/unit/Expressions/Procedures/DateTest.php new file mode 100644 index 00000000..e999b4c4 --- /dev/null +++ b/tests/unit/Expressions/Procedures/DateTest.php @@ -0,0 +1,45 @@ + new String_('bar')]); + + $date = new Date($map); + + $this->assertSame("date({foo: 'bar'})", $date->toQuery()); + } + + public function testEmpty(): void + { + $date = new Date(); + + $this->assertSame("date()", $date->toQuery()); + } + + public function testInstanceOfDateType(): void + { + $date = new Date(); + + $this->assertInstanceOf(DateType::class, $date); + } +} diff --git a/tests/unit/Expressions/Procedures/DateTimeTest.php b/tests/unit/Expressions/Procedures/DateTimeTest.php new file mode 100644 index 00000000..ff55e6bc --- /dev/null +++ b/tests/unit/Expressions/Procedures/DateTimeTest.php @@ -0,0 +1,45 @@ + new String_('bar')]); + + $dateTime = new DateTime($map); + + $this->assertSame("datetime({foo: 'bar'})", $dateTime->toQuery()); + } + + public function testEmpty(): void + { + $dateTime = new DateTime(); + + $this->assertSame("datetime()", $dateTime->toQuery()); + } + + public function testInstanceOfDateTimeType(): void + { + $dateTime = new DateTime(); + + $this->assertInstanceOf(DateTimeType::class, $dateTime); + } +} diff --git a/tests/unit/Expressions/Procedures/ExistsTest.php b/tests/unit/Expressions/Procedures/ExistsTest.php new file mode 100644 index 00000000..819e9169 --- /dev/null +++ b/tests/unit/Expressions/Procedures/ExistsTest.php @@ -0,0 +1,37 @@ +assertSame("exists('expression')", $exists->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $expression = Query::string("expression"); + $exists = new Exists($expression); + + $this->assertInstanceOf(BooleanType::class, $exists); + } +} diff --git a/tests/unit/Expressions/Procedures/IsEmptyTest.php b/tests/unit/Expressions/Procedures/IsEmptyTest.php new file mode 100644 index 00000000..5a68e26a --- /dev/null +++ b/tests/unit/Expressions/Procedures/IsEmptyTest.php @@ -0,0 +1,37 @@ +assertSame("isEmpty(['a', 'b'])", $isEmpty->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $list = Query::list(['a', 'b']); + $isEmpty = new IsEmpty($list); + + $this->assertInstanceOf(BooleanType::class, $isEmpty); + } +} diff --git a/tests/unit/Expressions/Procedures/LocalDateTimeTest.php b/tests/unit/Expressions/Procedures/LocalDateTimeTest.php new file mode 100644 index 00000000..2919960e --- /dev/null +++ b/tests/unit/Expressions/Procedures/LocalDateTimeTest.php @@ -0,0 +1,43 @@ + 'bar']); + $dateTime = new LocalDateTime($map); + + $this->assertSame("localdatetime({foo: 'bar'})", $dateTime->toQuery()); + } + + public function testEmpty(): void + { + $dateTime = new LocalDateTime(); + + $this->assertSame("localdatetime()", $dateTime->toQuery()); + } + + public function testInstanceOfLocalDateTimeType(): void + { + $dateTime = new LocalDateTime(); + + $this->assertInstanceOf(LocalDateTimeType::class, $dateTime); + } +} diff --git a/tests/unit/Expressions/Procedures/LocalTimeTest.php b/tests/unit/Expressions/Procedures/LocalTimeTest.php new file mode 100644 index 00000000..18949207 --- /dev/null +++ b/tests/unit/Expressions/Procedures/LocalTimeTest.php @@ -0,0 +1,43 @@ + 'bar']); + $time = new LocalTime($map); + + $this->assertSame("localtime({foo: 'bar'})", $time->toQuery()); + } + + public function testEmpty(): void + { + $time = new LocalTime(); + + $this->assertSame("localtime()", $time->toQuery()); + } + + public function testInstanceOfLocalTimeType(): void + { + $dateTime = new LocalTime(); + + $this->assertInstanceOf(LocalTimeType::class, $dateTime); + } +} diff --git a/tests/unit/Expressions/Procedures/NoneTest.php b/tests/unit/Expressions/Procedures/NoneTest.php new file mode 100644 index 00000000..2d320132 --- /dev/null +++ b/tests/unit/Expressions/Procedures/NoneTest.php @@ -0,0 +1,43 @@ +assertSame("none(variable IN ['foo', 'bar'] WHERE true)", $none->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $variable = Query::variable('variable'); + $list = Query::list(['foo', 'bar']); + $predicate = Query::boolean(true); + + $none = new None($variable, $list, $predicate); + + $this->assertInstanceOf(BooleanType::class, $none); + } +} diff --git a/tests/unit/Expressions/Procedures/PointTest.php b/tests/unit/Expressions/Procedures/PointTest.php new file mode 100644 index 00000000..52cbb594 --- /dev/null +++ b/tests/unit/Expressions/Procedures/PointTest.php @@ -0,0 +1,37 @@ + 1.5, 'longitude' => 4.2]); + $point = new Point($map); + + $this->assertSame("point({latitude: 1.5, longitude: 4.2})", $point->toQuery()); + } + + public function testInstanceOfPointType(): void + { + $map = Query::map(['latitude' => 1.5, 'longitude' => 4.2]); + $point = new Point($map); + + $this->assertInstanceOf(PointType::class, $point); + } +} diff --git a/tests/unit/Expressions/Procedures/ProcedureTest.php b/tests/unit/Expressions/Procedures/ProcedureTest.php new file mode 100644 index 00000000..efb44554 --- /dev/null +++ b/tests/unit/Expressions/Procedures/ProcedureTest.php @@ -0,0 +1,176 @@ +assertInstanceOf(Raw::class, $raw); + } + + public function testAll(): void + { + $variable = Query::variable('a'); + $list = Query::list([]); + $predicate = Query::boolean(true); + + $all = Procedure::all($variable, $list, $predicate); + + $this->assertInstanceOf(All::class, $all); + } + + public function testAny(): void + { + $variable = Query::variable('a'); + $list = Query::list([]); + $predicate = Query::boolean(true); + + $any = Procedure::any($variable, $list, $predicate); + + $this->assertInstanceOf(Any::class, $any); + } + + public function testExists(): void + { + $expression = Query::string("Hello World"); + + $exists = Procedure::exists($expression); + + $this->assertInstanceOf(Exists::class, $exists); + } + + public function testIsEmpty(): void + { + $list = Query::list([]); + + $isEmpty = Procedure::isEmpty($list); + + $this->assertInstanceOf(IsEmpty::class, $isEmpty); + } + + public function testNone(): void + { + $variable = Query::variable('a'); + $list = Query::list([]); + $predicate = Query::boolean(true); + + $none = Procedure::none($variable, $list, $predicate); + + $this->assertInstanceOf(None::class, $none); + } + + public function testSingle(): void + { + $variable = Query::variable('a'); + $list = Query::list([]); + $predicate = Query::boolean(true); + + $single = Procedure::single($variable, $list, $predicate); + + $this->assertInstanceOf(Single::class, $single); + } + + public function testPoint(): void + { + $map = Query::map([]); + + $point = Procedure::point($map); + + $this->assertInstanceOf(Point::class, $point); + } + + public function testDate(): void + { + $value = Query::string("Hello World!"); + + $date = Procedure::date($value); + + $this->assertInstanceOf(Date::class, $date); + + $date = Procedure::date(); + + $this->assertInstanceOf(Date::class, $date); + } + + public function testDateTime(): void + { + $value = Query::string("Hello World!"); + + $date = Procedure::datetime($value); + + $this->assertInstanceOf(DateTime::class, $date); + + $date = Procedure::datetime(); + + $this->assertInstanceOf(DateTime::class, $date); + } + + public function testLocalDateTime(): void + { + $value = Query::string("Hello World!"); + + $date = Procedure::localdatetime($value); + + $this->assertInstanceOf(LocalDateTime::class, $date); + + $date = Procedure::localdatetime(); + + $this->assertInstanceOf(LocalDateTime::class, $date); + } + + public function testLocalTime(): void + { + $value = Query::string("Hello World!"); + + $date = Procedure::localtime($value); + + $this->assertInstanceOf(LocalTime::class, $date); + + $date = Procedure::localtime(); + + $this->assertInstanceOf(LocalTime::class, $date); + } + + public function testTime(): void + { + $value = Query::string("Hello World!"); + + $date = Procedure::time($value); + + $this->assertInstanceOf(Time::class, $date); + + $date = Procedure::time(); + + $this->assertInstanceOf(Time::class, $date); + } +} diff --git a/tests/unit/Expressions/Procedures/RawTest.php b/tests/unit/Expressions/Procedures/RawTest.php new file mode 100644 index 00000000..34aa0947 --- /dev/null +++ b/tests/unit/Expressions/Procedures/RawTest.php @@ -0,0 +1,62 @@ +assertSame("foobar(a, b, c)", $raw->toQuery()); + } + + public function testInstanceOf(): void + { + $raw = new Raw('foo', [Literal::string('foo')]); + + $this->assertInstanceOf(BooleanType::class, $raw); + $this->assertInstanceOf(DateType::class, $raw); + $this->assertInstanceOf(DateTimeType::class, $raw); + $this->assertInstanceOf(FloatType::class, $raw); + $this->assertInstanceOf(IntegerType::class, $raw); + $this->assertInstanceOf(StringType::class, $raw); + $this->assertInstanceOf(MapType::class, $raw); + $this->assertInstanceOf(PointType::class, $raw); + $this->assertInstanceOf(ListType::class, $raw); + $this->assertInstanceOf(LocalDateTimeType::class, $raw); + $this->assertInstanceOf(LocalTimeType::class, $raw); + $this->assertInstanceOf(TimeType::class, $raw); + } +} diff --git a/tests/unit/Expressions/Procedures/SingleTest.php b/tests/unit/Expressions/Procedures/SingleTest.php new file mode 100644 index 00000000..cdda8057 --- /dev/null +++ b/tests/unit/Expressions/Procedures/SingleTest.php @@ -0,0 +1,48 @@ +createMock(AnyType::class); + $predicate->method('toQuery')->willReturn('predicate'); + + $single = new Single($variable, $list, $predicate); + + $this->assertSame("single(variable IN ['a', 'b'] WHERE predicate)", $single->toQuery()); + } + + public function testInstanceOfBooleanType(): void + { + $variable = new Variable("variable"); + $list = new List_([new String_('a'), new String_('b')]); + $predicate = $this->createMock(AnyType::class); + $predicate->method('toQuery')->willReturn('predicate'); + + $single = new Single($variable, $list, $predicate); + + $this->assertInstanceOf(BooleanType::class, $single); + } +} diff --git a/tests/unit/Expressions/Procedures/TimeTest.php b/tests/unit/Expressions/Procedures/TimeTest.php new file mode 100644 index 00000000..786b7469 --- /dev/null +++ b/tests/unit/Expressions/Procedures/TimeTest.php @@ -0,0 +1,45 @@ +new String_('bar')]); + + $time = new Time($map); + + $this->assertSame("time({foo: 'bar'})", $time->toQuery()); + } + + public function testEmpty(): void + { + $time = new Time(); + + $this->assertSame("time()", $time->toQuery()); + } + + public function testInstanceOfTimeType(): void + { + $time = new Time(); + + $this->assertInstanceOf(TimeType::class, $time); + } +} diff --git a/tests/unit/Expressions/PropertyTest.php b/tests/unit/Expressions/PropertyTest.php new file mode 100644 index 00000000..5c3c8ce0 --- /dev/null +++ b/tests/unit/Expressions/PropertyTest.php @@ -0,0 +1,93 @@ +assertSame($expected, $property->toQuery()); + } + + public function testGetProperty(): void + { + $property = new Property(new Variable("a"), 'a'); + + $this->assertSame('a', $property->getProperty()); + } + + public function testGetExpression(): void + { + $variable = new Variable('a'); + $property = new Property($variable, 'a'); + + $this->assertSame($variable, $property->getExpression()); + } + + public function testReplaceWithReturnsPropertyReplacement(): void + { + $property = new Property(new Variable("a"), 'a'); + + $this->assertInstanceOf(PropertyReplacement::class, $property->replaceWith(true)); + } + + public function testReplaceWithCastsPHPTypes(): void + { + $property = new Property(new Variable("a"), 'a'); + + $bool = $property->replaceWith(true); + $float = $property->replaceWith(1.0); + $int = $property->replaceWith(10); + $string = $property->replaceWith("foobar"); + + $this->assertEquals(new Boolean(true), $bool->getValue()); + $this->assertEquals(new Float_(1.0), $float->getValue()); + $this->assertEquals(new Integer(10), $int->getValue()); + $this->assertEquals(new String_("foobar"), $string->getValue()); + } + + public function testReplaceWithUsesSameProperty(): void + { + $property = new Property(new Variable("a"), 'a'); + + $replaceWith = $property->replaceWith("Hello World!"); + + $this->assertSame($property, $replaceWith->getProperty()); + } + + public function provideToQueryData(): array + { + return [ + [new Variable("a"), "a", "a.a"], + [new Map(["b" => new String_("c")]), "b", "{b: 'c'}.b"], + [new Variable("b"), "a", "b.a"], + [new Map([":" => new String_("c")]), ":", "{`:`: 'c'}.`:`"], + ]; + } +} diff --git a/tests/unit/Expressions/RawExpressionTest.php b/tests/unit/Expressions/RawExpressionTest.php new file mode 100644 index 00000000..813df55e --- /dev/null +++ b/tests/unit/Expressions/RawExpressionTest.php @@ -0,0 +1,33 @@ + 4)"); + + $this->assertSame("foobar(3 > 4)", $rawExpression->toQuery()); + } + + public function testGetExpression(): void + { + $rawExpression = new RawExpression("foobar(3 > 4)"); + + $this->assertSame("foobar(3 > 4)", $rawExpression->getExpression()); + } +} diff --git a/tests/unit/Expressions/VariableTest.php b/tests/unit/Expressions/VariableTest.php new file mode 100644 index 00000000..08c874c2 --- /dev/null +++ b/tests/unit/Expressions/VariableTest.php @@ -0,0 +1,117 @@ +assertMatchesRegularExpression('/var[0-9a-f]+/', $variable->toQuery()); + + $variable = new Variable(null); + + $this->assertMatchesRegularExpression('/var[0-9a-f]+/', $variable->toQuery()); + } + + public function testLabeled(): void + { + $variable = new Variable(); + $label = $variable->labeled('hello', 'world'); + + $this->assertInstanceOf(Label::class, $label); + $this->assertSame(['hello', 'world'], $label->getLabels()); + $this->assertSame($variable, $label->getVariable()); + } + + public function testAssign(): void + { + $variable = new Variable(); + $map = Query::map(['foo' => 'bar', 'boo' => 'far']); + $assignment = $variable->assign($map); + + $this->assertInstanceOf(PropertyReplacement::class, $assignment); + $this->assertSame($variable, $assignment->getProperty()); + $this->assertSame($map, $assignment->getValue()); + } + + public function testGetNameReturnsEscaped(): void + { + $variable = new Variable('$foo'); + + $this->assertSame('`$foo`', $variable->getName()); + } + + public function testEmptyNameIsNotAllowed(): void + { + $this->expectException(InvalidArgumentException::class); + + new Variable(''); + } + + public function testLongNameIsNotAllowed(): void + { + $name = str_repeat('a', 65535); + + $this->expectException(InvalidArgumentException::class); + + new Variable($name); + } + + /** + * @dataProvider provideToQueryData + */ + public function testToQuery(string $variable, string $expected): void + { + $variable = new Variable($variable); + + $this->assertSame($expected, $variable->toQuery()); + } + + /** + * @dataProvider providePropertyData + */ + public function testProperty(string $variable, string $property, Property $expected): void + { + $variable = new Variable($variable); + $property = $variable->property($property); + + $this->assertEquals($expected, $property); + } + + public function provideToQueryData(): array + { + return [ + ["a", "a"], + ["b", "b"], + ['$foo', '`$foo`'], + ]; + } + + public function providePropertyData(): array + { + return [ + ["a", "a", new Property(new Variable("a"), "a")], + ["a", "b", new Property(new Variable("a"), "b")], + ]; + } +} diff --git a/tests/unit/FunctionsTest.php b/tests/unit/FunctionsTest.php new file mode 100644 index 00000000..d6c2a7eb --- /dev/null +++ b/tests/unit/FunctionsTest.php @@ -0,0 +1,248 @@ +assertInstanceOf(Query::class, $query); + } + + public function testNodeReturnsNode(): void + { + $node = node('label'); + + $this->assertInstanceOf(Node::class, $node); + $this->assertSame('(:label)', $node->toQuery()); + } + + public function testNodeAcceptsEmptyLabel(): void + { + $node = node(); + + $this->assertInstanceOf(Node::class, $node); + $this->assertSame('()', $node->toQuery()); + } + + public function testNodeOnlyAcceptsStringLabel(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + node([]); + } + + public function testRelationshipReturnsRelationship(): void + { + $relationship = relationship(Relationship::DIR_RIGHT); + + $this->assertInstanceOf(Relationship::class, $relationship); + $this->assertSame('-->', $relationship->toQuery()); + } + + public function testVariableReturnsVariable(): void + { + $variable = variable('foobar'); + + $this->assertInstanceOf(Variable::class, $variable); + $this->assertSame('foobar', $variable->toQuery()); + } + + public function testVariableAcceptsEmptyArgument(): void + { + $variable = variable(); + + $this->assertInstanceOf(Variable::class, $variable); + $this->assertStringMatchesFormat('var%s', $variable->toQuery()); + } + + public function testVariableOnlyAcceptsString(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + variable([]); + } + + public function testLiteralReturnsLiteralClass(): void + { + $literal = literal(); + + $this->assertSame(Literal::class, $literal); + } + + public function testLiteralReturnsLiteral(): void + { + $literal = literal('foobar'); + + $this->assertInstanceOf(String_::class, $literal); + $this->assertSame("'foobar'", $literal->toQuery()); + + $literal = literal(true); + + $this->assertInstanceOf(Boolean::class, $literal); + $this->assertSame("true", $literal->toQuery()); + } + + public function testBooleanReturnsBoolean(): void + { + $boolean = \WikibaseSolutions\CypherDSL\boolean(true); + + $this->assertInstanceOf(Boolean::class, $boolean); + $this->assertSame('true', $boolean->toQuery()); + } + + public function testBooleanOnlyAcceptsBoolean(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + \WikibaseSolutions\CypherDSL\boolean([]); + } + + public function testStringReturnsString(): void + { + $string = string('hello world'); + + $this->assertInstanceOf(String_::class, $string); + $this->assertSame("'hello world'", $string->toQuery()); + } + + public function testStringOnlyAcceptsString(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + string([]); + } + + public function testIntegerReturnsInteger(): void + { + $integer = integer(1); + + $this->assertInstanceOf(Integer::class, $integer); + $this->assertSame("1", $integer->toQuery()); + } + + public function testIntegerOnlyAcceptsInteger(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + integer([]); + } + + public function testFloatReturnsFloat(): void + { + $float = float(1.1); + + $this->assertInstanceOf(Float_::class, $float); + $this->assertSame("1.1", $float->toQuery()); + } + + public function testFloatOnlyAcceptsFloat(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + float([]); + } + + public function testListReturnsList(): void + { + $list = list_(['a', 'b', 'c']); + + $this->assertInstanceOf(List_::class, $list); + $this->assertSame("['a', 'b', 'c']", $list->toQuery()); + } + + public function testListOnlyAcceptsArray(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + list_(1); + } + + public function testMapReturnsMap(): void + { + $map = map(['a' => 'b', 'c' => 'd']); + + $this->assertInstanceOf(Map::class, $map); + $this->assertSame("{a: 'b', c: 'd'}", $map->toQuery()); + } + + public function testMapOnlyAcceptsArray(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + map(1); + } + + public function testFunctionReturnsFuncClass(): void + { + $function = function_(); + + $this->assertSame(Procedure::class, $function); + } + + public function testRawReturnsRawExpression(): void + { + $raw = raw('(unimplemented feature)'); + + $this->assertInstanceOf(RawExpression::class, $raw); + $this->assertSame('(unimplemented feature)', $raw->toQuery()); + } + + public function testRawOnlyAcceptsString(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + raw([]); + } +} diff --git a/tests/unit/Patterns/NodeTest.php b/tests/unit/Patterns/NodeTest.php new file mode 100644 index 00000000..85b71d72 --- /dev/null +++ b/tests/unit/Patterns/NodeTest.php @@ -0,0 +1,326 @@ +assertSame("()", $node->toQuery()); + } + + public function testBacktickIsEscaped(): void + { + $node = new Node(); + + $node->withVariable('abcdr`eer'); + $this->assertEquals('(`abcdr``eer`)', $node->toQuery()); + } + + /** + * @dataProvider provideOnlyLabelData + */ + public function testOnlyLabel(string $expected, string ...$label): void + { + $node = new Node(); + $node->addLabel(...$label); + + $this->assertSame($expected, $node->toQuery()); + } + + /** + * @dataProvider provideOnlyNameData + */ + public function testOnlyName(string $name, string $expected): void + { + $node = new Node(); + $node->withVariable($name); + + $this->assertSame($expected, $node->toQuery()); + } + + /** + * @dataProvider provideOnlyPropertiesData + */ + public function testOnlyProperties(array $properties, string $expected): void + { + $node = new Node(); + $node->withProperties($properties); + + $this->assertSame($expected, $node->toQuery()); + } + + /** + * @dataProvider provideWithNameAndLabelData + */ + public function testWithNameAndLabel(string $name, string $label, string $expected): void + { + $node = new Node(); + $node->addLabel($label)->withVariable($name); + + $this->assertSame($expected, $node->toQuery()); + } + + /** + * @dataProvider provideWithNameAndPropertiesData + */ + public function testWithNameAndProperties(string $name, array $properties, string $expected): void + { + $node = new Node(); + $node->withVariable($name)->withProperties($properties); + + $this->assertSame($expected, $node->toQuery()); + } + + /** + * @dataProvider provideWithLabelAndPropertiesData + */ + public function testWithLabelAndProperties(string $label, array $properties, string $expected): void + { + $node = new Node(); + $node->addLabel($label)->withProperties($properties); + + $this->assertSame($expected, $node->toQuery()); + } + + /** + * @dataProvider provideWithNameAndLabelAndPropertiesData + */ + public function testWithNameAndLabelAndProperties(string $name, string $label, array $properties, string $expected): void + { + $node = new Node(); + $node->withVariable($name)->addLabel($label)->withProperties($properties); + + $this->assertSame($expected, $node->toQuery()); + } + + /** + * @dataProvider provideMultipleLabelsData + */ + public function testMultipleLabels(array $labels, string $expected): void + { + $node = new Node(); + $node->withLabels($labels); + + $this->assertSame($expected, $node->toQuery()); + } + + public function testSetterSameAsConstructor(): void + { + $label = "__test__"; + $viaConstructor = new Node($label); + $viaSetter = (new Node())->addLabel($label); + + $this->assertSame($viaConstructor->toQuery(), $viaSetter->toQuery(), "Setting label via setter has different effect than using constructor"); + } + + public function testAddingProperties(): void + { + $node = new Node(); + + $node->addProperty("foo", new String_("bar")); + $this->assertSame("({foo: 'bar'})", $node->toQuery()); + + $node->addProperty("foo", new String_("bar")); + $this->assertSame("({foo: 'bar'})", $node->toQuery()); + + $node->addProperty("baz", new String_("bar")); + $this->assertSame("({foo: 'bar', baz: 'bar'})", $node->toQuery()); + + $node->addProperty("baz", "test"); + $this->assertSame("({foo: 'bar', baz: 'test'})", $node->toQuery()); + + $node->addProperties(["foo" => new String_("baz"), "qux" => new String_("baz")]); + $this->assertSame("({foo: 'baz', baz: 'test', qux: 'baz'})", $node->toQuery()); + + $node->addProperties(new Map(["foo" => new String_("test")])); + $this->assertSame("({foo: 'test', baz: 'test', qux: 'baz'})", $node->toQuery()); + } + + public function testAddingPropertiesToVariableFails(): void + { + $node = new Node(); + + $node->withProperties(new Variable()); + $this->expectException(TypeError::class); + $node->addProperty('foo', 'bar'); + } + + public function testGetLabels(): void + { + $labels = ["hello", "world"]; + + $node = new Node(); + $node->withLabels($labels); + + $this->assertSame($labels, $node->getLabels()); + } + + public function testGetProperties(): void + { + $properties = new Map(['foo' => new String_('bar')]); + + $node = new Node(); + $node->withProperties($properties); + + $this->assertSame($properties, $node->getProperties()); + } + + public function testRelationship(): void + { + $node = new Node("City"); + $relationship = new Relationship(Relationship::DIR_RIGHT); + $relationship->addType("LIVES_IN"); + + $amsterdam = new Node("City"); + $amsterdam->withProperties(["city" => "Amsterdam"]); + + $this->assertSame("(:City)-[:LIVES_IN]->(:City {city: 'Amsterdam'})", $node->relationship($relationship, $amsterdam)->toQuery()); + } + + public function testRelationshipTo(): void + { + $node = new Node("City"); + $amsterdam = new Node("City"); + $amsterdam->withProperties(["city" => "Amsterdam"]); + + $relationship = $node->relationshipTo($amsterdam, "LIVES_IN"); + + $this->assertSame("(:City)-[:LIVES_IN]->(:City {city: 'Amsterdam'})", $relationship->toQuery()); + } + + public function testRelationshipFrom(): void + { + $node = new Node("City"); + $amsterdam = new Node("City"); + $amsterdam->withProperties(["city" => "Amsterdam"]); + + $relationship = $node->relationshipFrom($amsterdam, "LIVES_IN"); + + $this->assertSame("(:City)<-[:LIVES_IN]-(:City {city: 'Amsterdam'})", $relationship->toQuery()); + } + + public function testRelationshipUni(): void + { + $node = new Node("City"); + $amsterdam = new Node("City"); + $amsterdam->withProperties(["city" => "Amsterdam"]); + + $relationship = $node->relationshipUni($amsterdam, "LIVES_IN"); + + $this->assertSame("(:City)-[:LIVES_IN]-(:City {city: 'Amsterdam'})", $relationship->toQuery()); + } + + public function testLabeledSingleLabel(): void + { + $node = new Node(); + $labeled = $node->labeled('German'); + + $this->assertEquals(new Label($node->getVariable(), 'German'), $labeled); + } + + public function testLabeledMultipleLabels(): void + { + $node = new Node(); + $labeled = $node->labeled('German', 'Swedish'); + + $this->assertEquals(new Label($node->getVariable(), 'German', 'Swedish'), $labeled); + } + + public function provideOnlyLabelData(): array + { + return [ + ['(:a)', 'a'], + ['(:Foo:Bar)', 'Foo', 'Bar'], + ['(:`:`)', ':'], + ]; + } + + public function provideOnlyNameData(): array + { + return [ + ['a', '(a)'], + ['A', '(A)'], + ]; + } + + public function provideWithNameAndLabelData(): array + { + return [ + ['a', 'a', '(a:a)'], + ['A', ':', '(A:`:`)'], + ]; + } + + public function provideWithNameAndPropertiesData(): array + { + return [ + ['a', ['a' => new String_('b'), 'b' => new String_('c')], "(a {a: 'b', b: 'c'})"], + ['b', ['a' => new Integer(0), 'b' => new Float_(1)], "(b {a: 0, b: 1.0})"], + ['c', [':' => new List_([new Integer(1), new String_('a')])], "(c {`:`: [1, 'a']})"], + ]; + } + + public function provideWithLabelAndPropertiesData(): array + { + return [ + ['a', ['a' => new String_('b'), 'b' => new String_('c')], "(:a {a: 'b', b: 'c'})"], + ['b', ['a' => new Integer(0), 'b' => new Float_(1)], "(:b {a: 0, b: 1.0})"], + ['c', [':' => new List_([new Integer(1), new String_('a')])], "(:c {`:`: [1, 'a']})"], + ]; + } + + public function provideOnlyPropertiesData(): array + { + return [ + [['a' => new String_('b'), 'b' => new String_('c')], "({a: 'b', b: 'c'})"], + [['a' => new Integer(0), 'b' => new Float_(-1.0)], "({a: 0, b: -1.0})"], + [[':' => new List_([new Integer(1), new String_('a')])], "({`:`: [1, 'a']})"], + ]; + } + + public function provideWithNameAndLabelAndPropertiesData(): array + { + return [ + ['a', 'd', ['a' => new String_('b'), 'b' => new String_('c')], "(a:d {a: 'b', b: 'c'})"], + ['b', 'e', ['a' => new Integer(0), 'b' => new Float_(1)], "(b:e {a: 0, b: 1.0})"], + ['c', 'f', [':' => new List_([new Integer(1), new String_('a')])], "(c:f {`:`: [1, 'a']})"], + ]; + } + + public function provideMultipleLabelsData(): array + { + return [ + [['a'], '(:a)'], + [['A'], '(:A)'], + [[':'], '(:`:`)'], + [['a', 'b'], '(:a:b)'], + [['A', 'B'], '(:A:B)'], + [[':', 'a'], '(:`:`:a)'], + ]; + } +} diff --git a/tests/unit/Patterns/PathTest.php b/tests/unit/Patterns/PathTest.php new file mode 100644 index 00000000..63989de2 --- /dev/null +++ b/tests/unit/Patterns/PathTest.php @@ -0,0 +1,113 @@ +assertEquals('', (new Path())->toQuery()); + $this->assertEquals('', (new Path([], []))->toQuery()); + $this->assertEquals('', (new Path([], []))->toQuery()); + $this->assertEquals('', (new Path([], [new Relationship(Relationship::DIR_UNI)]))->toQuery()); + } + + public function testSingleNode(): void + { + $path = new Path(new Node()); + + $this->assertEquals('()', $path->toQuery()); + } + + public function testSingleNodeNamed(): void + { + $path = new Path(new Node()); + $path->withVariable('a'); + + $this->assertEquals('a = ()', $path->toQuery()); + } + + public function testSingle(): void + { + $path = new Path(new Node(), new Relationship(Relationship::DIR_UNI)); + + $this->assertEquals('()', $path->toQuery()); + } + + public function testSingleNoRel(): void + { + $path = new Path([new Node(), new Node()]); + + $this->assertEquals('()', $path->toQuery()); + } + + public function testPathMerge(): void + { + $pathX = new Path([new Node(), new Node()], [new Relationship(Relationship::DIR_UNI)]); + $pathY = new Path([new Node(), new Node()], [new Relationship(Relationship::DIR_UNI)]); + + $pathX->withVariable('x')->relationshipTo($pathY->withVariable('y')); + + $this->assertEquals('x = ()--()-->()--()', $pathX->toQuery()); + } + + public function testPathFewerNodes(): void + { + $path = new Path( + new Node, + [new Relationship(Relationship::DIR_UNI), new Relationship(Relationship::DIR_UNI)] + ); + $this->assertSame('()', $path->toQuery()); + } + + public function testRelationshipLong(): void + { + $path = new Path(new Node()); + $path->relationshipUni(new Node('Label')) + ->relationshipTo((new Node())->withVariable('b')) + ->relationshipFrom(new Node(), 'TYPE', ['x' => Query::literal('y')], 'c') + ->relationship(new Relationship(Relationship::DIR_UNI), (new Node())->withVariable('d')) + ->withVariable('a'); + + $this->assertEquals('a = ()--(:Label)-->(b)<-[c:TYPE {x: \'y\'}]-()--(d)', $path->toQuery()); + + $this->assertEquals([ + new Node(), + new Node('Label'), + (new Node())->withVariable('b'), + new Node(), + (new Node())->withVariable('d'), + ], $path->getNodes()); + + $this->assertEquals([ + new Relationship(Relationship::DIR_UNI), + new Relationship(Relationship::DIR_RIGHT), + (new Relationship(Relationship::DIR_LEFT)) + ->addType('TYPE') + ->addProperties(['x' => Query::literal('y')]) + ->withVariable('c'), + new Relationship(Relationship::DIR_UNI), + ], $path->getRelationships()); + } + + public function testPathCanBeTreatedAsBoolean(): void + { + $pathA = new Path([new Node(), new Node()], [new Relationship(Relationship::DIR_UNI)]); + $pathB = new Path([new Node(), new Node()], [new Relationship(Relationship::DIR_RIGHT)]); + + $this->assertSame("()--() AND ()-->()", $pathA->and($pathB, false)->toQuery()); + } +} diff --git a/tests/unit/Patterns/RelationshipTest.php b/tests/unit/Patterns/RelationshipTest.php new file mode 100644 index 00000000..ed80db63 --- /dev/null +++ b/tests/unit/Patterns/RelationshipTest.php @@ -0,0 +1,618 @@ +expectException(InvalidArgumentException::class); + $r = new Relationship(['--', '--']); + } + + public function testDirRight(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + + $this->assertSame("-->", $r->toQuery()); + } + + public function testDirLeft(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + + $this->assertSame("<--", $r->toQuery()); + } + + public function testDirUni(): void + { + $r = new Relationship(Relationship::DIR_UNI); + + $this->assertSame("--", $r->toQuery()); + } + + /** + * @dataProvider provideWithNameData + */ + public function testWithName(string $name, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withVariable($name); + + $this->assertSame($expected, $r->toQuery()); + } + + /** + * @dataProvider provideAddTypeData + */ + public function testAddType(string $type, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->addType($type); + + $this->assertSame($expected, $r->toQuery()); + } + + public function testAddTypeMultiple(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + $r->addType("a"); + $r->addType("b"); + $r->addType(":"); + + $this->assertSame("<-[:a|b|`:`]-", $r->toQuery()); + } + + /** + * @dataProvider provideWithTypesData + */ + public function testWithTypes(array $types, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withTypes($types); + + $this->assertSame($expected, $r->toQuery()); + } + + /** + * @dataProvider provideWithPropertiesData + */ + public function testWithProperties(array $properties, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withProperties($properties); + + $this->assertSame($expected, $r->toQuery()); + } + + /** + * @dataProvider provideWithNameAndTypeData + */ + public function testWithNameAndType(string $name, string $type, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withVariable($name)->addType($type); + + $this->assertSame($expected, $r->toQuery()); + } + + public function testWithNameAndMultipleTypes(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + $r->withVariable('a')->addType('a')->addType('b'); + + $this->assertSame('<-[a:a|b]-', $r->toQuery()); + } + + public function testWithVariableActualVariable(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + $r->withVariable(new Variable('a')); + + $this->assertSame('<-[a]-', $r->toQuery()); + } + + /** + * @dataProvider provideWithNameAndPropertiesData + */ + public function testWithNameAndProperties(string $name, array $properties, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withVariable($name)->withProperties($properties); + + $this->assertSame($expected, $r->toQuery()); + } + + /** + * @dataProvider provideWithNameAndTypeAndPropertiesData + */ + public function testWithNameAndTypeAndProperties(string $name, string $type, array $properties, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withVariable($name)->addType($type)->withProperties($properties); + + $this->assertSame($expected, $r->toQuery()); + } + + /** + * @dataProvider provideWithMultipleTypesData + */ + public function testWithMultipleTypes(string $name, array $types, array $properties, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withVariable($name)->withProperties($properties); + + foreach ($types as $type) { + $r->addType($type); + } + + $this->assertSame($expected, $r->toQuery()); + } + + /** + * @dataProvider provideVariableLengthRelationshipsWithNameData + */ + public function testVariableLengthRelationshipsWithName(string $name, ?int $minHops, ?int $maxHops, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withVariable($name); + + if (isset($minHops)) { + $r->withMinHops($minHops); + } + + if (isset($maxHops)) { + $r->withMaxHops($maxHops); + } + + $this->assertSame($expected, $r->toQuery()); + } + + /** + * @dataProvider provideVariableLengthRelationshipsWithTypeData + */ + public function testVariableLengthRelationshipsWithType(string $type, ?int $minHops, ?int $maxHops, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->addType($type); + + if (isset($minHops)) { + $r->withMinHops($minHops); + } + + if (isset($maxHops)) { + $r->withMaxHops($maxHops); + } + + $this->assertSame($expected, $r->toQuery()); + } + + /** + * @dataProvider provideVariableLengthRelationshipsWithPropertiesData + */ + public function testVariableLengthRelationshipsWithProperties(array $properties, ?int $minHops, ?int $maxHops, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withProperties($properties); + + if (isset($minHops)) { + $r->withMinHops($minHops); + } + + if (isset($maxHops)) { + $r->withMaxHops($maxHops); + } + + $this->assertSame($expected, $r->toQuery()); + } + + /** + * @dataProvider provideVariableLengthRelationshipsWithNameAndTypeAndPropertiesData + */ + public function testVariableLengthRelationshipsWithNameAndTypeAndProperties(string $name, string $type, array $properties, ?int $minHops, ?int $maxHops, array $direction, string $expected): void + { + $r = new Relationship($direction); + $r->withVariable($name)->addType($type)->withProperties($properties); + + if (isset($minHops)) { + $r->withMinHops($minHops); + } + + if (isset($maxHops)) { + $r->withMaxHops($maxHops); + } + + $this->assertSame($expected, $r->toQuery()); + } + + public function testArbitraryHops(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + $r->withVariable('hello')->addType('world')->addType('testing')->withProperties(['is' => 'a virtue']); + $r->withArbitraryHops(); + + $this->assertSame('<-[hello:world|testing* {is: \'a virtue\'}]-', $r->toQuery()); + } + + public function testExactLengthRelationships(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withVariable("tom") + ->addType("Person") + ->withProperties(['name' => 'Tom Hanks']); + + $r->withExactHops(10); + + $this->assertSame("-[tom:Person*10 {name: 'Tom Hanks'}]->", $r->toQuery()); + } + + public function testMinAndExactHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withMinHops(1); + + $this->expectException(LogicException::class); + + $r->withExactHops(1); + } + + public function testMaxAndExactHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withMaxHops(1); + + $this->expectException(LogicException::class); + + $r->withExactHops(1); + } + + public function testMinMaxAndExactHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withMinHops(1); + $r->withMaxHops(1); + + $this->expectException(LogicException::class); + + $r->withExactHops(1); + } + + public function testExactAndMinHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withExactHops(1); + + $this->expectException(LogicException::class); + + $r->withMinHops(1); + } + + public function testExactAndMaxHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withExactHops(1); + + $this->expectException(LogicException::class); + + $r->withMaxHops(1); + } + + public function testMaxHopsLessThanMinHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withMinHops(100); + + $this->expectException(DomainException::class); + + $r->withMaxHops(1); + } + + public function testMinHopsGreaterThanMaxHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withMaxHops(1); + + $this->expectException(DomainException::class); + + $r->withMinHops(100); + } + + public function testMinHopsLessThanZero(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + + $this->expectException(DomainException::class); + + $r->withMinHops(-1); + } + + public function testMaxHopsLessThanOne(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + + $this->expectException(DomainException::class); + + $r->withMaxHops(0); + } + + public function testMaxHopsLessThanZero(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + + $this->expectException(DomainException::class); + + $r->withMaxHops(-1); + } + + public function testExactHopsLessThanOne(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + + $this->expectException(DomainException::class); + + $r->withExactHops(0); + } + + public function testExactHopsLessThanZero(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + + $this->expectException(DomainException::class); + + $r->withExactHops(-1); + } + + public function testExactHopsWithArbitraryHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withArbitraryHops(); + + $this->expectException(LogicException::class); + + $r->withExactHops(5); + } + + public function testMinHopsWithArbitraryHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withArbitraryHops(); + + $this->expectException(LogicException::class); + + $r->withMinHops(5); + } + + public function testMaxHopsWithArbitraryHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withArbitraryHops(); + + $this->expectException(LogicException::class); + + $r->withMaxHops(5); + } + + public function testArbitraryHopsWithExactHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withExactHops(5); + + $this->expectException(LogicException::class); + + $r->withArbitraryHops(); + } + + public function testArbitraryHopsWithMinHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withMinHops(5); + + $this->expectException(LogicException::class); + + $r->withArbitraryHops(); + } + + public function testArbitraryHopsWithMaxHops(): void + { + $r = new Relationship(Relationship::DIR_RIGHT); + $r->withMaxHops(5); + + $this->expectException(LogicException::class); + + $r->withArbitraryHops(); + } + + public function testGetDirection(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + $this->assertSame(Relationship::DIR_LEFT, $r->getDirection()); + } + + public function testGetProperties(): void + { + $properties = Query::map(['foo' => 'bar']); + $r = new Relationship(Relationship::DIR_LEFT); + $this->assertNull($r->getProperties()); + + $r->withProperties($properties); + + $this->assertSame($properties, $r->getProperties()); + } + + public function testGetExactHops(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + $this->assertNull($r->getExactHops()); + + $r->withExactHops(12); + + $this->assertSame(12, $r->getExactHops()); + } + + public function testGetMaxHops(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + $this->assertNull($r->getMaxHops()); + + $r->withMaxHops(12); + + $this->assertSame(12, $r->getMaxHops()); + } + + public function testGetMinHops(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + $this->assertNull($r->getMinHops()); + + $r->withMinHops(12); + + $this->assertSame(12, $r->getMinHops()); + } + + public function testGetTypes(): void + { + $r = new Relationship(Relationship::DIR_LEFT); + $this->assertEmpty($r->getTypes()); + + $r->withTypes(['a', 'b']); + + $this->assertSame(['a', 'b'], $r->getTypes()); + } + + public function provideVariableLengthRelationshipsWithNameData(): array + { + return [ + ['b', 0, 100, Relationship::DIR_UNI, '-[b*0..100]-'], + ['b', 5, 5, Relationship::DIR_RIGHT, '-[b*5..5]->'], + ['a', 10, null, Relationship::DIR_UNI, '-[a*10..]-'], + ['a', null, 10, Relationship::DIR_LEFT, '<-[a*..10]-'], + ]; + } + + public function provideVariableLengthRelationshipsWithTypeData(): array + { + return [ + ['', 1, 100, Relationship::DIR_LEFT, '<-[*1..100]-'], + ['a', 10, null, Relationship::DIR_LEFT, '<-[:a*10..]-'], + [':', null, 10, Relationship::DIR_LEFT, '<-[:`:`*..10]-'], + ]; + } + + public function provideVariableLengthRelationshipsWithPropertiesData(): array + { + return [ + [[], 10, 100, Relationship::DIR_LEFT, "<-[*10..100]-"], + [[new String_('a')], 10, null, Relationship::DIR_LEFT, "<-[*10.. {`0`: 'a'}]-"], + [['a' => new String_('b')], null, 10, Relationship::DIR_LEFT, "<-[*..10 {a: 'b'}]-"], + ]; + } + + public function provideVariableLengthRelationshipsWithNameAndTypeAndPropertiesData(): array + { + return [ + ['a', 'a', [], 10, 100, Relationship::DIR_LEFT, "<-[a:a*10..100]-"], + ['b', 'a', [new String_('a')], null, 10, Relationship::DIR_LEFT, "<-[b:a*..10 {`0`: 'a'}]-"], + ['a', 'b', [new String_('a')], 10, 100, Relationship::DIR_LEFT, "<-[a:b*10..100 {`0`: 'a'}]-"], + ['a', '', ['a' => new String_('b')], null, 10, Relationship::DIR_LEFT, "<-[a*..10 {a: 'b'}]-"], + ['a', ':', ['a' => new String_('b'), new String_('c')], 10, null, Relationship::DIR_LEFT, "<-[a:`:`*10.. {a: 'b', `0`: 'c'}]-"], + ]; + } + + public function provideWithNameData(): array + { + return [ + ['a', Relationship::DIR_UNI, '-[a]-'], + ['a', Relationship::DIR_LEFT, '<-[a]-'], + ['a', Relationship::DIR_RIGHT, '-[a]->'], + ]; + } + + public function provideAddTypeData(): array + { + return [ + ['', Relationship::DIR_LEFT, '<--'], + ['a', Relationship::DIR_LEFT, '<-[:a]-'], + [':', Relationship::DIR_LEFT, '<-[:`:`]-'], + ]; + } + + public function provideWithTypesData(): array + { + return [ + [['', 'a'], Relationship::DIR_LEFT, '<-[:a]-'], + [['a', 'b'], Relationship::DIR_LEFT, '<-[:a|b]-'], + [['', 'a', 'b'], Relationship::DIR_LEFT, '<-[:a|b]-'], + [['a', '', 'b'], Relationship::DIR_LEFT, '<-[:a|b]-'], + [['a', 'b', ':'], Relationship::DIR_LEFT, '<-[:a|b|`:`]-'], + ]; + } + + public function provideWithPropertiesData(): array + { + return [ + [[], Relationship::DIR_LEFT, "<--"], + [[new String_('a')], Relationship::DIR_LEFT, "<-[{`0`: 'a'}]-"], + [['a' => new String_('b')], Relationship::DIR_LEFT, "<-[{a: 'b'}]-"], + [['a' => new String_('b'), new String_('c')], Relationship::DIR_LEFT, "<-[{a: 'b', `0`: 'c'}]-"], + [[':' => new Integer(12)], Relationship::DIR_LEFT, "<-[{`:`: 12}]-"], + [['a' => 'b', 'c' => 12, 'd' => 12.38], Relationship::DIR_LEFT, "<-[{a: 'b', c: 12, d: 12.38}]-"], + ]; + } + + public function provideWithNameAndTypeData(): array + { + return [ + ['a', '', Relationship::DIR_LEFT, '<-[a]-'], + ['a', 'b', Relationship::DIR_LEFT, '<-[a:b]-'], + ]; + } + + public function provideWithNameAndPropertiesData(): array + { + return [ + ['a', [], Relationship::DIR_LEFT, "<-[a]-"], + ['b', [new String_('a')], Relationship::DIR_LEFT, "<-[b {`0`: 'a'}]-"], + ]; + } + + public function provideWithNameAndTypeAndPropertiesData(): array + { + return [ + ['a', 'a', [], Relationship::DIR_LEFT, "<-[a:a]-"], + ['b', 'a', [new String_('a')], Relationship::DIR_LEFT, "<-[b:a {`0`: 'a'}]-"], + ['a', 'b', [new String_('a')], Relationship::DIR_LEFT, "<-[a:b {`0`: 'a'}]-"], + ['a', '', ['a' => new String_('b')], Relationship::DIR_LEFT, "<-[a {a: 'b'}]-"], + ['a', ':', ['a' => new String_('b'), new String_('c')], Relationship::DIR_LEFT, "<-[a:`:` {a: 'b', `0`: 'c'}]-"], + ]; + } + + public function provideWithMultipleTypesData(): array + { + return [ + ['a', [], [], Relationship::DIR_LEFT, "<-[a]-"], + ['b', ['a'], [new String_('a')], Relationship::DIR_LEFT, "<-[b:a {`0`: 'a'}]-"], + ['a', ['a', 'b', 'c'], [new String_('a')], Relationship::DIR_LEFT, "<-[a:a|b|c {`0`: 'a'}]-"], + ['a', ['a', 'b'], [], Relationship::DIR_LEFT, "<-[a:a|b]-"], + ]; + } +} diff --git a/tests/unit/QueryCallProcedureTest.php b/tests/unit/QueryCallProcedureTest.php new file mode 100644 index 00000000..df8c2f9d --- /dev/null +++ b/tests/unit/QueryCallProcedureTest.php @@ -0,0 +1,75 @@ +callProcedure($procedure); + + $this->assertSame("CALL localtime()", $statement->toQuery()); + } + + public function testProcedureWithVariableYield(): void + { + $procedure = Procedure::localtime(); + + $statement = Query::new()->callProcedure($procedure, Query::variable('a')); + + $this->assertSame("CALL localtime() YIELD a", $statement->toQuery()); + } + + public function testProcedureWithStringYield(): void + { + $procedure = Procedure::localtime(); + + $statement = Query::new()->callProcedure($procedure, 'a'); + + $this->assertSame("CALL localtime() YIELD a", $statement->toQuery()); + } + + public function testProcedureWithMultipleYields(): void + { + $procedure = Procedure::localtime(); + + $statement = Query::new()->callProcedure($procedure, ['a', Query::variable('b')]); + + $this->assertSame("CALL localtime() YIELD a, b", $statement->toQuery()); + } + + public function testCallProcedureString(): void + { + $statement = Query::new()->callProcedure('apoc.json'); + + $this->assertSame("CALL `apoc.json`()", $statement->toQuery()); + } + + public function testReturnsSameInstance(): void + { + $procedure = Procedure::localtime(); + + $expected = Query::new(); + $actual = $expected->callProcedure($procedure); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryCallTest.php b/tests/unit/QueryCallTest.php new file mode 100644 index 00000000..c73f8bab --- /dev/null +++ b/tests/unit/QueryCallTest.php @@ -0,0 +1,132 @@ +call(static function (Query $query): void + { + $query->match(Query::node('x')); + }); + + $this->assertSame('CALL { MATCH (:x) }', $query->toQuery()); + } + + public function testWithCallableOnlyAcceptsQuery(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->call(static function (int $query): void + { + }); + } + + public function testWithQuery(): void + { + $query = Query::new()->call(Query::new()->match(Query::node())); + + $this->assertSame('CALL { MATCH () }', $query->toQuery()); + } + + public function testWithEmptyQuery(): void + { + $query = Query::new()->call(Query::new()); + + $this->assertSame('', $query->toQuery()); + } + + public function testWithEmptyQueryAndVariables(): void + { + $query = Query::new()->call(Query::new(), ['a', 'b']); + + $this->assertSame('', $query->toQuery()); + } + + public function testWithVariables(): void + { + $query = Query::new()->call(static function (Query $query): void + { + $query->match(Query::node('x')); + }, Query::variable('x')); + + $this->assertSame('CALL { WITH x MATCH (:x) }', $query->toQuery()); + + $query = Query::new()->call(static function (Query $query): void + { + $query->match(Query::node('x')); + }, [Query::variable('x')]); + + $this->assertSame('CALL { WITH x MATCH (:x) }', $query->toQuery()); + + $query = Query::new()->call(static function (Query $query): void + { + $query->match(Query::node('x')); + }, [Query::variable('x'), Query::variable('y')]); + + $this->assertSame('CALL { WITH x, y MATCH (:x) }', $query->toQuery()); + + $query = Query::new()->call(static function (Query $query): void + { + $query->match(Query::node('x')); + }, 'x'); + + $this->assertSame('CALL { WITH x MATCH (:x) }', $query->toQuery()); + + $query = Query::new()->call(static function (Query $query): void + { + $query->match(Query::node('x')); + }, ['x', 'y']); + + $this->assertSame('CALL { WITH x, y MATCH (:x) }', $query->toQuery()); + + $query = Query::new()->call(static function (Query $query): void + { + $query->match(Query::node('x')); + }, Query::node()); + + $this->assertStringMatchesFormat('CALL { WITH var%s MATCH (:x) }', $query->toQuery()); + } + + public function testDoesNotAcceptAnyTypeAsSubquery(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->call('something bad'); + } + + public function testDoesNotAcceptAnyTypeAsVariables(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->call(Query::new(), 500); + } + + public function testReturnsSameInstance(): void + { + $expected = Query::new(); + $actual = $expected->call(Query::new()); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryCreateTest.php b/tests/unit/QueryCreateTest.php new file mode 100644 index 00000000..c46ecb1c --- /dev/null +++ b/tests/unit/QueryCreateTest.php @@ -0,0 +1,52 @@ +withVariable('hello'); + + $query = Query::new()->create($pattern); + + $this->assertSame('CREATE (hello)', $query->toQuery()); + } + + public function testCreateMultiplePatterns(): void + { + $hello = Query::node()->withVariable('hello'); + $world = Query::node()->withVariable('world'); + + $query = Query::new()->create([$hello, $world]); + + $this->assertSame('CREATE (hello), (world)', $query->toQuery()); + } + + public function testDoesNotAcceptRelationship(): void + { + $rel = Query::relationship(Relationship::DIR_RIGHT); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->create($rel); + } +} diff --git a/tests/unit/QueryDeleteTest.php b/tests/unit/QueryDeleteTest.php new file mode 100644 index 00000000..003f067a --- /dev/null +++ b/tests/unit/QueryDeleteTest.php @@ -0,0 +1,86 @@ +withVariable('tom')->getVariable(); + + $query = Query::new()->delete($tom); + + $this->assertSame('DELETE tom', $query->toQuery()); + } + + public function testMultipleStructuralType(): void + { + $tom = Query::node()->withVariable('tom')->getVariable(); + $jerry = Query::node()->withVariable('jerry')->getVariable(); + + $query = Query::new()->delete([$tom, $jerry]); + + $this->assertSame('DELETE tom, jerry', $query->toQuery()); + } + + public function testSinglePattern(): void + { + $tom = Query::node()->withVariable('tom'); + + $query = Query::new()->delete($tom); + + $this->assertSame('DELETE tom', $query->toQuery()); + } + + public function testMultiplePatterns(): void + { + $tom = Query::node()->withVariable('tom'); + $jerry = Query::node()->withVariable('jerry'); + + $query = Query::new()->delete([$tom, $jerry]); + + $this->assertSame('DELETE tom, jerry', $query->toQuery()); + } + + public function testPatternWithoutVariable(): void + { + $pattern = Query::node(); + + $query = Query::new()->delete($pattern); + + $this->assertStringMatchesFormat('DELETE var%s', $query->toQuery()); + } + + public function testDetachDelete(): void + { + $tom = Query::node()->withVariable('tom')->getVariable(); + + $query = Query::new()->delete($tom, true); + + $this->assertSame('DETACH DELETE tom', $query->toQuery()); + } + + public function testDetachDeleteDeprecated(): void + { + $tom = Query::node()->withVariable('tom')->getVariable(); + + $query = Query::new()->detachDelete($tom); + + $this->assertSame('DETACH DELETE tom', $query->toQuery()); + } +} diff --git a/tests/unit/QueryLimitTest.php b/tests/unit/QueryLimitTest.php new file mode 100644 index 00000000..78865dd7 --- /dev/null +++ b/tests/unit/QueryLimitTest.php @@ -0,0 +1,63 @@ +limit($expression)->build(); + + $this->assertSame("LIMIT 12", $statement); + } + + public function testWithExpression(): void + { + $expression = Query::integer(12)->plus(Query::integer(16)); + + $statement = (new Query())->limit($expression)->build(); + + $this->assertSame("LIMIT (12 + 16)", $statement); + } + + public function testDoesNotAcceptAnyType(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + (new Query())->limit("10")->build(); + } + + public function testWithPHPInteger(): void + { + $statement = (new Query())->limit(12)->build(); + + $this->assertSame("LIMIT 12", $statement); + } + + public function testReturnsSameInstance(): void + { + $expected = Query::new(); + $actual = $expected->limit(420); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryMatchTest.php b/tests/unit/QueryMatchTest.php new file mode 100644 index 00000000..14ba6a15 --- /dev/null +++ b/tests/unit/QueryMatchTest.php @@ -0,0 +1,75 @@ +withVariable('m'); + + $statement = Query::new()->match($m)->build(); + + $this->assertSame("MATCH (m:Movie)", $statement); + + $statement = Query::new()->match([$m, $m])->build(); + + $this->assertSame("MATCH (m:Movie), (m:Movie)", $statement); + } + + public function testDoesNotAcceptRelationship(): void + { + $r = Query::relationship(Relationship::DIR_LEFT); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->match($r); + } + + public function testDoesNotAcceptRelationshipWithNode(): void + { + $r = Query::relationship(Relationship::DIR_LEFT); + $m = Query::node(); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->match([$m, $r]); + } + + public function testDoesNotAcceptTypeOtherThanMatchablePattern(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->match(false); + } + + public function testReturnsSameInstance(): void + { + $m = Query::node(); + + $expected = Query::new(); + $actual = $expected->match($m); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryMergeTest.php b/tests/unit/QueryMergeTest.php new file mode 100644 index 00000000..3da7eaed --- /dev/null +++ b/tests/unit/QueryMergeTest.php @@ -0,0 +1,72 @@ +withVariable('a')->relationshipTo(Query::node()->withVariable('b')); + + $statement = Query::new()->merge($pattern); + + $this->assertSame("MERGE (a)-->(b)", $statement->toQuery()); + } + + public function testMergePatternWithOnCreate(): void + { + $pattern = Query::node()->withVariable('a')->relationshipTo(Query::node()->withVariable('b')); + $onCreate = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $statement = Query::new()->merge($pattern, $onCreate); + + $this->assertSame("MERGE (a)-->(b) ON CREATE SET a.a = 'b'", $statement->toQuery()); + } + + public function testMergePatternWithOnMatch(): void + { + $pattern = Query::node()->withVariable('a')->relationshipTo(Query::node()->withVariable('b')); + $onMatch = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $statement = Query::new()->merge($pattern, null, $onMatch); + + $this->assertSame("MERGE (a)-->(b) ON MATCH SET a.a = 'b'", $statement->toQuery()); + } + + public function testMergePatternWithBoth(): void + { + $pattern = Query::node()->withVariable('a')->relationshipTo(Query::node()->withVariable('b')); + $onCreate = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + $onMatch = (new SetClause())->add(Query::variable('a')->property('a')->replaceWith('b')); + + $statement = Query::new()->merge($pattern, $onCreate, $onMatch); + + $this->assertSame("MERGE (a)-->(b) ON CREATE SET a.a = 'b' ON MATCH SET a.a = 'b'", $statement->toQuery()); + } + + public function testReturnsSameInstance(): void + { + $m = Query::node(); + + $expected = Query::new(); + $actual = $expected->merge($m); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryOptionalMatchTest.php b/tests/unit/QueryOptionalMatchTest.php new file mode 100644 index 00000000..ccfa6a5c --- /dev/null +++ b/tests/unit/QueryOptionalMatchTest.php @@ -0,0 +1,75 @@ +withVariable('m'); + + $statement = Query::new()->optionalMatch($m)->build(); + + $this->assertSame("OPTIONAL MATCH (m:Movie)", $statement); + + $statement = Query::new()->optionalMatch([$m, $m])->build(); + + $this->assertSame("OPTIONAL MATCH (m:Movie), (m:Movie)", $statement); + } + + public function testDoesNotAcceptRelationship(): void + { + $r = Query::relationship(Relationship::DIR_LEFT); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->optionalMatch($r); + } + + public function testDoesNotAcceptRelationshipWithNode(): void + { + $r = Query::relationship(Relationship::DIR_LEFT); + $m = Query::node(); + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->optionalMatch([$m, $r]); + } + + public function testDoesNotAcceptTypeOtherThanMatchablePattern(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->optionalMatch(false); + } + + public function testReturnsSameInstance(): void + { + $m = Query::node(); + + $expected = Query::new(); + $actual = $expected->optionalMatch($m); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryOrderByTest.php b/tests/unit/QueryOrderByTest.php new file mode 100644 index 00000000..9c1312de --- /dev/null +++ b/tests/unit/QueryOrderByTest.php @@ -0,0 +1,84 @@ +orderBy([]); + + $this->assertSame("", $statement->toQuery()); + } + + public function testSingleProperty(): void + { + $property = Query::variable('a')->property('foo'); + $statement = Query::new()->orderBy($property); + + $this->assertSame("ORDER BY a.foo", $statement->toQuery()); + } + + public function testMultipleProperties(): void + { + $a = Query::variable('a')->property('foo'); + $b = Query::variable('b')->property('foo'); + + $statement = Query::new()->orderBy([$a, $b]); + + $this->assertSame("ORDER BY a.foo, b.foo", $statement->toQuery()); + } + + public function testDefaultIsNonDescending(): void + { + $a = Query::variable('a')->property('foo'); + + $default = Query::new()->orderBy($a); + $explicit = Query::new()->orderBy($a, false); + + $this->assertSame($explicit->toQuery(), $default->toQuery()); + } + + public function testDescending(): void + { + $a = Query::variable('a')->property('foo'); + + $statement = Query::new()->orderBy($a, true); + + $this->assertSame("ORDER BY a.foo DESCENDING", $statement->toQuery()); + } + + public function testDoesNotAcceptAnyType(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->orderBy("foo"); + } + + public function testReturnsSameInstance(): void + { + $a = Query::node()->property('a'); + + $expected = Query::new(); + $actual = $expected->orderBy($a); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryRawTest.php b/tests/unit/QueryRawTest.php new file mode 100644 index 00000000..7023e66a --- /dev/null +++ b/tests/unit/QueryRawTest.php @@ -0,0 +1,36 @@ +raw('UNIMPLEMENTED', 'clause body'); + + $this->assertSame('UNIMPLEMENTED clause body', $query->toQuery()); + } + + public function testReturnsSameInstance(): void + { + $expected = Query::new(); + $actual = $expected->raw('UNIMPLEMENTED', 'body'); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryRemoveTest.php b/tests/unit/QueryRemoveTest.php new file mode 100644 index 00000000..71e56fdb --- /dev/null +++ b/tests/unit/QueryRemoveTest.php @@ -0,0 +1,67 @@ +property('world'); + + $query = Query::new()->remove($prop); + + $this->assertSame('REMOVE hello.world', $query->toQuery()); + } + + public function testMultipleProperties(): void + { + $a = Query::variable('hello')->property('world'); + $b = Query::variable('world')->property('hello'); + + $query = Query::new()->remove([$a, $b]); + + $this->assertSame('REMOVE hello.world, world.hello', $query->toQuery()); + } + + public function testSingleLabel(): void + { + $tom = Query::variable('tom')->labeled('Actor'); + + $query = Query::new()->remove($tom); + + $this->assertSame('REMOVE tom:Actor', $query->toQuery()); + } + + public function testMultipleLabels(): void + { + $tom = Query::variable('tom')->labeled('Actor'); + $leonardo = Query::variable('leonardo')->labeled('Actor'); + + $query = Query::new()->remove([$tom, $leonardo]); + + $this->assertSame('REMOVE tom:Actor, leonardo:Actor', $query->toQuery()); + } + + public function testReturnsSameInstance(): void + { + $expected = Query::new(); + $actual = $expected->remove([Query::variable('tom')->labeled('Actor')]); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryReturningTest.php b/tests/unit/QueryReturningTest.php new file mode 100644 index 00000000..e7ff15f1 --- /dev/null +++ b/tests/unit/QueryReturningTest.php @@ -0,0 +1,87 @@ +withVariable('m'); + + $statement = Query::new()->returning($m)->build(); + + $this->assertSame("RETURN m", $statement); + } + + public function testReturningUnnamedNodeGeneratesName(): void + { + $m = Query::node('Movie'); + + $statement = Query::new()->returning($m)->build(); + + $this->assertMatchesRegularExpression("/(RETURN var[\da-f]+)/", $statement); + } + + public function testReturningNamedNodeAlias(): void + { + $m = Query::node('Movie')->withVariable('m'); + + $statement = Query::new()->returning(["n" => $m])->build(); + + $this->assertSame("RETURN m AS n", $statement); + } + + public function testReturningUnnamedNodeAlias(): void + { + $m = Query::node('Movie'); + + $statement = Query::new()->returning(["n" => $m])->build(); + + $this->assertMatchesRegularExpression("/(RETURN var[\da-f]+ AS n)/", $statement); + } + + public function testReturnDistinct(): void + { + $m = Query::node('Movie')->withVariable('m'); + + $statement = Query::new()->returning($m, true)->build(); + + $this->assertSame('RETURN DISTINCT m', $statement); + } + + public function testReturningRejectsNotAnyType(): void + { + $m = new class() + { + }; + + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->returning($m); + } + + public function testReturnsSameInstance(): void + { + $expected = Query::new(); + $actual = $expected->returning(Query::node()); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QuerySetTest.php b/tests/unit/QuerySetTest.php new file mode 100644 index 00000000..fd704c9d --- /dev/null +++ b/tests/unit/QuerySetTest.php @@ -0,0 +1,64 @@ +set(Query::variable('a')->labeled('foo'))->toQuery(); + + $this->assertSame('SET a:foo', $set); + } + + public function testSetMultipleLabels(): void + { + $set = Query::new()->set([Query::variable('a')->labeled('foo'), Query::variable('b')->labeled('foo')])->toQuery(); + + $this->assertSame('SET a:foo, b:foo', $set); + } + + public function testPropertyReplacement(): void + { + $set = Query::new()->set(Query::variable('a')->assign(['a' => 'b']))->toQuery(); + + $this->assertSame('SET a = {a: \'b\'}', $set); + } + + public function testPropertyReplacementSingleProperty(): void + { + $set = Query::new()->set(Query::variable('a')->property('b')->replaceWith('c'))->toQuery(); + + $this->assertSame('SET a.b = \'c\'', $set); + } + + public function testPropertyMutation(): void + { + $set = Query::new()->set(Query::variable('a')->assign(['a' => 'b'])->setMutate())->toQuery(); + + $this->assertSame('SET a += {a: \'b\'}', $set); + } + + public function testReturnsSameInstance(): void + { + $expected = Query::new(); + $actual = $expected->set(Query::variable('a')->assign(['a' => 'b'])); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QuerySkipTest.php b/tests/unit/QuerySkipTest.php new file mode 100644 index 00000000..c0d86767 --- /dev/null +++ b/tests/unit/QuerySkipTest.php @@ -0,0 +1,43 @@ +skip(10)->toQuery(); + + $this->assertSame("SKIP 10", $skip); + } + + public function testSkipInteger(): void + { + $skip = Query::new()->skip(Query::integer(10))->toQuery(); + + $this->assertSame("SKIP 10", $skip); + } + + public function testReturnsSameInstance(): void + { + $expected = Query::new(); + $actual = $expected->skip(10); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php new file mode 100644 index 00000000..a3fec160 --- /dev/null +++ b/tests/unit/QueryTest.php @@ -0,0 +1,442 @@ +assertInstanceOf(Query::class, $new); + } + + public function testIsNotSingleton(): void + { + $a = Query::new(); + $b = Query::new(); + + $this->assertNotSame($a, $b); + } + + public function testNodeWithoutLabel(): void + { + $actual = Query::node(); + $expected = new Node(); + + $this->assertEquals($expected, $actual); + } + + public function testNodeWithLabel(): void + { + $label = "m"; + + $actual = Query::node($label); + $expected = (new Node())->addLabel($label); + + $this->assertEquals($expected, $actual); + } + + public function testRelationship(): void + { + $directions = [Relationship::DIR_UNI, Relationship::DIR_LEFT, Relationship::DIR_RIGHT]; + + foreach ($directions as $direction) { + $expected = new Relationship($direction); + $actual = Query::relationship($direction); + + $this->assertEquals($expected, $actual); + } + } + + public function testInvalidRelationship(): void + { + $this->expectException(InvalidArgumentException::class); + + Query::relationship(['bad', 'value']); + } + + public function testVariable(): void + { + $variable = Query::variable("foo"); + $this->assertInstanceOf(Variable::class, $variable); + $this->assertSame("foo", $variable->getName()); + } + + public function testVariableEmpty(): void + { + $this->assertInstanceOf(Variable::class, Query::variable()); + + $this->assertMatchesRegularExpression('/var[0-9a-f]+/', Query::variable()->toQuery()); + } + + public function testParameter(): void + { + $this->assertInstanceOf(Parameter::class, Query::parameter("foo")); + } + + public function testParameterEmpty(): void + { + $this->assertInstanceOf(Parameter::class, Query::parameter()); + } + + /** + * @dataProvider provideLiteralData + * + * @param mixed $literal + */ + public function testLiteral($literal, PropertyType $expected): void + { + $actual = Query::literal($literal); + + $this->assertEquals($expected, $actual); + } + + public function testBoolean(): void + { + $actual = Query::boolean(true); + + $this->assertEquals(new Boolean(true), $actual); + } + + public function testInteger(): void + { + $actual = Query::integer(10); + + $this->assertEquals(new Integer(10), $actual); + } + + public function testList(): void + { + $list = Query::list([]); + + $this->assertInstanceOf(List_::class, $list); + } + + public function testFunction(): void + { + $function = Query::function(); + + $this->assertSame(Query::procedure(), $function); + } + + public function testProcedure(): void + { + $procedure = Query::procedure(); + + $this->assertSame(Procedure::class, $procedure); + } + + public function testRawExpression(): void + { + $rawExpression = Query::rawExpression('UNIMPLEMENTED'); + + $this->assertEquals(new RawExpression('UNIMPLEMENTED'), $rawExpression); + } + + public function testToString(): void + { + $query = Query::new()->skip(10); + + $this->assertSame('SKIP 10', $query->__toString()); + $this->assertSame('SKIP 10', (string) $query); + } + + public function testListOfLiterals(): void + { + $list = Query::list(["hello", "world", 1.0, 1, 2, 3, true]); + + $this->assertInstanceOf(List_::class, $list); + } + + public function testListOfMixed(): void + { + $list = Query::list([$this->createMock(AnyType::class), "world"]); + + $this->assertInstanceOf(List_::class, $list); + } + + public function testListOfAnyType(): void + { + $list = Query::list([ + $this->createMock(AnyType::class), + $this->createMock(AnyType::class), + ]); + + $this->assertInstanceOf(List_::class, $list); + } + + public function testNestedList(): void + { + $list = Query::list([Query::list([])]); + + $this->assertInstanceOf(List_::class, $list); + } + + public function testIteratorList(): void + { + $iterator = new class() implements Iterator + { + private int $count = 0; + + public function current(): int + { + return 1; + } + + public function next(): int + { + $this->count++; + + return 1; + } + + public function key(): int + { + return 0; + } + + public function valid(): bool + { + // In order to avoid an infinite loop + return $this->count < 10; + } + + public function rewind(): void + { + } + }; + + $list = Query::list($iterator); + + $this->assertInstanceOf(List_::class, $list); + } + + public function testInvalidList(): void + { + $this->expectException(TypeError::class); + Query::list([new class() + { + }, ]); + } + + public function testMap(): void + { + $map = Query::map([]); + + $this->assertInstanceOf(Map::class, $map); + } + + public function testAddClause(): void + { + $clauseMock = new RawClause('FOOBAR', 'foobar'); + $statement = (new Query())->addClause($clauseMock)->build(); + + $this->assertSame("FOOBAR foobar", $statement); + } + + public function testBuild(): void + { + $withClause = (new WithClause)->addEntry(new Variable("foobar")); + $whereClause = (new WhereClause)->addExpression(new Label(new Variable("foo"), "bar")); + + $query = new Query(); + $query->addClause($withClause)->addClause($whereClause); + + $statement = $query->build(); + + $this->assertSame("WITH foobar WHERE foo:bar", $statement); + + $variableMock = new Variable("a"); + $nodeMock = (new Node)->withVariable($variableMock); + + $pathMock = new Path([$nodeMock, (new Node)->withVariable('b')], [new Relationship(Relationship::DIR_RIGHT)]); + $numeralMock = new Integer(12); + $booleanMock = new GreaterThan($variableMock, new Variable('b'), false); + $propertyMock = new Property($variableMock, 'b'); + + $query = new Query(); + $statement = $query->match([$pathMock, $nodeMock]) + ->returning(["#" => $nodeMock], true) + ->create([$pathMock, $nodeMock]) + ->create($pathMock) + ->delete([$variableMock, $variableMock]) + ->detachDelete([$variableMock, $variableMock]) + ->limit($numeralMock) + ->merge($nodeMock) + ->optionalMatch([$nodeMock, $nodeMock]) + ->orderBy([$propertyMock, $propertyMock], true) + ->remove($propertyMock) + ->set([]) + ->where($booleanMock) + ->with(["#" => $nodeMock]) + ->build(); + + $this->assertSame("MATCH (a)-->(b), (a) RETURN DISTINCT a AS `#` CREATE (a)-->(b), (a) CREATE (a)-->(b) DELETE a, a DETACH DELETE a, a LIMIT 12 MERGE (a) OPTIONAL MATCH (a), (a) ORDER BY a.b, a.b DESCENDING REMOVE a.b WHERE a > b WITH a AS `#`", $statement); + } + + public function testBuildEmpty(): void + { + $query = new Query(); + + $this->assertSame("", $query->build()); + } + + public function testInt(): void + { + $literal = Query::literal(1); + $this->assertInstanceOf(Integer::class, $literal); + $this->assertEquals('1', $literal->toQuery()); + } + + public function testFloatLiteral(): void + { + $literal = Query::literal(1.2); + $this->assertInstanceOf(Float_::class, $literal); + $this->assertEquals('1.2', $literal->toQuery()); + } + + public function testFloat(): void + { + $float = Query::float(1.0); + + $this->assertEquals(new Float_(1.0), $float); + } + + public function testStringLiteral(): void + { + $literal = Query::literal('abc'); + $this->assertInstanceOf(String_::class, $literal); + $this->assertEquals("'abc'", $literal->toQuery()); + } + + public function testString(): void + { + $string = Query::string('foo'); + + $this->assertEquals(new String_('foo'), $string); + } + + public function testStringAble(): void + { + // @phpstan-ignore-next-line + $literal = Query::literal(new class() + { + public function __toString(): string + { + return 'stringable abc'; + } + }); + $this->assertInstanceOf(String_::class, $literal); + $this->assertEquals("'stringable abc'", $literal->toQuery()); + } + + public function testBool(): void + { + $literal = Query::literal(true); + $this->assertInstanceOf(Boolean::class, $literal); + $this->assertEquals("true", $literal->toQuery()); + } + + public function testInvalidLiteral(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::literal(Query::literal(true)); + } + + public function testLiteralReference(): void + { + $value = Query::literal(); + + $this->assertSame(Literal::class, $value); + } + + public function testCanConstruct(): void + { + $query = new Query(); + + $this->assertInstanceOf(Query::class, $query); + } + + public function testAutomaticIdentifierGeneration(): void + { + $node = Query::node(); + + $this->assertMatchesRegularExpression('/var[0-9a-f]+\.foo/', $node->property('foo')->toQuery()); + + $node->withVariable('foo'); + + $this->assertSame('foo.bar', $node->property('bar')->toQuery()); + + $node = Query::node(); + $statement = Query::new()->match($node)->returning($node)->build(); + + $this->assertMatchesRegularExpression('/MATCH \(var[0-9a-f]+\) RETURN var[0-9a-f]+/', $statement); + + $node = Query::node(); + + $this->assertInstanceOf(Variable::class, $node->getVariable()); + } + + /** + * @return mixed[][] + */ + public function provideLiteralData(): array + { + return [ + ['foobar', new String_('foobar')], + ['0', new String_('0')], + ['100', new String_('100')], + [0, new Integer(0)], + [100, new Integer(100)], + [10.0, new Float_(10.0)], + [69420.0, new Float_(69420)], + [10.0000000000000000000000000000001, new Float_(10.0000000000000000000000000000001)], + [false, new Boolean(false)], + [true, new Boolean(true)], + ]; + } +} diff --git a/tests/unit/QueryUnionTest.php b/tests/unit/QueryUnionTest.php new file mode 100644 index 00000000..5af04a31 --- /dev/null +++ b/tests/unit/QueryUnionTest.php @@ -0,0 +1,77 @@ +withVariable('x'); + $nodeY = Query::node('Y')->withVariable('y'); + + $query = Query::new()->match($nodeX)->returning($nodeX->getVariable()); + $right = Query::new()->match($nodeY)->returning($nodeY->getVariable()); + + $query = $query->union($right, true); + + $this->assertEquals('MATCH (x:X) RETURN x UNION ALL MATCH (y:Y) RETURN y', $query->toQuery()); + } + + public function testUnionQuery(): void + { + $nodeX = Query::node('X')->withVariable('x'); + $nodeY = Query::node('Y')->withVariable('y'); + + $query = Query::new()->match($nodeX)->returning($nodeX->getVariable()); + $right = Query::new()->match($nodeY)->returning($nodeY->getVariable()); + + $query = $query->union($right, false); + + $this->assertEquals('MATCH (x:X) RETURN x UNION MATCH (y:Y) RETURN y', $query->toQuery()); + } + + public function testUnionDecorator(): void + { + $nodeX = Query::node('X')->withVariable('x'); + + $query = Query::new()->match($nodeX)->returning($nodeX->getVariable()); + + $query = $query->union(static function (Query $query): void + { + $nodeY = Query::node('Y')->withVariable('y'); + $query->match($nodeY)->returning($nodeY->getVariable()); + }); + + $this->assertEquals('MATCH (x:X) RETURN x UNION MATCH (y:Y) RETURN y', $query->toQuery()); + } + + public function testUnionDecoratorAll(): void + { + $nodeX = Query::node('X')->withVariable('x'); + + $query = Query::new()->match($nodeX)->returning($nodeX->getVariable()); + + $query = $query->union(static function (Query $query): void + { + $nodeY = Query::node('Y')->withVariable('y'); + $query->match($nodeY)->returning($nodeY->getVariable()); + }, true); + + $this->assertEquals('MATCH (x:X) RETURN x UNION ALL MATCH (y:Y) RETURN y', $query->toQuery()); + } +} diff --git a/tests/unit/QueryWhereTest.php b/tests/unit/QueryWhereTest.php new file mode 100644 index 00000000..e8dcfb3f --- /dev/null +++ b/tests/unit/QueryWhereTest.php @@ -0,0 +1,81 @@ +where(true)->toQuery(); + + $this->assertSame('WHERE true', $where); + } + + public function testWhereSingleExpression(): void + { + $where = Query::new()->where(Query::literal(true))->toQuery(); + + $this->assertSame('WHERE true', $where); + } + + public function testWhereMultipleExpressions(): void + { + $where = Query::new()->where([true, true])->toQuery(); + + $this->assertSame('WHERE (true AND true)', $where); + } + + public function testDefaultUnifierIsConjunction(): void + { + $where = Query::new()->where([true, true])->toQuery(); + $whereAnd = Query::new()->where([true, true], WhereClause::AND)->toQuery(); + + $this->assertSame($whereAnd, $where); + } + + public function testUnifyWithOr(): void + { + $where = Query::new()->where([true, true], WhereClause::OR)->toQuery(); + + $this->assertSame('WHERE (true OR true)', $where); + } + + public function testUnifyWithXor(): void + { + $where = Query::new()->where([true, true], WhereClause::XOR)->toQuery(); + + $this->assertSame('WHERE (true XOR true)', $where); + } + + public function testDoesNotAcceptInvalidOperator(): void + { + $this->expectException(InvalidArgumentException::class); + + Query::new()->where([true, true], 'XAND')->toQuery(); + } + + public function testReturnsSameInstance(): void + { + $expected = Query::new(); + $actual = $expected->where(true); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/QueryWithTest.php b/tests/unit/QueryWithTest.php new file mode 100644 index 00000000..158d9e27 --- /dev/null +++ b/tests/unit/QueryWithTest.php @@ -0,0 +1,103 @@ +with('a')->build(); + + $this->assertSame('WITH a', $with); + } + + public function testWithSingleVariable(): void + { + $with = Query::new()->with(Query::variable('a'))->build(); + + $this->assertSame('WITH a', $with); + } + + public function testWithMultipleStringVariables(): void + { + $with = Query::new()->with(['a', 'b'])->build(); + + $this->assertSame('WITH a, b', $with); + } + + public function testWithMultipleVariables(): void + { + $with = Query::new()->with([Query::variable('a'), Query::variable('b')])->build(); + + $this->assertSame('WITH a, b', $with); + } + + public function testWithMultipleVariablesMixed(): void + { + $with = Query::new()->with([Query::variable('a'), 'b'])->build(); + + $this->assertSame('WITH a, b', $with); + } + + public function testWithSingleAliasString(): void + { + $with = Query::new()->with(['a' => 'b'])->build(); + + $this->assertSame('WITH \'b\' AS a', $with); + } + + public function testWithMultipleAliasesMixed(): void + { + $with = Query::new()->with(['a' => 'b', 'b' => Query::variable('c'), 'c'])->build(); + + $this->assertSame('WITH \'b\' AS a, c AS b, c', $with); + } + + public function testMultipleWith(): void + { + $with = Query::new()->with('a')->with('b')->build(); + + $this->assertSame('WITH a WITH b', $with); + } + + public function testDoesNotAcceptAnyType(): void + { + $this->expectException(TypeError::class); + + // @phpstan-ignore-next-line + Query::new()->with(new class + { + }); + } + + public function testInteger(): void + { + $with = Query::new()->with(['a' => 25])->build(); + + $this->assertSame('WITH 25 AS a', $with); + } + + public function testReturnsSameInstance(): void + { + $expected = Query::new(); + $actual = $expected->with('a'); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Syntax/AliasTest.php b/tests/unit/Syntax/AliasTest.php new file mode 100644 index 00000000..388934f5 --- /dev/null +++ b/tests/unit/Syntax/AliasTest.php @@ -0,0 +1,50 @@ +assertSame($original, $alias->getOriginal()); + } + + public function testGetVariable(): void + { + $original = Query::variable('a'); + $variable = Query::variable('b'); + + $alias = new Alias($original, $variable); + + $this->assertSame($variable, $alias->getVariable()); + } + + public function testToQuery(): void + { + $original = Query::variable('a'); + $variable = Query::variable('b'); + + $alias = new Alias($original, $variable); + + $this->assertSame('a AS b', $alias->toQuery()); + } +} diff --git a/tests/unit/Syntax/PropertyReplacementTest.php b/tests/unit/Syntax/PropertyReplacementTest.php new file mode 100644 index 00000000..4e18826a --- /dev/null +++ b/tests/unit/Syntax/PropertyReplacementTest.php @@ -0,0 +1,54 @@ +property('b'), Query::boolean(true)); + + $this->assertSame("a.b = true", $propRepl->toQuery()); + + $propRepl->setMutate(); + + $this->assertSame("a.b += true", $propRepl->toQuery()); + } + + public function testMutates(): void + { + $propRepl = new PropertyReplacement(Query::variable('a')->property('b'), Query::boolean(true)); + + $this->assertFalse($propRepl->mutates()); + + $propRepl->setMutate(); + + $this->assertTrue($propRepl->mutates()); + + $propRepl->setMutate(false); + + $this->assertFalse($propRepl->mutates()); + } + + public function testSetMutateReturnsSameInstance(): void + { + $expected = new PropertyReplacement(Query::variable('a')->property('b'), Query::boolean(true)); + $actual = $expected->setMutate(); + + $this->assertSame($expected, $actual); + } +} diff --git a/tests/unit/Traits/CastTraitTest.php b/tests/unit/Traits/CastTraitTest.php new file mode 100644 index 00000000..cc7c54d8 --- /dev/null +++ b/tests/unit/Traits/CastTraitTest.php @@ -0,0 +1,193 @@ +trait = new class + { + use CastTrait { + toListType as public; + toMapType as public; + toStringType as public; + toNumeralType as public; + toIntegerType as public; + toBooleanType as public; + toPropertyType as public; + toStructuralType as public; + toVariable as public; + toName as public; + toAnyType as public; + } + }; + } + + public function testToListType(): void + { + $list = $this->trait->toListType(['a', 'b', 'c']); + + $this->assertInstanceOf(ListType::class, $list); + + $list = Literal::list(['a', 'b', 'c']); + $list = $this->trait->toListType($list); + + $this->assertInstanceOf(ListType::class, $list); + } + + public function testToMapType(): void + { + $map = $this->trait->toMapType(['a' => 'a', 'b' => 'b', 'c' => 'c']); + + $this->assertInstanceOf(MapType::class, $map); + + $map = Literal::map(['a' => 'a', 'b' => 'b', 'c' => 'c']); + $map = $this->trait->toMapType($map); + + $this->assertInstanceOf(MapType::class, $map); + } + + public function testToStringType(): void + { + $string = $this->trait->toStringType('hello'); + + $this->assertInstanceOf(StringType::class, $string); + + $string = Literal::string('a'); + $string = $this->trait->toStringType($string); + + $this->assertInstanceOf(StringType::class, $string); + } + + public function testToNumeralType(): void + { + $numeral = $this->trait->toNumeralType(1); + + $this->assertInstanceOf(IntegerType::class, $numeral); + + $numeral = $this->trait->toNumeralType(1.1); + + $this->assertInstanceOf(FloatType::class, $numeral); + + $numeral = Literal::number(1.1); + $numeral = $this->trait->toNumeralType($numeral); + + $this->assertInstanceOf(FloatType::class, $numeral); + + $numeral = Literal::number(1); + $numeral = $this->trait->toNumeralType($numeral); + + $this->assertInstanceOf(IntegerType::class, $numeral); + } + + public function testToIntegerType(): void + { + $integer = $this->trait->toIntegerType(10); + + $this->assertInstanceOf(IntegerType::class, $integer); + + $integer = Literal::integer(10); + $integer = $this->trait->toIntegerType($integer); + + $this->assertInstanceOf(IntegerType::class, $integer); + } + + public function testToBooleanType(): void + { + $boolean = $this->trait->toBooleanType(true); + + $this->assertInstanceOf(BooleanType::class, $boolean); + + $boolean = Literal::boolean(true); + $boolean = $this->trait->toBooleanType($boolean); + + $this->assertInstanceOf(BooleanType::class, $boolean); + } + + public function testToPropertyType(): void + { + $property = $this->trait->toPropertyType('test'); + + $this->assertInstanceOf(StringType::class, $property); + + $property = $this->trait->toPropertyType(true); + + $this->assertInstanceOf(BooleanType::class, $property); + + $property = Literal::boolean(true); + $property = $this->trait->toBooleanType($property); + + $this->assertInstanceOf(BooleanType::class, $property); + } + + public function testToStructuralType(): void + { + $structural = $this->trait->toStructuralType(Query::node()); + + $this->assertInstanceOf(StructuralType::class, $structural); + + $structural = $this->trait->toStructuralType(Query::node()->getVariable()); + + $this->assertInstanceOf(StructuralType::class, $structural); + } + + public function testToVariable(): void + { + $variable = $this->trait->toVariable('a'); + + $this->assertInstanceOf(Variable::class, $variable); + + $variable = $this->trait->toVariable(Query::node()); + + $this->assertInstanceOf(Variable::class, $variable); + + $variable = $this->trait->toVariable(new Variable('a')); + + $this->assertInstanceOf(Variable::class, $variable); + } + + public function testToName(): void + { + $name = $this->trait->toName('a'); + + $this->assertInstanceOf(Variable::class, $name); + + $name = $this->trait->toName(new Variable('a')); + + $this->assertInstanceOf(Variable::class, $name); + } + + public function testToNameDoesNotAcceptPattern(): void + { + $this->expectException(TypeError::class); + + $this->trait->toName(Query::node()); + } +} diff --git a/tests/Unit/Traits/ErrorTraitTest.php b/tests/unit/Traits/ErrorTraitTest.php similarity index 69% rename from tests/Unit/Traits/ErrorTraitTest.php rename to tests/unit/Traits/ErrorTraitTest.php index c2e8669b..023ba06f 100644 --- a/tests/Unit/Traits/ErrorTraitTest.php +++ b/tests/unit/Traits/ErrorTraitTest.php @@ -1,24 +1,12 @@ -errorImpl = new ErrorImpl(); } @@ -120,14 +100,14 @@ public function failingAssertionsProvider(): array public function testGetTypeErrorText(): void { $this->assertEquals( - '$foo should be a WikibaseSolutions\CypherDSL\Tests\Unit\Traits\ErrorHelperDummyA object, int given.', - $this->errorImpl->call('getTypeErrorText', ['foo', [ErrorHelperDummyA::class], 5]) + '$foo should be a WikibaseSolutions\CypherDSL\Tests\Unit\Traits\ErrorHelperDummyA, int given.', + $this->errorImpl->call('typeError', ['foo', [ErrorHelperDummyA::class], 5])->getMessage() ); $this->assertEquals( '$foo should be a ' . 'WikibaseSolutions\CypherDSL\Tests\Unit\Traits\ErrorHelperDummyA or ' . - 'WikibaseSolutions\CypherDSL\Tests\Unit\Traits\ErrorHelperDummyB object, int given.', - $this->errorImpl->call('getTypeErrorText', ['foo', [ErrorHelperDummyA::class, ErrorHelperDummyB::class], 5]) + 'WikibaseSolutions\CypherDSL\Tests\Unit\Traits\ErrorHelperDummyB, int given.', + $this->errorImpl->call('typeError', ['foo', [ErrorHelperDummyA::class, ErrorHelperDummyB::class], 5])->getMessage() ); } @@ -150,8 +130,9 @@ public function getUserInputInfoProvider(): array ['float', 3.14], ['bool', true], ['array', ['foo', 'bar']], - ['class@anonymous', new class () { - }], + ['class@anonymous', new class() + { + }, ], [ErrorHelperDummyA::class, new ErrorHelperDummyA()], ]; } diff --git a/tests/Unit/Traits/EscapeTraitTest.php b/tests/unit/Traits/EscapeTraitTest.php similarity index 54% rename from tests/Unit/Traits/EscapeTraitTest.php rename to tests/unit/Traits/EscapeTraitTest.php index 33880c28..15426a4a 100644 --- a/tests/Unit/Traits/EscapeTraitTest.php +++ b/tests/unit/Traits/EscapeTraitTest.php @@ -1,52 +1,41 @@ -trait = $this->getMockForTrait(EscapeTrait::class); + $this->trait = new class + { + use EscapeTrait { + escape as public; + } + }; } /** - * @param string $expected * @dataProvider provideSafeValueIsNotEscapedData */ - public function testSafeValueIsNotEscaped(string $expected) + public function testSafeValueIsNotEscaped(string $expected): void { $actual = $this->trait->escape($expected); @@ -54,10 +43,9 @@ public function testSafeValueIsNotEscaped(string $expected) } /** - * @param string $value * @dataProvider provideUnsafeValueIsEscapedData */ - public function testUnsafeValueIsEscaped(string $value) + public function testUnsafeValueIsEscaped(string $value): void { $expected = sprintf("`%s`", $value); $actual = $this->trait->escape($value); @@ -65,6 +53,23 @@ public function testUnsafeValueIsEscaped(string $value) $this->assertSame($expected, $actual); } + /** + * @dataProvider provideValueWithBacktickIsProperlyEscapedData + */ + public function testValueWithBacktickIsProperlyEscaped($input, $expected): void + { + $this->assertSame('`foo``bar`', $this->trait->escape("foo`bar")); + } + + public function testValueWithMoreThan65534CharactersCannotBeEscaped(): void + { + $value = str_repeat('a', 65535); + + $this->expectException(InvalidArgumentException::class); + + $this->trait->escape($value); + } + public function provideSafeValueIsNotEscapedData(): array { return [ @@ -86,7 +91,6 @@ public function provideSafeValueIsNotEscapedData(): array public function provideUnsafeValueIsEscapedData(): array { return [ - [''], ['__FooBar__'], ['_'], ['__'], @@ -100,22 +104,14 @@ public function provideUnsafeValueIsEscapedData(): array ]; } - /** - * @dataProvider provideValueWithBacktickIsProperlyEscapedData - */ - public function testValueWithBacktickIsProperlyEscaped($input, $expected) - { - $this->assertSame('`foo``bar`', $this->trait->escape("foo`bar")); - } - public function provideValueWithBacktickIsProperlyEscapedData(): array { return [ - ['foo`bar','`foo``bar`'], - ['`foo','```foo`'], - ['foo`','`foo```'], - ['foo``bar','`foo````bar`'], - ['`foo`','```foo```'], + ['foo`bar', '`foo``bar`'], + ['`foo', '```foo`'], + ['foo`', '`foo```'], + ['foo``bar', '`foo````bar`'], + ['`foo`', '```foo```'], ]; } } diff --git a/tests/unit/Traits/NameGenerationTraitTest.php b/tests/unit/Traits/NameGenerationTraitTest.php new file mode 100644 index 00000000..b76c33b7 --- /dev/null +++ b/tests/unit/Traits/NameGenerationTraitTest.php @@ -0,0 +1,46 @@ +trait = new class() + { + use NameGenerationTrait { + generateIdentifier as public; + } + }; + } + + public function testGenerateIdentifierWithoutPrefix(): void + { + $this->assertMatchesRegularExpression('/^var\w{32}$/', $this->trait->generateIdentifier()); + } + + public function testGenerateIdentifierWithPrefix(): void + { + $this->assertMatchesRegularExpression('/^x\w{32}$/', $this->trait->generateIdentifier('x')); + } + + public function testGenerateIdentifierWithPrefixAndLength(): void + { + $this->assertMatchesRegularExpression('/^x\w{16}$/', $this->trait->generateIdentifier('x', 16)); + } +} diff --git a/tests/unit/Traits/PatternTraits/PatternTraitTest.php b/tests/unit/Traits/PatternTraits/PatternTraitTest.php new file mode 100644 index 00000000..cbb9ce57 --- /dev/null +++ b/tests/unit/Traits/PatternTraits/PatternTraitTest.php @@ -0,0 +1,90 @@ +stub = $this->getMockForTrait(PatternTrait::class); + } + + public function testWithVariable(): void + { + $this->stub->withVariable("hello"); + + $this->assertSame("hello", $this->stub->getVariable()->getName()); + + $variable = Query::variable("hello"); + $this->stub->withVariable($variable); + + $this->assertSame($variable, $this->stub->getVariable()); + } + + public function testGetVariableWithoutVariable(): void + { + $variable = $this->stub->getVariable(); + + $this->assertInstanceOf(Variable::class, $variable); + $this->assertStringMatchesFormat("var%s", $variable->getName()); + } + + public function testGetVariable(): void + { + $variable = Query::variable('a'); + $this->stub->withVariable($variable); + + $this->assertSame($variable, $this->stub->getVariable()); + } + + public function testWithVariableReturnsSameInstance(): void + { + $expected = $this->stub; + $actual = $expected->withVariable('foo'); + + $this->assertSame($expected, $actual); + } + + public function testDoesNotAcceptAnyType(): void + { + $this->expectException(TypeError::class); + + $this->stub->withVariable(new stdClass()); + } + + /** + * @doesNotPerformAssertions + */ + public function testImplementsPatternCompletely(): void + { + new class implements Pattern + { + use PatternTrait; + + public function toQuery(): string + { + return ''; + } + }; + } +} diff --git a/tests/unit/Traits/PatternTraits/PropertyPatternTraitTest.php b/tests/unit/Traits/PatternTraits/PropertyPatternTraitTest.php new file mode 100644 index 00000000..efc7eba4 --- /dev/null +++ b/tests/unit/Traits/PatternTraits/PropertyPatternTraitTest.php @@ -0,0 +1,136 @@ +stub = $this->getMockForTrait(PropertyPatternTrait::class); + } + + public function testProperty(): void + { + $this->stub->withVariable("hello"); + + $this->assertSame("hello.world", $this->stub->property("world")->toQuery()); + } + + public function testWithProperties(): void + { + $properties = Query::map(['a' => 'b']); + $this->stub->withProperties($properties); + + $this->assertSame($properties, $this->stub->getProperties()); + } + + public function testWithPropertiesLiteral(): void + { + $this->stub->withProperties(['a' => 'b']); + + $this->assertSame('{a: \'b\'}', $this->stub->getProperties()->toQuery()); + } + + public function testAddProperty(): void + { + $this->stub->withProperties(['a' => 'b']); + $this->stub->addProperty('hello', 'world'); + + $this->assertSame('{a: \'b\', hello: \'world\'}', $this->stub->getProperties()->toQuery()); + } + + public function testAddPropertyWithoutMap(): void + { + $this->stub->addProperty('a', 'b'); + + $this->assertSame('{a: \'b\'}', $this->stub->getProperties()->toQuery()); + } + + public function testAddPropertyToNotMapThrowsException(): void + { + $this->stub->withProperties(Query::variable('foobar')); + + $this->expectException(TypeError::class); + + $this->stub->addProperty('hello', 'world'); + } + + public function testAddPropertyReturnsSameInstance(): void + { + $actual = $this->stub->addProperty('hello', 'world'); + + $this->assertSame($actual, $this->stub); + } + + public function testAddProperties(): void + { + $this->stub->withProperties(['a' => 'b']); + $this->stub->addProperties(['c' => 'd', 'e' => 'f']); + + $this->assertSame('{a: \'b\', c: \'d\', e: \'f\'}', $this->stub->getProperties()->toQuery()); + } + + public function testAddPropertiesWithoutMap(): void + { + $this->stub->addProperties(['a' => 'b']); + + $this->assertSame('{a: \'b\'}', $this->stub->getProperties()->toQuery()); + } + + public function testAddPropertiesToNotMapThrowsException(): void + { + $this->stub->withProperties(Query::variable('foo')); + + $this->expectException(TypeError::class); + + $this->stub->addProperties(['c' => 'd', 'e' => 'f']); + } + + public function testAddPropertiesReturnsSameInstance(): void + { + $actual = $this->stub->withProperties(['a' => 'b']); + + $this->assertSame($this->stub, $actual); + } + + public function testAddPropertiesDoesNotAcceptMapType(): void + { + $this->expectException(TypeError::class); + + $this->stub->addProperties(Query::variable('foobar')); + } + + /** + * @doesNotPerformAssertions + */ + public function testImplementsPatternCompletely(): void + { + new class implements PropertyPattern + { + use PropertyPatternTrait; + + public function toQuery(): string + { + return ''; + } + }; + } +} diff --git a/tests/unit/Traits/TypeTraits/AnyTypeTraitTest.php b/tests/unit/Traits/TypeTraits/AnyTypeTraitTest.php new file mode 100644 index 00000000..8a4e02ab --- /dev/null +++ b/tests/unit/Traits/TypeTraits/AnyTypeTraitTest.php @@ -0,0 +1,315 @@ +a = new class() implements AnyType + { + use AnyTypeTrait; + + public function toQuery(): string + { + return '420'; + } + }; + + $this->b = new class() implements AnyType + { + use AnyTypeTrait; + + public function toQuery(): string + { + return '1337'; + } + }; + } + + public function testAlias(): void + { + $b = Query::variable('b'); + $alias = $this->a->alias($b); + + $this->assertInstanceOf(Alias::class, $alias); + $this->assertSame($this->a, $alias->getOriginal()); + $this->assertSame($b, $alias->getVariable()); + } + + public function testAliasLiteral(): void + { + $alias = $this->a->alias('b'); + + $this->assertInstanceOf(Alias::class, $alias); + $this->assertSame($this->a, $alias->getOriginal()); + $this->assertEquals(new Variable('b'), $alias->getVariable()); + } + + public function testGt(): void + { + $gt = $this->a->gt($this->b); + + $this->assertInstanceOf(GreaterThan::class, $gt); + + $this->assertTrue($gt->insertsParentheses()); + $this->assertSame($this->a, $gt->getLeft()); + $this->assertSame($this->b, $gt->getRight()); + } + + public function testGtLiteral(): void + { + $gt = $this->a->gt(10); + + $this->assertInstanceOf(GreaterThan::class, $gt); + + $this->assertTrue($gt->insertsParentheses()); + $this->assertSame($this->a, $gt->getLeft()); + $this->assertEquals(new Integer(10), $gt->getRight()); + } + + public function testGtNoParentheses(): void + { + $gt = $this->a->gt($this->b, false); + + $this->assertInstanceOf(GreaterThan::class, $gt); + + $this->assertFalse($gt->insertsParentheses()); + $this->assertSame($this->a, $gt->getLeft()); + $this->assertSame($this->b, $gt->getRight()); + } + + public function testGte(): void + { + $gte = $this->a->gte($this->b); + + $this->assertInstanceOf(GreaterThanOrEqual::class, $gte); + + $this->assertTrue($gte->insertsParentheses()); + $this->assertEquals($this->a, $gte->getLeft()); + $this->assertEquals($this->b, $gte->getRight()); + } + + public function testGteLiteral(): void + { + $gte = $this->a->gte(10); + + $this->assertInstanceOf(GreaterThanOrEqual::class, $gte); + + $this->assertTrue($gte->insertsParentheses()); + $this->assertSame($this->a, $gte->getLeft()); + $this->assertEquals(new Integer(10), $gte->getRight()); + } + + public function testGteNoParentheses(): void + { + $gte = $this->a->gte($this->b, false); + + $this->assertInstanceOf(GreaterThanOrEqual::class, $gte); + + $this->assertFalse($gte->insertsParentheses()); + $this->assertEquals($this->a, $gte->getLeft()); + $this->assertEquals($this->b, $gte->getRight()); + } + + public function testLt(): void + { + $lt = $this->a->lt($this->b); + + $this->assertInstanceOf(LessThan::class, $lt); + + $this->assertTrue($lt->insertsParentheses()); + $this->assertEquals($this->a, $lt->getLeft()); + $this->assertEquals($this->b, $lt->getRight()); + } + + public function testLtLiteral(): void + { + $lt = $this->a->lt(10); + + $this->assertInstanceOf(LessThan::class, $lt); + + $this->assertTrue($lt->insertsParentheses()); + $this->assertSame($this->a, $lt->getLeft()); + $this->assertEquals(new Integer(10), $lt->getRight()); + } + + public function testLtNoParentheses(): void + { + $lt = $this->a->lt($this->b, false); + + $this->assertInstanceOf(LessThan::class, $lt); + + $this->assertFalse($lt->insertsParentheses()); + $this->assertEquals($this->a, $lt->getLeft()); + $this->assertEquals($this->b, $lt->getRight()); + } + + public function testLte(): void + { + $lte = $this->a->lte($this->b); + + $this->assertInstanceOf(LessThanOrEqual::class, $lte); + + $this->assertTrue($lte->insertsParentheses()); + $this->assertEquals($this->a, $lte->getLeft()); + $this->assertEquals($this->b, $lte->getRight()); + } + + public function testLteLiteral(): void + { + $lte = $this->a->lte(10); + + $this->assertInstanceOf(LessThanOrEqual::class, $lte); + + $this->assertTrue($lte->insertsParentheses()); + $this->assertSame($this->a, $lte->getLeft()); + $this->assertEquals(new Integer(10), $lte->getRight()); + } + + public function testLteNoParentheses(): void + { + $lte = $this->a->lte($this->b, false); + + $this->assertInstanceOf(LessThanOrEqual::class, $lte); + + $this->assertFalse($lte->insertsParentheses()); + $this->assertEquals($this->a, $lte->getLeft()); + $this->assertEquals($this->b, $lte->getRight()); + } + + public function testEquals(): void + { + $equals = $this->a->equals($this->b); + + $this->assertInstanceOf(Equality::class, $equals); + + $this->assertTrue($equals->insertsParentheses()); + $this->assertEquals($this->a, $equals->getLeft()); + $this->assertEquals($this->b, $equals->getRight()); + } + + public function testEqualsLiteral(): void + { + $equals = $this->a->equals(10); + + $this->assertInstanceOf(Equality::class, $equals); + + $this->assertTrue($equals->insertsParentheses()); + $this->assertSame($this->a, $equals->getLeft()); + $this->assertEquals(new Integer(10), $equals->getRight()); + } + + public function testEqualsNoParentheses(): void + { + $equals = $this->a->equals($this->b, false); + + $this->assertInstanceOf(Equality::class, $equals); + + $this->assertFalse($equals->insertsParentheses()); + $this->assertEquals($this->a, $equals->getLeft()); + $this->assertEquals($this->b, $equals->getRight()); + } + + public function testNotEquals(): void + { + $notEquals = $this->a->notEquals($this->b); + + $this->assertInstanceOf(Inequality::class, $notEquals); + + $this->assertTrue($notEquals->insertsParentheses()); + $this->assertEquals($this->a, $notEquals->getLeft()); + $this->assertEquals($this->b, $notEquals->getRight()); + } + + public function testNotEqualsLiteral(): void + { + $notEquals = $this->a->notEquals(10); + + $this->assertInstanceOf(Inequality::class, $notEquals); + + $this->assertTrue($notEquals->insertsParentheses()); + $this->assertSame($this->a, $notEquals->getLeft()); + $this->assertEquals(new Integer(10), $notEquals->getRight()); + } + + public function testNotEqualsNoParentheses(): void + { + $notEquals = $this->a->notEquals($this->b, false); + + $this->assertInstanceOf(Inequality::class, $notEquals); + + $this->assertFalse($notEquals->insertsParentheses()); + $this->assertEquals($this->a, $notEquals->getLeft()); + $this->assertEquals($this->b, $notEquals->getRight()); + } + + public function testIsNull(): void + { + $isNull = $this->a->isNull(); + + $this->assertInstanceOf(IsNull::class, $isNull); + + $this->assertTrue($isNull->insertsParentheses()); + $this->assertEquals($this->a, $isNull->getExpression()); + } + + public function testIsNullNoParentheses(): void + { + $isNull = $this->a->isNull(false); + + $this->assertInstanceOf(IsNull::class, $isNull); + + $this->assertFalse($isNull->insertsParentheses()); + $this->assertEquals($this->a, $isNull->getExpression()); + } + + public function testIsNotNull(): void + { + $isNotNull = $this->a->isNotNull(); + + $this->assertInstanceOf(IsNotNull::class, $isNotNull); + + $this->assertTrue($isNotNull->insertsParentheses()); + $this->assertEquals($this->a, $isNotNull->getExpression()); + } + + public function testIsNotNullNoParentheses(): void + { + $isNotNull = $this->a->isNotNull(false); + + $this->assertInstanceOf(IsNotNull::class, $isNotNull); + + $this->assertFalse($isNotNull->insertsParentheses()); + $this->assertEquals($this->a, $isNotNull->getExpression()); + } +} diff --git a/tests/unit/Traits/TypeTraits/CompositeTypeTraits/ListTypeTraitTest.php b/tests/unit/Traits/TypeTraits/CompositeTypeTraits/ListTypeTraitTest.php new file mode 100644 index 00000000..00ac4bc2 --- /dev/null +++ b/tests/unit/Traits/TypeTraits/CompositeTypeTraits/ListTypeTraitTest.php @@ -0,0 +1,40 @@ +a = new Property(new Variable('foo'), 'bar'); + $this->list = new List_; + } + + public function testHas(): void + { + $has = $this->list->has($this->a); + + $this->assertInstanceOf(In::class, $has); + } +} diff --git a/tests/unit/Traits/TypeTraits/MethodTraits/PropertyMethodTraitTest.php b/tests/unit/Traits/TypeTraits/MethodTraits/PropertyMethodTraitTest.php new file mode 100644 index 00000000..2f83ebf0 --- /dev/null +++ b/tests/unit/Traits/TypeTraits/MethodTraits/PropertyMethodTraitTest.php @@ -0,0 +1,35 @@ +a = new Map; + } + + public function testProperty(): void + { + $property = $this->a->property("foo"); + + $this->assertInstanceOf(Property::class, $property); + } +} diff --git a/tests/unit/Traits/TypeTraits/PropertyTypeTraits/BooleanTypeTraitTest.php b/tests/unit/Traits/TypeTraits/PropertyTypeTraits/BooleanTypeTraitTest.php new file mode 100644 index 00000000..aed6fd4c --- /dev/null +++ b/tests/unit/Traits/TypeTraits/PropertyTypeTraits/BooleanTypeTraitTest.php @@ -0,0 +1,136 @@ +a = new class() implements BooleanType + { + use BooleanTypeTrait; + + public function toQuery(): string + { + return ''; + } + }; + $this->b = new Boolean(false); + } + + public function testAnd(): void + { + $and = $this->a->and($this->b); + + $this->assertInstanceOf(Conjunction::class, $and); + + $this->assertTrue($and->insertsParentheses()); + $this->assertEquals($this->a, $and->getLeft()); + $this->assertEquals($this->b, $and->getRight()); + } + + public function testAndLiteral(): void + { + $xor = $this->a->and(false); + + $this->assertInstanceOf(Conjunction::class, $xor); + } + + public function testAndNoParentheses(): void + { + $and = $this->a->and($this->b, false); + + $this->assertInstanceOf(Conjunction::class, $and); + + $this->assertFalse($and->insertsParentheses()); + $this->assertEquals($this->a, $and->getLeft()); + $this->assertEquals($this->b, $and->getRight()); + } + + public function testOr(): void + { + $or = $this->a->or($this->b); + + $this->assertInstanceOf(Disjunction::class, $or); + + $this->assertTrue($or->insertsParentheses()); + $this->assertEquals($this->a, $or->getLeft()); + $this->assertEquals($this->b, $or->getRight()); + } + + public function testOrLiteral(): void + { + $xor = $this->a->or(false); + + $this->assertInstanceOf(Disjunction::class, $xor); + } + + public function testOrNoParentheses(): void + { + $or = $this->a->or($this->b, false); + + $this->assertInstanceOf(Disjunction::class, $or); + + $this->assertFalse($or->insertsParentheses()); + $this->assertEquals($this->a, $or->getLeft()); + $this->assertEquals($this->b, $or->getRight()); + } + + public function testXor(): void + { + $xor = $this->a->xor($this->b); + + $this->assertInstanceOf(ExclusiveDisjunction::class, $xor); + + $this->assertTrue($xor->insertsParentheses()); + $this->assertEquals($this->a, $xor->getLeft()); + $this->assertEquals($this->b, $xor->getRight()); + } + + public function testXorLiteral(): void + { + $xor = $this->a->xor(false); + + $this->assertInstanceOf(ExclusiveDisjunction::class, $xor); + } + + public function testXorNoParentheses(): void + { + $xor = $this->a->xor($this->b, false); + + $this->assertInstanceOf(ExclusiveDisjunction::class, $xor); + + $this->assertFalse($xor->insertsParentheses()); + $this->assertEquals($this->a, $xor->getLeft()); + $this->assertEquals($this->b, $xor->getRight()); + } + + public function testNot(): void + { + $not = $this->a->not(); + + $this->assertInstanceOf(Negation::class, $not); + } +} diff --git a/tests/Unit/Traits/NumeralTypeTraitTest.php b/tests/unit/Traits/TypeTraits/PropertyTypeTraits/NumeralTypeTraitTest.php similarity index 64% rename from tests/Unit/Traits/NumeralTypeTraitTest.php rename to tests/unit/Traits/TypeTraits/PropertyTypeTraits/NumeralTypeTraitTest.php index 5c07af80..d066bc39 100644 --- a/tests/Unit/Traits/NumeralTypeTraitTest.php +++ b/tests/unit/Traits/TypeTraits/PropertyTypeTraits/NumeralTypeTraitTest.php @@ -1,59 +1,38 @@ -a = new class () implements NumeralType { + $this->a = new class() implements NumeralType + { use NumeralTypeTrait; public function toQuery(): string @@ -61,7 +40,7 @@ public function toQuery(): string return '10'; } }; - $this->b = $this->getQueryConvertableMock(NumeralType::class, "15"); + $this->b = new Integer(15); } public function testPlus(): void @@ -75,6 +54,13 @@ public function testPlus(): void $this->assertEquals($this->b, $plus->getRight()); } + public function testPlusLiteral(): void + { + $plus = $this->a->plus(1); + + $this->assertInstanceOf(Addition::class, $plus); + } + public function testPlusNoParentheses(): void { $plus = $this->a->plus($this->b, false); @@ -97,6 +83,13 @@ public function testDivide(): void $this->assertEquals($this->b, $divide->getRight()); } + public function testDivideLiteral(): void + { + $divide = $this->a->divide(1); + + $this->assertInstanceOf(Division::class, $divide); + } + public function testDivideNoParentheses(): void { $divide = $this->a->divide($this->b, false); @@ -119,6 +112,13 @@ public function testExponentiate(): void $this->assertEquals($this->b, $exponentiate->getRight()); } + public function testExponentiateLiteral(): void + { + $exponentiate = $this->a->exponentiate(1); + + $this->assertInstanceOf(Exponentiation::class, $exponentiate); + } + public function testExponentiateNoParentheses(): void { $exponentiate = $this->a->exponentiate($this->b, false); @@ -134,18 +134,25 @@ public function testMod(): void { $mod = $this->a->mod($this->b); - $this->assertInstanceOf(Modulo::class, $mod); + $this->assertInstanceOf(ModuloDivision::class, $mod); $this->assertTrue($mod->insertsParentheses()); $this->assertEquals($this->a, $mod->getLeft()); $this->assertEquals($this->b, $mod->getRight()); } + public function testModLiteral(): void + { + $mod = $this->a->mod(1); + + $this->assertInstanceOf(ModuloDivision::class, $mod); + } + public function testModNoParentheses(): void { $mod = $this->a->mod($this->b, false); - $this->assertInstanceOf(Modulo::class, $mod); + $this->assertInstanceOf(ModuloDivision::class, $mod); $this->assertFalse($mod->insertsParentheses()); $this->assertEquals($this->a, $mod->getLeft()); @@ -163,6 +170,13 @@ public function testTimes(): void $this->assertEquals($this->b, $times->getRight()); } + public function testTimesLiteral(): void + { + $times = $this->a->times(1); + + $this->assertInstanceOf(Multiplication::class, $times); + } + public function testTimesNoParentheses(): void { $times = $this->a->times($this->b, false); @@ -185,6 +199,13 @@ public function testMinus(): void $this->assertEquals($this->b, $minus->getRight()); } + public function testMinusLiteral(): void + { + $minus = $this->a->minus(1); + + $this->assertInstanceOf(Subtraction::class, $minus); + } + public function testMinusNoParentheses(): void { $minus = $this->a->minus($this->b, false); @@ -200,6 +221,6 @@ public function testNegate(): void { $negate = $this->a->negate(); - $this->assertInstanceOf(Minus::class, $negate); + $this->assertInstanceOf(UnaryMinus::class, $negate); } } diff --git a/tests/unit/Traits/TypeTraits/PropertyTypeTraits/PropertyTypeTraitTest.php b/tests/unit/Traits/TypeTraits/PropertyTypeTraits/PropertyTypeTraitTest.php new file mode 100644 index 00000000..984df1b1 --- /dev/null +++ b/tests/unit/Traits/TypeTraits/PropertyTypeTraits/PropertyTypeTraitTest.php @@ -0,0 +1,69 @@ +a = new class() implements PropertyType + { + use PropertyTypeTrait; + + public function toQuery(): string + { + return '10'; + } + }; + $this->list = new List_([new String_('foobar')]); + } + + public function testIn(): void + { + $in = $this->a->in($this->list); + + $this->assertInstanceOf(In::class, $in); + + $this->assertTrue($in->insertsParentheses()); + $this->assertEquals($this->a, $in->getLeft()); + $this->assertEquals($this->list, $in->getRight()); + } + + public function testInLiteral(): void + { + $in = $this->a->in(['a', 'b', 'c']); + + $this->assertInstanceOf(In::class, $in); + } + + public function testInNoParentheses(): void + { + $in = $this->a->in($this->list, false); + + $this->assertInstanceOf(In::class, $in); + + $this->assertFalse($in->insertsParentheses()); + $this->assertEquals($this->a, $in->getLeft()); + $this->assertEquals($this->list, $in->getRight()); + } +} diff --git a/tests/Unit/Traits/StringTypeTraitTest.php b/tests/unit/Traits/TypeTraits/PropertyTypeTraits/StringTypeTraitTest.php similarity index 64% rename from tests/Unit/Traits/StringTypeTraitTest.php rename to tests/unit/Traits/TypeTraits/PropertyTypeTraits/StringTypeTraitTest.php index b3f32f5d..ef2cabad 100644 --- a/tests/Unit/Traits/StringTypeTraitTest.php +++ b/tests/unit/Traits/TypeTraits/PropertyTypeTraits/StringTypeTraitTest.php @@ -1,56 +1,35 @@ -a = new class () implements StringType { + $this->a = new class() implements StringType + { use StringTypeTrait; public function toQuery(): string @@ -58,7 +37,7 @@ public function toQuery(): string return '10'; } }; - $this->b = $this->getQueryConvertableMock(StringType::class, "15"); + $this->b = new String_('15'); } public function testContains(): void @@ -72,6 +51,13 @@ public function testContains(): void $this->assertEquals($this->b, $contains->getRight()); } + public function testContainsLiteral(): void + { + $contains = $this->a->contains('test'); + + $this->assertInstanceOf(Contains::class, $contains); + } + public function testContainsNoParentheses(): void { $contains = $this->a->contains($this->b, false); @@ -94,6 +80,13 @@ public function testEndsWith(): void $this->assertEquals($this->b, $endsWith->getRight()); } + public function testEndsWithLiteral(): void + { + $endsWith = $this->a->endsWith('test'); + + $this->assertInstanceOf(EndsWith::class, $endsWith); + } + public function testEndsWithNoParentheses(): void { $endsWith = $this->a->endsWith($this->b, false); @@ -116,6 +109,13 @@ public function testStartsWith(): void $this->assertEquals($this->b, $startsWith->getRight()); } + public function testStartsWithLiteral(): void + { + $startsWith = $this->a->startsWith('test'); + + $this->assertInstanceOf(StartsWith::class, $startsWith); + } + public function testStartsWithNoParentheses(): void { $startsWith = $this->a->startsWith($this->b, false); @@ -138,6 +138,13 @@ public function testRegex(): void $this->assertEquals($this->b, $regex->getRight()); } + public function testRegexLiteral(): void + { + $regex = $this->a->regex('/test/'); + + $this->assertInstanceOf(Regex::class, $regex); + } + public function testRegexNoParentheses(): void { $regex = $this->a->regex($this->b, false);