Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
4 changes: 2 additions & 2 deletions src/Db/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]);
Expand Down
4 changes: 2 additions & 2 deletions tests/TestCase/Db/Adapter/MysqlAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion tests/TestCase/Db/Adapter/SqliteAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
12 changes: 6 additions & 6 deletions tests/TestCase/Db/Table/TableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

/**
Expand Down Expand Up @@ -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());
}

/**
Expand Down Expand Up @@ -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()
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
235 changes: 235 additions & 0 deletions tests/comparisons/Diff/refresh_schema_dumps.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<?php
declare(strict_types=1);

/**
* # Check which Diff lock fixtures need refresh
* php tests/comparisons/Diff/refresh_schema_dumps.php
*
* # Regenerate all Diff lock fixtures in place
* php tests/comparisons/Diff/refresh_schema_dumps.php --write
*
* # Regenerate only one fixture
* php tests/comparisons/Diff/refresh_schema_dumps.php --write --file tests/comparisons/Diff/decimalChange/schema-dump-test_comparisons_mysql.lock
*/

require dirname(__DIR__, 3) . '/vendor/autoload.php';

const EXIT_OK = 0;
const EXIT_UPDATES_AVAILABLE = 1;
const EXIT_ERROR = 2;

/**
* Defaults for known typed properties that can be introduced between CakePHP releases.
*
* @var array<string, array<string, mixed>>
*/
$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<int, string> $selectedFiles
* @return array<int, string>
*/
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<string, array<string, mixed>> $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<int, ReflectionProperty>
*/
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];
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.