├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Formatter.php ├── Helper │ ├── Comment.php │ ├── Indent.php │ ├── NewLine.php │ ├── Parentheses.php │ ├── Token.php │ └── WhiteSpace.php └── Tokenizer │ ├── Parser │ ├── Boundary.php │ ├── Comment.php │ ├── LiteralString.php │ ├── Numeral.php │ ├── Quoted.php │ ├── Reserved.php │ ├── UserDefined.php │ └── WhiteSpace.php │ └── Tokenizer.php └── tests ├── FormatterTest.php ├── Resources └── expectedQueries.sql └── Tokenizer └── TokenizerTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: IBpLC0WCtjsqFy5M7PSyMvz2yMpd81xLD 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | bin/ 3 | build/ 4 | vendor/ 5 | composer.lock 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - "5.5" 4 | - "5.6" 5 | - "7.0" 6 | - "hhvm" 7 | 8 | before_script: 9 | - composer install 10 | 11 | script: 12 | - bin/phpunit --coverage-text 13 | 14 | matrix: 15 | allow_failures: 16 | - php: "hhvm" 17 | branches: 18 | only: 19 | - master 20 | 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | First of all, **thank you** for contributing, **you are awesome**! 5 | 6 | Here are a few rules to follow in order to ease code reviews, and discussions before 7 | maintainers accept and merge your work. 8 | 9 | You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and 10 | [PSR-2](http://www.php-fig.org/psr/2/). If you don't know about any of them, you 11 | should really read the recommendations. Can't wait? Use the [PHP-CS-Fixer 12 | tool](http://cs.sensiolabs.org/). 13 | 14 | You MUST run the test suite. 15 | 16 | You MUST write (or update) unit tests. 17 | 18 | You SHOULD write documentation. 19 | 20 | Please, write [commit messages that make 21 | sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), 22 | and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing) 23 | before submitting your Pull Request. 24 | 25 | One may ask you to [squash your 26 | commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) 27 | too. This is used to "clean" your Pull Request before merging it (we don't want 28 | commits such as `fix tests`, `fix 2`, `fix 3`, etc.). 29 | 30 | Also, while creating your Pull Request on GitHub, you MUST write a description 31 | which gives the context and/or explains why you are creating it. 32 | 33 | Thank you! 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nil Portugués Calderó 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SQL Query Formatter 2 | ================= 3 | 4 | [![Build Status](https://travis-ci.org/nilportugues/php-sql-query-formatter.svg)](https://travis-ci.org/nilportugues/php-sql-query-formatter) 5 | [![Coverage Status](https://img.shields.io/coveralls/nilportugues/sql-query-formatter.svg)](https://coveralls.io/r/nilportugues/sql-query-formatter?branch=master) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nilportugues/sql-query-formatter/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/nilportugues/sql-query-formatter/?branch=master) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/a57aa8f3-bbe1-43a5-941e-689d8435ab20/mini.png)](https://insight.sensiolabs.com/projects/a57aa8f3-bbe1-43a5-941e-689d8435ab20) 7 | [![Latest Stable Version](https://poser.pugx.org/nilportugues/sql-query-formatter/v/stable)](https://packagist.org/packages/nilportugues/sql-query-formatter) 8 | [![Total Downloads](https://poser.pugx.org/nilportugues/sql-query-formatter/downloads)](https://packagist.org/packages/nilportugues/sql-query-formatter) 9 | [![License](https://poser.pugx.org/nilportugues/sql-query-formatter/license)](https://packagist.org/packages/nilportugues/sql-query-formatter) 10 | [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://paypal.me/nilportugues) 11 | 12 | A very lightweight PHP class that re-formats unreadable or computer-generated SQL query statements to human-friendly readable text. 13 | 14 | * [1.Installation](#block1) 15 | * [2. Features](#block2) 16 | * [3. Usage](#block3) 17 | * [4. Code Quality](#block5) 18 | * [5. Author](#block6) 19 | * [6. Special Thanks](#block6) 20 | * [7. License](#block7) 21 | 22 | 23 | ## 1.Installation 24 | The recommended way to install the SQL Query Formatter is through [Composer](http://getcomposer.org). Run the following command to install it: 25 | 26 | ```sh 27 | php composer.phar require nilportugues/sql-query-formatter 28 | ``` 29 | 30 | 31 | ## 2. Features 32 | 33 | **Human readable SQL formatting** 34 | 35 | - Human readable plain text. No colours, no highlighting. Plain text is good enough in most cases. 36 | 37 | **Data Binding Awareness** 38 | 39 | - SQL Query Formatter takes data binding seriously. 40 | - Placeholder syntax such as `:variable` or `?` is taken into account and is preserved when formatting. 41 | 42 | 43 | 44 | ## 3. Usage 45 | 46 | Sample code: 47 | ```php 48 | format($query); 61 | ``` 62 | 63 | Real output: 64 | ```sql 65 | SELECT 66 | user.user_id, 67 | user.username, 68 | ( 69 | SELECT 70 | role.role_name 71 | FROM 72 | role 73 | WHERE 74 | (role.role_id = :v1) 75 | LIMIT 76 | :v2, 77 | :v3 78 | ) AS user_role, 79 | ( 80 | SELECT 81 | role.role_name 82 | FROM 83 | role 84 | WHERE 85 | (role.role_id = :v4) 86 | LIMIT 87 | :v5, 88 | :v6 89 | ) AS role 90 | FROM 91 | user 92 | WHERE 93 | (user.user_id = :v7) 94 | 95 | ``` 96 | 97 | 98 | ## 4. Fully tested 99 | Testing has been done using PHPUnit and [Travis-CI](https://travis-ci.org). All code has been tested to be compatible from PHP 5.4 up to PHP 5.6 and [HHVM (nightly release)](http://hhvm.com/). 100 | 101 | To run the test suite, you need [Composer](http://getcomposer.org): 102 | 103 | ```bash 104 | php composer.phar install --dev 105 | bin/phpunit 106 | ``` 107 | 108 | 109 | 110 | ## 5. Author 111 | Nil Portugués Calderó 112 | 113 | - 114 | - [http://nilportugues.com](http://nilportugues.com) 115 | 116 | 117 | 118 | ## 6. Special Thanks 119 | I would like to thank the following people: 120 | 121 | - [Jeremy Dorn](mailto:jeremy@jeremydorn.com) for his [sql-formatter](https://github.com/jdorn/sql-formatter) implementation I used as a basis for building this version. 122 | 123 | 124 | 125 | ## 7. License 126 | SQL Query Formatter is licensed under the MIT license. 127 | 128 | ``` 129 | Copyright (c) 2015 Nil Portugués Calderó 130 | 131 | Permission is hereby granted, free of charge, to any person obtaining a copy 132 | of this software and associated documentation files (the "Software"), to deal 133 | in the Software without restriction, including without limitation the rights 134 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 135 | copies of the Software, and to permit persons to whom the Software is 136 | furnished to do so, subject to the following conditions: 137 | 138 | The above copyright notice and this permission notice shall be included in 139 | all copies or substantial portions of the Software. 140 | 141 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 142 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 143 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 144 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 145 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 146 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 147 | THE SOFTWARE. 148 | ``` 149 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"nilportugues/sql-query-formatter", 3 | "description":"A very lightweight PHP class that reformats unreadable and computer-generated SQL query statements to human-friendly, readable text.", 4 | "keywords": [ "sql", "mysql", "query", "formatter", "format", "parser", "tokenizer", "reformat", "sql server" ], 5 | "type":"library", 6 | "license":"MIT", 7 | "homepage":"http://nilportugues.com", 8 | "authors": 9 | [ 10 | { 11 | "name":"Nil Portugués Calderó", 12 | "email":"contact@nilportugues.com", 13 | "homepage":"http://nilportugues.com", 14 | "role":"Lead Developer" 15 | } 16 | ], 17 | "autoload":{ 18 | "psr-4":{ 19 | "NilPortugues\\Sql\\QueryFormatter\\":"src/" 20 | } 21 | }, 22 | "autoload-dev":{ 23 | "psr-4":{ 24 | "NilPortugues\\Tests\\Sql\\QueryFormatter\\":"tests/" 25 | } 26 | }, 27 | "require": 28 | { 29 | "php": ">=5.5" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "4.*", 33 | "fabpot/php-cs-fixer": "~1.9", 34 | "nilportugues/php_backslasher": "~0.2" 35 | }, 36 | "config": 37 | { 38 | "bin-dir": "bin" 39 | }, 40 | "minimum-stability": "stable" 41 | } 42 | 43 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ./tests 31 | 32 | 33 | 34 | 35 | 36 | ./ 37 | 38 | ./vendor/ 39 | ./tests/ 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/Formatter.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 6/26/14 5 | * Time: 12:10 AM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Helper\Comment; 14 | use NilPortugues\Sql\QueryFormatter\Helper\Indent; 15 | use NilPortugues\Sql\QueryFormatter\Helper\NewLine; 16 | use NilPortugues\Sql\QueryFormatter\Helper\Parentheses; 17 | use NilPortugues\Sql\QueryFormatter\Helper\Token; 18 | use NilPortugues\Sql\QueryFormatter\Helper\WhiteSpace; 19 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 20 | 21 | /** 22 | * Lightweight Formatter heavily based on https://github.com/jdorn/sql-formatter. 23 | * 24 | * Class Formatter 25 | */ 26 | class Formatter 27 | { 28 | /** 29 | * @var Tokenizer 30 | */ 31 | protected $tokenizer; 32 | 33 | /** 34 | * @var NewLine 35 | */ 36 | protected $newLine; 37 | 38 | /** 39 | * @var Parentheses 40 | */ 41 | protected $parentheses; 42 | 43 | /** 44 | * @var string 45 | */ 46 | protected $tab = ' '; 47 | /** 48 | * @var int 49 | */ 50 | protected $inlineCount = 0; 51 | 52 | /** 53 | * @var bool 54 | */ 55 | protected $clauseLimit = false; 56 | /** 57 | * @var string 58 | */ 59 | protected $formattedSql = ''; 60 | /** 61 | * @var Indent 62 | */ 63 | protected $indentation; 64 | 65 | /** 66 | * @var Comment 67 | */ 68 | protected $comment; 69 | 70 | /** 71 | * Returns a SQL string in a readable human-friendly format. 72 | * 73 | * @param string $sql 74 | * 75 | * @return string 76 | */ 77 | public function format($sql) 78 | { 79 | $this->reset(); 80 | $tab = "\t"; 81 | 82 | $originalTokens = $this->tokenizer->tokenize((string) $sql); 83 | $tokens = WhiteSpace::removeTokenWhitespace($originalTokens); 84 | 85 | foreach ($tokens as $i => $token) { 86 | $queryValue = $token[Tokenizer::TOKEN_VALUE]; 87 | $this->indentation->increaseSpecialIndent()->increaseBlockIndent(); 88 | $addedNewline = $this->newLine->addNewLineBreak($tab); 89 | 90 | if ($this->comment->stringHasCommentToken($token)) { 91 | $this->formattedSql = $this->comment->writeCommentBlock($token, $tab, $queryValue); 92 | continue; 93 | } 94 | 95 | if ($this->parentheses->getInlineParentheses()) { 96 | if ($this->parentheses->stringIsClosingParentheses($token)) { 97 | $this->parentheses->writeInlineParenthesesBlock($tab, $queryValue); 98 | continue; 99 | } 100 | $this->newLine->writeNewLineForLongCommaInlineValues($token); 101 | $this->inlineCount += \strlen($token[Tokenizer::TOKEN_VALUE]); 102 | } 103 | 104 | switch ($token) { 105 | case $this->parentheses->stringIsOpeningParentheses($token): 106 | $tokens = $this->formatOpeningParenthesis($token, $i, $tokens, $originalTokens); 107 | break; 108 | 109 | case $this->parentheses->stringIsClosingParentheses($token): 110 | $this->indentation->decreaseIndentLevelUntilIndentTypeIsSpecial($this); 111 | $this->newLine->addNewLineBeforeToken($addedNewline, $tab); 112 | break; 113 | 114 | case $this->stringIsEndOfLimitClause($token): 115 | $this->clauseLimit = false; 116 | break; 117 | 118 | case $token[Tokenizer::TOKEN_VALUE] === ',' && false === $this->parentheses->getInlineParentheses(): 119 | $this->newLine->writeNewLineBecauseOfComma(); 120 | break; 121 | 122 | case Token::isTokenTypeReservedTopLevel($token): 123 | $queryValue = $this->formatTokenTypeReservedTopLevel($addedNewline, $tab, $token, $queryValue); 124 | break; 125 | 126 | case $this->newLine->isTokenTypeReservedNewLine($token): 127 | $this->newLine->addNewLineBeforeToken($addedNewline, $tab); 128 | 129 | if (WhiteSpace::tokenHasExtraWhiteSpaces($token)) { 130 | $queryValue = \preg_replace('/\s+/', ' ', $queryValue); 131 | } 132 | break; 133 | } 134 | 135 | $this->formatBoundaryCharacterToken($token, $i, $tokens, $originalTokens); 136 | $this->formatWhiteSpaceToken($token, $queryValue); 137 | $this->formatDashToken($token, $i, $tokens); 138 | } 139 | 140 | return \trim(\str_replace(["\t", " \n"], [$this->tab, "\n"], $this->formattedSql))."\n"; 141 | } 142 | 143 | /** 144 | * 145 | */ 146 | public function reset() 147 | { 148 | $this->tokenizer = new Tokenizer(); 149 | $this->indentation = new Indent(); 150 | $this->parentheses = new Parentheses($this, $this->indentation); 151 | $this->newLine = new NewLine($this, $this->indentation, $this->parentheses); 152 | $this->comment = new Comment($this, $this->indentation, $this->newLine); 153 | 154 | $this->formattedSql = ''; 155 | } 156 | 157 | /** 158 | * @param $token 159 | * @param $i 160 | * @param array $tokens 161 | * @param array $originalTokens 162 | * 163 | * @return array 164 | */ 165 | protected function formatOpeningParenthesis($token, $i, array &$tokens, array &$originalTokens) 166 | { 167 | $length = 0; 168 | for ($j = 1; $j <= 250; ++$j) { 169 | if (isset($tokens[$i + $j])) { 170 | $next = $tokens[$i + $j]; 171 | if ($this->parentheses->stringIsClosingParentheses($next)) { 172 | $this->parentheses->writeNewInlineParentheses(); 173 | break; 174 | } 175 | 176 | if ($this->parentheses->invalidParenthesesTokenValue($next) 177 | || $this->parentheses->invalidParenthesesTokenType($next) 178 | ) { 179 | break; 180 | } 181 | 182 | $length += \strlen($next[Tokenizer::TOKEN_VALUE]); 183 | } 184 | } 185 | $this->newLine->writeNewLineForLongInlineValues($length); 186 | 187 | if (WhiteSpace::isPrecedingCurrentTokenOfTokenTypeWhiteSpace($originalTokens, $token)) { 188 | $this->formattedSql = \rtrim($this->formattedSql, ' '); 189 | } 190 | 191 | $this->newLine->addNewLineAfterOpeningParentheses(); 192 | 193 | return $tokens; 194 | } 195 | 196 | /** 197 | * @param $token 198 | * 199 | * @return bool 200 | */ 201 | protected function stringIsEndOfLimitClause($token) 202 | { 203 | return $this->clauseLimit 204 | && $token[Tokenizer::TOKEN_VALUE] !== ',' 205 | && $token[Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_NUMBER 206 | && $token[Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_WHITESPACE; 207 | } 208 | 209 | /** 210 | * @param bool $addedNewline 211 | * @param string $tab 212 | * @param $token 213 | * @param $queryValue 214 | * 215 | * @return mixed 216 | */ 217 | protected function formatTokenTypeReservedTopLevel($addedNewline, $tab, $token, $queryValue) 218 | { 219 | $this->indentation 220 | ->setIncreaseSpecialIndent(true) 221 | ->decreaseSpecialIndentIfCurrentIndentTypeIsSpecial(); 222 | 223 | $this->newLine->writeNewLineBecauseOfTopLevelReservedWord($addedNewline, $tab); 224 | 225 | if (WhiteSpace::tokenHasExtraWhiteSpaces($token)) { 226 | $queryValue = \preg_replace('/\s+/', ' ', $queryValue); 227 | } 228 | Token::tokenHasLimitClause($token, $this->parentheses, $this); 229 | 230 | return $queryValue; 231 | } 232 | 233 | /** 234 | * @param $token 235 | * @param $i 236 | * @param array $tokens 237 | * @param array $originalTokens 238 | */ 239 | protected function formatBoundaryCharacterToken($token, $i, array &$tokens, array &$originalTokens) 240 | { 241 | if (Token::tokenHasMultipleBoundaryCharactersTogether($token, $tokens, $i, $originalTokens)) { 242 | $this->formattedSql = \rtrim($this->formattedSql, ' '); 243 | } 244 | } 245 | 246 | /** 247 | * @param $token 248 | * @param $queryValue 249 | */ 250 | protected function formatWhiteSpaceToken($token, $queryValue) 251 | { 252 | if (WhiteSpace::tokenHasExtraWhiteSpaceLeft($token)) { 253 | $this->formattedSql = \rtrim($this->formattedSql, ' '); 254 | } 255 | 256 | $this->formattedSql .= $queryValue.' '; 257 | 258 | if (WhiteSpace::tokenHasExtraWhiteSpaceRight($token)) { 259 | $this->formattedSql = \rtrim($this->formattedSql, ' '); 260 | } 261 | } 262 | 263 | /** 264 | * @param $token 265 | * @param $i 266 | * @param array $tokens 267 | */ 268 | protected function formatDashToken($token, $i, array &$tokens) 269 | { 270 | if (Token::tokenIsMinusSign($token, $tokens, $i)) { 271 | $previousTokenType = $tokens[$i - 1][Tokenizer::TOKEN_TYPE]; 272 | 273 | if (WhiteSpace::tokenIsNumberAndHasExtraWhiteSpaceRight($previousTokenType)) { 274 | $this->formattedSql = \rtrim($this->formattedSql, ' '); 275 | } 276 | } 277 | } 278 | 279 | /** 280 | * @return string 281 | */ 282 | public function getFormattedSql() 283 | { 284 | return $this->formattedSql; 285 | } 286 | 287 | /** 288 | * @param string $formattedSql 289 | * 290 | * @return $this 291 | */ 292 | public function setFormattedSql($formattedSql) 293 | { 294 | $this->formattedSql = $formattedSql; 295 | 296 | return $this; 297 | } 298 | 299 | /** 300 | * @param $string 301 | * 302 | * @return $this 303 | */ 304 | public function appendToFormattedSql($string) 305 | { 306 | $this->formattedSql .= $string; 307 | 308 | return $this; 309 | } 310 | 311 | /** 312 | * @return int 313 | */ 314 | public function getInlineCount() 315 | { 316 | return $this->inlineCount; 317 | } 318 | 319 | /** 320 | * @param int $inlineCount 321 | * 322 | * @return $this 323 | */ 324 | public function setInlineCount($inlineCount) 325 | { 326 | $this->inlineCount = $inlineCount; 327 | 328 | return $this; 329 | } 330 | 331 | /** 332 | * @return bool 333 | */ 334 | public function getClauseLimit() 335 | { 336 | return $this->clauseLimit; 337 | } 338 | 339 | /** 340 | * @param bool $clauseLimit 341 | * 342 | * @return $this 343 | */ 344 | public function setClauseLimit($clauseLimit) 345 | { 346 | $this->clauseLimit = $clauseLimit; 347 | 348 | return $this; 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/Helper/Comment.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/22/14 5 | * Time: 10:09 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Helper; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Formatter; 14 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 15 | 16 | /** 17 | * Class Comment. 18 | */ 19 | class Comment 20 | { 21 | /** 22 | * @var \NilPortugues\Sql\QueryFormatter\Formatter 23 | */ 24 | protected $formatter; 25 | 26 | /** 27 | * @var Indent 28 | */ 29 | protected $indentation; 30 | 31 | /** 32 | * @var NewLine 33 | */ 34 | protected $newLine; 35 | 36 | /** 37 | * @param Formatter $formatter 38 | * @param Indent $indentation 39 | * @param NewLine $newLine 40 | */ 41 | public function __construct(Formatter $formatter, Indent $indentation, NewLine $newLine) 42 | { 43 | $this->formatter = $formatter; 44 | $this->indentation = $indentation; 45 | $this->newLine = $newLine; 46 | } 47 | 48 | /** 49 | * @param $token 50 | * 51 | * @return bool 52 | */ 53 | public function stringHasCommentToken($token) 54 | { 55 | return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_COMMENT 56 | || $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BLOCK_COMMENT; 57 | } 58 | 59 | /** 60 | * @param $token 61 | * @param string $tab 62 | * @param $queryValue 63 | * 64 | * @return string 65 | */ 66 | public function writeCommentBlock($token, $tab, $queryValue) 67 | { 68 | if ($token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BLOCK_COMMENT) { 69 | $indent = \str_repeat($tab, $this->indentation->getIndentLvl()); 70 | 71 | $this->formatter->appendToFormattedSql("\n".$indent); 72 | $queryValue = \str_replace("\n", "\n".$indent, $queryValue); 73 | } 74 | 75 | $this->formatter->appendToFormattedSql($queryValue); 76 | $this->newLine->setNewline(true); 77 | 78 | return $this->formatter->getFormattedSql(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Helper/Indent.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/22/14 5 | * Time: 11:37 AM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Helper; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Formatter; 14 | 15 | /** 16 | * Class Indent. 17 | */ 18 | class Indent 19 | { 20 | /** 21 | * @var bool 22 | */ 23 | protected $inlineIndented = false; 24 | 25 | /** 26 | * @var bool 27 | */ 28 | protected $increaseSpecialIndent = false; 29 | 30 | /** 31 | * @var int 32 | */ 33 | protected $indentLvl = 0; 34 | 35 | /** 36 | * @var bool 37 | */ 38 | protected $increaseBlockIndent = false; 39 | 40 | /** 41 | * @var array 42 | */ 43 | protected $indentTypes = []; 44 | 45 | /** 46 | * Increase the Special Indent if increaseSpecialIndent is true after the current iteration. 47 | * 48 | * @return $this 49 | */ 50 | public function increaseSpecialIndent() 51 | { 52 | if ($this->increaseSpecialIndent) { 53 | ++$this->indentLvl; 54 | $this->increaseSpecialIndent = false; 55 | \array_unshift($this->indentTypes, 'special'); 56 | } 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Increase the Block Indent if increaseBlockIndent is true after the current iteration. 63 | * 64 | * @return $this 65 | */ 66 | public function increaseBlockIndent() 67 | { 68 | if ($this->increaseBlockIndent) { 69 | ++$this->indentLvl; 70 | $this->increaseBlockIndent = false; 71 | \array_unshift($this->indentTypes, 'block'); 72 | } 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * Closing parentheses decrease the block indent level. 79 | * 80 | * @param Formatter $formatter 81 | * 82 | * @return $this 83 | */ 84 | public function decreaseIndentLevelUntilIndentTypeIsSpecial(Formatter $formatter) 85 | { 86 | $formatter->setFormattedSql(\rtrim($formatter->getFormattedSql(), ' ')); 87 | --$this->indentLvl; 88 | 89 | while ($j = \array_shift($this->indentTypes)) { 90 | if ('special' !== $j) { 91 | break; 92 | } 93 | --$this->indentLvl; 94 | } 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * @return $this 101 | */ 102 | public function decreaseSpecialIndentIfCurrentIndentTypeIsSpecial() 103 | { 104 | \reset($this->indentTypes); 105 | 106 | if (\current($this->indentTypes) === 'special') { 107 | --$this->indentLvl; 108 | \array_shift($this->indentTypes); 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * @return bool 116 | */ 117 | public function getIncreaseBlockIndent() 118 | { 119 | return $this->increaseBlockIndent; 120 | } 121 | 122 | /** 123 | * @return bool 124 | */ 125 | public function getIncreaseSpecialIndent() 126 | { 127 | return $this->increaseSpecialIndent; 128 | } 129 | 130 | /** 131 | * @return int 132 | */ 133 | public function getIndentLvl() 134 | { 135 | return $this->indentLvl; 136 | } 137 | 138 | /** 139 | * @return mixed 140 | */ 141 | public function getIndentTypes() 142 | { 143 | return $this->indentTypes; 144 | } 145 | 146 | /** 147 | * @param bool $increaseBlockIndent 148 | * 149 | * @return $this 150 | */ 151 | public function setIncreaseBlockIndent($increaseBlockIndent) 152 | { 153 | $this->increaseBlockIndent = $increaseBlockIndent; 154 | 155 | return $this; 156 | } 157 | 158 | /** 159 | * @param bool $increaseSpecialIndent 160 | * 161 | * @return $this 162 | */ 163 | public function setIncreaseSpecialIndent($increaseSpecialIndent) 164 | { 165 | $this->increaseSpecialIndent = $increaseSpecialIndent; 166 | 167 | return $this; 168 | } 169 | 170 | /** 171 | * @param int $indentLvl 172 | * 173 | * @return $this 174 | */ 175 | public function setIndentLvl($indentLvl) 176 | { 177 | $this->indentLvl = $indentLvl; 178 | 179 | return $this; 180 | } 181 | 182 | /** 183 | * @param array $indentTypes 184 | * 185 | * @return $this 186 | */ 187 | public function setIndentTypes($indentTypes) 188 | { 189 | $this->indentTypes = $indentTypes; 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * @param bool $inlineIndented 196 | * 197 | * @return $this 198 | */ 199 | public function setInlineIndented($inlineIndented) 200 | { 201 | $this->inlineIndented = $inlineIndented; 202 | 203 | return $this; 204 | } 205 | 206 | /** 207 | * @return bool 208 | */ 209 | public function getInlineIndented() 210 | { 211 | return $this->inlineIndented; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/Helper/NewLine.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/22/14 5 | * Time: 11:37 AM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Helper; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Formatter; 14 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 15 | 16 | /** 17 | * Class NewLine. 18 | */ 19 | class NewLine 20 | { 21 | /** 22 | * @var bool 23 | */ 24 | protected $newline = false; 25 | 26 | /** 27 | * @var \NilPortugues\Sql\QueryFormatter\Formatter 28 | */ 29 | protected $formatter; 30 | 31 | /** 32 | * @var Indent 33 | */ 34 | protected $indentation; 35 | 36 | /** 37 | * @var Parentheses 38 | */ 39 | protected $parentheses; 40 | 41 | /** 42 | * @param Formatter $formatter 43 | * @param Indent $indentation 44 | * @param Parentheses $parentheses 45 | */ 46 | public function __construct(Formatter $formatter, Indent $indentation, Parentheses $parentheses) 47 | { 48 | $this->formatter = $formatter; 49 | $this->indentation = $indentation; 50 | $this->parentheses = $parentheses; 51 | } 52 | 53 | /** 54 | * Adds a new line break if needed. 55 | * 56 | * @param string $tab 57 | * 58 | * @return bool 59 | */ 60 | public function addNewLineBreak($tab) 61 | { 62 | $addedNewline = false; 63 | 64 | if (true === $this->newline) { 65 | $this->formatter->appendToFormattedSql("\n".str_repeat($tab, $this->indentation->getIndentLvl())); 66 | $this->newline = false; 67 | $addedNewline = true; 68 | } 69 | 70 | return $addedNewline; 71 | } 72 | 73 | /** 74 | * @param $token 75 | */ 76 | public function writeNewLineForLongCommaInlineValues($token) 77 | { 78 | if (',' === $token[Tokenizer::TOKEN_VALUE]) { 79 | if ($this->formatter->getInlineCount() >= 30) { 80 | $this->formatter->setInlineCount(0); 81 | $this->newline = true; 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * @param int $length 88 | */ 89 | public function writeNewLineForLongInlineValues($length) 90 | { 91 | if ($this->parentheses->getInlineParentheses() && $length > 30) { 92 | $this->indentation->setIncreaseBlockIndent(true); 93 | $this->indentation->setInlineIndented(true); 94 | $this->newline = true; 95 | } 96 | } 97 | 98 | /** 99 | * Adds a new line break for an opening parentheses for a non-inline expression. 100 | */ 101 | public function addNewLineAfterOpeningParentheses() 102 | { 103 | if (false === $this->parentheses->getInlineParentheses()) { 104 | $this->indentation->setIncreaseBlockIndent(true); 105 | $this->newline = true; 106 | } 107 | } 108 | 109 | /** 110 | * @param bool $addedNewline 111 | * @param string $tab 112 | */ 113 | public function addNewLineBeforeToken($addedNewline, $tab) 114 | { 115 | if (false === $addedNewline) { 116 | $this->formatter->appendToFormattedSql( 117 | "\n".str_repeat($tab, $this->indentation->getIndentLvl()) 118 | ); 119 | } 120 | } 121 | 122 | /** 123 | * Add a newline before the top level reserved word if necessary and indent. 124 | * 125 | * @param bool $addedNewline 126 | * @param string $tab 127 | */ 128 | public function writeNewLineBecauseOfTopLevelReservedWord($addedNewline, $tab) 129 | { 130 | if (false === $addedNewline) { 131 | $this->formatter->appendToFormattedSql("\n"); 132 | } else { 133 | $this->formatter->setFormattedSql(\rtrim($this->formatter->getFormattedSql(), $tab)); 134 | } 135 | $this->formatter->appendToFormattedSql(\str_repeat($tab, $this->indentation->getIndentLvl())); 136 | 137 | $this->newline = true; 138 | } 139 | 140 | /** 141 | * Commas start a new line unless they are found within inline parentheses or SQL 'LIMIT' clause. 142 | * If the previous TOKEN_VALUE is 'LIMIT', undo new line. 143 | */ 144 | public function writeNewLineBecauseOfComma() 145 | { 146 | $this->newline = true; 147 | 148 | if (true === $this->formatter->getClauseLimit()) { 149 | $this->newline = false; 150 | $this->formatter->setClauseLimit(false); 151 | } 152 | } 153 | 154 | /** 155 | * @param $token 156 | * 157 | * @return bool 158 | */ 159 | public function isTokenTypeReservedNewLine($token) 160 | { 161 | return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_RESERVED_NEWLINE; 162 | } 163 | 164 | /** 165 | * @return bool 166 | */ 167 | public function getNewline() 168 | { 169 | return $this->newline; 170 | } 171 | 172 | /** 173 | * @param bool $newline 174 | * 175 | * @return $this 176 | */ 177 | public function setNewline($newline) 178 | { 179 | $this->newline = $newline; 180 | 181 | return $this; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Helper/Parentheses.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/22/14 5 | * Time: 11:37 AM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Helper; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Formatter; 14 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 15 | 16 | /** 17 | * Class Parentheses. 18 | */ 19 | class Parentheses 20 | { 21 | /** 22 | * @var bool 23 | */ 24 | protected $inlineParentheses = false; 25 | /** 26 | * @var \NilPortugues\Sql\QueryFormatter\Formatter 27 | */ 28 | protected $formatter; 29 | 30 | /** 31 | * @var Indent 32 | */ 33 | protected $indentation; 34 | 35 | /** 36 | * @param Formatter $formatter 37 | * @param Indent $indentation 38 | */ 39 | public function __construct(Formatter $formatter, Indent $indentation) 40 | { 41 | $this->formatter = $formatter; 42 | $this->indentation = $indentation; 43 | } 44 | 45 | /** 46 | * @return bool 47 | */ 48 | public function getInlineParentheses() 49 | { 50 | return $this->inlineParentheses; 51 | } 52 | 53 | /** 54 | * @param bool $inlineParentheses 55 | * 56 | * @return $this 57 | */ 58 | public function setInlineParentheses($inlineParentheses) 59 | { 60 | $this->inlineParentheses = $inlineParentheses; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * @param $token 67 | * 68 | * @return bool 69 | */ 70 | public function stringIsOpeningParentheses($token) 71 | { 72 | return $token[Tokenizer::TOKEN_VALUE] === '('; 73 | } 74 | 75 | /** 76 | * 77 | */ 78 | public function writeNewInlineParentheses() 79 | { 80 | $this->inlineParentheses = true; 81 | $this->formatter->setInlineCount(0); 82 | $this->indentation->setInlineIndented(false); 83 | } 84 | 85 | /** 86 | * @param $token 87 | * 88 | * @return bool 89 | */ 90 | public function invalidParenthesesTokenValue($token) 91 | { 92 | return $token[Tokenizer::TOKEN_VALUE] === ';' 93 | || $token[Tokenizer::TOKEN_VALUE] === '('; 94 | } 95 | 96 | /** 97 | * @param $token 98 | * 99 | * @return bool 100 | */ 101 | public function invalidParenthesesTokenType($token) 102 | { 103 | return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_RESERVED_TOP_LEVEL 104 | || $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_RESERVED_NEWLINE 105 | || $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_COMMENT 106 | || $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BLOCK_COMMENT; 107 | } 108 | 109 | /** 110 | * @param $token 111 | * 112 | * @return bool 113 | */ 114 | public function stringIsClosingParentheses($token) 115 | { 116 | return $token[Tokenizer::TOKEN_VALUE] === ')'; 117 | } 118 | 119 | /** 120 | * @param string $tab 121 | * @param $queryValue 122 | */ 123 | public function writeInlineParenthesesBlock($tab, $queryValue) 124 | { 125 | $this->formatter->setFormattedSql(\rtrim($this->formatter->getFormattedSql(), ' ')); 126 | 127 | if ($this->indentation->getInlineIndented()) { 128 | $indentTypes = $this->indentation->getIndentTypes(); 129 | \array_shift($indentTypes); 130 | $this->indentation->setIndentTypes($indentTypes); 131 | $this->indentation->setIndentLvl($this->indentation->getIndentLvl() - 1); 132 | 133 | $this->formatter->appendToFormattedSql("\n".str_repeat($tab, $this->indentation->getIndentLvl())); 134 | } 135 | 136 | $this->inlineParentheses = false; 137 | $this->formatter->appendToFormattedSql($queryValue.' '); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Helper/Token.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/22/14 5 | * Time: 11:38 AM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Helper; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Formatter; 14 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 15 | 16 | /** 17 | * Class Token. 18 | */ 19 | final class Token 20 | { 21 | /** 22 | * @var array 23 | */ 24 | public static $reserved = [ 25 | 'ACCESSIBLE', 26 | 'ACTION', 27 | 'AGAINST', 28 | 'AGGREGATE', 29 | 'ALGORITHM', 30 | 'ALL', 31 | 'ALTER', 32 | 'ANALYSE', 33 | 'ANALYZE', 34 | 'AS', 35 | 'ASC', 36 | 'AUTOCOMMIT', 37 | 'AUTO_INCREMENT', 38 | 'BACKUP', 39 | 'BEGIN', 40 | 'BETWEEN', 41 | 'BINLOG', 42 | 'BOTH', 43 | 'CASCADE', 44 | 'CASE', 45 | 'CHANGE', 46 | 'CHANGED', 47 | 'CHARACTER SET', 48 | 'CHARSET', 49 | 'CHECK', 50 | 'CHECKSUM', 51 | 'COLLATE', 52 | 'COLLATION', 53 | 'COLUMN', 54 | 'COLUMNS', 55 | 'COMMENT', 56 | 'COMMIT', 57 | 'COMMITTED', 58 | 'COMPRESSED', 59 | 'CONCURRENT', 60 | 'CONSTRAINT', 61 | 'CONTAINS', 62 | 'CONVERT', 63 | 'CREATE', 64 | 'CROSS', 65 | 'CURRENT_TIMESTAMP', 66 | 'DATABASE', 67 | 'DATABASES', 68 | 'DAY', 69 | 'DAY_HOUR', 70 | 'DAY_MINUTE', 71 | 'DAY_SECOND', 72 | 'DEFAULT', 73 | 'DEFINER', 74 | 'DELAYED', 75 | 'DELETE', 76 | 'DESC', 77 | 'DESCRIBE', 78 | 'DETERMINISTIC', 79 | 'DISTINCT', 80 | 'DISTINCTROW', 81 | 'DIV', 82 | 'DO', 83 | 'DUMPFILE', 84 | 'DUPLICATE', 85 | 'DYNAMIC', 86 | 'ELSE', 87 | 'ENCLOSED', 88 | 'END', 89 | 'ENGINE', 90 | 'ENGINE_TYPE', 91 | 'ENGINES', 92 | 'ESCAPE', 93 | 'ESCAPED', 94 | 'EVENTS', 95 | 'EXEC', 96 | 'EXECUTE', 97 | 'EXISTS', 98 | 'EXPLAIN', 99 | 'EXTENDED', 100 | 'FAST', 101 | 'FIELDS', 102 | 'FILE', 103 | 'FIRST', 104 | 'FIXED', 105 | 'FLUSH', 106 | 'FOR', 107 | 'FORCE', 108 | 'FOREIGN', 109 | 'FULL', 110 | 'FULLTEXT', 111 | 'FUNCTION', 112 | 'GLOBAL', 113 | 'GRANT', 114 | 'GRANTS', 115 | 'GROUP_CONCAT', 116 | 'HEAP', 117 | 'HIGH_PRIORITY', 118 | 'HOSTS', 119 | 'HOUR', 120 | 'HOUR_MINUTE', 121 | 'HOUR_SECOND', 122 | 'IDENTIFIED', 123 | 'IF', 124 | 'IFNULL', 125 | 'IGNORE', 126 | 'IN', 127 | 'INDEX', 128 | 'INDEXES', 129 | 'INFILE', 130 | 'INSERT', 131 | 'INSERT_ID', 132 | 'INSERT_METHOD', 133 | 'INTERVAL', 134 | 'INTO', 135 | 'INVOKER', 136 | 'IS', 137 | 'ISOLATION', 138 | 'KEY', 139 | 'KEYS', 140 | 'KILL', 141 | 'LAST_INSERT_ID', 142 | 'LEADING', 143 | 'LEVEL', 144 | 'LIKE', 145 | 'LINEAR', 146 | 'LINES', 147 | 'LOAD', 148 | 'LOCAL', 149 | 'LOCK', 150 | 'LOCKS', 151 | 'LOGS', 152 | 'LOW_PRIORITY', 153 | 'MARIA', 154 | 'MASTER', 155 | 'MASTER_CONNECT_RETRY', 156 | 'MASTER_HOST', 157 | 'MASTER_LOG_FILE', 158 | 'MATCH', 159 | 'MAX_CONNECTIONS_PER_HOUR', 160 | 'MAX_QUERIES_PER_HOUR', 161 | 'MAX_ROWS', 162 | 'MAX_UPDATES_PER_HOUR', 163 | 'MAX_USER_CONNECTIONS', 164 | 'MEDIUM', 165 | 'MERGE', 166 | 'MINUTE', 167 | 'MINUTE_SECOND', 168 | 'MIN_ROWS', 169 | 'MODE', 170 | 'MODIFY', 171 | 'MONTH', 172 | 'MRG_MYISAM', 173 | 'MYISAM', 174 | 'NAMES', 175 | 'NATURAL', 176 | 'NOT', 177 | 'NOW()', 178 | 'NULL', 179 | 'OFFSET', 180 | 'ON', 181 | 'OPEN', 182 | 'OPTIMIZE', 183 | 'OPTION', 184 | 'OPTIONALLY', 185 | 'ON UPDATE', 186 | 'ON DELETE', 187 | 'OUTFILE', 188 | 'PACK_KEYS', 189 | 'PAGE', 190 | 'PARTIAL', 191 | 'PARTITION', 192 | 'PARTITIONS', 193 | 'PASSWORD', 194 | 'PRIMARY', 195 | 'PRIVILEGES', 196 | 'PROCEDURE', 197 | 'PROCESS', 198 | 'PROCESSLIST', 199 | 'PURGE', 200 | 'QUICK', 201 | 'RANGE', 202 | 'RAID0', 203 | 'RAID_CHUNKS', 204 | 'RAID_CHUNKSIZE', 205 | 'RAID_TYPE', 206 | 'READ', 207 | 'READ_ONLY', 208 | 'READ_WRITE', 209 | 'REFERENCES', 210 | 'REGEXP', 211 | 'RELOAD', 212 | 'RENAME', 213 | 'REPAIR', 214 | 'REPEATABLE', 215 | 'REPLACE', 216 | 'REPLICATION', 217 | 'RESET', 218 | 'RESTORE', 219 | 'RESTRICT', 220 | 'RETURN', 221 | 'RETURNS', 222 | 'REVOKE', 223 | 'RLIKE', 224 | 'ROLLBACK', 225 | 'ROW', 226 | 'ROWS', 227 | 'ROW_FORMAT', 228 | 'SECOND', 229 | 'SECURITY', 230 | 'SEPARATOR', 231 | 'SERIALIZABLE', 232 | 'SESSION', 233 | 'SHARE', 234 | 'SHOW', 235 | 'SHUTDOWN', 236 | 'SLAVE', 237 | 'SONAME', 238 | 'SOUNDS', 239 | 'SQL', 240 | 'SQL_AUTO_IS_NULL', 241 | 'SQL_BIG_RESULT', 242 | 'SQL_BIG_SELECTS', 243 | 'SQL_BIG_TABLES', 244 | 'SQL_BUFFER_RESULT', 245 | 'SQL_CALC_FOUND_ROWS', 246 | 'SQL_LOG_BIN', 247 | 'SQL_LOG_OFF', 248 | 'SQL_LOG_UPDATE', 249 | 'SQL_LOW_PRIORITY_UPDATES', 250 | 'SQL_MAX_JOIN_SIZE', 251 | 'SQL_QUOTE_SHOW_CREATE', 252 | 'SQL_SAFE_UPDATES', 253 | 'SQL_SELECT_LIMIT', 254 | 'SQL_SLAVE_SKIP_COUNTER', 255 | 'SQL_SMALL_RESULT', 256 | 'SQL_WARNINGS', 257 | 'SQL_CACHE', 258 | 'SQL_NO_CACHE', 259 | 'START', 260 | 'STARTING', 261 | 'STATUS', 262 | 'STOP', 263 | 'STORAGE', 264 | 'STRAIGHT_JOIN', 265 | 'STRING', 266 | 'STRIPED', 267 | 'SUPER', 268 | 'TABLE', 269 | 'TABLES', 270 | 'TEMPORARY', 271 | 'TERMINATED', 272 | 'THEN', 273 | 'TO', 274 | 'TRAILING', 275 | 'TRANSACTIONAL', 276 | 'TRUE', 277 | 'TRUNCATE', 278 | 'TYPE', 279 | 'TYPES', 280 | 'UNCOMMITTED', 281 | 'UNIQUE', 282 | 'UNLOCK', 283 | 'UNSIGNED', 284 | 'USAGE', 285 | 'USE', 286 | 'USING', 287 | 'VARIABLES', 288 | 'VIEW', 289 | 'WHEN', 290 | 'WITH', 291 | 'WORK', 292 | 'WRITE', 293 | 'YEAR_MONTH', 294 | ]; 295 | 296 | /** 297 | * @var array 298 | */ 299 | public static $reservedTopLevel = [ 300 | 'SELECT', 301 | 'FROM', 302 | 'WHERE', 303 | 'SET', 304 | 'ORDER BY', 305 | 'GROUP BY', 306 | 'LIMIT', 307 | 'DROP', 308 | 'VALUES', 309 | 'UPDATE', 310 | 'HAVING', 311 | 'ADD', 312 | 'AFTER', 313 | 'ALTER TABLE', 314 | 'DELETE FROM', 315 | 'UNION ALL', 316 | 'UNION', 317 | 'EXCEPT', 318 | 'INTERSECT', 319 | ]; 320 | 321 | /** 322 | * @var array 323 | */ 324 | public static $reservedNewLine = [ 325 | 'LEFT OUTER JOIN', 326 | 'RIGHT OUTER JOIN', 327 | 'LEFT JOIN', 328 | 'RIGHT JOIN', 329 | 'OUTER JOIN', 330 | 'INNER JOIN', 331 | 'JOIN', 332 | 'XOR', 333 | 'OR', 334 | 'AND', 335 | ]; 336 | 337 | /** 338 | * @var array 339 | */ 340 | public static $functions = [ 341 | 'ABS', 342 | 'ACOS', 343 | 'ADDDATE', 344 | 'ADDTIME', 345 | 'AES_DECRYPT', 346 | 'AES_ENCRYPT', 347 | 'AREA', 348 | 'ASBINARY', 349 | 'ASCII', 350 | 'ASIN', 351 | 'ASTEXT', 352 | 'ATAN', 353 | 'ATAN2', 354 | 'AVG', 355 | 'BDMPOLYFROMTEXT', 356 | 'BDMPOLYFROMWKB', 357 | 'BDPOLYFROMTEXT', 358 | 'BDPOLYFROMWKB', 359 | 'BENCHMARK', 360 | 'BIN', 361 | 'BIT_AND', 362 | 'BIT_COUNT', 363 | 'BIT_LENGTH', 364 | 'BIT_OR', 365 | 'BIT_XOR', 366 | 'BOUNDARY', 367 | 'BUFFER', 368 | 'CAST', 369 | 'CEIL', 370 | 'CEILING', 371 | 'CENTROID', 372 | 'CHAR', 373 | 'CHARACTER_LENGTH', 374 | 'CHARSET', 375 | 'CHAR_LENGTH', 376 | 'COALESCE', 377 | 'COERCIBILITY', 378 | 'COLLATION', 379 | 'COMPRESS', 380 | 'CONCAT', 381 | 'CONCAT_WS', 382 | 'CONNECTION_ID', 383 | 'CONTAINS', 384 | 'CONV', 385 | 'CONVERT', 386 | 'CONVERT_TZ', 387 | 'CONVEXHULL', 388 | 'COS', 389 | 'COT', 390 | 'COUNT', 391 | 'CRC32', 392 | 'CROSSES', 393 | 'CURDATE', 394 | 'CURRENT_DATE', 395 | 'CURRENT_TIME', 396 | 'CURRENT_TIMESTAMP', 397 | 'CURRENT_USER', 398 | 'CURTIME', 399 | 'DATABASE', 400 | 'DATE', 401 | 'DATEDIFF', 402 | 'DATE_ADD', 403 | 'DATE_DIFF', 404 | 'DATE_FORMAT', 405 | 'DATE_SUB', 406 | 'DAY', 407 | 'DAYNAME', 408 | 'DAYOFMONTH', 409 | 'DAYOFWEEK', 410 | 'DAYOFYEAR', 411 | 'DECODE', 412 | 'DEFAULT', 413 | 'DEGREES', 414 | 'DES_DECRYPT', 415 | 'DES_ENCRYPT', 416 | 'DIFFERENCE', 417 | 'DIMENSION', 418 | 'DISJOINT', 419 | 'DISTANCE', 420 | 'ELT', 421 | 'ENCODE', 422 | 'ENCRYPT', 423 | 'ENDPOINT', 424 | 'ENVELOPE', 425 | 'EQUALS', 426 | 'EXP', 427 | 'EXPORT_SET', 428 | 'EXTERIORRING', 429 | 'EXTRACT', 430 | 'EXTRACTVALUE', 431 | 'FIELD', 432 | 'FIND_IN_SET', 433 | 'FLOOR', 434 | 'FORMAT', 435 | 'FOUND_ROWS', 436 | 'FROM_DAYS', 437 | 'FROM_UNIXTIME', 438 | 'GEOMCOLLFROMTEXT', 439 | 'GEOMCOLLFROMWKB', 440 | 'GEOMETRYCOLLECTION', 441 | 'GEOMETRYCOLLECTIONFROMTEXT', 442 | 'GEOMETRYCOLLECTIONFROMWKB', 443 | 'GEOMETRYFROMTEXT', 444 | 'GEOMETRYFROMWKB', 445 | 'GEOMETRYN', 446 | 'GEOMETRYTYPE', 447 | 'GEOMFROMTEXT', 448 | 'GEOMFROMWKB', 449 | 'GET_FORMAT', 450 | 'GET_LOCK', 451 | 'GLENGTH', 452 | 'GREATEST', 453 | 'GROUP_CONCAT', 454 | 'GROUP_UNIQUE_USERS', 455 | 'HEX', 456 | 'HOUR', 457 | 'IF', 458 | 'IFNULL', 459 | 'INET_ATON', 460 | 'INET_NTOA', 461 | 'INSERT', 462 | 'INSTR', 463 | 'INTERIORRINGN', 464 | 'INTERSECTION', 465 | 'INTERSECTS', 466 | 'INTERVAL', 467 | 'ISCLOSED', 468 | 'ISEMPTY', 469 | 'ISNULL', 470 | 'ISRING', 471 | 'ISSIMPLE', 472 | 'IS_FREE_LOCK', 473 | 'IS_USED_LOCK', 474 | 'LAST_DAY', 475 | 'LAST_INSERT_ID', 476 | 'LCASE', 477 | 'LEAST', 478 | 'LEFT', 479 | 'LENGTH', 480 | 'LINEFROMTEXT', 481 | 'LINEFROMWKB', 482 | 'LINESTRING', 483 | 'LINESTRINGFROMTEXT', 484 | 'LINESTRINGFROMWKB', 485 | 'LN', 486 | 'LOAD_FILE', 487 | 'LOCALTIME', 488 | 'LOCALTIMESTAMP', 489 | 'LOCATE', 490 | 'LOG', 491 | 'LOG10', 492 | 'LOG2', 493 | 'LOWER', 494 | 'LPAD', 495 | 'LTRIM', 496 | 'MAKEDATE', 497 | 'MAKETIME', 498 | 'MAKE_SET', 499 | 'MASTER_POS_WAIT', 500 | 'MAX', 501 | 'MBRCONTAINS', 502 | 'MBRDISJOINT', 503 | 'MBREQUAL', 504 | 'MBRINTERSECTS', 505 | 'MBROVERLAPS', 506 | 'MBRTOUCHES', 507 | 'MBRWITHIN', 508 | 'MD5', 509 | 'MICROSECOND', 510 | 'MID', 511 | 'MIN', 512 | 'MINUTE', 513 | 'MLINEFROMTEXT', 514 | 'MLINEFROMWKB', 515 | 'MOD', 516 | 'MONTH', 517 | 'MONTHNAME', 518 | 'MPOINTFROMTEXT', 519 | 'MPOINTFROMWKB', 520 | 'MPOLYFROMTEXT', 521 | 'MPOLYFROMWKB', 522 | 'MULTILINESTRING', 523 | 'MULTILINESTRINGFROMTEXT', 524 | 'MULTILINESTRINGFROMWKB', 525 | 'MULTIPOINT', 526 | 'MULTIPOINTFROMTEXT', 527 | 'MULTIPOINTFROMWKB', 528 | 'MULTIPOLYGON', 529 | 'MULTIPOLYGONFROMTEXT', 530 | 'MULTIPOLYGONFROMWKB', 531 | 'NAME_CONST', 532 | 'NULLIF', 533 | 'NUMGEOMETRIES', 534 | 'NUMINTERIORRINGS', 535 | 'NUMPOINTS', 536 | 'OCT', 537 | 'OCTET_LENGTH', 538 | 'OLD_PASSWORD', 539 | 'ORD', 540 | 'OVERLAPS', 541 | 'PASSWORD', 542 | 'PERIOD_ADD', 543 | 'PERIOD_DIFF', 544 | 'PI', 545 | 'POINT', 546 | 'POINTFROMTEXT', 547 | 'POINTFROMWKB', 548 | 'POINTN', 549 | 'POINTONSURFACE', 550 | 'POLYFROMTEXT', 551 | 'POLYFROMWKB', 552 | 'POLYGON', 553 | 'POLYGONFROMTEXT', 554 | 'POLYGONFROMWKB', 555 | 'POSITION', 556 | 'POW', 557 | 'POWER', 558 | 'QUARTER', 559 | 'QUOTE', 560 | 'RADIANS', 561 | 'RAND', 562 | 'RELATED', 563 | 'RELEASE_LOCK', 564 | 'REPEAT', 565 | 'REPLACE', 566 | 'REVERSE', 567 | 'RIGHT', 568 | 'ROUND', 569 | 'ROW_COUNT', 570 | 'RPAD', 571 | 'RTRIM', 572 | 'SCHEMA', 573 | 'SECOND', 574 | 'SEC_TO_TIME', 575 | 'SESSION_USER', 576 | 'SHA', 577 | 'SHA1', 578 | 'SIGN', 579 | 'SIN', 580 | 'SLEEP', 581 | 'SOUNDEX', 582 | 'SPACE', 583 | 'SQRT', 584 | 'SRID', 585 | 'STARTPOINT', 586 | 'STD', 587 | 'STDDEV', 588 | 'STDDEV_POP', 589 | 'STDDEV_SAMP', 590 | 'STRCMP', 591 | 'STR_TO_DATE', 592 | 'SUBDATE', 593 | 'SUBSTR', 594 | 'SUBSTRING', 595 | 'SUBSTRING_INDEX', 596 | 'SUBTIME', 597 | 'SUM', 598 | 'SYMDIFFERENCE', 599 | 'SYSDATE', 600 | 'SYSTEM_USER', 601 | 'TAN', 602 | 'TIME', 603 | 'TIMEDIFF', 604 | 'TIMESTAMP', 605 | 'TIMESTAMPADD', 606 | 'TIMESTAMPDIFF', 607 | 'TIME_FORMAT', 608 | 'TIME_TO_SEC', 609 | 'TOUCHES', 610 | 'TO_DAYS', 611 | 'TRIM', 612 | 'TRUNCATE', 613 | 'UCASE', 614 | 'UNCOMPRESS', 615 | 'UNCOMPRESSED_LENGTH', 616 | 'UNHEX', 617 | 'UNIQUE_USERS', 618 | 'UNIX_TIMESTAMP', 619 | 'UPDATEXML', 620 | 'UPPER', 621 | 'USER', 622 | 'UTC_DATE', 623 | 'UTC_TIME', 624 | 'UTC_TIMESTAMP', 625 | 'UUID', 626 | 'VARIANCE', 627 | 'VAR_POP', 628 | 'VAR_SAMP', 629 | 'VERSION', 630 | 'WEEK', 631 | 'WEEKDAY', 632 | 'WEEKOFYEAR', 633 | 'WITHIN', 634 | 'X', 635 | 'Y', 636 | 'YEAR', 637 | 'YEARWEEK', 638 | ]; 639 | 640 | /** 641 | * @var array 642 | */ 643 | public static $boundaries = [ 644 | ',', 645 | ';', 646 | ')', 647 | '(', 648 | '.', 649 | '=', 650 | '<', 651 | '>', 652 | '+', 653 | '-', 654 | '*', 655 | '/', 656 | '!', 657 | '^', 658 | '%', 659 | '|', 660 | '&', 661 | '#', 662 | ]; 663 | 664 | /** 665 | * @param $token 666 | * 667 | * @return bool 668 | */ 669 | public static function isTokenTypeReservedTopLevel($token) 670 | { 671 | return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_RESERVED_TOP_LEVEL; 672 | } 673 | 674 | /** 675 | * @param string $token 676 | * @param Parentheses $parentheses 677 | * @param Formatter $formatter 678 | */ 679 | public static function tokenHasLimitClause($token, Parentheses $parentheses, Formatter $formatter) 680 | { 681 | if ('LIMIT' === $token[Tokenizer::TOKEN_VALUE] && false === $parentheses->getInlineParentheses()) { 682 | $formatter->setClauseLimit(true); 683 | } 684 | } 685 | 686 | /** 687 | * @param $token 688 | * @param $tokens 689 | * @param $i 690 | * @param $originalTokens 691 | * 692 | * @return bool 693 | */ 694 | public static function tokenHasMultipleBoundaryCharactersTogether($token, &$tokens, $i, &$originalTokens) 695 | { 696 | return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BOUNDARY 697 | && self::tokenPreviousCharacterIsBoundary($tokens, $i) 698 | && self::tokenPreviousCharacterIsWhiteSpace($token, $originalTokens); 699 | } 700 | 701 | /** 702 | * @param $tokens 703 | * @param $i 704 | * 705 | * @return bool 706 | */ 707 | public static function tokenPreviousCharacterIsBoundary(&$tokens, $i) 708 | { 709 | return (isset($tokens[$i - 1]) && $tokens[$i - 1][Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BOUNDARY); 710 | } 711 | 712 | /** 713 | * @param $token 714 | * @param $originalTokens 715 | * 716 | * @return bool 717 | */ 718 | public static function tokenPreviousCharacterIsWhiteSpace($token, &$originalTokens) 719 | { 720 | return (isset($originalTokens[$token['i'] - 1]) 721 | && $originalTokens[$token['i'] - 1][Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_WHITESPACE); 722 | } 723 | 724 | /** 725 | * @param $token 726 | * @param $tokens 727 | * @param $i 728 | * 729 | * @return bool 730 | */ 731 | public static function tokenIsMinusSign($token, &$tokens, $i) 732 | { 733 | return '-' === $token[Tokenizer::TOKEN_VALUE] 734 | && self::tokenNextCharacterIsNumber($tokens, $i) 735 | && isset($tokens[$i - 1]); 736 | } 737 | 738 | /** 739 | * @param $tokens 740 | * @param $i 741 | * 742 | * @return bool 743 | */ 744 | public static function tokenNextCharacterIsNumber(&$tokens, $i) 745 | { 746 | return (isset($tokens[$i + 1]) 747 | && $tokens[$i + 1][Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_NUMBER); 748 | } 749 | } 750 | -------------------------------------------------------------------------------- /src/Helper/WhiteSpace.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/22/14 5 | * Time: 1:19 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Helper; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class WhiteSpace. 17 | */ 18 | class WhiteSpace 19 | { 20 | /** 21 | * @param $token 22 | * 23 | * @return bool 24 | */ 25 | public static function tokenHasExtraWhiteSpaceLeft($token) 26 | { 27 | return 28 | $token[Tokenizer::TOKEN_VALUE] === '.' 29 | || $token[Tokenizer::TOKEN_VALUE] === ',' 30 | || $token[Tokenizer::TOKEN_VALUE] === ';'; 31 | } 32 | 33 | /** 34 | * @param $token 35 | * 36 | * @return bool 37 | */ 38 | public static function tokenHasExtraWhiteSpaceRight($token) 39 | { 40 | return 41 | $token[Tokenizer::TOKEN_VALUE] === '(' 42 | || $token[Tokenizer::TOKEN_VALUE] === '.'; 43 | } 44 | 45 | /** 46 | * @param $tokenType 47 | * 48 | * @return bool 49 | */ 50 | public static function tokenIsNumberAndHasExtraWhiteSpaceRight($tokenType) 51 | { 52 | return 53 | $tokenType !== Tokenizer::TOKEN_TYPE_QUOTE 54 | && $tokenType !== Tokenizer::TOKEN_TYPE_BACK_TICK_QUOTE 55 | && $tokenType !== Tokenizer::TOKEN_TYPE_WORD 56 | && $tokenType !== Tokenizer::TOKEN_TYPE_NUMBER; 57 | } 58 | 59 | /** 60 | * @param $token 61 | * 62 | * @return bool 63 | */ 64 | public static function tokenHasExtraWhiteSpaces($token) 65 | { 66 | return \strpos($token[Tokenizer::TOKEN_VALUE], ' ') !== false 67 | || \strpos($token[Tokenizer::TOKEN_VALUE], "\n") !== false 68 | || \strpos($token[Tokenizer::TOKEN_VALUE], "\t") !== false; 69 | } 70 | 71 | /** 72 | * @param $originalTokens 73 | * @param $token 74 | * 75 | * @return bool 76 | */ 77 | public static function isPrecedingCurrentTokenOfTokenTypeWhiteSpace($originalTokens, $token) 78 | { 79 | return isset($originalTokens[$token['i'] - 1]) 80 | && $originalTokens[$token['i'] - 1][Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_WHITESPACE; 81 | } 82 | 83 | /** 84 | * @param $originalTokens 85 | * 86 | * @return array 87 | */ 88 | public static function removeTokenWhitespace(array &$originalTokens) 89 | { 90 | $tokens = []; 91 | foreach ($originalTokens as $i => &$token) { 92 | if ($token[Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_WHITESPACE) { 93 | $token['i'] = $i; 94 | $tokens[] = $token; 95 | } 96 | } 97 | 98 | return $tokens; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Tokenizer/Parser/Boundary.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/23/14 5 | * Time: 1:34 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class Boundary. 17 | */ 18 | final class Boundary 19 | { 20 | /** 21 | * @param Tokenizer $tokenizer 22 | * @param string $string 23 | * @param array $matches 24 | */ 25 | public static function isBoundary(Tokenizer $tokenizer, $string, array &$matches) 26 | { 27 | if (!$tokenizer->getNextToken() && 28 | self::isBoundaryCharacter($string, $matches, $tokenizer->getRegexBoundaries()) 29 | ) { 30 | $tokenizer->setNextToken(self::getBoundaryCharacter($matches)); 31 | } 32 | } 33 | 34 | /** 35 | * @param string $string 36 | * @param array $matches 37 | * @param string $regexBoundaries 38 | * 39 | * @return bool 40 | */ 41 | protected static function isBoundaryCharacter($string, array &$matches, $regexBoundaries) 42 | { 43 | return (1 == \preg_match('/^('.$regexBoundaries.')/', $string, $matches)); 44 | } 45 | 46 | /** 47 | * @param array $matches 48 | * 49 | * @return array 50 | */ 51 | protected static function getBoundaryCharacter(array &$matches) 52 | { 53 | return [ 54 | Tokenizer::TOKEN_VALUE => $matches[1], 55 | Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_BOUNDARY, 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Tokenizer/Parser/Comment.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/23/14 5 | * Time: 1:22 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class Comment. 17 | */ 18 | final class Comment 19 | { 20 | /** 21 | * @param Tokenizer $tokenizer 22 | * @param string $string 23 | */ 24 | public static function isComment(Tokenizer $tokenizer, $string) 25 | { 26 | if (!$tokenizer->getNextToken() && self::isCommentString($string)) { 27 | $tokenizer->setNextToken(self::getCommentString($string)); 28 | } 29 | } 30 | 31 | /** 32 | * @param string $string 33 | * 34 | * @return bool 35 | */ 36 | protected static function isCommentString($string) 37 | { 38 | return !empty($string[0]) && ($string[0] === '#' || self::isTwoCharacterComment($string)); 39 | } 40 | 41 | /** 42 | * @param string $string 43 | * 44 | * @return bool 45 | */ 46 | protected static function isTwoCharacterComment($string) 47 | { 48 | return !empty($string[1]) && (isset($string[1]) && (self::startsWithDoubleDash($string) || self::startsAsBlock($string))); 49 | } 50 | 51 | /** 52 | * @param string $string 53 | * 54 | * @return bool 55 | */ 56 | protected static function startsWithDoubleDash($string) 57 | { 58 | return !empty($string[1]) && ($string[0] === '-' && ($string[1] === $string[0])); 59 | } 60 | 61 | /** 62 | * @param string $string 63 | * 64 | * @return bool 65 | */ 66 | protected static function startsAsBlock($string) 67 | { 68 | return !empty($string[1]) && ($string[0] === '/' && $string[1] === '*'); 69 | } 70 | 71 | /** 72 | * @param string $string 73 | * 74 | * @return array 75 | */ 76 | protected static function getCommentString($string) 77 | { 78 | $last = \strpos($string, '*/', 2) + 2; 79 | $type = Tokenizer::TOKEN_TYPE_BLOCK_COMMENT; 80 | 81 | if (!empty($string[0]) && ($string[0] === '-' || $string[0] === '#')) { 82 | $last = \strpos($string, "\n"); 83 | $type = Tokenizer::TOKEN_TYPE_COMMENT; 84 | } 85 | 86 | $last = ($last === false) ? \strlen($string) : $last; 87 | 88 | return [ 89 | Tokenizer::TOKEN_VALUE => \substr($string, 0, $last), 90 | Tokenizer::TOKEN_TYPE => $type, 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Tokenizer/Parser/LiteralString.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/23/14 5 | * Time: 1:36 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class LiteralString. 17 | */ 18 | final class LiteralString 19 | { 20 | /** 21 | * @param Tokenizer $tokenizer 22 | * @param string $string 23 | * @param array $matches 24 | */ 25 | public static function isFunction(Tokenizer $tokenizer, $string, array &$matches) 26 | { 27 | if (!$tokenizer->getNextToken() && self::isFunctionString($string, $matches, $tokenizer->getRegexFunction())) { 28 | $tokenizer->setNextToken(self::getFunctionString($string, $matches)); 29 | } 30 | } 31 | 32 | /** 33 | * A function must be succeeded by '('. 34 | * This makes it so that a function such as "COUNT(" is considered a function, but "COUNT" alone is not function. 35 | * 36 | * @param string $string 37 | * @param array $matches 38 | * @param string $regexFunction 39 | * 40 | * @return bool 41 | */ 42 | protected static function isFunctionString($string, array &$matches, $regexFunction) 43 | { 44 | return (1 == \preg_match('/^('.$regexFunction.'[(]|\s|[)])/', \strtoupper($string), $matches)); 45 | } 46 | 47 | /** 48 | * @param string $string 49 | * @param array $matches 50 | * 51 | * @return array 52 | */ 53 | protected static function getFunctionString($string, array &$matches) 54 | { 55 | return [ 56 | Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_RESERVED, 57 | Tokenizer::TOKEN_VALUE => \substr($string, 0, \strlen($matches[1]) - 1), 58 | ]; 59 | } 60 | 61 | /** 62 | * @param Tokenizer $tokenizer 63 | * @param string $string 64 | * @param array $matches 65 | */ 66 | public static function getNonReservedString(Tokenizer $tokenizer, $string, array &$matches) 67 | { 68 | if (!$tokenizer->getNextToken()) { 69 | $data = []; 70 | 71 | if (1 == \preg_match('/^(.*?)($|\s|["\'`]|'.$tokenizer->getRegexBoundaries().')/', $string, $matches)) { 72 | $data = [ 73 | Tokenizer::TOKEN_VALUE => $matches[1], 74 | Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_WORD, 75 | ]; 76 | } 77 | 78 | $tokenizer->setNextToken($data); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Tokenizer/Parser/Numeral.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/23/14 5 | * Time: 1:32 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class Numeral. 17 | */ 18 | final class Numeral 19 | { 20 | /** 21 | * @param Tokenizer $tokenizer 22 | * @param string $string 23 | * @param array $matches 24 | * 25 | * @return array 26 | */ 27 | public static function isNumeral(Tokenizer $tokenizer, $string, array &$matches) 28 | { 29 | if (!$tokenizer->getNextToken() && self::isNumeralString($string, $matches, $tokenizer->getRegexBoundaries())) { 30 | $tokenizer->setNextToken(self::getNumeralString($matches)); 31 | } 32 | } 33 | 34 | /** 35 | * @param string $string 36 | * @param array $matches 37 | * @param string $regexBoundaries 38 | * 39 | * @return bool 40 | */ 41 | protected static function isNumeralString($string, array &$matches, $regexBoundaries) 42 | { 43 | return (1 == \preg_match( 44 | '/^([0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)($|\s|"\'`|'.$regexBoundaries.')/', 45 | $string, 46 | $matches 47 | )); 48 | } 49 | 50 | /** 51 | * @param array $matches 52 | * 53 | * @return array 54 | */ 55 | protected static function getNumeralString(array &$matches) 56 | { 57 | return [Tokenizer::TOKEN_VALUE => $matches[1], Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_NUMBER]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Tokenizer/Parser/Quoted.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/23/14 5 | * Time: 1:23 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class Quoted. 17 | */ 18 | final class Quoted 19 | { 20 | /** 21 | * @param Tokenizer $tokenizer 22 | * @param string $string 23 | */ 24 | public static function isQuoted(Tokenizer $tokenizer, $string) 25 | { 26 | if (!$tokenizer->getNextToken() && self::isQuotedString($string)) { 27 | $tokenizer->setNextToken(self::getQuotedString($string)); 28 | } 29 | } 30 | 31 | /** 32 | * @param string $string 33 | * 34 | * @return bool 35 | */ 36 | protected static function isQuotedString($string) 37 | { 38 | return !empty($string[0]) && ($string[0] === '"' || $string[0] === '\'' || $string[0] === '`' || $string[0] === '['); 39 | } 40 | 41 | /** 42 | * @param string $string 43 | * 44 | * @return array 45 | */ 46 | protected static function getQuotedString($string) 47 | { 48 | $tokenType = Tokenizer::TOKEN_TYPE_QUOTE; 49 | 50 | if (!empty($string[0]) && ($string[0] === '`' || $string[0] === '[')) { 51 | $tokenType = Tokenizer::TOKEN_TYPE_BACK_TICK_QUOTE; 52 | } 53 | 54 | return [ 55 | Tokenizer::TOKEN_TYPE => $tokenType, 56 | Tokenizer::TOKEN_VALUE => self::wrapStringWithQuotes($string), 57 | ]; 58 | } 59 | 60 | /** 61 | * This checks for the following patterns: 62 | * 1. backtick quoted string using `` to escape 63 | * 2. square bracket quoted string (SQL Server) using ]] to escape 64 | * 3. double quoted string using "" or \" to escape 65 | * 4. single quoted string using '' or \' to escape. 66 | * 67 | * @param string $string 68 | * 69 | * @return null 70 | */ 71 | public static function wrapStringWithQuotes($string) 72 | { 73 | $returnString = null; 74 | 75 | $regex = '/^(((`[^`]*($|`))+)|((\[[^\]]*($|\]))(\][^\]]*($|\]))*)|'. 76 | '(("[^"\\\\]*(?:\\\\.[^"\\\\]*)*("|$))+)|((\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*(\'|$))+))/s'; 77 | 78 | if (1 == \preg_match($regex, $string, $matches)) { 79 | $returnString = $matches[1]; 80 | } 81 | 82 | return $returnString; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Tokenizer/Parser/Reserved.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/23/14 5 | * Time: 1:18 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class Reserved. 17 | */ 18 | final class Reserved 19 | { 20 | /** 21 | * @var array 22 | */ 23 | protected static $regex = [ 24 | Tokenizer::TOKEN_TYPE_RESERVED_TOP_LEVEL => 'getRegexReservedTopLevel', 25 | Tokenizer::TOKEN_TYPE_RESERVED_NEWLINE => 'getRegexReservedNewLine', 26 | Tokenizer::TOKEN_TYPE_RESERVED => 'getRegexReserved', 27 | ]; 28 | 29 | /** 30 | * @param Tokenizer $tokenizer 31 | * @param string $string 32 | * @param array|null $previous 33 | * 34 | * @return array 35 | */ 36 | public static function isReserved(Tokenizer $tokenizer, $string, $previous) 37 | { 38 | $tokenData = []; 39 | 40 | if (!$tokenizer->getNextToken() && self::isReservedPrecededByDotCharacter($previous)) { 41 | $upperCase = \strtoupper($string); 42 | 43 | self::getReservedString($tokenData, Tokenizer::TOKEN_TYPE_RESERVED_TOP_LEVEL, $string, $tokenizer); 44 | self::getReservedString($tokenData, Tokenizer::TOKEN_TYPE_RESERVED_NEWLINE, $upperCase, $tokenizer); 45 | self::getReservedString($tokenData, Tokenizer::TOKEN_TYPE_RESERVED, $string, $tokenizer); 46 | 47 | $tokenizer->setNextToken($tokenData); 48 | } 49 | } 50 | 51 | /** 52 | * A reserved word cannot be preceded by a "." in order to differentiate "mytable.from" from the token "from". 53 | * 54 | * @param $previous 55 | * 56 | * @return bool 57 | */ 58 | protected static function isReservedPrecededByDotCharacter($previous) 59 | { 60 | return !$previous || !isset($previous[Tokenizer::TOKEN_VALUE]) || $previous[Tokenizer::TOKEN_VALUE] !== '.'; 61 | } 62 | 63 | /** 64 | * @param array $tokenData 65 | * @param $type 66 | * @param string $string 67 | * @param Tokenizer $tokenizer 68 | */ 69 | protected static function getReservedString(array &$tokenData, $type, $string, Tokenizer $tokenizer) 70 | { 71 | $matches = []; 72 | $method = self::$regex[$type]; 73 | 74 | if (empty($tokenData) && self::isReservedString( 75 | $string, 76 | $matches, 77 | $tokenizer->$method(), 78 | $tokenizer->getRegexBoundaries() 79 | ) 80 | ) { 81 | $tokenData = self::getStringTypeArray($type, $string, $matches); 82 | } 83 | } 84 | 85 | /** 86 | * @param string $upper 87 | * @param array $matches 88 | * @param string $regexReserved 89 | * @param string $regexBoundaries 90 | * 91 | * @return bool 92 | */ 93 | protected static function isReservedString($upper, array &$matches, $regexReserved, $regexBoundaries) 94 | { 95 | return 1 == \preg_match( 96 | '/^('.$regexReserved.')($|\s|'.$regexBoundaries.')/', 97 | \strtoupper($upper), 98 | $matches 99 | ); 100 | } 101 | 102 | /** 103 | * @param string $type 104 | * @param string $string 105 | * @param array $matches 106 | * 107 | * @return array 108 | */ 109 | protected static function getStringTypeArray($type, $string, array &$matches) 110 | { 111 | return [ 112 | Tokenizer::TOKEN_TYPE => $type, 113 | Tokenizer::TOKEN_VALUE => \substr($string, 0, \strlen($matches[1])), 114 | ]; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Tokenizer/Parser/UserDefined.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/23/14 5 | * Time: 1:26 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class UserDefined. 17 | */ 18 | final class UserDefined 19 | { 20 | /** 21 | * @param Tokenizer $tokenizer 22 | * @param string $string 23 | * 24 | * @return array 25 | */ 26 | public static function isUserDefinedVariable(Tokenizer $tokenizer, $string) 27 | { 28 | if (!$tokenizer->getNextToken() && self::isUserDefinedVariableString($string)) { 29 | $tokenizer->setNextToken(self::getUserDefinedVariableString($string)); 30 | } 31 | } 32 | 33 | /** 34 | * @param string $string 35 | * 36 | * @return bool 37 | */ 38 | protected static function isUserDefinedVariableString(&$string) 39 | { 40 | return !empty($string[0]) && !empty($string[1]) && ($string[0] === '@' && isset($string[1])); 41 | } 42 | 43 | /** 44 | * Gets the user defined variables for in quoted or non-quoted fashion. 45 | * 46 | * @param string $string 47 | * 48 | * @return array 49 | */ 50 | protected static function getUserDefinedVariableString(&$string) 51 | { 52 | $returnData = [ 53 | Tokenizer::TOKEN_VALUE => null, 54 | Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_VARIABLE, 55 | ]; 56 | 57 | self::setTokenValueStartingWithAtSymbolAndWrapped($returnData, $string); 58 | self::setTokenValueStartingWithAtSymbol($returnData, $string); 59 | 60 | return $returnData; 61 | } 62 | 63 | /** 64 | * @param array $returnData 65 | * @param string $string 66 | */ 67 | protected static function setTokenValueStartingWithAtSymbolAndWrapped(array &$returnData, $string) 68 | { 69 | if (!empty($string[1]) && ($string[1] === '"' || $string[1] === '\'' || $string[1] === '`')) { 70 | $returnData[Tokenizer::TOKEN_VALUE] = '@'.Quoted::wrapStringWithQuotes(\substr($string, 1)); 71 | } 72 | } 73 | 74 | /** 75 | * @param array $returnData 76 | * @param string $string 77 | */ 78 | protected static function setTokenValueStartingWithAtSymbol(array &$returnData, $string) 79 | { 80 | if (null === $returnData[Tokenizer::TOKEN_VALUE]) { 81 | $matches = []; 82 | \preg_match('/^(@[a-zA-Z0-9\._\$]+)/', $string, $matches); 83 | if ($matches) { 84 | $returnData[Tokenizer::TOKEN_VALUE] = $matches[1]; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Tokenizer/Parser/WhiteSpace.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 12/23/14 5 | * Time: 1:19 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class WhiteSpace. 17 | */ 18 | final class WhiteSpace 19 | { 20 | /** 21 | * @param Tokenizer $tokenizer 22 | * @param string $string 23 | * @param array $matches 24 | */ 25 | public static function isWhiteSpace(Tokenizer $tokenizer, $string, array &$matches) 26 | { 27 | if (self::isWhiteSpaceString($string, $matches)) { 28 | $tokenizer->setNextToken(self::getWhiteSpaceString($matches)); 29 | } 30 | } 31 | 32 | /** 33 | * @param string $string 34 | * @param array $matches 35 | * 36 | * @return bool 37 | */ 38 | public static function isWhiteSpaceString($string, array &$matches) 39 | { 40 | return (1 == \preg_match('/^\s+/', $string, $matches)); 41 | } 42 | 43 | /** 44 | * @param array $matches 45 | * 46 | * @return array 47 | */ 48 | public static function getWhiteSpaceString(array &$matches) 49 | { 50 | return [ 51 | Tokenizer::TOKEN_VALUE => $matches[0], 52 | Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_WHITESPACE, 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Tokenizer/Tokenizer.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 6/26/14 5 | * Time: 12:10 AM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Sql\QueryFormatter\Tokenizer; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Helper\Token; 14 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Boundary; 15 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Comment; 16 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Numeral; 17 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Quoted; 18 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Reserved; 19 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\LiteralString; 20 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\UserDefined; 21 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\WhiteSpace; 22 | 23 | /** 24 | * Class Tokenizer. 25 | */ 26 | class Tokenizer 27 | { 28 | const TOKEN_TYPE_WHITESPACE = 0; 29 | const TOKEN_TYPE_WORD = 1; 30 | const TOKEN_TYPE_QUOTE = 2; 31 | const TOKEN_TYPE_BACK_TICK_QUOTE = 3; 32 | const TOKEN_TYPE_RESERVED = 4; 33 | const TOKEN_TYPE_RESERVED_TOP_LEVEL = 5; 34 | const TOKEN_TYPE_RESERVED_NEWLINE = 6; 35 | const TOKEN_TYPE_BOUNDARY = 7; 36 | const TOKEN_TYPE_COMMENT = 8; 37 | const TOKEN_TYPE_BLOCK_COMMENT = 9; 38 | const TOKEN_TYPE_NUMBER = 10; 39 | const TOKEN_TYPE_ERROR = 11; 40 | const TOKEN_TYPE_VARIABLE = 12; 41 | const TOKEN_TYPE = 0; 42 | const TOKEN_VALUE = 1; 43 | 44 | /** 45 | * @var string 46 | */ 47 | protected $regexBoundaries; 48 | 49 | /** 50 | * @var string 51 | */ 52 | protected $regexReserved; 53 | 54 | /** 55 | * @var string 56 | */ 57 | protected $regexReservedNewLine; 58 | 59 | /** 60 | * @var string 61 | */ 62 | protected $regexReservedTopLevel; 63 | 64 | /** 65 | * @var string 66 | */ 67 | protected $regexFunction; 68 | 69 | /** 70 | * @var int 71 | */ 72 | protected $maxCacheKeySize = 15; 73 | 74 | /** 75 | * @var array 76 | */ 77 | protected $tokenCache = []; 78 | 79 | /** 80 | * @var array 81 | */ 82 | protected $nextToken = []; 83 | 84 | /** 85 | * @var int 86 | */ 87 | protected $currentStringLength = 0; 88 | 89 | /** 90 | * @var int 91 | */ 92 | protected $oldStringLength = 0; 93 | 94 | /** 95 | * @var string 96 | */ 97 | protected $previousToken = ''; 98 | 99 | /** 100 | * @var int 101 | */ 102 | protected $tokenLength = 0; 103 | 104 | /** 105 | * @var array 106 | */ 107 | protected $tokens = []; 108 | 109 | /** 110 | * Builds all the regular expressions needed to Tokenize the input. 111 | */ 112 | public function __construct() 113 | { 114 | $reservedMap = \array_combine(Token::$reserved, \array_map('strlen', Token::$reserved)); 115 | \arsort($reservedMap); 116 | Token::$reserved = \array_keys($reservedMap); 117 | 118 | $this->regexFunction = $this->initRegex(Token::$functions); 119 | $this->regexBoundaries = $this->initRegex(Token::$boundaries); 120 | $this->regexReserved = $this->initRegex(Token::$reserved); 121 | $this->regexReservedTopLevel = \str_replace(' ', '\\s+', $this->initRegex(Token::$reservedTopLevel)); 122 | $this->regexReservedNewLine = \str_replace(' ', '\\s+', $this->initRegex(Token::$reservedNewLine)); 123 | } 124 | 125 | /** 126 | * @param $variable 127 | * 128 | * @return string 129 | */ 130 | protected function initRegex($variable) 131 | { 132 | return '('.implode('|', \array_map(array($this, 'quoteRegex'), $variable)).')'; 133 | } 134 | 135 | /** 136 | * Takes a SQL string and breaks it into tokens. 137 | * Each token is an associative array with type and value. 138 | * 139 | * @param string $string 140 | * 141 | * @return array 142 | */ 143 | public function tokenize($string) 144 | { 145 | return (\strlen($string) > 0) ? $this->processTokens($string) : []; 146 | } 147 | 148 | /** 149 | * @param string $string 150 | * 151 | * @return array 152 | */ 153 | protected function processTokens($string) 154 | { 155 | $this->tokens = []; 156 | $this->previousToken = ''; 157 | $this->currentStringLength = \strlen($string); 158 | $this->oldStringLength = \strlen($string) + 1; 159 | 160 | while ($this->currentStringLength >= 0) { 161 | if ($this->oldStringLength <= $this->currentStringLength) { 162 | break; 163 | } 164 | $string = $this->processOneToken($string); 165 | } 166 | 167 | return $this->tokens; 168 | } 169 | 170 | /** 171 | * @param string $string 172 | * 173 | * @return string 174 | */ 175 | protected function processOneToken($string) 176 | { 177 | $token = $this->getToken($string, $this->currentStringLength, $this->previousToken); 178 | $this->tokens[] = $token; 179 | $this->tokenLength = \strlen($token[self::TOKEN_VALUE]); 180 | $this->previousToken = $token; 181 | 182 | $this->oldStringLength = $this->currentStringLength; 183 | $this->currentStringLength -= $this->tokenLength; 184 | 185 | return \substr($string, $this->tokenLength); 186 | } 187 | 188 | /** 189 | * @param string $string 190 | * @param int $currentStringLength 191 | * @param string string 192 | * 193 | * @return array|mixed 194 | */ 195 | protected function getToken($string, $currentStringLength, $previousToken) 196 | { 197 | $cacheKey = $this->useTokenCache($string, $currentStringLength); 198 | if (!empty($cacheKey) && isset($this->tokenCache[$cacheKey])) { 199 | return $this->getNextTokenFromCache($cacheKey); 200 | } 201 | 202 | return $this->getNextTokenFromString($string, $previousToken, $cacheKey); 203 | } 204 | 205 | /** 206 | * @param string $string 207 | * @param int $currentStringLength 208 | * 209 | * @return string 210 | */ 211 | protected function useTokenCache($string, $currentStringLength) 212 | { 213 | $cacheKey = ''; 214 | 215 | if ($currentStringLength >= $this->maxCacheKeySize) { 216 | $cacheKey = \substr($string, 0, $this->maxCacheKeySize); 217 | } 218 | 219 | return $cacheKey; 220 | } 221 | 222 | /** 223 | * @param string $cacheKey 224 | * 225 | * @return mixed 226 | */ 227 | protected function getNextTokenFromCache($cacheKey) 228 | { 229 | return $this->tokenCache[$cacheKey]; 230 | } 231 | 232 | /** 233 | * Get the next token and the token type and store it in cache. 234 | * 235 | * @param string $string 236 | * @param string $token 237 | * @param string $cacheKey 238 | * 239 | * @return array 240 | */ 241 | protected function getNextTokenFromString($string, $token, $cacheKey) 242 | { 243 | $token = $this->parseNextToken($string, $token); 244 | 245 | if ($cacheKey && \strlen($token[self::TOKEN_VALUE]) < $this->maxCacheKeySize) { 246 | $this->tokenCache[$cacheKey] = $token; 247 | } 248 | 249 | return $token; 250 | } 251 | 252 | /** 253 | * Return the next token and token type in a SQL string. 254 | * Quoted strings, comments, reserved words, whitespace, and punctuation are all their own tokens. 255 | * 256 | * @param string $string The SQL string 257 | * @param array $previous The result of the previous parseNextToken() call 258 | * 259 | * @return array An associative array containing the type and value of the token. 260 | */ 261 | protected function parseNextToken($string, $previous = null) 262 | { 263 | $matches = []; 264 | $this->nextToken = []; 265 | 266 | WhiteSpace::isWhiteSpace($this, $string, $matches); 267 | Comment::isComment($this, $string); 268 | Quoted::isQuoted($this, $string); 269 | UserDefined::isUserDefinedVariable($this, $string); 270 | Numeral::isNumeral($this, $string, $matches); 271 | Boundary::isBoundary($this, $string, $matches); 272 | Reserved::isReserved($this, $string, $previous); 273 | LiteralString::isFunction($this, $string, $matches); 274 | LiteralString::getNonReservedString($this, $string, $matches); 275 | 276 | return $this->nextToken; 277 | } 278 | 279 | /** 280 | * @return array 281 | */ 282 | public function getNextToken() 283 | { 284 | return $this->nextToken; 285 | } 286 | 287 | /** 288 | * @param array $nextToken 289 | * 290 | * @return $this 291 | */ 292 | public function setNextToken($nextToken) 293 | { 294 | $this->nextToken = $nextToken; 295 | 296 | return $this; 297 | } 298 | 299 | /** 300 | * @return string 301 | */ 302 | public function getRegexBoundaries() 303 | { 304 | return $this->regexBoundaries; 305 | } 306 | 307 | /** 308 | * @return string 309 | */ 310 | public function getRegexFunction() 311 | { 312 | return $this->regexFunction; 313 | } 314 | 315 | /** 316 | * @return string 317 | */ 318 | public function getRegexReserved() 319 | { 320 | return $this->regexReserved; 321 | } 322 | 323 | /** 324 | * @return string 325 | */ 326 | public function getRegexReservedNewLine() 327 | { 328 | return $this->regexReservedNewLine; 329 | } 330 | 331 | /** 332 | * @return string 333 | */ 334 | public function getRegexReservedTopLevel() 335 | { 336 | return $this->regexReservedTopLevel; 337 | } 338 | 339 | /** 340 | * Helper function for building regular expressions for reserved words and boundary characters. 341 | * 342 | * @param string $string 343 | * 344 | * @return string 345 | */ 346 | protected function quoteRegex($string) 347 | { 348 | return \preg_quote($string, '/'); 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /tests/FormatterTest.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 6/26/14 5 | * Time: 9:11 PM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Tests\Sql\QueryFormatter; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Formatter; 14 | 15 | /** 16 | * Class FormatterTest. 17 | */ 18 | class FormatterTest extends \PHPUnit_Framework_TestCase 19 | { 20 | /** 21 | * @var string 22 | */ 23 | private $querySeparator = "----------SEPARATOR----------\n"; 24 | 25 | /** 26 | * @var string 27 | */ 28 | private $expectedResultFilePath = '/Resources/expectedQueries.sql'; 29 | 30 | /** 31 | * @return array 32 | */ 33 | private function readExpectedQueryFile() 34 | { 35 | $expectedQueryArray = \explode( 36 | $this->querySeparator, 37 | \file_get_contents(\realpath(\dirname(__FILE__)).$this->expectedResultFilePath) 38 | ); 39 | $expectedQueryArray = \array_filter($expectedQueryArray); 40 | 41 | return $expectedQueryArray; 42 | } 43 | 44 | /** 45 | * Data provider reading the test Queries. 46 | */ 47 | public function sqlQueryDataProvider() 48 | { 49 | $expectedQueryArray = $this->readExpectedQueryFile(); 50 | 51 | $queryTestSet = array(); 52 | foreach ($expectedQueryArray as $expectedQuery) { 53 | $queryTestSet[] = array(\preg_replace('/\\s+/', ' ', $expectedQuery), $expectedQuery); 54 | } 55 | 56 | return $queryTestSet; 57 | } 58 | 59 | /** 60 | * @test 61 | * @dataProvider sqlQueryDataProvider 62 | * 63 | * @param $notIndented 64 | * @param $indented 65 | */ 66 | public function itShouldReformatNoIndentQueriesToIndentedVersions($notIndented, $indented) 67 | { 68 | $formatter = new Formatter(); 69 | $result = $formatter->format($notIndented); 70 | 71 | $this->assertSame($indented, $result); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Resources/expectedQueries.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | customer_id, 3 | customer_name, 4 | COUNT(order_id) AS total 5 | FROM 6 | customers 7 | INNER JOIN orders ON customers.customer_id = orders.customer_id 8 | GROUP BY 9 | customer_id, 10 | customer_name 11 | HAVING 12 | COUNT(order_id) > 5 13 | ORDER BY 14 | COUNT(order_id) DESC; 15 | ----------SEPARATOR---------- 16 | UPDATE 17 | customers 18 | SET 19 | totalorders = ordersummary.total 20 | FROM 21 | ( 22 | SELECT 23 | customer_id, 24 | count(order_id) As total 25 | FROM 26 | orders 27 | GROUP BY 28 | customer_id 29 | ) As ordersummary 30 | WHERE 31 | customers.customer_id = ordersummary.customer_id 32 | ----------SEPARATOR---------- 33 | SELECT 34 | * 35 | FROM 36 | sometable 37 | UNION ALL 38 | SELECT 39 | * 40 | FROM 41 | someothertable; 42 | ----------SEPARATOR---------- 43 | SET 44 | NAMES 'utf8'; 45 | ----------SEPARATOR---------- 46 | CREATE TABLE `PREFIX_address` ( 47 | `id_address` int(10) unsigned NOT NULL auto_increment, 48 | `id_country` int(10) unsigned NOT NULL, 49 | `id_state` int(10) unsigned default NULL, 50 | `id_customer` int(10) unsigned NOT NULL default '0', 51 | `id_manufacturer` int(10) unsigned NOT NULL default '0', 52 | `id_supplier` int(10) unsigned NOT NULL default '0', 53 | `id_warehouse` int(10) unsigned NOT NULL default '0', 54 | `alias` varchar(32) NOT NULL, 55 | `company` varchar(64) default NULL, 56 | `lastname` varchar(32) NOT NULL, 57 | `firstname` varchar(32) NOT NULL, 58 | `address1` varchar(128) NOT NULL, 59 | `address2` varchar(128) default NULL, 60 | `postcode` varchar(12) default NULL, 61 | `city` varchar(64) NOT NULL, 62 | `other` text, 63 | `phone` varchar(16) default NULL, 64 | `phone_mobile` varchar(16) default NULL, 65 | `vat_number` varchar(32) default NULL, 66 | `dni` varchar(16) DEFAULT NULL, 67 | `date_add` datetime NOT NULL, 68 | `date_upd` datetime NOT NULL, 69 | `active` tinyint(1) unsigned NOT NULL default '1', 70 | `deleted` tinyint(1) unsigned NOT NULL default '0', 71 | PRIMARY KEY (`id_address`), 72 | KEY `address_customer` (`id_customer`), 73 | KEY `id_country` (`id_country`), 74 | KEY `id_state` (`id_state`), 75 | KEY `id_manufacturer` (`id_manufacturer`), 76 | KEY `id_supplier` (`id_supplier`), 77 | KEY `id_warehouse` (`id_warehouse`) 78 | ) ENGINE = ENGINE_TYPE DEFAULT CHARSET = utf8 79 | ----------SEPARATOR---------- 80 | CREATE TABLE `PREFIX_alias` ( 81 | `id_alias` int(10) unsigned NOT NULL auto_increment, 82 | `alias` varchar(255) NOT NULL, 83 | `search` varchar(255) NOT NULL, 84 | `active` tinyint(1) NOT NULL default '1', 85 | PRIMARY KEY (`id_alias`), 86 | UNIQUE KEY `alias` (`alias`) 87 | ) ENGINE = ENGINE_TYPE DEFAULT CHARSET = utf8 88 | ----------SEPARATOR---------- 89 | CREATE TABLE `PREFIX_carrier` ( 90 | `id_carrier` int(10) unsigned NOT NULL AUTO_INCREMENT, 91 | `id_reference` int(10) unsigned NOT NULL, 92 | `id_tax_rules_group` int(10) unsigned DEFAULT '0', 93 | `name` varchar(64) NOT NULL, 94 | `url` varchar(255) DEFAULT NULL, 95 | `active` tinyint(1) unsigned NOT NULL DEFAULT '0', 96 | `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', 97 | `shipping_handling` tinyint(1) unsigned NOT NULL DEFAULT '1', 98 | `range_behavior` tinyint(1) unsigned NOT NULL DEFAULT '0', 99 | `is_module` tinyint(1) unsigned NOT NULL DEFAULT '0', 100 | `is_free` tinyint(1) unsigned NOT NULL DEFAULT '0', 101 | `shipping_external` tinyint(1) unsigned NOT NULL DEFAULT '0', 102 | `need_range` tinyint(1) unsigned NOT NULL DEFAULT '0', 103 | `external_module_name` varchar(64) DEFAULT NULL, 104 | `shipping_method` int(2) NOT NULL DEFAULT '0', 105 | `position` int(10) unsigned NOT NULL default '0', 106 | `max_width` int(10) DEFAULT 0, 107 | `max_height` int(10) DEFAULT 0, 108 | `max_depth` int(10) DEFAULT 0, 109 | `max_weight` int(10) DEFAULT 0, 110 | `grade` int(10) DEFAULT 0, 111 | PRIMARY KEY (`id_carrier`), 112 | KEY `deleted` (`deleted`, `active`), 113 | KEY `id_tax_rules_group` (`id_tax_rules_group`) 114 | ) ENGINE = ENGINE_TYPE DEFAULT CHARSET = utf8 115 | ----------SEPARATOR---------- 116 | CREATE TABLE IF NOT EXISTS `PREFIX_specific_price_rule` ( 117 | `id_specific_price_rule` int(10) unsigned NOT NULL AUTO_INCREMENT, 118 | `name` VARCHAR(255) NOT NULL, 119 | `id_shop` int(11) unsigned NOT NULL DEFAULT '1', 120 | `id_currency` int(10) unsigned NOT NULL, 121 | `id_country` int(10) unsigned NOT NULL, 122 | `id_group` int(10) unsigned NOT NULL, 123 | `from_quantity` mediumint(8) unsigned NOT NULL, 124 | `price` DECIMAL(20, 6), 125 | `reduction` decimal(20, 6) NOT NULL, 126 | `reduction_type` enum('amount', 'percentage') NOT NULL, 127 | `from` datetime NOT NULL, 128 | `to` datetime NOT NULL, 129 | PRIMARY KEY (`id_specific_price_rule`), 130 | KEY `id_product` ( 131 | `id_shop`, `id_currency`, `id_country`, 132 | `id_group`, `from_quantity`, `from`, 133 | `to` 134 | ) 135 | ) ENGINE = ENGINE_TYPE DEFAULT CHARSET = utf8 136 | ----------SEPARATOR---------- 137 | UPDATE 138 | `PREFIX_configuration` 139 | SET 140 | value = '6' 141 | WHERE 142 | name = 'PS_SEARCH_WEIGHT_PNAME' 143 | ----------SEPARATOR---------- 144 | UPDATE 145 | `PREFIX_hook_module` 146 | SET 147 | position = 1 148 | WHERE 149 | id_hook = ( 150 | SELECT 151 | id_hook 152 | FROM 153 | `PREFIX_hook` 154 | WHERE 155 | name = 'displayPayment' 156 | ) 157 | AND id_module = ( 158 | SELECT 159 | id_module 160 | FROM 161 | `PREFIX_module` 162 | WHERE 163 | name = 'cheque' 164 | ) 165 | OR id_hook = ( 166 | SELECT 167 | id_hook 168 | FROM 169 | `PREFIX_hook` 170 | WHERE 171 | name = 'displayPaymentReturn' 172 | ) 173 | AND id_module = ( 174 | SELECT 175 | id_module 176 | FROM 177 | `PREFIX_module` 178 | WHERE 179 | name = 'cheque' 180 | ) 181 | OR id_hook = ( 182 | SELECT 183 | id_hook 184 | FROM 185 | `PREFIX_hook` 186 | WHERE 187 | name = 'displayHome' 188 | ) 189 | AND id_module = ( 190 | SELECT 191 | id_module 192 | FROM 193 | `PREFIX_module` 194 | WHERE 195 | name = 'homeslider' 196 | ) 197 | OR id_hook = ( 198 | SELECT 199 | id_hook 200 | FROM 201 | `PREFIX_hook` 202 | WHERE 203 | name = 'actionAuthentication' 204 | ) 205 | AND id_module = ( 206 | SELECT 207 | id_module 208 | FROM 209 | `PREFIX_module` 210 | WHERE 211 | name = 'statsdata' 212 | ) 213 | OR id_hook = ( 214 | SELECT 215 | id_hook 216 | FROM 217 | `PREFIX_hook` 218 | WHERE 219 | name = 'actionShopDataDuplication' 220 | ) 221 | AND id_module = ( 222 | SELECT 223 | id_module 224 | FROM 225 | `PREFIX_module` 226 | WHERE 227 | name = 'homeslider' 228 | ) 229 | OR id_hook = ( 230 | SELECT 231 | id_hook 232 | FROM 233 | `PREFIX_hook` 234 | WHERE 235 | name = 'displayTop' 236 | ) 237 | AND id_module = ( 238 | SELECT 239 | id_module 240 | FROM 241 | `PREFIX_module` 242 | WHERE 243 | name = 'blocklanguages' 244 | ) 245 | OR id_hook = ( 246 | SELECT 247 | id_hook 248 | FROM 249 | `PREFIX_hook` 250 | WHERE 251 | name = 'actionCustomerAccountAdd' 252 | ) 253 | AND id_module = ( 254 | SELECT 255 | id_module 256 | FROM 257 | `PREFIX_module` 258 | WHERE 259 | name = 'statsdata' 260 | ) 261 | OR id_hook = ( 262 | SELECT 263 | id_hook 264 | FROM 265 | `PREFIX_hook` 266 | WHERE 267 | name = 'displayCustomerAccount' 268 | ) 269 | AND id_module = ( 270 | SELECT 271 | id_module 272 | FROM 273 | `PREFIX_module` 274 | WHERE 275 | name = 'favoriteproducts' 276 | ) 277 | OR id_hook = ( 278 | SELECT 279 | id_hook 280 | FROM 281 | `PREFIX_hook` 282 | WHERE 283 | name = 'displayAdminStatsModules' 284 | ) 285 | AND id_module = ( 286 | SELECT 287 | id_module 288 | FROM 289 | `PREFIX_module` 290 | WHERE 291 | name = 'statsvisits' 292 | ) 293 | OR id_hook = ( 294 | SELECT 295 | id_hook 296 | FROM 297 | `PREFIX_hook` 298 | WHERE 299 | name = 'displayAdminStatsGraphEngine' 300 | ) 301 | AND id_module = ( 302 | SELECT 303 | id_module 304 | FROM 305 | `PREFIX_module` 306 | WHERE 307 | name = 'graphvisifire' 308 | ) 309 | OR id_hook = ( 310 | SELECT 311 | id_hook 312 | FROM 313 | `PREFIX_hook` 314 | WHERE 315 | name = 'displayAdminStatsGridEngine' 316 | ) 317 | AND id_module = ( 318 | SELECT 319 | id_module 320 | FROM 321 | `PREFIX_module` 322 | WHERE 323 | name = 'gridhtml' 324 | ) 325 | OR id_hook = ( 326 | SELECT 327 | id_hook 328 | FROM 329 | `PREFIX_hook` 330 | WHERE 331 | name = 'displayLeftColumnProduct' 332 | ) 333 | AND id_module = ( 334 | SELECT 335 | id_module 336 | FROM 337 | `PREFIX_module` 338 | WHERE 339 | name = 'blocksharefb' 340 | ) 341 | OR id_hook = ( 342 | SELECT 343 | id_hook 344 | FROM 345 | `PREFIX_hook` 346 | WHERE 347 | name = 'actionSearch' 348 | ) 349 | AND id_module = ( 350 | SELECT 351 | id_module 352 | FROM 353 | `PREFIX_module` 354 | WHERE 355 | name = 'statssearch' 356 | ) 357 | OR id_hook = ( 358 | SELECT 359 | id_hook 360 | FROM 361 | `PREFIX_hook` 362 | WHERE 363 | name = 'actionCategoryAdd' 364 | ) 365 | AND id_module = ( 366 | SELECT 367 | id_module 368 | FROM 369 | `PREFIX_module` 370 | WHERE 371 | name = 'blockcategories' 372 | ) 373 | OR id_hook = ( 374 | SELECT 375 | id_hook 376 | FROM 377 | `PREFIX_hook` 378 | WHERE 379 | name = 'actionCategoryUpdate' 380 | ) 381 | AND id_module = ( 382 | SELECT 383 | id_module 384 | FROM 385 | `PREFIX_module` 386 | WHERE 387 | name = 'blockcategories' 388 | ) 389 | OR id_hook = ( 390 | SELECT 391 | id_hook 392 | FROM 393 | `PREFIX_hook` 394 | WHERE 395 | name = 'actionCategoryDelete' 396 | ) 397 | AND id_module = ( 398 | SELECT 399 | id_module 400 | FROM 401 | `PREFIX_module` 402 | WHERE 403 | name = 'blockcategories' 404 | ) 405 | OR id_hook = ( 406 | SELECT 407 | id_hook 408 | FROM 409 | `PREFIX_hook` 410 | WHERE 411 | name = 'actionAdminMetaSave' 412 | ) 413 | AND id_module = ( 414 | SELECT 415 | id_module 416 | FROM 417 | `PREFIX_module` 418 | WHERE 419 | name = 'blockcategories' 420 | ) 421 | OR id_hook = ( 422 | SELECT 423 | id_hook 424 | FROM 425 | `PREFIX_hook` 426 | WHERE 427 | name = 'displayMyAccountBlock' 428 | ) 429 | AND id_module = ( 430 | SELECT 431 | id_module 432 | FROM 433 | `PREFIX_module` 434 | WHERE 435 | name = 'favoriteproducts' 436 | ) 437 | OR id_hook = ( 438 | SELECT 439 | id_hook 440 | FROM 441 | `PREFIX_hook` 442 | WHERE 443 | name = 'displayFooter' 444 | ) 445 | AND id_module = ( 446 | SELECT 447 | id_module 448 | FROM 449 | `PREFIX_module` 450 | WHERE 451 | name = 'blockreinsurance' 452 | ) 453 | ----------SEPARATOR---------- 454 | ALTER TABLE 455 | `PREFIX_employee` 456 | ADD 457 | `bo_color` varchar(32) default NULL 458 | AFTER 459 | `stats_date_to` 460 | ----------SEPARATOR---------- 461 | INSERT INTO `PREFIX_cms_category_lang` 462 | VALUES 463 | ( 464 | 1, 3, 'Inicio', '', 'home', NULL, NULL, 465 | NULL 466 | ) 467 | ----------SEPARATOR---------- 468 | INSERT INTO `PREFIX_cms_category` 469 | VALUES 470 | (1, 0, 0, 1, NOW(), NOW(), 0) 471 | ----------SEPARATOR---------- 472 | UPDATE 473 | `PREFIX_cms_category` 474 | SET 475 | `position` = 0 476 | ----------SEPARATOR---------- 477 | ALTER TABLE 478 | `PREFIX_customer` 479 | ADD 480 | `note` text 481 | AFTER 482 | `secure_key` 483 | ----------SEPARATOR---------- 484 | ALTER TABLE 485 | `PREFIX_contact` 486 | ADD 487 | `customer_service` tinyint(1) NOT NULL DEFAULT 0 488 | AFTER 489 | `email` 490 | ----------SEPARATOR---------- 491 | INSERT INTO `PREFIX_specific_price` ( 492 | `id_product`, `id_shop`, `id_currency`, 493 | `id_country`, `id_group`, `priority`, 494 | `price`, `from_quantity`, `reduction`, 495 | `reduction_type`, `from`, `to` 496 | ) ( 497 | SELECT 498 | dq.`id_product`, 499 | 1, 500 | 1, 501 | 0, 502 | 1, 503 | 0, 504 | 0.00, 505 | dq.`quantity`, 506 | IF( 507 | dq.`id_discount_type` = 2, dq.`value`, 508 | dq.`value` / 100 509 | ), 510 | IF ( 511 | dq.`id_discount_type` = 2, 'amount', 512 | 'percentage' 513 | ), 514 | '0000-00-00 00:00:00', 515 | '0000-00-00 00:00:00' 516 | FROM 517 | `PREFIX_discount_quantity` dq 518 | INNER JOIN `PREFIX_product` p ON (p.`id_product` = dq.`id_product`) 519 | ) 520 | ----------SEPARATOR---------- 521 | DROP 522 | TABLE `PREFIX_discount_quantity` 523 | ----------SEPARATOR---------- 524 | INSERT INTO `PREFIX_specific_price` ( 525 | `id_product`, `id_shop`, `id_currency`, 526 | `id_country`, `id_group`, `priority`, 527 | `price`, `from_quantity`, `reduction`, 528 | `reduction_type`, `from`, `to` 529 | ) ( 530 | SELECT 531 | p.`id_product`, 532 | 1, 533 | 0, 534 | 0, 535 | 0, 536 | 0, 537 | 0.00, 538 | 1, 539 | IF( 540 | p.`reduction_price` > 0, p.`reduction_price`, 541 | p.`reduction_percent` / 100 542 | ), 543 | IF( 544 | p.`reduction_price` > 0, 'amount', 545 | 'percentage' 546 | ), 547 | IF ( 548 | p.`reduction_from` = p.`reduction_to`, 549 | '0000-00-00 00:00:00', p.`reduction_from` 550 | ), 551 | IF ( 552 | p.`reduction_from` = p.`reduction_to`, 553 | '0000-00-00 00:00:00', p.`reduction_to` 554 | ) 555 | FROM 556 | `PREFIX_product` p 557 | WHERE 558 | p.`reduction_price` 559 | OR p.`reduction_percent` 560 | ) 561 | ----------SEPARATOR---------- 562 | ALTER TABLE 563 | `PREFIX_product` 564 | DROP 565 | `reduction_price`, 566 | DROP 567 | `reduction_percent`, 568 | DROP 569 | `reduction_from`, 570 | DROP 571 | `reduction_to` 572 | ----------SEPARATOR---------- 573 | INSERT INTO `PREFIX_configuration` ( 574 | `name`, `value`, `date_add`, `date_upd` 575 | ) 576 | VALUES 577 | ( 578 | 'PS_SPECIFIC_PRICE_PRIORITIES', 579 | 'id_shop;id_currency;id_country;id_group', 580 | NOW(), NOW() 581 | ), 582 | ('PS_TAX_DISPLAY', 0, NOW(), NOW()), 583 | ( 584 | 'PS_SMARTY_FORCE_COMPILE', 1, NOW(), 585 | NOW() 586 | ), 587 | ( 588 | 'PS_DISTANCE_UNIT', 'km', NOW(), NOW() 589 | ), 590 | ( 591 | 'PS_STORES_DISPLAY_CMS', 0, NOW(), 592 | NOW() 593 | ), 594 | ( 595 | 'PS_STORES_DISPLAY_FOOTER', 0, NOW(), 596 | NOW() 597 | ), 598 | ( 599 | 'PS_STORES_SIMPLIFIED', 0, NOW(), 600 | NOW() 601 | ), 602 | ( 603 | 'PS_STATSDATA_CUSTOMER_PAGESVIEWS', 604 | 1, NOW(), NOW() 605 | ), 606 | ( 607 | 'PS_STATSDATA_PAGESVIEWS', 1, NOW(), 608 | NOW() 609 | ), 610 | ( 611 | 'PS_STATSDATA_PLUGINS', 1, NOW(), 612 | NOW() 613 | ) 614 | ----------SEPARATOR---------- 615 | INSERT INTO `PREFIX_configuration` ( 616 | `name`, `value`, `date_add`, `date_upd` 617 | ) 618 | VALUES 619 | ( 620 | 'PS_CONDITIONS_CMS_ID', 621 | IFNULL( 622 | ( 623 | SELECT 624 | `id_cms` 625 | FROM 626 | `PREFIX_cms` 627 | WHERE 628 | `id_cms` = 3 629 | ), 630 | 0 631 | ), 632 | NOW(), 633 | NOW() 634 | ) 635 | ----------SEPARATOR---------- 636 | CREATE TEMPORARY TABLE `PREFIX_configuration_tmp` (`value` text) 637 | ----------SEPARATOR---------- 638 | SET 639 | @defaultOOS = ( 640 | SELECT 641 | value 642 | FROM 643 | `PREFIX_configuration` 644 | WHERE 645 | name = 'PS_ORDER_OUT_OF_STOCK' 646 | ) 647 | ----------SEPARATOR---------- 648 | UPDATE 649 | `PREFIX_product` p 650 | SET 651 | `cache_default_attribute` = 0 652 | WHERE 653 | `id_product` NOT IN ( 654 | SELECT 655 | `id_product` 656 | FROM 657 | `PREFIX_product_attribute` 658 | ) 659 | ----------SEPARATOR---------- 660 | INSERT INTO `PREFIX_hook` ( 661 | `name`, `title`, `description`, `position` 662 | ) 663 | VALUES 664 | ( 665 | 'processCarrier', 'Carrier Process', 666 | NULL, 0 667 | ) 668 | ----------SEPARATOR---------- 669 | INSERT INTO `PREFIX_stock_mvt_reason_lang` ( 670 | `id_stock_mvt_reason`, `id_lang`, 671 | `name` 672 | ) 673 | VALUES 674 | (1, 1, 'Order'), 675 | (1, 2, 'Commande'), 676 | (2, 1, 'Missing Stock Movement'), 677 | ( 678 | 2, 2, 'Mouvement de stock manquant' 679 | ), 680 | (3, 1, 'Restocking'), 681 | (3, 2, 'Réssort') 682 | ----------SEPARATOR---------- 683 | INSERT INTO `PREFIX_meta_lang` ( 684 | `id_lang`, `id_meta`, `title`, `url_rewrite` 685 | ) 686 | VALUES 687 | ( 688 | 1, 689 | ( 690 | SELECT 691 | `id_meta` 692 | FROM 693 | `PREFIX_meta` 694 | WHERE 695 | `page` = 'authentication' 696 | ), 697 | 'Authentication', 698 | 'authentication' 699 | ), 700 | ( 701 | 2, 702 | ( 703 | SELECT 704 | `id_meta` 705 | FROM 706 | `PREFIX_meta` 707 | WHERE 708 | `page` = 'authentication' 709 | ), 710 | 'Authentification', 711 | 'authentification' 712 | ), 713 | ( 714 | 3, 715 | ( 716 | SELECT 717 | `id_meta` 718 | FROM 719 | `PREFIX_meta` 720 | WHERE 721 | `page` = 'authentication' 722 | ), 723 | 'Autenticación', 724 | 'autenticacion' 725 | ) 726 | ----------SEPARATOR---------- 727 | LOCK TABLES `admin_assert` WRITE 728 | ----------SEPARATOR---------- 729 | UNLOCK TABLES 730 | ----------SEPARATOR---------- 731 | DROP 732 | TABLE IF EXISTS `admin_role` 733 | ----------SEPARATOR---------- 734 | SELECT 735 | -- This is a test 736 | ----------SEPARATOR---------- 737 | SELECT 738 | * 739 | LIMIT 740 | 1; 741 | SELECT 742 | a, 743 | b, 744 | c, 745 | d 746 | FROM 747 | e 748 | LIMIT 749 | 1, 2; 750 | SELECT 751 | 1, 752 | 2, 753 | 3 754 | WHERE 755 | a in (1, 2, 3, 4, 5) 756 | AND b = 5; 757 | ----------SEPARATOR---------- 758 | SELECT 759 | count - 50 760 | WHERE 761 | a - 50 = b 762 | WHERE 763 | 1 764 | AND -50 765 | WHERE 766 | -50 = a 767 | WHERE 768 | a = -50 769 | WHERE 770 | 1 771 | /*test*/ 772 | -50 773 | WHERE 774 | 1 775 | AND -50; 776 | ----------SEPARATOR---------- 777 | SELECT 778 | @"weird variable name"; 779 | ----------SEPARATOR---------- 780 | SELECT 781 | "no closing quote 782 | ----------SEPARATOR---------- 783 | SELECT 784 | [sqlserver] 785 | FROM 786 | [escap[e]]d style]; 787 | ----------SEPARATOR---------- 788 | SELECT 789 | count(*) AS all_columns, 790 | `Column1`, 791 | `Testing`, 792 | `Testing Three` 793 | FROM 794 | `Table1` 795 | WHERE 796 | (Column1 = :v1) 797 | AND (`Column2` = `Column3`) 798 | AND ( 799 | `Column2` = `Column3` 800 | OR Column4 >= NOW() 801 | ) 802 | GROUP BY 803 | Column1 804 | ORDER BY 805 | Column3 DESC 806 | LIMIT 807 | :v2, 808 | :v3 809 | -------------------------------------------------------------------------------- /tests/Tokenizer/TokenizerTest.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 6/26/14 5 | * Time: 2:19 AM. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace NilPortugues\Tests\Sql\QueryFormatter\Tokenizer; 12 | 13 | use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer; 14 | 15 | /** 16 | * Class TokenizerTest. 17 | */ 18 | class TokenizerTest extends \PHPUnit_Framework_TestCase 19 | { 20 | /** 21 | * @var Tokenizer 22 | */ 23 | private $tokenizer; 24 | 25 | /** 26 | * 27 | */ 28 | protected function setUp() 29 | { 30 | $this->tokenizer = new Tokenizer(); 31 | } 32 | 33 | /** 34 | * 35 | */ 36 | protected function tearDown() 37 | { 38 | $this->tokenizer = null; 39 | } 40 | 41 | /** 42 | * @test 43 | */ 44 | public function itShouldTokenizeWhiteSpace() 45 | { 46 | $sql = <<tokenizer->tokenize($sql); 50 | 51 | $whiteSpacesFound = false; 52 | foreach ($result as $token) { 53 | if (' ' === $token[Tokenizer::TOKEN_VALUE]) { 54 | $whiteSpacesFound = true; 55 | break; 56 | } 57 | } 58 | 59 | $this->assertTrue($whiteSpacesFound); 60 | } 61 | 62 | /** 63 | * Comment starting with # will be treated as tokens representing the whole comment. 64 | * 65 | * @test 66 | */ 67 | public function itShouldTokenizeCommentStartingWithSymbolNumber() 68 | { 69 | $sql = <<tokenizer->tokenize($sql); 74 | 75 | $commentFound = false; 76 | foreach ($result as $token) { 77 | if ('# A comment' === $token[Tokenizer::TOKEN_VALUE]) { 78 | $commentFound = true; 79 | break; 80 | } 81 | } 82 | 83 | $this->assertTrue($commentFound); 84 | } 85 | 86 | /** 87 | * Comment starting with -- will be treated as tokens representing the whole comment. 88 | * 89 | * @test 90 | */ 91 | public function itShouldTokenizeCommentStartingWithDash() 92 | { 93 | $sql = <<tokenizer->tokenize($sql); 99 | 100 | $commentFound = false; 101 | foreach ($result as $token) { 102 | if ('-- This is another comment' === $token[Tokenizer::TOKEN_VALUE]) { 103 | $commentFound = true; 104 | break; 105 | } 106 | } 107 | 108 | $this->assertTrue($commentFound); 109 | } 110 | 111 | /** 112 | * Comment blocks in SQL Server fashion will be treated as tokens representing the whole comment. 113 | * 114 | * @test 115 | */ 116 | public function itShouldTokenizeCommentWithBlockComment() 117 | { 118 | $sql = <<tokenizer->tokenize($sql); 122 | 123 | $commentFound = false; 124 | foreach ($result as $token) { 125 | if ('/* This is a block comment */' === $token[Tokenizer::TOKEN_VALUE]) { 126 | $commentFound = true; 127 | break; 128 | } 129 | } 130 | 131 | $this->assertTrue($commentFound); 132 | } 133 | 134 | /** 135 | * Unterminated comment block will be processed incorrectly by the tokenizer, 136 | * yet we should be able to detect this error in order to handle the resulting situation. 137 | * 138 | * @test 139 | */ 140 | public function itShouldTokenizeCommentWithUnterminatedBlockComment() 141 | { 142 | $sql = <<tokenizer->tokenize($sql); 146 | 147 | $commentStartFound = false; 148 | foreach ($result as $token) { 149 | if ('/*' === $token[Tokenizer::TOKEN_VALUE]) { 150 | $commentStartFound = true; 151 | break; 152 | } 153 | } 154 | $this->assertTrue($commentStartFound); 155 | } 156 | 157 | /** 158 | * @test 159 | */ 160 | public function itShouldTokenizeQuoted() 161 | { 162 | $sql = <<tokenizer->tokenize($sql); 166 | 167 | $quotedFound = false; 168 | foreach ($result as $token) { 169 | if ('`position`' === $token[Tokenizer::TOKEN_VALUE]) { 170 | $quotedFound = true; 171 | break; 172 | } 173 | } 174 | 175 | $this->assertTrue($quotedFound); 176 | } 177 | 178 | /** 179 | * SQL Server syntax for defining custom variables. 180 | * 181 | * @test 182 | */ 183 | public function itShouldTokenizeUserDefinedVariableWithQuotations() 184 | { 185 | $sql = <<tokenizer->tokenize($sql); 189 | 190 | $quotedFound = false; 191 | foreach ($result as $token) { 192 | if ('@find' === $token[Tokenizer::TOKEN_VALUE]) { 193 | $quotedFound = true; 194 | break; 195 | } 196 | } 197 | 198 | $this->assertTrue($quotedFound); 199 | } 200 | 201 | /** 202 | * This validates Microsoft SQL Server syntax for user variables. 203 | * 204 | * @test 205 | */ 206 | public function itShouldTokenizeUserDefinedVariableWithAtSymbol() 207 | { 208 | $sql = <<tokenizer->tokenize($sql); 212 | 213 | $this->assertNotEmpty($result); 214 | } 215 | 216 | /** 217 | * This test is an edge case. 218 | * 219 | * Given the provided statement, will loop forever if no condition is checking total amount 220 | * of the input string processed. This will happen because the tokenizer has no matching case 221 | * and string processing will not progress. 222 | * 223 | * @test 224 | */ 225 | public function itShouldTokenizeUserDefinedVariableNoProgressTokenizer() 226 | { 227 | $sql = <<tokenizer->tokenize($sql); 231 | 232 | $userVariableNotFound = true; 233 | foreach ($result as $token) { 234 | if ('@' === $token[Tokenizer::TOKEN_VALUE]) { 235 | $userVariableNotFound = false; 236 | break; 237 | } 238 | } 239 | 240 | $this->assertTrue($userVariableNotFound); 241 | } 242 | 243 | /** 244 | * @test 245 | */ 246 | public function itShouldTokenizeNumeralInteger() 247 | { 248 | $sql = <<tokenizer->tokenize($sql); 252 | 253 | $numeralIntegerFound = false; 254 | foreach ($result as $token) { 255 | if ('1' == $token[Tokenizer::TOKEN_VALUE]) { 256 | $numeralIntegerFound = true; 257 | break; 258 | } 259 | } 260 | 261 | $this->assertTrue($numeralIntegerFound); 262 | } 263 | 264 | /** 265 | * @test 266 | */ 267 | public function itShouldTokenizeNumeralNegativeIntegerAsPositiveInteger() 268 | { 269 | $sql = <<tokenizer->tokenize($sql); 273 | 274 | $numeralIntegerFound = false; 275 | foreach ($result as $token) { 276 | if ('1' == $token[Tokenizer::TOKEN_VALUE]) { 277 | $numeralIntegerFound = true; 278 | break; 279 | } 280 | } 281 | $this->assertTrue($numeralIntegerFound); 282 | } 283 | 284 | /** 285 | * @test 286 | */ 287 | public function itShouldTokenizeNumeralFloat() 288 | { 289 | $sql = <<tokenizer->tokenize($sql); 293 | 294 | $numeralIntegerFound = false; 295 | foreach ($result as $token) { 296 | if ('3.14' == $token[Tokenizer::TOKEN_VALUE]) { 297 | $numeralIntegerFound = true; 298 | break; 299 | } 300 | } 301 | $this->assertTrue($numeralIntegerFound); 302 | } 303 | 304 | /** 305 | * @test 306 | */ 307 | public function itShouldTokenizeNumeralNegativeFloatAsPositiveFloat() 308 | { 309 | $sql = <<tokenizer->tokenize($sql); 313 | 314 | $numeralIntegerFound = false; 315 | foreach ($result as $token) { 316 | if ('3.14' == $token[Tokenizer::TOKEN_VALUE]) { 317 | $numeralIntegerFound = true; 318 | break; 319 | } 320 | } 321 | $this->assertTrue($numeralIntegerFound); 322 | } 323 | 324 | /** 325 | * Check if boundary characters are in array. Boundary characters are: ;:)(.=<>+-\/!^%|&#. 326 | * 327 | * @test 328 | */ 329 | public function itShouldTokenizeBoundaryCharacter() 330 | { 331 | $sql = 'SELECT id_user, name FROM users'; 332 | 333 | $result = $this->tokenizer->tokenize($sql); 334 | 335 | $boundaryFound = false; 336 | foreach ($result as $token) { 337 | if (',' === $token[Tokenizer::TOKEN_VALUE]) { 338 | $boundaryFound = true; 339 | break; 340 | } 341 | } 342 | $this->assertTrue($boundaryFound); 343 | } 344 | 345 | /** 346 | * Tokenize columns should not be a problem, even if using a reserved word as a column name. 347 | * Example: users.user_id 348 | * Example of edge cases: users.desc, user.from. 349 | * 350 | * @test 351 | */ 352 | public function itShouldTokenizeReservedWordPrecededByDotCharacter() 353 | { 354 | $sql = <<tokenizer->tokenize($sql); 358 | 359 | $reservedTokenFound = false; 360 | foreach ($result as $token) { 361 | if ('desc' == $token[Tokenizer::TOKEN_VALUE]) { 362 | $reservedTokenFound = true; 363 | break; 364 | } 365 | } 366 | 367 | $this->assertTrue($reservedTokenFound); 368 | } 369 | 370 | /** 371 | * @test 372 | */ 373 | public function itShouldTokenizeReservedTopLevel() 374 | { 375 | $sql = 'SELECT id_user, name FROM users'; 376 | $result = $this->tokenizer->tokenize($sql); 377 | 378 | $reservedTopLevelTokenFound = false; 379 | 380 | foreach ($result as $token) { 381 | if ('FROM' === $token[Tokenizer::TOKEN_VALUE]) { 382 | $reservedTopLevelTokenFound = true; 383 | break; 384 | } 385 | } 386 | $this->assertTrue($reservedTopLevelTokenFound); 387 | } 388 | 389 | /** 390 | * @test 391 | */ 392 | public function itShouldTokenizeReservedNewLine() 393 | { 394 | $sql = <<tokenizer->tokenize($sql); 398 | 399 | $reservedNewLineTokenFound = false; 400 | foreach ($result as $token) { 401 | if ('OR' === $token[Tokenizer::TOKEN_VALUE]) { 402 | $reservedNewLineTokenFound = true; 403 | break; 404 | } 405 | } 406 | $this->assertTrue($reservedNewLineTokenFound); 407 | } 408 | 409 | /** 410 | * @test 411 | */ 412 | public function itShouldTokenizeReserved() 413 | { 414 | $sql = << 5 419 | ORDER BY COUNT(order_id) DESC; 420 | SQL; 421 | $result = $this->tokenizer->tokenize($sql); 422 | 423 | $reservedTokenFound = false; 424 | foreach ($result as $token) { 425 | if ('INNER JOIN' === $token[Tokenizer::TOKEN_VALUE]) { 426 | $reservedTokenFound = true; 427 | break; 428 | } 429 | } 430 | $this->assertTrue($reservedTokenFound); 431 | } 432 | 433 | /** 434 | * @test 435 | */ 436 | public function itShouldTokenizeFunction() 437 | { 438 | $sql = << 5 ORDER BY COUNT(order_id) DESC; 441 | SQL; 442 | $result = $this->tokenizer->tokenize($sql); 443 | 444 | $functionFound = false; 445 | foreach ($result as $token) { 446 | if ('COUNT' === $token[Tokenizer::TOKEN_VALUE]) { 447 | $functionFound = true; 448 | break; 449 | } 450 | } 451 | $this->assertTrue($functionFound); 452 | } 453 | } 454 | --------------------------------------------------------------------------------