API Refactor dataobject schema management into separate service

API Allow table_name to be configured via Config
This commit is contained in:
Damian Mooyman 2016-05-25 17:09:29 +12:00
parent 19a27d22a3
commit 5e8ae41d47
42 changed files with 1658 additions and 946 deletions

View File

@ -271,7 +271,7 @@ JSON;
* @return array * @return array
*/ */
protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) { protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) {
$baseClass = ClassInfo::baseDataClass($changeSetItem->ObjectClass); $baseClass = DataObject::getSchema()->baseDataClass($changeSetItem->ObjectClass);
$baseSingleton = DataObject::singleton($baseClass); $baseSingleton = DataObject::singleton($baseClass);
$thumbnailWidth = (int)$this->config()->thumbnail_width; $thumbnailWidth = (int)$this->config()->thumbnail_width;
$thumbnailHeight = (int)$this->config()->thumbnail_height; $thumbnailHeight = (int)$this->config()->thumbnail_height;

View File

@ -268,7 +268,7 @@ class Director implements TemplateGlobalProvider {
// These are needed so that calling Director::test() does not muck with whoever is calling it. // These are needed so that calling Director::test() does not muck with whoever is calling it.
// Really, it's some inappropriate coupling and should be resolved by making less use of statics. // Really, it's some inappropriate coupling and should be resolved by making less use of statics.
$oldStage = Versioned::get_stage(); $oldReadingMode = Versioned::get_reading_mode();
$getVars = array(); $getVars = array();
if (!$httpMethod) $httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET"; if (!$httpMethod) $httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET";
@ -294,7 +294,7 @@ class Director implements TemplateGlobalProvider {
// Set callback to invoke prior to return // Set callback to invoke prior to return
$onCleanup = function() use( $onCleanup = function() use(
$existingRequestVars, $existingGetVars, $existingPostVars, $existingSessionVars, $existingRequestVars, $existingGetVars, $existingPostVars, $existingSessionVars,
$existingCookies, $existingServer, $existingRequirementsBackend, $oldStage $existingCookies, $existingServer, $existingRequirementsBackend, $oldReadingMode
) { ) {
// Restore the super globals // Restore the super globals
$_REQUEST = $existingRequestVars; $_REQUEST = $existingRequestVars;
@ -308,7 +308,7 @@ class Director implements TemplateGlobalProvider {
// These are needed so that calling Director::test() does not muck with whoever is calling it. // These are needed so that calling Director::test() does not muck with whoever is calling it.
// Really, it's some inappropriate coupling and should be resolved by making less use of statics // Really, it's some inappropriate coupling and should be resolved by making less use of statics
Versioned::set_stage($oldStage); Versioned::set_reading_mode($oldReadingMode);
Injector::unnest(); // Restore old CookieJar, etc Injector::unnest(); // Restore old CookieJar, etc
Config::unnest(); Config::unnest();

View File

@ -78,8 +78,9 @@ class ClassInfo {
* Returns an array of the current class and all its ancestors and children * Returns an array of the current class and all its ancestors and children
* which require a DB table. * which require a DB table.
* *
* @todo Move this into {@see DataObjectSchema}
*
* @param string|object $class * @param string|object $class
* @todo Move this into data object
* @return array * @return array
*/ */
public static function dataClassesFor($class) { public static function dataClassesFor($class) {
@ -104,28 +105,11 @@ class ClassInfo {
} }
/** /**
* Returns the root class (the first to extend from DataObject) for the * @deprecated 4.0..5.0
* passed class.
*
* @param string|object $class
* @return string
*/ */
public static function baseDataClass($class) { public static function baseDataClass($class) {
if(is_string($class) && !class_exists($class)) return null; Deprecation::notice('5.0', 'Use DataObject::getSchema()->baseDataClass()');
return DataObject::getSchema()->baseDataClass($class);
$class = self::class_name($class);
if (!is_subclass_of($class, 'DataObject')) {
throw new InvalidArgumentException("$class is not a subclass of DataObject");
}
while ($next = get_parent_class($class)) {
if ($next == 'DataObject') {
return $class;
}
$class = $next;
}
} }
/** /**
@ -174,8 +158,6 @@ class ClassInfo {
public static function class_name($nameOrObject) { public static function class_name($nameOrObject) {
if (is_object($nameOrObject)) { if (is_object($nameOrObject)) {
return get_class($nameOrObject); return get_class($nameOrObject);
} elseif (!self::exists($nameOrObject)) {
throw new InvalidArgumentException("Class {$nameOrObject} doesn't exist");
} }
$reflection = new ReflectionClass($nameOrObject); $reflection = new ReflectionClass($nameOrObject);
return $reflection->getName(); return $reflection->getName();
@ -291,53 +273,12 @@ class ClassInfo {
return strtolower(self::$method_from_cache[$lClass][$lMethod]) == $lCompclass; return strtolower(self::$method_from_cache[$lClass][$lMethod]) == $lCompclass;
} }
/** /**
* Returns the table name in the class hierarchy which contains a given * @deprecated 4.0..5.0
* field column for a {@link DataObject}. If the field does not exist, this
* will return null.
*
* @param string $candidateClass
* @param string $fieldName
*
* @return string
*/ */
public static function table_for_object_field($candidateClass, $fieldName) { public static function table_for_object_field($candidateClass, $fieldName) {
if(!$candidateClass Deprecation::notice('5.0', 'Use DataObject::getSchema()->tableForField()');
|| !$fieldName return DataObject::getSchema()->tableForField($candidateClass, $fieldName);
|| !class_exists($candidateClass)
|| !is_subclass_of($candidateClass, 'DataObject')
) {
return null;
}
//normalise class name
$candidateClass = self::class_name($candidateClass);
$exists = self::exists($candidateClass);
// Short circuit for fixed fields
$fixed = DataObject::config()->fixed_fields;
if($exists && isset($fixed[$fieldName])) {
return self::baseDataClass($candidateClass);
}
// Find regular field
while($candidateClass && $candidateClass != 'DataObject' && $exists) {
if( DataObject::has_own_table($candidateClass)
&& DataObject::has_own_table_database_field($candidateClass, $fieldName)
) {
break;
}
$candidateClass = get_parent_class($candidateClass);
$exists = $candidateClass && self::exists($candidateClass);
}
if(!$candidateClass || !$exists) {
return null;
}
return $candidateClass;
} }
} }

View File

@ -176,19 +176,13 @@ class Convert {
* table, or column name. Supports encoding of multi identfiers separated by * table, or column name. Supports encoding of multi identfiers separated by
* a delimiter (e.g. ".") * a delimiter (e.g. ".")
* *
* @param string|array $identifier The identifier to escape. E.g. 'SiteTree.Title' * @param string|array $identifier The identifier to escape. E.g. 'SiteTree.Title' or list of identifiers
* to be joined via the separator.
* @param string $separator The string that delimits subsequent identifiers * @param string $separator The string that delimits subsequent identifiers
* @return string|array The escaped identifier. E.g. '"SiteTree"."Title"' * @return string The escaped identifier. E.g. '"SiteTree"."Title"'
*/ */
public static function symbol2sql($identifier, $separator = '.') { public static function symbol2sql($identifier, $separator = '.') {
if(is_array($identifier)) { return DB::get_conn()->escapeIdentifier($identifier, $separator);
foreach($identifier as $k => $v) {
$identifier[$k] = self::symbol2sql($v, $separator);
}
return $identifier;
} else {
return DB::get_conn()->escapeIdentifier($identifier, $separator);
}
} }
/** /**

View File

@ -59,13 +59,14 @@ class FixtureBlueprint {
} }
/** /**
* @param String $identifier Unique identifier for this fixture type * @param string $identifier Unique identifier for this fixture type
* @param Array $data Map of property names to their values. * @param array $data Map of property names to their values.
* @param Array $fixtures Map of fixture names to an associative array of their in-memory * @param array $fixtures Map of fixture names to an associative array of their in-memory
* identifiers mapped to their database IDs. Used to look up * identifiers mapped to their database IDs. Used to look up
* existing fixtures which might be referenced in the $data attribute * existing fixtures which might be referenced in the $data attribute
* via the => notation. * via the => notation.
* @return DataObject * @return DataObject
* @throws Exception
*/ */
public function createObject($identifier, $data = null, $fixtures = null) { public function createObject($identifier, $data = null, $fixtures = null) {
// We have to disable validation while we import the fixtures, as the order in // We have to disable validation while we import the fixtures, as the order in
@ -89,12 +90,13 @@ class FixtureBlueprint {
// The database needs to allow inserting values into the foreign key column (ID in our case) // The database needs to allow inserting values into the foreign key column (ID in our case)
$conn = DB::get_conn(); $conn = DB::get_conn();
$baseTable = DataObject::getSchema()->baseDataTable($class);
if(method_exists($conn, 'allowPrimaryKeyEditing')) { if(method_exists($conn, 'allowPrimaryKeyEditing')) {
$conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), true); $conn->allowPrimaryKeyEditing($baseTable, true);
} }
$obj->write(false, true); $obj->write(false, true);
if(method_exists($conn, 'allowPrimaryKeyEditing')) { if(method_exists($conn, 'allowPrimaryKeyEditing')) {
$conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), false); $conn->allowPrimaryKeyEditing($baseTable, false);
} }
} }
@ -206,7 +208,8 @@ class FixtureBlueprint {
} }
/** /**
* @param Array $defaults * @param array $defaults
* @return $this
*/ */
public function setDefaults($defaults) { public function setDefaults($defaults) {
$this->defaults = $defaults; $this->defaults = $defaults;
@ -214,14 +217,14 @@ class FixtureBlueprint {
} }
/** /**
* @return Array * @return array
*/ */
public function getDefaults() { public function getDefaults() {
return $this->defaults; return $this->defaults;
} }
/** /**
* @return String * @return string
*/ */
public function getClass() { public function getClass() {
return $this->class; return $this->class;
@ -230,8 +233,9 @@ class FixtureBlueprint {
/** /**
* See class documentation. * See class documentation.
* *
* @param String $type * @param string $type
* @param callable $callback * @param callable $callback
* @return $this
*/ */
public function addCallback($type, $callback) { public function addCallback($type, $callback) {
if(!array_key_exists($type, $this->callbacks)) { if(!array_key_exists($type, $this->callbacks)) {
@ -243,12 +247,15 @@ class FixtureBlueprint {
} }
/** /**
* @param String $type * @param string $type
* @param callable $callback * @param callable $callback
* @return $this
*/ */
public function removeCallback($type, $callback) { public function removeCallback($type, $callback) {
$pos = array_search($callback, $this->callbacks[$type]); $pos = array_search($callback, $this->callbacks[$type]);
if($pos !== false) unset($this->callbacks[$type][$pos]); if($pos !== false) {
unset($this->callbacks[$type][$pos]);
}
return $this; return $this;
} }
@ -263,7 +270,7 @@ class FixtureBlueprint {
* Parse a value from a fixture file. If it starts with => * Parse a value from a fixture file. If it starts with =>
* it will get an ID from the fixture dictionary * it will get an ID from the fixture dictionary
* *
* @param string $fieldVal * @param string $value
* @param array $fixtures See {@link createObject()} * @param array $fixtures See {@link createObject()}
* @param string $class If the value parsed is a class relation, this parameter * @param string $class If the value parsed is a class relation, this parameter
* will be given the value of that class's name * will be given the value of that class's name
@ -293,13 +300,16 @@ class FixtureBlueprint {
} }
protected function overrideField($obj, $fieldName, $value, $fixtures = null) { protected function overrideField($obj, $fieldName, $value, $fixtures = null) {
$table = ClassInfo::table_for_object_field(get_class($obj), $fieldName); $class = get_class($obj);
$table = DataObject::getSchema()->tableForField($class, $fieldName);
$value = $this->parseValue($value, $fixtures); $value = $this->parseValue($value, $fixtures);
DB::manipulate(array( DB::manipulate(array(
$table => array( $table => array(
"command" => "update", "id" => $obj->ID, "command" => "update",
"fields" => array($fieldName => $value) "id" => $obj->ID,
"class" => $class,
"fields" => array($fieldName => $value),
) )
)); ));
$obj->$fieldName = $value; $obj->$fieldName = $value;

View File

@ -489,6 +489,59 @@ offset, if not provided as an argument, will default to 0.
Note that the `limit` argument order is different from a MySQL LIMIT clause. Note that the `limit` argument order is different from a MySQL LIMIT clause.
</div> </div>
### Mapping classes to tables with DataObjectSchema
Note that in most cases, the underlying database table for any DataObject instance will be the same as the class name.
However in cases where dealing with namespaced classes, especially when using DB schema which don't support
slashes in table names, it is necessary to provide an alternate mapping.
For instance, the below model will be stored in the table name `BannerImage`
:::php
namespace SilverStripe\BannerManager;
class BannerImage extends \DataObject {
private static $table_name = 'BannerImage';
}
Note that any model class which does not explicitly declare a `table_name` config option will have a name
automatically generated for them. In the above case, the table name would have been
`SilverStripe\BannerManager\BannerImage`
When creating raw SQL queries that contain table names, it is necessary to ensure your queries have the correct
table. This functionality can be provided by the [api:DataObjectSchema] service, which can be accessed via
`DataObject::getSchema()`. This service provides the following methods, most of which have a table and class
equivalent version.
Methods which return class names:
* `tableClass($table)` Finds the class name for a given table. This also handles suffixed tables such as `Table_Live`.
* `baseDataClass($class)` Returns the base data class for the given class.
* `classForField($class, $field)` Finds the specific class that directly holds the given field
Methods which return table names:
* `tableName($class)` Returns the table name for a given class or object.
* `baseDataTable($class)` Returns the base data class for the given class.
* `tableForField($class, $field)` Finds the specific class that directly holds the given field and returns the table.
Note that in cases where the class name is required, an instance of the object may be substituted.
For example, if running a query against a particular model, you will need to ensure you use the correct
table and column.
:::php
public function countDuplicates($model, $fieldToCheck) {
$table = DataObject::getSchema()->tableForField($model, $field);
$query = new SQLSelect();
$query->setFrom("\"{$table}\"");
$query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]);
return $query->count();
}
### Raw SQL ### Raw SQL
Occasionally, the system described above won't let you do exactly what you need to do. In these situations, we have Occasionally, the system described above won't let you do exactly what you need to do. In these situations, we have
@ -620,3 +673,4 @@ To retrieve a news article, SilverStripe joins the [api:SiteTree], [api:Page] an
* [api:DataObject] * [api:DataObject]
* [api:DataList] * [api:DataList]
* [api:DataQuery] * [api:DataQuery]
* [api:DataObjectSchema]

View File

@ -83,7 +83,7 @@
* Versioned constructor now only allows a single string to declare whether staging is enabled or not. The * Versioned constructor now only allows a single string to declare whether staging is enabled or not. The
number of names of stages are no longer able to be specified. See below for upgrading notes for models number of names of stages are no longer able to be specified. See below for upgrading notes for models
with custom stages. with custom stages.
* `reading_stage` is now `set_stage` * `reading_stage` is now `set_stage` and throws an error if setting an invalid stage.
* `current_stage` is now `get_stage` * `current_stage` is now `get_stage`
* `getVersionedStages` is gone. * `getVersionedStages` is gone.
* `get_live_stage` is removed. Use the `Versioned::LIVE` constant instead. * `get_live_stage` is removed. Use the `Versioned::LIVE` constant instead.
@ -91,8 +91,13 @@
* `$versionableExtensions` is now `private static` instead of `protected static` * `$versionableExtensions` is now `private static` instead of `protected static`
* `hasStages` is addded to check if an object has a given stage. * `hasStages` is addded to check if an object has a given stage.
* `stageTable` is added to get the table for a given class and stage. * `stageTable` is added to get the table for a given class and stage.
* Any extension declared via `versionableExtensions` config on Versioned dataobject must now
`VersionableExtension` interface at a minimum. `Translatable` has been removed from default
`versionableExtensions`
* `ChangeSet` and `ChangeSetItem` have been added for batch publishing of versioned dataobjects. * `ChangeSet` and `ChangeSetItem` have been added for batch publishing of versioned dataobjects.
* `FormAction::setValidationExempt` can be used to turn on or off form validation for individual actions * `FormAction::setValidationExempt` can be used to turn on or off form validation for individual actions
* `DataObject.table_name` config can now be used to customise the database table for any record.
* `DataObjectSchema` class added to assist with mapping between classes and tables.
### Front-end build tooling for CMS interface ### Front-end build tooling for CMS interface
@ -163,6 +168,10 @@ admin/font/ => admin/client/dist/font/
* `debugmethods` querystring argument has been removed from debugging. * `debugmethods` querystring argument has been removed from debugging.
* The following ClassInfo methods are now deprecated:
* `ClassInfo::baseDataClass` - Use `DataObject::getSchema()->baseDataClass()` instead.
* `ClassInfo::table_for_object_field` - Use `DataObject::getSchema()->tableForField()` instead
### ORM ### ORM
* `DataList::getRelation` is removed, as it was mutable. Use `DataList::applyRelation` instead, which is immutable. * `DataList::getRelation` is removed, as it was mutable. Use `DataList::applyRelation` instead, which is immutable.
@ -563,6 +572,36 @@ these references should be replaced with `SQLSelect`. Legacy code which generate
`SQLQuery` can still communicate with new code that expects `SQLSelect` as it is a `SQLQuery` can still communicate with new code that expects `SQLSelect` as it is a
subclass of `SQLSelect`, but the inverse is not true. subclass of `SQLSelect`, but the inverse is not true.
### Update code that references table names
A major change in 4.0.0 is that now tables and class names can differ from model to model. In order to
fix a table name, to prevent it being changed (for instance, when applying a namespace to a model)
the `table_name` config can be applied to any DataObject class.
:::php
namespace SilverStripe\BannerManager;
class BannerImage extends \DataObject {
private static $table_name = 'BannerImage';
}
In order to ensure you are using the correct table for any class a new [api:DataObjectSchema] service
is available to manage these mappings.
:::php
public function countDuplicates($model, $fieldToCheck) {
$table = DataObject::getSchema()->tableForField($model, $field);
$query = new SQLSelect();
$query->setFrom("\"{$table}\"");
$query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]);
return $query->count();
}
See [versioned documentation](/developer_guides/model/data_model_and_orm) for more information.
### Update implementations of augmentSQL ### Update implementations of augmentSQL
Since this method now takes a `SQLSelect` as a first parameter, existing code referencing the deprecated `SQLQuery` Since this method now takes a `SQLSelect` as a first parameter, existing code referencing the deprecated `SQLQuery`

View File

@ -155,7 +155,7 @@ class AssetControlExtension extends \DataExtension
} }
// Unauthenticated member to use for checking visibility // Unauthenticated member to use for checking visibility
$baseClass = \ClassInfo::baseDataClass($this->owner); $baseClass = $this->owner->baseClass();
$filter = array("\"{$baseClass}\".\"ID\"" => $this->owner->ID); $filter = array("\"{$baseClass}\".\"ID\"" => $this->owner->ID);
$stages = $this->owner->getVersionedStages(); // {@see Versioned::getVersionedStages} $stages = $this->owner->getVersionedStages(); // {@see Versioned::getVersionedStages}
foreach ($stages as $stage) { foreach ($stages as $stage) {

View File

@ -511,12 +511,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
$relationModelName = $query->applyRelation($relations, $linearOnly); $relationModelName = $query->applyRelation($relations, $linearOnly);
// Find the db field the relation belongs to // Find the db field the relation belongs to
$className = ClassInfo::table_for_object_field($relationModelName, $fieldName); $columnName = DataObject::getSchema()->sqlColumnForField($relationModelName, $fieldName);
if(empty($className)) {
$className = $relationModelName;
}
$columnName = '"'.$className.'"."'.$fieldName.'"';
} }
); );
} }

View File

@ -193,8 +193,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
protected static $_cache_has_own_table = array(); protected static $_cache_has_own_table = array();
protected static $_cache_get_one; protected static $_cache_get_one;
protected static $_cache_get_class_ancestry; protected static $_cache_get_class_ancestry;
protected static $_cache_composite_fields = array();
protected static $_cache_database_fields = array();
protected static $_cache_field_labels = array(); protected static $_cache_field_labels = array();
/** /**
@ -220,6 +218,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
'AssetControl' => '\\SilverStripe\\Filesystem\\AssetControlExtension' 'AssetControl' => '\\SilverStripe\\Filesystem\\AssetControlExtension'
); );
/**
* Override table name for this class. If ignored will default to FQN of class.
* This option is not inheritable, and must be set on each class.
* If left blank naming will default to the legacy (3.x) behaviour.
*
* @var string
*/
private static $table_name = null;
/** /**
* Non-static relationship cache, indexed by component name. * Non-static relationship cache, indexed by component name.
*/ */
@ -230,6 +237,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/ */
protected $unsavedRelations; protected $unsavedRelations;
/**
* Get schema object
*
* @return DataObjectSchema
*/
public static function getSchema() {
return Injector::inst()->get('DataObjectSchema');
}
/** /**
* Return the complete map of fields to specification on this object, including fixed_fields. * Return the complete map of fields to specification on this object, including fixed_fields.
* "ID" will be included on every table. * "ID" will be included on every table.
@ -246,74 +262,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(empty($class)) { if(empty($class)) {
$class = get_called_class(); $class = get_called_class();
} }
return static::getSchema()->databaseFields($class);
// Refresh cache
self::cache_database_fields($class);
// Return cached values
return self::$_cache_database_fields[$class];
}
/**
* Cache all database and composite fields for the given class.
* Will do nothing if already cached
*
* @param string $class Class name to cache
*/
protected static function cache_database_fields($class) {
// Skip if already cached
if( isset(self::$_cache_database_fields[$class])
&& isset(self::$_cache_composite_fields[$class])
) {
return;
}
$compositeFields = array();
$dbFields = array();
// Ensure fixed fields appear at the start
$fixedFields = self::config()->fixed_fields;
if(get_parent_class($class) === 'DataObject') {
// Merge fixed with ClassName spec and custom db fields
$dbFields = $fixedFields;
} else {
$dbFields['ID'] = $fixedFields['ID'];
}
// Check each DB value as either a field or composite field
$db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array();
foreach($db as $fieldName => $fieldSpec) {
$fieldClass = strtok($fieldSpec, '(');
if(singleton($fieldClass) instanceof DBComposite) {
$compositeFields[$fieldName] = $fieldSpec;
} else {
$dbFields[$fieldName] = $fieldSpec;
}
}
// Add in all has_ones
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: array();
foreach($hasOne as $fieldName => $hasOneClass) {
if($hasOneClass === 'DataObject') {
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
} else {
$dbFields["{$fieldName}ID"] = 'ForeignKey';
}
}
// Merge composite fields into DB
foreach($compositeFields as $fieldName => $fieldSpec) {
$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
$fieldObj->setTable($class);
$nestedFields = $fieldObj->compositeDatabaseFields();
foreach($nestedFields as $nestedName => $nestedSpec) {
$dbFields["{$fieldName}{$nestedName}"] = $nestedSpec;
}
}
// Return cached results
self::$_cache_database_fields[$class] = $dbFields;
self::$_cache_composite_fields[$class] = $compositeFields;
} }
/** /**
@ -337,10 +286,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$class = get_called_class(); $class = get_called_class();
} }
// Get all fields
$fields = self::database_fields($class);
// Remove fixed fields. This assumes that NO fixed_fields are composite // Remove fixed fields. This assumes that NO fixed_fields are composite
$fields = static::getSchema()->databaseFields($class);
$fields = array_diff_key($fields, self::config()->fixed_fields); $fields = array_diff_key($fields, self::config()->fixed_fields);
return $fields; return $fields;
} }
@ -377,24 +324,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(empty($class)) { if(empty($class)) {
$class = get_called_class(); $class = get_called_class();
} }
if($class === 'DataObject') { return static::getSchema()->compositeFields($class, $aggregated);
return array();
}
// Refresh cache
self::cache_database_fields($class);
// Get fields for this class
$compositeFields = self::$_cache_composite_fields[$class];
if(!$aggregated) {
return $compositeFields;
}
// Recursively merge
return array_merge(
$compositeFields,
self::composite_fields(get_parent_class($class))
);
} }
/** /**
@ -1250,10 +1180,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param string $now Timestamp to use for the current time * @param string $now Timestamp to use for the current time
* @param bool $isNewRecord Whether this should be treated as a new record write * @param bool $isNewRecord Whether this should be treated as a new record write
* @param array $manipulation Manipulation to write to * @param array $manipulation Manipulation to write to
* @param string $class Table and Class to select and write to * @param string $class Class of table to manipulate
*/ */
protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) { protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
$manipulation[$class] = array(); $table = $this->getSchema()->tableName($class);
$manipulation[$table] = array();
// Extract records for this table // Extract records for this table
foreach($this->record as $fieldName => $fieldValue) { foreach($this->record as $fieldName => $fieldValue) {
@ -1261,7 +1192,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Check if this record pertains to this table, and // Check if this record pertains to this table, and
// we're not attempting to reset the BaseTable->ID // we're not attempting to reset the BaseTable->ID
if( empty($this->changed[$fieldName]) if( empty($this->changed[$fieldName])
|| ($class === $baseTable && $fieldName === 'ID') || ($table === $baseTable && $fieldName === 'ID')
|| (!self::has_own_table_database_field($class, $fieldName) || (!self::has_own_table_database_field($class, $fieldName)
&& !self::is_composite_field($class, $fieldName, false)) && !self::is_composite_field($class, $fieldName, false))
) { ) {
@ -1276,25 +1207,26 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
// Write to manipulation // Write to manipulation
$fieldObj->writeToManipulation($manipulation[$class]); $fieldObj->writeToManipulation($manipulation[$table]);
} }
// Ensure update of Created and LastEdited columns // Ensure update of Created and LastEdited columns
if($baseTable === $class) { if($baseTable === $table) {
$manipulation[$class]['fields']['LastEdited'] = $now; $manipulation[$table]['fields']['LastEdited'] = $now;
if($isNewRecord) { if($isNewRecord) {
$manipulation[$class]['fields']['Created'] $manipulation[$table]['fields']['Created']
= empty($this->record['Created']) = empty($this->record['Created'])
? $now ? $now
: $this->record['Created']; : $this->record['Created'];
$manipulation[$class]['fields']['ClassName'] = $this->class; $manipulation[$table]['fields']['ClassName'] = $this->class;
} }
} }
// Inserts done one the base table are performed in another step, so the manipulation should instead // Inserts done one the base table are performed in another step, so the manipulation should instead
// attempt an update, as though it were a normal update. // attempt an update, as though it were a normal update.
$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update'; $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
$manipulation[$class]['id'] = $this->record['ID']; $manipulation[$table]['id'] = $this->record['ID'];
$manipulation[$table]['class'] = $class;
} }
/** /**
@ -1379,7 +1311,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if($hasChanges || $forceWrite || $isNewRecord) { if($hasChanges || $forceWrite || $isNewRecord) {
// New records have their insert into the base data table done first, so that they can pass the // New records have their insert into the base data table done first, so that they can pass the
// generated primary key on to the rest of the manipulation // generated primary key on to the rest of the manipulation
$baseTable = ClassInfo::baseDataClass($this->class); $baseTable = $this->baseTable();
$this->writeBaseRecord($baseTable, $now); $this->writeBaseRecord($baseTable, $now);
// Write the DB manipulation for all changed fields // Write the DB manipulation for all changed fields
@ -1730,7 +1662,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
case 'has_one': { case 'has_one': {
// Mock has_many // Mock has_many
$joinField = "{$remoteRelation}ID"; $joinField = "{$remoteRelation}ID";
$componentClass = ClassInfo::table_for_object_field($remoteClass, $joinField); $componentClass = static::getSchema()->classForField($remoteClass, $joinField);
$result = HasManyList::create($componentClass, $joinField); $result = HasManyList::create($componentClass, $joinField);
if ($this->model) { if ($this->model) {
$result->setDataModel($this->model); $result->setDataModel($this->model);
@ -1998,12 +1930,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Return all of the database fields in this object * Return all of the database fields in this object
* *
* @param string $fieldName Limit the output to a specific field name * @param string $fieldName Limit the output to a specific field name
* @param bool $includeTable If returning a single column, prefix the column with the table name * @param bool $includeClass If returning a single column, prefix the column with the class name
* in Table.Column(spec) format * in Table.Column(spec) format
* @return array|string|null The database fields, or if searching a single field, just this one field if found * @return array|string|null The database fields, or if searching a single field,
* Field will be a string in ClassName(args) format, or Table.ClassName(args) format if $includeTable is true * just this one field if found. Field will be a string in FieldClass(args)
* format, or RecordClass.FieldClass(args) format if $includeClass is true
*/ */
public function db($fieldName = null, $includeTable = false) { public function db($fieldName = null, $includeClass = false) {
$classes = ClassInfo::ancestry($this, true); $classes = ClassInfo::ancestry($this, true);
// If we're looking for a specific field, we want to hit subclasses first as they may override field types // If we're looking for a specific field, we want to hit subclasses first as they may override field types
@ -2021,17 +1954,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Check for search field // Check for search field
if($fieldName && isset($db[$fieldName])) { if($fieldName && isset($db[$fieldName])) {
// Return found field // Return found field
if(!$includeTable) { if(!$includeClass) {
return $db[$fieldName]; return $db[$fieldName];
} }
return $class . "." . $db[$fieldName];
// Set table for the given field
if(in_array($fieldName, $this->config()->fixed_fields)) {
$table = $this->baseTable();
} else {
$table = $class;
}
return $table . "." . $db[$fieldName];
} }
} }
@ -2179,54 +2105,60 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
foreach($classes as $class) { foreach($classes as $class) {
$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED); $manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
// Check if the component is defined in many_many on this class // Check if the component is defined in many_many on this class
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null; if(isset($manyMany[$component])) {
if($candidate) { $candidate = $manyMany[$component];
$parentField = $class . "ID"; $classTable = static::getSchema()->tableName($class);
$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID"; $candidateTable = static::getSchema()->tableName($candidate);
return array($class, $candidate, $parentField, $childField, "{$class}_$component"); $parentField = "{$classTable}ID";
$childField = $class === $candidate ? "ChildID" : "{$candidateTable}ID";
$joinTable = "{$classTable}_{$component}";
return array($class, $candidate, $parentField, $childField, $joinTable);
} }
// Check if the component is defined in belongs_many_many on this class // Check if the component is defined in belongs_many_many on this class
$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED); $belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null; if(!isset($belongsManyMany[$component])) {
if($candidate) { continue;
// Extract class and relation name from dot-notation }
if(strpos($candidate, '.') !== false) {
list($candidate, $relationName) = explode('.', $candidate, 2);
}
$childField = $candidate . "ID"; // Extract class and relation name from dot-notation
$candidate = $belongsManyMany[$component];
$relationName = null;
if(strpos($candidate, '.') !== false) {
list($candidate, $relationName) = explode('.', $candidate, 2);
}
$candidateTable = static::getSchema()->tableName($candidate);
$childField = $candidateTable . "ID";
// We need to find the inverse component name // We need to find the inverse component name, if not explicitly given
$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED); $otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
if(!$otherManyMany) { if(!$relationName && $otherManyMany) {
throw new LogicException("Inverse component of $candidate not found ({$this->class})"); foreach($otherManyMany as $inverseComponentName => $childClass) {
} if($childClass === $class || is_subclass_of($class, $childClass)) {
$relationName = $inverseComponentName;
// If we've got a relation name (extracted from dot-notation), we can already work out break;
// the join table and candidate class name...
if(isset($relationName) && isset($otherManyMany[$relationName])) {
$candidateClass = $otherManyMany[$relationName];
$joinTable = "{$candidate}_{$relationName}";
} else {
// ... otherwise, we need to loop over the many_manys and find a relation that
// matches up to this class
foreach($otherManyMany as $inverseComponentName => $candidateClass) {
if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
$joinTable = "{$candidate}_{$inverseComponentName}";
break;
}
} }
} }
// If we could work out the join table, we've got all the info we need
if(isset($joinTable)) {
$parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
return array($class, $candidate, $parentField, $childField, $joinTable);
}
throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
} }
// Check valid relation found
if(!$relationName || !$otherManyMany || !isset($otherManyMany[$relationName])) {
throw new LogicException("Inverse component of $candidate not found ({$this->class})");
}
// If we've got a relation name (extracted from dot-notation), we can already work out
// the join table and candidate class name...
$childClass = $otherManyMany[$relationName];
$joinTable = "{$candidateTable}_{$relationName}";
// If we could work out the join table, we've got all the info we need
if ($childClass === $candidate) {
$parentField = "ChildID";
} else {
$childTable = static::getSchema()->tableName($childClass);
$parentField = "{$childTable}ID";
}
return array($class, $candidate, $parentField, $childField, $joinTable);
} }
} }
@ -2470,16 +2402,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/** /**
* Loads all the stub fields that an initial lazy load didn't load fully. * Loads all the stub fields that an initial lazy load didn't load fully.
* *
* @param string $tableClass Base table to load the values from. Others are joined as required. * @param string $class Class to load the values from. Others are joined as required.
* Not specifying a tableClass will load all lazy fields from all tables. * Not specifying a tableClass will load all lazy fields from all tables.
* @return bool Flag if lazy loading succeeded * @return bool Flag if lazy loading succeeded
*/ */
protected function loadLazyFields($tableClass = null) { protected function loadLazyFields($class = null) {
if(!$this->isInDB() || !is_numeric($this->ID)) { if(!$this->isInDB() || !is_numeric($this->ID)) {
return false; return false;
} }
if (!$tableClass) { if (!$class) {
$loaded = array(); $loaded = array();
foreach ($this->record as $key => $value) { foreach ($this->record as $key => $value) {
@ -2492,7 +2424,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return false; return false;
} }
$dataQuery = new DataQuery($tableClass); $dataQuery = new DataQuery($class);
// Reset query parameter context to that of this DataObject // Reset query parameter context to that of this DataObject
if($params = $this->getSourceQueryParams()) { if($params = $this->getSourceQueryParams()) {
@ -2503,16 +2435,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Limit query to the current record, unless it has the Versioned extension, // Limit query to the current record, unless it has the Versioned extension,
// in which case it requires special handling through augmentLoadLazyFields() // in which case it requires special handling through augmentLoadLazyFields()
$baseTable = ClassInfo::baseDataClass($this); $baseIDColumn = static::getSchema()->sqlColumnForField($this, 'ID');
$dataQuery->where([ $dataQuery->where([
"\"{$baseTable}\".\"ID\"" => $this->record['ID'] $baseIDColumn => $this->record['ID']
])->limit(1); ])->limit(1);
$columns = array(); $columns = array();
// Add SQL for fields, both simple & multi-value // Add SQL for fields, both simple & multi-value
// TODO: This is copy & pasted from buildSQL(), it could be moved into a method // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
$databaseFields = self::database_fields($tableClass); $databaseFields = self::database_fields($class);
if($databaseFields) foreach($databaseFields as $k => $v) { if($databaseFields) foreach($databaseFields as $k => $v) {
if(!isset($this->record[$k]) || $this->record[$k] === null) { if(!isset($this->record[$k]) || $this->record[$k] === null) {
$columns[] = $k; $columns[] = $k;
@ -2805,19 +2737,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return bool * @return bool
*/ */
public static function has_own_table($dataClass) { public static function has_own_table($dataClass) {
if(!is_subclass_of($dataClass,'DataObject')) return false; if(!is_subclass_of($dataClass, 'DataObject')) {
return false;
$dataClass = ClassInfo::class_name($dataClass);
if(!isset(self::$_cache_has_own_table[$dataClass])) {
if(get_parent_class($dataClass) == 'DataObject') {
self::$_cache_has_own_table[$dataClass] = true;
} else {
self::$_cache_has_own_table[$dataClass]
= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
}
} }
return self::$_cache_has_own_table[$dataClass]; $fields = static::database_fields($dataClass);
return !empty($fields);
} }
/** /**
@ -3263,12 +3187,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Reset all global caches associated with DataObject. * Reset all global caches associated with DataObject.
*/ */
public static function reset() { public static function reset() {
// @todo Decouple these
DBClassName::clear_classname_cache(); DBClassName::clear_classname_cache();
ClassInfo::reset_db_cache();
static::getSchema()->reset();
self::$_cache_has_own_table = array(); self::$_cache_has_own_table = array();
self::$_cache_get_one = array(); self::$_cache_get_one = array();
self::$_cache_composite_fields = array();
self::$_cache_database_fields = array();
self::$_cache_get_class_ancestry = array();
self::$_cache_field_labels = array(); self::$_cache_field_labels = array();
} }
@ -3286,25 +3210,27 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING); user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
} }
// Check filter column // Pass to get_one
if(is_subclass_of($callerClass, 'DataObject')) { $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
$baseClass = ClassInfo::baseDataClass($callerClass);
$column = "\"$baseClass\".\"ID\"";
} else{
// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
$column = '"ID"';
}
// Relegate to get_one
return DataObject::get_one($callerClass, array($column => $id), $cache); return DataObject::get_one($callerClass, array($column => $id), $cache);
} }
/** /**
* Get the name of the base table for this object * Get the name of the base table for this object
*
* @return string
*/ */
public function baseTable() { public function baseTable() {
$tableClasses = ClassInfo::dataClassesFor($this->class); return static::getSchema()->baseDataTable($this);
return array_shift($tableClasses); }
/**
* Get the base class for this object
*
* @return string
*/
public function baseClass() {
return static::getSchema()->baseDataClass($this);
} }
/** /**
@ -3403,19 +3329,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public function requireTable() { public function requireTable() {
// Only build the table if we've actually got fields // Only build the table if we've actually got fields
$fields = self::database_fields($this->class); $fields = self::database_fields($this->class);
$table = static::getSchema()->tableName($this->class);
$extensions = self::database_extensions($this->class); $extensions = self::database_extensions($this->class);
$indexes = $this->databaseIndexes(); $indexes = $this->databaseIndexes();
// Validate relationship configuration // Validate relationship configuration
$this->validateModelDefinitions(); $this->validateModelDefinitions();
if($fields) { if($fields) {
$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class)); $hasAutoIncPK = get_parent_class($this) === 'DataObject';
DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'), DB::require_table(
$extensions); $table, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'), $extensions
);
} else { } else {
DB::dont_require_table($this->class); DB::dont_require_table($table);
} }
// Build any child tables for many_many items // Build any child tables for many_many items
@ -3423,9 +3350,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$extras = $this->uninherited('many_many_extraFields'); $extras = $this->uninherited('many_many_extraFields');
foreach($manyMany as $relationship => $childClass) { foreach($manyMany as $relationship => $childClass) {
// Build field list // Build field list
if($this->class === $childClass) {
$childField = "ChildID";
} else {
$childTable = $this->getSchema()->tableName($childClass);
$childField = "{$childTable}ID";
}
$manymanyFields = array( $manymanyFields = array(
"{$this->class}ID" => "Int", "{$table}ID" => "Int",
(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int", $childField => "Int",
); );
if(isset($extras[$relationship])) { if(isset($extras[$relationship])) {
$manymanyFields = array_merge($manymanyFields, $extras[$relationship]); $manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
@ -3433,12 +3366,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Build index list // Build index list
$manymanyIndexes = array( $manymanyIndexes = array(
"{$this->class}ID" => true, "{$table}ID" => true,
(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true, $childField => true,
); );
$manyManyTable = "{$table}_$relationship";
DB::require_table("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null, DB::require_table($manyManyTable, $manymanyFields, $manymanyIndexes, true, null, $extensions);
$extensions);
} }
} }

362
model/DataObjectSchema.php Normal file
View File

@ -0,0 +1,362 @@
<?php
use SilverStripe\Framework\Core\Configurable;
use SilverStripe\Framework\Core\Injectable;
use SilverStripe\Model\FieldType\DBComposite;
/**
* Provides dataobject and database schema mapping functionality
*/
class DataObjectSchema {
use Injectable;
use Configurable;
/**
* Default separate for table namespaces. Can be set to any string for
* databases that do not support some characters.
*
* Defaults to \ to to conform to 3.x convention.
*
* @config
* @var string
*/
private static $table_namespace_separator = '\\';
/**
* Cache of database fields
*
* @var array
*/
protected $databaseFields = [];
/**
* Cache of composite database field
*
* @var array
*/
protected $compositeFields = [];
/**
* Cache of table names
*
* @var array
*/
protected $tableNames = [];
/**
* Clear cached table names
*/
public function reset() {
$this->tableNames = [];
$this->databaseFields = [];
$this->compositeFields = [];
}
/**
* Get all table names
*
* @return array
*/
public function getTableNames() {
$this->cacheTableNames();
return $this->tableNames;
}
/**
* Given a DataObject class and a field on that class, determine the appropriate SQL for
* selecting / filtering on in a SQL string. Note that $class must be a valid class, not an
* arbitrary table.
*
* The result will be a standard ANSI-sql quoted string in "Table"."Column" format.
*
* @param string $class Class name (not a table).
* @param string $field Name of field that belongs to this class (or a parent class)
* @return string The SQL identifier string for the corresponding column for this field
*/
public function sqlColumnForField($class, $field) {
$table = $this->tableForField($class, $field);
if(!$table) {
throw new InvalidArgumentException("\"{$field}\" is not a field on class \"{$class}\"");
}
return "\"{$table}\".\"{$field}\"";
}
/**
* Get table name for the given class.
*
* Note that this does not confirm a table actually exists (or should exist), but returns
* the name that would be used if this table did exist.
*
* @param string $class
* @return string Returns the table name, or null if there is no table
*/
public function tableName($class) {
$tables = $this->getTableNames();
$class = ClassInfo::class_name($class);
if(isset($tables[$class])) {
return $tables[$class];
}
return null;
}
/**
* Returns the root class (the first to extend from DataObject) for the
* passed class.
*
* @param string|object $class
* @return string
* @throws InvalidArgumentException
*/
public function baseDataClass($class) {
$class = ClassInfo::class_name($class);
$current = $class;
while ($next = get_parent_class($current)) {
if ($next === 'DataObject') {
return $current;
}
$current = $next;
}
throw new InvalidArgumentException("$class is not a subclass of DataObject");
}
/**
* Get the base table
*
* @param string|object $class
* @return string
*/
public function baseDataTable($class) {
return $this->tableName($this->baseDataClass($class));
}
/**
* Find the class for the given table
*
* @param string $table
* @return string|null The FQN of the class, or null if not found
*/
public function tableClass($table) {
$tables = $this->getTableNames();
$class = array_search($table, $tables, true);
if($class) {
return $class;
}
// If there is no class for this table, strip table modifiers (e.g. _Live / _versions)
// from the end and re-attempt a search.
if(preg_match('/^(?<class>.+)(_[^_]+)$/i', $table, $matches)) {
$table = $matches['class'];
$class = array_search($table, $tables, true);
if($class) {
return $class;
}
}
return null;
}
/**
* Cache all table names if necessary
*/
protected function cacheTableNames() {
if($this->tableNames) {
return;
}
$this->tableNames = [];
foreach(ClassInfo::subclassesFor('DataObject') as $class) {
if($class === 'DataObject') {
continue;
}
$table = $this->buildTableName($class);
// Check for conflicts
$conflict = array_search($table, $this->tableNames, true);
if($conflict) {
throw new LogicException(
"Multiple classes (\"{$class}\", \"{$conflict}\") map to the same table: \"{$table}\""
);
}
$this->tableNames[$class] = $table;
}
}
/**
* Generate table name for a class.
*
* Note: some DB schema have a hard limit on table name length. This is not enforced by this method.
* See dev/build errors for details in case of table name violation.
*
* @param string $class
* @return string
*/
protected function buildTableName($class) {
$table = Config::inst()->get($class, 'table_name', Config::UNINHERITED);
// Generate default table name
if(!$table) {
$separator = $this->config()->table_namespace_separator;
$table = str_replace('\\', $separator, trim($class, '\\'));
}
return $table;
}
/**
* Return the complete map of fields to specification on this object, including fixed_fields.
* "ID" will be included on every table.
*
* @param string $class Class name to query from
* @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
*/
public function databaseFields($class) {
$class = ClassInfo::class_name($class);
if($class === 'DataObject') {
return [];
}
$this->cacheDatabaseFields($class);
return $this->databaseFields[$class];
}
/**
* Returns a list of all the composite if the given db field on the class is a composite field.
* Will check all applicable ancestor classes and aggregate results.
*
* Can be called directly on an object. E.g. Member::composite_fields(), or Member::composite_fields(null, true)
* to aggregate.
*
* Includes composite has_one (Polymorphic) fields
*
* @param string $class Name of class to check
* @param bool $aggregated Include fields in entire hierarchy, rather than just on this table
* @return array List of composite fields and their class spec
*/
public function compositeFields($class, $aggregated = true) {
$class = ClassInfo::class_name($class);
if($class === 'DataObject') {
return [];
}
$this->cacheDatabaseFields($class);
// Get fields for this class
$compositeFields = $this->compositeFields[$class];
if(!$aggregated) {
return $compositeFields;
}
// Recursively merge
$parentFields = $this->compositeFields(get_parent_class($class));
return array_merge($compositeFields, $parentFields);
}
/**
* Cache all database and composite fields for the given class.
* Will do nothing if already cached
*
* @param string $class Class name to cache
*/
protected function cacheDatabaseFields($class) {
// Skip if already cached
if (isset($this->databaseFields[$class]) && isset($this->compositeFields[$class])) {
return;
}
$compositeFields = array();
$dbFields = array();
// Ensure fixed fields appear at the start
$fixedFields = DataObject::config()->fixed_fields;
if(get_parent_class($class) === 'DataObject') {
// Merge fixed with ClassName spec and custom db fields
$dbFields = $fixedFields;
} else {
$dbFields['ID'] = $fixedFields['ID'];
}
// Check each DB value as either a field or composite field
$db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array();
foreach($db as $fieldName => $fieldSpec) {
$fieldClass = strtok($fieldSpec, '(');
if(singleton($fieldClass) instanceof DBComposite) {
$compositeFields[$fieldName] = $fieldSpec;
} else {
$dbFields[$fieldName] = $fieldSpec;
}
}
// Add in all has_ones
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: array();
foreach($hasOne as $fieldName => $hasOneClass) {
if($hasOneClass === 'DataObject') {
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
} else {
$dbFields["{$fieldName}ID"] = 'ForeignKey';
}
}
// Merge composite fields into DB
foreach($compositeFields as $fieldName => $fieldSpec) {
$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
$fieldObj->setTable($class);
$nestedFields = $fieldObj->compositeDatabaseFields();
foreach($nestedFields as $nestedName => $nestedSpec) {
$dbFields["{$fieldName}{$nestedName}"] = $nestedSpec;
}
}
// Prevent field-less tables
if(count($dbFields) < 2) {
$dbFields = [];
}
// Return cached results
$this->databaseFields[$class] = $dbFields;
$this->compositeFields[$class] = $compositeFields;
}
/**
* Returns the table name in the class hierarchy which contains a given
* field column for a {@link DataObject}. If the field does not exist, this
* will return null.
*
* @param string $candidateClass
* @param string $fieldName
* @return string
*/
public function tableForField($candidateClass, $fieldName) {
$class = $this->classForField($candidateClass, $fieldName);
if($class) {
return $this->tableName($class);
}
return null;
}
/**
* Returns the class name in the class hierarchy which contains a given
* field column for a {@link DataObject}. If the field does not exist, this
* will return null.
*
* @param string $candidateClass
* @param string $fieldName
* @return string
*/
public function classForField($candidateClass, $fieldName) {
// normalise class name
$candidateClass = ClassInfo::class_name($candidateClass);
if($candidateClass === 'DataObject') {
return null;
}
// Short circuit for fixed fields
$fixed = DataObject::config()->fixed_fields;
if(isset($fixed[$fieldName])) {
return $this->baseDataClass($candidateClass);
}
// Find regular field
while($candidateClass) {
$fields = $this->databaseFields($candidateClass);
if(isset($fields[$fieldName])) {
return $candidateClass;
}
$candidateClass = get_parent_class($candidateClass);
}
return null;
}
}

View File

@ -24,6 +24,16 @@ class DataQuery {
protected $query; protected $query;
/** /**
* Map of all field names to an array of conflicting column SQL
*
* E.g.
* array(
* 'Title' => array(
* '"MyTable"."Title"',
* '"AnotherTable"."Title"',
* )
* )
*
* @var array * @var array
*/ */
protected $collidingFields = array(); protected $collidingFields = array();
@ -31,7 +41,7 @@ class DataQuery {
private $queriedColumns = null; private $queriedColumns = null;
/** /**
* @var Boolean * @var bool
*/ */
private $queryFinalised = false; private $queryFinalised = false;
@ -131,15 +141,12 @@ class DataQuery {
* Set up the simplest initial query * Set up the simplest initial query
*/ */
protected function initialiseQuery() { protected function initialiseQuery() {
// Get the tables to join to. // Join on base table and let lazy loading join subtables
// Don't get any subclass tables - let lazy loading do that. $baseClass = DataObject::getSchema()->baseDataClass($this->dataClass());
$tableClasses = ClassInfo::ancestry($this->dataClass, true); if(!$baseClass) {
if(!$tableClasses) {
throw new InvalidArgumentException("DataQuery::create() Can't find data classes for '{$this->dataClass}'"); throw new InvalidArgumentException("DataQuery::create() Can't find data classes for '{$this->dataClass}'");
} }
$baseClass = array_shift($tableClasses);
// Build our intial query // Build our intial query
$this->query = new SQLSelect(array()); $this->query = new SQLSelect(array());
$this->query->setDistinct(true); $this->query->setDistinct(true);
@ -148,7 +155,8 @@ class DataQuery {
$this->sort($sort); $this->sort($sort);
} }
$this->query->setFrom("\"$baseClass\""); $baseTable = DataObject::getSchema()->tableName($baseClass);
$this->query->setFrom("\"{$baseTable}\"");
$obj = Injector::inst()->get($baseClass); $obj = Injector::inst()->get($baseClass);
$obj->extend('augmentDataQueryCreation', $this->query, $this); $obj->extend('augmentDataQueryCreation', $this->query, $this);
@ -165,13 +173,18 @@ class DataQuery {
* @return SQLSelect The finalised sql query * @return SQLSelect The finalised sql query
*/ */
public function getFinalisedQuery($queriedColumns = null) { public function getFinalisedQuery($queriedColumns = null) {
if(!$queriedColumns) $queriedColumns = $this->queriedColumns; if(!$queriedColumns) {
$queriedColumns = $this->queriedColumns;
}
if($queriedColumns) { if($queriedColumns) {
$queriedColumns = array_merge($queriedColumns, array('Created', 'LastEdited', 'ClassName')); $queriedColumns = array_merge($queriedColumns, array('Created', 'LastEdited', 'ClassName'));
} }
$schema = DataObject::getSchema();
$query = clone $this->query; $query = clone $this->query;
$ancestorTables = ClassInfo::ancestry($this->dataClass, true); $baseDataClass = $schema->baseDataClass($this->dataClass());
$baseIDColumn = $schema->sqlColumnForField($baseDataClass, 'ID');
$ancestorClasses = ClassInfo::ancestry($this->dataClass(), true);
// Generate the list of tables to iterate over and the list of columns required // Generate the list of tables to iterate over and the list of columns required
// by any existing where clauses. This second step is skipped if we're fetching // by any existing where clauses. This second step is skipped if we're fetching
@ -180,20 +193,25 @@ class DataQuery {
// Specifying certain columns allows joining of child tables // Specifying certain columns allows joining of child tables
$tableClasses = ClassInfo::dataClassesFor($this->dataClass); $tableClasses = ClassInfo::dataClassesFor($this->dataClass);
// Ensure that any filtered columns are included in the selected columns
foreach ($query->getWhereParameterised($parameters) as $where) { foreach ($query->getWhereParameterised($parameters) as $where) {
// Check for just the column, in the form '"Column" = ?' and the form '"Table"."Column"' = ? // Check for any columns in the form '"Column" = ?' or '"Table"."Column"' = ?
if (preg_match('/^"([^"]+)"/', $where, $matches) || if(preg_match_all(
preg_match('/^"([^"]+)"\."[^"]+"/', $where, $matches)) { '/(?:"(?<table>[^"]+)"\.)?"(?<column>[^"]+)"(?:[^\.]|$)/',
if (!in_array($matches[1], $queriedColumns)) $queriedColumns[] = $matches[1]; $where, $matches, PREG_SET_ORDER
)) {
foreach($matches as $match) {
$column = $match['column'];
if (!in_array($column, $queriedColumns)) {
$queriedColumns[] = $column;
}
}
} }
} }
} else { } else {
$tableClasses = $ancestorTables; $tableClasses = $ancestorClasses;
} }
$tableNames = array_values($tableClasses);
$baseClass = $tableNames[0];
// Iterate over the tables and check what we need to select from them. If any selects are made (or the table is // Iterate over the tables and check what we need to select from them. If any selects are made (or the table is
// required for a select) // required for a select)
foreach($tableClasses as $tableClass) { foreach($tableClasses as $tableClass) {
@ -208,7 +226,9 @@ class DataQuery {
} }
// If this is a subclass without any explicitly requested columns, omit this from the query // If this is a subclass without any explicitly requested columns, omit this from the query
if(!in_array($tableClass, $ancestorTables) && empty($selectColumns)) continue; if(!in_array($tableClass, $ancestorClasses) && empty($selectColumns)) {
continue;
}
// Select necessary columns (unless an explicitly empty array) // Select necessary columns (unless an explicitly empty array)
if($selectColumns !== array()) { if($selectColumns !== array()) {
@ -216,51 +236,61 @@ class DataQuery {
} }
// Join if not the base table // Join if not the base table
if($tableClass !== $baseClass) { if($tableClass !== $baseDataClass) {
$query->addLeftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"", $tableClass, 10); $tableName = $schema->tableName($tableClass);
$query->addLeftJoin(
$tableName,
"\"{$tableName}\".\"ID\" = {$baseIDColumn}",
$tableName,
10
);
} }
} }
// Resolve colliding fields // Resolve colliding fields
if($this->collidingFields) { if($this->collidingFields) {
foreach($this->collidingFields as $k => $collisions) { foreach($this->collidingFields as $collisionField => $collisions) {
$caseClauses = array(); $caseClauses = array();
foreach($collisions as $collision) { foreach($collisions as $collision) {
if(preg_match('/^"([^"]+)"/', $collision, $matches)) { if(preg_match('/^"(?<table>[^"]+)"\./', $collision, $matches)) {
$collisionBase = $matches[1]; $collisionTable = $matches['table'];
if(class_exists($collisionBase)) { $collisionClass = $schema->tableClass($collisionTable);
$collisionClasses = ClassInfo::subclassesFor($collisionBase); if($collisionClass) {
$collisionClasses = Convert::raw2sql($collisionClasses, true); $collisionClassColumn = $schema->sqlColumnForField($collisionClass, 'ClassName');
$caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN (" $collisionClasses = ClassInfo::subclassesFor($collisionClass);
. implode(", ", $collisionClasses) . ") THEN $collision"; $collisionClassesSQL = implode(', ', Convert::raw2sql($collisionClasses, true));
$caseClauses[] = "WHEN {$collisionClassColumn} IN ({$collisionClassesSQL}) THEN $collision";
} }
} else { } else {
user_error("Bad collision item '$collision'", E_USER_WARNING); user_error("Bad collision item '$collision'", E_USER_WARNING);
} }
} }
$query->selectField("CASE " . implode( " ", $caseClauses) . " ELSE NULL END", $k); $query->selectField("CASE " . implode( " ", $caseClauses) . " ELSE NULL END", $collisionField);
} }
} }
if($this->filterByClassName) { if($this->filterByClassName) {
// If querying the base class, don't bother filtering on class name // If querying the base class, don't bother filtering on class name
if($this->dataClass != $baseClass) { if($this->dataClass != $baseDataClass) {
// Get the ClassName values to filter to // Get the ClassName values to filter to
$classNames = ClassInfo::subclassesFor($this->dataClass); $classNames = ClassInfo::subclassesFor($this->dataClass);
$classNamesPlaceholders = DB::placeholders($classNames); $classNamesPlaceholders = DB::placeholders($classNames);
$baseClassColumn = $schema->sqlColumnForField($baseDataClass, 'ClassName');
$query->addWhere(array( $query->addWhere(array(
"\"$baseClass\".\"ClassName\" IN ($classNamesPlaceholders)" => $classNames "{$baseClassColumn} IN ($classNamesPlaceholders)" => $classNames
)); ));
} }
} }
$query->selectField("\"$baseClass\".\"ID\"", "ID"); // Select ID
$query->selectField($baseIDColumn, "ID");
// Select RecordClassName
$baseClassColumn = $schema->sqlColumnForField($baseDataClass, 'ClassName');
$query->selectField(" $query->selectField("
CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" CASE WHEN {$baseClassColumn} IS NOT NULL THEN {$baseClassColumn}
ELSE ".Convert::raw2sql($baseClass, true)." END", ELSE ".Convert::raw2sql($baseDataClass, true)." END",
"RecordClassName" "RecordClassName"
); );
@ -283,9 +313,6 @@ class DataQuery {
* @return null * @return null
*/ */
protected function ensureSelectContainsOrderbyColumns($query, $originalSelect = array()) { protected function ensureSelectContainsOrderbyColumns($query, $originalSelect = array()) {
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
$baseClass = array_shift($tableClasses);
if($orderby = $query->getOrderBy()) { if($orderby = $query->getOrderBy()) {
$newOrderby = array(); $newOrderby = array();
$i = 0; $i = 0;
@ -309,10 +336,9 @@ class DataQuery {
} }
if(count($parts) == 1) { if(count($parts) == 1) {
// Get expression for sort value
if(DataObject::has_own_table_database_field($baseClass, $parts[0])) { $qualCol = DataObject::getSchema()->sqlColumnForField($this->dataClass(), $parts[0]);;
$qualCol = "\"$baseClass\".\"{$parts[0]}\""; if(!$qualCol) {
} else {
$qualCol = "\"$parts[0]\""; $qualCol = "\"$parts[0]\"";
} }
@ -369,10 +395,12 @@ class DataQuery {
/** /**
* Return the number of records in this query. * Return the number of records in this query.
* Note that this will issue a separate SELECT COUNT() query. * Note that this will issue a separate SELECT COUNT() query.
*
* @return int
*/ */
public function count() { public function count() {
$baseClass = ClassInfo::baseDataClass($this->dataClass); $quotedColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID');
return $this->getFinalisedQuery()->count("DISTINCT \"$baseClass\".\"ID\""); return $this->getFinalisedQuery()->count("DISTINCT {$quotedColumn}");
} }
/** /**
@ -450,7 +478,7 @@ class DataQuery {
* Update the SELECT clause of the query with the columns from the given table * Update the SELECT clause of the query with the columns from the given table
* *
* @param SQLSelect $query * @param SQLSelect $query
* @param string $tableClass * @param string $tableClass Class to select from
* @param array $columns * @param array $columns
*/ */
protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) { protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) {
@ -461,19 +489,23 @@ class DataQuery {
foreach($databaseFields as $k => $v) { foreach($databaseFields as $k => $v) {
if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) { if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) {
// Update $collidingFields if necessary // Update $collidingFields if necessary
if($expressionForField = $query->expressionForField($k)) { $expressionForField = $query->expressionForField($k);
if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($expressionForField); $quotedField = DataObject::getSchema()->sqlColumnForField($tableClass, $k);
$this->collidingFields[$k][] = "\"$tableClass\".\"$k\""; if($expressionForField) {
if(!isset($this->collidingFields[$k])) {
$this->collidingFields[$k] = array($expressionForField);
}
$this->collidingFields[$k][] = $quotedField;
} else { } else {
$query->selectField("\"$tableClass\".\"$k\"", $k); $query->selectField($quotedField, $k);
} }
} }
} }
foreach($compositeFields as $k => $v) { foreach($compositeFields as $k => $v) {
if((is_null($columns) || in_array($k, $columns)) && $v) { if((is_null($columns) || in_array($k, $columns)) && $v) {
$tableName = DataObject::getSchema()->tableName($tableClass);
$dbO = Object::create_from_string($v, $k); $dbO = Object::create_from_string($v, $k);
$dbO->setTable($tableClass); $dbO->setTable($tableName);
$dbO->addToQuery($query); $dbO->addToQuery($query);
} }
} }
@ -724,18 +756,19 @@ class DataQuery {
"Could not join polymorphic has_one relationship {$localField} on {$localClass}" "Could not join polymorphic has_one relationship {$localField} on {$localClass}"
); );
} }
$schema = DataObject::getSchema();
// Skip if already joined // Skip if already joined
if($this->query->isJoinedTo($foreignClass)) { $foreignBaseClass = $schema->baseDataClass($foreignClass);
$foreignBaseTable = $schema->tableName($foreignBaseClass);
if($this->query->isJoinedTo($foreignBaseTable)) {
return; return;
} }
$realModelClass = ClassInfo::table_for_object_field($localClass, "{$localField}ID"); // Join base table
$foreignBase = ClassInfo::baseDataClass($foreignClass); $foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID');
$this->query->addLeftJoin( $localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID");
$foreignBase, $this->query->addLeftJoin($foreignBaseTable, "{$foreignIDColumn} = {$localColumn}");
"\"$foreignBase\".\"ID\" = \"{$realModelClass}\".\"{$localField}ID\""
);
/** /**
* add join clause to the component's ancestry classes so that the search filter could search on * add join clause to the component's ancestry classes so that the search filter could search on
@ -745,8 +778,9 @@ class DataQuery {
if(!empty($ancestry)){ if(!empty($ancestry)){
$ancestry = array_reverse($ancestry); $ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){ foreach($ancestry as $ancestor){
if($ancestor != $foreignBase) { $ancestorTable = $schema->tableName($ancestor);
$this->query->addLeftJoin($ancestor, "\"$foreignBase\".\"ID\" = \"$ancestor\".\"ID\""); if($ancestorTable !== $foreignBaseTable) {
$this->query->addLeftJoin($ancestorTable, "{$foreignIDColumn} = \"{$ancestorTable}\".\"ID\"");
} }
} }
} }
@ -765,28 +799,30 @@ class DataQuery {
if(!$foreignClass || $foreignClass === 'DataObject') { if(!$foreignClass || $foreignClass === 'DataObject') {
throw new InvalidArgumentException("Could not find a has_many relationship {$localField} on {$localClass}"); throw new InvalidArgumentException("Could not find a has_many relationship {$localField} on {$localClass}");
} }
$schema = DataObject::getSchema();
// Skip if already joined // Skip if already joined
if($this->query->isJoinedTo($foreignClass)) { $foreignTable = $schema->tableName($foreignClass);
if($this->query->isJoinedTo($foreignTable)) {
return; return;
} }
// Join table with associated has_one // Join table with associated has_one
/** @var DataObject $model */ /** @var DataObject $model */
$model = singleton($localClass); $model = singleton($localClass);
$ancestry = $model->getClassAncestry();
$foreignKey = $model->getRemoteJoinField($localField, 'has_many', $polymorphic); $foreignKey = $model->getRemoteJoinField($localField, 'has_many', $polymorphic);
$localIDColumn = $schema->sqlColumnForField($localClass, 'ID');
if($polymorphic) { if($polymorphic) {
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}ID");
$foreignKeyClassColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}Class");
$localClassColumn = $schema->sqlColumnForField($localClass, 'ClassName');
$this->query->addLeftJoin( $this->query->addLeftJoin(
$foreignClass, $foreignTable,
"\"$foreignClass\".\"{$foreignKey}ID\" = \"{$ancestry[0]}\".\"ID\" AND " "{$foreignKeyIDColumn} = {$localIDColumn} AND {$foreignKeyClassColumn} = {$localClassColumn}"
. "\"$foreignClass\".\"{$foreignKey}Class\" = \"{$ancestry[0]}\".\"ClassName\""
); );
} else { } else {
$this->query->addLeftJoin( $foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, $foreignKey);
$foreignClass, $this->query->addLeftJoin($foreignTable, "{$foreignKeyIDColumn} = {$localIDColumn}");
"\"$foreignClass\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\""
);
} }
/** /**
@ -795,9 +831,10 @@ class DataQuery {
*/ */
$ancestry = ClassInfo::ancestry($foreignClass, true); $ancestry = ClassInfo::ancestry($foreignClass, true);
$ancestry = array_reverse($ancestry); $ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){ foreach($ancestry as $ancestor) {
if($ancestor != $foreignClass){ $ancestorTable = $schema->tableName($ancestor);
$this->query->addInnerJoin($ancestor, "\"$foreignClass\".\"ID\" = \"$ancestor\".\"ID\""); if($ancestorTable !== $foreignTable) {
$this->query->addInnerJoin($ancestorTable, "\"{$foreignTable}\".\"ID\" = \"{$ancestorTable}\".\"ID\"");
} }
} }
} }
@ -812,16 +849,23 @@ class DataQuery {
* @param string $relationTable Name of relation table * @param string $relationTable Name of relation table
*/ */
protected function joinManyManyRelationship($parentClass, $componentClass, $parentField, $componentField, $relationTable) { protected function joinManyManyRelationship($parentClass, $componentClass, $parentField, $componentField, $relationTable) {
$parentBaseClass = ClassInfo::baseDataClass($parentClass); $schema = DataObject::getSchema();
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
// Join on parent table
$parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID');
$this->query->addLeftJoin( $this->query->addLeftJoin(
$relationTable, $relationTable,
"\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\"" "\"$relationTable\".\"$parentField\" = {$parentIDColumn}"
); );
if (!$this->query->isJoinedTo($componentBaseClass)) {
// Join on base table of component class
$componentBaseClass = $schema->baseDataClass($componentClass);
$componentBaseTable = $schema->tableName($componentBaseClass);
$componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID');
if (!$this->query->isJoinedTo($componentBaseTable)) {
$this->query->addLeftJoin( $this->query->addLeftJoin(
$componentBaseClass, $componentBaseTable,
"\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\"" "\"$relationTable\".\"$componentField\" = {$componentIDColumn}"
); );
} }
@ -831,9 +875,10 @@ class DataQuery {
*/ */
$ancestry = ClassInfo::ancestry($componentClass, true); $ancestry = ClassInfo::ancestry($componentClass, true);
$ancestry = array_reverse($ancestry); $ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){ foreach($ancestry as $ancestor) {
if($ancestor != $componentBaseClass && !$this->query->isJoinedTo($ancestor)){ $ancestorTable = $schema->tableName($ancestor);
$this->query->addInnerJoin($ancestor, "\"$componentBaseClass\".\"ID\" = \"$ancestor\".\"ID\""); if($ancestorTable != $componentBaseTable && !$this->query->isJoinedTo($ancestorTable)) {
$this->query->addLeftJoin($ancestorTable, "{$componentIDColumn} = \"{$ancestorTable}\".\"ID\"");
} }
} }
} }
@ -866,7 +911,7 @@ class DataQuery {
*/ */
public function selectFromTable($table, $fields) { public function selectFromTable($table, $fields) {
$fieldExpressions = array_map(function($item) use($table) { $fieldExpressions = array_map(function($item) use($table) {
return "\"$table\".\"$item\""; return "\"{$table}\".\"{$item}\"";
}, $fields); }, $fields);
$this->query->setSelect($fieldExpressions); $this->query->setSelect($fieldExpressions);
@ -908,7 +953,7 @@ class DataQuery {
// Special case for ID, if not provided // Special case for ID, if not provided
if($field === 'ID') { if($field === 'ID') {
return DataObject::quoted_column('ID', $this->dataClass); return DataObject::getSchema()->sqlColumnForField($this->dataClass, 'ID');
} }
return null; return null;
} }

View File

@ -91,13 +91,14 @@ class DBClassName extends DBEnum {
return $this->baseClass; return $this->baseClass;
} }
// Default to the basename of the record // Default to the basename of the record
$schema = DataObject::getSchema();
if($this->record) { if($this->record) {
return ClassInfo::baseDataClass($this->record); return $schema->baseDataClass($this->record);
} }
// During dev/build only the table is assigned // During dev/build only the table is assigned
$tableClass = $this->getClassNameFromTable($this->getTable()); $tableClass = $schema->tableClass($this->getTable());
if($tableClass) { if($tableClass && ($baseClass = $schema->baseDataClass($tableClass))) {
return $tableClass; return $baseClass;
} }
// Fallback to global default // Fallback to global default
return 'DataObject'; return 'DataObject';
@ -114,28 +115,6 @@ class DBClassName extends DBEnum {
return $this; return $this;
} }
/**
* Given a table name, find the base data class
*
* @param string $table
* @return string|null
*/
protected function getClassNameFromTable($table) {
if(empty($table)) {
return null;
}
$class = ClassInfo::baseDataClass($table);
if($class) {
return $class;
}
// If there is no class for this table, strip table modifiers (_Live / _versions) off the end
if(preg_match('/^(?<class>.+)(_[^_]+)$/i', $table, $matches)) {
return $this->getClassNameFromTable($matches['class']);
}
return null;
}
/** /**
* Get list of classnames that should be selectable * Get list of classnames that should be selectable
* *

View File

@ -35,15 +35,18 @@ class HasManyList extends RelationList {
} }
protected function foreignIDFilter($id = null) { protected function foreignIDFilter($id = null) {
if ($id === null) $id = $this->getForeignID(); if ($id === null) {
$id = $this->getForeignID();
}
// Apply relation filter // Apply relation filter
$key = "\"$this->foreignKey\""; $key = DataObject::getSchema()->sqlColumnForField($this->dataClass(), $this->getForeignKey());
if(is_array($id)) { if(is_array($id)) {
return array("$key IN (".DB::placeholders($id).")" => $id); return array("$key IN (".DB::placeholders($id).")" => $id);
} else if($id !== null){ } else if($id !== null){
return array($key => $id); return array($key => $id);
} }
return null;
} }
/** /**

View File

@ -449,32 +449,32 @@ class Hierarchy extends DataExtension {
* Mark this DataObject as expanded. * Mark this DataObject as expanded.
*/ */
public function markExpanded() { public function markExpanded() {
self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true; self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true; self::$expanded[$this->owner->baseClass()][$this->owner->ID] = true;
} }
/** /**
* Mark this DataObject as unexpanded. * Mark this DataObject as unexpanded.
*/ */
public function markUnexpanded() { public function markUnexpanded() {
self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true; self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = false; self::$expanded[$this->owner->baseClass()][$this->owner->ID] = false;
} }
/** /**
* Mark this DataObject's tree as opened. * Mark this DataObject's tree as opened.
*/ */
public function markOpened() { public function markOpened() {
self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true; self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true; self::$treeOpened[$this->owner->baseClass()][$this->owner->ID] = true;
} }
/** /**
* Mark this DataObject's tree as closed. * Mark this DataObject's tree as closed.
*/ */
public function markClosed() { public function markClosed() {
if(isset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID])) { if(isset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID])) {
unset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID]); unset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID]);
} }
} }
@ -484,7 +484,7 @@ class Hierarchy extends DataExtension {
* @return bool * @return bool
*/ */
public function isMarked() { public function isMarked() {
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = $this->owner->baseClass();
$id = $this->owner->ID; $id = $this->owner->ID;
return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false; return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
} }
@ -495,7 +495,7 @@ class Hierarchy extends DataExtension {
* @return bool * @return bool
*/ */
public function isExpanded() { public function isExpanded() {
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = $this->owner->baseClass();
$id = $this->owner->ID; $id = $this->owner->ID;
return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false; return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
} }
@ -506,7 +506,7 @@ class Hierarchy extends DataExtension {
* @return bool * @return bool
*/ */
public function isTreeOpened() { public function isTreeOpened() {
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = $this->owner->baseClass();
$id = $this->owner->ID; $id = $this->owner->ID;
return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false; return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
} }
@ -593,7 +593,7 @@ class Hierarchy extends DataExtension {
public function doAllChildrenIncludingDeleted($context = null) { public function doAllChildrenIncludingDeleted($context = null) {
if(!$this->owner) user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner'); if(!$this->owner) user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = $this->owner->baseClass();
if($baseClass) { if($baseClass) {
$stageChildren = $this->owner->stageChildren(true); $stageChildren = $this->owner->stageChildren(true);
@ -630,9 +630,13 @@ class Hierarchy extends DataExtension {
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied'); throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
} }
$baseClass=ClassInfo::baseDataClass($this->owner->class); $baseTable = $this->owner->baseTable();
return Versioned::get_including_deleted($baseClass, $parentIDColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'ParentID');
"\"ParentID\" = " . (int)$this->owner->ID, "\"$baseClass\".\"ID\" ASC"); return Versioned::get_including_deleted(
$this->owner->baseClass(),
[ $parentIDColumn => $this->owner->ID ],
"\"{$baseTable}\".\"ID\" ASC"
);
} }
/** /**
@ -646,8 +650,7 @@ class Hierarchy extends DataExtension {
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied'); throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
} }
return Versioned::get_including_deleted(ClassInfo::baseDataClass($this->owner->class), return $this->AllHistoricalChildren()->count();
"\"ParentID\" = " . (int)$this->owner->ID)->count();
} }
/** /**
@ -689,7 +692,7 @@ class Hierarchy extends DataExtension {
* @return DataList * @return DataList
*/ */
public function stageChildren($showAll = false) { public function stageChildren($showAll = false) {
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = $this->owner->baseClass();
$hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy; $hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
$hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree; $hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
$staged = $baseClass::get() $staged = $baseClass::get()
@ -722,7 +725,7 @@ class Hierarchy extends DataExtension {
throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied'); throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
} }
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = $this->owner->baseClass();
$hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy; $hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
$hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree; $hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
$children = $baseClass::get() $children = $baseClass::get()
@ -822,7 +825,7 @@ class Hierarchy extends DataExtension {
} }
$nextNode = null; $nextNode = null;
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = $this->owner->baseClass();
$children = $baseClass::get() $children = $baseClass::get()
->filter('ParentID', (int)$this->owner->ID) ->filter('ParentID', (int)$this->owner->ID)
@ -832,7 +835,7 @@ class Hierarchy extends DataExtension {
} }
// Try all the siblings of this node after the given node // Try all the siblings of this node after the given node
/*if( $siblings = DataObject::get( ClassInfo::baseDataClass($this->owner->class), /*if( $siblings = DataObject::get( $this->owner->baseClass(),
"\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\" "\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\"
> {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) ) $searchNodes->merge( $siblings );*/ > {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) ) $searchNodes->merge( $siblings );*/

View File

@ -49,7 +49,7 @@ class ManyManyList extends RelationList {
* @param string $joinTable The name of the table whose entries define the content of this many_many relation. * @param string $joinTable The name of the table whose entries define the content of this many_many relation.
* @param string $localKey The key in the join table that maps to the dataClass' PK. * @param string $localKey The key in the join table that maps to the dataClass' PK.
* @param string $foreignKey The key in the join table that maps to joined class' PK. * @param string $foreignKey The key in the join table that maps to joined class' PK.
* @param string $extraFields A map of field => fieldtype of extra fields on the join table. * @param array $extraFields A map of field => fieldtype of extra fields on the join table.
* *
* @example new ManyManyList('Group','Group_Members', 'GroupID', 'MemberID'); * @example new ManyManyList('Group','Group_Members', 'GroupID', 'MemberID');
*/ */
@ -69,8 +69,11 @@ class ManyManyList extends RelationList {
*/ */
protected function linkJoinTable() { protected function linkJoinTable() {
// Join to the many-many join table // Join to the many-many join table
$baseClass = ClassInfo::baseDataClass($this->dataClass); $dataClassIDColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID');
$this->dataQuery->innerJoin($this->joinTable, "\"{$this->joinTable}\".\"{$this->localKey}\" = \"{$baseClass}\".\"ID\""); $this->dataQuery->innerJoin(
$this->joinTable,
"\"{$this->joinTable}\".\"{$this->localKey}\" = {$dataClassIDColumn}"
);
// Add the extra fields to the query // Add the extra fields to the query
if($this->extraFields) { if($this->extraFields) {
@ -184,6 +187,7 @@ class ManyManyList extends RelationList {
* @param mixed $item * @param mixed $item
* @param array $extraFields A map of additional columns to insert into the joinTable. * @param array $extraFields A map of additional columns to insert into the joinTable.
* Column names should be ANSI quoted. * Column names should be ANSI quoted.
* @throws Exception
*/ */
public function add($item, $extraFields = array()) { public function add($item, $extraFields = array()) {
// Ensure nulls or empty strings are correctly treated as empty arrays // Ensure nulls or empty strings are correctly treated as empty arrays
@ -292,7 +296,9 @@ class ManyManyList extends RelationList {
user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING); user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING);
} }
$query->addWhere(array("\"{$this->localKey}\"" => $itemID)); $query->addWhere(array(
"\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
));
$query->execute(); $query->execute();
} }
@ -303,15 +309,16 @@ class ManyManyList extends RelationList {
* @return void * @return void
*/ */
public function removeAll() { public function removeAll() {
$base = ClassInfo::baseDataClass($this->dataClass());
// Remove the join to the join table to avoid MySQL row locking issues. // Remove the join to the join table to avoid MySQL row locking issues.
$query = $this->dataQuery(); $query = $this->dataQuery();
$foreignFilter = $query->getQueryParam('Foreign.Filter'); $foreignFilter = $query->getQueryParam('Foreign.Filter');
$query->removeFilterOn($foreignFilter); $query->removeFilterOn($foreignFilter);
// Select ID column
$selectQuery = $query->query(); $selectQuery = $query->query();
$selectQuery->setSelect("\"{$base}\".\"ID\""); $dataClassIDColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID');
$selectQuery->setSelect($dataClassIDColumn);
$from = $selectQuery->getFrom(); $from = $selectQuery->getFrom();
unset($from[$this->joinTable]); unset($from[$this->joinTable]);
@ -364,7 +371,7 @@ class ManyManyList extends RelationList {
user_error("Can't call ManyManyList::getExtraData() until a foreign ID is set", E_USER_WARNING); user_error("Can't call ManyManyList::getExtraData() until a foreign ID is set", E_USER_WARNING);
} }
$query->addWhere(array( $query->addWhere(array(
"\"{$this->localKey}\"" => $itemID "\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
)); ));
$queryResult = $query->execute()->current(); $queryResult = $query->execute()->current();
if ($queryResult) { if ($queryResult) {

View File

@ -174,26 +174,6 @@ abstract class DBConnector {
*/ */
abstract public function quoteString($value); abstract public function quoteString($value);
/**
* Escapes an identifier (table / database name). Typically the value
* is simply double quoted. Don't pass in already escaped identifiers in,
* as this will double escape the value!
*
* @param string $value The identifier to escape
* @param string $separator optional identifier splitter
*/
public function escapeIdentifier($value, $separator = '.') {
// ANSI standard id escape is to surround with double quotes
if(empty($separator)) return '"'.trim($value).'"';
// Split, escape, and glue back multiple identifiers
$segments = array();
foreach(explode($separator, $value) as $item) {
$segments[] = $this->escapeIdentifier($item, null);
}
return implode($separator, $segments);
}
/** /**
* Executes the following query with the specified error level. * Executes the following query with the specified error level.
* Implementations of this function should respect previewWrite and benchmarkQuery * Implementations of this function should respect previewWrite and benchmarkQuery

View File

@ -232,11 +232,18 @@ abstract class SS_Database {
* is simply double quoted. Don't pass in already escaped identifiers in, * is simply double quoted. Don't pass in already escaped identifiers in,
* as this will double escape the value! * as this will double escape the value!
* *
* @param string $value The identifier to escape * @param string|array $value The identifier to escape or list of split components
* @param string $separator optional identifier splitter * @param string $separator Splitter for each component
* @return string
*/ */
public function escapeIdentifier($value, $separator = '.') { public function escapeIdentifier($value, $separator = '.') {
return $this->connector->escapeIdentifier($value, $separator); // Split string into components
if(!is_array($value)) {
$value = explode($separator, $value);
}
// Implode quoted column
return '"' . implode('"'.$separator.'"', $value) . '"';
} }
/** /**

View File

@ -117,7 +117,7 @@ class ChangeSet extends DataObject {
$references = [ $references = [
'ObjectID' => $object->ID, 'ObjectID' => $object->ID,
'ObjectClass' => ClassInfo::baseDataClass($object) 'ObjectClass' => $object->baseClass(),
]; ];
// Get existing item in case already added // Get existing item in case already added
@ -146,7 +146,7 @@ class ChangeSet extends DataObject {
public function removeObject(DataObject $object) { public function removeObject(DataObject $object) {
$item = ChangeSetItem::get()->filter([ $item = ChangeSetItem::get()->filter([
'ObjectID' => $object->ID, 'ObjectID' => $object->ID,
'ObjectClass' => ClassInfo::baseDataClass($object), 'ObjectClass' => $object->baseClass(),
'ChangeSetID' => $this->ID 'ChangeSetID' => $this->ID
])->first(); ])->first();
@ -159,9 +159,17 @@ class ChangeSet extends DataObject {
$this->sync(); $this->sync();
} }
protected function implicitKey($item) { /**
if ($item instanceof ChangeSetItem) return $item->ObjectClass.'.'.$item->ObjectID; * Build identifying string key for this object
return ClassInfo::baseDataClass($item).'.'.$item->ID; *
* @param DataObject $item
* @return string
*/
protected function implicitKey(DataObject $item) {
if ($item instanceof ChangeSetItem) {
return $item->ObjectClass.'.'.$item->ObjectID;
}
return $item->baseClass().'.'.$item->ID;
} }
protected function calculateImplicit() { protected function calculateImplicit() {
@ -174,16 +182,18 @@ class ChangeSet extends DataObject {
/** @var string[][] $references List of which explicit items reference each thing in referenced */ /** @var string[][] $references List of which explicit items reference each thing in referenced */
$references = array(); $references = array();
/** @var ChangeSetItem $item */
foreach ($this->Changes()->filter(['Added' => ChangeSetItem::EXPLICITLY]) as $item) { foreach ($this->Changes()->filter(['Added' => ChangeSetItem::EXPLICITLY]) as $item) {
$explicitKey = $this->implicitKey($item); $explicitKey = $this->implicitKey($item);
$explicit[$explicitKey] = true; $explicit[$explicitKey] = true;
foreach ($item->findReferenced() as $referee) { foreach ($item->findReferenced() as $referee) {
/** @var DataObject $referee */
$key = $this->implicitKey($referee); $key = $this->implicitKey($referee);
$referenced[$key] = [ $referenced[$key] = [
'ObjectID' => $referee->ID, 'ObjectID' => $referee->ID,
'ObjectClass' => ClassInfo::baseDataClass($referee) 'ObjectClass' => $referee->baseClass(),
]; ];
$references[$key][] = $item->ID; $references[$key][] = $item->ID;
@ -220,6 +230,7 @@ class ChangeSet extends DataObject {
$implicit = $this->calculateImplicit(); $implicit = $this->calculateImplicit();
// Adjust the existing implicit ChangeSetItems for this ChangeSet // Adjust the existing implicit ChangeSetItems for this ChangeSet
/** @var ChangeSetItem $item */
foreach ($this->Changes()->filter(['Added' => ChangeSetItem::IMPLICITLY]) as $item) { foreach ($this->Changes()->filter(['Added' => ChangeSetItem::IMPLICITLY]) as $item) {
$objectKey = $this->implicitKey($item); $objectKey = $this->implicitKey($item);

View File

@ -67,7 +67,7 @@ class ChangeSetItem extends DataObject implements Thumbnail {
public function onBeforeWrite() { public function onBeforeWrite() {
// Make sure ObjectClass refers to the base data class in the case of old or wrong code // Make sure ObjectClass refers to the base data class in the case of old or wrong code
$this->ObjectClass = ClassInfo::baseDataClass($this->ObjectClass); $this->ObjectClass = $this->getSchema()->baseDataClass($this->ObjectClass);
parent::onBeforeWrite(); parent::onBeforeWrite();
} }
@ -328,7 +328,7 @@ class ChangeSetItem extends DataObject implements Thumbnail {
public static function get_for_object($object) { public static function get_for_object($object) {
return ChangeSetItem::get()->filter([ return ChangeSetItem::get()->filter([
'ObjectID' => $object->ID, 'ObjectID' => $object->ID,
'ObjectClass' => ClassInfo::baseDataClass($object) 'ObjectClass' => $object->baseClass(),
]); ]);
} }
@ -342,7 +342,7 @@ class ChangeSetItem extends DataObject implements Thumbnail {
public static function get_for_object_by_id($objectID, $objectClass) { public static function get_for_object_by_id($objectID, $objectClass) {
return ChangeSetItem::get()->filter([ return ChangeSetItem::get()->filter([
'ObjectID' => $objectID, 'ObjectID' => $objectID,
'ObjectClass' => ClassInfo::baseDataClass($objectClass) 'ObjectClass' => static::getSchema()->baseDataClass($objectClass)
]); ]);
} }

View File

@ -0,0 +1,24 @@
<?php
/**
* Minimum level extra fields required by extensions that are versonable
*/
interface VersionableExtension {
/**
* Determine if the given table is versionable
*
* @param string $table
* @return bool True if versioned tables should be built for the given suffix
*/
public function isVersionedTable($table);
/**
* Update fields and indexes for the versonable suffix table
*
* @param string $suffix Table suffix being built
* @param array $fields List of fields in this model
* @param array $indexes List of indexes in this model
*/
public function updateVersionableFields($suffix, &$fields, &$indexes);
}

View File

@ -112,14 +112,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
*/ */
private static $prepopulate_versionnumber_cache = true; private static $prepopulate_versionnumber_cache = true;
/**
* Keep track of the archive tables that have been created.
*
* @config
* @var array
*/
private static $archive_tables = array();
/** /**
* Additional database indexes for the new * Additional database indexes for the new
* "_versions" table. Used in {@link augmentDatabase()}. * "_versions" table. Used in {@link augmentDatabase()}.
@ -163,13 +155,13 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* array('Extension1' => 'suffix1', 'Extension2' => array('suffix2', 'suffix3'))); * array('Extension1' => 'suffix1', 'Extension2' => array('suffix2', 'suffix3')));
* *
* *
* Make sure your extension has a static $enabled-property that determines if it is * Your extension must implement VersionableExtension interface in order to
* processed by Versioned. * apply custom tables for versioned.
* *
* @config * @config
* @var array * @var array
*/ */
private static $versionableExtensions = array('Translatable' => 'lang'); private static $versionableExtensions = [];
/** /**
* Permissions necessary to view records outside of the live stage (e.g. archive / draft stage). * Permissions necessary to view records outside of the live stage (e.g. archive / draft stage).
@ -275,7 +267,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
*/ */
protected function getLastEditedForVersion($version) { protected function getLastEditedForVersion($version) {
// Cache key // Cache key
$baseTable = ClassInfo::baseDataClass($this->owner); $baseTable = $this->baseTable();
$id = $this->owner->ID; $id = $this->owner->ID;
$key = "{$baseTable}#{$id}/{$version}"; $key = "{$baseTable}#{$id}/{$version}";
@ -298,7 +290,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
return $date; return $date;
} }
/**
* Updates query parameters of relations attached to versioned dataobjects
*
* @param array $params
*/
public function updateInheritableQueryParams(&$params) { public function updateInheritableQueryParams(&$params) {
// Skip if versioned isn't set // Skip if versioned isn't set
if(!isset($params['Versioned.mode'])) { if(!isset($params['Versioned.mode'])) {
@ -306,7 +302,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
} }
// Adjust query based on original selection criterea // Adjust query based on original selection criterea
$owner = $this->owner;
switch($params['Versioned.mode']) { switch($params['Versioned.mode']) {
case 'all_versions': { case 'all_versions': {
// Versioned.mode === all_versions doesn't inherit very well, so default to stage // Versioned.mode === all_versions doesn't inherit very well, so default to stage
@ -350,8 +345,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
return; return;
} }
$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass()); $baseTable = $this->baseTable();
$versionedMode = $dataQuery->getQueryParam('Versioned.mode'); $versionedMode = $dataQuery->getQueryParam('Versioned.mode');
switch($versionedMode) { switch($versionedMode) {
// Reading a specific stage (Stage or Live) // Reading a specific stage (Stage or Live)
@ -392,7 +386,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
} }
$tempName = 'ExclusionarySource_'.$excluding; $tempName = 'ExclusionarySource_'.$excluding;
$excludingTable = $baseTable . ($excluding && $excluding != static::DRAFT ? "_$excluding" : ''); $excludingTable = $this->baseTable($excluding);
$query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")'); $query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")');
$query->renameTable($tempName, $excludingTable); $query->renameTable($tempName, $excludingTable);
@ -486,6 +480,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
]); ]);
break; break;
} }
case 'all_versions':
default: { default: {
// If all versions are requested, ensure that records are sorted by this field // If all versions are requested, ensure that records are sorted by this field
$query->addOrderBy(sprintf('"%s_versions"."%s"', $baseTable, 'Version')); $query->addOrderBy(sprintf('"%s_versions"."%s"', $baseTable, 'Version'));
@ -507,11 +502,26 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @return bool True if this table should be versioned * @return bool True if this table should be versioned
*/ */
protected function isTableVersioned($table) { protected function isTableVersioned($table) {
if(!class_exists($table)) { $schema = DataObject::getSchema();
$tableClass = $schema->tableClass($table);
if(empty($tableClass)) {
return false; return false;
} }
$baseClass = ClassInfo::baseDataClass($this->owner);
return is_a($table, $baseClass, true); // Check that this class belongs to the same tree
$baseClass = $schema->baseDataClass($this->owner);
if(!is_a($tableClass, $baseClass, true)) {
return false;
}
// Check that this isn't a derived table
// (e.g. _Live, or a many_many table)
$mainTable = $schema->tableName($tableClass);
if($mainTable !== $table) {
return false;
}
return true;
} }
/** /**
@ -527,7 +537,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
// metadata set on the query object. This prevents regular queries from // metadata set on the query object. This prevents regular queries from
// accidentally querying the *_versions tables. // accidentally querying the *_versions tables.
$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode'); $versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
$dataClass = ClassInfo::baseDataClass($dataQuery->dataClass());
$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive', 'version'); $modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive', 'version');
if( if(
!empty($dataObject->Version) && !empty($dataObject->Version) &&
@ -535,36 +544,21 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
) { ) {
// This will ensure that augmentSQL will select only the same version as the owner, // This will ensure that augmentSQL will select only the same version as the owner,
// regardless of how this object was initially selected // regardless of how this object was initially selected
$versionColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'Version');
$dataQuery->where([ $dataQuery->where([
"\"$dataClass\".\"Version\"" => $dataObject->Version $versionColumn => $dataObject->Version
]); ]);
$dataQuery->setQueryParam('Versioned.mode', 'all_versions'); $dataQuery->setQueryParam('Versioned.mode', 'all_versions');
} }
} }
/**
* Called by {@link SapphireTest} when the database is reset.
*
* @todo Reduce the coupling between this and SapphireTest, somehow.
*/
public static function on_db_reset() {
// Drop all temporary tables
$db = DB::get_conn();
foreach(static::$archive_tables as $tableName) {
if(method_exists($db, 'dropTable')) $db->dropTable($tableName);
else $db->query("DROP TABLE \"$tableName\"");
}
// Remove references to them
static::$archive_tables = array();
}
public function augmentDatabase() { public function augmentDatabase() {
$owner = $this->owner; $owner = $this->owner;
$classTable = $owner->class; $class = get_class($owner);
$baseTable = $this->baseTable();
$classTable = $owner->getSchema()->tableName($owner);
$isRootClass = ($owner->class == ClassInfo::baseDataClass($owner->class)); $isRootClass = $class === $owner->baseClass();
// Build a list of suffixes whose tables need versioning // Build a list of suffixes whose tables need versioning
$allSuffixes = array(); $allSuffixes = array();
@ -572,7 +566,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
if(count($versionableExtensions)){ if(count($versionableExtensions)){
foreach ($versionableExtensions as $versionableExtension => $suffixes) { foreach ($versionableExtensions as $versionableExtension => $suffixes) {
if ($owner->hasExtension($versionableExtension)) { if ($owner->hasExtension($versionableExtension)) {
$allSuffixes = array_merge($allSuffixes, (array)$suffixes);
foreach ((array)$suffixes as $suffix) { foreach ((array)$suffixes as $suffix) {
$allSuffixes[$suffix] = $versionableExtension; $allSuffixes[$suffix] = $versionableExtension;
} }
@ -581,49 +574,56 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
} }
// Add the default table with an empty suffix to the list (table name = class name) // Add the default table with an empty suffix to the list (table name = class name)
array_push($allSuffixes,''); $allSuffixes[''] = null;
foreach ($allSuffixes as $key => $suffix) { foreach ($allSuffixes as $suffix => $extension) {
// check that this is a valid suffix // Check tables for this build
if (!is_int($key)) continue; if ($suffix) {
$suffixBaseTable = "{$baseTable}_{$suffix}";
if ($suffix) $table = "{$classTable}_$suffix"; $suffixTable = "{$classTable}_{$suffix}";
else $table = $classTable; } else {
$suffixBaseTable = $baseTable;
$suffixTable = $classTable;
}
$fields = DataObject::database_fields($owner->class); $fields = DataObject::database_fields($owner->class);
unset($fields['ID']); unset($fields['ID']);
if($fields) { if($fields) {
$options = Config::inst()->get($owner->class, 'create_table_options', Config::FIRST_SET); $options = Config::inst()->get($owner->class, 'create_table_options', Config::FIRST_SET);
$indexes = $owner->databaseIndexes(); $indexes = $owner->databaseIndexes();
if ($suffix && ($ext = $owner->getExtensionInstance($allSuffixes[$suffix]))) { $extensionClass = $allSuffixes[$suffix];
if (!$ext->isVersionedTable($table)) continue; if ($suffix && ($extension = $owner->getExtensionInstance($extensionClass))) {
$ext->setOwner($owner); if (!$extension instanceof VersionableExtension) {
$fields = $ext->fieldsInExtraTables($suffix); throw new LogicException(
$ext->clearOwner(); "Extension {$extensionClass} must implement VersionableExtension"
$indexes = $fields['indexes']; );
$fields = $fields['db']; }
// Allow versionable extension to customise table fields and indexes
$extension->setOwner($owner);
if ($extension->isVersionedTable($suffixTable)) {
$extension->updateVersionableFields($suffix, $fields, $indexes);
}
$extension->clearOwner();
} }
// Create tables for other stages // Build _Live table
if($this->hasStages()) { if($this->hasStages()) {
// Extra tables for _Live, etc. $liveTable = $this->stageTable($suffixTable, static::LIVE);
// Change unique indexes to 'index'. Versioned tables may run into unique indexing difficulties
// otherwise.
$liveTable = $this->stageTable($table, static::LIVE);
$indexes = $this->uniqueToIndex($indexes);
DB::require_table($liveTable, $fields, $indexes, false, $options); DB::require_table($liveTable, $fields, $indexes, false, $options);
} }
// Build _versions table
//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
$nonUniqueIndexes = $this->uniqueToIndex($indexes);
if($isRootClass) { if($isRootClass) {
// Create table for all versions // Create table for all versions
$versionFields = array_merge( $versionFields = array_merge(
Config::inst()->get('Versioned', 'db_for_versions_table'), Config::inst()->get('Versioned', 'db_for_versions_table'),
(array)$fields (array)$fields
); );
$versionIndexes = array_merge( $versionIndexes = array_merge(
Config::inst()->get('Versioned', 'indexes_for_versions_table'), Config::inst()->get('Versioned', 'indexes_for_versions_table'),
(array)$indexes (array)$nonUniqueIndexes
); );
} else { } else {
// Create fields for any tables of subclasses // Create fields for any tables of subclasses
@ -634,86 +634,74 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
), ),
(array)$fields (array)$fields
); );
//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
$indexes = $this->uniqueToIndex($indexes);
$versionIndexes = array_merge( $versionIndexes = array_merge(
array( array(
'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'), 'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'),
'RecordID' => true, 'RecordID' => true,
'Version' => true, 'Version' => true,
), ),
(array)$indexes (array)$nonUniqueIndexes
); );
} }
if(DB::get_schema()->hasTable("{$table}_versions")) { // Cleanup any orphans
// Fix data that lacks the uniqueness constraint (since this was added later and $this->cleanupVersionedOrphans("{$suffixBaseTable}_versions", "{$suffixTable}_versions");
// bugs meant that the constraint was validated)
$duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\"
FROM \"{$table}_versions\" GROUP BY \"RecordID\", \"Version\"
HAVING COUNT(*) > 1");
foreach($duplications as $dup) { // Build versions table
DB::alteration_message("Removing {$table}_versions duplicate data for " DB::require_table("{$suffixTable}_versions", $versionFields, $versionIndexes, true, $options);
."{$dup['RecordID']}/{$dup['Version']}" ,"deleted");
DB::prepared_query(
"DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = ?
AND \"Version\" = ? AND \"ID\" != ?",
array($dup['RecordID'], $dup['Version'], $dup['ID'])
);
}
// Remove junk which has no data in parent classes. Only needs to run the following
// when versioned data is spread over multiple tables
if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) {
foreach($versionedTables as $child) {
if($table === $child) break; // only need subclasses
// Select all orphaned version records
$orphanedQuery = SQLSelect::create()
->selectField("\"{$table}_versions\".\"ID\"")
->setFrom("\"{$table}_versions\"");
// If we have a parent table limit orphaned records
// to only those that exist in this
if(DB::get_schema()->hasTable("{$child}_versions")) {
$orphanedQuery
->addLeftJoin(
"{$child}_versions",
"\"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\""
)
->addWhere("\"{$child}_versions\".\"ID\" IS NULL");
}
$count = $orphanedQuery->count();
if($count > 0) {
DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
$ids = $orphanedQuery->execute()->column();
foreach($ids as $id) {
DB::prepared_query(
"DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?",
array($id)
);
}
}
}
}
}
DB::require_table("{$table}_versions", $versionFields, $versionIndexes, true, $options);
} else { } else {
DB::dont_require_table("{$table}_versions"); DB::dont_require_table("{$suffixTable}_versions");
if($this->hasStages()) { if($this->hasStages()) {
$liveTable = $this->stageTable($table, static::LIVE); $liveTable = $this->stageTable($suffixTable, static::LIVE);
DB::dont_require_table($liveTable); DB::dont_require_table($liveTable);
} }
} }
} }
} }
/**
* Cleanup orphaned records in the _versions table
*
* @param string $baseTable base table to use as authoritative source of records
* @param string $childTable Sub-table to clean orphans from
*/
protected function cleanupVersionedOrphans($baseTable, $childTable) {
// Skip if child table doesn't exist
if(!DB::get_schema()->hasTable($childTable)) {
return;
}
// Skip if tables are the same
if($childTable === $baseTable) {
return;
}
// Select all orphaned version records
$orphanedQuery = SQLSelect::create()
->selectField("\"{$childTable}\".\"ID\"")
->setFrom("\"{$childTable}\"");
// If we have a parent table limit orphaned records
// to only those that exist in this
if(DB::get_schema()->hasTable($baseTable)) {
$orphanedQuery
->addLeftJoin(
$baseTable,
"\"{$childTable}\".\"RecordID\" = \"{$baseTable}\".\"RecordID\"
AND \"{$childTable}\".\"Version\" = \"{$baseTable}\".\"Version\""
)
->addWhere("\"{$baseTable}\".\"ID\" IS NULL");
}
$count = $orphanedQuery->count();
if($count > 0) {
DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
$ids = $orphanedQuery->execute()->column();
foreach($ids as $id) {
DB::prepared_query("DELETE FROM \"{$childTable}\" WHERE \"ID\" = ?", array($id));
}
}
}
/** /**
* Helper for augmentDatabase() to find unique indexes and convert them to non-unique * Helper for augmentDatabase() to find unique indexes and convert them to non-unique
* *
@ -747,23 +735,26 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* Generates a ($table)_version DB manipulation and injects it into the current $manipulation * Generates a ($table)_version DB manipulation and injects it into the current $manipulation
* *
* @param array $manipulation Source manipulation data * @param array $manipulation Source manipulation data
* @param string $table Name of table * @param string $class Class
* @param string $table Table Table for this class
* @param int $recordID ID of record to version * @param int $recordID ID of record to version
*/ */
protected function augmentWriteVersioned(&$manipulation, $table, $recordID) { protected function augmentWriteVersioned(&$manipulation, $class, $table, $recordID) {
$baseDataClass = ClassInfo::baseDataClass($table); $baseDataClass = DataObject::getSchema()->baseDataClass($class);
$baseDataTable = DataObject::getSchema()->tableName($baseDataClass);
// Set up a new entry in (table)_versions // Set up a new entry in (table)_versions
$newManipulation = array( $newManipulation = array(
"command" => "insert", "command" => "insert",
"fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null "fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null,
"class" => $class,
); );
// Add any extra, unchanged fields to the version record. // Add any extra, unchanged fields to the version record.
$data = DB::prepared_query("SELECT * FROM \"$table\" WHERE \"ID\" = ?", array($recordID))->record(); $data = DB::prepared_query("SELECT * FROM \"{$table}\" WHERE \"ID\" = ?", array($recordID))->record();
if ($data) { if ($data) {
$fields = DataObject::database_fields($table); $fields = DataObject::database_fields($class);
if (is_array($fields)) { if (is_array($fields)) {
$data = array_intersect_key($data, $fields); $data = array_intersect_key($data, $fields);
@ -784,13 +775,13 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
$nextVersion = 0; $nextVersion = 0;
if($recordID) { if($recordID) {
$nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1 $nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1
FROM \"{$baseDataClass}_versions\" WHERE \"RecordID\" = ?", FROM \"{$baseDataTable}_versions\" WHERE \"RecordID\" = ?",
array($recordID) array($recordID)
)->value(); )->value();
} }
$nextVersion = $nextVersion ?: 1; $nextVersion = $nextVersion ?: 1;
if($table === $baseDataClass) { if($class === $baseDataClass) {
// Write AuthorID for baseclass // Write AuthorID for baseclass
$userID = (Member::currentUser()) ? Member::currentUser()->ID : 0; $userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
$newManipulation['fields']['AuthorID'] = $userID; $newManipulation['fields']['AuthorID'] = $userID;
@ -830,13 +821,13 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
// get Version number from base data table on write // get Version number from base data table on write
$version = null; $version = null;
$owner = $this->owner; $owner = $this->owner;
$baseDataClass = ClassInfo::baseDataClass($owner->class); $baseDataTable = DataObject::getSchema()->baseDataTable($owner);
if(isset($manipulation[$baseDataClass]['fields'])) { if(isset($manipulation[$baseDataTable]['fields'])) {
if ($this->migratingVersion) { if ($this->migratingVersion) {
$manipulation[$baseDataClass]['fields']['Version'] = $this->migratingVersion; $manipulation[$baseDataTable]['fields']['Version'] = $this->migratingVersion;
} }
if (isset($manipulation[$baseDataClass]['fields']['Version'])) { if (isset($manipulation[$baseDataTable]['fields']['Version'])) {
$version = $manipulation[$baseDataClass]['fields']['Version']; $version = $manipulation[$baseDataTable]['fields']['Version'];
} }
} }
@ -845,7 +836,8 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
foreach($tables as $table) { foreach($tables as $table) {
// Make sure that the augmented write is being applied to a table that can be versioned // Make sure that the augmented write is being applied to a table that can be versioned
if( !$this->canBeVersioned($table) ) { $class = isset($manipulation[$table]['class']) ? $manipulation[$table]['class'] : null;
if(!$class || !$this->canBeVersioned($class) ) {
unset($manipulation[$table]); unset($manipulation[$table]);
continue; continue;
} }
@ -864,7 +856,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
} elseif(empty($version)) { } elseif(empty($version)) {
// If we haven't got a version #, then we're creating a new version. // If we haven't got a version #, then we're creating a new version.
// Otherwise, we're just copying a version to another table // Otherwise, we're just copying a version to another table
$this->augmentWriteVersioned($manipulation, $table, $id); $this->augmentWriteVersioned($manipulation, $class, $table, $id);
} }
// Remove "Version" column from subclasses of baseDataClass // Remove "Version" column from subclasses of baseDataClass
@ -879,7 +871,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
// If we're editing Live, then use (table)_Live instead of (table) // If we're editing Live, then use (table)_Live instead of (table)
if($this->hasStages() && static::get_stage() === static::LIVE) { if($this->hasStages() && static::get_stage() === static::LIVE) {
$this->augmentWriteStaged($manipulation, $table, $id); $this->augmentWriteStaged($manipulation, $class, $id);
} }
} }
@ -1008,9 +1000,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
protected function lookupReverseOwners() { protected function lookupReverseOwners() {
// Find all classes with 'owns' config // Find all classes with 'owns' config
$lookup = array(); $lookup = array();
foreach(ClassInfo::subclassesFor(DataObject::class) as $class) { foreach(ClassInfo::subclassesFor('DataObject') as $class) {
// Ensure this class is versioned // Ensure this class is versioned
if(!Object::has_extension($class, Versioned::class)) { if(!Object::has_extension($class, 'Versioned')) {
continue; continue;
} }
@ -1372,16 +1364,16 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
} }
/** /**
* Determine if a table is supporting the Versioned extensions (e.g. * Determine if a class is supporting the Versioned extensions (e.g.
* $table_versions does exists). * $table_versions does exists).
* *
* @param string $table Table name * @param string $class Class name
* @return boolean * @return boolean
*/ */
public function canBeVersioned($table) { public function canBeVersioned($class) {
return ClassInfo::exists($table) return ClassInfo::exists($class)
&& is_subclass_of($table, 'DataObject') && is_subclass_of($class, 'DataObject')
&& DataObject::has_own_table($table); && DataObject::has_own_table($class);
} }
/** /**
@ -1392,14 +1384,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @return boolean Returns false if the field isn't in the table, true otherwise * @return boolean Returns false if the field isn't in the table, true otherwise
*/ */
public function hasVersionField($table) { public function hasVersionField($table) {
// Strip "_Live" from end of table
$live = static::LIVE;
if($this->hasStages() && preg_match("/^(?<table>.*)_{$live}$/", $table, $matches)) {
$table = $matches['table'];
}
// Base table has version field // Base table has version field
return $table === ClassInfo::baseDataClass($table); $class = DataObject::getSchema()->tableClass($table);
return $class === DataObject::getSchema()->baseDataClass($class);
} }
/** /**
@ -1416,11 +1403,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
if ($owner->hasExtension($versionableExtension)) { if ($owner->hasExtension($versionableExtension)) {
$ext = $owner->getExtensionInstance($versionableExtension); $ext = $owner->getExtensionInstance($versionableExtension);
$ext->setOwner($owner); $ext->setOwner($owner);
$table = $ext->extendWithSuffix($table); $table = $ext->extendWithSuffix($table);
$ext->clearOwner(); $ext->clearOwner();
}
} }
} }
}
return $table; return $table;
} }
@ -1433,12 +1420,12 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
public function latestPublished() { public function latestPublished() {
// Get the root data object class - this will have the version field // Get the root data object class - this will have the version field
$owner = $this->owner; $owner = $this->owner;
$table1 = ClassInfo::baseDataClass($owner); $draftTable = $this->baseTable();
$table2 = $this->stageTable($table1, static::LIVE); $liveTable = $this->stageTable($draftTable, static::LIVE);
return DB::prepared_query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\" return DB::prepared_query("SELECT \"$draftTable\".\"Version\" = \"$liveTable\".\"Version\" FROM \"$draftTable\"
INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\" INNER JOIN \"$liveTable\" ON \"$draftTable\".\"ID\" = \"$liveTable\".\"ID\"
WHERE \"$table1\".\"ID\" = ?", WHERE \"$draftTable\".\"ID\" = ?",
array($owner->ID) array($owner->ID)
)->value(); )->value();
} }
@ -1522,7 +1509,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
$joinClass = $owner->hasManyComponent($relationship); $joinClass = $owner->hasManyComponent($relationship);
$joinField = $owner->getRemoteJoinField($relationship, 'has_many', $polymorphic); $joinField = $owner->getRemoteJoinField($relationship, 'has_many', $polymorphic);
$idField = $polymorphic ? "{$joinField}ID" : $joinField; $idField = $polymorphic ? "{$joinField}ID" : $joinField;
$joinTable = ClassInfo::table_for_object_field($joinClass, $idField); $joinTable = DataObject::getSchema()->tableForField($joinClass, $idField);
// Generate update query which will unlink disowned objects // Generate update query which will unlink disowned objects
$targetTable = $this->stageTable($joinTable, $targetStage); $targetTable = $this->stageTable($joinTable, $targetStage);
@ -1604,14 +1591,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
$owner->invokeWithExtensions('onBeforeUnpublish'); $owner->invokeWithExtensions('onBeforeUnpublish');
$origStage = static::get_stage(); $origReadingMode = static::get_reading_mode();
static::set_stage(static::LIVE); static::set_stage(static::LIVE);
// This way our ID won't be unset // This way our ID won't be unset
$clone = clone $owner; $clone = clone $owner;
$clone->delete(); $clone->delete();
static::set_stage($origStage); static::set_reading_mode($origReadingMode);
$owner->invokeWithExtensions('onAfterUnpublish'); $owner->invokeWithExtensions('onAfterUnpublish');
return true; return true;
@ -1688,7 +1675,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
$owner = $this->owner; $owner = $this->owner;
$owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion); $owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
$baseClass = ClassInfo::baseDataClass($owner->class); $baseClass = $owner->baseClass();
/** @var Versioned|DataObject $from */ /** @var Versioned|DataObject $from */
if(is_numeric($fromStage)) { if(is_numeric($fromStage)) {
@ -1875,28 +1862,30 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
/** /**
* Return the base table - the class that directly extends DataObject. * Return the base table - the class that directly extends DataObject.
* *
* Protected so it doesn't conflict with DataObject::baseTable()
*
* @param string $stage * @param string $stage
* @return string * @return string
*/ */
public function baseTable($stage = null) { protected function baseTable($stage = null) {
$baseClass = ClassInfo::baseDataClass($this->owner); $baseTable = $this->owner->baseTable();
return $this->stageTable($baseClass, $stage); return $this->stageTable($baseTable, $stage);
} }
/** /**
* Given a class and stage determine the table name. * Given a table and stage determine the table name.
* *
* Note: Stages this asset does not exist in will default to the draft table. * Note: Stages this asset does not exist in will default to the draft table.
* *
* @param string $class * @param string $table Main table
* @param string $stage * @param string $stage
* @return string Table name * @return string Staged table name
*/ */
public function stageTable($class, $stage) { public function stageTable($table, $stage) {
if($this->hasStages() && $stage === static::LIVE) { if($this->hasStages() && $stage === static::LIVE) {
return "{$class}_{$stage}"; return "{$table}_{$stage}";
} }
return $class; return $table;
} }
//-----------------------------------------------------------------------------------------------// //-----------------------------------------------------------------------------------------------//
@ -2025,8 +2014,12 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* Set the reading stage. * Set the reading stage.
* *
* @param string $stage New reading stage. * @param string $stage New reading stage.
* @throws InvalidArgumentException
*/ */
public static function set_stage($stage) { public static function set_stage($stage) {
if(!in_array($stage, [static::LIVE, static::DRAFT])) {
throw new \InvalidArgumentException("Invalid stage name \"{$stage}\"");
}
static::set_reading_mode('Stage.' . $stage); static::set_reading_mode('Stage.' . $stage);
} }
@ -2069,8 +2062,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @return int * @return int
*/ */
public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) { public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
$baseClass = ClassInfo::baseDataClass($class); $baseClass = DataObject::getSchema()->baseDataClass($class);
$stageTable = ($stage == static::DRAFT) ? $baseClass : "{$baseClass}_{$stage}"; $stageTable = DataObject::getSchema()->tableName($baseClass);
if($stage === static::LIVE) {
$stageTable .= "_{$stage}";
}
// cached call // cached call
if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) { if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
@ -2126,8 +2122,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
$parameters = $idList; $parameters = $idList;
} }
$baseClass = ClassInfo::baseDataClass($class); /** @var Versioned|DataObject $singleton */
$stageTable = ($stage == static::DRAFT) ? $baseClass : "{$baseClass}_{$stage}"; $singleton = DataObject::singleton($class);
$baseClass = $singleton->baseClass();
$baseTable = $singleton->baseTable();
$stageTable = $singleton->stageTable($baseTable, $stage);
$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map(); $versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
@ -2173,7 +2172,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
Versioned::set_reading_mode($oldMode); Versioned::set_reading_mode($oldMode);
// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive) // Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
$baseClass = ClassInfo::baseDataClass($owner->class); $baseClass = $owner->baseClass();
self::$cache_versionnumber[$baseClass][$stage][$owner->ID] = null; self::$cache_versionnumber[$baseClass][$stage][$owner->ID] = null;
} }
@ -2214,7 +2213,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
public function onAfterRollback($version) { public function onAfterRollback($version) {
// Find record at this version // Find record at this version
$baseClass = ClassInfo::baseDataClass($this->owner); $baseClass = DataObject::getSchema()->baseDataClass($this->owner);
/** @var Versioned|DataObject $recordVersion */ /** @var Versioned|DataObject $recordVersion */
$recordVersion = static::get_version($baseClass, $this->owner->ID, $version); $recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
@ -2234,7 +2233,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @return DataObject * @return DataObject
*/ */
public static function get_latest_version($class, $id) { public static function get_latest_version($class, $id) {
$baseClass = ClassInfo::baseDataClass($class); $baseClass = DataObject::getSchema()->baseDataClass($class);
$list = DataList::create($baseClass) $list = DataList::create($baseClass)
->setDataQueryParam("Versioned.mode", "latest_versions"); ->setDataQueryParam("Versioned.mode", "latest_versions");
@ -2277,8 +2276,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
return true; return true;
} }
$baseClass = ClassInfo::baseDataClass($owner->class); $table = $this->baseTable(static::LIVE);
$table = $this->stageTable($baseClass, static::LIVE);
$result = DB::prepared_query( $result = DB::prepared_query(
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?", "SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
array($owner->ID) array($owner->ID)
@ -2297,7 +2295,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
return false; return false;
} }
$table = ClassInfo::baseDataClass($owner->class); $table = $this->baseTable();
$result = DB::prepared_query( $result = DB::prepared_query(
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?", "SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
array($owner->ID) array($owner->ID)
@ -2341,7 +2339,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @return DataObject * @return DataObject
*/ */
public static function get_version($class, $id, $version) { public static function get_version($class, $id, $version) {
$baseClass = ClassInfo::baseDataClass($class); $baseClass = DataObject::getSchema()->baseDataClass($class);
$list = DataList::create($baseClass) $list = DataList::create($baseClass)
->setDataQueryParam([ ->setDataQueryParam([
"Versioned.mode" => 'version', "Versioned.mode" => 'version',

View File

@ -93,8 +93,11 @@ class SearchContext extends Object {
*/ */
protected function applyBaseTableFields() { protected function applyBaseTableFields() {
$classes = ClassInfo::dataClassesFor($this->modelClass); $classes = ClassInfo::dataClassesFor($this->modelClass);
$fields = array("\"".ClassInfo::baseDataClass($this->modelClass).'".*'); $baseTable = DataObject::getSchema()->baseDataTable($this->modelClass);
if($this->modelClass != $classes[0]) $fields[] = '"'.$classes[0].'".*'; $fields = array("\"{$baseTable}\".*");
if($this->modelClass != $classes[0]) {
$fields[] = '"'.$classes[0].'".*';
}
//$fields = array_keys($model->db()); //$fields = array_keys($model->db());
$fields[] = '"'.$classes[0].'".\"ClassName\" AS "RecordClassName"'; $fields[] = '"'.$classes[0].'".\"ClassName\" AS "RecordClassName"';
return $fields; return $fields;

View File

@ -51,6 +51,7 @@ class FulltextFilter extends SearchFilter {
* MyDataObject::get()->filter('SearchFields:fulltext', 'search term') * MyDataObject::get()->filter('SearchFields:fulltext', 'search term')
* </code> * </code>
* *
* @throws Exception
* @return string * @return string
*/ */
public function getDbName() { public function getDbName() {
@ -65,8 +66,11 @@ class FulltextFilter extends SearchFilter {
if(preg_match('/^fulltext\s+\((.+)\)$/i', $index, $matches)) { if(preg_match('/^fulltext\s+\((.+)\)$/i', $index, $matches)) {
return $this->prepareColumns($matches[1]); return $this->prepareColumns($matches[1]);
} else { } else {
throw new Exception("Invalid fulltext index format for '" . $this->getName() throw new Exception(sprintf(
. "' on '" . $this->model . "'"); "Invalid fulltext index format for '%s' on '%s'",
$this->getName(),
$this->model
));
} }
} }
} }
@ -78,13 +82,14 @@ class FulltextFilter extends SearchFilter {
* Adds table identifier to the every column. * Adds table identifier to the every column.
* Columns must have table identifier to prevent duplicate column name error. * Columns must have table identifier to prevent duplicate column name error.
* *
* @param array $columns
* @return string * @return string
*/ */
protected function prepareColumns($columns) { protected function prepareColumns($columns) {
$cols = preg_split('/"?\s*,\s*"?/', trim($columns, '(") ')); $cols = preg_split('/"?\s*,\s*"?/', trim($columns, '(") '));
$class = ClassInfo::table_for_object_field($this->model, current($cols)); $table = DataObject::getSchema()->tableForField($this->model, current($cols));
$cols = array_map(function($col) use ($class) { $cols = array_map(function($col) use ($table) {
return sprintf('"%s"."%s"', $class, $col); return sprintf('"%s"."%s"', $table, $col);
}, $cols); }, $cols);
return implode(',', $cols); return implode(',', $cols);
} }

View File

@ -161,9 +161,10 @@ abstract class SearchFilter extends Object {
*/ */
public function getDbName() { public function getDbName() {
// Special handler for "NULL" relations // Special handler for "NULL" relations
if($this->name == "NULL") { if($this->name === "NULL") {
return $this->name; return $this->name;
} }
// Ensure that we're dealing with a DataObject. // Ensure that we're dealing with a DataObject.
if (!is_subclass_of($this->model, 'DataObject')) { if (!is_subclass_of($this->model, 'DataObject')) {
throw new InvalidArgumentException( throw new InvalidArgumentException(
@ -171,19 +172,16 @@ abstract class SearchFilter extends Object {
); );
} }
$candidateClass = ClassInfo::table_for_object_field( // Find table this field belongs to
$this->model, $table = DataObject::getSchema()->tableForField($this->model, $this->name);
$this->name if(!$table) {
);
if($candidateClass == 'DataObject') {
// fallback to the provided name in the event of a joined column // fallback to the provided name in the event of a joined column
// name (as the candidate class doesn't check joined records) // name (as the candidate class doesn't check joined records)
$parts = explode('.', $this->fullName); $parts = explode('.', $this->fullName);
return '"' . implode('"."', $parts) . '"'; return '"' . implode('"."', $parts) . '"';
} }
return sprintf('"%s"."%s"', $candidateClass, $this->name); return sprintf('"%s"."%s"', $table, $this->name);
} }
/** /**

View File

@ -992,33 +992,46 @@ class Security extends Controller implements TemplateGlobalProvider {
*/ */
public static function database_is_ready() { public static function database_is_ready() {
// Used for unit tests // Used for unit tests
if(self::$force_database_is_ready !== NULL) return self::$force_database_is_ready; if(self::$force_database_is_ready !== null) {
return self::$force_database_is_ready;
}
if(self::$database_is_ready) return self::$database_is_ready; if(self::$database_is_ready) {
return self::$database_is_ready;
}
$requiredTables = ClassInfo::dataClassesFor('Member'); $requiredClasses = ClassInfo::dataClassesFor('Member');
$requiredTables[] = 'Group'; $requiredClasses[] = 'Group';
$requiredTables[] = 'Permission'; $requiredClasses[] = 'Permission';
foreach($requiredTables as $table) { foreach($requiredClasses as $class) {
// Skip test classes, as not all test classes are scaffolded at once // Skip test classes, as not all test classes are scaffolded at once
if(is_subclass_of($table, 'TestOnly')) continue; if(is_subclass_of($class, 'TestOnly')) {
continue;
}
// if any of the tables aren't created in the database // if any of the tables aren't created in the database
if(!ClassInfo::hasTable($table)) return false; $table = DataObject::getSchema()->tableName($class);
if(!ClassInfo::hasTable($table)) {
return false;
}
// HACK: DataExtensions aren't applied until a class is instantiated for // HACK: DataExtensions aren't applied until a class is instantiated for
// the first time, so create an instance here. // the first time, so create an instance here.
singleton($table); singleton($class);
// if any of the tables don't have all fields mapped as table columns // if any of the tables don't have all fields mapped as table columns
$dbFields = DB::field_list($table); $dbFields = DB::field_list($table);
if(!$dbFields) return false; if(!$dbFields) {
return false;
}
$objFields = DataObject::database_fields($table); $objFields = DataObject::database_fields($class);
$missingFields = array_diff_key($objFields, $dbFields); $missingFields = array_diff_key($objFields, $dbFields);
if($missingFields) return false; if($missingFields) {
return false;
}
} }
self::$database_is_ready = true; self::$database_is_ready = true;

View File

@ -8,10 +8,13 @@ class ClassInfoTest extends SapphireTest {
protected $extraDataObjects = array( protected $extraDataObjects = array(
'ClassInfoTest_BaseClass', 'ClassInfoTest_BaseClass',
'ClassInfoTest_BaseDataClass',
'ClassInfoTest_ChildClass', 'ClassInfoTest_ChildClass',
'ClassInfoTest_GrandChildClass', 'ClassInfoTest_GrandChildClass',
'ClassInfoTest_BaseDataClass', 'ClassInfoTest_HasFields',
'ClassInfoTest_NoFields', 'ClassInfoTest_NoFields',
'ClassInfoTest_WithCustomTable',
'ClassInfoTest_WithRelation',
); );
public function setUp() { public function setUp() {
@ -26,6 +29,7 @@ class ClassInfoTest extends SapphireTest {
$this->assertTrue(ClassInfo::exists('CLASSINFOTEST')); $this->assertTrue(ClassInfo::exists('CLASSINFOTEST'));
$this->assertTrue(ClassInfo::exists('stdClass')); $this->assertTrue(ClassInfo::exists('stdClass'));
$this->assertTrue(ClassInfo::exists('stdCLASS')); $this->assertTrue(ClassInfo::exists('stdCLASS'));
$this->assertFalse(ClassInfo::exists('SomeNonExistantClass'));
} }
public function testSubclassesFor() { public function testSubclassesFor() {
@ -50,12 +54,15 @@ class ClassInfoTest extends SapphireTest {
); );
} }
public function testClassName() { public function testClassName()
{
$this->assertEquals('ClassInfoTest', ClassInfo::class_name($this)); $this->assertEquals('ClassInfoTest', ClassInfo::class_name($this));
$this->assertEquals('ClassInfoTest', ClassInfo::class_name('ClassInfoTest')); $this->assertEquals('ClassInfoTest', ClassInfo::class_name('ClassInfoTest'));
$this->assertEquals('ClassInfoTest', ClassInfo::class_name('CLaSsInfOTEsT')); $this->assertEquals('ClassInfoTest', ClassInfo::class_name('CLaSsInfOTEsT'));
}
// This is for backwards compatiblity and will be removed in 4.0 public function testNonClassName() {
$this->setExpectedException('ReflectionException', 'Class IAmAClassThatDoesNotExist does not exist');
$this->assertEquals('IAmAClassThatDoesNotExist', ClassInfo::class_name('IAmAClassThatDoesNotExist')); $this->assertEquals('IAmAClassThatDoesNotExist', ClassInfo::class_name('IAmAClassThatDoesNotExist'));
} }
@ -76,21 +83,6 @@ class ClassInfoTest extends SapphireTest {
); );
} }
/**
* @covers ClassInfo::baseDataClass()
*/
public function testBaseDataClass() {
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_BaseClass'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('classinfotest_baseclass'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_ChildClass'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('CLASSINFOTEST_CHILDCLASS'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_GrandChildClass'));
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_GRANDChildClass'));
$this->setExpectedException('InvalidArgumentException');
ClassInfo::baseDataClass('DataObject');
}
/** /**
* @covers ClassInfo::ancestry() * @covers ClassInfo::ancestry()
*/ */
@ -125,7 +117,8 @@ class ClassInfoTest extends SapphireTest {
$expect = array( $expect = array(
'ClassInfoTest_BaseDataClass' => 'ClassInfoTest_BaseDataClass', 'ClassInfoTest_BaseDataClass' => 'ClassInfoTest_BaseDataClass',
'ClassInfoTest_HasFields' => 'ClassInfoTest_HasFields', 'ClassInfoTest_HasFields' => 'ClassInfoTest_HasFields',
'ClassInfoTest_WithRelation' => 'ClassInfoTest_WithRelation' 'ClassInfoTest_WithRelation' => 'ClassInfoTest_WithRelation',
'ClassInfoTest_WithCustomTable' => 'ClassInfoTest_WithCustomTable',
); );
$classes = array( $classes = array(
@ -152,62 +145,6 @@ class ClassInfoTest extends SapphireTest {
$this->assertEquals($expect, ClassInfo::dataClassesFor(strtolower($classes[2]))); $this->assertEquals($expect, ClassInfo::dataClassesFor(strtolower($classes[2])));
} }
public function testTableForObjectField() {
$this->assertEquals('ClassInfoTest_WithRelation',
ClassInfo::table_for_object_field('ClassInfoTest_WithRelation', 'RelationID')
);
$this->assertEquals('ClassInfoTest_WithRelation',
ClassInfo::table_for_object_field('ClassInfoTest_withrelation', 'RelationID')
);
$this->assertEquals('ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('ClassInfoTest_BaseDataClass', 'Title')
);
$this->assertEquals('ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('ClassInfoTest_HasFields', 'Title')
);
$this->assertEquals('ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('ClassInfoTest_NoFields', 'Title')
);
$this->assertEquals('ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('classinfotest_nofields', 'Title')
);
$this->assertEquals('ClassInfoTest_HasFields',
ClassInfo::table_for_object_field('ClassInfoTest_HasFields', 'Description')
);
// existing behaviour fallback to DataObject? Should be null.
$this->assertEquals('DataObject',
ClassInfo::table_for_object_field('ClassInfoTest_BaseClass', 'Nonexist')
);
$this->assertNull(
ClassInfo::table_for_object_field('SomeFakeClassHere', 'Title')
);
$this->assertNull(
ClassInfo::table_for_object_field('Object', 'Title')
);
$this->assertNull(
ClassInfo::table_for_object_field(null, null)
);
// Test fixed fields
$this->assertEquals(
'ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('ClassInfoTest_HasFields', 'ID')
);
$this->assertEquals(
'ClassInfoTest_BaseDataClass',
ClassInfo::table_for_object_field('ClassInfoTest_NoFields', 'Created')
);
}
} }
/** /**
@ -270,6 +207,13 @@ class ClassInfoTest_HasFields extends ClassInfoTest_NoFields {
); );
} }
class ClassInfoTest_WithCustomTable extends ClassInfoTest_NoFields {
private static $table_name = 'CITWithCustomTable';
private static $db = array(
'Description' => 'Text'
);
}
/** /**
* @package framework * @package framework
* @subpackage tests * @subpackage tests

View File

@ -28,7 +28,7 @@ class ChangeSetItemTest extends SapphireTest {
$item = new ChangeSetItem([ $item = new ChangeSetItem([
'ObjectID' => $object->ID, 'ObjectID' => $object->ID,
'ObjectClass' => ClassInfo::baseDataClass($object->ClassName) 'ObjectClass' => $object->baseClass(),
]); ]);
$this->assertEquals( $this->assertEquals(
@ -80,7 +80,7 @@ class ChangeSetItemTest extends SapphireTest {
$item = new ChangeSetItem([ $item = new ChangeSetItem([
'ObjectID' => $object->ID, 'ObjectID' => $object->ID,
'ObjectClass' => ClassInfo::baseDataClass($object) 'ObjectClass' => $object->baseClass(),
]); ]);
$item->write(); $item->write();

View File

@ -150,7 +150,10 @@ class ChangeSetTest extends SapphireTest {
$object = $this->objFromFixture($class, $identifier); $object = $this->objFromFixture($class, $identifier);
foreach($items as $i => $item) { foreach($items as $i => $item) {
if ($item->ObjectClass == ClassInfo::baseDataClass($object) && $item->ObjectID == $object->ID && $item->Added == $mode) { if ( $item->ObjectClass == $object->baseClass()
&& $item->ObjectID == $object->ID
&& $item->Added == $mode
) {
unset($items[$i]); unset($items[$i]);
continue 2; continue 2;
} }

View File

@ -9,32 +9,10 @@ class DataListTest extends SapphireTest {
// Borrow the model from DataObjectTest // Borrow the model from DataObjectTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array( public function setUpOnce() {
// From DataObjectTest $this->extraDataObjects = DataObjectTest::$extra_data_objects;
'DataObjectTest_Team', parent::setUpOnce();
'DataObjectTest_Fixture', }
'DataObjectTest_SubTeam',
'OtherSubclassWithSameField',
'DataObjectTest_FieldlessTable',
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
'DataObjectTest_Play',
'DataObjectTest_Ploy',
'DataObjectTest_Bogey',
'ManyManyListTest_Product',
'ManyManyListTest_Category',
);
public function testFilterDataObjectByCreatedDate() { public function testFilterDataObjectByCreatedDate() {
// create an object to test with // create an object to test with

View File

@ -0,0 +1,354 @@
<?php
/**
* Tests schema inspection of DataObjects
*/
class DataObjectSchemaTest extends SapphireTest
{
protected static $fixture_file = 'DataObjectSchemaTest.yml';
protected $extraDataObjects = array(
// Classes in base namespace
'DataObjectSchemaTest_BaseClass',
'DataObjectSchemaTest_BaseDataClass',
'DataObjectSchemaTest_ChildClass',
'DataObjectSchemaTest_GrandChildClass',
'DataObjectSchemaTest_HasFields',
'DataObjectSchemaTest_NoFields',
'DataObjectSchemaTest_WithCustomTable',
'DataObjectSchemaTest_WithRelation',
// Classes in sub-namespace (See DataObjectSchemaTest_Namespacejd.php)
'Namespaced\DOST\MyObject',
'Namespaced\DOST\MyObject_CustomTable',
'Namespaced\DOST\MyObject_NestedObject',
'Namespaced\DOST\MyObject_NamespacedTable',
'Namespaced\DOST\MyObject_Namespaced_Subclass',
'Namespaced\DOST\MyObject_NoFields',
);
/**
* Test table name generation
*/
public function testTableName() {
$schema = DataObject::getSchema();
// Non-namespaced tables
$this->assertEquals(
'DataObjectSchemaTest_WithRelation',
$schema->tableName('DataObjectSchemaTest_WithRelation')
);
$this->assertEquals(
'DOSTWithCustomTable',
$schema->tableName('DataObjectSchemaTest_WithCustomTable')
);
// Namespaced tables
$this->assertEquals(
'Namespaced\DOST\MyObject',
$schema->tableName('Namespaced\DOST\MyObject')
);
$this->assertEquals(
'CustomNamespacedTable',
$schema->tableName('Namespaced\DOST\MyObject_CustomTable')
);
$this->assertEquals(
'Namespaced\DOST\MyObject_NestedObject',
$schema->tableName('Namespaced\DOST\MyObject_NestedObject')
);
$this->assertEquals(
'Custom\NamespacedTable',
$schema->tableName('Namespaced\DOST\MyObject_NamespacedTable')
);
$this->assertEquals(
'Custom\SubclassedTable',
$schema->tableName('Namespaced\DOST\MyObject_Namespaced_Subclass')
);
$this->assertEquals(
'Namespaced\DOST\MyObject_NoFields',
$schema->tableName('Namespaced\DOST\MyObject_NoFields')
);
}
/**
* Test that the class name is convertable from the table
*/
public function testClassNameForTable() {
$schema = DataObject::getSchema();
// Tables that aren't classes
$this->assertNull($schema->tableClass('NotARealTable'));
// Non-namespaced tables
$this->assertEquals(
'DataObjectSchemaTest_WithRelation',
$schema->tableClass('DataObjectSchemaTest_WithRelation')
);
$this->assertEquals(
'DataObjectSchemaTest_WithCustomTable',
$schema->tableClass('DOSTWithCustomTable')
);
// Namespaced tables
$this->assertEquals(
'Namespaced\DOST\MyObject',
$schema->tableClass('Namespaced\DOST\MyObject')
);
$this->assertEquals(
'Namespaced\DOST\MyObject_CustomTable',
$schema->tableClass('CustomNamespacedTable')
);
$this->assertEquals(
'Namespaced\DOST\MyObject_NestedObject',
$schema->tableClass('Namespaced\DOST\MyObject_NestedObject')
);
$this->assertEquals(
'Namespaced\DOST\MyObject_NamespacedTable',
$schema->tableClass('Custom\NamespacedTable')
);
$this->assertEquals(
'Namespaced\DOST\MyObject_Namespaced_Subclass',
$schema->tableClass('Custom\SubclassedTable')
);
$this->assertEquals(
'Namespaced\DOST\MyObject_NoFields',
$schema->tableClass('Namespaced\DOST\MyObject_NoFields')
);
}
/**
* Test non-namespaced tables
*/
public function testTableForObjectField() {
$schema = DataObject::getSchema();
$this->assertEquals(
'DataObjectSchemaTest_WithRelation',
$schema->tableForField('DataObjectSchemaTest_WithRelation', 'RelationID')
);
$this->assertEquals(
'DataObjectSchemaTest_WithRelation',
$schema->tableForField('DataObjectSchemaTest_withrelation', 'RelationID')
);
$this->assertEquals(
'DataObjectSchemaTest_BaseDataClass',
$schema->tableForField('DataObjectSchemaTest_BaseDataClass', 'Title')
);
$this->assertEquals(
'DataObjectSchemaTest_BaseDataClass',
$schema->tableForField('DataObjectSchemaTest_HasFields', 'Title')
);
$this->assertEquals(
'DataObjectSchemaTest_BaseDataClass',
$schema->tableForField('DataObjectSchemaTest_NoFields', 'Title')
);
$this->assertEquals(
'DataObjectSchemaTest_BaseDataClass',
$schema->tableForField('DataObjectSchemaTest_nofields', 'Title')
);
$this->assertEquals(
'DataObjectSchemaTest_HasFields',
$schema->tableForField('DataObjectSchemaTest_HasFields', 'Description')
);
// Class and table differ for this model
$this->assertEquals(
'DOSTWithCustomTable',
$schema->tableForField('DataObjectSchemaTest_WithCustomTable', 'Description')
);
$this->assertEquals(
'DataObjectSchemaTest_WithCustomTable',
$schema->classForField('DataObjectSchemaTest_WithCustomTable', 'Description')
);
$this->assertNull(
$schema->tableForField('DataObjectSchemaTest_WithCustomTable', 'NotAField')
);
$this->assertNull(
$schema->classForField('DataObjectSchemaTest_WithCustomTable', 'NotAField')
);
// Non-existant fields shouldn't match any table
$this->assertNull(
$schema->tableForField('DataObjectSchemaTest_BaseClass', 'Nonexist')
);
$this->assertNull(
$schema->tableForField('Object', 'Title')
);
// Test fixed fields
$this->assertEquals(
'DataObjectSchemaTest_BaseDataClass',
$schema->tableForField('DataObjectSchemaTest_HasFields', 'ID')
);
$this->assertEquals(
'DataObjectSchemaTest_BaseDataClass',
$schema->tableForField('DataObjectSchemaTest_NoFields', 'Created')
);
}
/**
* Check table for fields with namespaced objects can be found
*/
public function testTableForNamespacedObjectField() {
$schema = DataObject::getSchema();
// MyObject
$this->assertEquals(
'Namespaced\DOST\MyObject',
$schema->tableForField('Namespaced\DOST\MyObject', 'Title')
);
// MyObject_CustomTable
$this->assertEquals(
'CustomNamespacedTable',
$schema->tableForField('Namespaced\DOST\MyObject_CustomTable', 'Title')
);
// MyObject_NestedObject
$this->assertEquals(
'Namespaced\DOST\MyObject',
$schema->tableForField('Namespaced\DOST\MyObject_NestedObject', 'Title')
);
$this->assertEquals(
'Namespaced\DOST\MyObject_NestedObject',
$schema->tableForField('Namespaced\DOST\MyObject_NestedObject', 'Content')
);
// MyObject_NamespacedTable
$this->assertEquals(
'Custom\NamespacedTable',
$schema->tableForField('Namespaced\DOST\MyObject_NamespacedTable', 'Description')
);
$this->assertEquals(
'Custom\NamespacedTable',
$schema->tableForField('Namespaced\DOST\MyObject_NamespacedTable', 'OwnerID')
);
// MyObject_Namespaced_Subclass
$this->assertEquals(
'Custom\NamespacedTable',
$schema->tableForField('Namespaced\DOST\MyObject_Namespaced_Subclass', 'OwnerID')
);
$this->assertEquals(
'Custom\NamespacedTable',
$schema->tableForField('Namespaced\DOST\MyObject_Namespaced_Subclass', 'Title')
);
$this->assertEquals(
'Custom\NamespacedTable',
$schema->tableForField('Namespaced\DOST\MyObject_Namespaced_Subclass', 'ID')
);
$this->assertEquals(
'Custom\SubclassedTable',
$schema->tableForField('Namespaced\DOST\MyObject_Namespaced_Subclass', 'Details')
);
// MyObject_NoFields
$this->assertEquals(
'Namespaced\DOST\MyObject_NoFields',
$schema->tableForField('Namespaced\DOST\MyObject_NoFields', 'Created')
);
}
/**
* Test that relations join on the correct columns
*/
public function testRelationsQuery() {
$namespaced1 = $this->objFromFixture('Namespaced\DOST\MyObject_NamespacedTable', 'namespaced1');
$nofields = $this->objFromFixture('Namespaced\DOST\MyObject_NoFields', 'nofields1');
$subclass1 = $this->objFromFixture('Namespaced\DOST\MyObject_Namespaced_Subclass', 'subclass1');
$customtable1 = $this->objFromFixture('Namespaced\DOST\MyObject_CustomTable', 'customtable1');
$customtable3 = $this->objFromFixture('Namespaced\DOST\MyObject_CustomTable', 'customtable3');
// Check has_one / has_many
$this->assertEquals($nofields->ID, $namespaced1->Owner()->ID);
$this->assertDOSEquals([
['Title' => 'Namespaced 1'],
], $nofields->Owns());
// Check many_many / belongs_many_many
$this->assertDOSEquals(
[
['Title' => 'Custom Table 1'],
['Title' => 'Custom Table 2'],
],
$subclass1->Children()
);
$this->assertDOSEquals(
[
['Title' => 'Subclass 1', 'Details' => 'Oh, Hi!',]]
,
$customtable1->Parents()
);
$this->assertEmpty($customtable3->Parents()->count());
}
/**
* @covers DataObjectSchema::baseDataClass()
*/
public function testBaseDataClass() {
$schema = DataObject::getSchema();
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_BaseClass'));
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_baseclass'));
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_ChildClass'));
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_CHILDCLASS'));
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_GrandChildClass'));
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_GRANDChildClass'));
$this->setExpectedException('InvalidArgumentException');
$schema->baseDataClass('DataObject');
}
}
class DataObjectSchemaTest_BaseClass extends DataObject implements TestOnly {
}
class DataObjectSchemaTest_ChildClass extends DataObjectSchemaTest_BaseClass {
}
class DataObjectSchemaTest_GrandChildClass extends DataObjectSchemaTest_ChildClass {
}
class DataObjectSchemaTest_BaseDataClass extends DataObject implements TestOnly {
private static $db = array(
'Title' => 'Varchar'
);
}
class DataObjectSchemaTest_NoFields extends DataObjectSchemaTest_BaseDataClass {
}
class DataObjectSchemaTest_HasFields extends DataObjectSchemaTest_NoFields {
private static $db = array(
'Description' => 'Varchar'
);
}
class DataObjectSchemaTest_WithCustomTable extends DataObjectSchemaTest_NoFields {
private static $table_name = 'DOSTWithCustomTable';
private static $db = array(
'Description' => 'Text'
);
}
class DataObjectSchemaTest_WithRelation extends DataObjectSchemaTest_NoFields {
private static $has_one = array(
'Relation' => 'DataObjectSchemaTest_HasFields'
);
}

View File

@ -0,0 +1,36 @@
Namespaced\DOST\MyObject:
object1:
Title: 'Object 1'
Description: 'Description 1'
Namespaced\DOST\MyObject_CustomTable:
customtable1:
Title: 'Custom Table 1'
Description: 'Description A'
customtable2:
Title: 'Custom Table 2'
Description: 'Description B'
customtable3:
Title: 'Custom Table 3'
Description: 'Orphaned item'
Namespaced\DOST\MyObject_NestedObject:
nested1:
Title: 'Nested 1'
Description: 'Nested Description'
Content: '<p>Hello!</p>'
Namespaced\DOST\MyObject_NoFields:
nofields1: {}
Namespaced\DOST\MyObject_NamespacedTable:
namespaced1:
Title: 'Namespaced 1'
Owner: =>Namespaced\DOST\MyObject_NoFields.nofields1
Namespaced\DOST\MyObject_Namespaced_Subclass:
subclass1:
Title: 'Subclass 1'
Details: 'Oh, Hi!'
Children: =>Namespaced\DOST\MyObject_CustomTable.customtable1, =>Namespaced\DOST\MyObject_CustomTable.customtable2

View File

@ -0,0 +1,78 @@
<?php
/**
* Namespaced dataobjcets used by DataObjectSchemaTest
*/
namespace Namespaced\DOST;
/**
* Basic namespaced object
*/
class MyObject extends \DataObject implements \TestOnly {
private static $db = [
'Title' => 'Varchar',
'Description' => 'Text',
];
}
/**
* Namespaced object with custom table
*/
class MyObject_CustomTable extends \DataObject implements \TestOnly {
private static $table_name = 'CustomNamespacedTable';
private static $db = [
'Title' => 'Varchar',
'Description' => 'Text',
];
private static $belongs_many_many = [
'Parents' => 'Namespaced\DOST\MyObject_Namespaced_Subclass',
];
}
/**
* Namespaced subclassed object
*/
class MyObject_NestedObject extends MyObject implements \TestOnly {
private static $db = [
'Content' => 'HTMLText',
];
}
/**
* Namespaced object with custom table that itself is namespaced
*/
class MyObject_NamespacedTable extends \DataObject implements \TestOnly {
private static $table_name = 'Custom\NamespacedTable';
private static $db = [
'Title' => 'Varchar',
'Description' => 'Text',
];
private static $has_one = [
'Owner' => 'Namespaced\DOST\MyObject_NoFields',
];
}
/**
* Subclass of a namespaced class
* Has a many_many to another namespaced table
*/
class MyObject_Namespaced_Subclass extends MyObject_NamespacedTable implements \TestOnly {
private static $table_name = 'Custom\SubclassedTable';
private static $db = [
'Details' => 'Varchar',
];
private static $many_many = [
'Children' => 'Namespaced\DOST\MyObject_CustomTable',
];
}
/**
* Namespaced class without any fields
* has a has_many to another namespaced table
*/
class MyObject_NoFields extends \DataObject implements \TestOnly {
private static $has_many = [
'Owns' => 'Namespaced\DOST\MyObject_NamespacedTable',
];
}

View File

@ -10,7 +10,12 @@ class DataObjectTest extends SapphireTest {
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array( /**
* Standard set of dataobject test classes
*
* @var array
*/
public static $extra_data_objects = array(
'DataObjectTest_Team', 'DataObjectTest_Team',
'DataObjectTest_Fixture', 'DataObjectTest_Fixture',
'DataObjectTest_SubTeam', 'DataObjectTest_SubTeam',
@ -32,10 +37,17 @@ class DataObjectTest extends SapphireTest {
'DataObjectTest_Play', 'DataObjectTest_Play',
'DataObjectTest_Ploy', 'DataObjectTest_Ploy',
'DataObjectTest_Bogey', 'DataObjectTest_Bogey',
// From ManyManyListTest
'ManyManyListTest_ExtraFields',
'ManyManyListTest_Product', 'ManyManyListTest_Product',
'ManyManyListTest_Category', 'ManyManyListTest_Category',
); );
public function setUpOnce() {
$this->extraDataObjects = static::$extra_data_objects;
parent::setUpOnce();
}
public function testDb() { public function testDb() {
$obj = new DataObjectTest_TeamComment(); $obj = new DataObjectTest_TeamComment();
$dbFields = $obj->db(); $dbFields = $obj->db();

View File

@ -75,7 +75,7 @@ class DataQueryTest extends SapphireTest {
//test many_many with separate inheritance //test many_many with separate inheritance
$newDQ = new DataQuery('DataQueryTest_C'); $newDQ = new DataQuery('DataQueryTest_C');
$baseDBTable = ClassInfo::baseDataClass('DataQueryTest_C'); $baseDBTable = DataObject::getSchema()->baseDataTable('DataQueryTest_C');
$newDQ->applyRelation('ManyTestAs'); $newDQ->applyRelation('ManyTestAs');
//check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses) //check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses)
$this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable)); $this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable));
@ -84,7 +84,7 @@ class DataQueryTest extends SapphireTest {
//test many_many with shared inheritance //test many_many with shared inheritance
$newDQ = new DataQuery('DataQueryTest_E'); $newDQ = new DataQuery('DataQueryTest_E');
$baseDBTable = ClassInfo::baseDataClass('DataQueryTest_E'); $baseDBTable = DataObject::getSchema()->baseDataTable('DataQueryTest_E');
//check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses) //check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses)
$this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable)); $this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable));
//check we are explicitly selecting "FROM" the DO's table //check we are explicitly selecting "FROM" the DO's table

View File

@ -5,32 +5,10 @@ class HasManyListTest extends SapphireTest {
// Borrow the model from DataObjectTest // Borrow the model from DataObjectTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array( public function setUpOnce() {
// From DataObjectTest $this->extraDataObjects = DataObjectTest::$extra_data_objects;
'DataObjectTest_Team', parent::setUpOnce();
'DataObjectTest_Fixture', }
'DataObjectTest_SubTeam',
'OtherSubclassWithSameField',
'DataObjectTest_FieldlessTable',
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
'DataObjectTest_Play',
'DataObjectTest_Ploy',
'DataObjectTest_Bogey',
'ManyManyListTest_Product',
'ManyManyListTest_Category',
);
public function testRelationshipEmptyOnNewRecords() { public function testRelationshipEmptyOnNewRecords() {
// Relies on the fact that (unrelated) comments exist in the fixture file already // Relies on the fact that (unrelated) comments exist in the fixture file already

View File

@ -10,35 +10,10 @@ class ManyManyListTest extends SapphireTest {
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array( public function setUpOnce() {
// From DataObjectTest $this->extraDataObjects = DataObjectTest::$extra_data_objects;
'DataObjectTest_Team', parent::setUpOnce();
'DataObjectTest_Fixture', }
'DataObjectTest_SubTeam',
'OtherSubclassWithSameField',
'DataObjectTest_FieldlessTable',
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
'DataObjectTest_Play',
'DataObjectTest_Ploy',
'DataObjectTest_Bogey',
// From ManyManyListTest
'ManyManyListTest_ExtraFields',
'ManyManyListTest_Product',
'ManyManyListTest_Category',
);
public function testAddCompositedExtraFields() { public function testAddCompositedExtraFields() {
$obj = new ManyManyListTest_ExtraFields(); $obj = new ManyManyListTest_ExtraFields();

View File

@ -9,32 +9,10 @@ class SS_MapTest extends SapphireTest {
// Borrow the model from DataObjectTest // Borrow the model from DataObjectTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array( public function setUpOnce() {
// From DataObjectTest $this->extraDataObjects = DataObjectTest::$extra_data_objects;
'DataObjectTest_Team', parent::setUpOnce();
'DataObjectTest_Fixture', }
'DataObjectTest_SubTeam',
'OtherSubclassWithSameField',
'DataObjectTest_FieldlessTable',
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
'DataObjectTest_Play',
'DataObjectTest_Ploy',
'DataObjectTest_Bogey',
'ManyManyListTest_Product',
'ManyManyListTest_Category',
);
public function testValues() { public function testValues() {

View File

@ -15,30 +15,10 @@ class PolymorphicHasManyListTest extends SapphireTest {
// Borrow the model from DataObjectTest // Borrow the model from DataObjectTest
protected static $fixture_file = 'DataObjectTest.yml'; protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array( public function setUpOnce() {
// From DataObjectTest $this->extraDataObjects = DataObjectTest::$extra_data_objects;
'DataObjectTest_Team', parent::setUpOnce();
'DataObjectTest_Fixture', }
'DataObjectTest_SubTeam',
'OtherSubclassWithSameField',
'DataObjectTest_FieldlessTable',
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
'DataObjectTest_Play',
'DataObjectTest_Ploy',
'DataObjectTest_Bogey',
);
public function testRelationshipEmptyOnNewRecords() { public function testRelationshipEmptyOnNewRecords() {
// Relies on the fact that (unrelated) comments exist in the fixture file already // Relies on the fact that (unrelated) comments exist in the fixture file already

View File

@ -68,30 +68,24 @@ class VersionableExtensionsTest_DataObject extends DataObject implements TestOnl
} }
class VersionableExtensionsTest_Extension extends DataExtension implements TestOnly { class VersionableExtensionsTest_Extension extends DataExtension implements VersionableExtension, TestOnly {
public function isVersionedTable($table){ public function isVersionedTable($table) {
return true; return true;
} }
/** /**
* fieldsInExtraTables function. * Update fields and indexes for the versonable suffix table
* *
* @access public * @param string $suffix Table suffix being built
* @param mixed $suffix * @param array $fields List of fields in this model
* @param array $indexes List of indexes in this model
* @return array * @return array
*/ */
public function fieldsInExtraTables($suffix){ public function updateVersionableFields($suffix, &$fields, &$indexes){
$fields = array(); $indexes['ExtraField'] = true;
//$fields['db'] = DataObject::database_fields($this->owner->class); $fields['ExtraField'] = 'Varchar()';
$fields['indexes'] = $this->owner->databaseIndexes();
$fields['db'] = array_merge(
DataObject::database_fields($this->owner->class)
);
return $fields;
} }
} }

View File

@ -33,7 +33,7 @@ class VersionedTest extends SapphireTest {
'VersionedTest_WithIndexes_versions' => 'VersionedTest_WithIndexes_versions' =>
array('value' => false, 'message' => 'Unique indexes are no longer unique in _versions table'), array('value' => false, 'message' => 'Unique indexes are no longer unique in _versions table'),
'VersionedTest_WithIndexes_Live' => 'VersionedTest_WithIndexes_Live' =>
array('value' => false, 'message' => 'Unique indexes are no longer unique in _Live table'), array('value' => true, 'message' => 'Unique indexes are unique in _Live table'),
); );
// Test each table's performance // Test each table's performance
@ -56,7 +56,7 @@ class VersionedTest extends SapphireTest {
if (in_array($indexSpec['value'], $expectedColumns)) { if (in_array($indexSpec['value'], $expectedColumns)) {
$isUnique = $indexSpec['type'] === 'unique'; $isUnique = $indexSpec['type'] === 'unique';
$this->assertEquals($isUnique, $expectation['value'], $expectation['message']); $this->assertEquals($isUnique, $expectation['value'], $expectation['message']);
} }
} }
} }
} }
@ -314,7 +314,7 @@ class VersionedTest extends SapphireTest {
} }
public function testWritingNewToStage() { public function testWritingNewToStage() {
$origStage = Versioned::get_stage(); $origReadingMode = Versioned::get_reading_mode();
Versioned::set_stage(Versioned::DRAFT); Versioned::set_stage(Versioned::DRAFT);
$page = new VersionedTest_DataObject(); $page = new VersionedTest_DataObject();
@ -333,7 +333,7 @@ class VersionedTest extends SapphireTest {
$this->assertEquals(1, $stage->count()); $this->assertEquals(1, $stage->count());
$this->assertEquals($stage->First()->Title, 'testWritingNewToStage'); $this->assertEquals($stage->First()->Title, 'testWritingNewToStage');
Versioned::set_stage($origStage); Versioned::set_reading_mode($origReadingMode);
} }
/** /**
@ -343,7 +343,7 @@ class VersionedTest extends SapphireTest {
* the VersionedTest_DataObject record though. * the VersionedTest_DataObject record though.
*/ */
public function testWritingNewToLive() { public function testWritingNewToLive() {
$origStage = Versioned::get_stage(); $origReadingMode = Versioned::get_reading_mode();
Versioned::set_stage(Versioned::LIVE); Versioned::set_stage(Versioned::LIVE);
$page = new VersionedTest_DataObject(); $page = new VersionedTest_DataObject();
@ -362,7 +362,7 @@ class VersionedTest extends SapphireTest {
)); ));
$this->assertEquals(0, $stage->count()); $this->assertEquals(0, $stage->count());
Versioned::set_stage($origStage); Versioned::set_reading_mode($origReadingMode);
} }
/** /**

View File

@ -144,8 +144,7 @@ class SSViewerCacheBlockTest extends SapphireTest {
} }
public function testVersionedCache() { public function testVersionedCache() {
$origReadingMode = Versioned::get_reading_mode();
$origStage = Versioned::get_stage();
// Run without caching in stage to prove data is uncached // Run without caching in stage to prove data is uncached
$this->_reset(false); $this->_reset(false);
@ -211,7 +210,7 @@ class SSViewerCacheBlockTest extends SapphireTest {
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data) $this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
); );
Versioned::set_stage($origStage); Versioned::set_reading_mode($origReadingMode);
} }
/** /**