silverstripe-framework/tests/model/VersionedTest.php
Fred Condo b88a0955a5 API: Support string descriptors for unique indexes in Versioned
- Document the format for descriptor arrays
- Implement the behaviour that developers have come to expect for
  string descriptors of indexes
- Add test for handling of unique indexes (MySQL & sqlite3)
- Resolve #2403

Versioned needs to convert unique indexes to non-unique for its suffixed
tables, such as Foo_Live and Foo_versions. Because DataObject accepts
string descriptors such as array('UniqIDX' => 'unique (Uniq)') as well
as array-based descriptors, Versioned needs to recognize string
descriptors. This patch accomplishes that. Before, Versioned would fail
to convert string-described indexes to non-unique, resulting in run-time
errors when creating a new version of an object.
2013-12-06 10:02:44 -08:00

643 lines
21 KiB
PHP

<?php
/**
* @package framework
* @subpackage tests
*/
class VersionedTest extends SapphireTest {
protected static $fixture_file = 'VersionedTest.yml';
protected $extraDataObjects = array(
'VersionedTest_DataObject',
'VersionedTest_Subclass',
'VersionedTest_RelatedWithoutVersion',
'VersionedTest_SingleStage',
'VersionedTest_WithIndexes',
);
protected $requiredExtensions = array(
"VersionedTest_DataObject" => array('Versioned'),
"VersionedTest_WithIndexes" => array('Versioned'),
);
public function testUniqueIndexes() {
$table_expectations = array(
'VersionedTest_WithIndexes' =>
array('value' => 1, 'message' => 'Unique indexes are unique in main table'),
'VersionedTest_WithIndexes_versions' =>
array('value' => 0, 'message' => 'Unique indexes are no longer unique in _versions table'),
'VersionedTest_WithIndexes_Live' =>
array('value' => 0, 'message' => 'Unique indexes are no longer unique in _Live table'),
);
// Check for presence of all unique indexes
$db = DB::getConn();
$db_class = get_class($db);
$tables = array_keys($table_expectations);
switch ($db_class) {
case 'MySQLDatabase':
$our_indexes = array('UniqA_idx', 'UniqS_idx');
foreach ($tables as $t) {
$indexes = array_keys($db->indexList($t));
sort($indexes);
$this->assertEquals(
array_values($our_indexes), array_values(array_intersect($indexes, $our_indexes)),
"$t has both indexes");
}
break;
case 'SQLite3Database':
$our_indexes = array('"UniqA"', '"UniqS"');
foreach ($tables as $t) {
$indexes = array_values($db->indexList($t));
sort($indexes);
$this->assertEquals(array_values($our_indexes),
array_values(array_intersect(array_values($indexes), $our_indexes)), "$t has both indexes");
}
break;
default:
$this->markTestSkipped("Test for DBMS $db_class not implemented; skipped.");
break;
}
// Check unique -> non-unique conversion
foreach ($table_expectations as $table_name => $expectation) {
$indexes = $db->indexList($table_name);
foreach ($indexes as $idx_name => $idx_value) {
if (in_array($idx_name, $our_indexes)) {
$match_value = preg_match('/unique/', $idx_value);
if (false === $match_value) {
user_error('preg_match failure');
}
$this->assertEquals($match_value, $expectation['value'], $expectation['message']);
}
}
}
}
public function testDeletingOrphanedVersions() {
$obj = new VersionedTest_Subclass();
$obj->ExtraField = 'Foo'; // ensure that child version table gets written
$obj->write();
$obj->publish('Stage', 'Live');
$obj->ExtraField = 'Bar'; // ensure that child version table gets written
$obj->write();
$obj->publish('Stage', 'Live');
$versions = DB::query("SELECT COUNT(*) FROM \"VersionedTest_Subclass_versions\""
. " WHERE \"RecordID\" = '$obj->ID'")->value();
$this->assertGreaterThan(0, $versions, 'At least 1 version exists in the history of the page');
// Force orphaning of all versions created earlier, only on parent record.
// The child versiones table should still have the correct relationship
DB::query("DELETE FROM \"VersionedTest_DataObject_versions\" WHERE \"RecordID\" = $obj->ID");
// insert a record with no primary key (ID)
DB::query("INSERT INTO \"VersionedTest_DataObject_versions\" (\"RecordID\") VALUES ($obj->ID)");
// run the script which should clean that up
$obj->augmentDatabase();
$versions = DB::query("SELECT COUNT(*) FROM \"VersionedTest_Subclass_versions\""
. " WHERE \"RecordID\" = '$obj->ID'")->value();
$this->assertEquals(0, $versions, 'Orphaned versions on child tables are removed');
// test that it doesn't delete records that we need
$obj->write();
$obj->publish('Stage', 'Live');
$count = DB::query("SELECT COUNT(*) FROM \"VersionedTest_Subclass_versions\""
. " WHERE \"RecordID\" = '$obj->ID'")->value();
$obj->augmentDatabase();
$count2 = DB::query("SELECT COUNT(*) FROM \"VersionedTest_Subclass_versions\""
. " WHERE \"RecordID\" = '$obj->ID'")->value();
$this->assertEquals($count, $count2);
}
public function testForceChangeUpdatesVersion() {
$obj = new VersionedTest_DataObject();
$obj->Name = "test";
$obj->write();
$oldVersion = $obj->Version;
$obj->forceChange();
$obj->write();
$this->assertTrue(
($obj->Version > $oldVersion),
"A object Version is increased when just calling forceChange() without any other changes"
);
}
/**
* Test Versioned::get_including_deleted()
*/
public function testGetIncludingDeleted() {
// Get all ids of pages
$allPageIDs = DataObject::get(
'VersionedTest_DataObject',
"\"ParentID\" = 0", "\"VersionedTest_DataObject\".\"ID\" ASC"
)->column('ID');
// Modify a page, ensuring that the Version ID and Record ID will differ,
// and then subsequently delete it
$targetPage = $this->objFromFixture('VersionedTest_DataObject', 'page3');
$targetPage->Content = 'To be deleted';
$targetPage->write();
$targetPage->delete();
// Get all items, ignoring deleted
$remainingPages = DataObject::get("VersionedTest_DataObject", "\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC");
// Check that page 3 has gone
$this->assertNotNull($remainingPages);
$this->assertEquals(array("Page 1", "Page 2"), $remainingPages->column('Title'));
// Get all including deleted
$allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC");
// Check that page 3 is still there
$this->assertEquals(array("Page 1", "Page 2", "Page 3"), $allPages->column('Title'));
// Check that the returned pages have the correct IDs
$this->assertEquals($allPageIDs, $allPages->column('ID'));
// Check that this still works if we switch to reading the other stage
Versioned::reading_stage("Live");
$allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC");
$this->assertEquals(array("Page 1", "Page 2", "Page 3"), $allPages->column('Title'));
// Check that the returned pages still have the correct IDs
$this->assertEquals($allPageIDs, $allPages->column('ID'));
}
public function testVersionedFieldsAdded() {
$obj = new VersionedTest_DataObject();
// Check that the Version column is added as a full-fledged column
$this->assertInstanceOf('Int', $obj->dbObject('Version'));
$obj2 = new VersionedTest_Subclass();
// Check that the Version column is added as a full-fledged column
$this->assertInstanceOf('Int', $obj2->dbObject('Version'));
}
public function testVersionedFieldsNotInCMS() {
$obj = new VersionedTest_DataObject();
// the version field in cms causes issues with Versioned::augmentWrite()
$this->assertNull($obj->getCMSFields()->dataFieldByName('Version'));
}
public function testPublishCreateNewVersion() {
$page1 = $this->objFromFixture('VersionedTest_DataObject', 'page1');
$page1->Content = 'orig';
$page1->write();
$oldVersion = $page1->Version;
$page1->publish('Stage', 'Live', false);
$this->assertEquals($oldVersion, $page1->Version, 'publish() with $createNewVersion=FALSE');
$page1->Content = 'changed';
$page1->write();
$oldVersion = $page1->Version;
$page1->publish('Stage', 'Live', true);
$this->assertTrue($oldVersion < $page1->Version, 'publish() with $createNewVersion=TRUE');
}
public function testRollbackTo() {
$page1 = $this->objFromFixture('VersionedTest_DataObject', 'page1');
$page1->Content = 'orig';
$page1->write();
$page1->publish('Stage', 'Live');
$origVersion = $page1->Version;
$page1->Content = 'changed';
$page1->write();
$page1->publish('Stage', 'Live');
$changedVersion = $page1->Version;
$page1->doRollbackTo($origVersion);
$page1 = Versioned::get_one_by_stage('VersionedTest_DataObject', 'Stage',
sprintf('"VersionedTest_DataObject"."ID" = %d', $page1->ID));
$this->assertTrue($page1->Version > $changedVersion, 'Create a new higher version number');
$this->assertEquals('orig', $page1->Content, 'Copies the content from the old version');
}
public function testDeleteFromStage() {
$page1 = $this->objFromFixture('VersionedTest_DataObject', 'page1');
$pageID = $page1->ID;
$page1->Content = 'orig';
$page1->write();
$page1->publish('Stage', 'Live');
$this->assertEquals(1,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(1,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject_Live" WHERE "ID" = '.$pageID)->value());
$page1->deleteFromStage('Live');
// Confirm that deleteFromStage() doesn't manipulate the original record
$this->assertEquals($pageID, $page1->ID);
$this->assertEquals(1,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(0,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject_Live" WHERE "ID" = '.$pageID)->value());
$page1->delete();
$this->assertEquals(0, $page1->ID);
$this->assertEquals(0,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject" WHERE "ID" = '.$pageID)->value());
$this->assertEquals(0,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject_Live" WHERE "ID" = '.$pageID)->value());
}
public function testWritingNewToStage() {
$origStage = Versioned::current_stage();
Versioned::reading_stage("Stage");
$page = new VersionedTest_DataObject();
$page->Title = "testWritingNewToStage";
$page->URLSegment = "testWritingNewToStage";
$page->write();
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live',
"\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
$this->assertEquals(0, $live->count());
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage',
"\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
$this->assertEquals(1, $stage->count());
$this->assertEquals($stage->First()->Title, 'testWritingNewToStage');
Versioned::reading_stage($origStage);
}
/**
* This tests for the situation described in the ticket #5596.
* Writing new Page to live first creates a row in VersionedTest_DataObject table (to get the new ID),
* then "changes it's mind" in Versioned and writes VersionedTest_DataObject_Live. It does not remove
* the VersionedTest_DataObject record though.
*/
public function testWritingNewToLive() {
$origStage = Versioned::current_stage();
Versioned::reading_stage("Live");
$page = new VersionedTest_DataObject();
$page->Title = "testWritingNewToLive";
$page->URLSegment = "testWritingNewToLive";
$page->write();
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live',
"\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
$this->assertEquals(1, $live->count());
$this->assertEquals($live->First()->Title, 'testWritingNewToLive');
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage',
"\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
$this->assertEquals(0, $stage->count());
Versioned::reading_stage($origStage);
}
/**
* Tests DataObject::hasOwnTableDatabaseField
*/
public function testHasOwnTableDatabaseFieldWithVersioned() {
$noversion = new DataObject();
$versioned = new VersionedTest_DataObject();
$versionedSub = new VersionedTest_Subclass();
$versionField = new VersionedTest_UnversionedWithField();
$this->assertFalse(
(bool) $noversion->hasOwnTableDatabaseField('Version'),
'Plain models have no version field.'
);
$this->assertEquals(
'Int', $versioned->hasOwnTableDatabaseField('Version'),
'The versioned ext adds an Int version field.'
);
$this->assertEquals(
'Int', $versionedSub->hasOwnTableDatabaseField('Version'),
'Sub-classes of a versioned model have a Version field.'
);
$this->assertEquals(
'Varchar', $versionField->hasOwnTableDatabaseField('Version'),
'Models w/o Versioned can have their own Version field.'
);
}
/**
* Test that SQLQuery::queriedTables() applies the version-suffixes properly.
*/
public function testQueriedTables() {
Versioned::reading_stage('Live');
$this->assertEquals(array(
'VersionedTest_DataObject_Live',
'VersionedTest_Subclass_Live',
), DataObject::get('VersionedTest_Subclass')->dataQuery()->query()->queriedTables());
}
public function testGetVersionWhenClassnameChanged() {
$obj = new VersionedTest_DataObject;
$obj->Name = "test";
$obj->write();
$obj->Name = "test2";
$obj->ClassName = "VersionedTest_Subclass";
$obj->write();
$subclassVersion = $obj->Version;
$obj->Name = "test3";
$obj->ClassName = "VersionedTest_DataObject";
$obj->write();
// We should be able to pass the subclass and still get the correct class back
$obj2 = Versioned::get_version("VersionedTest_Subclass", $obj->ID, $subclassVersion);
$this->assertInstanceOf("VersionedTest_Subclass", $obj2);
$this->assertEquals("test2", $obj2->Name);
$obj3 = Versioned::get_latest_version("VersionedTest_Subclass", $obj->ID);
$this->assertEquals("test3", $obj3->Name);
$this->assertInstanceOf("VersionedTest_DataObject", $obj3);
}
public function testArchiveVersion() {
// In 2005 this file was created
SS_Datetime::set_mock_now('2005-01-01 00:00:00');
$testPage = new VersionedTest_Subclass();
$testPage->Title = 'Archived page';
$testPage->Content = 'This is the content from 2005';
$testPage->ExtraField = '2005';
$testPage->write();
// In 2007 we updated it
SS_Datetime::set_mock_now('2007-01-01 00:00:00');
$testPage->Content = "It's 2007 already!";
$testPage->ExtraField = '2007';
$testPage->write();
// In 2009 we updated it again
SS_Datetime::set_mock_now('2009-01-01 00:00:00');
$testPage->Content = "I'm enjoying 2009";
$testPage->ExtraField = '2009';
$testPage->write();
// End mock, back to the present day:)
SS_Datetime::clear_mock_now();
// Test 1 - 2006 Content
singleton('VersionedTest_Subclass')->flushCache(true);
Versioned::set_reading_mode('Archive.2006-01-01 00:00:00');
$testPage2006 = DataObject::get('VersionedTest_Subclass')->filter(array('Title' => 'Archived page'))->first();
$this->assertInstanceOf("VersionedTest_Subclass", $testPage2006);
$this->assertEquals("2005", $testPage2006->ExtraField);
$this->assertEquals("This is the content from 2005", $testPage2006->Content);
// Test 2 - 2008 Content
singleton('VersionedTest_Subclass')->flushCache(true);
Versioned::set_reading_mode('Archive.2008-01-01 00:00:00');
$testPage2008 = DataObject::get('VersionedTest_Subclass')->filter(array('Title' => 'Archived page'))->first();
$this->assertInstanceOf("VersionedTest_Subclass", $testPage2008);
$this->assertEquals("2007", $testPage2008->ExtraField);
$this->assertEquals("It's 2007 already!", $testPage2008->Content);
// Test 3 - Today
singleton('VersionedTest_Subclass')->flushCache(true);
Versioned::set_reading_mode('Stage.Stage');
$testPageCurrent = DataObject::get('VersionedTest_Subclass')->filter(array('Title' => 'Archived page'))
->first();
$this->assertInstanceOf("VersionedTest_Subclass", $testPageCurrent);
$this->assertEquals("2009", $testPageCurrent->ExtraField);
$this->assertEquals("I'm enjoying 2009", $testPageCurrent->Content);
}
public function testAllVersions()
{
// In 2005 this file was created
SS_Datetime::set_mock_now('2005-01-01 00:00:00');
$testPage = new VersionedTest_Subclass();
$testPage->Title = 'Archived page';
$testPage->Content = 'This is the content from 2005';
$testPage->ExtraField = '2005';
$testPage->write();
// In 2007 we updated it
SS_Datetime::set_mock_now('2007-01-01 00:00:00');
$testPage->Content = "It's 2007 already!";
$testPage->ExtraField = '2007';
$testPage->write();
// Check both versions are returned
$versions = Versioned::get_all_versions('VersionedTest_Subclass', $testPage->ID);
$content = array();
$extraFields = array();
foreach($versions as $version)
{
$content[] = $version->Content;
$extraFields[] = $version->ExtraField;
}
$this->assertEquals($versions->Count(), 2, 'All versions returned');
$this->assertEquals($content, array('This is the content from 2005', "It's 2007 already!"),
'Version fields returned');
$this->assertEquals($extraFields, array('2005', '2007'), 'Version fields returned');
// In 2009 we updated it again
SS_Datetime::set_mock_now('2009-01-01 00:00:00');
$testPage->Content = "I'm enjoying 2009";
$testPage->ExtraField = '2009';
$testPage->write();
// End mock, back to the present day:)
SS_Datetime::clear_mock_now();
$versions = Versioned::get_all_versions('VersionedTest_Subclass', $testPage->ID);
$content = array();
$extraFields = array();
foreach($versions as $version)
{
$content[] = $version->Content;
$extraFields[] = $version->ExtraField;
}
$this->assertEquals($versions->Count(), 3, 'Additional all versions returned');
$this->assertEquals($content,
array('This is the content from 2005', "It's 2007 already!", "I'm enjoying 2009"),
'Additional version fields returned');
$this->assertEquals($extraFields, array('2005', '2007', '2009'), 'Additional version fields returned');
}
public function testArchiveRelatedDataWithoutVersioned() {
SS_Datetime::set_mock_now('2009-01-01 00:00:00');
$relatedData = new VersionedTest_RelatedWithoutVersion();
$relatedData->Name = 'Related Data';
$relatedDataId = $relatedData->write();
$testData = new VersionedTest_DataObject();
$testData->Title = 'Test';
$testData->Content = 'Before Content';
$testData->Related()->add($relatedData);
$id = $testData->write();
SS_Datetime::set_mock_now('2010-01-01 00:00:00');
$testData->Content = 'After Content';
$testData->write();
$_GET['archiveDate'] = '2009-01-01 19:00:00';
Versioned::reading_archived_date('2009-01-01 19:00:00');
$fetchedData = VersionedTest_DataObject::get()->byId($id);
$this->assertEquals('Before Content', $fetchedData->Content, 'We see the correct content of the older version');
$relatedData = VersionedTest_RelatedWithoutVersion::get()->byId($relatedDataId);
$this->assertEquals(
1,
$relatedData->Related()->count(),
'We have a relation, with no version table, querying it still works'
);
}
public function testVersionedWithSingleStage() {
$tables = DB::tableList();
$this->assertContains(
'VersionedTest_SingleStage',
array_values($tables),
'Contains base table'
);
$this->assertContains(
'VersionedTest_SingleStage_versions',
array_values($tables),
'Contains versions table'
);
$this->assertNotContains(
'VersionedTest_SingleStage_Live',
array_values($tables),
'Does not contain separate table with _Live suffix'
);
$this->assertNotContains(
'VersionedTest_SingleStage_Stage',
array_values($tables),
'Does not contain separate table with _Stage suffix'
);
Versioned::reading_stage("Stage");
$obj = new VersionedTest_SingleStage(array('Name' => 'MyObj'));
$obj->write();
$this->assertNotNull(
VersionedTest_SingleStage::get()->byID($obj->ID),
'Writes to and reads from default stage if its set explicitly'
);
Versioned::reading_stage("Live");
$obj = new VersionedTest_SingleStage(array('Name' => 'MyObj'));
$obj->write();
$this->assertNotNull(
VersionedTest_SingleStage::get()->byID($obj->ID),
'Writes to and reads from default stage even if a non-matching stage is set'
);
}
}
/**
* @package framework
* @subpackage tests
*/
class VersionedTest_DataObject extends DataObject implements TestOnly {
private static $db = array(
"Name" => "Varchar",
'Title' => 'Varchar',
'Content' => 'HTMLText'
);
private static $extensions = array(
"Versioned('Stage', 'Live')"
);
private static $has_one = array(
'Parent' => 'VersionedTest_DataObject'
);
private static $many_many = array(
'Related' => 'VersionedTest_RelatedWithoutVersion'
);
}
class VersionedTest_WithIndexes extends DataObject implements TestOnly {
private static $db = array(
'UniqA' => 'Int',
'UniqS' => 'Int',
);
private static $extensions = array(
"Versioned('Stage', 'Live')"
);
private static $indexes = array(
'UniqS_idx' => 'unique ("UniqS")',
'UniqA_idx' => array('type' => 'unique', 'name' => 'UniqA_idx', 'value' => '"UniqA"',),
);
}
/**
* @package framework
* @subpackage tests
*/
class VersionedTest_RelatedWithoutVersion extends DataObject implements TestOnly {
private static $db = array(
'Name' => 'Varchar'
);
private static $belongs_many_many = array(
'Related' => 'VersionedTest_DataObject'
);
}
/**
* @package framework
* @subpackage tests
*/
class VersionedTest_Subclass extends VersionedTest_DataObject implements TestOnly {
private static $db = array(
"ExtraField" => "Varchar",
);
private static $extensions = array(
"Versioned('Stage', 'Live')"
);
}
/**
* @package framework
* @subpackage tests
*/
class VersionedTest_UnversionedWithField extends DataObject implements TestOnly {
private static $db = array('Version' => 'Varchar(255)');
}
class VersionedTest_SingleStage extends DataObject implements TestOnly {
private static $db = array(
'Name' => 'Varchar'
);
private static $extensions = array(
'Versioned("Stage")'
);
}