Skip to content

Commit d489db8

Browse files
authored
Implement identity in PostgresAdapter (#2085)
1 parent f067f37 commit d489db8

File tree

3 files changed

+265
-25
lines changed

3 files changed

+265
-25
lines changed

src/Phinx/Db/Adapter/PostgresAdapter.php

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222

2323
class PostgresAdapter extends PdoAdapter
2424
{
25+
public const GENERATED_ALWAYS = 'ALWAYS';
26+
public const GENERATED_BY_DEFAULT = 'BY DEFAULT';
27+
2528
/**
2629
* @var string[]
2730
*/
@@ -44,6 +47,13 @@ class PostgresAdapter extends PdoAdapter
4447
*/
4548
protected $columnsWithComments = [];
4649

50+
/**
51+
* Use identity columns if available (Postgres >= 10.0)
52+
*
53+
* @var bool
54+
*/
55+
protected $useIdentity;
56+
4757
/**
4858
* {@inheritDoc}
4959
*
@@ -61,7 +71,6 @@ public function connect(): void
6171
}
6272

6373
$options = $this->getOptions();
64-
6574
$dsn = 'pgsql:dbname=' . $options['name'];
6675

6776
if (isset($options['host'])) {
@@ -77,7 +86,8 @@ public function connect(): void
7786

7887
// use custom data fetch mode
7988
if (!empty($options['fetch_mode'])) {
80-
$driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
89+
$driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] =
90+
constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
8191
}
8292

8393
// pass \PDO::ATTR_PERSISTENT to driver options instead of useless setting it after instantiation
@@ -99,6 +109,8 @@ public function connect(): void
99109
);
100110
}
101111

112+
$this->useIdentity = (float)$db->getAttribute(PDO::ATTR_SERVER_VERSION) >= 10;
113+
102114
$this->setConnection($db);
103115
}
104116
}
@@ -231,7 +243,11 @@ public function createTable(Table $table, array $columns = [], array $indexes =
231243

232244
$this->columnsWithComments = [];
233245
foreach ($columns as $column) {
234-
$sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
246+
$sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column);
247+
if ($this->useIdentity && $column->getIdentity() && $column->getGenerated() !== null) {
248+
$sql .= sprintf(' GENERATED %s AS IDENTITY', $column->getGenerated());
249+
}
250+
$sql .= ', ';
235251

236252
// set column comments, if needed
237253
if ($column->getComment()) {
@@ -401,14 +417,15 @@ public function getColumns(string $tableName): array
401417
'SELECT column_name, data_type, udt_name, is_identity, is_nullable,
402418
column_default, character_maximum_length, numeric_precision, numeric_scale,
403419
datetime_precision
420+
%s
404421
FROM information_schema.columns
405422
WHERE table_schema = %s AND table_name = %s
406423
ORDER BY ordinal_position',
424+
$this->useIdentity ? ', identity_generation' : '',
407425
$this->getConnection()->quote($parts['schema']),
408426
$this->getConnection()->quote($parts['table'])
409427
);
410428
$columnsInfo = $this->fetchAll($sql);
411-
412429
foreach ($columnsInfo as $columnInfo) {
413430
$isUserDefined = strtoupper(trim($columnInfo['data_type'])) === 'USER-DEFINED';
414431

@@ -426,20 +443,28 @@ public function getColumns(string $tableName): array
426443
} else {
427444
$columnDefault = Literal::from($columnInfo['column_default']);
428445
}
429-
} elseif (preg_match('/^\D[a-z_\d]*\(.*\)$/', $columnInfo['column_default'])) {
446+
} elseif (
447+
$columnInfo['column_default'] !== null &&
448+
preg_match('/^\D[a-z_\d]*\(.*\)$/', $columnInfo['column_default'])
449+
) {
430450
$columnDefault = Literal::from($columnInfo['column_default']);
431451
} else {
432452
$columnDefault = $columnInfo['column_default'];
433453
}
434454

435455
$column = new Column();
456+
436457
$column->setName($columnInfo['column_name'])
437458
->setType($columnType)
438459
->setNull($columnInfo['is_nullable'] === 'YES')
439460
->setDefault($columnDefault)
440461
->setIdentity($columnInfo['is_identity'] === 'YES')
441462
->setScale($columnInfo['numeric_scale']);
442463

464+
if ($this->useIdentity) {
465+
$column->setGenerated($columnInfo['identity_generation']);
466+
}
467+
443468
if (preg_match('/\bwith time zone$/', $columnInfo['data_type'])) {
444469
$column->setTimezone(true);
445470
}
@@ -492,9 +517,11 @@ protected function getAddColumnInstructions(Table $table, Column $column): Alter
492517
{
493518
$instructions = new AlterInstructions();
494519
$instructions->addAlter(sprintf(
495-
'ADD %s %s',
520+
'ADD %s %s %s',
496521
$this->quoteColumnName($column->getName()),
497-
$this->getColumnSqlDefinition($column)
522+
$this->getColumnSqlDefinition($column),
523+
$column->isIdentity() && $column->getGenerated() !== null && $this->useIdentity ?
524+
sprintf('GENERATED %s AS IDENTITY', $column->getGenerated()) : ''
498525
));
499526

500527
if ($column->getComment()) {
@@ -509,8 +536,11 @@ protected function getAddColumnInstructions(Table $table, Column $column): Alter
509536
*
510537
* @throws \InvalidArgumentException
511538
*/
512-
protected function getRenameColumnInstructions(string $tableName, string $columnName, string $newColumnName): AlterInstructions
513-
{
539+
protected function getRenameColumnInstructions(
540+
string $tableName,
541+
string $columnName,
542+
string $newColumnName
543+
): AlterInstructions {
514544
$parts = $this->getSchemaName($tableName);
515545
$sql = sprintf(
516546
'SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS column_exists
@@ -542,8 +572,11 @@ protected function getRenameColumnInstructions(string $tableName, string $column
542572
/**
543573
* @inheritDoc
544574
*/
545-
protected function getChangeColumnInstructions(string $tableName, string $columnName, Column $newColumn): AlterInstructions
546-
{
575+
protected function getChangeColumnInstructions(
576+
string $tableName,
577+
string $columnName,
578+
Column $newColumn
579+
): AlterInstructions {
547580
$quotedColumnName = $this->quoteColumnName($columnName);
548581
$instructions = new AlterInstructions();
549582
if ($newColumn->getType() === 'boolean') {
@@ -581,13 +614,33 @@ protected function getChangeColumnInstructions(string $tableName, string $column
581614
}
582615
$instructions->addAlter($sql);
583616

617+
$column = $this->getColumn($tableName, $columnName);
618+
619+
if ($this->useIdentity) {
620+
// process identity
621+
$sql = sprintf(
622+
'ALTER COLUMN %s',
623+
$quotedColumnName
624+
);
625+
if ($newColumn->isIdentity() && $newColumn->getGenerated() !== null) {
626+
if ($column->isIdentity()) {
627+
$sql .= sprintf(' SET GENERATED %s', $newColumn->getGenerated());
628+
} else {
629+
$sql .= sprintf(' ADD GENERATED %s AS IDENTITY', $newColumn->getGenerated());
630+
}
631+
} else {
632+
$sql .= ' DROP IDENTITY IF EXISTS';
633+
}
634+
$instructions->addAlter($sql);
635+
}
636+
584637
// process null
585638
$sql = sprintf(
586639
'ALTER COLUMN %s',
587640
$quotedColumnName
588641
);
589642

590-
if ($newColumn->isNull()) {
643+
if (!$newColumn->getIdentity() && !$column->getIdentity() && $newColumn->isNull()) {
591644
$sql .= ' DROP NOT NULL';
592645
} else {
593646
$sql .= ' SET NOT NULL';
@@ -601,7 +654,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column
601654
$quotedColumnName,
602655
$this->getDefaultValueDefinition($newColumn->getDefault(), $newColumn->getType())
603656
));
604-
} else {
657+
} elseif (!$newColumn->getIdentity()) {
605658
//drop default
606659
$instructions->addAlter(sprintf(
607660
'ALTER COLUMN %s DROP DEFAULT',
@@ -627,6 +680,23 @@ protected function getChangeColumnInstructions(string $tableName, string $column
627680
return $instructions;
628681
}
629682

683+
/**
684+
* @param string $tableName Table name
685+
* @param string $columnName Column name
686+
* @return ?\Phinx\Db\Table\Column
687+
*/
688+
protected function getColumn(string $tableName, string $columnName): ?Column
689+
{
690+
$columns = $this->getColumns($tableName);
691+
foreach ($columns as $column) {
692+
if ($column->getName() === $columnName) {
693+
return $column;
694+
}
695+
}
696+
697+
return null;
698+
}
699+
630700
/**
631701
* @inheritDoc
632702
*/
@@ -1028,7 +1098,7 @@ public function getSqlType($type, ?int $limit = null): array
10281098
return ['name' => $type];
10291099
}
10301100
// Return array type
1031-
throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by Postgresql.');
1101+
throw new UnsupportedColumnTypeException('Column type `' . $type . '` is not supported by Postgresql.');
10321102
}
10331103
}
10341104

@@ -1099,7 +1169,9 @@ public function getPhinxType(string $sqlType): string
10991169
case 'macaddr':
11001170
return static::PHINX_TYPE_MACADDR;
11011171
default:
1102-
throw new UnsupportedColumnTypeException('Column type "' . $sqlType . '" is not supported by Postgresql.');
1172+
throw new UnsupportedColumnTypeException(
1173+
'Column type `' . $sqlType . '` is not supported by Postgresql.'
1174+
);
11031175
}
11041176
}
11051177

@@ -1163,7 +1235,8 @@ protected function getDefaultValueDefinition($default, ?string $columnType = nul
11631235
protected function getColumnSqlDefinition(Column $column): string
11641236
{
11651237
$buffer = [];
1166-
if ($column->isIdentity()) {
1238+
1239+
if ($column->isIdentity() && (!$this->useIdentity || $column->getGenerated() === null)) {
11671240
if ($column->getType() === 'smallinteger') {
11681241
$buffer[] = 'SMALLSERIAL';
11691242
} elseif ($column->getType() === 'biginteger') {
@@ -1305,7 +1378,9 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta
13051378
{
13061379
$parts = $this->getSchemaName($tableName);
13071380

1308-
$constraintName = $foreignKey->getConstraint() ?: ($parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey');
1381+
$constraintName = $foreignKey->getConstraint() ?: (
1382+
$parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey'
1383+
);
13091384
$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) .
13101385
' FOREIGN KEY ("' . implode('", "', $foreignKey->getColumns()) . '")' .
13111386
" REFERENCES {$this->quoteTableName($foreignKey->getReferencedTable()->getName())} (\"" .

src/Phinx/Db/Table/Column.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace Phinx\Db\Table;
99

1010
use Phinx\Db\Adapter\AdapterInterface;
11+
use Phinx\Db\Adapter\PostgresAdapter;
1112
use RuntimeException;
1213

1314
/**
@@ -85,6 +86,13 @@ class Column
8586
*/
8687
protected $identity = false;
8788

89+
/**
90+
* Postgres-only column option for identity (always|default)
91+
*
92+
* @var ?string
93+
*/
94+
protected $generated = PostgresAdapter::GENERATED_ALWAYS;
95+
8896
/**
8997
* @var int|null
9098
*/
@@ -275,6 +283,29 @@ public function getDefault()
275283
return $this->default;
276284
}
277285

286+
/**
287+
* Sets generated option for identity columns. Ignored otherwise.
288+
*
289+
* @param string|null $generated Generated option
290+
* @return $this
291+
*/
292+
public function setGenerated(?string $generated)
293+
{
294+
$this->generated = $generated;
295+
296+
return $this;
297+
}
298+
299+
/**
300+
* Gets generated option for identity columns. Null otherwise
301+
*
302+
* @return string|null
303+
*/
304+
public function getGenerated(): ?string
305+
{
306+
return $this->generated;
307+
}
308+
278309
/**
279310
* Sets whether or not the column is an identity column.
280311
*
@@ -709,6 +740,7 @@ protected function getValidOptions(): array
709740
'srid',
710741
'seed',
711742
'increment',
743+
'generated',
712744
];
713745
}
714746

0 commit comments

Comments
 (0)