-
-
Notifications
You must be signed in to change notification settings - Fork 108
Add WITH support #334
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
Add WITH support #334
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?php | ||
/** | ||
* `WITH` keyword builder. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace PhpMyAdmin\SqlParser\Components; | ||
|
||
use PhpMyAdmin\SqlParser\Component; | ||
use PhpMyAdmin\SqlParser\Parser; | ||
use RuntimeException; | ||
|
||
/** | ||
* `WITH` keyword builder. | ||
*/ | ||
final class WithKeyword extends Component | ||
{ | ||
/** @var string */ | ||
public $name; | ||
|
||
/** @var ArrayObj[] */ | ||
public $columns = []; | ||
|
||
/** @var Parser */ | ||
public $statement; | ||
|
||
public function __construct(string $name) | ||
{ | ||
$this->name = $name; | ||
} | ||
|
||
/** | ||
* @param WithKeyword $component | ||
* @param mixed[] $options | ||
* | ||
* @return string | ||
*/ | ||
public static function build($component, array $options = []) | ||
{ | ||
if (! $component instanceof WithKeyword) { | ||
throw new RuntimeException('Can not build a component that is not a WithKeyword'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about LogicException ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LogicException are more often used outside of running code, at compile time, here as we would hit that during the "runtime" it's more appropriate. See also https://stackoverflow.com/questions/5586404/logicexception-vs-runtimeexception. I hesitated here, and I even wanted to pick an exception of the package but ParseException wasn't a good fit. |
||
} | ||
|
||
if (! isset($component->statement)) { | ||
throw new RuntimeException('No statement inside WITH'); | ||
williamdes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
$str = $component->name; | ||
|
||
if ($component->columns) { | ||
$str .= ArrayObj::build($component->columns); | ||
} | ||
|
||
$str .= ' AS ('; | ||
|
||
foreach ($component->statement->statements as $statement) { | ||
soyuka marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe check that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As |
||
$str .= $statement->build(); | ||
} | ||
|
||
$str .= ')'; | ||
|
||
return $str; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
<?php | ||
/** | ||
* `WITH` statement. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace PhpMyAdmin\SqlParser\Statements; | ||
|
||
use PhpMyAdmin\SqlParser\Components\Array2d; | ||
use PhpMyAdmin\SqlParser\Components\OptionsArray; | ||
use PhpMyAdmin\SqlParser\Components\WithKeyword; | ||
use PhpMyAdmin\SqlParser\Exceptions\ParserException; | ||
use PhpMyAdmin\SqlParser\Parser; | ||
use PhpMyAdmin\SqlParser\Statement; | ||
use PhpMyAdmin\SqlParser\Token; | ||
use PhpMyAdmin\SqlParser\TokensList; | ||
use PhpMyAdmin\SqlParser\Translator; | ||
|
||
use function array_slice; | ||
use function count; | ||
|
||
/** | ||
* `WITH` statement. | ||
|
||
* WITH [RECURSIVE] query_name [ (column_name [,...]) ] AS (SELECT ...) [, ...] | ||
*/ | ||
final class WithStatement extends Statement | ||
{ | ||
/** | ||
* Options for `WITH` statements and their slot ID. | ||
* | ||
* @var mixed[] | ||
*/ | ||
public static $OPTIONS = ['RECURSIVE' => 1]; | ||
|
||
/** | ||
* The clauses of this statement, in order. | ||
* | ||
* @see Statement::$CLAUSES | ||
* | ||
* @var mixed[] | ||
*/ | ||
public static $CLAUSES = [ | ||
'WITH' => [ | ||
'WITH', | ||
2, | ||
], | ||
// Used for options. | ||
'_OPTIONS' => [ | ||
'_OPTIONS', | ||
1, | ||
], | ||
'AS' => [ | ||
'AS', | ||
2, | ||
], | ||
]; | ||
|
||
/** @var WithKeyword[] */ | ||
public $withers = []; | ||
|
||
/** | ||
* @param Parser $parser the instance that requests parsing | ||
* @param TokensList $list the list of tokens to be parsed | ||
*/ | ||
public function parse(Parser $parser, TokensList $list) | ||
{ | ||
++$list->idx; // Skipping `WITH`. | ||
|
||
// parse any options if provided | ||
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS); | ||
++$list->idx; | ||
|
||
/** | ||
* The state of the parser. | ||
* | ||
* Below are the states of the parser. | ||
williamdes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* 0 ---------------- [ name ] -----------------> 1 | ||
* 1 -------------- [( columns )] AS ----------------> 2 | ||
* 2 ------------------ [ , ] --------------------> 0 | ||
* | ||
* @var int | ||
*/ | ||
$state = 0; | ||
$wither = null; | ||
|
||
for (; $list->idx < $list->count; ++$list->idx) { | ||
/** | ||
* Token parsed at this moment. | ||
* | ||
* @var Token | ||
*/ | ||
$token = $list->tokens[$list->idx]; | ||
|
||
// Skipping whitespaces and comments. | ||
if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) { | ||
continue; | ||
} | ||
|
||
if ($token->type === Token::TYPE_NONE) { | ||
$wither = $token->value; | ||
$this->withers[$wither] = new WithKeyword($wither); | ||
$state = 1; | ||
continue; | ||
} | ||
|
||
if ($state === 1) { | ||
if ($token->value === '(') { | ||
$this->withers[$wither]->columns = Array2d::parse($parser, $list); | ||
continue; | ||
} | ||
|
||
if ($token->keyword === 'AS') { | ||
++$list->idx; | ||
$state = 2; | ||
continue; | ||
} | ||
} elseif ($state === 2) { | ||
if ($token->value === '(') { | ||
soyuka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
++$list->idx; | ||
$subList = $this->getSubTokenList($list); | ||
if ($subList instanceof ParserException) { | ||
$parser->errors[] = $subList; | ||
continue; | ||
} | ||
|
||
$subParser = new Parser($subList); | ||
|
||
if (count($subParser->errors)) { | ||
foreach ($subParser->errors as $error) { | ||
$parser->errors[] = $error; | ||
} | ||
} | ||
|
||
$this->withers[$wither]->statement = $subParser; | ||
continue; | ||
} | ||
|
||
// There's another WITH expression to parse, go back to state=0 | ||
if ($token->value === ',') { | ||
$list->idx++; | ||
$state = 0; | ||
soyuka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
continue; | ||
} | ||
|
||
// No more WITH expressions, we're done with this statement | ||
break; | ||
} | ||
} | ||
|
||
--$list->idx; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function build() | ||
{ | ||
$str = 'WITH '; | ||
|
||
foreach ($this->withers as $wither) { | ||
$str .= $str === 'WITH ' ? '' : ', '; | ||
$str .= WithKeyword::build($wither); | ||
} | ||
|
||
return $str; | ||
} | ||
|
||
/** | ||
* Get tokens within the WITH expression to use them in another parser | ||
* | ||
* @return ParserException|TokensList | ||
*/ | ||
private function getSubTokenList(TokensList $list) | ||
{ | ||
$idx = $list->idx; | ||
/** @var Token $token */ | ||
$token = $list->tokens[$list->idx]; | ||
$openParenthesis = 0; | ||
|
||
while ($list->idx < $list->count) { | ||
if ($token->value === '(') { | ||
++$openParenthesis; | ||
} elseif ($token->value === ')') { | ||
if (--$openParenthesis === -1) { | ||
break; | ||
} | ||
} | ||
|
||
++$list->idx; | ||
if (! isset($list->tokens[$list->idx])) { | ||
break; | ||
} | ||
|
||
$token = $list->tokens[$list->idx]; | ||
} | ||
|
||
// performance improvement: return the error to avoid a try/catch in the loop | ||
if ($list->idx === $list->count) { | ||
--$list->idx; | ||
|
||
return new ParserException( | ||
Translator::gettext('A closing bracket was expected.'), | ||
$token | ||
); | ||
} | ||
|
||
$length = $list->idx - $idx; | ||
|
||
return new TokensList(array_slice($list->tokens, $idx, $length), $length); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,11 +50,7 @@ public function __construct(array $tokens = [], $count = -1) | |
} | ||
|
||
$this->tokens = $tokens; | ||
if ($count !== -1) { | ||
return; | ||
} | ||
|
||
$this->count = count($tokens); | ||
$this->count = $count === -1 ? count($tokens) : $count; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You change the behavior, are you sure ? 😃 Before There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, there was a bug in the parser actually. |
||
} | ||
|
||
/** | ||
|
Uh oh!
There was an error while loading. Please reload this page.