Skip to content

Commit

Permalink
Merge pull request #7371 from kenjis/fix-modifyColumn-SQLite3
Browse files Browse the repository at this point in the history
fix: [SQLite3][Postgres][SQLSRV][OCI8] Forge::modifyColumn() changes NULL constraint incorrectly
  • Loading branch information
kenjis authored Apr 27, 2023
2 parents e4438f7 + 3b416c8 commit 90762f3
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 22 deletions.
16 changes: 11 additions & 5 deletions system/Database/Forge.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class Forge
*
* @internal Used for marking nullable fields. Not covered by BC promise.
*/
protected $null = '';
protected $null = 'NULL';

/**
* DEFAULT value representation in CREATE/ALTER TABLE statements
Expand Down Expand Up @@ -562,7 +562,7 @@ public function createTable(string $table, bool $ifNotExists = false, array $att
}

/**
* @return string
* @return string SQL string
*
* @deprecated $ifNotExists is no longer used, and will be removed.
*/
Expand Down Expand Up @@ -897,13 +897,19 @@ protected function _processFields(bool $createTable = false): array
$this->_attributeDefault($attributes, $field);

if (isset($attributes['NULL'])) {
$nullString = ' ' . $this->null;

if ($attributes['NULL'] === true) {
$field['null'] = empty($this->null) ? '' : ' ' . $this->null;
$field['null'] = empty($this->null) ? '' : $nullString;
} elseif ($attributes['NULL'] === $nullString) {
$field['null'] = $nullString;
} elseif ($attributes['NULL'] === '') {
$field['null'] = '';
} else {
$field['null'] = ' NOT NULL';
$field['null'] = ' NOT ' . $this->null;
}
} elseif ($createTable === true) {
$field['null'] = ' NOT NULL';
$field['null'] = ' NOT ' . $this->null;
}

$this->_attributeAutoIncrement($attributes, $field);
Expand Down
13 changes: 10 additions & 3 deletions system/Database/OCI8/Forge.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,17 @@ protected function _alterTable(string $alterType, string $table, $field)
// If a null constraint is added to a column with a null constraint,
// ORA-01451 will occur,
// so add null constraint is used only when it is different from the current null constraint.
$isWantToAddNull = strpos($field[$i]['null'], ' NOT') === false;
$currentNullAddable = $nullableMap[$field[$i]['name']];
// If a not null constraint is added to a column with a not null constraint,
// ORA-01442 will occur.
$wantToAddNull = strpos($field[$i]['null'], ' NOT') === false;
$currentNullable = $nullableMap[$field[$i]['name']];

if ($isWantToAddNull === $currentNullAddable) {
if ($wantToAddNull === true && $currentNullable === true) {
$field[$i]['null'] = '';
} elseif ($field[$i]['null'] === '' && $currentNullable === false) {
// Nullable by default
$field[$i]['null'] = ' NULL';
} elseif ($wantToAddNull === false && $currentNullable === false) {
$field[$i]['null'] = '';
}
}
Expand Down
8 changes: 5 additions & 3 deletions system/Database/Postgre/Forge.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,12 @@ protected function _alterTable(string $alterType, string $table, $field)
. " SET DEFAULT {$data['default']}";
}

if (isset($data['null'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. ($data['null'] === true ? ' DROP' : ' SET') . ' NOT NULL';
$nullable = true; // Nullable by default.
if (isset($data['null']) && ($data['null'] === false || $data['null'] === ' NOT ' . $this->null)) {
$nullable = false;
}
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. ($nullable === true ? ' DROP' : ' SET') . ' NOT NULL';

if (! empty($data['new_name'])) {
$sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($data['name'])
Expand Down
8 changes: 5 additions & 3 deletions system/Database/SQLSRV/Forge.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,12 @@ protected function _alterTable(string $alterType, string $table, $field)
. " DEFAULT {$data['default']} FOR " . $this->db->escapeIdentifiers($data['name']);
}

if (isset($data['null'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. ($data['null'] === true ? ' DROP' : '') . " {$data['type']}{$data['length']} NOT NULL";
$nullable = true; // Nullable by default.
if (isset($data['null']) && ($data['null'] === false || $data['null'] === ' NOT ' . $this->null)) {
$nullable = false;
}
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. " {$data['type']}{$data['length']} " . ($nullable === true ? '' : 'NOT') . ' NULL';

if (! empty($data['comment'])) {
$sqls[] = 'EXEC sys.sp_addextendedproperty '
Expand Down
12 changes: 6 additions & 6 deletions system/Database/SQLite3/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,14 @@ public function dropColumn($columns)
*
* @return Table
*/
public function modifyColumn(array $field)
public function modifyColumn(array $fields)
{
$field = $field[0];

$oldName = $field['name'];
unset($field['name']);
foreach ($fields as $field) {
$oldName = $field['name'];
unset($field['name']);

$this->fields[$oldName] = $field;
$this->fields[$oldName] = $field;
}

return $this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function up()
'constraint' => 255,
],
]);
$this->forge->createTable('foo', true);
$this->forge->createTable('foo');

$this->db->table('foo')->insert([
'key' => 'foobar',
Expand Down
4 changes: 4 additions & 0 deletions tests/system/Commands/Database/MigrateStatusTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use CodeIgniter\CLI\CLI;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\StreamFilterTrait;
use Config\Database;

/**
* @group DatabaseLive
Expand All @@ -29,6 +30,9 @@ final class MigrateStatusTest extends CIUnitTestCase

protected function setUp(): void
{
$forge = Database::forge();
$forge->dropTable('foo', true);

parent::setUp();

if (! is_file($this->migrationFileFrom)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ final class DatabaseTestCaseMigrationOnce1Test extends CIUnitTestCase

protected function setUp(): void
{
$forge = Database::forge();
$forge->dropTable('foo', true);

$this->setUpMethods[] = 'setUpAddNamespace';

parent::setUp();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Config\Database;
use Config\Services;

/**
Expand Down Expand Up @@ -55,6 +56,9 @@ final class DatabaseTestCaseMigrationOnce2Test extends CIUnitTestCase

protected function setUp(): void
{
$forge = Database::forge();
$forge->dropTable('foo', true);

$this->setUpMethods[] = 'setUpAddNamespace';

parent::setUp();
Expand Down
4 changes: 4 additions & 0 deletions tests/system/Database/DatabaseTestCaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Config\Database;
use Config\Services;
use Tests\Support\Database\Seeds\AnotherSeeder;
use Tests\Support\Database\Seeds\CITestSeeder;
Expand Down Expand Up @@ -60,6 +61,9 @@ final class DatabaseTestCaseTest extends CIUnitTestCase

protected function setUp(): void
{
$forge = Database::forge();
$forge->dropTable('foo', true);

$this->setUpMethods[] = 'setUpAddNamespace';

parent::setUp();
Expand Down
87 changes: 87 additions & 0 deletions tests/system/Database/Live/ForgeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Config\Database;
use LogicException;
use RuntimeException;
use stdClass;
use Tests\Support\Database\Seeds\CITestSeeder;

/**
Expand Down Expand Up @@ -1276,6 +1278,91 @@ public function testModifyColumnRename()
$this->forge->dropTable('forge_test_three', true);
}

public function testModifyColumnNullTrue()
{
// @TODO remove this in `4.4` branch
if ($this->db->DBDriver === 'SQLSRV') {
$this->markTestSkipped('SQLSRV does not support getFieldData() nullable.');
}

$this->forge->dropTable('forge_test_modify', true);

$this->forge->addField([
'col1' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'col2' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'col3' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
]);
$this->forge->createTable('forge_test_modify');

$this->forge->modifyColumn('forge_test_modify', [
'col1' => ['type' => 'VARCHAR', 'constraint' => 1],
'col2' => ['type' => 'VARCHAR', 'constraint' => 1, 'null' => true],
'col3' => ['type' => 'VARCHAR', 'constraint' => 1, 'null' => false],
]);

$this->db->resetDataCache();

$col1 = $this->getMetaData('col1', 'forge_test_modify');
$this->assertTrue($col1->nullable);
$col2 = $this->getMetaData('col2', 'forge_test_modify');
$this->assertTrue($col2->nullable);
$col3 = $this->getMetaData('col3', 'forge_test_modify');
$this->assertFalse($col3->nullable);

$this->forge->dropTable('forge_test_modify', true);
}

public function testModifyColumnNullFalse()
{
// @TODO remove this in `4.4` branch
if ($this->db->DBDriver === 'SQLSRV') {
$this->markTestSkipped('SQLSRV does not support getFieldData() nullable.');
}

$this->forge->dropTable('forge_test_modify', true);

$this->forge->addField([
'col1' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
'col2' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
'col3' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
]);
$this->forge->createTable('forge_test_modify');

$this->forge->modifyColumn('forge_test_modify', [
'col1' => ['type' => 'VARCHAR', 'constraint' => 1],
'col2' => ['type' => 'VARCHAR', 'constraint' => 1, 'null' => true],
'col3' => ['type' => 'VARCHAR', 'constraint' => 1, 'null' => false],
]);

$this->db->resetDataCache();

$col1 = $this->getMetaData('col1', 'forge_test_modify');
$this->assertTrue($col1->nullable); // Nullable by default.
$col2 = $this->getMetaData('col2', 'forge_test_modify');
$this->assertTrue($col2->nullable);
$col3 = $this->getMetaData('col3', 'forge_test_modify');
$this->assertFalse($col3->nullable);

$this->forge->dropTable('forge_test_modify', true);
}

private function getMetaData(string $column, string $table): stdClass
{
$fields = $this->db->getFieldData($table);

$name = array_search(
$column,
array_column($fields, 'name'),
true
);

if ($name === false) {
throw new LogicException('Column not found: ' . $column);
}

return $fields[$name];
}

public function testConnectWithArrayGroup()
{
$group = config('Database');
Expand Down
9 changes: 9 additions & 0 deletions tests/system/Database/Migrations/MigrationRunnerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,9 @@ public function testLatestSuccess()

public function testRegressSuccess()
{
$forge = Database::forge();
$forge->dropTable('foo', true);

$runner = new MigrationRunner($this->config);
$runner->setSilent(false)
->setNamespace('Tests\Support\MigrationTestMigrations')
Expand All @@ -368,6 +371,9 @@ public function testRegressSuccess()

public function testLatestTriggersEvent()
{
$forge = Database::forge();
$forge->dropTable('foo', true);

$runner = new MigrationRunner($this->config);
$runner->setSilent(false)
->setNamespace('Tests\Support\MigrationTestMigrations');
Expand All @@ -387,6 +393,9 @@ public function testLatestTriggersEvent()

public function testRegressTriggersEvent()
{
$forge = Database::forge();
$forge->dropTable('foo', true);

$runner = new MigrationRunner($this->config);
$runner->setSilent(false)
->setNamespace('Tests\Support\MigrationTestMigrations');
Expand Down
15 changes: 15 additions & 0 deletions user_guide_src/source/changelogs/v4.3.4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ Redirect Status Code
always be used when you don't specify a status code. In previous versions,
302 might be changed.

.. _v434-forge-modifycolumn:

Forge::modifyColumn()
---------------------

- The :ref:`$forge->modifyColumn() <db-forge-modifyColumn>` has been fixed. Due
to a bug, in previous versions, SQLite3/Postgres/SQLSRV might change
``NULL``/``NOT NULL`` unpredictably.
- In previous versions, the OCI8 driver did not change ``NULL``/``NOT NULL``
when you don't specify the ``null`` key.
- Now in all database drivers ``$forge->modifyColumn()`` always sets ``NULL``
when you don't specify the ``null`` key.
- The ``NULL``/``NOT NULL`` change may still be unexpectedly, it is recommended
to always specify the ``null`` key.

Message Changes
***************

Expand Down
13 changes: 13 additions & 0 deletions user_guide_src/source/dbmgmt/forge.rst
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ Used to remove multiple columns from a table.
Modifying a Field in a Table
============================

.. _db-forge-modifyColumn:

$forge->modifyColumn()
----------------------

Expand All @@ -294,6 +296,17 @@ change the name, you can add a "name" key into the field defining array.

.. literalinclude:: forge/026.php

.. note:: The ``modifyColumn()`` may unexpectedly change ``NULL``/``NOT NULL``.
So it is recommended to always specify the value for ``null`` key. Unlike when creating
a table, if ``null`` is not specified, the column will be ``NULL``, not
``NOT NULL``.

.. note:: Due to a bug, prior v4.3.3, SQLite3 may not set ``NOT NULL`` even if you
specify ``'null' => false``.

.. note:: Due to a bug, prior v4.3.3, Postgres and SQLSRV set ``NOT NULL`` even
if you specify ``'null' => false``.

.. _db-forge-adding-keys-to-a-table:

Adding Keys to a Table
Expand Down
3 changes: 2 additions & 1 deletion user_guide_src/source/dbmgmt/forge/026.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
'old_name' => [
'name' => 'new_name',
'type' => 'TEXT',
'null' => false,
],
];
$forge->modifyColumn('table_name', $fields);
// gives ALTER TABLE `table_name` CHANGE `old_name` `new_name` TEXT
// gives ALTER TABLE `table_name` CHANGE `old_name` `new_name` TEXT NOT NULL
12 changes: 12 additions & 0 deletions user_guide_src/source/installation/upgrade_434.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ Redirect Status Code
:ref:`ChangeLog v4.3.4 <v434-redirect-status-code>` and if the code is not
what you want, :ref:`specify status codes <response-redirect-status-code>`.

Forge::modifyColumn() and NULL
==============================

A bug fix may have changed the NULL constraint in the result of
:ref:`$forge->modifyColumn() <db-forge-modifyColumn>`. See
:ref:`Change Log <v434-forge-modifycolumn>`.
To set the desired NULL constraint, change ``Forge::modifyColumn()`` to always
specify the ``null`` key.

Note that the bug may have changed unexpected NULL constraints in previous
versions.

Breaking Enhancements
*********************

Expand Down

0 comments on commit 90762f3

Please sign in to comment.