Skip to content

Fix #22 : Validate order of clauses in the parsing of statements #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,9 @@ public function parse()
$lastStatement->last = $statement->last;

$unionType = false;

// Validate clause order
$statement->validateClauseOrder($this, $list);
continue;
}

Expand All @@ -556,9 +559,15 @@ public function parse()
}
$lastTransaction = null;
}

// Validate clause order
$statement->validateClauseOrder($this, $list);
continue;
}

// Validate clause order
$statement->validateClauseOrder($this, $list);

// Finally, storing the statement.
if ($lastTransaction !== null) {
$lastTransaction->statements[] = $statement;
Expand Down
43 changes: 43 additions & 0 deletions src/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,47 @@ public function __toString()
{
return $this->build();
}

/**
* Validates the order of the clauses in parsed statement
* Ideally this should be called after successfully
* completing the parsing of each statement
*
* @param Parser $parser The instance that requests parsing.
* @param TokensList $list The list of tokens to be parsed.
*
* @return boolean
*/
public function validateClauseOrder($parser, $list)
{
$clauses = array_flip(array_keys($this->getClauses()));

if (empty($clauses)
|| count($clauses) == 0
) {
return true;
}

$minIdx = -1;
foreach ($clauses as $clauseType => $index) {
$clauseStartIdx = Utils\Query::getClauseStartOffset(
$this,
$list,
$clauseType
);

if ($clauseStartIdx != -1 && $clauseStartIdx < $minIdx) {
$token = $list->tokens[$clauseStartIdx];
$parser->error(
__('Unexpected ordering of clauses.'),
$token
);
return false;
} elseif ($clauseStartIdx != -1) {
$minIdx = $clauseStartIdx;
}
}

return true;
}
}
62 changes: 62 additions & 0 deletions src/Utils/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -783,4 +783,66 @@ public static function getFirstStatement($query, $delimiter = null)

return array(trim($statement), $query, $delimiter);
}

/**
* Gets a starting offset of a specific clause.
*
* @param Statement $statement The parsed query that has to be modified.
* @param TokensList $list The list of tokens.
* @param string $clause The clause to be returned.
*
* @return int
*/
public static function getClauseStartOffset($statement, $list, $clause)
{

/**
* The index of the current clause.
*
* @var int $currIdx
*/
$currIdx = 0;

/**
* The count of brackets.
* We keep track of them so we won't insert the clause in a subquery.
*
* @var int $brackets
*/
$brackets = 0;

/**
* The clauses of this type of statement and their index.
*
* @var array $clauses
*/
$clauses = array_flip(array_keys($statement->getClauses()));

for ($i = $statement->first; $i <= $statement->last; ++$i) {
$token = $list->tokens[$i];

if ($token->type === Token::TYPE_COMMENT) {
continue;
}

if ($token->type === Token::TYPE_OPERATOR) {
if ($token->value === '(') {
++$brackets;
} elseif ($token->value === ')') {
--$brackets;
}
}

if ($brackets == 0) {
if (($token->type === Token::TYPE_KEYWORD)
&& (isset($clauses[$token->value]))
&& ($clause === $token->value)
) {
return $i;
}
}
}

return -1;
}
}
1 change: 1 addition & 0 deletions tests/Parser/SelectStatementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public function testSelectProvider()
array('parser/parseSelectJoinNaturalLeftOuter'),
array('parser/parseSelectJoinNaturalRightOuter'),
array('parser/parseSelectJoinMultiple'),
array('parser/parseSelectWrongOrder'),
);
}
}
1 change: 1 addition & 0 deletions tests/data/parser/parseSelectWrongOrder.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT pid, name2 FROM tablename LIMIT 10 WHERE pid = 20
1 change: 1 addition & 0 deletions tests/data/parser/parseSelectWrongOrder.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a:4:{s:5:"query";s:56:"SELECT pid, name2 FROM tablename LIMIT 10 WHERE pid = 20";s:5:"lexer";O:15:"SqlParser\Lexer":8:{s:6:"strict";b:0;s:3:"str";s:56:"SELECT pid, name2 FROM tablename LIMIT 10 WHERE pid = 20";s:3:"len";i:56;s:4:"last";i:56;s:4:"list";O:20:"SqlParser\TokensList":3:{s:6:"tokens";a:23:{i:0;O:15:"SqlParser\Token":5:{s:5:"token";s:6:"SELECT";s:5:"value";s:6:"SELECT";s:4:"type";i:1;s:5:"flags";i:3;s:8:"position";i:0;}i:1;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:6;}i:2;O:15:"SqlParser\Token":5:{s:5:"token";s:3:"pid";s:5:"value";s:3:"pid";s:4:"type";i:0;s:5:"flags";i:0;s:8:"position";i:7;}i:3;O:15:"SqlParser\Token":5:{s:5:"token";s:1:",";s:5:"value";s:1:",";s:4:"type";i:2;s:5:"flags";i:16;s:8:"position";i:10;}i:4;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:11;}i:5;O:15:"SqlParser\Token":5:{s:5:"token";s:5:"name2";s:5:"value";s:5:"name2";s:4:"type";i:0;s:5:"flags";i:0;s:8:"position";i:12;}i:6;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:17;}i:7;O:15:"SqlParser\Token":5:{s:5:"token";s:4:"FROM";s:5:"value";s:4:"FROM";s:4:"type";i:1;s:5:"flags";i:3;s:8:"position";i:18;}i:8;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:22;}i:9;O:15:"SqlParser\Token":5:{s:5:"token";s:9:"tablename";s:5:"value";s:9:"tablename";s:4:"type";i:0;s:5:"flags";i:0;s:8:"position";i:23;}i:10;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:32;}i:11;O:15:"SqlParser\Token":5:{s:5:"token";s:5:"LIMIT";s:5:"value";s:5:"LIMIT";s:4:"type";i:1;s:5:"flags";i:3;s:8:"position";i:33;}i:12;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:38;}i:13;O:15:"SqlParser\Token":5:{s:5:"token";s:2:"10";s:5:"value";i:10;s:4:"type";i:6;s:5:"flags";i:0;s:8:"position";i:39;}i:14;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:41;}i:15;O:15:"SqlParser\Token":5:{s:5:"token";s:5:"WHERE";s:5:"value";s:5:"WHERE";s:4:"type";i:1;s:5:"flags";i:3;s:8:"position";i:42;}i:16;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:47;}i:17;O:15:"SqlParser\Token":5:{s:5:"token";s:3:"pid";s:5:"value";s:3:"pid";s:4:"type";i:0;s:5:"flags";i:0;s:8:"position";i:48;}i:18;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:51;}i:19;O:15:"SqlParser\Token":5:{s:5:"token";s:1:"=";s:5:"value";s:1:"=";s:4:"type";i:2;s:5:"flags";i:2;s:8:"position";i:52;}i:20;O:15:"SqlParser\Token":5:{s:5:"token";s:1:" ";s:5:"value";s:1:" ";s:4:"type";i:3;s:5:"flags";i:0;s:8:"position";i:53;}i:21;O:15:"SqlParser\Token":5:{s:5:"token";s:2:"20";s:5:"value";i:20;s:4:"type";i:6;s:5:"flags";i:0;s:8:"position";i:54;}i:22;O:15:"SqlParser\Token":5:{s:5:"token";N;s:5:"value";N;s:4:"type";i:9;s:5:"flags";i:0;s:8:"position";N;}}s:5:"count";i:23;s:3:"idx";i:23;}s:9:"delimiter";s:1:";";s:12:"delimiterLen";i:1;s:6:"errors";a:0:{}}s:6:"parser";O:16:"SqlParser\Parser":5:{s:4:"list";r:8;s:6:"strict";b:0;s:6:"errors";a:0:{}s:10:"statements";a:1:{i:0;O:36:"SqlParser\Statements\SelectStatement":15:{s:4:"expr";a:2:{i:0;O:31:"SqlParser\Components\Expression":7:{s:8:"database";N;s:5:"table";N;s:6:"column";s:3:"pid";s:4:"expr";s:3:"pid";s:5:"alias";N;s:8:"function";N;s:8:"subquery";N;}i:1;O:31:"SqlParser\Components\Expression":7:{s:8:"database";N;s:5:"table";N;s:6:"column";s:5:"name2";s:4:"expr";s:5:"name2";s:5:"alias";N;s:8:"function";N;s:8:"subquery";N;}}s:4:"from";a:1:{i:0;O:31:"SqlParser\Components\Expression":7:{s:8:"database";N;s:5:"table";s:9:"tablename";s:6:"column";N;s:4:"expr";s:9:"tablename";s:5:"alias";N;s:8:"function";N;s:8:"subquery";N;}}s:9:"partition";N;s:5:"where";a:1:{i:0;O:30:"SqlParser\Components\Condition":3:{s:11:"identifiers";a:1:{i:0;s:3:"pid";}s:10:"isOperator";b:0;s:4:"expr";s:8:"pid = 20";}}s:5:"group";N;s:6:"having";N;s:5:"order";N;s:5:"limit";O:26:"SqlParser\Components\Limit":2:{s:6:"offset";i:0;s:8:"rowCount";i:10;}s:9:"procedure";N;s:4:"into";N;s:4:"join";N;s:5:"union";a:0:{}s:7:"options";O:33:"SqlParser\Components\OptionsArray":1:{s:7:"options";a:0:{}}s:5:"first";i:0;s:4:"last";i:21;}}s:8:"brackets";i:0;}s:6:"errors";a:2:{s:5:"lexer";a:0:{}s:6:"parser";a:1:{i:0;a:3:{i:0;s:31:"Unexpected ordering of clauses.";i:1;r:76;i:2;i:0;}}}}