├── .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 | Yii 4 | 5 | 6 | Oracle 7 | 8 |

Yii Database Oracle driver

9 |
10 |

11 | 12 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/db-oracle/v)](https://packagist.org/packages/yiisoft/db-oracle) 13 | [![Total Downloads](https://poser.pugx.org/yiisoft/db-oracle/downloads)](https://packagist.org/packages/yiisoft/db-oracle) 14 | [![rector](https://github.com/yiisoft/db-oracle/actions/workflows/rector.yml/badge.svg)](https://github.com/yiisoft/db-oracle/actions/workflows/rector.yml) 15 | [![codecov](https://codecov.io/gh/yiisoft/db-oracle/branch/master/graph/badge.svg?token=XGJAFXVHSH)](https://codecov.io/gh/yiisoft/db-oracle) 16 | [![StyleCI](https://github.styleci.io/repos/114756574/shield?branch=master)](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**|[![build](https://github.com/yiisoft/db-oracle/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/yiisoft/db-oracle/actions/workflows/build.yml) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fdb-oracle%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/db-oracle/master) [![static analysis](https://github.com/yiisoft/db-oracle/actions/workflows/static.yml/badge.svg?branch=dev)](https://github.com/yiisoft/db-oracle/actions/workflows/static.yml) [![type-coverage](https://shepherd.dev/github/yiisoft/db-oracle/coverage.svg)](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 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 65 | 66 | ## Follow updates 67 | 68 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 69 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 70 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 71 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 72 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](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 | --------------------------------------------------------------------------------