/** * Adds table identifier to the every column. * Columns must have table identifier to prevent duplicate column name error. * * @return string */ protected function prepareColumns($columns) { $cols = preg_split('/"?\\s*,\\s*"?/', trim($columns, '(") ')); $class = ClassInfo::table_for_object_field($this->model, current($cols)); $cols = array_map(function ($col) use($class) { return sprintf('"%s"."%s"', $class, $col); }, $cols); return implode(',', $cols); }
/** * Assign a sort number when object is written * @see DataExtension::onBeforeWrite() */ public function onBeforeWrite() { if (!$this->owner->exists() || !$this->owner->SortOrder) { // get the table in the ancestry that has the SortOrder field $table = ClassInfo::table_for_object_field($this->owner->class, 'SortOrder'); $sql = new SQLQuery('MAX("SortOrder")', $table); $val = $sql->execute()->value(); $this->owner->SortOrder = is_numeric($val) ? $val + 1 : 1; } }
public function testTableForObjectField() { $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_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)); }
public function run($request) { $db = DB::tableList(); foreach (self::GetVersionedClass() as $class) { $table = ClassInfo::table_for_object_field($class, 'ID'); $vTable = $table . '_versions'; if (!in_array($vTable, $db)) { continue; } echo "Clear records for class {$class} <br/>"; // TODO: WE SHOULD CLEAR BY ID TO AVOID INCONSISTENT DB // Keep 50 last records DB::query("DELETE FROM {$vTable}\n WHERE id <= (\n SELECT id\n FROM (\n SELECT id\n FROM {$vTable}\n ORDER BY id DESC\n LIMIT 1 OFFSET 50\n ) selection\n )"); $this->vacuumTable($vTable); } }
/** * Traverse the relationship fields, and add the table * mappings to the query object state. This has to be called * in any overloaded {@link SearchFilter->apply()} methods manually. * * @param String|array $relation The array/dot-syntax relation to follow * @return The model class of the related item */ public function applyRelation($relation) { // NO-OP if (!$relation) { return $this->dataClass; } if (is_string($relation)) { $relation = explode(".", $relation); } $modelClass = $this->dataClass; foreach ($relation as $rel) { $model = singleton($modelClass); if ($component = $model->has_one($rel)) { if (!$this->query->isJoinedTo($component)) { $foreignKey = $rel; $realModelClass = ClassInfo::table_for_object_field($modelClass, "{$foreignKey}ID"); $this->query->addLeftJoin($component, "\"{$component}\".\"ID\" = \"{$realModelClass}\".\"{$foreignKey}ID\""); /** * add join clause to the component's ancestry classes so that the search filter could search on * its ancestor fields. */ $ancestry = ClassInfo::ancestry($component, true); if (!empty($ancestry)) { $ancestry = array_reverse($ancestry); foreach ($ancestry as $ancestor) { if ($ancestor != $component) { $this->query->addInnerJoin($ancestor, "\"{$component}\".\"ID\" = \"{$ancestor}\".\"ID\""); } } } } $modelClass = $component; } elseif ($component = $model->has_many($rel)) { if (!$this->query->isJoinedTo($component)) { $ancestry = $model->getClassAncestry(); $foreignKey = $model->getRemoteJoinField($rel); $this->query->addLeftJoin($component, "\"{$component}\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\""); /** * add join clause to the component's ancestry classes so that the search filter could search on * its ancestor fields. */ $ancestry = ClassInfo::ancestry($component, true); if (!empty($ancestry)) { $ancestry = array_reverse($ancestry); foreach ($ancestry as $ancestor) { if ($ancestor != $component) { $this->query->addInnerJoin($ancestor, "\"{$component}\".\"ID\" = \"{$ancestor}\".\"ID\""); } } } } $modelClass = $component; } elseif ($component = $model->many_many($rel)) { list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component; $parentBaseClass = ClassInfo::baseDataClass($parentClass); $componentBaseClass = ClassInfo::baseDataClass($componentClass); $this->query->addInnerJoin($relationTable, "\"{$relationTable}\".\"{$parentField}\" = \"{$parentBaseClass}\".\"ID\""); $this->query->addLeftJoin($componentBaseClass, "\"{$relationTable}\".\"{$componentField}\" = \"{$componentBaseClass}\".\"ID\""); if (ClassInfo::hasTable($componentClass)) { $this->query->addLeftJoin($componentClass, "\"{$relationTable}\".\"{$componentField}\" = \"{$componentClass}\".\"ID\""); } $modelClass = $componentClass; } } return $modelClass; }
protected function overrideField($obj, $fieldName, $value, $fixtures = null) { $table = ClassInfo::table_for_object_field(get_class($obj), $fieldName); $value = $this->parseValue($value, $fixtures); DB::manipulate(array($table => array("command" => "update", "id" => $obj->ID, "fields" => array($fieldName => $value)))); $obj->{$fieldName} = $value; }
/** * Given a field or relation name, apply it safely to this datalist. * * Unlike getRelationName, this is immutable and will fallback to the quoted field * name if not a relation. * * @param string $field Name of field or relation to apply * @param string &$columnName Quoted column name * @param bool $linearOnly Set to true to restrict to linear relations only. Set this * if this relation will be used for sorting, and should not include duplicate rows. * @return DataList DataList with this relation applied */ public function applyRelation($field, &$columnName = null, $linearOnly = false) { // If field is invalid, return it without modification if (!$this->isValidRelationName($field)) { $columnName = $field; return $this; } // Simple fields without relations are mapped directly if (strpos($field, '.') === false) { $columnName = '"' . $field . '"'; return $this; } return $this->alterDataQuery(function (DataQuery $query, DataList $list) use($field, &$columnName, $linearOnly) { $relations = explode('.', $field); $fieldName = array_pop($relations); // Apply $relationModelName = $query->applyRelation($field, $linearOnly); // Find the db field the relation belongs to $className = ClassInfo::table_for_object_field($relationModelName, $fieldName); if (empty($className)) { $className = $relationModelName; } $columnName = '"' . $className . '"."' . $fieldName . '"'; }); }
/** * Normalizes the field name to table mapping. * * @return string */ public function getDbName() { // Special handler for "NULL" relations if ($this->name == "NULL") { return $this->name; } $candidateClass = ClassInfo::table_for_object_field($this->model, $this->name); if ($candidateClass == 'DataObject') { // fallback to the provided name in the event of a joined column // name (as the candidate class doesn't check joined records) $parts = explode('.', $this->fullName); return '"' . implode('"."', $parts) . '"'; } return "\"{$candidateClass}\".\"{$this->name}\""; }
/** * Returns the manipulated (sorted) DataList. Field names will simply add an * 'ORDER BY' clause, relation names will add appropriate joins to the * {@link DataQuery} first. * * @param GridField * @param SS_List * @return SS_List */ public function getManipulatedData(GridField $gridField, SS_List $dataList) { if (!$this->checkDataType($dataList)) { return $dataList; } $state = $gridField->State->GridFieldSortableHeader; if ($state->SortColumn == "") { return $dataList; } $column = $state->SortColumn; // if we have a relation column with dot notation if (strpos($column, '.') !== false) { $lastAlias = $dataList->dataClass(); $tmpItem = singleton($lastAlias); $parts = explode('.', $state->SortColumn); for ($idx = 0; $idx < sizeof($parts); $idx++) { $methodName = $parts[$idx]; // If we're not on the last item, we're looking at a relation if ($idx !== sizeof($parts) - 1) { // Traverse to the relational list $tmpItem = $tmpItem->{$methodName}(); $joinClass = ClassInfo::table_for_object_field($lastAlias, $methodName . "ID"); // if the field isn't in the object tree then it is likely // been aliased. In that event, assume what the user has // provided is the correct value if (!$joinClass) { $joinClass = $lastAlias; } $dataList = $dataList->leftJoin($tmpItem->class, '"' . $methodName . '"."ID" = "' . $joinClass . '"."' . $methodName . 'ID"', $methodName); // Store the last 'alias' name as it'll be used for the next // join, or the 'sort' column $lastAlias = $methodName; } else { // Change relation.relation.fieldname to alias.fieldname $column = $lastAlias . '.' . $methodName; } } } // We need to manually create our ORDER BY "Foo"."Bar" string for relations, // as ->sort() won't do it by itself. Blame PostgreSQL for making this necessary $pieces = explode('.', $column); $column = '"' . implode('"."', $pieces) . '"'; return $dataList->sort($column, $state->SortDirection); }
public static function merge($records) { $all = array(); $all_but_oldest = array(); $all_but_latest = array(); $latest = null; $oldest = null; foreach ($records as $r) { if (!is_object($r)) { $r = (object) $r; } if (!$r instanceof Member) { $r = Member::get()->byID($r->ID); } if (!$latest) { $latest = $r; } else { if (strtotime($r->LastEdited) > strtotime($latest->LastEdited)) { $latest = $r; } } if (!$oldest) { $oldest = $r; } else { if ($r->ID < $oldest->ID) { $oldest = $r->ID; } } $all[] = $r; } foreach ($all as $a) { if ($a->ID == $oldest->ID) { continue; } $all_but_oldest[] = $a; } foreach ($all as $a) { if ($a->ID == $latest->ID) { continue; } $all_but_latest[] = $a; } if (class_exists('Subsite')) { Subsite::$disable_subsite_filter = true; } Config::inst()->update('DataObject', 'validation_enabled', false); // Rewrite all relations so everything is pointing to oldest // For some reason, the code in merge fails to do this properly $tables = DB::tableList(); $objects = ClassInfo::subclassesFor('DataObject'); foreach ($objects as $o) { $config = $o::config(); if ($config->has_one) { foreach ($config->has_one as $name => $class) { if ($class == 'Member') { $table = ClassInfo::table_for_object_field($o, $name . 'ID'); if ($table && in_array(strtolower($table), $tables)) { foreach ($all_but_oldest as $a) { $sql = "UPDATE {$table} SET " . $name . 'ID = ' . $oldest->ID . ' WHERE ' . $name . 'ID = ' . $a->ID; DB::alteration_message($sql); DB::query($sql); } } } } } if ($config->has_many) { foreach ($config->has_many as $name => $class) { if ($class == 'Member') { $table = ClassInfo::table_for_object_field($o, $name . 'ID'); if ($table && in_array(strtolower($table), $tables)) { foreach ($all_but_oldest as $a) { $sql = "UPDATE {$table} SET " . $name . 'ID = ' . $oldest->ID . ' WHERE ' . $name . 'ID = ' . $a->ID; DB::alteration_message($sql); DB::query($sql); } } } } } if ($config->many_many) { foreach ($config->many_many as $name => $class) { if ($class == 'Member') { $table = ClassInfo::table_for_object_field($o, $name . 'ID'); if ($table && in_array(strtolower($table), $tables)) { foreach ($all_but_oldest as $a) { $sql = "UPDATE {$table} SET " . $name . 'ID = ' . $oldest->ID . ' WHERE ' . $name . 'ID = ' . $a->ID; DB::alteration_message($sql); DB::query($sql); } } } } } } // Now, we update to oldest record with the latest info $orgOldest = $oldest; $oldest->merge($latest, 'right', false); foreach ($all_but_oldest as $a) { $a->delete(); } try { $oldest->write(); } catch (Exception $ex) { $orgOldest->write(); } }
/** * Join the given class to this query with the given key * * @param string $localClass Name of class that has the has_one to the joined class * @param string $localField Name of the has_one relationship to joi * @param string $foreignClass Class to join */ protected function joinHasOneRelation($localClass, $localField, $foreignClass) { if (!$foreignClass) { throw new InvalidArgumentException("Could not find a has_one relationship {$localField} on {$localClass}"); } if ($foreignClass === 'DataObject') { throw new InvalidArgumentException("Could not join polymorphic has_one relationship {$localField} on {$localClass}"); } // Skip if already joined if ($this->query->isJoinedTo($foreignClass)) { return; } $realModelClass = ClassInfo::table_for_object_field($localClass, "{$localField}ID"); $foreignBase = ClassInfo::baseDataClass($foreignClass); $this->query->addLeftJoin($foreignBase, "\"{$foreignBase}\".\"ID\" = \"{$realModelClass}\".\"{$localField}ID\""); /** * add join clause to the component's ancestry classes so that the search filter could search on * its ancestor fields. */ $ancestry = ClassInfo::ancestry($foreignClass, true); if (!empty($ancestry)) { $ancestry = array_reverse($ancestry); foreach ($ancestry as $ancestor) { if ($ancestor != $foreignBase) { $this->query->addLeftJoin($ancestor, "\"{$foreignBase}\".\"ID\" = \"{$ancestor}\".\"ID\""); } } } }
/** * Normalizes the field name to table mapping. * * @return string */ public function getDbName() { // Special handler for "NULL" relations if ($this->name == "NULL") { return $this->name; } // Ensure that we're dealing with a DataObject. if (!is_subclass_of($this->model, 'DataObject')) { throw new InvalidArgumentException("Model supplied to " . get_class($this) . " should be an instance of DataObject."); } $candidateClass = ClassInfo::table_for_object_field($this->model, $this->name); if ($candidateClass == 'DataObject') { // fallback to the provided name in the event of a joined column // name (as the candidate class doesn't check joined records) $parts = explode('.', $this->fullName); return '"' . implode('"."', $parts) . '"'; } return sprintf('"%s"."%s"', $candidateClass, $this->name); }