Merge pull request #11337 from udan11/formatter
Implemented new formatter in the sql-parser library
This commit is contained in:
commit
ba3d3fd8e4
@ -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);
|
||||
|
||||
@ -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.
|
||||
@ -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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
43
libraries/sql-parser/src/Statements/SetStatement.php
Normal file
43
libraries/sql-parser/src/Statements/SetStatement.php
Normal 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;
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -209,7 +209,7 @@ class UtfString implements \ArrayAccess
|
||||
/**
|
||||
* Returns the contained string.
|
||||
*
|
||||
* @return strin
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
|
||||
512
libraries/sql-parser/src/Utils/Formatter.php
Normal file
512
libraries/sql-parser/src/Utils/Formatter.php
Normal 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;
|
||||
}
|
||||
}
|
||||
2
lint.php
2
lint.php
@ -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();
|
||||
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user