Index: class.ux_t3lib_sqlparser.php =================================================================== --- class.ux_t3lib_sqlparser.php (revision 27129) +++ class.ux_t3lib_sqlparser.php (working copy) @@ -183,94 +183,108 @@ // Find "modifier", eg. "NOT or !" $stack[$level][$pnt[$level]]['modifier'] = trim($this->nextPart($parseString, '^(!|NOT[[:space:]]+)')); - // Support calculated value only for: - // - "&" (boolean AND) - // - "+" (addition) - // - "-" (substraction) - // - "*" (multiplication) - // - "/" (division) - // - "%" (modulo) - $calcOperators = '&|\+|-|\*|\/|%'; + // See if condition is EXISTS with a subquery + if (preg_match('/^EXISTS[[:space:]]*[(]/', $parseString)) { + $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(EXISTS)'); + $this->nextPart($parseString, '^([(])'); + $stack[$level][$pnt[$level]]['func']['subquery'] = $this->parseSELECT($parseString); + // Seek to new position in parseString after parsing of the subquery + $parseString = $stack[$level][$pnt[$level]]['func']['subquery']['parseString']; + unset($stack[$level][$pnt[$level]]['func']['subquery']['parseString']); + if (!$this->nextPart($parseString, '^([)])')) { + return 'No ) parenthesis at end of subquery'; + } + } else { - // Fieldname: - if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)([[:space:]]+|' . $calcOperators . '|<=|>=|<|>|=|!=|IS)')) { - - // Parse field name into field and table: - $tableField = explode('.', $fieldName, 2); - if (count($tableField) == 2) { - $stack[$level][$pnt[$level]]['table'] = $tableField[0]; - $stack[$level][$pnt[$level]]['field'] = $tableField[1]; - } else { - $stack[$level][$pnt[$level]]['table'] = ''; - $stack[$level][$pnt[$level]]['field'] = $tableField[0]; - } - } else { - return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); - } - - // See if the value is calculated: - $stack[$level][$pnt[$level]]['calc'] = $this->nextPart($parseString, '^(' . $calcOperators . ')'); - if (strlen($stack[$level][$pnt[$level]]['calc'])) { - // Finding value for calculation: - $calc_value = $this->getValue($parseString); - $stack[$level][$pnt[$level]]['calc_value'] = $calc_value; - if (count($calc_value) == 1 && is_string($calc_value[0])) { - // Value is a field, store it to allow DBAL to post-process it (quoting, remapping) - $tableField = explode('.', $calc_value[0], 2); + // Support calculated value only for: + // - "&" (boolean AND) + // - "+" (addition) + // - "-" (substraction) + // - "*" (multiplication) + // - "/" (division) + // - "%" (modulo) + $calcOperators = '&|\+|-|\*|\/|%'; + + // Fieldname: + if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)([[:space:]]+|' . $calcOperators . '|<=|>=|<|>|=|!=|IS)')) { + + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); if (count($tableField) == 2) { - $stack[$level][$pnt[$level]]['calc_table'] = $tableField[0]; - $stack[$level][$pnt[$level]]['calc_field'] = $tableField[1]; + $stack[$level][$pnt[$level]]['table'] = $tableField[0]; + $stack[$level][$pnt[$level]]['field'] = $tableField[1]; } else { - $stack[$level][$pnt[$level]]['calc_table'] = ''; - $stack[$level][$pnt[$level]]['calc_field'] = $tableField[0]; + $stack[$level][$pnt[$level]]['table'] = ''; + $stack[$level][$pnt[$level]]['field'] = $tableField[0]; } + } else { + return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); } - } - - // Find "comparator": - $stack[$level][$pnt[$level]]['comparator'] = $this->nextPart($parseString, '^(<=|>=|<|>|=|!=|NOT[[:space:]]+IN|IN|NOT[[:space:]]+LIKE|LIKE|IS[[:space:]]+NOT|IS)'); - if (strlen($stack[$level][$pnt[$level]]['comparator'])) { - if (preg_match('/^CONCAT[[:space:]]*\(/', $parseString)) { - $this->nextPart($parseString, '^(CONCAT[[:space:]]?[(])'); - $values = array( - 'operator' => 'CONCAT', - 'args' => array(), - ); - $cnt = 0; - while ($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)')) { - // Parse field name into field and table: - $tableField = explode('.', $fieldName, 2); + + // See if the value is calculated: + $stack[$level][$pnt[$level]]['calc'] = $this->nextPart($parseString, '^(' . $calcOperators . ')'); + if (strlen($stack[$level][$pnt[$level]]['calc'])) { + // Finding value for calculation: + $calc_value = $this->getValue($parseString); + $stack[$level][$pnt[$level]]['calc_value'] = $calc_value; + if (count($calc_value) == 1 && is_string($calc_value[0])) { + // Value is a field, store it to allow DBAL to post-process it (quoting, remapping) + $tableField = explode('.', $calc_value[0], 2); if (count($tableField) == 2) { - $values['args'][$cnt]['table'] = $tableField[0]; - $values['args'][$cnt]['field'] = $tableField[1]; + $stack[$level][$pnt[$level]]['calc_table'] = $tableField[0]; + $stack[$level][$pnt[$level]]['calc_field'] = $tableField[1]; } else { - $values['args'][$cnt]['table'] = ''; - $values['args'][$cnt]['field'] = $tableField[0]; + $stack[$level][$pnt[$level]]['calc_table'] = ''; + $stack[$level][$pnt[$level]]['calc_field'] = $tableField[0]; } - // Looking for comma: - $this->nextPart($parseString, '^(,)'); - $cnt++; } - // Look for ending parenthesis: - $this->nextPart($parseString, '([)])'); - $stack[$level][$pnt[$level]]['value'] = $values; - } else if (t3lib_div::inList('IN,NOT IN', $stack[$level][$pnt[$level]]['comparator']) && preg_match('/^[(][[:space:]]*SELECT[[:space:]]+/', $parseString)) { - $this->nextPart($parseString, '^([(])'); - $stack[$level][$pnt[$level]]['subquery'] = $this->parseSELECT($parseString); - // Seek to new position in parseString after parsing of the subquery - $parseString = $stack[$level][$pnt[$level]]['subquery']['parseString']; - unset($stack[$level][$pnt[$level]]['subquery']['parseString']); - if (!$this->nextPart($parseString, '^([)])')) { - return 'No ) parenthesis at end of subquery'; + } + + // Find "comparator": + $stack[$level][$pnt[$level]]['comparator'] = $this->nextPart($parseString, '^(<=|>=|<|>|=|!=|NOT[[:space:]]+IN|IN|NOT[[:space:]]+LIKE|LIKE|IS[[:space:]]+NOT|IS)'); + if (strlen($stack[$level][$pnt[$level]]['comparator'])) { + if (preg_match('/^CONCAT[[:space:]]*\(/', $parseString)) { + $this->nextPart($parseString, '^(CONCAT[[:space:]]?[(])'); + $values = array( + 'operator' => 'CONCAT', + 'args' => array(), + ); + $cnt = 0; + while ($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)')) { + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); + if (count($tableField) == 2) { + $values['args'][$cnt]['table'] = $tableField[0]; + $values['args'][$cnt]['field'] = $tableField[1]; + } else { + $values['args'][$cnt]['table'] = ''; + $values['args'][$cnt]['field'] = $tableField[0]; + } + // Looking for comma: + $this->nextPart($parseString, '^(,)'); + $cnt++; + } + // Look for ending parenthesis: + $this->nextPart($parseString, '([)])'); + $stack[$level][$pnt[$level]]['value'] = $values; + } else if (t3lib_div::inList('IN,NOT IN', $stack[$level][$pnt[$level]]['comparator']) && preg_match('/^[(][[:space:]]*SELECT[[:space:]]+/', $parseString)) { + $this->nextPart($parseString, '^([(])'); + $stack[$level][$pnt[$level]]['subquery'] = $this->parseSELECT($parseString); + // Seek to new position in parseString after parsing of the subquery + $parseString = $stack[$level][$pnt[$level]]['subquery']['parseString']; + unset($stack[$level][$pnt[$level]]['subquery']['parseString']); + if (!$this->nextPart($parseString, '^([)])')) { + return 'No ) parenthesis at end of subquery'; + } + } else { + // Finding value for comparator: + $stack[$level][$pnt[$level]]['value'] = $this->getValue($parseString, $stack[$level][$pnt[$level]]['comparator']); + if ($this->parse_error) { + return $this->parse_error; + } } - } else { - // Finding value for comparator: - $stack[$level][$pnt[$level]]['value'] = $this->getValue($parseString, $stack[$level][$pnt[$level]]['comparator']); - if ($this->parse_error) { - return $this->parse_error; - } } - } + } // Finished, increase pointer: $pnt[$level]++; @@ -282,6 +296,13 @@ return $stack[0]; } + // Checking if we are back to level 0 and we should still decrease level, + // meaning we were probably parsing a subquery and should return here: + if ($level === 0 && preg_match('/^[)]/', $parseString)) { + // Return the stacks lowest level: + return $stack[0]; + } + // Checking if the current level is ended, in that case do stack management: while ($this->nextPart($parseString,'^([)])')) { $level--; // Decrease level: @@ -392,6 +413,8 @@ // Look for sublevel: if (is_array($v['sub'])) { $output .= ' (' . trim($this->nativeCompileWhereClause($v['sub'])) . ')'; + } elseif (isset($v['func'])) { + $output .= ' ' . trim($v['modifier']) . ' ' . $v['func']['type'] . ' (' . $this->compileSELECT($v['func']['subquery']) . ')'; } else { // Set field/table with modifying prefix if any: @@ -755,6 +778,8 @@ // Look for sublevel: if (is_array($v['sub'])) { $output .= ' (' . trim($this->compileWhereClause($v['sub'], $functionMapping)) . ')'; + } elseif (isset($v['func'])) { + $output .= ' ' . trim($v['modifier']) . ' ' . $v['func']['type'] . ' (' . $this->compileSELECT($v['func']['subquery']) . ')'; } else { // Set field/table with modifying prefix if any: Index: class.ux_t3lib_db.php =================================================================== --- class.ux_t3lib_db.php (revision 27126) +++ class.ux_t3lib_db.php (working copy) @@ -1082,6 +1082,8 @@ // Look for sublevel: if (is_array($where_clause[$k]['sub'])) { $where_clause[$k]['sub'] = $this->_quoteWhereClause($where_clause[$k]['sub']); + } elseif (isset($v['func'])) { + $where_clause[$k]['func']['subquery'] = $this->quoteSELECTsubquery($v['func']['subquery']); } else { if ($where_clause[$k]['table'] != '') { $where_clause[$k]['table'] = $this->quoteName($where_clause[$k]['table']); @@ -2526,6 +2528,11 @@ // Look for sublevel (WHERE parts only) if (is_array($sqlPartArray[$k]['sub'])) { $this->map_sqlParts($sqlPartArray[$k]['sub'], $defaultTable); // Call recursively! + } elseif (isset($sqlPartArray[$k]['func'])) { + $subqueryDefaultTable = $sqlPartArray[$k]['func']['subquery']['FROM'][0]['table']; + $this->map_sqlParts($sqlPartArray[$k]['func']['subquery']['SELECT'], $subqueryDefaultTable); + $this->map_sqlParts($sqlPartArray[$k]['func']['subquery']['FROM'], $subqueryDefaultTable); + $this->map_sqlParts($sqlPartArray[$k]['func']['subquery']['WHERE'], $subqueryDefaultTable); } else { // For the field, look for table mapping (generic): $t = $sqlPartArray[$k]['table'] ? $sqlPartArray[$k]['table'] : $defaultTable; Index: tests/sqlparser_general_testcase.php =================================================================== --- tests/sqlparser_general_testcase.php (revision 27126) +++ tests/sqlparser_general_testcase.php (working copy) @@ -307,5 +307,29 @@ $this->assertEquals($expected, $actual); } + + /** + * @test + * @see http://bugs.typo3.org/view.php?id=12758 + */ + public function whereClauseSupportsExistsKeyword() { + $parseString = 'EXISTS (SELECT * FROM tx_crawler_queue WHERE tx_crawler_queue.process_id = tx_crawler_process.process_id AND tx_crawler_queue.exec_time = 0)'; + $whereParts = $this->fixture->parseWhereClause($parseString); + + $this->assertTrue(is_array($whereParts), $whereParts); + $this->assertTrue(empty($parseString), 'parseString is not empty'); + } + + /** + * @test + * @see http://bugs.typo3.org/view.php?id=12758 + */ + public function existsClauseIsProperlyCompiled() { + $sql = 'SELECT * FROM tx_crawler_process WHERE active = 0 AND NOT EXISTS (SELECT * FROM tx_crawler_queue WHERE tx_crawler_queue.process_id = tx_crawler_process.process_id AND tx_crawler_queue.exec_time = 0)'; + $expected = 'SELECT * FROM tx_crawler_process WHERE active = 0 AND NOT EXISTS (SELECT * FROM tx_crawler_queue WHERE tx_crawler_queue.process_id = tx_crawler_process.process_id AND tx_crawler_queue.exec_time = 0)'; + $actual = $this->cleanSql($this->fixture->debug_testSQL($sql)); + + $this->assertEquals($expected, $actual); + } } ?> \ No newline at end of file Index: tests/db_oracle_testcase.php =================================================================== --- tests/db_oracle_testcase.php (revision 27129) +++ tests/db_oracle_testcase.php (working copy) @@ -526,5 +526,53 @@ $expected .= ')'; $this->assertEquals($expected, $query); } + + /** + * @test + * @see http://bugs.typo3.org/view.php?id=12758 + */ + public function existsWhereClauseIsProperlyQuoted() { + $query = $this->cleanSql($GLOBALS['TYPO3_DB']->SELECTquery( + '*', + 'tx_crawler_process', + 'active = 0 AND NOT EXISTS (' . + $GLOBALS['TYPO3_DB']->SELECTsubquery( + '*', + 'tx_crawler_queue', + 'tx_crawler_queue.process_id = tx_crawler_process.process_id AND tx_crawler_queue.exec_time = 0)' + ) . + ')' + )); + $expected = 'SELECT * FROM "tx_crawler_process" WHERE "active" = 0 AND NOT EXISTS ('; + $expected .= 'SELECT * FROM "tx_crawler_queue" WHERE "tx_crawler_queue"."process_id" = "tx_crawler_process"."process_id" AND "tx_crawler_queue"."exec_time" = 0'; + $expected .= ')'; + $this->assertEquals($expected, $query); + } + + /** + * @test + * @see http://bugs.typo3.org/view.php?id=12758 + */ + public function subqueryIsRemappedForExistsWhereClause() { + $selectFields = '*'; + $fromTables = 'tx_crawler_process'; + $whereClause = 'active = 0 AND NOT EXISTS (' . + $GLOBALS['TYPO3_DB']->SELECTsubquery( + '*', + 'tx_crawler_queue', + 'tx_crawler_queue.process_id = tx_crawler_process.process_id AND tx_crawler_queue.exec_time = 0' + ) . + ')'; + $groupBy = ''; + $orderBy = ''; + + $GLOBALS['TYPO3_DB']->_callRef('map_remapSELECTQueryParts', $selectFields, $fromTables, $whereClause, $groupBy, $orderBy); + $query = $this->cleanSql($GLOBALS['TYPO3_DB']->SELECTquery($selectFields, $fromTables, $whereClause, $groupBy, $orderBy)); + + $expected = 'SELECT * FROM "tx_crawler_ps" WHERE "is_active" = 0 AND NOT EXISTS ('; + $expected .= 'SELECT * FROM "tx_crawler_queue" WHERE "tx_crawler_queue"."process_id" = "tx_crawler_ps"."ps_id" AND "tx_crawler_queue"."exec_time" = 0'; + $expected .= ')'; + $this->assertEquals($expected, $query); + } } ?> \ No newline at end of file