├── .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 │ ├── ColumnBuilder.php │ ├── ColumnDefinitionBuilder.php │ ├── ColumnFactory.php │ └── DateTimeColumn.php ├── Command.php ├── Connection.php ├── DDLQueryBuilder.php ├── DMLQueryBuilder.php ├── DQLQueryBuilder.php ├── Driver.php ├── Dsn.php ├── IndexMethod.php ├── IndexType.php ├── QueryBuilder.php ├── Quoter.php ├── Schema.php ├── SqlParser.php ├── TableSchema.php └── Transaction.php └── tests ├── .env ├── Builder └── InconditionBuilderTest.php ├── ColumnBuilderTest.php ├── ColumnFactoryTest.php ├── ColumnTest.php ├── CommandTest.php ├── ConnectionTest.php ├── DsnTest.php ├── PdoCommandTest.php ├── PdoConnectionTest.php ├── Provider ├── ColumnBuilderProvider.php ├── ColumnFactoryProvider.php ├── ColumnProvider.php ├── CommandPDOProvider.php ├── CommandProvider.php ├── QueryBuilderProvider.php ├── QuoterProvider.php ├── SchemaProvider.php ├── SqlParserProvider.php └── Type │ ├── BinaryProvider.php │ ├── BitProvider.php │ ├── CharProvider.php │ ├── DateProvider.php │ ├── GeometryProvider.php │ ├── VarBinaryProvider.php │ └── VarCharProvider.php ├── QueryBuilderTest.php ├── QueryGetTableAliasTest.php ├── QueryTest.php ├── QuoterTest.php ├── SchemaTest.php ├── SqlParserTest.php ├── Support ├── Fixture │ ├── Type │ │ ├── bigint.sql │ │ ├── binary.sql │ │ ├── bit.sql │ │ ├── char.sql │ │ ├── date.sql │ │ ├── decimal.sql │ │ ├── float.sql │ │ ├── geometry.sql │ │ ├── image.sql │ │ ├── int.sql │ │ ├── json.sql │ │ ├── money.sql │ │ ├── ntext.sql │ │ ├── numeric.sql │ │ ├── real.sql │ │ ├── rowversion.sql │ │ ├── smallint.sql │ │ ├── smallmoney.sql │ │ ├── text.sql │ │ ├── tinyint.sql │ │ ├── uniqueidentifier.sql │ │ ├── varbinary.sql │ │ └── varchar.sql │ └── mssql.sql └── TestTrait.php ├── Type ├── BigIntTest.php ├── BinaryTest.php ├── BitTest.php ├── CharTest.php ├── DateTest.php ├── DecimalTest.php ├── FloatTest.php ├── GeometryTest.php ├── ImageTest.php ├── IntTest.php ├── JsonTest.php ├── MoneyTest.php ├── NTextTest.php ├── NumericTest.php ├── RealTest.php ├── RowversionTest.php ├── SmallIntTest.php ├── SmallMoneyTest.php ├── TextTest.php ├── TinyIntTest.php ├── UniqueidentifierTest.php ├── VarBinaryTest.php └── VarCharTest.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 | # MSSQL Server driver for Yii Database Change Log 2 | 3 | ## 2.0.0 under development 4 | 5 | - New #277: Implement `ColumnSchemaInterface` classes according to the data type of database table columns 6 | for type casting performance. Related with yiisoft/db#752 (@Tigrov) 7 | - Enh #293, #357: Implement and use `SqlParser` class (@Tigrov) 8 | - Chg #306: Remove parameter `$withColumn` from `Quoter::getTableNameParts()` method (@Tigrov) 9 | - Chg #308: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) 10 | - Enh #312: Refactor `bit` type (@Tigrov) 11 | - Enh #315: Refactor PHP type of `ColumnSchemaInterface` instances (@Tigrov) 12 | - Enh #317: Raise minimum PHP version to `^8.1` with minor refactoring (@Tigrov) 13 | - New #316, #327: Implement `ColumnFactory` class (@Tigrov) 14 | - Enh #319: Separate column type constants (@Tigrov) 15 | - New #320: Realize `ColumnBuilder` class (@Tigrov) 16 | - Enh #321: Update according changes in `ColumnSchemaInterface` (@Tigrov) 17 | - New #322, #330, #340: Add `ColumnDefinitionBuilder` class (@Tigrov) 18 | - Enh #323: Refactor `Dsn` class (@Tigrov) 19 | - Enh #324: Use constructor to create columns and initialize properties (@Tigrov) 20 | - Enh #327: Refactor `Schema::findColumns()` method (@Tigrov) 21 | - Enh #328: Refactor `Schema::normalizeDefaultValue()` method and move it to `ColumnFactory` class (@Tigrov) 22 | - Enh #331: Refactor according to changes #902 in `yiisoft/db` package (@Tigrov) 23 | - Chg #333: Update `QueryBuilder` constructor (@Tigrov) 24 | - Enh #332: Use `ColumnDefinitionBuilder` to generate table column SQL representation (@Tigrov) 25 | - Enh #335: Remove `ColumnInterface` (@Tigrov) 26 | - Enh #337: Rename `ColumnSchemaInterface` to `ColumnInterface` (@Tigrov) 27 | - Enh #338: Replace `DbArrayHelper::getColumn()` with `array_column()` (@Tigrov) 28 | - New #339: Add `IndexType` and `IndexMethod` classes (@Tigrov) 29 | - Bug #343: Explicitly mark nullable parameters (@vjik) 30 | - New #342: Support JSON type (@Tigrov) 31 | - Chg #344: Change supported PHP versions to `8.1 - 8.4` (@Tigrov) 32 | - Chg #344: Change return type of `Command::insertWithReturningPks()` method to `array|false` (@Tigrov) 33 | - New #345: Add parameters `$ifExists` and `$cascade` to `CommandInterface::dropTable()` and 34 | `DDLQueryBuilderInterface::dropTable()` methods (@vjik) 35 | - Chg #348: Remove usage of `hasLimit()` and `hasOffset()` methods of `DQLQueryBuilder` class (@Tigrov) 36 | - Enh #350: Refactor according changes in `db` package (@Tigrov) 37 | - New #349: Add `caseSensitive` option to like condition (@vjik) 38 | - Enh #352: Remove `getCacheKey()` and `getCacheTag()` methods from `Schema` class (@Tigrov) 39 | - Enh #355, #356: Use `DbArrayHelper::arrange()` instead of `DbArrayHelper::index()` method (@Tigrov) 40 | - New #353: Realize `Schema::loadResultColumn()` method (@Tigrov) 41 | - Enh #300: Remove realization of `Connection::createBatchQueryResult()` method (@Tigrov) 42 | - Bug #360: Fix columns order in composite primary key (@Tigrov) 43 | - New #358: Use `DateTimeColumn` class for datetime column types (@Tigrov) 44 | - New #361: Implement `DMLQueryBuilder::upsertWithReturningPks()` method (@Tigrov) 45 | - Enh #361: Refactor `DMLQueryBuilder::insertWithReturningPks()` method (@Tigrov) 46 | - Chg #363: Add alias in `DQLQueryBuilder::selectExists()` method for consistency with other DBMS (@Tigrov) 47 | 48 | ## 1.2.0 March 21, 2024 49 | 50 | - Enh #286: Change property `Schema::$typeMap` to constant `Schema::TYPE_MAP` (@Tigrov) 51 | - Enh #291: Resolve deprecated methods (@Tigrov) 52 | - Enh #292: Minor refactoring of `Command` and `Quoter` (@Tigrov) 53 | - Bug #287: Fix `DMLQueryBuilder::insertWithReturningPks()` and `Command::insertWithReturningPks()` methods (@Tigrov) 54 | 55 | ## 1.1.0 November 12, 2023 56 | 57 | - Enh #283: Move methods from `Command` to `AbstractPdoCommand` class (@Tigrov) 58 | - Bug #275: Refactor `DMLQueryBuilder`, related with yiisoft/db#746 (@Tigrov) 59 | - Bug #278: Remove `RECURSIVE` expression from CTE queries (@Tigrov) 60 | - Bug #280: Fix type boolean (@terabytesoftw) 61 | - Bug #282: Fix `DDLQueryBuilder::alterColumn()` for columns with default null (@Tigrov) 62 | 63 | ## 1.0.1 July 24, 2023 64 | 65 | - Enh #271: Typecast refactoring (@Tigrov) 66 | 67 | ## 1.0.0 April 12, 2023 68 | 69 | - Initial release. 70 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software () 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

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

Yii Database MSSQL Server driver

9 |
10 |

11 | 12 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/db-mssql/v)](https://packagist.org/packages/yiisoft/db-mssql) 13 | [![Total Downloads](https://poser.pugx.org/yiisoft/db-mssql/downloads)](https://packagist.org/packages/yiisoft/db-mssql) 14 | [![Code Coverage](https://codecov.io/gh/yiisoft/db-mssql/branch/master/graph/badge.svg?token=UF9VERNMYU)](https://codecov.io/gh/yiisoft/db-mssql) 15 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fdb-mssql%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/db-mssql/master) 16 | [![static analysis](https://github.com/yiisoft/db-mssql/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/db-mssql/actions?query=workflow%3A%22static+analysis%22) 17 | [![type-coverage](https://shepherd.dev/github/yiisoft/db-mssql/coverage.svg)](https://shepherd.dev/github/yiisoft/db-mssql) 18 | [![psalm-level](https://shepherd.dev/github/yiisoft/db-mssql/level.svg)](https://shepherd.dev/github/yiisoft/db-mssql) 19 | 20 | MSSQL Server driver for [Yii Database](https://github.com/yiisoft/db) is a package for working with [MSSQL] databases in PHP. 21 | 22 | The package provides a set of classes for connecting to a [MSSQL] database, creating and executing commands, and working with data. 23 | It also includes a set of tools for building and executing queries, including support for parameter binding, as well 24 | as tools for working with transactions. 25 | 26 | To use it, you will need to have the PHP [MSSQL extension] installed and enabled on your server. You will also need to 27 | have access to a SQL Server database and the necessary credentials to connect to it. 28 | 29 | [MSSQL]: https://www.microsoft.com/sql-server 30 | [MSSQL extension]: https://pecl.php.net/package/sqlsrv 31 | 32 | ## Support version 33 | 34 | | PHP | MSSQL Version | CI-Actions | 35 | |---------------|---------------|------------| 36 | | **8.1 - 8.4** | **2017 - 2022**| [![Build status](https://github.com/yiisoft/db-mssql/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/db-mssql/actions/workflows/build.yml) 37 | 38 | ## Installation 39 | 40 | The package could be installed with [Composer](https://getcomposer.org): 41 | 42 | ```shell 43 | composer require yiisoft/db-mssql 44 | ``` 45 | 46 | ## Documentation 47 | 48 | To configure connection to MSSQL database check [Connecting MSSQL](https://github.com/yiisoft/db/blob/master/docs/guide/en/connection/mssql.md). 49 | 50 | [Check the documentation docs](https://github.com/yiisoft/db/blob/master/docs/guide/en/README.md) to learn about usage. 51 | 52 | - [Internals](docs/internals.md) 53 | 54 | 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. 55 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 56 | 57 | ## License 58 | 59 | The Yii Database MSSQL Server driver is free software. It is released under the terms of the BSD License. 60 | Please see [`LICENSE`](./LICENSE.md) for more information. 61 | 62 | Maintained by [Yii Software](https://www.yiiframework.com/). 63 | 64 | ## Support the project 65 | 66 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 67 | 68 | ## Follow updates 69 | 70 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 71 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 72 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 73 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 74 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/db-mssql", 3 | "description": "MSSQL Server driver for Yii Database", 4 | "keywords": [ 5 | "yii", 6 | "mssql", 7 | "sql server", 8 | "database", 9 | "sql", 10 | "dbal", 11 | "query-builder" 12 | ], 13 | "type": "library", 14 | "license": "BSD-3-Clause", 15 | "support": { 16 | "issues": "https://github.com/yiisoft/db-mssql/issues?state=open", 17 | "source": "https://github.com/yiisoft/db-mssql", 18 | "forum": "https://www.yiiframework.com/forum/", 19 | "wiki": "https://www.yiiframework.com/wiki/", 20 | "irc": "ircs://irc.libera.chat:6697/yii", 21 | "chat": "https://t.me/yii3en" 22 | }, 23 | "funding": [ 24 | { 25 | "type": "opencollective", 26 | "url": "https://opencollective.com/yiisoft" 27 | }, 28 | { 29 | "type": "github", 30 | "url": "https://github.com/sponsors/yiisoft" 31 | } 32 | ], 33 | "require": { 34 | "php": "8.1 - 8.4", 35 | "ext-pdo": "*", 36 | "yiisoft/db": "dev-master" 37 | }, 38 | "require-dev": { 39 | "maglnet/composer-require-checker": "^4.7.1", 40 | "phpunit/phpunit": "^10.5.45", 41 | "rector/rector": "^2.0.10", 42 | "roave/infection-static-analysis-plugin": "^1.35", 43 | "spatie/phpunit-watcher": "^1.24", 44 | "vimeo/psalm": "^5.26.1 || ^6.8.8", 45 | "vlucas/phpdotenv": "^5.6.1", 46 | "yiisoft/aliases": "^2.0", 47 | "yiisoft/cache-file": "^3.2", 48 | "yiisoft/var-dumper": "^1.7" 49 | }, 50 | "autoload": { 51 | "psr-4": { 52 | "Yiisoft\\Db\\Mssql\\": "src" 53 | } 54 | }, 55 | "autoload-dev": { 56 | "psr-4": { 57 | "Yiisoft\\Db\\Mssql\\Tests\\": "tests", 58 | "Yiisoft\\Db\\Tests\\": "vendor/yiisoft/db/tests" 59 | }, 60 | "files": ["tests/bootstrap.php"] 61 | }, 62 | "config": { 63 | "sort-packages": true, 64 | "allow-plugins": { 65 | "infection/extension-installer": true, 66 | "composer/package-versions-deprecated": true 67 | } 68 | }, 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 | mssql: 5 | image: mcr.microsoft.com/mssql/server:2022-latest 6 | environment: 7 | - SA_PASSWORD=YourStrong!Passw0rd 8 | - ACCEPT_EULA=Y 9 | ports: 10 | # : < MSSQL Port running inside container> 11 | - "1433:1433" 12 | expose: 13 | # Opens port 1433 on the container 14 | - '1433' 15 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "timeout": 45, 8 | "logs": { 9 | "text": "php:\/\/stderr", 10 | "stryker": { 11 | "report": "master" 12 | } 13 | }, 14 | "mutators": { 15 | "@default": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 13 | __DIR__ . '/src', 14 | /** 15 | * Disabled ./tests directory due to different branches with main package when testing 16 | */ 17 | // __DIR__ . '/tests', 18 | ]); 19 | 20 | // register a single rule 21 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 22 | 23 | // define sets of rules 24 | $rectorConfig->sets([ 25 | LevelSetList::UP_TO_PHP_81, 26 | ]); 27 | 28 | $rectorConfig->skip([ 29 | NullToStrictStringFuncCallArgRector::class, 30 | ReadOnlyPropertyRector::class, 31 | ]); 32 | }; 33 | -------------------------------------------------------------------------------- /src/Builder/InConditionBuilder.php: -------------------------------------------------------------------------------- 1 | $column) { 62 | if ($column instanceof ExpressionInterface) { 63 | $quotedColumns[$i] = $columns[$i] = $this->queryBuilder->buildExpression($column); 64 | continue; 65 | } 66 | 67 | $quotedColumns[$i] = !str_contains($column, '(') 68 | ? $this->queryBuilder->getQuoter()->quoteColumnName($column) : $column; 69 | } 70 | 71 | $vss = []; 72 | 73 | /** @psalm-var string[][] $values */ 74 | foreach ($values as $value) { 75 | $vs = []; 76 | foreach ($columns as $i => $column) { 77 | if (isset($value[$column])) { 78 | $phName = $this->queryBuilder->bindParam($value[$column], $params); 79 | $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName; 80 | } else { 81 | $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL'; 82 | } 83 | } 84 | $vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')'; 85 | } 86 | 87 | return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Builder/LikeConditionBuilder.php: -------------------------------------------------------------------------------- 1 | '[%]', 21 | '_' => '[_]', 22 | '[' => '[[]', 23 | ']' => '[]]', 24 | '\\' => '[\\]', 25 | ]; 26 | 27 | public function build(LikeConditionInterface $expression, array &$params = []): string 28 | { 29 | if ($expression->getCaseSensitive() === true) { 30 | throw new NotSupportedException('MSSQL doesn\'t support case-sensitive "LIKE" conditions.'); 31 | } 32 | return parent::build($expression, $params); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Column/BinaryColumn.php: -------------------------------------------------------------------------------- 1 | getDbType() === 'varbinary') { 19 | if ($value instanceof ParamInterface && is_string($value->getValue())) { 20 | /** @psalm-var string */ 21 | $value = $value->getValue(); 22 | } 23 | 24 | if (is_string($value)) { 25 | return new Expression('CONVERT(VARBINARY(MAX), ' . ('0x' . bin2hex($value)) . ')'); 26 | } 27 | } 28 | 29 | return parent::dbTypecast($value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Column/ColumnBuilder.php: -------------------------------------------------------------------------------- 1 | buildType($column) 42 | . $this->buildAutoIncrement($column) 43 | . $this->buildPrimaryKey($column) 44 | . $this->buildUnique($column) 45 | . $this->buildNotNull($column) 46 | . $this->buildDefault($column) 47 | . $this->buildCheck($column) 48 | . $this->buildReferences($column) 49 | . $this->buildExtra($column); 50 | } 51 | 52 | public function buildAlter(ColumnInterface $column): string 53 | { 54 | return $this->buildType($column) 55 | . $this->buildNotNull($column) 56 | . $this->buildExtra($column); 57 | } 58 | 59 | protected function buildCheck(ColumnInterface $column): string 60 | { 61 | $check = $column->getCheck(); 62 | 63 | if (empty($check)) { 64 | $name = $column->getName(); 65 | 66 | if (empty($name)) { 67 | return ''; 68 | } 69 | 70 | return match ($column->getType()) { 71 | ColumnType::ARRAY, ColumnType::STRUCTURED, ColumnType::JSON => 72 | ' CHECK (isjson(' . $this->queryBuilder->getQuoter()->quoteSimpleColumnName($name) . ') > 0)', 73 | default => '', 74 | }; 75 | } 76 | 77 | return " CHECK ($check)"; 78 | } 79 | 80 | protected function buildOnDelete(string $onDelete): string 81 | { 82 | if (strtoupper($onDelete) === ReferentialAction::RESTRICT) { 83 | return ''; 84 | } 85 | 86 | return " ON DELETE $onDelete"; 87 | } 88 | 89 | protected function buildOnUpdate(string $onUpdate): string 90 | { 91 | if (strtoupper($onUpdate) === ReferentialAction::RESTRICT) { 92 | return ''; 93 | } 94 | 95 | return " ON UPDATE $onUpdate"; 96 | } 97 | 98 | protected function getDbType(ColumnInterface $column): string 99 | { 100 | $size = $column->getSize(); 101 | 102 | /** @psalm-suppress DocblockTypeContradiction */ 103 | $dbType = $column->getDbType() ?? match ($column->getType()) { 104 | ColumnType::BOOLEAN => 'bit', 105 | ColumnType::BIT => match (true) { 106 | $size === null => 'bigint', 107 | $size === 1 => 'bit', 108 | $size <= 8 => 'tinyint', 109 | $size <= 16 => 'smallint', 110 | $size <= 32 => 'int', 111 | $size <= 64 => 'bigint', 112 | default => 'varbinary(' . ceil($size / 8) . ')', 113 | }, 114 | ColumnType::TINYINT => 'tinyint', 115 | ColumnType::SMALLINT => 'smallint', 116 | ColumnType::INTEGER => 'int', 117 | ColumnType::BIGINT => 'bigint', 118 | ColumnType::FLOAT => 'real', 119 | ColumnType::DOUBLE => 'float(53)', 120 | ColumnType::DECIMAL => 'decimal', 121 | ColumnType::MONEY => 'money', 122 | ColumnType::CHAR => 'nchar', 123 | ColumnType::STRING => 'nvarchar(' . (($size ?? '255') ?: 'max') . ')', 124 | ColumnType::TEXT => 'nvarchar(max)', 125 | ColumnType::BINARY => 'varbinary(max)', 126 | ColumnType::UUID => 'uniqueidentifier', 127 | ColumnType::TIMESTAMP => 'datetime2', 128 | ColumnType::DATETIME => 'datetime2', 129 | ColumnType::DATETIMETZ => 'datetimeoffset', 130 | ColumnType::TIME => 'time', 131 | ColumnType::TIMETZ => 'time', 132 | ColumnType::DATE => 'date', 133 | ColumnType::ARRAY => 'nvarchar(max)', 134 | ColumnType::STRUCTURED => 'nvarchar(max)', 135 | ColumnType::JSON => 'nvarchar(max)', 136 | default => 'nvarchar', 137 | }; 138 | 139 | return match ($dbType) { 140 | 'timestamp' => 'datetime2', 141 | 'varchar' => 'varchar(' . ($size ?: 'max') . ')', 142 | 'nvarchar' => 'nvarchar(' . ($size ?: 'max') . ')', 143 | 'varbinary' => 'varbinary(' . ($size ?: 'max') . ')', 144 | default => $dbType, 145 | }; 146 | } 147 | 148 | protected function getDefaultUuidExpression(): string 149 | { 150 | return 'newid()'; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Column/ColumnFactory.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected const TYPE_MAP = [ 26 | /** Exact numbers */ 27 | 'bit' => ColumnType::BOOLEAN, 28 | 'tinyint' => ColumnType::TINYINT, 29 | 'smallint' => ColumnType::SMALLINT, 30 | 'int' => ColumnType::INTEGER, 31 | 'bigint' => ColumnType::BIGINT, 32 | 'numeric' => ColumnType::DECIMAL, 33 | 'decimal' => ColumnType::DECIMAL, 34 | 'smallmoney' => ColumnType::MONEY, 35 | 'money' => ColumnType::MONEY, 36 | 37 | /** Approximate numbers */ 38 | 'float' => ColumnType::FLOAT, 39 | 'real' => ColumnType::FLOAT, 40 | 'double' => ColumnType::DOUBLE, 41 | 42 | /** Date and time */ 43 | 'smalldatetime' => ColumnType::DATETIME, 44 | 'datetime' => ColumnType::DATETIME, 45 | 'datetime2' => ColumnType::DATETIME, 46 | 'datetimeoffset' => ColumnType::DATETIMETZ, 47 | 'time' => ColumnType::TIME, 48 | 'date' => ColumnType::DATE, 49 | 50 | /** Character strings */ 51 | 'char' => ColumnType::CHAR, 52 | 'varchar' => ColumnType::STRING, 53 | 'text' => ColumnType::TEXT, 54 | 55 | /** Unicode character strings */ 56 | 'nchar' => ColumnType::CHAR, 57 | 'nvarchar' => ColumnType::STRING, 58 | 'ntext' => ColumnType::TEXT, 59 | 60 | /** Binary strings */ 61 | 'binary' => ColumnType::BINARY, 62 | 'varbinary' => ColumnType::BINARY, 63 | 'image' => ColumnType::BINARY, 64 | 65 | /** 66 | * Other data types 'cursor' type can't be used with tables 67 | */ 68 | 'timestamp' => ColumnType::BINARY, 69 | 'hierarchyid' => ColumnType::STRING, 70 | 'uniqueidentifier' => ColumnType::UUID, 71 | 'sql_variant' => ColumnType::STRING, 72 | 'xml' => ColumnType::STRING, 73 | 'table' => ColumnType::STRING, 74 | ]; 75 | 76 | public function fromPseudoType(string $pseudoType, array $info = []): ColumnInterface 77 | { 78 | if ($pseudoType === PseudoType::UUID_PK_SEQ && !isset($info['defaultValue'])) { 79 | $info['defaultValue'] = new Expression('newsequentialid()'); 80 | } 81 | 82 | return parent::fromPseudoType($pseudoType, $info); 83 | } 84 | 85 | protected function getColumnClass(string $type, array $info = []): string 86 | { 87 | return match ($type) { 88 | ColumnType::BINARY => BinaryColumn::class, 89 | ColumnType::TIMESTAMP => DateTimeColumn::class, 90 | ColumnType::DATETIME => DateTimeColumn::class, 91 | ColumnType::DATETIMETZ => DateTimeColumn::class, 92 | ColumnType::TIME => DateTimeColumn::class, 93 | ColumnType::TIMETZ => DateTimeColumn::class, 94 | ColumnType::DATE => DateTimeColumn::class, 95 | default => parent::getColumnClass($type, $info), 96 | }; 97 | } 98 | 99 | protected function getType(string $dbType, array $info = []): string 100 | { 101 | if (isset($info['check'], $info['name']) && $info['check'] === "(isjson([{$info['name']}])>(0))") { 102 | return ColumnType::JSON; 103 | } 104 | 105 | return parent::getType($dbType, $info); 106 | } 107 | 108 | protected function normalizeNotNullDefaultValue(string $defaultValue, ColumnInterface $column): mixed 109 | { 110 | if ($defaultValue[0] === '(' && $defaultValue[-1] === ')') { 111 | $defaultValue = substr($defaultValue, 1, -1); 112 | } 113 | 114 | if (str_starts_with($defaultValue, '0x')) { 115 | return hex2bin(substr($defaultValue, 2)); 116 | } 117 | 118 | return parent::normalizeNotNullDefaultValue($defaultValue, $column); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Column/DateTimeColumn.php: -------------------------------------------------------------------------------- 1 | getDbType()) { 12 | 'smalldatetime' => '', 13 | 'datetime' => '.v', 14 | default => parent::getMillisecondsFormat(), 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | setSql($sql)->queryColumn(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | setSql($sql); 29 | } 30 | 31 | if ($this->logger !== null) { 32 | $command->setLogger($this->logger); 33 | } 34 | 35 | if ($this->profiler !== null) { 36 | $command->setProfiler($this->profiler); 37 | } 38 | 39 | return $command->bindValues($params); 40 | } 41 | 42 | public function createTransaction(): TransactionInterface 43 | { 44 | return new Transaction($this); 45 | } 46 | 47 | public function getColumnFactory(): ColumnFactoryInterface 48 | { 49 | return $this->columnFactory ??= new ColumnFactory(); 50 | } 51 | 52 | public function getQueryBuilder(): QueryBuilderInterface 53 | { 54 | return $this->queryBuilder ??= new QueryBuilder($this); 55 | } 56 | 57 | public function getQuoter(): QuoterInterface 58 | { 59 | return $this->quoter ??= new Quoter(['[', ']'], ['[', ']'], $this->getTablePrefix()); 60 | } 61 | 62 | public function getSchema(): SchemaInterface 63 | { 64 | return $this->schema ??= new Schema($this, $this->schemaCache); 65 | } 66 | 67 | /** 68 | * Initializes the DB connection. 69 | * 70 | * This method is invoked right after the DB connection is established. 71 | */ 72 | protected function initConnection(): void 73 | { 74 | $this->pdo = $this->driver->createConnection(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/DMLQueryBuilder.php: -------------------------------------------------------------------------------- 1 | schema->getTableSchema($table); 36 | $primaryKeys = $tableSchema?->getPrimaryKey(); 37 | 38 | if (empty($primaryKeys)) { 39 | return $this->insert($table, $columns, $params); 40 | } 41 | 42 | /** @var TableSchema $tableSchema */ 43 | [$declareSql, $outputSql, $selectSql] = $this->prepareReturningParts($tableSchema, $primaryKeys); 44 | [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params); 45 | 46 | return $declareSql 47 | . 'INSERT INTO ' . $this->quoter->quoteTableName($table) 48 | . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '') 49 | . $outputSql 50 | . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : ' ' . $values) . ';' 51 | . $selectSql; 52 | } 53 | 54 | /** 55 | * @throws InvalidArgumentException 56 | */ 57 | public function resetSequence(string $table, int|string|null $value = null): string 58 | { 59 | $tableSchema = $this->schema->getTableSchema($table); 60 | 61 | if ($tableSchema === null) { 62 | throw new InvalidArgumentException("Table not found: '$table'."); 63 | } 64 | 65 | $sequenceName = $tableSchema->getSequenceName(); 66 | 67 | if ($sequenceName === null) { 68 | throw new InvalidArgumentException("There is not sequence associated with table '$table'.'"); 69 | } 70 | 71 | $tableName = $this->quoter->quoteTableName($table); 72 | 73 | if ($value === null) { 74 | return "DBCC CHECKIDENT ('$tableName', RESEED, 0) WITH NO_INFOMSGS;DBCC CHECKIDENT ('$tableName', RESEED)"; 75 | } 76 | 77 | return "DBCC CHECKIDENT ('$tableName', RESEED, $value)"; 78 | } 79 | 80 | /** 81 | * @throws Exception 82 | * @throws InvalidArgumentException 83 | * @throws InvalidConfigException 84 | * @throws JsonException 85 | * @throws NotSupportedException 86 | */ 87 | public function upsert( 88 | string $table, 89 | array|QueryInterface $insertColumns, 90 | array|bool $updateColumns = true, 91 | array &$params = [], 92 | ): string { 93 | /** @psalm-var Constraint[] $constraints */ 94 | $constraints = []; 95 | 96 | [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns( 97 | $table, 98 | $insertColumns, 99 | $updateColumns, 100 | $constraints 101 | ); 102 | 103 | if (empty($uniqueNames)) { 104 | return $this->insert($table, $insertColumns, $params); 105 | } 106 | 107 | $onCondition = ['or']; 108 | $quotedTableName = $this->quoter->quoteTableName($table); 109 | 110 | foreach ($constraints as $constraint) { 111 | $constraintCondition = ['and']; 112 | $columnNames = (array) $constraint->getColumnNames(); 113 | 114 | /** @psalm-var string[] $columnNames */ 115 | foreach ($columnNames as $name) { 116 | $quotedName = $this->quoter->quoteColumnName($name); 117 | $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName"; 118 | } 119 | 120 | $onCondition[] = $constraintCondition; 121 | } 122 | 123 | $on = $this->queryBuilder->buildCondition($onCondition, $params); 124 | 125 | [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params); 126 | 127 | $mergeSql = 'MERGE ' . $quotedTableName . ' WITH (HOLDLOCK) USING (' 128 | . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : $values) 129 | . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') ' . "ON ($on)"; 130 | 131 | $insertValues = []; 132 | 133 | foreach ($insertNames as $quotedName) { 134 | $insertValues[] = '[EXCLUDED].' . $quotedName; 135 | } 136 | 137 | $insertSql = 'INSERT (' . implode(', ', $insertNames) . ') VALUES (' . implode(', ', $insertValues) . ')'; 138 | 139 | if ($updateColumns === false || $updateNames === []) { 140 | /** there are no columns to update */ 141 | return "$mergeSql WHEN NOT MATCHED THEN $insertSql;"; 142 | } 143 | 144 | if ($updateColumns === true) { 145 | $updateColumns = []; 146 | 147 | /** @psalm-var string[] $updateNames */ 148 | foreach ($updateNames as $quotedName) { 149 | $updateColumns[$quotedName] = new Expression('[EXCLUDED].' . $quotedName); 150 | } 151 | } 152 | 153 | [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params); 154 | 155 | return "$mergeSql WHEN MATCHED THEN UPDATE SET " . implode(', ', $updates) 156 | . " WHEN NOT MATCHED THEN $insertSql;"; 157 | } 158 | 159 | public function upsertWithReturningPks( 160 | string $table, 161 | array|QueryInterface $insertColumns, 162 | array|bool $updateColumns = true, 163 | array &$params = [], 164 | ): string { 165 | [$uniqueNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns); 166 | 167 | if (empty($uniqueNames)) { 168 | return $this->insertWithReturningPks($table, $insertColumns, $params); 169 | } 170 | 171 | $upsertSql = $this->upsert($table, $insertColumns, $updateColumns, $params); 172 | 173 | $tableSchema = $this->schema->getTableSchema($table); 174 | $primaryKeys = $tableSchema?->getPrimaryKey(); 175 | 176 | if (empty($primaryKeys)) { 177 | return $upsertSql; 178 | } 179 | 180 | /** @var TableSchema $tableSchema */ 181 | [$declareSql, $outputSql, $selectSql] = $this->prepareReturningParts($tableSchema, $primaryKeys); 182 | 183 | return $declareSql 184 | . rtrim($upsertSql, ';') 185 | . $outputSql . ';' 186 | . $selectSql; 187 | } 188 | 189 | /** 190 | * Prepares SQL parts for a returning query. 191 | * 192 | * @param TableSchema $tableSchema The table schema. 193 | * @param string[] $returnColumns The columns to return. 194 | * 195 | * @return string[] List of Declare SQL, output SQL and select SQL. 196 | */ 197 | private function prepareReturningParts( 198 | TableSchema $tableSchema, 199 | array $returnColumns, 200 | ): array { 201 | $createdCols = []; 202 | $insertedCols = []; 203 | $columns = array_intersect_key($tableSchema->getColumns(), array_fill_keys($returnColumns, true)); 204 | 205 | $columnDefinitionBuilder = $this->queryBuilder->getColumnDefinitionBuilder(); 206 | 207 | foreach ($columns as $name => $column) { 208 | if ($column->getDbType() === 'timestamp') { 209 | $dbType = $column->isNotNull() ? 'binary(8)' : 'varbinary(8)'; 210 | } else { 211 | $dbType = $columnDefinitionBuilder->buildType($column); 212 | } 213 | 214 | $quotedName = $this->quoter->quoteColumnName($name); 215 | $createdCols[] = $quotedName . ' ' . $dbType . ($column->isNotNull() ? '' : ' NULL'); 216 | $insertedCols[] = 'INSERTED.' . $quotedName; 217 | } 218 | 219 | return [ 220 | 'SET NOCOUNT ON;DECLARE @temporary_inserted TABLE (' . implode(', ', $createdCols) . ');', 221 | ' OUTPUT ' . implode(',', $insertedCols) . ' INTO @temporary_inserted', 222 | 'SELECT * FROM @temporary_inserted;', 223 | ]; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/DQLQueryBuilder.php: -------------------------------------------------------------------------------- 1 | buildOrderBy($orderBy, $params); 33 | 34 | return $orderByString === '' ? $sql : $sql . $this->separator . $orderByString; 35 | } 36 | 37 | return $this->newBuildOrderByAndLimit($sql, $orderBy, $limit, $offset, $params); 38 | } 39 | 40 | public function selectExists(string $rawSql): string 41 | { 42 | return 'SELECT CASE WHEN EXISTS(' . $rawSql . ') THEN 1 ELSE 0 END AS [0]'; 43 | } 44 | 45 | protected function defaultExpressionBuilders(): array 46 | { 47 | return [ 48 | ...parent::defaultExpressionBuilders(), 49 | InCondition::class => InConditionBuilder::class, 50 | LikeCondition::class => LikeConditionBuilder::class, 51 | ]; 52 | } 53 | 54 | /** 55 | * Builds the `ORDER BY`/`LIMIT`/`OFFSET` clauses for SQL SERVER 2012 or newer. 56 | * 57 | * @param string $sql The existing SQL (without `ORDER BY`/`LIMIT`/`OFFSET`). 58 | * @param array $orderBy The order by columns. See {@see Query::orderBy} for more details on how to specify 59 | * this parameter. 60 | * @param ExpressionInterface|int|null $limit The limit number. See {@see Query::limit} for more details. 61 | * @param ExpressionInterface|int|null $offset The offset number. See {@see Query::offset} for more details. 62 | * @param array $params The binding parameters to populate. 63 | * 64 | * @throws Exception 65 | * @throws InvalidArgumentException 66 | * 67 | * @return string The SQL completed with `ORDER BY`/`LIMIT`/`OFFSET` (if any). 68 | */ 69 | protected function newBuildOrderByAndLimit( 70 | string $sql, 71 | array $orderBy, 72 | ExpressionInterface|int|null $limit, 73 | ExpressionInterface|int|null $offset, 74 | array &$params = [] 75 | ): string { 76 | $orderByString = $this->buildOrderBy($orderBy, $params); 77 | 78 | if ($orderByString === '') { 79 | /** `ORDER BY` clause is required when `FETCH` and `OFFSET` are in the SQL */ 80 | $orderByString = 'ORDER BY (SELECT NULL)'; 81 | } 82 | 83 | $sql .= $this->separator . $orderByString; 84 | 85 | /** 86 | * @link https://technet.microsoft.com/en-us/library/gg699618.aspx 87 | */ 88 | $offsetString = !empty($offset) 89 | ? ($offset instanceof ExpressionInterface ? $this->buildExpression($offset) : (string) $offset) 90 | : '0'; 91 | $sql .= $this->separator . 'OFFSET ' . $offsetString . ' ROWS'; 92 | 93 | if ($limit !== null) { 94 | $sql .= $this->separator . 'FETCH NEXT ' 95 | . ($limit instanceof ExpressionInterface ? $this->buildExpression($limit) : (string) $limit) 96 | . ' ROWS ONLY'; 97 | } 98 | 99 | return $sql; 100 | } 101 | 102 | /** 103 | * Extracts table alias if there is one or returns `false`. 104 | * 105 | * @psalm-return string[]|bool 106 | */ 107 | protected function extractAlias(string $table): array|bool 108 | { 109 | if (preg_match('/^\[.*]$/', $table)) { 110 | return false; 111 | } 112 | 113 | return parent::extractAlias($table); 114 | } 115 | 116 | public function buildWithQueries(array $withs, array &$params): string 117 | { 118 | /** @psalm-var array{query:string|Query, alias:ExpressionInterface|string, recursive:bool}[] $withs */ 119 | foreach ($withs as &$with) { 120 | $with['recursive'] = false; 121 | } 122 | 123 | return parent::buildWithQueries($withs, $params); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Driver.php: -------------------------------------------------------------------------------- 1 | attributes += [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]; 20 | return parent::createConnection(); 21 | } 22 | 23 | public function getDriverName(): string 24 | { 25 | return 'sqlsrv'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Dsn.php: -------------------------------------------------------------------------------- 1 | $options 18 | */ 19 | public function __construct( 20 | string $driver = 'sqlsrv', 21 | string $host = '127.0.0.1', 22 | string|null $databaseName = null, 23 | string|null $port = '1433', 24 | array $options = [] 25 | ) { 26 | parent::__construct($driver, $host, $databaseName, $port, $options); 27 | } 28 | 29 | /** 30 | * @return string the Data Source Name, or DSN, has the information required to connect to the database. 31 | * Please refer to the [PHP manual](https://php.net/manual/en/pdo.construct.php) on the format of the DSN string. 32 | * 33 | * The `driver` array key is used as the driver prefix of the DSN, all further key-value pairs are rendered as 34 | * `key=value` and concatenated by `;`. For example: 35 | * 36 | * ```php 37 | * $dsn = new Dsn('sqlsrv', 'localhost', 'yiitest', '1433'); 38 | * $driver = new Driver($dsn->asString(), 'username', 'password'); 39 | * $db = new Connection($driver, $schemaCache); 40 | * ``` 41 | * 42 | * Will result in the DSN string `sqlsrv:Server=localhost,1433;Database=yiitest`. 43 | */ 44 | public function asString(): string 45 | { 46 | $driver = $this->getDriver(); 47 | $host = $this->getHost(); 48 | $port = $this->getPort(); 49 | $databaseName = $this->getDatabaseName(); 50 | $options = $this->getOptions(); 51 | 52 | $dsn = "$driver:Server=$host"; 53 | 54 | if (!empty($port)) { 55 | $dsn .= ",$port"; 56 | } 57 | 58 | if (!empty($databaseName)) { 59 | $dsn .= ";Database=$databaseName"; 60 | } 61 | 62 | if (!empty($options)) { 63 | foreach ($options as $key => $value) { 64 | $dsn .= ";$key=$value"; 65 | } 66 | } 67 | 68 | return $dsn; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/IndexMethod.php: -------------------------------------------------------------------------------- 1 | getQuoter(); 23 | $schema = $db->getSchema(); 24 | 25 | parent::__construct( 26 | $db, 27 | new DDLQueryBuilder($this, $quoter, $schema), 28 | new DMLQueryBuilder($this, $quoter, $schema), 29 | new DQLQueryBuilder($this, $quoter), 30 | new ColumnDefinitionBuilder($this), 31 | ); 32 | } 33 | 34 | protected function createSqlParser(string $sql): SqlParser 35 | { 36 | return new SqlParser($sql); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Quoter.php: -------------------------------------------------------------------------------- 1 | 0) { 31 | $parts = array_slice($matches[0], -4, 4); 32 | 33 | return array_map($this->unquoteSimpleTableName(...), $parts); 34 | } 35 | 36 | return [$this->unquoteSimpleTableName($name)]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/SqlParser.php: -------------------------------------------------------------------------------- 1 | length - 1; 15 | 16 | while ($this->position < $length) { 17 | $pos = $this->position++; 18 | 19 | match ($this->sql[$pos]) { 20 | ':' => ($word = $this->parseWord()) === '' 21 | ? $this->skipChars(':') 22 | : $result = ':' . $word, 23 | '"', "'" => $this->skipQuotedWithoutEscape($this->sql[$pos]), 24 | '[' => $this->sql[$this->position] === '[' 25 | ? $this->skipToAfterString(']]') 26 | : $this->skipQuotedWithoutEscape(']'), 27 | '-' => $this->sql[$this->position] === '-' 28 | ? ++$this->position && $this->skipToAfterChar("\n") 29 | : null, 30 | '/' => $this->sql[$this->position] === '*' 31 | ? ++$this->position && $this->skipToAfterString('*/') 32 | : null, 33 | default => null, 34 | }; 35 | 36 | if ($result !== null) { 37 | $position = $pos; 38 | 39 | return $result; 40 | } 41 | } 42 | 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/TableSchema.php: -------------------------------------------------------------------------------- 1 | db->createCommand("SAVE TRANSACTION $name")->execute(); 27 | } 28 | 29 | /** 30 | * Releases an existing savepoint. 31 | * 32 | * @param string $name the savepoint name. 33 | */ 34 | public function releaseSavepoint(string $name): void 35 | { 36 | // does nothing as MSSQL doesn't support this 37 | } 38 | 39 | /** 40 | * Rolls back to a before created savepoint. 41 | * 42 | * @param string $name the savepoint name. 43 | * 44 | * @throws Exception|InvalidConfigException|Throwable 45 | */ 46 | public function rollBackSavepoint(string $name): void 47 | { 48 | $this->db->createCommand("ROLLBACK TRANSACTION $name")->execute(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/.env: -------------------------------------------------------------------------------- 1 | ENVIRONMENT=local 2 | YII_MSSQL_DATABASE=tempdb 3 | YII_MSSQL_HOST=mssql 4 | YII_MSSQL_PORT=1433 5 | YII_MSSQL_USER=SA 6 | YII_MSSQL_PASSWORD=YourStrong!Passw0rd 7 | -------------------------------------------------------------------------------- /tests/Builder/InconditionBuilderTest.php: -------------------------------------------------------------------------------- 1 | getConnection(); 34 | $inCondition = new InCondition( 35 | ['id'], 36 | 'in', 37 | (new Query($db))->select('id')->from('users')->where(['active' => 1]), 38 | ); 39 | 40 | $this->expectException(NotSupportedException::class); 41 | $this->expectExceptionMessage( 42 | 'Yiisoft\Db\Mssql\Builder\InConditionBuilder::buildSubqueryInCondition is not supported by MSSQL.' 43 | ); 44 | 45 | (new InConditionBuilder($db->getQueryBuilder()))->build($inCondition); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/ColumnBuilderTest.php: -------------------------------------------------------------------------------- 1 | createCommand()->insert( 40 | 'type', 41 | [ 42 | 'int_col' => 1, 43 | 'char_col' => str_repeat('x', 100), 44 | 'char_col3' => null, 45 | 'float_col' => 1.234, 46 | 'blob_col' => "\x10\x11\x12", 47 | 'datetime_col' => '2023-07-11 14:50:00.123', 48 | 'bool_col' => false, 49 | 'json_col' => [['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], 50 | ] 51 | )->execute(); 52 | } 53 | 54 | private function assertTypecastedValues(array $result, bool $allTypecasted = false): void 55 | { 56 | $this->assertSame(1, $result['int_col']); 57 | $this->assertSame(str_repeat('x', 100), $result['char_col']); 58 | $this->assertNull($result['char_col3']); 59 | $this->assertSame(1.234, $result['float_col']); 60 | $this->assertSame("\x10\x11\x12", $result['blob_col']); 61 | $this->assertEquals(new DateTimeImmutable('2023-07-11 14:50:00.123', new DateTimeZone('UTC')), $result['datetime_col']); 62 | $this->assertFalse($result['bool_col']); 63 | 64 | if ($allTypecasted) { 65 | $this->assertSame([['a' => 1, 'b' => null, 'c' => [1, 3, 5]]], $result['json_col']); 66 | } else { 67 | $this->assertSame('[{"a":1,"b":null,"c":[1,3,5]}]', $result['json_col']); 68 | } 69 | } 70 | 71 | public function testQueryWithTypecasting(): void 72 | { 73 | $db = $this->getConnection(true); 74 | 75 | $this->insertTypeValues($db); 76 | 77 | $query = (new Query($db))->from('type')->withTypecasting(); 78 | 79 | $result = $query->one(); 80 | 81 | $this->assertTypecastedValues($result); 82 | 83 | $result = $query->all(); 84 | 85 | $this->assertTypecastedValues($result[0]); 86 | 87 | $db->close(); 88 | } 89 | 90 | public function testCommandWithPhpTypecasting(): void 91 | { 92 | $db = $this->getConnection(true); 93 | 94 | $this->insertTypeValues($db); 95 | 96 | $command = $db->createCommand('SELECT * FROM type')->withPhpTypecasting(); 97 | 98 | $result = $command->queryOne(); 99 | 100 | $this->assertTypecastedValues($result); 101 | 102 | $result = $command->queryAll(); 103 | 104 | $this->assertTypecastedValues($result[0]); 105 | 106 | $db->close(); 107 | } 108 | 109 | public function testSelectWithPhpTypecasting(): void 110 | { 111 | $db = $this->getConnection(); 112 | 113 | $sql = "SELECT null AS [null], 1 AS [1], 2.5 AS [2.5], 'string' AS [string]"; 114 | 115 | $expected = [ 116 | 'null' => null, 117 | 1 => 1, 118 | '2.5' => 2.5, 119 | 'string' => 'string', 120 | ]; 121 | 122 | $result = $db->createCommand($sql) 123 | ->withPhpTypecasting() 124 | ->queryOne(); 125 | 126 | $this->assertSame($expected, $result); 127 | 128 | $result = $db->createCommand($sql) 129 | ->withPhpTypecasting() 130 | ->queryAll(); 131 | 132 | $this->assertSame([$expected], $result); 133 | 134 | $result = $db->createCommand('SELECT 2.5') 135 | ->withPhpTypecasting() 136 | ->queryScalar(); 137 | 138 | $this->assertSame(2.5, $result); 139 | 140 | $result = $db->createCommand('SELECT 2.5 UNION SELECT 3.3') 141 | ->withPhpTypecasting() 142 | ->queryColumn(); 143 | 144 | $this->assertSame([2.5, 3.3], $result); 145 | 146 | $db->close(); 147 | } 148 | 149 | public function testPhpTypeCast(): void 150 | { 151 | $db = $this->getConnection(true); 152 | $schema = $db->getSchema(); 153 | $columns = $schema->getTableSchema('type')->getColumns(); 154 | 155 | $this->insertTypeValues($db); 156 | 157 | $query = (new Query($db))->from('type')->one(); 158 | 159 | $result = []; 160 | 161 | foreach ($columns as $columnName => $column) { 162 | $result[$columnName] = $column->phpTypecast($query[$columnName]); 163 | } 164 | 165 | $this->assertTypecastedValues($result, true); 166 | 167 | $db->close(); 168 | } 169 | 170 | public function testColumnInstance() 171 | { 172 | $db = $this->getConnection(true); 173 | $schema = $db->getSchema(); 174 | $tableSchema = $schema->getTableSchema('type'); 175 | 176 | $this->assertInstanceOf(IntegerColumn::class, $tableSchema->getColumn('int_col')); 177 | $this->assertInstanceOf(StringColumn::class, $tableSchema->getColumn('char_col')); 178 | $this->assertInstanceOf(DoubleColumn::class, $tableSchema->getColumn('float_col')); 179 | $this->assertInstanceOf(BinaryColumn::class, $tableSchema->getColumn('blob_col')); 180 | $this->assertInstanceOf(BooleanColumn::class, $tableSchema->getColumn('bool_col')); 181 | } 182 | 183 | #[DataProviderExternal(ColumnProvider::class, 'predefinedTypes')] 184 | public function testPredefinedType(string $className, string $type, string $phpType) 185 | { 186 | parent::testPredefinedType($className, $type, $phpType); 187 | } 188 | 189 | #[DataProviderExternal(ColumnProvider::class, 'dbTypecastColumns')] 190 | public function testDbTypecastColumns(ColumnInterface $column, array $values) 191 | { 192 | parent::testDbTypecastColumns($column, $values); 193 | } 194 | 195 | #[DataProviderExternal(ColumnProvider::class, 'phpTypecastColumns')] 196 | public function testPhpTypecastColumns(ColumnInterface $column, array $values) 197 | { 198 | parent::testPhpTypecastColumns($column, $values); 199 | } 200 | 201 | public function testBinaryColumn() 202 | { 203 | $binaryCol = new BinaryColumn(); 204 | $binaryCol->dbType('varbinary'); 205 | 206 | $this->assertEquals( 207 | new Expression('CONVERT(VARBINARY(MAX), 0x101112)'), 208 | $binaryCol->dbTypecast("\x10\x11\x12"), 209 | ); 210 | $this->assertEquals( 211 | new Expression('CONVERT(VARBINARY(MAX), 0x101112)'), 212 | $binaryCol->dbTypecast(new Param("\x10\x11\x12", PDO::PARAM_LOB)), 213 | ); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /tests/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | getConnection(true); 38 | 39 | $transaction = $db->beginTransaction(TransactionInterface::READ_UNCOMMITTED); 40 | $transaction->commit(); 41 | 42 | $transaction = $db->beginTransaction(TransactionInterface::READ_COMMITTED); 43 | $transaction->commit(); 44 | 45 | $transaction = $db->beginTransaction(TransactionInterface::REPEATABLE_READ); 46 | $transaction->commit(); 47 | 48 | $transaction = $db->beginTransaction(TransactionInterface::SERIALIZABLE); 49 | $transaction->commit(); 50 | 51 | /* should not be any exception so far */ 52 | $this->assertTrue(true); 53 | } 54 | 55 | /** 56 | * @throws Exception 57 | * @throws InvalidConfigException 58 | * @throws Throwable 59 | */ 60 | public function testTransactionShortcutCustom(): void 61 | { 62 | $db = $this->getConnection(); 63 | 64 | $result = $db->transaction( 65 | static function (PdoConnectionInterface $db): bool { 66 | $db->createCommand()->insert('profile', ['description' => 'test transaction shortcut'])->execute(); 67 | return true; 68 | }, 69 | TransactionInterface::READ_UNCOMMITTED, 70 | ); 71 | 72 | $this->assertTrue($result, 'transaction shortcut valid value should be returned from callback'); 73 | 74 | $profilesCount = $db->createCommand( 75 | <<queryScalar(); 79 | 80 | $this->assertSame('1', $profilesCount, 'profile should be inserted in transaction shortcut'); 81 | } 82 | 83 | /** 84 | * @throws Exception 85 | * @throws InvalidConfigException 86 | */ 87 | public function testSettingDefaultAttributes(): void 88 | { 89 | $db = $this->getConnection(); 90 | 91 | $this->assertSame(PDO::ERRMODE_EXCEPTION, $db->getActivePDO()?->getAttribute(PDO::ATTR_ERRMODE)); 92 | } 93 | 94 | public function testGetColumnFactory(): void 95 | { 96 | $db = $this->getConnection(); 97 | 98 | $this->assertInstanceOf(ColumnFactory::class, $db->getColumnFactory()); 99 | 100 | $db->close(); 101 | } 102 | 103 | public function testUserDefinedColumnFactory(): void 104 | { 105 | $columnFactory = new ColumnFactory(); 106 | 107 | $db = new Connection($this->getDriver(), DbHelper::getSchemaCache(), $columnFactory); 108 | 109 | $this->assertSame($columnFactory, $db->getColumnFactory()); 110 | 111 | $db->close(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/DsnTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 20 | 'sqlsrv:Server=127.0.0.1,1433;Database=yiitest', 21 | (new Dsn('sqlsrv', '127.0.0.1', 'yiitest', '1433'))->asString(), 22 | ); 23 | } 24 | 25 | public function testAsStringWithDatabaseName(): void 26 | { 27 | $this->assertSame('sqlsrv:Server=127.0.0.1,1433', (new Dsn('sqlsrv', '127.0.0.1'))->asString()); 28 | } 29 | 30 | public function testAsStringWithDatabaseNameWithEmptyString(): void 31 | { 32 | $this->assertSame('sqlsrv:Server=127.0.0.1,1433', (new Dsn('sqlsrv', '127.0.0.1', ''))->asString()); 33 | } 34 | 35 | public function testAsStringWithDatabaseNameWithNull(): void 36 | { 37 | $this->assertSame('sqlsrv:Server=127.0.0.1,1433', (new Dsn('sqlsrv', '127.0.0.1', null))->asString()); 38 | } 39 | 40 | public function testAsStringWithEmptyPort(): void 41 | { 42 | $this->assertSame( 43 | 'sqlsrv:Server=127.0.0.1;Database=yiitest', 44 | (new Dsn('sqlsrv', '127.0.0.1', 'yiitest', ''))->asString(), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/PdoCommandTest.php: -------------------------------------------------------------------------------- 1 | getConnection(); 33 | 34 | // One sequence, two tables 35 | $tableName1 = 'seqtable1'; 36 | $tableName2 = 'seqtable2'; 37 | $sequenceName = 'sequence1'; 38 | 39 | $command = $db->createCommand(); 40 | 41 | if ($db->getSchema()->getTableSchema($tableName1) !== null) { 42 | $command->dropTable($tableName1)->execute(); 43 | } 44 | 45 | if ($db->getSchema()->getTableSchema($tableName2) !== null) { 46 | $command->dropTable($tableName2)->execute(); 47 | } 48 | 49 | $command->setSql( 50 | <<execute(); 54 | 55 | $command->setSql( 56 | <<execute(); 60 | $command->setSql( 61 | <<execute(); 65 | $command->setSql( 66 | <<execute(); 70 | $command->insert( 71 | $tableName1, 72 | ['seqnum' => new Expression("NEXT VALUE FOR $sequenceName"), 'SomeNumber' => 20], 73 | )->execute(); 74 | $command->insert( 75 | $tableName1, 76 | ['seqnum' => new Expression("NEXT VALUE FOR $sequenceName"), 'SomeNumber' => 40], 77 | )->execute(); 78 | $command->insert( 79 | $tableName1, 80 | ['seqnum' => new Expression("NEXT VALUE FOR $sequenceName"), 'SomeNumber' => 60], 81 | )->execute(); 82 | $command->insert($tableName2, ['SomeValue' => 20])->execute(); 83 | 84 | // Return the last inserted ID for the table with a sequence 85 | $this->assertSame('3', $db->getLastInsertId($sequenceName)); 86 | 87 | // Return the last inserted ID for the table with an identity column 88 | $this->assertSame('1', $db->getLastInsertId()); 89 | 90 | // Return empty sting 91 | $this->assertEmpty($db->getLastInsertId($tableName1)); 92 | 93 | // Return empty sting 94 | $this->assertEmpty($db->getLastInsertId($tableName2)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Provider/ColumnBuilderProvider.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 | 'id', 32 | ':id', 33 | 1, 34 | PDO::PARAM_INT, 35 | 0, 36 | null, 37 | [ 38 | 'id' => '1', 39 | 'email' => 'user1@example.com', 40 | 'name' => 'user1', 41 | 'address' => 'address1', 42 | 'status' => '1', 43 | 'profile_id' => '1', 44 | ], 45 | ], 46 | [ 47 | 'status', 48 | ':status', 49 | 2, 50 | PDO::PARAM_STR, 51 | 0, 52 | PDO::SQLSRV_ENCODING_UTF8, 53 | [ 54 | 'id' => '3', 55 | 'email' => 'user3@example.com', 56 | 'name' => 'user3', 57 | 'address' => 'address3', 58 | 'status' => '2', 59 | 'profile_id' => '2', 60 | ], 61 | ], 62 | ]; 63 | } 64 | 65 | public static function bindParamsNonWhere(): array 66 | { 67 | return[ 68 | [ 69 | << 'string', 'integer' => 1234], JSON_THROW_ON_ERROR), 50 | json_encode(['string' => 'string', 'integer' => 1234], JSON_THROW_ON_ERROR), 51 | ], 52 | [ 53 | serialize(['string' => 'string', 'integer' => 1234]), 54 | new Param(serialize(['string' => 'string', 'integer' => 1234]), PDO::PARAM_LOB), 55 | ], 56 | [ 57 | 'simple string', 58 | 'simple string', 59 | ], 60 | ]; 61 | } 62 | 63 | public static function rawSql(): array 64 | { 65 | $rawSql = parent::rawSql(); 66 | 67 | foreach ($rawSql as &$values) { 68 | $values[2] = strtr($values[2], [ 69 | 'FALSE' => '0', 70 | 'TRUE' => '1', 71 | ]); 72 | } 73 | 74 | return $rawSql; 75 | } 76 | 77 | public static function createIndex(): array 78 | { 79 | return [ 80 | ...parent::createIndex(), 81 | [['col1' => ColumnBuilder::integer()], ['col1'], IndexType::CLUSTERED, null], 82 | [['col1' => ColumnBuilder::integer()], ['col1'], IndexType::NONCLUSTERED, null], 83 | [['col1' => ColumnBuilder::integer()], ['col1'], IndexType::UNIQUE, null], 84 | [['col1' => ColumnBuilder::integer()], ['col1'], IndexType::UNIQUE_CLUSTERED, null], 85 | [['id' => ColumnBuilder::primaryKey(), 'col1' => 'geometry'], ['col1'], IndexType::SPATIAL, IndexMethod::GEOMETRY_GRID . ' WITH(BOUNDING_BOX = (0, 0, 100, 100))'], 86 | [['id' => ColumnBuilder::primaryKey(), 'col1' => 'geometry'], ['col1'], IndexType::SPATIAL, IndexMethod::GEOMETRY_AUTO_GRID . ' WITH(BOUNDING_BOX = (0, 0, 100, 100))'], 87 | [['id' => ColumnBuilder::primaryKey(), 'col1' => 'geography'], ['col1'], IndexType::SPATIAL, null], 88 | [['id' => ColumnBuilder::primaryKey(), 'col1' => 'geography'], ['col1'], IndexType::SPATIAL, IndexMethod::GEOGRAPHY_GRID], 89 | [['id' => ColumnBuilder::primaryKey(), 'col1' => 'geography'], ['col1'], IndexType::SPATIAL, IndexMethod::GEOGRAPHY_AUTO_GRID], 90 | [['col1' => ColumnBuilder::integer()], ['col1'], IndexType::COLUMNSTORE, null], 91 | [['id' => ColumnBuilder::primaryKey(), 'col1' => 'xml'], ['col1'], IndexType::PRIMARY_XML, null], 92 | ]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Provider/QuoterProvider.php: -------------------------------------------------------------------------------- 1 | getConnection(true); 33 | 34 | $selectExpression = 'CONCAT(customer.name, \' in \', p.description) name'; 35 | 36 | $result = (new Query($db)) 37 | ->select([$selectExpression]) 38 | ->from('customer') 39 | ->innerJoin('profile p', '[[customer]].[[profile_id]] = [[p]].[[id]]') 40 | ->indexBy('id') 41 | ->column(); 42 | 43 | $this->assertSame([1 => 'user1 in profile customer 1', 3 => 'user3 in profile customer 3'], $result); 44 | } 45 | 46 | /** 47 | * @throws Exception 48 | * @throws InvalidConfigException 49 | * @throws Throwable 50 | */ 51 | public function testUnion(): void 52 | { 53 | $db = $this->getConnection(); 54 | 55 | $subQueryFromItem = (new Query($db))->select(['id', 'name'])->from('item')->limit(2); 56 | $subQueryFromCategory = (new Query($db))->select(['id', 'name'])->from(['category'])->limit(2); 57 | $subQueryUnion = (new Query($db))->select(['id', 'name'])->from($subQueryFromCategory); 58 | 59 | $query = (new Query($db))->select(['id', 'name'])->from($subQueryFromItem)->union($subQueryUnion); 60 | $data = $query->all(); 61 | 62 | $this->assertCount(4, $data); 63 | } 64 | 65 | #[DataProvider('dataLikeCaseSensitive')] 66 | public function testLikeCaseSensitive(mixed $expected, string $value): void 67 | { 68 | $db = $this->getConnection(true); 69 | 70 | $query = (new Query($db)) 71 | ->select('name') 72 | ->from('customer') 73 | ->where(['like', 'name', $value, 'caseSensitive' => true]); 74 | 75 | $this->expectException(NotSupportedException::class); 76 | $this->expectExceptionMessage('MSSQL doesn\'t support case-sensitive "LIKE" conditions.'); 77 | $query->scalar(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/QuoterTest.php: -------------------------------------------------------------------------------- 1 | getConnection(true); 49 | $schema = $db->getSchema(); 50 | $tUpsert = $schema->getTableSchema('T_upsert'); 51 | 52 | $this->assertInstanceOf(TableSchemaInterface::class, $tUpsert); 53 | $this->assertContains([0 => 'email', 1 => 'recovery_email'], $schema->findUniqueIndexes($tUpsert)); 54 | } 55 | 56 | public function testGetDefaultSchema(): void 57 | { 58 | $db = $this->getConnection(); 59 | 60 | $schema = $db->getSchema(); 61 | 62 | $this->assertSame('dbo', $schema->getDefaultSchema()); 63 | } 64 | 65 | /** 66 | * @throws NotSupportedException 67 | */ 68 | public function testGetSchemaNames(): void 69 | { 70 | $db = $this->getConnection(true); 71 | 72 | $schema = $db->getSchema(); 73 | $schemas = $schema->getSchemaNames(); 74 | 75 | $this->assertSame(['dbo', 'guest'], $schemas); 76 | } 77 | 78 | public function testGetViewNames(): void 79 | { 80 | $db = $this->getConnection(true); 81 | 82 | $schema = $db->getSchema(); 83 | 84 | $this->assertSame(['animal_view'], $schema->getViewNames()); 85 | $this->assertSame(['animal_view'], $schema->getViewNames('dbo')); 86 | $this->assertSame(['animal_view'], $schema->getViewNames('dbo', true)); 87 | } 88 | 89 | /** 90 | * @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\SchemaProvider::constraints 91 | * 92 | * @throws Exception 93 | */ 94 | public function testTableSchemaConstraints(string $tableName, string $type, mixed $expected): void 95 | { 96 | parent::testTableSchemaConstraints($tableName, $type, $expected); 97 | } 98 | 99 | /** 100 | * @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\SchemaProvider::constraints 101 | * 102 | * @throws Exception 103 | */ 104 | public function testTableSchemaConstraintsWithPdoLowercase(string $tableName, string $type, mixed $expected): void 105 | { 106 | parent::testTableSchemaConstraintsWithPdoLowercase($tableName, $type, $expected); 107 | } 108 | 109 | /** 110 | * @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\SchemaProvider::constraints 111 | * 112 | * @throws Exception 113 | */ 114 | public function testTableSchemaConstraintsWithPdoUppercase(string $tableName, string $type, mixed $expected): void 115 | { 116 | parent::testTableSchemaConstraintsWithPdoUppercase($tableName, $type, $expected); 117 | } 118 | 119 | /** 120 | * @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\SchemaProvider::tableSchemaWithDbSchemes 121 | */ 122 | public function testTableSchemaWithDbSchemes(string $tableName, string $expectedTableName): void 123 | { 124 | $db = $this->getConnection(); 125 | 126 | $commandMock = $this->createMock(CommandInterface::class); 127 | $commandMock->method('queryAll')->willReturn([]); 128 | $mockDb = $this->createMock(PdoConnectionInterface::class); 129 | $mockDb->method('getQuoter')->willReturn($db->getQuoter()); 130 | $mockDb 131 | ->method('createCommand') 132 | ->with( 133 | self::callback(static fn ($sql) => true), 134 | self::callback( 135 | function ($params) use ($expectedTableName) { 136 | $this->assertEquals($expectedTableName, $params[':fullName']); 137 | 138 | return true; 139 | } 140 | ) 141 | ) 142 | ->willReturn($commandMock); 143 | $schema = new Schema($mockDb, DbHelper::getSchemaCache()); 144 | $schema->getTablePrimaryKey($tableName); 145 | } 146 | 147 | public function withIndexDataProvider(): array 148 | { 149 | return [ 150 | ...parent::withIndexDataProvider(), 151 | [ 152 | 'indexType' => SchemaInterface::INDEX_CLUSTERED, 153 | 'indexMethod' => null, 154 | 'columnType' => 'varchar(16)', 155 | ], 156 | [ 157 | 'indexType' => SchemaInterface::INDEX_NONCLUSTERED, 158 | 'indexMethod' => null, 159 | 'columnType' => 'varchar(16)', 160 | ], 161 | ]; 162 | } 163 | 164 | public function testNotConnectionPDO(): void 165 | { 166 | $db = $this->createMock(ConnectionInterface::class); 167 | $schema = new Schema($db, DbHelper::getSchemaCache()); 168 | 169 | $this->expectException(NotSupportedException::class); 170 | $this->expectExceptionMessage('Only PDO connections are supported.'); 171 | 172 | $schema->refresh(); 173 | } 174 | 175 | public function testNegativeDefaultValues(): void 176 | { 177 | $db = $this->getConnection(true); 178 | 179 | $schema = $db->getSchema(); 180 | $table = $schema->getTableSchema('negative_default_values'); 181 | 182 | $this->assertNotNull($table); 183 | $this->assertSame(-123, $table->getColumn('smallint_col')?->getDefaultValue()); 184 | $this->assertSame(-123, $table->getColumn('int_col')?->getDefaultValue()); 185 | $this->assertSame(-123, $table->getColumn('bigint_col')?->getDefaultValue()); 186 | $this->assertSame(-12345.6789, $table->getColumn('float_col')?->getDefaultValue()); 187 | $this->assertEquals(-33.22, $table->getColumn('numeric_col')?->getDefaultValue()); 188 | 189 | $db->close(); 190 | } 191 | 192 | #[DataProviderExternal(SchemaProvider::class, 'resultColumns')] 193 | public function testGetResultColumn(ColumnInterface|null $expected, array $info): void 194 | { 195 | parent::testGetResultColumn($expected, $info); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tests/SqlParserTest.php: -------------------------------------------------------------------------------- 1 | getDriver(), DbHelper::getSchemaCache()); 26 | 27 | if ($fixture) { 28 | DbHelper::loadFixture($db, __DIR__ . "/Fixture/$this->fixture"); 29 | } 30 | 31 | return $db; 32 | } 33 | 34 | protected static function getDb(): Connection 35 | { 36 | $dsn = (new Dsn( 37 | host: self::getHost(), 38 | databaseName: self::getDatabaseName(), 39 | port: self::getPort(), 40 | options: ['Encrypt' => 'no'] 41 | ))->asString(); 42 | 43 | return new Connection( 44 | new Driver($dsn, self::getUsername(), self::getPassword()), 45 | DbHelper::getSchemaCache(), 46 | ); 47 | } 48 | 49 | protected function getDsn(): string 50 | { 51 | if ($this->dsn === '') { 52 | $this->dsn = (new Dsn( 53 | host: self::getHost(), 54 | databaseName: self::getDatabaseName(), 55 | port: self::getPort(), 56 | options: ['Encrypt' => 'no'] 57 | ))->asString(); 58 | } 59 | 60 | return $this->dsn; 61 | } 62 | 63 | protected function getDriverName(): string 64 | { 65 | return 'sqlsrv'; 66 | } 67 | 68 | protected function setDsn(string $dsn): void 69 | { 70 | $this->dsn = $dsn; 71 | } 72 | 73 | protected function setFixture(string $fixture): void 74 | { 75 | $this->fixture = $fixture; 76 | } 77 | 78 | protected function getDriver(): Driver 79 | { 80 | return new Driver($this->getDsn(), self::getUsername(), self::getPassword()); 81 | } 82 | 83 | private static function getDatabaseName(): string 84 | { 85 | return getenv('YII_MSSQL_DATABASE') ?: 'yiitest'; 86 | } 87 | 88 | private static function getHost(): string 89 | { 90 | return getenv('YII_MSSQL_HOST') ?: '127.0.0.1'; 91 | } 92 | 93 | private static function getPort(): string 94 | { 95 | return getenv('YII_MSSQL_PORT') ?: '1433'; 96 | } 97 | 98 | private static function getUsername(): string 99 | { 100 | return getenv('YII_MSSQL_USER') ?: 'SA'; 101 | } 102 | 103 | private static function getPassword(): string 104 | { 105 | return getenv('YII_MSSQL_PASSWORD') ?: 'YourStrong!Passw0rd'; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Type/BigIntTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 37 | 38 | $tableSchema = $db->getTableSchema('bigint_default'); 39 | 40 | $this->assertSame('bigint', $tableSchema?->getColumn('Mybigint')->getDbType()); 41 | $this->assertSame('int', $tableSchema?->getColumn('Mybigint')->getPhpType()); 42 | $this->assertSame(9_223_372_036_854_775_807, $tableSchema?->getColumn('Mybigint')->getDefaultValue()); 43 | 44 | $db->createCommand()->dropTable('bigint_default')->execute(); 45 | } 46 | 47 | /** 48 | * @throws Exception 49 | * @throws InvalidConfigException 50 | * @throws InvalidArgumentException 51 | * @throws NotSupportedException 52 | * @throws Throwable 53 | */ 54 | public function testCreateTableWithInsert(): void 55 | { 56 | $db = $this->buildTable(); 57 | 58 | $command = $db->createCommand(); 59 | $command->insert('bigint_default', [])->execute(); 60 | 61 | $this->assertSame( 62 | $this->getColumns(), 63 | $command->setSql( 64 | <<queryOne(), 68 | ); 69 | 70 | $db->createCommand()->dropTable('bigint_default')->execute(); 71 | } 72 | 73 | /** 74 | * @throws Exception 75 | * @throws InvalidConfigException 76 | * @throws InvalidArgumentException 77 | * @throws NotSupportedException 78 | * @throws Throwable 79 | */ 80 | public function testDefaultValue(): void 81 | { 82 | $this->setFixture('Type/bigint.sql'); 83 | 84 | $db = $this->getConnection(true); 85 | $tableSchema = $db->getTableSchema('bigint_default'); 86 | 87 | $this->assertSame('bigint', $tableSchema?->getColumn('Mybigint')->getDbType()); 88 | $this->assertSame('int', $tableSchema?->getColumn('Mybigint')->getPhpType()); 89 | $this->assertSame(9_223_372_036_854_775_807, $tableSchema?->getColumn('Mybigint')->getDefaultValue()); 90 | 91 | $db->createCommand()->dropTable('bigint_default')->execute(); 92 | } 93 | 94 | /** 95 | * @throws Exception 96 | * @throws InvalidConfigException 97 | * @throws InvalidArgumentException 98 | * @throws NotSupportedException 99 | * @throws Throwable 100 | */ 101 | public function testDefaultValueWithInsert(): void 102 | { 103 | $this->setFixture('Type/bigint.sql'); 104 | 105 | $db = $this->getConnection(true); 106 | $command = $db->createCommand(); 107 | $command->insert('bigint_default', [])->execute(); 108 | 109 | $this->assertSame( 110 | $this->getColumns(), 111 | $command->setSql( 112 | <<queryOne(), 116 | ); 117 | 118 | $db->createCommand()->dropTable('bigint_default')->execute(); 119 | } 120 | 121 | /** 122 | * Max value is `9223372036854775807`, but when the value is greater than `9223372036854775807` it is out of range 123 | * and save as `9223372036854775807`. 124 | * 125 | * @throws Exception 126 | * @throws InvalidConfigException 127 | * @throws InvalidArgumentException 128 | * @throws NotSupportedException 129 | * @throws Throwable 130 | */ 131 | public function testMaxValue(): void 132 | { 133 | $this->setFixture('Type/bigint.sql'); 134 | 135 | $db = $this->getConnection(true); 136 | $command = $db->createCommand(); 137 | $command->insert('bigint', ['Mybigint1' => '9223372036854775807', 'Mybigint2' => '0'])->execute(); 138 | 139 | $this->assertSame( 140 | [ 141 | 'id' => '1', 142 | 'Mybigint1' => '9223372036854775807', 143 | 'Mybigint2' => '0', 144 | ], 145 | $command->setSql( 146 | <<queryOne() 150 | ); 151 | 152 | $command->insert('bigint', ['Mybigint1' => '9223372036854775808', 'Mybigint2' => null])->execute(); 153 | 154 | $this->assertSame( 155 | [ 156 | 'id' => '2', 157 | 'Mybigint1' => '9223372036854775807', 158 | 'Mybigint2' => null, 159 | ], 160 | $command->setSql( 161 | <<queryOne() 165 | ); 166 | 167 | $db->createCommand()->dropTable('bigint')->execute(); 168 | } 169 | 170 | /** 171 | * Min value is `-9223372036854775808`, but when the value is less than `-9223372036854775808` it is out of range 172 | * and save as `-9223372036854775808`. 173 | * 174 | * @throws Exception 175 | * @throws InvalidConfigException 176 | * @throws InvalidArgumentException 177 | * @throws NotSupportedException 178 | * @throws Throwable 179 | */ 180 | public function testMinValue(): void 181 | { 182 | $this->setFixture('Type/bigint.sql'); 183 | 184 | $db = $this->getConnection(true); 185 | $command = $db->createCommand(); 186 | $command->insert('bigint', ['Mybigint1' => '-9223372036854775808', 'Mybigint2' => '0'])->execute(); 187 | 188 | $this->assertSame( 189 | [ 190 | 'id' => '1', 191 | 'Mybigint1' => '-9223372036854775808', 192 | 'Mybigint2' => '0', 193 | ], 194 | $command->setSql( 195 | <<queryOne() 199 | ); 200 | 201 | $command->insert('bigint', ['Mybigint1' => '-9223372036854775809', 'Mybigint2' => null])->execute(); 202 | 203 | $this->assertSame( 204 | [ 205 | 'id' => '2', 206 | 'Mybigint1' => '-9223372036854775808', 207 | 'Mybigint2' => null, 208 | ], 209 | $command->setSql( 210 | <<queryOne() 214 | ); 215 | 216 | $db->createCommand()->dropTable('bigint')->execute(); 217 | } 218 | 219 | private function buildTable(): ConnectionInterface 220 | { 221 | $db = $this->getConnection(); 222 | 223 | $command = $db->createCommand(); 224 | 225 | if ($db->getSchema()->getTableSchema('bigint_default') !== null) { 226 | $command->dropTable('bigint_default')->execute(); 227 | } 228 | 229 | $command->createTable( 230 | 'bigint_default', 231 | [ 232 | 'id' => 'INT IDENTITY NOT NULL', 233 | 'Mybigint' => 'BIGINT DEFAULT 9223372036854775807', // Max value 234 | ], 235 | )->execute(); 236 | 237 | return $db; 238 | } 239 | 240 | private function getColumns(): array 241 | { 242 | return [ 243 | 'id' => '1', 244 | 'Mybigint' => '9223372036854775807', 245 | ]; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /tests/Type/BinaryTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 45 | 46 | $tableSchema = $db->getTableSchema('binary_default'); 47 | 48 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 49 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 50 | $this->assertSame($size, $tableSchema?->getColumn($column)->getSize()); 51 | $this->assertEquals($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 52 | 53 | $db->createCommand()->dropTable('binary_default')->execute(); 54 | } 55 | 56 | /** 57 | * @throws Exception 58 | * @throws InvalidConfigException 59 | * @throws InvalidArgumentException 60 | * @throws NotSupportedException 61 | * @throws Throwable 62 | */ 63 | public function testCreateTableWithInsert(): void 64 | { 65 | $db = $this->buildTable(); 66 | 67 | $command = $db->createCommand(); 68 | $command->insert('binary_default', [])->execute(); 69 | 70 | $this->assertSame( 71 | $this->getColumns(), 72 | $command->setSql( 73 | <<queryOne(), 77 | ); 78 | 79 | $db->createCommand()->dropTable('binary_default')->execute(); 80 | } 81 | 82 | /** 83 | * @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\Type\BinaryProvider::columns 84 | * 85 | * @throws Exception 86 | * @throws InvalidConfigException 87 | * @throws InvalidArgumentException 88 | * @throws NotSupportedException 89 | * @throws Throwable 90 | */ 91 | public function testDefaultValue( 92 | string $column, 93 | string $dbType, 94 | string $phpType, 95 | int $size, 96 | Expression $defaultValue 97 | ): void { 98 | $this->setFixture('Type/binary.sql'); 99 | 100 | $db = $this->getConnection(true); 101 | $tableSchema = $db->getTableSchema('binary_default'); 102 | 103 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 104 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 105 | $this->assertSame($size, $tableSchema?->getColumn($column)->getSize()); 106 | $this->assertEquals($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 107 | 108 | $db->createCommand()->dropTable('binary_default')->execute(); 109 | } 110 | 111 | /** 112 | * @throws Exception 113 | * @throws InvalidConfigException 114 | * @throws InvalidArgumentException 115 | * @throws NotSupportedException 116 | * @throws Throwable 117 | */ 118 | public function testDefaultValueWithInsert(): void 119 | { 120 | $this->setFixture('Type/binary.sql'); 121 | 122 | $db = $this->getConnection(true); 123 | $command = $db->createCommand(); 124 | $command->insert('binary_default', [])->execute(); 125 | 126 | $this->assertSame( 127 | $this->getColumns(), 128 | $command->setSql( 129 | <<queryOne(), 133 | ); 134 | 135 | $db->createCommand()->dropTable('binary_default')->execute(); 136 | } 137 | 138 | /** 139 | * When the value is greater than the maximum value, the value is truncated. 140 | * 141 | * @throws Exception 142 | * @throws InvalidConfigException 143 | * @throws InvalidArgumentException 144 | * @throws NotSupportedException 145 | * @throws Throwable 146 | */ 147 | public function testMaxValue(): void 148 | { 149 | $this->setFixture('Type/binary.sql'); 150 | 151 | $db = $this->getConnection(true); 152 | $command = $db->createCommand(); 153 | $command->insert('binary', [ 154 | 'Mybinary1' => new Expression('CONVERT(binary(10), \'binary_default_value\')'), 155 | 'Mybinary3' => new Expression('CONVERT(binary(1), \'bb\')'), 156 | ])->execute(); 157 | 158 | $this->assertSame( 159 | [ 160 | 'id' => '1', 161 | 'Mybinary1' => '0x62696E6172795F646566', 162 | 'Mybinary2' => null, 163 | 'Mybinary3' => 'b', 164 | 'Mybinary4' => null, 165 | ], 166 | $command->setSql( 167 | <<queryOne() 171 | ); 172 | 173 | $db->createCommand()->dropTable('binary')->execute(); 174 | } 175 | 176 | /** 177 | * @throws Exception 178 | * @throws InvalidConfigException 179 | * @throws InvalidArgumentException 180 | * @throws NotSupportedException 181 | * @throws Throwable 182 | */ 183 | public function testValue(): void 184 | { 185 | $this->setFixture('Type/binary.sql'); 186 | 187 | $db = $this->getConnection(true); 188 | $command = $db->createCommand(); 189 | $command->insert('binary', [ 190 | 'Mybinary1' => new Expression('CONVERT(binary(10), \'binary\')'), 191 | 'Mybinary2' => new Expression('CONVERT(binary(10), null)'), 192 | 'Mybinary3' => new Expression('CONVERT(binary(1), \'b\')'), 193 | 'Mybinary4' => new Expression('CONVERT(binary(1), null)'), 194 | ])->execute(); 195 | 196 | $this->assertSame( 197 | [ 198 | 'id' => '1', 199 | 'Mybinary1' => '0x62696E61727900000000', 200 | 'Mybinary2' => null, 201 | 'Mybinary3' => 'b', 202 | 'Mybinary4' => null, 203 | ], 204 | $command->setSql( 205 | <<queryOne() 209 | ); 210 | 211 | $db->createCommand()->dropTable('binary')->execute(); 212 | } 213 | 214 | private function buildTable(): ConnectionInterface 215 | { 216 | $db = $this->getConnection(); 217 | 218 | $command = $db->createCommand(); 219 | 220 | if ($db->getSchema()->getTableSchema('binary_default') !== null) { 221 | $command->dropTable('binary_default')->execute(); 222 | } 223 | 224 | $command->createTable( 225 | 'binary_default', 226 | [ 227 | 'id' => 'INT IDENTITY NOT NULL', 228 | 'Mybinary1' => 'BINARY(10) DEFAULT CONVERT(binary(10), \'binary\')', 229 | 'Mybinary2' => 'BINARY(1) DEFAULT CONVERT(binary(1), \'b\')', 230 | ], 231 | )->execute(); 232 | 233 | return $db; 234 | } 235 | 236 | private function getColumns(): array 237 | { 238 | return [ 239 | 'id' => '1', 240 | 'Mybinary1' => '0x62696E61727900000000', 241 | 'Mybinary2' => 'b', 242 | ]; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /tests/Type/CharTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 44 | 45 | $tableSchema = $db->getTableSchema('char_default'); 46 | 47 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 48 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 49 | $this->assertSame($size, $tableSchema?->getColumn($column)->getSize()); 50 | $this->assertSame($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 51 | 52 | $db->createCommand()->dropTable('char_default')->execute(); 53 | } 54 | 55 | /** 56 | * @throws Exception 57 | * @throws InvalidConfigException 58 | * @throws InvalidArgumentException 59 | * @throws NotSupportedException 60 | * @throws Throwable 61 | */ 62 | public function testCreateTableWithInsert(): void 63 | { 64 | $db = $this->buildTable(); 65 | 66 | $command = $db->createCommand(); 67 | $command->insert('char_default', [])->execute(); 68 | 69 | $this->assertSame( 70 | $this->getColumns(), 71 | $command->setSql( 72 | <<queryOne(), 76 | ); 77 | 78 | $db->createCommand()->dropTable('char_default')->execute(); 79 | } 80 | 81 | /** 82 | * @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\Type\CharProvider::columns 83 | * 84 | * @throws Exception 85 | * @throws InvalidConfigException 86 | * @throws InvalidArgumentException 87 | * @throws NotSupportedException 88 | * @throws Throwable 89 | */ 90 | public function testDefaultValue( 91 | string $column, 92 | string $dbType, 93 | string $phpType, 94 | int $size, 95 | string $defaultValue 96 | ): void { 97 | $this->setFixture('Type/char.sql'); 98 | 99 | $db = $this->getConnection(true); 100 | $tableSchema = $db->getTableSchema('char_default'); 101 | 102 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 103 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 104 | $this->assertSame($size, $tableSchema?->getColumn($column)->getSize()); 105 | $this->assertSame($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 106 | 107 | $db->createCommand()->dropTable('char_default')->execute(); 108 | } 109 | 110 | /** 111 | * @throws Exception 112 | * @throws InvalidConfigException 113 | * @throws InvalidArgumentException 114 | * @throws NotSupportedException 115 | * @throws Throwable 116 | */ 117 | public function testDefaultValueWithInsert(): void 118 | { 119 | $this->setFixture('Type/char.sql'); 120 | 121 | $db = $this->getConnection(true); 122 | $command = $db->createCommand(); 123 | $command->insert('char_default', [])->execute(); 124 | 125 | $this->assertSame( 126 | $this->getColumns(), 127 | $command->setSql( 128 | <<queryOne(), 132 | ); 133 | 134 | $db->createCommand()->dropTable('char_default')->execute(); 135 | } 136 | 137 | /** 138 | * @throws Exception 139 | * @throws InvalidConfigException 140 | * @throws InvalidArgumentException 141 | * @throws NotSupportedException 142 | * @throws Throwable 143 | */ 144 | public function testValue(): void 145 | { 146 | $this->setFixture('Type/char.sql'); 147 | 148 | $db = $this->getConnection(true); 149 | $command = $db->createCommand(); 150 | $command->insert( 151 | 'char', 152 | [ 153 | 'Mychar1' => '0123456789', 154 | 'Mychar2' => null, 155 | 'Mychar3' => 'b', 156 | 'Mychar4' => null, 157 | ], 158 | )->execute(); 159 | 160 | $this->assertSame( 161 | [ 162 | 'id' => '1', 163 | 'Mychar1' => '0123456789', 164 | 'Mychar2' => null, 165 | 'Mychar3' => 'b', 166 | 'Mychar4' => null, 167 | ], 168 | $command->setSql( 169 | <<queryOne() 173 | ); 174 | 175 | $db->createCommand()->dropTable('char')->execute(); 176 | } 177 | 178 | /** 179 | * @throws Exception 180 | * @throws InvalidConfigException 181 | * @throws InvalidArgumentException 182 | * @throws NotSupportedException 183 | * @throws Throwable 184 | */ 185 | public function testValueException(): void 186 | { 187 | $this->setFixture('Type/char.sql'); 188 | 189 | $db = $this->getConnection(true); 190 | $command = $db->createCommand(); 191 | 192 | $this->expectException(Exception::class); 193 | $this->expectExceptionMessage( 194 | '[SQL Server]String or binary data would be truncated' 195 | ); 196 | 197 | $command->insert('char', ['Mychar1' => '01234567891'])->execute(); 198 | } 199 | 200 | private function buildTable(): ConnectionInterface 201 | { 202 | $db = $this->getConnection(); 203 | 204 | $command = $db->createCommand(); 205 | 206 | if ($db->getSchema()->getTableSchema('char_default') !== null) { 207 | $command->dropTable('char_default')->execute(); 208 | } 209 | 210 | $command->createTable( 211 | 'char_default', 212 | [ 213 | 'id' => 'INT IDENTITY NOT NULL', 214 | 'Mychar1' => 'CHAR(10) DEFAULT \'char\'', // Max value 215 | 'Mychar2' => 'CHAR(1) DEFAULT \'c\'', // Max value 216 | ], 217 | )->execute(); 218 | 219 | return $db; 220 | } 221 | 222 | private function getColumns(): array 223 | { 224 | return [ 225 | 'id' => '1', 226 | 'Mychar1' => 'char ', 227 | 'Mychar2' => 'c', 228 | ]; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /tests/Type/DateTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 46 | 47 | $tableSchema = $db->getTableSchema('date_default'); 48 | 49 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 50 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 51 | $this->assertEquals($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 52 | 53 | $db->createCommand()->insert('date_default', [])->execute(); 54 | } 55 | 56 | /** 57 | * @throws Exception 58 | * @throws InvalidConfigException 59 | * @throws InvalidArgumentException 60 | * @throws NotSupportedException 61 | * @throws Throwable 62 | */ 63 | public function testCreateTableWithInsert(): void 64 | { 65 | $db = $this->buildTable(); 66 | 67 | $command = $db->createCommand(); 68 | $command->insert('date_default', [])->execute(); 69 | 70 | $this->assertSame( 71 | $this->getColumns(), 72 | $command->setSql( 73 | <<queryOne(), 77 | ); 78 | 79 | $db->createCommand()->dropTable('date_default')->execute(); 80 | } 81 | 82 | /** 83 | * @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\Type\DateProvider::columns 84 | * 85 | * @throws Exception 86 | * @throws InvalidConfigException 87 | * @throws InvalidArgumentException 88 | * @throws NotSupportedException 89 | * @throws Throwable 90 | */ 91 | public function testDefaultValue( 92 | string $column, 93 | string $dbType, 94 | string $phpType, 95 | DateTimeImmutable $defaultValue 96 | ): void { 97 | $this->setFixture('Type/date.sql'); 98 | 99 | $db = $this->getConnection(true); 100 | $tableSchema = $db->getTableSchema('date_default'); 101 | 102 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 103 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 104 | $this->assertEquals($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 105 | 106 | $db->createCommand()->dropTable('date_default')->execute(); 107 | } 108 | 109 | /** 110 | * @throws Exception 111 | * @throws InvalidConfigException 112 | * @throws InvalidArgumentException 113 | * @throws NotSupportedException 114 | * @throws Throwable 115 | */ 116 | public function testDefaultValueWithInsert(): void 117 | { 118 | $this->setFixture('Type/date.sql'); 119 | 120 | $db = $this->getConnection(true); 121 | $command = $db->createCommand(); 122 | $command->insert('date_default', [])->execute(); 123 | 124 | $this->assertSame( 125 | $this->getColumns(), 126 | $command->setSql( 127 | <<queryOne(), 131 | ); 132 | 133 | $db->createCommand()->dropTable('date_default')->execute(); 134 | } 135 | 136 | /** 137 | * @throws Exception 138 | * @throws InvalidConfigException 139 | * @throws InvalidArgumentException 140 | * @throws NotSupportedException 141 | * @throws Throwable 142 | */ 143 | public function testValue(): void 144 | { 145 | $this->setFixture('Type/date.sql'); 146 | 147 | $db = $this->getConnection(true); 148 | $command = $db->createCommand()->withDbTypecasting(false); 149 | $command->insert('date', [ 150 | 'Mydate1' => '2007-05-08', 151 | 'Mydate2' => null, 152 | 'Mydatetime1' => '2007-05-08 12:35:29.123', 153 | 'Mydatetime2' => null, 154 | 'Mydatetimeoffset1' => '2007-05-08 12:35:29.1234567 +12:15', 155 | 'Mydatetimeoffset2' => null, 156 | 'Mytime1' => '12:35:29.1234567', 157 | 'Mytime2' => null, 158 | ])->execute(); 159 | 160 | $this->assertSame( 161 | [ 162 | 'id' => '1', 163 | 'Mydate1' => '2007-05-08', 164 | 'Mydate2' => null, 165 | 'Mydatetime1' => '2007-05-08 12:35:29.123', 166 | 'Mydatetime2' => null, 167 | 'Mydatetimeoffset1' => '2007-05-08 12:35:29.1234567 +12:15', 168 | 'Mydatetimeoffset2' => null, 169 | 'Mytime1' => '12:35:29.1234567', 170 | 'Mytime2' => null, 171 | ], 172 | $command->setSql( 173 | <<queryOne() 177 | ); 178 | 179 | $db->createCommand()->dropTable('date')->execute(); 180 | } 181 | 182 | /** 183 | * @throws Exception 184 | * @throws InvalidConfigException 185 | * @throws InvalidArgumentException 186 | * @throws NotSupportedException 187 | * @throws Throwable 188 | */ 189 | public function testValueException(): void 190 | { 191 | $this->setFixture('Type/date.sql'); 192 | 193 | $db = $this->getConnection(true); 194 | 195 | $this->expectException(Exception::class); 196 | $this->expectExceptionMessage( 197 | '[SQL Server]Conversion failed when converting date and/or time from character string.' 198 | ); 199 | 200 | $db->createCommand()->insert('date', ['Mydate1' => '0000-00-00'])->execute(); 201 | } 202 | 203 | private function buildTable(): ConnectionInterface 204 | { 205 | $db = $this->getConnection(); 206 | 207 | $command = $db->createCommand(); 208 | 209 | if ($db->getSchema()->getTableSchema('date_default') !== null) { 210 | $command->dropTable('date_default')->execute(); 211 | } 212 | 213 | $command->createTable( 214 | 'date_default', 215 | [ 216 | 'id' => 'INT IDENTITY NOT NULL', 217 | 'Mydate' => 'DATE DEFAULT \'2007-05-08\'', 218 | 'Mydatetime' => 'DATETIME DEFAULT \'2007-05-08 12:35:29.123\'', 219 | 'Mydatetime2' => ColumnBuilder::datetime(7)->defaultValue(new Expression("'2007-05-08 12:35:29.1234567'")), 220 | 'Mydatetimeoffset' => ColumnBuilder::datetimeWithTimezone(7)->defaultValue(new Expression("'2007-05-08 12:35:29.1234567 +12:15'")), 221 | 'Mytime' => ColumnBuilder::time(7)->defaultValue(new Expression("'12:35:29.1234567'")), 222 | ], 223 | )->execute(); 224 | 225 | return $db; 226 | } 227 | 228 | private function getColumns(): array 229 | { 230 | return [ 231 | 'id' => '1', 232 | 'Mydate' => '2007-05-08', 233 | 'Mydatetime' => '2007-05-08 12:35:29.123', 234 | 'Mydatetime2' => '2007-05-08 12:35:29.1234567', 235 | 'Mydatetimeoffset' => '2007-05-08 12:35:29.1234567 +12:15', 236 | 'Mytime' => '12:35:29.1234567', 237 | ]; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /tests/Type/GeometryTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 44 | 45 | $tableSchema = $db->getTableSchema('geometry_default'); 46 | 47 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 48 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 49 | $this->assertEquals($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 50 | 51 | $db->createCommand()->dropTable('geometry_default')->execute(); 52 | } 53 | 54 | /** 55 | * @throws Exception 56 | * @throws InvalidConfigException 57 | * @throws InvalidArgumentException 58 | * @throws NotSupportedException 59 | * @throws Throwable 60 | */ 61 | public function testCreateTableWithInsert(): void 62 | { 63 | $db = $this->buildTable(); 64 | 65 | $command = $db->createCommand(); 66 | $command->insert('geometry_default', [])->execute(); 67 | 68 | $this->assertSame( 69 | $this->getColumns(), 70 | $command->setSql( 71 | <<queryOne(), 75 | ); 76 | 77 | $db->createCommand()->dropTable('geometry_default')->execute(); 78 | } 79 | 80 | /** 81 | * @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\Type\GeometryProvider::columns 82 | * 83 | * @throws Exception 84 | * @throws InvalidConfigException 85 | * @throws InvalidArgumentException 86 | * @throws NotSupportedException 87 | * @throws Throwable 88 | */ 89 | public function testDefaultValue( 90 | string $column, 91 | string $dbType, 92 | string $phpType, 93 | Expression|null $defaultValue 94 | ): void { 95 | $this->setFixture('Type/geometry.sql'); 96 | 97 | $db = $this->getConnection(true); 98 | $tableSchema = $db->getTableSchema('geometry_default'); 99 | 100 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 101 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 102 | $this->assertEquals($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 103 | 104 | $db->createCommand()->dropTable('geometry_default')->execute(); 105 | } 106 | 107 | /** 108 | * @throws Exception 109 | * @throws InvalidConfigException 110 | * @throws InvalidArgumentException 111 | * @throws NotSupportedException 112 | * @throws Throwable 113 | */ 114 | public function testDefaultValueWithInsert(): void 115 | { 116 | $this->setFixture('Type/geometry.sql'); 117 | 118 | $db = $this->getConnection(true); 119 | $command = $db->createCommand(); 120 | $command->insert('geometry_default', [])->execute(); 121 | 122 | $this->assertSame( 123 | $this->getColumns(), 124 | $command->setSql( 125 | <<queryOne(), 129 | ); 130 | 131 | $db->createCommand()->dropTable('geometry_default')->execute(); 132 | } 133 | 134 | /** 135 | * @throws Exception 136 | * @throws InvalidConfigException 137 | * @throws InvalidArgumentException 138 | * @throws NotSupportedException 139 | * @throws Throwable 140 | */ 141 | public function testValue(): void 142 | { 143 | $this->setFixture('Type/geometry.sql'); 144 | 145 | $db = $this->getConnection(true); 146 | $command = $db->createCommand(); 147 | $command->insert( 148 | 'geometry', 149 | [ 150 | 'Mygeometry1' => new Expression('geometry::STGeomFromText(\'LINESTRING(100 100,20 180,180 180)\', 0)'), 151 | ], 152 | )->execute(); 153 | 154 | $this->assertSame( 155 | [ 156 | 'id' => '1', 157 | 'Mygeometry1' => 'LINESTRING (100 100, 20 180, 180 180)', 158 | 'Mygeometry2' => 'LINESTRING (100 100, 20 180, 180 180)', 159 | ], 160 | $command->setSql( 161 | <<queryOne() 165 | ); 166 | 167 | $db->createCommand()->dropTable('geometry')->execute(); 168 | } 169 | 170 | private function buildTable(): ConnectionInterface 171 | { 172 | $db = $this->getConnection(); 173 | 174 | $command = $db->createCommand(); 175 | 176 | if ($db->getSchema()->getTableSchema('geometry_default') !== null) { 177 | $command->dropTable('geometry_default')->execute(); 178 | } 179 | 180 | $command->createTable( 181 | 'geometry_default', 182 | [ 183 | 'id' => 'INT IDENTITY NOT NULL', 184 | 'Mygeometry1' => 'GEOMETRY DEFAULT [geometry]::STGeomFromText(\'POINT(0 0)\',(0))', 185 | 'Mygeometry2' => 'GEOMETRY', 186 | ], 187 | )->execute(); 188 | 189 | return $db; 190 | } 191 | 192 | private function getColumns(): array 193 | { 194 | return [ 195 | 'id' => '1', 196 | 'Mygeometry1' => 'POINT (0 0)', 197 | 'Mygeometry2' => null, 198 | ]; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /tests/Type/ImageTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 38 | 39 | $tableSchema = $db->getTableSchema('image_default'); 40 | 41 | $this->assertSame('image', $tableSchema?->getColumn('Myimage')->getDbType()); 42 | $this->assertSame('mixed', $tableSchema?->getColumn('Myimage')->getPhpType()); 43 | $this->assertSame('image', $tableSchema?->getColumn('Myimage')->getDefaultValue()); 44 | 45 | $db->createCommand()->dropTable('image_default')->execute(); 46 | } 47 | 48 | /** 49 | * @throws Exception 50 | * @throws InvalidConfigException 51 | * @throws InvalidArgumentException 52 | * @throws NotSupportedException 53 | * @throws Throwable 54 | */ 55 | public function testCreateTableWithInsert(): void 56 | { 57 | $db = $this->buildTable(); 58 | 59 | $command = $db->createCommand(); 60 | $command->insert('image_default', [])->execute(); 61 | 62 | $this->assertSame( 63 | $this->getColumns(), 64 | $command->setSql( 65 | <<queryOne(), 69 | ); 70 | 71 | $db->createCommand()->dropTable('image_default')->execute(); 72 | } 73 | 74 | /** 75 | * @throws Exception 76 | * @throws InvalidConfigException 77 | * @throws InvalidArgumentException 78 | * @throws NotSupportedException 79 | * @throws Throwable 80 | */ 81 | public function testDefaultValue(): void 82 | { 83 | $this->setFixture('Type/image.sql'); 84 | 85 | $db = $this->getConnection(true); 86 | $tableSchema = $db->getTableSchema('image_default'); 87 | 88 | $this->assertSame('image', $tableSchema?->getColumn('Myimage')->getDbType()); 89 | $this->assertSame('mixed', $tableSchema?->getColumn('Myimage')->getPhpType()); 90 | $this->assertSame('image', $tableSchema?->getColumn('Myimage')->getDefaultValue()); 91 | 92 | $db->createCommand()->dropTable('image_default')->execute(); 93 | } 94 | 95 | /** 96 | * @throws Exception 97 | * @throws InvalidConfigException 98 | * @throws InvalidArgumentException 99 | * @throws NotSupportedException 100 | * @throws Throwable 101 | */ 102 | public function testDefaultValueWithInsert(): void 103 | { 104 | $this->setFixture('Type/image.sql'); 105 | 106 | $db = $this->getConnection(true); 107 | $command = $db->createCommand(); 108 | $command->insert('image_default', [])->execute(); 109 | 110 | $this->assertSame( 111 | $this->getColumns(), 112 | $command->setSql( 113 | <<queryOne(), 117 | ); 118 | 119 | $db->createCommand()->dropTable('image_default')->execute(); 120 | } 121 | 122 | /** 123 | * @throws Exception 124 | * @throws InvalidConfigException 125 | * @throws InvalidArgumentException 126 | * @throws NotSupportedException 127 | * @throws Throwable 128 | */ 129 | public function testValue(): void 130 | { 131 | $this->setFixture('Type/image.sql'); 132 | 133 | $db = $this->getConnection(true); 134 | $command = $db->createCommand(); 135 | $command->insert( 136 | 'image', 137 | ['Myimage1' => new Expression('CONVERT(image, 0x30313233343536373839)'), 'Myimage2' => null] 138 | )->execute(); 139 | 140 | $this->assertSame( 141 | [ 142 | 'id' => '1', 143 | 'Myimage1' => '0123456789', 144 | 'Myimage2' => null, 145 | ], 146 | $command->setSql( 147 | <<queryOne() 151 | ); 152 | 153 | $db->createCommand()->dropTable('image')->execute(); 154 | } 155 | 156 | private function buildTable(): ConnectionInterface 157 | { 158 | $db = $this->getConnection(); 159 | 160 | $command = $db->createCommand(); 161 | 162 | if ($db->getSchema()->getTableSchema('image_default') !== null) { 163 | $command->dropTable('image_default')->execute(); 164 | } 165 | 166 | $command->createTable( 167 | 'image_default', 168 | [ 169 | 'id' => 'INT IDENTITY NOT NULL', 170 | 'Myimage' => 'IMAGE DEFAULT \'image\'', 171 | ], 172 | )->execute(); 173 | 174 | return $db; 175 | } 176 | 177 | private function getColumns(): array 178 | { 179 | return [ 180 | 'id' => '1', 181 | 'Myimage' => 'image', 182 | ]; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /tests/Type/NTextTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 37 | 38 | $tableSchema = $db->getTableSchema('ntext_default'); 39 | 40 | $this->assertSame('ntext', $tableSchema?->getColumn('Myntext')->getDbType()); 41 | $this->assertSame('string', $tableSchema?->getColumn('Myntext')->getPhpType()); 42 | $this->assertSame('ntext', $tableSchema?->getColumn('Myntext')->getDefaultValue()); 43 | 44 | $db->createCommand()->dropTable('ntext_default')->execute(); 45 | } 46 | 47 | /** 48 | * @throws Exception 49 | * @throws InvalidConfigException 50 | * @throws InvalidArgumentException 51 | * @throws NotSupportedException 52 | * @throws Throwable 53 | */ 54 | public function testCreateTableWithInsert(): void 55 | { 56 | $db = $this->buildTable(); 57 | 58 | $command = $db->createCommand(); 59 | $command->insert('ntext_default', [])->execute(); 60 | 61 | $this->assertSame( 62 | $this->getColumns(), 63 | $command->setSql( 64 | <<queryOne(), 68 | ); 69 | 70 | $db->createCommand()->dropTable('ntext_default')->execute(); 71 | } 72 | 73 | /** 74 | * @throws Exception 75 | * @throws InvalidConfigException 76 | * @throws InvalidArgumentException 77 | * @throws NotSupportedException 78 | * @throws Throwable 79 | */ 80 | public function testDefaultValue(): void 81 | { 82 | $this->setFixture('Type/ntext.sql'); 83 | 84 | $db = $this->getConnection(true); 85 | $tableSchema = $db->getTableSchema('ntext_default'); 86 | 87 | $this->assertSame('ntext', $tableSchema?->getColumn('Myntext')->getDbType()); 88 | $this->assertSame('string', $tableSchema?->getColumn('Myntext')->getPhpType()); 89 | $this->assertSame('ntext', $tableSchema?->getColumn('Myntext')->getDefaultValue()); 90 | 91 | $db->createCommand()->dropTable('ntext_default')->execute(); 92 | } 93 | 94 | /** 95 | * @throws Exception 96 | * @throws InvalidConfigException 97 | * @throws InvalidArgumentException 98 | * @throws NotSupportedException 99 | * @throws Throwable 100 | */ 101 | public function testDefaultValueWithInsert(): void 102 | { 103 | $this->setFixture('Type/ntext.sql'); 104 | 105 | $db = $this->getConnection(true); 106 | $command = $db->createCommand(); 107 | $command->insert('ntext_default', [])->execute(); 108 | 109 | $this->assertSame( 110 | $this->getColumns(), 111 | $command->setSql( 112 | <<queryOne(), 116 | ); 117 | 118 | $db->createCommand()->dropTable('ntext_default')->execute(); 119 | } 120 | 121 | /** 122 | * @throws Exception 123 | * @throws InvalidConfigException 124 | * @throws InvalidArgumentException 125 | * @throws NotSupportedException 126 | * @throws Throwable 127 | */ 128 | public function testValue(): void 129 | { 130 | $this->setFixture('Type/ntext.sql'); 131 | 132 | $db = $this->getConnection(true); 133 | $command = $db->createCommand(); 134 | $command->insert('ntext', ['Myntext1' => '0123456789', 'Myntext2' => null])->execute(); 135 | 136 | $this->assertSame( 137 | [ 138 | 'id' => '1', 139 | 'Myntext1' => '0123456789', 140 | 'Myntext2' => null, 141 | ], 142 | $command->setSql( 143 | <<queryOne() 147 | ); 148 | 149 | $db->createCommand()->dropTable('ntext')->execute(); 150 | } 151 | 152 | private function buildTable(): ConnectionInterface 153 | { 154 | $db = $this->getConnection(); 155 | 156 | $command = $db->createCommand(); 157 | 158 | if ($db->getSchema()->getTableSchema('ntext_default') !== null) { 159 | $command->dropTable('ntext_default')->execute(); 160 | } 161 | 162 | $command->createTable( 163 | 'ntext_default', 164 | [ 165 | 'id' => 'INT IDENTITY NOT NULL', 166 | 'Myntext' => 'NTEXT DEFAULT \'ntext\'', 167 | ], 168 | )->execute(); 169 | 170 | return $db; 171 | } 172 | 173 | private function getColumns(): array 174 | { 175 | return [ 176 | 'id' => '1', 177 | 'Myntext' => 'ntext', 178 | ]; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/Type/RowversionTest.php: -------------------------------------------------------------------------------- 1 | setFixture('Type/rowversion.sql'); 36 | 37 | $db = $this->getConnection(true); 38 | $tableSchema = $db->getTableSchema('rowversion'); 39 | 40 | $this->assertSame('timestamp', $tableSchema?->getColumn('Myrowversion')->getDbType()); 41 | $this->assertSame('mixed', $tableSchema?->getColumn('Myrowversion')->getPhpType()); 42 | $this->assertNull($tableSchema?->getColumn('Myrowversion')->getDefaultValue()); 43 | 44 | $command = $db->createCommand(); 45 | $command->insert('rowversion', [])->execute(); 46 | 47 | $this->assertIsNumeric( 48 | $command->setSql( 49 | <<queryScalar() 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Type/TextTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 37 | 38 | $tableSchema = $db->getTableSchema('text_default'); 39 | 40 | $this->assertSame('text', $tableSchema?->getColumn('Mytext')->getDbType()); 41 | $this->assertSame('string', $tableSchema?->getColumn('Mytext')->getPhpType()); 42 | $this->assertSame('text', $tableSchema?->getColumn('Mytext')->getDefaultValue()); 43 | 44 | $db->createCommand()->dropTable('text_default')->execute(); 45 | } 46 | 47 | /** 48 | * @throws Exception 49 | * @throws InvalidConfigException 50 | * @throws InvalidArgumentException 51 | * @throws NotSupportedException 52 | * @throws Throwable 53 | */ 54 | public function testCreateTableWithInsert(): void 55 | { 56 | $db = $this->buildTable(); 57 | 58 | $command = $db->createCommand(); 59 | $command->insert('text_default', [])->execute(); 60 | 61 | $this->assertSame( 62 | $this->getColumns(), 63 | $command->setSql( 64 | <<queryOne(), 68 | ); 69 | 70 | $db->createCommand()->dropTable('text_default')->execute(); 71 | } 72 | 73 | /** 74 | * @throws Exception 75 | * @throws InvalidConfigException 76 | * @throws InvalidArgumentException 77 | * @throws NotSupportedException 78 | * @throws Throwable 79 | */ 80 | public function testDefaultValue(): void 81 | { 82 | $this->setFixture('Type/text.sql'); 83 | 84 | $db = $this->getConnection(true); 85 | $tableSchema = $db->getTableSchema('text_default'); 86 | 87 | $this->assertSame('text', $tableSchema?->getColumn('Mytext')->getDbType()); 88 | $this->assertSame('string', $tableSchema?->getColumn('Mytext')->getPhpType()); 89 | $this->assertSame('text', $tableSchema?->getColumn('Mytext')->getDefaultValue()); 90 | 91 | $db->createCommand()->dropTable('text_default')->execute(); 92 | } 93 | 94 | /** 95 | * @throws Exception 96 | * @throws InvalidConfigException 97 | * @throws InvalidArgumentException 98 | * @throws NotSupportedException 99 | * @throws Throwable 100 | */ 101 | public function testDefaultValueWithInsert(): void 102 | { 103 | $this->setFixture('Type/text.sql'); 104 | 105 | $db = $this->getConnection(true); 106 | $command = $db->createCommand(); 107 | $command->insert('text_default', [])->execute(); 108 | 109 | $this->assertSame( 110 | $this->getColumns(), 111 | $command->setSql( 112 | <<queryOne(), 116 | ); 117 | 118 | $db->createCommand()->dropTable('text_default')->execute(); 119 | } 120 | 121 | /** 122 | * @throws Exception 123 | * @throws InvalidConfigException 124 | * @throws InvalidArgumentException 125 | * @throws NotSupportedException 126 | * @throws Throwable 127 | */ 128 | public function testValue(): void 129 | { 130 | $this->setFixture('Type/text.sql'); 131 | 132 | $db = $this->getConnection(true); 133 | $command = $db->createCommand(); 134 | $command->insert('text', ['Mytext1' => '0123456789', 'Mytext2' => null])->execute(); 135 | 136 | $this->assertSame( 137 | [ 138 | 'id' => '1', 139 | 'Mytext1' => '0123456789', 140 | 'Mytext2' => null, 141 | ], 142 | $command->setSql( 143 | <<queryOne() 147 | ); 148 | 149 | $db->createCommand()->dropTable('text')->execute(); 150 | } 151 | 152 | private function buildTable(): ConnectionInterface 153 | { 154 | $db = $this->getConnection(); 155 | 156 | $command = $db->createCommand(); 157 | 158 | if ($db->getSchema()->getTableSchema('text_default') !== null) { 159 | $command->dropTable('text_default')->execute(); 160 | } 161 | 162 | $command->createTable( 163 | 'text_default', 164 | [ 165 | 'id' => 'INT IDENTITY NOT NULL', 166 | 'Mytext' => 'TEXT DEFAULT \'text\'', 167 | ], 168 | )->execute(); 169 | 170 | return $db; 171 | } 172 | 173 | private function getColumns(): array 174 | { 175 | return [ 176 | 'id' => '1', 177 | 'Mytext' => 'text', 178 | ]; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/Type/UniqueidentifierTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 37 | 38 | $tableSchema = $db->getTableSchema('uniqueidentifier_default'); 39 | 40 | $this->assertSame('uniqueidentifier', $tableSchema?->getColumn('Myuniqueidentifier')->getDbType()); 41 | $this->assertSame('string', $tableSchema?->getColumn('Myuniqueidentifier')->getPhpType()); 42 | $this->assertSame( 43 | '12345678-1234-1234-1234-123456789012', 44 | $tableSchema?->getColumn('Myuniqueidentifier')->getDefaultValue(), 45 | ); 46 | 47 | $db->createCommand()->dropTable('uniqueidentifier_default')->execute(); 48 | } 49 | 50 | /** 51 | * @throws Exception 52 | * @throws InvalidConfigException 53 | * @throws InvalidArgumentException 54 | * @throws NotSupportedException 55 | * @throws Throwable 56 | */ 57 | public function testCreateTableWithInsert(): void 58 | { 59 | $db = $this->buildTable(); 60 | 61 | $command = $db->createCommand(); 62 | $command->insert('uniqueidentifier_default', [])->execute(); 63 | 64 | $this->assertSame( 65 | $this->getColumns(), 66 | $command->setSql( 67 | <<queryOne(), 71 | ); 72 | 73 | $db->createCommand()->dropTable('uniqueidentifier_default')->execute(); 74 | } 75 | 76 | /** 77 | * @throws Exception 78 | * @throws InvalidConfigException 79 | * @throws InvalidArgumentException 80 | * @throws NotSupportedException 81 | * @throws Throwable 82 | */ 83 | public function testDefaultValue(): void 84 | { 85 | $this->setFixture('Type/uniqueidentifier.sql'); 86 | 87 | $db = $this->getConnection(true); 88 | $tableSchema = $db->getTableSchema('uniqueidentifier_default'); 89 | 90 | $this->assertSame('uniqueidentifier', $tableSchema?->getColumn('Myuniqueidentifier')->getDbType()); 91 | $this->assertSame('string', $tableSchema?->getColumn('Myuniqueidentifier')->getPhpType()); 92 | $this->assertSame( 93 | '12345678-1234-1234-1234-123456789012', 94 | $tableSchema?->getColumn('Myuniqueidentifier')->getDefaultValue(), 95 | ); 96 | 97 | $db->createCommand()->dropTable('uniqueidentifier_default')->execute(); 98 | } 99 | 100 | /** 101 | * @throws Exception 102 | * @throws InvalidConfigException 103 | * @throws InvalidArgumentException 104 | * @throws NotSupportedException 105 | * @throws Throwable 106 | */ 107 | public function testDefaultValueWithInsert(): void 108 | { 109 | $this->setFixture('Type/uniqueidentifier.sql'); 110 | 111 | $db = $this->getConnection(true); 112 | $command = $db->createCommand(); 113 | $command->insert('uniqueidentifier_default', [])->execute(); 114 | 115 | $this->assertSame( 116 | $this->getColumns(), 117 | $command->setSql( 118 | <<queryOne(), 122 | ); 123 | 124 | $db->createCommand()->dropTable('uniqueidentifier_default')->execute(); 125 | } 126 | 127 | /** 128 | * Max value is 36 characters. 129 | * 130 | * @throws Exception 131 | * @throws InvalidConfigException 132 | * @throws InvalidArgumentException 133 | * @throws NotSupportedException 134 | * @throws Throwable 135 | */ 136 | public function testValue(): void 137 | { 138 | $this->setFixture('Type/uniqueidentifier.sql'); 139 | 140 | $db = $this->getConnection(true); 141 | $command = $db->createCommand(); 142 | $command->insert( 143 | 'uniqueidentifier', 144 | ['Myuniqueidentifier1' => '12345678-1234-1234-1234-123456789012', 'Myuniqueidentifier2' => null], 145 | )->execute(); 146 | 147 | $this->assertSame( 148 | [ 149 | 'id' => '1', 150 | 'Myuniqueidentifier1' => '12345678-1234-1234-1234-123456789012', 151 | 'Myuniqueidentifier2' => null, 152 | ], 153 | $command->setSql( 154 | <<queryOne() 158 | ); 159 | 160 | $db->createCommand()->dropTable('uniqueidentifier')->execute(); 161 | } 162 | 163 | /** 164 | * @throws Exception 165 | * @throws InvalidConfigException 166 | * @throws InvalidArgumentException 167 | * @throws NotSupportedException 168 | * @throws Throwable 169 | */ 170 | public function testValueException(): void 171 | { 172 | $this->setFixture('Type/uniqueidentifier.sql'); 173 | 174 | $db = $this->getConnection(true); 175 | 176 | $this->expectException(Exception::class); 177 | $this->expectExceptionMessage( 178 | '[SQL Server]Conversion failed when converting from a character string to uniqueidentifier.' 179 | ); 180 | 181 | $command = $db->createCommand(); 182 | $command->insert('uniqueidentifier', ['Myuniqueidentifier1' => '1'])->execute(); 183 | } 184 | 185 | /** 186 | * When you insert a value that is longer than 36 characters, the value is truncated to 36 characters. 187 | * 188 | * @throws Exception 189 | * @throws InvalidConfigException 190 | * @throws InvalidArgumentException 191 | * @throws NotSupportedException 192 | * @throws Throwable 193 | */ 194 | public function testValueLength(): void 195 | { 196 | $this->setFixture('Type/uniqueidentifier.sql'); 197 | 198 | $db = $this->getConnection(true); 199 | $command = $db->createCommand(); 200 | $command->insert( 201 | 'uniqueidentifier', 202 | ['Myuniqueidentifier1' => '12345678-1234-1234-1234-1234567890123', 'Myuniqueidentifier2' => null], 203 | )->execute(); 204 | 205 | $this->assertSame( 206 | [ 207 | 'id' => '1', 208 | 'Myuniqueidentifier1' => '12345678-1234-1234-1234-123456789012', 209 | 'Myuniqueidentifier2' => null, 210 | ], 211 | $command->setSql( 212 | <<queryOne() 216 | ); 217 | 218 | $db->createCommand()->dropTable('uniqueidentifier')->execute(); 219 | } 220 | 221 | private function buildTable(): ConnectionInterface 222 | { 223 | $db = $this->getConnection(); 224 | 225 | $command = $db->createCommand(); 226 | 227 | if ($db->getSchema()->getTableSchema('uniqueidentifier_default') !== null) { 228 | $command->dropTable('uniqueidentifier_default')->execute(); 229 | } 230 | 231 | $command->createTable( 232 | 'uniqueidentifier_default', 233 | [ 234 | 'id' => 'INT IDENTITY NOT NULL', 235 | 'Myuniqueidentifier' => 'UNIQUEIDENTIFIER DEFAULT \'12345678-1234-1234-1234-123456789012\'', 236 | ], 237 | )->execute(); 238 | 239 | return $db; 240 | } 241 | 242 | private function getColumns(): array 243 | { 244 | return [ 245 | 'id' => '1', 246 | 'Myuniqueidentifier' => '12345678-1234-1234-1234-123456789012', 247 | ]; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /tests/Type/VarCharTest.php: -------------------------------------------------------------------------------- 1 | buildTable(); 45 | 46 | $tableSchema = $db->getTableSchema('varchar_default'); 47 | 48 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 49 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 50 | $this->assertSame($size, $tableSchema?->getColumn($column)->getSize()); 51 | $this->assertEquals($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 52 | 53 | $db->createCommand()->dropTable('varchar_default')->execute(); 54 | } 55 | 56 | /** 57 | * @throws Exception 58 | * @throws InvalidConfigException 59 | * @throws InvalidArgumentException 60 | * @throws NotSupportedException 61 | * @throws Throwable 62 | */ 63 | public function testCreateTableWithInsert(): void 64 | { 65 | $db = $this->buildTable(); 66 | 67 | $command = $db->createCommand(); 68 | $command->insert('varchar_default', [])->execute(); 69 | 70 | $this->assertSame( 71 | $this->getColumns(), 72 | $command->setSql( 73 | <<queryOne(), 77 | ); 78 | 79 | $db->createCommand()->dropTable('varchar_default')->execute(); 80 | } 81 | 82 | /** 83 | * @dataProvider \Yiisoft\Db\Mssql\Tests\Provider\Type\VarCharProvider::columns 84 | * 85 | * @throws Exception 86 | * @throws InvalidConfigException 87 | * @throws InvalidArgumentException 88 | * @throws NotSupportedException 89 | * @throws Throwable 90 | */ 91 | public function testDefaultValue( 92 | string $column, 93 | string $dbType, 94 | string $phpType, 95 | int $size, 96 | string|Expression $defaultValue 97 | ): void { 98 | $this->setFixture('Type/varchar.sql'); 99 | 100 | $db = $this->getConnection(true); 101 | $tableSchema = $db->getTableSchema('varchar_default'); 102 | 103 | $this->assertSame($dbType, $tableSchema?->getColumn($column)->getDbType()); 104 | $this->assertSame($phpType, $tableSchema?->getColumn($column)->getPhpType()); 105 | $this->assertSame($size, $tableSchema?->getColumn($column)->getSize()); 106 | $this->assertEquals($defaultValue, $tableSchema?->getColumn($column)->getDefaultValue()); 107 | 108 | $db->createCommand()->dropTable('varchar_default')->execute(); 109 | } 110 | 111 | /** 112 | * @throws Exception 113 | * @throws InvalidConfigException 114 | * @throws InvalidArgumentException 115 | * @throws NotSupportedException 116 | * @throws Throwable 117 | */ 118 | public function testDefaultValueWithInsert(): void 119 | { 120 | $this->setFixture('Type/varchar.sql'); 121 | 122 | $db = $this->getConnection(true); 123 | $command = $db->createCommand(); 124 | $command->insert('varchar_default', [])->execute(); 125 | 126 | $this->assertSame( 127 | $this->getColumns(), 128 | $command->setSql( 129 | <<queryOne(), 133 | ); 134 | 135 | $db->createCommand()->dropTable('varchar_default')->execute(); 136 | } 137 | 138 | /** 139 | * @throws Exception 140 | * @throws InvalidConfigException 141 | * @throws InvalidArgumentException 142 | * @throws NotSupportedException 143 | * @throws Throwable 144 | */ 145 | public function testValue(): void 146 | { 147 | $this->setFixture('Type/varchar.sql'); 148 | 149 | $db = $this->getConnection(true); 150 | $command = $db->createCommand(); 151 | $command->insert( 152 | 'varchar', 153 | [ 154 | 'Myvarchar1' => '0123456789', 155 | 'Myvarchar2' => null, 156 | 'Myvarchar3' => str_repeat('b', 100), 157 | 'Myvarchar4' => null, 158 | ], 159 | )->execute(); 160 | 161 | $this->assertSame( 162 | [ 163 | 'id' => '1', 164 | 'Myvarchar1' => '0123456789', 165 | 'Myvarchar2' => null, 166 | 'Myvarchar3' => str_repeat('b', 100), 167 | 'Myvarchar4' => null, 168 | ], 169 | $command->setSql( 170 | <<queryOne() 174 | ); 175 | 176 | $db->createCommand()->dropTable('varchar')->execute(); 177 | } 178 | 179 | /** 180 | * @throws Exception 181 | * @throws InvalidConfigException 182 | * @throws InvalidArgumentException 183 | * @throws NotSupportedException 184 | * @throws Throwable 185 | */ 186 | public function testValueException(): void 187 | { 188 | $this->setFixture('Type/varchar.sql'); 189 | 190 | $db = $this->getConnection(true); 191 | $command = $db->createCommand(); 192 | 193 | $this->expectException(Exception::class); 194 | $this->expectExceptionMessage( 195 | '[SQL Server]String or binary data would be truncated' 196 | ); 197 | 198 | $command->insert('varchar', ['Myvarchar1' => '01234567891'])->execute(); 199 | } 200 | 201 | private function buildTable(): ConnectionInterface 202 | { 203 | $db = $this->getConnection(); 204 | 205 | $command = $db->createCommand(); 206 | 207 | if ($db->getSchema()->getTableSchema('varchar_default') !== null) { 208 | $command->dropTable('varchar_default')->execute(); 209 | } 210 | 211 | $command->createTable( 212 | 'varchar_default', 213 | [ 214 | 'id' => 'INT IDENTITY NOT NULL', 215 | 'Myvarchar1' => 'VARCHAR(10) DEFAULT \'varchar\'', 216 | 'Myvarchar2' => 'VARCHAR(100) DEFAULT \'v\'', 217 | 'Myvarchar3' => 'VARCHAR(20) DEFAULT TRY_CONVERT(varchar(20), year(getdate()))', 218 | ], 219 | )->execute(); 220 | 221 | return $db; 222 | } 223 | 224 | private function getColumns(): array 225 | { 226 | return [ 227 | 'id' => '1', 228 | 'Myvarchar1' => 'varchar', 229 | 'Myvarchar2' => 'v', 230 | 'Myvarchar3' => date('Y'), 231 | ]; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | load(); 8 | } 9 | --------------------------------------------------------------------------------