--- ../cake_1.2.0.4451alpha/cake/libs/model/datasources/dbo_source.php 2007-02-02 22:02:14.000000000 -0500 +++ cake/libs/model/datasources/dbo_source.php 2007-02-15 18:56:56.000000000 -0500 @@ -591,4 +591,8 @@ } } + + //DAL: recursive query joins! + $this->__generateRecursiveAssociationQuery($model, $queryData, $linkedModels); + // Build final query SQL $query = $this->generateAssociationQuery($model, $null, null, null, null, $queryData, false, $null); @@ -1120,4 +1124,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. @@ -1331,4 +1487,21 @@ $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; @@ -1342,5 +1515,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); if (isset($match['1']['0'])) { @@ -1360,17 +1535,9 @@ $conditions = rtrim($conditions); } - } - return $clause . $conditions; - } else { - $clause = ' WHERE '; - $out = $this->conditionKeysToString($conditions); - if (empty($out)) { - return $clause . ' (1 = 1)'; - } - return $clause . ' (' . join(') AND (', $out) . ')'; - } + return $conditions; } /** * Creates a WHERE clause by parsing given conditions array. Used by DboSource::conditions(). + * This one keeps the interface contract. * * @param array $conditions Array or string of conditions @@ -1378,4 +1545,14 @@ */ function conditionKeysToString($conditions) { + $recursivePaths = array(); + return $this->__conditionKeysToString($conditions, $recursivePaths); + } + /** + * Creates a WHERE clause by parsing given conditions array. Used by DboSource::conditions(). + * + * @param array $conditions Array or string of conditions + * @return string SQL fragment + */ + function __conditionKeysToString($conditions, &$recursivePaths) { $c = 0; $data = null; @@ -1387,5 +1564,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) . ')'; @@ -1397,5 +1574,5 @@ $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]); @@ -1409,12 +1586,12 @@ } } else { - $out[] = '(' . join(') AND (', $this->conditionKeysToString($value)) . ')'; + $out[] = '(' . join(') AND (', $this->__conditionKeysToString($value,$recursivePaths)) . ')'; } } elseif(is_numeric($key)) { $data = ' ' . $value; } elseif($value === null || (is_array($value) && empty($value))) { - $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?|(?:' . join('\\x20)|(?:', $this->__sqlOps) . '\\x20)|<=?(?![^>]+>)\\x20?|[>=!]{1,3}(?!<)\\x20?)?(.*)/i', $value, $match)) { if (preg_match('/(\\x20[\\w]*\\x20)/', $key, $regs)) { @@ -1433,9 +1610,9 @@ 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 { $match['2'] = $this->value($match['2']); $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']; } } @@ -1449,4 +1626,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.