Skip to content

Commit 21456eb

Browse files
LordSimaljhandelmarkstory
authored
Fix compatibility with IDENTITY_INSERT (#826)
* testing * add transaction * Proposed solution with some refactoring in AbstractAdapter so we don't have to copy too much code to SqlServerAdapter. (could be dryer if we wanted) * Fix tests & Adapter comments --------- Co-authored-by: Josh <josh@liveoak.ws> Co-authored-by: Mark Story <mark@mark-story.com>
1 parent 1501714 commit 21456eb

3 files changed

Lines changed: 303 additions & 142 deletions

File tree

src/Db/Adapter/AbstractAdapter.php

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
23
declare(strict_types=1);
34

45
/**
@@ -602,6 +603,34 @@ public function fetchAll(string $sql): array
602603
* @inheritDoc
603604
*/
604605
public function insert(TableMetadata $table, array $row): void
606+
{
607+
$sql = $this->generateInsertSql($table, $row);
608+
609+
if ($this->isDryRunEnabled()) {
610+
$this->io->out($sql);
611+
} else {
612+
$vals = [];
613+
foreach ($row as $value) {
614+
$placeholder = '?';
615+
if ($value instanceof Literal || $value instanceof PhinxLiteral) {
616+
$placeholder = (string)$value;
617+
}
618+
if ($placeholder === '?') {
619+
$vals[] = $value;
620+
}
621+
}
622+
$this->getConnection()->execute($sql, $vals);
623+
}
624+
}
625+
626+
/**
627+
* Generates the SQL for an insert.
628+
*
629+
* @param \Migrations\Db\Table\Table $table The table to insert into
630+
* @param array $row The row to insert
631+
* @return string
632+
*/
633+
protected function generateInsertSql(TableMetadata $table, array $row): string
605634
{
606635
$sql = sprintf(
607636
'INSERT INTO %s ',
@@ -618,22 +647,18 @@ public function insert(TableMetadata $table, array $row): void
618647

619648
if ($this->isDryRunEnabled()) {
620649
$sql .= ' VALUES (' . implode(', ', array_map($this->quoteValue(...), $row)) . ');';
621-
$this->io->out($sql);
650+
return $sql;
622651
} else {
623652
$values = [];
624-
$vals = [];
625653
foreach ($row as $value) {
626654
$placeholder = '?';
627655
if ($value instanceof Literal || $value instanceof PhinxLiteral) {
628656
$placeholder = (string)$value;
629657
}
630658
$values[] = $placeholder;
631-
if ($placeholder === '?') {
632-
$vals[] = $value;
633-
}
634659
}
635660
$sql .= ' VALUES (' . implode(',', $values) . ')';
636-
$this->getConnection()->execute($sql, $vals);
661+
return $sql;
637662
}
638663
}
639664

@@ -683,6 +708,39 @@ protected function quoteString(string $value): string
683708
* @inheritDoc
684709
*/
685710
public function bulkinsert(TableMetadata $table, array $rows): void
711+
{
712+
$sql = $this->generateBulkInsertSql($table, $rows);
713+
714+
if ($this->isDryRunEnabled()) {
715+
$this->io->out($sql);
716+
} else {
717+
$vals = [];
718+
foreach ($rows as $row) {
719+
foreach ($row as $v) {
720+
$placeholder = '?';
721+
if ($v instanceof Literal || $v instanceof PhinxLiteral) {
722+
$placeholder = (string)$v;
723+
}
724+
if ($placeholder == '?') {
725+
if (is_bool($v)) {
726+
$vals[] = $this->castToBool($v);
727+
} else {
728+
$vals[] = $v;
729+
}
730+
}
731+
}
732+
}
733+
$this->getConnection()->execute($sql, $vals);
734+
}
735+
}
736+
/**
737+
* Generates the SQL for a bulk insert.
738+
*
739+
* @param \Migrations\Db\Table\Table $table The table to insert into
740+
* @param array $rows The rows to insert
741+
* @return string
742+
*/
743+
protected function generateBulkInsertSql(TableMetadata $table, array $rows): string
686744
{
687745
$sql = sprintf(
688746
'INSERT INTO %s ',
@@ -698,9 +756,8 @@ public function bulkinsert(TableMetadata $table, array $rows): void
698756
return '(' . implode(', ', array_map($this->quoteValue(...), $row)) . ')';
699757
}, $rows);
700758
$sql .= implode(', ', $values) . ';';
701-
$this->io->out($sql);
759+
return $sql;
702760
} else {
703-
$vals = [];
704761
$queries = [];
705762
foreach ($rows as $row) {
706763
$values = [];
@@ -710,19 +767,12 @@ public function bulkinsert(TableMetadata $table, array $rows): void
710767
$placeholder = (string)$v;
711768
}
712769
$values[] = $placeholder;
713-
if ($placeholder == '?') {
714-
if (is_bool($v)) {
715-
$vals[] = $this->castToBool($v);
716-
} else {
717-
$vals[] = $v;
718-
}
719-
}
720770
}
721771
$query = '(' . implode(', ', $values) . ')';
722772
$queries[] = $query;
723773
}
724774
$sql .= implode(',', $queries);
725-
$this->getConnection()->execute($sql, $vals);
775+
return $sql;
726776
}
727777
}
728778

src/Db/Adapter/SqlserverAdapter.php

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
23
declare(strict_types=1);
34

45
/**
@@ -17,6 +18,8 @@
1718
use Migrations\Db\Table\Index;
1819
use Migrations\Db\Table\Table;
1920
use Migrations\MigrationInterface;
21+
use Migrations\Db\Table\Table as TableMetadata;
22+
use Phinx\Util\Literal as PhinxLiteral;
2023

2124
/**
2225
* Migrations SqlServer Adapter.
@@ -104,8 +107,8 @@ public function createTable(Table $table, array $columns = [], array $indexes =
104107
// Handle id => "field_name" to support AUTO_INCREMENT
105108
$column = new Column();
106109
$column->setName($options['id'])
107-
->setType('integer')
108-
->setOptions(['identity' => true]);
110+
->setType('integer')
111+
->setOptions(['identity' => true]);
109112

110113
array_unshift($columns, $column);
111114
if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) {
@@ -337,11 +340,11 @@ public function getColumns(string $tableName): array
337340

338341
$column = new Column();
339342
$column->setName($columnInfo['name'])
340-
->setType($type)
341-
->setNull($columnInfo['null'] !== 'NO')
342-
->setDefault($this->parseDefault($columnInfo['default']))
343-
->setIdentity($columnInfo['identity'] === '1')
344-
->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
343+
->setType($type)
344+
->setNull($columnInfo['null'] !== 'NO')
345+
->setDefault($this->parseDefault($columnInfo['default']))
346+
->setIdentity($columnInfo['identity'] === '1')
347+
->setComment($this->getColumnComment($columnInfo['table_name'], $columnInfo['name']));
345348

346349
if (!empty($columnInfo['char_length'])) {
347350
$column->setLimit((int)$columnInfo['char_length']);
@@ -652,7 +655,7 @@ public function hasIndexByName(string $tableName, string $indexName): bool
652655

653656
foreach ($indexes as $name => $index) {
654657
if ($name === $indexName) {
655-
return true;
658+
return true;
656659
}
657660
}
658661

@@ -938,15 +941,15 @@ public function getSqlType(Literal|string $type, ?int $limit = null): array
938941
return ['name' => 'uniqueidentifier'];
939942
case static::PHINX_TYPE_FILESTREAM:
940943
return ['name' => 'varbinary', 'limit' => 'max'];
941-
// Geospatial database types
944+
// Geospatial database types
942945
case static::PHINX_TYPE_GEOGRAPHY:
943946
case static::PHINX_TYPE_POINT:
944947
case static::PHINX_TYPE_LINESTRING:
945948
case static::PHINX_TYPE_POLYGON:
946949
// SQL Server stores all spatial data using a single data type.
947950
// Specific types (point, polygon, etc) are set at insert time.
948951
return ['name' => 'geography'];
949-
// Geometry specific type
952+
// Geometry specific type
950953
case static::PHINX_TYPE_GEOMETRY:
951954
return ['name' => 'geometry'];
952955
default:
@@ -1312,4 +1315,82 @@ public function migrated(MigrationInterface $migration, string $direction, strin
13121315

13131316
return parent::migrated($migration, $direction, $startTime, $endTime);
13141317
}
1318+
/**
1319+
* @inheritDoc
1320+
*/
1321+
public function insert(TableMetadata $table, array $row): void
1322+
{
1323+
$sql = $this->generateInsertSql($table, $row);
1324+
1325+
$sql = $this->updateSQLForIdentityInsert($table->getName(), $sql);
1326+
1327+
1328+
if ($this->isDryRunEnabled()) {
1329+
$this->io->out($sql);
1330+
} else {
1331+
$vals = [];
1332+
foreach ($row as $value) {
1333+
$placeholder = '?';
1334+
if ($value instanceof Literal || $value instanceof PhinxLiteral) {
1335+
$placeholder = (string)$value;
1336+
}
1337+
if ($placeholder === '?') {
1338+
$vals[] = $value;
1339+
}
1340+
}
1341+
$this->getConnection()->execute($sql, $vals);
1342+
}
1343+
}
1344+
/**
1345+
* @inheritDoc
1346+
*/
1347+
public function bulkinsert(TableMetadata $table, array $rows): void
1348+
{
1349+
$sql = $this->generateBulkInsertSql($table, $rows);
1350+
1351+
$sql = $this->updateSQLForIdentityInsert($table->getName(), $sql);
1352+
1353+
if ($this->isDryRunEnabled()) {
1354+
$this->io->out($sql);
1355+
} else {
1356+
$vals = [];
1357+
foreach ($rows as $row) {
1358+
foreach ($row as $v) {
1359+
$placeholder = '?';
1360+
if ($v instanceof Literal || $v instanceof PhinxLiteral) {
1361+
$placeholder = (string)$v;
1362+
}
1363+
if ($placeholder == '?') {
1364+
if (is_bool($v)) {
1365+
$vals[] = $this->castToBool($v);
1366+
} else {
1367+
$vals[] = $v;
1368+
}
1369+
}
1370+
}
1371+
}
1372+
$this->getConnection()->execute($sql, $vals);
1373+
}
1374+
}
1375+
/**
1376+
* @param string $tableName Table name
1377+
* @param string $sql SQL statement
1378+
* @return string
1379+
*/
1380+
private function updateSQLForIdentityInsert(string $tableName, string $sql): string
1381+
{
1382+
$options = $this->getOptions();
1383+
if (isset($options['identity_insert']) && $options['identity_insert'] == true) {
1384+
$identityInsertStart = sprintf(
1385+
'SET IDENTITY_INSERT %s ON',
1386+
$this->quoteTableName($tableName)
1387+
);
1388+
$identityInsertEnd = sprintf(
1389+
'SET IDENTITY_INSERT %s OFF',
1390+
$this->quoteTableName($tableName)
1391+
);
1392+
$sql = $identityInsertStart . ';' . PHP_EOL . $sql . ';' . PHP_EOL . $identityInsertEnd;
1393+
}
1394+
return $sql;
1395+
}
13151396
}

0 commit comments

Comments
 (0)