Skip to content

Commit 04ae763

Browse files
committed
Add support for LOCK and UNLOCK Statements
Signed-off-by: Deven Bansod <[email protected]>
1 parent e07403c commit 04ae763

39 files changed

+503
-0
lines changed

src/Components/LockExpression.php

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
3+
/**
4+
* Parses a reference to a LOCK expression.
5+
*/
6+
7+
namespace PhpMyAdmin\SqlParser\Components;
8+
9+
use PhpMyAdmin\SqlParser\Component;
10+
use PhpMyAdmin\SqlParser\Parser;
11+
use PhpMyAdmin\SqlParser\Token;
12+
use PhpMyAdmin\SqlParser\TokensList;
13+
14+
/**
15+
* Parses a reference to a LOCK expression.
16+
*
17+
* @category Components
18+
*
19+
* @license https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
20+
*/
21+
class LockExpression extends Component
22+
{
23+
/**
24+
* The table to be locked.
25+
*
26+
* @var Expression
27+
*/
28+
public $table;
29+
30+
/**
31+
* The type of lock to be applied.
32+
*
33+
* @var string
34+
*/
35+
public $type;
36+
37+
/**
38+
* @param Parser $parser the parser that serves as context
39+
* @param TokensList $list the list of tokens that are being parsed
40+
* @param array $options parameters for parsing
41+
*
42+
* @return CaseExpression
43+
*/
44+
public static function parse(Parser $parser, TokensList $list, array $options = array())
45+
{
46+
$ret = new self();
47+
48+
/**
49+
* The state of the parser.
50+
*
51+
* Below are the states of the parser.
52+
*
53+
* 0 ---------------- [ tbl_name ] -----------------> 1
54+
* 1 ---------------- [ lock_type ] ----------------> 2
55+
* 2 -------------------- [ , ] --------------------> break
56+
*
57+
* @var int
58+
*/
59+
$state = 0;
60+
61+
$prevToken = null;
62+
63+
for (; $list->idx < $list->count; ++$list->idx) {
64+
/**
65+
* Token parsed at this moment.
66+
*
67+
* @var Token
68+
*/
69+
$token = $list->tokens[$list->idx];
70+
71+
// End of statement.
72+
if ($token->type === Token::TYPE_DELIMITER
73+
|| ($token->type === Token::TYPE_OPERATOR
74+
&& $token->value === ',')
75+
) {
76+
break;
77+
}
78+
79+
// Skipping whitespaces and comments.
80+
if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
81+
continue;
82+
}
83+
84+
if ($state === 0) {
85+
$ret->table = Expression::parse($parser, $list, array('parseField' => 'table'));
86+
$state = 1;
87+
} elseif ($state === 1) {
88+
// parse lock type
89+
$ret->type = self::parseLockType($parser, $list);
90+
$state = 2;
91+
}
92+
$prevToken = $token;
93+
}
94+
95+
// 2 is the only valid end state
96+
if ($state !== 2) {
97+
$parser->error('Unexpected end of LOCK expression.', $prevToken);
98+
}
99+
100+
--$list->idx;
101+
102+
return $ret;
103+
}
104+
105+
/**
106+
* @param LockExpression|LockExpression[] $component the component to be built
107+
* @param array $options parameters for building
108+
*
109+
* @return string
110+
*/
111+
public static function build($component, array $options = array())
112+
{
113+
if (is_array($component)) {
114+
return implode(', ', $component);
115+
}
116+
117+
return $component->table . ' ' . $component->type;
118+
}
119+
120+
private static function parseLockType($parser, $list) {
121+
$lockType = '';
122+
123+
/**
124+
* The state of the parser.
125+
*
126+
* Below are the states of the parser.
127+
*
128+
* 0 ---------------- [ READ ] -----------------> 1
129+
* 0 ------------- [ LOW_PRIORITY ] ------------> 2
130+
* 0 ---------------- [ WRITE ] ----------------> 3
131+
* 1 ---------------- [ LOCAL ] ----------------> 3
132+
* 2 ---------------- [ WRITE ] ----------------> 3
133+
*
134+
* @var int
135+
*/
136+
$state = 0;
137+
138+
$prevToken = null;
139+
140+
for (; $list->idx < $list->count; ++$list->idx) {
141+
/**
142+
* Token parsed at this moment.
143+
*
144+
* @var Token
145+
*/
146+
$token = $list->tokens[$list->idx];
147+
148+
// End of statement.
149+
if ($token->type === Token::TYPE_DELIMITER
150+
|| ($token->type === Token::TYPE_OPERATOR
151+
&& $token->value === ',')
152+
) {
153+
--$list->idx;
154+
break;
155+
}
156+
157+
// Skipping whitespaces and comments.
158+
if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
159+
continue;
160+
}
161+
162+
// We only expect keywords
163+
if ($token->type !== Token::TYPE_KEYWORD) {
164+
$parser->error('Unexpected token.', $token);
165+
break;
166+
}
167+
168+
if ($state === 0) {
169+
if ($token->keyword === 'READ') {
170+
$state = 1;
171+
} elseif ($token->keyword === 'LOW_PRIORITY') {
172+
$state = 2;
173+
} elseif ($token->keyword === 'WRITE') {
174+
$state = 3;
175+
} else {
176+
$parser->error('Unexpected keyword.', $token);
177+
break;
178+
}
179+
$lockType .= $token->keyword;
180+
} elseif ($state === 1) {
181+
if ($token->keyword === 'LOCAL') {
182+
$lockType .= ' ' . $token->keyword;
183+
$state = 3;
184+
} else {
185+
$parser->error('Unexpected keyword.', $token);
186+
break;
187+
}
188+
} elseif ($state === 2) {
189+
if ($token->keyword === 'WRITE') {
190+
$lockType .= ' ' . $token->keyword;
191+
$state = 3;
192+
// parsing over
193+
break;
194+
} else {
195+
$parser->error('Unexpected keyword.', $token);
196+
break;
197+
}
198+
}
199+
200+
$prevToken = $token;
201+
}
202+
203+
// Only two possible end states
204+
if ($state !== 1 && $state !== 3) {
205+
$parser->error('Unexpected end of Lock expression.', $prevToken);
206+
}
207+
208+
return $lockType;
209+
}
210+
}

src/Parser.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ class Parser extends Core
8888
'START TRANSACTION' => 'PhpMyAdmin\\SqlParser\\Statements\\TransactionStatement',
8989

9090
'PURGE' => 'PhpMyAdmin\\SqlParser\\Statements\\PurgeStatement',
91+
92+
// Lock statements
93+
// https://dev.mysql.com/doc/refman/5.7/en/lock-tables.html
94+
'LOCK' => 'PhpMyAdmin\\SqlParser\\Statements\\LockStatement',
95+
'UNLOCK' => 'PhpMyAdmin\\SqlParser\\Statements\\LockStatement',
9196
);
9297

9398
/**

src/Statements/LockStatement.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
/**
4+
* `LOCK` statement.
5+
*/
6+
7+
namespace PhpMyAdmin\SqlParser\Statements;
8+
9+
use PhpMyAdmin\SqlParser\Components\LockExpression;
10+
use PhpMyAdmin\SqlParser\Parser;
11+
use PhpMyAdmin\SqlParser\Statement;
12+
use PhpMyAdmin\SqlParser\Token;
13+
use PhpMyAdmin\SqlParser\TokensList;
14+
15+
/**
16+
* `LOCK` statement.
17+
*
18+
* @category Statements
19+
*
20+
* @license https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
21+
*/
22+
class LockStatement extends Statement
23+
{
24+
/**
25+
* Tables with their Lock expressions.
26+
*
27+
* @var LockExpression[]
28+
*/
29+
public $locked = array();
30+
31+
/**
32+
* Whether it's a LOCK statement
33+
* if false, it's an UNLOCK statement
34+
*/
35+
public $isLock = true;
36+
37+
/**
38+
* @param Parser $parser the instance that requests parsing
39+
* @param TokensList $list the list of tokens to be parsed
40+
*/
41+
public function parse(Parser $parser, TokensList $list)
42+
{
43+
if ($list->tokens[$list->idx]->value === 'UNLOCK') {
44+
// this is in fact an UNLOCK statement
45+
$this->isLock = false;
46+
}
47+
++$list->idx; // Skipping `LOCK`.
48+
49+
/**
50+
* The state of the parser.
51+
*
52+
* Below are the states of the parser.
53+
*
54+
* 0 ---------------- [ TABLES ] -----------------> 1
55+
* 1 -------------- [ lock_expr ] ----------------> 2
56+
* 2 ------------------ [ , ] --------------------> 1
57+
*
58+
* @var int
59+
*/
60+
$state = 0;
61+
62+
/**
63+
* Previous parsed token
64+
*/
65+
$prevToken = null;
66+
67+
for (; $list->idx < $list->count; ++$list->idx) {
68+
/**
69+
* Token parsed at this moment.
70+
*
71+
* @var Token
72+
*/
73+
$token = $list->tokens[$list->idx];
74+
75+
// End of statement.
76+
if ($token->type === Token::TYPE_DELIMITER) {
77+
break;
78+
}
79+
80+
// Skipping whitespaces and comments.
81+
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
82+
continue;
83+
}
84+
85+
if ($state === 0) {
86+
if ($token->type === Token::TYPE_KEYWORD) {
87+
if ($token->keyword !== 'TABLES') {
88+
$parser->error('Unexpected keyword.', $token);
89+
break;
90+
}
91+
$state = 1;
92+
continue;
93+
} else {
94+
$parser->error('Unexpected token.', $token);
95+
break;
96+
}
97+
} elseif ($state === 1) {
98+
if (!$this->isLock) {
99+
// UNLOCK statement should not have any more tokens
100+
$parser->error('Unexpected token.', $token);
101+
break;
102+
}
103+
$this->locked[] = LockExpression::parse($parser, $list);
104+
$state = 2;
105+
} elseif ($state === 2) {
106+
if ($token->value === ',') {
107+
// move over to parsing next lock expression
108+
$state = 1;
109+
} else {
110+
break;
111+
}
112+
}
113+
114+
$prevToken = $token;
115+
}
116+
117+
if ($state !== 2 && $prevToken != null) {
118+
$parser->error('Unexpected end of LOCK statement.', $prevToken);
119+
}
120+
}
121+
122+
/**
123+
* @return string
124+
*/
125+
public function build()
126+
{
127+
return trim(($this->isLock ? 'LOCK' : 'UNLOCK')
128+
. ' TABLES ' . LockExpression::build($this->locked));
129+
}
130+
}

0 commit comments

Comments
 (0)