From d3441386cb016757f01e961e9fc6a772b352e1e7 Mon Sep 17 00:00:00 2001 From: Morton Jonuschat <m.jonuschat@mojocode.de> Date: Fri, 7 Aug 2015 10:12:07 +0200 Subject: [PATCH] [!!!][TASK] Move SqlParser into EXT:dbal EXT:dbal has been the sole user of SqlParser for some time. Tests for functionality of the core SqlParser have been spread between the Dbal and the Core tests with the bulk of the tests for the core functionality happening in EXT:dbal. The two SqlParsers have been merged, parsing and compiling SQL has been split into separate Classes for separation of concerns. Resolves: #68401 Releases: master Change-Id: I930bbbdc7e0ac427ca856f686d601fc0bbe48e33 Reviewed-on: http://review.typo3.org/42347 Reviewed-by: Benni Mack <benni@typo3.org> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de> Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de> Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch> Tested-by: Christian Kuhn <lolli@schwarzbu.ch> --- .../Migrations/Code/ClassAliasMap.php | 3 +- .../core/Classes/Database/SqlParser.php | 2100 ---------------- ...eaking-68401-SqlParserMovedIntoEXTdbal.rst | 36 + .../Tests/Unit/Database/SqlParserTest.php | 492 ---- .../Classes/Database/DatabaseConnection.php | 12 +- .../SqlCompilers/AbstractCompiler.php | 316 +++ .../Classes/Database/SqlCompilers/Adodb.php | 590 +++++ .../Classes/Database/SqlCompilers/Mysql.php | 311 +++ .../dbal/Classes/Database/SqlParser.php | 2129 ++++++++++++----- .../Tests/Unit/Database/AbstractTestCase.php | 6 +- .../Database/DatabaseConnectionOracleTest.php | 8 +- .../DatabaseConnectionPostgresqlTest.php | 4 +- .../Tests/Unit/Database/SqlParserTest.php | 495 +++- typo3/sysext/dbal/ext_localconf.php | 1 - 14 files changed, 3320 insertions(+), 3183 deletions(-) delete mode 100644 typo3/sysext/core/Classes/Database/SqlParser.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-68401-SqlParserMovedIntoEXTdbal.rst delete mode 100644 typo3/sysext/core/Tests/Unit/Database/SqlParserTest.php create mode 100644 typo3/sysext/dbal/Classes/Database/SqlCompilers/AbstractCompiler.php create mode 100644 typo3/sysext/dbal/Classes/Database/SqlCompilers/Adodb.php create mode 100644 typo3/sysext/dbal/Classes/Database/SqlCompilers/Mysql.php diff --git a/typo3/sysext/compatibility6/Migrations/Code/ClassAliasMap.php b/typo3/sysext/compatibility6/Migrations/Code/ClassAliasMap.php index e1e85551ccfe..31e12cae333d 100644 --- a/typo3/sysext/compatibility6/Migrations/Code/ClassAliasMap.php +++ b/typo3/sysext/compatibility6/Migrations/Code/ClassAliasMap.php @@ -172,7 +172,8 @@ return array( 't3lib_refindex' => \TYPO3\CMS\Core\Database\ReferenceIndex::class, 't3lib_loadDBGroup' => \TYPO3\CMS\Core\Database\RelationHandler::class, 't3lib_softrefproc' => \TYPO3\CMS\Core\Database\SoftReferenceIndex::class, - 't3lib_sqlparser' => \TYPO3\CMS\Core\Database\SqlParser::class, + 't3lib_sqlparser' => \TYPO3\CMS\Dbal\Database\SqlParser::class, + 'TYPO3\\CMS\\Core\\Database\\SqlParser' => \TYPO3\CMS\Dbal\Database\SqlParser::class, 't3lib_extTables_PostProcessingHook' => \TYPO3\CMS\Core\Database\TableConfigurationPostProcessingHookInterface::class, 't3lib_TCEmain' => \TYPO3\CMS\Core\DataHandling\DataHandler::class, 't3lib_TCEmain_checkModifyAccessListHook' => \TYPO3\CMS\Core\DataHandling\DataHandlerCheckModifyAccessListHookInterface::class, diff --git a/typo3/sysext/core/Classes/Database/SqlParser.php b/typo3/sysext/core/Classes/Database/SqlParser.php deleted file mode 100644 index 70e7dafb5b5d..000000000000 --- a/typo3/sysext/core/Classes/Database/SqlParser.php +++ /dev/null @@ -1,2100 +0,0 @@ -<?php -namespace TYPO3\CMS\Core\Database; - -/* - * This file is part of the TYPO3 CMS project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ - -/** - * TYPO3 SQL parser class. - */ -class SqlParser { - - /** - * Parsing error string - * - * @var string - */ - public $parse_error = ''; - - /** - * Last stop keyword used. - * - * @var string - */ - public $lastStopKeyWord = ''; - - /** - * Find "comparator" - * - * @var array - */ - static protected $comparatorPatterns = array( - '<=', - '>=', - '<>', - '<', - '>', - '=', - '!=', - 'NOT[[:space:]]+IN', - 'IN', - 'NOT[[:space:]]+LIKE[[:space:]]+BINARY', - 'LIKE[[:space:]]+BINARY', - 'NOT[[:space:]]+LIKE', - 'LIKE', - 'IS[[:space:]]+NOT', - 'IS', - 'BETWEEN', - 'NOT[[:space]]+BETWEEN' - ); - - /** - * Whitespaces in a query - * - * @var array - */ - static protected $interQueryWhitespaces = array(' ', TAB, CR, LF); - - /** - * Default constructor - */ - public function __construct() {} - - /************************************* - * - * SQL Parsing, full queries - * - **************************************/ - /** - * Parses any single SQL query - * - * @param string $parseString SQL query - * @return array Result array with all the parts in - or error message string - * @see compileSQL(), debug_testSQL() - */ - public function parseSQL($parseString) { - // Prepare variables: - $parseString = $this->trimSQL($parseString); - $this->parse_error = ''; - $result = array(); - // Finding starting keyword of string: - $_parseString = $parseString; - // Protecting original string... - $keyword = $this->nextPart($_parseString, '^(SELECT|UPDATE|INSERT[[:space:]]+INTO|DELETE[[:space:]]+FROM|EXPLAIN|(DROP|CREATE|ALTER|TRUNCATE)[[:space:]]+TABLE|CREATE[[:space:]]+DATABASE)[[:space:]]+'); - $keyword = $this->normalizeKeyword($keyword); - switch ($keyword) { - case 'SELECT': - // Parsing SELECT query: - $result = $this->parseSELECT($parseString); - break; - case 'UPDATE': - // Parsing UPDATE query: - $result = $this->parseUPDATE($parseString); - break; - case 'INSERTINTO': - // Parsing INSERT query: - $result = $this->parseINSERT($parseString); - break; - case 'DELETEFROM': - // Parsing DELETE query: - $result = $this->parseDELETE($parseString); - break; - case 'EXPLAIN': - // Parsing EXPLAIN SELECT query: - $result = $this->parseEXPLAIN($parseString); - break; - case 'DROPTABLE': - // Parsing DROP TABLE query: - $result = $this->parseDROPTABLE($parseString); - break; - case 'ALTERTABLE': - // Parsing ALTER TABLE query: - $result = $this->parseALTERTABLE($parseString); - break; - case 'CREATETABLE': - // Parsing CREATE TABLE query: - $result = $this->parseCREATETABLE($parseString); - break; - case 'CREATEDATABASE': - // Parsing CREATE DATABASE query: - $result = $this->parseCREATEDATABASE($parseString); - break; - case 'TRUNCATETABLE': - // Parsing TRUNCATE TABLE query: - $result = $this->parseTRUNCATETABLE($parseString); - break; - default: - $result = $this->parseError('"' . $keyword . '" is not a keyword', $parseString); - } - return $result; - } - - /** - * Parsing SELECT query - * - * @param string $parseString SQL string with SELECT query to parse - * @param array $parameterReferences Array holding references to either named (:name) or question mark (?) parameters found - * @return mixed Returns array with components of SELECT query on success, otherwise an error message string. - * @see compileSELECT() - */ - protected function parseSELECT($parseString, &$parameterReferences = NULL) { - // Removing SELECT: - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr($parseString, 6)); - // Init output variable: - $result = array(); - if ($parameterReferences === NULL) { - $result['parameters'] = array(); - $parameterReferences = &$result['parameters']; - } - $result['type'] = 'SELECT'; - // Looking for STRAIGHT_JOIN keyword: - $result['STRAIGHT_JOIN'] = $this->nextPart($parseString, '^(STRAIGHT_JOIN)[[:space:]]+'); - // Select fields: - $result['SELECT'] = $this->parseFieldList($parseString, '^(FROM)[[:space:]]+'); - if ($this->parse_error) { - return $this->parse_error; - } - // Continue if string is not ended: - if ($parseString) { - // Get table list: - $result['FROM'] = $this->parseFromTables($parseString, '^(WHERE)[[:space:]]+'); - if ($this->parse_error) { - return $this->parse_error; - } - // If there are more than just the tables (a WHERE clause that would be...) - if ($parseString) { - // Get WHERE clause: - $result['WHERE'] = $this->parseWhereClause($parseString, '^((GROUP|ORDER)[[:space:]]+BY|LIMIT)[[:space:]]+', $parameterReferences); - if ($this->parse_error) { - return $this->parse_error; - } - // If the WHERE clause parsing was stopped by GROUP BY, ORDER BY or LIMIT, then proceed with parsing: - if ($this->lastStopKeyWord) { - // GROUP BY parsing: - if ($this->lastStopKeyWord === 'GROUPBY') { - $result['GROUPBY'] = $this->parseFieldList($parseString, '^(ORDER[[:space:]]+BY|LIMIT)[[:space:]]+'); - if ($this->parse_error) { - return $this->parse_error; - } - } - // ORDER BY parsing: - if ($this->lastStopKeyWord === 'ORDERBY') { - $result['ORDERBY'] = $this->parseFieldList($parseString, '^(LIMIT)[[:space:]]+'); - if ($this->parse_error) { - return $this->parse_error; - } - } - // LIMIT parsing: - if ($this->lastStopKeyWord === 'LIMIT') { - if (preg_match('/^([0-9]+|[0-9]+[[:space:]]*,[[:space:]]*[0-9]+)$/', trim($parseString))) { - $result['LIMIT'] = $parseString; - } else { - return $this->parseError('No value for limit!', $parseString); - } - } - } - } - } else { - return $this->parseError('No table to select from!', $parseString); - } - // Store current parseString in the result array for possible further processing (e.g., subquery support by DBAL) - $result['parseString'] = $parseString; - // Return result: - return $result; - } - - /** - * Parsing UPDATE query - * - * @param string $parseString SQL string with UPDATE query to parse - * @return mixed Returns array with components of UPDATE query on success, otherwise an error message string. - * @see compileUPDATE() - */ - protected function parseUPDATE($parseString) { - // Removing UPDATE - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr($parseString, 6)); - // Init output variable: - $result = array(); - $result['type'] = 'UPDATE'; - // Get table: - $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); - // Continue if string is not ended: - if ($result['TABLE']) { - if ($parseString && $this->nextPart($parseString, '^(SET)[[:space:]]+')) { - $comma = TRUE; - // Get field/value pairs: - while ($comma) { - if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]*=')) { - // Strip off "=" sign. - $this->nextPart($parseString, '^(=)'); - $value = $this->getValue($parseString); - $result['FIELDS'][$fieldName] = $value; - } else { - return $this->parseError('No fieldname found', $parseString); - } - $comma = $this->nextPart($parseString, '^(,)'); - } - // WHERE - if ($this->nextPart($parseString, '^(WHERE)')) { - $result['WHERE'] = $this->parseWhereClause($parseString); - if ($this->parse_error) { - return $this->parse_error; - } - } - } else { - return $this->parseError('Query missing SET...', $parseString); - } - } else { - return $this->parseError('No table found!', $parseString); - } - // Should be no more content now: - if ($parseString) { - return $this->parseError('Still content in clause after parsing!', $parseString); - } - // Return result: - return $result; - } - - /** - * Parsing INSERT query - * - * @param string $parseString SQL string with INSERT query to parse - * @return mixed Returns array with components of INSERT query on success, otherwise an error message string. - * @see compileINSERT() - */ - protected function parseINSERT($parseString) { - // Removing INSERT - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 4)); - // Init output variable: - $result = array(); - $result['type'] = 'INSERT'; - // Get table: - $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)([[:space:]]+|\\()'); - if ($result['TABLE']) { - // In this case there are no field names mentioned in the SQL! - if ($this->nextPart($parseString, '^(VALUES)([[:space:]]+|\\()')) { - // Get values/fieldnames (depending...) - $result['VALUES_ONLY'] = $this->getValue($parseString, 'IN'); - if ($this->parse_error) { - return $this->parse_error; - } - if (preg_match('/^,/', $parseString)) { - $result['VALUES_ONLY'] = array($result['VALUES_ONLY']); - $result['EXTENDED'] = '1'; - while ($this->nextPart($parseString, '^(,)') === ',') { - $result['VALUES_ONLY'][] = $this->getValue($parseString, 'IN'); - if ($this->parse_error) { - return $this->parse_error; - } - } - } - } else { - // There are apparently fieldnames listed: - $fieldNames = $this->getValue($parseString, '_LIST'); - if ($this->parse_error) { - return $this->parse_error; - } - // "VALUES" keyword binds the fieldnames to values: - if ($this->nextPart($parseString, '^(VALUES)([[:space:]]+|\\()')) { - $result['FIELDS'] = array(); - do { - // Using the "getValue" function to get the field list... - $values = $this->getValue($parseString, 'IN'); - if ($this->parse_error) { - return $this->parse_error; - } - $insertValues = array(); - foreach ($fieldNames as $k => $fN) { - if (preg_match('/^[[:alnum:]_]+$/', $fN)) { - if (isset($values[$k])) { - if (!isset($insertValues[$fN])) { - $insertValues[$fN] = $values[$k]; - } else { - return $this->parseError('Fieldname ("' . $fN . '") already found in list!', $parseString); - } - } else { - return $this->parseError('No value set!', $parseString); - } - } else { - return $this->parseError('Invalid fieldname ("' . $fN . '")', $parseString); - } - } - if (isset($values[$k + 1])) { - return $this->parseError('Too many values in list!', $parseString); - } - $result['FIELDS'][] = $insertValues; - } while ($this->nextPart($parseString, '^(,)') === ','); - if (count($result['FIELDS']) === 1) { - $result['FIELDS'] = $result['FIELDS'][0]; - } else { - $result['EXTENDED'] = '1'; - } - } else { - return $this->parseError('VALUES keyword expected', $parseString); - } - } - } else { - return $this->parseError('No table found!', $parseString); - } - // Should be no more content now: - if ($parseString) { - return $this->parseError('Still content after parsing!', $parseString); - } - // Return result - return $result; - } - - /** - * Parsing DELETE query - * - * @param string $parseString SQL string with DELETE query to parse - * @return mixed Returns array with components of DELETE query on success, otherwise an error message string. - * @see compileDELETE() - */ - protected function parseDELETE($parseString) { - // Removing DELETE - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 4)); - // Init output variable: - $result = array(); - $result['type'] = 'DELETE'; - // Get table: - $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); - if ($result['TABLE']) { - // WHERE - if ($this->nextPart($parseString, '^(WHERE)')) { - $result['WHERE'] = $this->parseWhereClause($parseString); - if ($this->parse_error) { - return $this->parse_error; - } - } - } else { - return $this->parseError('No table found!', $parseString); - } - // Should be no more content now: - if ($parseString) { - return $this->parseError('Still content in clause after parsing!', $parseString); - } - // Return result: - return $result; - } - - /** - * Parsing EXPLAIN query - * - * @param string $parseString SQL string with EXPLAIN query to parse - * @return mixed Returns array with components of EXPLAIN query on success, otherwise an error message string. - * @see parseSELECT() - */ - protected function parseEXPLAIN($parseString) { - // Removing EXPLAIN - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr($parseString, 6)); - // Init output variable: - $result = $this->parseSELECT($parseString); - if (is_array($result)) { - $result['type'] = 'EXPLAIN'; - } - return $result; - } - - /** - * Parsing CREATE TABLE query - * - * @param string $parseString SQL string starting with CREATE TABLE - * @return mixed Returns array with components of CREATE TABLE query on success, otherwise an error message string. - * @see compileCREATETABLE() - */ - protected function parseCREATETABLE($parseString) { - // Removing CREATE TABLE - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 5)); - // Init output variable: - $result = array(); - $result['type'] = 'CREATETABLE'; - // Get table: - $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]*\\(', TRUE); - if ($result['TABLE']) { - // While the parseString is not yet empty: - while ($parseString !== '') { - // Getting key - if ($key = $this->nextPart($parseString, '^(KEY|PRIMARY KEY|UNIQUE KEY|UNIQUE)([[:space:]]+|\\()')) { - $key = $this->normalizeKeyword($key); - switch ($key) { - case 'PRIMARYKEY': - $result['KEYS']['PRIMARYKEY'] = $this->getValue($parseString, '_LIST'); - if ($this->parse_error) { - return $this->parse_error; - } - break; - case 'UNIQUE': - - case 'UNIQUEKEY': - if ($keyName = $this->nextPart($parseString, '^([[:alnum:]_]+)([[:space:]]+|\\()')) { - $result['KEYS']['UNIQUE'] = array($keyName => $this->getValue($parseString, '_LIST')); - if ($this->parse_error) { - return $this->parse_error; - } - } else { - return $this->parseError('No keyname found', $parseString); - } - break; - case 'KEY': - if ($keyName = $this->nextPart($parseString, '^([[:alnum:]_]+)([[:space:]]+|\\()')) { - $result['KEYS'][$keyName] = $this->getValue($parseString, '_LIST', 'INDEX'); - if ($this->parse_error) { - return $this->parse_error; - } - } else { - return $this->parseError('No keyname found', $parseString); - } - break; - } - } elseif ($fieldName = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+')) { - // Getting field: - $result['FIELDS'][$fieldName]['definition'] = $this->parseFieldDef($parseString); - if ($this->parse_error) { - return $this->parse_error; - } - } - // Finding delimiter: - $delim = $this->nextPart($parseString, '^(,|\\))'); - if (!$delim) { - return $this->parseError('No delimiter found', $parseString); - } elseif ($delim === ')') { - break; - } - } - // Finding what is after the table definition - table type in MySQL - if ($delim === ')') { - if ($this->nextPart($parseString, '^((ENGINE|TYPE)[[:space:]]*=)')) { - $result['engine'] = $parseString; - $parseString = ''; - } - } else { - return $this->parseError('No fieldname found!', $parseString); - } - } else { - return $this->parseError('No table found!', $parseString); - } - // Should be no more content now: - if ($parseString) { - return $this->parseError('Still content in clause after parsing!', $parseString); - } - return $result; - } - - /** - * Parsing ALTER TABLE query - * - * @param string $parseString SQL string starting with ALTER TABLE - * @return mixed Returns array with components of ALTER TABLE query on success, otherwise an error message string. - * @see compileALTERTABLE() - */ - protected function parseALTERTABLE($parseString) { - // Removing ALTER TABLE - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr(ltrim(substr($parseString, 5)), 5)); - // Init output variable: - $result = array(); - $result['type'] = 'ALTERTABLE'; - // Get table: - $hasBackquote = $this->nextPart($parseString, '^(`)') === '`'; - $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)' . ($hasBackquote ? '`' : '') . '[[:space:]]+'); - if ($hasBackquote && $this->nextPart($parseString, '^(`)') !== '`') { - return $this->parseError('No end backquote found!', $parseString); - } - if ($result['TABLE']) { - if ($result['action'] = $this->nextPart($parseString, '^(CHANGE|DROP[[:space:]]+KEY|DROP[[:space:]]+PRIMARY[[:space:]]+KEY|ADD[[:space:]]+KEY|ADD[[:space:]]+PRIMARY[[:space:]]+KEY|ADD[[:space:]]+UNIQUE|DROP|ADD|RENAME|DEFAULT[[:space:]]+CHARACTER[[:space:]]+SET|ENGINE)([[:space:]]+|\\(|=)')) { - $actionKey = $this->normalizeKeyword($result['action']); - // Getting field: - if (\TYPO3\CMS\Core\Utility\GeneralUtility::inList('ADDPRIMARYKEY,DROPPRIMARYKEY,ENGINE', $actionKey) || ($fieldKey = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'))) { - switch ($actionKey) { - case 'ADD': - $result['FIELD'] = $fieldKey; - $result['definition'] = $this->parseFieldDef($parseString); - if ($this->parse_error) { - return $this->parse_error; - } - break; - case 'DROP': - case 'RENAME': - $result['FIELD'] = $fieldKey; - break; - case 'CHANGE': - $result['FIELD'] = $fieldKey; - if ($result['newField'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+')) { - $result['definition'] = $this->parseFieldDef($parseString); - if ($this->parse_error) { - return $this->parse_error; - } - } else { - return $this->parseError('No NEW field name found', $parseString); - } - break; - case 'ADDKEY': - case 'ADDPRIMARYKEY': - case 'ADDUNIQUE': - $result['KEY'] = $fieldKey; - $result['fields'] = $this->getValue($parseString, '_LIST', 'INDEX'); - if ($this->parse_error) { - return $this->parse_error; - } - break; - case 'DROPKEY': - $result['KEY'] = $fieldKey; - break; - case 'DROPPRIMARYKEY': - // @todo ??? - break; - case 'DEFAULTCHARACTERSET': - $result['charset'] = $fieldKey; - break; - case 'ENGINE': - $result['engine'] = $this->nextPart($parseString, '^=[[:space:]]*([[:alnum:]]+)[[:space:]]+', TRUE); - break; - } - } else { - return $this->parseError('No field name found', $parseString); - } - } else { - return $this->parseError('No action CHANGE, DROP or ADD found!', $parseString); - } - } else { - return $this->parseError('No table found!', $parseString); - } - // Should be no more content now: - if ($parseString) { - return $this->parseError('Still content in clause after parsing!', $parseString); - } - return $result; - } - - /** - * Parsing DROP TABLE query - * - * @param string $parseString SQL string starting with DROP TABLE - * @return mixed Returns array with components of DROP TABLE query on success, otherwise an error message string. - */ - protected function parseDROPTABLE($parseString) { - // Removing DROP TABLE - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr(ltrim(substr($parseString, 4)), 5)); - // Init output variable: - $result = array(); - $result['type'] = 'DROPTABLE'; - // IF EXISTS - $result['ifExists'] = $this->nextPart($parseString, '^(IF[[:space:]]+EXISTS[[:space:]]+)'); - // Get table: - $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); - if ($result['TABLE']) { - // Should be no more content now: - if ($parseString) { - return $this->parseError('Still content in clause after parsing!', $parseString); - } - return $result; - } else { - return $this->parseError('No table found!', $parseString); - } - } - - /** - * Parsing CREATE DATABASE query - * - * @param string $parseString SQL string starting with CREATE DATABASE - * @return mixed Returns array with components of CREATE DATABASE query on success, otherwise an error message string. - */ - protected function parseCREATEDATABASE($parseString) { - // Removing CREATE DATABASE - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 8)); - // Init output variable: - $result = array(); - $result['type'] = 'CREATEDATABASE'; - // Get table: - $result['DATABASE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); - if ($result['DATABASE']) { - // Should be no more content now: - if ($parseString) { - return $this->parseError('Still content in clause after parsing!', $parseString); - } - return $result; - } else { - return $this->parseError('No database found!', $parseString); - } - } - - /** - * Parsing TRUNCATE TABLE query - * - * @param string $parseString SQL string starting with TRUNCATE TABLE - * @return mixed Returns array with components of TRUNCATE TABLE query on success, otherwise an error message string. - */ - protected function parseTRUNCATETABLE($parseString) { - // Removing TRUNCATE TABLE - $parseString = $this->trimSQL($parseString); - $parseString = ltrim(substr(ltrim(substr($parseString, 8)), 5)); - // Init output variable: - $result = array(); - $result['type'] = 'TRUNCATETABLE'; - // Get table: - $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); - if ($result['TABLE']) { - // Should be no more content now: - if ($parseString) { - return $this->parseError('Still content in clause after parsing!', $parseString); - } - return $result; - } else { - return $this->parseError('No table found!', $parseString); - } - } - - /************************************** - * - * SQL Parsing, helper functions for parts of queries - * - **************************************/ - /** - * Parsing the fields in the "SELECT [$selectFields] FROM" part of a query into an array. - * The output from this function can be compiled back into a field list with ->compileFieldList() - * Will detect the keywords "DESC" and "ASC" after the table name; thus is can be used for parsing the more simply ORDER BY and GROUP BY field lists as well! - * - * @param string $parseString The string with fieldnames, eg. "title, uid AS myUid, max(tstamp), count(*)" etc. NOTICE: passed by reference! - * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(FROM)([[:space:]]*)' - * @return array If successful parsing, returns an array, otherwise an error string. - * @see compileFieldList() - */ - public function parseFieldList(&$parseString, $stopRegex = '') { - $stack = array(); - // Contains the parsed content - if ($parseString === '') { - return $stack; - } - // @todo - should never happen, why does it? - // Pointer to positions in $stack - $pnt = 0; - // Indicates the parenthesis level we are at. - $level = 0; - // Recursivity brake. - $loopExit = 0; - // Prepare variables: - $parseString = $this->trimSQL($parseString); - $this->lastStopKeyWord = ''; - $this->parse_error = ''; - // Parse any SQL hint / comments - $stack[$pnt]['comments'] = $this->nextPart($parseString, '^(\\/\\*.*\\*\\/)'); - // $parseString is continuously shortened by the process and we keep parsing it till it is zero: - while ($parseString !== '') { - // Checking if we are inside / outside parenthesis (in case of a function like count(), max(), min() etc...): - // Inside parenthesis here (does NOT detect if values in quotes are used, the only token is ")" or "("): - if ($level > 0) { - // Accumulate function content until next () parenthesis: - $funcContent = $this->nextPart($parseString, '^([^()]*.)'); - $stack[$pnt]['func_content.'][] = array( - 'level' => $level, - 'func_content' => substr($funcContent, 0, -1) - ); - $stack[$pnt]['func_content'] .= $funcContent; - // Detecting ( or ) - switch (substr($stack[$pnt]['func_content'], -1)) { - case '(': - $level++; - break; - case ')': - $level--; - // If this was the last parenthesis: - if (!$level) { - $stack[$pnt]['func_content'] = substr($stack[$pnt]['func_content'], 0, -1); - // Remove any whitespace after the parenthesis. - $parseString = ltrim($parseString); - } - break; - } - } else { - // Outside parenthesis, looking for next field: - // Looking for a flow-control construct (only known constructs supported) - if (preg_match('/^case([[:space:]][[:alnum:]\\*._]+)?[[:space:]]when/i', $parseString)) { - $stack[$pnt]['type'] = 'flow-control'; - $stack[$pnt]['flow-control'] = $this->parseCaseStatement($parseString); - // Looking for "AS" alias: - if ($as = $this->nextPart($parseString, '^(AS)[[:space:]]+')) { - $stack[$pnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)(,|[[:space:]]+)'); - $stack[$pnt]['as_keyword'] = $as; - } - } else { - // Looking for a known function (only known functions supported) - $func = $this->nextPart($parseString, '^(count|max|min|floor|sum|avg)[[:space:]]*\\('); - if ($func) { - // Strip off "(" - $parseString = trim(substr($parseString, 1)); - $stack[$pnt]['type'] = 'function'; - $stack[$pnt]['function'] = $func; - // increse parenthesis level counter. - $level++; - } else { - $stack[$pnt]['distinct'] = $this->nextPart($parseString, '^(distinct[[:space:]]+)'); - // Otherwise, look for regular fieldname: - if (($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)(,|[[:space:]]+)')) !== '') { - $stack[$pnt]['type'] = 'field'; - // Explode fieldname into field and table: - $tableField = explode('.', $fieldName, 2); - if (count($tableField) === 2) { - $stack[$pnt]['table'] = $tableField[0]; - $stack[$pnt]['field'] = $tableField[1]; - } else { - $stack[$pnt]['table'] = ''; - $stack[$pnt]['field'] = $tableField[0]; - } - } else { - return $this->parseError('No field name found as expected in parseFieldList()', $parseString); - } - } - } - } - // After a function or field we look for "AS" alias and a comma to separate to the next field in the list: - if (!$level) { - // Looking for "AS" alias: - if ($as = $this->nextPart($parseString, '^(AS)[[:space:]]+')) { - $stack[$pnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)(,|[[:space:]]+)'); - $stack[$pnt]['as_keyword'] = $as; - } - // Looking for "ASC" or "DESC" keywords (for ORDER BY) - if ($sDir = $this->nextPart($parseString, '^(ASC|DESC)([[:space:]]+|,)')) { - $stack[$pnt]['sortDir'] = $sDir; - } - // Looking for stop-keywords: - if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) { - $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord); - return $stack; - } - // Looking for comma (since the stop-keyword did not trigger a return...) - if ($parseString !== '' && !$this->nextPart($parseString, '^(,)')) { - return $this->parseError('No comma found as expected in parseFieldList()', $parseString); - } - // Increasing pointer: - $pnt++; - } - // Check recursivity brake: - $loopExit++; - if ($loopExit > 500) { - return $this->parseError('More than 500 loops, exiting prematurely in parseFieldList()...', $parseString); - } - } - // Return result array: - return $stack; - } - - /** - * Parsing a CASE ... WHEN flow-control construct. - * The output from this function can be compiled back with ->compileCaseStatement() - * - * @param string $parseString The string with the CASE ... WHEN construct, eg. "CASE field WHEN 1 THEN 0 ELSE ..." etc. NOTICE: passed by reference! - * @return array If successful parsing, returns an array, otherwise an error string. - * @see compileCaseConstruct() - */ - protected function parseCaseStatement(&$parseString) { - $result = array(); - $result['type'] = $this->nextPart($parseString, '^(case)[[:space:]]+'); - if (!preg_match('/^when[[:space:]]+/i', $parseString)) { - $value = $this->getValue($parseString); - if (!(isset($value[1]) || is_numeric($value[0]))) { - $result['case_field'] = $value[0]; - } else { - $result['case_value'] = $value; - } - } - $result['when'] = array(); - while ($this->nextPart($parseString, '^(when)[[:space:]]')) { - $when = array(); - $when['when_value'] = $this->parseWhereClause($parseString, '^(then)[[:space:]]+'); - $when['then_value'] = $this->getValue($parseString); - $result['when'][] = $when; - } - if ($this->nextPart($parseString, '^(else)[[:space:]]+')) { - $result['else'] = $this->getValue($parseString); - } - if (!$this->nextPart($parseString, '^(end)[[:space:]]+')) { - return $this->parseError('No "end" keyword found as expected in parseCaseStatement()', $parseString); - } - return $result; - } - - /** - * Parsing a CAST definition in the "JOIN [$parseString] ..." part of a query into an array. - * The success of this parsing determines if that part of the query is supported by TYPO3. - * - * @param string $parseString JOIN clause to parse. NOTICE: passed by reference! - * @return mixed If successful parsing, returns an array, otherwise an error string. - */ - protected function parseCastStatement(&$parseString) { - $this->nextPart($parseString, '^(CAST)[[:space:]]*'); - $parseString = trim(substr($parseString, 1)); - $castDefinition = array('type' => 'cast'); - // Strip off "(" - if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) { - // Parse field name into field and table: - $tableField = explode('.', $fieldName, 2); - if (count($tableField) === 2) { - $castDefinition['table'] = $tableField[0]; - $castDefinition['field'] = $tableField[1]; - } else { - $castDefinition['table'] = ''; - $castDefinition['field'] = $tableField[0]; - } - } else { - return $this->parseError('No casted join field found in parseCastStatement()!', $parseString); - } - if ($this->nextPart($parseString, '^([[:space:]]*AS[[:space:]]*)')) { - $castDefinition['datatype'] = $this->getValue($parseString); - } - if (!$this->nextPart($parseString, '^([)])')) { - return $this->parseError('No end parenthesis at end of CAST function', $parseString); - } - return $castDefinition; - } - - /** - * Parsing the tablenames in the "FROM [$parseString] WHERE" part of a query into an array. - * The success of this parsing determines if that part of the query is supported by TYPO3. - * - * @param string $parseString List of tables, eg. "pages, tt_content" or "pages A, pages B". NOTICE: passed by reference! - * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(WHERE)([[:space:]]*)' - * @return array If successful parsing, returns an array, otherwise an error string. - * @see compileFromTables() - */ - public function parseFromTables(&$parseString, $stopRegex = '') { - // Prepare variables: - $parseString = $this->trimSQL($parseString); - $this->lastStopKeyWord = ''; - $this->parse_error = ''; - // Contains the parsed content - $stack = array(); - // Pointer to positions in $stack - $pnt = 0; - // Recursivity brake. - $loopExit = 0; - // $parseString is continously shortend by the process and we keep parsing it till it is zero: - while ($parseString !== '') { - // Looking for the table: - if ($stack[$pnt]['table'] = $this->nextPart($parseString, '^([[:alnum:]_]+)(,|[[:space:]]+)')) { - // Looking for stop-keywords before fetching potential table alias: - if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) { - $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord); - return $stack; - } - if (!preg_match('/^(LEFT|RIGHT|JOIN|INNER)[[:space:]]+/i', $parseString)) { - $stack[$pnt]['as_keyword'] = $this->nextPart($parseString, '^(AS[[:space:]]+)'); - $stack[$pnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]*'); - } - } else { - return $this->parseError('No table name found as expected in parseFromTables()!', $parseString); - } - // Looking for JOIN - $joinCnt = 0; - while ($join = $this->nextPart($parseString, '^(((INNER|(LEFT|RIGHT)([[:space:]]+OUTER)?)[[:space:]]+)?JOIN)[[:space:]]+')) { - $stack[$pnt]['JOIN'][$joinCnt]['type'] = $join; - if ($stack[$pnt]['JOIN'][$joinCnt]['withTable'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+', 1)) { - if (!preg_match('/^ON[[:space:]]+/i', $parseString)) { - $stack[$pnt]['JOIN'][$joinCnt]['as_keyword'] = $this->nextPart($parseString, '^(AS[[:space:]]+)'); - $stack[$pnt]['JOIN'][$joinCnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); - } - if (!$this->nextPart($parseString, '^(ON[[:space:]]+)')) { - return $this->parseError('No join condition found in parseFromTables()!', $parseString); - } - $stack[$pnt]['JOIN'][$joinCnt]['ON'] = array(); - $condition = array('operator' => ''); - $parseCondition = TRUE; - while ($parseCondition) { - if (($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)[[:space:]]*(<=|>=|<|>|=|!=)')) !== '') { - // Parse field name into field and table: - $tableField = explode('.', $fieldName, 2); - $condition['left'] = array(); - if (count($tableField) === 2) { - $condition['left']['table'] = $tableField[0]; - $condition['left']['field'] = $tableField[1]; - } else { - $condition['left']['table'] = ''; - $condition['left']['field'] = $tableField[0]; - } - } elseif (preg_match('/^CAST[[:space:]]*[(]/i', $parseString)) { - $condition['left'] = $this->parseCastStatement($parseString); - // Return the parse error - if (!is_array($condition['left'])) { - return $condition['left']; - } - } else { - return $this->parseError('No join field found in parseFromTables()!', $parseString); - } - // Find "comparator": - $condition['comparator'] = $this->nextPart($parseString, '^(<=|>=|<|>|=|!=)'); - if (preg_match('/^CAST[[:space:]]*[(]/i', $parseString)) { - $condition['right'] = $this->parseCastStatement($parseString); - // Return the parse error - if (!is_array($condition['right'])) { - return $condition['right']; - } - } elseif (($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)')) !== '') { - // Parse field name into field and table: - $tableField = explode('.', $fieldName, 2); - $condition['right'] = array(); - if (count($tableField) === 2) { - $condition['right']['table'] = $tableField[0]; - $condition['right']['field'] = $tableField[1]; - } else { - $condition['right']['table'] = ''; - $condition['right']['field'] = $tableField[0]; - } - } elseif ($value = $this->getValue($parseString)) { - $condition['right']['value'] = $value; - } else { - return $this->parseError('No join field found in parseFromTables()!', $parseString); - } - $stack[$pnt]['JOIN'][$joinCnt]['ON'][] = $condition; - if (($operator = $this->nextPart($parseString, '^(AND|OR)')) !== '') { - $condition = array('operator' => $operator); - } else { - $parseCondition = FALSE; - } - } - $joinCnt++; - } else { - return $this->parseError('No join table found in parseFromTables()!', $parseString); - } - } - // Looking for stop-keywords: - if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) { - $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord); - return $stack; - } - // Looking for comma: - if ($parseString !== '' && !$this->nextPart($parseString, '^(,)')) { - return $this->parseError('No comma found as expected in parseFromTables()', $parseString); - } - // Increasing pointer: - $pnt++; - // Check recursivity brake: - $loopExit++; - if ($loopExit > 500) { - return $this->parseError('More than 500 loops, exiting prematurely in parseFromTables()...', $parseString); - } - } - // Return result array: - return $stack; - } - - /** - * Parsing the WHERE clause fields in the "WHERE [$parseString] ..." part of a query into a multidimensional array. - * The success of this parsing determines if that part of the query is supported by TYPO3. - * - * @param string $parseString WHERE clause to parse. NOTICE: passed by reference! - * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(GROUP BY|ORDER BY|LIMIT)([[:space:]]*)' - * @param array $parameterReferences Array holding references to either named (:name) or question mark (?) parameters found - * @return mixed If successful parsing, returns an array, otherwise an error string. - */ - public function parseWhereClause(&$parseString, $stopRegex = '', array &$parameterReferences = array()) { - // Prepare variables: - $parseString = $this->trimSQL($parseString); - $this->lastStopKeyWord = ''; - $this->parse_error = ''; - // Contains the parsed content - $stack = array(0 => array()); - // Pointer to positions in $stack - $pnt = array(0 => 0); - // Determines parenthesis level - $level = 0; - // Recursivity brake. - $loopExit = 0; - // $parseString is continuously shortened by the process and we keep parsing it till it is zero: - while ($parseString !== '') { - // Look for next parenthesis level: - $newLevel = $this->nextPart($parseString, '^([(])'); - // If new level is started, manage stack/pointers: - if ($newLevel === '(') { - // Increase level - $level++; - // Reset pointer for this level - $pnt[$level] = 0; - // Reset stack for this level - $stack[$level] = array(); - } else { - // If no new level is started, just parse the current level: - // Find "modifier", eg. "NOT or !" - $stack[$level][$pnt[$level]]['modifier'] = trim($this->nextPart($parseString, '^(!|NOT[[:space:]]+)')); - // See if condition is EXISTS with a subquery - if (preg_match('/^EXISTS[[:space:]]*[(]/i', $parseString)) { - $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(EXISTS)[[:space:]]*'); - // Strip off "(" - $parseString = trim(substr($parseString, 1)); - $stack[$level][$pnt[$level]]['func']['subquery'] = $this->parseSELECT($parseString, $parameterReferences); - // 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 { - // See if LOCATE function is found - if (preg_match('/^LOCATE[[:space:]]*[(]/i', $parseString)) { - $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(LOCATE)[[:space:]]*'); - // Strip off "(" - $parseString = trim(substr($parseString, 1)); - $stack[$level][$pnt[$level]]['func']['substr'] = $this->getValue($parseString); - if (!$this->nextPart($parseString, '^(,)')) { - return $this->parseError('No comma found as expected in parseWhereClause()', $parseString); - } - if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) { - // Parse field name into field and table: - $tableField = explode('.', $fieldName, 2); - if (count($tableField) === 2) { - $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0]; - $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1]; - } else { - $stack[$level][$pnt[$level]]['func']['table'] = ''; - $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0]; - } - } else { - return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); - } - if ($this->nextPart($parseString, '^(,)')) { - $stack[$level][$pnt[$level]]['func']['pos'] = $this->getValue($parseString); - } - if (!$this->nextPart($parseString, '^([)])')) { - return $this->parseError('No ) parenthesis at end of function', $parseString); - } - } elseif (preg_match('/^IFNULL[[:space:]]*[(]/i', $parseString)) { - $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(IFNULL)[[:space:]]*'); - $parseString = trim(substr($parseString, 1)); - // Strip off "(" - if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) { - // Parse field name into field and table: - $tableField = explode('.', $fieldName, 2); - if (count($tableField) === 2) { - $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0]; - $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1]; - } else { - $stack[$level][$pnt[$level]]['func']['table'] = ''; - $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0]; - } - } else { - return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); - } - if ($this->nextPart($parseString, '^(,)')) { - $stack[$level][$pnt[$level]]['func']['default'] = $this->getValue($parseString); - } - if (!$this->nextPart($parseString, '^([)])')) { - return $this->parseError('No ) parenthesis at end of function', $parseString); - } - } elseif (preg_match('/^CAST[[:space:]]*[(]/i', $parseString)) { - $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(CAST)[[:space:]]*'); - $parseString = trim(substr($parseString, 1)); - // Strip off "(" - if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) { - // Parse field name into field and table: - $tableField = explode('.', $fieldName, 2); - if (count($tableField) === 2) { - $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0]; - $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1]; - } else { - $stack[$level][$pnt[$level]]['func']['table'] = ''; - $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0]; - } - } else { - return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); - } - if ($this->nextPart($parseString, '^([[:space:]]*AS[[:space:]]*)')) { - $stack[$level][$pnt[$level]]['func']['datatype'] = $this->getValue($parseString); - } - if (!$this->nextPart($parseString, '^([)])')) { - return $this->parseError('No ) parenthesis at end of function', $parseString); - } - } elseif (preg_match('/^FIND_IN_SET[[:space:]]*[(]/i', $parseString)) { - $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(FIND_IN_SET)[[:space:]]*'); - // Strip off "(" - $parseString = trim(substr($parseString, 1)); - if ($str = $this->getValue($parseString)) { - $stack[$level][$pnt[$level]]['func']['str'] = $str; - if ($fieldName = $this->nextPart($parseString, '^,[[:space:]]*([[:alnum:]._]+)[[:space:]]*', TRUE)) { - // Parse field name into field and table: - $tableField = explode('.', $fieldName, 2); - if (count($tableField) === 2) { - $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0]; - $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1]; - } else { - $stack[$level][$pnt[$level]]['func']['table'] = ''; - $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0]; - } - } else { - return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); - } - if (!$this->nextPart($parseString, '^([)])')) { - return $this->parseError('No ) parenthesis at end of function', $parseString); - } - } else { - return $this->parseError('No item to look for found as expected in parseWhereClause()', $parseString); - } - } else { - // 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]]['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 ((string)$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) { - $stack[$level][$pnt[$level]]['calc_table'] = $tableField[0]; - $stack[$level][$pnt[$level]]['calc_field'] = $tableField[1]; - } else { - $stack[$level][$pnt[$level]]['calc_table'] = ''; - $stack[$level][$pnt[$level]]['calc_field'] = $tableField[0]; - } - } - } - } - $stack[$level][$pnt[$level]]['comparator'] = $this->nextPart($parseString, '^(' . implode('|', self::$comparatorPatterns) . ')'); - if ($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 (\TYPO3\CMS\Core\Utility\GeneralUtility::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, $parameterReferences); - // Seek to new position in parseString after parsing of the subquery - if (!empty($stack[$level][$pnt[$level]]['subquery']['parseString'])) { - $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 { - if (\TYPO3\CMS\Core\Utility\GeneralUtility::inList('BETWEEN,NOT BETWEEN', $stack[$level][$pnt[$level]]['comparator'])) { - $stack[$level][$pnt[$level]]['values'] = array(); - $stack[$level][$pnt[$level]]['values'][0] = $this->getValue($parseString); - if (!$this->nextPart($parseString, '^(AND)')) { - return $this->parseError('No AND operator found as expected in parseWhereClause()', $parseString); - } - $stack[$level][$pnt[$level]]['values'][1] = $this->getValue($parseString); - } else { - // Finding value for comparator: - $stack[$level][$pnt[$level]]['value'] = &$this->getValueOrParameter($parseString, $stack[$level][$pnt[$level]]['comparator'], '', $parameterReferences); - if ($this->parse_error) { - return $this->parse_error; - } - } - } - } - } - } - // Finished, increase pointer: - $pnt[$level]++; - // Checking if we are back to level 0 and we should still decrease level, - // meaning we were probably parsing as subquery and should return here: - if ($level === 0 && preg_match('/^[)]/', $parseString)) { - // Return the stacks lowest level: - 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: - // Copy stack - $stack[$level][$pnt[$level]]['sub'] = $stack[$level + 1]; - // Increase pointer of the new level - $pnt[$level]++; - // Make recursivity check: - $loopExit++; - if ($loopExit > 500) { - return $this->parseError('More than 500 loops (in search for exit parenthesis), exiting prematurely in parseWhereClause()...', $parseString); - } - } - // Detecting the operator for the next level: - $op = $this->nextPart($parseString, '^(AND[[:space:]]+NOT|&&[[:space:]]+NOT|OR[[:space:]]+NOT|OR[[:space:]]+NOT|\\|\\|[[:space:]]+NOT|AND|&&|OR|\\|\\|)(\\(|[[:space:]]+)'); - if ($op) { - // Normalize boolean operator - $op = str_replace(array('&&', '||'), array('AND', 'OR'), $op); - $stack[$level][$pnt[$level]]['operator'] = $op; - } elseif ($parseString !== '') { - // Looking for stop-keywords: - if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) { - $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord); - return $stack[0]; - } else { - return $this->parseError('No operator, but parsing not finished in parseWhereClause().', $parseString); - } - } - } - // Make recursivity check: - $loopExit++; - if ($loopExit > 500) { - return $this->parseError('More than 500 loops, exiting prematurely in parseWhereClause()...', $parseString); - } - } - // Return the stacks lowest level: - return $stack[0]; - } - - /** - * Parsing the WHERE clause fields in the "WHERE [$parseString] ..." part of a query into a multidimensional array. - * The success of this parsing determines if that part of the query is supported by TYPO3. - * - * @param string $parseString WHERE clause to parse. NOTICE: passed by reference! - * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(GROUP BY|ORDER BY|LIMIT)([[:space:]]*)' - * @return mixed If successful parsing, returns an array, otherwise an error string. - */ - public function parseFieldDef(&$parseString, $stopRegex = '') { - // Prepare variables: - $parseString = $this->trimSQL($parseString); - $this->lastStopKeyWord = ''; - $this->parse_error = ''; - $result = array(); - // Field type: - if ($result['fieldType'] = $this->nextPart($parseString, '^(int|smallint|tinyint|mediumint|bigint|double|numeric|decimal|float|varchar|char|text|tinytext|mediumtext|longtext|blob|tinyblob|mediumblob|longblob)([[:space:],]+|\\()')) { - // Looking for value: - if ($parseString[0] === '(') { - $parseString = substr($parseString, 1); - if ($result['value'] = $this->nextPart($parseString, '^([^)]*)')) { - $parseString = ltrim(substr($parseString, 1)); - } else { - return $this->parseError('No end-parenthesis for value found in parseFieldDef()!', $parseString); - } - } - // Looking for keywords - while ($keyword = $this->nextPart($parseString, '^(DEFAULT|NOT[[:space:]]+NULL|AUTO_INCREMENT|UNSIGNED)([[:space:]]+|,|\\))')) { - $keywordCmp = $this->normalizeKeyword($keyword); - $result['featureIndex'][$keywordCmp]['keyword'] = $keyword; - switch ($keywordCmp) { - case 'DEFAULT': - $result['featureIndex'][$keywordCmp]['value'] = $this->getValue($parseString); - break; - } - } - } else { - return $this->parseError('Field type unknown in parseFieldDef()!', $parseString); - } - return $result; - } - - /************************************ - * - * Parsing: Helper functions - * - ************************************/ - /** - * Strips off a part of the parseString and returns the matching part. - * Helper function for the parsing methods. - * - * @param string $parseString Parse string; if $regex finds anything the value of the first () level will be stripped of the string in the beginning. Further $parseString is left-trimmed (on success). Notice; parsestring is passed by reference. - * @param string $regex Regex to find a matching part in the beginning of the string. Rules: You MUST start the regex with "^" (finding stuff in the beginning of string) and the result of the first parenthesis is what will be returned to you (and stripped of the string). Eg. '^(AND|OR|&&)[[:space:]]+' will return AND, OR or && if found and having one of more whitespaces after it, plus shorten $parseString with that match and any space after (by ltrim()) - * @param bool $trimAll If set the full match of the regex is stripped of the beginning of the string! - * @return string The value of the first parenthesis level of the REGEX. - */ - protected function nextPart(&$parseString, $regex, $trimAll = FALSE) { - $reg = array(); - // Adding space char because [[:space:]]+ is often a requirement in regex's - if (preg_match('/' . $regex . '/i', $parseString . ' ', $reg)) { - $parseString = ltrim(substr($parseString, strlen($reg[$trimAll ? 0 : 1]))); - return $reg[1]; - } - // No match found - return ''; - } - - /** - * Finds value or either named (:name) or question mark (?) parameter markers at the beginning - * of $parseString, returns result and strips it of parseString. - * This method returns a pointer to the parameter or value that was found. In case of a parameter - * the pointer is a reference to the corresponding item in array $parameterReferences. - * - * @param string $parseString The parseString - * @param string $comparator The comparator used before. - * @param string $mode The mode, e.g., "INDEX - * @param mixed The value (string/integer) or parameter (:name/?). Otherwise an array with error message in first key (0) - */ - protected function &getValueOrParameter(&$parseString, $comparator = '', $mode = '', array &$parameterReferences = array()) { - $parameter = $this->nextPart($parseString, '^(\\:[[:alnum:]_]+|\\?)'); - if ($parameter === '?') { - if (!isset($parameterReferences['?'])) { - $parameterReferences['?'] = array(); - } - $value = array('?'); - $parameterReferences['?'][] = &$value; - } elseif ($parameter !== '') { - // named parameter - if (isset($parameterReferences[$parameter])) { - // Use the same reference as last time we encountered this parameter - $value = &$parameterReferences[$parameter]; - } else { - $value = array($parameter); - $parameterReferences[$parameter] = &$value; - } - } else { - $value = $this->getValue($parseString, $comparator, $mode); - } - return $value; - } - - /** - * Finds value in beginning of $parseString, returns result and strips it of parseString - * - * @param string $parseString The parseString, eg. "(0,1,2,3) ..." or "('asdf','qwer') ..." or "1234 ..." or "'My string value here' ... - * @param string $comparator The comparator used before. If "NOT IN" or "IN" then the value is expected to be a list of values. Otherwise just an integer (un-quoted) or string (quoted) - * @param string $mode The mode, eg. "INDEX - * @return mixed The value (string/integer). Otherwise an array with error message in first key (0) - */ - protected function getValue(&$parseString, $comparator = '', $mode = '') { - $value = ''; - if (\TYPO3\CMS\Core\Utility\GeneralUtility::inList('NOTIN,IN,_LIST', strtoupper(str_replace(array(' ', LF, CR, TAB), '', $comparator)))) { - // List of values: - if ($this->nextPart($parseString, '^([(])')) { - $listValues = array(); - $comma = ','; - while ($comma === ',') { - $listValues[] = $this->getValue($parseString); - if ($mode === 'INDEX') { - // Remove any length restriction on INDEX definition - $this->nextPart($parseString, '^([(]\\d+[)])'); - } - $comma = $this->nextPart($parseString, '^([,])'); - } - $out = $this->nextPart($parseString, '^([)])'); - if ($out) { - if ($comparator === '_LIST') { - $kVals = array(); - foreach ($listValues as $vArr) { - $kVals[] = $vArr[0]; - } - return $kVals; - } else { - return $listValues; - } - } else { - return array($this->parseError('No ) parenthesis in list', $parseString)); - } - } else { - return array($this->parseError('No ( parenthesis starting the list', $parseString)); - } - } else { - // Just plain string value, in quotes or not: - // Quote? - $firstChar = $parseString[0]; - switch ($firstChar) { - case '"': - $value = array($this->getValueInQuotes($parseString, '"'), '"'); - break; - case '\'': - $value = array($this->getValueInQuotes($parseString, '\''), '\''); - break; - default: - $reg = array(); - if (preg_match('/^([[:alnum:]._-]+(?:\\([0-9]+\\))?)/i', $parseString, $reg)) { - $parseString = ltrim(substr($parseString, strlen($reg[0]))); - $value = array($reg[1]); - } - } - } - return $value; - } - - /** - * Get value in quotes from $parseString. - * NOTICE: If a query being parsed was prepared for another database than MySQL this function should probably be changed - * - * @param string $parseString String from which to find value in quotes. Notice that $parseString is passed by reference and is shortend by the output of this function. - * @param string $quote The quote used; input either " or ' - * @return string The value, passed through stripslashes() ! - */ - protected function getValueInQuotes(&$parseString, $quote) { - $parts = explode($quote, substr($parseString, 1)); - $buffer = ''; - foreach ($parts as $k => $v) { - $buffer .= $v; - $reg = array(); - preg_match('/\\\\$/', $v, $reg); - if ($reg && strlen($reg[0]) % 2) { - $buffer .= $quote; - } else { - $parseString = ltrim(substr($parseString, strlen($buffer) + 2)); - return $this->parseStripslashes($buffer); - } - } - } - - /** - * Strip slashes function used for parsing - * NOTICE: If a query being parsed was prepared for another database than MySQL this function should probably be changed - * - * @param string $str Input string - * @return string Output string - */ - protected function parseStripslashes($str) { - $search = array('\\\\', '\\\'', '\\"', '\0', '\n', '\r', '\Z'); - $replace = array('\\', '\'', '"', "\x00", "\x0a", "\x0d", "\x1a"); - - return str_replace($search, $replace, $str); - } - - /** - * Add slashes function used for compiling queries - * NOTICE: If a query being parsed was prepared for another database than MySQL this function should probably be changed - * - * @param string $str Input string - * @return string Output string - */ - protected function compileAddslashes($str) { - $search = array('\\', '\'', '"', "\x00", "\x0a", "\x0d", "\x1a"); - $replace = array('\\\\', '\\\'', '\\"', '\0', '\n', '\r', '\Z'); - - return str_replace($search, $replace, $str); - } - - /** - * Setting the internal error message value, $this->parse_error and returns that value. - * - * @param string $msg Input error message - * @param string $restQuery Remaining query to parse. - * @return string Error message. - */ - protected function parseError($msg, $restQuery) { - $this->parse_error = 'SQL engine parse ERROR: ' . $msg . ': near "' . substr($restQuery, 0, 50) . '"'; - return $this->parse_error; - } - - /** - * Trimming SQL as preparation for parsing. - * ";" in the end is stripped off. - * White space is trimmed away around the value - * A single space-char is added in the end - * - * @param string $str Input string - * @return string Output string - */ - protected function trimSQL($str) { - return rtrim(rtrim(trim($str), ';')) . ' '; - } - - /************************* - * - * Compiling queries - * - *************************/ - /** - * Compiles an SQL query from components - * - * @param array $components Array of SQL query components - * @return string SQL query - * @see parseSQL() - */ - public function compileSQL($components) { - switch ($components['type']) { - case 'SELECT': - $query = $this->compileSELECT($components); - break; - case 'UPDATE': - $query = $this->compileUPDATE($components); - break; - case 'INSERT': - $query = $this->compileINSERT($components); - break; - case 'DELETE': - $query = $this->compileDELETE($components); - break; - case 'EXPLAIN': - $query = 'EXPLAIN ' . $this->compileSELECT($components); - break; - case 'DROPTABLE': - $query = 'DROP TABLE' . ($components['ifExists'] ? ' IF EXISTS' : '') . ' ' . $components['TABLE']; - break; - case 'CREATETABLE': - $query = $this->compileCREATETABLE($components); - break; - case 'ALTERTABLE': - $query = $this->compileALTERTABLE($components); - break; - case 'TRUNCATETABLE': - $query = $this->compileTRUNCATETABLE($components); - break; - } - return $query; - } - - /** - * Compiles a SELECT statement from components array - * - * @param array $components Array of SQL query components - * @return string SQL SELECT query - * @see parseSELECT() - */ - protected function compileSELECT($components) { - // Initialize: - $where = $this->compileWhereClause($components['WHERE']); - $groupBy = $this->compileFieldList($components['GROUPBY']); - $orderBy = $this->compileFieldList($components['ORDERBY']); - $limit = $components['LIMIT']; - // Make query: - $query = 'SELECT ' . ($components['STRAIGHT_JOIN'] ?: '') . ' ' . - $this->compileFieldList($components['SELECT']) . - ' FROM ' . $this->compileFromTables($components['FROM']) . ($where !== '' ? - ' WHERE ' . $where : '') . ($groupBy !== '' ? - ' GROUP BY ' . $groupBy : '') . ($orderBy !== '' ? - ' ORDER BY ' . $orderBy : '') . ((string)$limit !== '' ? - ' LIMIT ' . $limit : ''); - return $query; - } - - /** - * Compiles an UPDATE statement from components array - * - * @param array $components Array of SQL query components - * @return string SQL UPDATE query - * @see parseUPDATE() - */ - protected function compileUPDATE($components) { - // Where clause: - $where = $this->compileWhereClause($components['WHERE']); - // Fields - $fields = array(); - foreach ($components['FIELDS'] as $fN => $fV) { - $fields[] = $fN . '=' . $fV[1] . $this->compileAddslashes($fV[0]) . $fV[1]; - } - // Make query: - $query = 'UPDATE ' . $components['TABLE'] . ' SET ' . implode(',', $fields) . - ($where !== '' ? ' WHERE ' . $where : ''); - - return $query; - } - - /** - * Compiles an INSERT statement from components array - * - * @param array $components Array of SQL query components - * @return string SQL INSERT query - * @see parseINSERT() - */ - protected function compileINSERT($components) { - $values = array(); - if (isset($components['VALUES_ONLY']) && is_array($components['VALUES_ONLY'])) { - $valuesComponents = $components['EXTENDED'] === '1' ? $components['VALUES_ONLY'] : array($components['VALUES_ONLY']); - $tableFields = array(); - } else { - $valuesComponents = $components['EXTENDED'] === '1' ? $components['FIELDS'] : array($components['FIELDS']); - $tableFields = array_keys($valuesComponents[0]); - } - foreach ($valuesComponents as $valuesComponent) { - $fields = array(); - foreach ($valuesComponent as $fV) { - $fields[] = $fV[1] . $this->compileAddslashes($fV[0]) . $fV[1]; - } - $values[] = '(' . implode(',', $fields) . ')'; - } - // Make query: - $query = 'INSERT INTO ' . $components['TABLE']; - if (!empty($tableFields)) { - $query .= ' (' . implode(',', $tableFields) . ')'; - } - $query .= ' VALUES ' . implode(',', $values); - - return $query; - } - - /** - * Compiles an DELETE statement from components array - * - * @param array $components Array of SQL query components - * @return string SQL DELETE query - * @see parseDELETE() - */ - protected function compileDELETE($components) { - // Where clause: - $where = $this->compileWhereClause($components['WHERE']); - // Make query: - $query = 'DELETE FROM ' . $components['TABLE'] . ($where !== '' ? ' WHERE ' . $where : ''); - - return $query; - } - - /** - * Compiles a CREATE TABLE statement from components array - * - * @param array $components Array of SQL query components - * @return string SQL CREATE TABLE query - * @see parseCREATETABLE() - */ - protected function compileCREATETABLE($components) { - // Create fields and keys: - $fieldsKeys = array(); - foreach ($components['FIELDS'] as $fN => $fCfg) { - $fieldsKeys[] = $fN . ' ' . $this->compileFieldCfg($fCfg['definition']); - } - if ($components['KEYS']) { - foreach ($components['KEYS'] as $kN => $kCfg) { - if ($kN === 'PRIMARYKEY') { - $fieldsKeys[] = 'PRIMARY KEY (' . implode(',', $kCfg) . ')'; - } elseif ($kN === 'UNIQUE') { - $key = key($kCfg); - $fields = current($kCfg); - $fieldsKeys[] = 'UNIQUE KEY ' . $key . ' (' . implode(',', $fields) . ')'; - } else { - $fieldsKeys[] = 'KEY ' . $kN . ' (' . implode(',', $kCfg) . ')'; - } - } - } - // Make query: - $query = 'CREATE TABLE ' . $components['TABLE'] . ' (' . - implode(',', $fieldsKeys) . ')' . - ($components['engine'] ? ' ENGINE=' . $components['engine'] : ''); - - return $query; - } - - /** - * Compiles an ALTER TABLE statement from components array - * - * @param array $components Array of SQL query components - * @return string SQL ALTER TABLE query - * @see parseALTERTABLE() - */ - protected function compileALTERTABLE($components) { - // Make query: - $query = 'ALTER TABLE ' . $components['TABLE'] . ' ' . $components['action'] . ' ' . ($components['FIELD'] ?: $components['KEY']); - // Based on action, add the final part: - switch ($this->normalizeKeyword($components['action'])) { - case 'ADD': - $query .= ' ' . $this->compileFieldCfg($components['definition']); - break; - case 'CHANGE': - $query .= ' ' . $components['newField'] . ' ' . $this->compileFieldCfg($components['definition']); - break; - case 'DROP': - case 'DROPKEY': - break; - case 'ADDKEY': - case 'ADDPRIMARYKEY': - case 'ADDUNIQUE': - $query .= ' (' . implode(',', $components['fields']) . ')'; - break; - case 'DEFAULTCHARACTERSET': - $query .= $components['charset']; - break; - case 'ENGINE': - $query .= '= ' . $components['engine']; - break; - } - // Return query - return $query; - } - - /** - * Compiles a TRUNCATE TABLE statement from components array - * - * @param array $components Array of SQL query components - * @return string SQL TRUNCATE TABLE query - * @see parseTRUNCATETABLE() - */ - protected function compileTRUNCATETABLE(array $components) { - // Make query: - $query = 'TRUNCATE TABLE ' . $components['TABLE']; - // Return query - return $query; - } - - /************************************** - * - * Compiling queries, helper functions for parts of queries - * - **************************************/ - /** - * Compiles a "SELECT [output] FROM..:" field list based on input array (made with ->parseFieldList()) - * Can also compile field lists for ORDER BY and GROUP BY. - * - * @param array $selectFields Array of select fields, (made with ->parseFieldList()) - * @param bool $compileComments Whether comments should be compiled - * @return string Select field string - * @see parseFieldList() - */ - public function compileFieldList($selectFields, $compileComments = TRUE) { - // Prepare buffer variable: - $fields = ''; - // Traverse the selectFields if any: - if (is_array($selectFields)) { - $outputParts = array(); - foreach ($selectFields as $k => $v) { - // Detecting type: - switch ($v['type']) { - case 'function': - $outputParts[$k] = $v['function'] . '(' . $v['func_content'] . ')'; - break; - case 'flow-control': - if ($v['flow-control']['type'] === 'CASE') { - $outputParts[$k] = $this->compileCaseStatement($v['flow-control']); - } - break; - case 'field': - $outputParts[$k] = ($v['distinct'] ? $v['distinct'] : '') . ($v['table'] ? $v['table'] . '.' : '') . $v['field']; - break; - } - // Alias: - if ($v['as']) { - $outputParts[$k] .= ' ' . $v['as_keyword'] . ' ' . $v['as']; - } - // Specifically for ORDER BY and GROUP BY field lists: - if ($v['sortDir']) { - $outputParts[$k] .= ' ' . $v['sortDir']; - } - } - if ($compileComments && $selectFields[0]['comments']) { - $fields = $selectFields[0]['comments'] . ' '; - } - $fields .= implode(', ', $outputParts); - } - return $fields; - } - - /** - * Compiles a CASE ... WHEN flow-control construct based on input array (made with ->parseCaseStatement()) - * - * @param array $components Array of case components, (made with ->parseCaseStatement()) - * @return string Case when string - * @see parseCaseStatement() - */ - protected function compileCaseStatement(array $components) { - $statement = 'CASE'; - if (isset($components['case_field'])) { - $statement .= ' ' . $components['case_field']; - } elseif (isset($components['case_value'])) { - $statement .= ' ' . $components['case_value'][1] . $components['case_value'][0] . $components['case_value'][1]; - } - foreach ($components['when'] as $when) { - $statement .= ' WHEN '; - $statement .= $this->compileWhereClause($when['when_value']); - $statement .= ' THEN '; - $statement .= $when['then_value'][1] . $when['then_value'][0] . $when['then_value'][1]; - } - if (isset($components['else'])) { - $statement .= ' ELSE '; - $statement .= $components['else'][1] . $components['else'][0] . $components['else'][1]; - } - $statement .= ' END'; - return $statement; - } - - /** - * Compile a "JOIN table ON [output] = ..." identifier - * - * @param array $identifierParts Array of identifier parts - * @return string - * @see parseCastStatement() - * @see parseFromTables() - */ - protected function compileJoinIdentifier($identifierParts) { - if ($identifierParts['type'] === 'cast') { - return sprintf('CAST(%s AS %s)', - $identifierParts['table'] ? $identifierParts['table'] . '.' . $identifierParts['field'] : $identifierParts['field'], - $identifierParts['datatype'][0] - ); - } else { - return $identifierParts['table'] ? $identifierParts['table'] . '.' . $identifierParts['field'] : $identifierParts['field']; - } - } - - /** - * Compiles a "FROM [output] WHERE..:" table list based on input array (made with ->parseFromTables()) - * - * @param array $tablesArray Array of table names, (made with ->parseFromTables()) - * @return string Table name string - * @see parseFromTables() - */ - public function compileFromTables($tablesArray) { - // Prepare buffer variable: - $outputParts = array(); - // Traverse the table names: - if (is_array($tablesArray)) { - foreach ($tablesArray as $k => $v) { - // Set table name: - $outputParts[$k] = $v['table']; - // Add alias AS if there: - if ($v['as']) { - $outputParts[$k] .= ' ' . $v['as_keyword'] . ' ' . $v['as']; - } - if (is_array($v['JOIN'])) { - foreach ($v['JOIN'] as $join) { - $outputParts[$k] .= ' ' . $join['type'] . ' ' . $join['withTable']; - // Add alias AS if there: - if (isset($join['as']) && $join['as']) { - $outputParts[$k] .= ' ' . $join['as_keyword'] . ' ' . $join['as']; - } - $outputParts[$k] .= ' ON '; - foreach ($join['ON'] as $condition) { - if ($condition['operator'] !== '') { - $outputParts[$k] .= ' ' . $condition['operator'] . ' '; - } - $outputParts[$k] .= $this->compileJoinIdentifier($condition['left']); - $outputParts[$k] .= $condition['comparator']; - if (!empty($condition['right']['value'])) { - $value = $condition['right']['value']; - $outputParts[$k] .= $value[1] . $this->compileAddslashes($value[0]) . $value[1]; - } else { - $outputParts[$k] .= $this->compileJoinIdentifier($condition['right']); - } - } - } - } - } - } - // Return imploded buffer: - return implode(', ', $outputParts); - } - - /** - * Implodes an array of WHERE clause configuration into a WHERE clause. - * - * @param array $clauseArray WHERE clause configuration - * @return string WHERE clause as string. - * @see explodeWhereClause() - */ - public function compileWhereClause($clauseArray) { - // Prepare buffer variable: - $output = ''; - // Traverse clause array: - if (is_array($clauseArray)) { - foreach ($clauseArray as $k => $v) { - // Set operator: - $output .= $v['operator'] ? ' ' . $v['operator'] : ''; - // Look for sublevel: - if (is_array($v['sub'])) { - $output .= ' (' . trim($this->compileWhereClause($v['sub'])) . ')'; - } elseif (isset($v['func']) && $v['func']['type'] === 'EXISTS') { - $output .= ' ' . trim($v['modifier']) . ' EXISTS (' . $this->compileSELECT($v['func']['subquery']) . ')'; - } else { - if (isset($v['func']) && $v['func']['type'] === 'LOCATE') { - $output .= ' ' . trim($v['modifier']) . ' LOCATE('; - $output .= $v['func']['substr'][1] . $v['func']['substr'][0] . $v['func']['substr'][1]; - $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= isset($v['func']['pos']) ? ', ' . $v['func']['pos'][0] : ''; - $output .= ')'; - } elseif (isset($v['func']) && $v['func']['type'] === 'IFNULL') { - $output .= ' ' . trim($v['modifier']) . ' IFNULL('; - $output .= ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= ', ' . $v['func']['default'][1] . $this->compileAddslashes($v['func']['default'][0]) . $v['func']['default'][1]; - $output .= ')'; - } elseif (isset($v['func']) && $v['func']['type'] === 'CAST') { - $output .= ' ' . trim($v['modifier']) . ' CAST('; - $output .= ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= ' AS ' . $v['func']['datatype'][0]; - $output .= ')'; - } elseif (isset($v['func']) && $v['func']['type'] === 'FIND_IN_SET') { - $output .= ' ' . trim($v['modifier']) . ' FIND_IN_SET('; - $output .= $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1]; - $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= ')'; - } else { - // Set field/table with modifying prefix if any: - $output .= ' ' . trim(($v['modifier'] . ' ' . ($v['table'] ? $v['table'] . '.' : '') . $v['field'])); - // Set calculation, if any: - if ($v['calc']) { - $output .= $v['calc'] . $v['calc_value'][1] . $this->compileAddslashes($v['calc_value'][0]) . $v['calc_value'][1]; - } - } - // Set comparator: - if ($v['comparator']) { - $output .= ' ' . $v['comparator']; - // Detecting value type; list or plain: - if (\TYPO3\CMS\Core\Utility\GeneralUtility::inList('NOTIN,IN', $this->normalizeKeyword($v['comparator']))) { - if (isset($v['subquery'])) { - $output .= ' (' . $this->compileSELECT($v['subquery']) . ')'; - } else { - $valueBuffer = array(); - foreach ($v['value'] as $realValue) { - $valueBuffer[] = $realValue[1] . $this->compileAddslashes($realValue[0]) . $realValue[1]; - } - $output .= ' (' . trim(implode(',', $valueBuffer)) . ')'; - } - } else { - if (\TYPO3\CMS\Core\Utility\GeneralUtility::inList('BETWEEN,NOT BETWEEN', $v['comparator'])) { - $lbound = $v['values'][0]; - $ubound = $v['values'][1]; - $output .= ' ' . $lbound[1] . $this->compileAddslashes($lbound[0]) . $lbound[1]; - $output .= ' AND '; - $output .= $ubound[1] . $this->compileAddslashes($ubound[0]) . $ubound[1]; - } else { - if (isset($v['value']['operator'])) { - $values = array(); - foreach ($v['value']['args'] as $fieldDef) { - $values[] = ($fieldDef['table'] ? $fieldDef['table'] . '.' : '') . $fieldDef['field']; - } - $output .= ' ' . $v['value']['operator'] . '(' . implode(',', $values) . ')'; - } else { - $output .= ' ' . $v['value'][1] . $this->compileAddslashes($v['value'][0]) . $v['value'][1]; - } - } - } - } - } - } - } - // Return output buffer: - return $output; - } - - /** - * Compile field definition - * - * @param array $fieldCfg Field definition parts - * @return string Field definition string - */ - public function compileFieldCfg($fieldCfg) { - // Set type: - $cfg = $fieldCfg['fieldType']; - // Add value, if any: - if ((string)$fieldCfg['value'] !== '') { - $cfg .= '(' . $fieldCfg['value'] . ')'; - } - // Add additional features: - if (is_array($fieldCfg['featureIndex'])) { - foreach ($fieldCfg['featureIndex'] as $featureDef) { - $cfg .= ' ' . $featureDef['keyword']; - // Add value if found: - if (is_array($featureDef['value'])) { - $cfg .= ' ' . $featureDef['value'][1] . $this->compileAddslashes($featureDef['value'][0]) . $featureDef['value'][1]; - } - } - } - // Return field definition string: - return $cfg; - } - - /************************* - * - * Debugging - * - *************************/ - /** - * Check parsability of input SQL part string; Will parse and re-compile after which it is compared - * - * @param string $part Part definition of string; "SELECT" = fieldlist (also ORDER BY and GROUP BY), "FROM" = table list, "WHERE" = Where clause. - * @param string $str SQL string to verify parsability of - * @return mixed Returns array with string 1 and 2 if error, otherwise FALSE - */ - public function debug_parseSQLpart($part, $str) { - $retVal = FALSE; - switch ($part) { - case 'SELECT': - $retVal = $this->debug_parseSQLpartCompare($str, $this->compileFieldList($this->parseFieldList($str))); - break; - case 'FROM': - $retVal = $this->debug_parseSQLpartCompare($str, $this->compileFromTables($this->parseFromTables($str))); - break; - case 'WHERE': - $retVal = $this->debug_parseSQLpartCompare($str, $this->compileWhereClause($this->parseWhereClause($str))); - break; - } - return $retVal; - } - - /** - * Compare two query strings by stripping away whitespace. - * - * @param string $str SQL String 1 - * @param string $newStr SQL string 2 - * @param bool $caseInsensitive If TRUE, the strings are compared insensitive to case - * @return mixed Returns array with string 1 and 2 if error, otherwise FALSE - */ - public function debug_parseSQLpartCompare($str, $newStr, $caseInsensitive = FALSE) { - if ($caseInsensitive) { - $str1 = strtoupper($str); - $str2 = strtoupper($newStr); - } else { - $str1 = $str; - $str2 = $newStr; - } - - // Fixing escaped chars: - $search = array(NUL, LF, CR, SUB); - $replace = array("\x00", "\x0a", "\x0d", "\x1a"); - $str1 = str_replace($search, $replace, $str1); - $str2 = str_replace($search, $replace, $str2); - - $search = self::$interQueryWhitespaces; - if (str_replace($search, '', $this->trimSQL($str1)) !== str_replace($search, '', $this->trimSQL($str2))) { - return array( - str_replace($search, ' ', $str), - str_replace($search, ' ', $newStr), - ); - } - } - - /** - * Normalizes the keyword by removing any separator and changing to uppercase - * - * @param string $keyword The keyword being normalized - * @return string - */ - protected function normalizeKeyword($keyword) { - return strtoupper(str_replace(self::$interQueryWhitespaces, '', $keyword)); - } -} diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-68401-SqlParserMovedIntoEXTdbal.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-68401-SqlParserMovedIntoEXTdbal.rst new file mode 100644 index 000000000000..bbdcf3d55cd6 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-68401-SqlParserMovedIntoEXTdbal.rst @@ -0,0 +1,36 @@ +================================================ +Breaking: #68401 - SqlParser moved into EXT:dbal +================================================ + +Description +=========== + +The SQL Parser included with the core has not been in use by anything +except EXT:dbal for some time. The SQL parser has been merged with the +version in EXT:dbal which now provides parsing and compiling of SQL +statements for MySQL as well as other DBMS. + + +Impact +====== + +There is no impact for the core as EXT:dbal was the sole user of the SQL +parser and it has been migrated into EXT:dbal. + +As the parsing and the compiling of SQL statements has been separated into +multiple classes the non-public interface of ``SqlParser`` has changed. +Classes extending SqlParser need to be adjusted to the new interface. + + +Affected Installations +====================== + +Installations with 3rd party extensions that use ``\TYPO3\CMS\Core\Database\SqlParser``. + + +Migration +========= + +Update the code to use ``\TYPO3\CMS\Dbal\Database\SqlParser`` instead of +``\TYPO3\CMS\Core\Database\SqlParser`` or install EXT:compatibility6 which +maps the old class names to the new ones in EXT:dbal. diff --git a/typo3/sysext/core/Tests/Unit/Database/SqlParserTest.php b/typo3/sysext/core/Tests/Unit/Database/SqlParserTest.php deleted file mode 100644 index 115287d91549..000000000000 --- a/typo3/sysext/core/Tests/Unit/Database/SqlParserTest.php +++ /dev/null @@ -1,492 +0,0 @@ -<?php -namespace TYPO3\CMS\Core\Tests\Unit\Database; - -/* - * This file is part of the TYPO3 CMS project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ - -/** - * Testcase for TYPO3\CMS\Core\Database\SqlParser - */ -class SqlParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase { - - /** - * @var \TYPO3\CMS\Core\Database\SqlParser|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface - */ - protected $subject; - - protected function setUp() { - $this->subject = $this->getAccessibleMock(\TYPO3\CMS\Core\Database\SqlParser::class, array('dummy')); - } - - /** - * Regression test - * - * @test - */ - public function compileWhereClauseDoesNotDropClauses() { - $clauses = array( - 0 => array( - 'modifier' => '', - 'table' => 'pages', - 'field' => 'fe_group', - 'calc' => '', - 'comparator' => '=', - 'value' => array( - 0 => '', - 1 => '\'' - ) - ), - 1 => array( - 'operator' => 'OR', - 'modifier' => '', - 'func' => array( - 'type' => 'IFNULL', - 'default' => array( - 0 => '1', - 1 => '\'' - ), - 'table' => 'pages', - 'field' => 'fe_group' - ) - ), - 2 => array( - 'operator' => 'OR', - 'modifier' => '', - 'table' => 'pages', - 'field' => 'fe_group', - 'calc' => '', - 'comparator' => '=', - 'value' => array( - 0 => '0', - 1 => '\'' - ) - ), - 3 => array( - 'operator' => 'OR', - 'modifier' => '', - 'func' => array( - 'type' => 'FIND_IN_SET', - 'str' => array( - 0 => '0', - 1 => '\'' - ), - 'table' => 'pages', - 'field' => 'fe_group' - ), - 'comparator' => '' - ), - 4 => array( - 'operator' => 'OR', - 'modifier' => '', - 'func' => array( - 'type' => 'FIND_IN_SET', - 'str' => array( - 0 => '-1', - 1 => '\'' - ), - 'table' => 'pages', - 'field' => 'fe_group' - ), - 'comparator' => '' - ), - 5 => array( - 'operator' => 'OR', - 'modifier' => '', - 'func' => array( - 'type' => 'CAST', - 'table' => 'pages', - 'field' => 'fe_group', - 'datatype' => 'CHAR' - ), - 'comparator' => '=', - 'value' => array( - 0 => '', - 1 => '\'' - ) - ) - ); - $output = $this->subject->compileWhereClause($clauses); - $parts = explode(' OR ', $output); - $this->assertSame(count($clauses), count($parts)); - $this->assertContains('IFNULL', $output); - } - - /** - * Data provider for trimSqlReallyTrimsAllWhitespace - * - * @see trimSqlReallyTrimsAllWhitespace - */ - public function trimSqlReallyTrimsAllWhitespaceDataProvider() { - return array( - 'Nothing to trim' => array('SELECT * FROM test WHERE 1=1;', 'SELECT * FROM test WHERE 1=1 '), - 'Space after ;' => array('SELECT * FROM test WHERE 1=1; ', 'SELECT * FROM test WHERE 1=1 '), - 'Space before ;' => array('SELECT * FROM test WHERE 1=1 ;', 'SELECT * FROM test WHERE 1=1 '), - 'Space before and after ;' => array('SELECT * FROM test WHERE 1=1 ; ', 'SELECT * FROM test WHERE 1=1 '), - 'Linefeed after ;' => array('SELECT * FROM test WHERE 1=1' . LF . ';', 'SELECT * FROM test WHERE 1=1 '), - 'Linefeed before ;' => array('SELECT * FROM test WHERE 1=1;' . LF, 'SELECT * FROM test WHERE 1=1 '), - 'Linefeed before and after ;' => array('SELECT * FROM test WHERE 1=1' . LF . ';' . LF, 'SELECT * FROM test WHERE 1=1 '), - 'Tab after ;' => array('SELECT * FROM test WHERE 1=1' . TAB . ';', 'SELECT * FROM test WHERE 1=1 '), - 'Tab before ;' => array('SELECT * FROM test WHERE 1=1;' . TAB, 'SELECT * FROM test WHERE 1=1 '), - 'Tab before and after ;' => array('SELECT * FROM test WHERE 1=1' . TAB . ';' . TAB, 'SELECT * FROM test WHERE 1=1 '), - ); - } - - /** - * @test - * @dataProvider trimSqlReallyTrimsAllWhitespaceDataProvider - * @param string $sql The SQL to trim - * @param string $expected The expected trimmed SQL with single space at the end - */ - public function trimSqlReallyTrimsAllWhitespace($sql, $expected) { - $result = $this->subject->_call('trimSQL', $sql); - $this->assertSame($expected, $result); - } - - - /** - * Data provider for getValueReturnsCorrectValues - * - * @see getValueReturnsCorrectValues - */ - public function getValueReturnsCorrectValuesDataProvider() { - return array( - // description => array($parseString, $comparator, $mode, $expected) - 'key definition without length' => array('(pid,input_1), ', '_LIST', 'INDEX', array('pid', 'input_1')), - 'key definition with length' => array('(pid,input_1(30)), ', '_LIST', 'INDEX', array('pid', 'input_1(30)')), - 'key definition without length (no mode)' => array('(pid,input_1), ', '_LIST', '', array('pid', 'input_1')), - 'key definition with length (no mode)' => array('(pid,input_1(30)), ', '_LIST', '', array('pid', 'input_1(30)')), - 'test1' => array('input_1 varchar(255) DEFAULT \'\' NOT NULL,', '', '', array('input_1')), - 'test2' => array('varchar(255) DEFAULT \'\' NOT NULL,', '', '', array('varchar(255)')), - 'test3' => array('DEFAULT \'\' NOT NULL,', '', '', array('DEFAULT')), - 'test4' => array('\'\' NOT NULL,', '', '', array('', '\'')), - 'test5' => array('NOT NULL,', '', '', array('NOT')), - 'test6' => array('NULL,', '', '', array('NULL')), - 'getValueOrParameter' => array('NULL,', '', '', array('NULL')), - ); - } - - /** - * @test - * @dataProvider getValueReturnsCorrectValuesDataProvider - * @param string $parseString the string to parse - * @param string $comparator The comparator used before. If "NOT IN" or "IN" then the value is expected to be a list of values. Otherwise just an integer (un-quoted) or string (quoted) - * @param string $mode The mode, eg. "INDEX - * @param string $expected - */ - public function getValueReturnsCorrectValues($parseString, $comparator, $mode, $expected) { - $result = $this->subject->_callRef('getValue', $parseString, $comparator, $mode); - $this->assertSame($expected, $result); - } - - /** - * Data provider for parseSQL - * - * @see parseSQL - */ - public function parseSQLDataProvider() { - $testSql = array(); - $testSql[] = 'CREATE TABLE tx_demo ('; - $testSql[] = ' uid int(11) NOT NULL auto_increment,'; - $testSql[] = ' pid int(11) DEFAULT \'0\' NOT NULL,'; - - $testSql[] = ' tstamp int(11) unsigned DEFAULT \'0\' NOT NULL,'; - $testSql[] = ' crdate int(11) unsigned DEFAULT \'0\' NOT NULL,'; - $testSql[] = ' cruser_id int(11) unsigned DEFAULT \'0\' NOT NULL,'; - $testSql[] = ' deleted tinyint(4) unsigned DEFAULT \'0\' NOT NULL,'; - $testSql[] = ' hidden tinyint(4) unsigned DEFAULT \'0\' NOT NULL,'; - $testSql[] = ' starttime int(11) unsigned DEFAULT \'0\' NOT NULL,'; - $testSql[] = ' endtime int(11) unsigned DEFAULT \'0\' NOT NULL,'; - - $testSql[] = ' input_1 varchar(255) DEFAULT \'\' NOT NULL,'; - $testSql[] = ' input_2 varchar(255) DEFAULT \'\' NOT NULL,'; - $testSql[] = ' select_child int(11) unsigned DEFAULT \'0\' NOT NULL,'; - - $testSql[] = ' PRIMARY KEY (uid),'; - $testSql[] = ' KEY parent (pid,input_1),'; - $testSql[] = ' KEY bar (tstamp,input_1(200),input_2(100),endtime)'; - $testSql[] = ');'; - $testSql = implode("\n", $testSql); - $expected = array( - 'type' => 'CREATETABLE', - 'TABLE' => 'tx_demo', - 'FIELDS' => array( - 'uid' => array( - 'definition' => array( - 'fieldType' => 'int', - 'value' => '11', - 'featureIndex' => array( - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ), - 'AUTO_INCREMENT' => array( - 'keyword' => 'auto_increment' - ) - ) - ) - ), - 'pid' => array( - 'definition' => array( - 'fieldType' => 'int', - 'value' => '11', - 'featureIndex' => array( - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '0', - 1 => '\'', - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'tstamp' => array( - 'definition' => array( - 'fieldType' => 'int', - 'value' => '11', - 'featureIndex' => array( - 'UNSIGNED' => array( - 'keyword' => 'unsigned' - ), - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '0', - 1 => '\'' - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'crdate' => array( - 'definition' => array( - 'fieldType' => 'int', - 'value' => '11', - 'featureIndex' => array( - 'UNSIGNED' => array( - 'keyword' => 'unsigned' - ), - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '0', - 1 => '\'' - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'cruser_id' => array( - 'definition' => array( - 'fieldType' => 'int', - 'value' => '11', - 'featureIndex' => array( - 'UNSIGNED' => array( - 'keyword' => 'unsigned' - ), - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '0', - 1 => '\'', - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'deleted' => array( - 'definition' => array( - 'fieldType' => 'tinyint', - 'value' => '4', - 'featureIndex' => array( - 'UNSIGNED' => array( - 'keyword' => 'unsigned' - ), - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '0', - 1 => '\'' - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'hidden' => array( - 'definition' => array( - 'fieldType' => 'tinyint', - 'value' => '4', - 'featureIndex' => array( - 'UNSIGNED' => array( - 'keyword' => 'unsigned' - ), - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '0', - 1 => '\'' - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'starttime' => array( - 'definition' => array( - 'fieldType' => 'int', - 'value' => '11', - 'featureIndex' => array( - 'UNSIGNED' => array( - 'keyword' => 'unsigned' - ), - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '0', - 1 => '\'' - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'endtime' => array( - 'definition' => array( - 'fieldType' => 'int', - 'value' => '11', - 'featureIndex' => array( - 'UNSIGNED' => array( - 'keyword' => 'unsigned' - ), - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '0', - 1 => '\'', - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'input_1' => array( - 'definition' => array( - 'fieldType' => 'varchar', - 'value' => '255', - 'featureIndex' => array( - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '', - 1 => '\'', - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'input_2' => array( - 'definition' => array( - 'fieldType' => 'varchar', - 'value' => '255', - 'featureIndex' => array( - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '', - 1 => '\'', - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ), - 'select_child' => array( - 'definition' => array( - 'fieldType' => 'int', - 'value' => '11', - 'featureIndex' => array( - 'UNSIGNED' => array( - 'keyword' => 'unsigned' - ), - 'DEFAULT' => array( - 'keyword' => 'DEFAULT', - 'value' => array( - 0 => '0', - 1 => '\'' - ) - ), - 'NOTNULL' => array( - 'keyword' => 'NOT NULL' - ) - ) - ) - ) - ), - 'KEYS' => array( - 'PRIMARYKEY' => array( - 0 => 'uid' - ), - 'parent' => array( - 0 => 'pid', - 1 => 'input_1', - ), - 'bar' => array( - 0 => 'tstamp', - 1 => 'input_1(200)', - 2 => 'input_2(100)', - 3 => 'endtime', - ) - ) - ); - - return array( - 'test1' => array($testSql, $expected) - ); - } - - /** - * @test - * @dataProvider parseSQLDataProvider - * @param string $sql The SQL to trim - * @param array $expected The expected trimmed SQL with single space at the end - */ - public function parseSQL($sql, $expected) { - $result = $this->subject->_callRef('parseSQL', $sql); - $this->assertSame($expected, $result); - } -} diff --git a/typo3/sysext/dbal/Classes/Database/DatabaseConnection.php b/typo3/sysext/dbal/Classes/Database/DatabaseConnection.php index 392b6f9dddd7..51ec6ae442cf 100644 --- a/typo3/sysext/dbal/Classes/Database/DatabaseConnection.php +++ b/typo3/sysext/dbal/Classes/Database/DatabaseConnection.php @@ -145,7 +145,7 @@ class DatabaseConnection extends \TYPO3\CMS\Core\Database\DatabaseConnection { /** * SQL parser * - * @var \TYPO3\CMS\Core\Database\SqlParser + * @var \TYPO3\CMS\Dbal\Database\SqlParser */ public $SQLparser; @@ -209,7 +209,7 @@ class DatabaseConnection extends \TYPO3\CMS\Core\Database\DatabaseConnection { */ public function __construct() { // Set SQL parser object for internal use: - $this->SQLparser = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\SqlParser::class, $this); + $this->SQLparser = GeneralUtility::makeInstance(\TYPO3\CMS\Dbal\Database\SqlParser::class, $this); $this->installerSql = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Service\SqlSchemaMigrationService::class); $this->queryCache = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('dbal'); // Set internal variables with configuration: @@ -936,7 +936,7 @@ class DatabaseConnection extends \TYPO3\CMS\Core\Database\DatabaseConnection { * Executes a query. * EXPERIMENTAL since TYPO3 4.4. * - * @param array $queryParts SQL parsed by method parseSQL() of \TYPO3\CMS\Core\Database\SqlParser + * @param array $queryParts SQL parsed by method parseSQL() of \TYPO3\CMS\Dbal\Database\SqlParser * @return \mysqli_result|object MySQLi result object / DBAL object * @see self::sql_query() */ @@ -3417,7 +3417,7 @@ class DatabaseConnection extends \TYPO3\CMS\Core\Database\DatabaseConnection { } /** - * Generic mapping of table/field names arrays (as parsed by \TYPO3\CMS\Core\Database\SqlParser) + * Generic mapping of table/field names arrays (as parsed by \TYPO3\CMS\Dbal\Database\SqlParser) * * @param array $sqlPartArray Array with parsed SQL parts; Takes both fields, tables, where-parts, group and order-by. Passed by reference. * @param string $defaultTable Default table name to assume if no table is found in $sqlPartArray @@ -3612,10 +3612,10 @@ class DatabaseConnection extends \TYPO3\CMS\Core\Database\DatabaseConnection { } /** - * Will do table/field mapping on a general \TYPO3\CMS\Core\Database\SqlParser-compliant SQL query + * Will do table/field mapping on a general \TYPO3\CMS\Dbal\Database\SqlParser-compliant SQL query * (May still not support all query types...) * - * @param array $parsedQuery Parsed QUERY as from \TYPO3\CMS\Core\Database\SqlParser::parseSQL(). NOTICE: Passed by reference! + * @param array $parsedQuery Parsed QUERY as from \TYPO3\CMS\Dbal\Database\SqlParser::parseSQL(). NOTICE: Passed by reference! * @throws \InvalidArgumentException * @return void * @see \TYPO3\CMS\Core\Database\SqlParser::parseSQL() diff --git a/typo3/sysext/dbal/Classes/Database/SqlCompilers/AbstractCompiler.php b/typo3/sysext/dbal/Classes/Database/SqlCompilers/AbstractCompiler.php new file mode 100644 index 000000000000..100ec906513d --- /dev/null +++ b/typo3/sysext/dbal/Classes/Database/SqlCompilers/AbstractCompiler.php @@ -0,0 +1,316 @@ +<?php +namespace TYPO3\CMS\Dbal\Database\SqlCompilers; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Dbal\Database\DatabaseConnection; + +/** + * Abstract base class for SQL compilers + */ +abstract class AbstractCompiler { + + /** + * @var \TYPO3\CMS\Dbal\Database\DatabaseConnection + */ + protected $databaseConnection; + + /** + * @param \TYPO3\CMS\Dbal\Database\DatabaseConnection $databaseConnection + */ + public function __construct(DatabaseConnection $databaseConnection) { + $this->databaseConnection = $databaseConnection; + } + + /** + * Compiles an SQL query from components + * + * @param array $components Array of SQL query components + * @return string SQL query + * @see parseSQL() + */ + public function compileSQL($components) { + $query = ''; + switch ($components['type']) { + case 'SELECT': + $query = $this->compileSELECT($components); + break; + case 'UPDATE': + $query = $this->compileUPDATE($components); + break; + case 'INSERT': + $query = $this->compileINSERT($components); + break; + case 'DELETE': + $query = $this->compileDELETE($components); + break; + case 'EXPLAIN': + $query = 'EXPLAIN ' . $this->compileSELECT($components); + break; + case 'DROPTABLE': + $query = 'DROP TABLE' . ($components['ifExists'] ? ' IF EXISTS' : '') . ' ' . $components['TABLE']; + break; + case 'CREATETABLE': + $query = $this->compileCREATETABLE($components); + break; + case 'ALTERTABLE': + $query = $this->compileALTERTABLE($components); + break; + case 'TRUNCATETABLE': + $query = $this->compileTRUNCATETABLE($components); + break; + } + return $query; + } + + /** + * Compiles a SELECT statement from components array + * + * @param array $components Array of SQL query components + * @return string SQL SELECT query + * @see parseSELECT() + */ + protected function compileSELECT($components) { + // Initialize: + $where = $this->compileWhereClause($components['WHERE']); + $groupBy = $this->compileFieldList($components['GROUPBY']); + $orderBy = $this->compileFieldList($components['ORDERBY']); + $limit = $components['LIMIT']; + // Make query: + $query = 'SELECT ' . ($components['STRAIGHT_JOIN'] ?: '') . ' ' . + $this->compileFieldList($components['SELECT']) . + ' FROM ' . $this->compileFromTables($components['FROM']) . ($where !== '' ? + ' WHERE ' . $where : '') . ($groupBy !== '' ? + ' GROUP BY ' . $groupBy : '') . ($orderBy !== '' ? + ' ORDER BY ' . $orderBy : '') . ((string)$limit !== '' ? + ' LIMIT ' . $limit : ''); + return $query; + } + + /** + * Compiles an UPDATE statement from components array + * + * @param array $components Array of SQL query components + * @return string SQL UPDATE query + * @see parseUPDATE() + */ + protected function compileUPDATE($components) { + // Where clause: + $where = $this->compileWhereClause($components['WHERE']); + // Fields + $fields = array(); + foreach ($components['FIELDS'] as $fN => $fV) { + $fields[] = $fN . '=' . $fV[1] . $this->compileAddslashes($fV[0]) . $fV[1]; + } + // Make query: + $query = 'UPDATE ' . $components['TABLE'] . ' SET ' . implode(',', $fields) . + ($where !== '' ? ' WHERE ' . $where : ''); + + return $query; + } + + /** + * Compiles an INSERT statement from components array + * + * @param array $components Array of SQL query components + * @return string SQL INSERT query + * @see parseINSERT() + */ + abstract protected function compileINSERT($components); + + /** + * Compiles an DELETE statement from components array + * + * @param array $components Array of SQL query components + * @return string SQL DELETE query + * @see parseDELETE() + */ + protected function compileDELETE($components) { + // Where clause: + $where = $this->compileWhereClause($components['WHERE']); + // Make query: + $query = 'DELETE FROM ' . $components['TABLE'] . ($where !== '' ? ' WHERE ' . $where : ''); + + return $query; + } + + /** + * Compiles a CREATE TABLE statement from components array + * + * @param array $components Array of SQL query components + * @return array array with SQL CREATE TABLE/INDEX command(s) + * @see parseCREATETABLE() + */ + abstract protected function compileCREATETABLE($components); + + /** + * Compiles an ALTER TABLE statement from components array + * + * @param array Array of SQL query components + * @return string SQL ALTER TABLE query + * @see parseALTERTABLE() + */ + abstract protected function compileALTERTABLE($components); + + /** + * Compiles a TRUNCATE TABLE statement from components array + * + * @param array $components Array of SQL query components + * @return string SQL TRUNCATE TABLE query + * @see parseTRUNCATETABLE() + */ + protected function compileTRUNCATETABLE(array $components) { + // Make query: + $query = 'TRUNCATE TABLE ' . $components['TABLE']; + // Return query + return $query; + } + + /** + * Compiles a "SELECT [output] FROM..:" field list based on input array (made with ->parseFieldList()) + * Can also compile field lists for ORDER BY and GROUP BY. + * + * @param array $selectFields Array of select fields, (made with ->parseFieldList()) + * @param bool $compileComments Whether comments should be compiled + * @param bool $functionMapping Whether function mapping should take place + * @return string Select field string + * @see parseFieldList() + */ + abstract public function compileFieldList($selectFields, $compileComments = TRUE, $functionMapping = TRUE); + + /** + * Implodes an array of WHERE clause configuration into a WHERE clause. + * + * DBAL-specific: The only(!) handled "calc" operators supported by parseWhereClause() are: + * - the bitwise logical and (&) + * - the addition (+) + * - the substraction (-) + * - the multiplication (*) + * - the division (/) + * - the modulo (%) + * + * @param array $clauseArray + * @param bool $functionMapping + * @return string WHERE clause as string. + * @see \TYPO3\CMS\Core\Database\SqlParser::parseWhereClause() + */ + abstract public function compileWhereClause($clauseArray, $functionMapping = TRUE); + + /** + * Add slashes function used for compiling queries + * This method overrides the method from \TYPO3\CMS\Dbal\Database\NativeSqlParser because + * the input string is already properly escaped. + * + * @param string $str Input string + * @return string Output string + */ + abstract protected function compileAddslashes($str); + + /** + * Compile a "JOIN table ON [output] = ..." identifier + * + * @param array $identifierParts Array of identifier parts + * @return string + * @see parseCastStatement() + * @see parseFromTables() + */ + protected function compileJoinIdentifier($identifierParts) { + if ($identifierParts['type'] === 'cast') { + return sprintf('CAST(%s AS %s)', + $identifierParts['table'] ? $identifierParts['table'] . '.' . $identifierParts['field'] : $identifierParts['field'], + $identifierParts['datatype'][0] + ); + } else { + return $identifierParts['table'] ? $identifierParts['table'] . '.' . $identifierParts['field'] : $identifierParts['field']; + } + } + + /** + * Compiles a "FROM [output] WHERE..:" table list based on input array (made with ->parseFromTables()) + * + * @param array $tablesArray Array of table names, (made with ->parseFromTables()) + * @return string Table name string + * @see parseFromTables() + */ + public function compileFromTables($tablesArray) { + // Prepare buffer variable: + $outputParts = array(); + // Traverse the table names: + if (is_array($tablesArray)) { + foreach ($tablesArray as $k => $v) { + // Set table name: + $outputParts[$k] = $v['table']; + // Add alias AS if there: + if ($v['as']) { + $outputParts[$k] .= ' ' . $v['as_keyword'] . ' ' . $v['as']; + } + if (is_array($v['JOIN'])) { + foreach ($v['JOIN'] as $join) { + $outputParts[$k] .= ' ' . $join['type'] . ' ' . $join['withTable']; + // Add alias AS if there: + if (isset($join['as']) && $join['as']) { + $outputParts[$k] .= ' ' . $join['as_keyword'] . ' ' . $join['as']; + } + $outputParts[$k] .= ' ON '; + foreach ($join['ON'] as $condition) { + if ($condition['operator'] !== '') { + $outputParts[$k] .= ' ' . $condition['operator'] . ' '; + } + $outputParts[$k] .= $this->compileJoinIdentifier($condition['left']); + $outputParts[$k] .= $condition['comparator']; + if (!empty($condition['right']['value'])) { + $value = $condition['right']['value']; + $outputParts[$k] .= $value[1] . $this->compileAddslashes($value[0]) . $value[1]; + } else { + $outputParts[$k] .= $this->compileJoinIdentifier($condition['right']); + } + } + } + } + } + } + // Return imploded buffer: + return implode(', ', $outputParts); + } + + /** + * Compiles a CASE ... WHEN flow-control construct based on input array (made with ->parseCaseStatement()) + * + * @param array $components Array of case components, (made with ->parseCaseStatement()) + * @param bool $functionMapping Whether function mapping should take place + * @return string case when string + * @see parseCaseStatement() + */ + protected function compileCaseStatement(array $components, $functionMapping = TRUE) { + $statement = 'CASE'; + if (isset($components['case_field'])) { + $statement .= ' ' . $components['case_field']; + } elseif (isset($components['case_value'])) { + $statement .= ' ' . $components['case_value'][1] . $components['case_value'][0] . $components['case_value'][1]; + } + foreach ($components['when'] as $when) { + $statement .= ' WHEN '; + $statement .= $this->compileWhereClause($when['when_value'], $functionMapping); + $statement .= ' THEN '; + $statement .= $when['then_value'][1] . $when['then_value'][0] . $when['then_value'][1]; + } + if (isset($components['else'])) { + $statement .= ' ELSE '; + $statement .= $components['else'][1] . $components['else'][0] . $components['else'][1]; + } + $statement .= ' END'; + return $statement; + } + +} diff --git a/typo3/sysext/dbal/Classes/Database/SqlCompilers/Adodb.php b/typo3/sysext/dbal/Classes/Database/SqlCompilers/Adodb.php new file mode 100644 index 000000000000..7bb9bda13b28 --- /dev/null +++ b/typo3/sysext/dbal/Classes/Database/SqlCompilers/Adodb.php @@ -0,0 +1,590 @@ +<?php +namespace TYPO3\CMS\Dbal\Database\SqlCompilers; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Dbal\Database\Specifics; +use TYPO3\CMS\Dbal\Database\SqlParser; + +/** + * SQL Compiler for ADOdb connections + */ +class Adodb extends AbstractCompiler { + /** + * Compiles an INSERT statement from components array + * + * @param array Array of SQL query components + * @return string SQL INSERT query / array + * @see parseINSERT() + */ + protected function compileINSERT($components) { + $values = array(); + if (isset($components['VALUES_ONLY']) && is_array($components['VALUES_ONLY'])) { + $valuesComponents = $components['EXTENDED'] === '1' ? $components['VALUES_ONLY'] : array($components['VALUES_ONLY']); + $tableFields = array_keys($this->databaseConnection->cache_fieldType[$components['TABLE']]); + } else { + $valuesComponents = $components['EXTENDED'] === '1' ? $components['FIELDS'] : array($components['FIELDS']); + $tableFields = array_keys($valuesComponents[0]); + } + foreach ($valuesComponents as $valuesComponent) { + $fields = array(); + $fc = 0; + foreach ($valuesComponent as $fV) { + $fields[$tableFields[$fc++]] = $fV[0]; + } + $values[] = $fields; + } + return count($values) === 1 ? $values[0] : $values; + } + + /** + * Compiles a CREATE TABLE statement from components array + * + * @param array $components Array of SQL query components + * @return array array with SQL CREATE TABLE/INDEX command(s) + * @see parseCREATETABLE() + */ + protected function compileCREATETABLE($components) { + // Create fields and keys: + $fieldsKeys = array(); + $indexKeys = array(); + foreach ($components['FIELDS'] as $fN => $fCfg) { + $handlerKey = $this->databaseConnection->handler_getFromTableList($components['TABLE']); + $fieldsKeys[$fN] = $this->databaseConnection->quoteName($fN, $handlerKey, TRUE) . ' ' . $this->compileFieldCfg($fCfg['definition']); + } + if (isset($components['KEYS']) && is_array($components['KEYS'])) { + foreach ($components['KEYS'] as $kN => $kCfg) { + if ($kN === 'PRIMARYKEY') { + foreach ($kCfg as $field) { + $fieldsKeys[$field] .= ' PRIMARY'; + } + } elseif ($kN === 'UNIQUE') { + foreach ($kCfg as $n => $field) { + $indexKeys = array_merge($indexKeys, $this->compileCREATEINDEX($n, $components['TABLE'], $field, array('UNIQUE'))); + } + } else { + $indexKeys = array_merge($indexKeys, $this->compileCREATEINDEX($kN, $components['TABLE'], $kCfg)); + } + } + } + // Generally create without OID on PostgreSQL + $tableOptions = array('postgres' => 'WITHOUT OIDS'); + // Fetch table/index generation query: + $tableName = $this->databaseConnection->quoteName($components['TABLE'], NULL, TRUE); + $query = array_merge($this->databaseConnection->handlerInstance[$this->databaseConnection->lastHandlerKey]->DataDictionary->CreateTableSQL($tableName, implode(',' . LF, $fieldsKeys), $tableOptions), $indexKeys); + return $query; + } + + /** + * Compiles an ALTER TABLE statement from components array + * + * @param array Array of SQL query components + * @return string SQL ALTER TABLE query + * @see parseALTERTABLE() + */ + protected function compileALTERTABLE($components) { + $query = ''; + $tableName = $this->databaseConnection->quoteName($components['TABLE'], NULL, TRUE); + $fieldName = $this->databaseConnection->quoteName($components['FIELD'], NULL, TRUE); + switch (strtoupper(str_replace(array(' ', "\n", "\r", "\t"), '', $components['action']))) { + case 'ADD': + $query = $this->databaseConnection->handlerInstance[$this->databaseConnection->lastHandlerKey]->DataDictionary->AddColumnSQL($tableName, $fieldName . ' ' . $this->compileFieldCfg($components['definition'])); + break; + case 'CHANGE': + $query = $this->databaseConnection->handlerInstance[$this->databaseConnection->lastHandlerKey]->DataDictionary->AlterColumnSQL($tableName, $fieldName . ' ' . $this->compileFieldCfg($components['definition'])); + break; + case 'DROP': + + case 'DROPKEY': + $query = $this->compileDROPINDEX($components['KEY'], $components['TABLE']); + break; + + case 'ADDKEY': + $query = $this->compileCREATEINDEX($components['KEY'], $components['TABLE'], $components['fields']); + break; + case 'ADDUNIQUE': + $query = $this->compileCREATEINDEX($components['KEY'], $components['TABLE'], $components['fields'], array('UNIQUE')); + break; + case 'ADDPRIMARYKEY': + // @todo ??? + break; + case 'DEFAULTCHARACTERSET': + + case 'ENGINE': + // @todo ??? + break; + } + return $query; + } + + /** + * Compiles CREATE INDEX statements from component information + * + * MySQL only needs uniqueness of index names per table, but many DBMS require uniqueness of index names per schema. + * The table name is hashed and prepended to the index name to make sure index names are unique. + * + * @param string $indexName + * @param string $tableName + * @param array $indexFields + * @param array $indexOptions + * @return array + * @see compileALTERTABLE() + */ + protected function compileCREATEINDEX($indexName, $tableName, $indexFields, $indexOptions = array()) { + $indexIdentifier = $this->databaseConnection->quoteName(hash('crc32b', $tableName) . '_' . $indexName, NULL, TRUE); + $dbmsSpecifics = $this->databaseConnection->getSpecifics(); + $keepFieldLengths = $dbmsSpecifics->specificExists(Specifics\AbstractSpecifics::PARTIAL_STRING_INDEX) && $dbmsSpecifics->getSpecific(Specifics\AbstractSpecifics::PARTIAL_STRING_INDEX); + + foreach ($indexFields as $key => $fieldName) { + if (!$keepFieldLengths) { + $fieldName = preg_replace('/\A([^\(]+)(\(\d+\))/', '\\1', $fieldName); + } + // Quote the fieldName in backticks with escaping, ADOdb will replace the backticks with the correct quoting + $indexFields[$key] = '`' . str_replace('`', '``', $fieldName) . '`'; + } + + return $this->databaseConnection->handlerInstance[$this->databaseConnection->handler_getFromTableList($tableName)]->DataDictionary->CreateIndexSQL( + $indexIdentifier, $this->databaseConnection->quoteName($tableName, NULL, TRUE), $indexFields, $indexOptions + ); + } + + /** + * Compiles DROP INDEX statements from component information + * + * MySQL only needs uniqueness of index names per table, but many DBMS require uniqueness of index names per schema. + * The table name is hashed and prepended to the index name to make sure index names are unique. + * + * @param $indexName + * @param $tableName + * @return array + * @see compileALTERTABLE() + */ + protected function compileDROPINDEX($indexName, $tableName) { + $indexIdentifier = $this->databaseConnection->quoteName(hash('crc32b', $tableName) . '_' . $indexName, NULL, TRUE); + + return $this->databaseConnection->handlerInstance[$this->databaseConnection->handler_getFromTableList($tableName)]->DataDictionary->DropIndexSQL( + $indexIdentifier, $this->databaseConnection->quoteName($tableName) + ); + } + + /** + * Compiles a "SELECT [output] FROM..:" field list based on input array (made with ->parseFieldList()) + * Can also compile field lists for ORDER BY and GROUP BY. + * + * @param array $selectFields Array of select fields, (made with ->parseFieldList()) + * @param bool $compileComments Whether comments should be compiled + * @param bool $functionMapping Whether function mapping should take place + * @return string Select field string + * @see parseFieldList() + */ + public function compileFieldList($selectFields, $compileComments = TRUE, $functionMapping = TRUE) { + $output = ''; + // Traverse the selectFields if any: + if (is_array($selectFields)) { + $outputParts = array(); + foreach ($selectFields as $k => $v) { + // Detecting type: + switch ($v['type']) { + case 'function': + $outputParts[$k] = $v['function'] . '(' . $v['func_content'] . ')'; + break; + case 'flow-control': + if ($v['flow-control']['type'] === 'CASE') { + $outputParts[$k] = $this->compileCaseStatement($v['flow-control'], $functionMapping); + } + break; + case 'field': + $outputParts[$k] = ($v['distinct'] ? $v['distinct'] : '') . ($v['table'] ? $v['table'] . '.' : '') . $v['field']; + break; + } + // Alias: + if ($v['as']) { + $outputParts[$k] .= ' ' . $v['as_keyword'] . ' ' . $v['as']; + } + // Specifically for ORDER BY and GROUP BY field lists: + if ($v['sortDir']) { + $outputParts[$k] .= ' ' . $v['sortDir']; + } + } + // @todo Handle SQL hints in comments according to current DBMS + if (FALSE && $selectFields[0]['comments']) { + $output = $selectFields[0]['comments'] . ' '; + } + $output .= implode(', ', $outputParts); + } + return $output; + } + + /** + * Add slashes function used for compiling queries + * This method overrides the method from \TYPO3\CMS\Dbal\Database\NativeSqlParser because + * the input string is already properly escaped. + * + * @param string $str Input string + * @return string Output string + */ + protected function compileAddslashes($str) { + return $str; + } + + /** + * Compile field definition + * + * @param array $fieldCfg Field definition parts + * @return string Field definition string + */ + protected function compileFieldCfg($fieldCfg) { + // Set type: + $type = $this->databaseConnection->getSpecifics()->getMetaFieldType($fieldCfg['fieldType']); + $cfg = $type; + // Add value, if any: + if ((string)$fieldCfg['value'] !== '' && in_array($type, array('C', 'C2'))) { + $cfg .= ' ' . $fieldCfg['value']; + } elseif (!isset($fieldCfg['value']) && in_array($type, array('C', 'C2'))) { + $cfg .= ' 255'; + } + // Add additional features: + $noQuote = TRUE; + if (is_array($fieldCfg['featureIndex'])) { + // MySQL assigns DEFAULT value automatically if NOT NULL, fake this here + // numeric fields get 0 as default, other fields an empty string + if (isset($fieldCfg['featureIndex']['NOTNULL']) && !isset($fieldCfg['featureIndex']['DEFAULT']) && !isset($fieldCfg['featureIndex']['AUTO_INCREMENT'])) { + switch ($type) { + case 'I8': + + case 'F': + + case 'N': + $fieldCfg['featureIndex']['DEFAULT'] = array('keyword' => 'DEFAULT', 'value' => array('0', '')); + break; + default: + $fieldCfg['featureIndex']['DEFAULT'] = array('keyword' => 'DEFAULT', 'value' => array('', '\'')); + } + } + foreach ($fieldCfg['featureIndex'] as $feature => $featureDef) { + switch (TRUE) { + case $feature === 'UNSIGNED' && !$this->databaseConnection->runningADOdbDriver('mysql'): + case $feature === 'NOTNULL' && $this->databaseConnection->runningADOdbDriver('oci8'): + continue; + case $feature === 'AUTO_INCREMENT': + $cfg .= ' AUTOINCREMENT'; + break; + case $feature === 'NOTNULL': + $cfg .= ' NOTNULL'; + break; + default: + $cfg .= ' ' . $featureDef['keyword']; + } + // Add value if found: + if (is_array($featureDef['value'])) { + if ($featureDef['value'][0] === '') { + $cfg .= ' "\'\'"'; + } else { + $cfg .= ' ' . $featureDef['value'][1] . $this->compileAddslashes($featureDef['value'][0]) . $featureDef['value'][1]; + if (!is_numeric($featureDef['value'][0])) { + $noQuote = FALSE; + } + } + } + } + } + if ($noQuote) { + $cfg .= ' NOQUOTE'; + } + // Return field definition string: + return $cfg; + } + + /** + * Implodes an array of WHERE clause configuration into a WHERE clause. + * + * DBAL-specific: The only(!) handled "calc" operators supported by parseWhereClause() are: + * - the bitwise logical and (&) + * - the addition (+) + * - the substraction (-) + * - the multiplication (*) + * - the division (/) + * - the modulo (%) + * + * @param array $clauseArray + * @param bool $functionMapping + * @return string WHERE clause as string. + * @see \TYPO3\CMS\Core\Database\SqlParser::parseWhereClause() + */ + public function compileWhereClause($clauseArray, $functionMapping = TRUE) { + // Prepare buffer variable: + $output = ''; + // Traverse clause array: + if (is_array($clauseArray)) { + foreach ($clauseArray as $v) { + // Set operator: + $output .= $v['operator'] ? ' ' . $v['operator'] : ''; + // Look for sublevel: + if (is_array($v['sub'])) { + $output .= ' (' . trim($this->compileWhereClause($v['sub'], $functionMapping)) . ')'; + } elseif (isset($v['func']) && $v['func']['type'] === 'EXISTS') { + $output .= ' ' . trim($v['modifier']) . ' EXISTS (' . $this->compileSELECT($v['func']['subquery']) . ')'; + } else { + if (isset($v['func']) && $v['func']['type'] === 'LOCATE') { + $output .= ' ' . trim($v['modifier']); + switch (TRUE) { + case $this->databaseConnection->runningADOdbDriver('mssql') && $functionMapping: + $output .= ' CHARINDEX('; + $output .= $v['func']['substr'][1] . $v['func']['substr'][0] . $v['func']['substr'][1]; + $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= isset($v['func']['pos']) ? ', ' . $v['func']['pos'][0] : ''; + $output .= ')'; + break; + case $this->databaseConnection->runningADOdbDriver('oci8') && $functionMapping: + $output .= ' INSTR('; + $output .= ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= ', ' . $v['func']['substr'][1] . $v['func']['substr'][0] . $v['func']['substr'][1]; + $output .= isset($v['func']['pos']) ? ', ' . $v['func']['pos'][0] : ''; + $output .= ')'; + break; + default: + $output .= ' LOCATE('; + $output .= $v['func']['substr'][1] . $v['func']['substr'][0] . $v['func']['substr'][1]; + $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= isset($v['func']['pos']) ? ', ' . $v['func']['pos'][0] : ''; + $output .= ')'; + } + } elseif (isset($v['func']) && $v['func']['type'] === 'IFNULL') { + $output .= ' ' . trim($v['modifier']) . ' '; + switch (TRUE) { + case $this->databaseConnection->runningADOdbDriver('mssql') && $functionMapping: + $output .= 'ISNULL'; + break; + case $this->databaseConnection->runningADOdbDriver('oci8') && $functionMapping: + $output .= 'NVL'; + break; + default: + $output .= 'IFNULL'; + } + $output .= '('; + $output .= ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= ', ' . $v['func']['default'][1] . $this->compileAddslashes($v['func']['default'][0]) . $v['func']['default'][1]; + $output .= ')'; + } elseif (isset($v['func']) && $v['func']['type'] === 'FIND_IN_SET') { + $output .= ' ' . trim($v['modifier']) . ' '; + if ($functionMapping) { + switch (TRUE) { + case $this->databaseConnection->runningADOdbDriver('mssql'): + $field = ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + if (!isset($v['func']['str_like'])) { + $v['func']['str_like'] = $v['func']['str'][0]; + } + $output .= '\',\'+' . $field . '+\',\' LIKE \'%,' . $v['func']['str_like'] . ',%\''; + break; + case $this->databaseConnection->runningADOdbDriver('oci8'): + $field = ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + if (!isset($v['func']['str_like'])) { + $v['func']['str_like'] = $v['func']['str'][0]; + } + $output .= '\',\'||' . $field . '||\',\' LIKE \'%,' . $v['func']['str_like'] . ',%\''; + break; + case $this->databaseConnection->runningADOdbDriver('postgres'): + $output .= ' FIND_IN_SET('; + $output .= $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1]; + $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= ') != 0'; + break; + default: + $field = ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + if (!isset($v['func']['str_like'])) { + $v['func']['str_like'] = $v['func']['str'][0]; + } + $output .= '(' . $field . ' LIKE \'%,' . $v['func']['str_like'] . ',%\'' . ' OR ' . $field . ' LIKE \'' . $v['func']['str_like'] . ',%\'' . ' OR ' . $field . ' LIKE \'%,' . $v['func']['str_like'] . '\'' . ' OR ' . $field . '= ' . $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1] . ')'; + } + } else { + switch (TRUE) { + case $this->databaseConnection->runningADOdbDriver('mssql'): + + case $this->databaseConnection->runningADOdbDriver('oci8'): + + case $this->databaseConnection->runningADOdbDriver('postgres'): + $output .= ' FIND_IN_SET('; + $output .= $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1]; + $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= ')'; + break; + default: + $field = ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + if (!isset($v['func']['str_like'])) { + $v['func']['str_like'] = $v['func']['str'][0]; + } + $output .= '(' . $field . ' LIKE \'%,' . $v['func']['str_like'] . ',%\'' . ' OR ' . $field . ' LIKE \'' . $v['func']['str_like'] . ',%\'' . ' OR ' . $field . ' LIKE \'%,' . $v['func']['str_like'] . '\'' . ' OR ' . $field . '= ' . $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1] . ')'; + } + } + } else { + // Set field/table with modifying prefix if any: + $output .= ' ' . trim($v['modifier']) . ' '; + // DBAL-specific: Set calculation, if any: + if ($v['calc'] === '&' && $functionMapping) { + switch (TRUE) { + case $this->databaseConnection->runningADOdbDriver('oci8'): + // Oracle only knows BITAND(x,y) - sigh + $output .= 'BITAND(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . ',' . $v['calc_value'][1] . $this->compileAddslashes($v['calc_value'][0]) . $v['calc_value'][1] . ')'; + break; + default: + // MySQL, MS SQL Server, PostgreSQL support the &-syntax + $output .= trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . $v['calc'] . $v['calc_value'][1] . $this->compileAddslashes($v['calc_value'][0]) . $v['calc_value'][1]; + } + } elseif ($v['calc']) { + $output .= trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . $v['calc']; + if (isset($v['calc_table'])) { + $output .= trim(($v['calc_table'] ? $v['calc_table'] . '.' : '') . $v['calc_field']); + } else { + $output .= $v['calc_value'][1] . $this->compileAddslashes($v['calc_value'][0]) . $v['calc_value'][1]; + } + } elseif (!($this->databaseConnection->runningADOdbDriver('oci8') && preg_match('/(NOT )?LIKE( BINARY)?/', $v['comparator']) && $functionMapping)) { + $output .= trim(($v['table'] ? $v['table'] . '.' : '') . $v['field']); + } + } + // Set comparator: + if ($v['comparator']) { + $isLikeOperator = preg_match('/(NOT )?LIKE( BINARY)?/', $v['comparator']); + switch (TRUE) { + case $this->databaseConnection->runningADOdbDriver('oci8') && $isLikeOperator && $functionMapping: + // Oracle cannot handle LIKE on CLOB fields - sigh + if (isset($v['value']['operator'])) { + $values = array(); + foreach ($v['value']['args'] as $fieldDef) { + $values[] = ($fieldDef['table'] ? $fieldDef['table'] . '.' : '') . $fieldDef['field']; + } + $compareValue = ' ' . $v['value']['operator'] . '(' . implode(',', $values) . ')'; + } else { + $compareValue = $v['value'][1] . $this->compileAddslashes(trim($v['value'][0], '%')) . $v['value'][1]; + } + if (GeneralUtility::isFirstPartOfStr($v['comparator'], 'NOT')) { + $output .= 'NOT '; + } + // To be on the safe side + $isLob = TRUE; + if ($v['table']) { + // Table and field names are quoted: + $tableName = substr($v['table'], 1, strlen($v['table']) - 2); + $fieldName = substr($v['field'], 1, strlen($v['field']) - 2); + $fieldType = $this->databaseConnection->sql_field_metatype($tableName, $fieldName); + $isLob = $fieldType === 'B' || $fieldType === 'XL'; + } + if (strtoupper(substr($v['comparator'], -6)) === 'BINARY') { + if ($isLob) { + $output .= '(dbms_lob.instr(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . ', ' . $compareValue . ',1,1) > 0)'; + } else { + $output .= '(instr(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . ', ' . $compareValue . ',1,1) > 0)'; + } + } else { + if ($isLob) { + $output .= '(dbms_lob.instr(LOWER(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . '), ' . GeneralUtility::strtolower($compareValue) . ',1,1) > 0)'; + } else { + $output .= '(instr(LOWER(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . '), ' . GeneralUtility::strtolower($compareValue) . ',1,1) > 0)'; + } + } + break; + default: + if ($isLikeOperator && $functionMapping) { + if ($this->databaseConnection->runningADOdbDriver('postgres') || $this->databaseConnection->runningADOdbDriver('postgres64') || $this->databaseConnection->runningADOdbDriver('postgres7') || $this->databaseConnection->runningADOdbDriver('postgres8')) { + // Remap (NOT)? LIKE to (NOT)? ILIKE + // and (NOT)? LIKE BINARY to (NOT)? LIKE + switch ($v['comparator']) { + case 'LIKE': + $v['comparator'] = 'ILIKE'; + break; + case 'NOT LIKE': + $v['comparator'] = 'NOT ILIKE'; + break; + default: + $v['comparator'] = str_replace(' BINARY', '', $v['comparator']); + } + } else { + // No more BINARY operator + $v['comparator'] = str_replace(' BINARY', '', $v['comparator']); + } + } + $output .= ' ' . $v['comparator']; + // Detecting value type; list or plain: + $comparator = SqlParser::normalizeKeyword($v['comparator']); + if (GeneralUtility::inList('NOTIN,IN', $comparator)) { + if (isset($v['subquery'])) { + $output .= ' (' . $this->compileSELECT($v['subquery']) . ')'; + } else { + $valueBuffer = array(); + foreach ($v['value'] as $realValue) { + $valueBuffer[] = $realValue[1] . $this->compileAddslashes($realValue[0]) . $realValue[1]; + } + + $dbmsSpecifics = $this->databaseConnection->getSpecifics(); + if ($dbmsSpecifics === NULL) { + $output .= ' (' . trim(implode(',', $valueBuffer)) . ')'; + } else { + $chunkedList = $dbmsSpecifics->splitMaxExpressions($valueBuffer); + $chunkCount = count($chunkedList); + + if ($chunkCount === 1) { + $output .= ' (' . trim(implode(',', $valueBuffer)) . ')'; + } else { + $listExpressions = array(); + $field = trim(($v['table'] ? $v['table'] . '.' : '') . $v['field']); + + switch ($comparator) { + case 'IN': + $operator = 'OR'; + break; + case 'NOTIN': + $operator = 'AND'; + break; + default: + $operator = ''; + } + + for ($i = 0; $i < $chunkCount; ++$i) { + $listPart = trim(implode(',', $chunkedList[$i])); + $listExpressions[] = ' (' . $listPart . ')'; + } + + $implodeString = ' ' . $operator . ' ' . $field . ' ' . $v['comparator']; + + // add opening brace before field + $lastFieldPos = strrpos($output, $field); + $output = substr_replace($output, '(', $lastFieldPos, 0); + $output .= implode($implodeString, $listExpressions) . ')'; + } + } + } + } elseif (GeneralUtility::inList('BETWEEN,NOT BETWEEN', $v['comparator'])) { + $lbound = $v['values'][0]; + $ubound = $v['values'][1]; + $output .= ' ' . $lbound[1] . $this->compileAddslashes($lbound[0]) . $lbound[1]; + $output .= ' AND '; + $output .= $ubound[1] . $this->compileAddslashes($ubound[0]) . $ubound[1]; + } elseif (isset($v['value']['operator'])) { + $values = array(); + foreach ($v['value']['args'] as $fieldDef) { + $values[] = ($fieldDef['table'] ? $fieldDef['table'] . '.' : '') . $fieldDef['field']; + } + $output .= ' ' . $v['value']['operator'] . '(' . implode(',', $values) . ')'; + } else { + $output .= ' ' . $v['value'][1] . $this->compileAddslashes($v['value'][0]) . $v['value'][1]; + } + } + } + } + } + } + return $output; + } + +} diff --git a/typo3/sysext/dbal/Classes/Database/SqlCompilers/Mysql.php b/typo3/sysext/dbal/Classes/Database/SqlCompilers/Mysql.php new file mode 100644 index 000000000000..08dbfb3bd54d --- /dev/null +++ b/typo3/sysext/dbal/Classes/Database/SqlCompilers/Mysql.php @@ -0,0 +1,311 @@ +<?php +namespace TYPO3\CMS\Dbal\Database\SqlCompilers; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Dbal\Database\SqlParser; + +/** + * SQL Compiler for native MySQL connections + */ +class Mysql extends AbstractCompiler { + + /** + * Compiles an INSERT statement from components array + * + * @param array $components Array of SQL query components + * @return string SQL INSERT query + * @see parseINSERT() + */ + protected function compileINSERT($components) { + $values = array(); + if (isset($components['VALUES_ONLY']) && is_array($components['VALUES_ONLY'])) { + $valuesComponents = $components['EXTENDED'] === '1' ? $components['VALUES_ONLY'] : array($components['VALUES_ONLY']); + $tableFields = array(); + } else { + $valuesComponents = $components['EXTENDED'] === '1' ? $components['FIELDS'] : array($components['FIELDS']); + $tableFields = array_keys($valuesComponents[0]); + } + foreach ($valuesComponents as $valuesComponent) { + $fields = array(); + foreach ($valuesComponent as $fV) { + $fields[] = $fV[1] . $this->compileAddslashes($fV[0]) . $fV[1]; + } + $values[] = '(' . implode(',', $fields) . ')'; + } + // Make query: + $query = 'INSERT INTO ' . $components['TABLE']; + if (!empty($tableFields)) { + $query .= ' (' . implode(',', $tableFields) . ')'; + } + $query .= ' VALUES ' . implode(',', $values); + + return $query; + } + + /** + * Compiles a CREATE TABLE statement from components array + * + * @param array $components Array of SQL query components + * @return string SQL CREATE TABLE query + * @see parseCREATETABLE() + */ + protected function compileCREATETABLE($components) { + // Create fields and keys: + $fieldsKeys = array(); + foreach ($components['FIELDS'] as $fN => $fCfg) { + $fieldsKeys[] = $fN . ' ' . $this->compileFieldCfg($fCfg['definition']); + } + if ($components['KEYS']) { + foreach ($components['KEYS'] as $kN => $kCfg) { + if ($kN === 'PRIMARYKEY') { + $fieldsKeys[] = 'PRIMARY KEY (' . implode(',', $kCfg) . ')'; + } elseif ($kN === 'UNIQUE') { + $key = key($kCfg); + $fields = current($kCfg); + $fieldsKeys[] = 'UNIQUE KEY ' . $key . ' (' . implode(',', $fields) . ')'; + } else { + $fieldsKeys[] = 'KEY ' . $kN . ' (' . implode(',', $kCfg) . ')'; + } + } + } + // Make query: + $query = 'CREATE TABLE ' . $components['TABLE'] . ' (' . + implode(',', $fieldsKeys) . ')' . + ($components['engine'] ? ' ENGINE=' . $components['engine'] : ''); + + return $query; + } + + /** + * Compiles an ALTER TABLE statement from components array + * + * @param array $components Array of SQL query components + * @return string SQL ALTER TABLE query + * @see parseALTERTABLE() + */ + protected function compileALTERTABLE($components) { + // Make query: + $query = 'ALTER TABLE ' . $components['TABLE'] . ' ' . $components['action'] . ' ' . ($components['FIELD'] ?: $components['KEY']); + // Based on action, add the final part: + switch (SqlParser::normalizeKeyword($components['action'])) { + case 'ADD': + $query .= ' ' . $this->compileFieldCfg($components['definition']); + break; + case 'CHANGE': + $query .= ' ' . $components['newField'] . ' ' . $this->compileFieldCfg($components['definition']); + break; + case 'DROP': + case 'DROPKEY': + break; + case 'ADDKEY': + case 'ADDPRIMARYKEY': + case 'ADDUNIQUE': + $query .= ' (' . implode(',', $components['fields']) . ')'; + break; + case 'DEFAULTCHARACTERSET': + $query .= $components['charset']; + break; + case 'ENGINE': + $query .= '= ' . $components['engine']; + break; + } + // Return query + return $query; + } + + /** + * Compiles a "SELECT [output] FROM..:" field list based on input array (made with ->parseFieldList()) + * Can also compile field lists for ORDER BY and GROUP BY. + * + * @param array $selectFields Array of select fields, (made with ->parseFieldList()) + * @param bool $compileComments Whether comments should be compiled + * @param bool $functionMapping + * @return string Select field string + * @see parseFieldList() + */ + public function compileFieldList($selectFields, $compileComments = TRUE, $functionMapping = TRUE) { + // Prepare buffer variable: + $fields = ''; + // Traverse the selectFields if any: + if (is_array($selectFields)) { + $outputParts = array(); + foreach ($selectFields as $k => $v) { + // Detecting type: + switch ($v['type']) { + case 'function': + $outputParts[$k] = $v['function'] . '(' . $v['func_content'] . ')'; + break; + case 'flow-control': + if ($v['flow-control']['type'] === 'CASE') { + $outputParts[$k] = $this->compileCaseStatement($v['flow-control']); + } + break; + case 'field': + $outputParts[$k] = ($v['distinct'] ? $v['distinct'] : '') . ($v['table'] ? $v['table'] . '.' : '') . $v['field']; + break; + } + // Alias: + if ($v['as']) { + $outputParts[$k] .= ' ' . $v['as_keyword'] . ' ' . $v['as']; + } + // Specifically for ORDER BY and GROUP BY field lists: + if ($v['sortDir']) { + $outputParts[$k] .= ' ' . $v['sortDir']; + } + } + if ($compileComments && $selectFields[0]['comments']) { + $fields = $selectFields[0]['comments'] . ' '; + } + $fields .= implode(', ', $outputParts); + } + return $fields; + } + + /** + * Add slashes function used for compiling queries + * This method overrides the method from \TYPO3\CMS\Dbal\Database\NativeSqlParser because + * the input string is already properly escaped. + * + * @param string $str Input string + * @return string Output string + */ + protected function compileAddslashes($str) { + $search = array('\\', '\'', '"', "\x00", "\x0a", "\x0d", "\x1a"); + $replace = array('\\\\', '\\\'', '\\"', '\0', '\n', '\r', '\Z'); + + return str_replace($search, $replace, $str); + } + + /** + * Compile field definition + * + * @param array $fieldCfg Field definition parts + * @return string Field definition string + */ + public function compileFieldCfg($fieldCfg) { + // Set type: + $cfg = $fieldCfg['fieldType']; + // Add value, if any: + if ((string)$fieldCfg['value'] !== '') { + $cfg .= '(' . $fieldCfg['value'] . ')'; + } + // Add additional features: + if (is_array($fieldCfg['featureIndex'])) { + foreach ($fieldCfg['featureIndex'] as $featureDef) { + $cfg .= ' ' . $featureDef['keyword']; + // Add value if found: + if (is_array($featureDef['value'])) { + $cfg .= ' ' . $featureDef['value'][1] . $this->compileAddslashes($featureDef['value'][0]) . $featureDef['value'][1]; + } + } + } + // Return field definition string: + return $cfg; + } + + /** + * Implodes an array of WHERE clause configuration into a WHERE clause. + * + * @param array $clauseArray WHERE clause configuration + * @param bool $functionMapping + * @return string WHERE clause as string. + * @see explodeWhereClause() + */ + public function compileWhereClause($clauseArray, $functionMapping = TRUE) { + // Prepare buffer variable: + $output = ''; + // Traverse clause array: + if (is_array($clauseArray)) { + foreach ($clauseArray as $k => $v) { + // Set operator: + $output .= $v['operator'] ? ' ' . $v['operator'] : ''; + // Look for sublevel: + if (is_array($v['sub'])) { + $output .= ' (' . trim($this->compileWhereClause($v['sub'])) . ')'; + } elseif (isset($v['func']) && $v['func']['type'] === 'EXISTS') { + $output .= ' ' . trim($v['modifier']) . ' EXISTS (' . $this->compileSELECT($v['func']['subquery']) . ')'; + } else { + if (isset($v['func']) && $v['func']['type'] === 'LOCATE') { + $output .= ' ' . trim($v['modifier']) . ' LOCATE('; + $output .= $v['func']['substr'][1] . $v['func']['substr'][0] . $v['func']['substr'][1]; + $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= isset($v['func']['pos']) ? ', ' . $v['func']['pos'][0] : ''; + $output .= ')'; + } elseif (isset($v['func']) && $v['func']['type'] === 'IFNULL') { + $output .= ' ' . trim($v['modifier']) . ' IFNULL('; + $output .= ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= ', ' . $v['func']['default'][1] . $this->compileAddslashes($v['func']['default'][0]) . $v['func']['default'][1]; + $output .= ')'; + } elseif (isset($v['func']) && $v['func']['type'] === 'CAST') { + $output .= ' ' . trim($v['modifier']) . ' CAST('; + $output .= ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= ' AS ' . $v['func']['datatype'][0]; + $output .= ')'; + } elseif (isset($v['func']) && $v['func']['type'] === 'FIND_IN_SET') { + $output .= ' ' . trim($v['modifier']) . ' FIND_IN_SET('; + $output .= $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1]; + $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; + $output .= ')'; + } else { + // Set field/table with modifying prefix if any: + $output .= ' ' . trim(($v['modifier'] . ' ' . ($v['table'] ? $v['table'] . '.' : '') . $v['field'])); + // Set calculation, if any: + if ($v['calc']) { + $output .= $v['calc'] . $v['calc_value'][1] . $this->compileAddslashes($v['calc_value'][0]) . $v['calc_value'][1]; + } + } + // Set comparator: + if ($v['comparator']) { + $output .= ' ' . $v['comparator']; + // Detecting value type; list or plain: + if (GeneralUtility::inList('NOTIN,IN', SqlParser::normalizeKeyword($v['comparator']))) { + if (isset($v['subquery'])) { + $output .= ' (' . $this->compileSELECT($v['subquery']) . ')'; + } else { + $valueBuffer = array(); + foreach ($v['value'] as $realValue) { + $valueBuffer[] = $realValue[1] . $this->compileAddslashes($realValue[0]) . $realValue[1]; + } + $output .= ' (' . trim(implode(',', $valueBuffer)) . ')'; + } + } else { + if (GeneralUtility::inList('BETWEEN,NOT BETWEEN', $v['comparator'])) { + $lbound = $v['values'][0]; + $ubound = $v['values'][1]; + $output .= ' ' . $lbound[1] . $this->compileAddslashes($lbound[0]) . $lbound[1]; + $output .= ' AND '; + $output .= $ubound[1] . $this->compileAddslashes($ubound[0]) . $ubound[1]; + } else { + if (isset($v['value']['operator'])) { + $values = array(); + foreach ($v['value']['args'] as $fieldDef) { + $values[] = ($fieldDef['table'] ? $fieldDef['table'] . '.' : '') . $fieldDef['field']; + } + $output .= ' ' . $v['value']['operator'] . '(' . implode(',', $values) . ')'; + } else { + $output .= ' ' . $v['value'][1] . $this->compileAddslashes($v['value'][0]) . $v['value'][1]; + } + } + } + } + } + } + } + // Return output buffer: + return $output; + } + +} diff --git a/typo3/sysext/dbal/Classes/Database/SqlParser.php b/typo3/sysext/dbal/Classes/Database/SqlParser.php index 387708d6f009..df47261168d1 100644 --- a/typo3/sysext/dbal/Classes/Database/SqlParser.php +++ b/typo3/sysext/dbal/Classes/Database/SqlParser.php @@ -19,20 +19,77 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; /** * PHP SQL engine / server */ -class SqlParser extends \TYPO3\CMS\Core\Database\SqlParser { +class SqlParser { + + /** + * Parsing error string + * + * @var string + */ + public $parse_error = ''; + + /** + * Last stop keyword used. + * + * @var string + */ + public $lastStopKeyWord = ''; + + /** + * Find "comparator" + * + * @var array + */ + static protected $comparatorPatterns = array( + '<=', + '>=', + '<>', + '<', + '>', + '=', + '!=', + 'NOT[[:space:]]+IN', + 'IN', + 'NOT[[:space:]]+LIKE[[:space:]]+BINARY', + 'LIKE[[:space:]]+BINARY', + 'NOT[[:space:]]+LIKE', + 'LIKE', + 'IS[[:space:]]+NOT', + 'IS', + 'BETWEEN', + 'NOT[[:space]]+BETWEEN' + ); + + /** + * Whitespaces in a query + * + * @var array + */ + static protected $interQueryWhitespaces = array(' ', TAB, CR, LF); /** * @var DatabaseConnection */ protected $databaseConnection; + /** + * @var SqlCompilers\Mysql + */ + protected $nativeSqlCompiler; + + /** + * @var SqlCompilers\Adodb + */ + + protected $sqlCompiler; + /** * @param DatabaseConnection $databaseConnection */ public function __construct(DatabaseConnection $databaseConnection = NULL) { - parent::__construct(); - $this->databaseConnection = $databaseConnection ?: $GLOBALS['TYPO3_DB']; + $this->sqlCompiler = GeneralUtility::makeInstance(SqlCompilers\Adodb::class); + $this->nativeSqlCompiler = GeneralUtility::makeInstance(SqlCompilers\Mysql::class); } /** @@ -48,15 +105,39 @@ class SqlParser extends \TYPO3\CMS\Core\Database\SqlParser { if ($this->databaseConnection->runningADOdbDriver('mssql')) { $value = $this->getValueInQuotesMssql($parseString, $quote); } else { - $value = parent::getValueInQuotes($parseString, $quote); + $value = $this->getValueInQuotesGeneric($parseString, $quote); } break; default: - $value = parent::getValueInQuotes($parseString, $quote); + $value = $this->getValueInQuotesGeneric($parseString, $quote); } return $value; } + /** + * Get value in quotes from $parseString. + * NOTICE: If a query being parsed was prepared for another database than MySQL this function should probably be changed + * + * @param string $parseString String from which to find value in quotes. Notice that $parseString is passed by reference and is shortend by the output of this function. + * @param string $quote The quote used; input either " or ' + * @return string The value, passed through stripslashes() ! + */ + protected function getValueInQuotesGeneric(&$parseString, $quote) { + $parts = explode($quote, substr($parseString, 1)); + $buffer = ''; + foreach ($parts as $k => $v) { + $buffer .= $v; + $reg = array(); + preg_match('/\\\\$/', $v, $reg); + if ($reg && strlen($reg[0]) % 2) { + $buffer .= $quote; + } else { + $parseString = ltrim(substr($parseString, strlen($buffer) + 2)); + return $this->parseStripslashes($buffer); + } + } + } + /** * Gets value in quotes from $parseString. This method targets MSSQL exclusively. * @@ -107,373 +188,1275 @@ class SqlParser extends \TYPO3\CMS\Core\Database\SqlParser { return ''; } + + /************************************* + * + * SQL Parsing, full queries + * + **************************************/ /** - * Compiles a "SELECT [output] FROM..:" field list based on input array (made with ->parseFieldList()) - * Can also compile field lists for ORDER BY and GROUP BY. + * Parses any single SQL query * - * @param array $selectFields Array of select fields, (made with ->parseFieldList()) - * @param bool $compileComments Whether comments should be compiled - * @param bool $functionMapping Whether function mapping should take place - * @return string Select field string - * @see parseFieldList() + * @param string $parseString SQL query + * @return array Result array with all the parts in - or error message string + * @see compileSQL(), debug_testSQL() */ - public function compileFieldList($selectFields, $compileComments = TRUE, $functionMapping = TRUE) { - $output = ''; - switch ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->lastHandlerKey]['type']) { - case 'native': - $output = parent::compileFieldList($selectFields, $compileComments); + public function parseSQL($parseString) { + // Prepare variables: + $parseString = $this->trimSQL($parseString); + $this->parse_error = ''; + $result = array(); + // Finding starting keyword of string: + $_parseString = $parseString; + // Protecting original string... + $keyword = $this->nextPart($_parseString, '^(SELECT|UPDATE|INSERT[[:space:]]+INTO|DELETE[[:space:]]+FROM|EXPLAIN|(DROP|CREATE|ALTER|TRUNCATE)[[:space:]]+TABLE|CREATE[[:space:]]+DATABASE)[[:space:]]+'); + $keyword = $this->normalizeKeyword($keyword); + switch ($keyword) { + case 'SELECT': + // Parsing SELECT query: + $result = $this->parseSELECT($parseString); break; - case 'adodb': - // Traverse the selectFields if any: - if (is_array($selectFields)) { - $outputParts = array(); - foreach ($selectFields as $k => $v) { - // Detecting type: - switch ($v['type']) { - case 'function': - $outputParts[$k] = $v['function'] . '(' . $v['func_content'] . ')'; - break; - case 'flow-control': - if ($v['flow-control']['type'] === 'CASE') { - $outputParts[$k] = $this->compileCaseStatement($v['flow-control'], $functionMapping); - } - break; - case 'field': - $outputParts[$k] = ($v['distinct'] ? $v['distinct'] : '') . ($v['table'] ? $v['table'] . '.' : '') . $v['field']; - break; - } - // Alias: - if ($v['as']) { - $outputParts[$k] .= ' ' . $v['as_keyword'] . ' ' . $v['as']; + case 'UPDATE': + // Parsing UPDATE query: + $result = $this->parseUPDATE($parseString); + break; + case 'INSERTINTO': + // Parsing INSERT query: + $result = $this->parseINSERT($parseString); + break; + case 'DELETEFROM': + // Parsing DELETE query: + $result = $this->parseDELETE($parseString); + break; + case 'EXPLAIN': + // Parsing EXPLAIN SELECT query: + $result = $this->parseEXPLAIN($parseString); + break; + case 'DROPTABLE': + // Parsing DROP TABLE query: + $result = $this->parseDROPTABLE($parseString); + break; + case 'ALTERTABLE': + // Parsing ALTER TABLE query: + $result = $this->parseALTERTABLE($parseString); + break; + case 'CREATETABLE': + // Parsing CREATE TABLE query: + $result = $this->parseCREATETABLE($parseString); + break; + case 'CREATEDATABASE': + // Parsing CREATE DATABASE query: + $result = $this->parseCREATEDATABASE($parseString); + break; + case 'TRUNCATETABLE': + // Parsing TRUNCATE TABLE query: + $result = $this->parseTRUNCATETABLE($parseString); + break; + default: + $result = $this->parseError('"' . $keyword . '" is not a keyword', $parseString); + } + return $result; + } + + /** + * Parsing SELECT query + * + * @param string $parseString SQL string with SELECT query to parse + * @param array $parameterReferences Array holding references to either named (:name) or question mark (?) parameters found + * @return mixed Returns array with components of SELECT query on success, otherwise an error message string. + * @see compileSELECT() + */ + protected function parseSELECT($parseString, &$parameterReferences = NULL) { + // Removing SELECT: + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr($parseString, 6)); + // Init output variable: + $result = array(); + if ($parameterReferences === NULL) { + $result['parameters'] = array(); + $parameterReferences = &$result['parameters']; + } + $result['type'] = 'SELECT'; + // Looking for STRAIGHT_JOIN keyword: + $result['STRAIGHT_JOIN'] = $this->nextPart($parseString, '^(STRAIGHT_JOIN)[[:space:]]+'); + // Select fields: + $result['SELECT'] = $this->parseFieldList($parseString, '^(FROM)[[:space:]]+'); + if ($this->parse_error) { + return $this->parse_error; + } + // Continue if string is not ended: + if ($parseString) { + // Get table list: + $result['FROM'] = $this->parseFromTables($parseString, '^(WHERE)[[:space:]]+'); + if ($this->parse_error) { + return $this->parse_error; + } + // If there are more than just the tables (a WHERE clause that would be...) + if ($parseString) { + // Get WHERE clause: + $result['WHERE'] = $this->parseWhereClause($parseString, '^((GROUP|ORDER)[[:space:]]+BY|LIMIT)[[:space:]]+', $parameterReferences); + if ($this->parse_error) { + return $this->parse_error; + } + // If the WHERE clause parsing was stopped by GROUP BY, ORDER BY or LIMIT, then proceed with parsing: + if ($this->lastStopKeyWord) { + // GROUP BY parsing: + if ($this->lastStopKeyWord === 'GROUPBY') { + $result['GROUPBY'] = $this->parseFieldList($parseString, '^(ORDER[[:space:]]+BY|LIMIT)[[:space:]]+'); + if ($this->parse_error) { + return $this->parse_error; } - // Specifically for ORDER BY and GROUP BY field lists: - if ($v['sortDir']) { - $outputParts[$k] .= ' ' . $v['sortDir']; + } + // ORDER BY parsing: + if ($this->lastStopKeyWord === 'ORDERBY') { + $result['ORDERBY'] = $this->parseFieldList($parseString, '^(LIMIT)[[:space:]]+'); + if ($this->parse_error) { + return $this->parse_error; } } - // @todo Handle SQL hints in comments according to current DBMS - if (FALSE && $selectFields[0]['comments']) { - $output = $selectFields[0]['comments'] . ' '; + // LIMIT parsing: + if ($this->lastStopKeyWord === 'LIMIT') { + if (preg_match('/^([0-9]+|[0-9]+[[:space:]]*,[[:space:]]*[0-9]+)$/', trim($parseString))) { + $result['LIMIT'] = $parseString; + } else { + return $this->parseError('No value for limit!', $parseString); + } } - $output .= implode(', ', $outputParts); } - break; + } + } else { + return $this->parseError('No table to select from!', $parseString); } - return $output; + // Store current parseString in the result array for possible further processing (e.g., subquery support by DBAL) + $result['parseString'] = $parseString; + // Return result: + return $result; } /** - * Compiles a CASE ... WHEN flow-control construct based on input array (made with ->parseCaseStatement()) + * Parsing UPDATE query * - * @param array $components Array of case components, (made with ->parseCaseStatement()) - * @param bool $functionMapping Whether function mapping should take place - * @return string case when string - * @see parseCaseStatement() + * @param string $parseString SQL string with UPDATE query to parse + * @return mixed Returns array with components of UPDATE query on success, otherwise an error message string. + * @see compileUPDATE() */ - protected function compileCaseStatement(array $components, $functionMapping = TRUE) { - $output = ''; - switch ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->lastHandlerKey]['type']) { - case 'native': - $output = parent::compileCaseStatement($components); - break; - case 'adodb': - $statement = 'CASE'; - if (isset($components['case_field'])) { - $statement .= ' ' . $components['case_field']; - } elseif (isset($components['case_value'])) { - $statement .= ' ' . $components['case_value'][1] . $components['case_value'][0] . $components['case_value'][1]; - } - foreach ($components['when'] as $when) { - $statement .= ' WHEN '; - $statement .= $this->compileWhereClause($when['when_value'], $functionMapping); - $statement .= ' THEN '; - $statement .= $when['then_value'][1] . $when['then_value'][0] . $when['then_value'][1]; - } - if (isset($components['else'])) { - $statement .= ' ELSE '; - $statement .= $components['else'][1] . $components['else'][0] . $components['else'][1]; - } - $statement .= ' END'; - $output = $statement; - break; + protected function parseUPDATE($parseString) { + // Removing UPDATE + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr($parseString, 6)); + // Init output variable: + $result = array(); + $result['type'] = 'UPDATE'; + // Get table: + $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); + // Continue if string is not ended: + if ($result['TABLE']) { + if ($parseString && $this->nextPart($parseString, '^(SET)[[:space:]]+')) { + $comma = TRUE; + // Get field/value pairs: + while ($comma) { + if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]*=')) { + // Strip off "=" sign. + $this->nextPart($parseString, '^(=)'); + $value = $this->getValue($parseString); + $result['FIELDS'][$fieldName] = $value; + } else { + return $this->parseError('No fieldname found', $parseString); + } + $comma = $this->nextPart($parseString, '^(,)'); + } + // WHERE + if ($this->nextPart($parseString, '^(WHERE)')) { + $result['WHERE'] = $this->parseWhereClause($parseString); + if ($this->parse_error) { + return $this->parse_error; + } + } + } else { + return $this->parseError('Query missing SET...', $parseString); + } + } else { + return $this->parseError('No table found!', $parseString); + } + // Should be no more content now: + if ($parseString) { + return $this->parseError('Still content in clause after parsing!', $parseString); } - return $output; + // Return result: + return $result; } /** - * Add slashes function used for compiling queries - * This method overrides the method from \TYPO3\CMS\Core\Database\SqlParser because - * the input string is already properly escaped. + * Parsing INSERT query * - * @param string $str Input string - * @return string Output string + * @param string $parseString SQL string with INSERT query to parse + * @return mixed Returns array with components of INSERT query on success, otherwise an error message string. + * @see compileINSERT() */ - protected function compileAddslashes($str) { - return $str; + protected function parseINSERT($parseString) { + // Removing INSERT + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 4)); + // Init output variable: + $result = array(); + $result['type'] = 'INSERT'; + // Get table: + $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)([[:space:]]+|\\()'); + if ($result['TABLE']) { + // In this case there are no field names mentioned in the SQL! + if ($this->nextPart($parseString, '^(VALUES)([[:space:]]+|\\()')) { + // Get values/fieldnames (depending...) + $result['VALUES_ONLY'] = $this->getValue($parseString, 'IN'); + if ($this->parse_error) { + return $this->parse_error; + } + if (preg_match('/^,/', $parseString)) { + $result['VALUES_ONLY'] = array($result['VALUES_ONLY']); + $result['EXTENDED'] = '1'; + while ($this->nextPart($parseString, '^(,)') === ',') { + $result['VALUES_ONLY'][] = $this->getValue($parseString, 'IN'); + if ($this->parse_error) { + return $this->parse_error; + } + } + } + } else { + // There are apparently fieldnames listed: + $fieldNames = $this->getValue($parseString, '_LIST'); + if ($this->parse_error) { + return $this->parse_error; + } + // "VALUES" keyword binds the fieldnames to values: + if ($this->nextPart($parseString, '^(VALUES)([[:space:]]+|\\()')) { + $result['FIELDS'] = array(); + do { + // Using the "getValue" function to get the field list... + $values = $this->getValue($parseString, 'IN'); + if ($this->parse_error) { + return $this->parse_error; + } + $insertValues = array(); + foreach ($fieldNames as $k => $fN) { + if (preg_match('/^[[:alnum:]_]+$/', $fN)) { + if (isset($values[$k])) { + if (!isset($insertValues[$fN])) { + $insertValues[$fN] = $values[$k]; + } else { + return $this->parseError('Fieldname ("' . $fN . '") already found in list!', $parseString); + } + } else { + return $this->parseError('No value set!', $parseString); + } + } else { + return $this->parseError('Invalid fieldname ("' . $fN . '")', $parseString); + } + } + if (isset($values[$k + 1])) { + return $this->parseError('Too many values in list!', $parseString); + } + $result['FIELDS'][] = $insertValues; + } while ($this->nextPart($parseString, '^(,)') === ','); + if (count($result['FIELDS']) === 1) { + $result['FIELDS'] = $result['FIELDS'][0]; + } else { + $result['EXTENDED'] = '1'; + } + } else { + return $this->parseError('VALUES keyword expected', $parseString); + } + } + } else { + return $this->parseError('No table found!', $parseString); + } + // Should be no more content now: + if ($parseString) { + return $this->parseError('Still content after parsing!', $parseString); + } + // Return result + return $result; } - /************************* + /** + * Parsing DELETE query * - * Compiling queries + * @param string $parseString SQL string with DELETE query to parse + * @return mixed Returns array with components of DELETE query on success, otherwise an error message string. + * @see compileDELETE() + */ + protected function parseDELETE($parseString) { + // Removing DELETE + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 4)); + // Init output variable: + $result = array(); + $result['type'] = 'DELETE'; + // Get table: + $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); + if ($result['TABLE']) { + // WHERE + if ($this->nextPart($parseString, '^(WHERE)')) { + $result['WHERE'] = $this->parseWhereClause($parseString); + if ($this->parse_error) { + return $this->parse_error; + } + } + } else { + return $this->parseError('No table found!', $parseString); + } + // Should be no more content now: + if ($parseString) { + return $this->parseError('Still content in clause after parsing!', $parseString); + } + // Return result: + return $result; + } + + /** + * Parsing EXPLAIN query * - *************************/ + * @param string $parseString SQL string with EXPLAIN query to parse + * @return mixed Returns array with components of EXPLAIN query on success, otherwise an error message string. + * @see parseSELECT() + */ + protected function parseEXPLAIN($parseString) { + // Removing EXPLAIN + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr($parseString, 6)); + // Init output variable: + $result = $this->parseSELECT($parseString); + if (is_array($result)) { + $result['type'] = 'EXPLAIN'; + } + return $result; + } + /** - * Compiles an INSERT statement from components array + * Parsing CREATE TABLE query * - * @param array Array of SQL query components - * @return string SQL INSERT query / array - * @see parseINSERT() + * @param string $parseString SQL string starting with CREATE TABLE + * @return mixed Returns array with components of CREATE TABLE query on success, otherwise an error message string. + * @see compileCREATETABLE() */ - protected function compileINSERT($components) { - $query = ''; - switch ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->lastHandlerKey]['type']) { - case 'native': - $query = parent::compileINSERT($components); - break; - case 'adodb': - $values = array(); - if (isset($components['VALUES_ONLY']) && is_array($components['VALUES_ONLY'])) { - $valuesComponents = $components['EXTENDED'] === '1' ? $components['VALUES_ONLY'] : array($components['VALUES_ONLY']); - $tableFields = array_keys($this->databaseConnection->cache_fieldType[$components['TABLE']]); - } else { - $valuesComponents = $components['EXTENDED'] === '1' ? $components['FIELDS'] : array($components['FIELDS']); - $tableFields = array_keys($valuesComponents[0]); - } - foreach ($valuesComponents as $valuesComponent) { - $fields = array(); - $fc = 0; - foreach ($valuesComponent as $fV) { - $fields[$tableFields[$fc++]] = $fV[0]; + protected function parseCREATETABLE($parseString) { + // Removing CREATE TABLE + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 5)); + // Init output variable: + $result = array(); + $result['type'] = 'CREATETABLE'; + // Get table: + $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]*\\(', TRUE); + if ($result['TABLE']) { + // While the parseString is not yet empty: + while ($parseString !== '') { + // Getting key + if ($key = $this->nextPart($parseString, '^(KEY|PRIMARY KEY|UNIQUE KEY|UNIQUE)([[:space:]]+|\\()')) { + $key = $this->normalizeKeyword($key); + switch ($key) { + case 'PRIMARYKEY': + $result['KEYS']['PRIMARYKEY'] = $this->getValue($parseString, '_LIST'); + if ($this->parse_error) { + return $this->parse_error; + } + break; + case 'UNIQUE': + + case 'UNIQUEKEY': + if ($keyName = $this->nextPart($parseString, '^([[:alnum:]_]+)([[:space:]]+|\\()')) { + $result['KEYS']['UNIQUE'] = array($keyName => $this->getValue($parseString, '_LIST')); + if ($this->parse_error) { + return $this->parse_error; + } + } else { + return $this->parseError('No keyname found', $parseString); + } + break; + case 'KEY': + if ($keyName = $this->nextPart($parseString, '^([[:alnum:]_]+)([[:space:]]+|\\()')) { + $result['KEYS'][$keyName] = $this->getValue($parseString, '_LIST', 'INDEX'); + if ($this->parse_error) { + return $this->parse_error; + } + } else { + return $this->parseError('No keyname found', $parseString); + } + break; + } + } elseif ($fieldName = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+')) { + // Getting field: + $result['FIELDS'][$fieldName]['definition'] = $this->parseFieldDef($parseString); + if ($this->parse_error) { + return $this->parse_error; } - $values[] = $fields; } - $query = count($values) === 1 ? $values[0] : $values; - break; + // Finding delimiter: + $delim = $this->nextPart($parseString, '^(,|\\))'); + if (!$delim) { + return $this->parseError('No delimiter found', $parseString); + } elseif ($delim === ')') { + break; + } + } + // Finding what is after the table definition - table type in MySQL + if ($delim === ')') { + if ($this->nextPart($parseString, '^((ENGINE|TYPE)[[:space:]]*=)')) { + $result['engine'] = $parseString; + $parseString = ''; + } + } else { + return $this->parseError('No fieldname found!', $parseString); + } + } else { + return $this->parseError('No table found!', $parseString); + } + // Should be no more content now: + if ($parseString) { + return $this->parseError('Still content in clause after parsing!', $parseString); } - return $query; + return $result; } /** - * Compiles a CREATE TABLE statement from components array + * Parsing ALTER TABLE query * - * @param array $components Array of SQL query components - * @return array array with SQL CREATE TABLE/INDEX command(s) - * @see parseCREATETABLE() - */ - public function compileCREATETABLE($components) { - $query = array(); - // Execute query (based on handler derived from the TABLE name which we actually know for once!) - switch ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->handler_getFromTableList($components['TABLE'])]['type']) { - case 'native': - $query[] = parent::compileCREATETABLE($components); - break; - case 'adodb': - // Create fields and keys: - $fieldsKeys = array(); - $indexKeys = array(); - foreach ($components['FIELDS'] as $fN => $fCfg) { - $handlerKey = $this->databaseConnection->handler_getFromTableList($components['TABLE']); - $fieldsKeys[$fN] = $this->databaseConnection->quoteName($fN, $handlerKey, TRUE) . ' ' . $this->compileFieldCfg($fCfg['definition']); - } - if (isset($components['KEYS']) && is_array($components['KEYS'])) { - foreach ($components['KEYS'] as $kN => $kCfg) { - if ($kN === 'PRIMARYKEY') { - foreach ($kCfg as $field) { - $fieldsKeys[$field] .= ' PRIMARY'; + * @param string $parseString SQL string starting with ALTER TABLE + * @return mixed Returns array with components of ALTER TABLE query on success, otherwise an error message string. + * @see compileALTERTABLE() + */ + protected function parseALTERTABLE($parseString) { + // Removing ALTER TABLE + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr(ltrim(substr($parseString, 5)), 5)); + // Init output variable: + $result = array(); + $result['type'] = 'ALTERTABLE'; + // Get table: + $hasBackquote = $this->nextPart($parseString, '^(`)') === '`'; + $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)' . ($hasBackquote ? '`' : '') . '[[:space:]]+'); + if ($hasBackquote && $this->nextPart($parseString, '^(`)') !== '`') { + return $this->parseError('No end backquote found!', $parseString); + } + if ($result['TABLE']) { + if ($result['action'] = $this->nextPart($parseString, '^(CHANGE|DROP[[:space:]]+KEY|DROP[[:space:]]+PRIMARY[[:space:]]+KEY|ADD[[:space:]]+KEY|ADD[[:space:]]+PRIMARY[[:space:]]+KEY|ADD[[:space:]]+UNIQUE|DROP|ADD|RENAME|DEFAULT[[:space:]]+CHARACTER[[:space:]]+SET|ENGINE)([[:space:]]+|\\(|=)')) { + $actionKey = $this->normalizeKeyword($result['action']); + // Getting field: + if (GeneralUtility::inList('ADDPRIMARYKEY,DROPPRIMARYKEY,ENGINE', $actionKey) || ($fieldKey = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'))) { + switch ($actionKey) { + case 'ADD': + $result['FIELD'] = $fieldKey; + $result['definition'] = $this->parseFieldDef($parseString); + if ($this->parse_error) { + return $this->parse_error; + } + break; + case 'DROP': + case 'RENAME': + $result['FIELD'] = $fieldKey; + break; + case 'CHANGE': + $result['FIELD'] = $fieldKey; + if ($result['newField'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+')) { + $result['definition'] = $this->parseFieldDef($parseString); + if ($this->parse_error) { + return $this->parse_error; + } + } else { + return $this->parseError('No NEW field name found', $parseString); } - } elseif ($kN === 'UNIQUE') { - foreach ($kCfg as $n => $field) { - $indexKeys = array_merge($indexKeys, $this->compileCREATEINDEX($n, $components['TABLE'], $field, array('UNIQUE'))); + break; + case 'ADDKEY': + case 'ADDPRIMARYKEY': + case 'ADDUNIQUE': + $result['KEY'] = $fieldKey; + $result['fields'] = $this->getValue($parseString, '_LIST', 'INDEX'); + if ($this->parse_error) { + return $this->parse_error; } - } else { - $indexKeys = array_merge($indexKeys, $this->compileCREATEINDEX($kN, $components['TABLE'], $kCfg)); - } + break; + case 'DROPKEY': + $result['KEY'] = $fieldKey; + break; + case 'DROPPRIMARYKEY': + // @todo ??? + break; + case 'DEFAULTCHARACTERSET': + $result['charset'] = $fieldKey; + break; + case 'ENGINE': + $result['engine'] = $this->nextPart($parseString, '^=[[:space:]]*([[:alnum:]]+)[[:space:]]+', TRUE); + break; } + } else { + return $this->parseError('No field name found', $parseString); } - // Generally create without OID on PostgreSQL - $tableOptions = array('postgres' => 'WITHOUT OIDS'); - // Fetch table/index generation query: - $tableName = $this->databaseConnection->quoteName($components['TABLE'], NULL, TRUE); - $query = array_merge($this->databaseConnection->handlerInstance[$this->databaseConnection->lastHandlerKey]->DataDictionary->CreateTableSQL($tableName, implode(',' . LF, $fieldsKeys), $tableOptions), $indexKeys); - break; + } else { + return $this->parseError('No action CHANGE, DROP or ADD found!', $parseString); + } + } else { + return $this->parseError('No table found!', $parseString); } - return $query; + // Should be no more content now: + if ($parseString) { + return $this->parseError('Still content in clause after parsing!', $parseString); + } + return $result; } /** - * Compiles an ALTER TABLE statement from components array + * Parsing DROP TABLE query * - * @param array Array of SQL query components - * @return string SQL ALTER TABLE query - * @see parseALTERTABLE() + * @param string $parseString SQL string starting with DROP TABLE + * @return mixed Returns array with components of DROP TABLE query on success, otherwise an error message string. */ - public function compileALTERTABLE($components) { - $query = ''; - // Execute query (based on handler derived from the TABLE name which we actually know for once!) - switch ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->lastHandlerKey]['type']) { - case 'native': - $query = parent::compileALTERTABLE($components); - break; - case 'adodb': - $tableName = $this->databaseConnection->quoteName($components['TABLE'], NULL, TRUE); - $fieldName = $this->databaseConnection->quoteName($components['FIELD'], NULL, TRUE); - switch (strtoupper(str_replace(array(' ', "\n", "\r", "\t"), '', $components['action']))) { - case 'ADD': - $query = $this->databaseConnection->handlerInstance[$this->databaseConnection->lastHandlerKey]->DataDictionary->AddColumnSQL($tableName, $fieldName . ' ' . $this->compileFieldCfg($components['definition'])); - break; - case 'CHANGE': - $query = $this->databaseConnection->handlerInstance[$this->databaseConnection->lastHandlerKey]->DataDictionary->AlterColumnSQL($tableName, $fieldName . ' ' . $this->compileFieldCfg($components['definition'])); - break; - case 'DROP': + protected function parseDROPTABLE($parseString) { + // Removing DROP TABLE + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr(ltrim(substr($parseString, 4)), 5)); + // Init output variable: + $result = array(); + $result['type'] = 'DROPTABLE'; + // IF EXISTS + $result['ifExists'] = $this->nextPart($parseString, '^(IF[[:space:]]+EXISTS[[:space:]]+)'); + // Get table: + $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); + if ($result['TABLE']) { + // Should be no more content now: + if ($parseString) { + return $this->parseError('Still content in clause after parsing!', $parseString); + } + return $result; + } else { + return $this->parseError('No table found!', $parseString); + } + } - case 'DROPKEY': - $query = $this->compileDROPINDEX($components['KEY'], $components['TABLE']); - break; + /** + * Parsing CREATE DATABASE query + * + * @param string $parseString SQL string starting with CREATE DATABASE + * @return mixed Returns array with components of CREATE DATABASE query on success, otherwise an error message string. + */ + protected function parseCREATEDATABASE($parseString) { + // Removing CREATE DATABASE + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr(ltrim(substr($parseString, 6)), 8)); + // Init output variable: + $result = array(); + $result['type'] = 'CREATEDATABASE'; + // Get table: + $result['DATABASE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); + if ($result['DATABASE']) { + // Should be no more content now: + if ($parseString) { + return $this->parseError('Still content in clause after parsing!', $parseString); + } + return $result; + } else { + return $this->parseError('No database found!', $parseString); + } + } - case 'ADDKEY': - $query = $this->compileCREATEINDEX($components['KEY'], $components['TABLE'], $components['fields']); - break; - case 'ADDUNIQUE': - $query = $this->compileCREATEINDEX($components['KEY'], $components['TABLE'], $components['fields'], array('UNIQUE')); - break; - case 'ADDPRIMARYKEY': - // @todo ??? - break; - case 'DEFAULTCHARACTERSET': + /** + * Parsing TRUNCATE TABLE query + * + * @param string $parseString SQL string starting with TRUNCATE TABLE + * @return mixed Returns array with components of TRUNCATE TABLE query on success, otherwise an error message string. + */ + protected function parseTRUNCATETABLE($parseString) { + // Removing TRUNCATE TABLE + $parseString = $this->trimSQL($parseString); + $parseString = ltrim(substr(ltrim(substr($parseString, 8)), 5)); + // Init output variable: + $result = array(); + $result['type'] = 'TRUNCATETABLE'; + // Get table: + $result['TABLE'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); + if ($result['TABLE']) { + // Should be no more content now: + if ($parseString) { + return $this->parseError('Still content in clause after parsing!', $parseString); + } + return $result; + } else { + return $this->parseError('No table found!', $parseString); + } + } - case 'ENGINE': - // @todo ??? + /************************************** + * + * SQL Parsing, helper functions for parts of queries + * + **************************************/ + /** + * Parsing the fields in the "SELECT [$selectFields] FROM" part of a query into an array. + * The output from this function can be compiled back into a field list with ->compileFieldList() + * Will detect the keywords "DESC" and "ASC" after the table name; thus is can be used for parsing the more simply ORDER BY and GROUP BY field lists as well! + * + * @param string $parseString The string with fieldnames, eg. "title, uid AS myUid, max(tstamp), count(*)" etc. NOTICE: passed by reference! + * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(FROM)([[:space:]]*)' + * @return array If successful parsing, returns an array, otherwise an error string. + * @see compileFieldList() + */ + public function parseFieldList(&$parseString, $stopRegex = '') { + $stack = array(); + // Contains the parsed content + if ($parseString === '') { + return $stack; + } + // @todo - should never happen, why does it? + // Pointer to positions in $stack + $pnt = 0; + // Indicates the parenthesis level we are at. + $level = 0; + // Recursivity brake. + $loopExit = 0; + // Prepare variables: + $parseString = $this->trimSQL($parseString); + $this->lastStopKeyWord = ''; + $this->parse_error = ''; + // Parse any SQL hint / comments + $stack[$pnt]['comments'] = $this->nextPart($parseString, '^(\\/\\*.*\\*\\/)'); + // $parseString is continuously shortened by the process and we keep parsing it till it is zero: + while ($parseString !== '') { + // Checking if we are inside / outside parenthesis (in case of a function like count(), max(), min() etc...): + // Inside parenthesis here (does NOT detect if values in quotes are used, the only token is ")" or "("): + if ($level > 0) { + // Accumulate function content until next () parenthesis: + $funcContent = $this->nextPart($parseString, '^([^()]*.)'); + $stack[$pnt]['func_content.'][] = array( + 'level' => $level, + 'func_content' => substr($funcContent, 0, -1) + ); + $stack[$pnt]['func_content'] .= $funcContent; + // Detecting ( or ) + switch (substr($stack[$pnt]['func_content'], -1)) { + case '(': + $level++; + break; + case ')': + $level--; + // If this was the last parenthesis: + if (!$level) { + $stack[$pnt]['func_content'] = substr($stack[$pnt]['func_content'], 0, -1); + // Remove any whitespace after the parenthesis. + $parseString = ltrim($parseString); + } break; } - break; + } else { + // Outside parenthesis, looking for next field: + // Looking for a flow-control construct (only known constructs supported) + if (preg_match('/^case([[:space:]][[:alnum:]\\*._]+)?[[:space:]]when/i', $parseString)) { + $stack[$pnt]['type'] = 'flow-control'; + $stack[$pnt]['flow-control'] = $this->parseCaseStatement($parseString); + // Looking for "AS" alias: + if ($as = $this->nextPart($parseString, '^(AS)[[:space:]]+')) { + $stack[$pnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)(,|[[:space:]]+)'); + $stack[$pnt]['as_keyword'] = $as; + } + } else { + // Looking for a known function (only known functions supported) + $func = $this->nextPart($parseString, '^(count|max|min|floor|sum|avg)[[:space:]]*\\('); + if ($func) { + // Strip off "(" + $parseString = trim(substr($parseString, 1)); + $stack[$pnt]['type'] = 'function'; + $stack[$pnt]['function'] = $func; + // increse parenthesis level counter. + $level++; + } else { + $stack[$pnt]['distinct'] = $this->nextPart($parseString, '^(distinct[[:space:]]+)'); + // Otherwise, look for regular fieldname: + if (($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)(,|[[:space:]]+)')) !== '') { + $stack[$pnt]['type'] = 'field'; + // Explode fieldname into field and table: + $tableField = explode('.', $fieldName, 2); + if (count($tableField) === 2) { + $stack[$pnt]['table'] = $tableField[0]; + $stack[$pnt]['field'] = $tableField[1]; + } else { + $stack[$pnt]['table'] = ''; + $stack[$pnt]['field'] = $tableField[0]; + } + } else { + return $this->parseError('No field name found as expected in parseFieldList()', $parseString); + } + } + } + } + // After a function or field we look for "AS" alias and a comma to separate to the next field in the list: + if (!$level) { + // Looking for "AS" alias: + if ($as = $this->nextPart($parseString, '^(AS)[[:space:]]+')) { + $stack[$pnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)(,|[[:space:]]+)'); + $stack[$pnt]['as_keyword'] = $as; + } + // Looking for "ASC" or "DESC" keywords (for ORDER BY) + if ($sDir = $this->nextPart($parseString, '^(ASC|DESC)([[:space:]]+|,)')) { + $stack[$pnt]['sortDir'] = $sDir; + } + // Looking for stop-keywords: + if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) { + $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord); + return $stack; + } + // Looking for comma (since the stop-keyword did not trigger a return...) + if ($parseString !== '' && !$this->nextPart($parseString, '^(,)')) { + return $this->parseError('No comma found as expected in parseFieldList()', $parseString); + } + // Increasing pointer: + $pnt++; + } + // Check recursivity brake: + $loopExit++; + if ($loopExit > 500) { + return $this->parseError('More than 500 loops, exiting prematurely in parseFieldList()...', $parseString); + } } - return $query; + // Return result array: + return $stack; } /** - * Compiles CREATE INDEX statements from component information - * - * MySQL only needs uniqueness of index names per table, but many DBMS require uniqueness of index names per schema. - * The table name is hashed and prepended to the index name to make sure index names are unique. + * Parsing a CASE ... WHEN flow-control construct. + * The output from this function can be compiled back with ->compileCaseStatement() * - * @param string $indexName - * @param string $tableName - * @param array $indexFields - * @param array $indexOptions - * @return array - * @see compileALTERTABLE() + * @param string $parseString The string with the CASE ... WHEN construct, eg. "CASE field WHEN 1 THEN 0 ELSE ..." etc. NOTICE: passed by reference! + * @return array If successful parsing, returns an array, otherwise an error string. + * @see compileCaseConstruct() */ - public function compileCREATEINDEX($indexName, $tableName, $indexFields, $indexOptions = array()) { - $indexIdentifier = $this->databaseConnection->quoteName(hash('crc32b', $tableName) . '_' . $indexName, NULL, TRUE); - $dbmsSpecifics = $this->databaseConnection->getSpecifics(); - $keepFieldLengths = $dbmsSpecifics->specificExists(Specifics\AbstractSpecifics::PARTIAL_STRING_INDEX) && $dbmsSpecifics->getSpecific(Specifics\AbstractSpecifics::PARTIAL_STRING_INDEX); - - foreach ($indexFields as $key => $fieldName) { - if (!$keepFieldLengths) { - $fieldName = preg_replace('/\A([^\(]+)(\(\d+\))/', '\\1', $fieldName); + protected function parseCaseStatement(&$parseString) { + $result = array(); + $result['type'] = $this->nextPart($parseString, '^(case)[[:space:]]+'); + if (!preg_match('/^when[[:space:]]+/i', $parseString)) { + $value = $this->getValue($parseString); + if (!(isset($value[1]) || is_numeric($value[0]))) { + $result['case_field'] = $value[0]; + } else { + $result['case_value'] = $value; } - // Quote the fieldName in backticks with escaping, ADOdb will replace the backticks with the correct quoting - $indexFields[$key] = '`' . str_replace('`', '``', $fieldName) . '`'; } - - return $this->databaseConnection->handlerInstance[$this->databaseConnection->handler_getFromTableList($tableName)]->DataDictionary->CreateIndexSQL( - $indexIdentifier, $this->databaseConnection->quoteName($tableName, NULL, TRUE), $indexFields, $indexOptions - ); + $result['when'] = array(); + while ($this->nextPart($parseString, '^(when)[[:space:]]')) { + $when = array(); + $when['when_value'] = $this->parseWhereClause($parseString, '^(then)[[:space:]]+'); + $when['then_value'] = $this->getValue($parseString); + $result['when'][] = $when; + } + if ($this->nextPart($parseString, '^(else)[[:space:]]+')) { + $result['else'] = $this->getValue($parseString); + } + if (!$this->nextPart($parseString, '^(end)[[:space:]]+')) { + return $this->parseError('No "end" keyword found as expected in parseCaseStatement()', $parseString); + } + return $result; } /** - * Compiles DROP INDEX statements from component information - * - * MySQL only needs uniqueness of index names per table, but many DBMS require uniqueness of index names per schema. - * The table name is hashed and prepended to the index name to make sure index names are unique. + * Parsing a CAST definition in the "JOIN [$parseString] ..." part of a query into an array. + * The success of this parsing determines if that part of the query is supported by TYPO3. * - * @param $indexName - * @param $tableName - * @return array - * @see compileALTERTABLE() + * @param string $parseString JOIN clause to parse. NOTICE: passed by reference! + * @return mixed If successful parsing, returns an array, otherwise an error string. */ - public function compileDROPINDEX($indexName, $tableName) { - $indexIdentifier = $this->databaseConnection->quoteName(hash('crc32b', $tableName) . '_' . $indexName, NULL, TRUE); - - return $this->databaseConnection->handlerInstance[$this->databaseConnection->handler_getFromTableList($tableName)]->DataDictionary->DropIndexSQL( - $indexIdentifier, $this->databaseConnection->quoteName($tableName) - ); + protected function parseCastStatement(&$parseString) { + $this->nextPart($parseString, '^(CAST)[[:space:]]*'); + $parseString = trim(substr($parseString, 1)); + $castDefinition = array('type' => 'cast'); + // Strip off "(" + if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) { + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); + if (count($tableField) === 2) { + $castDefinition['table'] = $tableField[0]; + $castDefinition['field'] = $tableField[1]; + } else { + $castDefinition['table'] = ''; + $castDefinition['field'] = $tableField[0]; + } + } else { + return $this->parseError('No casted join field found in parseCastStatement()!', $parseString); + } + if ($this->nextPart($parseString, '^([[:space:]]*AS[[:space:]]*)')) { + $castDefinition['datatype'] = $this->getValue($parseString); + } + if (!$this->nextPart($parseString, '^([)])')) { + return $this->parseError('No end parenthesis at end of CAST function', $parseString); + } + return $castDefinition; } /** - * Compile field definition + * Parsing the tablenames in the "FROM [$parseString] WHERE" part of a query into an array. + * The success of this parsing determines if that part of the query is supported by TYPO3. * - * @param array $fieldCfg Field definition parts - * @return string Field definition string + * @param string $parseString List of tables, eg. "pages, tt_content" or "pages A, pages B". NOTICE: passed by reference! + * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(WHERE)([[:space:]]*)' + * @return array If successful parsing, returns an array, otherwise an error string. + * @see compileFromTables() */ - public function compileFieldCfg($fieldCfg) { - $cfg = ''; - switch ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->lastHandlerKey]['type']) { - case 'native': - $cfg = parent::compileFieldCfg($fieldCfg); - break; - case 'adodb': - // Set type: - $type = $this->databaseConnection->getSpecifics()->getMetaFieldType($fieldCfg['fieldType']); - $cfg = $type; - // Add value, if any: - if ((string)$fieldCfg['value'] !== '' && in_array($type, array('C', 'C2'))) { - $cfg .= ' ' . $fieldCfg['value']; - } elseif (!isset($fieldCfg['value']) && in_array($type, array('C', 'C2'))) { - $cfg .= ' 255'; - } - // Add additional features: - $noQuote = TRUE; - if (is_array($fieldCfg['featureIndex'])) { - // MySQL assigns DEFAULT value automatically if NOT NULL, fake this here - // numeric fields get 0 as default, other fields an empty string - if (isset($fieldCfg['featureIndex']['NOTNULL']) && !isset($fieldCfg['featureIndex']['DEFAULT']) && !isset($fieldCfg['featureIndex']['AUTO_INCREMENT'])) { - switch ($type) { - case 'I8': - - case 'F': - - case 'N': - $fieldCfg['featureIndex']['DEFAULT'] = array('keyword' => 'DEFAULT', 'value' => array('0', '')); - break; - default: - $fieldCfg['featureIndex']['DEFAULT'] = array('keyword' => 'DEFAULT', 'value' => array('', '\'')); + public function parseFromTables(&$parseString, $stopRegex = '') { + // Prepare variables: + $parseString = $this->trimSQL($parseString); + $this->lastStopKeyWord = ''; + $this->parse_error = ''; + // Contains the parsed content + $stack = array(); + // Pointer to positions in $stack + $pnt = 0; + // Recursivity brake. + $loopExit = 0; + // $parseString is continously shortend by the process and we keep parsing it till it is zero: + while ($parseString !== '') { + // Looking for the table: + if ($stack[$pnt]['table'] = $this->nextPart($parseString, '^([[:alnum:]_]+)(,|[[:space:]]+)')) { + // Looking for stop-keywords before fetching potential table alias: + if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) { + $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord); + return $stack; + } + if (!preg_match('/^(LEFT|RIGHT|JOIN|INNER)[[:space:]]+/i', $parseString)) { + $stack[$pnt]['as_keyword'] = $this->nextPart($parseString, '^(AS[[:space:]]+)'); + $stack[$pnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]*'); + } + } else { + return $this->parseError('No table name found as expected in parseFromTables()!', $parseString); + } + // Looking for JOIN + $joinCnt = 0; + while ($join = $this->nextPart($parseString, '^(((INNER|(LEFT|RIGHT)([[:space:]]+OUTER)?)[[:space:]]+)?JOIN)[[:space:]]+')) { + $stack[$pnt]['JOIN'][$joinCnt]['type'] = $join; + if ($stack[$pnt]['JOIN'][$joinCnt]['withTable'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+', 1)) { + if (!preg_match('/^ON[[:space:]]+/i', $parseString)) { + $stack[$pnt]['JOIN'][$joinCnt]['as_keyword'] = $this->nextPart($parseString, '^(AS[[:space:]]+)'); + $stack[$pnt]['JOIN'][$joinCnt]['as'] = $this->nextPart($parseString, '^([[:alnum:]_]+)[[:space:]]+'); + } + if (!$this->nextPart($parseString, '^(ON[[:space:]]+)')) { + return $this->parseError('No join condition found in parseFromTables()!', $parseString); + } + $stack[$pnt]['JOIN'][$joinCnt]['ON'] = array(); + $condition = array('operator' => ''); + $parseCondition = TRUE; + while ($parseCondition) { + if (($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)[[:space:]]*(<=|>=|<|>|=|!=)')) !== '') { + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); + $condition['left'] = array(); + if (count($tableField) === 2) { + $condition['left']['table'] = $tableField[0]; + $condition['left']['field'] = $tableField[1]; + } else { + $condition['left']['table'] = ''; + $condition['left']['field'] = $tableField[0]; + } + } elseif (preg_match('/^CAST[[:space:]]*[(]/i', $parseString)) { + $condition['left'] = $this->parseCastStatement($parseString); + // Return the parse error + if (!is_array($condition['left'])) { + return $condition['left']; + } + } else { + return $this->parseError('No join field found in parseFromTables()!', $parseString); + } + // Find "comparator": + $condition['comparator'] = $this->nextPart($parseString, '^(<=|>=|<|>|=|!=)'); + if (preg_match('/^CAST[[:space:]]*[(]/i', $parseString)) { + $condition['right'] = $this->parseCastStatement($parseString); + // Return the parse error + if (!is_array($condition['right'])) { + return $condition['right']; + } + } elseif (($fieldName = $this->nextPart($parseString, '^([[:alnum:]._]+)')) !== '') { + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); + $condition['right'] = array(); + if (count($tableField) === 2) { + $condition['right']['table'] = $tableField[0]; + $condition['right']['field'] = $tableField[1]; + } else { + $condition['right']['table'] = ''; + $condition['right']['field'] = $tableField[0]; + } + } elseif ($value = $this->getValue($parseString)) { + $condition['right']['value'] = $value; + } else { + return $this->parseError('No join field found in parseFromTables()!', $parseString); } + $stack[$pnt]['JOIN'][$joinCnt]['ON'][] = $condition; + if (($operator = $this->nextPart($parseString, '^(AND|OR)')) !== '') { + $condition = array('operator' => $operator); + } else { + $parseCondition = FALSE; + } + } + $joinCnt++; + } else { + return $this->parseError('No join table found in parseFromTables()!', $parseString); + } + } + // Looking for stop-keywords: + if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) { + $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord); + return $stack; + } + // Looking for comma: + if ($parseString !== '' && !$this->nextPart($parseString, '^(,)')) { + return $this->parseError('No comma found as expected in parseFromTables()', $parseString); + } + // Increasing pointer: + $pnt++; + // Check recursivity brake: + $loopExit++; + if ($loopExit > 500) { + return $this->parseError('More than 500 loops, exiting prematurely in parseFromTables()...', $parseString); + } + } + // Return result array: + return $stack; + } + + /** + * Parsing the WHERE clause fields in the "WHERE [$parseString] ..." part of a query into a multidimensional array. + * The success of this parsing determines if that part of the query is supported by TYPO3. + * + * @param string $parseString WHERE clause to parse. NOTICE: passed by reference! + * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(GROUP BY|ORDER BY|LIMIT)([[:space:]]*)' + * @param array $parameterReferences Array holding references to either named (:name) or question mark (?) parameters found + * @return mixed If successful parsing, returns an array, otherwise an error string. + */ + public function parseWhereClause(&$parseString, $stopRegex = '', array &$parameterReferences = array()) { + // Prepare variables: + $parseString = $this->trimSQL($parseString); + $this->lastStopKeyWord = ''; + $this->parse_error = ''; + // Contains the parsed content + $stack = array(0 => array()); + // Pointer to positions in $stack + $pnt = array(0 => 0); + // Determines parenthesis level + $level = 0; + // Recursivity brake. + $loopExit = 0; + // $parseString is continuously shortened by the process and we keep parsing it till it is zero: + while ($parseString !== '') { + // Look for next parenthesis level: + $newLevel = $this->nextPart($parseString, '^([(])'); + // If new level is started, manage stack/pointers: + if ($newLevel === '(') { + // Increase level + $level++; + // Reset pointer for this level + $pnt[$level] = 0; + // Reset stack for this level + $stack[$level] = array(); + } else { + // If no new level is started, just parse the current level: + // Find "modifier", eg. "NOT or !" + $stack[$level][$pnt[$level]]['modifier'] = trim($this->nextPart($parseString, '^(!|NOT[[:space:]]+)')); + // See if condition is EXISTS with a subquery + if (preg_match('/^EXISTS[[:space:]]*[(]/i', $parseString)) { + $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(EXISTS)[[:space:]]*'); + // Strip off "(" + $parseString = trim(substr($parseString, 1)); + $stack[$level][$pnt[$level]]['func']['subquery'] = $this->parseSELECT($parseString, $parameterReferences); + // 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'; } - foreach ($fieldCfg['featureIndex'] as $feature => $featureDef) { - switch (TRUE) { - case $feature === 'UNSIGNED' && !$this->databaseConnection->runningADOdbDriver('mysql'): - case $feature === 'NOTNULL' && $this->databaseConnection->runningADOdbDriver('oci8'): - continue; - case $feature === 'AUTO_INCREMENT': - $cfg .= ' AUTOINCREMENT'; - break; - case $feature === 'NOTNULL': - $cfg .= ' NOTNULL'; - break; - default: - $cfg .= ' ' . $featureDef['keyword']; + } else { + // See if LOCATE function is found + if (preg_match('/^LOCATE[[:space:]]*[(]/i', $parseString)) { + $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(LOCATE)[[:space:]]*'); + // Strip off "(" + $parseString = trim(substr($parseString, 1)); + $stack[$level][$pnt[$level]]['func']['substr'] = $this->getValue($parseString); + if (!$this->nextPart($parseString, '^(,)')) { + return $this->parseError('No comma found as expected in parseWhereClause()', $parseString); + } + if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) { + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); + if (count($tableField) === 2) { + $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0]; + $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1]; + } else { + $stack[$level][$pnt[$level]]['func']['table'] = ''; + $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0]; + } + } else { + return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); + } + if ($this->nextPart($parseString, '^(,)')) { + $stack[$level][$pnt[$level]]['func']['pos'] = $this->getValue($parseString); + } + if (!$this->nextPart($parseString, '^([)])')) { + return $this->parseError('No ) parenthesis at end of function', $parseString); + } + } elseif (preg_match('/^IFNULL[[:space:]]*[(]/i', $parseString)) { + $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(IFNULL)[[:space:]]*'); + $parseString = trim(substr($parseString, 1)); + // Strip off "(" + if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) { + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); + if (count($tableField) === 2) { + $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0]; + $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1]; + } else { + $stack[$level][$pnt[$level]]['func']['table'] = ''; + $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0]; + } + } else { + return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); + } + if ($this->nextPart($parseString, '^(,)')) { + $stack[$level][$pnt[$level]]['func']['default'] = $this->getValue($parseString); + } + if (!$this->nextPart($parseString, '^([)])')) { + return $this->parseError('No ) parenthesis at end of function', $parseString); + } + } elseif (preg_match('/^CAST[[:space:]]*[(]/i', $parseString)) { + $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(CAST)[[:space:]]*'); + $parseString = trim(substr($parseString, 1)); + // Strip off "(" + if ($fieldName = $this->nextPart($parseString, '^([[:alnum:]\\*._]+)[[:space:]]*')) { + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); + if (count($tableField) === 2) { + $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0]; + $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1]; + } else { + $stack[$level][$pnt[$level]]['func']['table'] = ''; + $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0]; + } + } else { + return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); + } + if ($this->nextPart($parseString, '^([[:space:]]*AS[[:space:]]*)')) { + $stack[$level][$pnt[$level]]['func']['datatype'] = $this->getValue($parseString); + } + if (!$this->nextPart($parseString, '^([)])')) { + return $this->parseError('No ) parenthesis at end of function', $parseString); + } + } elseif (preg_match('/^FIND_IN_SET[[:space:]]*[(]/i', $parseString)) { + $stack[$level][$pnt[$level]]['func']['type'] = $this->nextPart($parseString, '^(FIND_IN_SET)[[:space:]]*'); + // Strip off "(" + $parseString = trim(substr($parseString, 1)); + if ($str = $this->getValue($parseString)) { + $stack[$level][$pnt[$level]]['func']['str'] = $str; + if ($fieldName = $this->nextPart($parseString, '^,[[:space:]]*([[:alnum:]._]+)[[:space:]]*', TRUE)) { + // Parse field name into field and table: + $tableField = explode('.', $fieldName, 2); + if (count($tableField) === 2) { + $stack[$level][$pnt[$level]]['func']['table'] = $tableField[0]; + $stack[$level][$pnt[$level]]['func']['field'] = $tableField[1]; + } else { + $stack[$level][$pnt[$level]]['func']['table'] = ''; + $stack[$level][$pnt[$level]]['func']['field'] = $tableField[0]; + } + } else { + return $this->parseError('No field name found as expected in parseWhereClause()', $parseString); + } + if (!$this->nextPart($parseString, '^([)])')) { + return $this->parseError('No ) parenthesis at end of function', $parseString); + } + } else { + return $this->parseError('No item to look for found as expected in parseWhereClause()', $parseString); + } + } else { + // 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]]['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 ((string)$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) { + $stack[$level][$pnt[$level]]['calc_table'] = $tableField[0]; + $stack[$level][$pnt[$level]]['calc_field'] = $tableField[1]; + } else { + $stack[$level][$pnt[$level]]['calc_table'] = ''; + $stack[$level][$pnt[$level]]['calc_field'] = $tableField[0]; + } + } } - // Add value if found: - if (is_array($featureDef['value'])) { - if ($featureDef['value'][0] === '') { - $cfg .= ' "\'\'"'; + } + $stack[$level][$pnt[$level]]['comparator'] = $this->nextPart($parseString, '^(' . implode('|', self::$comparatorPatterns) . ')'); + if ($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 (GeneralUtility::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, $parameterReferences); + // Seek to new position in parseString after parsing of the subquery + if (!empty($stack[$level][$pnt[$level]]['subquery']['parseString'])) { + $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 { - $cfg .= ' ' . $featureDef['value'][1] . $this->compileAddslashes($featureDef['value'][0]) . $featureDef['value'][1]; - if (!is_numeric($featureDef['value'][0])) { - $noQuote = FALSE; + if (GeneralUtility::inList('BETWEEN,NOT BETWEEN', $stack[$level][$pnt[$level]]['comparator'])) { + $stack[$level][$pnt[$level]]['values'] = array(); + $stack[$level][$pnt[$level]]['values'][0] = $this->getValue($parseString); + if (!$this->nextPart($parseString, '^(AND)')) { + return $this->parseError('No AND operator found as expected in parseWhereClause()', $parseString); + } + $stack[$level][$pnt[$level]]['values'][1] = $this->getValue($parseString); + } else { + // Finding value for comparator: + $stack[$level][$pnt[$level]]['value'] = &$this->getValueOrParameter($parseString, $stack[$level][$pnt[$level]]['comparator'], '', $parameterReferences); + if ($this->parse_error) { + return $this->parse_error; + } } } } } } - if ($noQuote) { - $cfg .= ' NOQUOTE'; + // Finished, increase pointer: + $pnt[$level]++; + // Checking if we are back to level 0 and we should still decrease level, + // meaning we were probably parsing as subquery and should return here: + if ($level === 0 && preg_match('/^[)]/', $parseString)) { + // Return the stacks lowest level: + return $stack[0]; } - break; + // 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: + // Copy stack + $stack[$level][$pnt[$level]]['sub'] = $stack[$level + 1]; + // Increase pointer of the new level + $pnt[$level]++; + // Make recursivity check: + $loopExit++; + if ($loopExit > 500) { + return $this->parseError('More than 500 loops (in search for exit parenthesis), exiting prematurely in parseWhereClause()...', $parseString); + } + } + // Detecting the operator for the next level: + $op = $this->nextPart($parseString, '^(AND[[:space:]]+NOT|&&[[:space:]]+NOT|OR[[:space:]]+NOT|OR[[:space:]]+NOT|\\|\\|[[:space:]]+NOT|AND|&&|OR|\\|\\|)(\\(|[[:space:]]+)'); + if ($op) { + // Normalize boolean operator + $op = str_replace(array('&&', '||'), array('AND', 'OR'), $op); + $stack[$level][$pnt[$level]]['operator'] = $op; + } elseif ($parseString !== '') { + // Looking for stop-keywords: + if ($stopRegex && ($this->lastStopKeyWord = $this->nextPart($parseString, $stopRegex))) { + $this->lastStopKeyWord = $this->normalizeKeyword($this->lastStopKeyWord); + return $stack[0]; + } else { + return $this->parseError('No operator, but parsing not finished in parseWhereClause().', $parseString); + } + } + } + // Make recursivity check: + $loopExit++; + if ($loopExit > 500) { + return $this->parseError('More than 500 loops, exiting prematurely in parseWhereClause()...', $parseString); + } + } + // Return the stacks lowest level: + return $stack[0]; + } + + /** + * Parsing the WHERE clause fields in the "WHERE [$parseString] ..." part of a query into a multidimensional array. + * The success of this parsing determines if that part of the query is supported by TYPO3. + * + * @param string $parseString WHERE clause to parse. NOTICE: passed by reference! + * @param string $stopRegex Regular expressing to STOP parsing, eg. '^(GROUP BY|ORDER BY|LIMIT)([[:space:]]*)' + * @return mixed If successful parsing, returns an array, otherwise an error string. + */ + public function parseFieldDef(&$parseString, $stopRegex = '') { + // Prepare variables: + $parseString = $this->trimSQL($parseString); + $this->lastStopKeyWord = ''; + $this->parse_error = ''; + $result = array(); + // Field type: + if ($result['fieldType'] = $this->nextPart($parseString, '^(int|smallint|tinyint|mediumint|bigint|double|numeric|decimal|float|varchar|char|text|tinytext|mediumtext|longtext|blob|tinyblob|mediumblob|longblob)([[:space:],]+|\\()')) { + // Looking for value: + if ($parseString[0] === '(') { + $parseString = substr($parseString, 1); + if ($result['value'] = $this->nextPart($parseString, '^([^)]*)')) { + $parseString = ltrim(substr($parseString, 1)); + } else { + return $this->parseError('No end-parenthesis for value found in parseFieldDef()!', $parseString); + } + } + // Looking for keywords + while ($keyword = $this->nextPart($parseString, '^(DEFAULT|NOT[[:space:]]+NULL|AUTO_INCREMENT|UNSIGNED)([[:space:]]+|,|\\))')) { + $keywordCmp = $this->normalizeKeyword($keyword); + $result['featureIndex'][$keywordCmp]['keyword'] = $keyword; + switch ($keywordCmp) { + case 'DEFAULT': + $result['featureIndex'][$keywordCmp]['value'] = $this->getValue($parseString); + break; + } + } + } else { + return $this->parseError('Field type unknown in parseFieldDef()!', $parseString); } - // Return field definition string: - return $cfg; + return $result; } /** @@ -490,6 +1473,227 @@ class SqlParser extends \TYPO3\CMS\Core\Database\SqlParser { return !is_numeric($featureIndex['DEFAULT']['value'][0]) && empty($featureIndex['DEFAULT']['value'][0]); } + /************************************ + * + * Parsing: Helper functions + * + ************************************/ + /** + * Strips off a part of the parseString and returns the matching part. + * Helper function for the parsing methods. + * + * @param string $parseString Parse string; if $regex finds anything the value of the first () level will be stripped of the string in the beginning. Further $parseString is left-trimmed (on success). Notice; parsestring is passed by reference. + * @param string $regex Regex to find a matching part in the beginning of the string. Rules: You MUST start the regex with "^" (finding stuff in the beginning of string) and the result of the first parenthesis is what will be returned to you (and stripped of the string). Eg. '^(AND|OR|&&)[[:space:]]+' will return AND, OR or && if found and having one of more whitespaces after it, plus shorten $parseString with that match and any space after (by ltrim()) + * @param bool $trimAll If set the full match of the regex is stripped of the beginning of the string! + * @return string The value of the first parenthesis level of the REGEX. + */ + protected function nextPart(&$parseString, $regex, $trimAll = FALSE) { + $reg = array(); + // Adding space char because [[:space:]]+ is often a requirement in regex's + if (preg_match('/' . $regex . '/i', $parseString . ' ', $reg)) { + $parseString = ltrim(substr($parseString, strlen($reg[$trimAll ? 0 : 1]))); + return $reg[1]; + } + // No match found + return ''; + } + + /** + * Normalizes the keyword by removing any separator and changing to uppercase + * + * @param string $keyword The keyword being normalized + * @return string + */ + public static function normalizeKeyword($keyword) { + return strtoupper(str_replace(self::$interQueryWhitespaces, '', $keyword)); + } + + /** + * Finds value or either named (:name) or question mark (?) parameter markers at the beginning + * of $parseString, returns result and strips it of parseString. + * This method returns a pointer to the parameter or value that was found. In case of a parameter + * the pointer is a reference to the corresponding item in array $parameterReferences. + * + * @param string $parseString The parseString + * @param string $comparator The comparator used before. + * @param string $mode The mode, e.g., "INDEX + * @param mixed The value (string/integer) or parameter (:name/?). Otherwise an array with error message in first key (0) + */ + protected function &getValueOrParameter(&$parseString, $comparator = '', $mode = '', array &$parameterReferences = array()) { + $parameter = $this->nextPart($parseString, '^(\\:[[:alnum:]_]+|\\?)'); + if ($parameter === '?') { + if (!isset($parameterReferences['?'])) { + $parameterReferences['?'] = array(); + } + $value = array('?'); + $parameterReferences['?'][] = &$value; + } elseif ($parameter !== '') { + // named parameter + if (isset($parameterReferences[$parameter])) { + // Use the same reference as last time we encountered this parameter + $value = &$parameterReferences[$parameter]; + } else { + $value = array($parameter); + $parameterReferences[$parameter] = &$value; + } + } else { + $value = $this->getValue($parseString, $comparator, $mode); + } + return $value; + } + + /** + * Finds value in beginning of $parseString, returns result and strips it of parseString + * + * @param string $parseString The parseString, eg. "(0,1,2,3) ..." or "('asdf','qwer') ..." or "1234 ..." or "'My string value here' ... + * @param string $comparator The comparator used before. If "NOT IN" or "IN" then the value is expected to be a list of values. Otherwise just an integer (un-quoted) or string (quoted) + * @param string $mode The mode, eg. "INDEX + * @return mixed The value (string/integer). Otherwise an array with error message in first key (0) + */ + protected function getValue(&$parseString, $comparator = '', $mode = '') { + $value = ''; + if (GeneralUtility::inList('NOTIN,IN,_LIST', strtoupper(str_replace(array(' ', LF, CR, TAB), '', $comparator)))) { + // List of values: + if ($this->nextPart($parseString, '^([(])')) { + $listValues = array(); + $comma = ','; + while ($comma === ',') { + $listValues[] = $this->getValue($parseString); + if ($mode === 'INDEX') { + // Remove any length restriction on INDEX definition + $this->nextPart($parseString, '^([(]\\d+[)])'); + } + $comma = $this->nextPart($parseString, '^([,])'); + } + $out = $this->nextPart($parseString, '^([)])'); + if ($out) { + if ($comparator === '_LIST') { + $kVals = array(); + foreach ($listValues as $vArr) { + $kVals[] = $vArr[0]; + } + return $kVals; + } else { + return $listValues; + } + } else { + return array($this->parseError('No ) parenthesis in list', $parseString)); + } + } else { + return array($this->parseError('No ( parenthesis starting the list', $parseString)); + } + } else { + // Just plain string value, in quotes or not: + // Quote? + $firstChar = $parseString[0]; + switch ($firstChar) { + case '"': + $value = array($this->getValueInQuotes($parseString, '"'), '"'); + break; + case '\'': + $value = array($this->getValueInQuotes($parseString, '\''), '\''); + break; + default: + $reg = array(); + if (preg_match('/^([[:alnum:]._-]+(?:\\([0-9]+\\))?)/i', $parseString, $reg)) { + $parseString = ltrim(substr($parseString, strlen($reg[0]))); + $value = array($reg[1]); + } + } + } + return $value; + } + + /** + * Strip slashes function used for parsing + * NOTICE: If a query being parsed was prepared for another database than MySQL this function should probably be changed + * + * @param string $str Input string + * @return string Output string + */ + protected function parseStripslashes($str) { + $search = array('\\\\', '\\\'', '\\"', '\0', '\n', '\r', '\Z'); + $replace = array('\\', '\'', '"', "\x00", "\x0a", "\x0d", "\x1a"); + + return str_replace($search, $replace, $str); + } + + /** + * Setting the internal error message value, $this->parse_error and returns that value. + * + * @param string $msg Input error message + * @param string $restQuery Remaining query to parse. + * @return string Error message. + */ + protected function parseError($msg, $restQuery) { + $this->parse_error = 'SQL engine parse ERROR: ' . $msg . ': near "' . substr($restQuery, 0, 50) . '"'; + return $this->parse_error; + } + + /** + * Trimming SQL as preparation for parsing. + * ";" in the end is stripped off. + * White space is trimmed away around the value + * A single space-char is added in the end + * + * @param string $str Input string + * @return string Output string + */ + protected function trimSQL($str) { + return rtrim(rtrim(trim($str), ';')) . ' '; + } + + /************************* + * + * Compiling queries + * + *************************/ + + /** + * Compiles an SQL query from components + * + * @param array $components Array of SQL query components + * @return string SQL query + * @see parseSQL() + */ + public function compileSQL($components) { + return $this->getSqlCompiler()->compileSQL($components); + } + + /** + * Compiles a "SELECT [output] FROM..:" field list based on input array (made with ->parseFieldList()) + * Can also compile field lists for ORDER BY and GROUP BY. + * + * @param array $selectFields Array of select fields, (made with ->parseFieldList()) + * @param bool $compileComments Whether comments should be compiled + * @return string Select field string + * @see parseFieldList() + */ + public function compileFieldList($selectFields, $compileComments = TRUE) { + return $this->getSqlCompiler()->compileFieldList($selectFields, $compileComments); + } + + /** + * Compiles a "FROM [output] WHERE..:" table list based on input array (made with ->parseFromTables()) + * + * @param array $tablesArray Array of table names, (made with ->parseFromTables()) + * @return string Table name string + * @see parseFromTables() + */ + public function compileFromTables($tablesArray) { + return $this->getSqlCompiler()->compileFromTables($tablesArray); + } + + /** + * Compile field definition + * + * @param array $fieldCfg Field definition parts + * @return string Field definition string + */ + public function compileFieldCfg($fieldCfg) { + return $this->getSqlCompiler()->compileFieldCfg($fieldCfg); + } + /** * Implodes an array of WHERE clause configuration into a WHERE clause. * @@ -507,277 +1711,25 @@ class SqlParser extends \TYPO3\CMS\Core\Database\SqlParser { * @see \TYPO3\CMS\Core\Database\SqlParser::parseWhereClause() */ public function compileWhereClause($clauseArray, $functionMapping = TRUE) { - $output = ''; - switch ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->lastHandlerKey]['type']) { - case 'native': - $output = parent::compileWhereClause($clauseArray); - break; - case 'adodb': - // Prepare buffer variable: - $output = ''; - // Traverse clause array: - if (is_array($clauseArray)) { - foreach ($clauseArray as $v) { - // Set operator: - $output .= $v['operator'] ? ' ' . $v['operator'] : ''; - // Look for sublevel: - if (is_array($v['sub'])) { - $output .= ' (' . trim($this->compileWhereClause($v['sub'], $functionMapping)) . ')'; - } elseif (isset($v['func']) && $v['func']['type'] === 'EXISTS') { - $output .= ' ' . trim($v['modifier']) . ' EXISTS (' . $this->compileSELECT($v['func']['subquery']) . ')'; - } else { - if (isset($v['func']) && $v['func']['type'] === 'LOCATE') { - $output .= ' ' . trim($v['modifier']); - switch (TRUE) { - case $this->databaseConnection->runningADOdbDriver('mssql') && $functionMapping: - $output .= ' CHARINDEX('; - $output .= $v['func']['substr'][1] . $v['func']['substr'][0] . $v['func']['substr'][1]; - $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= isset($v['func']['pos']) ? ', ' . $v['func']['pos'][0] : ''; - $output .= ')'; - break; - case $this->databaseConnection->runningADOdbDriver('oci8') && $functionMapping: - $output .= ' INSTR('; - $output .= ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= ', ' . $v['func']['substr'][1] . $v['func']['substr'][0] . $v['func']['substr'][1]; - $output .= isset($v['func']['pos']) ? ', ' . $v['func']['pos'][0] : ''; - $output .= ')'; - break; - default: - $output .= ' LOCATE('; - $output .= $v['func']['substr'][1] . $v['func']['substr'][0] . $v['func']['substr'][1]; - $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= isset($v['func']['pos']) ? ', ' . $v['func']['pos'][0] : ''; - $output .= ')'; - } - } elseif (isset($v['func']) && $v['func']['type'] === 'IFNULL') { - $output .= ' ' . trim($v['modifier']) . ' '; - switch (TRUE) { - case $this->databaseConnection->runningADOdbDriver('mssql') && $functionMapping: - $output .= 'ISNULL'; - break; - case $this->databaseConnection->runningADOdbDriver('oci8') && $functionMapping: - $output .= 'NVL'; - break; - default: - $output .= 'IFNULL'; - } - $output .= '('; - $output .= ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= ', ' . $v['func']['default'][1] . $this->compileAddslashes($v['func']['default'][0]) . $v['func']['default'][1]; - $output .= ')'; - } elseif (isset($v['func']) && $v['func']['type'] === 'FIND_IN_SET') { - $output .= ' ' . trim($v['modifier']) . ' '; - if ($functionMapping) { - switch (TRUE) { - case $this->databaseConnection->runningADOdbDriver('mssql'): - $field = ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - if (!isset($v['func']['str_like'])) { - $v['func']['str_like'] = $v['func']['str'][0]; - } - $output .= '\',\'+' . $field . '+\',\' LIKE \'%,' . $v['func']['str_like'] . ',%\''; - break; - case $this->databaseConnection->runningADOdbDriver('oci8'): - $field = ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - if (!isset($v['func']['str_like'])) { - $v['func']['str_like'] = $v['func']['str'][0]; - } - $output .= '\',\'||' . $field . '||\',\' LIKE \'%,' . $v['func']['str_like'] . ',%\''; - break; - case $this->databaseConnection->runningADOdbDriver('postgres'): - $output .= ' FIND_IN_SET('; - $output .= $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1]; - $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= ') != 0'; - break; - default: - $field = ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - if (!isset($v['func']['str_like'])) { - $v['func']['str_like'] = $v['func']['str'][0]; - } - $output .= '(' . $field . ' LIKE \'%,' . $v['func']['str_like'] . ',%\'' . ' OR ' . $field . ' LIKE \'' . $v['func']['str_like'] . ',%\'' . ' OR ' . $field . ' LIKE \'%,' . $v['func']['str_like'] . '\'' . ' OR ' . $field . '= ' . $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1] . ')'; - } - } else { - switch (TRUE) { - case $this->databaseConnection->runningADOdbDriver('mssql'): - - case $this->databaseConnection->runningADOdbDriver('oci8'): - - case $this->databaseConnection->runningADOdbDriver('postgres'): - $output .= ' FIND_IN_SET('; - $output .= $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1]; - $output .= ', ' . ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - $output .= ')'; - break; - default: - $field = ($v['func']['table'] ? $v['func']['table'] . '.' : '') . $v['func']['field']; - if (!isset($v['func']['str_like'])) { - $v['func']['str_like'] = $v['func']['str'][0]; - } - $output .= '(' . $field . ' LIKE \'%,' . $v['func']['str_like'] . ',%\'' . ' OR ' . $field . ' LIKE \'' . $v['func']['str_like'] . ',%\'' . ' OR ' . $field . ' LIKE \'%,' . $v['func']['str_like'] . '\'' . ' OR ' . $field . '= ' . $v['func']['str'][1] . $v['func']['str'][0] . $v['func']['str'][1] . ')'; - } - } - } else { - // Set field/table with modifying prefix if any: - $output .= ' ' . trim($v['modifier']) . ' '; - // DBAL-specific: Set calculation, if any: - if ($v['calc'] === '&' && $functionMapping) { - switch (TRUE) { - case $this->databaseConnection->runningADOdbDriver('oci8'): - // Oracle only knows BITAND(x,y) - sigh - $output .= 'BITAND(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . ',' . $v['calc_value'][1] . $this->compileAddslashes($v['calc_value'][0]) . $v['calc_value'][1] . ')'; - break; - default: - // MySQL, MS SQL Server, PostgreSQL support the &-syntax - $output .= trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . $v['calc'] . $v['calc_value'][1] . $this->compileAddslashes($v['calc_value'][0]) . $v['calc_value'][1]; - } - } elseif ($v['calc']) { - $output .= trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . $v['calc']; - if (isset($v['calc_table'])) { - $output .= trim(($v['calc_table'] ? $v['calc_table'] . '.' : '') . $v['calc_field']); - } else { - $output .= $v['calc_value'][1] . $this->compileAddslashes($v['calc_value'][0]) . $v['calc_value'][1]; - } - } elseif (!($this->databaseConnection->runningADOdbDriver('oci8') && preg_match('/(NOT )?LIKE( BINARY)?/', $v['comparator']) && $functionMapping)) { - $output .= trim(($v['table'] ? $v['table'] . '.' : '') . $v['field']); - } - } - // Set comparator: - if ($v['comparator']) { - $isLikeOperator = preg_match('/(NOT )?LIKE( BINARY)?/', $v['comparator']); - switch (TRUE) { - case $this->databaseConnection->runningADOdbDriver('oci8') && $isLikeOperator && $functionMapping: - // Oracle cannot handle LIKE on CLOB fields - sigh - if (isset($v['value']['operator'])) { - $values = array(); - foreach ($v['value']['args'] as $fieldDef) { - $values[] = ($fieldDef['table'] ? $fieldDef['table'] . '.' : '') . $fieldDef['field']; - } - $compareValue = ' ' . $v['value']['operator'] . '(' . implode(',', $values) . ')'; - } else { - $compareValue = $v['value'][1] . $this->compileAddslashes(trim($v['value'][0], '%')) . $v['value'][1]; - } - if (GeneralUtility::isFirstPartOfStr($v['comparator'], 'NOT')) { - $output .= 'NOT '; - } - // To be on the safe side - $isLob = TRUE; - if ($v['table']) { - // Table and field names are quoted: - $tableName = substr($v['table'], 1, strlen($v['table']) - 2); - $fieldName = substr($v['field'], 1, strlen($v['field']) - 2); - $fieldType = $this->databaseConnection->sql_field_metatype($tableName, $fieldName); - $isLob = $fieldType === 'B' || $fieldType === 'XL'; - } - if (strtoupper(substr($v['comparator'], -6)) === 'BINARY') { - if ($isLob) { - $output .= '(dbms_lob.instr(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . ', ' . $compareValue . ',1,1) > 0)'; - } else { - $output .= '(instr(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . ', ' . $compareValue . ',1,1) > 0)'; - } - } else { - if ($isLob) { - $output .= '(dbms_lob.instr(LOWER(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . '), ' . GeneralUtility::strtolower($compareValue) . ',1,1) > 0)'; - } else { - $output .= '(instr(LOWER(' . trim((($v['table'] ? $v['table'] . '.' : '') . $v['field'])) . '), ' . GeneralUtility::strtolower($compareValue) . ',1,1) > 0)'; - } - } - break; - default: - if ($isLikeOperator && $functionMapping) { - if ($this->databaseConnection->runningADOdbDriver('postgres') || $this->databaseConnection->runningADOdbDriver('postgres64') || $this->databaseConnection->runningADOdbDriver('postgres7') || $this->databaseConnection->runningADOdbDriver('postgres8')) { - // Remap (NOT)? LIKE to (NOT)? ILIKE - // and (NOT)? LIKE BINARY to (NOT)? LIKE - switch ($v['comparator']) { - case 'LIKE': - $v['comparator'] = 'ILIKE'; - break; - case 'NOT LIKE': - $v['comparator'] = 'NOT ILIKE'; - break; - default: - $v['comparator'] = str_replace(' BINARY', '', $v['comparator']); - } - } else { - // No more BINARY operator - $v['comparator'] = str_replace(' BINARY', '', $v['comparator']); - } - } - $output .= ' ' . $v['comparator']; - // Detecting value type; list or plain: - $comparator = $this->normalizeKeyword($v['comparator']); - if (GeneralUtility::inList('NOTIN,IN', $comparator)) { - if (isset($v['subquery'])) { - $output .= ' (' . $this->compileSELECT($v['subquery']) . ')'; - } else { - $valueBuffer = array(); - foreach ($v['value'] as $realValue) { - $valueBuffer[] = $realValue[1] . $this->compileAddslashes($realValue[0]) . $realValue[1]; - } - - $dbmsSpecifics = $this->databaseConnection->getSpecifics(); - if ($dbmsSpecifics === NULL) { - $output .= ' (' . trim(implode(',', $valueBuffer)) . ')'; - } else { - $chunkedList = $dbmsSpecifics->splitMaxExpressions($valueBuffer); - $chunkCount = count($chunkedList); - - if ($chunkCount === 1) { - $output .= ' (' . trim(implode(',', $valueBuffer)) . ')'; - } else { - $listExpressions = array(); - $field = trim(($v['table'] ? $v['table'] . '.' : '') . $v['field']); - - switch ($comparator) { - case 'IN': - $operator = 'OR'; - break; - case 'NOTIN': - $operator = 'AND'; - break; - default: - $operator = ''; - } - - for ($i = 0; $i < $chunkCount; ++$i) { - $listPart = trim(implode(',', $chunkedList[$i])); - $listExpressions[] = ' (' . $listPart . ')'; - } - - $implodeString = ' ' . $operator . ' ' . $field . ' ' . $v['comparator']; - - // add opening brace before field - $lastFieldPos = strrpos($output, $field); - $output = substr_replace($output, '(', $lastFieldPos, 0); - $output .= implode($implodeString, $listExpressions) . ')'; - } - } - } - } elseif (GeneralUtility::inList('BETWEEN,NOT BETWEEN', $v['comparator'])) { - $lbound = $v['values'][0]; - $ubound = $v['values'][1]; - $output .= ' ' . $lbound[1] . $this->compileAddslashes($lbound[0]) . $lbound[1]; - $output .= ' AND '; - $output .= $ubound[1] . $this->compileAddslashes($ubound[0]) . $ubound[1]; - } elseif (isset($v['value']['operator'])) { - $values = array(); - foreach ($v['value']['args'] as $fieldDef) { - $values[] = ($fieldDef['table'] ? $fieldDef['table'] . '.' : '') . $fieldDef['field']; - } - $output .= ' ' . $v['value']['operator'] . '(' . implode(',', $values) . ')'; - } else { - $output .= ' ' . $v['value'][1] . $this->compileAddslashes($v['value'][0]) . $v['value'][1]; - } - } - } - } - } - } - break; + return $this->getSqlCompiler()->compileWhereClause($clauseArray, $functionMapping); + } + + /** + * @return \TYPO3\CMS\Dbal\Database\SqlCompilers\Adodb|\TYPO3\CMS\Dbal\Database\SqlCompilers\Mysql + */ + protected function getSqlCompiler() { + if ((string)$this->databaseConnection->handlerCfg[$this->databaseConnection->lastHandlerKey]['type'] === 'native') { + return $this->nativeSqlCompiler; + } else { + return $this->sqlCompiler; } - return $output; } + /************************* + * + * Debugging + * + *************************/ /** * Performs the ultimate test of the parser: Direct a SQL query in; You will get it back (through the parsed and re-compiled) if no problems, otherwise the script will print the error and exit * @@ -806,4 +1758,59 @@ class SqlParser extends \TYPO3\CMS\Core\Database\SqlParser { } } + /** + * Check parsability of input SQL part string; Will parse and re-compile after which it is compared + * + * @param string $part Part definition of string; "SELECT" = fieldlist (also ORDER BY and GROUP BY), "FROM" = table list, "WHERE" = Where clause. + * @param string $str SQL string to verify parsability of + * @return mixed Returns array with string 1 and 2 if error, otherwise FALSE + */ + public function debug_parseSQLpart($part, $str) { + $retVal = FALSE; + switch ($part) { + case 'SELECT': + $retVal = $this->debug_parseSQLpartCompare($str, $this->compileFieldList($this->parseFieldList($str))); + break; + case 'FROM': + $retVal = $this->debug_parseSQLpartCompare($str, $this->getSqlCompiler()->compileFromTables($this->parseFromTables($str))); + break; + case 'WHERE': + $retVal = $this->debug_parseSQLpartCompare($str, $this->getSqlCompiler()->compileWhereClause($this->parseWhereClause($str))); + break; + } + return $retVal; + } + + /** + * Compare two query strings by stripping away whitespace. + * + * @param string $str SQL String 1 + * @param string $newStr SQL string 2 + * @param bool $caseInsensitive If TRUE, the strings are compared insensitive to case + * @return mixed Returns array with string 1 and 2 if error, otherwise FALSE + */ + public function debug_parseSQLpartCompare($str, $newStr, $caseInsensitive = FALSE) { + if ($caseInsensitive) { + $str1 = strtoupper($str); + $str2 = strtoupper($newStr); + } else { + $str1 = $str; + $str2 = $newStr; + } + + // Fixing escaped chars: + $search = array(NUL, LF, CR, SUB); + $replace = array("\x00", "\x0a", "\x0d", "\x1a"); + $str1 = str_replace($search, $replace, $str1); + $str2 = str_replace($search, $replace, $str2); + + $search = self::$interQueryWhitespaces; + if (str_replace($search, '', $this->trimSQL($str1)) !== str_replace($search, '', $this->trimSQL($str2))) { + return array( + str_replace($search, ' ', $str), + str_replace($search, ' ', $newStr), + ); + } + } + } diff --git a/typo3/sysext/dbal/Tests/Unit/Database/AbstractTestCase.php b/typo3/sysext/dbal/Tests/Unit/Database/AbstractTestCase.php index c6f0969e084f..fc09dd168164 100644 --- a/typo3/sysext/dbal/Tests/Unit/Database/AbstractTestCase.php +++ b/typo3/sysext/dbal/Tests/Unit/Database/AbstractTestCase.php @@ -14,6 +14,8 @@ namespace TYPO3\CMS\Dbal\Tests\Unit\Database; * The TYPO3 project - inspiring people to share! */ +use TYPO3\CMS\Core\Utility\GeneralUtility; + require_once __DIR__ . '/../../../../adodb/adodb/adodb.inc.php'; require_once __DIR__ . '/../../../../adodb/adodb/drivers/adodb-mssql.inc.php'; require_once __DIR__ . '/../../../../adodb/adodb/drivers/adodb-oci8.inc.php'; @@ -45,6 +47,8 @@ abstract class AbstractTestCase extends \TYPO3\CMS\Core\Tests\UnitTestCase { // Inject SqlParser - Its logic is tested with the tests, too. $sqlParser = $this->getAccessibleMock(\TYPO3\CMS\Dbal\Database\SqlParser::class, array('dummy'), array(), '', FALSE); $sqlParser->_set('databaseConnection', $subject); + $sqlParser->_set('sqlCompiler', GeneralUtility::makeInstance(\TYPO3\CMS\Dbal\Database\SqlCompilers\Adodb::class, $subject)); + $sqlParser->_set('nativeSqlCompiler', GeneralUtility::makeInstance(\TYPO3\CMS\Dbal\Database\SqlCompilers\Mysql::class, $subject)); $subject->SQLparser = $sqlParser; // Mock away schema migration service from install tool @@ -81,4 +85,4 @@ abstract class AbstractTestCase extends \TYPO3\CMS\Core\Tests\UnitTestCase { return trim($sql); } -} \ No newline at end of file +} diff --git a/typo3/sysext/dbal/Tests/Unit/Database/DatabaseConnectionOracleTest.php b/typo3/sysext/dbal/Tests/Unit/Database/DatabaseConnectionOracleTest.php index 5c41c82e4006..73f7cafce9df 100644 --- a/typo3/sysext/dbal/Tests/Unit/Database/DatabaseConnectionOracleTest.php +++ b/typo3/sysext/dbal/Tests/Unit/Database/DatabaseConnectionOracleTest.php @@ -131,7 +131,7 @@ class DatabaseConnectionOracleTest extends AbstractTestCase { $parseString .= 'VALUES (\'1\', \'0\', \'2\', \'0\', \'Africa\');'; $components = $this->subject->SQLparser->_callRef('parseINSERT', $parseString); $this->assertTrue(is_array($components), $components); - $insert = $this->subject->SQLparser->_callRef('compileINSERT', $components); + $insert = $this->subject->SQLparser->compileSQL($components); $expected = array( 'uid' => '1', 'pid' => '0', @@ -151,7 +151,7 @@ class DatabaseConnectionOracleTest extends AbstractTestCase { $parseString = 'INSERT INTO static_territories VALUES (\'1\', \'0\', \'2\', \'0\', \'Africa\'),(\'2\', \'0\', \'9\', \'0\', \'Oceania\'),' . '(\'3\', \'0\', \'19\', \'0\', \'Americas\'),(\'4\', \'0\', \'142\', \'0\', \'Asia\');'; $components = $this->subject->SQLparser->_callRef('parseINSERT', $parseString); $this->assertTrue(is_array($components), $components); - $insert = $this->subject->SQLparser->_callRef('compileINSERT', $components); + $insert = $this->subject->SQLparser->compileSQL($components); $insertCount = count($insert); $this->assertEquals(4, $insertCount); for ($i = 0; $i < $insertCount; $i++) { @@ -643,7 +643,7 @@ class DatabaseConnectionOracleTest extends AbstractTestCase { '; $components = $this->subject->SQLparser->_callRef('parseCREATETABLE', $parseString); $this->assertTrue(is_array($components), 'Not an array: ' . $components); - $sqlCommands = $this->subject->SQLparser->_call('compileCREATETABLE', $components); + $sqlCommands = $this->subject->SQLparser->compileSQL($components); $this->assertTrue(is_array($sqlCommands), 'Not an array: ' . $sqlCommands); $this->assertEquals(6, count($sqlCommands)); $expected = $this->cleanSql(' @@ -683,7 +683,7 @@ class DatabaseConnectionOracleTest extends AbstractTestCase { '; $components = $this->subject->SQLparser->_callRef('parseCREATETABLE', $parseString); $this->assertTrue(is_array($components), 'Not an array: ' . $components); - $sqlCommands = $this->subject->SQLparser->_call('compileCREATETABLE', $components); + $sqlCommands = $this->subject->SQLparser->compileSQL($components); $this->assertTrue(is_array($sqlCommands), 'Not an array: ' . $sqlCommands); $this->assertEquals(4, count($sqlCommands)); $expected = $this->cleanSql(' diff --git a/typo3/sysext/dbal/Tests/Unit/Database/DatabaseConnectionPostgresqlTest.php b/typo3/sysext/dbal/Tests/Unit/Database/DatabaseConnectionPostgresqlTest.php index 638f1f306f81..7de4814ae92c 100644 --- a/typo3/sysext/dbal/Tests/Unit/Database/DatabaseConnectionPostgresqlTest.php +++ b/typo3/sysext/dbal/Tests/Unit/Database/DatabaseConnectionPostgresqlTest.php @@ -153,7 +153,7 @@ class DatabaseConnectionPostgresqlTest extends AbstractTestCase { $components = $this->subject->SQLparser->_callRef('parseALTERTABLE', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->SQLparser->_callRef('compileALTERTABLE', $components); + $result = $this->subject->SQLparser->compileSQL($components); $expected = array('CREATE INDEX "dd81ee97_parent" ON "sys_collection" ("pid", "deleted")'); $this->assertSame($expected, $this->cleanSql($result)); } @@ -167,7 +167,7 @@ class DatabaseConnectionPostgresqlTest extends AbstractTestCase { $components = $this->subject->SQLparser->_callRef('parseALTERTABLE', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->SQLparser->_callRef('compileALTERTABLE', $components); + $result = $this->subject->SQLparser->compileSQL($components); $expected = array('DROP INDEX "dd81ee97_parent"'); $this->assertSame($expected, $this->cleanSql($result)); } diff --git a/typo3/sysext/dbal/Tests/Unit/Database/SqlParserTest.php b/typo3/sysext/dbal/Tests/Unit/Database/SqlParserTest.php index f5b0cd7c3414..6e9c4d5a6a45 100644 --- a/typo3/sysext/dbal/Tests/Unit/Database/SqlParserTest.php +++ b/typo3/sysext/dbal/Tests/Unit/Database/SqlParserTest.php @@ -14,6 +14,8 @@ namespace TYPO3\CMS\Dbal\Tests\Unit\Database; * The TYPO3 project - inspiring people to share! */ +use TYPO3\CMS\Core\Utility\GeneralUtility; + /** * Test case */ @@ -33,10 +35,473 @@ class SqlParserTest extends AbstractTestCase { $mockDatabaseConnection = $this->getMock(\TYPO3\CMS\Dbal\Database\DatabaseConnection::class, array(), array(), '', FALSE); $mockDatabaseConnection->lastHandlerKey = '_DEFAULT'; $subject->_set('databaseConnection', $mockDatabaseConnection); + $subject->_set('sqlCompiler', GeneralUtility::makeInstance(\TYPO3\CMS\Dbal\Database\SqlCompilers\Adodb::class, $mockDatabaseConnection)); + $subject->_set('nativeSqlCompiler', GeneralUtility::makeInstance(\TYPO3\CMS\Dbal\Database\SqlCompilers\Mysql::class, $mockDatabaseConnection)); $this->subject = $subject; } + /** + * Regression test + * + * @test + */ + public function compileWhereClauseDoesNotDropClauses() { + $clauses = array( + 0 => array( + 'modifier' => '', + 'table' => 'pages', + 'field' => 'fe_group', + 'calc' => '', + 'comparator' => '=', + 'value' => array( + 0 => '', + 1 => '\'' + ) + ), + 1 => array( + 'operator' => 'OR', + 'modifier' => '', + 'func' => array( + 'type' => 'IFNULL', + 'default' => array( + 0 => '1', + 1 => '\'' + ), + 'table' => 'pages', + 'field' => 'fe_group' + ) + ), + 2 => array( + 'operator' => 'OR', + 'modifier' => '', + 'table' => 'pages', + 'field' => 'fe_group', + 'calc' => '', + 'comparator' => '=', + 'value' => array( + 0 => '0', + 1 => '\'' + ) + ), + 3 => array( + 'operator' => 'OR', + 'modifier' => '', + 'func' => array( + 'type' => 'FIND_IN_SET', + 'str' => array( + 0 => '0', + 1 => '\'' + ), + 'table' => 'pages', + 'field' => 'fe_group' + ), + 'comparator' => '' + ), + 4 => array( + 'operator' => 'OR', + 'modifier' => '', + 'func' => array( + 'type' => 'FIND_IN_SET', + 'str' => array( + 0 => '-1', + 1 => '\'' + ), + 'table' => 'pages', + 'field' => 'fe_group' + ), + 'comparator' => '' + ), + 5 => array( + 'operator' => 'OR', + 'modifier' => '', + 'func' => array( + 'type' => 'CAST', + 'table' => 'pages', + 'field' => 'fe_group', + 'datatype' => 'CHAR' + ), + 'comparator' => '=', + 'value' => array( + 0 => '', + 1 => '\'' + ) + ) + ); + $output = $this->subject->compileWhereClause($clauses); + $parts = explode(' OR ', $output); + $this->assertSame(count($clauses), count($parts)); + $this->assertContains('IFNULL', $output); + } + + /** + * Data provider for trimSqlReallyTrimsAllWhitespace + * + * @see trimSqlReallyTrimsAllWhitespace + */ + public function trimSqlReallyTrimsAllWhitespaceDataProvider() { + return array( + 'Nothing to trim' => array('SELECT * FROM test WHERE 1=1;', 'SELECT * FROM test WHERE 1=1 '), + 'Space after ;' => array('SELECT * FROM test WHERE 1=1; ', 'SELECT * FROM test WHERE 1=1 '), + 'Space before ;' => array('SELECT * FROM test WHERE 1=1 ;', 'SELECT * FROM test WHERE 1=1 '), + 'Space before and after ;' => array('SELECT * FROM test WHERE 1=1 ; ', 'SELECT * FROM test WHERE 1=1 '), + 'Linefeed after ;' => array('SELECT * FROM test WHERE 1=1' . LF . ';', 'SELECT * FROM test WHERE 1=1 '), + 'Linefeed before ;' => array('SELECT * FROM test WHERE 1=1;' . LF, 'SELECT * FROM test WHERE 1=1 '), + 'Linefeed before and after ;' => array('SELECT * FROM test WHERE 1=1' . LF . ';' . LF, 'SELECT * FROM test WHERE 1=1 '), + 'Tab after ;' => array('SELECT * FROM test WHERE 1=1' . TAB . ';', 'SELECT * FROM test WHERE 1=1 '), + 'Tab before ;' => array('SELECT * FROM test WHERE 1=1;' . TAB, 'SELECT * FROM test WHERE 1=1 '), + 'Tab before and after ;' => array('SELECT * FROM test WHERE 1=1' . TAB . ';' . TAB, 'SELECT * FROM test WHERE 1=1 '), + ); + } + + /** + * @test + * @dataProvider trimSqlReallyTrimsAllWhitespaceDataProvider + * @param string $sql The SQL to trim + * @param string $expected The expected trimmed SQL with single space at the end + */ + public function trimSqlReallyTrimsAllWhitespace($sql, $expected) { + $result = $this->subject->_call('trimSQL', $sql); + $this->assertSame($expected, $result); + } + + /** + * Data provider for getValueReturnsCorrectValues + * + * @see getValueReturnsCorrectValues + */ + public function getValueReturnsCorrectValuesDataProvider() { + return array( + // description => array($parseString, $comparator, $mode, $expected) + 'key definition without length' => array('(pid,input_1), ', '_LIST', 'INDEX', array('pid', 'input_1')), + 'key definition with length' => array('(pid,input_1(30)), ', '_LIST', 'INDEX', array('pid', 'input_1(30)')), + 'key definition without length (no mode)' => array('(pid,input_1), ', '_LIST', '', array('pid', 'input_1')), + 'key definition with length (no mode)' => array('(pid,input_1(30)), ', '_LIST', '', array('pid', 'input_1(30)')), + 'test1' => array('input_1 varchar(255) DEFAULT \'\' NOT NULL,', '', '', array('input_1')), + 'test2' => array('varchar(255) DEFAULT \'\' NOT NULL,', '', '', array('varchar(255)')), + 'test3' => array('DEFAULT \'\' NOT NULL,', '', '', array('DEFAULT')), + 'test4' => array('\'\' NOT NULL,', '', '', array('', '\'')), + 'test5' => array('NOT NULL,', '', '', array('NOT')), + 'test6' => array('NULL,', '', '', array('NULL')), + 'getValueOrParameter' => array('NULL,', '', '', array('NULL')), + ); + } + + /** + * @test + * @dataProvider getValueReturnsCorrectValuesDataProvider + * @param string $parseString the string to parse + * @param string $comparator The comparator used before. If "NOT IN" or "IN" then the value is expected to be a list of values. Otherwise just an integer (un-quoted) or string (quoted) + * @param string $mode The mode, eg. "INDEX + * @param string $expected + */ + public function getValueReturnsCorrectValues($parseString, $comparator, $mode, $expected) { + $result = $this->subject->_callRef('getValue', $parseString, $comparator, $mode); + $this->assertSame($expected, $result); + } + + /** + * Data provider for parseSQL + * + * @see parseSQL + */ + public function parseSQLDataProvider() { + $testSql = array(); + $testSql[] = 'CREATE TABLE tx_demo ('; + $testSql[] = ' uid int(11) NOT NULL auto_increment,'; + $testSql[] = ' pid int(11) DEFAULT \'0\' NOT NULL,'; + + $testSql[] = ' tstamp int(11) unsigned DEFAULT \'0\' NOT NULL,'; + $testSql[] = ' crdate int(11) unsigned DEFAULT \'0\' NOT NULL,'; + $testSql[] = ' cruser_id int(11) unsigned DEFAULT \'0\' NOT NULL,'; + $testSql[] = ' deleted tinyint(4) unsigned DEFAULT \'0\' NOT NULL,'; + $testSql[] = ' hidden tinyint(4) unsigned DEFAULT \'0\' NOT NULL,'; + $testSql[] = ' starttime int(11) unsigned DEFAULT \'0\' NOT NULL,'; + $testSql[] = ' endtime int(11) unsigned DEFAULT \'0\' NOT NULL,'; + + $testSql[] = ' input_1 varchar(255) DEFAULT \'\' NOT NULL,'; + $testSql[] = ' input_2 varchar(255) DEFAULT \'\' NOT NULL,'; + $testSql[] = ' select_child int(11) unsigned DEFAULT \'0\' NOT NULL,'; + + $testSql[] = ' PRIMARY KEY (uid),'; + $testSql[] = ' KEY parent (pid,input_1),'; + $testSql[] = ' KEY bar (tstamp,input_1(200),input_2(100),endtime)'; + $testSql[] = ');'; + $testSql = implode("\n", $testSql); + $expected = array( + 'type' => 'CREATETABLE', + 'TABLE' => 'tx_demo', + 'FIELDS' => array( + 'uid' => array( + 'definition' => array( + 'fieldType' => 'int', + 'value' => '11', + 'featureIndex' => array( + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ), + 'AUTO_INCREMENT' => array( + 'keyword' => 'auto_increment' + ) + ) + ) + ), + 'pid' => array( + 'definition' => array( + 'fieldType' => 'int', + 'value' => '11', + 'featureIndex' => array( + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '0', + 1 => '\'', + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'tstamp' => array( + 'definition' => array( + 'fieldType' => 'int', + 'value' => '11', + 'featureIndex' => array( + 'UNSIGNED' => array( + 'keyword' => 'unsigned' + ), + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '0', + 1 => '\'' + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'crdate' => array( + 'definition' => array( + 'fieldType' => 'int', + 'value' => '11', + 'featureIndex' => array( + 'UNSIGNED' => array( + 'keyword' => 'unsigned' + ), + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '0', + 1 => '\'' + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'cruser_id' => array( + 'definition' => array( + 'fieldType' => 'int', + 'value' => '11', + 'featureIndex' => array( + 'UNSIGNED' => array( + 'keyword' => 'unsigned' + ), + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '0', + 1 => '\'', + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'deleted' => array( + 'definition' => array( + 'fieldType' => 'tinyint', + 'value' => '4', + 'featureIndex' => array( + 'UNSIGNED' => array( + 'keyword' => 'unsigned' + ), + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '0', + 1 => '\'' + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'hidden' => array( + 'definition' => array( + 'fieldType' => 'tinyint', + 'value' => '4', + 'featureIndex' => array( + 'UNSIGNED' => array( + 'keyword' => 'unsigned' + ), + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '0', + 1 => '\'' + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'starttime' => array( + 'definition' => array( + 'fieldType' => 'int', + 'value' => '11', + 'featureIndex' => array( + 'UNSIGNED' => array( + 'keyword' => 'unsigned' + ), + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '0', + 1 => '\'' + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'endtime' => array( + 'definition' => array( + 'fieldType' => 'int', + 'value' => '11', + 'featureIndex' => array( + 'UNSIGNED' => array( + 'keyword' => 'unsigned' + ), + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '0', + 1 => '\'', + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'input_1' => array( + 'definition' => array( + 'fieldType' => 'varchar', + 'value' => '255', + 'featureIndex' => array( + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '', + 1 => '\'', + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'input_2' => array( + 'definition' => array( + 'fieldType' => 'varchar', + 'value' => '255', + 'featureIndex' => array( + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '', + 1 => '\'', + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ), + 'select_child' => array( + 'definition' => array( + 'fieldType' => 'int', + 'value' => '11', + 'featureIndex' => array( + 'UNSIGNED' => array( + 'keyword' => 'unsigned' + ), + 'DEFAULT' => array( + 'keyword' => 'DEFAULT', + 'value' => array( + 0 => '0', + 1 => '\'' + ) + ), + 'NOTNULL' => array( + 'keyword' => 'NOT NULL' + ) + ) + ) + ) + ), + 'KEYS' => array( + 'PRIMARYKEY' => array( + 0 => 'uid' + ), + 'parent' => array( + 0 => 'pid', + 1 => 'input_1', + ), + 'bar' => array( + 0 => 'tstamp', + 1 => 'input_1(200)', + 2 => 'input_2(100)', + 3 => 'endtime', + ) + ) + ); + + return array( + 'test1' => array($testSql, $expected) + ); + } + + /** + * @test + * @dataProvider parseSQLDataProvider + * @param string $sql The SQL to trim + * @param array $expected The expected trimmed SQL with single space at the end + */ + public function parseSQL($sql, $expected) { + $result = $this->subject->_callRef('parseSQL', $sql); + $this->assertSame($expected, $result); + } + /** * @test */ @@ -165,7 +630,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseINSERT', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileINSERT', $components); + $result = $this->subject->compileSQL($components); $expected = 'INSERT INTO static_country_zones VALUES (\'483\',\'0\',\'NL\',\'NLD\',\'528\',\'DR\',\'Drenthe\',\'\')'; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -178,7 +643,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseINSERT', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileINSERT', $components); + $result = $this->subject->compileSQL($components); $expected = 'INSERT INTO static_country_zones VALUES (\'483\',\'0\',\'NL\',\'NLD\',\'528\',\'DR\',\'Drenthe\',\'\')'; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -192,7 +657,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseINSERT', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileINSERT', $components); + $result = $this->subject->compileSQL($components); $expected = 'INSERT INTO static_territories (uid,pid,tr_iso_nr,tr_parent_iso_nr,tr_name_en) '; $expected .= 'VALUES (\'1\',\'0\',\'2\',\'0\',\'Africa\')'; $this->assertEquals($expected, $this->cleanSql($result)); @@ -206,7 +671,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseINSERT', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileINSERT', $components); + $result = $this->subject->compileSQL($components); $expected = 'INSERT INTO static_territories VALUES (\'1\',\'0\',\'2\',\'0\',\'Africa\'),(\'2\',\'0\',\'9\',\'0\',\'Oceania\'),(\'3\',\'0\',\'19\',\'0\',\'Americas\'),(\'4\',\'0\',\'142\',\'0\',\'Asia\')'; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -220,7 +685,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseINSERT', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileINSERT', $components); + $result = $this->subject->compileSQL($components); $expected = 'INSERT INTO static_territories (uid,pid,tr_iso_nr,tr_parent_iso_nr,tr_name_en) '; $expected .= 'VALUES (\'1\',\'0\',\'2\',\'0\',\'Africa\'),(\'2\',\'0\',\'9\',\'0\',\'Oceania\')'; $this->assertEquals($expected, $this->cleanSql($result)); @@ -264,7 +729,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseSELECT', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileSELECT', $components); + $result = $this->subject->compileSQL($components); $expected = 'SELECT * FROM tx_irfaq_q_cat_mm WHERE IFNULL(tx_irfaq_q_cat_mm.uid_foreign, 0) = 1'; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -289,7 +754,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseSELECT', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileSELECT', $components); + $result = $this->subject->compileSQL($components); $expected = 'SELECT * FROM sys_category WHERE CAST(parent AS CHAR) != \'\''; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -303,7 +768,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseALTERTABLE', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileALTERTABLE', $components); + $result = $this->subject->compileSQL($components); $expected = 'ALTER TABLE tx_realurl_pathcache ENGINE = InnoDB'; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -317,7 +782,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseALTERTABLE', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileALTERTABLE', $components); + $result = $this->subject->compileSQL($components); $expected = 'ALTER TABLE index_phash DEFAULT CHARACTER SET utf8'; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -331,7 +796,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseALTERTABLE', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileALTERTABLE', $components); + $result = $this->subject->compileSQL($components); $expected = 'ALTER TABLE sys_collection ADD KEY parent (pid,deleted)'; $this->assertSame($expected, $this->cleanSql($result)); } @@ -345,7 +810,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseALTERTABLE', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileALTERTABLE', $components); + $result = $this->subject->compileSQL($components); $expected = 'ALTER TABLE sys_collection DROP KEY parent'; $this->assertSame($expected, $this->cleanSql($result)); } @@ -359,7 +824,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseSELECT', $parseString); $this->assertInternalType('array', $components); - $result = $this->subject->_callRef('compileSELECT', $components); + $result = $this->subject->compileSQL($components); $expected = 'SELECT * FROM fe_users WHERE FIND_IN_SET(10, usergroup)'; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -751,7 +1216,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseSELECT', $sql); $components['parameters'][':pageId'][0] = $pageId; - $result = $this->subject->_callRef('compileSELECT', $components); + $result = $this->subject->compileSQL($components); $expected = 'SELECT * FROM pages WHERE uid = 12 OR uid IN (SELECT uid FROM pages WHERE pid = 12)'; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -766,7 +1231,7 @@ class SqlParserTest extends AbstractTestCase { $components = $this->subject->_callRef('parseSELECT', $sql); $components['parameters'][':pid'][0] = $pid; - $result = $this->subject->_callRef('compileSELECT', $components); + $result = $this->subject->compileSQL($components); $expected = 'SELECT * FROM pages WHERE pid = ' . $pid . ' AND title NOT LIKE \':pid\''; $this->assertEquals($expected, $this->cleanSql($result)); } @@ -784,7 +1249,7 @@ class SqlParserTest extends AbstractTestCase { $components['parameters']['?'][$i][0] = $parameterValues[$i]; } - $result = $this->subject->_callRef('compileSELECT', $components); + $result = $this->subject->compileSQL($components); $expected = 'SELECT * FROM pages WHERE pid = 12 AND timestamp < 1281782690 AND title != \'How to test?\''; $this->assertEquals($expected, $this->cleanSql($result)); } diff --git a/typo3/sysext/dbal/ext_localconf.php b/typo3/sysext/dbal/ext_localconf.php index 60aaa507abba..31f004d008a1 100644 --- a/typo3/sysext/dbal/ext_localconf.php +++ b/typo3/sysext/dbal/ext_localconf.php @@ -2,7 +2,6 @@ defined('TYPO3_MODE') or die(); $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Core\Database\DatabaseConnection::class] = array('className' => \TYPO3\CMS\Dbal\Database\DatabaseConnection::class); -$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Core\Database\SqlParser::class] = array('className' => \TYPO3\CMS\Dbal\Database\SqlParser::class); $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList::class] = array('className' => \TYPO3\CMS\Dbal\RecordList\DatabaseRecordList::class); // Register caches if not already done in localconf.php or a previously loaded extension. -- GitLab