Skip to content

Commit 8464175

Browse files
committed
Fix #347 - MySQL 8.0 table structure KEY expression not recognized
Signed-off-by: William Desportes <williamdes@wdes.fr>
1 parent 985ba00 commit 8464175

File tree

3 files changed

+232
-8
lines changed

3 files changed

+232
-8
lines changed

src/Components/Key.php

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class Key extends Component
4343
),
4444
'COMMENT' => array(
4545
4,
46-
'var=',
46+
'var',
4747
)
4848
);
4949

@@ -68,6 +68,13 @@ class Key extends Component
6868
*/
6969
public $type;
7070

71+
/**
72+
* The expression if it is not using a name.
73+
*
74+
* @var Expression|null
75+
*/
76+
public $expr = null;
77+
7178
/**
7279
* The options of this key.
7380
*
@@ -118,12 +125,16 @@ public static function parse(Parser $parser, TokensList $list, array $options =
118125
*
119126
* Below are the states of the parser.
120127
*
121-
* 0 ----------------------[ type ]-----------------------> 1
128+
* 0 ---------------------[ type ]---------------------------> 1
122129
*
123-
* 1 ----------------------[ name ]-----------------------> 1
124-
* 1 ---------------------[ columns ]---------------------> 2
130+
* 1 ---------------------[ name ]---------------------------> 1
131+
* 1 ---------------------[ columns ]------------------------> 2
132+
* 1 ---------------------[ expression ]---------------------> 5
125133
*
126-
* 2 ---------------------[ options ]---------------------> 3
134+
* 2 ---------------------[ column length ]------------------> 3
135+
* 3 ---------------------[ column length ]------------------> 2
136+
* 2 ---------------------[ options ]------------------------> 4
137+
* 5 ---------------------[ expression ]---------------------> 4
127138
*
128139
* @var int
129140
*/
@@ -152,7 +163,15 @@ public static function parse(Parser $parser, TokensList $list, array $options =
152163
$state = 1;
153164
} elseif ($state === 1) {
154165
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
155-
$state = 2;
166+
// Switch to expression mode
167+
if (
168+
isset($list->tokens[$list->idx + 1])
169+
&& $list->tokens[$list->idx + 1]->value === '('
170+
) {
171+
$state = 5;
172+
} else {
173+
$state = 2;
174+
}
156175
} else {
157176
$ret->name = $token->value;
158177
}
@@ -180,6 +199,16 @@ public static function parse(Parser $parser, TokensList $list, array $options =
180199
$ret->options = OptionsArray::parse($parser, $list, static::$KEY_OPTIONS);
181200
++$list->idx;
182201
break;
202+
} elseif ($state === 5) {
203+
$ret->expr = Expression::parse(
204+
$parser,
205+
$list,
206+
array(
207+
'parenthesesDelimited' => true
208+
)
209+
);
210+
++$list->idx;// skip end parenthese
211+
$state = 4;// go back to state 4 to fetch options
183212
}
184213
}
185214

@@ -201,6 +230,10 @@ public static function build($component, array $options = array())
201230
$ret .= Context::escape($component->name) . ' ';
202231
}
203232

233+
if ($component->expr !== null) {
234+
return $ret . '(' . $component->expr . ')' . ' ' . $component->options;
235+
}
236+
204237
$columns = array();
205238
foreach ($component->columns as $column) {
206239
$tmp = '';

tests/Builder/CreateStatementTest.php

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ public function testBuilderCreateProcedure()
402402
. ' SQL SECURITY INVOKER NO SQL SQL SECURITY INVOKER SELECT _var'
403403
);
404404

405-
/** @var CreateStatement */
405+
/** @var CreateStatement $stmt */
406406
$stmt = $parser->statements[0];
407407

408408
$this->assertSame(
@@ -475,7 +475,7 @@ public function testBuilderCreateFunction()
475475
. 'END'
476476
);
477477

478-
/** @var CreateStatement */
478+
/** @var CreateStatement $stmt */
479479
$stmt = $parser->statements[0];
480480

481481
$this->assertSame(
@@ -616,4 +616,74 @@ public function testBuildSelect()
616616
$parser->statements[0]->build()
617617
);
618618
}
619+
620+
public function testBuildCreateTableComplexIndexes()
621+
{
622+
$parser = new Parser(
623+
<<<'SQL'
624+
CREATE TABLE `page_rebuild_control` (
625+
`proc_row_number` int DEFAULT NULL,
626+
`place_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
627+
`place_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
628+
`place_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
629+
`waterway_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
630+
`cache_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
631+
`place_active` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
632+
`alias_type` int NOT NULL DEFAULT '0',
633+
`status` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
634+
`time_taken` float DEFAULT NULL,
635+
PRIMARY KEY (`place_id`,`place_type`) USING BTREE,
636+
KEY `place_type_idx` (`place_type`(10)),
637+
KEY `cached_time_idx` (`cache_updated`),
638+
KEY `active_idx` (`place_active`),
639+
KEY `status_idx` (`status`),
640+
KEY `waterway_idx` (`waterway_id`),
641+
KEY `time_taken_idx` (`time_taken`),
642+
KEY `alias_type_idx` (`alias_type`),
643+
KEY `updated_tz_ind2` ((convert_tz(`cache_updated`,_utf8mb4'GMT',_utf8mb4'GB'))) COMMENT 'foo\'s',
644+
KEY `updated_tz_ind` ((convert_tz(`cache_updated`,_utf8mb4'GMT',_utf8mb4'GB')))
645+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
646+
SQL
647+
);
648+
649+
/** @var CreateStatement $stmt */
650+
$stmt = $parser->statements[0];
651+
652+
$tableBody = <<<'SQL'
653+
(
654+
`proc_row_number` int DEFAULT NULL,
655+
`place_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
656+
`place_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
657+
`place_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
658+
`waterway_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
659+
`cache_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
660+
`place_active` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
661+
`alias_type` int NOT NULL DEFAULT '0',
662+
`status` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
663+
`time_taken` float DEFAULT NULL,
664+
PRIMARY KEY (`place_id`,`place_type`) USING BTREE,
665+
KEY `place_type_idx` (`place_type`(10)),
666+
KEY `cached_time_idx` (`cache_updated`),
667+
KEY `active_idx` (`place_active`),
668+
KEY `status_idx` (`status`),
669+
KEY `waterway_idx` (`waterway_id`),
670+
KEY `time_taken_idx` (`time_taken`),
671+
KEY `alias_type_idx` (`alias_type`),
672+
KEY `updated_tz_ind2` ((convert_tz(`cache_updated`,_utf8mb4'GMT',_utf8mb4'GB'))) COMMENT 'foo\'s',
673+
KEY `updated_tz_ind` ((convert_tz(`cache_updated`,_utf8mb4'GMT',_utf8mb4'GB')))
674+
)
675+
SQL;
676+
677+
$this->assertEquals(
678+
$tableBody,
679+
CreateDefinition::build($stmt->fields)
680+
);
681+
682+
$this->assertEquals(
683+
'CREATE TABLE `page_rebuild_control` '
684+
. $tableBody
685+
. ' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
686+
$stmt->build()
687+
);
688+
}
619689
}

tests/Components/KeyTest.php

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace PhpMyAdmin\SqlParser\Tests\Components;
44

5+
use PhpMyAdmin\SqlParser\Components\Expression;
56
use PhpMyAdmin\SqlParser\Components\Key;
7+
use PhpMyAdmin\SqlParser\Components\OptionsArray;
68
use PhpMyAdmin\SqlParser\Parser;
79
use PhpMyAdmin\SqlParser\Tests\TestCase;
810

@@ -14,6 +16,125 @@ public function testParse()
1416
new Parser(),
1517
$this->getTokensList('')
1618
);
19+
$this->assertNull($component->type);
20+
$this->assertNull($component->options);
1721
$this->assertNull($component->name);
22+
$this->assertNull($component->expr);
23+
$this->assertSame([], $component->columns);
24+
}
25+
26+
public function testParseKeyWithoutOptions()
27+
{
28+
$component = Key::parse(
29+
new Parser(),
30+
$this->getTokensList('KEY `alias_type_idx` (`alias_type`),')
31+
);
32+
$this->assertEquals('KEY', $component->type);
33+
$this->assertEquals('alias_type_idx', $component->name);
34+
$this->assertEquals(new OptionsArray(), $component->options);
35+
$this->assertNull($component->expr);
36+
$this->assertSame([
37+
[
38+
'name' => 'alias_type',
39+
]
40+
], $component->columns);
41+
}
42+
43+
public function testParseKeyWithLengthWithoutOptions()
44+
{
45+
$component = Key::parse(
46+
new Parser(),
47+
$this->getTokensList('KEY `alias_type_idx` (`alias_type`(10)),')
48+
);
49+
$this->assertEquals('KEY', $component->type);
50+
$this->assertEquals('alias_type_idx', $component->name);
51+
$this->assertEquals(new OptionsArray(), $component->options);
52+
$this->assertNull($component->expr);
53+
$this->assertSame([
54+
[
55+
'name' => 'alias_type',
56+
'length' => 10,
57+
]
58+
], $component->columns);
59+
}
60+
61+
public function testParseKeyWithLengthWithOptions()
62+
{
63+
$component = Key::parse(
64+
new Parser(),
65+
$this->getTokensList('KEY `alias_type_idx` (`alias_type`(10)) COMMENT \'my comment\',')
66+
);
67+
$this->assertEquals('KEY', $component->type);
68+
$this->assertEquals('alias_type_idx', $component->name);
69+
$this->assertEquals(new OptionsArray(
70+
[
71+
4 => [
72+
'name' => 'COMMENT',
73+
'equals' => false,
74+
'expr' => '\'my comment\'',
75+
'value' => 'my comment',
76+
]
77+
]
78+
), $component->options);
79+
$this->assertNull($component->expr);
80+
$this->assertSame([
81+
[
82+
'name' => 'alias_type',
83+
'length' => 10,
84+
]
85+
], $component->columns);
86+
}
87+
88+
public function testParseKeyExpressionWithoutOptions()
89+
{
90+
$component = Key::parse(
91+
new Parser(),
92+
$this->getTokensList(
93+
'KEY `updated_tz_ind2` ((convert_tz(`cache_updated`,_utf8mb4\'GMT\',_utf8mb4\'GB\'))),'
94+
)
95+
);
96+
$this->assertEquals('KEY', $component->type);
97+
$this->assertEquals('updated_tz_ind2', $component->name);
98+
$this->assertEquals(new OptionsArray(), $component->options);
99+
$expr = new Expression(
100+
'(convert_tz(`cache_updated`,_utf8mb4\'GMT\',_utf8mb4\'GB\'))'
101+
);
102+
$expr->function = 'convert_tz';
103+
$this->assertEquals(
104+
$expr,
105+
$component->expr
106+
);
107+
$this->assertSame([], $component->columns);
108+
}
109+
110+
public function testParseKeyExpressionWithOptions()
111+
{
112+
$component = Key::parse(
113+
new Parser(),
114+
$this->getTokensList(
115+
'KEY `updated_tz_ind2` ((convert_tz(`cache_updated`,_utf8mb4\'GMT\',_utf8mb4\'GB\'))) COMMENT \'my comment\','
116+
)
117+
);
118+
$this->assertEquals('KEY', $component->type);
119+
$this->assertEquals('updated_tz_ind2', $component->name);
120+
$this->assertEquals(new OptionsArray(
121+
[
122+
4 => [
123+
'name' => 'COMMENT',
124+
'equals' => false,
125+
'expr' => '\'my comment\'',
126+
'value' => 'my comment',
127+
]
128+
]
129+
), $component->options);
130+
$expr = new Expression(
131+
'(convert_tz(`cache_updated`,_utf8mb4\'GMT\',_utf8mb4\'GB\'))'
132+
);
133+
$expr->function = 'convert_tz';
134+
$this->assertEquals(
135+
$expr,
136+
$component->expr
137+
);
138+
$this->assertSame([], $component->columns);
18139
}
19140
}

0 commit comments

Comments
 (0)