Repository: cakephp/cakephp
Pull request #18325: Implement columnDefinitionSql for postgres
Author: login: markstory
Created: 2025-04-14T03:49:39Z | Merged: 2025-04-15T14:37:42Z

I did this a few weeks back and forgot to open the pull request :man_facepalming: 


Diff:
diff --git a/src/Database/Schema/PostgresSchemaDialect.php b/src/Database/Schema/PostgresSchemaDialect.php
index 39f81b981e8..2007ce4a3ab 100644
--- a/src/Database/Schema/PostgresSchemaDialect.php
+++ b/src/Database/Schema/PostgresSchemaDialect.php
@@ -609,12 +609,39 @@ public function columnSql(TableSchema $schema, string $name): string
     {
         $data = $schema->getColumn($name);
         assert($data !== null);
+        $data['name'] = $name;
 
         $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
         if ($sql !== null) {
             return $sql;
         }
+        $autoIncrementTypes = [
+            TableSchemaInterface::TYPE_TINYINTEGER,
+            TableSchemaInterface::TYPE_SMALLINTEGER,
+            TableSchemaInterface::TYPE_INTEGER,
+            TableSchemaInterface::TYPE_BIGINTEGER,
+        ];
+        $primaryKey = $schema->getPrimaryKey();
+        if (
+            in_array($data['type'], $autoIncrementTypes, true) &&
+            $primaryKey === [$name] && $name === 'id'
+        ) {
+            $data['autoIncrement'] = true;
+        }
 
+        return $this->columnDefinitionSql($data);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function columnDefinitionSql(array $column): string
+    {
+        $name = $column['name'];
+        $column += [
+            'length' => null,
+            'precision' => null,
+        ];
         $out = $this->_driver->quoteIdentifier($name);
         $typeMap = [
             TableSchemaInterface::TYPE_TINYINTEGER => ' SMALLINT',
@@ -648,41 +675,40 @@ public function columnSql(TableSchema $schema, string $name): string
             TableSchemaInterface::TYPE_INTEGER,
             TableSchemaInterface::TYPE_BIGINTEGER,
         ];
+        $autoIncrement = (bool)($column['autoIncrement'] ?? false);
         if (
-            in_array($data['type'], $autoIncrementTypes, true) &&
-            (
-                ($schema->getPrimaryKey() === [$name] && $name === 'id') || $data['autoIncrement']
-            )
+            in_array($column['type'], $autoIncrementTypes, true) &&
+            $autoIncrement
         ) {
-            $typeMap[$data['type']] = str_replace('INT', 'SERIAL', $typeMap[$data['type']]);
-            unset($data['default']);
+            $typeMap[$column['type']] = str_replace('INT', 'SERIAL', $typeMap[$column['type']]);
+            unset($column['default']);
         }
 
-        if (isset($typeMap[$data['type']])) {
-            $out .= $typeMap[$data['type']];
+        if (isset($typeMap[$column['type']])) {
+            $out .= $typeMap[$column['type']];
         }
 
-        if ($data['type'] === TableSchemaInterface::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
+        if ($column['type'] === TableSchemaInterface::TYPE_TEXT && $column['length'] !== TableSchema::LENGTH_TINY) {
             $out .= ' TEXT';
         }
-        if ($data['type'] === TableSchemaInterface::TYPE_BINARY) {
+        if ($column['type'] === TableSchemaInterface::TYPE_BINARY) {
             $out .= ' BYTEA';
         }
 
-        if ($data['type'] === TableSchemaInterface::TYPE_CHAR) {
-            $out .= '(' . $data['length'] . ')';
+        if ($column['type'] === TableSchemaInterface::TYPE_CHAR) {
+            $out .= '(' . $column['length'] . ')';
         }
 
         if (
-            $data['type'] === TableSchemaInterface::TYPE_STRING ||
+            $column['type'] === TableSchemaInterface::TYPE_STRING ||
             (
-                $data['type'] === TableSchemaInterface::TYPE_TEXT &&
-                $data['length'] === TableSchema::LENGTH_TINY
+                $column['type'] === TableSchemaInterface::TYPE_TEXT &&
+                $column['length'] === TableSchema::LENGTH_TINY
             )
         ) {
             $out .= ' VARCHAR';
-            if (isset($data['length']) && $data['length'] !== '') {
-                $out .= '(' . $data['length'] . ')';
+            if (isset($column['length']) && $column['length'] !== '') {
+                $out .= '(' . $column['length'] . ')';
             }
         }
 
@@ -691,8 +717,8 @@ public function columnSql(TableSchema $schema, string $name): string
             TableSchemaInterface::TYPE_STRING,
             TableSchemaInterface::TYPE_CHAR,
         ];
-        if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
-            $out .= ' COLLATE "' . $data['collate'] . '"';
+        if (in_array($column['type'], $hasCollate, true) && isset($column['collate']) && $column['collate'] !== '') {
+            $out .= ' COLLATE "' . $column['collate'] . '"';
         }
 
         $hasPrecision = [
@@ -703,24 +729,24 @@ public function columnSql(TableSchema $schema, string $name): string
             TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL,
             TableSchemaInterface::TYPE_TIMESTAMP_TIMEZONE,
         ];
-        if (in_array($data['type'], $hasPrecision) && isset($data['precision'])) {
-            $out .= '(' . $data['precision'] . ')';
+        if (in_array($column['type'], $hasPrecision) && isset($column['precision'])) {
+            $out .= '(' . $column['precision'] . ')';
         }
 
         if (
-            $data['type'] === TableSchemaInterface::TYPE_DECIMAL &&
+            $column['type'] === TableSchemaInterface::TYPE_DECIMAL &&
             (
-                isset($data['length']) ||
-                isset($data['precision'])
+                isset($column['length']) ||
+                isset($column['precision'])
             )
         ) {
-            $out .= '(' . $data['length'] . ',' . (int)$data['precision'] . ')';
+            $out .= '(' . $column['length'] . ',' . (int)$column['precision'] . ')';
         }
-        if (in_array($data['type'], TableSchemaInterface::GEOSPATIAL_TYPES)) {
-            $out = sprintf($out, $data['srid'] ?? self::DEFAULT_SRID);
+        if (in_array($column['type'], TableSchemaInterface::GEOSPATIAL_TYPES)) {
+            $out = sprintf($out, $column['srid'] ?? self::DEFAULT_SRID);
         }
 
-        if (isset($data['null']) && $data['null'] === false) {
+        if (isset($column['null']) && $column['null'] === false) {
             $out .= ' NOT NULL';
         }
 
@@ -732,18 +758,18 @@ public function columnSql(TableSchema $schema, string $name): string
             TableSchemaInterface::TYPE_TIMESTAMP_TIMEZONE,
         ];
         if (
-            isset($data['default']) &&
-            in_array($data['type'], $datetimeTypes) &&
-            strtolower($data['default']) === 'current_timestamp'
+            isset($column['default']) &&
+            in_array($column['type'], $datetimeTypes) &&
+            strtolower($column['default']) === 'current_timestamp'
         ) {
             $out .= ' DEFAULT CURRENT_TIMESTAMP';
-        } elseif (isset($data['default'])) {
-            $defaultValue = $data['default'];
-            if ($data['type'] === 'boolean') {
+        } elseif (isset($column['default'])) {
+            $defaultValue = $column['default'];
+            if ($column['type'] === 'boolean') {
                 $defaultValue = (bool)$defaultValue;
             }
             $out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue);
-        } elseif (isset($data['null']) && $data['null'] !== false) {
+        } elseif (isset($column['null']) && $column['null'] !== false) {
             $out .= ' DEFAULT NULL';
         }
 
diff --git a/src/Database/Schema/SqlserverSchemaDialect.php b/src/Database/Schema/SqlserverSchemaDialect.php
index 09b5f06e645..0158014ef80 100644
--- a/src/Database/Schema/SqlserverSchemaDialect.php
+++ b/src/Database/Schema/SqlserverSchemaDialect.php
@@ -581,12 +581,40 @@ public function columnSql(TableSchema $schema, string $name): string
     {
         $data = $schema->getColumn($name);
         assert($data !== null);
+        $data['name'] = $name;
 
         $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
         if ($sql !== null) {
             return $sql;
         }
+        $autoIncrementTypes = [
+            TableSchemaInterface::TYPE_TINYINTEGER,
+            TableSchemaInterface::TYPE_SMALLINTEGER,
+            TableSchemaInterface::TYPE_INTEGER,
+            TableSchemaInterface::TYPE_BIGINTEGER,
+        ];
+        $primaryKey = $schema->getPrimaryKey();
+        if (
+            in_array($data['type'], $autoIncrementTypes, true) &&
+            $primaryKey === [$name] &&
+            $name === 'id'
+        ) {
+            $data['autoIncrement'] = true;
+        }
 
+        return $this->columnDefinitionSql($data);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function columnDefinitionSql(array $column): string
+    {
+        $name = $column['name'];
+        $column += [
+            'length' => null,
+            'precision' => null,
+        ];
         $out = $this->_driver->quoteIdentifier($name);
         $typeMap = [
             TableSchemaInterface::TYPE_TINYINTEGER => ' TINYINT',
@@ -614,8 +642,8 @@ public function columnSql(TableSchema $schema, string $name): string
             TableSchemaInterface::TYPE_POLYGON => ' GEOGRAPHY',
         ];
 
-        if (isset($typeMap[$data['type']])) {
-            $out .= $typeMap[$data['type']];
+        if (isset($typeMap[$column['type']])) {
+            $out .= $typeMap[$column['type']];
         }
 
         $autoIncrementTypes = [
@@ -624,50 +652,49 @@ public function columnSql(TableSchema $schema, string $name): string
             TableSchemaInterface::TYPE_INTEGER,
             TableSchemaInterface::TYPE_BIGINTEGER,
         ];
+        $autoIncrement = (bool)($column['autoIncrement'] ?? false);
         if (
-            in_array($data['type'], $autoIncrementTypes, true) &&
-            (
-                ($schema->getPrimaryKey() === [$name] && $name === 'id') || $data['autoIncrement']
-            )
+            in_array($column['type'], $autoIncrementTypes, true) &&
+            $autoIncrement
         ) {
             $out .= ' IDENTITY(1, 1)';
-            unset($data['default']);
+            unset($column['default']);
         }
 
-        if ($data['type'] === TableSchemaInterface::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
+        if ($column['type'] === TableSchemaInterface::TYPE_TEXT && $column['length'] !== TableSchema::LENGTH_TINY) {
             $out .= ' NVARCHAR(MAX)';
         }
 
-        if ($data['type'] === TableSchemaInterface::TYPE_CHAR) {
-            $out .= '(' . $data['length'] . ')';
+        if ($column['type'] === TableSchemaInterface::TYPE_CHAR) {
+            $out .= '(' . $column['length'] . ')';
         }
 
-        if ($data['type'] === TableSchemaInterface::TYPE_BINARY) {
+        if ($column['type'] === TableSchemaInterface::TYPE_BINARY) {
             if (
-                !isset($data['length'])
-                || in_array($data['length'], [TableSchema::LENGTH_MEDIUM, TableSchema::LENGTH_LONG], true)
+                !isset($column['length'])
+                || in_array($column['length'], [TableSchema::LENGTH_MEDIUM, TableSchema::LENGTH_LONG], true)
             ) {
-                $data['length'] = 'MAX';
+                $column['length'] = 'MAX';
             }
 
-            if ($data['length'] === 1) {
+            if ($column['length'] === 1) {
                 $out .= ' BINARY(1)';
             } else {
                 $out .= ' VARBINARY';
 
-                $out .= sprintf('(%s)', $data['length']);
+                $out .= sprintf('(%s)', $column['length']);
             }
         }
 
         if (
-            $data['type'] === TableSchemaInterface::TYPE_STRING ||
+            $column['type'] === TableSchemaInterface::TYPE_STRING ||
             (
-                $data['type'] === TableSchemaInterface::TYPE_TEXT &&
-                $data['length'] === TableSchema::LENGTH_TINY
+                $column['type'] === TableSchemaInterface::TYPE_TEXT &&
+                $column['length'] === TableSchema::LENGTH_TINY
             )
         ) {
             $type = ' NVARCHAR';
-            $length = $data['length'] ?? TableSchema::LENGTH_TINY;
+            $length = $column['length'] ?? TableSchema::LENGTH_TINY;
             $out .= sprintf('%s(%d)', $type, $length);
         }
 
@@ -676,8 +703,8 @@ public function columnSql(TableSchema $schema, string $name): string
             TableSchemaInterface::TYPE_STRING,
             TableSchemaInterface::TYPE_CHAR,
         ];
-        if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
-            $out .= ' COLLATE ' . $data['collate'];
+        if (in_array($column['type'], $hasCollate, true) && isset($column['collate']) && $column['collate'] !== '') {
+            $out .= ' COLLATE ' . $column['collate'];
         }
 
         $precisionTypes = [
@@ -687,21 +714,21 @@ public function columnSql(TableSchema $schema, string $name): string
             TableSchemaInterface::TYPE_TIMESTAMP,
             TableSchemaInterface::TYPE_TIMESTAMP_FRACTIONAL,
         ];
-        if (in_array($data['type'], $precisionTypes, true) && isset($data['precision'])) {
-            $out .= '(' . (int)$data['precision'] . ')';
+        if (in_array($column['type'], $precisionTypes, true) && isset($column['precision'])) {
+            $out .= '(' . (int)$column['precision'] . ')';
         }
 
         if (
-            $data['type'] === TableSchemaInterface::TYPE_DECIMAL &&
+            $column['type'] === TableSchemaInterface::TYPE_DECIMAL &&
             (
-                isset($data['length']) ||
-                isset($data['precision'])
+                isset($column['length']) ||
+                isset($column['precision'])
             )
         ) {
-            $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
+            $out .= '(' . (int)$column['length'] . ',' . (int)$column['precision'] . ')';
         }
 
-        if (isset($data['null']) && $data['null'] === false) {
+        if (isset($column['null']) && $column['null'] === false) {
             $out .= ' NOT NULL';
         }
 
@@ -720,17 +747,17 @@ public function columnSql(TableSchema $schema, string $name): string
             'sysdatetimeoffset()',
         ];
         if (
-            isset($data['default']) &&
-            in_array($data['type'], $dateTimeTypes, true) &&
-            in_array(strtolower($data['default']), $dateTimeDefaults, true)
+            isset($column['default']) &&
+            in_array($column['type'], $dateTimeTypes, true) &&
+            in_array(strtolower($column['default']), $dateTimeDefaults, true)
         ) {
-            $out .= ' DEFAULT ' . strtoupper($data['default']);
-        } elseif (isset($data['default'])) {
-            $default = is_bool($data['default'])
-                ? (int)$data['default']
-                : $this->_driver->schemaValue($data['default']);
+            $out .= ' DEFAULT ' . strtoupper($column['default']);
+        } elseif (isset($column['default'])) {
+            $default = is_bool($column['default'])
+                ? (int)$column['default']
+                : $this->_driver->schemaValue($column['default']);
             $out .= ' DEFAULT ' . $default;
-        } elseif (isset($data['null']) && $data['null'] !== false) {
+        } elseif (isset($column['null']) && $column['null'] !== false) {
             $out .= ' DEFAULT NULL';
         }
 
diff --git a/tests/TestCase/Database/Schema/PostgresSchemaDialectTest.php b/tests/TestCase/Database/Schema/PostgresSchemaDialectTest.php
index ec49e4080b4..b1424bb4d9c 100644
--- a/tests/TestCase/Database/Schema/PostgresSchemaDialectTest.php
+++ b/tests/TestCase/Database/Schema/PostgresSchemaDialectTest.php
@@ -1120,6 +1120,9 @@ public function testColumnSql(string $name, array $data, string $expected): void
 
         $table = (new TableSchema('schema_articles'))->addColumn($name, $data);
         $this->assertEquals($expected, $schema->columnSql($table, $name));
+
+        $data['name'] = $name;
+        $this->assertEquals($expected, $schema->columnDefinitionSql($data));
     }
 
     /**
diff --git a/tests/TestCase/Database/Schema/SqlserverSchemaDialectTest.php b/tests/TestCase/Database/Schema/SqlserverSchemaDialectTest.php
index abff44109be..8e9b7aa2965 100644
--- a/tests/TestCase/Database/Schema/SqlserverSchemaDialectTest.php
+++ b/tests/TestCase/Database/Schema/SqlserverSchemaDialectTest.php
@@ -969,6 +969,9 @@ public function testColumnSql(string $name, array $data, string $expected): void
 
         $table = (new TableSchema('schema_articles'))->addColumn($name, $data);
         $this->assertEquals($expected, $schema->columnSql($table, $name));
+
+        $data['name'] = $name;
+        $this->assertEquals($expected, $schema->columnDefinitionSql($data));
     }
 
     /**


Reviews:
user: login: othercorey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T07:33:56Z
user: login: markstory
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T02:48:22Z
user: login: othercorey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T02:56:52Z
user: login: othercorey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T02:57:27Z
user: login: markstory
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T03:17:17Z
user: login: othercorey
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T04:27:55Z

Review comments:
diff_hunk: @@ -609,12 +609,39 @@ public function columnSql(TableSchema $schema, string $name): string
     {
         $data = $schema->getColumn($name);
         assert($data !== null);
+        $data['name'] = $name;
 
         $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
         if ($sql !== null) {
             return $sql;
         }
+        $autoIncrementTypes = [
+            TableSchemaInterface::TYPE_TINYINTEGER,
+            TableSchemaInterface::TYPE_SMALLINTEGER,
+            TableSchemaInterface::TYPE_INTEGER,
+            TableSchemaInterface::TYPE_BIGINTEGER,
+        ];
+        $primaryKey = $schema->getPrimaryKey();
+        if (
+            in_array($data['type'], $autoIncrementTypes, true) &&
+            $primaryKey === [$name]
+        ) {
+            $data['autoIncrement'] = true;
path: src/Database/Schema/PostgresSchemaDialect.php
user: login: othercorey
body: This drops the `id` name requirement.
created_at: 2025-04-14T07:33:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -609,12 +609,39 @@ public function columnSql(TableSchema $schema, string $name): string
     {
         $data = $schema->getColumn($name);
         assert($data !== null);
+        $data['name'] = $name;
 
         $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
         if ($sql !== null) {
             return $sql;
         }
+        $autoIncrementTypes = [
+            TableSchemaInterface::TYPE_TINYINTEGER,
+            TableSchemaInterface::TYPE_SMALLINTEGER,
+            TableSchemaInterface::TYPE_INTEGER,
+            TableSchemaInterface::TYPE_BIGINTEGER,
+        ];
+        $primaryKey = $schema->getPrimaryKey();
+        if (
+            in_array($data['type'], $autoIncrementTypes, true) &&
+            $primaryKey === [$name]
+        ) {
+            $data['autoIncrement'] = true;
path: src/Database/Schema/PostgresSchemaDialect.php
user: login: markstory
body: I was trying to align how postgres works with other database platforms which don't require the column to be called `id`.
created_at: 2025-04-15T02:48:22Z
author_association: MEMBER
diff_hunk: @@ -609,12 +609,39 @@ public function columnSql(TableSchema $schema, string $name): string
     {
         $data = $schema->getColumn($name);
         assert($data !== null);
+        $data['name'] = $name;
 
         $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
         if ($sql !== null) {
             return $sql;
         }
+        $autoIncrementTypes = [
+            TableSchemaInterface::TYPE_TINYINTEGER,
+            TableSchemaInterface::TYPE_SMALLINTEGER,
+            TableSchemaInterface::TYPE_INTEGER,
+            TableSchemaInterface::TYPE_BIGINTEGER,
+        ];
+        $primaryKey = $schema->getPrimaryKey();
+        if (
+            in_array($data['type'], $autoIncrementTypes, true) &&
+            $primaryKey === [$name]
+        ) {
+            $data['autoIncrement'] = true;
path: src/Database/Schema/PostgresSchemaDialect.php
user: login: othercorey
body: Feels like a mistake to force every integer key to be auto-increment.
created_at: 2025-04-15T02:56:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -609,12 +609,39 @@ public function columnSql(TableSchema $schema, string $name): string
     {
         $data = $schema->getColumn($name);
         assert($data !== null);
+        $data['name'] = $name;
 
         $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
         if ($sql !== null) {
             return $sql;
         }
+        $autoIncrementTypes = [
+            TableSchemaInterface::TYPE_TINYINTEGER,
+            TableSchemaInterface::TYPE_SMALLINTEGER,
+            TableSchemaInterface::TYPE_INTEGER,
+            TableSchemaInterface::TYPE_BIGINTEGER,
+        ];
+        $primaryKey = $schema->getPrimaryKey();
+        if (
+            in_array($data['type'], $autoIncrementTypes, true) &&
+            $primaryKey === [$name]
+        ) {
+            $data['autoIncrement'] = true;
path: src/Database/Schema/PostgresSchemaDialect.php
user: login: othercorey
body: In any case, I don't think we can make that change in 5.x.
created_at: 2025-04-15T02:57:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -609,12 +609,39 @@ public function columnSql(TableSchema $schema, string $name): string
     {
         $data = $schema->getColumn($name);
         assert($data !== null);
+        $data['name'] = $name;
 
         $sql = $this->_getTypeSpecificColumnSql($data['type'], $schema, $name);
         if ($sql !== null) {
             return $sql;
         }
+        $autoIncrementTypes = [
+            TableSchemaInterface::TYPE_TINYINTEGER,
+            TableSchemaInterface::TYPE_SMALLINTEGER,
+            TableSchemaInterface::TYPE_INTEGER,
+            TableSchemaInterface::TYPE_BIGINTEGER,
+        ];
+        $primaryKey = $schema->getPrimaryKey();
+        if (
+            in_array($data['type'], $autoIncrementTypes, true) &&
+            $primaryKey === [$name]
+        ) {
+            $data['autoIncrement'] = true;
path: src/Database/Schema/PostgresSchemaDialect.php
user: login: markstory
body: Fair point, I'll add the column name check back in.
created_at: 2025-04-15T03:16:21Z
author_association: MEMBER

PR comments:
user: login: othercorey
created_at: 2025-04-14T07:35:53Z
body: We need to move away from SERIAL for auto-increment. It's no longer the suggested method. Instead, postgres uses the standards format: `GENERATE ALWAYS AS IDENTITY`.

https://www.postgresql.org/docs/current/ddl-identity-columns.html

This creates a proper type instead of a psuedo-type.
author_association: CONTRIBUTOR
user: login: markstory
created_at: 2025-04-15T02:46:07Z
body: > We need to move away from SERIAL for auto-increment. It's no longer the suggested method. Instead, postgres uses the standards format: GENERATE ALWAYS AS IDENTITY.

I'm up for making that change. I don't think it presents a risk to backwards compatibility, as the effective schema is the same between `SERIAL` and `INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY`.
author_association: MEMBER