├── .phpunit-watcher.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── docker-compose.yml ├── infection.json.dist ├── psalm.xml ├── rector.php ├── src ├── Builder │ ├── ArrayExpressionBuilder.php │ ├── ArrayOverlapsConditionBuilder.php │ ├── JsonExpressionBuilder.php │ ├── JsonOverlapsConditionBuilder.php │ ├── LikeConditionBuilder.php │ └── StructuredExpressionBuilder.php ├── Column │ ├── ArrayColumn.php │ ├── ArrayLazyColumn.php │ ├── BigIntColumn.php │ ├── BinaryColumn.php │ ├── BitColumn.php │ ├── BooleanColumn.php │ ├── ColumnBuilder.php │ ├── ColumnDefinitionBuilder.php │ ├── ColumnDefinitionParser.php │ ├── ColumnFactory.php │ ├── IntegerColumn.php │ ├── SequenceColumnInterface.php │ ├── SequenceColumnTrait.php │ ├── StructuredColumn.php │ └── StructuredLazyColumn.php ├── Command.php ├── Connection.php ├── DDLQueryBuilder.php ├── DMLQueryBuilder.php ├── DQLQueryBuilder.php ├── Data │ ├── ArrayParser.php │ ├── LazyArray.php │ ├── StructuredLazyArray.php │ └── StructuredParser.php ├── Driver.php ├── Dsn.php ├── IndexMethod.php ├── QueryBuilder.php ├── Schema.php ├── SqlParser.php ├── TableSchema.php └── Transaction.php └── tests ├── .env ├── ArrayExpressionBuilderTest.php ├── ArrayParserTest.php ├── BatchQueryResultTest.php ├── ColumnBuilderTest.php ├── ColumnDefinitionParserTest.php ├── ColumnFactoryTest.php ├── ColumnTest.php ├── CommandTest.php ├── ConnectionTest.php ├── DsnTest.php ├── JsonExpressionBuilderTest.php ├── PDODriverTest.php ├── PdoCommandTest.php ├── PdoConnectionTest.php ├── Provider ├── ColumnBuilderProvider.php ├── ColumnDefinitionParserProvider.php ├── ColumnFactoryProvider.php ├── ColumnProvider.php ├── CommandPDOProvider.php ├── CommandProvider.php ├── QueryBuilderProvider.php ├── QuoterProvider.php ├── SchemaProvider.php ├── SqlParserProvider.php └── StructuredTypeProvider.php ├── QueryBuilderTest.php ├── QueryTest.php ├── QuoterTest.php ├── SchemaTest.php ├── SqlParserTest.php ├── StructuredExpressionBuilderTest.php ├── StructuredParserTest.php ├── Support ├── Fixture │ ├── pgsql.sql │ ├── pgsql10.sql │ ├── pgsql11.sql │ └── pgsql12.sql └── TestTrait.php └── bootstrap.php /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | risky: true 3 | 4 | version: 8.1 5 | 6 | finder: 7 | exclude: 8 | - docs 9 | - vendor 10 | 11 | enabled: 12 | - alpha_ordered_traits 13 | - array_indentation 14 | - array_push 15 | - combine_consecutive_issets 16 | - combine_consecutive_unsets 17 | - combine_nested_dirname 18 | - declare_strict_types 19 | - dir_constant 20 | - fully_qualified_strict_types 21 | - function_to_constant 22 | - hash_to_slash_comment 23 | - is_null 24 | - logical_operators 25 | - magic_constant_casing 26 | - magic_method_casing 27 | - method_separation 28 | - modernize_types_casting 29 | - native_function_casing 30 | - native_function_type_declaration_casing 31 | - no_alias_functions 32 | - no_empty_comment 33 | - no_empty_phpdoc 34 | - no_empty_statement 35 | - no_extra_block_blank_lines 36 | - no_short_bool_cast 37 | - no_superfluous_elseif 38 | - no_unneeded_control_parentheses 39 | - no_unneeded_curly_braces 40 | - no_unneeded_final_method 41 | - no_unset_cast 42 | - no_unused_imports 43 | - no_unused_lambda_imports 44 | - no_useless_else 45 | - no_useless_return 46 | - normalize_index_brace 47 | - php_unit_dedicate_assert 48 | - php_unit_dedicate_assert_internal_type 49 | - php_unit_expectation 50 | - php_unit_mock 51 | - php_unit_mock_short_will_return 52 | - php_unit_namespaced 53 | - php_unit_no_expectation_annotation 54 | - phpdoc_no_empty_return 55 | - phpdoc_no_useless_inheritdoc 56 | - phpdoc_order 57 | - phpdoc_property 58 | - phpdoc_scalar 59 | - phpdoc_singular_inheritdoc 60 | - phpdoc_trim 61 | - phpdoc_trim_consecutive_blank_line_separation 62 | - phpdoc_type_to_var 63 | - phpdoc_types 64 | - phpdoc_types_order 65 | - print_to_echo 66 | - regular_callable_call 67 | - return_assignment 68 | - self_accessor 69 | - self_static_accessor 70 | - set_type_to_cast 71 | - short_array_syntax 72 | - short_list_syntax 73 | - simplified_if_return 74 | - single_quote 75 | - standardize_not_equals 76 | - ternary_to_null_coalescing 77 | - trailing_comma_in_multiline_array 78 | - unalign_double_arrow 79 | - unalign_equals 80 | - empty_loop_body_braces 81 | - integer_literal_case 82 | - union_type_without_spaces 83 | 84 | disabled: 85 | - function_declaration 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL driver for Yii Database Change Log 2 | 3 | ## 2.0.0 under development 4 | 5 | - Enh #336, #405: Implement and use `SqlParser` class (@Tigrov) 6 | - New #315: Implement `ColumnSchemaInterface` classes according to the data type of database table columns 7 | for type casting performance. Related with yiisoft/db#752 (@Tigrov) 8 | - Chg #348: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) 9 | - Enh #349: Add method chaining for column classes (@Tigrov) 10 | - New #350: Add array overlaps and JSON overlaps condition builders (@Tigrov) 11 | - Enh #353: Update `bit` type according to main PR yiisoft/db#860 (@Tigrov) 12 | - Enh #354: Refactor PHP type of `ColumnSchemaInterface` instances (@Tigrov) 13 | - Enh #356: Raise minimum PHP version to `^8.1` with minor refactoring (@Tigrov) 14 | - New #355, #368, #370, #399: Implement `ColumnFactory` class (@Tigrov) 15 | - Enh #359: Separate column type constants (@Tigrov) 16 | - Enh #359: Remove `Schema::TYPE_ARRAY` and `Schema::TYPE_STRUCTURED` constants (@Tigrov) 17 | - New #360: Realize `ColumnBuilder` class (@Tigrov) 18 | - Enh #362: Update according changes in `ColumnSchemaInterface` (@Tigrov) 19 | - New #364, #372: Add `ColumnDefinitionBuilder` class (@Tigrov) 20 | - Enh #365: Refactor `Dsn` class (@Tigrov) 21 | - Enh #366: Use constructor to create columns and initialize properties (@Tigrov) 22 | - Enh #370: Refactor `Schema::normalizeDefaultValue()` method and move it to `ColumnFactory` class (@Tigrov) 23 | - New #373: Override `QueryBuilder::prepareBinary()` method (@Tigrov) 24 | - Chg #375: Update `QueryBuilder` constructor (@Tigrov) 25 | - Enh #374: Use `ColumnDefinitionBuilder` to generate table column SQL representation (@Tigrov) 26 | - Enh #378: Improve loading schemas of views (@Tigrov) 27 | - Enh #379: Remove `ColumnInterface` (@Tigrov) 28 | - Enh #380: Rename `ColumnSchemaInterface` to `ColumnInterface` (@Tigrov) 29 | - Enh #381, #383: Add `ColumnDefinitionParser` class (@Tigrov) 30 | - Enh #382: Replace `DbArrayHelper::getColumn()` with `array_column()` (@Tigrov) 31 | - New #384: Add `IndexMethod` class (@Tigrov) 32 | - Bug #387: Explicitly mark nullable parameters (@vjik) 33 | - Enh #386: Refactor array, structured and JSON expression builders (@Tigrov) 34 | - Chg #388: Change supported PHP versions to `8.1 - 8.4` (@Tigrov) 35 | - Enh #388: Minor refactoring (@Tigrov) 36 | - Chg #390: Remove `yiisoft/json` dependency (@Tigrov) 37 | - Enh #393: Refactor according changes in `db` package (@Tigrov) 38 | - New #391: Add `caseSensitive` option to like condition (@vjik) 39 | - Enh #396: Remove `getCacheKey()` and `getCacheTag()` methods from `Schema` class (@Tigrov) 40 | - Enh #403, #404: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov) 41 | - New #397: Realize `Schema::loadResultColumn()` method (@Tigrov) 42 | - New #407: Use `DateTimeColumn` class for datetime column types (@Tigrov) 43 | - New #408: Implement `DMLQueryBuilder::upsertWithReturningPks()` method (@Tigrov) 44 | 45 | ## 1.3.0 March 21, 2024 46 | 47 | - Enh #303, #338: Support structured type (@Tigrov) 48 | - Enh #324: Change property `Schema::$typeMap` to constant `Schema::TYPE_MAP` (@Tigrov) 49 | - Enh #330: Create instance of `ArrayParser` directly (@Tigrov) 50 | - Enh #333: Resolve deprecated methods (@Tigrov) 51 | - Enh #334: Minor `DDLQueryBuilder` refactoring (@Tigrov) 52 | - Bug #316, #6: Support table view constraints (@Tigrov) 53 | - Bug #331: Exclude from index column names fields specified in `INCLUDE` clause (@Tigrov) 54 | 55 | ## 1.2.0 November 12, 2023 56 | 57 | - Chg #319: Remove use of abstract type `SchemaInterface::TYPE_JSONB` (@Tigrov) 58 | - Enh #300: Refactor `ArrayExpressionBuilder` (@Tigrov) 59 | - Enh #301: Refactor `JsonExpressionBuilder` (@Tigrov) 60 | - Enh #302: Refactor `ColumnSchema` (@Tigrov) 61 | - Enh #321: Move methods from `Command` to `AbstractPdoCommand` class (@Tigrov) 62 | - Bug #302: Fix incorrect convert string value for BIT type (@Tigrov) 63 | - Bug #309: Fix retrieving sequence name from default value (@Tigrov) 64 | - Bug #313: Refactor `DMLQueryBuilder`, related with yiisoft/db#746 (@Tigrov) 65 | 66 | ## 1.1.0 July 24, 2023 67 | 68 | - Chg #288: Typecast refactoring (@Tigrov) 69 | - Chg #291: Update phpTypecast for bool type (@Tigrov) 70 | - Enh #282: Support `numeric` arrays, improve support of domain types and `int` and `varchar` array types (@Tigrov) 71 | - Enh #284: Add tests for `binary` type and fix casting of default value (@Tigrov) 72 | - Enh #289: Array parser refactoring (@Tigrov) 73 | - Enh #294: Refactoring of `Schema::normalizeDefaultValue()` method (@Tigrov) 74 | - Bug #287: Fix `bit` type (@Tigrov) 75 | - Bug #295: Fix multiline and single quote in default string value, add support for PostgreSQL 9.4 parentheses around negative numeric default values (@Tigrov) 76 | - Bug #296: Prevent possible issues with array default values `('{one,two}'::text[])::varchar[]`, remove `ArrayParser::parseString()` (@Tigrov) 77 | 78 | ## 1.0.0 April 12, 2023 79 | 80 | - Initial release. 81 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software () 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Yii 4 | 5 | 6 | PostgreSQL 7 | 8 |

Yii Database PostgreSQL driver

9 |
10 |

11 | 12 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/db-pgsql/v)](https://packagist.org/packages/yiisoft/db-pgsql) 13 | [![Total Downloads](https://poser.pugx.org/yiisoft/db-pgsql/downloads)](https://packagist.org/packages/yiisoft/db-pgsql) 14 | [![rector](https://github.com/yiisoft/db-pgsql/actions/workflows/rector.yml/badge.svg)](https://github.com/yiisoft/db-pgsql/actions/workflows/rector.yml) 15 | [![codecov](https://codecov.io/gh/yiisoft/db-pgsql/branch/master/graph/badge.svg?token=3FGN91IVZA)](https://codecov.io/gh/yiisoft/db-pgsql) 16 | [![StyleCI](https://github.styleci.io/repos/145220173/shield?branch=master)](https://github.styleci.io/repos/145220173?branch=master) 17 | 18 | PostgreSQL driver for [Yii Database](https://github.com/yiisoft/db) is a [PostgreSQL] database adapter. 19 | The package provides a database connection interface and a set of classes for interacting with a [PostgreSQL] database. 20 | It allows you to perform common database operations such as executing queries, building and executing `INSERT`, `UPDATE`, 21 | and `DELETE` statements, and working with transactions. It also provides support for PostgreSQL-specific features such 22 | as stored procedures and server-side cursors. 23 | 24 | [PostgreSQL]: https://www.postgresql.org/ 25 | 26 | ## Support version 27 | 28 | | PHP | PostgreSQL Version | CI-Actions 29 | |---------------|--------------------|----------- 30 | | **8.1 - 8.4** | **9 - 17** |[![build](https://github.com/yiisoft/db-pgsql/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/yiisoft/db-pgsql/actions/workflows/build.yml) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fdb-pgsql%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/db-pgsql/master) [![static analysis](https://github.com/yiisoft/db-pgsql/actions/workflows/static.yml/badge.svg?branch=dev)](https://github.com/yiisoft/db-pgsql/actions/workflows/static.yml) [![type-coverage](https://shepherd.dev/github/yiisoft/db-pgsql/coverage.svg)](https://shepherd.dev/github/yiisoft/db-pgsql) 31 | 32 | ## Installation 33 | 34 | The package could be installed with [Composer](https://getcomposer.org): 35 | 36 | ```shell 37 | composer require yiisoft/db-pgsql 38 | ``` 39 | 40 | ## Documentation 41 | 42 | For config connection to PostgreSQL database check [Connecting PostgreSQL](https://github.com/yiisoft/db/blob/master/docs/guide/en/connection/pgsql.md). 43 | 44 | [Check the documentation](https://github.com/yiisoft/db/blob/master/docs/guide/en/README.md) to learn about usage. 45 | 46 | - [Internals](docs/internals.md) 47 | 48 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that. 49 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 50 | 51 | ## License 52 | 53 | The Yii Database PostgreSQL driver is free software. It is released under the terms of the BSD License. 54 | Please see [`LICENSE`](./LICENSE.md) for more information. 55 | 56 | Maintained by [Yii Software](https://www.yiiframework.com/). 57 | 58 | ## Support the project 59 | 60 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 61 | 62 | ## Follow updates 63 | 64 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 65 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 66 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 67 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 68 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/db-pgsql", 3 | "type": "library", 4 | "description": "PostgreSQL driver for Yii Database", 5 | "keywords": [ 6 | "yii", 7 | "pgsql", 8 | "database", 9 | "sql", 10 | "dbal", 11 | "query-builder" 12 | ], 13 | "homepage": "https://www.yiiframework.com/", 14 | "license": "BSD-3-Clause", 15 | "support": { 16 | "issues": "https://github.com/yiisoft/db-pgsql/issues?state=open", 17 | "source": "https://github.com/yiisoft/db-pgsql", 18 | "forum": "https://www.yiiframework.com/forum/", 19 | "wiki": "https://www.yiiframework.com/wiki/", 20 | "irc": "ircs://irc.libera.chat:6697/yii", 21 | "chat": "https://t.me/yii3en" 22 | }, 23 | "funding": [ 24 | { 25 | "type": "opencollective", 26 | "url": "https://opencollective.com/yiisoft" 27 | }, 28 | { 29 | "type": "github", 30 | "url": "https://github.com/sponsors/yiisoft" 31 | } 32 | ], 33 | "require": { 34 | "php": "8.1 - 8.4", 35 | "ext-pdo": "*", 36 | "ext-pdo_pgsql": "*", 37 | "yiisoft/db": "dev-master" 38 | }, 39 | "require-dev": { 40 | "maglnet/composer-require-checker": "^4.7.1", 41 | "phpunit/phpunit": "^10.5.45", 42 | "rector/rector": "^2.0.10", 43 | "roave/infection-static-analysis-plugin": "^1.35", 44 | "spatie/phpunit-watcher": "^1.24", 45 | "vimeo/psalm": "^5.26.1 || ^6.8.8", 46 | "vlucas/phpdotenv": "^5.6.1", 47 | "yiisoft/aliases": "^2.0", 48 | "yiisoft/cache-file": "^3.2", 49 | "yiisoft/var-dumper": "^1.7" 50 | }, 51 | "autoload": { 52 | "psr-4": { 53 | "Yiisoft\\Db\\Pgsql\\": "src" 54 | } 55 | }, 56 | "autoload-dev": { 57 | "psr-4": { 58 | "Yiisoft\\Db\\Pgsql\\Tests\\": "tests", 59 | "Yiisoft\\Db\\Tests\\": "vendor/yiisoft/db/tests" 60 | }, 61 | "files": ["tests/bootstrap.php"] 62 | }, 63 | "config": { 64 | "sort-packages": true, 65 | "allow-plugins": { 66 | "infection/extension-installer": true, 67 | "composer/package-versions-deprecated": true 68 | } 69 | }, 70 | "scripts": { 71 | "test": "phpunit --testdox --no-interaction", 72 | "test-watch": "phpunit-watcher watch" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pgsql: 3 | image: postgres:17 4 | environment: 5 | POSTGRES_USER: root 6 | POSTGRES_PASSWORD: root 7 | POSTGRES_DB: yiitest 8 | ports: 9 | # : 10 | - 5432:5432 11 | volumes: 12 | - type: tmpfs 13 | target: /var/lib/postgresql/data 14 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "timeout": 45, 8 | "logs": { 9 | "text": "php:\/\/stderr", 10 | "stryker": { 11 | "report": "master" 12 | } 13 | }, 14 | "mutators": { 15 | "@default": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 13 | __DIR__ . '/src', 14 | /** 15 | * Disabled ./tests directory due to different branches with main package when testing 16 | */ 17 | // __DIR__ . '/tests', 18 | ]); 19 | 20 | // register a single rule 21 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 22 | 23 | // define sets of rules 24 | $rectorConfig->sets([ 25 | LevelSetList::UP_TO_PHP_81, 26 | ]); 27 | 28 | $rectorConfig->skip([ 29 | NullToStrictStringFuncCallArgRector::class, 30 | ReadOnlyPropertyRector::class, 31 | ]); 32 | }; 33 | -------------------------------------------------------------------------------- /src/Builder/ArrayExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | getColumn($expression); 35 | $dbType = $this->getColumnDbType($column); 36 | 37 | $typeHint = $this->getTypeHint($dbType, $column?->getDimension() ?? 1); 38 | 39 | return $this->queryBuilder->bindParam($param, $params) . $typeHint; 40 | } 41 | 42 | protected function buildSubquery(QueryInterface $query, ArrayExpression $expression, array &$params): string 43 | { 44 | $column = $this->getColumn($expression); 45 | $dbType = $this->getColumnDbType($column); 46 | 47 | return $this->buildNestedSubquery($query, $dbType, $column?->getDimension() ?? 1, $params); 48 | } 49 | 50 | protected function buildValue(iterable $value, ArrayExpression $expression, array &$params): string 51 | { 52 | $column = $this->getColumn($expression); 53 | $dbType = $this->getColumnDbType($column); 54 | 55 | return $this->buildNestedValue($value, $dbType, $column?->getColumn(), $column?->getDimension() ?? 1, $params); 56 | } 57 | 58 | protected function getLazyArrayValue(LazyArrayInterface $value): array|string 59 | { 60 | if ($value instanceof LazyArray) { 61 | return $value->getRawValue(); 62 | } 63 | 64 | return $value->getValue(); 65 | } 66 | 67 | /** 68 | * @param string[] $placeholders 69 | */ 70 | private function buildNestedArray(array $placeholders, string $dbType, int $dimension): string 71 | { 72 | $typeHint = $this->getTypeHint($dbType, $dimension); 73 | 74 | return 'ARRAY[' . implode(',', $placeholders) . ']' . $typeHint; 75 | } 76 | 77 | private function buildNestedSubquery(QueryInterface $query, string $dbType, int $dimension, array &$params): string 78 | { 79 | [$sql, $params] = $this->queryBuilder->build($query, $params); 80 | 81 | return "ARRAY($sql)" . $this->getTypeHint($dbType, $dimension); 82 | } 83 | 84 | private function buildNestedValue(iterable $value, string $dbType, ColumnInterface|null $column, int $dimension, array &$params): string 85 | { 86 | $placeholders = []; 87 | 88 | if ($dimension > 1) { 89 | /** @var iterable|null $item */ 90 | foreach ($value as $item) { 91 | if ($item === null) { 92 | $placeholders[] = 'NULL'; 93 | } elseif ($item instanceof ExpressionInterface) { 94 | $placeholders[] = $item instanceof QueryInterface 95 | ? $this->buildNestedSubquery($item, $dbType, $dimension - 1, $params) 96 | : $this->queryBuilder->buildExpression($item, $params); 97 | } else { 98 | $placeholders[] = $this->buildNestedValue($item, $dbType, $column, $dimension - 1, $params); 99 | } 100 | } 101 | } else { 102 | $value = $this->dbTypecast($value, $column); 103 | 104 | foreach ($value as $item) { 105 | if ($item instanceof ExpressionInterface) { 106 | $placeholders[] = $this->queryBuilder->buildExpression($item, $params); 107 | } else { 108 | $placeholders[] = $this->queryBuilder->bindParam($item, $params); 109 | } 110 | } 111 | } 112 | 113 | return $this->buildNestedArray($placeholders, $dbType, $dimension); 114 | } 115 | 116 | private function getColumn(ArrayExpression $expression): AbstractArrayColumn|null 117 | { 118 | $type = $expression->getType(); 119 | 120 | if ($type === null || $type instanceof AbstractArrayColumn) { 121 | return $type; 122 | } 123 | 124 | $info = []; 125 | 126 | if ($type instanceof ColumnInterface) { 127 | $info['column'] = $type; 128 | } elseif ($type !== ColumnType::ARRAY) { 129 | $column = $this 130 | ->queryBuilder 131 | ->getColumnFactory() 132 | ->fromDefinition($type); 133 | 134 | if ($column instanceof AbstractArrayColumn) { 135 | return $column; 136 | } 137 | 138 | $info['column'] = $column; 139 | } 140 | 141 | /** @var AbstractArrayColumn */ 142 | return $this 143 | ->queryBuilder 144 | ->getColumnFactory() 145 | ->fromType(ColumnType::ARRAY, $info); 146 | } 147 | 148 | private function getColumnDbType(AbstractArrayColumn|null $column): string 149 | { 150 | if ($column === null) { 151 | return ''; 152 | } 153 | 154 | return rtrim($this->queryBuilder->getColumnDefinitionBuilder()->buildType($column), '[]'); 155 | } 156 | 157 | /** 158 | * Return the type hint expression based on type and dimension. 159 | */ 160 | private function getTypeHint(string $dbType, int $dimension): string 161 | { 162 | if (empty($dbType)) { 163 | return ''; 164 | } 165 | 166 | return '::' . $dbType . str_repeat('[]', $dimension); 167 | } 168 | 169 | /** 170 | * Converts array values for use in a db query. 171 | * 172 | * @param iterable $value The array or iterable object. 173 | * @param ColumnInterface|null $column The column instance to typecast values. 174 | * 175 | * @return iterable Converted values. 176 | */ 177 | private function dbTypecast(iterable $value, ColumnInterface|null $column): iterable 178 | { 179 | if ($column === null) { 180 | return $value; 181 | } 182 | 183 | if (!is_array($value)) { 184 | $value = iterator_to_array($value, false); 185 | } 186 | 187 | return array_map($column->dbTypecast(...), $value); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Builder/ArrayOverlapsConditionBuilder.php: -------------------------------------------------------------------------------- 1 | prepareColumn($expression->getColumn()); 35 | $values = $expression->getValues(); 36 | 37 | if ($values instanceof JsonExpression) { 38 | /** @psalm-suppress MixedArgument */ 39 | $values = new ArrayExpression($values->getValue()); 40 | } elseif (!$values instanceof ExpressionInterface) { 41 | $values = new ArrayExpression($values); 42 | } 43 | 44 | $values = $this->queryBuilder->buildExpression($values, $params); 45 | 46 | return "$column::text[] && $values::text[]"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Builder/JsonExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | baseExpressionBuilder = new BaseJsonExpressionBuilder($queryBuilder); 29 | } 30 | 31 | /** 32 | * The Method builds the raw SQL from the $expression that won't be additionally escaped or quoted. 33 | * 34 | * @param JsonExpression $expression The expression to build. 35 | * @param array $params The binding parameters. 36 | * 37 | * @throws Exception 38 | * @throws InvalidArgumentException 39 | * @throws InvalidConfigException 40 | * @throws JsonException 41 | * @throws NotSupportedException 42 | * 43 | * @return string The raw SQL that won't be additionally escaped or quoted. 44 | */ 45 | public function build(ExpressionInterface $expression, array &$params = []): string 46 | { 47 | $statement = $this->baseExpressionBuilder->build($expression, $params); 48 | 49 | if ($expression->getValue() instanceof ArrayExpression) { 50 | $statement = 'array_to_json(' . $statement . ')'; 51 | } 52 | 53 | return $statement . $this->getTypeHint($expression); 54 | } 55 | 56 | /** 57 | * @return string The typecast expression based on {@see JsonExpression::getType()}. 58 | */ 59 | private function getTypeHint(JsonExpression $expression): string 60 | { 61 | $type = $expression->getType(); 62 | 63 | if ($type === null) { 64 | return ''; 65 | } 66 | 67 | return '::' . $type; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Builder/JsonOverlapsConditionBuilder.php: -------------------------------------------------------------------------------- 1 | prepareColumn($expression->getColumn()); 35 | $values = $expression->getValues(); 36 | 37 | if ($values instanceof JsonExpression) { 38 | /** @psalm-suppress MixedArgument */ 39 | $values = new ArrayExpression($values->getValue()); 40 | } elseif (!$values instanceof ExpressionInterface) { 41 | $values = new ArrayExpression($values); 42 | } 43 | 44 | $values = $this->queryBuilder->buildExpression($values, $params); 45 | 46 | return "ARRAY(SELECT jsonb_array_elements_text($column::jsonb)) && $values::text[]"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Builder/LikeConditionBuilder.php: -------------------------------------------------------------------------------- 1 | getCaseSensitive()) { 19 | true => 'LIKE', 20 | false => 'ILIKE', 21 | default => $operator, 22 | }; 23 | 24 | return [$andor, $not, $operator]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Builder/StructuredExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | queryBuilder->bindParam($param, $params) . $this->getTypeHint($expression); 33 | } 34 | 35 | protected function buildSubquery(QueryInterface $query, StructuredExpression $expression, array &$params): string 36 | { 37 | [$sql, $params] = $this->queryBuilder->build($query, $params); 38 | 39 | return "($sql)" . $this->getTypeHint($expression); 40 | } 41 | 42 | protected function buildValue(array|object $value, StructuredExpression $expression, array &$params): string 43 | { 44 | $value = $this->prepareValues($value, $expression); 45 | /** @psalm-var string[] $placeholders */ 46 | $placeholders = $this->buildPlaceholders($value, $expression, $params); 47 | 48 | return 'ROW(' . implode(',', $placeholders) . ')' . $this->getTypeHint($expression); 49 | } 50 | 51 | protected function getLazyArrayValue(LazyArrayInterface $value): array|string 52 | { 53 | if ($value instanceof StructuredLazyArray) { 54 | return $value->getRawValue(); 55 | } 56 | 57 | return $value->getValue(); 58 | } 59 | 60 | /** 61 | * Builds a placeholder array out of $expression value. 62 | * 63 | * @param array $value The expression value. 64 | * @param StructuredExpression $expression The structured expression. 65 | * @param array $params The binding parameters. 66 | * 67 | * @throws Exception 68 | * @throws InvalidArgumentException 69 | * @throws InvalidConfigException 70 | * @throws NotSupportedException 71 | */ 72 | private function buildPlaceholders(array $value, StructuredExpression $expression, array &$params): array 73 | { 74 | $type = $expression->getType(); 75 | $columns = $type instanceof AbstractStructuredColumn ? $type->getColumns() : []; 76 | 77 | $placeholders = []; 78 | 79 | /** @psalm-var int|string $columnName */ 80 | foreach ($value as $columnName => $item) { 81 | if (isset($columns[$columnName])) { 82 | $item = $columns[$columnName]->dbTypecast($item); 83 | } 84 | 85 | if ($item instanceof ExpressionInterface) { 86 | $placeholders[] = $this->queryBuilder->buildExpression($item, $params); 87 | } else { 88 | $placeholders[] = $this->queryBuilder->bindParam($item, $params); 89 | } 90 | } 91 | 92 | return $placeholders; 93 | } 94 | 95 | /** 96 | * Returns the type hint expression based on type. 97 | */ 98 | private function getTypeHint(StructuredExpression $expression): string 99 | { 100 | $type = $expression->getType(); 101 | 102 | if ($type instanceof AbstractStructuredColumn) { 103 | $type = $type->getDbType(); 104 | } 105 | 106 | if (empty($type)) { 107 | return ''; 108 | } 109 | 110 | return '::' . $type; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Column/ArrayColumn.php: -------------------------------------------------------------------------------- 1 | getColumn(), $this->dimension))->getValue(); 22 | } 23 | 24 | return $value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Column/ArrayLazyColumn.php: -------------------------------------------------------------------------------- 1 | getColumn(), $this->dimension); 22 | } 23 | 24 | return $value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Column/BigIntColumn.php: -------------------------------------------------------------------------------- 1 | str_pad(decbin((int) $value), (int) $this->getSize(), '0', STR_PAD_LEFT), 22 | 'NULL' => null, 23 | 'boolean' => $value ? '1' : '0', 24 | 'string' => $value === '' ? null : $value, 25 | default => $value instanceof ExpressionInterface ? $value : (string) $value, 26 | }; 27 | } 28 | 29 | public function phpTypecast(mixed $value): int|null 30 | { 31 | /** @var int|string|null $value */ 32 | if (is_string($value)) { 33 | /** @var int */ 34 | return bindec($value); 35 | } 36 | 37 | return $value; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Column/BooleanColumn.php: -------------------------------------------------------------------------------- 1 | buildType($column) 43 | . $this->buildNotNull($column) 44 | . $this->buildPrimaryKey($column) 45 | . $this->buildUnique($column) 46 | . $this->buildDefault($column) 47 | . $this->buildCheck($column) 48 | . $this->buildReferences($column) 49 | . $this->buildExtra($column); 50 | } 51 | 52 | public function buildAlter(ColumnInterface $column): string 53 | { 54 | return $this->buildType($column) 55 | . $this->buildExtra($column); 56 | } 57 | 58 | public function buildType(ColumnInterface $column): string 59 | { 60 | if ($column instanceof AbstractArrayColumn) { 61 | if (!empty($column->getDbType())) { 62 | $dbType = parent::buildType($column); 63 | 64 | if ($dbType[-1] === ']') { 65 | return $dbType; 66 | } 67 | } else { 68 | $dbType = parent::buildType($column->getColumn() ?? $column); 69 | } 70 | 71 | return $dbType . str_repeat('[]', $column->getDimension()); 72 | } 73 | 74 | return parent::buildType($column); 75 | } 76 | 77 | protected function getDbType(ColumnInterface $column): string 78 | { 79 | $dbType = $column->getDbType(); 80 | 81 | /** @psalm-suppress DocblockTypeContradiction */ 82 | return match ($dbType) { 83 | default => $dbType, 84 | null => match ($column->getType()) { 85 | ColumnType::BOOLEAN => 'boolean', 86 | ColumnType::BIT => 'varbit', 87 | ColumnType::TINYINT => $column->isAutoIncrement() ? 'smallserial' : 'smallint', 88 | ColumnType::SMALLINT => $column->isAutoIncrement() ? 'smallserial' : 'smallint', 89 | ColumnType::INTEGER => $column->isAutoIncrement() ? 'serial' : 'integer', 90 | ColumnType::BIGINT => $column->isAutoIncrement() ? 'bigserial' : 'bigint', 91 | ColumnType::FLOAT => 'real', 92 | ColumnType::DOUBLE => 'double precision', 93 | ColumnType::DECIMAL => 'numeric', 94 | ColumnType::MONEY => 'money', 95 | ColumnType::CHAR => 'char', 96 | ColumnType::STRING => 'varchar(' . ($column->getSize() ?? 255) . ')', 97 | ColumnType::TEXT => 'text', 98 | ColumnType::BINARY => 'bytea', 99 | ColumnType::UUID => 'uuid', 100 | ColumnType::TIMESTAMP => 'timestamp', 101 | ColumnType::DATETIME => 'timestamp', 102 | ColumnType::DATETIMETZ => 'timestamptz', 103 | ColumnType::TIME => 'time', 104 | ColumnType::TIMETZ => 'timetz', 105 | ColumnType::DATE => 'date', 106 | ColumnType::STRUCTURED => 'jsonb', 107 | ColumnType::JSON => 'jsonb', 108 | default => 'varchar', 109 | }, 110 | 'timestamp without time zone' => 'timestamp', 111 | 'timestamp with time zone' => 'timestamptz', 112 | 'time without time zone' => 'time', 113 | 'time with time zone' => 'timetz', 114 | }; 115 | } 116 | 117 | protected function getDefaultUuidExpression(): string 118 | { 119 | $serverVersion = $this->queryBuilder->getServerInfo()->getVersion(); 120 | 121 | if (version_compare($serverVersion, '13', '<')) { 122 | return "uuid_in(overlay(overlay(md5(now()::text || random()::text) placing '4' from 13) placing" 123 | . ' to_hex(floor(4 * random() + 8)::int)::text from 17)::cstring)'; 124 | } 125 | 126 | return 'gen_random_uuid()'; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Column/ColumnDefinitionParser.php: -------------------------------------------------------------------------------- 1 | $type]; 35 | 36 | $typeDetails = $matches[4] ?? $matches[2] ?? ''; 37 | 38 | if ($typeDetails !== '') { 39 | if ($type === 'enum') { 40 | $info += $this->enumInfo($typeDetails); 41 | } else { 42 | $info += $this->sizeInfo($typeDetails); 43 | } 44 | } 45 | 46 | if (isset($matches[5])) { 47 | /** @psalm-var positive-int */ 48 | $info['dimension'] = substr_count($matches[5], '['); 49 | } 50 | 51 | $extra = substr($definition, strlen($matches[0])); 52 | 53 | return $info + $this->extraInfo($extra); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Column/ColumnFactory.php: -------------------------------------------------------------------------------- 1 | , 25 | * comment?: string|null, 26 | * computed?: bool|string, 27 | * dbTimezone?: string, 28 | * db_type?: string|null, 29 | * default_value?: mixed, 30 | * dimension?: int|string, 31 | * enum_values?: array|null, 32 | * extra?: string|null, 33 | * fromResult?: bool, 34 | * primary_key?: bool|string, 35 | * name?: string, 36 | * not_null?: bool|string|null, 37 | * reference?: ForeignKeyConstraint|null, 38 | * sequence_name?: string|null, 39 | * scale?: int|string|null, 40 | * schema?: string|null, 41 | * size?: int|string|null, 42 | * table?: string|null, 43 | * type?: string, 44 | * unique?: bool|string, 45 | * } 46 | */ 47 | final class ColumnFactory extends AbstractColumnFactory 48 | { 49 | /** 50 | * The mapping from physical column types (keys) to abstract column types (values). 51 | * 52 | * @link https://www.postgresql.org/docs/current/datatype.html#DATATYPE-TABLE 53 | * 54 | * @var string[] 55 | * @psalm-var array 56 | */ 57 | protected const TYPE_MAP = [ 58 | 'bool' => ColumnType::BOOLEAN, 59 | 'boolean' => ColumnType::BOOLEAN, 60 | 'bit' => ColumnType::BIT, 61 | 'bit varying' => ColumnType::BIT, 62 | 'varbit' => ColumnType::BIT, 63 | 'smallint' => ColumnType::SMALLINT, 64 | 'int2' => ColumnType::SMALLINT, 65 | 'smallserial' => ColumnType::SMALLINT, 66 | 'serial2' => ColumnType::SMALLINT, 67 | 'int4' => ColumnType::INTEGER, 68 | 'int' => ColumnType::INTEGER, 69 | 'integer' => ColumnType::INTEGER, 70 | 'serial' => ColumnType::INTEGER, 71 | 'serial4' => ColumnType::INTEGER, 72 | 'bigint' => ColumnType::BIGINT, 73 | 'int8' => ColumnType::BIGINT, 74 | 'bigserial' => ColumnType::BIGINT, 75 | 'serial8' => ColumnType::BIGINT, 76 | 'oid' => ColumnType::BIGINT, // shouldn't be used. it's pg internal! 77 | 'pg_lsn' => ColumnType::BIGINT, 78 | 'real' => ColumnType::FLOAT, 79 | 'float4' => ColumnType::FLOAT, 80 | 'float8' => ColumnType::DOUBLE, 81 | 'double precision' => ColumnType::DOUBLE, 82 | 'decimal' => ColumnType::DECIMAL, 83 | 'numeric' => ColumnType::DECIMAL, 84 | 'money' => ColumnType::MONEY, 85 | 'char' => ColumnType::CHAR, 86 | 'character' => ColumnType::CHAR, 87 | 'bpchar' => ColumnType::CHAR, 88 | 'character varying' => ColumnType::STRING, 89 | 'varchar' => ColumnType::STRING, 90 | 'text' => ColumnType::TEXT, 91 | 'bytea' => ColumnType::BINARY, 92 | 'abstime' => ColumnType::DATETIME, 93 | 'timestamp' => ColumnType::DATETIME, 94 | 'timestamp without time zone' => ColumnType::DATETIME, 95 | 'timestamp with time zone' => ColumnType::DATETIMETZ, 96 | 'timestamptz' => ColumnType::DATETIMETZ, 97 | 'time' => ColumnType::TIME, 98 | 'time without time zone' => ColumnType::TIME, 99 | 'time with time zone' => ColumnType::TIMETZ, 100 | 'timetz' => ColumnType::TIMETZ, 101 | 'date' => ColumnType::DATE, 102 | 'interval' => ColumnType::STRING, 103 | 'box' => ColumnType::STRING, 104 | 'circle' => ColumnType::STRING, 105 | 'point' => ColumnType::STRING, 106 | 'line' => ColumnType::STRING, 107 | 'lseg' => ColumnType::STRING, 108 | 'polygon' => ColumnType::STRING, 109 | 'path' => ColumnType::STRING, 110 | 'cidr' => ColumnType::STRING, 111 | 'inet' => ColumnType::STRING, 112 | 'macaddr' => ColumnType::STRING, 113 | 'macaddr8' => ColumnType::STRING, 114 | 'tsquery' => ColumnType::STRING, 115 | 'tsvector' => ColumnType::STRING, 116 | 'txid_snapshot' => ColumnType::STRING, 117 | 'unknown' => ColumnType::STRING, 118 | 'uuid' => ColumnType::STRING, 119 | 'xml' => ColumnType::STRING, 120 | 'json' => ColumnType::JSON, 121 | 'jsonb' => ColumnType::JSON, 122 | ]; 123 | 124 | public function fromType(string $type, array $info = []): ColumnInterface 125 | { 126 | $column = parent::fromType($type, $info); 127 | 128 | if ($column instanceof StructuredColumn) { 129 | $this->initializeStructuredDefaultValue($column); 130 | } 131 | 132 | return $column; 133 | } 134 | 135 | protected function columnDefinitionParser(): ColumnDefinitionParser 136 | { 137 | return new ColumnDefinitionParser(); 138 | } 139 | 140 | protected function getColumnClass(string $type, array $info = []): string 141 | { 142 | return match ($type) { 143 | ColumnType::BOOLEAN => BooleanColumn::class, 144 | ColumnType::BIT => BitColumn::class, 145 | ColumnType::TINYINT => IntegerColumn::class, 146 | ColumnType::SMALLINT => IntegerColumn::class, 147 | ColumnType::INTEGER => IntegerColumn::class, 148 | ColumnType::BIGINT => PHP_INT_SIZE !== 8 149 | ? BigIntColumn::class 150 | : IntegerColumn::class, 151 | ColumnType::BINARY => BinaryColumn::class, 152 | ColumnType::ARRAY => ArrayColumn::class, 153 | ColumnType::STRUCTURED => StructuredColumn::class, 154 | default => parent::getColumnClass($type, $info), 155 | }; 156 | } 157 | 158 | protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnInterface $column): mixed 159 | { 160 | /** @var string $value */ 161 | $value = preg_replace("/::[^:']+$/", '$1', $defaultValue); 162 | 163 | if (str_starts_with($value, "B'") && $value[-1] === "'") { 164 | return $column->phpTypecast(substr($value, 2, -1)); 165 | } 166 | 167 | $value = parent::normalizeNotNullDefaultValue($value, $column); 168 | 169 | if ($value instanceof Expression) { 170 | return new Expression($defaultValue); 171 | } 172 | 173 | return $value; 174 | } 175 | 176 | /** 177 | * Initializes the default value for structured columns. 178 | */ 179 | private function initializeStructuredDefaultValue(StructuredColumn $column): void 180 | { 181 | /** @psalm-var array|null $defaultValue */ 182 | $defaultValue = $column->getDefaultValue(); 183 | 184 | if (is_array($defaultValue)) { 185 | foreach ($column->getColumns() as $structuredColumnName => $structuredColumn) { 186 | if (isset($defaultValue[$structuredColumnName])) { 187 | $structuredColumn->defaultValue($defaultValue[$structuredColumnName]); 188 | 189 | if ($structuredColumn instanceof StructuredColumn) { 190 | $this->initializeStructuredDefaultValue($structuredColumn); 191 | } 192 | } 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Column/IntegerColumn.php: -------------------------------------------------------------------------------- 1 | sequenceName; 20 | } 21 | 22 | public function sequenceName(string|null $sequenceName): static 23 | { 24 | $this->sequenceName = $sequenceName; 25 | return $this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Column/StructuredColumn.php: -------------------------------------------------------------------------------- 1 | columns))->getValue(); 22 | } 23 | 24 | return $value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Column/StructuredLazyColumn.php: -------------------------------------------------------------------------------- 1 | columns); 22 | } 23 | 24 | return $value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | setSql($sql)->queryColumn(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | setSql($sql); 31 | } 32 | 33 | if ($this->logger !== null) { 34 | $command->setLogger($this->logger); 35 | } 36 | 37 | if ($this->profiler !== null) { 38 | $command->setProfiler($this->profiler); 39 | } 40 | 41 | return $command->bindValues($params); 42 | } 43 | 44 | public function createTransaction(): TransactionInterface 45 | { 46 | return new Transaction($this); 47 | } 48 | 49 | public function getColumnFactory(): ColumnFactoryInterface 50 | { 51 | return $this->columnFactory ??= new ColumnFactory(); 52 | } 53 | 54 | public function getLastInsertId(?string $sequenceName = null): string 55 | { 56 | if ($sequenceName === null) { 57 | throw new InvalidArgumentException('PostgreSQL not support lastInsertId without sequence name.'); 58 | } 59 | 60 | return parent::getLastInsertId($sequenceName); 61 | } 62 | 63 | public function getQueryBuilder(): QueryBuilderInterface 64 | { 65 | return $this->queryBuilder ??= new QueryBuilder($this); 66 | } 67 | 68 | public function getQuoter(): QuoterInterface 69 | { 70 | return $this->quoter ??= new Quoter('"', '"', $this->getTablePrefix()); 71 | } 72 | 73 | public function getSchema(): SchemaInterface 74 | { 75 | return $this->schema ??= new Schema($this, $this->schemaCache); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/DDLQueryBuilder.php: -------------------------------------------------------------------------------- 1 | quoter->quoteColumnName($column); 33 | $tableName = $this->quoter->quoteTableName($table); 34 | 35 | /** 36 | * @link https://github.com/yiisoft/yii2/issues/4492 37 | * @link https://www.postgresql.org/docs/9.1/static/sql-altertable.html 38 | */ 39 | if (is_string($type)) { 40 | if (preg_match('/^(DROP|SET|RESET|USING)\s+/i', $type) === 1) { 41 | return "ALTER TABLE $tableName ALTER COLUMN $columnName $type"; 42 | } 43 | 44 | $type = $this->queryBuilder->getColumnFactory()->fromDefinition($type); 45 | } 46 | 47 | $columnDefinitionBuilder = $this->queryBuilder->getColumnDefinitionBuilder(); 48 | 49 | $multiAlterStatement = ["ALTER COLUMN $columnName TYPE " . $columnDefinitionBuilder->buildAlter($type)]; 50 | 51 | if ($type->hasDefaultValue()) { 52 | $defaultValue = $type->dbTypecast($type->getDefaultValue()); 53 | $defaultValue = $this->queryBuilder->prepareValue($defaultValue); 54 | 55 | $multiAlterStatement[] = "ALTER COLUMN $columnName SET DEFAULT $defaultValue"; 56 | } 57 | 58 | match ($type->isNotNull()) { 59 | true => $multiAlterStatement[] = "ALTER COLUMN $columnName SET NOT NULL", 60 | false => $multiAlterStatement[] = "ALTER COLUMN $columnName DROP NOT NULL", 61 | default => null, 62 | }; 63 | 64 | $check = $type->getCheck(); 65 | if (!empty($check)) { 66 | $constraintPrefix = preg_replace('/\W/', '', $table . '_' . $column); 67 | $multiAlterStatement[] = "ADD CONSTRAINT {$constraintPrefix}_check CHECK ($check)"; 68 | } 69 | 70 | if ($type->isUnique()) { 71 | $multiAlterStatement[] = "ADD UNIQUE ($columnName)"; 72 | } 73 | 74 | return "ALTER TABLE $tableName " . implode(', ', $multiAlterStatement); 75 | } 76 | 77 | /** 78 | * @throws Throwable 79 | */ 80 | public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string 81 | { 82 | /** @psalm-var Schema $schemaInstance */ 83 | $schemaInstance = $this->schema; 84 | $enable = $check ? 'ENABLE' : 'DISABLE'; 85 | $schema = $schema ?: $schemaInstance->getDefaultSchema(); 86 | $tableNames = []; 87 | $viewNames = []; 88 | 89 | if ($schema !== null) { 90 | $tableNames = $table ? [$table] : $schemaInstance->getTableNames($schema); 91 | $viewNames = $schemaInstance->getViewNames($schema); 92 | } 93 | 94 | $tableNames = array_diff($tableNames, $viewNames); 95 | $command = ''; 96 | 97 | /** @psalm-var string[] $tableNames */ 98 | foreach ($tableNames as $tableName) { 99 | $tableName = $this->quoter->quoteTableName("$schema.$tableName"); 100 | $command .= "ALTER TABLE $tableName $enable TRIGGER ALL; "; 101 | } 102 | 103 | return $command; 104 | } 105 | 106 | public function createIndex(string $table, string $name, array|string $columns, ?string $indexType = null, ?string $indexMethod = null): string 107 | { 108 | return 'CREATE ' . (!empty($indexType) ? $indexType . ' ' : '') . 'INDEX ' 109 | . $this->quoter->quoteTableName($name) . ' ON ' 110 | . $this->quoter->quoteTableName($table) 111 | . (!empty($indexMethod) ? " USING $indexMethod" : '') 112 | . ' (' . $this->queryBuilder->buildColumns($columns) . ')'; 113 | } 114 | 115 | public function dropDefaultValue(string $table, string $name): string 116 | { 117 | throw new NotSupportedException(__METHOD__ . ' is not supported by PostgreSQL.'); 118 | } 119 | 120 | public function dropIndex(string $table, string $name): string 121 | { 122 | if (str_contains($table, '.') && !str_contains($name, '.')) { 123 | if (str_contains($table, '{{')) { 124 | /** @var string $table */ 125 | $table = preg_replace('/{{(.*?)}}/', '\1', $table); 126 | [$schema] = explode('.', $table); 127 | if (!str_contains($schema, '%')) { 128 | $name = $schema . '.' . $name; 129 | } else { 130 | $name = '{{' . $schema . '.' . $name . '}}'; 131 | } 132 | } else { 133 | [$schema] = explode('.', $table); 134 | $name = $schema . '.' . $name; 135 | } 136 | } 137 | 138 | return 'DROP INDEX ' . $this->quoter->quoteTableName($name); 139 | } 140 | 141 | public function truncateTable(string $table): string 142 | { 143 | return 'TRUNCATE TABLE ' . $this->quoter->quoteTableName($table) . ' RESTART IDENTITY'; 144 | } 145 | 146 | public function renameTable(string $oldName, string $newName): string 147 | { 148 | return 'ALTER TABLE ' 149 | . $this->quoter->quoteTableName($oldName) 150 | . ' RENAME TO ' 151 | . $this->quoter->quoteTableName($newName); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/DMLQueryBuilder.php: -------------------------------------------------------------------------------- 1 | insert($table, $columns, $params); 23 | 24 | return $this->appendReturningPksClause($sql, $table); 25 | } 26 | 27 | public function resetSequence(string $table, int|string|null $value = null): string 28 | { 29 | $tableSchema = $this->schema->getTableSchema($table); 30 | 31 | if ($tableSchema === null) { 32 | throw new InvalidArgumentException("Table not found: '$table'."); 33 | } 34 | 35 | $sequence = $tableSchema->getSequenceName(); 36 | 37 | if ($sequence === null) { 38 | throw new InvalidArgumentException("There is not sequence associated with table '$table'."); 39 | } 40 | 41 | /** @link https://www.postgresql.org/docs/8.1/static/functions-sequence.html */ 42 | $sequence = $this->quoter->quoteTableName($sequence); 43 | 44 | if ($value === null) { 45 | $table = $this->quoter->quoteTableName($table); 46 | $key = $tableSchema->getPrimaryKey()[0]; 47 | $key = $this->quoter->quoteColumnName($key); 48 | $value = "(SELECT COALESCE(MAX($key),0) FROM $table)+1"; 49 | } 50 | 51 | return "SELECT SETVAL('$sequence',$value,false)"; 52 | } 53 | 54 | public function upsert( 55 | string $table, 56 | array|QueryInterface $insertColumns, 57 | array|bool $updateColumns = true, 58 | array &$params = [], 59 | ): string { 60 | $insertSql = $this->insert($table, $insertColumns, $params); 61 | 62 | [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns); 63 | 64 | if (empty($uniqueNames)) { 65 | return $insertSql; 66 | } 67 | 68 | if ($updateColumns === false || $updateNames === []) { 69 | /** there are no columns to update */ 70 | return "$insertSql ON CONFLICT DO NOTHING"; 71 | } 72 | 73 | if ($updateColumns === true) { 74 | $updateColumns = []; 75 | 76 | /** @psalm-var string[] $updateNames */ 77 | foreach ($updateNames as $name) { 78 | $updateColumns[$name] = new Expression( 79 | 'EXCLUDED.' . $this->quoter->quoteColumnName($name) 80 | ); 81 | } 82 | } 83 | 84 | [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params); 85 | 86 | return $insertSql 87 | . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET ' . implode(', ', $updates); 88 | } 89 | 90 | public function upsertWithReturningPks( 91 | string $table, 92 | array|QueryInterface $insertColumns, 93 | array|bool $updateColumns = true, 94 | array &$params = [], 95 | ): string { 96 | $sql = $this->upsert($table, $insertColumns, $updateColumns, $params); 97 | 98 | return $this->appendReturningPksClause($sql, $table); 99 | } 100 | 101 | private function appendReturningPksClause(string $sql, string $table): string 102 | { 103 | $returnColumns = $this->schema->getTableSchema($table)?->getPrimaryKey(); 104 | 105 | if (!empty($returnColumns)) { 106 | $returnColumns = array_map($this->quoter->quoteColumnName(...), $returnColumns); 107 | 108 | $sql .= ' RETURNING ' . implode(', ', $returnColumns); 109 | } 110 | 111 | return $sql; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/DQLQueryBuilder.php: -------------------------------------------------------------------------------- 1 | LikeCondition::class, 38 | 'NOT ILIKE' => LikeCondition::class, 39 | 'OR ILIKE' => LikeCondition::class, 40 | 'OR NOT ILIKE' => LikeCondition::class, 41 | ]; 42 | } 43 | 44 | protected function defaultExpressionBuilders(): array 45 | { 46 | return [ 47 | ...parent::defaultExpressionBuilders(), 48 | ArrayExpression::class => ArrayExpressionBuilder::class, 49 | ArrayOverlapsCondition::class => ArrayOverlapsConditionBuilder::class, 50 | JsonExpression::class => JsonExpressionBuilder::class, 51 | JsonOverlapsCondition::class => JsonOverlapsConditionBuilder::class, 52 | StructuredExpression::class => StructuredExpressionBuilder::class, 53 | LikeCondition::class => LikeConditionBuilder::class, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Data/ArrayParser.php: -------------------------------------------------------------------------------- 1 | |null 22 | */ 23 | public function parse(string $value): array|null 24 | { 25 | return $value[0] === '{' 26 | ? $this->parseArray($value) 27 | : null; 28 | } 29 | 30 | /** 31 | * Parse PostgreSQL array encoded in string. 32 | * 33 | * @param string $value String to parse. 34 | * @param int $i parse starting position. 35 | * 36 | * @return (array|string|null)[] Parsed value. 37 | * 38 | * @psalm-return list 39 | */ 40 | private function parseArray(string $value, int &$i = 0): array 41 | { 42 | if ($value[++$i] === '}') { 43 | ++$i; 44 | return []; 45 | } 46 | 47 | for ($result = [];; ++$i) { 48 | $result[] = match ($value[$i]) { 49 | '{' => $this->parseArray($value, $i), 50 | ',', '}' => null, 51 | '"' => $this->parseQuotedString($value, $i), 52 | default => $this->parseUnquotedString($value, $i), 53 | }; 54 | 55 | if ($value[$i] === '}') { 56 | ++$i; 57 | return $result; 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * Parses quoted string. 64 | */ 65 | private function parseQuotedString(string $value, int &$i): string 66 | { 67 | for ($result = '', ++$i;; ++$i) { 68 | if ($value[$i] === '\\') { 69 | ++$i; 70 | } elseif ($value[$i] === '"') { 71 | ++$i; 72 | return $result; 73 | } 74 | 75 | $result .= $value[$i]; 76 | } 77 | } 78 | 79 | /** 80 | * Parses unquoted string. 81 | */ 82 | private function parseUnquotedString(string $value, int &$i): string|null 83 | { 84 | for ($result = '';; ++$i) { 85 | if (in_array($value[$i], [',', '}'], true)) { 86 | return $result !== 'NULL' 87 | ? $result 88 | : null; 89 | } 90 | 91 | $result .= $value[$i]; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Data/LazyArray.php: -------------------------------------------------------------------------------- 1 | parse($value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Data/StructuredLazyArray.php: -------------------------------------------------------------------------------- 1 | parse($value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Data/StructuredParser.php: -------------------------------------------------------------------------------- 1 | |null 22 | */ 23 | public function parse(string $value): array|null 24 | { 25 | if ($value[0] !== '(') { 26 | return null; 27 | } 28 | 29 | return $this->parseComposite($value); 30 | } 31 | 32 | /** 33 | * Parses PostgreSQL composite type value encoded in string. 34 | * 35 | * @param string $value String to parse. 36 | * 37 | * @return (string|null)[] Parsed value. 38 | * 39 | * @psalm-return non-empty-list 40 | */ 41 | private function parseComposite(string $value): array 42 | { 43 | for ($result = [], $i = 1;; ++$i) { 44 | $result[] = match ($value[$i]) { 45 | ',', ')' => null, 46 | '"' => $this->parseQuotedString($value, $i), 47 | default => $this->parseUnquotedString($value, $i), 48 | }; 49 | 50 | if ($value[$i] === ')') { 51 | return $result; 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * Parses quoted string. 58 | */ 59 | private function parseQuotedString(string $value, int &$i): string 60 | { 61 | for ($result = '', ++$i;; ++$i) { 62 | if ($value[$i] === '\\') { 63 | ++$i; 64 | } elseif ($value[$i] === '"') { 65 | ++$i; 66 | return $result; 67 | } 68 | 69 | $result .= $value[$i]; 70 | } 71 | } 72 | 73 | /** 74 | * Parses unquoted string. 75 | */ 76 | private function parseUnquotedString(string $value, int &$i): string 77 | { 78 | for ($result = '';; ++$i) { 79 | if (in_array($value[$i], [',', ')'], true)) { 80 | return $result; 81 | } 82 | 83 | $result .= $value[$i]; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Driver.php: -------------------------------------------------------------------------------- 1 | attributes += [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]; 20 | $pdo = parent::createConnection(); 21 | 22 | if ($this->charset !== null) { 23 | $pdo->exec('SET NAMES ' . $pdo->quote($this->charset)); 24 | } 25 | 26 | return $pdo; 27 | } 28 | 29 | public function getDriverName(): string 30 | { 31 | return 'pgsql'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Dsn.php: -------------------------------------------------------------------------------- 1 | $options 18 | */ 19 | public function __construct( 20 | string $driver = 'pgsql', 21 | string $host = '127.0.0.1', 22 | string|null $databaseName = 'postgres', 23 | string $port = '5432', 24 | array $options = [] 25 | ) { 26 | if (empty($databaseName)) { 27 | $databaseName = 'postgres'; 28 | } 29 | 30 | parent::__construct($driver, $host, $databaseName, $port, $options); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/IndexMethod.php: -------------------------------------------------------------------------------- 1 | getQuoter(); 21 | $schema = $db->getSchema(); 22 | 23 | parent::__construct( 24 | $db, 25 | new DDLQueryBuilder($this, $quoter, $schema), 26 | new DMLQueryBuilder($this, $quoter, $schema), 27 | new DQLQueryBuilder($this, $quoter), 28 | new ColumnDefinitionBuilder($this), 29 | ); 30 | } 31 | 32 | protected function prepareBinary(string $binary): string 33 | { 34 | return "'\x" . bin2hex($binary) . "'::bytea"; 35 | } 36 | 37 | protected function createSqlParser(string $sql): SqlParser 38 | { 39 | return new SqlParser($sql); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/SqlParser.php: -------------------------------------------------------------------------------- 1 | length - 1; 15 | 16 | while ($this->position < $length) { 17 | $pos = $this->position++; 18 | 19 | match ($this->sql[$pos]) { 20 | ':' => ($word = $this->parseWord()) === '' 21 | ? $this->skipChars(':') 22 | : $result = ':' . $word, 23 | '"', "'" => $this->skipQuotedWithoutEscape($this->sql[$pos]), 24 | 'e', 'E' => $this->sql[$this->position] === "'" 25 | ? ++$this->position && $this->skipQuotedWithEscape("'") 26 | : $this->skipIdentifier(), 27 | '$' => $this->skipQuotedWithDollar(), 28 | '-' => $this->sql[$this->position] === '-' 29 | ? ++$this->position && $this->skipToAfterChar("\n") 30 | : null, 31 | '/' => $this->sql[$this->position] === '*' 32 | ? ++$this->position && $this->skipToAfterString('*/') 33 | : null, 34 | // Identifiers can contain dollar sign which can be used for quoting. Skip them. 35 | '_','a', 'b', 'c', 'd', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 36 | 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 37 | 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' => $this->skipIdentifier(), 38 | default => null, 39 | }; 40 | 41 | if ($result !== null) { 42 | $position = $pos; 43 | 44 | return $result; 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | 51 | /** 52 | * Skips dollar-quoted string. 53 | */ 54 | private function skipQuotedWithDollar(): void 55 | { 56 | $pos = $this->position; 57 | $identifier = $this->parseIdentifier(); 58 | 59 | if ($this->sql[$this->position] !== '$') { 60 | $this->position = $pos; 61 | return; 62 | } 63 | 64 | ++$this->position; 65 | 66 | $this->skipToAfterString('$' . $identifier . '$'); 67 | } 68 | 69 | /** 70 | * Skips an identifier. Equals to `[$\w]+` in regular expressions. 71 | */ 72 | private function skipIdentifier(): void 73 | { 74 | $continue = true; 75 | 76 | while ($continue && $this->position < $this->length) { 77 | match ($this->sql[$this->position]) { 78 | '$', '_', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 79 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 80 | 'v', 'w', 'x', 'y', 'z', 81 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 82 | 'V', 'W', 'X', 'Y', 'Z' => ++$this->position, 83 | default => $continue = false, 84 | }; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/TableSchema.php: -------------------------------------------------------------------------------- 1 | 1, ':qp1' => 2, ':qp2' => 3]], 40 | [ 41 | new ArrayIterator(['a', 'b', 'c']), 42 | 'varchar', 43 | 'ARRAY[:qp0,:qp1,:qp2]::varchar[]', 44 | [':qp0' => 'a', ':qp1' => 'b', ':qp2' => 'c'], 45 | ], 46 | [ 47 | new LazyArray('{1,2,3}'), 48 | 'int[]', 49 | ':qp0::int[]', 50 | [':qp0' => new Param('{1,2,3}', DataType::STRING)], 51 | ], 52 | [ 53 | new \Yiisoft\Db\Schema\Data\LazyArray('[1,2,3]'), 54 | ColumnBuilder::integer(), 55 | 'ARRAY[:qp0,:qp1,:qp2]::integer[]', 56 | [':qp0' => 1, ':qp1' => 2, ':qp2' => 3], 57 | ], 58 | [ 59 | new StructuredLazyArray('(1,2,3)'), 60 | 'int', 61 | 'ARRAY[:qp0,:qp1,:qp2]::int[]', 62 | [':qp0' => 1, ':qp1' => 2, ':qp2' => 3], 63 | ], 64 | [ 65 | new JsonLazyArray('[1,2,3]'), 66 | ColumnBuilder::array(ColumnBuilder::integer()), 67 | 'ARRAY[:qp0,:qp1,:qp2]::integer[]', 68 | [':qp0' => 1, ':qp1' => 2, ':qp2' => 3], 69 | ], 70 | [[new Expression('now()')], null, 'ARRAY[now()]', []], 71 | [ 72 | [new JsonExpression(['a' => null, 'b' => 123, 'c' => [4, 5]]), new JsonExpression([true])], 73 | null, 74 | 'ARRAY[:qp0,:qp1]', 75 | [ 76 | ':qp0' => new Param('{"a":null,"b":123,"c":[4,5]}', DataType::STRING), 77 | ':qp1' => new Param('[true]', DataType::STRING), 78 | ], 79 | ], 80 | [ 81 | [new JsonExpression(['a' => null, 'b' => 123, 'c' => [4, 5]]), new JsonExpression([true])], 82 | 'jsonb', 83 | 'ARRAY[:qp0,:qp1]::jsonb[]', 84 | [ 85 | ':qp0' => new Param('{"a":null,"b":123,"c":[4,5]}', DataType::STRING), 86 | ':qp1' => new Param('[true]', DataType::STRING), 87 | ], 88 | ], 89 | [ 90 | [ 91 | null, 92 | new StructuredExpression(['value' => 11.11, 'currency_code' => 'USD']), 93 | new StructuredExpression(['value' => null, 'currency_code' => null]), 94 | ], 95 | null, 96 | 'ARRAY[:qp0,ROW(:qp1,:qp2),ROW(:qp3,:qp4)]', 97 | [':qp0' => null, ':qp1' => 11.11, ':qp2' => 'USD', ':qp3' => null, ':qp4' => null], 98 | ], 99 | [ 100 | (new Query(self::getDb()))->select('id')->from('users')->where(['active' => 1]), 101 | null, 102 | 'ARRAY(SELECT "id" FROM "users" WHERE "active"=:qp0)', 103 | [':qp0' => 1], 104 | ], 105 | [ 106 | [(new Query(self::getDb()))->select('id')->from('users')->where(['active' => 1])], 107 | 'integer[][]', 108 | 'ARRAY[ARRAY(SELECT "id" FROM "users" WHERE "active"=:qp0)::integer[]]::integer[][]', 109 | [':qp0' => 1], 110 | ], 111 | [ 112 | [[[true], [false, null]], [['t', 'f'], null], null], 113 | 'bool[][][]', 114 | 'ARRAY[ARRAY[ARRAY[:qp0]::bool[],ARRAY[:qp1,:qp2]::bool[]]::bool[][],ARRAY[ARRAY[:qp3,:qp4]::bool[],NULL]::bool[][],NULL]::bool[][][]', 115 | [ 116 | ':qp0' => true, 117 | ':qp1' => false, 118 | ':qp2' => null, 119 | ':qp3' => 't', 120 | ':qp4' => 'f', 121 | ], 122 | ], 123 | [ 124 | ['a' => '1', 'b' => null], 125 | ColumnType::STRING, 126 | 'ARRAY[:qp0,:qp1]::varchar(255)[]', 127 | [':qp0' => '1', ':qp1' => null], 128 | ], 129 | [ 130 | '{1,2,3}', 131 | 'string[]', 132 | ':qp0::varchar(255)[]', 133 | [':qp0' => new Param('{1,2,3}', DataType::STRING)], 134 | ], 135 | [ 136 | [[1, null], null], 137 | 'int[][]', 138 | 'ARRAY[ARRAY[:qp0,:qp1]::int[],NULL]::int[][]', 139 | [':qp0' => '1', ':qp1' => null], 140 | ], 141 | ]; 142 | } 143 | 144 | #[DataProvider('buildProvider')] 145 | public function testBuild( 146 | iterable|LazyArrayInterface|Query|string|null $value, 147 | ColumnInterface|string|null $type, 148 | string $expected, 149 | array $expectedParams 150 | ): void { 151 | $db = $this->getConnection(); 152 | $qb = $db->getQueryBuilder(); 153 | 154 | $params = []; 155 | $builder = new ArrayExpressionBuilder($qb); 156 | $expression = new ArrayExpression($value, $type); 157 | 158 | $this->assertSame($expected, $builder->build($expression, $params)); 159 | $this->assertEquals($expectedParams, $params); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tests/ArrayParserTest.php: -------------------------------------------------------------------------------- 1 | assertSame([0 => null, 1 => null], $arrayParse->parse('{NULL,NULL}')); 22 | $this->assertSame([], $arrayParse->parse('{}')); 23 | $this->assertSame([0 => null, 1 => null], $arrayParse->parse('{,}')); 24 | $this->assertSame([0 => '1', 1 => '2', 2 => '3'], $arrayParse->parse('{1,2,3}')); 25 | $this->assertSame([0 => '1', 1 => '-2', 2 => null, 3 => '42'], $arrayParse->parse('{1,-2,NULL,42}')); 26 | $this->assertSame([[0 => 'text'], [0 => null], [0 => '1']], $arrayParse->parse('{{text},{NULL},{1}}')); 27 | $this->assertSame([0 => ''], $arrayParse->parse('{""}')); 28 | $this->assertSame( 29 | [0 => '[",","null",true,"false","f"]'], 30 | $arrayParse->parse('{"[\",\",\"null\",true,\"false\",\"f\"]"}') 31 | ); 32 | 33 | // Similar cases can be in default values 34 | $this->assertSame(null, $arrayParse->parse("'{one,two}'::text[]")); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/BatchQueryResultTest.php: -------------------------------------------------------------------------------- 1 | getConnection(); 34 | $columnFactory = $db->getColumnFactory(); 35 | 36 | // For array type 37 | $column = $columnFactory->fromType(ColumnType::ARRAY, ['dbType' => $dbType]); 38 | 39 | $this->assertInstanceOf(ArrayColumn::class, $column); 40 | $this->assertInstanceOf($expectedInstanceOf, $column->getColumn()); 41 | $this->assertSame($expectedType, $column->getColumn()->getType()); 42 | $this->assertSame($dbType, $column->getColumn()->getDbType()); 43 | 44 | $db->close(); 45 | } 46 | 47 | #[DataProviderExternal(ColumnFactoryProvider::class, 'definitions')] 48 | public function testFromDefinition(string $definition, ColumnInterface $expected): void 49 | { 50 | parent::testFromDefinition($definition, $expected); 51 | } 52 | 53 | #[DataProviderExternal(ColumnFactoryProvider::class, 'pseudoTypes')] 54 | public function testFromPseudoType(string $pseudoType, ColumnInterface $expected): void 55 | { 56 | parent::testFromPseudoType($pseudoType, $expected); 57 | } 58 | 59 | #[DataProviderExternal(ColumnFactoryProvider::class, 'types')] 60 | public function testFromType(string $type, string $expectedType, string $expectedInstanceOf): void 61 | { 62 | parent::testFromType($type, $expectedType, $expectedInstanceOf); 63 | 64 | $db = $this->getConnection(); 65 | $columnFactory = $db->getColumnFactory(); 66 | 67 | // For array type 68 | $column = $columnFactory->fromType(ColumnType::ARRAY, ['column' => $columnFactory->fromType($type)]); 69 | 70 | $this->assertInstanceOf(ArrayColumn::class, $column); 71 | $this->assertInstanceOf($expectedInstanceOf, $column->getColumn()); 72 | $this->assertSame($expectedType, $column->getColumn()->getType()); 73 | 74 | $db->close(); 75 | } 76 | 77 | #[DataProviderExternal(ColumnFactoryProvider::class, 'defaultValueRaw')] 78 | public function testFromTypeDefaultValueRaw(string $type, string|null $defaultValueRaw, mixed $expected): void 79 | { 80 | parent::testFromTypeDefaultValueRaw($type, $defaultValueRaw, $expected); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/CommandTest.php: -------------------------------------------------------------------------------- 1 | getConnection(); 36 | 37 | $command = $db->createCommand(); 38 | 39 | $this->expectException(NotSupportedException::class); 40 | $this->expectExceptionMessage( 41 | 'Yiisoft\Db\Pgsql\DDLQueryBuilder::addDefaultValue is not supported by PostgreSQL.' 42 | ); 43 | 44 | $command->addDefaultValue('{{table}}', '{{name}}', 'column', 'value'); 45 | 46 | $db->close(); 47 | } 48 | 49 | /** 50 | * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CommandProvider::batchInsert 51 | * 52 | * @throws Throwable 53 | */ 54 | public function testBatchInsert( 55 | string $table, 56 | iterable $values, 57 | array $columns, 58 | string $expected, 59 | array $expectedParams = [], 60 | int $insertedRow = 1 61 | ): void { 62 | parent::testBatchInsert($table, $values, $columns, $expected, $expectedParams, $insertedRow); 63 | } 64 | 65 | /** 66 | * @throws Exception 67 | * @throws InvalidConfigException 68 | * @throws Throwable 69 | */ 70 | public function testBooleanValuesInsert(): void 71 | { 72 | $db = $this->getConnection(true); 73 | 74 | $command = $db->createCommand(); 75 | $command->insert('{{bool_values}}', ['bool_col' => true]); 76 | 77 | $this->assertSame(1, $command->execute()); 78 | 79 | $command = $db->createCommand(); 80 | $command->insert('{{bool_values}}', ['bool_col' => false]); 81 | 82 | $this->assertSame(1, $command->execute()); 83 | 84 | $command->setSql( 85 | <<assertSame(1, $command->queryScalar()); 91 | 92 | $command->setSql( 93 | <<assertSame(1, $command->queryScalar()); 99 | 100 | $db->close(); 101 | } 102 | 103 | /** 104 | * @throws Exception 105 | * @throws InvalidConfigException 106 | * @throws Throwable 107 | */ 108 | public function testBooleanValuesBatchInsert(): void 109 | { 110 | $db = $this->getConnection(true); 111 | 112 | $command = $db->createCommand(); 113 | $command->insertBatch('{{bool_values}}', [[true], [false]], ['bool_col']); 114 | 115 | $this->assertSame(2, $command->execute()); 116 | 117 | $command->setSql( 118 | <<assertSame(1, $command->queryScalar()); 124 | 125 | $command->setSql( 126 | <<assertSame(1, $command->queryScalar()); 132 | 133 | $db->close(); 134 | } 135 | 136 | /** 137 | * @throws Exception 138 | * @throws InvalidConfigException 139 | * @throws Throwable 140 | */ 141 | public function testDelete(): void 142 | { 143 | $db = $this->getConnection(true); 144 | 145 | $command = $db->createCommand(); 146 | $command->delete('{{customer}}', ['id' => 2])->execute(); 147 | $chekSql = <<setSql($chekSql); 151 | 152 | $this->assertSame(2, $command->queryScalar()); 153 | 154 | $command->delete('{{customer}}', ['id' => 3])->execute(); 155 | $command->setSql($chekSql); 156 | 157 | $this->assertSame(1, $command->queryScalar()); 158 | 159 | $db->close(); 160 | } 161 | 162 | /** 163 | * @throws Exception 164 | * @throws InvalidConfigException 165 | */ 166 | public function testDropDefaultValue(): void 167 | { 168 | $db = $this->getConnection(); 169 | 170 | $command = $db->createCommand(); 171 | 172 | $this->expectException(NotSupportedException::class); 173 | $this->expectExceptionMessage( 174 | 'Yiisoft\Db\Pgsql\DDLQueryBuilder::dropDefaultValue is not supported by PostgreSQL.' 175 | ); 176 | 177 | $command->dropDefaultValue('{{table}}', '{{name}}'); 178 | 179 | $db->close(); 180 | } 181 | 182 | /** 183 | * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CommandProvider::rawSql 184 | * 185 | * @throws Exception 186 | * @throws InvalidConfigException 187 | * @throws NotSupportedException 188 | */ 189 | public function testGetRawSql(string $sql, array $params, string $expectedRawSql): void 190 | { 191 | parent::testGetRawSql($sql, $params, $expectedRawSql); 192 | } 193 | 194 | /** 195 | * @throws Exception 196 | * @throws InvalidConfigException 197 | * @throws Throwable 198 | * 199 | * {@link https://github.com/yiisoft/yii2/issues/11498} 200 | */ 201 | public function testSaveSerializedObject(): void 202 | { 203 | $db = $this->getConnection(); 204 | 205 | $command = $db->createCommand(); 206 | $command = $command->insert( 207 | '{{type}}', 208 | [ 209 | 'int_col' => 1, 210 | 'char_col' => 'serialize', 211 | 'float_col' => 5.6, 212 | 'bool_col' => true, 213 | 'blob_col' => serialize($db), 214 | ], 215 | ); 216 | 217 | $this->assertSame(1, $command->execute()); 218 | 219 | $command->update('{{type}}', ['blob_col' => serialize($db)], ['char_col' => 'serialize']); 220 | 221 | $this->assertSame(1, $command->execute()); 222 | 223 | $db->close(); 224 | } 225 | 226 | /** 227 | * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CommandProvider::update 228 | * 229 | * @throws Exception 230 | * @throws Throwable 231 | */ 232 | public function testUpdate( 233 | string $table, 234 | array $columns, 235 | array|string $conditions, 236 | array $params, 237 | array $expectedValues, 238 | int $expectedCount, 239 | ): void { 240 | parent::testUpdate($table, $columns, $conditions, $params, $expectedValues, $expectedCount); 241 | } 242 | 243 | /** 244 | * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CommandProvider::upsert 245 | * 246 | * @throws Exception 247 | * @throws Throwable 248 | */ 249 | public function testUpsert(array $firstData, array $secondData): void 250 | { 251 | parent::testUpsert($firstData, $secondData); 252 | } 253 | 254 | public function testinsertWithReturningPksUuid(): void 255 | { 256 | $db = $this->getConnection(true); 257 | 258 | $command = $db->createCommand(); 259 | $result = $command->insertWithReturningPks( 260 | '{{%table_uuid}}', 261 | [ 262 | 'col' => 'test', 263 | ], 264 | ); 265 | 266 | $this->assertIsString($result['uuid']); 267 | 268 | // for example ['uuid' => 738146be-87b1-49f2-9913-36142fb6fcbe] 269 | $this->assertStringMatchesFormat('%s-%s-%s-%s-%s', $result['uuid']); 270 | 271 | $this->assertEquals(36, strlen($result['uuid'])); 272 | 273 | $db->close(); 274 | } 275 | 276 | public function testShowDatabases(): void 277 | { 278 | $this->assertSame([self::getDatabaseName()], self::getDb()->createCommand()->showDatabases()); 279 | } 280 | 281 | #[DataProviderExternal(CommandProvider::class, 'createIndex')] 282 | public function testCreateIndex(array $columns, array $indexColumns, string|null $indexType, string|null $indexMethod): void 283 | { 284 | parent::testCreateIndex($columns, $indexColumns, $indexType, $indexMethod); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /tests/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | getConnection(); 36 | 37 | $db->setEmulatePrepare(true); 38 | $db->open(); 39 | 40 | $this->assertTrue($db->getEmulatePrepare()); 41 | 42 | $db->close(); 43 | } 44 | 45 | /** 46 | * @throws Exception 47 | * @throws InvalidConfigException 48 | */ 49 | public function testSettingDefaultAttributes(): void 50 | { 51 | $db = $this->getConnection(); 52 | 53 | $this->assertSame(PDO::ERRMODE_EXCEPTION, $db->getActivePDO()->getAttribute(PDO::ATTR_ERRMODE)); 54 | 55 | $db->close(); 56 | $db->setEmulatePrepare(true); 57 | $db->open(); 58 | 59 | $this->assertEquals(true, $db->getActivePDO()->getAttribute(PDO::ATTR_EMULATE_PREPARES)); 60 | 61 | $db->close(); 62 | $db->setEmulatePrepare(false); 63 | $db->open(); 64 | 65 | $this->assertEquals(false, $db->getActivePDO()->getAttribute(PDO::ATTR_EMULATE_PREPARES)); 66 | 67 | $db->close(); 68 | } 69 | 70 | /** 71 | * @throws Exception 72 | * @throws InvalidConfigException 73 | * @throws NotSupportedException 74 | * @throws Throwable 75 | */ 76 | public function testTransactionIsolation(): void 77 | { 78 | $db = $this->getConnection(); 79 | 80 | $transaction = $db->beginTransaction(); 81 | $transaction->setIsolationLevel(TransactionInterface::READ_UNCOMMITTED); 82 | $transaction->commit(); 83 | 84 | $transaction = $db->beginTransaction(); 85 | $transaction->setIsolationLevel(TransactionInterface::READ_COMMITTED); 86 | $transaction->commit(); 87 | 88 | $transaction = $db->beginTransaction(); 89 | $transaction->setIsolationLevel(TransactionInterface::REPEATABLE_READ); 90 | $transaction->commit(); 91 | 92 | $transaction = $db->beginTransaction(); 93 | $transaction->setIsolationLevel(TransactionInterface::SERIALIZABLE); 94 | $transaction->commit(); 95 | 96 | $transaction = $db->beginTransaction(); 97 | $transaction->setIsolationLevel(TransactionInterface::SERIALIZABLE . ' READ ONLY DEFERRABLE'); 98 | $transaction->commit(); 99 | 100 | /* should not be any exception so far */ 101 | $this->assertTrue(true); 102 | 103 | $db->close(); 104 | } 105 | 106 | /** 107 | * @throws Exception 108 | * @throws InvalidConfigException 109 | * @throws Throwable 110 | */ 111 | public function testTransactionShortcutCustom(): void 112 | { 113 | $db = $this->getConnection(true); 114 | 115 | $this->assertTrue( 116 | $db->transaction( 117 | static function (ConnectionInterface $db) { 118 | $db->createCommand()->insert('profile', ['description' => 'test transaction shortcut'])->execute(); 119 | 120 | return true; 121 | }, 122 | TransactionInterface::READ_UNCOMMITTED, 123 | ), 124 | 'transaction shortcut valid value should be returned from callback', 125 | ); 126 | 127 | $this->assertEquals( 128 | 1, 129 | $db->createCommand( 130 | <<queryScalar(), 134 | 'profile should be inserted in transaction shortcut', 135 | ); 136 | 137 | $db->close(); 138 | } 139 | 140 | public function testGetColumnFactory(): void 141 | { 142 | $db = $this->getConnection(); 143 | 144 | $this->assertInstanceOf(ColumnFactory::class, $db->getColumnFactory()); 145 | 146 | $db->close(); 147 | } 148 | 149 | public function testUserDefinedColumnFactory(): void 150 | { 151 | $columnFactory = new ColumnFactory(); 152 | 153 | $db = new Connection($this->getDriver(), DbHelper::getSchemaCache(), $columnFactory); 154 | 155 | $this->assertSame($columnFactory, $db->getColumnFactory()); 156 | 157 | $db->close(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tests/DsnTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 20 | 'pgsql:host=localhost;dbname=yiitest;port=5432', 21 | (new Dsn('pgsql', 'localhost', 'yiitest'))->asString(), 22 | ); 23 | } 24 | 25 | public function testAsStringWithDatabaseName(): void 26 | { 27 | $this->assertSame( 28 | 'pgsql:host=localhost;dbname=postgres;port=5432', 29 | (new Dsn('pgsql', 'localhost'))->asString(), 30 | ); 31 | } 32 | 33 | public function testAsStringWithDatabaseNameWithEmptyString(): void 34 | { 35 | $this->assertSame( 36 | 'pgsql:host=localhost;dbname=postgres;port=5432', 37 | (new Dsn('pgsql', 'localhost', ''))->asString(), 38 | ); 39 | } 40 | 41 | public function testAsStringWithDatabaseNameWithNull(): void 42 | { 43 | $this->assertSame( 44 | 'pgsql:host=localhost;dbname=postgres;port=5432', 45 | (new Dsn('pgsql', 'localhost', null))->asString(), 46 | ); 47 | } 48 | 49 | public function testAsStringWithOptions(): void 50 | { 51 | $this->assertSame( 52 | 'pgsql:host=localhost;dbname=yiitest;port=5433;charset=utf8', 53 | (new Dsn('pgsql', 'localhost', 'yiitest', '5433', ['charset' => 'utf8']))->asString(), 54 | ); 55 | } 56 | 57 | public function testAsStringWithPort(): void 58 | { 59 | $this->assertSame( 60 | 'pgsql:host=localhost;dbname=yiitest;port=5433', 61 | (new Dsn('pgsql', 'localhost', 'yiitest', '5433'))->asString(), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/JsonExpressionBuilderTest.php: -------------------------------------------------------------------------------- 1 | 1, 'b' => null, 'c' => ['d' => 'e']], '{"a":1,"b":null,"c":{"d":"e"}}'], 45 | ['[1,2,3]', '[1,2,3]'], 46 | ['{"a":1,"b":null,"c":{"d":"e"}}', '{"a":1,"b":null,"c":{"d":"e"}}'], 47 | ]; 48 | } 49 | 50 | #[DataProvider('buildProvider')] 51 | public function testBuild(mixed $value, string $expected): void 52 | { 53 | $db = $this->getConnection(); 54 | $qb = $db->getQueryBuilder(); 55 | 56 | $params = []; 57 | $builder = new JsonExpressionBuilder($qb); 58 | $expression = new JsonExpression($value); 59 | 60 | $this->assertSame(':qp0', $builder->build($expression, $params)); 61 | $this->assertEquals([':qp0' => new Param($expected, DataType::STRING)], $params); 62 | } 63 | 64 | public function testBuildArrayExpression(): void 65 | { 66 | $db = $this->getConnection(); 67 | $qb = $db->getQueryBuilder(); 68 | 69 | $params = []; 70 | $builder = new JsonExpressionBuilder($qb); 71 | $expression = new JsonExpression(new ArrayExpression([1,2,3])); 72 | 73 | $this->assertSame('array_to_json(ARRAY[:qp0,:qp1,:qp2])', $builder->build($expression, $params)); 74 | $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); 75 | 76 | $params = []; 77 | $expression = new JsonExpression(new ArrayExpression([1,2,3]), 'jsonb'); 78 | 79 | $this->assertSame('array_to_json(ARRAY[:qp0,:qp1,:qp2])::jsonb', $builder->build($expression, $params)); 80 | $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); 81 | } 82 | 83 | public function testBuildNull(): void 84 | { 85 | $db = $this->getConnection(); 86 | $qb = $db->getQueryBuilder(); 87 | 88 | $params = []; 89 | $builder = new JsonExpressionBuilder($qb); 90 | $expression = new JsonExpression(null); 91 | 92 | $this->assertSame('NULL', $builder->build($expression, $params)); 93 | $this->assertSame([], $params); 94 | } 95 | 96 | public function testBuildQueryExpression(): void 97 | { 98 | $db = $this->getConnection(); 99 | $qb = $db->getQueryBuilder(); 100 | 101 | $params = []; 102 | $builder = new JsonExpressionBuilder($qb); 103 | $expression = new JsonExpression((new Query($db))->select('json_field')->from('json_table')); 104 | 105 | $this->assertSame('(SELECT "json_field" FROM "json_table")', $builder->build($expression, $params)); 106 | $this->assertSame([], $params); 107 | 108 | $expression = new JsonExpression((new Query($db))->select('json_field')->from('json_table'), 'jsonb'); 109 | 110 | $this->assertSame('(SELECT "json_field" FROM "json_table")::jsonb', $builder->build($expression, $params)); 111 | $this->assertSame([], $params); 112 | } 113 | 114 | public function testBuildWithType(): void 115 | { 116 | $db = $this->getConnection(); 117 | $qb = $db->getQueryBuilder(); 118 | 119 | $params = []; 120 | $builder = new JsonExpressionBuilder($qb); 121 | $expression = new JsonExpression([1, 2, 3], 'jsonb'); 122 | 123 | $this->assertSame(':qp0::jsonb', $builder->build($expression, $params)); 124 | $this->assertEquals([':qp0' => new Param('[1,2,3]', DataType::STRING)], $params); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/PDODriverTest.php: -------------------------------------------------------------------------------- 1 | getConnection(); 29 | 30 | $pdo = $db->getActivePDO(); 31 | $charset = $pdo->query('SHOW client_encoding', PDO::FETCH_ASSOC)->fetch(); 32 | 33 | $this->assertEqualsIgnoringCase('UTF8', array_values($charset)[0]); 34 | 35 | $pdoDriver = $this->getDriver(); 36 | $pdoDriver->charset('latin1'); 37 | $pdo = $pdoDriver->createConnection(); 38 | $charset = $pdo->query('SHOW client_encoding', PDO::FETCH_ASSOC)->fetch(); 39 | 40 | $this->assertEqualsIgnoringCase('latin1', array_values($charset)[0]); 41 | 42 | $db->close(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/PdoCommandTest.php: -------------------------------------------------------------------------------- 1 | getConnection(true); 43 | 44 | $arrValue = [1, 2, 3, 4]; 45 | $insertedData = $db->createCommand()->insertWithReturningPks('{{%table_with_array_col}}', ['array_col' => $arrValue]); 46 | 47 | $this->assertGreaterThan(0, $insertedData['id']); 48 | 49 | $selectData = $db->createCommand('select * from {{%table_with_array_col}} where id=:id', $insertedData)->queryOne(); 50 | 51 | $this->assertEquals('{1,2,3,4}', $selectData['array_col']); 52 | 53 | $column = $db->getTableSchema('{{%table_with_array_col}}')->getColumn('array_col'); 54 | 55 | $this->assertSame($arrValue, $column->phpTypecast($selectData['array_col'])); 56 | 57 | $db->close(); 58 | } 59 | 60 | public function testCommandLogging(): void 61 | { 62 | $db = $this->getConnection(true); 63 | 64 | $sql = 'SELECT * FROM "customer" LIMIT 1'; 65 | 66 | /** @var AbstractPdoCommand $command */ 67 | $command = $db->createCommand(); 68 | $this->assertInstanceOf(PdoCommandInterface::class, $command); 69 | $this->assertInstanceOf(LoggerAwareInterface::class, $command); 70 | $command->setSql($sql); 71 | 72 | $this->assertSame($sql, $command->getSql()); 73 | 74 | $command->setLogger($this->createQueryLogger($sql, ['Yiisoft\Db\Driver\Pdo\AbstractPdoCommand::queryOne'])); 75 | $command->queryOne(); 76 | 77 | $command->setLogger($this->createQueryLogger($sql, ['Yiisoft\Db\Driver\Pdo\AbstractPdoCommand::queryAll'])); 78 | $command->queryAll(); 79 | 80 | $command->setLogger($this->createQueryLogger($sql, ['Yiisoft\Db\Driver\Pdo\AbstractPdoCommand::queryColumn'])); 81 | $command->queryColumn(); 82 | 83 | $command->setLogger($this->createQueryLogger($sql, ['Yiisoft\Db\Driver\Pdo\AbstractPdoCommand::queryScalar'])); 84 | $command->queryScalar(); 85 | 86 | $command->setLogger($this->createQueryLogger($sql, ['Yiisoft\Db\Driver\Pdo\AbstractPdoCommand::query'])); 87 | $command->query(); 88 | 89 | $command->setLogger($this->createQueryLogger($sql, ['Yiisoft\Db\Driver\Pdo\AbstractPdoCommand::execute'])); 90 | $command->execute(); 91 | 92 | $sql = 'INSERT INTO "customer" ("name", "email") VALUES (\'test\', \'email@email\') RETURNING "id"'; 93 | $command->setLogger($this->createQueryLogger($sql, ['Yiisoft\Db\Driver\Pdo\AbstractPdoCommand::insertWithReturningPks'])); 94 | $command->insertWithReturningPks('{{%customer}}', ['name' => 'test', 'email' => 'email@email']); 95 | 96 | $db->close(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/PdoConnectionTest.php: -------------------------------------------------------------------------------- 1 | getConnection(true); 33 | 34 | $command = $db->createCommand(); 35 | $command->insert( 36 | 'customer', 37 | [ 38 | 'name' => 'Some {{weird}} name', 39 | 'email' => 'test@example.com', 40 | 'address' => 'Some {{%weird}} address', 41 | ] 42 | )->execute(); 43 | 44 | $this->assertSame('4', $db->getLastInsertId('public.customer_id_seq')); 45 | 46 | $db->close(); 47 | } 48 | 49 | /** 50 | * @throws Exception 51 | * @throws InvalidConfigException 52 | * @throws InvalidCallException 53 | * @throws Throwable 54 | */ 55 | public function testGetLastInsertIDWithException(): void 56 | { 57 | $db = $this->getConnection(true); 58 | 59 | $command = $db->createCommand(); 60 | $command->insert('item', ['name' => 'Yii2 starter', 'category_id' => 1])->execute(); 61 | $command->insert('item', ['name' => 'Yii3 starter', 'category_id' => 1])->execute(); 62 | 63 | $this->expectException(InvalidArgumentException::class); 64 | $this->expectExceptionMessage('PostgreSQL not support lastInsertId without sequence name.'); 65 | 66 | $db->getLastInsertId(); 67 | 68 | $db->close(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Provider/ColumnBuilderProvider.php: -------------------------------------------------------------------------------- 1 | 'double precision']], 14 | ['character varying(126)', ['type' => 'character varying', 'size' => 126]], 15 | ['bit varying(8)', ['type' => 'bit varying', 'size' => 8]], 16 | ['timestamp without time zone', ['type' => 'timestamp without time zone']], 17 | ['timestamp(3) with time zone', ['type' => 'timestamp with time zone', 'size' => 3]], 18 | ['time without time zone', ['type' => 'time without time zone']], 19 | ['time (3) with time zone', ['type' => 'time with time zone', 'size' => 3]], 20 | ['int[]', ['type' => 'int', 'dimension' => 1]], 21 | ['character varying(126)[][]', ['type' => 'character varying', 'size' => 126, 'dimension' => 2]], 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Provider/ColumnFactoryProvider.php: -------------------------------------------------------------------------------- 1 | [ColumnType::BINARY, ColumnType::BINARY, BinaryColumn::class], 138 | 'boolean' => [ColumnType::BOOLEAN, ColumnType::BOOLEAN, BooleanColumn::class], 139 | 'tinyint' => [ColumnType::TINYINT, ColumnType::TINYINT, IntegerColumn::class], 140 | 'smallint' => [ColumnType::SMALLINT, ColumnType::SMALLINT, IntegerColumn::class], 141 | 'integer' => [ColumnType::INTEGER, ColumnType::INTEGER, IntegerColumn::class], 142 | 'bigint' => [ColumnType::BIGINT, ColumnType::BIGINT, IntegerColumn::class], 143 | 'array' => [ColumnType::ARRAY, ColumnType::ARRAY, ArrayColumn::class], 144 | 'structured' => [ColumnType::STRUCTURED, ColumnType::STRUCTURED, StructuredColumn::class], 145 | ]; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests/Provider/ColumnProvider.php: -------------------------------------------------------------------------------- 1 | [ 95 | (new ArrayColumn())->column(new IntegerColumn()), 96 | [ 97 | [null, null], 98 | [[], '{}'], 99 | [[1, 2, 3, null], '{1,2,3,}'], 100 | ], 101 | ], 102 | 'arrayLazy' => [ 103 | $column = (new ArrayLazyColumn())->column(new IntegerColumn()), 104 | [ 105 | [null, null], 106 | [new LazyArray('{}', $column->getColumn()), '{}'], 107 | [new LazyArray('{1,2,3,}', $column->getColumn()), '{1,2,3,}'], 108 | ], 109 | ], 110 | 'structured' => [ 111 | (new StructuredColumn())->columns(['int' => new IntegerColumn(), 'bool' => new BooleanColumn()]), 112 | [ 113 | [null, null], 114 | [['int' => null, 'bool' => null], '(,)'], 115 | [['int' => 1, 'bool' => true], '(1,t)'], 116 | ], 117 | ], 118 | 'structuredLazy' => [ 119 | $structuredCol = (new StructuredLazyColumn())->columns(['int' => new IntegerColumn(), 'bool' => new BooleanColumn()]), 120 | [ 121 | [null, null], 122 | [new StructuredLazyArray('(,)', $structuredCol->getColumns()), '(,)'], 123 | [new StructuredLazyArray('(1,t)', $structuredCol->getColumns()), '(1,t)'], 124 | ], 125 | ], 126 | ]; 127 | } 128 | 129 | public static function phpTypecastArrayColumns() 130 | { 131 | $utcTimezone = new DateTimeZone('UTC'); 132 | 133 | return [ 134 | // [column, values] 135 | [ 136 | new IntegerColumn(), 137 | [ 138 | // [dimension, expected, typecast value] 139 | [1, [1, 2, 3, null], '{1,2,3,}'], 140 | [2, [[1, 2], [3], null], '{{1,2},{3},}'], 141 | ], 142 | ], 143 | [ 144 | new BigIntColumn(), 145 | [ 146 | [1, ['1', '2', '9223372036854775807'], '{1,2,9223372036854775807}'], 147 | [2, [['1', '2'], ['9223372036854775807']], '{{1,2},{9223372036854775807}}'], 148 | ], 149 | ], 150 | [ 151 | new DoubleColumn(), 152 | [ 153 | [1, [1.0, 2.2, null], '{1,2.2,}'], 154 | [2, [[1.0], [2.2, null]], '{{1},{2.2,}}'], 155 | ], 156 | ], 157 | [ 158 | new BooleanColumn(), 159 | [ 160 | [1, [true, false, null], '{t,f,}'], 161 | [2, [[true, false, null]], '{{t,f,}}'], 162 | ], 163 | ], 164 | [ 165 | new StringColumn(), 166 | [ 167 | [1, ['1', '2', '', null], '{1,2,"",}'], 168 | [2, [['1', '2'], [''], [null]], '{{1,2},{""},{NULL}}'], 169 | ], 170 | ], 171 | [ 172 | new BinaryColumn(), 173 | [ 174 | [1, ["\x10\x11", '', null], '{\x1011,"",}'], 175 | [2, [["\x10\x11"], ['', null]], '{{\x1011},{"",}}'], 176 | ], 177 | ], 178 | [ 179 | new DateTimeColumn(), 180 | [ 181 | [ 182 | 1, 183 | [ 184 | new DateTimeImmutable('2025-04-19 14:11:35', $utcTimezone), 185 | new DateTimeImmutable('2025-04-19 00:00:00', $utcTimezone), 186 | null, 187 | ], 188 | '{2025-04-19 14:11:35,2025-04-19 00:00:00,}', 189 | ], 190 | [ 191 | 2, 192 | [ 193 | [new DateTimeImmutable('2025-04-19 14:11:35', $utcTimezone)], 194 | [new DateTimeImmutable('2025-04-19 00:00:00', $utcTimezone), null], 195 | ], 196 | '{{2025-04-19 14:11:35},{2025-04-19 00:00:00,}}', 197 | ], 198 | ], 199 | ], 200 | [ 201 | new JsonColumn(), 202 | [ 203 | [1, [[1, 2, 3], null], '{"[1,2,3]",}'], 204 | [1, [[1, 2, 3]], '{{1,2,3}}'], 205 | [2, [[[1, 2, 3, null], null]], '{{"[1,2,3,null]",}}'], 206 | ], 207 | ], 208 | [ 209 | new BitColumn(), 210 | [ 211 | [1, [0b1011, 0b1001, null], '{1011,1001,}'], 212 | [2, [[0b1011, 0b1001, null]], '{{1011,1001,}}'], 213 | ], 214 | ], 215 | [ 216 | new StructuredColumn(), 217 | [ 218 | [1, [['10', 'USD'], null], '{"(10,USD)",}'], 219 | [2, [[['10', 'USD'], null]], '{{"(10,USD)",}}'], 220 | ], 221 | ], 222 | ]; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /tests/Provider/CommandPDOProvider.php: -------------------------------------------------------------------------------- 1 | 1, 15 | 'email' => 'user1@example.com', 16 | 'name' => 'user1', 17 | 'address' => 'address1', 18 | 'status' => 1, 19 | 'bool_status' => true, 20 | 'profile_id' => 1, 21 | ]; 22 | 23 | return $bindParam; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Provider/CommandProvider.php: -------------------------------------------------------------------------------- 1 | 'silverfire', 'is_active' => true, 'langs' => ['Ukrainian', 'Russian', 'English']] 33 | ), 34 | 1, 35 | 1.0, 36 | '', 37 | false, 38 | ], 39 | ], 40 | ['json_col', 'int_col', 'float_col', 'char_col', 'bool_col'], 41 | 'expected' => << [ 45 | ':qp0' => '{"username":"silverfire","is_active":true,"langs":["Ukrainian","Russian","English"]}', 46 | ':qp1' => 1, 47 | ':qp2' => 1.0, 48 | ':qp3' => '', 49 | ':qp4' => false, 50 | ], 51 | ]; 52 | 53 | $batchInsert['binds params from arrayExpression'] = [ 54 | '{{%type}}', 55 | [[new ArrayExpression([1, null, 3], 'int'), 1, 1.0, '', false]], 56 | ['intarray_col', 'int_col', 'float_col', 'char_col', 'bool_col'], 57 | 'expected' => << [':qp0' => 1, ':qp1' => null, ':qp2' => 3, ':qp3' => 1, ':qp4' => 1.0, ':qp5' => '', ':qp6' => false], 61 | ]; 62 | 63 | $batchInsert['casts string to int according to the table schema'] = [ 64 | '{{%type}}', 65 | [['3', '1.1', '', false]], 66 | ['int_col', 'float_col', 'char_col', 'bool_col'], 67 | 'expected' => << [':qp0' => 3, ':qp1' => 1.1, ':qp2' => '', ':qp3' => false], 71 | ]; 72 | 73 | $batchInsert['binds params from jsonbExpression'] = [ 74 | '{{%type}}', 75 | [[new JsonExpression(['a' => true]), 1, 1.1, '', false]], 76 | ['jsonb_col', 'int_col', 'float_col', 'char_col', 'bool_col'], 77 | 'expected' => << [':qp0' => '{"a":true}', ':qp1' => 1, ':qp2' => 1.1, ':qp3' => '', ':qp4' => false], 81 | ]; 82 | 83 | 84 | return $batchInsert; 85 | } 86 | 87 | public static function rawSql(): array 88 | { 89 | return array_merge(parent::rawSql(), [ 90 | [ 91 | 'SELECT * FROM customer WHERE id::integer IN (:in, :out)', 92 | [':in' => 1, ':out' => 2], 93 | << ColumnBuilder::integer()], ['col1'], null, IndexMethod::BTREE], 105 | [['col1' => ColumnBuilder::integer()], ['col1'], null, IndexMethod::HASH], 106 | [['col1' => ColumnBuilder::integer()], ['col1'], null, IndexMethod::BRIN], 107 | [['col1' => ColumnBuilder::array()], ['col1'], null, IndexMethod::GIN], 108 | [['col1' => 'point'], ['col1'], null, IndexMethod::GIST], 109 | [['col1' => 'point'], ['col1'], null, IndexMethod::SPGIST], 110 | ]; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/Provider/QuoterProvider.php: -------------------------------------------------------------------------------- 1 | new IntegerColumn( 22 | dbType: 'int4', 23 | primaryKey: true, 24 | notNull: true, 25 | autoIncrement: true, 26 | sequenceName: 'test_structured_type_id_seq', 27 | scale: 0, 28 | ), 29 | 'price_col' => new StructuredColumn( 30 | dbType: 'currency_money_structured', 31 | defaultValue: null, 32 | columns: [ 33 | 'value' => new DoubleColumn( 34 | ColumnType::DECIMAL, 35 | dbType: 'numeric', 36 | name: 'value', 37 | notNull: false, 38 | size: 10, 39 | scale: 2, 40 | defaultValue: null, 41 | ), 42 | 'currency_code' => new StringColumn( 43 | ColumnType::CHAR, 44 | dbType: 'bpchar', 45 | name: 'currency_code', 46 | notNull: false, 47 | size: 3, 48 | defaultValue: null, 49 | ), 50 | ], 51 | ), 52 | 'price_default' => new StructuredColumn( 53 | dbType: 'currency_money_structured', 54 | defaultValue: ['value' => 5.0, 'currency_code' => 'USD'], 55 | columns: [ 56 | 'value' => new DoubleColumn( 57 | ColumnType::DECIMAL, 58 | dbType: 'numeric', 59 | defaultValue: 5.0, 60 | name: 'value', 61 | notNull: false, 62 | size: 10, 63 | scale: 2, 64 | ), 65 | 'currency_code' => new StringColumn( 66 | ColumnType::CHAR, 67 | dbType: 'bpchar', 68 | defaultValue: 'USD', 69 | name: 'currency_code', 70 | notNull: false, 71 | size: 3, 72 | ), 73 | ], 74 | ), 75 | 'price_array' => new ArrayColumn( 76 | dbType: 'currency_money_structured', 77 | defaultValue: [ 78 | null, 79 | ['value' => 10.55, 'currency_code' => 'USD'], 80 | ['value' => -1.0, 'currency_code' => null], 81 | ], 82 | dimension: 1, 83 | column: new StructuredColumn( 84 | dbType: 'currency_money_structured', 85 | columns: [ 86 | 'value' => new DoubleColumn( 87 | ColumnType::DECIMAL, 88 | dbType: 'numeric', 89 | name: 'value', 90 | notNull: false, 91 | size: 10, 92 | scale: 2, 93 | defaultValue: null, 94 | ), 95 | 'currency_code' => new StringColumn( 96 | ColumnType::CHAR, 97 | dbType: 'bpchar', 98 | name: 'currency_code', 99 | notNull: false, 100 | size: 3, 101 | defaultValue: null, 102 | ), 103 | ], 104 | name: 'price_array', 105 | notNull: false, 106 | ), 107 | ), 108 | 'price_array2' => new ArrayColumn( 109 | dbType: 'currency_money_structured', 110 | dimension: 2, 111 | column: new StructuredColumn( 112 | dbType: 'currency_money_structured', 113 | columns: [ 114 | 'value' => new DoubleColumn( 115 | ColumnType::DECIMAL, 116 | dbType: 'numeric', 117 | name: 'value', 118 | notNull: false, 119 | size: 10, 120 | scale: 2, 121 | defaultValue: null, 122 | ), 123 | 'currency_code' => new StringColumn( 124 | ColumnType::CHAR, 125 | dbType: 'bpchar', 126 | name: 'currency_code', 127 | notNull: false, 128 | size: 3, 129 | defaultValue: null, 130 | ), 131 | ], 132 | name: 'price_array2', 133 | notNull: false, 134 | ), 135 | ), 136 | 'range_price_col' => new StructuredColumn( 137 | dbType: 'range_price_structured', 138 | defaultValue: [ 139 | 'price_from' => ['value' => 0.0, 'currency_code' => 'USD'], 140 | 'price_to' => ['value' => 100.0, 'currency_code' => 'USD'], 141 | ], 142 | columns: [ 143 | 'price_from' => new StructuredColumn( 144 | dbType: 'currency_money_structured', 145 | defaultValue: ['value' => 0.0, 'currency_code' => 'USD'], 146 | columns: [ 147 | 'value' => new DoubleColumn( 148 | ColumnType::DECIMAL, 149 | dbType: 'numeric', 150 | name: 'value', 151 | notNull: false, 152 | size: 10, 153 | scale: 2, 154 | defaultValue: 0.0, 155 | ), 156 | 'currency_code' => new StringColumn( 157 | ColumnType::CHAR, 158 | dbType: 'bpchar', 159 | name: 'currency_code', 160 | notNull: false, 161 | size: 3, 162 | defaultValue: 'USD', 163 | ), 164 | ], 165 | name: 'price_from', 166 | notNull: false, 167 | ), 168 | 'price_to' => new StructuredColumn( 169 | dbType: 'currency_money_structured', 170 | defaultValue: ['value' => 100.0, 'currency_code' => 'USD'], 171 | columns: [ 172 | 'value' => new DoubleColumn( 173 | ColumnType::DECIMAL, 174 | dbType: 'numeric', 175 | name: 'value', 176 | notNull: false, 177 | size: 10, 178 | scale: 2, 179 | defaultValue: 100.0, 180 | ), 181 | 'currency_code' => new StringColumn( 182 | ColumnType::CHAR, 183 | dbType: 'bpchar', 184 | name: 'currency_code', 185 | notNull: false, 186 | size: 3, 187 | defaultValue: 'USD', 188 | ), 189 | ], 190 | name: 'price_to', 191 | notNull: false, 192 | ), 193 | ], 194 | ), 195 | ], 196 | 'test_structured_type', 197 | ], 198 | ]; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /tests/QueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | getConnection(); 41 | 42 | $qb = $db->getQueryBuilder(); 43 | 44 | $this->expectException(NotSupportedException::class); 45 | $this->expectExceptionMessage( 46 | 'Yiisoft\Db\Pgsql\DDLQueryBuilder::addDefaultValue is not supported by PostgreSQL.' 47 | ); 48 | 49 | $qb->addDefaultValue('T_constraints_1', 'CN_pk', 'C_default', 1); 50 | 51 | $db->close(); 52 | } 53 | 54 | #[DataProviderExternal(QueryBuilderProvider::class, 'alterColumn')] 55 | public function testAlterColumn(string|ColumnInterface $type, string $expected): void 56 | { 57 | parent::testAlterColumn($type, $expected); 58 | } 59 | 60 | #[DataProviderExternal(QueryBuilderProvider::class, 'addForeignKey')] 61 | public function testAddForeignKey( 62 | string $name, 63 | string $table, 64 | array|string $columns, 65 | string $refTable, 66 | array|string $refColumns, 67 | string|null $delete, 68 | string|null $update, 69 | string $expected 70 | ): void { 71 | parent::testAddForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update, $expected); 72 | } 73 | 74 | #[DataProviderExternal(QueryBuilderProvider::class, 'addPrimaryKey')] 75 | public function testAddPrimaryKey(string $name, string $table, array|string $columns, string $expected): void 76 | { 77 | parent::testAddPrimaryKey($name, $table, $columns, $expected); 78 | } 79 | 80 | #[DataProviderExternal(QueryBuilderProvider::class, 'addUnique')] 81 | public function testAddUnique(string $name, string $table, array|string $columns, string $expected): void 82 | { 83 | parent::testAddUnique($name, $table, $columns, $expected); 84 | } 85 | 86 | #[DataProviderExternal(QueryBuilderProvider::class, 'batchInsert')] 87 | public function testBatchInsert( 88 | string $table, 89 | iterable $rows, 90 | array $columns, 91 | string $expected, 92 | array $expectedParams = [], 93 | ): void { 94 | parent::testBatchInsert($table, $rows, $columns, $expected, $expectedParams); 95 | } 96 | 97 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildCondition')] 98 | public function testBuildCondition( 99 | array|ExpressionInterface|string $condition, 100 | string|null $expected, 101 | array $expectedParams 102 | ): void { 103 | parent::testBuildCondition($condition, $expected, $expectedParams); 104 | } 105 | 106 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildLikeCondition')] 107 | public function testBuildLikeCondition( 108 | array|ExpressionInterface $condition, 109 | string $expected, 110 | array $expectedParams 111 | ): void { 112 | parent::testBuildLikeCondition($condition, $expected, $expectedParams); 113 | } 114 | 115 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildFrom')] 116 | public function testBuildWithFrom(mixed $table, string $expectedSql, array $expectedParams = []): void 117 | { 118 | parent::testBuildWithFrom($table, $expectedSql, $expectedParams); 119 | } 120 | 121 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildWhereExists')] 122 | public function testBuildWithWhereExists(string $cond, string $expectedQuerySql): void 123 | { 124 | parent::testBuildWithWhereExists($cond, $expectedQuerySql); 125 | } 126 | 127 | public function testCheckIntegrity(): void 128 | { 129 | $db = $this->getConnection(); 130 | 131 | $qb = $db->getQueryBuilder(); 132 | 133 | $this->assertSame( 134 | <<checkIntegrity('public', 'item'), 138 | ); 139 | 140 | $db->close(); 141 | } 142 | 143 | public function testCheckIntegrityExecute(): void 144 | { 145 | $db = $this->getConnection(true); 146 | 147 | $db->createCommand()->checkIntegrity('public', 'item', false)->execute(); 148 | $command = $db->createCommand( 149 | <<execute(); 154 | 155 | $db->createCommand()->checkIntegrity('public', 'item')->execute(); 156 | 157 | $this->expectException(IntegrityException::class); 158 | $this->expectExceptionMessage( 159 | 'SQLSTATE[23503]: Foreign key violation: 7 ERROR: insert or update on table "item" violates foreign key constraint "item_category_id_fkey"' 160 | ); 161 | 162 | $command->execute(); 163 | 164 | $db->close(); 165 | } 166 | 167 | public function testCreateTable(): void 168 | { 169 | $db = $this->getConnection(); 170 | 171 | $qb = $db->getQueryBuilder(); 172 | 173 | $this->assertSame( 174 | <<createTable( 184 | 'test', 185 | [ 186 | 'id' => 'pk', 187 | 'name' => 'string(255) NOT NULL', 188 | 'email' => 'string(255) NOT NULL', 189 | 'status' => 'integer NOT NULL', 190 | 'created_at' => 'datetime NOT NULL', 191 | ], 192 | ), 193 | ); 194 | 195 | $db->close(); 196 | } 197 | 198 | #[DataProviderExternal(QueryBuilderProvider::class, 'delete')] 199 | public function testDelete(string $table, array|string $condition, string $expectedSQL, array $expectedParams): void 200 | { 201 | parent::testDelete($table, $condition, $expectedSQL, $expectedParams); 202 | } 203 | 204 | public function testDropCommentFromColumn(): void 205 | { 206 | $db = $this->getConnection(true); 207 | 208 | $qb = $db->getQueryBuilder(); 209 | 210 | $this->assertSame( 211 | <<dropCommentFromColumn('customer', 'id'), 215 | ); 216 | 217 | $db->close(); 218 | } 219 | 220 | public function testDropDefaultValue(): void 221 | { 222 | $db = $this->getConnection(true); 223 | 224 | $qb = $db->getQueryBuilder(); 225 | 226 | $this->expectException(NotSupportedException::class); 227 | $this->expectExceptionMessage( 228 | 'Yiisoft\Db\Pgsql\DDLQueryBuilder::dropDefaultValue is not supported by PostgreSQL.' 229 | ); 230 | 231 | $qb->dropDefaultValue('T_constraints_1', 'CN_pk'); 232 | 233 | $db->close(); 234 | } 235 | 236 | public function testDropIndex(): void 237 | { 238 | $db = $this->getConnection(); 239 | 240 | $qb = $db->getQueryBuilder(); 241 | 242 | $this->assertSame( 243 | <<dropIndex('{{table}}', 'index'), 247 | ); 248 | 249 | $this->assertSame( 250 | <<dropIndex('schema.table', 'index'), 254 | ); 255 | 256 | $this->assertSame( 257 | <<dropIndex('{{schema.table}}', 'index'), 261 | ); 262 | 263 | $this->assertEquals( 264 | <<dropIndex('{{schema2.table}}', 'schema.index'), 268 | ); 269 | 270 | $this->assertSame( 271 | <<dropIndex('{{schema.%table}}', 'index'), 275 | ); 276 | 277 | $this->assertSame( 278 | <<dropIndex('{{%schema.table}}', 'index'), 282 | ); 283 | 284 | $db->close(); 285 | } 286 | 287 | #[DataProviderExternal(QueryBuilderProvider::class, 'insert')] 288 | public function testInsert( 289 | string $table, 290 | array|QueryInterface $columns, 291 | array $params, 292 | string $expectedSQL, 293 | array $expectedParams 294 | ): void { 295 | parent::testInsert($table, $columns, $params, $expectedSQL, $expectedParams); 296 | } 297 | 298 | #[DataProviderExternal(QueryBuilderProvider::class, 'insertWithReturningPks')] 299 | public function testInsertWithReturningPks( 300 | string $table, 301 | array|QueryInterface $columns, 302 | array $params, 303 | string $expectedSQL, 304 | array $expectedParams 305 | ): void { 306 | parent::testInsertWithReturningPks($table, $columns, $params, $expectedSQL, $expectedParams); 307 | } 308 | 309 | public function testRenameTable(): void 310 | { 311 | $db = $this->getConnection(); 312 | 313 | $qb = $db->getQueryBuilder(); 314 | 315 | $this->assertSame( 316 | <<renameTable('alpha', 'alpha-test'), 320 | ); 321 | 322 | $db->close(); 323 | } 324 | 325 | public function testResetSequence(): void 326 | { 327 | $db = $this->getConnection(true); 328 | 329 | $qb = $db->getQueryBuilder(); 330 | 331 | $this->assertSame( 332 | <<resetSequence('item'), 336 | ); 337 | 338 | $this->assertSame( 339 | <<resetSequence('item', 4), 343 | ); 344 | 345 | $this->assertEquals( 346 | <<resetSequence('item', '1'), 350 | ); 351 | 352 | $db->close(); 353 | } 354 | 355 | public function testResetSequencePgsql12(): void 356 | { 357 | if (version_compare($this->getConnection()->getServerInfo()->getVersion(), '12.0', '<')) { 358 | $this->markTestSkipped('PostgreSQL < 12.0 does not support GENERATED AS IDENTITY columns.'); 359 | } 360 | 361 | $this->setFixture('pgsql12.sql'); 362 | 363 | $db = $this->getConnection(true); 364 | 365 | $qb = $db->getQueryBuilder(); 366 | 367 | $this->assertSame( 368 | <<resetSequence('item_12'), 372 | ); 373 | 374 | $this->assertSame( 375 | <<resetSequence('item_12', 4), 379 | ); 380 | 381 | $this->assertSame( 382 | <<resetSequence('item', '1'), 386 | ); 387 | 388 | $db->close(); 389 | } 390 | 391 | public function testTruncateTable(): void 392 | { 393 | $db = $this->getConnection(); 394 | 395 | $qb = $db->getQueryBuilder(); 396 | $sql = $qb->truncateTable('customer'); 397 | 398 | $this->assertSame( 399 | <<truncateTable('T_constraints_1'); 406 | 407 | $this->assertSame( 408 | <<close(); 415 | } 416 | 417 | #[DataProviderExternal(QueryBuilderProvider::class, 'update')] 418 | public function testUpdate( 419 | string $table, 420 | array $columns, 421 | array|string $condition, 422 | array $params, 423 | string $expectedSql, 424 | array $expectedParams, 425 | ): void { 426 | parent::testUpdate($table, $columns, $condition, $params, $expectedSql, $expectedParams); 427 | } 428 | 429 | #[DataProviderExternal(QueryBuilderProvider::class, 'upsert')] 430 | public function testUpsert( 431 | string $table, 432 | array|QueryInterface $insertColumns, 433 | array|bool $updateColumns, 434 | string $expectedSql, 435 | array $expectedParams 436 | ): void { 437 | parent::testUpsert($table, $insertColumns, $updateColumns, $expectedSql, $expectedParams); 438 | } 439 | 440 | #[DataProviderExternal(QueryBuilderProvider::class, 'upsertWithReturningPks')] 441 | public function testUpsertWithReturningPks( 442 | string $table, 443 | array|QueryInterface $insertColumns, 444 | array|bool $updateColumns, 445 | string $expectedSql, 446 | array $expectedParams 447 | ): void { 448 | parent::testUpsertWithReturningPks($table, $insertColumns, $updateColumns, $expectedSql, $expectedParams); 449 | } 450 | 451 | #[DataProviderExternal(QueryBuilderProvider::class, 'selectScalar')] 452 | public function testSelectScalar(array|bool|float|int|string $columns, string $expected): void 453 | { 454 | parent::testSelectScalar($columns, $expected); 455 | } 456 | 457 | public function testArrayOverlapsConditionBuilder(): void 458 | { 459 | $db = $this->getConnection(); 460 | $qb = $db->getQueryBuilder(); 461 | 462 | $params = []; 463 | $sql = $qb->buildExpression(new ArrayOverlapsCondition('column', [1, 2, 3]), $params); 464 | 465 | $this->assertSame('"column"::text[] && ARRAY[:qp0,:qp1,:qp2]::text[]', $sql); 466 | $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); 467 | 468 | // Test column as Expression 469 | $params = []; 470 | $sql = $qb->buildExpression(new ArrayOverlapsCondition(new Expression('column'), [1, 2, 3]), $params); 471 | 472 | $this->assertSame('column::text[] && ARRAY[:qp0,:qp1,:qp2]::text[]', $sql); 473 | $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); 474 | 475 | $db->close(); 476 | } 477 | 478 | public function testJsonOverlapsConditionBuilder(): void 479 | { 480 | $db = $this->getConnection(); 481 | $qb = $db->getQueryBuilder(); 482 | 483 | $params = []; 484 | $sql = $qb->buildExpression(new JsonOverlapsCondition('column', [1, 2, 3]), $params); 485 | 486 | $this->assertSame( 487 | 'ARRAY(SELECT jsonb_array_elements_text("column"::jsonb)) && ARRAY[:qp0,:qp1,:qp2]::text[]', 488 | $sql 489 | ); 490 | $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); 491 | 492 | $db->close(); 493 | } 494 | 495 | #[DataProviderExternal(QueryBuilderProvider::class, 'overlapsCondition')] 496 | public function testOverlapsCondition(iterable|ExpressionInterface $values, int $expectedCount): void 497 | { 498 | $db = $this->getConnection(); 499 | $query = new Query($db); 500 | 501 | $count = $query 502 | ->from('array_and_json_types') 503 | ->where(new ArrayOverlapsCondition('intarray_col', $values)) 504 | ->count(); 505 | 506 | $this->assertSame($expectedCount, $count); 507 | 508 | $count = $query 509 | ->from('array_and_json_types') 510 | ->setWhere(new JsonOverlapsCondition('json_col', $values)) 511 | ->count(); 512 | 513 | $this->assertSame($expectedCount, $count); 514 | 515 | $count = $query 516 | ->from('array_and_json_types') 517 | ->setWhere(new JsonOverlapsCondition('jsonb_col', $values)) 518 | ->count(); 519 | 520 | $this->assertSame($expectedCount, $count); 521 | 522 | $db->close(); 523 | } 524 | 525 | #[DataProviderExternal(QueryBuilderProvider::class, 'overlapsCondition')] 526 | public function testOverlapsConditionOperator(iterable|ExpressionInterface $values, int $expectedCount): void 527 | { 528 | $db = $this->getConnection(); 529 | $query = new Query($db); 530 | 531 | $count = $query 532 | ->from('array_and_json_types') 533 | ->where(['array overlaps', 'intarray_col', $values]) 534 | ->count(); 535 | 536 | $this->assertSame($expectedCount, $count); 537 | 538 | $count = $query 539 | ->from('array_and_json_types') 540 | ->setWhere(['json overlaps', 'json_col', $values]) 541 | ->count(); 542 | 543 | $this->assertSame($expectedCount, $count); 544 | 545 | $count = $query 546 | ->from('array_and_json_types') 547 | ->setWhere(['json overlaps', 'jsonb_col', $values]) 548 | ->count(); 549 | 550 | $this->assertSame($expectedCount, $count); 551 | 552 | $db->close(); 553 | } 554 | 555 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildColumnDefinition')] 556 | public function testBuildColumnDefinition(string $expected, ColumnInterface|string $column): void 557 | { 558 | parent::testBuildColumnDefinition($expected, $column); 559 | } 560 | 561 | #[DataProviderExternal(QueryBuilderProvider::class, 'prepareParam')] 562 | public function testPrepareParam(string $expected, mixed $value, int $type): void 563 | { 564 | parent::testPrepareParam($expected, $value, $type); 565 | } 566 | 567 | #[DataProviderExternal(QueryBuilderProvider::class, 'prepareValue')] 568 | public function testPrepareValue(string $expected, mixed $value): void 569 | { 570 | parent::testPrepareValue($expected, $value); 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /tests/QueryTest.php: -------------------------------------------------------------------------------- 1 | getConnection(true); 31 | 32 | $selectExpression = "(customer.name || ' in ' || p.description) AS name"; 33 | 34 | $result = (new Query($db)) 35 | ->select([$selectExpression]) 36 | ->from('customer') 37 | ->innerJoin('profile p', '[[customer]].[[profile_id]] = [[p]].[[id]]') 38 | ->indexBy('id') 39 | ->column(); 40 | 41 | $this->assertSame([1 => 'user1 in profile customer 1', 3 => 'user3 in profile customer 3'], $result); 42 | 43 | $db->close(); 44 | } 45 | 46 | /** 47 | * @throws Exception 48 | * @throws InvalidConfigException 49 | * @throws Throwable 50 | */ 51 | public function testBooleanValues(): void 52 | { 53 | $db = $this->getConnection(true); 54 | 55 | $command = $db->createCommand(); 56 | $command->insertBatch('bool_values', [[true], [false]], ['bool_col'])->execute(); 57 | 58 | $this->assertSame(1, (new Query($db))->from('bool_values')->where('bool_col = TRUE')->count()); 59 | $this->assertSame(1, (new Query($db))->from('bool_values')->where('bool_col = FALSE')->count()); 60 | $this->assertSame( 61 | 2, 62 | (new Query($db))->from('bool_values')->where('bool_col IN (TRUE, FALSE)')->count() 63 | ); 64 | $this->assertSame(1, (new Query($db))->from('bool_values')->where(['bool_col' => true])->count()); 65 | $this->assertSame(1, (new Query($db))->from('bool_values')->where(['bool_col' => false])->count()); 66 | $this->assertSame( 67 | 2, 68 | (new Query($db))->from('bool_values')->where(['bool_col' => [true, false]])->count() 69 | ); 70 | $this->assertSame( 71 | 1, 72 | (new Query($db))->from('bool_values')->where('bool_col = :bool_col', ['bool_col' => true])->count() 73 | ); 74 | $this->assertSame( 75 | 1, 76 | (new Query($db))->from('bool_values')->where('bool_col = :bool_col', ['bool_col' => false])->count() 77 | ); 78 | 79 | $db->close(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/QuoterTest.php: -------------------------------------------------------------------------------- 1 | ColumnBuilder::integer(), 33 | 'currency_code' => ColumnBuilder::string()->defaultValue('USD'), 34 | ]); 35 | 36 | return [ 37 | [null, null, 'NULL', []], 38 | [[null, null], null, 'ROW(:qp0,:qp1)', [':qp0' => null, ':qp1' => null]], 39 | [['5', 'USD'], null, 'ROW(:qp0,:qp1)', [':qp0' => '5', ':qp1' => 'USD']], 40 | [ 41 | new ArrayIterator(['5', 'USD']), 42 | $column, 43 | 'ROW(:qp0,:qp1)::currency_money', 44 | [':qp0' => 5, ':qp1' => 'USD'], 45 | ], 46 | [ 47 | new StructuredLazyArray('(5,USD)'), 48 | $column, 49 | ':qp0::currency_money', 50 | [':qp0' => new Param('(5,USD)', DataType::STRING)], 51 | ], 52 | [ 53 | new \Yiisoft\Db\Schema\Data\StructuredLazyArray('["5","USD"]'), 54 | $column, 55 | 'ROW(:qp0,:qp1)::currency_money', 56 | [':qp0' => 5, ':qp1' => 'USD'], 57 | ], 58 | [ 59 | new LazyArray('{5,USD}'), 60 | $column, 61 | 'ROW(:qp0,:qp1)::currency_money', 62 | [':qp0' => 5, ':qp1' => 'USD'], 63 | ], 64 | [ 65 | new JsonLazyArray('["5","USD"]'), 66 | $column, 67 | 'ROW(:qp0,:qp1)::currency_money', 68 | [':qp0' => 5, ':qp1' => 'USD'], 69 | ], 70 | [ 71 | (new Query(self::getDb()))->select('price')->from('product')->where(['id' => 1]), 72 | null, 73 | '(SELECT "price" FROM "product" WHERE "id"=:qp0)', 74 | [':qp0' => 1], 75 | ], 76 | [ 77 | (new Query(self::getDb()))->select('price')->from('product')->where(['id' => 1]), 78 | 'currency_money', 79 | '(SELECT "price" FROM "product" WHERE "id"=:qp0)::currency_money', 80 | [':qp0' => 1], 81 | ], 82 | [ 83 | ['value' => '5', 'currency_code' => 'USD'], 84 | $column, 85 | 'ROW(:qp0,:qp1)::currency_money', 86 | [':qp0' => 5, ':qp1' => 'USD'], 87 | ], 88 | [ 89 | ['currency_code' => 'USD', 'value' => '5'], 90 | $column, 91 | 'ROW(:qp0,:qp1)::currency_money', 92 | [':qp0' => 5, ':qp1' => 'USD'], 93 | ], 94 | [['value' => '5'], $column, 'ROW(:qp0,:qp1)::currency_money', [':qp0' => 5, ':qp1' => 'USD']], 95 | [['value' => '5'], null, 'ROW(:qp0)', [':qp0' => '5']], 96 | [ 97 | ['value' => '5', 'currency_code' => 'USD', 'extra' => 'value'], 98 | $column, 99 | 'ROW(:qp0,:qp1)::currency_money', 100 | [':qp0' => 5, ':qp1' => 'USD'], 101 | ], 102 | [(object) ['value' => '5', 'currency_code' => 'USD'], 103 | 'currency_money', 104 | 'ROW(:qp0,:qp1)::currency_money', 105 | [':qp0' => 5, ':qp1' => 'USD'], 106 | ], 107 | ['(5,USD)', null, ':qp0', [':qp0' => new Param('(5,USD)', DataType::STRING)]], 108 | ]; 109 | } 110 | 111 | #[DataProvider('buildProvider')] 112 | public function testBuild( 113 | array|object|string|null $value, 114 | AbstractStructuredColumn|string|null $type, 115 | string $expected, 116 | array $expectedParams 117 | ): void { 118 | $db = $this->getConnection(); 119 | $qb = $db->getQueryBuilder(); 120 | 121 | $params = []; 122 | $builder = new StructuredExpressionBuilder($qb); 123 | $expression = new StructuredExpression($value, $type); 124 | 125 | $this->assertSame($expected, $builder->build($expression, $params)); 126 | $this->assertEquals($expectedParams, $params); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/StructuredParserTest.php: -------------------------------------------------------------------------------- 1 | assertSame([null], $parser->parse('()')); 20 | $this->assertSame([0 => null, 1 => null], $parser->parse('(,)')); 21 | $this->assertSame([0 => '10.0', 1 => 'USD'], $parser->parse('(10.0,USD)')); 22 | $this->assertSame([0 => '1', 1 => '-2', 2 => null, 3 => '42'], $parser->parse('(1,-2,,42)')); 23 | $this->assertSame([0 => ''], $parser->parse('("")')); 24 | $this->assertSame( 25 | [0 => '[",","null",true,"false","f"]'], 26 | $parser->parse('("[\",\",\"null\",true,\"false\",\"f\"]")') 27 | ); 28 | 29 | // Default values can have any expressions 30 | $this->assertSame(null, $parser->parse("'(10.0,USD)::structured_type'")); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Support/Fixture/pgsql.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS "composite_fk" CASCADE; 2 | DROP TABLE IF EXISTS "order_item" CASCADE; 3 | DROP TABLE IF EXISTS "item" CASCADE; 4 | DROP SEQUENCE IF EXISTS "nextval_item_id_seq_2" CASCADE; 5 | DROP TABLE IF EXISTS "order_item_with_null_fk" CASCADE; 6 | DROP TABLE IF EXISTS "order" CASCADE; 7 | DROP TABLE IF EXISTS "order_with_null_fk" CASCADE; 8 | DROP TABLE IF EXISTS "category" CASCADE; 9 | DROP TABLE IF EXISTS "customer" CASCADE; 10 | DROP TABLE IF EXISTS "profile" CASCADE; 11 | DROP TABLE IF EXISTS "quoter" CASCADE; 12 | DROP TABLE IF EXISTS "type" CASCADE; 13 | DROP TABLE IF EXISTS "null_values" CASCADE; 14 | DROP TABLE IF EXISTS "negative_default_values" CASCADE; 15 | DROP TABLE IF EXISTS "constraints" CASCADE; 16 | DROP TABLE IF EXISTS "bool_values" CASCADE; 17 | DROP TABLE IF EXISTS "animal" CASCADE; 18 | DROP TABLE IF EXISTS "default_pk" CASCADE; 19 | DROP TABLE IF EXISTS "notauto_pk" CASCADE; 20 | DROP TABLE IF EXISTS "document" CASCADE; 21 | DROP TABLE IF EXISTS "comment" CASCADE; 22 | DROP TABLE IF EXISTS "dossier"; 23 | DROP TABLE IF EXISTS "employee"; 24 | DROP TABLE IF EXISTS "department"; 25 | DROP TABLE IF EXISTS "alpha"; 26 | DROP TABLE IF EXISTS "beta"; 27 | DROP VIEW IF EXISTS "animal_view"; 28 | DROP VIEW IF EXISTS "T_constraints_4_view"; 29 | DROP VIEW IF EXISTS "T_constraints_3_view"; 30 | DROP VIEW IF EXISTS "T_constraints_2_view"; 31 | DROP VIEW IF EXISTS "T_constraints_1_view"; 32 | DROP TABLE IF EXISTS "T_constraints_6"; 33 | DROP TABLE IF EXISTS "T_constraints_5"; 34 | DROP TABLE IF EXISTS "T_constraints_4"; 35 | DROP TABLE IF EXISTS "T_constraints_3"; 36 | DROP TABLE IF EXISTS "T_constraints_2"; 37 | DROP TABLE IF EXISTS "T_constraints_1"; 38 | DROP TABLE IF EXISTS "T_upsert"; 39 | DROP TABLE IF EXISTS "T_upsert_1"; 40 | DROP TABLE IF EXISTS "table_with_array_col"; 41 | DROP TABLE IF EXISTS "table_uuid"; 42 | 43 | DROP SCHEMA IF EXISTS "schema1" CASCADE; 44 | DROP SCHEMA IF EXISTS "schema2" CASCADE; 45 | 46 | CREATE SCHEMA "schema1"; 47 | CREATE SCHEMA "schema2"; 48 | 49 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 50 | 51 | CREATE TABLE "constraints" 52 | ( 53 | id integer not null, 54 | field1 varchar(255) 55 | ); 56 | 57 | CREATE TABLE "profile" ( 58 | id serial not null primary key, 59 | description varchar(128) NOT NULL 60 | ); 61 | 62 | CREATE TABLE "schema1"."profile" ( 63 | id serial not null primary key, 64 | description varchar(128) NOT NULL 65 | ); 66 | 67 | CREATE TABLE "quoter" ( 68 | id serial not null primary key, 69 | name varchar(16) NOT NULL, 70 | description varchar(128) NOT NULL 71 | ); 72 | 73 | CREATE TABLE "customer" ( 74 | id serial not null primary key, 75 | email varchar(128) NOT NULL, 76 | name varchar(128), 77 | address text, 78 | status integer DEFAULT 0, 79 | bool_status boolean DEFAULT FALSE, 80 | profile_id integer 81 | ); 82 | 83 | comment on column public.customer.email is 'someone@example.com'; 84 | 85 | CREATE TABLE "category" ( 86 | id serial not null primary key, 87 | name varchar(128) NOT NULL 88 | ); 89 | 90 | CREATE TABLE "item" ( 91 | id serial not null primary key, 92 | name varchar(128) NOT NULL, 93 | category_id integer NOT NULL references "category"(id) on UPDATE CASCADE on DELETE CASCADE 94 | ); 95 | CREATE SEQUENCE "nextval_item_id_seq_2"; 96 | 97 | CREATE TABLE "order" ( 98 | id serial not null primary key, 99 | customer_id integer NOT NULL references "customer"(id) on UPDATE CASCADE on DELETE CASCADE, 100 | created_at integer NOT NULL, 101 | total decimal(10,0) NOT NULL 102 | ); 103 | 104 | CREATE TABLE "order_with_null_fk" ( 105 | id serial not null primary key, 106 | customer_id integer, 107 | created_at integer NOT NULL, 108 | total decimal(10,0) NOT NULL 109 | ); 110 | 111 | CREATE TABLE "order_item" ( 112 | order_id integer NOT NULL references "order"(id) on UPDATE CASCADE on DELETE CASCADE, 113 | item_id integer NOT NULL references "item"(id) on UPDATE CASCADE on DELETE CASCADE, 114 | quantity integer NOT NULL, 115 | subtotal decimal(10,0) NOT NULL, 116 | PRIMARY KEY (order_id,item_id) 117 | ); 118 | 119 | CREATE TABLE "order_item_with_null_fk" ( 120 | order_id integer, 121 | item_id integer, 122 | quantity integer NOT NULL, 123 | subtotal decimal(10,0) NOT NULL 124 | ); 125 | 126 | CREATE TABLE "composite_fk" ( 127 | id integer NOT NULL, 128 | order_id integer NOT NULL, 129 | item_id integer NOT NULL, 130 | PRIMARY KEY (id), 131 | CONSTRAINT FK_composite_fk_order_item FOREIGN KEY (order_id, item_id) REFERENCES "order_item" (order_id, item_id) ON DELETE CASCADE 132 | ); 133 | 134 | CREATE TABLE "null_values" ( 135 | id serial NOT NULL, 136 | var1 INT NULL, 137 | var2 INT NULL, 138 | var3 INT DEFAULT NULL, 139 | stringcol VARCHAR(32) DEFAULT NULL, 140 | PRIMARY KEY (id) 141 | ); 142 | 143 | CREATE TABLE "type" ( 144 | int_col integer NOT NULL, 145 | int_col2 integer DEFAULT '1', 146 | tinyint_col smallint DEFAULT '1', 147 | smallint_col smallint DEFAULT '1', 148 | char_col char(100) NOT NULL, 149 | char_col2 varchar(100) DEFAULT 'some''thing', 150 | char_col3 text, 151 | char_col4 character varying DEFAULT E'first line\nsecond line', 152 | float_col double precision NOT NULL, 153 | float_col2 double precision DEFAULT '1.23', 154 | blob_col bytea DEFAULT 'a binary value', 155 | numeric_col decimal(5,2) DEFAULT '33.22', 156 | timestamp_col timestamp NOT NULL DEFAULT '2002-01-01 00:00:00', 157 | timestamp_default TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 158 | bool_col boolean NOT NULL, 159 | bool_col2 boolean DEFAULT TRUE, 160 | bit_col BIT(8) NOT NULL DEFAULT B'10000010', -- 130 161 | varbit_col VARBIT NOT NULL DEFAULT '100'::bit, -- 4 162 | bigint_col BIGINT, 163 | intarray_col integer[], 164 | numericarray_col numeric(5,2)[], 165 | varchararray_col varchar(100)[], 166 | textarray2_col text[][], 167 | json_col json DEFAULT '{"a":1}', 168 | jsonb_col jsonb, 169 | jsonarray_col json[] 170 | ); 171 | 172 | CREATE TABLE "bool_values" ( 173 | id serial not null primary key, 174 | bool_col bool, 175 | default_true bool not null default TRUE, 176 | default_qtrueq boolean not null default 'TRUE', 177 | default_t boolean not null default 'T', 178 | default_yes boolean not null default 'yes', 179 | default_on boolean not null default 'on', 180 | default_1 boolean not null default '1', 181 | default_false boolean not null default FALSE, 182 | default_qfalseq boolean not null default 'FALSE', 183 | default_f boolean not null default 'F', 184 | default_no boolean not null default 'no', 185 | default_off boolean not null default 'off', 186 | default_0 boolean not null default '0', 187 | default_array boolean[] not null default '{null,TRUE,"TRUE",T,yes,on,1,FALSE,"FALSE",F,no,off,0}' 188 | ); 189 | 190 | CREATE TABLE "negative_default_values" ( 191 | tinyint_col smallint default '-123', 192 | smallint_col smallint default '-123', 193 | int_col integer default '-123', 194 | bigint_col bigint default '-123', 195 | float_col double precision default '-12345.6789', 196 | numeric_col decimal(5,2) default '-33.22' 197 | ); 198 | 199 | CREATE TABLE "animal" ( 200 | id serial primary key, 201 | type varchar(255) not null 202 | ); 203 | 204 | CREATE TABLE "default_pk" ( 205 | id integer not null default 5 primary key, 206 | type varchar(255) not null 207 | ); 208 | 209 | CREATE TABLE "notauto_pk" ( 210 | id_1 INTEGER, 211 | id_2 DECIMAL(5,2), 212 | type VARCHAR(255) NOT NULL, 213 | PRIMARY KEY (id_1, id_2) 214 | ); 215 | 216 | CREATE TABLE "document" ( 217 | id serial primary key, 218 | title varchar(255) not null, 219 | content text, 220 | version integer not null default 0 221 | ); 222 | 223 | CREATE TABLE "comment" ( 224 | id serial primary key, 225 | name varchar(255) not null, 226 | message text not null 227 | ); 228 | 229 | CREATE TABLE "department" ( 230 | id serial not null primary key, 231 | title VARCHAR(255) NOT NULL 232 | ); 233 | 234 | CREATE TABLE "employee" ( 235 | id INTEGER NOT NULL not null, 236 | department_id INTEGER NOT NULL, 237 | first_name VARCHAR(255) NOT NULL, 238 | last_name VARCHAR(255) NOT NULL, 239 | PRIMARY KEY (id, department_id) 240 | ); 241 | 242 | CREATE TABLE "dossier" ( 243 | id serial not null primary key, 244 | department_id INTEGER NOT NULL, 245 | employee_id INTEGER NOT NULL, 246 | summary VARCHAR(255) NOT NULL 247 | ); 248 | 249 | CREATE TABLE "alpha" ( 250 | id INTEGER NOT NULL, 251 | string_identifier VARCHAR(255) NOT NULL, 252 | PRIMARY KEY (id) 253 | ); 254 | 255 | CREATE TABLE "beta" ( 256 | id INTEGER NOT NULL, 257 | alpha_string_identifier VARCHAR(255) NOT NULL, 258 | PRIMARY KEY (id) 259 | ); 260 | 261 | CREATE VIEW "animal_view" AS SELECT * FROM "animal"; 262 | 263 | INSERT INTO "animal" (type) VALUES ('yiiunit\data\ar\Cat'); 264 | INSERT INTO "animal" (type) VALUES ('yiiunit\data\ar\Dog'); 265 | 266 | 267 | INSERT INTO "profile" (description) VALUES ('profile customer 1'); 268 | INSERT INTO "profile" (description) VALUES ('profile customer 3'); 269 | 270 | INSERT INTO "schema1"."profile" (description) VALUES ('profile customer 1'); 271 | INSERT INTO "schema1"."profile" (description) VALUES ('profile customer 3'); 272 | 273 | INSERT INTO "customer" (email, name, address, status, bool_status, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, true, 1); 274 | INSERT INTO "customer" (email, name, address, status, bool_status) VALUES ('user2@example.com', 'user2', 'address2', 1, true); 275 | INSERT INTO "customer" (email, name, address, status, bool_status, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, false, 2); 276 | 277 | INSERT INTO "category" (name) VALUES ('Books'); 278 | INSERT INTO "category" (name) VALUES ('Movies'); 279 | 280 | INSERT INTO "item" (name, category_id) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); 281 | INSERT INTO "item" (name, category_id) VALUES ('Yii 1.1 Application Development Cookbook', 1); 282 | INSERT INTO "item" (name, category_id) VALUES ('Ice Age', 2); 283 | INSERT INTO "item" (name, category_id) VALUES ('Toy Story', 2); 284 | INSERT INTO "item" (name, category_id) VALUES ('Cars', 2); 285 | 286 | INSERT INTO "order" (customer_id, created_at, total) VALUES (1, 1325282384, 110.0); 287 | INSERT INTO "order" (customer_id, created_at, total) VALUES (2, 1325334482, 33.0); 288 | INSERT INTO "order" (customer_id, created_at, total) VALUES (2, 1325502201, 40.0); 289 | 290 | INSERT INTO "order_with_null_fk" (customer_id, created_at, total) VALUES (1, 1325282384, 110.0); 291 | INSERT INTO "order_with_null_fk" (customer_id, created_at, total) VALUES (2, 1325334482, 33.0); 292 | INSERT INTO "order_with_null_fk" (customer_id, created_at, total) VALUES (2, 1325502201, 40.0); 293 | 294 | INSERT INTO "order_item" (order_id, item_id, quantity, subtotal) VALUES (1, 1, 1, 30.0); 295 | INSERT INTO "order_item" (order_id, item_id, quantity, subtotal) VALUES (1, 2, 2, 40.0); 296 | INSERT INTO "order_item" (order_id, item_id, quantity, subtotal) VALUES (2, 4, 1, 10.0); 297 | INSERT INTO "order_item" (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); 298 | INSERT INTO "order_item" (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); 299 | INSERT INTO "order_item" (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); 300 | 301 | INSERT INTO "order_item_with_null_fk" (order_id, item_id, quantity, subtotal) VALUES (1, 1, 1, 30.0); 302 | INSERT INTO "order_item_with_null_fk" (order_id, item_id, quantity, subtotal) VALUES (1, 2, 2, 40.0); 303 | INSERT INTO "order_item_with_null_fk" (order_id, item_id, quantity, subtotal) VALUES (2, 4, 1, 10.0); 304 | INSERT INTO "order_item_with_null_fk" (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); 305 | INSERT INTO "order_item_with_null_fk" (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); 306 | INSERT INTO "order_item_with_null_fk" (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); 307 | 308 | INSERT INTO "document" (title, content, version) VALUES ('Yii 2.0 guide', 'This is Yii 2.0 guide', 0); 309 | 310 | INSERT INTO "department" (id, title) VALUES (1, 'IT'); 311 | INSERT INTO "department" (id, title) VALUES (2, 'accounting'); 312 | 313 | INSERT INTO "employee" (id, department_id, first_name, last_name) VALUES (1, 1, 'John', 'Doe'); 314 | INSERT INTO "employee" (id, department_id, first_name, last_name) VALUES (1, 2, 'Ann', 'Smith'); 315 | INSERT INTO "employee" (id, department_id, first_name, last_name) VALUES (2, 2, 'Will', 'Smith'); 316 | 317 | INSERT INTO "dossier" (id, department_id, employee_id, summary) VALUES (1, 1, 1, 'Excellent employee.'); 318 | INSERT INTO "dossier" (id, department_id, employee_id, summary) VALUES (2, 2, 1, 'Brilliant employee.'); 319 | INSERT INTO "dossier" (id, department_id, employee_id, summary) VALUES (3, 2, 2, 'Good employee.'); 320 | 321 | INSERT INTO "alpha" (id, string_identifier) VALUES (1, '1'); 322 | INSERT INTO "alpha" (id, string_identifier) VALUES (2, '1a'); 323 | INSERT INTO "alpha" (id, string_identifier) VALUES (3, '01'); 324 | INSERT INTO "alpha" (id, string_identifier) VALUES (4, '001'); 325 | INSERT INTO "alpha" (id, string_identifier) VALUES (5, '2'); 326 | INSERT INTO "alpha" (id, string_identifier) VALUES (6, '2b'); 327 | INSERT INTO "alpha" (id, string_identifier) VALUES (7, '02'); 328 | INSERT INTO "alpha" (id, string_identifier) VALUES (8, '002'); 329 | 330 | INSERT INTO "beta" (id, alpha_string_identifier) VALUES (1, '1'); 331 | INSERT INTO "beta" (id, alpha_string_identifier) VALUES (2, '01'); 332 | INSERT INTO "beta" (id, alpha_string_identifier) VALUES (3, '001'); 333 | INSERT INTO "beta" (id, alpha_string_identifier) VALUES (4, '001'); 334 | INSERT INTO "beta" (id, alpha_string_identifier) VALUES (5, '2'); 335 | INSERT INTO "beta" (id, alpha_string_identifier) VALUES (6, '2b'); 336 | INSERT INTO "beta" (id, alpha_string_identifier) VALUES (7, '2b'); 337 | INSERT INTO "beta" (id, alpha_string_identifier) VALUES (8, '02'); 338 | 339 | /* bit test, see https://github.com/yiisoft/yii2/issues/9006 */ 340 | 341 | DROP TABLE IF EXISTS "bit_values" CASCADE; 342 | 343 | CREATE TABLE "bit_values" ( 344 | id serial not null primary key, 345 | val bit(1) not null 346 | ); 347 | 348 | INSERT INTO "bit_values" (id, val) VALUES (1, '0'), (2, '1'); 349 | 350 | DROP TABLE IF EXISTS "array_and_json_types" CASCADE; 351 | CREATE TABLE "array_and_json_types" ( 352 | id SERIAL NOT NULL PRIMARY KEY, 353 | intarray_col INT[], 354 | textarray2_col TEXT[][], 355 | json_col JSON, 356 | jsonb_col JSONB, 357 | jsonarray_col JSON[] 358 | ); 359 | 360 | INSERT INTO "array_and_json_types" (intarray_col, json_col, jsonb_col) VALUES (null, null, null); 361 | INSERT INTO "array_and_json_types" (intarray_col, json_col, jsonb_col) VALUES ('{1,2,3,null}', '[1,2,3,null]', '[1,2,3,null]'); 362 | INSERT INTO "array_and_json_types" (intarray_col, json_col, jsonb_col) VALUES ('{3,4,5}', '[3,4,5]', '[3,4,5]'); 363 | 364 | CREATE TABLE "T_constraints_1" 365 | ( 366 | "C_id" INT NOT NULL PRIMARY KEY, 367 | "C_not_null" INT NOT NULL, 368 | "C_check" VARCHAR(255) NULL CHECK ("C_check" <> ''), 369 | "C_unique" INT NOT NULL, 370 | "C_default" INT NOT NULL DEFAULT 0, 371 | CONSTRAINT "CN_unique" UNIQUE ("C_unique") 372 | ); 373 | 374 | CREATE TABLE "T_constraints_2" 375 | ( 376 | "C_id_1" INT NOT NULL, 377 | "C_id_2" INT NOT NULL, 378 | "C_index_1" INT NULL, 379 | "C_index_2_1" INT NULL, 380 | "C_index_2_2" INT NULL, 381 | CONSTRAINT "CN_constraints_2_multi" UNIQUE ("C_index_2_1", "C_index_2_2"), 382 | CONSTRAINT "CN_pk" PRIMARY KEY ("C_id_1", "C_id_2") 383 | ); 384 | 385 | CREATE INDEX "CN_constraints_2_single" ON "T_constraints_2" ("C_index_1"); 386 | 387 | CREATE TABLE "T_constraints_3" 388 | ( 389 | "C_id" INT NOT NULL, 390 | "C_fk_id_1" INT NOT NULL, 391 | "C_fk_id_2" INT NOT NULL, 392 | CONSTRAINT "CN_constraints_3" FOREIGN KEY ("C_fk_id_1", "C_fk_id_2") REFERENCES "T_constraints_2" ("C_id_1", "C_id_2") ON DELETE CASCADE ON UPDATE CASCADE 393 | ); 394 | 395 | CREATE TABLE "T_constraints_4" 396 | ( 397 | "C_id" INT NOT NULL PRIMARY KEY, 398 | "C_col_1" INT NULL, 399 | "C_col_2" INT NOT NULL, 400 | CONSTRAINT "CN_constraints_4" UNIQUE ("C_col_1", "C_col_2") 401 | ); 402 | 403 | CREATE TABLE "schema1"."T_constraints_5" 404 | ( 405 | "C_id_1" INT NOT NULL, 406 | "C_id_2" INT NOT NULL, 407 | "C_index_1" INT NULL, 408 | "C_index_2_1" INT NULL, 409 | "C_index_2_2" INT NULL, 410 | CONSTRAINT "CN_constraints_5_multi" UNIQUE ("C_index_2_1", "C_index_2_2"), 411 | CONSTRAINT "CN_pk" PRIMARY KEY ("C_id_1", "C_id_2") 412 | ); 413 | 414 | CREATE INDEX "CN_constraints_5_single" ON "schema1"."T_constraints_5" ("C_index_1"); 415 | 416 | CREATE TABLE "T_constraints_6" 417 | ( 418 | "C_id" INT NOT NULL, 419 | "C_fk_id_1" INT NOT NULL, 420 | "C_fk_id_2" INT NOT NULL, 421 | CONSTRAINT "CN_constraints_6" FOREIGN KEY ("C_fk_id_1", "C_fk_id_2") REFERENCES "schema1"."T_constraints_5" ("C_id_1", "C_id_2") ON DELETE CASCADE ON UPDATE CASCADE 422 | ); 423 | 424 | CREATE VIEW "T_constraints_1_view" AS SELECT 'first_value', * FROM "T_constraints_1"; 425 | CREATE VIEW "T_constraints_2_view" AS SELECT 'first_value', * FROM "T_constraints_2"; 426 | CREATE VIEW "T_constraints_3_view" AS SELECT 'first_value', * FROM "T_constraints_3"; 427 | CREATE VIEW "T_constraints_4_view" AS SELECT 'first_value', * FROM "T_constraints_4"; 428 | 429 | CREATE TABLE "T_upsert" 430 | ( 431 | "id" SERIAL NOT NULL PRIMARY KEY, 432 | "ts" BIGINT NULL, 433 | "email" VARCHAR(128) NOT NULL UNIQUE, 434 | "recovery_email" VARCHAR(128) NULL, 435 | "address" TEXT NULL, 436 | "status" SMALLINT NOT NULL DEFAULT 0, 437 | "orders" INT NOT NULL DEFAULT 0, 438 | "profile_id" INT NULL, 439 | UNIQUE ("email", "recovery_email") 440 | ); 441 | 442 | CREATE TABLE "T_upsert_1" 443 | ( 444 | "a" INT NOT NULL PRIMARY KEY 445 | ); 446 | 447 | DROP TYPE IF EXISTS "my_type"; 448 | DROP TYPE IF EXISTS "schema2"."my_type2"; 449 | 450 | CREATE TYPE "my_type" AS enum('VAL1', 'VAL2', 'VAL3'); 451 | CREATE TYPE "schema2"."my_type2" AS enum('VAL1', 'VAL2', 'VAL3'); 452 | 453 | CREATE TABLE "schema2"."custom_type_test_table" ( 454 | "id" SERIAL NOT NULL PRIMARY KEY, 455 | "test_type" "my_type"[], 456 | "test_type2" "schema2"."my_type2"[] 457 | ); 458 | INSERT INTO "schema2"."custom_type_test_table" ("test_type", "test_type2") 459 | VALUES (array['VAL1']::"my_type"[], array['VAL2']::"schema2"."my_type2"[]); 460 | 461 | CREATE TABLE "table_with_array_col" ( 462 | "id" SERIAL NOT NULL PRIMARY KEY, 463 | "array_col" integer ARRAY[4] 464 | ); 465 | 466 | CREATE TABLE "table_uuid" ( 467 | "uuid" uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), 468 | "col" varchar(16) 469 | ); 470 | 471 | DROP TYPE IF EXISTS "currency_money_structured" CASCADE; 472 | DROP TYPE IF EXISTS "range_price_structured" CASCADE; 473 | DROP TABLE IF EXISTS "test_structured_type" CASCADE; 474 | 475 | CREATE TYPE "currency_money_structured" AS ( 476 | "value" numeric(10,2), 477 | "currency_code" char(3) 478 | ); 479 | 480 | CREATE TYPE "range_price_structured" AS ( 481 | "price_from" "currency_money_structured", 482 | "price_to" "currency_money_structured" 483 | ); 484 | 485 | CREATE TABLE "test_structured_type" 486 | ( 487 | "id" SERIAL NOT NULL PRIMARY KEY, 488 | "price_col" "currency_money_structured", 489 | "price_default" "currency_money_structured" DEFAULT '(5,USD)', 490 | "price_array" "currency_money_structured"[] DEFAULT '{null,"(10.55,USD)","(-1,)"}', 491 | "price_array2" "currency_money_structured"[][], 492 | "range_price_col" "range_price_structured" DEFAULT '("(0,USD)","(100,USD)")' 493 | ); 494 | -------------------------------------------------------------------------------- /tests/Support/Fixture/pgsql10.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS "partitioned" CASCADE; 2 | 3 | CREATE TABLE "partitioned" ( 4 | city_id int not null, 5 | logdate date not null 6 | ) PARTITION BY RANGE ("logdate"); -------------------------------------------------------------------------------- /tests/Support/Fixture/pgsql11.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS "table_index"; 2 | 3 | CREATE TABLE "table_index" ( 4 | "id" serial PRIMARY KEY, 5 | "one_unique" integer UNIQUE, 6 | "two_unique_1" integer, 7 | "two_unique_2" integer, 8 | "unique_index" integer, 9 | "non_unique_index" integer, 10 | UNIQUE ("two_unique_1", "two_unique_2") 11 | ); 12 | 13 | CREATE UNIQUE INDEX ON "table_index" ("unique_index") INCLUDE ("non_unique_index"); 14 | CREATE INDEX ON "table_index" ("non_unique_index") INCLUDE ("unique_index"); 15 | -------------------------------------------------------------------------------- /tests/Support/Fixture/pgsql12.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS "generated" CASCADE; 2 | DROP TABLE IF EXISTS "item_12" CASCADE; 3 | 4 | CREATE TABLE "generated" ( 5 | id_always int GENERATED ALWAYS AS IDENTITY, 6 | id_primary int GENERATED ALWAYS AS IDENTITY primary key, 7 | id_default int GENERATED BY DEFAULT AS IDENTITY 8 | ); 9 | 10 | CREATE TABLE "item_12" ( 11 | id int GENERATED ALWAYS AS IDENTITY primary key, 12 | name varchar(128) NOT NULL, 13 | category_id integer NOT NULL references "category"(id) on UPDATE CASCADE on DELETE CASCADE 14 | ); 15 | 16 | INSERT INTO "item_12" (name, category_id) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); 17 | INSERT INTO "item_12" (name, category_id) VALUES ('Yii 1.1 Application Development Cookbook', 1); 18 | INSERT INTO "item_12" (name, category_id) VALUES ('Ice Age', 2); 19 | INSERT INTO "item_12" (name, category_id) VALUES ('Toy Story', 2); 20 | -------------------------------------------------------------------------------- /tests/Support/TestTrait.php: -------------------------------------------------------------------------------- 1 | getDriver(), DbHelper::getSchemaCache()); 26 | 27 | if ($fixture) { 28 | DbHelper::loadFixture($db, __DIR__ . "/Fixture/$this->fixture"); 29 | } 30 | 31 | return $db; 32 | } 33 | 34 | protected static function getDb(): Connection 35 | { 36 | $dsn = (new Dsn( 37 | host: self::getHost(), 38 | databaseName: self::getDatabaseName(), 39 | port: self::getPort(), 40 | ))->asString(); 41 | $driver = new Driver($dsn, self::getUsername(), self::getPassword()); 42 | $driver->charset('utf8'); 43 | 44 | return new Connection($driver, DbHelper::getSchemaCache()); 45 | } 46 | 47 | protected function getDsn(): string 48 | { 49 | if ($this->dsn === '') { 50 | $this->dsn = (new Dsn( 51 | host: self::getHost(), 52 | databaseName: self::getDatabaseName(), 53 | port: self::getPort(), 54 | ))->asString(); 55 | } 56 | 57 | return $this->dsn; 58 | } 59 | 60 | protected function getDriverName(): string 61 | { 62 | return 'pgsql'; 63 | } 64 | 65 | protected function setDsn(string $dsn): void 66 | { 67 | $this->dsn = $dsn; 68 | } 69 | 70 | protected function setFixture(string $fixture): void 71 | { 72 | $this->fixture = $fixture; 73 | } 74 | 75 | public static function setUpBeforeClass(): void 76 | { 77 | $db = self::getDb(); 78 | 79 | DbHelper::loadFixture($db, __DIR__ . '/Fixture/pgsql.sql'); 80 | 81 | $db->close(); 82 | } 83 | 84 | protected function getDriver(): Driver 85 | { 86 | $driver = new Driver($this->getDsn(), self::getUsername(), self::getPassword()); 87 | $driver->charset('utf8'); 88 | 89 | return $driver; 90 | } 91 | 92 | private static function getDatabaseName(): string 93 | { 94 | return getenv('YII_PGSQL_DATABASE') ?: 'yiitest'; 95 | } 96 | 97 | private static function getHost(): string 98 | { 99 | return getenv('YII_PGSQL_HOST') ?: '127.0.0.1'; 100 | } 101 | 102 | private static function getPort(): string 103 | { 104 | return getenv('YII_PGSQL_PORT') ?: '5432'; 105 | } 106 | 107 | private static function getUsername(): string 108 | { 109 | return getenv('YII_PGSQL_USER') ?: 'root'; 110 | } 111 | 112 | private static function getPassword(): string 113 | { 114 | return getenv('YII_PGSQL_PASSWORD') ?: 'root'; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | load(); 8 | } 9 | --------------------------------------------------------------------------------