Merge pull request #11337 from udan11/formatter

Implemented new formatter in the sql-parser library
This commit is contained in:
Marc Delisle 2015-08-01 07:29:10 -04:00
commit ba3d3fd8e4
17 changed files with 678 additions and 1477 deletions

View File

@ -6,13 +6,15 @@
* @package PhpMyAdmin
*/
/**
* Loading common files. Used to check for authorization, localization and to
* load the parsing library.
*/
require_once 'libraries/common.inc.php';
require_once 'libraries/sql-formatter/lib/SqlFormatter.php';
$query = isset($_POST['sql']) ? $_POST['sql'] : '';
$query = !empty($_POST['sql']) ? $_POST['sql'] : '';
SqlFormatter::$tab = "\t";
$query = SqlFormatter::format($query, false);
$query = SqlParser\Utils\Formatter::format($query);
$response = PMA_Response::getInstance();
$response->addJSON("sql", $query);

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 Jeremy Dorn <jeremy@jeremydorn.com>
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

@ -1,185 +0,0 @@
SqlFormatter
=============
A lightweight php class for formatting sql statements.
It can automatically indent and add line breaks in addition to syntax highlighting.
History
============
I found myself having to debug auto-generated SQL statements all the time and
wanted some way to easily output formatted HTML without having to include a
huge library or copy and paste into online formatters.
I was originally planning to extract the formatting code from PhpMyAdmin,
but that was 10,000+ lines of code and used global variables.
I saw that other people had the same problem and used Stack Overflow user
losif's answer as a starting point. http://stackoverflow.com/a/3924147
Usage
============
The SqlFormatter class has a static method 'format' which takes a SQL string
as input and returns a formatted HTML block inside a pre tag.
Sample usage:
```php
<?php
require_once('SqlFormatter.php');
$query = "SELECT count(*),`Column1`,`Testing`, `Testing Three` FROM `Table1`
WHERE Column1 = 'testing' AND ( (`Column2` = `Column3` OR Column4 >= NOW()) )
GROUP BY Column1 ORDER BY Column3 DESC LIMIT 5,10";
echo SqlFormatter::format($query);
```
Output:
![](http://jdorn.github.com/sql-formatter/format-highlight.png)
Formatting Only
-------------------------
If you don't want syntax highlighting and only want the indentations and
line breaks, pass in false as the second parameter.
This is useful for outputting to error logs or other non-html formats.
```php
<?php
echo SqlFormatter::format($query, false);
```
Output:
![](http://jdorn.github.com/sql-formatter/format.png)
Syntax Highlighting Only
-------------------------
There is a separate method 'highlight' that preserves all original whitespace
and just adds syntax highlighting.
This is useful for sql that is already well formatted and just needs to be a little
easier to read.
```php
<?php
echo SqlFormatter::highlight($query);
```
Output:
![](http://jdorn.github.com/sql-formatter/highlight.png)
Compress Query
--------------------------
The compress method removes all comments and compresses whitespace.
This is useful for outputting queries that can be copy pasted to the command line easily.
```
-- This is a comment
SELECT
/* This is another comment
On more than one line */
Id #This is one final comment
as temp, DateCreated as Created FROM MyTable;
```
```php
echo SqlFormatter::compress($query)
```
Output:
```
SELECT Id as temp, DateCreated as Created FROM MyTable;
```
Remove Comments
------------------------
If you want to keep all original whitespace formatting and just remove comments,
you can use the removeComments method instead of compress.
```
-- This is a comment
SELECT
/* This is another comment
On more than one line */
Id #This is one final comment
as temp, DateCreated as Created FROM MyTable;
```
```php
<?php
echo SqlFormatter::removeComments($query);
```
Output:
```
SELECT
Id
as temp, DateCreated as Created FROM MyTable;
```
Split SQL String into Queries
--------------------------
Another feature, which is unrelated to formatting, is the ability to break up a SQL string into multiple queries.
For Example:
```sql
DROP TABLE IF EXISTS MyTable;
CREATE TABLE MyTable ( id int );
INSERT INTO MyTable (id)
VALUES
(1),(2),(3),(4);
SELECT * FROM MyTable;
```
```php
<?php
$queries = SqlFormatter::splitQuery($sql);
```
Result:
1. `DROP TABLE IF EXISTS MyTable`;
2. `CREATE TABLE MyTable ( id int )`;
3. `INSERT INTO MyTable (id) VALUES (1),(2),(3),(4)`;
4. `SELECT * FROM MyTable`;
### Why Not Regular Expressions?
Why not just use `explode(';', $sql)` or a regular expression?
The following example sql and others like it are _impossible_ to split correctly using regular expressions, no matter how complex.
```
SELECT ";"; SELECT ";\"; a;";
SELECT ";
abc";
SELECT a,b #comment;
FROM test;
```
SqlFormatter breaks the string into tokens instead of using regular expressions and will correctly produce:
1. `SELECT ";"`;
2. `SELECT ";\"; a;"`;
3. `SELECT "; abc"`;
4. `SELECT a,b #comment;
FROM test`;
Please note, the splitQuery method will still fail in the following cases:
* The DELIMITER command can be used to change the delimiter from the default ';' to something else.
* The CREATE PROCEDURE command has a ';' in the middle of it
* The USE command is not terminated with a ';'

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,8 @@ namespace SqlParser {
* @param TokensList $list The list of tokens that are being parsed.
* @param array $options Parameters for parsing.
*
* @throws \Exception Not implemented yet.
*
* @return mixed
*/
public static function parse(
@ -69,6 +71,8 @@ namespace SqlParser {
*
* @param mixed $component The component to be built.
*
* @throws \Exception Not implemented yet.
*
* @return string
*/
public static function build($component)

View File

@ -9,9 +9,7 @@
namespace SqlParser\Components;
use SqlParser\Component;
use SqlParser\Parser;
use SqlParser\Token;
use SqlParser\TokensList;
use SqlParser\Statements\SelectStatement;
/**
* `UNION` keyword builder.

View File

@ -416,6 +416,10 @@ abstract class Context
*/
public static function load($context = '')
{
/**
* @var Context $context
*/
if (empty($context)) {
$context = self::$defaultContext;
}
@ -467,6 +471,7 @@ abstract class Context
);
continue;
}
// Last generated context was valid (did not throw any exceptions).
// So we return it, to let the user know what context was loaded.
return $context;

View File

@ -251,22 +251,31 @@ namespace SqlParser {
$this->str[$this->last],
$this->last
);
} elseif (($token->type === Token::TYPE_SYMBOL)
} elseif (($lastToken !== null)
&& ($token->type === Token::TYPE_SYMBOL)
&& ($token->flags & Token::FLAG_SYMBOL_VARIABLE)
&& ($lastToken !== null)
&& (($lastToken->type === Token::TYPE_STRING)
|| (($lastToken->type === Token::TYPE_SYMBOL)
&& ($lastToken->flags & Token::FLAG_SYMBOL_BACKTICK)))
) {
// Handles ```... FROM 'user'@'%' ...```.
if ((($lastToken->type === Token::TYPE_SYMBOL)
&& ($lastToken->flags & Token::FLAG_SYMBOL_BACKTICK))
|| ($lastToken->type === Token::TYPE_STRING)
) {
$lastToken->token .= $token->token;
$lastToken->type = Token::TYPE_SYMBOL;
$lastToken->flags = Token::FLAG_SYMBOL_USER;
$lastToken->value .= '@' . $token->value;
continue;
}
$lastToken->token .= $token->token;
$lastToken->type = Token::TYPE_SYMBOL;
$lastToken->flags = Token::FLAG_SYMBOL_USER;
$lastToken->value .= '@' . $token->value;
continue;
} elseif (($lastToken !== null)
&& ($token->type === Token::TYPE_KEYWORD)
&& ($lastToken->type === Token::TYPE_OPERATOR)
&& ($lastToken->value === '.')
) {
// Handles ```... tbl.FROM ...```. In this case, FROM is not
// a reserved word.
$token->type = Token::TYPE_NONE;
$token->flags = 0;
$token->value = $token->token;
}
$token->position = $lastIdx;
$list->tokens[$list->count++] = $token;
@ -306,6 +315,16 @@ namespace SqlParser {
while ((++$this->last < $this->len) && (!Context::isWhitespace($this->str[$this->last]))) {
$this->delimiter .= $this->str[$this->last];
}
if (empty($this->delimiter)) {
$this->error(
__('Expected delimiter.'),
'',
$this->last
);
$this->delimiter = ';';
}
--$this->last;
// Saving the delimiter and its token.
@ -598,7 +617,7 @@ namespace SqlParser {
} elseif (($this->last + 1 < $this->len)
&& ($this->str[$this->last] === '0')
&& (($this->str[$this->last + 1] === 'x')
|| ($this->str[$this->last + 1] === 'X'))
|| ($this->str[$this->last + 1] === 'X'))
) {
$token .= $this->str[$this->last++];
$state = 2;

View File

@ -51,7 +51,12 @@ namespace SqlParser {
*/
public static $STATEMENT_PARSERS = array(
// MySQL Utility Statements
'EXPLAIN' => 'SqlParser\\Statements\\ExplainStatement',
'DESCRIBE' => 'SqlParser\\Statements\\ExplainStatement',
'HELP' => '',
'USE' => '',
'STATUS' => '',
// Table Maintenance Statements
// https://dev.mysql.com/doc/refman/5.7/en/table-maintenance-sql.html
@ -65,7 +70,7 @@ namespace SqlParser {
// Database Administration Statements
// https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-server-administration.html
'SET' => '',
'SET' => 'SqlParser\\Statements\\SetStatement',
'SHOW' => 'SqlParser\\Statements\\ShowStatement',
// Data Definition Statements.
@ -327,7 +332,7 @@ namespace SqlParser {
/**
* Last transaction.
* @var TransactionStatement
* @var TransactionStatement $lastTransaction
*/
$lastTransaction = null;
@ -386,10 +391,15 @@ namespace SqlParser {
// Checking if it is a known statement that can be parsed.
if (empty(static::$STATEMENT_PARSERS[$token->value])) {
$this->error(
__('Unrecognized statement type.'),
$token
);
if (!isset(static::$STATEMENT_PARSERS[$token->value])) {
// A statement is considered recognized if the parser
// is aware that it is a statement, but it does not have
// a parser for it yet.
$this->error(
__('Unrecognized statement type.'),
$token
);
}
// Skipping to the end of this statement.
$list->getNextOfType(Token::TYPE_DELIMITER);
//
@ -425,6 +435,10 @@ namespace SqlParser {
&& ($lastStatement instanceof SelectStatement)
&& ($statement instanceof SelectStatement)
) {
/**
* This SELECT statement.
* @var SelectStatement $statement
*/
/**
* Last SELECT statement.
@ -449,6 +463,10 @@ namespace SqlParser {
// Handles transactions.
if ($statement instanceof TransactionStatement) {
/**
* @var TransactionStatement $statement
*/
if ($statement->type === TransactionStatement::TYPE_BEGIN) {
$lastTransaction = $statement;
$this->statements[] = $statement;

View File

@ -124,7 +124,7 @@ abstract class Statement
/**
* The builder (parser) of this clause.
* @var string $class
* @var Component $class
*/
$class = Parser::$KEYWORD_PARSERS[$name]['class'];
@ -165,14 +165,11 @@ abstract class Statement
public function parse(Parser $parser, TokensList $list)
{
/**
* Whether the beginning of this statement was previously parsed.
*
* This is used to delimit statements that don't use the usual
* delimiters.
*
* @var bool
* Array containing all list of clauses parsed.
* This is used to check for duplicates.
* @var array
*/
$parsedBeginning = false;
$parsedClauses = array();
// This may be corrected by the parser.
$this->first = $list->idx;
@ -216,7 +213,7 @@ abstract class Statement
/**
* The name of the class that is used for parsing.
* @var string $class
* @var Component $class
*/
$class = null;
@ -232,6 +229,20 @@ abstract class Statement
*/
$options = array();
// Looking for duplicated clauses.
if ((!empty(Parser::$KEYWORD_PARSERS[$token->value]))
|| (!empty(Parser::$STATEMENT_PARSERS[$token->value]))
) {
if (!empty($parsedClauses[$token->value])) {
$parser->error(
__('This type of clause was previously parsed.'), $token
);
break;
}
$parsedClauses[$token->value] = true;
}
// Checking if this is the beginning of a clause.
if (!empty(Parser::$KEYWORD_PARSERS[$token->value])) {
$class = Parser::$KEYWORD_PARSERS[$token->value]['class'];
$field = Parser::$KEYWORD_PARSERS[$token->value]['field'];
@ -240,17 +251,22 @@ abstract class Statement
}
}
// Checking if this is the beginning of the statement.
if (!empty(Parser::$STATEMENT_PARSERS[$token->value])) {
if ($parsedBeginning) {
// New statement has started. We let the parser construct a
// new statement and parse that one
if ((!empty(static::$CLAUSES)) // Undefined for some statements.
&& (empty(static::$CLAUSES[$token->value]))
) {
// Some keywords (e.g. `SET`) may be the beginning of a
// statement and a clause.
// If such keyword was found and it cannot be a clause of
// this statement it means it is a new statement, but no
// delimiter was found between them.
$parser->error(
__('A new statement was found, but no delimiter between them.'),
$token
);
break;
}
$parsedBeginning = true;
if (!$parsedOptions) {
if (empty(static::$OPTIONS[$token->value])) {
// Skipping keyword because if it is not a option.

View File

@ -45,6 +45,22 @@ class ReplaceStatement extends Statement
'DELAYED' => 1,
);
/**
* The clauses of this statement, in order.
*
* @see Statement::$CLAUSES
*
* @var array
*/
public static $CLAUSES = array(
'REPLACE' => array('REPLACE', 2),
// Used for options.
'_OPTIONS' => array('_OPTIONS', 1),
'INTO' => array('FROM', 3),
'VALUES' => array('VALUES', 1),
'SET' => array('PARTITION', 3),
);
/**
* Tables used as target for this statement.
*

View File

@ -0,0 +1,43 @@
<?php
/**
* `SET` statement.
*
* @package SqlParser
* @subpackage Statements
*/
namespace SqlParser\Statements;
use SqlParser\Statement;
use SqlParser\Components\SetOperation;
/**
* `SET` statement.
*
* @category Statements
* @package SqlParser
* @subpackage Statements
* @author Dan Ungureanu <udan1107@gmail.com>
* @license http://opensource.org/licenses/GPL-2.0 GNU Public License
*/
class SetStatement extends Statement
{
/**
* The clauses of this statement, in order.
*
* @see Statement::$CLAUSES
*
* @var array
*/
public static $CLAUSES = array(
'SET' => array('SET', 3),
);
/**
* The updated values.
*
* @var SetOperation[]
*/
public $set;
}

View File

@ -10,9 +10,7 @@ namespace SqlParser\Statements;
use SqlParser\Parser;
use SqlParser\Statement;
use SqlParser\Token;
use SqlParser\TokensList;
use SqlParser\Components\Expression;
use SqlParser\Components\OptionsArray;
/**
@ -51,7 +49,7 @@ class TransactionStatement extends Statement
/**
* The list of statements in this transaction.
*
* @var Statements[]
* @var Statement[]
*/
public $statements;
@ -110,6 +108,9 @@ class TransactionStatement extends Statement
$ret = OptionsArray::build($this->options);
if ($this->type === TransactionStatement::TYPE_BEGIN) {
foreach ($this->statements as $statement) {
/**
* @var SelectStatement $statement
*/
$ret .= ';' . $statement->build();
}
$ret .= ';' . $this->end->build();

View File

@ -209,7 +209,7 @@ class UtfString implements \ArrayAccess
/**
* Returns the contained string.
*
* @return strin
* @return string
*/
public function __toString()
{

View File

@ -0,0 +1,512 @@
<?php
/**
* Utilities that are used for formatting queries.
*
* @package SqlParser
* @subpackage Utils
*/
namespace SqlParser\Utils;
use SqlParser\Lexer;
use SqlParser\Parser;
use SqlParser\Token;
use SqlParser\TokensList;
/**
* Utilities that are used for formatting queries.
*
* @category Misc
* @package SqlParser
* @subpackage Utils
* @author Dan Ungureanu <udan1107@gmail.com>
* @license http://opensource.org/licenses/GPL-2.0 GNU Public License
*/
class Formatter
{
/**
* The formatting options.
*
* @var array
*/
public $options;
/**
* Clauses that must be inlined.
*
* These clauses usually are short and it's nicer to have them inline.
*
* @var array
*/
public static $INLINE_CLAUSES = array(
'CREATE' => true,
'PROCEDURE' => true,
'LIMIT' => true,
);
/**
* Constructor.
*
* @param array $options The formatting options.
*/
public function __construct(array $options = array())
{
// The specified formatting options are merged with the default values.
$this->options = array_merge(
array(
/**
* The format of the result.
* @var string The type ('text', 'cli' or 'html')
*/
'type' => php_sapi_name() == 'cli' ? 'cli' : 'text',
/**
* The line ending used.
* By default, for text this is "\n" and for HTML this is "<br/>".
* @var string
*/
'line_ending' => $this->options['type'] == 'html' ? '<br/>' : "\n",
/**
* The string used for indentation.
* @var string
*/
'indentation' => " ",
/**
* Whether comments should be removed or not.
* @var bool
*/
'remove_comments' => false,
/**
* Whether each clause should be on a new line.
* @var bool
*/
'clause_newline' => true,
/**
* Whether each part should be on a new line.
* Parts are delimited by brackets and commas.
* @var bool
*/
'parts_newline' => true,
/**
* Whether each part of each clause should be indented.
* @var bool
*/
'indent_parts' => true,
/**
* The styles used for HTML formatting.
* array($type, $flags, $span, $callback)
* @var array[]
*/
'formats' => array(
array(
'type' => Token::TYPE_KEYWORD,
'flags' => Token::FLAG_KEYWORD_RESERVED,
'html' => 'class="sql-reserved"',
'cli' => "\e[35m",
'function' => 'strtoupper',
),
array(
'type' => Token::TYPE_KEYWORD,
'flags' => 0,
'html' => 'class="sql-keyword"',
'cli' => "\e[95m",
'function' => 'strtoupper',
),
array(
'type' => Token::TYPE_COMMENT,
'flags' => 0,
'html' => 'class="sql-comment"',
'cli' => "\e[37m",
'function' => '',
),
array(
'type' => Token::TYPE_BOOL,
'flags' => 0,
'html' => 'class="sql-atom"',
'cli' => "\e[36m",
'function' => 'strtoupper',
),
array(
'type' => Token::TYPE_NUMBER,
'flags' => 0,
'html' => 'class="sql-number"',
'cli' => "\e[92m",
'function' => 'strtolower',
),
array(
'type' => Token::TYPE_STRING,
'flags' => 0,
'html' => 'class="sql-string"',
'cli' => "\e[91m",
'function' => '',
),
array(
'type' => Token::TYPE_SYMBOL,
'flags' => 0,
'html' => 'class="sql-variable"',
'cli' => "\e[36m",
'function' => '',
),
)
),
$options
);
// `parts_newline` requires `clause_newline`
$this->options['parts_newline'] &= $this->options['clause_newline'];
}
/**
* Formats the given list of tokens.
*
* @param TokensList $list The list of tokens.
*
* @return string
*/
public function formatList($list)
{
/**
* The query to be returned.
* @var string $ret
*/
$ret = '';
/**
* The indentation level.
* @var int $indent
*/
$indent = 0;
/**
* Whether the line ended.
* @var bool $lineEnded
*/
$lineEnded = false;
/**
* The name of the last clause.
* @var string $lastClause
*/
$lastClause = '';
/**
* A stack that keeps track of the indentation level every time a new
* block is found.
* @var array $blocksIndentation
*/
$blocksIndentation = array();
/**
* A stack that keeps track of the line endings every time a new block
* is found.
* @var array $blocksLineEndings
*/
$blocksLineEndings = array();
/**
* Whether clause's options were formatted.
* @var bool $formattedOptions
*/
$formattedOptions = false;
/**
* Previously parsed token.
* @var Token $prev
*/
$prev = null;
/**
* Comments are being formatted separately to maintain the whitespaces
* before and after them.
* @var string $comment
*/
$comment = '';
// In order to be able to format the queries correctly, the next token
// must be taken into consideration. The loop below uses two pointers,
// `$prev` and `$curr` which store two consecutive tokens.
// Actually, at every iteration the previous token is being used.
for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
* @var Token $curr
*/
$curr = $list->tokens[$list->idx];
if ($curr->type === Token::TYPE_WHITESPACE) {
// Whitespaces are skipped because the formatter adds its own.
continue;
} elseif ($curr->type === Token::TYPE_COMMENT) {
// Whether the comments should be parsed.
if (!empty($this->options['remove_comments'])) {
continue;
}
if ($list->tokens[$list->idx - 1]->type === Token::TYPE_WHITESPACE) {
// The whitespaces before and after are preserved for
// formatting reasons.
$comment .= $list->tokens[$list->idx - 1]->token;
}
$comment .= $this->toString($curr);
if (($list->tokens[$list->idx + 1]->type === Token::TYPE_WHITESPACE)
&& ($list->tokens[$list->idx + 2]->type !== Token::TYPE_COMMENT)
) {
// Adding the next whitespace only there is no comment that
// follows it immediately which may cause adding a
// whitespace twice.
$comment .= $list->tokens[$list->idx + 1]->token;
}
// Everything was handled here, no need to continue.
continue;
}
// Checking if pointers were initialized.
if ($prev !== null) {
// Checking if a new clause started.
if (static::isClause($prev)) {
$lastClause = $prev->value;
$formattedOptions = false;
}
// The options of a clause should stay on the same line and everything that follows.
if (($this->options['parts_newline'])
&& (!$formattedOptions)
&& (empty(self::$INLINE_CLAUSES[$lastClause]))
&& ($curr->type != Token::TYPE_KEYWORD)
) {
$formattedOptions = true;
$lineEnded = true;
++$indent;
}
// Checking if this clause ended.
if ($tmp = static::isClause($curr)) {
if (($tmp == 2) || ($this->options['clause_newline'])) {
$lineEnded = true;
if ($this->options['parts_newline']) {
--$indent;
}
}
}
// Indenting BEGIN ... END blocks.
if (($prev->type === Token::TYPE_KEYWORD) && ($prev->value === 'BEGIN')) {
$lineEnded = true;
array_push($blocksIndentation, $indent);
++$indent;
} elseif (($curr->type === Token::TYPE_KEYWORD) && ($curr->value === 'END')) {
$lineEnded = true;
$indent = array_pop($blocksIndentation);
}
// Formatting fragments delimited by comma.
if (($prev->type === Token::TYPE_OPERATOR) && ($prev->value === ',')) {
// Fragments delimited by a comma are broken into multiple
// pieces only if the clause if the clause is not inlined or
// this fragment is between brackets that were on new line.
if (((empty(self::$INLINE_CLAUSES[$lastClause]))
&& ($this->options['parts_newline']))
|| (end($blocksLineEndings) === true)
) {
$lineEnded = true;
}
}
// Handling brackets.
// Brackets are indented only if the length of the fragment between
// them is longer than 30 characters.
if (($prev->type === Token::TYPE_OPERATOR) && ($prev->value === '(')) {
array_push($blocksIndentation, $indent);
if (static::getGroupLength($list) > 30) {
++$indent;
$lineEnded = true;
}
array_push($blocksLineEndings, $lineEnded);
} elseif (($curr->type === Token::TYPE_OPERATOR) && ($curr->value === ')')) {
$indent = array_pop($blocksIndentation);
$lineEnded |= array_pop($blocksLineEndings);
}
// Delimiter must be placed on the same line with the last
// clause.
if ($curr->type === Token::TYPE_DELIMITER) {
$lineEnded = false;
}
// Adding the token.
$ret .= $this->toString($prev);
// Finishing the line.
if ($lineEnded) {
if ($indent < 0) {
// TODO: Make sure this never occurs and delete it.
$indent = 0;
}
if ($curr->type !== Token::TYPE_COMMENT) {
$ret .= $this->options['line_ending']
. str_repeat($this->options['indentation'], $indent);
}
$lineEnded = false;
} else {
// If the line ended there is no point in adding whitespaces.
// Also, some tokens do not have spaces before or after them.
if (!((($prev->type === Token::TYPE_OPERATOR) && (($prev->value === '.') || ($prev->value === '(')))
// No space after . (
|| (($curr->type === Token::TYPE_OPERATOR) && (($curr->value === '.') || ($curr->value === ',') || ($curr->value === '(') || ($curr->value === ')')))
// No space before . , )
|| (($curr->type === Token::TYPE_DELIMITER)) && (mb_strlen($curr->value, 'UTF-8') < 2))
// A space after delimiters that are longer than 2 characters.
|| ($prev->value === 'DELIMITER')
) {
$ret .= ' ';
}
}
}
if (!empty($comment)) {
$ret .= $comment;
$comment = '';
}
// Saving the next token as the one that will be processed during
// the next iteration.
$prev = $curr;
}
return $ret;
}
/**
* Tries to print the query and returns the result.
*
* @param Token $token The token to be printed.
*
* @return string
*/
public function toString($token)
{
$text = $token->token;
foreach ($this->options['formats'] as $format) {
if (($token->type === $format['type'])
&& (($token->flags & $format['flags']) === $format['flags'])
) {
// Running transformation function.
if (!empty($format['function'])) {
$func = $format['function'];
$text = $func($text);
}
// Formatting HTML.
if ($this->options['type'] === 'html') {
return '<span ' . $format['html'] . '>' . $text . '</span>';
} elseif ($this->options['type'] === 'cli') {
return $format['cli'] . $text;
}
break;
}
}
if ($this->options['type'] === 'cli') {
return "\e[39m" . $text;
}
return $text;
}
/**
* Formats a query.
*
* @param string $query The query to be formatted
* @param array $options The formatting options.
*
* @return string The formatted string.
*/
public static function format($query, array $options = array())
{
$lexer = new Lexer($query);
$formatter = new Formatter($options);
return $formatter->formatList($lexer->list);
}
/**
* Computes the length of a group.
*
* A group is delimited by a pair of brackets.
*
* @param TokensList $list The list of tokens.
*
* @return int
*/
public static function getGroupLength($list)
{
/**
* The number of opening brackets found.
* This counter starts at one because by the time this function called,
* the list already advanced one position and the opening bracket was
* already parsed.
* @var int
*/
$count = 1;
/**
* The length of this group.
* @var int
*/
$length = 0;
for ($idx = $list->idx; $idx < $list->count; ++$idx) {
// Counting the brackets.
if ($list->tokens[$idx]->type === Token::TYPE_OPERATOR) {
if ($list->tokens[$idx]->value === '(') {
++$count;
} elseif ($list->tokens[$idx]->value === ')') {
--$count;
if ($count == 0) {
break;
}
}
}
// Keeping track of this group's length.
$length += mb_strlen($list->tokens[$idx]->value, 'UTF-8');
}
return $length;
}
/**
* Checks if a token is a statement or a clause inside a statement.
*
* @param Token $token The token to be checked.
*
* @return int|bool
*/
public static function isClause($token)
{
if ((($token->type === Token::TYPE_NONE) && (strtoupper($token->token) === 'DELIMITER'))
|| (($token->type === Token::TYPE_KEYWORD) && (isset(Parser::$STATEMENT_PARSERS[$token->value])))
) {
return 2;
} elseif (($token->type === Token::TYPE_KEYWORD) && (isset(Parser::$KEYWORD_PARSERS[$token->value]))) {
return 1;
}
return false;
}
}

View File

@ -28,7 +28,7 @@ require_once 'libraries/Linter.class.php';
*
* @var string
*/
$sql_query = $_REQUEST['sql_query'];
$sql_query = !empty($_POST['sql_query']) ? $_POST['sql_query'] : '';
// Disabling standard response.
$response = PMA_Response::getInstance();

View File

@ -1,130 +0,0 @@
<?php
/* vim: set expandtab sw=4 ts=4 sts=4: */
/**
* tests for sql-formatter/lib/SqlFormatter.php
*
* @package PhpMyAdmin-test
*/
/*
* Include to test.
*/
require_once 'libraries/sql-formatter/lib/SqlFormatter.php';
/**
* tests for SqlFormatter
*
* @package PhpMyAdmin-test
*/
class SqlFormatter_Test extends PHPUnit_Framework_TestCase
{
/**
* Data provider for testSqlFormatter_format
*
* @return array with test data
*/
public function formatDataProvider() {
return array(
array(
"SELECT * FROM `test`",
"SELECT
*
FROM
`test`",
),
array(
"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;",
"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;"
),
array(
"SELECT a,b as c FROM `ab`; UPDATE `cd` SET `col` = REPLACE(col, 'find', 'replace')
WHERE row_id in (SELECT row_id FROM new_table WHERE col = 's' AND col2 = '3') LIMIT 256",
"SELECT
a,
b as c
FROM
`ab`;
UPDATE
`cd`
SET
`col` = REPLACE(col, 'find', 'replace')
WHERE
row_id in (
SELECT
row_id
FROM
new_table
WHERE
col = 's'
AND col2 = '3'
)
LIMIT
256"
),
array(
"INSERT INTO `a_long_table_name_it_is_really_log_but_still_not_that_long`
(a, b, c, d, e, f, g, a, b, c, d, e, f, c, d, e)
VALUES (1, 0, '', 1, NOW(), NOW(), 0),
(1, 0, 'helloabcdefgijk', 1, 'hello_world_again', NOW(), 0)",
"INSERT INTO `a_long_table_name_it_is_really_log_but_still_not_that_long` (
a, b, c, d, e, f, g, a, b, c, d, e, f, c, d, e
)
VALUES
(1, 0, '', 1, NOW(), NOW(), 0),
(
1, 0, 'helloabcdefgijk', 1, 'hello_world_again',
NOW(), 0
)"
),
array(
"ALTER TABLE `PREFIX_product` DROP `reduction_price`,DROP `reduction_percent`,
DROP `reduction_from`, DROP `reduction_to`",
"ALTER TABLE
`PREFIX_product`
DROP
`reduction_price`,
DROP
`reduction_percent`,
DROP
`reduction_from`,
DROP
`reduction_to`"
),
);
}
/**
* Test for SqlFormatter::format
*
* @return void
*
* @dataProvider formatDataProvider
*/
public function testSqlFormatter_format($query, $expected)
{
SqlFormatter::$tab = "\t";
$this->assertEquals(
$expected,
SqlFormatter::format($query, false)
);
}
}