Index: dbo_source.php =================================================================== --- dbo_source.php (revision 3819) +++ dbo_source.php (working copy) @@ -385,11 +385,11 @@ } else { $text = 'query'; } - + if (php_sapi_name() != 'cli') { print ("\n\n"); print ("\n\n\n\n"); - + foreach($log as $k => $i) { print ("\n"); } @@ -550,6 +550,10 @@ } } } + + // recursive query joins! + $this->__generateRecursiveAssociationQuery($model, $queryData, $linkedModels); + // Build final query SQL $query = $this->generateAssociationQuery($model, $null, null, null, null, $queryData, false, $null); $resultSet = $this->fetchAll($query, $model->cacheQueries, $model->name); @@ -625,7 +629,9 @@ if (isset($model->{$className}) && is_object($model->{$className})) { $data = $model->{$className}->afterFind(array(array($key => $results[$i][$key]))); } else { - $data = $model->{$className}->afterFind(array(array($key => $results[$i][$key]))); + //DAL: this makes no sense. The 'if' above proves it ... + //DAL: $data = $model->{$className}->afterFind(array(array($key => $results[$i][$key]))); + $data[0][$key]=& $results[$i][$key]; } $results[$i][$key] = $data[0][$key]; } @@ -1054,6 +1060,155 @@ } } } + + /** + * 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 = array_merge($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->{$assoc}; + 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->{$assoc}; + $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. * @@ -1256,33 +1411,7 @@ if (trim($conditions) == '') { $conditions = ' 1 = 1'; } else { - $start = null; - $end = null; - - if (!empty($this->startQuote)) { - $start = '\\\\' . $this->startQuote . '\\\\'; - } - $end = $this->endQuote; - - if (!empty($this->endQuote)) { - $end = '\\\\' . $this->endQuote . '\\\\'; - } - preg_match_all('/(?:\'[^\'\\\]*(?:\\\.[^\'\\\]*)*\')|([a-z0-9_' . $start . $end . ']*\\.[a-z0-9_' . $start . $end . ']*)/i', $conditions, $match, PREG_PATTERN_ORDER); - - if (isset($match['1']['0'])) { - $pregCount = count($match['1']); - - for($i = 0; $i < $pregCount; $i++) { - if (!empty($match['1'][$i]) && !is_numeric($match['1'][$i])) { - $conditions = preg_replace('/^' . $match['0'][$i] . '/', ' '.$this->name($match['1'][$i]), $conditions); - if (strpos($conditions, '(' . $match['0'][$i]) === false) { - $conditions = preg_replace('/[^\w]' . $match['0'][$i] . '/', ' '.$this->name($match['1'][$i]), $conditions); - } else { - $conditions = preg_replace('/' . $match['0'][$i] . '/', ' '.$this->name($match['1'][$i]), $conditions); - } - } - } - } + $conditions = $this->__escapeConditionFieldNames($conditions); } return $clause . $conditions; } else { @@ -1295,8 +1424,60 @@ } } + /** + * Place DB field name escapes on a condition string. + */ + function __escapeConditionFieldNames($conditions) { + + $start = null; + $end = null; + + if (!empty($this->startQuote)) { + $start = '\\\\' . $this->startQuote . '\\\\'; + } + $end = $this->endQuote; + + if (!empty($this->endQuote)) { + $end = '\\\\' . $this->endQuote . '\\\\'; + } + //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'])) { + $pregCount = count($match['1']); + for($i = 0; $i < $pregCount; $i++) { + if (!empty($match['1'][$i]) && !is_numeric($match['1'][$i])) { + $conditions = preg_replace('/^' . $match['0'][$i] . '/', ' '.$this->name($match['1'][$i]), $conditions); + if (strpos($conditions, '(' . $match['0'][$i]) === false) { + $conditions = preg_replace('/[^\w]' . $match['0'][$i] . '/', ' '.$this->name($match['1'][$i]), $conditions); + } else { + $conditions = preg_replace('/' . $match['0'][$i] . '/', ' '.$this->name($match['1'][$i]), $conditions); + } + } + } + } + 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; $out = array(); @@ -1306,7 +1487,7 @@ foreach($conditions as $key => $value) { if (in_array(strtolower(trim($key)), $bool)) { $join = ' ' . strtoupper($key) . ' '; - $value = $this->conditionKeysToString($value); + $value = $this->conditionKeysToString($value, recursivePaths); if (strpos($join, 'NOT')) { $out[] = 'NOT (' . join(') ' . strtoupper($key) . ' (', $value) . ')'; } else { @@ -1317,7 +1498,7 @@ $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]); $data .= $value[0]; @@ -1329,14 +1510,14 @@ $data[strlen($data) - 2] = ')'; } } 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)) { $clause = $regs['1']; @@ -1353,13 +1534,13 @@ 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'])) { $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']; } } @@ -1371,6 +1552,46 @@ } 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', '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. * @@ -1556,4 +1777,4 @@ } } } -?> \ No newline at end of file +?>
{$this->_queriesCnt} {$text} took {$this->_queriesTime} ms
NrQueryErrorAffectedNum. rowsTook (ms)
" . ($k + 1) . "{$i['query']}{$i['error']}{$i['affected']}{$i['numRows']}{$i['took']}