--- /Users/david/Desktop/downloads/cake_1.1.12.4205/cake/libs/model/datasources/dbo_source.php 2006-12-25 07:06:13.000000000 -0500 +++ dbo_source.1.1.x.4202_recursive-query.ph_.txt 2007-02-18 11:42:22.000000000 -0500 @@ -551,8 +551,42 @@ } } + + // recursive query joins! + $this->__generateRecursiveAssociationQuery($model, &$queryData, &$linkedModels); + // Build final query SQL $query = $this->generateAssociationQuery($model, $null, null, null, null, $queryData, false, $null); +//pr($query); $resultSet = $this->fetchAll($query, $model->cacheQueries, $model->name); +//pr($resultSet); $filtered = $this->__filterResults($resultSet, $model); +//pr($linkedModels); + + $recursiveLinkedModels = array(); + foreach ($linkedModels as $linkedModel) { + if ( strpos($linkedModel, '::') !== false ) { + $split1 = explode('/', $linkedModel); + if ($resultSet && (count($resultSet) > 0) && isset($resultSet[0][$split1[1]])) { + $recursiveLinkedModels[] = explode('::', $split1[1]); + } + } + } +//pr($recursiveLinkedModels); + foreach ($recursiveLinkedModels as $split2) { + foreach ($resultSet as $k => $v) { + if (!isset($resultSet[$k][$split2[0]])) + $resultSet[$k][$split2[0]] = array(); + $rs =& $resultSet[$k][$split2[0]]; + for ($i=1; $i < count($split2)-1; $i++) { + if (!isset($rs[$split2[$i]])) + $rs[$split2[$i]] = array(); + $rs =& $rs[$split2[$i]]; + } + $split1 = implode('::',$split2); + $rs[$split2[count($split2)-1]] =& $resultSet[$k][$split1]; + unset($resultSet[$k][$split1]); + } + } +//pr($resultSet); if ($model->recursive > 0) { @@ -626,5 +660,11 @@ $data = $model->{$className}->afterFind(array(array($key => $results[$i][$key]))); } + + if (isset($data[0][$key])) { $results[$i][$key] = $data[0][$key]; + } else { + unset($results[$i][$key]); + } +// $results[$i][$key] = $data[0][$key]; } } @@ -1076,4 +1116,156 @@ } } + + /** + * Find the recursive query joins for conditions like A::B::C.fieldname + * + *@param $model Root of the query. + *@param $queryData + *@param $linkedModels List of previously linked models. + */ + function __generateRecursiveAssociationQuery(&$model, &$queryData, &$linkedModels) { + $data = ''; + $null = null; + + $loaded_links =& $linkedModels; + + $conditions =& $queryData['conditions']; + if (is_array($conditions)) { + $condition_keys = $this->conditionKeys($conditions); + } else { + // cheat: get_escape... to place ticks around the keys then explode them out + // while we're here, correct the escaping + $conditions = $queryData['conditions'] = $this->__escapeConditionFieldNames($conditions); + $condition_keys = explode("`", $conditions); + } + + foreach ($condition_keys as $key) { + if (strstr($key,'::') !== false) { + // it looks close, can we get there from here? + + $condition_key = $key; + $pos = strrchr($condition_key,'.'); + if (false !== $pos) { + $condition_key = substr($condition_key, 0, strlen($condition_key)-strlen($pos)); + } + $condition_path = explode('::',$condition_key); + + $link_path = ''; + $link_key = ''; + $link_stack = array(); + $replacements = array(); + foreach ($condition_path as $path_key => $condition_element) { + if ($path_key === 0) { + // the first is different. many joins are already there + foreach($model->__associations as $type) { + foreach($model->{$type} as $assoc => $assocData) { + if ($assoc === $condition_element) { + $linkModel =& $model->{$assocData['className']}; + if (!in_array($type . '/' . $condition_element, $loaded_links)) { + // not loaded yet + + $external = isset($assocData['external']); + + if ($external) { + // external recursive queries are broke! + Object::log("external recursive query not supported ($assoc)", LOG_ERROR); + return; + } + + if ( $type === 'hasMany' ) { + // cheat: hasMany acts like hasOne on a nested join + $type = 'hasOne'; + $result = $this->generateAssociationQuery(&$model, &$linkModel, $type, $assoc, $assocData, &$queryData, $external, &$null); + } + else if ( $type === 'hasAndBelongsToMany') { + // these are broke for our use in generateAssociationQuery + // permission to modify this behavior might eliminate.... + Object::log("HABTM recursive query not supported ($assoc)", LOG_ERROR); + return; + } else { + $result = $this->generateAssociationQuery(&$model, &$linkModel, $type, $assoc, $assocData, &$queryData, $external, &$null); + if (is_string($result)) { + $result = str_replace('{$__cakeID__$}', $model->name . '.' . $model->primaryKey, $result); + $result = str_replace('{$__cakeForeignKey__$}', $assoc . '.' . $assocData['foreignKey'], $result); + $queryData['joins'][] = " ,($result) {$this->alias} $assoc "; + $loaded_links[] = $type . '/' . $assoc; + } + } + } + $link_stack[] = $linkModel; + $link_path = $assoc; + $model->__assocname__ = $model->name; + } + } + } + } else { + $amodel = $link_stack[count($link_stack)-1]; + foreach($amodel->__associations as $type) { + foreach($amodel->{$type} as $assoc => $assocData) { + if ($assoc === $condition_element) { + $linkModel =& $amodel->{$assocData['className']}; + $external = isset($assocData['external']); + + $link_stack[] = $linkModel; + $link_path .= '::' . $assoc; + + if ($external) { + // external recursive queries are broke! + Object::log("external recursive query not supported ($link_path)", LOG_ERROR); + return; + } + + $replacements[$assoc] = $link_path; + + $save_conditions = $assocData['conditions']; + $save_modelname = $amodel->name; + if (isset($replacements[$amodel->name])) { + $amodel->name = $replacements[$amodel->name]; + } + $amodel->__assocname__ = $amodel->name; + + if (is_string($assocData['conditions'])) { + $assocData['conditions'] = $this->__escapeConditionFieldNames($this->__scrubConditionKey($assocData['conditions'], $replacements)); + } else { + $assocData['conditions'] = $this->__conditionKeysToString( (array)($assocData['conditions']), $replacements ); + } + + if ( $type === 'hasMany' ) { + // cheat: hasMany acts like hasOne on a nested join + $type = 'hasOne'; + $result = $this->generateAssociationQuery( &$amodel, &$linkModel, $type, $link_path, $assocData, &$queryData, $external, &$null); + } else if ( $type === 'hasAndBelongsToMany') { + // these are broke for our use in generateAssociationQuery + // permission to modify this behavior might eliminate.... + Object::log("HABTM recursive query not supported ($link_path)", LOG_ERROR); + return; + } else { + $result = $this->generateAssociationQuery(&$amodel, &$linkModel, $type, $link_path, $assocData, &$queryData, $external, &$null); + if (is_string($result)) { + $result = str_replace('{$__cakeID__$}', $amodel->__assocname__ . '.' . $amodel->primaryKey, $result); + $result = str_replace('{$__cakeForeignKey__$}', $link_path . '.' . $assocData['foreignKey'], $result); + $queryData['joins'][] = " ,($result) {$this->alias} $assoc "; + + } + + $loaded_links[] = $type . '/' . $link_path; + } + + //restore model state + $assocData['conditions'] = $save_conditions; + $amodel->name = $save_modelname; + } + } + } + } + } + + }// check for -> + + }// if is_array + + return $data; + } + /** * Generates and executes an SQL UPDATE statement for given model, fields, and values. @@ -1278,4 +1470,22 @@ $conditions = ' 1 = 1'; } else { + $conditions = $this->__escapeConditionFieldNames($conditions); + } + return $clause . $conditions; + } else { + $clause = ' WHERE '; + $out = $this->conditionKeysToString($conditions); + if (empty($out)) { + return $clause . ' (1 = 1)'; + } + return $clause . ' (' . join(') AND (', $out) . ')'; + } + } + + /** + * Place DB field name escapes on a condition string. + */ + function __escapeConditionFieldNames($conditions) { + $start = null; $end = null; @@ -1289,5 +1499,7 @@ $end = '\\\\' . $this->endQuote . '\\\\'; } - preg_match_all('/(?:\'[^\'\\\]*(?:\\\.[^\'\\\]*)*\')|([a-z0-9_' . $start . $end . ']*\\.[a-z0-9_' . $start . $end . ']*)/i', $conditions, $match, PREG_PATTERN_ORDER); + //DAL: add : as part of linkModel name + // preg_match_all('/(?:\'[^\'\\\]*(?:\\\.[^\'\\\]*)*\')|([a-z0-9_' . $start . $end . ']*\\.[a-z0-9_' . $start . $end . ']*)/i', $conditions, $match, PREG_PATTERN_ORDER); + preg_match_all('/(?:\'[^\'\\\]*(?:\\\.[^\'\\\]*)*\')|([\\:a-z0-9_' . $start . $end . ']*\\.[a-z0-9_' . $start . $end . ']*)/i', $conditions, $match, PREG_PATTERN_ORDER); if (isset($match['1']['0'])) { @@ -1305,18 +1517,22 @@ } } - } - return $clause . $conditions; - } else { - $clause = ' WHERE '; - $out = $this->conditionKeysToString($conditions); - if (empty($out)) { - return $clause . ' (1 = 1)'; - } - return $clause . ' (' . join(') AND (', $out) . ')'; - } + return $conditions; } + /** + * Convert a conditions array into a flat list of string conditions. + * This one keeps the interface contract. + */ function conditionKeysToString($conditions) { - + $recursivePaths = array(); + return $this->__conditionKeysToString($conditions, $recursivePaths); + } + /** + * Convert a conditions array into a flat list of string conditions. + * + * @param $conditions array of query conditions + * @param $recursivePaths array optional map for recursive association queries contains SimpleName => FullPathName. + */ + function __conditionKeysToString($conditions, &$recursivePaths) { $c = 0; $data = null; @@ -1328,5 +1544,5 @@ if (in_array(strtolower(trim($key)), $bool)) { $join = ' ' . strtoupper($key) . ' '; - $value = $this->conditionKeysToString($value); + $value = $this->__conditionKeysToString($value, $recursivePaths); if (strpos($join, 'NOT') !== false) { $out[] = 'NOT (' . join(') ' . strtoupper($key) . ' (', $value) . ')'; @@ -1336,8 +1552,7 @@ } else { if (is_array($value) && !empty($value)) { - $keys = array_keys($value); if ($keys[0] === 0) { - $data = $this->name($key) . ' IN ('; + $data = $this->__scrubConditionKey($this->name($key),$recursivePaths) . ' IN ('; if (strpos($value[0], '-!') === 0){ $value[0] = str_replace('-!', '', $value[0]); @@ -1351,12 +1566,12 @@ } } else { - $out[] = '(' . join(') AND (', $this->conditionKeysToString($value)) . ')'; + $out[] = '(' . join(') AND (', $this->__conditionKeysToString($value, $recursivePaths)) . ')'; } } elseif(is_numeric($key)) { $data = ' ' . $value; } elseif($value === null) { - $data = $this->name($key) . ' IS NULL'; + $data = $this->__scrubConditionKey($this->name($key), $recursivePaths) . ' IS NULL'; } elseif($value === '') { - $data = $this->name($key) . " = ''"; + $data = $this->__scrubConditionKey($this->name($key)) . " = ''"; } elseif(preg_match('/^([a-z]*\\([a-z0-9]*\\)\\x20?|(?:like\\x20)|(?:or\\x20)|(?:not\\x20)|(?:in\\x20)|(?:between\\x20)|(?:regexp\\x20)|[<> = !]{1,3}\\x20?)?(.*)/i', $value, $match)) { if (preg_match('/(\\x20[\\w]*\\x20)/', $key, $regs)) { @@ -1375,5 +1590,5 @@ if (strpos($match['2'], '-!') === 0) { $match['2'] = str_replace('-!', '', $match['2']); - $data = $this->name($key) . ' ' . $match['1'] . ' ' . $match['2']; + $data = $this->__scrubConditionKey($this->name($key), $recursivePaths) . ' ' . $match['1'] . ' ' . $match['2']; } else { if ($match['2'] != '' && !is_numeric($match['2'])) { @@ -1381,5 +1596,5 @@ $match['2'] = str_replace(' AND ', "' AND '", $match['2']); } - $data = $this->name($key) . ' ' . $match['1'] . ' ' . $match['2']; + $data = $this->__scrubConditionKey($this->name($key), $recursivePaths) . ' ' . $match['1'] . ' ' . $match['2']; } } @@ -1393,4 +1608,44 @@ return $out; } + + /** + * Place the recursive query paths onto the field names. + */ + function __scrubConditionKey($text, &$recursivePathMap) { + foreach ($recursivePathMap as $pathkey => $pathvalue) { + $text = str_replace($pathkey, $pathvalue, $text); + } + return $text; + } + + /** + * Get the list of condition's key + * + * @param array $conditions Query conditions + * @param array out optional set of keys to add - not typically used by users. + * @return array keys + */ + function conditionKeys($conditions, &$out=array()) { + + $bool = array('and', 'or', 'not', 'and not', 'or not', 'xor', '||', '&&'); + + foreach($conditions as $key => $value) { + if (in_array(strtolower(trim($key)), $bool)) { + $this->conditionKeys($value, $out); + } else { + if (is_array($value) && !empty($value)) { + $keys = array_keys($value); + if ($keys[0] !== 0) { + $this->conditionKeys($value, $out); + } + } + if (!is_numeric($key)) { + $out[] = $key; + } + } + } + return $out; + } + /** * Returns a limit statement in the correct format for the particular database.