├── .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
│ ├── InConditionBuilder.php
│ └── LikeConditionBuilder.php
├── Column
│ ├── BinaryColumn.php
│ ├── BooleanColumn.php
│ ├── ColumnBuilder.php
│ ├── ColumnDefinitionBuilder.php
│ ├── ColumnDefinitionParser.php
│ ├── ColumnFactory.php
│ ├── DateTimeColumn.php
│ └── JsonColumn.php
├── Command.php
├── Connection.php
├── DDLQueryBuilder.php
├── DMLQueryBuilder.php
├── DQLQueryBuilder.php
├── Driver.php
├── Dsn.php
├── IndexType.php
├── QueryBuilder.php
├── Quoter.php
├── Schema.php
├── ServerInfo.php
├── SqlParser.php
├── TableSchema.php
└── Transaction.php
└── tests
├── .env
├── BatchQueryResultTest.php
├── ColumnBuilderTest.php
├── ColumnDefinitionParserTest.php
├── ColumnFactoryTest.php
├── ColumnTest.php
├── CommandTest.php
├── ConnectionTest.php
├── DsnTest.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
├── QueryBuilderTest.php
├── QueryGetTableAliasTest.php
├── QueryTest.php
├── QuoterTest.php
├── Runtime
└── .gitignore
├── SchemaTest.php
├── SqlParserTest.php
├── Support
├── Fixture
│ ├── oci.sql
│ └── oci21.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 | # Oracle driver for Yii Database Change Log
2 |
3 | ## 2.0.0 under development
4 |
5 | - Enh #268: Rename `batchInsert()` to `insertBatch()` in `DMLQueryBuilder` and change parameters
6 | from `$table, $columns, $rows` to `$table, $rows, $columns = []` (@Tigrov)
7 | - Enh #260: Support `Traversable` values for `DMLQueryBuilder::batchInsert()` method with empty columns (@Tigrov)
8 | - Enh #255, #321: Implement and use `SqlParser` class (@Tigrov)
9 | - New #236: Implement `ColumnSchemaInterface` classes according to the data type of database table columns
10 | for type casting performance. Related with yiisoft/db#752 (@Tigrov)
11 | - Chg #272: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov)
12 | - Enh #275: Refactor PHP type of `ColumnSchemaInterface` instances (@Tigrov)
13 | - Enh #277: Raise minimum PHP version to `^8.1` with minor refactoring (@Tigrov)
14 | - New #276, #288: Implement `ColumnFactory` class (@Tigrov)
15 | - Enh #279: Separate column type constants (@Tigrov)
16 | - New #280, #291: Realize `ColumnBuilder` class (@Tigrov)
17 | - Enh #281: Update according changes in `ColumnSchemaInterface` (@Tigrov)
18 | - New #282, #291, #299, #302: Add `ColumnDefinitionBuilder` class (@Tigrov)
19 | - Bug #285: Fix `DMLQueryBuilder::insertBatch()` method (@Tigrov)
20 | - Enh #283: Refactor `Dsn` class (@Tigrov)
21 | - Enh #286: Use constructor to create columns and initialize properties (@Tigrov)
22 | - Enh #288, #317: Refactor `Schema::findColumns()` method (@Tigrov)
23 | - Enh #289: Refactor `Schema::normalizeDefaultValue()` method and move it to `ColumnFactory` class (@Tigrov)
24 | - New #292: Override `QueryBuilder::prepareBinary()` method (@Tigrov)
25 | - Chg #294: Update `QueryBuilder` constructor (@Tigrov)
26 | - Enh #293: Use `ColumnDefinitionBuilder` to generate table column SQL representation (@Tigrov)
27 | - Enh #296: Remove `ColumnInterface` (@Tigrov)
28 | - Enh #298: Rename `ColumnSchemaInterface` to `ColumnInterface` (@Tigrov)
29 | - Enh #298: Refactor `DMLQueryBuilder::prepareInsertValues()` method (@Tigrov)
30 | - Enh #299: Add `ColumnDefinitionParser` class (@Tigrov)
31 | - Enh #299: Convert database types to lower case (@Tigrov)
32 | - Enh #300: Replace `DbArrayHelper::getColumn()` with `array_column()` (@Tigrov)
33 | - New #301: Add `IndexType` class (@Tigrov)
34 | - New #303: Support JSON type (@Tigrov)
35 | - Bug #305: Explicitly mark nullable parameters (@vjik)
36 | - Chg #306: Change supported PHP versions to `8.1 - 8.4` (@Tigrov)
37 | - Enh #306: Minor refactoring (@Tigrov)
38 | - New #307: Add parameters `$ifExists` and `$cascade` to `CommandInterface::dropTable()` and
39 | `DDLQueryBuilderInterface::dropTable()` methods (@vjik)
40 | - Chg #310: Remove usage of `hasLimit()` and `hasOffset()` methods of `DQLQueryBuilder` class (@Tigrov)
41 | - Enh #313: Refactor according changes in `db` package (@Tigrov)
42 | - New #311: Add `caseSensitive` option to like condition (@vjik)
43 | - Enh #315: Remove `getCacheKey()` and `getCacheTag()` methods from `Schema` class (@Tigrov)
44 | - Enh #319: Support `boolean` type (@Tigrov)
45 | - Enh #318, #320: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov)
46 | - New #316: Realize `Schema::loadResultColumn()` method (@Tigrov)
47 | - New #323: Use `DateTimeColumn` class for datetime column types (@Tigrov)
48 | - Enh #324: Refactor `Command::insertWithReturningPks()` method (@Tigrov)
49 | - Enh #325: Refactor `DMLQueryBuilder::upsert()` method (@Tigrov)
50 | - Chg #326: Add alias in `DQLQueryBuilder::selectExists()` method for consistency with other DBMS (@Tigrov)
51 |
52 | ## 1.3.0 March 21, 2024
53 |
54 | - Enh #248: Change property `Schema::$typeMap` to constant `Schema::TYPE_MAP` (@Tigrov)
55 | - Enh #251: Allow to use `DMLQueryBuilderInterface::batchInsert()` method with empty columns (@Tigrov)
56 | - Enh #253: Resolve deprecated methods (@Tigrov)
57 | - Bug #238: Fix execution `Query` without table(s) to select from (@Tigrov)
58 | - Bug #250: Fix `Command::insertWithReturningPks()` method for table without primary keys (@Tigrov)
59 | - Bug #254: Fix, table sequence name should be null if sequence name not found (@Tigrov)
60 |
61 | ## 1.2.0 November 12, 2023
62 |
63 | - Enh #230: Improve column type #230 (@Tigrov)
64 | - Enh #243: Move methods from `Command` to `AbstractPdoCommand` class (@Tigrov)
65 | - Bug #233: Refactor `DMLQueryBuilder`, related with yiisoft/db#746 (@Tigrov)
66 | - Bug #240: Remove `RECURSIVE` expression from CTE queries (@Tigrov)
67 | - Bug #242: Fix `AbstractDMLQueryBuilder::batchInsert()` for values as associative arrays,
68 | related with yiisoft/db#769 (@Tigrov)
69 |
70 | ## 1.1.0 July 24, 2023
71 |
72 | - Enh #225: Typecast refactoring (@Tigrov)
73 | - Enh #226: Add support for auto increment in primary key column. (@terabytesoftw)
74 | - Bug #229: Fix bugs related with default value (@Tigrov)
75 |
76 | ## 1.0.0 April 12, 2023
77 |
78 | - Initial release.
79 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/)
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 Oracle driver
9 |
10 |
11 |
12 | [](https://packagist.org/packages/yiisoft/db-oracle)
13 | [](https://packagist.org/packages/yiisoft/db-oracle)
14 | [](https://github.com/yiisoft/db-oracle/actions/workflows/rector.yml)
15 | [](https://codecov.io/gh/yiisoft/db-oracle)
16 | [](https://github.styleci.io/repos/114756574?branch=master)
17 |
18 | Oracle driver for [Yii Database](https://github.com/yiisoft/db) is a database driver for [Oracle] databases.
19 |
20 | The package allows you to connect to [Oracle] databases from your application and perform various database operations
21 | such as executing queries, creating and modifying database schema, and processing data. It supports a wide range of
22 | [Oracle] versions and provides a simple and efficient interface for working with [Oracle] databases.
23 |
24 | To use the package, you need to have the [Oracle] client library installed and configured on your server, and you need
25 | to specify the correct database connection parameters. Once you have done this, you can use the driver to connect to
26 | your Oracle database and perform various database operations as needed.
27 |
28 | [Oracle]: https://www.oracle.com/database/technologies/
29 |
30 | ## Support version
31 |
32 | | PHP | Oracle Version | CI-Actions |
33 | |---------------|----------------|------------|
34 | | **8.1 - 8.4** | **12c - 21c**|[](https://github.com/yiisoft/db-oracle/actions/workflows/build.yml) [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/db-oracle/master) [](https://github.com/yiisoft/db-oracle/actions/workflows/static.yml) [](https://shepherd.dev/github/yiisoft/db-oracle)
35 |
36 | ## Installation
37 |
38 | The package could be installed with [Composer](https://getcomposer.org):
39 |
40 | ```php
41 | composer require yiisoft/db-oracle
42 | ```
43 |
44 | ## Documentation
45 |
46 | For config connection to Oracle database check [Connecting Oracle](https://github.com/yiisoft/db/blob/master/docs/guide/en/connection/oracle.md).
47 |
48 | [Check the documentation docs](https://github.com/yiisoft/db/blob/master/docs/guide/en/README.md) to learn about usage.
49 |
50 | - [Internals](docs/internals.md)
51 |
52 | 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.
53 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
54 |
55 | ## License
56 |
57 | The Yii Database Oracle driver is free software. It is released under the terms of the BSD License.
58 | Please see [`LICENSE`](./LICENSE.md) for more information.
59 |
60 | Maintained by [Yii Software](https://www.yiiframework.com/).
61 |
62 | ## Support the project
63 |
64 | [](https://opencollective.com/yiisoft)
65 |
66 | ## Follow updates
67 |
68 | [](https://www.yiiframework.com/)
69 | [](https://twitter.com/yiiframework)
70 | [](https://t.me/yii3en)
71 | [](https://www.facebook.com/groups/yiitalk)
72 | [](https://yiiframework.com/go/slack)
73 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/db-oracle",
3 | "description": "Oracle driver for Yii Database",
4 | "keywords": [
5 | "yii",
6 | "oracle",
7 | "database",
8 | "sql",
9 | "dbal",
10 | "query-builder"
11 | ],
12 | "type": "library",
13 | "license": "BSD-3-Clause",
14 | "support": {
15 | "issues": "https://github.com/yiisoft/db-oracle/issues?state=open",
16 | "source": "https://github.com/yiisoft/db-oracle",
17 | "forum": "https://www.yiiframework.com/forum/",
18 | "wiki": "https://www.yiiframework.com/wiki/",
19 | "irc": "ircs://irc.libera.chat:6697/yii",
20 | "chat": "https://t.me/yii3en"
21 | },
22 | "funding": [
23 | {
24 | "type": "opencollective",
25 | "url": "https://opencollective.com/yiisoft"
26 | },
27 | {
28 | "type": "github",
29 | "url": "https://github.com/sponsors/yiisoft"
30 | }
31 | ],
32 | "require": {
33 | "ext-pdo": "*",
34 | "php": "8.1 - 8.4",
35 | "yiisoft/db": "dev-master"
36 | },
37 | "require-dev": {
38 | "maglnet/composer-require-checker": "^4.7.1",
39 | "phpunit/phpunit": "^10.5.45",
40 | "rector/rector": "^2.0.10",
41 | "roave/infection-static-analysis-plugin": "^1.35",
42 | "spatie/phpunit-watcher": "^1.24",
43 | "vimeo/psalm": "^5.26.1 || ^6.8.8",
44 | "vlucas/phpdotenv": "^5.6.1",
45 | "yiisoft/aliases": "^2.0",
46 | "yiisoft/cache-file": "^3.2",
47 | "yiisoft/var-dumper": "^1.7"
48 | },
49 | "autoload": {
50 | "psr-4": {
51 | "Yiisoft\\Db\\Oracle\\": "src"
52 | }
53 | },
54 | "autoload-dev": {
55 | "psr-4": {
56 | "Yiisoft\\Db\\Oracle\\Tests\\": "tests",
57 | "Yiisoft\\Db\\Tests\\": "vendor/yiisoft/db/tests"
58 | },
59 | "files": ["tests/bootstrap.php"]
60 | },
61 | "config": {
62 | "sort-packages": true,
63 | "allow-plugins": {
64 | "infection/extension-installer": true,
65 | "composer/package-versions-deprecated": true
66 | }
67 | },
68 | "prefer-stable": true,
69 | "scripts": {
70 | "test": "phpunit --testdox --no-interaction",
71 | "test-watch": "phpunit-watcher watch"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | oracle:
5 | image: gvenzl/oracle-xe:21
6 | ports:
7 | - 1521:1521
8 | environment:
9 | ORACLE_PASSWORD : root
10 | ORACLE_DATABASE : yiitest
11 | APP_USER: yiitest
12 | APP_USER_PASSWORD: root
13 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src"
5 | ]
6 | },
7 | "logs": {
8 | "text": "php:\/\/stderr",
9 | "stryker": {
10 | "report": "master"
11 | }
12 | },
13 | "mutators": {
14 | "@default": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | paths([
15 | __DIR__ . '/src',
16 | /**
17 | * Disabled ./tests directory due to different branches with main package when testing
18 | */
19 | // __DIR__ . '/tests',
20 | ]);
21 |
22 | // register a single rule
23 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
24 |
25 | // define sets of rules
26 | $rectorConfig->sets([
27 | LevelSetList::UP_TO_PHP_81,
28 | ]);
29 |
30 | $rectorConfig->skip([
31 | NullToStrictStringFuncCallArgRector::class,
32 | ReadOnlyPropertyRector::class,
33 | SensitiveHereNowDocRector::class,
34 | RemoveParentCallWithoutParentRector::class,
35 | ]);
36 | };
37 |
--------------------------------------------------------------------------------
/src/Builder/InConditionBuilder.php:
--------------------------------------------------------------------------------
1 | splitCondition($expression, $params);
42 |
43 | return $splitCondition ?? parent::build($expression, $params);
44 | }
45 |
46 | /**
47 | * Oracle DBMS doesn't support more than 1000 parameters in `IN` condition.
48 | *
49 | * This method splits long `IN` condition into series of smaller ones.
50 | *
51 | * @param array $params The binding parameters.
52 | *
53 | * @throws Exception
54 | * @throws InvalidArgumentException
55 | * @throws InvalidConfigException
56 | * @throws NotSupportedException
57 | *
58 | * @return string|null `null` when split isn't required. Otherwise - built SQL condition.
59 | */
60 | protected function splitCondition(InConditionInterface $condition, array &$params): string|null
61 | {
62 | $operator = $condition->getOperator();
63 | $values = $condition->getValues();
64 | $column = $condition->getColumn();
65 |
66 | if (!is_array($values)) {
67 | return null;
68 | }
69 |
70 | $maxParameters = 1000;
71 | $count = count($values);
72 |
73 | if ($count <= $maxParameters) {
74 | return null;
75 | }
76 |
77 | $slices = [];
78 |
79 | for ($i = 0; $i < $count; $i += $maxParameters) {
80 | $slices[] = $this->queryBuilder->createConditionFromArray(
81 | [$operator, $column, array_slice($values, $i, $maxParameters)]
82 | );
83 | }
84 |
85 | array_unshift($slices, ($operator === 'IN') ? 'OR' : 'AND');
86 |
87 | return $this->queryBuilder->buildCondition($slices, $params);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Builder/LikeConditionBuilder.php:
--------------------------------------------------------------------------------
1 | '!%',
28 | '_' => '!_',
29 | '!' => '!!',
30 | ];
31 |
32 | public function __construct(private QueryBuilderInterface $queryBuilder)
33 | {
34 | parent::__construct($queryBuilder, $this->getEscapeSql());
35 | }
36 |
37 | /**
38 | * @throws Exception
39 | */
40 | public function build(LikeConditionInterface $expression, array &$params = []): string
41 | {
42 | if (!isset($this->escapingReplacements['\\'])) {
43 | /*
44 | * Different pdo_oci8 versions may or may not implement `PDO::quote()`, so {@see Quoter::quoteValue()} may or
45 | * may not quote `\`.
46 | */
47 | $this->escapingReplacements['\\'] = substr($this->queryBuilder->getQuoter()->quoteValue('\\'), 1, -1);
48 | }
49 |
50 | return parent::build($expression, $params);
51 | }
52 |
53 | protected function prepareColumn(LikeConditionInterface $expression, array &$params): string
54 | {
55 | $column = parent::prepareColumn($expression, $params);
56 |
57 | if ($expression->getCaseSensitive() === false) {
58 | $column = 'LOWER(' . $column . ')';
59 | }
60 |
61 | return $column;
62 | }
63 |
64 | protected function preparePlaceholderName(
65 | string|ExpressionInterface $value,
66 | LikeConditionInterface $expression,
67 | ?array $escape,
68 | array &$params,
69 | ): string {
70 | $placeholderName = parent::preparePlaceholderName($value, $expression, $escape, $params);
71 |
72 | if ($expression->getCaseSensitive() === false) {
73 | $placeholderName = 'LOWER(' . $placeholderName . ')';
74 | }
75 |
76 | return $placeholderName;
77 | }
78 |
79 | /**
80 | * @return string Character used to escape special characters in `LIKE` conditions. By default, it's assumed to be
81 | * `!`.
82 | */
83 | private function getEscapeSql(): string
84 | {
85 | return $this->escapeCharacter !== '' ? " ESCAPE '$this->escapeCharacter'" : '';
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Column/BinaryColumn.php:
--------------------------------------------------------------------------------
1 | getDbType() === 'blob') {
18 | if ($value instanceof ParamInterface && is_string($value->getValue())) {
19 | /** @var string */
20 | $value = $value->getValue();
21 | }
22 |
23 | if (is_string($value)) {
24 | return new Expression('TO_BLOB(UTL_RAW.CAST_TO_RAW(:value))', ['value' => $value]);
25 | }
26 | }
27 |
28 | return parent::dbTypecast($value);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Column/BooleanColumn.php:
--------------------------------------------------------------------------------
1 | '1',
20 | false => '0',
21 | null, '' => null,
22 | default => $value instanceof ExpressionInterface ? $value : ($value ? '1' : '0'),
23 | };
24 | }
25 |
26 | /** @psalm-mutation-free */
27 | public function getPhpType(): string
28 | {
29 | return PhpType::BOOL;
30 | }
31 |
32 | public function phpTypecast(mixed $value): bool|null
33 | {
34 | if ($value === null) {
35 | return null;
36 | }
37 |
38 | return $value && $value !== "\0";
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Column/ColumnBuilder.php:
--------------------------------------------------------------------------------
1 | buildType($column)
42 | . $this->buildAutoIncrement($column)
43 | . $this->buildDefault($column)
44 | . $this->buildPrimaryKey($column)
45 | . $this->buildUnique($column)
46 | . $this->buildNotNull($column)
47 | . $this->buildCheck($column)
48 | . $this->buildReferences($column)
49 | . $this->buildExtra($column);
50 | }
51 |
52 | protected function buildCheck(ColumnInterface $column): string
53 | {
54 | $check = $column->getCheck();
55 |
56 | if (empty($check)) {
57 | $name = $column->getName();
58 |
59 | if (empty($name)) {
60 | return '';
61 | }
62 |
63 | return match ($column->getType()) {
64 | ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON =>
65 | version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '<')
66 | ? ' CHECK (' . $this->queryBuilder->getQuoter()->quoteSimpleColumnName($name) . ' IS JSON)'
67 | : '',
68 | ColumnType::BOOLEAN =>
69 | ' CHECK (' . $this->queryBuilder->getQuoter()->quoteSimpleColumnName($name) . ' IN (0,1))',
70 | default => '',
71 | };
72 | }
73 |
74 | return " CHECK ($check)";
75 | }
76 |
77 | protected function buildOnDelete(string $onDelete): string
78 | {
79 | return match ($onDelete = strtoupper($onDelete)) {
80 | ReferentialAction::CASCADE,
81 | ReferentialAction::SET_NULL => " ON DELETE $onDelete",
82 | default => '',
83 | };
84 | }
85 |
86 | protected function buildOnUpdate(string $onUpdate): string
87 | {
88 | return '';
89 | }
90 |
91 | protected function getDbType(ColumnInterface $column): string
92 | {
93 | $dbType = $column->getDbType();
94 | $size = $column->getSize();
95 | $scale = $column->getScale();
96 |
97 | /** @psalm-suppress DocblockTypeContradiction */
98 | return match ($dbType) {
99 | default => $dbType,
100 | null => match ($column->getType()) {
101 | ColumnType::BOOLEAN => 'char(1)',
102 | ColumnType::BIT => match (true) {
103 | $size === null => 'number(38)',
104 | $size <= 126 => 'number(' . ceil(log10(2 ** $size)) . ')',
105 | default => 'raw(' . ceil($size / 8) . ')',
106 | },
107 | ColumnType::TINYINT => 'number(' . ($size ?? 3) . ')',
108 | ColumnType::SMALLINT => 'number(' . ($size ?? 5) . ')',
109 | ColumnType::INTEGER => 'number(' . ($size ?? 10) . ')',
110 | ColumnType::BIGINT => 'number(' . ($size ?? 20) . ')',
111 | ColumnType::FLOAT => 'binary_float',
112 | ColumnType::DOUBLE => 'binary_double',
113 | ColumnType::DECIMAL => 'number(' . ($size ?? 10) . ',' . ($scale ?? 0) . ')',
114 | ColumnType::MONEY => 'number(' . ($size ?? 19) . ',' . ($scale ?? 4) . ')',
115 | ColumnType::CHAR => 'char',
116 | ColumnType::STRING => 'varchar2(' . ($size ?? 255) . ')',
117 | ColumnType::TEXT => 'clob',
118 | ColumnType::BINARY => 'blob',
119 | ColumnType::UUID => 'raw(16)',
120 | ColumnType::TIMESTAMP => 'timestamp',
121 | ColumnType::DATETIME => 'timestamp',
122 | ColumnType::DATETIMETZ => 'timestamp' . ($size !== null ? "($size)" : '') . ' with time zone',
123 | ColumnType::TIME => 'interval day(0) to second',
124 | ColumnType::TIMETZ => 'interval day(0) to second',
125 | ColumnType::DATE => 'date',
126 | ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON =>
127 | version_compare($this->queryBuilder->getServerInfo()->getVersion(), '21', '>=')
128 | ? 'json'
129 | : 'clob',
130 | default => 'varchar2',
131 | },
132 | 'timestamp with time zone' => 'timestamp' . ($size !== null ? "($size)" : '') . ' with time zone',
133 | 'timestamp with local time zone' => 'timestamp' . ($size !== null ? "($size)" : '') . ' with local time zone',
134 | 'interval day to second' => 'interval day' . ($scale !== null ? "($scale)" : '') . ' to second' . ($size !== null ? "($size)" : ''),
135 | 'interval year to month' => 'interval year' . ($scale !== null ? "($scale)" : '') . ' to month',
136 | };
137 | }
138 |
139 | protected function getDefaultUuidExpression(): string
140 | {
141 | return 'sys_guid()';
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/Column/ColumnDefinitionParser.php:
--------------------------------------------------------------------------------
1 | $type];
36 |
37 | $typeDetails = $matches[6] ?? $matches[2] ?? '';
38 |
39 | if ($typeDetails !== '') {
40 | if ($type === 'enum') {
41 | $info += $this->enumInfo($typeDetails);
42 | } else {
43 | $info += $this->sizeInfo($typeDetails);
44 | }
45 | }
46 |
47 | $scale = $matches[5] ?? $matches[3] ?? '';
48 |
49 | if ($scale !== '') {
50 | $info += ['scale' => (int) $scale];
51 | }
52 |
53 | if (isset($matches[7])) {
54 | /** @psalm-var positive-int */
55 | $info['dimension'] = substr_count($matches[7], '[');
56 | }
57 |
58 | $extra = substr($definition, strlen($matches[0]));
59 |
60 | return $info + $this->extraInfo($extra);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Column/ColumnFactory.php:
--------------------------------------------------------------------------------
1 |
28 | */
29 | protected const TYPE_MAP = [
30 | 'char' => ColumnType::CHAR,
31 | 'nchar' => ColumnType::CHAR,
32 | 'character' => ColumnType::CHAR,
33 | 'varchar' => ColumnType::STRING,
34 | 'varchar2' => ColumnType::STRING,
35 | 'nvarchar2' => ColumnType::STRING,
36 | 'clob' => ColumnType::TEXT,
37 | 'nclob' => ColumnType::TEXT,
38 | 'blob' => ColumnType::BINARY,
39 | 'bfile' => ColumnType::BINARY,
40 | 'long raw' => ColumnType::BINARY,
41 | 'raw' => ColumnType::BINARY,
42 | 'number' => ColumnType::DECIMAL,
43 | 'binary_float' => ColumnType::FLOAT, // 32 bit
44 | 'binary_double' => ColumnType::DOUBLE, // 64 bit
45 | 'float' => ColumnType::DOUBLE, // 126 bit
46 | 'date' => ColumnType::DATE,
47 | 'timestamp' => ColumnType::DATETIME,
48 | 'timestamp with time zone' => ColumnType::DATETIMETZ,
49 | 'timestamp with local time zone' => ColumnType::DATETIME,
50 | 'interval day to second' => ColumnType::STRING,
51 | 'interval year to month' => ColumnType::STRING,
52 | 'json' => ColumnType::JSON,
53 |
54 | /** Deprecated */
55 | 'long' => ColumnType::TEXT,
56 | ];
57 |
58 | protected function columnDefinitionParser(): ColumnDefinitionParser
59 | {
60 | return new ColumnDefinitionParser();
61 | }
62 |
63 | protected function getType(string $dbType, array $info = []): string
64 | {
65 | if ($dbType === 'number') {
66 | return match ($info['scale'] ?? null) {
67 | null => ColumnType::DOUBLE,
68 | 0 => ColumnType::INTEGER,
69 | default => ColumnType::DECIMAL,
70 | };
71 | }
72 |
73 | if (isset($info['check'], $info['name'])) {
74 | if (strcasecmp($info['check'], '"' . $info['name'] . '" is json') === 0) {
75 | return ColumnType::JSON;
76 | }
77 |
78 | if (isset($info['size'])
79 | && $dbType === 'char'
80 | && $info['size'] === 1
81 | && strcasecmp($info['check'], '"' . $info['name'] . '" in (0,1)') === 0
82 | ) {
83 | return ColumnType::BOOLEAN;
84 | }
85 | }
86 |
87 | if ($dbType === 'interval day to second' && isset($info['scale']) && $info['scale'] === 0) {
88 | return ColumnType::TIME;
89 | }
90 |
91 | return parent::getType($dbType, $info);
92 | }
93 |
94 | protected function getColumnClass(string $type, array $info = []): string
95 | {
96 | return match ($type) {
97 | ColumnType::BINARY => BinaryColumn::class,
98 | ColumnType::BOOLEAN => BooleanColumn::class,
99 | ColumnType::DATETIME => DateTimeColumn::class,
100 | ColumnType::DATETIMETZ => DateTimeColumn::class,
101 | ColumnType::TIME => DateTimeColumn::class,
102 | ColumnType::TIMETZ => DateTimeColumn::class,
103 | ColumnType::DATE => DateTimeColumn::class,
104 | ColumnType::JSON => JsonColumn::class,
105 | default => parent::getColumnClass($type, $info),
106 | };
107 | }
108 |
109 | protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnInterface $column): mixed
110 | {
111 | $value = parent::normalizeNotNullDefaultValue(rtrim($defaultValue), $column);
112 |
113 | if ($column instanceof DateTimeColumn
114 | && $value instanceof Expression
115 | && preg_match(self::DATETIME_REGEX, (string) $value, $matches) === 1
116 | ) {
117 | return date_create_immutable($matches[1]) !== false
118 | ? $column->phpTypecast($matches[1])
119 | : new Expression($matches[1]);
120 | }
121 |
122 | return $value;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/Column/DateTimeColumn.php:
--------------------------------------------------------------------------------
1 | [!WARNING]
20 | * > Oracle DBMS converts `TIMESTAMP WITH LOCAL TIME ZONE` column type values from database session time zone
21 | * > to the database time zone for storage, and back from the database time zone to the session time zone when retrieve
22 | * > the values.
23 | *
24 | * `TIMESTAMP WITH LOCAL TIME ZONE` database type does not store time zone offset and require to convert datetime values
25 | * to the database session time zone before insert and back to the PHP time zone after retrieve the values.
26 | * This will be done in the {@see dbTypecast()} and {@see phpTypecast()} methods and guarantees that the values
27 | * are stored in the database in the correct time zone.
28 | *
29 | * To avoid possible time zone issues with the datetime values conversion, it is recommended to set the PHP and database
30 | * time zones to UTC.
31 | */
32 | final class DateTimeColumn extends \Yiisoft\Db\Schema\Column\DateTimeColumn
33 | {
34 | public function dbTypecast(mixed $value): string|ExpressionInterface|null
35 | {
36 | $value = parent::dbTypecast($value);
37 |
38 | if (!is_string($value)) {
39 | return $value;
40 | }
41 |
42 | $value = str_replace(["'", '"', "\000", "\032"], '', $value);
43 |
44 | return match ($this->getType()) {
45 | ColumnType::TIMESTAMP, ColumnType::DATETIME, ColumnType::DATETIMETZ => new Expression("TIMESTAMP '$value'"),
46 | ColumnType::TIME, ColumnType::TIMETZ => new Expression(
47 | "INTERVAL '$value' DAY(0) TO SECOND" . (($size = $this->getSize()) !== null ? "($size)" : '')
48 | ),
49 | ColumnType::DATE => new Expression("DATE '$value'"),
50 | default => $value,
51 | };
52 | }
53 |
54 | public function phpTypecast(mixed $value): DateTimeImmutable|null
55 | {
56 | if (is_string($value) && match ($this->getType()) {
57 | ColumnType::TIME, ColumnType::TIMETZ => true,
58 | default => false,
59 | }) {
60 | $value = explode(' ', $value, 2)[1] ?? $value;
61 | }
62 |
63 | return parent::phpTypecast($value);
64 | }
65 |
66 | protected function getFormat(): string
67 | {
68 | return $this->format ??= match ($this->getType()) {
69 | ColumnType::TIME, ColumnType::TIMETZ => '0 H:i:s' . $this->getMillisecondsFormat(),
70 | default => parent::getFormat(),
71 | };
72 | }
73 |
74 | protected function shouldConvertTimezone(): bool
75 | {
76 | return $this->shouldConvertTimezone ??= !empty($this->dbTimezone) && match ($this->getType()) {
77 | ColumnType::DATETIMETZ,
78 | ColumnType::DATE => false,
79 | default => true,
80 | };
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Column/JsonColumn.php:
--------------------------------------------------------------------------------
1 | db->getSchema()->getTableSchema($table);
30 | $returnColumns = $tableSchema?->getPrimaryKey() ?? [];
31 |
32 | if ($returnColumns === []) {
33 | if ($this->insert($table, $columns)->execute() === 0) {
34 | return false;
35 | }
36 |
37 | return [];
38 | }
39 |
40 | if ($columns instanceof QueryInterface) {
41 | throw new NotSupportedException(
42 | __METHOD__ . '() is not supported by Oracle when inserting sub-query.'
43 | );
44 | }
45 |
46 | $params = [];
47 | $sql = $this->getQueryBuilder()->insert($table, $columns, $params);
48 |
49 | /** @var TableSchema $tableSchema */
50 | $tableColumns = $tableSchema->getColumns();
51 | $returnParams = [];
52 |
53 | foreach ($returnColumns as $name) {
54 | $phName = AbstractQueryBuilder::PARAM_PREFIX . (count($params) + count($returnParams));
55 |
56 | $returnParams[$phName] = [
57 | 'column' => $name,
58 | 'value' => '',
59 | ];
60 |
61 | $column = $tableColumns[$name];
62 |
63 | if ($column->getPhpType() !== PhpType::INT) {
64 | $returnParams[$phName]['dataType'] = PDO::PARAM_STR;
65 | } else {
66 | $returnParams[$phName]['dataType'] = PDO::PARAM_INT;
67 | }
68 |
69 | $returnParams[$phName]['size'] = ($column->getSize() ?? 3998) + 2;
70 | }
71 |
72 | $quotedReturnColumns = array_map($this->db->getQuoter()->quoteColumnName(...), $returnColumns);
73 |
74 | $sql .= ' RETURNING ' . implode(', ', $quotedReturnColumns) . ' INTO ' . implode(', ', array_keys($returnParams));
75 |
76 | $this->setSql($sql)->bindValues($params);
77 | $this->prepare(false);
78 |
79 | /** @psalm-var array $returnParams */
80 | foreach ($returnParams as $name => &$value) {
81 | $this->bindParam($name, $value['value'], $value['dataType'], $value['size']);
82 | }
83 |
84 | unset($value);
85 |
86 | if ($this->execute() === 0) {
87 | return false;
88 | }
89 |
90 | $result = [];
91 |
92 | foreach ($returnParams as $value) {
93 | $result[$value['column']] = $value['value'];
94 | }
95 |
96 | if ($this->phpTypecasting) {
97 | foreach ($result as $column => &$value) {
98 | $value = $tableColumns[$column]->phpTypecast($value);
99 | }
100 | }
101 |
102 | return $result;
103 | }
104 |
105 | public function showDatabases(): array
106 | {
107 | $sql = <<setSql($sql)->queryColumn();
112 | }
113 |
114 | protected function bindPendingParams(): void
115 | {
116 | $paramsPassedByReference = [];
117 |
118 | $params = $this->params;
119 |
120 | foreach ($params as $name => $value) {
121 | if (PDO::PARAM_STR === $value->getType()) {
122 | /** @var mixed */
123 | $paramsPassedByReference[$name] = $value->getValue();
124 | $this->pdoStatement?->bindParam(
125 | $name,
126 | $paramsPassedByReference[$name],
127 | $value->getType(),
128 | strlen((string) $value->getValue())
129 | );
130 | } else {
131 | $this->pdoStatement?->bindValue($name, $value->getValue(), $value->getType());
132 | }
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/Connection.php:
--------------------------------------------------------------------------------
1 | setSql($sql);
35 | }
36 |
37 | if ($this->logger !== null) {
38 | $command->setLogger($this->logger);
39 | }
40 |
41 | if ($this->profiler !== null) {
42 | $command->setProfiler($this->profiler);
43 | }
44 |
45 | return $command->bindValues($params);
46 | }
47 |
48 | public function createTransaction(): TransactionInterface
49 | {
50 | return new Transaction($this);
51 | }
52 |
53 | public function getColumnFactory(): ColumnFactoryInterface
54 | {
55 | return $this->columnFactory ??= new ColumnFactory();
56 | }
57 |
58 | /**
59 | * Override base behaviour to support Oracle sequences.
60 | *
61 | * @throws Exception
62 | * @throws InvalidConfigException
63 | * @throws InvalidCallException
64 | * @throws Throwable
65 | */
66 | public function getLastInsertId(?string $sequenceName = null): string
67 | {
68 | if ($sequenceName === null) {
69 | throw new InvalidArgumentException('Oracle not support lastInsertId without sequence name.');
70 | }
71 |
72 | if ($this->isActive()) {
73 | // get the last insert id from connection
74 | $sequenceName = $this->getQuoter()->quoteSimpleTableName($sequenceName);
75 |
76 | return (string) $this->createCommand("SELECT $sequenceName.CURRVAL FROM DUAL")->queryScalar();
77 | }
78 |
79 | throw new InvalidCallException('DB Connection is not active.');
80 | }
81 |
82 | public function getQueryBuilder(): QueryBuilderInterface
83 | {
84 | return $this->queryBuilder ??= new QueryBuilder($this);
85 | }
86 |
87 | public function getQuoter(): QuoterInterface
88 | {
89 | return $this->quoter ??= new Quoter('"', '"', $this->getTablePrefix());
90 | }
91 |
92 | public function getSchema(): SchemaInterface
93 | {
94 | return $this->schema ??= new Schema($this, $this->schemaCache, strtoupper($this->driver->getUsername()));
95 | }
96 |
97 | public function getServerInfo(): ServerInfoInterface
98 | {
99 | return $this->serverInfo ??= new ServerInfo($this);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/DDLQueryBuilder.php:
--------------------------------------------------------------------------------
1 | quoter->quoteTableName($table)
32 | . ' ADD CONSTRAINT ' . $this->quoter->quoteColumnName($name)
33 | . ' FOREIGN KEY (' . $this->queryBuilder->buildColumns($columns) . ')'
34 | . ' REFERENCES ' . $this->quoter->quoteTableName($referenceTable)
35 | . ' (' . $this->queryBuilder->buildColumns($referenceColumns) . ')';
36 |
37 | if ($delete !== null) {
38 | $sql .= ' ON DELETE ' . $delete;
39 | }
40 |
41 | if ($update !== null) {
42 | throw new Exception('Oracle does not support ON UPDATE clause.');
43 | }
44 |
45 | return $sql;
46 | }
47 |
48 | public function alterColumn(string $table, string $column, ColumnInterface|string $type): string
49 | {
50 | return 'ALTER TABLE '
51 | . $this->quoter->quoteTableName($table)
52 | . ' MODIFY '
53 | . $this->quoter->quoteColumnName($column)
54 | . ' ' . $this->queryBuilder->buildColumnDefinition($type);
55 | }
56 |
57 | public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
58 | {
59 | throw new NotSupportedException(__METHOD__ . ' is not supported by Oracle.');
60 | }
61 |
62 | public function dropCommentFromColumn(string $table, string $column): string
63 | {
64 | return 'COMMENT ON COLUMN '
65 | . $this->quoter->quoteTableName($table)
66 | . '.'
67 | . $this->quoter->quoteColumnName($column)
68 | . " IS ''";
69 | }
70 |
71 | public function dropCommentFromTable(string $table): string
72 | {
73 | return 'COMMENT ON TABLE ' . $this->quoter->quoteTableName($table) . " IS ''";
74 | }
75 |
76 | public function dropDefaultValue(string $table, string $name): string
77 | {
78 | throw new NotSupportedException(__METHOD__ . ' is not supported by Oracle.');
79 | }
80 |
81 | public function dropIndex(string $table, string $name): string
82 | {
83 | return 'DROP INDEX ' . $this->quoter->quoteTableName($name);
84 | }
85 |
86 | /**
87 | * @throws NotSupportedException Oracle doesn't support "IF EXISTS" option on drop table.
88 | */
89 | public function dropTable(string $table, bool $ifExists = false, bool $cascade = false): string
90 | {
91 | if ($ifExists) {
92 | throw new NotSupportedException('Oracle doesn\'t support "IF EXISTS" option on drop table.');
93 | }
94 | return 'DROP TABLE '
95 | . $this->quoter->quoteTableName($table)
96 | . ($cascade ? ' CASCADE CONSTRAINTS' : '');
97 | }
98 |
99 | public function renameTable(string $oldName, string $newName): string
100 | {
101 | return 'ALTER TABLE ' . $this->quoter->quoteTableName($oldName) . ' RENAME TO ' .
102 | $this->quoter->quoteTableName($newName);
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/DMLQueryBuilder.php:
--------------------------------------------------------------------------------
1 | prepareTraversable($rows);
28 | }
29 |
30 | if (empty($rows)) {
31 | return '';
32 | }
33 |
34 | $columns = $this->extractColumnNames($rows, $columns);
35 | $values = $this->prepareBatchInsertValues($table, $rows, $columns, $params);
36 |
37 | if (empty($values)) {
38 | return '';
39 | }
40 |
41 | $query = 'INSERT INTO ' . $this->quoter->quoteTableName($table);
42 |
43 | if (count($columns) > 0) {
44 | $quotedColumnNames = array_map($this->quoter->quoteColumnName(...), $columns);
45 |
46 | $query .= ' (' . implode(', ', $quotedColumnNames) . ')';
47 | }
48 |
49 | return $query . "\nSELECT " . implode(" FROM DUAL UNION ALL\nSELECT ", $values) . ' FROM DUAL';
50 | }
51 |
52 | public function insertWithReturningPks(string $table, array|QueryInterface $columns, array &$params = []): string
53 | {
54 | throw new NotSupportedException(__METHOD__ . ' is not supported by Oracle.');
55 | }
56 |
57 | /**
58 | * @link https://docs.oracle.com/cd/B28359_01/server.111/b28286/statements_9016.htm#SQLRF01606
59 | */
60 | public function upsert(
61 | string $table,
62 | array|QueryInterface $insertColumns,
63 | array|bool $updateColumns = true,
64 | array &$params = [],
65 | ): string {
66 | $constraints = [];
67 |
68 | [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
69 | $table,
70 | $insertColumns,
71 | $updateColumns,
72 | $constraints
73 | );
74 |
75 | if (empty($uniqueNames)) {
76 | return $this->insert($table, $insertColumns, $params);
77 | }
78 |
79 | $onCondition = ['or'];
80 | $quotedTableName = $this->quoter->quoteTableName($table);
81 |
82 | foreach ($constraints as $constraint) {
83 | $columnNames = (array) $constraint->getColumnNames();
84 | $constraintCondition = ['and'];
85 | /** @psalm-var string[] $columnNames */
86 | foreach ($columnNames as $name) {
87 | $quotedName = $this->quoter->quoteColumnName($name);
88 | $constraintCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
89 | }
90 |
91 | $onCondition[] = $constraintCondition;
92 | }
93 |
94 | $on = $this->queryBuilder->buildCondition($onCondition, $params);
95 |
96 | [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
97 |
98 | if (!empty($placeholders)) {
99 | $usingSelectValues = [];
100 |
101 | foreach ($insertNames as $index => $name) {
102 | $usingSelectValues[$name] = new Expression($placeholders[$index]);
103 | }
104 |
105 | $values = $this->queryBuilder->buildSelect($usingSelectValues, $params)
106 | . ' ' . $this->queryBuilder->buildFrom(['DUAL'], $params);
107 | }
108 |
109 | $insertValues = [];
110 | $quotedInsertNames = array_map($this->quoter->quoteColumnName(...), $insertNames);
111 |
112 | foreach ($quotedInsertNames as $quotedName) {
113 | $insertValues[] = '"EXCLUDED".' . $quotedName;
114 | }
115 |
116 | $mergeSql = 'MERGE INTO ' . $quotedTableName . ' USING (' . $values . ') "EXCLUDED" ON (' . $on . ')';
117 | $insertSql = 'INSERT (' . implode(', ', $quotedInsertNames) . ')'
118 | . ' VALUES (' . implode(', ', $insertValues) . ')';
119 |
120 | if ($updateColumns === false || $updateNames === []) {
121 | /** there are no columns to update */
122 | return "$mergeSql WHEN NOT MATCHED THEN $insertSql";
123 | }
124 |
125 | if ($updateColumns === true) {
126 | $updateColumns = [];
127 | /** @psalm-var string[] $updateNames */
128 | foreach ($updateNames as $name) {
129 | $updateColumns[$name] = new Expression('"EXCLUDED".' . $this->quoter->quoteColumnName($name));
130 | }
131 | }
132 |
133 | $updates = $this->prepareUpdateSets($table, $updateColumns, $params);
134 | $updateSql = 'UPDATE SET ' . implode(', ', $updates);
135 |
136 | return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql";
137 | }
138 |
139 | public function upsertReturning(
140 | string $table,
141 | array|QueryInterface $insertColumns,
142 | array|bool $updateColumns = true,
143 | array|null $returnColumns = null,
144 | array &$params = [],
145 | ): string {
146 | throw new NotSupportedException(__METHOD__ . '() is not supported by Oracle.');
147 | }
148 |
149 | protected function prepareInsertValues(string $table, array|QueryInterface $columns, array $params = []): array
150 | {
151 | if (empty($columns)) {
152 | $names = [];
153 | $placeholders = [];
154 | $tableSchema = $this->schema->getTableSchema($table);
155 |
156 | if ($tableSchema !== null) {
157 | if (!empty($tableSchema->getPrimaryKey())) {
158 | $names = $tableSchema->getPrimaryKey();
159 | } else {
160 | /**
161 | * @psalm-suppress PossiblyNullArgument
162 | * @var string[] $names
163 | */
164 | $names = [array_key_first($tableSchema->getColumns())];
165 | }
166 |
167 | $placeholders = array_fill(0, count($names), 'DEFAULT');
168 | }
169 |
170 | return [$names, $placeholders, '', $params];
171 | }
172 |
173 | return parent::prepareInsertValues($table, $columns, $params);
174 | }
175 |
176 | public function resetSequence(string $table, int|string|null $value = null): string
177 | {
178 | $tableSchema = $this->schema->getTableSchema($table);
179 |
180 | if ($tableSchema === null) {
181 | throw new InvalidArgumentException("Table not found: '$table'.");
182 | }
183 |
184 | $sequenceName = $tableSchema->getSequenceName();
185 |
186 | if ($sequenceName === null) {
187 | throw new InvalidArgumentException("There is not sequence associated with table '$table'.");
188 | }
189 |
190 | if ($value === null && count($tableSchema->getPrimaryKey()) > 1) {
191 | throw new InvalidArgumentException("Can't reset sequence for composite primary key in table: $table");
192 | }
193 |
194 | /**
195 | * Oracle needs at least many queries to reset a sequence (see adding transactions and/or use an alter method to
196 | * avoid grant issue?)
197 | */
198 | return 'declare
199 | lastSeq number' . ($value !== null ? (' := ' . $value) : '') . ';
200 | begin' . ($value === null ? '
201 | SELECT MAX("' . $tableSchema->getPrimaryKey()[0] . '") + 1 INTO lastSeq FROM "' . $tableSchema->getName() . '";' : '') . '
202 | if lastSeq IS NULL then lastSeq := 1; end if;
203 | execute immediate \'DROP SEQUENCE "' . $sequenceName . '"\';
204 | execute immediate \'CREATE SEQUENCE "' . $sequenceName . '" START WITH \' || lastSeq || \' INCREMENT BY 1 NOMAXVALUE NOCACHE\';
205 | end;';
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/DQLQueryBuilder.php:
--------------------------------------------------------------------------------
1 | buildOrderBy($orderBy, $params);
30 |
31 | if ($orderByString !== '') {
32 | $sql .= $this->separator . $orderByString;
33 | }
34 |
35 | $filters = [];
36 |
37 | if (!empty($offset)) {
38 | $filters[] = 'rowNumId > ' .
39 | ($offset instanceof ExpressionInterface ? $this->buildExpression($offset) : (string) $offset);
40 | }
41 |
42 | if ($limit !== null) {
43 | $filters[] = 'rownum <= ' .
44 | ($limit instanceof ExpressionInterface ? $this->buildExpression($limit) : (string) $limit);
45 | }
46 |
47 | if (empty($filters)) {
48 | return $sql;
49 | }
50 |
51 | $filter = implode(' AND ', $filters);
52 | return << InConditionBuilder::class,
87 | LikeCondition::class => LikeConditionBuilder::class,
88 | ];
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Driver.php:
--------------------------------------------------------------------------------
1 | attributes += [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
20 |
21 | $pdo = parent::createConnection();
22 |
23 | $pdo->exec(
24 | << $options
18 | */
19 | public function __construct(
20 | string $driver = 'oci',
21 | string $host = '127.0.0.1',
22 | string|null $databaseName = null,
23 | string $port = '1521',
24 | array $options = []
25 | ) {
26 | parent::__construct($driver, $host, $databaseName, $port, $options);
27 | }
28 |
29 | /**
30 | * @return string The Data Source Name, or DSN, contains the information required to connect to the database.
31 | *
32 | * Please refer to the [PHP manual](https://php.net/manual/en/pdo.construct.php) on the format of the DSN string.
33 | *
34 | * The `driver` array key is used as the driver prefix of the DSN, all further key-value pairs are rendered as
35 | * `key=value` and concatenated by `;`. For example:
36 | *
37 | * ```php
38 | * $dsn = new Dsn('oci', 'localhost', 'yiitest', '1521', ['charset' => 'AL32UTF8']);
39 | * $connection = new Connection($dsn->asString(), 'system', 'root');
40 | * ```
41 | *
42 | * Will result in the DSN string `oci:dbname=localhost:1521/yiitest;charset=AL32UTF8`.
43 | */
44 | public function asString(): string
45 | {
46 | $driver = $this->getDriver();
47 | $host = $this->getHost();
48 | $databaseName = $this->getDatabaseName();
49 | $port = $this->getPort();
50 | $options = $this->getOptions();
51 |
52 | $dsn = "$driver:dbname=$host:$port";
53 |
54 | if (!empty($databaseName)) {
55 | $dsn .= "/$databaseName";
56 | }
57 |
58 | foreach ($options as $key => $value) {
59 | $dsn .= ";$key=$value";
60 | }
61 |
62 | return $dsn;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/IndexType.php:
--------------------------------------------------------------------------------
1 | getQuoter();
25 | $schema = $db->getSchema();
26 |
27 | parent::__construct(
28 | $db,
29 | new DDLQueryBuilder($this, $quoter, $schema),
30 | new DMLQueryBuilder($this, $quoter, $schema),
31 | new DQLQueryBuilder($this, $quoter),
32 | new ColumnDefinitionBuilder($this),
33 | );
34 | }
35 |
36 | protected function prepareBinary(string $binary): string
37 | {
38 | return "HEXTORAW('" . bin2hex($binary) . "')";
39 | }
40 |
41 | protected function createSqlParser(string $sql): SqlParser
42 | {
43 | return new SqlParser($sql);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Quoter.php:
--------------------------------------------------------------------------------
1 | timezone) || $refresh) {
18 | /** @var string */
19 | $this->timezone = $this->db->createCommand('SELECT SESSIONTIMEZONE FROM DUAL')->queryScalar();
20 | }
21 |
22 | return $this->timezone;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/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->skipToAfterChar('"'),
24 | "'" => $this->skipQuotedWithoutEscape($this->sql[$pos]),
25 | 'q', 'Q' => $this->sql[$this->position] === "'"
26 | ? $this->skipQuotedWithQ()
27 | : null,
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 | default => null,
35 | };
36 |
37 | if ($result !== null) {
38 | $position = $pos;
39 |
40 | return $result;
41 | }
42 | }
43 |
44 | return null;
45 | }
46 |
47 | /**
48 | * Skips quoted string with Q-operator.
49 | */
50 | private function skipQuotedWithQ(): void
51 | {
52 | $endChar = match ($this->sql[++$this->position]) {
53 | '[' => ']',
54 | '<' => '>',
55 | '{' => '}',
56 | '(' => ')',
57 | default => $this->sql[$this->position],
58 | };
59 |
60 | ++$this->position;
61 |
62 | $this->skipToAfterString("$endChar'");
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/TableSchema.php:
--------------------------------------------------------------------------------
1 | foreignKeys[] = $to;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Transaction.php:
--------------------------------------------------------------------------------
1 | createCommand()->insert(
42 | 'type',
43 | [
44 | 'int_col' => 1,
45 | 'char_col' => str_repeat('x', 100),
46 | 'char_col3' => null,
47 | 'float_col' => 1.234,
48 | 'blob_col' => "\x10\x11\x12",
49 | 'timestamp_col' => new Expression("TIMESTAMP '2023-07-11 14:50:23'"),
50 | 'timestamp_local' => '2023-07-11 14:50:23',
51 | 'time_col' => new DateTimeImmutable('14:50:23'),
52 | 'bool_col' => false,
53 | 'bit_col' => 0b0110_0110, // 102
54 | 'json_col' => [['a' => 1, 'b' => null, 'c' => [1, 3, 5]]],
55 | ]
56 | )->execute();
57 | }
58 |
59 | private function assertTypecastedValues(array $result, bool $allTypecasted = false): void
60 | {
61 | $utcTimezone = new DateTimeZone('UTC');
62 |
63 | $this->assertSame(1, $result['int_col']);
64 | $this->assertSame(str_repeat('x', 100), $result['char_col']);
65 | $this->assertNull($result['char_col3']);
66 | $this->assertSame(1.234, $result['float_col']);
67 | $this->assertSame("\x10\x11\x12", stream_get_contents($result['blob_col']));
68 | $this->assertEquals(new DateTimeImmutable('2023-07-11 14:50:23', $utcTimezone), $result['timestamp_col']);
69 | $this->assertEquals(new DateTimeImmutable('2023-07-11 14:50:23', $utcTimezone), $result['timestamp_local']);
70 | $this->assertEquals(new DateTimeImmutable('14:50:23'), $result['time_col']);
71 | $this->assertEquals(false, $result['bool_col']);
72 | $this->assertSame(0b0110_0110, $result['bit_col']);
73 |
74 | if ($allTypecasted) {
75 | $this->assertSame([['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], $result['json_col']);
76 | } else {
77 | $this->assertSame('[{"a":1,"b":null,"c":[1,3,5]}]', stream_get_contents($result['json_col']));
78 | }
79 | }
80 |
81 | public function testQueryWithTypecasting(): void
82 | {
83 | $db = $this->getConnection();
84 | $varsion = $db->getServerInfo()->getVersion();
85 | $db->close();
86 |
87 | $isOldVersion = version_compare($varsion, '21', '<');
88 |
89 | if (!$isOldVersion) {
90 | $this->fixture = 'oci21.sql';
91 | }
92 |
93 | $db = $this->getConnection(true);
94 |
95 | $this->insertTypeValues($db);
96 |
97 | $query = (new Query($db))->from('type')->withTypecasting();
98 |
99 | $result = $query->one();
100 |
101 | $this->assertTypecastedValues($result, !$isOldVersion);
102 |
103 | $result = $query->all();
104 |
105 | $this->assertTypecastedValues($result[0], !$isOldVersion);
106 |
107 | $db->close();
108 | }
109 |
110 | public function testCommandWithPhpTypecasting(): void
111 | {
112 | $db = $this->getConnection();
113 | $varsion = $db->getServerInfo()->getVersion();
114 | $db->close();
115 |
116 | $isOldVersion = version_compare($varsion, '21', '<');
117 |
118 | if (!$isOldVersion) {
119 | $this->fixture = 'oci21.sql';
120 | }
121 |
122 | $db = $this->getConnection(true);
123 |
124 | $this->insertTypeValues($db);
125 |
126 | $command = $db->createCommand('SELECT * FROM "type"');
127 |
128 | $result = $command->withPhpTypecasting()->queryOne();
129 |
130 | $this->assertTypecastedValues($result, !$isOldVersion);
131 |
132 | $result = $command->withPhpTypecasting()->queryAll();
133 |
134 | $this->assertTypecastedValues($result[0], !$isOldVersion);
135 |
136 | $db->close();
137 | }
138 |
139 | public function testSelectWithPhpTypecasting(): void
140 | {
141 | $db = $this->getConnection();
142 |
143 | $sql = "SELECT null, 1, 2.5, 'string' FROM DUAL";
144 |
145 | $expected = [
146 | 'NULL' => null,
147 | 1 => 1.0,
148 | '2.5' => 2.5,
149 | "'STRING'" => 'string',
150 | ];
151 |
152 | $result = $db->createCommand($sql)
153 | ->withPhpTypecasting()
154 | ->queryOne();
155 |
156 | $this->assertSame($expected, $result);
157 |
158 | $result = $db->createCommand($sql)
159 | ->withPhpTypecasting()
160 | ->queryAll();
161 |
162 | $this->assertSame([$expected], $result);
163 |
164 | $result = $db->createCommand('SELECT 2.5 FROM DUAL')
165 | ->withPhpTypecasting()
166 | ->queryScalar();
167 |
168 | $this->assertSame(2.5, $result);
169 |
170 | $result = $db->createCommand('SELECT 2.5 FROM DUAL UNION SELECT 3.3 FROM DUAL')
171 | ->withPhpTypecasting()
172 | ->queryColumn();
173 |
174 | $this->assertSame([2.5, 3.3], $result);
175 |
176 | $db->close();
177 | }
178 |
179 | public function testPhpTypeCast(): void
180 | {
181 | $db = $this->getConnection();
182 |
183 | if (version_compare($db->getServerInfo()->getVersion(), '21', '>=')) {
184 | $this->fixture = 'oci21.sql';
185 | }
186 |
187 | $db->close();
188 | $db = $this->getConnection(true);
189 | $schema = $db->getSchema();
190 | $columns = $schema->getTableSchema('type')->getColumns();
191 |
192 | $this->insertTypeValues($db);
193 |
194 | $query = (new Query($db))->from('type')->one();
195 |
196 | $result = [];
197 |
198 | foreach ($columns as $columnName => $column) {
199 | $result[$columnName] = $column->phpTypecast($query[$columnName]);
200 | }
201 |
202 | $this->assertTypecastedValues($result, true);
203 |
204 | $db->close();
205 | }
206 |
207 | public function testColumnInstance(): void
208 | {
209 | $db = $this->getConnection();
210 |
211 | if (version_compare($db->getServerInfo()->getVersion(), '21', '>=')) {
212 | $this->fixture = 'oci21.sql';
213 | }
214 |
215 | $db->close();
216 | $db = $this->getConnection(true);
217 |
218 | $schema = $db->getSchema();
219 | $tableSchema = $schema->getTableSchema('type');
220 |
221 | $this->assertInstanceOf(IntegerColumn::class, $tableSchema->getColumn('int_col'));
222 | $this->assertInstanceOf(StringColumn::class, $tableSchema->getColumn('char_col'));
223 | $this->assertInstanceOf(DoubleColumn::class, $tableSchema->getColumn('float_col'));
224 | $this->assertInstanceOf(BinaryColumn::class, $tableSchema->getColumn('blob_col'));
225 | $this->assertInstanceOf(JsonColumn::class, $tableSchema->getColumn('json_col'));
226 | }
227 |
228 | #[DataProviderExternal(ColumnProvider::class, 'predefinedTypes')]
229 | public function testPredefinedType(string $className, string $type, string $phpType)
230 | {
231 | parent::testPredefinedType($className, $type, $phpType);
232 | }
233 |
234 | #[DataProviderExternal(ColumnProvider::class, 'dbTypecastColumns')]
235 | public function testDbTypecastColumns(ColumnInterface $column, array $values)
236 | {
237 | parent::testDbTypecastColumns($column, $values);
238 | }
239 |
240 | #[DataProviderExternal(ColumnProvider::class, 'phpTypecastColumns')]
241 | public function testPhpTypecastColumns(ColumnInterface $column, array $values)
242 | {
243 | parent::testPhpTypecastColumns($column, $values);
244 | }
245 |
246 | public function testBinaryColumn(): void
247 | {
248 | $binaryCol = new BinaryColumn();
249 | $binaryCol->dbType('blob');
250 |
251 | $this->assertInstanceOf(Expression::class, $binaryCol->dbTypecast("\x10\x11\x12"));
252 | $this->assertInstanceOf(
253 | Expression::class,
254 | $binaryCol->dbTypecast(new Param("\x10\x11\x12", PDO::PARAM_LOB)),
255 | );
256 | }
257 |
258 | public function testJsonColumn(): void
259 | {
260 | $jsonCol = new JsonColumn();
261 |
262 | $this->assertNull($jsonCol->phpTypecast(null));
263 | }
264 |
265 | public function testUniqueColumn(): void
266 | {
267 | $db = $this->getConnection(true);
268 | $schema = $db->getSchema();
269 |
270 | $this->assertTrue($schema->getTableSchema('T_constraints_1')?->getColumn('C_unique')->isUnique());
271 | $this->assertFalse($schema->getTableSchema('T_constraints_2')?->getColumn('C_index_2_1')->isUnique());
272 | $this->assertFalse($schema->getTableSchema('T_constraints_2')?->getColumn('C_index_2_2')->isUnique());
273 | $this->assertTrue($schema->getTableSchema('T_upsert')?->getColumn('email')->isUnique());
274 | $this->assertFalse($schema->getTableSchema('T_upsert')?->getColumn('recovery_email')->isUnique());
275 | }
276 |
277 | public function testTimestampColumnOnDifferentTimezones(): void
278 | {
279 | $db = $this->getConnection();
280 | $schema = $db->getSchema();
281 | $command = $db->createCommand();
282 | $tableName = 'timestamp_column_test';
283 |
284 | $command->setSql("ALTER SESSION SET TIME_ZONE = '+03:00'")->execute();
285 |
286 | $this->assertSame('+03:00', $db->getServerInfo()->getTimezone());
287 |
288 | $phpTimezone = date_default_timezone_get();
289 | date_default_timezone_set('America/New_York');
290 |
291 | if ($schema->hasTable($tableName)) {
292 | $command->dropTable($tableName)->execute();
293 | }
294 |
295 | $command->createTable(
296 | $tableName,
297 | [
298 | 'timestamp_col' => ColumnBuilder::timestamp(),
299 | 'datetime_col' => ColumnBuilder::datetime(),
300 | ]
301 | )->execute();
302 |
303 | $command->insert($tableName, [
304 | 'timestamp_col' => new DateTimeImmutable('2025-04-19 14:11:35'),
305 | 'datetime_col' => new DateTimeImmutable('2025-04-19 14:11:35'),
306 | ])->execute();
307 |
308 | $command->setSql("ALTER SESSION SET TIME_ZONE = '+04:00'")->execute();
309 |
310 | $this->assertSame('+04:00', $db->getServerInfo()->getTimezone(true));
311 |
312 | $columns = $schema->getTableSchema($tableName, true)->getColumns();
313 | $query = (new Query($db))->from($tableName);
314 |
315 | $result = $query->one();
316 |
317 | $this->assertEquals(new DateTimeImmutable('2025-04-19 14:11:35'), $columns['timestamp_col']->phpTypecast($result['timestamp_col']));
318 | $this->assertEquals(new DateTimeImmutable('2025-04-19 14:11:35'), $columns['datetime_col']->phpTypecast($result['datetime_col']));
319 |
320 | $result = $query->withTypecasting()->one();
321 |
322 | $this->assertEquals(new DateTimeImmutable('2025-04-19 14:11:35'), $result['timestamp_col']);
323 | $this->assertEquals(new DateTimeImmutable('2025-04-19 14:11:35'), $result['datetime_col']);
324 |
325 | date_default_timezone_set($phpTimezone);
326 |
327 | $db->close();
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/tests/ConnectionTest.php:
--------------------------------------------------------------------------------
1 | getConnection();
38 |
39 | $db->open();
40 | $serialized = serialize($db);
41 | $unserialized = unserialize($serialized);
42 |
43 | $this->assertInstanceOf(ConnectionInterface::class, $unserialized);
44 | $this->assertSame('123', $unserialized->createCommand('SELECT 123 FROM DUAL')->queryScalar());
45 |
46 | $db->close();
47 | }
48 |
49 | /**
50 | * @throws Exception
51 | * @throws InvalidConfigException
52 | */
53 | public function testSettingDefaultAttributes(): void
54 | {
55 | $db = $this->getConnection();
56 |
57 | $this->assertSame(PDO::ERRMODE_EXCEPTION, $db->getActivePDO()?->getAttribute(PDO::ATTR_ERRMODE));
58 |
59 | $db->close();
60 | }
61 |
62 | /**
63 | * @throws Exception
64 | * @throws InvalidConfigException
65 | * @throws NotSupportedException
66 | * @throws Throwable
67 | */
68 | public function testTransactionIsolation(): void
69 | {
70 | $db = $this->getConnection();
71 |
72 | $transaction = $db->beginTransaction(TransactionInterface::READ_COMMITTED);
73 | $transaction->commit();
74 |
75 | /* should not be any exception so far */
76 | $this->assertTrue(true);
77 |
78 | $transaction = $db->beginTransaction(TransactionInterface::SERIALIZABLE);
79 | $transaction->commit();
80 |
81 | /* should not be any exception so far */
82 | $this->assertTrue(true);
83 |
84 | $db->close();
85 | }
86 |
87 | /**
88 | * @throws Exception
89 | * @throws InvalidConfigException
90 | * @throws Throwable
91 | */
92 | public function testTransactionShortcutCustom(): void
93 | {
94 | $db = $this->getConnection(true);
95 |
96 | $command = $db->createCommand();
97 |
98 | $this->assertTrue(
99 | $db->transaction(
100 | static function (ConnectionInterface $db) {
101 | $db->createCommand()->insert('profile', ['description' => 'test transaction shortcut'])->execute();
102 |
103 | return true;
104 | },
105 | TransactionInterface::READ_COMMITTED,
106 | ),
107 | 'transaction shortcut valid value should be returned from callback',
108 | );
109 |
110 | $this->assertSame(
111 | '1',
112 | $command->setSql(
113 | <<queryScalar(),
117 | 'profile should be inserted in transaction shortcut',
118 | );
119 |
120 | $db->close();
121 | }
122 |
123 | public function testSerialized(): void
124 | {
125 | $connection = $this->getConnection();
126 | $connection->open();
127 | $serialized = serialize($connection);
128 | $this->assertNotNull($connection->getPDO());
129 |
130 | $unserialized = unserialize($serialized);
131 | $this->assertInstanceOf(PdoConnectionInterface::class, $unserialized);
132 | $this->assertNull($unserialized->getPDO());
133 | $this->assertEquals(123, $unserialized->createCommand('SELECT 123 FROM DUAL')->queryScalar());
134 | $this->assertNotNull($connection->getPDO());
135 | }
136 |
137 | public function testGetColumnFactory(): void
138 | {
139 | $db = $this->getConnection();
140 |
141 | $this->assertInstanceOf(ColumnFactory::class, $db->getColumnFactory());
142 |
143 | $db->close();
144 | }
145 |
146 | public function testUserDefinedColumnFactory(): void
147 | {
148 | $columnFactory = new ColumnFactory();
149 |
150 | $db = new Connection($this->getDriver(), DbHelper::getSchemaCache(), $columnFactory);
151 |
152 | $this->assertSame($columnFactory, $db->getColumnFactory());
153 |
154 | $db->close();
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/tests/DsnTest.php:
--------------------------------------------------------------------------------
1 | assertSame(
20 | 'oci:dbname=localhost:1521;charset=AL32UTF8',
21 | (new Dsn('oci', 'localhost', port: '1521', options: ['charset' => 'AL32UTF8']))->asString(),
22 | );
23 | }
24 |
25 | public function testAsStringWithDatabaseNameWithEmptyString(): void
26 | {
27 | $this->assertSame(
28 | 'oci:dbname=localhost:1521;charset=AL32UTF8',
29 | (new Dsn('oci', 'localhost', '', '1521', ['charset' => 'AL32UTF8']))->asString(),
30 | );
31 | }
32 |
33 | public function testAsStringWithDatabaseNameWithNull(): void
34 | {
35 | $this->assertSame(
36 | 'oci:dbname=localhost:1521;charset=AL32UTF8',
37 | (new Dsn('oci', 'localhost', null, '1521', ['charset' => 'AL32UTF8']))->asString(),
38 | );
39 | }
40 |
41 | /**
42 | * Oracle service name it support only in version 18 and higher, for docker image gvenzl/oracle-xe:18
43 | */
44 | public function testAsStringWithService(): void
45 | {
46 | $this->assertSame(
47 | 'oci:dbname=localhost:1521/yiitest;charset=AL32UTF8',
48 | (new Dsn('oci', 'localhost', 'yiitest', '1521', ['charset' => 'AL32UTF8']))->asString(),
49 | );
50 | }
51 |
52 | public function testAsStringWithSID(): void
53 | {
54 | $this->assertSame(
55 | 'oci:dbname=localhost:1521/XE;charset=AL32UTF8',
56 | (new Dsn('oci', 'localhost', 'XE', '1521', ['charset' => 'AL32UTF8']))->asString(),
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/PdoCommandTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('It must be implemented.');
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/PdoConnectionTest.php:
--------------------------------------------------------------------------------
1 | getConnection(true);
34 |
35 | $command = $db->createCommand();
36 | $command->insert('item', ['name' => 'Yii2 starter', 'category_id' => 1])->execute();
37 | $command->insert('item', ['name' => 'Yii3 starter', 'category_id' => 1])->execute();
38 |
39 | $this->assertSame('7', $db->getLastInsertId('item_SEQ'));
40 |
41 | $db->close();
42 | }
43 |
44 | /**
45 | * @throws Exception
46 | * @throws InvalidConfigException
47 | * @throws InvalidCallException
48 | * @throws Throwable
49 | */
50 | public function testGetLastInsertIDWithException(): void
51 | {
52 | $db = $this->getConnection(true);
53 |
54 | $command = $db->createCommand();
55 | $command->insert('item', ['name' => 'Yii2 starter', 'category_id' => 1])->execute();
56 | $command->insert('item', ['name' => 'Yii3 starter', 'category_id' => 1])->execute();
57 |
58 | $this->expectException(InvalidArgumentException::class);
59 | $this->expectExceptionMessage('Oracle not support lastInsertId without sequence name.');
60 |
61 | $db->getLastInsertId();
62 | }
63 |
64 | /**
65 | * @throws Exception
66 | * @throws InvalidConfigException
67 | * @throws InvalidCallException
68 | * @throws Throwable
69 | */
70 | public function testGetLastInsertIdWithTwoConnection()
71 | {
72 | $db1 = $this->getConnection();
73 | $db2 = $this->getConnection();
74 |
75 | $sql = 'INSERT INTO {{profile}}([[description]]) VALUES (\'non duplicate1\')';
76 | $db1->createCommand($sql)->execute();
77 |
78 | $sql = 'INSERT INTO {{profile}}([[description]]) VALUES (\'non duplicate2\')';
79 | $db2->createCommand($sql)->execute();
80 |
81 | $this->assertNotEquals($db1->getLastInsertId('profile_SEQ'), $db2->getLastInsertId('profile_SEQ'));
82 | $this->assertNotEquals($db2->getLastInsertId('profile_SEQ'), $db1->getLastInsertId('profile_SEQ'));
83 |
84 | $db1->close();
85 | $db2->close();
86 | }
87 |
88 | public function testGetServerInfo(): void
89 | {
90 | $db = $this->getConnection();
91 | $serverInfo = $db->getServerInfo();
92 |
93 | $this->assertInstanceOf(ServerInfo::class, $serverInfo);
94 |
95 | $dbTimezone = $serverInfo->getTimezone();
96 |
97 | $this->assertSame(6, strlen($dbTimezone));
98 |
99 | $db->createCommand("ALTER SESSION SET TIME_ZONE = '+06:15'")->execute();
100 |
101 | $this->assertSame($dbTimezone, $serverInfo->getTimezone());
102 | $this->assertNotSame($dbTimezone, $serverInfo->getTimezone(true));
103 | $this->assertSame('+06:15', $serverInfo->getTimezone());
104 |
105 | $db->close();
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/tests/Provider/ColumnBuilderProvider.php:
--------------------------------------------------------------------------------
1 | 'long raw']],
14 | ['interval day to second', ['type' => 'interval day to second']],
15 | ['interval day to second (2)', ['type' => 'interval day to second', 'size' => 2]],
16 | ['interval day(0) to second(2)', ['type' => 'interval day to second', 'size' => 2, 'scale' => 0]],
17 | ['timestamp with time zone', ['type' => 'timestamp with time zone']],
18 | ['timestamp (3) with time zone', ['type' => 'timestamp with time zone', 'size' => 3]],
19 | ['timestamp(3) with local time zone', ['type' => 'timestamp with local time zone', 'size' => 3]],
20 | ['interval year to month', ['type' => 'interval year to month']],
21 | ['interval year (3) to month', ['type' => 'interval year to month', 'scale' => 3]],
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Provider/ColumnFactoryProvider.php:
--------------------------------------------------------------------------------
1 | dbType('clob');
55 | $definitions['text NOT NULL'][0] = 'clob NOT NULL';
56 | $definitions['text NOT NULL'][1]->dbType('clob');
57 | $definitions['decimal(10,2)'][0] = 'number(10,2)';
58 | $definitions['decimal(10,2)'][1]->dbType('number');
59 | $definitions['bigint UNSIGNED'][1] = new BigIntColumn(unsigned: true);
60 | $definitions['integer[]'] = ['number(10,0)[]', new ArrayColumn(dbType: 'number', size: 10, column: new IntegerColumn(dbType: 'number', size: 10))];
61 |
62 | return [
63 | ...$definitions,
64 | ['interval day to second', new StringColumn(dbType: 'interval day to second')],
65 | ['interval day(0) to second', new DateTimeColumn(ColumnType::TIME, dbType: 'interval day to second', scale: 0)],
66 | ['interval day (0) to second(6)', new DateTimeColumn(ColumnType::TIME, dbType: 'interval day to second', scale: 0, size: 6)],
67 | ['interval day to second (0)', new StringColumn(dbType: 'interval day to second', size: 0)],
68 | ['interval year to month', new StringColumn(dbType: 'interval year to month')],
69 | ['interval year (2) to month', new StringColumn(dbType: 'interval year to month', scale: 2)],
70 | ];
71 | }
72 |
73 | public static function defaultValueRaw(): array
74 | {
75 | $defaultValueRaw = parent::defaultValueRaw();
76 |
77 | $defaultValueRaw[] = [ColumnType::STRING, 'NULL ', null];
78 | $defaultValueRaw[] = [ColumnType::STRING, "'str''ing' ", "str'ing"];
79 | $defaultValueRaw[] = [ColumnType::INTEGER, '-1 ', -1];
80 | $defaultValueRaw[] = [ColumnType::DATETIME, 'now() ', new Expression('now()')];
81 |
82 | return $defaultValueRaw;
83 | }
84 |
85 | public static function types(): array
86 | {
87 | $types = parent::types();
88 |
89 | $types['binary'][2] = BinaryColumn::class;
90 | $types['boolean'][2] = BooleanColumn::class;
91 | $types['json'][2] = JsonColumn::class;
92 |
93 | return $types;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/Provider/ColumnProvider.php:
--------------------------------------------------------------------------------
1 | '1',
23 | 'email' => 'user1@example.com',
24 | 'name' => 'user1',
25 | 'address' => 'address1',
26 | 'status' => '1',
27 | 'profile_id' => '1',
28 | ],
29 | ],
30 | ];
31 | }
32 |
33 | public static function bindParamsNonWhere(): array
34 | {
35 | return [
36 | [
37 | << [
31 | ':qp1' => '1',
32 | ':qp2' => 'test string2',
33 | ':qp3' => '0',
34 | ],
35 | 'issue11242' => [
36 | ':qp1' => '1',
37 | ],
38 | 'table name with column name with brackets' => [
39 | ':qp1' => '0',
40 | ],
41 | 'binds params from expression' => [
42 | ':qp2' => '0',
43 | ],
44 | 'with associative values with different keys' => [
45 | ':qp1' => '1',
46 | ],
47 | 'with associative values with different keys and columns with keys' => [
48 | ':qp1' => '1',
49 | ],
50 | 'with associative values with keys of column names' => [
51 | ':qp0' => '1',
52 | ':qp1' => '10',
53 | ],
54 | 'with associative values with keys of column keys' => [
55 | ':qp0' => '1',
56 | ':qp1' => '10',
57 | ],
58 | 'with shuffled indexes of values' => [
59 | ':qp0' => '1',
60 | ':qp1' => '10',
61 | ],
62 | 'empty columns and associative values' => [
63 | ':qp1' => '1',
64 | ],
65 | 'empty columns and objects' => [
66 | ':qp1' => '1',
67 | ],
68 | 'empty columns and a Traversable value' => [
69 | ':qp1' => '1',
70 | ],
71 | 'empty columns and Traversable values' => [
72 | ':qp1' => '1',
73 | ],
74 | 'binds json params' => [
75 | ':qp1' => '1',
76 | ':qp2' => '{"a":1,"b":true,"c":[1,2,3]}',
77 | ':qp3' => 'b',
78 | ':qp4' => '0',
79 | ':qp5' => '{"d":"e","f":false,"g":[4,5,null]}',
80 | ],
81 | ];
82 |
83 | foreach ($replaceParams as $key => $expectedParams) {
84 | DbHelper::changeSqlForOracleBatchInsert($batchInsert[$key]['expected'], $expectedParams);
85 | $batchInsert[$key]['expectedParams'] = array_merge($batchInsert[$key]['expectedParams'], $expectedParams);
86 | }
87 |
88 | $batchInsert['multirow']['expected'] = << 'string', 'integer' => 1234], JSON_THROW_ON_ERROR),
126 | json_encode(['string' => 'string', 'integer' => 1234], JSON_THROW_ON_ERROR),
127 | ],
128 | [
129 | serialize(['string' => 'string', 'integer' => 1234]),
130 | new Param(serialize(['string' => 'string', 'integer' => 1234]), PDO::PARAM_LOB),
131 | ],
132 | ['simple string', 'simple string'],
133 | ];
134 | }
135 |
136 | public static function rawSql(): array
137 | {
138 | $rawSql = parent::rawSql();
139 |
140 | foreach ($rawSql as &$values) {
141 | $values[2] = strtr($values[2], [
142 | 'FALSE' => "'0'",
143 | 'TRUE' => "'1'",
144 | ]);
145 | }
146 |
147 | return $rawSql;
148 | }
149 |
150 | public static function createIndex(): array
151 | {
152 | return [
153 | ...parent::createIndex(),
154 | [['col1' => ColumnBuilder::integer()], ['col1'], IndexType::UNIQUE, null],
155 | [['col1' => ColumnBuilder::integer()], ['col1'], IndexType::BITMAP, null],
156 | ];
157 | }
158 |
159 | public static function upsertReturning(): array
160 | {
161 | return [['table', [], true, ['col1'], [], []]];
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/tests/Provider/QuoterProvider.php:
--------------------------------------------------------------------------------
1 | getServerInfo()->getTimezone();
30 | $db->close();
31 |
32 | return [
33 | [
34 | [
35 | 'int_col' => new IntegerColumn(
36 | dbType: 'number',
37 | notNull: true,
38 | scale: 0,
39 | ),
40 | 'int_col2' => new IntegerColumn(
41 | dbType: 'number',
42 | scale: 0,
43 | defaultValue: 1,
44 | ),
45 | 'tinyint_col' => new IntegerColumn(
46 | dbType: 'number',
47 | size: 3,
48 | scale: 0,
49 | defaultValue: 1,
50 | ),
51 | 'smallint_col' => new IntegerColumn(
52 | dbType: 'number',
53 | scale: 0,
54 | defaultValue: 1,
55 | ),
56 | 'char_col' => new StringColumn(
57 | ColumnType::CHAR,
58 | dbType: 'char',
59 | notNull: true,
60 | size: 100,
61 | ),
62 | 'char_col2' => new StringColumn(
63 | dbType: 'varchar2',
64 | size: 100,
65 | defaultValue: 'some\'thing',
66 | ),
67 | 'char_col3' => new StringColumn(
68 | dbType: 'varchar2',
69 | size: 4000,
70 | ),
71 | 'nvarchar_col' => new StringColumn(
72 | dbType: 'nvarchar2',
73 | size: 100,
74 | defaultValue: '',
75 | ),
76 | 'float_col' => new DoubleColumn(
77 | dbType: 'float',
78 | notNull: true,
79 | size: 126,
80 | ),
81 | 'float_col2' => new DoubleColumn(
82 | dbType: 'float',
83 | size: 126,
84 | defaultValue: 1.23,
85 | ),
86 | 'blob_col' => new BinaryColumn(
87 | dbType: 'blob',
88 | ),
89 | 'numeric_col' => new DoubleColumn(
90 | ColumnType::DECIMAL,
91 | dbType: 'number',
92 | size: 5,
93 | scale: 2,
94 | defaultValue: 33.22,
95 | ),
96 | 'timestamp_col' => new DateTimeColumn(
97 | dbType: 'timestamp',
98 | notNull: true,
99 | size: 6,
100 | defaultValue: new DateTimeImmutable('2002-01-01 00:00:00', new DateTimeZone('UTC')),
101 | shouldConvertTimezone: true,
102 | ),
103 | 'timestamp_local' => new DateTimeColumn(
104 | dbType: 'timestamp with local time zone',
105 | size:6,
106 | dbTimezone: $dbTimezone,
107 | ),
108 | 'time_col' => new DateTimeColumn(
109 | ColumnType::TIME,
110 | dbType: 'interval day to second',
111 | size: 0,
112 | scale: 0,
113 | defaultValue: new DateTimeImmutable('10:33:21', new DateTimeZone('UTC')),
114 | shouldConvertTimezone: true,
115 | ),
116 | 'interval_day_col' => new StringColumn(
117 | dbType: 'interval day to second',
118 | size: 0,
119 | scale: 1,
120 | defaultValue: new Expression("INTERVAL '2 04:56:12' DAY(1) TO SECOND(0)"),
121 | ),
122 | 'bool_col' => new BooleanColumn(
123 | dbType: 'char',
124 | check: '"bool_col" in (0,1)',
125 | notNull: true,
126 | size: 1,
127 | ),
128 | 'bool_col2' => new BooleanColumn(
129 | dbType: 'char',
130 | check: '"bool_col2" in (0,1)',
131 | size: 1,
132 | defaultValue: true,
133 | ),
134 | 'ts_default' => new DateTimeColumn(
135 | dbType: 'timestamp',
136 | notNull: true,
137 | size: 6,
138 | defaultValue: new Expression('CURRENT_TIMESTAMP'),
139 | ),
140 | 'bit_col' => new IntegerColumn(
141 | dbType: 'number',
142 | notNull: true,
143 | size: 3,
144 | scale: 0,
145 | defaultValue: 130, // b'10000010'
146 | ),
147 | 'json_col' => new JsonColumn(
148 | dbType: 'clob',
149 | defaultValue: ['a' => 1],
150 | check: '"json_col" is json',
151 | ),
152 | ],
153 | 'type',
154 | ],
155 | [
156 | [
157 | 'id' => new IntegerColumn(
158 | dbType: 'number',
159 | primaryKey: true,
160 | notNull: true,
161 | autoIncrement: true,
162 | scale: 0,
163 | ),
164 | 'type' => new StringColumn(
165 | dbType: 'varchar2',
166 | notNull: true,
167 | size: 255,
168 | ),
169 | ],
170 | 'animal',
171 | ],
172 | ];
173 | }
174 |
175 | public static function constraints(): array
176 | {
177 | $constraints = parent::constraints();
178 |
179 | $constraints['1: check'][2][0]->expression('"C_check" <> \'\'');
180 | $constraints['1: check'][2][] = (new CheckConstraint())
181 | ->name(AnyValue::getInstance())
182 | ->columnNames(['C_id'])
183 | ->expression('"C_id" IS NOT NULL');
184 | $constraints['1: check'][2][] = (new CheckConstraint())
185 | ->name(AnyValue::getInstance())
186 | ->columnNames(['C_not_null'])
187 | ->expression('"C_not_null" IS NOT NULL');
188 | $constraints['1: check'][2][] = (new CheckConstraint())
189 | ->name(AnyValue::getInstance())
190 | ->columnNames(['C_unique'])
191 | ->expression('"C_unique" IS NOT NULL');
192 | $constraints['1: check'][2][] = (new CheckConstraint())
193 | ->name(AnyValue::getInstance())
194 | ->columnNames(['C_default'])
195 | ->expression('"C_default" IS NOT NULL');
196 |
197 | $constraints['2: check'][2][] = (new CheckConstraint())
198 | ->name(AnyValue::getInstance())
199 | ->columnNames(['C_id_1'])
200 | ->expression('"C_id_1" IS NOT NULL');
201 | $constraints['2: check'][2][] = (new CheckConstraint())
202 | ->name(AnyValue::getInstance())
203 | ->columnNames(['C_id_2'])
204 | ->expression('"C_id_2" IS NOT NULL');
205 |
206 | $constraints['3: foreign key'][2][0]->foreignSchemaName('SYSTEM');
207 | $constraints['3: foreign key'][2][0]->onUpdate(null);
208 | $constraints['3: index'][2] = [];
209 | $constraints['3: check'][2][] = (new CheckConstraint())
210 | ->name(AnyValue::getInstance())
211 | ->columnNames(['C_fk_id_1'])
212 | ->expression('"C_fk_id_1" IS NOT NULL');
213 | $constraints['3: check'][2][] = (new CheckConstraint())
214 | ->name(AnyValue::getInstance())
215 | ->columnNames(['C_fk_id_2'])
216 | ->expression('"C_fk_id_2" IS NOT NULL');
217 | $constraints['3: check'][2][] = (new CheckConstraint())
218 | ->name(AnyValue::getInstance())
219 | ->columnNames(['C_id'])
220 | ->expression('"C_id" IS NOT NULL');
221 |
222 | $constraints['4: check'][2][] = (new CheckConstraint())
223 | ->name(AnyValue::getInstance())
224 | ->columnNames(['C_id'])
225 | ->expression('"C_id" IS NOT NULL');
226 | $constraints['4: check'][2][] = (new CheckConstraint())
227 | ->name(AnyValue::getInstance())
228 | ->columnNames(['C_col_2'])
229 | ->expression('"C_col_2" IS NOT NULL');
230 |
231 | return $constraints;
232 | }
233 |
234 | public static function resultColumns(): array
235 | {
236 | return [
237 | [null, []],
238 | [null, ['oci:decl_type' => '']],
239 | [new IntegerColumn(dbType: 'number', name: 'int_col', notNull: true, size: 38, scale: 0), [
240 | 'oci:decl_type' => 'NUMBER',
241 | 'native_type' => 'NUMBER',
242 | 'pdo_type' => 2,
243 | 'scale' => 0,
244 | 'flags' => ['not_null'],
245 | 'name' => 'int_col',
246 | 'len' => 22,
247 | 'precision' => 38,
248 | ]],
249 | [new IntegerColumn(dbType: 'number', name: 'tinyint_col', notNull: false, size: 3, scale: 0), [
250 | 'oci:decl_type' => 'NUMBER',
251 | 'native_type' => 'NUMBER',
252 | 'pdo_type' => 2,
253 | 'scale' => 0,
254 | 'flags' => ['nullable'],
255 | 'name' => 'tinyint_col',
256 | 'len' => 22,
257 | 'precision' => 3,
258 | ]],
259 | [new StringColumn(ColumnType::CHAR, dbType: 'char', name: 'char_col', notNull: true, size: 100), [
260 | 'oci:decl_type' => 'CHAR',
261 | 'native_type' => 'CHAR',
262 | 'pdo_type' => 2,
263 | 'scale' => 0,
264 | 'flags' => ['not_null'],
265 | 'name' => 'char_col',
266 | 'len' => 100,
267 | 'precision' => 0,
268 | ]],
269 | [new StringColumn(dbType: 'varchar2', name: 'char_col2', notNull: false, size: 100), [
270 | 'oci:decl_type' => 'VARCHAR2',
271 | 'native_type' => 'VARCHAR2',
272 | 'pdo_type' => 2,
273 | 'scale' => 0,
274 | 'flags' => ['nullable'],
275 | 'name' => 'char_col2',
276 | 'len' => 100,
277 | 'precision' => 0,
278 | ]],
279 | [new DoubleColumn(dbType: 'float', name: 'float_col', notNull: true, size: 126), [
280 | 'oci:decl_type' => 'FLOAT',
281 | 'native_type' => 'FLOAT',
282 | 'pdo_type' => 2,
283 | 'scale' => -127,
284 | 'flags' => ['not_null'],
285 | 'name' => 'float_col',
286 | 'len' => 22,
287 | 'precision' => 126,
288 | ]],
289 | [new BinaryColumn(dbType: 'blob', name: 'blob_col', notNull: false, size: 4000), [
290 | 'oci:decl_type' => 'BLOB',
291 | 'native_type' => 'BLOB',
292 | 'pdo_type' => 3,
293 | 'scale' => 0,
294 | 'flags' => ['blob', 'nullable'],
295 | 'name' => 'blob_col',
296 | 'len' => 4000,
297 | 'precision' => 0,
298 | ]],
299 | [new DoubleColumn(ColumnType::DECIMAL, dbType: 'number', name: 'numeric_col', notNull: false, size: 5, scale: 2), [
300 | 'oci:decl_type' => 'NUMBER',
301 | 'native_type' => 'NUMBER',
302 | 'pdo_type' => 2,
303 | 'scale' => 2,
304 | 'flags' => ['nullable'],
305 | 'name' => 'numeric_col',
306 | 'len' => 22,
307 | 'precision' => 5,
308 | ]],
309 | [new DateTimeColumn(dbType: 'timestamp', name: 'timestamp_col', notNull: true, size: 6), [
310 | 'oci:decl_type' => 'TIMESTAMP',
311 | 'native_type' => 'TIMESTAMP',
312 | 'pdo_type' => 2,
313 | 'scale' => 6,
314 | 'flags' => ['not_null'],
315 | 'name' => 'timestamp_col',
316 | 'len' => 11,
317 | 'precision' => 0,
318 | ]],
319 | [new DateTimeColumn(ColumnType::TIME, dbType: 'interval day to second', name: 'time_col', notNull: false, size: 0), [
320 | 'oci:decl_type' => 'INTERVAL DAY TO SECOND',
321 | 'native_type' => 'INTERVAL DAY TO SECOND',
322 | 'pdo_type' => 2,
323 | 'scale' => 0,
324 | 'flags' => ['nullable'],
325 | 'name' => 'time_col',
326 | 'len' => 11,
327 | 'precision' => 0,
328 | ]],
329 | [new BinaryColumn(dbType: 'clob', name: 'json_col', notNull: false, size: 4000), [
330 | 'oci:decl_type' => 'CLOB',
331 | 'native_type' => 'CLOB',
332 | 'pdo_type' => 3,
333 | 'scale' => 0,
334 | 'flags' => ['blob', 'nullable'],
335 | 'name' => 'json_col',
336 | 'len' => 4000,
337 | 'precision' => 0,
338 | ]],
339 | [new JsonColumn(dbType: 'json', name: 'json_col', notNull: false, size: 8200), [
340 | 'oci:decl_type' => 119,
341 | 'native_type' => 'UNKNOWN',
342 | 'pdo_type' => 2,
343 | 'scale' => 0,
344 | 'flags' => ['nullable'],
345 | 'name' => 'json_col',
346 | 'len' => 8200,
347 | 'precision' => 0,
348 | ]],
349 | [new StringColumn(dbType: 'varchar2', name: 'NULL', notNull: false), [
350 | 'oci:decl_type' => 'VARCHAR2',
351 | 'native_type' => 'VARCHAR2',
352 | 'pdo_type' => 2,
353 | 'scale' => 0,
354 | 'flags' => ['nullable'],
355 | 'name' => 'NULL',
356 | 'len' => 0,
357 | 'precision' => 0,
358 | ]],
359 | [new DoubleColumn(dbType: 'number', name: '1', notNull: false), [
360 | 'oci:decl_type' => 'NUMBER',
361 | 'native_type' => 'NUMBER',
362 | 'pdo_type' => 2,
363 | 'scale' => -127,
364 | 'flags' => ['nullable'],
365 | 'name' => '1',
366 | 'len' => 2,
367 | 'precision' => 0,
368 | ]],
369 | [new StringColumn(ColumnType::CHAR, dbType: 'char', name: "'STRING'", notNull: false, size: 6), [
370 | 'oci:decl_type' => 'CHAR',
371 | 'native_type' => 'CHAR',
372 | 'pdo_type' => 2,
373 | 'scale' => 0,
374 | 'flags' => ['nullable'],
375 | 'name' => "'STRING'",
376 | 'len' => 6,
377 | 'precision' => 0,
378 | ]],
379 | [new DateTimeColumn(ColumnType::DATETIMETZ, dbType: 'timestamp with time zone', name: 'TIMESTAMP(3)', notNull: false, size: 3), [
380 | 'oci:decl_type' => 'TIMESTAMP WITH TIMEZONE',
381 | 'native_type' => 'TIMESTAMP WITH TIMEZONE',
382 | 'pdo_type' => 2,
383 | 'scale' => 3,
384 | 'flags' => ['nullable'],
385 | 'name' => 'TIMESTAMP(3)',
386 | 'len' => 13,
387 | 'precision' => 0,
388 | ]],
389 | ];
390 | }
391 |
392 | public static function tableSchemaWithDbSchemes(): array
393 | {
394 | return [
395 | ['animal', 'animal', 'dbo'],
396 | ['dbo.animal', 'animal', 'dbo'],
397 | ['"dbo"."animal"', 'animal', 'dbo'],
398 | ['"other"."animal2"', 'animal2', 'other',],
399 | ['other."animal2"', 'animal2', 'other',],
400 | ['other.animal2', 'animal2', 'other',],
401 | ['catalog.other.animal2', 'animal2', 'other'],
402 | ];
403 | }
404 | }
405 |
--------------------------------------------------------------------------------
/tests/Provider/SqlParserProvider.php:
--------------------------------------------------------------------------------
1 | getConnection();
35 |
36 | $qb = $db->getQueryBuilder();
37 |
38 | $this->expectException(NotSupportedException::class);
39 | $this->expectExceptionMessage('Yiisoft\Db\Oracle\DDLQueryBuilder::addDefaultValue is not supported by Oracle.');
40 |
41 | $qb->addDefaultValue('T_constraints_1', 'CN_pk', 'C_default', 1);
42 | }
43 |
44 | #[DataProviderExternal(QueryBuilderProvider::class, 'addForeignKey')]
45 | public function testAddForeignKey(
46 | string $name,
47 | string $table,
48 | array|string $columns,
49 | string $refTable,
50 | array|string $refColumns,
51 | string|null $delete,
52 | string|null $update,
53 | string $expected
54 | ): void {
55 | // Oracle does not support ON UPDATE CASCADE
56 | parent::testAddForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, null, $expected);
57 | }
58 |
59 | public function testAddForeignKeyUpdateException(): void
60 | {
61 | $db = $this->getConnection();
62 |
63 | $qb = $db->getQueryBuilder();
64 |
65 | $this->expectException(Exception::class);
66 | $this->expectExceptionMessage('Oracle does not support ON UPDATE clause.');
67 |
68 | $qb->addForeignKey('T_constraints_1', 'fk1', 'C_fk1', 'T_constraints_2', 'C_fk2', 'CASCADE', 'CASCADE');
69 | }
70 |
71 | #[DataProviderExternal(QueryBuilderProvider::class, 'addPrimaryKey')]
72 | public function testAddPrimaryKey(string $name, string $table, array|string $columns, string $expected): void
73 | {
74 | parent::testAddPrimaryKey($name, $table, $columns, $expected);
75 | }
76 |
77 | #[DataProviderExternal(QueryBuilderProvider::class, 'addUnique')]
78 | public function testAddUnique(string $name, string $table, array|string $columns, string $expected): void
79 | {
80 | parent::testAddUnique($name, $table, $columns, $expected);
81 | }
82 |
83 | #[DataProviderExternal(QueryBuilderProvider::class, 'alterColumn')]
84 | public function testAlterColumn(string|ColumnInterface $type, string $expected): void
85 | {
86 | parent::testAlterColumn($type, $expected);
87 | }
88 |
89 | #[DataProviderExternal(QueryBuilderProvider::class, 'batchInsert')]
90 | public function testBatchInsert(
91 | string $table,
92 | iterable $rows,
93 | array $columns,
94 | string $expected,
95 | array $expectedParams = [],
96 | ): void {
97 | parent::testBatchInsert($table, $rows, $columns, $expected, $expectedParams);
98 | }
99 |
100 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildCondition')]
101 | public function testBuildCondition(
102 | array|ExpressionInterface|string $condition,
103 | string|null $expected,
104 | array $expectedParams
105 | ): void {
106 | parent::testBuildCondition($condition, $expected, $expectedParams);
107 | }
108 |
109 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildLikeCondition')]
110 | public function testBuildLikeCondition(
111 | array|ExpressionInterface $condition,
112 | string $expected,
113 | array $expectedParams
114 | ): void {
115 | parent::testBuildLikeCondition($condition, $expected, $expectedParams);
116 | }
117 |
118 | public function testBuildOrderByAndLimit(): void
119 | {
120 | $db = $this->getConnection();
121 |
122 | $qb = $db->getQueryBuilder();
123 | $query = (new Query($db))
124 | ->from('admin_user')
125 | ->orderBy(['id' => SORT_ASC, 'name' => SORT_DESC])
126 | ->limit(10)
127 | ->offset(5);
128 |
129 | $this->assertSame(
130 | << 5 AND rownum <= 10
133 | SQL,
134 | $qb->buildOrderByAndLimit(
135 | <<getOrderBy(),
139 | $query->getLimit(),
140 | $query->getOffset(),
141 | ),
142 | );
143 |
144 | $db->close();
145 | }
146 |
147 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildFrom')]
148 | public function testBuildWithFrom(mixed $table, string $expectedSql, array $expectedParams = []): void
149 | {
150 | parent::testBuildWithFrom($table, $expectedSql, $expectedParams);
151 | }
152 |
153 | public function testBuildWithLimit(): void
154 | {
155 | $db = $this->getConnection();
156 |
157 | $qb = $db->getQueryBuilder();
158 | $query = (new Query($db))->limit(10);
159 |
160 | [$sql, $params] = $qb->build($query);
161 |
162 | $this->assertSame(
163 | <<assertSame([], $params);
170 |
171 | $db->close();
172 | }
173 |
174 | public function testBuildWithOffset(): void
175 | {
176 | $db = $this->getConnection();
177 |
178 | $qb = $db->getQueryBuilder();
179 | $query = (new Query($db))->offset(10);
180 |
181 | [$sql, $params] = $qb->build($query);
182 |
183 | $this->assertSame(
184 | << 10
187 | SQL,
188 | $sql,
189 | );
190 | $this->assertSame([], $params);
191 |
192 | $db->close();
193 | }
194 |
195 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildWhereExists')]
196 | public function testBuildWithWhereExists(string $cond, string $expectedQuerySql): void
197 | {
198 | parent::testBuildWithWhereExists($cond, $expectedQuerySql);
199 | }
200 |
201 | public function testCheckIntegrity(): void
202 | {
203 | $db = $this->getConnection();
204 |
205 | $qb = $db->getQueryBuilder();
206 |
207 | $this->expectException(NotSupportedException::class);
208 | $this->expectExceptionMessage('Yiisoft\Db\Oracle\DDLQueryBuilder::checkIntegrity is not supported by Oracle.');
209 |
210 | $qb->checkIntegrity('', 'customer');
211 | }
212 |
213 | public function testCreateTable(): void
214 | {
215 | $db = $this->getConnection();
216 |
217 | $qb = $db->getQueryBuilder();
218 |
219 | $this->assertSame(
220 | <<createTable(
230 | 'test',
231 | [
232 | 'id' => 'pk',
233 | 'name' => 'string(255) NOT NULL',
234 | 'email' => 'string(255) NOT NULL',
235 | 'status' => 'integer NOT NULL',
236 | 'created_at' => 'datetime NOT NULL',
237 | ],
238 | ),
239 | );
240 |
241 | $db->close();
242 | }
243 |
244 | #[DataProviderExternal(QueryBuilderProvider::class, 'delete')]
245 | public function testDelete(string $table, array|string $condition, string $expectedSQL, array $expectedParams): void
246 | {
247 | parent::testDelete($table, $condition, $expectedSQL, $expectedParams);
248 | }
249 |
250 | public function testDropCommentFromColumn(): void
251 | {
252 | $db = $this->getConnection(true);
253 |
254 | $qb = $db->getQueryBuilder();
255 |
256 | $this->assertSame(
257 | <<dropCommentFromColumn('customer', 'id'),
261 | );
262 |
263 | $db->close();
264 | }
265 |
266 | public function testDropCommentFromTable(): void
267 | {
268 | $db = $this->getConnection();
269 |
270 | $qb = $db->getQueryBuilder();
271 |
272 | $this->assertSame(
273 | <<dropCommentFromTable('customer'),
277 | );
278 |
279 | $db->close();
280 | }
281 |
282 | public function testDropDefaultValue(): void
283 | {
284 | $db = $this->getConnection(true);
285 |
286 | $qb = $db->getQueryBuilder();
287 |
288 | $this->expectException(NotSupportedException::class);
289 | $this->expectExceptionMessage(
290 | 'Yiisoft\Db\Oracle\DDLQueryBuilder::dropDefaultValue is not supported by Oracle.'
291 | );
292 |
293 | $qb->dropDefaultValue('T_constraints_1', 'CN_pk');
294 | }
295 |
296 | public function testDropIndex(): void
297 | {
298 | $db = $this->getConnection();
299 |
300 | $qb = $db->getQueryBuilder();
301 |
302 | $this->assertSame(
303 | <<dropIndex('T_constraints_2', 'CN_constraints_2_single'),
307 | );
308 |
309 | $db->close();
310 | }
311 |
312 | #[DataProviderExternal(QueryBuilderProvider::class, 'insert')]
313 | public function testInsert(
314 | string $table,
315 | array|QueryInterface $columns,
316 | array $params,
317 | string $expectedSQL,
318 | array $expectedParams
319 | ): void {
320 | parent::testInsert($table, $columns, $params, $expectedSQL, $expectedParams);
321 | }
322 |
323 | #[DataProviderExternal(QueryBuilderProvider::class, 'insertWithReturningPks')]
324 | public function testInsertWithReturningPks(
325 | string $table,
326 | array|QueryInterface $columns,
327 | array $params,
328 | string $expectedSQL,
329 | array $expectedParams
330 | ): void {
331 | $this->expectException(NotSupportedException::class);
332 | $this->expectExceptionMessage(
333 | 'Yiisoft\Db\Oracle\DMLQueryBuilder::insertWithReturningPks is not supported by Oracle.',
334 | );
335 |
336 | $db = $this->getConnection(true);
337 | $qb = $db->getQueryBuilder();
338 | $qb->insertWithReturningPks($table, $columns, $params);
339 | }
340 |
341 | public function testRenameTable(): void
342 | {
343 | $db = $this->getConnection();
344 |
345 | $qb = $db->getQueryBuilder();
346 |
347 | $this->assertSame(
348 | <<renameTable('alpha', 'alpha-test'),
352 | );
353 |
354 | $db->close();
355 | }
356 |
357 | public function testResetSequence(): void
358 | {
359 | $db = $this->getConnection(true);
360 |
361 | $command = $db->createCommand();
362 | $qb = $db->getQueryBuilder();
363 |
364 | $checkSql = <<resetSequence('item');
368 |
369 | $this->assertSame(
370 | <<setSql($sql)->execute();
384 |
385 | $this->assertSame('6', $command->setSql($checkSql)->queryScalar());
386 |
387 | $sql = $qb->resetSequence('item', 4);
388 |
389 | $this->assertSame(
390 | <<setSql($sql)->execute();
403 |
404 | $this->assertEquals(4, $command->setSql($checkSql)->queryScalar());
405 |
406 | $sql = $qb->resetSequence('item', '1');
407 |
408 | $this->assertSame(
409 | <<setSql($sql)->execute();
422 |
423 | $this->assertSame('1', $db->createCommand($checkSql)->queryScalar());
424 |
425 | $db->close();
426 | }
427 |
428 | public function testResetNonExistSequenceException(): void
429 | {
430 | $db = $this->getConnection(true);
431 | $qb = $db->getQueryBuilder();
432 |
433 | $this->expectException(InvalidArgumentException::class);
434 | $this->expectExceptionMessage("There is not sequence associated with table 'default_multiple_pk'.");
435 | $qb->resetSequence('default_multiple_pk');
436 |
437 | $db->close();
438 | }
439 |
440 | public function testResetSequenceCompositeException(): void
441 | {
442 | self::markTestSkipped('Sequence name not found for composite primary key');
443 |
444 | $db = $this->getConnection(true);
445 | $qb = $db->getQueryBuilder();
446 |
447 | $this->expectException(InvalidArgumentException::class);
448 | $this->expectExceptionMessage("Can't reset sequence for composite primary key in table: employee");
449 | $qb->resetSequence('employee');
450 |
451 | $db->close();
452 | }
453 |
454 | public function testSelectExists(): void
455 | {
456 | $db = $this->getConnection();
457 | $qb = $db->getQueryBuilder();
458 |
459 | $sql = 'SELECT 1 FROM "customer" WHERE "id" = 1';
460 | // Alias is not required in Oracle, but it is added for consistency with other DBMS.
461 | $expected = 'SELECT CASE WHEN EXISTS(SELECT 1 FROM "customer" WHERE "id" = 1) THEN 1 ELSE 0 END AS "0" FROM DUAL';
462 |
463 | $this->assertSame($expected, $qb->selectExists($sql));
464 | }
465 |
466 | #[DataProviderExternal(QueryBuilderProvider::class, 'update')]
467 | public function testUpdate(
468 | string $table,
469 | array $columns,
470 | array|string $condition,
471 | array $params,
472 | string $expectedSql,
473 | array $expectedParams,
474 | ): void {
475 | parent::testUpdate($table, $columns, $condition, $params, $expectedSql, $expectedParams);
476 | }
477 |
478 | #[DataProviderExternal(QueryBuilderProvider::class, 'upsert')]
479 | public function testUpsert(
480 | string $table,
481 | array|QueryInterface $insertColumns,
482 | array|bool $updateColumns,
483 | string $expectedSql,
484 | array $expectedParams
485 | ): void {
486 | parent::testUpsert($table, $insertColumns, $updateColumns, $expectedSql, $expectedParams);
487 | }
488 |
489 | #[DataProviderExternal(QueryBuilderProvider::class, 'upsertReturning')]
490 | public function testUpsertReturning(
491 | string $table,
492 | array|QueryInterface $insertColumns,
493 | array|bool $updateColumns,
494 | array|null $returnColumns,
495 | string $expectedSql,
496 | array $expectedParams
497 | ): void {
498 | $db = $this->getConnection();
499 | $qb = $db->getQueryBuilder();
500 |
501 | $this->expectException(NotSupportedException::class);
502 | $this->expectExceptionMessage('Yiisoft\Db\Oracle\DMLQueryBuilder::upsertReturning() is not supported by Oracle.');
503 |
504 | $qb->upsertReturning($table, $insertColumns, $updateColumns);
505 | }
506 |
507 | public function testDefaultValues(): void
508 | {
509 | $db = $this->getConnection();
510 | $queryBuilder = $db->getQueryBuilder();
511 |
512 | // Non-primary key columns should have DEFAULT as value
513 | $this->assertSame(
514 | 'INSERT INTO "negative_default_values" ("tinyint_col") VALUES (DEFAULT)',
515 | $queryBuilder->insert('negative_default_values', []),
516 | );
517 | }
518 |
519 | #[DataProviderExternal(QueryBuilderProvider::class, 'selectScalar')]
520 | public function testSelectScalar(array|bool|float|int|string $columns, string $expected): void
521 | {
522 | parent::testSelectScalar($columns, $expected);
523 | }
524 |
525 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildColumnDefinition')]
526 | public function testBuildColumnDefinition(string $expected, ColumnInterface|string $column): void
527 | {
528 | parent::testBuildColumnDefinition($expected, $column);
529 | }
530 |
531 | #[DataProviderExternal(QueryBuilderProvider::class, 'buildValue')]
532 | public function testBuildValue(mixed $value, string $expected, array $expectedParams): void
533 | {
534 | parent::testBuildValue($value, $expected, $expectedParams);
535 | }
536 |
537 | #[DataProviderExternal(QueryBuilderProvider::class, 'prepareParam')]
538 | public function testPrepareParam(string $expected, mixed $value, int $type): void
539 | {
540 | parent::testPrepareParam($expected, $value, $type);
541 | }
542 |
543 | #[DataProviderExternal(QueryBuilderProvider::class, 'prepareValue')]
544 | public function testPrepareValue(string $expected, mixed $value): void
545 | {
546 | parent::testPrepareValue($expected, $value);
547 | }
548 |
549 | #[DataProvider('dataDropTable')]
550 | public function testDropTable(string $expected, ?bool $ifExists, ?bool $cascade): void
551 | {
552 | if ($ifExists) {
553 | $qb = $this->getConnection()->getQueryBuilder();
554 |
555 | $this->expectException(NotSupportedException::class);
556 | $this->expectExceptionMessage('Oracle doesn\'t support "IF EXISTS" option on drop table.');
557 |
558 | $cascade === null
559 | ? $qb->dropTable('customer', ifExists: true)
560 | : $qb->dropTable('customer', ifExists: true, cascade: $cascade);
561 |
562 | return;
563 | }
564 |
565 | if ($cascade) {
566 | $expected = str_replace('CASCADE', 'CASCADE CONSTRAINTS', $expected);
567 | }
568 |
569 | parent::testDropTable($expected, $ifExists, $cascade);
570 | }
571 | }
572 |
--------------------------------------------------------------------------------
/tests/QueryGetTableAliasTest.php:
--------------------------------------------------------------------------------
1 | getConnection(true);
28 |
29 | $selectExpression = "[[customer]].[[name]] || ' in ' || [[p]].[[description]] name";
30 |
31 | $result = (new Query($db))
32 | ->select([$selectExpression])
33 | ->from('customer')
34 | ->innerJoin('profile p', '[[customer]].[[profile_id]] = [[p]].[[id]]')
35 | ->indexBy('id')
36 | ->column();
37 |
38 | $this->assertSame([1 => 'user1 in profile customer 1', 3 => 'user3 in profile customer 3'], $result);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/QuoterTest.php:
--------------------------------------------------------------------------------
1 | getConnection();
38 | $version21 = version_compare($db->getServerInfo()->getVersion(), '21', '>=');
39 | $db->close();
40 |
41 | if ($version21 && $tableName === 'type') {
42 | $this->fixture = 'oci21.sql';
43 |
44 | $columns['json_col']->dbType('json');
45 | $columns['json_col']->check(null);
46 | }
47 |
48 | parent::testColumns($columns, $tableName);
49 | }
50 |
51 | /**
52 | * @throws Exception
53 | * @throws InvalidConfigException
54 | */
55 | public function testCompositeFk(): void
56 | {
57 | $db = $this->getConnection(true);
58 |
59 | $schema = $db->getSchema();
60 | $table = $schema->getTableSchema('composite_fk');
61 |
62 | $this->assertNotNull($table);
63 |
64 | $fk = $table->getForeignKeys();
65 |
66 | $this->assertCount(1, $fk);
67 | $this->assertSame('order_item', $fk[0][0]);
68 | $this->assertSame('order_id', $fk[0]['order_id']);
69 | $this->assertSame('item_id', $fk[0]['item_id']);
70 |
71 | $db->close();
72 | }
73 |
74 | /**
75 | * @throws Exception
76 | * @throws InvalidConfigException
77 | */
78 | public function testGetDefaultSchema(): void
79 | {
80 | $db = $this->getConnection();
81 |
82 | $schema = $db->getSchema();
83 |
84 | $this->assertSame('SYSTEM', $schema->getDefaultSchema());
85 |
86 | $db->close();
87 | }
88 |
89 | public function testGetSchemaDefaultValues(): void
90 | {
91 | $this->expectException(NotSupportedException::class);
92 | $this->expectExceptionMessage('Yiisoft\Db\Oracle\Schema::loadTableDefaultValues is not supported by Oracle.');
93 |
94 | parent::testGetSchemaDefaultValues();
95 | }
96 |
97 | /**
98 | * @throws Exception
99 | * @throws InvalidConfigException
100 | * @throws NotSupportedException
101 | */
102 | public function testGetSchemaNames(): void
103 | {
104 | $db = $this->getConnection(true);
105 |
106 | $schema = $db->getSchema();
107 |
108 | if (version_compare($db->getServerInfo()->getVersion(), '12', '>')) {
109 | $this->assertContains('SYSBACKUP', $schema->getSchemaNames());
110 | } else {
111 | $this->assertEmpty($schema->getSchemaNames());
112 | }
113 |
114 | $db->close();
115 | }
116 |
117 | /**
118 | * @throws Exception
119 | * @throws InvalidConfigException
120 | * @throws NotSupportedException
121 | */
122 | public function testGetTableNamesWithSchema(): void
123 | {
124 | $db = $this->getConnection(true);
125 |
126 | $schema = $db->getSchema();
127 | $tablesNames = $schema->getTableNames('SYSTEM');
128 |
129 | $expectedTableNames = [
130 | 'animal',
131 | 'animal_view',
132 | 'bit_values',
133 | 'category',
134 | 'composite_fk',
135 | 'constraints',
136 | 'customer',
137 | 'default_pk',
138 | 'department',
139 | 'document',
140 | 'dossier',
141 | 'employee',
142 | 'item',
143 | 'negative_default_values',
144 | 'null_values',
145 | 'order',
146 | 'order_item',
147 | 'order_item_with_null_fk',
148 | 'order_with_null_fk',
149 | 'profile',
150 | 'quoter',
151 | 'T_constraints_1',
152 | 'T_constraints_2',
153 | 'T_constraints_3',
154 | 'T_constraints_4',
155 | 'T_upsert',
156 | 'T_upsert_1',
157 | 'type',
158 | ];
159 |
160 | foreach ($expectedTableNames as $tableName) {
161 | $this->assertContains($tableName, $tablesNames);
162 | }
163 |
164 | $db->close();
165 | }
166 |
167 | /**
168 | * @throws Exception
169 | * @throws InvalidConfigException
170 | */
171 | public function testGetViewNames(): void
172 | {
173 | $db = $this->getConnection(true);
174 |
175 | $schema = $db->getSchema();
176 | $views = $schema->getViewNames();
177 |
178 | $this->assertContains('animal_view', $views);
179 |
180 | $db->close();
181 | }
182 |
183 | /**
184 | * @throws Exception
185 | * @throws InvalidConfigException
186 | */
187 | public function testGetViewNamesWithSchema(): void
188 | {
189 | $db = $this->getConnection(true);
190 |
191 | $schema = $db->getSchema();
192 | $views = $schema->getViewNames('SYSTEM');
193 |
194 | $this->assertContains('animal_view', $views);
195 |
196 | $db->close();
197 | }
198 |
199 | /**
200 | * @dataProvider \Yiisoft\Db\Oracle\Tests\Provider\SchemaProvider::constraints
201 | *
202 | * @throws Exception
203 | */
204 | public function testTableSchemaConstraints(string $tableName, string $type, mixed $expected): void
205 | {
206 | parent::testTableSchemaConstraints($tableName, $type, $expected);
207 | }
208 |
209 | /**
210 | * @dataProvider \Yiisoft\Db\Oracle\Tests\Provider\SchemaProvider::constraints
211 | *
212 | * @throws Exception
213 | */
214 | public function testTableSchemaConstraintsWithPdoLowercase(string $tableName, string $type, mixed $expected): void
215 | {
216 | parent::testTableSchemaConstraintsWithPdoLowercase($tableName, $type, $expected);
217 | }
218 |
219 | /**
220 | * @dataProvider \Yiisoft\Db\Oracle\Tests\Provider\SchemaProvider::constraints
221 | *
222 | * @throws Exception
223 | */
224 | public function testTableSchemaConstraintsWithPdoUppercase(string $tableName, string $type, mixed $expected): void
225 | {
226 | parent::testTableSchemaConstraintsWithPdoUppercase($tableName, $type, $expected);
227 | }
228 |
229 | /**
230 | * @dataProvider \Yiisoft\Db\Oracle\Tests\Provider\SchemaProvider::tableSchemaWithDbSchemes
231 | *
232 | * @throws Exception
233 | */
234 | public function testTableSchemaWithDbSchemes(
235 | string $tableName,
236 | string $expectedTableName,
237 | string $expectedSchemaName = ''
238 | ): void {
239 | $db = $this->getConnection();
240 |
241 | $commandMock = $this->createMock(CommandInterface::class);
242 | $commandMock->method('queryAll')->willReturn([]);
243 | $mockDb = $this->createMock(PdoConnectionInterface::class);
244 | $mockDb->method('getQuoter')->willReturn($db->getQuoter());
245 | $mockDb
246 | ->method('createCommand')
247 | ->with(
248 | self::callback(static fn ($sql) => true),
249 | self::callback(
250 | function ($params) use ($expectedTableName, $expectedSchemaName) {
251 | $this->assertEquals($expectedTableName, $params[':tableName']);
252 | $this->assertEquals($expectedSchemaName, $params[':schemaName']);
253 |
254 | return true;
255 | }
256 | )
257 | )
258 | ->willReturn($commandMock);
259 | $schema = new Schema($mockDb, DbHelper::getSchemaCache(), 'dbo');
260 | $schema->getTablePrimaryKey($tableName);
261 |
262 | $db->close();
263 | }
264 |
265 | public function testWorkWithDefaultValueConstraint(): void
266 | {
267 | $this->expectException(NotSupportedException::class);
268 | $this->expectExceptionMessage(
269 | 'Yiisoft\Db\Oracle\DDLQueryBuilder::addDefaultValue is not supported by Oracle.'
270 | );
271 |
272 | parent::testWorkWithDefaultValueConstraint();
273 | }
274 |
275 | public function testNotConnectionPDO(): void
276 | {
277 | $db = $this->createMock(ConnectionInterface::class);
278 | $schema = new Schema($db, DbHelper::getSchemaCache(), 'system');
279 |
280 | $this->expectException(NotSupportedException::class);
281 | $this->expectExceptionMessage('Only PDO connections are supported.');
282 |
283 | $schema->refresh();
284 | }
285 |
286 | #[DataProviderExternal(SchemaProvider::class, 'resultColumns')]
287 | public function testGetResultColumn(ColumnInterface|null $expected, array $info): void
288 | {
289 | parent::testGetResultColumn($expected, $info);
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/tests/SqlParserTest.php:
--------------------------------------------------------------------------------
1 | ''),
271 | "C_unique" INT NOT NULL,
272 | "C_default" INT DEFAULT 0 NOT NULL,
273 | CONSTRAINT "CN_unique" UNIQUE ("C_unique")
274 | );
275 |
276 | CREATE TABLE "T_constraints_2"
277 | (
278 | "C_id_1" INT NOT NULL,
279 | "C_id_2" INT NOT NULL,
280 | "C_index_1" INT NULL,
281 | "C_index_2_1" INT NULL,
282 | "C_index_2_2" INT NULL,
283 | CONSTRAINT "CN_constraints_2_multi" UNIQUE ("C_index_2_1", "C_index_2_2"),
284 | CONSTRAINT "CN_pk" PRIMARY KEY ("C_id_1", "C_id_2")
285 | );
286 |
287 | CREATE INDEX "CN_constraints_2_single" ON "T_constraints_2" ("C_index_1");
288 |
289 | CREATE TABLE "T_constraints_3"
290 | (
291 | "C_id" INT NOT NULL,
292 | "C_fk_id_1" INT NOT NULL,
293 | "C_fk_id_2" INT NOT NULL,
294 | 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
295 | );
296 |
297 | CREATE TABLE "T_constraints_4"
298 | (
299 | "C_id" INT NOT NULL PRIMARY KEY,
300 | "C_col_1" INT NULL,
301 | "C_col_2" INT NOT NULL,
302 | CONSTRAINT "CN_constraints_4" UNIQUE ("C_col_1", "C_col_2")
303 | );
304 |
305 | CREATE TABLE "T_upsert"
306 | (
307 | "id" INT NOT NULL PRIMARY KEY,
308 | "ts" INT NULL,
309 | "email" VARCHAR(128) NOT NULL UNIQUE,
310 | "recovery_email" VARCHAR(128) NULL,
311 | "address" CLOB NULL,
312 | "status" NUMBER(5,0) DEFAULT 0 NOT NULL,
313 | "orders" INT DEFAULT 0 NOT NULL,
314 | "profile_id" INT NULL,
315 | CONSTRAINT "CN_T_upsert_multi" UNIQUE ("email", "recovery_email")
316 | );
317 | CREATE SEQUENCE "T_upsert_SEQ";
318 |
319 | CREATE TABLE "T_upsert_1"
320 | (
321 | "a" INT NOT NULL PRIMARY KEY
322 | );
323 |
324 | CREATE TABLE "T_upsert_varbinary"
325 | (
326 | "id" integer not null,
327 | "blob_col" blob,
328 | CONSTRAINT "T_upsert_varbinary_PK" PRIMARY KEY ("id") ENABLE
329 | );
330 |
331 | /* TRIGGERS */
332 |
333 | CREATE TRIGGER "profile_TRG" BEFORE INSERT ON "profile" FOR EACH ROW BEGIN <> BEGIN
334 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "profile_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
335 | END COLUMN_SEQUENCES;
336 | END;
337 | /
338 | CREATE TRIGGER "customer_TRG" BEFORE INSERT ON "customer" FOR EACH ROW BEGIN <> BEGIN
339 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "customer_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
340 | END COLUMN_SEQUENCES;
341 | END;
342 | /
343 | CREATE TRIGGER "category_TRG" BEFORE INSERT ON "category" FOR EACH ROW BEGIN <> BEGIN
344 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "category_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
345 | END COLUMN_SEQUENCES;
346 | END;
347 | /
348 | CREATE TRIGGER "item_TRG" BEFORE INSERT ON "item" FOR EACH ROW BEGIN <> BEGIN
349 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "item_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
350 | END COLUMN_SEQUENCES;
351 | END;
352 | /
353 | CREATE TRIGGER "order_TRG" BEFORE INSERT ON "order" FOR EACH ROW BEGIN <> BEGIN
354 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "order_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
355 | END COLUMN_SEQUENCES;
356 | END;
357 | /
358 | CREATE TRIGGER "order_with_null_fk_TRG" BEFORE INSERT ON "order_with_null_fk" FOR EACH ROW BEGIN <> BEGIN
359 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "order_with_null_fk_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
360 | END COLUMN_SEQUENCES;
361 | END;
362 | /
363 | CREATE TRIGGER "null_values_TRG" BEFORE INSERT ON "null_values" FOR EACH ROW BEGIN <> BEGIN
364 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "null_values_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
365 | END COLUMN_SEQUENCES;
366 | END;
367 | /
368 | CREATE TRIGGER "bool_values_TRG" BEFORE INSERT ON "bool_values" FOR EACH ROW BEGIN <> BEGIN
369 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "bool_values_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
370 | END COLUMN_SEQUENCES;
371 | END;
372 | /
373 | CREATE TRIGGER "animal_TRG" BEFORE INSERT ON "animal" FOR EACH ROW BEGIN <> BEGIN
374 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "animal_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
375 | END COLUMN_SEQUENCES;
376 | END;
377 | /
378 | CREATE TRIGGER "document_TRG" BEFORE INSERT ON "document" FOR EACH ROW BEGIN <> BEGIN
379 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "document_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
380 | END COLUMN_SEQUENCES;
381 | END;
382 | /
383 | CREATE TRIGGER "T_upsert_TRG" BEFORE INSERT ON "T_upsert" FOR EACH ROW BEGIN <> BEGIN
384 | IF INSERTING AND :NEW."id" IS NULL THEN SELECT "T_upsert_SEQ".NEXTVAL INTO :NEW."id" FROM SYS.DUAL; END IF;
385 | END COLUMN_SEQUENCES;
386 | END;
387 | /
388 |
389 | /* TRIGGERS */
390 |
391 | INSERT INTO "animal" ("type") VALUES ('yiiunit\data\ar\Cat');
392 | INSERT INTO "animal" ("type") VALUES ('yiiunit\data\ar\Dog');
393 |
394 |
395 | INSERT INTO "profile" ("description") VALUES ('profile customer 1');
396 | INSERT INTO "profile" ("description") VALUES ('profile customer 3');
397 |
398 | INSERT INTO "customer" ("email", "name", "address", "status", "profile_id") VALUES ('user1@example.com', 'user1', 'address1', 1, 1);
399 | INSERT INTO "customer" ("email", "name", "address", "status") VALUES ('user2@example.com', 'user2', 'address2', 1);
400 | INSERT INTO "customer" ("email", "name", "address", "status", "profile_id") VALUES ('user3@example.com', 'user3', 'address3', 2, 2);
401 |
402 | INSERT INTO "category" ("name") VALUES ('Books');
403 | INSERT INTO "category" ("name") VALUES ('Movies');
404 |
405 | INSERT INTO "item" ("name", "category_id") VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1);
406 | INSERT INTO "item" ("name", "category_id") VALUES ('Yii 1.1 Application Development Cookbook', 1);
407 | INSERT INTO "item" ("name", "category_id") VALUES ('Ice Age', 2);
408 | INSERT INTO "item" ("name", "category_id") VALUES ('Toy Story', 2);
409 | INSERT INTO "item" ("name", "category_id") VALUES ('Cars', 2);
410 |
411 | INSERT INTO "order" ("customer_id", "created_at", "total") VALUES (1, 1325282384, 110.0);
412 | INSERT INTO "order" ("customer_id", "created_at", "total") VALUES (2, 1325334482, 33.0);
413 | INSERT INTO "order" ("customer_id", "created_at", "total") VALUES (2, 1325502201, 40.0);
414 |
415 | INSERT INTO "order_with_null_fk" ("customer_id", "created_at", "total") VALUES (1, 1325282384, 110.0);
416 | INSERT INTO "order_with_null_fk" ("customer_id", "created_at", "total") VALUES (2, 1325334482, 33.0);
417 | INSERT INTO "order_with_null_fk" ("customer_id", "created_at", "total") VALUES (2, 1325502201, 40.0);
418 |
419 | INSERT INTO "order_item" ("order_id", "item_id", "quantity", "subtotal") VALUES (1, 1, 1, 30.0);
420 | INSERT INTO "order_item" ("order_id", "item_id", "quantity", "subtotal") VALUES (1, 2, 2, 40.0);
421 | INSERT INTO "order_item" ("order_id", "item_id", "quantity", "subtotal") VALUES (2, 4, 1, 10.0);
422 | INSERT INTO "order_item" ("order_id", "item_id", "quantity", "subtotal") VALUES (2, 5, 1, 15.0);
423 | INSERT INTO "order_item" ("order_id", "item_id", "quantity", "subtotal") VALUES (2, 3, 1, 8.0);
424 | INSERT INTO "order_item" ("order_id", "item_id", "quantity", "subtotal") VALUES (3, 2, 1, 40.0);
425 |
426 | INSERT INTO "order_item_with_null_fk" ("order_id", "item_id", "quantity", "subtotal") VALUES (1, 1, 1, 30.0);
427 | INSERT INTO "order_item_with_null_fk" ("order_id", "item_id", "quantity", "subtotal") VALUES (1, 2, 2, 40.0);
428 | INSERT INTO "order_item_with_null_fk" ("order_id", "item_id", "quantity", "subtotal") VALUES (2, 4, 1, 10.0);
429 | INSERT INTO "order_item_with_null_fk" ("order_id", "item_id", "quantity", "subtotal") VALUES (2, 5, 1, 15.0);
430 | INSERT INTO "order_item_with_null_fk" ("order_id", "item_id", "quantity", "subtotal") VALUES (2, 3, 1, 8.0);
431 | INSERT INTO "order_item_with_null_fk" ("order_id", "item_id", "quantity", "subtotal") VALUES (3, 2, 1, 40.0);
432 |
433 | INSERT INTO "document" ("title", "content", "version") VALUES ('Yii 2.0 guide', 'This is Yii 2.0 guide', 0);
434 |
435 | INSERT INTO "department" ("id", "title") VALUES (1, 'IT');
436 | INSERT INTO "department" ("id", "title") VALUES (2, 'accounting');
437 |
438 | INSERT INTO "employee" ("id", "department_id", "first_name", "last_name") VALUES (1, 1, 'John', 'Doe');
439 | INSERT INTO "employee" ("id", "department_id", "first_name", "last_name") VALUES (1, 2, 'Ann', 'Smith');
440 | INSERT INTO "employee" ("id", "department_id", "first_name", "last_name") VALUES (2, 2, 'Will', 'Smith');
441 |
442 | INSERT INTO "dossier" ("id", "department_id", "employee_id", "summary") VALUES (1, 1, 1, 'Excellent employee.');
443 | INSERT INTO "dossier" ("id", "department_id", "employee_id", "summary") VALUES (2, 2, 1, 'Brilliant employee.');
444 | INSERT INTO "dossier" ("id", "department_id", "employee_id", "summary") VALUES (3, 2, 2, 'Good employee.');
445 |
446 | INSERT INTO "bit_values" ("id", "val")
447 | SELECT 1, '0' FROM SYS.DUAL
448 | UNION ALL SELECT 2, '1' FROM SYS.DUAL;
449 |
--------------------------------------------------------------------------------
/tests/Support/Fixture/oci21.sql:
--------------------------------------------------------------------------------
1 | BEGIN EXECUTE IMMEDIATE 'DROP TABLE "type"'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;--
2 |
3 | /* STATEMENTS */
4 |
5 | CREATE TABLE "type" (
6 | "int_col" integer NOT NULL,
7 | "int_col2" integer DEFAULT 1,
8 | "tinyint_col" number(3) DEFAULT 1,
9 | "smallint_col" smallint DEFAULT 1,
10 | "char_col" char(100) NOT NULL,
11 | "char_col2" varchar2(100) DEFAULT 'some''thing',
12 | "char_col3" varchar2(4000),
13 | "nvarchar_col" nvarchar2(100) DEFAULT '',
14 | "float_col" double precision NOT NULL,
15 | "float_col2" double precision DEFAULT 1.23,
16 | "blob_col" blob DEFAULT NULL,
17 | "numeric_col" decimal(5,2) DEFAULT 33.22,
18 | "timestamp_col" timestamp DEFAULT to_timestamp('2002-01-01 00:00:00', 'yyyy-mm-dd hh24:mi:ss') NOT NULL,
19 | "timestamp_local" timestamp with local time zone,
20 | "time_col" interval day (0) to second(0) DEFAULT INTERVAL '0 10:33:21' DAY(0) TO SECOND(0),
21 | "interval_day_col" interval day (1) to second(0) DEFAULT INTERVAL '2 04:56:12' DAY(1) TO SECOND(0),
22 | "bool_col" char NOT NULL check ("bool_col" in (0,1)),
23 | "bool_col2" char DEFAULT 1 check("bool_col2" in (0,1)),
24 | "ts_default" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
25 | "bit_col" number(3) DEFAULT 130 NOT NULL,
26 | "json_col" json DEFAULT '{"a":1}'
27 | );
28 |
29 | /* TRIGGERS */
30 |
31 | /* TRIGGERS */
32 |
--------------------------------------------------------------------------------
/tests/Support/TestTrait.php:
--------------------------------------------------------------------------------
1 | close();
25 | }
26 |
27 | protected function getConnection(bool $fixture = false): Connection
28 | {
29 | $db = new Connection($this->getDriver(), DbHelper::getSchemaCache());
30 |
31 | if ($fixture) {
32 | DbHelper::loadFixture($db, __DIR__ . "/Fixture/$this->fixture");
33 | }
34 |
35 | return $db;
36 | }
37 |
38 | protected static function getDb(): Connection
39 | {
40 | $dsn = (new Dsn(
41 | host: self::getHost(),
42 | databaseName: self::getSid(),
43 | port: self::getPort(),
44 | options: ['charset' => 'AL32UTF8'],
45 | ))->asString();
46 |
47 | return new Connection(new Driver($dsn, self::getUsername(), self::getPassword()), DbHelper::getSchemaCache());
48 | }
49 |
50 | protected function getDsn(): string
51 | {
52 | if ($this->dsn === '') {
53 | $this->dsn = (new Dsn(
54 | host: self::getHost(),
55 | databaseName: self::getSid(),
56 | port: self::getPort(),
57 | options: ['charset' => 'AL32UTF8'],
58 | ))->asString();
59 | }
60 |
61 | return $this->dsn;
62 | }
63 |
64 | protected function getDriverName(): string
65 | {
66 | return 'oci';
67 | }
68 |
69 | protected function setDsn(string $dsn): void
70 | {
71 | $this->dsn = $dsn;
72 | }
73 |
74 | protected function getDriver(): Driver
75 | {
76 | return new Driver($this->getDsn(), self::getUsername(), self::getPassword());
77 | }
78 |
79 | private static function getSid(): string
80 | {
81 | return getenv('YII_ORACLE_SID') ?: 'XE';
82 | }
83 |
84 | private static function getDatabaseName(): string
85 | {
86 | return getenv('YII_ORACLE_DATABASE') ?: 'YIITEST';
87 | }
88 |
89 | private static function getHost(): string
90 | {
91 | return getenv('YII_ORACLE_HOST') ?: 'localhost';
92 | }
93 |
94 | private static function getPort(): string
95 | {
96 | return getenv('YII_ORACLE_PORT') ?: '1521';
97 | }
98 |
99 | private static function getUsername(): string
100 | {
101 | return getenv('YII_ORACLE_USER') ?: 'system';
102 | }
103 |
104 | private static function getPassword(): string
105 | {
106 | return getenv('YII_ORACLE_PASSWORD') ?: 'root';
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | load();
8 | }
9 |
--------------------------------------------------------------------------------