├── .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 |
4 |
5 |
6 |
7 |
8 |
Yii Database PostgreSQL driver
9 |
10 |
11 |
12 | [](https://packagist.org/packages/yiisoft/db-pgsql)
13 | [](https://packagist.org/packages/yiisoft/db-pgsql)
14 | [](https://github.com/yiisoft/db-pgsql/actions/workflows/rector.yml)
15 | [](https://codecov.io/gh/yiisoft/db-pgsql)
16 | [](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** |[](https://github.com/yiisoft/db-pgsql/actions/workflows/build.yml) [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/db-pgsql/master) [](https://github.com/yiisoft/db-pgsql/actions/workflows/static.yml) [](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 | [](https://opencollective.com/yiisoft)
61 |
62 | ## Follow updates
63 |
64 | [](https://www.yiiframework.com/)
65 | [](https://twitter.com/yiiframework)
66 | [](https://t.me/yii3en)
67 | [](https://www.facebook.com/groups/yiitalk)
68 | [](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 |
--------------------------------------------------------------------------------