diff --git a/composer.json b/composer.json index 0d0597f15..1dac84d00 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ }, "require-dev": { "cakephp/bake": "^3.3", - "cakephp/cakephp": "^5.3.0", + "cakephp/cakephp": "^5.3.2", "cakephp/cakephp-codesniffer": "^5.0", "phpunit/phpunit": "^11.5.3 || ^12.1.3" }, diff --git a/src/Db/Table.php b/src/Db/Table.php index aeeb09066..711efff0d 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -729,8 +729,8 @@ public function addTimestamps(string|false|null $createdAt = 'created', string|f } if ($updatedAt) { $this->addColumn($updatedAt, $timestampType, [ - 'null' => true, - 'default' => null, + 'null' => false, + 'default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'timezone' => $withTimezone, ]); diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 85aded715..91cf6833f 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -576,8 +576,8 @@ public function testAddTimestampsFeatureFlag() $this->assertEquals('updated', $columns[2]->getName()); $this->assertEquals('datetime', $columns[2]->getType()); $this->assertContains($columns[2]->getUpdate(), ['CURRENT_TIMESTAMP', 'current_timestamp()']); - $this->assertTrue($columns[2]->isNull()); - $this->assertNull($columns[2]->getDefault()); + $this->assertFalse($columns[2]->isNull()); + $this->assertContains($columns[2]->getDefault(), ['CURRENT_TIMESTAMP', 'current_timestamp()']); } public function testCreateTableWithSchema() diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php index 2fd54a9bd..5b3c80fae 100644 --- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php @@ -2092,7 +2092,7 @@ public function testAlterTableColumnAdd() ['name' => 'string_col_2', 'type' => 'string', 'default' => null, 'null' => true], ['name' => 'string_col_3', 'type' => 'string', 'default' => null, 'null' => false], ['name' => 'created', 'type' => 'timestamp', 'default' => 'CURRENT_TIMESTAMP', 'null' => false], - ['name' => 'updated', 'type' => 'timestamp', 'default' => null, 'null' => true], + ['name' => 'updated', 'type' => 'timestamp', 'default' => 'CURRENT_TIMESTAMP', 'null' => false], ]; $this->assertEquals(count($expected), count($columns)); diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php index 21912fd58..7e400bea1 100644 --- a/tests/TestCase/Db/Table/TableTest.php +++ b/tests/TestCase/Db/Table/TableTest.php @@ -195,8 +195,8 @@ public function testAddTimestamps( $this->assertEquals('timestamp', $columns[1]->getType()); $this->assertEquals($withTimezone, $columns[1]->getTimezone()); $this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getUpdate()); - $this->assertTrue($columns[1]->isNull()); - $this->assertNull($columns[1]->getDefault()); + $this->assertFalse($columns[1]->isNull()); + $this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getDefault()); } /** @@ -246,8 +246,8 @@ public function testAddTimestampsNoCreated(AdapterInterface $adapter) $this->assertSame('timestamp', $columns[0]->getType()); $this->assertFalse($columns[0]->getTimezone()); $this->assertSame('CURRENT_TIMESTAMP', $columns[0]->getUpdate()); - $this->assertTrue($columns[0]->isNull()); - $this->assertNull($columns[0]->getDefault()); + $this->assertFalse($columns[0]->isNull()); + $this->assertSame('CURRENT_TIMESTAMP', $columns[0]->getDefault()); } /** @@ -299,8 +299,8 @@ public function testAddTimestampsWithTimezone( $this->assertEquals('timestamp', $columns[1]->getType()); $this->assertTrue($columns[1]->getTimezone()); $this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getUpdate()); - $this->assertTrue($columns[1]->isNull()); - $this->assertNull($columns[1]->getDefault()); + $this->assertFalse($columns[1]->isNull()); + $this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getDefault()); } public function testInsert() diff --git a/tests/comparisons/Diff/addRemove/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/addRemove/schema-dump-test_comparisons_mysql.lock index bd5dcdabf..567467f79 100644 Binary files a/tests/comparisons/Diff/addRemove/schema-dump-test_comparisons_mysql.lock and b/tests/comparisons/Diff/addRemove/schema-dump-test_comparisons_mysql.lock differ diff --git a/tests/comparisons/Diff/addRemove/schema-dump-test_comparisons_pgsql.lock b/tests/comparisons/Diff/addRemove/schema-dump-test_comparisons_pgsql.lock index c9926532b..0fda7cc04 100644 Binary files a/tests/comparisons/Diff/addRemove/schema-dump-test_comparisons_pgsql.lock and b/tests/comparisons/Diff/addRemove/schema-dump-test_comparisons_pgsql.lock differ diff --git a/tests/comparisons/Diff/decimalChange/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/decimalChange/schema-dump-test_comparisons_mysql.lock index 9c497e7b2..499dc7d32 100644 Binary files a/tests/comparisons/Diff/decimalChange/schema-dump-test_comparisons_mysql.lock and b/tests/comparisons/Diff/decimalChange/schema-dump-test_comparisons_mysql.lock differ diff --git a/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock index 10092ee02..58fa3362e 100644 Binary files a/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock and b/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock differ diff --git a/tests/comparisons/Diff/default/schema-dump-test_comparisons_pgsql.lock b/tests/comparisons/Diff/default/schema-dump-test_comparisons_pgsql.lock index 91799f2fb..399d95af1 100644 Binary files a/tests/comparisons/Diff/default/schema-dump-test_comparisons_pgsql.lock and b/tests/comparisons/Diff/default/schema-dump-test_comparisons_pgsql.lock differ diff --git a/tests/comparisons/Diff/refresh_schema_dumps.php b/tests/comparisons/Diff/refresh_schema_dumps.php new file mode 100644 index 000000000..075f709af --- /dev/null +++ b/tests/comparisons/Diff/refresh_schema_dumps.php @@ -0,0 +1,235 @@ +> + */ +$knownDefaults = [ + Cake\Database\Schema\Column::class => [ + 'fixed' => false, + ], +]; + +$options = getopt('', ['write', 'file:']); +$write = isset($options['write']); +$filesOption = $options['file'] ?? []; +$selectedFiles = is_array($filesOption) ? $filesOption : [$filesOption]; + +$files = getFixtureFiles(__DIR__, $selectedFiles); +if ($files === []) { + fwrite(STDERR, "No schema dump fixtures found.\n"); + exit(EXIT_ERROR); +} + +$updatedFiles = []; +$hasPendingUpdates = false; + +foreach ($files as $file) { + $serialized = file_get_contents($file); + if ($serialized === false) { + fwrite(STDERR, "Failed to read: {$file}\n"); + exit(EXIT_ERROR); + } + + $data = @unserialize($serialized); + if ($data === false && $serialized !== serialize(false)) { + fwrite(STDERR, "Failed to unserialize: {$file}\n"); + exit(EXIT_ERROR); + } + + $visited = new SplObjectStorage(); + fixUninitializedTypedProperties($data, $knownDefaults, $visited, $file); + + $normalized = serialize($data); + if ($normalized === $serialized) { + continue; + } + + $hasPendingUpdates = true; + if ($write) { + if (file_put_contents($file, $normalized) === false) { + fwrite(STDERR, "Failed to write: {$file}\n"); + exit(EXIT_ERROR); + } + $updatedFiles[] = $file; + echo "Updated {$file}\n"; + } else { + echo "Needs update {$file}\n"; + } +} + +if (!$hasPendingUpdates) { + echo "All schema dump fixtures are up to date.\n"; + exit(EXIT_OK); +} + +if (!$write) { + echo "Run with --write to regenerate fixtures.\n"; + exit(EXIT_UPDATES_AVAILABLE); +} + +echo sprintf("Updated %d fixture(s).\n", count($updatedFiles)); +exit(EXIT_OK); + +/** + * @param array $selectedFiles + * @return array + */ +function getFixtureFiles(string $baseDir, array $selectedFiles): array +{ + if ($selectedFiles !== []) { + $files = []; + foreach ($selectedFiles as $file) { + if ($file === '') { + continue; + } + $resolved = str_starts_with($file, '/') + ? $file + : realpath(getcwd() . DIRECTORY_SEPARATOR . $file); + if ($resolved === false || !is_file($resolved)) { + fwrite(STDERR, "Invalid --file path: {$file}\n"); + exit(EXIT_ERROR); + } + $files[] = $resolved; + } + + return $files; + } + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($baseDir)); + $files = []; + foreach ($iterator as $item) { + if (!$item->isFile()) { + continue; + } + $path = $item->getPathname(); + if (preg_match('/schema-dump-.*\.lock$/', $path) === 1) { + $files[] = $path; + } + } + sort($files); + + return $files; +} + +/** + * @param array> $knownDefaults + */ +function fixUninitializedTypedProperties( + mixed &$value, + array $knownDefaults, + SplObjectStorage $visited, + string $file +): void { + if (is_array($value)) { + foreach ($value as &$nested) { + fixUninitializedTypedProperties($nested, $knownDefaults, $visited, $file); + } + unset($nested); + + return; + } + + if (!is_object($value) || $visited->offsetExists($value)) { + return; + } + + $visited[$value] = true; + $class = new ReflectionClass($value); + $defaultsByClass = $knownDefaults[$class->getName()] ?? []; + + foreach (getAllProperties($class) as $property) { + if ($property->isStatic()) { + continue; + } + + if (!$property->isInitialized($value)) { + if (array_key_exists($property->getName(), $defaultsByClass)) { + $property->setValue($value, $defaultsByClass[$property->getName()]); + continue; + } + $inferred = inferDefaultValue($property); + if ($inferred['ok'] === false) { + $className = $class->getName(); + $propertyName = $property->getName(); + fwrite( + STDERR, + "Cannot infer default for uninitialized typed property " . + "{$className}::\${$propertyName} in {$file}\n" + ); + exit(EXIT_ERROR); + } + $property->setValue($value, $inferred['value']); + } + + $nested = $property->getValue($value); + fixUninitializedTypedProperties($nested, $knownDefaults, $visited, $file); + $property->setValue($value, $nested); + } +} + +/** + * @return array + */ +function getAllProperties(ReflectionClass $class): array +{ + $properties = []; + do { + foreach ($class->getProperties() as $property) { + $declaringClass = $property->getDeclaringClass()->getName(); + $name = $declaringClass . '::' . $property->getName(); + $properties[$name] = $property; + } + $class = $class->getParentClass(); + } while ($class !== false); + + return array_values($properties); +} + +/** + * @return array{ok: bool, value?: mixed} + */ +function inferDefaultValue(ReflectionProperty $property): array +{ + $type = $property->getType(); + if ($type === null) { + return ['ok' => true, 'value' => null]; + } + + if ($type->allowsNull()) { + return ['ok' => true, 'value' => null]; + } + + if ($type instanceof ReflectionNamedType) { + return match ($type->getName()) { + 'bool', 'false' => ['ok' => true, 'value' => false], + 'true' => ['ok' => true, 'value' => true], + 'int' => ['ok' => true, 'value' => 0], + 'float' => ['ok' => true, 'value' => 0.0], + 'string' => ['ok' => true, 'value' => ''], + 'array' => ['ok' => true, 'value' => []], + default => ['ok' => false], + }; + } + + return ['ok' => false]; +} diff --git a/tests/comparisons/Diff/simple/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/simple/schema-dump-test_comparisons_mysql.lock index 7aef17bcc..88789db9a 100644 Binary files a/tests/comparisons/Diff/simple/schema-dump-test_comparisons_mysql.lock and b/tests/comparisons/Diff/simple/schema-dump-test_comparisons_mysql.lock differ diff --git a/tests/comparisons/Diff/simple/schema-dump-test_comparisons_pgsql.lock b/tests/comparisons/Diff/simple/schema-dump-test_comparisons_pgsql.lock index 435ce5852..29007cb78 100644 Binary files a/tests/comparisons/Diff/simple/schema-dump-test_comparisons_pgsql.lock and b/tests/comparisons/Diff/simple/schema-dump-test_comparisons_pgsql.lock differ diff --git a/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock index 4744181c9..67884e41e 100644 Binary files a/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock and b/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock differ diff --git a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock index 4744181c9..67884e41e 100644 Binary files a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock and b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock differ diff --git a/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock index 522a9be3d..9e291c670 100644 Binary files a/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock and b/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/schema-dump-test_comparisons_mysql.lock differ