Добавлен цикл проверки сайтов

This commit is contained in:
2024-08-12 15:14:49 +03:00
parent 0b56cd37b5
commit 6b7199a326
115 changed files with 15964 additions and 88 deletions

View File

@ -0,0 +1 @@
repo_token: IBpLC0WCtjsqFy5M7PSyMvz2yMpd81xLD

View File

@ -0,0 +1,5 @@
.idea/
bin/
build/
vendor/
composer.lock

View File

@ -0,0 +1,16 @@
language: php
php:
- "5.5"
- "5.6"
- "7.0"
- "hhvm"
before_script:
- composer install
script:
- bin/phpunit --coverage-text
matrix:
allow_failures:
- php: "hhvm"

View File

@ -0,0 +1,33 @@
Contributing
============
First of all, **thank you** for contributing, **you are awesome**!
Here are a few rules to follow in order to ease code reviews, and discussions before
maintainers accept and merge your work.
You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and
[PSR-2](http://www.php-fig.org/psr/2/). If you don't know about any of them, you
should really read the recommendations. Can't wait? Use the [PHP-CS-Fixer
tool](http://cs.sensiolabs.org/).
You MUST run the test suite.
You MUST write (or update) unit tests.
You SHOULD write documentation.
Please, write [commit messages that make
sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
before submitting your Pull Request.
One may ask you to [squash your
commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
too. This is used to "clean" your Pull Request before merging it (we don't want
commits such as `fix tests`, `fix 2`, `fix 3`, etc.).
Also, while creating your Pull Request on GitHub, you MUST write a description
which gives the context and/or explains why you are creating it.
Thank you!

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Nil Portugués Calderó
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,143 @@
SQL Query Formatter
=================
[![Build Status](https://travis-ci.org/nilportugues/sql-query-formatter.svg)](https://travis-ci.org/nilportugues/sql-query-formatter) [![Coverage Status](https://img.shields.io/coveralls/nilportugues/sql-query-formatter.svg)](https://coveralls.io/r/nilportugues/sql-query-formatter?branch=master) [![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) [![Latest Stable Version](https://poser.pugx.org/nilportugues/sql-query-formatter/v/stable)](https://packagist.org/packages/nilportugues/sql-query-formatter) [![Total Downloads](https://poser.pugx.org/nilportugues/sql-query-formatter/downloads)](https://packagist.org/packages/nilportugues/sql-query-formatter) [![License](https://poser.pugx.org/nilportugues/sql-query-formatter/license)](https://packagist.org/packages/nilportugues/sql-query-formatter)
A very lightweight PHP class that re-formats unreadable or computer-generated SQL query statements to human-friendly readable text.
* [1.Installation](#block1)
* [2. Features](#block2)
* [3. Usage](#block3)
* [4. Code Quality](#block5)
* [5. Author](#block6)
* [6. Special Thanks](#block6)
* [7. License](#block7)
<a name="block1"></a>
## 1.Installation
The recommended way to install the SQL Query Formatter is through [Composer](http://getcomposer.org). Run the following command to install it:
```sh
php composer.phar require nilportugues/sql-query-formatter
```
<a name="block2"></a>
## 2. Features
**Human readable SQL formatting**
- Human readable plain text. No colours, no highlighting. Plain text is good enough in most cases.
**Data Binding Awareness**
- SQL Query Formatter takes data binding seriously.
- Placeholder syntax such as `:variable` or `?` is taken into account and is preserved when formatting.
<a name="block3"></a>
## 3. Usage
Sample code:
```php
<?php
use NilPortugues\Sql\QueryFormatter\Formatter;
$query = <<<SQL
SELECT user.user_id, user.username, (SELECT
role.role_name FROM role WHERE (role.role_id = :v1)
LIMIT :v2, :v3 ) AS user_role, (SELECT
role.role_name FROM role WHERE (role.role_id = :v4)
LIMIT :v5, :v6 ) AS role FROM user WHERE (user.user_id = :v7)
SQL;
$formatter = new Formatter();
echo $formatter->format($query);
```
Real output:
```sql
SELECT
user.user_id,
user.username,
(
SELECT
role.role_name
FROM
role
WHERE
(role.role_id = :v1)
LIMIT
:v2,
:v3
) AS user_role,
(
SELECT
role.role_name
FROM
role
WHERE
(role.role_id = :v4)
LIMIT
:v5,
:v6
) AS role
FROM
user
WHERE
(user.user_id = :v7)
```
<a name="block4"></a>
## 4. Fully tested
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/).
To run the test suite, you need [Composer](http://getcomposer.org):
```bash
php composer.phar install --dev
bin/phpunit
```
<a name="block5"></a>
## 5. Author
Nil Portugués Calderó
- <contact@nilportugues.com>
- [http://nilportugues.com](http://nilportugues.com)
<a name="block6"></a>
## 6. Special Thanks
I would like to thank the following people:
- [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.
<a name="block7"></a>
## 7. License
SQL Query Formatter is licensed under the MIT license.
```
Copyright (c) 2015 Nil Portugués Calderó
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```

View File

@ -0,0 +1,42 @@
{
"name":"nilportugues/sql-query-formatter",
"description":"A very lightweight PHP class that reformats unreadable and computer-generated SQL query statements to human-friendly, readable text.",
"keywords": [ "sql", "mysql", "query", "formatter", "format", "parser", "tokenizer", "reformat", "sql server" ],
"type":"library",
"license":"MIT",
"homepage":"http://nilportugues.com",
"authors":
[
{
"name":"Nil Portugués Calderó",
"email":"contact@nilportugues.com",
"homepage":"http://nilportugues.com",
"role":"Lead Developer"
}
],
"autoload":{
"psr-4":{
"NilPortugues\\Sql\\QueryFormatter\\":"src/"
}
},
"autoload-dev":{
"psr-4":{
"NilPortugues\\Tests\\Sql\\QueryFormatter\\":"tests/"
}
},
"require":
{
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "4.*",
"fabpot/php-cs-fixer": "~1.9",
"nilportugues/php_backslasher": "~0.2"
},
"config":
{
"bin-dir": "bin"
},
"minimum-stability": "stable"
}

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit cacheTokens="false"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
syntaxCheck="true"
bootstrap="vendor/autoload.php"
verbose="true">
<php>
<ini name="intl.default_locale" value="en_US.UTF-8" />
<ini name="intl.error_level" value="0" />
<ini name="memory_limit" value="-1" />
<ini name="max_execution_time" value="-1"/>
<ini name="date.timezone" value="Europe/Madrid" />
<ini name="error_reporting" value="E_ALL" />
</php>
<testsuites>
<testsuite name="Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./vendor/</directory>
<directory>./tests/</directory>
</exclude>
</whitelist>
</filter>
<logging>
<log type="junit" target="build/logs/junit.xml"/>
<log type="coverage-clover" target="build/logs/clover.xml"/>
<log type="coverage-html" target="build/coverage"/>
</logging>
</phpunit>

View File

@ -0,0 +1,350 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 6/26/14
* Time: 12:10 AM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter;
use NilPortugues\Sql\QueryFormatter\Helper\Comment;
use NilPortugues\Sql\QueryFormatter\Helper\Indent;
use NilPortugues\Sql\QueryFormatter\Helper\NewLine;
use NilPortugues\Sql\QueryFormatter\Helper\Parentheses;
use NilPortugues\Sql\QueryFormatter\Helper\Token;
use NilPortugues\Sql\QueryFormatter\Helper\WhiteSpace;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Lightweight Formatter heavily based on https://github.com/jdorn/sql-formatter.
*
* Class Formatter
*/
class Formatter
{
/**
* @var Tokenizer
*/
protected $tokenizer;
/**
* @var NewLine
*/
protected $newLine;
/**
* @var Parentheses
*/
protected $parentheses;
/**
* @var string
*/
protected $tab = ' ';
/**
* @var int
*/
protected $inlineCount = 0;
/**
* @var bool
*/
protected $clauseLimit = false;
/**
* @var string
*/
protected $formattedSql = '';
/**
* @var Indent
*/
protected $indentation;
/**
* @var Comment
*/
protected $comment;
/**
* Returns a SQL string in a readable human-friendly format.
*
* @param string $sql
*
* @return string
*/
public function format($sql)
{
$this->reset();
$tab = "\t";
$originalTokens = $this->tokenizer->tokenize((string) $sql);
$tokens = WhiteSpace::removeTokenWhitespace($originalTokens);
foreach ($tokens as $i => $token) {
$queryValue = $token[Tokenizer::TOKEN_VALUE];
$this->indentation->increaseSpecialIndent()->increaseBlockIndent();
$addedNewline = $this->newLine->addNewLineBreak($tab);
if ($this->comment->stringHasCommentToken($token)) {
$this->formattedSql = $this->comment->writeCommentBlock($token, $tab, $queryValue);
continue;
}
if ($this->parentheses->getInlineParentheses()) {
if ($this->parentheses->stringIsClosingParentheses($token)) {
$this->parentheses->writeInlineParenthesesBlock($tab, $queryValue);
continue;
}
$this->newLine->writeNewLineForLongCommaInlineValues($token);
$this->inlineCount += \strlen($token[Tokenizer::TOKEN_VALUE]);
}
switch ($token) {
case $this->parentheses->stringIsOpeningParentheses($token):
$tokens = $this->formatOpeningParenthesis($token, $i, $tokens, $originalTokens);
break;
case $this->parentheses->stringIsClosingParentheses($token):
$this->indentation->decreaseIndentLevelUntilIndentTypeIsSpecial($this);
$this->newLine->addNewLineBeforeToken($addedNewline, $tab);
break;
case $this->stringIsEndOfLimitClause($token):
$this->clauseLimit = false;
break;
case $token[Tokenizer::TOKEN_VALUE] === ',' && false === $this->parentheses->getInlineParentheses():
$this->newLine->writeNewLineBecauseOfComma();
break;
case Token::isTokenTypeReservedTopLevel($token):
$queryValue = $this->formatTokenTypeReservedTopLevel($addedNewline, $tab, $token, $queryValue);
break;
case $this->newLine->isTokenTypeReservedNewLine($token):
$this->newLine->addNewLineBeforeToken($addedNewline, $tab);
if (WhiteSpace::tokenHasExtraWhiteSpaces($token)) {
$queryValue = \preg_replace('/\s+/', ' ', $queryValue);
}
break;
}
$this->formatBoundaryCharacterToken($token, $i, $tokens, $originalTokens);
$this->formatWhiteSpaceToken($token, $queryValue);
$this->formatDashToken($token, $i, $tokens);
}
return \trim(\str_replace(["\t", " \n"], [$this->tab, "\n"], $this->formattedSql))."\n";
}
/**
*
*/
public function reset()
{
$this->tokenizer = new Tokenizer();
$this->indentation = new Indent();
$this->parentheses = new Parentheses($this, $this->indentation);
$this->newLine = new NewLine($this, $this->indentation, $this->parentheses);
$this->comment = new Comment($this, $this->indentation, $this->newLine);
$this->formattedSql = '';
}
/**
* @param $token
* @param $i
* @param array $tokens
* @param array $originalTokens
*
* @return array
*/
protected function formatOpeningParenthesis($token, $i, array &$tokens, array &$originalTokens)
{
$length = 0;
for ($j = 1; $j <= 250; ++$j) {
if (isset($tokens[$i + $j])) {
$next = $tokens[$i + $j];
if ($this->parentheses->stringIsClosingParentheses($next)) {
$this->parentheses->writeNewInlineParentheses();
break;
}
if ($this->parentheses->invalidParenthesesTokenValue($next)
|| $this->parentheses->invalidParenthesesTokenType($next)
) {
break;
}
$length += \strlen($next[Tokenizer::TOKEN_VALUE]);
}
}
$this->newLine->writeNewLineForLongInlineValues($length);
if (WhiteSpace::isPrecedingCurrentTokenOfTokenTypeWhiteSpace($originalTokens, $token)) {
$this->formattedSql = \rtrim($this->formattedSql, ' ');
}
$this->newLine->addNewLineAfterOpeningParentheses();
return $tokens;
}
/**
* @param $token
*
* @return bool
*/
protected function stringIsEndOfLimitClause($token)
{
return $this->clauseLimit
&& $token[Tokenizer::TOKEN_VALUE] !== ','
&& $token[Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_NUMBER
&& $token[Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_WHITESPACE;
}
/**
* @param bool $addedNewline
* @param string $tab
* @param $token
* @param $queryValue
*
* @return mixed
*/
protected function formatTokenTypeReservedTopLevel($addedNewline, $tab, $token, $queryValue)
{
$this->indentation
->setIncreaseSpecialIndent(true)
->decreaseSpecialIndentIfCurrentIndentTypeIsSpecial();
$this->newLine->writeNewLineBecauseOfTopLevelReservedWord($addedNewline, $tab);
if (WhiteSpace::tokenHasExtraWhiteSpaces($token)) {
$queryValue = \preg_replace('/\s+/', ' ', $queryValue);
}
Token::tokenHasLimitClause($token, $this->parentheses, $this);
return $queryValue;
}
/**
* @param $token
* @param $i
* @param array $tokens
* @param array $originalTokens
*/
protected function formatBoundaryCharacterToken($token, $i, array &$tokens, array &$originalTokens)
{
if (Token::tokenHasMultipleBoundaryCharactersTogether($token, $tokens, $i, $originalTokens)) {
$this->formattedSql = \rtrim($this->formattedSql, ' ');
}
}
/**
* @param $token
* @param $queryValue
*/
protected function formatWhiteSpaceToken($token, $queryValue)
{
if (WhiteSpace::tokenHasExtraWhiteSpaceLeft($token)) {
$this->formattedSql = \rtrim($this->formattedSql, ' ');
}
$this->formattedSql .= $queryValue.' ';
if (WhiteSpace::tokenHasExtraWhiteSpaceRight($token)) {
$this->formattedSql = \rtrim($this->formattedSql, ' ');
}
}
/**
* @param $token
* @param $i
* @param array $tokens
*/
protected function formatDashToken($token, $i, array &$tokens)
{
if (Token::tokenIsMinusSign($token, $tokens, $i)) {
$previousTokenType = $tokens[$i - 1][Tokenizer::TOKEN_TYPE];
if (WhiteSpace::tokenIsNumberAndHasExtraWhiteSpaceRight($previousTokenType)) {
$this->formattedSql = \rtrim($this->formattedSql, ' ');
}
}
}
/**
* @return string
*/
public function getFormattedSql()
{
return $this->formattedSql;
}
/**
* @param string $formattedSql
*
* @return $this
*/
public function setFormattedSql($formattedSql)
{
$this->formattedSql = $formattedSql;
return $this;
}
/**
* @param $string
*
* @return $this
*/
public function appendToFormattedSql($string)
{
$this->formattedSql .= $string;
return $this;
}
/**
* @return int
*/
public function getInlineCount()
{
return $this->inlineCount;
}
/**
* @param int $inlineCount
*
* @return $this
*/
public function setInlineCount($inlineCount)
{
$this->inlineCount = $inlineCount;
return $this;
}
/**
* @return bool
*/
public function getClauseLimit()
{
return $this->clauseLimit;
}
/**
* @param bool $clauseLimit
*
* @return $this
*/
public function setClauseLimit($clauseLimit)
{
$this->clauseLimit = $clauseLimit;
return $this;
}
}

View File

@ -0,0 +1,80 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/22/14
* Time: 10:09 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Helper;
use NilPortugues\Sql\QueryFormatter\Formatter;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class Comment.
*/
class Comment
{
/**
* @var \NilPortugues\Sql\QueryFormatter\Formatter
*/
protected $formatter;
/**
* @var Indent
*/
protected $indentation;
/**
* @var NewLine
*/
protected $newLine;
/**
* @param Formatter $formatter
* @param Indent $indentation
* @param NewLine $newLine
*/
public function __construct(Formatter $formatter, Indent $indentation, NewLine $newLine)
{
$this->formatter = $formatter;
$this->indentation = $indentation;
$this->newLine = $newLine;
}
/**
* @param $token
*
* @return bool
*/
public function stringHasCommentToken($token)
{
return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_COMMENT
|| $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BLOCK_COMMENT;
}
/**
* @param $token
* @param string $tab
* @param $queryValue
*
* @return string
*/
public function writeCommentBlock($token, $tab, $queryValue)
{
if ($token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BLOCK_COMMENT) {
$indent = \str_repeat($tab, $this->indentation->getIndentLvl());
$this->formatter->appendToFormattedSql("\n".$indent);
$queryValue = \str_replace("\n", "\n".$indent, $queryValue);
}
$this->formatter->appendToFormattedSql($queryValue);
$this->newLine->setNewline(true);
return $this->formatter->getFormattedSql();
}
}

View File

@ -0,0 +1,213 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/22/14
* Time: 11:37 AM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Helper;
use NilPortugues\Sql\QueryFormatter\Formatter;
/**
* Class Indent.
*/
class Indent
{
/**
* @var bool
*/
protected $inlineIndented = false;
/**
* @var bool
*/
protected $increaseSpecialIndent = false;
/**
* @var int
*/
protected $indentLvl = 0;
/**
* @var bool
*/
protected $increaseBlockIndent = false;
/**
* @var array
*/
protected $indentTypes = [];
/**
* Increase the Special Indent if increaseSpecialIndent is true after the current iteration.
*
* @return $this
*/
public function increaseSpecialIndent()
{
if ($this->increaseSpecialIndent) {
++$this->indentLvl;
$this->increaseSpecialIndent = false;
\array_unshift($this->indentTypes, 'special');
}
return $this;
}
/**
* Increase the Block Indent if increaseBlockIndent is true after the current iteration.
*
* @return $this
*/
public function increaseBlockIndent()
{
if ($this->increaseBlockIndent) {
++$this->indentLvl;
$this->increaseBlockIndent = false;
\array_unshift($this->indentTypes, 'block');
}
return $this;
}
/**
* Closing parentheses decrease the block indent level.
*
* @param Formatter $formatter
*
* @return $this
*/
public function decreaseIndentLevelUntilIndentTypeIsSpecial(Formatter $formatter)
{
$formatter->setFormattedSql(\rtrim($formatter->getFormattedSql(), ' '));
--$this->indentLvl;
while ($j = \array_shift($this->indentTypes)) {
if ('special' !== $j) {
break;
}
--$this->indentLvl;
}
return $this;
}
/**
* @return $this
*/
public function decreaseSpecialIndentIfCurrentIndentTypeIsSpecial()
{
\reset($this->indentTypes);
if (\current($this->indentTypes) === 'special') {
--$this->indentLvl;
\array_shift($this->indentTypes);
}
return $this;
}
/**
* @return bool
*/
public function getIncreaseBlockIndent()
{
return $this->increaseBlockIndent;
}
/**
* @return bool
*/
public function getIncreaseSpecialIndent()
{
return $this->increaseSpecialIndent;
}
/**
* @return int
*/
public function getIndentLvl()
{
return $this->indentLvl;
}
/**
* @return mixed
*/
public function getIndentTypes()
{
return $this->indentTypes;
}
/**
* @param bool $increaseBlockIndent
*
* @return $this
*/
public function setIncreaseBlockIndent($increaseBlockIndent)
{
$this->increaseBlockIndent = $increaseBlockIndent;
return $this;
}
/**
* @param bool $increaseSpecialIndent
*
* @return $this
*/
public function setIncreaseSpecialIndent($increaseSpecialIndent)
{
$this->increaseSpecialIndent = $increaseSpecialIndent;
return $this;
}
/**
* @param int $indentLvl
*
* @return $this
*/
public function setIndentLvl($indentLvl)
{
$this->indentLvl = $indentLvl;
return $this;
}
/**
* @param array $indentTypes
*
* @return $this
*/
public function setIndentTypes($indentTypes)
{
$this->indentTypes = $indentTypes;
return $this;
}
/**
* @param bool $inlineIndented
*
* @return $this
*/
public function setInlineIndented($inlineIndented)
{
$this->inlineIndented = $inlineIndented;
return $this;
}
/**
* @return bool
*/
public function getInlineIndented()
{
return $this->inlineIndented;
}
}

View File

@ -0,0 +1,183 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/22/14
* Time: 11:37 AM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Helper;
use NilPortugues\Sql\QueryFormatter\Formatter;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class NewLine.
*/
class NewLine
{
/**
* @var bool
*/
protected $newline = false;
/**
* @var \NilPortugues\Sql\QueryFormatter\Formatter
*/
protected $formatter;
/**
* @var Indent
*/
protected $indentation;
/**
* @var Parentheses
*/
protected $parentheses;
/**
* @param Formatter $formatter
* @param Indent $indentation
* @param Parentheses $parentheses
*/
public function __construct(Formatter $formatter, Indent $indentation, Parentheses $parentheses)
{
$this->formatter = $formatter;
$this->indentation = $indentation;
$this->parentheses = $parentheses;
}
/**
* Adds a new line break if needed.
*
* @param string $tab
*
* @return bool
*/
public function addNewLineBreak($tab)
{
$addedNewline = false;
if (true === $this->newline) {
$this->formatter->appendToFormattedSql("\n".str_repeat($tab, $this->indentation->getIndentLvl()));
$this->newline = false;
$addedNewline = true;
}
return $addedNewline;
}
/**
* @param $token
*/
public function writeNewLineForLongCommaInlineValues($token)
{
if (',' === $token[Tokenizer::TOKEN_VALUE]) {
if ($this->formatter->getInlineCount() >= 30) {
$this->formatter->setInlineCount(0);
$this->newline = true;
}
}
}
/**
* @param int $length
*/
public function writeNewLineForLongInlineValues($length)
{
if ($this->parentheses->getInlineParentheses() && $length > 30) {
$this->indentation->setIncreaseBlockIndent(true);
$this->indentation->setInlineIndented(true);
$this->newline = true;
}
}
/**
* Adds a new line break for an opening parentheses for a non-inline expression.
*/
public function addNewLineAfterOpeningParentheses()
{
if (false === $this->parentheses->getInlineParentheses()) {
$this->indentation->setIncreaseBlockIndent(true);
$this->newline = true;
}
}
/**
* @param bool $addedNewline
* @param string $tab
*/
public function addNewLineBeforeToken($addedNewline, $tab)
{
if (false === $addedNewline) {
$this->formatter->appendToFormattedSql(
"\n".str_repeat($tab, $this->indentation->getIndentLvl())
);
}
}
/**
* Add a newline before the top level reserved word if necessary and indent.
*
* @param bool $addedNewline
* @param string $tab
*/
public function writeNewLineBecauseOfTopLevelReservedWord($addedNewline, $tab)
{
if (false === $addedNewline) {
$this->formatter->appendToFormattedSql("\n");
} else {
$this->formatter->setFormattedSql(\rtrim($this->formatter->getFormattedSql(), $tab));
}
$this->formatter->appendToFormattedSql(\str_repeat($tab, $this->indentation->getIndentLvl()));
$this->newline = true;
}
/**
* Commas start a new line unless they are found within inline parentheses or SQL 'LIMIT' clause.
* If the previous TOKEN_VALUE is 'LIMIT', undo new line.
*/
public function writeNewLineBecauseOfComma()
{
$this->newline = true;
if (true === $this->formatter->getClauseLimit()) {
$this->newline = false;
$this->formatter->setClauseLimit(false);
}
}
/**
* @param $token
*
* @return bool
*/
public function isTokenTypeReservedNewLine($token)
{
return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_RESERVED_NEWLINE;
}
/**
* @return bool
*/
public function getNewline()
{
return $this->newline;
}
/**
* @param bool $newline
*
* @return $this
*/
public function setNewline($newline)
{
$this->newline = $newline;
return $this;
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/22/14
* Time: 11:37 AM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Helper;
use NilPortugues\Sql\QueryFormatter\Formatter;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class Parentheses.
*/
class Parentheses
{
/**
* @var bool
*/
protected $inlineParentheses = false;
/**
* @var \NilPortugues\Sql\QueryFormatter\Formatter
*/
protected $formatter;
/**
* @var Indent
*/
protected $indentation;
/**
* @param Formatter $formatter
* @param Indent $indentation
*/
public function __construct(Formatter $formatter, Indent $indentation)
{
$this->formatter = $formatter;
$this->indentation = $indentation;
}
/**
* @return bool
*/
public function getInlineParentheses()
{
return $this->inlineParentheses;
}
/**
* @param bool $inlineParentheses
*
* @return $this
*/
public function setInlineParentheses($inlineParentheses)
{
$this->inlineParentheses = $inlineParentheses;
return $this;
}
/**
* @param $token
*
* @return bool
*/
public function stringIsOpeningParentheses($token)
{
return $token[Tokenizer::TOKEN_VALUE] === '(';
}
/**
*
*/
public function writeNewInlineParentheses()
{
$this->inlineParentheses = true;
$this->formatter->setInlineCount(0);
$this->indentation->setInlineIndented(false);
}
/**
* @param $token
*
* @return bool
*/
public function invalidParenthesesTokenValue($token)
{
return $token[Tokenizer::TOKEN_VALUE] === ';'
|| $token[Tokenizer::TOKEN_VALUE] === '(';
}
/**
* @param $token
*
* @return bool
*/
public function invalidParenthesesTokenType($token)
{
return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_RESERVED_TOP_LEVEL
|| $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_RESERVED_NEWLINE
|| $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_COMMENT
|| $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BLOCK_COMMENT;
}
/**
* @param $token
*
* @return bool
*/
public function stringIsClosingParentheses($token)
{
return $token[Tokenizer::TOKEN_VALUE] === ')';
}
/**
* @param string $tab
* @param $queryValue
*/
public function writeInlineParenthesesBlock($tab, $queryValue)
{
$this->formatter->setFormattedSql(\rtrim($this->formatter->getFormattedSql(), ' '));
if ($this->indentation->getInlineIndented()) {
$indentTypes = $this->indentation->getIndentTypes();
\array_shift($indentTypes);
$this->indentation->setIndentTypes($indentTypes);
$this->indentation->setIndentLvl($this->indentation->getIndentLvl() - 1);
$this->formatter->appendToFormattedSql("\n".str_repeat($tab, $this->indentation->getIndentLvl()));
}
$this->inlineParentheses = false;
$this->formatter->appendToFormattedSql($queryValue.' ');
}
}

View File

@ -0,0 +1,749 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/22/14
* Time: 11:38 AM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Helper;
use NilPortugues\Sql\QueryFormatter\Formatter;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class Token.
*/
final class Token
{
/**
* @var array
*/
public static $reserved = [
'ACCESSIBLE',
'ACTION',
'AGAINST',
'AGGREGATE',
'ALGORITHM',
'ALL',
'ALTER',
'ANALYSE',
'ANALYZE',
'AS',
'ASC',
'AUTOCOMMIT',
'AUTO_INCREMENT',
'BACKUP',
'BEGIN',
'BETWEEN',
'BINLOG',
'BOTH',
'CASCADE',
'CASE',
'CHANGE',
'CHANGED',
'CHARACTER SET',
'CHARSET',
'CHECK',
'CHECKSUM',
'COLLATE',
'COLLATION',
'COLUMN',
'COLUMNS',
'COMMENT',
'COMMIT',
'COMMITTED',
'COMPRESSED',
'CONCURRENT',
'CONSTRAINT',
'CONTAINS',
'CONVERT',
'CREATE',
'CROSS',
'CURRENT_TIMESTAMP',
'DATABASE',
'DATABASES',
'DAY',
'DAY_HOUR',
'DAY_MINUTE',
'DAY_SECOND',
'DEFAULT',
'DEFINER',
'DELAYED',
'DELETE',
'DESC',
'DESCRIBE',
'DETERMINISTIC',
'DISTINCT',
'DISTINCTROW',
'DIV',
'DO',
'DUMPFILE',
'DUPLICATE',
'DYNAMIC',
'ELSE',
'ENCLOSED',
'END',
'ENGINE',
'ENGINE_TYPE',
'ENGINES',
'ESCAPE',
'ESCAPED',
'EVENTS',
'EXEC',
'EXECUTE',
'EXISTS',
'EXPLAIN',
'EXTENDED',
'FAST',
'FIELDS',
'FILE',
'FIRST',
'FIXED',
'FLUSH',
'FOR',
'FORCE',
'FOREIGN',
'FULL',
'FULLTEXT',
'FUNCTION',
'GLOBAL',
'GRANT',
'GRANTS',
'GROUP_CONCAT',
'HEAP',
'HIGH_PRIORITY',
'HOSTS',
'HOUR',
'HOUR_MINUTE',
'HOUR_SECOND',
'IDENTIFIED',
'IF',
'IFNULL',
'IGNORE',
'IN',
'INDEX',
'INDEXES',
'INFILE',
'INSERT',
'INSERT_ID',
'INSERT_METHOD',
'INTERVAL',
'INTO',
'INVOKER',
'IS',
'ISOLATION',
'KEY',
'KEYS',
'KILL',
'LAST_INSERT_ID',
'LEADING',
'LEVEL',
'LIKE',
'LINEAR',
'LINES',
'LOAD',
'LOCAL',
'LOCK',
'LOCKS',
'LOGS',
'LOW_PRIORITY',
'MARIA',
'MASTER',
'MASTER_CONNECT_RETRY',
'MASTER_HOST',
'MASTER_LOG_FILE',
'MATCH',
'MAX_CONNECTIONS_PER_HOUR',
'MAX_QUERIES_PER_HOUR',
'MAX_ROWS',
'MAX_UPDATES_PER_HOUR',
'MAX_USER_CONNECTIONS',
'MEDIUM',
'MERGE',
'MINUTE',
'MINUTE_SECOND',
'MIN_ROWS',
'MODE',
'MODIFY',
'MONTH',
'MRG_MYISAM',
'MYISAM',
'NAMES',
'NATURAL',
'NOT',
'NOW()',
'NULL',
'OFFSET',
'ON',
'OPEN',
'OPTIMIZE',
'OPTION',
'OPTIONALLY',
'ON UPDATE',
'ON DELETE',
'OUTFILE',
'PACK_KEYS',
'PAGE',
'PARTIAL',
'PARTITION',
'PARTITIONS',
'PASSWORD',
'PRIMARY',
'PRIVILEGES',
'PROCEDURE',
'PROCESS',
'PROCESSLIST',
'PURGE',
'QUICK',
'RANGE',
'RAID0',
'RAID_CHUNKS',
'RAID_CHUNKSIZE',
'RAID_TYPE',
'READ',
'READ_ONLY',
'READ_WRITE',
'REFERENCES',
'REGEXP',
'RELOAD',
'RENAME',
'REPAIR',
'REPEATABLE',
'REPLACE',
'REPLICATION',
'RESET',
'RESTORE',
'RESTRICT',
'RETURN',
'RETURNS',
'REVOKE',
'RLIKE',
'ROLLBACK',
'ROW',
'ROWS',
'ROW_FORMAT',
'SECOND',
'SECURITY',
'SEPARATOR',
'SERIALIZABLE',
'SESSION',
'SHARE',
'SHOW',
'SHUTDOWN',
'SLAVE',
'SONAME',
'SOUNDS',
'SQL',
'SQL_AUTO_IS_NULL',
'SQL_BIG_RESULT',
'SQL_BIG_SELECTS',
'SQL_BIG_TABLES',
'SQL_BUFFER_RESULT',
'SQL_CALC_FOUND_ROWS',
'SQL_LOG_BIN',
'SQL_LOG_OFF',
'SQL_LOG_UPDATE',
'SQL_LOW_PRIORITY_UPDATES',
'SQL_MAX_JOIN_SIZE',
'SQL_QUOTE_SHOW_CREATE',
'SQL_SAFE_UPDATES',
'SQL_SELECT_LIMIT',
'SQL_SLAVE_SKIP_COUNTER',
'SQL_SMALL_RESULT',
'SQL_WARNINGS',
'SQL_CACHE',
'SQL_NO_CACHE',
'START',
'STARTING',
'STATUS',
'STOP',
'STORAGE',
'STRAIGHT_JOIN',
'STRING',
'STRIPED',
'SUPER',
'TABLE',
'TABLES',
'TEMPORARY',
'TERMINATED',
'THEN',
'TO',
'TRAILING',
'TRANSACTIONAL',
'TRUE',
'TRUNCATE',
'TYPE',
'TYPES',
'UNCOMMITTED',
'UNIQUE',
'UNLOCK',
'UNSIGNED',
'USAGE',
'USE',
'USING',
'VARIABLES',
'VIEW',
'WHEN',
'WITH',
'WORK',
'WRITE',
'YEAR_MONTH',
];
/**
* @var array
*/
public static $reservedTopLevel = [
'SELECT',
'FROM',
'WHERE',
'SET',
'ORDER BY',
'GROUP BY',
'LIMIT',
'DROP',
'VALUES',
'UPDATE',
'HAVING',
'ADD',
'AFTER',
'ALTER TABLE',
'DELETE FROM',
'UNION ALL',
'UNION',
'EXCEPT',
'INTERSECT',
];
/**
* @var array
*/
public static $reservedNewLine = [
'LEFT OUTER JOIN',
'RIGHT OUTER JOIN',
'LEFT JOIN',
'RIGHT JOIN',
'OUTER JOIN',
'INNER JOIN',
'JOIN',
'XOR',
'OR',
'AND',
];
/**
* @var array
*/
public static $functions = [
'ABS',
'ACOS',
'ADDDATE',
'ADDTIME',
'AES_DECRYPT',
'AES_ENCRYPT',
'AREA',
'ASBINARY',
'ASCII',
'ASIN',
'ASTEXT',
'ATAN',
'ATAN2',
'AVG',
'BDMPOLYFROMTEXT',
'BDMPOLYFROMWKB',
'BDPOLYFROMTEXT',
'BDPOLYFROMWKB',
'BENCHMARK',
'BIN',
'BIT_AND',
'BIT_COUNT',
'BIT_LENGTH',
'BIT_OR',
'BIT_XOR',
'BOUNDARY',
'BUFFER',
'CAST',
'CEIL',
'CEILING',
'CENTROID',
'CHAR',
'CHARACTER_LENGTH',
'CHARSET',
'CHAR_LENGTH',
'COALESCE',
'COERCIBILITY',
'COLLATION',
'COMPRESS',
'CONCAT',
'CONCAT_WS',
'CONNECTION_ID',
'CONTAINS',
'CONV',
'CONVERT',
'CONVERT_TZ',
'CONVEXHULL',
'COS',
'COT',
'COUNT',
'CRC32',
'CROSSES',
'CURDATE',
'CURRENT_DATE',
'CURRENT_TIME',
'CURRENT_TIMESTAMP',
'CURRENT_USER',
'CURTIME',
'DATABASE',
'DATE',
'DATEDIFF',
'DATE_ADD',
'DATE_DIFF',
'DATE_FORMAT',
'DATE_SUB',
'DAY',
'DAYNAME',
'DAYOFMONTH',
'DAYOFWEEK',
'DAYOFYEAR',
'DECODE',
'DEFAULT',
'DEGREES',
'DES_DECRYPT',
'DES_ENCRYPT',
'DIFFERENCE',
'DIMENSION',
'DISJOINT',
'DISTANCE',
'ELT',
'ENCODE',
'ENCRYPT',
'ENDPOINT',
'ENVELOPE',
'EQUALS',
'EXP',
'EXPORT_SET',
'EXTERIORRING',
'EXTRACT',
'EXTRACTVALUE',
'FIELD',
'FIND_IN_SET',
'FLOOR',
'FORMAT',
'FOUND_ROWS',
'FROM_DAYS',
'FROM_UNIXTIME',
'GEOMCOLLFROMTEXT',
'GEOMCOLLFROMWKB',
'GEOMETRYCOLLECTION',
'GEOMETRYCOLLECTIONFROMTEXT',
'GEOMETRYCOLLECTIONFROMWKB',
'GEOMETRYFROMTEXT',
'GEOMETRYFROMWKB',
'GEOMETRYN',
'GEOMETRYTYPE',
'GEOMFROMTEXT',
'GEOMFROMWKB',
'GET_FORMAT',
'GET_LOCK',
'GLENGTH',
'GREATEST',
'GROUP_CONCAT',
'GROUP_UNIQUE_USERS',
'HEX',
'HOUR',
'IF',
'IFNULL',
'INET_ATON',
'INET_NTOA',
'INSERT',
'INSTR',
'INTERIORRINGN',
'INTERSECTION',
'INTERSECTS',
'INTERVAL',
'ISCLOSED',
'ISEMPTY',
'ISNULL',
'ISRING',
'ISSIMPLE',
'IS_FREE_LOCK',
'IS_USED_LOCK',
'LAST_DAY',
'LAST_INSERT_ID',
'LCASE',
'LEAST',
'LEFT',
'LENGTH',
'LINEFROMTEXT',
'LINEFROMWKB',
'LINESTRING',
'LINESTRINGFROMTEXT',
'LINESTRINGFROMWKB',
'LN',
'LOAD_FILE',
'LOCALTIME',
'LOCALTIMESTAMP',
'LOCATE',
'LOG',
'LOG10',
'LOG2',
'LOWER',
'LPAD',
'LTRIM',
'MAKEDATE',
'MAKETIME',
'MAKE_SET',
'MASTER_POS_WAIT',
'MAX',
'MBRCONTAINS',
'MBRDISJOINT',
'MBREQUAL',
'MBRINTERSECTS',
'MBROVERLAPS',
'MBRTOUCHES',
'MBRWITHIN',
'MD5',
'MICROSECOND',
'MID',
'MIN',
'MINUTE',
'MLINEFROMTEXT',
'MLINEFROMWKB',
'MOD',
'MONTH',
'MONTHNAME',
'MPOINTFROMTEXT',
'MPOINTFROMWKB',
'MPOLYFROMTEXT',
'MPOLYFROMWKB',
'MULTILINESTRING',
'MULTILINESTRINGFROMTEXT',
'MULTILINESTRINGFROMWKB',
'MULTIPOINT',
'MULTIPOINTFROMTEXT',
'MULTIPOINTFROMWKB',
'MULTIPOLYGON',
'MULTIPOLYGONFROMTEXT',
'MULTIPOLYGONFROMWKB',
'NAME_CONST',
'NULLIF',
'NUMGEOMETRIES',
'NUMINTERIORRINGS',
'NUMPOINTS',
'OCT',
'OCTET_LENGTH',
'OLD_PASSWORD',
'ORD',
'OVERLAPS',
'PASSWORD',
'PERIOD_ADD',
'PERIOD_DIFF',
'PI',
'POINT',
'POINTFROMTEXT',
'POINTFROMWKB',
'POINTN',
'POINTONSURFACE',
'POLYFROMTEXT',
'POLYFROMWKB',
'POLYGON',
'POLYGONFROMTEXT',
'POLYGONFROMWKB',
'POSITION',
'POW',
'POWER',
'QUARTER',
'QUOTE',
'RADIANS',
'RAND',
'RELATED',
'RELEASE_LOCK',
'REPEAT',
'REPLACE',
'REVERSE',
'RIGHT',
'ROUND',
'ROW_COUNT',
'RPAD',
'RTRIM',
'SCHEMA',
'SECOND',
'SEC_TO_TIME',
'SESSION_USER',
'SHA',
'SHA1',
'SIGN',
'SIN',
'SLEEP',
'SOUNDEX',
'SPACE',
'SQRT',
'SRID',
'STARTPOINT',
'STD',
'STDDEV',
'STDDEV_POP',
'STDDEV_SAMP',
'STRCMP',
'STR_TO_DATE',
'SUBDATE',
'SUBSTR',
'SUBSTRING',
'SUBSTRING_INDEX',
'SUBTIME',
'SUM',
'SYMDIFFERENCE',
'SYSDATE',
'SYSTEM_USER',
'TAN',
'TIME',
'TIMEDIFF',
'TIMESTAMP',
'TIMESTAMPADD',
'TIMESTAMPDIFF',
'TIME_FORMAT',
'TIME_TO_SEC',
'TOUCHES',
'TO_DAYS',
'TRIM',
'TRUNCATE',
'UCASE',
'UNCOMPRESS',
'UNCOMPRESSED_LENGTH',
'UNHEX',
'UNIQUE_USERS',
'UNIX_TIMESTAMP',
'UPDATEXML',
'UPPER',
'USER',
'UTC_DATE',
'UTC_TIME',
'UTC_TIMESTAMP',
'UUID',
'VARIANCE',
'VAR_POP',
'VAR_SAMP',
'VERSION',
'WEEK',
'WEEKDAY',
'WEEKOFYEAR',
'WITHIN',
'X',
'Y',
'YEAR',
'YEARWEEK',
];
/**
* @var array
*/
public static $boundaries = [
',',
';',
')',
'(',
'.',
'=',
'<',
'>',
'+',
'-',
'*',
'/',
'!',
'^',
'%',
'|',
'&',
'#',
];
/**
* @param $token
*
* @return bool
*/
public static function isTokenTypeReservedTopLevel($token)
{
return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_RESERVED_TOP_LEVEL;
}
/**
* @param string $token
* @param Parentheses $parentheses
* @param Formatter $formatter
*/
public static function tokenHasLimitClause($token, Parentheses $parentheses, Formatter $formatter)
{
if ('LIMIT' === $token[Tokenizer::TOKEN_VALUE] && false === $parentheses->getInlineParentheses()) {
$formatter->setClauseLimit(true);
}
}
/**
* @param $token
* @param $tokens
* @param $i
* @param $originalTokens
*
* @return bool
*/
public static function tokenHasMultipleBoundaryCharactersTogether($token, &$tokens, $i, &$originalTokens)
{
return $token[Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BOUNDARY
&& self::tokenPreviousCharacterIsBoundary($tokens, $i)
&& self::tokenPreviousCharacterIsWhiteSpace($token, $originalTokens);
}
/**
* @param $tokens
* @param $i
*
* @return bool
*/
public static function tokenPreviousCharacterIsBoundary(&$tokens, $i)
{
return (isset($tokens[$i - 1]) && $tokens[$i - 1][Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_BOUNDARY);
}
/**
* @param $token
* @param $originalTokens
*
* @return bool
*/
public static function tokenPreviousCharacterIsWhiteSpace($token, &$originalTokens)
{
return (isset($originalTokens[$token['i'] - 1])
&& $originalTokens[$token['i'] - 1][Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_WHITESPACE);
}
/**
* @param $token
* @param $tokens
* @param $i
*
* @return bool
*/
public static function tokenIsMinusSign($token, &$tokens, $i)
{
return '-' === $token[Tokenizer::TOKEN_VALUE]
&& self::tokenNextCharacterIsNumber($tokens, $i)
&& isset($tokens[$i - 1]);
}
/**
* @param $tokens
* @param $i
*
* @return bool
*/
public static function tokenNextCharacterIsNumber(&$tokens, $i)
{
return (isset($tokens[$i + 1])
&& $tokens[$i + 1][Tokenizer::TOKEN_TYPE] === Tokenizer::TOKEN_TYPE_NUMBER);
}
}

View File

@ -0,0 +1,100 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/22/14
* Time: 1:19 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Helper;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class WhiteSpace.
*/
class WhiteSpace
{
/**
* @param $token
*
* @return bool
*/
public static function tokenHasExtraWhiteSpaceLeft($token)
{
return
$token[Tokenizer::TOKEN_VALUE] === '.'
|| $token[Tokenizer::TOKEN_VALUE] === ','
|| $token[Tokenizer::TOKEN_VALUE] === ';';
}
/**
* @param $token
*
* @return bool
*/
public static function tokenHasExtraWhiteSpaceRight($token)
{
return
$token[Tokenizer::TOKEN_VALUE] === '('
|| $token[Tokenizer::TOKEN_VALUE] === '.';
}
/**
* @param $tokenType
*
* @return bool
*/
public static function tokenIsNumberAndHasExtraWhiteSpaceRight($tokenType)
{
return
$tokenType !== Tokenizer::TOKEN_TYPE_QUOTE
&& $tokenType !== Tokenizer::TOKEN_TYPE_BACK_TICK_QUOTE
&& $tokenType !== Tokenizer::TOKEN_TYPE_WORD
&& $tokenType !== Tokenizer::TOKEN_TYPE_NUMBER;
}
/**
* @param $token
*
* @return bool
*/
public static function tokenHasExtraWhiteSpaces($token)
{
return \strpos($token[Tokenizer::TOKEN_VALUE], ' ') !== false
|| \strpos($token[Tokenizer::TOKEN_VALUE], "\n") !== false
|| \strpos($token[Tokenizer::TOKEN_VALUE], "\t") !== false;
}
/**
* @param $originalTokens
* @param $token
*
* @return bool
*/
public static function isPrecedingCurrentTokenOfTokenTypeWhiteSpace($originalTokens, $token)
{
return isset($originalTokens[$token['i'] - 1])
&& $originalTokens[$token['i'] - 1][Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_WHITESPACE;
}
/**
* @param $originalTokens
*
* @return array
*/
public static function removeTokenWhitespace(array &$originalTokens)
{
$tokens = [];
foreach ($originalTokens as $i => &$token) {
if ($token[Tokenizer::TOKEN_TYPE] !== Tokenizer::TOKEN_TYPE_WHITESPACE) {
$token['i'] = $i;
$tokens[] = $token;
}
}
return $tokens;
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/23/14
* Time: 1:34 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class Boundary.
*/
final class Boundary
{
/**
* @param Tokenizer $tokenizer
* @param string $string
* @param array $matches
*/
public static function isBoundary(Tokenizer $tokenizer, $string, array &$matches)
{
if (!$tokenizer->getNextToken() &&
self::isBoundaryCharacter($string, $matches, $tokenizer->getRegexBoundaries())
) {
$tokenizer->setNextToken(self::getBoundaryCharacter($matches));
}
}
/**
* @param string $string
* @param array $matches
* @param string $regexBoundaries
*
* @return bool
*/
protected static function isBoundaryCharacter($string, array &$matches, $regexBoundaries)
{
return (1 == \preg_match('/^('.$regexBoundaries.')/', $string, $matches));
}
/**
* @param array $matches
*
* @return array
*/
protected static function getBoundaryCharacter(array &$matches)
{
return [
Tokenizer::TOKEN_VALUE => $matches[1],
Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_BOUNDARY,
];
}
}

View File

@ -0,0 +1,93 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/23/14
* Time: 1:22 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class Comment.
*/
final class Comment
{
/**
* @param Tokenizer $tokenizer
* @param string $string
*/
public static function isComment(Tokenizer $tokenizer, $string)
{
if (!$tokenizer->getNextToken() && self::isCommentString($string)) {
$tokenizer->setNextToken(self::getCommentString($string));
}
}
/**
* @param string $string
*
* @return bool
*/
protected static function isCommentString($string)
{
return !empty($string[0]) && ($string[0] === '#' || self::isTwoCharacterComment($string));
}
/**
* @param string $string
*
* @return bool
*/
protected static function isTwoCharacterComment($string)
{
return !empty($string[1]) && (isset($string[1]) && (self::startsWithDoubleDash($string) || self::startsAsBlock($string)));
}
/**
* @param string $string
*
* @return bool
*/
protected static function startsWithDoubleDash($string)
{
return !empty($string[1]) && ($string[0] === '-' && ($string[1] === $string[0]));
}
/**
* @param string $string
*
* @return bool
*/
protected static function startsAsBlock($string)
{
return !empty($string[1]) && ($string[0] === '/' && $string[1] === '*');
}
/**
* @param string $string
*
* @return array
*/
protected static function getCommentString($string)
{
$last = \strpos($string, '*/', 2) + 2;
$type = Tokenizer::TOKEN_TYPE_BLOCK_COMMENT;
if (!empty($string[0]) && ($string[0] === '-' || $string[0] === '#')) {
$last = \strpos($string, "\n");
$type = Tokenizer::TOKEN_TYPE_COMMENT;
}
$last = ($last === false) ? \strlen($string) : $last;
return [
Tokenizer::TOKEN_VALUE => \substr($string, 0, $last),
Tokenizer::TOKEN_TYPE => $type,
];
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/23/14
* Time: 1:36 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class LiteralString.
*/
final class LiteralString
{
/**
* @param Tokenizer $tokenizer
* @param string $string
* @param array $matches
*/
public static function isFunction(Tokenizer $tokenizer, $string, array &$matches)
{
if (!$tokenizer->getNextToken() && self::isFunctionString($string, $matches, $tokenizer->getRegexFunction())) {
$tokenizer->setNextToken(self::getFunctionString($string, $matches));
}
}
/**
* A function must be succeeded by '('.
* This makes it so that a function such as "COUNT(" is considered a function, but "COUNT" alone is not function.
*
* @param string $string
* @param array $matches
* @param string $regexFunction
*
* @return bool
*/
protected static function isFunctionString($string, array &$matches, $regexFunction)
{
return (1 == \preg_match('/^('.$regexFunction.'[(]|\s|[)])/', \strtoupper($string), $matches));
}
/**
* @param string $string
* @param array $matches
*
* @return array
*/
protected static function getFunctionString($string, array &$matches)
{
return [
Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_RESERVED,
Tokenizer::TOKEN_VALUE => \substr($string, 0, \strlen($matches[1]) - 1),
];
}
/**
* @param Tokenizer $tokenizer
* @param string $string
* @param array $matches
*/
public static function getNonReservedString(Tokenizer $tokenizer, $string, array &$matches)
{
if (!$tokenizer->getNextToken()) {
$data = [];
if (1 == \preg_match('/^(.*?)($|\s|["\'`]|'.$tokenizer->getRegexBoundaries().')/', $string, $matches)) {
$data = [
Tokenizer::TOKEN_VALUE => $matches[1],
Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_WORD,
];
}
$tokenizer->setNextToken($data);
}
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/23/14
* Time: 1:32 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class Numeral.
*/
final class Numeral
{
/**
* @param Tokenizer $tokenizer
* @param string $string
* @param array $matches
*
* @return array
*/
public static function isNumeral(Tokenizer $tokenizer, $string, array &$matches)
{
if (!$tokenizer->getNextToken() && self::isNumeralString($string, $matches, $tokenizer->getRegexBoundaries())) {
$tokenizer->setNextToken(self::getNumeralString($matches));
}
}
/**
* @param string $string
* @param array $matches
* @param string $regexBoundaries
*
* @return bool
*/
protected static function isNumeralString($string, array &$matches, $regexBoundaries)
{
return (1 == \preg_match(
'/^([0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)($|\s|"\'`|'.$regexBoundaries.')/',
$string,
$matches
));
}
/**
* @param array $matches
*
* @return array
*/
protected static function getNumeralString(array &$matches)
{
return [Tokenizer::TOKEN_VALUE => $matches[1], Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_NUMBER];
}
}

View File

@ -0,0 +1,84 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/23/14
* Time: 1:23 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class Quoted.
*/
final class Quoted
{
/**
* @param Tokenizer $tokenizer
* @param string $string
*/
public static function isQuoted(Tokenizer $tokenizer, $string)
{
if (!$tokenizer->getNextToken() && self::isQuotedString($string)) {
$tokenizer->setNextToken(self::getQuotedString($string));
}
}
/**
* @param string $string
*
* @return bool
*/
protected static function isQuotedString($string)
{
return !empty($string[0]) && ($string[0] === '"' || $string[0] === '\'' || $string[0] === '`' || $string[0] === '[');
}
/**
* @param string $string
*
* @return array
*/
protected static function getQuotedString($string)
{
$tokenType = Tokenizer::TOKEN_TYPE_QUOTE;
if (!empty($string[0]) && ($string[0] === '`' || $string[0] === '[')) {
$tokenType = Tokenizer::TOKEN_TYPE_BACK_TICK_QUOTE;
}
return [
Tokenizer::TOKEN_TYPE => $tokenType,
Tokenizer::TOKEN_VALUE => self::wrapStringWithQuotes($string),
];
}
/**
* This checks for the following patterns:
* 1. backtick quoted string using `` to escape
* 2. square bracket quoted string (SQL Server) using ]] to escape
* 3. double quoted string using "" or \" to escape
* 4. single quoted string using '' or \' to escape.
*
* @param string $string
*
* @return null
*/
public static function wrapStringWithQuotes($string)
{
$returnString = null;
$regex = '/^(((`[^`]*($|`))+)|((\[[^\]]*($|\]))(\][^\]]*($|\]))*)|'.
'(("[^"\\\\]*(?:\\\\.[^"\\\\]*)*("|$))+)|((\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*(\'|$))+))/s';
if (1 == \preg_match($regex, $string, $matches)) {
$returnString = $matches[1];
}
return $returnString;
}
}

View File

@ -0,0 +1,116 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/23/14
* Time: 1:18 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class Reserved.
*/
final class Reserved
{
/**
* @var array
*/
protected static $regex = [
Tokenizer::TOKEN_TYPE_RESERVED_TOP_LEVEL => 'getRegexReservedTopLevel',
Tokenizer::TOKEN_TYPE_RESERVED_NEWLINE => 'getRegexReservedNewLine',
Tokenizer::TOKEN_TYPE_RESERVED => 'getRegexReserved',
];
/**
* @param Tokenizer $tokenizer
* @param string $string
* @param array|null $previous
*
* @return array
*/
public static function isReserved(Tokenizer $tokenizer, $string, $previous)
{
$tokenData = [];
if (!$tokenizer->getNextToken() && self::isReservedPrecededByDotCharacter($previous)) {
$upperCase = \strtoupper($string);
self::getReservedString($tokenData, Tokenizer::TOKEN_TYPE_RESERVED_TOP_LEVEL, $string, $tokenizer);
self::getReservedString($tokenData, Tokenizer::TOKEN_TYPE_RESERVED_NEWLINE, $upperCase, $tokenizer);
self::getReservedString($tokenData, Tokenizer::TOKEN_TYPE_RESERVED, $string, $tokenizer);
$tokenizer->setNextToken($tokenData);
}
}
/**
* A reserved word cannot be preceded by a "." in order to differentiate "mytable.from" from the token "from".
*
* @param $previous
*
* @return bool
*/
protected static function isReservedPrecededByDotCharacter($previous)
{
return !$previous || !isset($previous[Tokenizer::TOKEN_VALUE]) || $previous[Tokenizer::TOKEN_VALUE] !== '.';
}
/**
* @param array $tokenData
* @param $type
* @param string $string
* @param Tokenizer $tokenizer
*/
protected static function getReservedString(array &$tokenData, $type, $string, Tokenizer $tokenizer)
{
$matches = [];
$method = self::$regex[$type];
if (empty($tokenData) && self::isReservedString(
$string,
$matches,
$tokenizer->$method(),
$tokenizer->getRegexBoundaries()
)
) {
$tokenData = self::getStringTypeArray($type, $string, $matches);
}
}
/**
* @param string $upper
* @param array $matches
* @param string $regexReserved
* @param string $regexBoundaries
*
* @return bool
*/
protected static function isReservedString($upper, array &$matches, $regexReserved, $regexBoundaries)
{
return 1 == \preg_match(
'/^('.$regexReserved.')($|\s|'.$regexBoundaries.')/',
\strtoupper($upper),
$matches
);
}
/**
* @param string $type
* @param string $string
* @param array $matches
*
* @return array
*/
protected static function getStringTypeArray($type, $string, array &$matches)
{
return [
Tokenizer::TOKEN_TYPE => $type,
Tokenizer::TOKEN_VALUE => \substr($string, 0, \strlen($matches[1])),
];
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/23/14
* Time: 1:26 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class UserDefined.
*/
final class UserDefined
{
/**
* @param Tokenizer $tokenizer
* @param string $string
*
* @return array
*/
public static function isUserDefinedVariable(Tokenizer $tokenizer, $string)
{
if (!$tokenizer->getNextToken() && self::isUserDefinedVariableString($string)) {
$tokenizer->setNextToken(self::getUserDefinedVariableString($string));
}
}
/**
* @param string $string
*
* @return bool
*/
protected static function isUserDefinedVariableString(&$string)
{
return !empty($string[0]) && !empty($string[1]) && ($string[0] === '@' && isset($string[1]));
}
/**
* Gets the user defined variables for in quoted or non-quoted fashion.
*
* @param string $string
*
* @return array
*/
protected static function getUserDefinedVariableString(&$string)
{
$returnData = [
Tokenizer::TOKEN_VALUE => null,
Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_VARIABLE,
];
self::setTokenValueStartingWithAtSymbolAndWrapped($returnData, $string);
self::setTokenValueStartingWithAtSymbol($returnData, $string);
return $returnData;
}
/**
* @param array $returnData
* @param string $string
*/
protected static function setTokenValueStartingWithAtSymbolAndWrapped(array &$returnData, $string)
{
if (!empty($string[1]) && ($string[1] === '"' || $string[1] === '\'' || $string[1] === '`')) {
$returnData[Tokenizer::TOKEN_VALUE] = '@'.Quoted::wrapStringWithQuotes(\substr($string, 1));
}
}
/**
* @param array $returnData
* @param string $string
*/
protected static function setTokenValueStartingWithAtSymbol(array &$returnData, $string)
{
if (null === $returnData[Tokenizer::TOKEN_VALUE]) {
$matches = [];
\preg_match('/^(@[a-zA-Z0-9\._\$]+)/', $string, $matches);
if ($matches) {
$returnData[Tokenizer::TOKEN_VALUE] = $matches[1];
}
}
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 12/23/14
* Time: 1:19 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Tokenizer\Parser;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class WhiteSpace.
*/
final class WhiteSpace
{
/**
* @param Tokenizer $tokenizer
* @param string $string
* @param array $matches
*/
public static function isWhiteSpace(Tokenizer $tokenizer, $string, array &$matches)
{
if (self::isWhiteSpaceString($string, $matches)) {
$tokenizer->setNextToken(self::getWhiteSpaceString($matches));
}
}
/**
* @param string $string
* @param array $matches
*
* @return bool
*/
public static function isWhiteSpaceString($string, array &$matches)
{
return (1 == \preg_match('/^\s+/', $string, $matches));
}
/**
* @param array $matches
*
* @return array
*/
public static function getWhiteSpaceString(array &$matches)
{
return [
Tokenizer::TOKEN_VALUE => $matches[0],
Tokenizer::TOKEN_TYPE => Tokenizer::TOKEN_TYPE_WHITESPACE,
];
}
}

View File

@ -0,0 +1,350 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 6/26/14
* Time: 12:10 AM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Sql\QueryFormatter\Tokenizer;
use NilPortugues\Sql\QueryFormatter\Helper\Token;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Boundary;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Comment;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Numeral;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Quoted;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\Reserved;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\LiteralString;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\UserDefined;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Parser\WhiteSpace;
/**
* Class Tokenizer.
*/
class Tokenizer
{
const TOKEN_TYPE_WHITESPACE = 0;
const TOKEN_TYPE_WORD = 1;
const TOKEN_TYPE_QUOTE = 2;
const TOKEN_TYPE_BACK_TICK_QUOTE = 3;
const TOKEN_TYPE_RESERVED = 4;
const TOKEN_TYPE_RESERVED_TOP_LEVEL = 5;
const TOKEN_TYPE_RESERVED_NEWLINE = 6;
const TOKEN_TYPE_BOUNDARY = 7;
const TOKEN_TYPE_COMMENT = 8;
const TOKEN_TYPE_BLOCK_COMMENT = 9;
const TOKEN_TYPE_NUMBER = 10;
const TOKEN_TYPE_ERROR = 11;
const TOKEN_TYPE_VARIABLE = 12;
const TOKEN_TYPE = 0;
const TOKEN_VALUE = 1;
/**
* @var string
*/
protected $regexBoundaries;
/**
* @var string
*/
protected $regexReserved;
/**
* @var string
*/
protected $regexReservedNewLine;
/**
* @var string
*/
protected $regexReservedTopLevel;
/**
* @var string
*/
protected $regexFunction;
/**
* @var int
*/
protected $maxCacheKeySize = 15;
/**
* @var array
*/
protected $tokenCache = [];
/**
* @var array
*/
protected $nextToken = [];
/**
* @var int
*/
protected $currentStringLength = 0;
/**
* @var int
*/
protected $oldStringLength = 0;
/**
* @var string
*/
protected $previousToken = '';
/**
* @var int
*/
protected $tokenLength = 0;
/**
* @var array
*/
protected $tokens = [];
/**
* Builds all the regular expressions needed to Tokenize the input.
*/
public function __construct()
{
$reservedMap = \array_combine(Token::$reserved, \array_map('strlen', Token::$reserved));
\arsort($reservedMap);
Token::$reserved = \array_keys($reservedMap);
$this->regexFunction = $this->initRegex(Token::$functions);
$this->regexBoundaries = $this->initRegex(Token::$boundaries);
$this->regexReserved = $this->initRegex(Token::$reserved);
$this->regexReservedTopLevel = \str_replace(' ', '\\s+', $this->initRegex(Token::$reservedTopLevel));
$this->regexReservedNewLine = \str_replace(' ', '\\s+', $this->initRegex(Token::$reservedNewLine));
}
/**
* @param $variable
*
* @return string
*/
protected function initRegex($variable)
{
return '('.implode('|', \array_map(array($this, 'quoteRegex'), $variable)).')';
}
/**
* Takes a SQL string and breaks it into tokens.
* Each token is an associative array with type and value.
*
* @param string $string
*
* @return array
*/
public function tokenize($string)
{
return (\strlen($string) > 0) ? $this->processTokens($string) : [];
}
/**
* @param string $string
*
* @return array
*/
protected function processTokens($string)
{
$this->tokens = [];
$this->previousToken = '';
$this->currentStringLength = \strlen($string);
$this->oldStringLength = \strlen($string) + 1;
while ($this->currentStringLength >= 0) {
if ($this->oldStringLength <= $this->currentStringLength) {
break;
}
$string = $this->processOneToken($string);
}
return $this->tokens;
}
/**
* @param string $string
*
* @return string
*/
protected function processOneToken($string)
{
$token = $this->getToken($string, $this->currentStringLength, $this->previousToken);
$this->tokens[] = $token;
$this->tokenLength = \strlen($token[self::TOKEN_VALUE]);
$this->previousToken = $token;
$this->oldStringLength = $this->currentStringLength;
$this->currentStringLength -= $this->tokenLength;
return \substr($string, $this->tokenLength);
}
/**
* @param string $string
* @param int $currentStringLength
* @param string string
*
* @return array|mixed
*/
protected function getToken($string, $currentStringLength, $previousToken)
{
$cacheKey = $this->useTokenCache($string, $currentStringLength);
if (!empty($cacheKey) && isset($this->tokenCache[$cacheKey])) {
return $this->getNextTokenFromCache($cacheKey);
}
return $this->getNextTokenFromString($string, $previousToken, $cacheKey);
}
/**
* @param string $string
* @param int $currentStringLength
*
* @return string
*/
protected function useTokenCache($string, $currentStringLength)
{
$cacheKey = '';
if ($currentStringLength >= $this->maxCacheKeySize) {
$cacheKey = \substr($string, 0, $this->maxCacheKeySize);
}
return $cacheKey;
}
/**
* @param string $cacheKey
*
* @return mixed
*/
protected function getNextTokenFromCache($cacheKey)
{
return $this->tokenCache[$cacheKey];
}
/**
* Get the next token and the token type and store it in cache.
*
* @param string $string
* @param string $token
* @param string $cacheKey
*
* @return array
*/
protected function getNextTokenFromString($string, $token, $cacheKey)
{
$token = $this->parseNextToken($string, $token);
if ($cacheKey && \strlen($token[self::TOKEN_VALUE]) < $this->maxCacheKeySize) {
$this->tokenCache[$cacheKey] = $token;
}
return $token;
}
/**
* Return the next token and token type in a SQL string.
* Quoted strings, comments, reserved words, whitespace, and punctuation are all their own tokens.
*
* @param string $string The SQL string
* @param array $previous The result of the previous parseNextToken() call
*
* @return array An associative array containing the type and value of the token.
*/
protected function parseNextToken($string, $previous = null)
{
$matches = [];
$this->nextToken = [];
WhiteSpace::isWhiteSpace($this, $string, $matches);
Comment::isComment($this, $string);
Quoted::isQuoted($this, $string);
UserDefined::isUserDefinedVariable($this, $string);
Numeral::isNumeral($this, $string, $matches);
Boundary::isBoundary($this, $string, $matches);
Reserved::isReserved($this, $string, $previous);
LiteralString::isFunction($this, $string, $matches);
LiteralString::getNonReservedString($this, $string, $matches);
return $this->nextToken;
}
/**
* @return array
*/
public function getNextToken()
{
return $this->nextToken;
}
/**
* @param array $nextToken
*
* @return $this
*/
public function setNextToken($nextToken)
{
$this->nextToken = $nextToken;
return $this;
}
/**
* @return string
*/
public function getRegexBoundaries()
{
return $this->regexBoundaries;
}
/**
* @return string
*/
public function getRegexFunction()
{
return $this->regexFunction;
}
/**
* @return string
*/
public function getRegexReserved()
{
return $this->regexReserved;
}
/**
* @return string
*/
public function getRegexReservedNewLine()
{
return $this->regexReservedNewLine;
}
/**
* @return string
*/
public function getRegexReservedTopLevel()
{
return $this->regexReservedTopLevel;
}
/**
* Helper function for building regular expressions for reserved words and boundary characters.
*
* @param string $string
*
* @return string
*/
protected function quoteRegex($string)
{
return \preg_quote($string, '/');
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 6/26/14
* Time: 9:11 PM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Tests\Sql\QueryFormatter;
use NilPortugues\Sql\QueryFormatter\Formatter;
/**
* Class FormatterTest.
*/
class FormatterTest extends \PHPUnit_Framework_TestCase
{
/**
* @var string
*/
private $querySeparator = "----------SEPARATOR----------\n";
/**
* @var string
*/
private $expectedResultFilePath = '/Resources/expectedQueries.sql';
/**
* @return array
*/
private function readExpectedQueryFile()
{
$expectedQueryArray = \explode(
$this->querySeparator,
\file_get_contents(\realpath(\dirname(__FILE__)).$this->expectedResultFilePath)
);
$expectedQueryArray = \array_filter($expectedQueryArray);
return $expectedQueryArray;
}
/**
* Data provider reading the test Queries.
*/
public function sqlQueryDataProvider()
{
$expectedQueryArray = $this->readExpectedQueryFile();
$queryTestSet = array();
foreach ($expectedQueryArray as $expectedQuery) {
$queryTestSet[] = array(\preg_replace('/\\s+/', ' ', $expectedQuery), $expectedQuery);
}
return $queryTestSet;
}
/**
* @test
* @dataProvider sqlQueryDataProvider
*
* @param $notIndented
* @param $indented
*/
public function itShouldReformatNoIndentQueriesToIndentedVersions($notIndented, $indented)
{
$formatter = new Formatter();
$result = $formatter->format($notIndented);
$this->assertSame($indented, $result);
}
}

View File

@ -0,0 +1,808 @@
SELECT
customer_id,
customer_name,
COUNT(order_id) AS total
FROM
customers
INNER JOIN orders ON customers.customer_id = orders.customer_id
GROUP BY
customer_id,
customer_name
HAVING
COUNT(order_id) > 5
ORDER BY
COUNT(order_id) DESC;
----------SEPARATOR----------
UPDATE
customers
SET
totalorders = ordersummary.total
FROM
(
SELECT
customer_id,
count(order_id) As total
FROM
orders
GROUP BY
customer_id
) As ordersummary
WHERE
customers.customer_id = ordersummary.customer_id
----------SEPARATOR----------
SELECT
*
FROM
sometable
UNION ALL
SELECT
*
FROM
someothertable;
----------SEPARATOR----------
SET
NAMES 'utf8';
----------SEPARATOR----------
CREATE TABLE `PREFIX_address` (
`id_address` int(10) unsigned NOT NULL auto_increment,
`id_country` int(10) unsigned NOT NULL,
`id_state` int(10) unsigned default NULL,
`id_customer` int(10) unsigned NOT NULL default '0',
`id_manufacturer` int(10) unsigned NOT NULL default '0',
`id_supplier` int(10) unsigned NOT NULL default '0',
`id_warehouse` int(10) unsigned NOT NULL default '0',
`alias` varchar(32) NOT NULL,
`company` varchar(64) default NULL,
`lastname` varchar(32) NOT NULL,
`firstname` varchar(32) NOT NULL,
`address1` varchar(128) NOT NULL,
`address2` varchar(128) default NULL,
`postcode` varchar(12) default NULL,
`city` varchar(64) NOT NULL,
`other` text,
`phone` varchar(16) default NULL,
`phone_mobile` varchar(16) default NULL,
`vat_number` varchar(32) default NULL,
`dni` varchar(16) DEFAULT NULL,
`date_add` datetime NOT NULL,
`date_upd` datetime NOT NULL,
`active` tinyint(1) unsigned NOT NULL default '1',
`deleted` tinyint(1) unsigned NOT NULL default '0',
PRIMARY KEY (`id_address`),
KEY `address_customer` (`id_customer`),
KEY `id_country` (`id_country`),
KEY `id_state` (`id_state`),
KEY `id_manufacturer` (`id_manufacturer`),
KEY `id_supplier` (`id_supplier`),
KEY `id_warehouse` (`id_warehouse`)
) ENGINE = ENGINE_TYPE DEFAULT CHARSET = utf8
----------SEPARATOR----------
CREATE TABLE `PREFIX_alias` (
`id_alias` int(10) unsigned NOT NULL auto_increment,
`alias` varchar(255) NOT NULL,
`search` varchar(255) NOT NULL,
`active` tinyint(1) NOT NULL default '1',
PRIMARY KEY (`id_alias`),
UNIQUE KEY `alias` (`alias`)
) ENGINE = ENGINE_TYPE DEFAULT CHARSET = utf8
----------SEPARATOR----------
CREATE TABLE `PREFIX_carrier` (
`id_carrier` int(10) unsigned NOT NULL AUTO_INCREMENT,
`id_reference` int(10) unsigned NOT NULL,
`id_tax_rules_group` int(10) unsigned DEFAULT '0',
`name` varchar(64) NOT NULL,
`url` varchar(255) DEFAULT NULL,
`active` tinyint(1) unsigned NOT NULL DEFAULT '0',
`deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
`shipping_handling` tinyint(1) unsigned NOT NULL DEFAULT '1',
`range_behavior` tinyint(1) unsigned NOT NULL DEFAULT '0',
`is_module` tinyint(1) unsigned NOT NULL DEFAULT '0',
`is_free` tinyint(1) unsigned NOT NULL DEFAULT '0',
`shipping_external` tinyint(1) unsigned NOT NULL DEFAULT '0',
`need_range` tinyint(1) unsigned NOT NULL DEFAULT '0',
`external_module_name` varchar(64) DEFAULT NULL,
`shipping_method` int(2) NOT NULL DEFAULT '0',
`position` int(10) unsigned NOT NULL default '0',
`max_width` int(10) DEFAULT 0,
`max_height` int(10) DEFAULT 0,
`max_depth` int(10) DEFAULT 0,
`max_weight` int(10) DEFAULT 0,
`grade` int(10) DEFAULT 0,
PRIMARY KEY (`id_carrier`),
KEY `deleted` (`deleted`, `active`),
KEY `id_tax_rules_group` (`id_tax_rules_group`)
) ENGINE = ENGINE_TYPE DEFAULT CHARSET = utf8
----------SEPARATOR----------
CREATE TABLE IF NOT EXISTS `PREFIX_specific_price_rule` (
`id_specific_price_rule` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`id_shop` int(11) unsigned NOT NULL DEFAULT '1',
`id_currency` int(10) unsigned NOT NULL,
`id_country` int(10) unsigned NOT NULL,
`id_group` int(10) unsigned NOT NULL,
`from_quantity` mediumint(8) unsigned NOT NULL,
`price` DECIMAL(20, 6),
`reduction` decimal(20, 6) NOT NULL,
`reduction_type` enum('amount', 'percentage') NOT NULL,
`from` datetime NOT NULL,
`to` datetime NOT NULL,
PRIMARY KEY (`id_specific_price_rule`),
KEY `id_product` (
`id_shop`, `id_currency`, `id_country`,
`id_group`, `from_quantity`, `from`,
`to`
)
) ENGINE = ENGINE_TYPE DEFAULT CHARSET = utf8
----------SEPARATOR----------
UPDATE
`PREFIX_configuration`
SET
value = '6'
WHERE
name = 'PS_SEARCH_WEIGHT_PNAME'
----------SEPARATOR----------
UPDATE
`PREFIX_hook_module`
SET
position = 1
WHERE
id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayPayment'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'cheque'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayPaymentReturn'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'cheque'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayHome'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'homeslider'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'actionAuthentication'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'statsdata'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'actionShopDataDuplication'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'homeslider'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayTop'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'blocklanguages'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'actionCustomerAccountAdd'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'statsdata'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayCustomerAccount'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'favoriteproducts'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayAdminStatsModules'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'statsvisits'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayAdminStatsGraphEngine'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'graphvisifire'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayAdminStatsGridEngine'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'gridhtml'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayLeftColumnProduct'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'blocksharefb'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'actionSearch'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'statssearch'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'actionCategoryAdd'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'blockcategories'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'actionCategoryUpdate'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'blockcategories'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'actionCategoryDelete'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'blockcategories'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'actionAdminMetaSave'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'blockcategories'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayMyAccountBlock'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'favoriteproducts'
)
OR id_hook = (
SELECT
id_hook
FROM
`PREFIX_hook`
WHERE
name = 'displayFooter'
)
AND id_module = (
SELECT
id_module
FROM
`PREFIX_module`
WHERE
name = 'blockreinsurance'
)
----------SEPARATOR----------
ALTER TABLE
`PREFIX_employee`
ADD
`bo_color` varchar(32) default NULL
AFTER
`stats_date_to`
----------SEPARATOR----------
INSERT INTO `PREFIX_cms_category_lang`
VALUES
(
1, 3, 'Inicio', '', 'home', NULL, NULL,
NULL
)
----------SEPARATOR----------
INSERT INTO `PREFIX_cms_category`
VALUES
(1, 0, 0, 1, NOW(), NOW(), 0)
----------SEPARATOR----------
UPDATE
`PREFIX_cms_category`
SET
`position` = 0
----------SEPARATOR----------
ALTER TABLE
`PREFIX_customer`
ADD
`note` text
AFTER
`secure_key`
----------SEPARATOR----------
ALTER TABLE
`PREFIX_contact`
ADD
`customer_service` tinyint(1) NOT NULL DEFAULT 0
AFTER
`email`
----------SEPARATOR----------
INSERT INTO `PREFIX_specific_price` (
`id_product`, `id_shop`, `id_currency`,
`id_country`, `id_group`, `priority`,
`price`, `from_quantity`, `reduction`,
`reduction_type`, `from`, `to`
) (
SELECT
dq.`id_product`,
1,
1,
0,
1,
0,
0.00,
dq.`quantity`,
IF(
dq.`id_discount_type` = 2, dq.`value`,
dq.`value` / 100
),
IF (
dq.`id_discount_type` = 2, 'amount',
'percentage'
),
'0000-00-00 00:00:00',
'0000-00-00 00:00:00'
FROM
`PREFIX_discount_quantity` dq
INNER JOIN `PREFIX_product` p ON (p.`id_product` = dq.`id_product`)
)
----------SEPARATOR----------
DROP
TABLE `PREFIX_discount_quantity`
----------SEPARATOR----------
INSERT INTO `PREFIX_specific_price` (
`id_product`, `id_shop`, `id_currency`,
`id_country`, `id_group`, `priority`,
`price`, `from_quantity`, `reduction`,
`reduction_type`, `from`, `to`
) (
SELECT
p.`id_product`,
1,
0,
0,
0,
0,
0.00,
1,
IF(
p.`reduction_price` > 0, p.`reduction_price`,
p.`reduction_percent` / 100
),
IF(
p.`reduction_price` > 0, 'amount',
'percentage'
),
IF (
p.`reduction_from` = p.`reduction_to`,
'0000-00-00 00:00:00', p.`reduction_from`
),
IF (
p.`reduction_from` = p.`reduction_to`,
'0000-00-00 00:00:00', p.`reduction_to`
)
FROM
`PREFIX_product` p
WHERE
p.`reduction_price`
OR p.`reduction_percent`
)
----------SEPARATOR----------
ALTER TABLE
`PREFIX_product`
DROP
`reduction_price`,
DROP
`reduction_percent`,
DROP
`reduction_from`,
DROP
`reduction_to`
----------SEPARATOR----------
INSERT INTO `PREFIX_configuration` (
`name`, `value`, `date_add`, `date_upd`
)
VALUES
(
'PS_SPECIFIC_PRICE_PRIORITIES',
'id_shop;id_currency;id_country;id_group',
NOW(), NOW()
),
('PS_TAX_DISPLAY', 0, NOW(), NOW()),
(
'PS_SMARTY_FORCE_COMPILE', 1, NOW(),
NOW()
),
(
'PS_DISTANCE_UNIT', 'km', NOW(), NOW()
),
(
'PS_STORES_DISPLAY_CMS', 0, NOW(),
NOW()
),
(
'PS_STORES_DISPLAY_FOOTER', 0, NOW(),
NOW()
),
(
'PS_STORES_SIMPLIFIED', 0, NOW(),
NOW()
),
(
'PS_STATSDATA_CUSTOMER_PAGESVIEWS',
1, NOW(), NOW()
),
(
'PS_STATSDATA_PAGESVIEWS', 1, NOW(),
NOW()
),
(
'PS_STATSDATA_PLUGINS', 1, NOW(),
NOW()
)
----------SEPARATOR----------
INSERT INTO `PREFIX_configuration` (
`name`, `value`, `date_add`, `date_upd`
)
VALUES
(
'PS_CONDITIONS_CMS_ID',
IFNULL(
(
SELECT
`id_cms`
FROM
`PREFIX_cms`
WHERE
`id_cms` = 3
),
0
),
NOW(),
NOW()
)
----------SEPARATOR----------
CREATE TEMPORARY TABLE `PREFIX_configuration_tmp` (`value` text)
----------SEPARATOR----------
SET
@defaultOOS = (
SELECT
value
FROM
`PREFIX_configuration`
WHERE
name = 'PS_ORDER_OUT_OF_STOCK'
)
----------SEPARATOR----------
UPDATE
`PREFIX_product` p
SET
`cache_default_attribute` = 0
WHERE
`id_product` NOT IN (
SELECT
`id_product`
FROM
`PREFIX_product_attribute`
)
----------SEPARATOR----------
INSERT INTO `PREFIX_hook` (
`name`, `title`, `description`, `position`
)
VALUES
(
'processCarrier', 'Carrier Process',
NULL, 0
)
----------SEPARATOR----------
INSERT INTO `PREFIX_stock_mvt_reason_lang` (
`id_stock_mvt_reason`, `id_lang`,
`name`
)
VALUES
(1, 1, 'Order'),
(1, 2, 'Commande'),
(2, 1, 'Missing Stock Movement'),
(
2, 2, 'Mouvement de stock manquant'
),
(3, 1, 'Restocking'),
(3, 2, 'Réssort')
----------SEPARATOR----------
INSERT INTO `PREFIX_meta_lang` (
`id_lang`, `id_meta`, `title`, `url_rewrite`
)
VALUES
(
1,
(
SELECT
`id_meta`
FROM
`PREFIX_meta`
WHERE
`page` = 'authentication'
),
'Authentication',
'authentication'
),
(
2,
(
SELECT
`id_meta`
FROM
`PREFIX_meta`
WHERE
`page` = 'authentication'
),
'Authentification',
'authentification'
),
(
3,
(
SELECT
`id_meta`
FROM
`PREFIX_meta`
WHERE
`page` = 'authentication'
),
'Autenticación',
'autenticacion'
)
----------SEPARATOR----------
LOCK TABLES `admin_assert` WRITE
----------SEPARATOR----------
UNLOCK TABLES
----------SEPARATOR----------
DROP
TABLE IF EXISTS `admin_role`
----------SEPARATOR----------
SELECT
-- This is a test
----------SEPARATOR----------
SELECT
*
LIMIT
1;
SELECT
a,
b,
c,
d
FROM
e
LIMIT
1, 2;
SELECT
1,
2,
3
WHERE
a in (1, 2, 3, 4, 5)
AND b = 5;
----------SEPARATOR----------
SELECT
count - 50
WHERE
a - 50 = b
WHERE
1
AND -50
WHERE
-50 = a
WHERE
a = -50
WHERE
1
/*test*/
-50
WHERE
1
AND -50;
----------SEPARATOR----------
SELECT
@"weird variable name";
----------SEPARATOR----------
SELECT
"no closing quote
----------SEPARATOR----------
SELECT
[sqlserver]
FROM
[escap[e]]d style];
----------SEPARATOR----------
SELECT
count(*) AS all_columns,
`Column1`,
`Testing`,
`Testing Three`
FROM
`Table1`
WHERE
(Column1 = :v1)
AND (`Column2` = `Column3`)
AND (
`Column2` = `Column3`
OR Column4 >= NOW()
)
GROUP BY
Column1
ORDER BY
Column3 DESC
LIMIT
:v2,
:v3

View File

@ -0,0 +1,453 @@
<?php
/**
* Author: Nil Portugués Calderó <contact@nilportugues.com>
* Date: 6/26/14
* Time: 2:19 AM.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace NilPortugues\Tests\Sql\QueryFormatter\Tokenizer;
use NilPortugues\Sql\QueryFormatter\Tokenizer\Tokenizer;
/**
* Class TokenizerTest.
*/
class TokenizerTest extends \PHPUnit_Framework_TestCase
{
/**
* @var Tokenizer
*/
private $tokenizer;
/**
*
*/
protected function setUp()
{
$this->tokenizer = new Tokenizer();
}
/**
*
*/
protected function tearDown()
{
$this->tokenizer = null;
}
/**
* @test
*/
public function itShouldTokenizeWhiteSpace()
{
$sql = <<<SQL
SELECT * FROM users;
SQL;
$result = $this->tokenizer->tokenize($sql);
$whiteSpacesFound = false;
foreach ($result as $token) {
if (' ' === $token[Tokenizer::TOKEN_VALUE]) {
$whiteSpacesFound = true;
break;
}
}
$this->assertTrue($whiteSpacesFound);
}
/**
* Comment starting with # will be treated as tokens representing the whole comment.
*
* @test
*/
public function itShouldTokenizeCommentStartingWithSymbolNumber()
{
$sql = <<<SQL
SELECT * FROM users # A comment
WHERE user_id = 2;
SQL;
$result = $this->tokenizer->tokenize($sql);
$commentFound = false;
foreach ($result as $token) {
if ('# A comment' === $token[Tokenizer::TOKEN_VALUE]) {
$commentFound = true;
break;
}
}
$this->assertTrue($commentFound);
}
/**
* Comment starting with -- will be treated as tokens representing the whole comment.
*
* @test
*/
public function itShouldTokenizeCommentStartingWithDash()
{
$sql = <<<SQL
SELECT * FROM
-- This is another comment
users
SQL;
$result = $this->tokenizer->tokenize($sql);
$commentFound = false;
foreach ($result as $token) {
if ('-- This is another comment' === $token[Tokenizer::TOKEN_VALUE]) {
$commentFound = true;
break;
}
}
$this->assertTrue($commentFound);
}
/**
* Comment blocks in SQL Server fashion will be treated as tokens representing the whole comment.
*
* @test
*/
public function itShouldTokenizeCommentWithBlockComment()
{
$sql = <<<SQL
SELECT * FROM /* This is a block comment */ WHERE 1 = 2;
SQL;
$result = $this->tokenizer->tokenize($sql);
$commentFound = false;
foreach ($result as $token) {
if ('/* This is a block comment */' === $token[Tokenizer::TOKEN_VALUE]) {
$commentFound = true;
break;
}
}
$this->assertTrue($commentFound);
}
/**
* Unterminated comment block will be processed incorrectly by the tokenizer,
* yet we should be able to detect this error in order to handle the resulting situation.
*
* @test
*/
public function itShouldTokenizeCommentWithUnterminatedBlockComment()
{
$sql = <<<SQL
SELECT * FROM /* This is a block comment WHERE 1 = 2;
SQL;
$result = $this->tokenizer->tokenize($sql);
$commentStartFound = false;
foreach ($result as $token) {
if ('/*' === $token[Tokenizer::TOKEN_VALUE]) {
$commentStartFound = true;
break;
}
}
$this->assertTrue($commentStartFound);
}
/**
* @test
*/
public function itShouldTokenizeQuoted()
{
$sql = <<<SQL
UPDATE `PREFIX_cms_category` SET `position` = 0
SQL;
$result = $this->tokenizer->tokenize($sql);
$quotedFound = false;
foreach ($result as $token) {
if ('`position`' === $token[Tokenizer::TOKEN_VALUE]) {
$quotedFound = true;
break;
}
}
$this->assertTrue($quotedFound);
}
/**
* SQL Server syntax for defining custom variables.
*
* @test
*/
public function itShouldTokenizeUserDefinedVariableWithQuotations()
{
$sql = <<<SQL
SELECT au_lname, au_fname, phone FROM authors WHERE au_lname LIKE @find;
SQL;
$result = $this->tokenizer->tokenize($sql);
$quotedFound = false;
foreach ($result as $token) {
if ('@find' === $token[Tokenizer::TOKEN_VALUE]) {
$quotedFound = true;
break;
}
}
$this->assertTrue($quotedFound);
}
/**
* This validates Microsoft SQL Server syntax for user variables.
*
* @test
*/
public function itShouldTokenizeUserDefinedVariableWithAtSymbol()
{
$sql = <<<SQL
SELECT @"weird variable name";
SQL;
$result = $this->tokenizer->tokenize($sql);
$this->assertNotEmpty($result);
}
/**
* This test is an edge case.
*
* Given the provided statement, will loop forever if no condition is checking total amount
* of the input string processed. This will happen because the tokenizer has no matching case
* and string processing will not progress.
*
* @test
*/
public function itShouldTokenizeUserDefinedVariableNoProgressTokenizer()
{
$sql = <<<SQL
SELECT @ and b; /* Edge case */
SQL;
$result = $this->tokenizer->tokenize($sql);
$userVariableNotFound = true;
foreach ($result as $token) {
if ('@' === $token[Tokenizer::TOKEN_VALUE]) {
$userVariableNotFound = false;
break;
}
}
$this->assertTrue($userVariableNotFound);
}
/**
* @test
*/
public function itShouldTokenizeNumeralInteger()
{
$sql = <<<SQL
SELECT user_id FROM user WHERE user_id = 1;
SQL;
$result = $this->tokenizer->tokenize($sql);
$numeralIntegerFound = false;
foreach ($result as $token) {
if ('1' == $token[Tokenizer::TOKEN_VALUE]) {
$numeralIntegerFound = true;
break;
}
}
$this->assertTrue($numeralIntegerFound);
}
/**
* @test
*/
public function itShouldTokenizeNumeralNegativeIntegerAsPositiveInteger()
{
$sql = <<<SQL
SELECT user_id FROM user WHERE user_id = -1;
SQL;
$result = $this->tokenizer->tokenize($sql);
$numeralIntegerFound = false;
foreach ($result as $token) {
if ('1' == $token[Tokenizer::TOKEN_VALUE]) {
$numeralIntegerFound = true;
break;
}
}
$this->assertTrue($numeralIntegerFound);
}
/**
* @test
*/
public function itShouldTokenizeNumeralFloat()
{
$sql = <<<SQL
SELECT user_id FROM user WHERE user_id = 3.14;
SQL;
$result = $this->tokenizer->tokenize($sql);
$numeralIntegerFound = false;
foreach ($result as $token) {
if ('3.14' == $token[Tokenizer::TOKEN_VALUE]) {
$numeralIntegerFound = true;
break;
}
}
$this->assertTrue($numeralIntegerFound);
}
/**
* @test
*/
public function itShouldTokenizeNumeralNegativeFloatAsPositiveFloat()
{
$sql = <<<SQL
SELECT user_id FROM user WHERE user_id = -3.14;
SQL;
$result = $this->tokenizer->tokenize($sql);
$numeralIntegerFound = false;
foreach ($result as $token) {
if ('3.14' == $token[Tokenizer::TOKEN_VALUE]) {
$numeralIntegerFound = true;
break;
}
}
$this->assertTrue($numeralIntegerFound);
}
/**
* Check if boundary characters are in array. Boundary characters are: ;:)(.=<>+-\/!^%|&#.
*
* @test
*/
public function itShouldTokenizeBoundaryCharacter()
{
$sql = 'SELECT id_user, name FROM users';
$result = $this->tokenizer->tokenize($sql);
$boundaryFound = false;
foreach ($result as $token) {
if (',' === $token[Tokenizer::TOKEN_VALUE]) {
$boundaryFound = true;
break;
}
}
$this->assertTrue($boundaryFound);
}
/**
* Tokenize columns should not be a problem, even if using a reserved word as a column name.
* Example: users.user_id
* Example of edge cases: users.desc, user.from.
*
* @test
*/
public function itShouldTokenizeReservedWordPrecededByDotCharacter()
{
$sql = <<<SQL
SELECT users.desc as userId FROM users;
SQL;
$result = $this->tokenizer->tokenize($sql);
$reservedTokenFound = false;
foreach ($result as $token) {
if ('desc' == $token[Tokenizer::TOKEN_VALUE]) {
$reservedTokenFound = true;
break;
}
}
$this->assertTrue($reservedTokenFound);
}
/**
* @test
*/
public function itShouldTokenizeReservedTopLevel()
{
$sql = 'SELECT id_user, name FROM users';
$result = $this->tokenizer->tokenize($sql);
$reservedTopLevelTokenFound = false;
foreach ($result as $token) {
if ('FROM' === $token[Tokenizer::TOKEN_VALUE]) {
$reservedTopLevelTokenFound = true;
break;
}
}
$this->assertTrue($reservedTopLevelTokenFound);
}
/**
* @test
*/
public function itShouldTokenizeReservedNewLine()
{
$sql = <<<SQL
UPDATE users SET registration_date = "0000-00-00" WHERE id_user = 1 OR id_user = 2;
SQL;
$result = $this->tokenizer->tokenize($sql);
$reservedNewLineTokenFound = false;
foreach ($result as $token) {
if ('OR' === $token[Tokenizer::TOKEN_VALUE]) {
$reservedNewLineTokenFound = true;
break;
}
}
$this->assertTrue($reservedNewLineTokenFound);
}
/**
* @test
*/
public function itShouldTokenizeReserved()
{
$sql = <<<SQL
SELECT customer_id, customer_name, COUNT(order_id) as total
FROM customers INNER JOIN orders ON customers.customer_id = orders.customer_id
GROUP BY customer_id, customer_name
HAVING COUNT(order_id) > 5
ORDER BY COUNT(order_id) DESC;
SQL;
$result = $this->tokenizer->tokenize($sql);
$reservedTokenFound = false;
foreach ($result as $token) {
if ('INNER JOIN' === $token[Tokenizer::TOKEN_VALUE]) {
$reservedTokenFound = true;
break;
}
}
$this->assertTrue($reservedTokenFound);
}
/**
* @test
*/
public function itShouldTokenizeFunction()
{
$sql = <<<SQL
SELECT customer_id, customer_name, COUNT(order_id) as total FROM customers GROUP BY customer_id, customer_name
HAVING COUNT(order_id) > 5 ORDER BY COUNT(order_id) DESC;
SQL;
$result = $this->tokenizer->tokenize($sql);
$functionFound = false;
foreach ($result as $token) {
if ('COUNT' === $token[Tokenizer::TOKEN_VALUE]) {
$functionFound = true;
break;
}
}
$this->assertTrue($functionFound);
}
}