Skip to content

Commit 79cedf4

Browse files
committed
no issue - fix SQL Server
1 parent 35f0add commit 79cedf4

File tree

10 files changed

+132
-31
lines changed

10 files changed

+132
-31
lines changed

dev.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ do_test() {
223223
if [[ -n $@ ]];then shift;fi
224224

225225
case $suit in
226-
mysql57|mysql80|mariadb11|mysql|postgresql10|postgresql16|postgresql|sqlsrv2019|sqlsrv|sqlite) do_composer_install && do_test_$suit "$@";;
226+
mysql57|mysql80|mariadb11|mysql|postgresql10|postgresql16|postgresql|sqlsrv2019|sqlsrv|sqlsrv2019|sqlsrv2022|sqlite) do_composer_install && do_test_$suit "$@";;
227227
*) do_test_notice;;
228228
esac
229229
}

src/Bridge/AbstractBridge.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use MakinaCorpus\QueryBuilder\Platform\Transaction\MySQLTransaction;
2424
use MakinaCorpus\QueryBuilder\Platform\Transaction\PostgreSQLTransaction;
2525
use MakinaCorpus\QueryBuilder\Platform\Transaction\SQLiteTransaction;
26+
use MakinaCorpus\QueryBuilder\Platform\Transaction\SQLServerTransaction;
2627
use MakinaCorpus\QueryBuilder\Platform\Writer\MariaDBWriter;
2728
use MakinaCorpus\QueryBuilder\Platform\Writer\MySQL8Writer;
2829
use MakinaCorpus\QueryBuilder\Platform\Writer\MySQLWriter;
@@ -294,7 +295,8 @@ protected function doCreateTransaction(int $isolationLevel = Transaction::REPEAT
294295
Platform::MYSQL => new MySQLTransaction($this, $isolationLevel),
295296
Platform::POSTGRESQL => new PostgreSQLTransaction($this, $isolationLevel),
296297
Platform::SQLITE => new SQLiteTransaction($this, $isolationLevel),
297-
default => new QueryBuilderError(\sprintf("Transactions are not supported yet for vendor '%s'", $this->getServerFlavor())),
298+
Platform::SQLSERVER => new SQLServerTransaction($this, $isolationLevel),
299+
default => throw new QueryBuilderError(\sprintf("Transactions are not supported yet for vendor '%s'", $this->getServerFlavor())),
298300
};
299301
}
300302

src/Converter/InputConverter/DateInputConverter.php

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public function supportedInputTypes(): array
5050
'date',
5151
'datetime',
5252
'datetimez',
53+
'datetime2',
5354
'time with time zone',
5455
'time',
5556
'timestamp with time zone',
@@ -97,6 +98,7 @@ public function toSql(string $type, mixed $value, ConverterContext $context): nu
9798

9899
case 'datetime':
99100
case 'datetimez':
101+
case 'datetime2':
100102
case 'timestamp with time zone':
101103
case 'timestamp':
102104
case 'timestampz':

src/Converter/OutputConverter/DateOutputConverter.php

+6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ public function fromSql(string $type, int|float|string $value, ConverterContext
3838
if (\preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $value)) {
3939
$userTimeZone = new \DateTimeZone($context->getClientTimeZone());
4040

41+
// SQL Server has a digit more for precision than the others.
42+
// let's just strip ip.
43+
if (\preg_match('/\.\d{7}$/', $value)) {
44+
$value = \substr($value, 0, -1);
45+
}
46+
4147
// Attempt all possible outcomes.
4248
if ($ret = \DateTimeImmutable::createFromFormat(DateInputConverter::FORMAT_DATETIME_USEC_TZ, $value)) {
4349
// Time zone is within the date, as an offset. Convert the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\QueryBuilder\Platform\Transaction;
6+
7+
use MakinaCorpus\QueryBuilder\Expression\Raw;
8+
9+
class SQLServerTransaction extends AbstractTransaction
10+
{
11+
#[\Override]
12+
protected function doTransactionStart(int $isolationLevel): void
13+
{
14+
$this->doChangeLevel($isolationLevel);
15+
$this->executor->executeStatement("BEGIN TRANSACTION");
16+
}
17+
18+
#[\Override]
19+
protected function doChangeLevel(int $isolationLevel): void
20+
{
21+
$this->executor->executeStatement("SET TRANSACTION ISOLATION LEVEL ?", new Raw(self::getIsolationLevelString($isolationLevel)));
22+
}
23+
24+
#[\Override]
25+
protected function doCreateSavepoint(string $name): void
26+
{
27+
$this->executor->executeStatement("SAVE TRANSACTION ?::id", $name);
28+
}
29+
30+
#[\Override]
31+
protected function doRollbackToSavepoint(string $name): void
32+
{
33+
$this->executor->executeStatement("ROLLBACK TRANSACTION ?::id", $name);
34+
}
35+
36+
#[\Override]
37+
protected function doRollback(): void
38+
{
39+
$this->executor->executeStatement("ROLLBACK TRANSACTION");
40+
}
41+
42+
#[\Override]
43+
protected function doCommit(): void
44+
{
45+
$this->executor->executeStatement("COMMIT TRANSACTION");
46+
}
47+
48+
#[\Override]
49+
protected function doDeferConstraints(array $constraints): void
50+
{
51+
@\trigger_error(\sprintf("SQL Server does not support deferred constraints during transaction '%s'", $this->getName()), E_USER_NOTICE);
52+
}
53+
54+
#[\Override]
55+
protected function doDeferAll(): void
56+
{
57+
@\trigger_error(\sprintf("SQL Server does not support deferred constraints during transaction '%s'", $this->getName()), E_USER_NOTICE);
58+
}
59+
60+
#[\Override]
61+
protected function doImmediateConstraints(array $constraints): void
62+
{
63+
@\trigger_error(\sprintf("SQL Server does not support deferred constraints during transaction '%s'", $this->getName()), E_USER_NOTICE);
64+
}
65+
66+
#[\Override]
67+
protected function doImmediateAll(): void
68+
{
69+
@\trigger_error(\sprintf("SQL Server does not support deferred constraints during transaction '%s'", $this->getName()), E_USER_NOTICE);
70+
}
71+
}

src/Platform/Writer/SQLServerWriter.php

+33-17
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55
namespace MakinaCorpus\QueryBuilder\Platform\Writer;
66

77
use MakinaCorpus\QueryBuilder\Error\UnsupportedFeatureError;
8+
use MakinaCorpus\QueryBuilder\Expression;
89
use MakinaCorpus\QueryBuilder\Expression\Aggregate;
910
use MakinaCorpus\QueryBuilder\Expression\Cast;
1011
use MakinaCorpus\QueryBuilder\Expression\Concat;
1112
use MakinaCorpus\QueryBuilder\Expression\CurrentTimestamp;
1213
use MakinaCorpus\QueryBuilder\Expression\DateAdd;
1314
use MakinaCorpus\QueryBuilder\Expression\DateInterval;
15+
use MakinaCorpus\QueryBuilder\Expression\DateIntervalUnit;
1416
use MakinaCorpus\QueryBuilder\Expression\DateSub;
1517
use MakinaCorpus\QueryBuilder\Expression\Lpad;
1618
use MakinaCorpus\QueryBuilder\Expression\Random;
1719
use MakinaCorpus\QueryBuilder\Expression\StringHash;
1820
use MakinaCorpus\QueryBuilder\Expression\TableName;
19-
use MakinaCorpus\QueryBuilder\Expression\Value;
2021
use MakinaCorpus\QueryBuilder\Writer\Writer;
2122
use MakinaCorpus\QueryBuilder\Writer\WriterContext;
2223

@@ -36,6 +37,16 @@ protected function shouldEscapeAggregateFunctionName(): bool
3637
return false;
3738
}
3839

40+
/**
41+
* This is nasty, but we don't what the user will want, just cast dates
42+
* to the maximum extent possible.
43+
*/
44+
#[\Override]
45+
protected function getDateTimeCastType(): string
46+
{
47+
return 'datetime2';
48+
}
49+
3950
#[\Override]
4051
protected function formatCurrentTimestamp(CurrentTimestamp $expression, WriterContext $context): string
4152
{
@@ -109,22 +120,33 @@ protected function formatStringHash(StringHash $expression, WriterContext $conte
109120
return 'lower(convert(nvarchar(32), hashbytes(' . $escapedAlgo . ', ' . $this->format($value, $context) . '), 2))';
110121
}
111122

123+
protected function formatDateAddRecursion(Expression $date, array $values, WriterContext $context, bool $negate = false): string
124+
{
125+
if (empty($values)) {
126+
return $this->format($this->toDate($date, $context), $context);
127+
}
128+
129+
$unit = \array_shift($values);
130+
\assert($unit instanceof DateIntervalUnit);
131+
132+
$delta = $this->format($this->toInt($unit->getValue(), $context), $context);
133+
if ($negate) {
134+
$delta = '0 - ' . $delta;
135+
}
136+
137+
return 'dateadd(' . $unit->getUnit() . ', ' . $delta . ', ' . $this->formatDateAddRecursion($date, $values, $context, $negate) . ')';
138+
}
139+
112140
#[\Override]
113141
protected function formatDateAdd(DateAdd $expression, WriterContext $context): string
114142
{
115143
$interval = $expression->getInterval();
116144

117145
if ($interval instanceof DateInterval) {
118-
$ret = $this->format($expression->getDate(), $context);
119-
120-
foreach ($interval->getValues() as $unit) {
121-
$ret = 'dateadd(' . $unit->getUnit() . ', ' . $this->format($unit->getValue(), $context) . ', ' . $ret . ')';
122-
}
123-
} else {
124-
throw new UnsupportedFeatureError("SQLServer does not support DATEADD(expr,expr).");
146+
return $this->formatDateAddRecursion($expression->getDate(), $interval->getValues(), $context, false);
125147
}
126148

127-
return $ret;
149+
throw new UnsupportedFeatureError("SQLServer does not support DATEADD(expr,expr).");
128150
}
129151

130152
#[\Override]
@@ -133,16 +155,10 @@ protected function formatDateSub(DateSub $expression, WriterContext $context): s
133155
$interval = $expression->getInterval();
134156

135157
if ($interval instanceof DateInterval) {
136-
$ret = $this->format($expression->getDate(), $context);
137-
138-
foreach ($interval->getValues() as $unit) {
139-
$ret = 'dateadd(' . $unit->getUnit() . ', 0 - ' . $this->format($unit->getValue(), $context) . ', ' . $ret . ')';
140-
}
141-
} else {
142-
throw new UnsupportedFeatureError("SQLServer does not support DATEADD(expr,expr).");
158+
return $this->formatDateAddRecursion($expression->getDate(), $interval->getValues(), $context, true);
143159
}
144160

145-
return $ret;
161+
throw new UnsupportedFeatureError("SQLServer does not support DATEADD(expr,expr).");
146162
}
147163

148164
#[\Override]

src/Testing/FunctionalTestCaseTrait.php

+1
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ private function getConnectionParameters(): array
245245
if (\str_contains($driver, 'sqlsrv')) {
246246
// https://stackoverflow.com/questions/71688125/odbc-driver-18-for-sql-serverssl-provider-error1416f086
247247
$driverOptions['TrustServerCertificate'] = "true";
248+
$driverOptions['MultipleActiveResultSets'] = "false";
248249
}
249250

250251
return \array_filter([

tests/Bridge/AbstractErrorConverterTestCase.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ protected function createSchema(): void
7979
data nvarchar(500) DEFAULT NULL,
8080
CONSTRAINT bar_foo_id_fk FOREIGN KEY (foo_id)
8181
REFERENCES bar (id)
82-
ON DELETE CASCADE
82+
ON DELETE NO ACTION
8383
)
8484
SQL
8585
);

tests/Functional/TransactionFunctionalTest.php

+11-11
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ protected function createSchema(): void
5959
$bridge->executeStatement(
6060
<<<SQL
6161
CREATE TABLE transaction_test (
62-
id serial PRIMARY KEY,
62+
id int IDENTITY(1,1) PRIMARY KEY,
6363
foo integer NOT NULL,
6464
bar nvarchar(255) DEFAULT NULL
6565
)
@@ -70,15 +70,13 @@ protected function createSchema(): void
7070
alter table transaction_test
7171
add constraint transaction_test_foo
7272
unique (foo)
73-
deferrable
7473
SQL
7574
);
7675
$bridge->executeStatement(
7776
<<<SQL
7877
alter table transaction_test
7978
add constraint transaction_test_bar
8079
unique (bar)
81-
deferrable
8280
SQL
8381
);
8482
break;
@@ -158,8 +156,8 @@ public function testTransaction()
158156
->executeQuery()
159157
;
160158

161-
// @todo Row count doesn't work with SQLite
162-
if ($this->ifDatabaseNot(Platform::SQLITE)) {
159+
// @todo Row count doesn't work with SQLite and SQLServer
160+
if ($this->ifDatabaseNot(Platform::SQLITE) && $this->ifDatabaseNot(Platform::SQLSERVER)) {
163161
self::assertSame(4, $result->rowCount());
164162
}
165163
self::assertSame('a', $result->fetchRow()->get('bar'));
@@ -208,8 +206,8 @@ public function testNestedTransactionCreatesSavepoint()
208206
->executeQuery()
209207
;
210208

211-
// @todo Row count doesn't work with SQLite
212-
if ($this->ifDatabaseNot(Platform::SQLITE)) {
209+
// @todo Row count doesn't work with SQLite and SQLServer
210+
if ($this->ifDatabaseNot(Platform::SQLITE) && $this->ifDatabaseNot(Platform::SQLSERVER)) {
213211
self::assertSame(2, $result->rowCount());
214212
}
215213
self::assertSame('g', $result->fetchRow()->get('bar'));
@@ -257,8 +255,8 @@ public function testNestedTransactionRollbackToSavepointTransparently()
257255
->executeQuery()
258256
;
259257

260-
// @todo Row count doesn't work with SQLite
261-
if ($this->ifDatabaseNot(Platform::SQLITE)) {
258+
// @todo Row count doesn't work with SQLite and SQLServer
259+
if ($this->ifDatabaseNot(Platform::SQLITE) && $this->ifDatabaseNot(Platform::SQLSERVER)) {
262260
self::assertSame(1, $result->rowCount());
263261
}
264262
self::assertSame('f', $result->fetchRow()->get('bar'));
@@ -271,6 +269,7 @@ public function testImmediateTransactionFail()
271269
{
272270
// @todo Support IMMEDIATE in the BEGIN statement for SQLite.
273271
self::skipIfDatabase(Platform::SQLITE);
272+
self::skipIfDatabase(Platform::SQLSERVER, 'SQL Server can not deffer constraints');
274273

275274
self::expectNotToPerformAssertions();
276275

@@ -319,6 +318,7 @@ public function testDeferredTransactionFail()
319318
{
320319
// @todo Support IMMEDIATE in the BEGIN statement for SQLite.
321320
self::skipIfDatabase(Platform::SQLITE);
321+
self::skipIfDatabase(Platform::SQLSERVER, 'SQL Server can not deffer constraints');
322322

323323
self::expectNotToPerformAssertions();
324324

@@ -431,8 +431,8 @@ public function testTransactionRollback()
431431
->executeQuery()
432432
;
433433

434-
// @todo Row count doesn't work with SQLite
435-
if ($this->ifDatabaseNot(Platform::SQLITE)) {
434+
// @todo Row count doesn't work with SQLite and SQLServer
435+
if ($this->ifDatabaseNot(Platform::SQLITE) && $this->ifDatabaseNot(Platform::SQLSERVER)) {
436436
self::assertSame(3, $result->rowCount());
437437
} else {
438438
$count = 0;

tests/Platform/Schema/AbstractSchemaTestCase.php

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ abstract class AbstractSchemaTestCase extends FunctionalTestCase
1818
/** @before */
1919
protected function createSchema(): void
2020
{
21+
$this->skipIfDatabase(Platform::SQLITE);
22+
$this->skipIfDatabase(Platform::SQLSERVER);
23+
2124
$bridge = $this->getBridge();
2225

2326
foreach ([

0 commit comments

Comments
 (0)