├── .gitignore ├── .travis.yml ├── LICENSE ├── README.MD ├── composer.json ├── phpunit.xml ├── src ├── Parser.php └── Table │ ├── DbEntity.php │ ├── ExtraEntity.php │ ├── FieldEntity.php │ ├── IEntity.php │ ├── IndexEntity.php │ └── TableEntity.php └── tests ├── ParserTest.php └── dump.sql /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | /.idea 4 | /clover.xml 5 | /report/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - 7.1 7 | 8 | before_script: 9 | - composer install 10 | - travis_retry composer self-update 11 | - travis_retry composer install --no-interaction --prefer-source --dev 12 | 13 | script: 14 | - phpunit --coverage-clover=coverage.xml -v 15 | 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 phpple 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | Phpple Altable 2 | ================ 3 | 4 | 本项目用来对通过mysqldump出来的表结构数据进行解析,分析出数据库、数据表、字段、主键、索引等信息。 5 | 6 | [![Latest Stable Version](https://img.shields.io/packagist/v/phpple/altable.svg?style=flat-square)](https://packagist.org/packages/phpple/altable) 7 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%205.6-8892BF.svg?style=flat-square)](https://php.net/) 8 | [![Build Status](https://img.shields.io/travis/phpple/altable/master.svg?style=flat-square)](https://travis-ci.org/phpple/altable) 9 | [![codecov](https://codecov.io/gh/phpple/altable/branch/master/graph/badge.svg)](https://codecov.io/gh/phpple/altable) 10 | 11 | ## 使用步骤 12 | 13 | composer引入项目 14 | ```bash 15 | composer require phpple/altable 16 | ``` 17 | 18 | 通过mysqldump导出需要的表结构: 19 | ```bash 20 | mysqldump --all-databases --no-data > dump.sql 21 | ``` 22 | 23 | 24 | 编写php脚本分析数据库结构 25 | ```php 26 | dbFilters = [ 32 | // 整个库不予分析 33 | 'mysql' => null, 34 | // foo.bar不予分析 35 | 'foo' => ['bar'], 36 | ]; 37 | $dbs = $parser->parse(__DIR__.'/dump.sql'); 38 | 39 | // 开始进行分析 40 | foreach($dbs as $db) { 41 | foreach($db->tables as $table) { 42 | foreach($table->fields as $field) { 43 | if ($field->name == 'uid') { 44 | echo "`{$db->name}`.`{$table->name}` found uid field"; 45 | } 46 | } 47 | } 48 | } 49 | 50 | // 通过名称查找名称为foo的DB 51 | $parser->find($dbs, 'foo'); 52 | 53 | // 通过名称查找名称为foo.bar的Table 54 | $parser->find($dbs, 'foo', 'bar'); 55 | 56 | // 通过名称查找表foo.bar里的字段uid 57 | $parser->find($dbs, 'foo', 'bar', 'uid'); 58 | ``` 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpple/altable", 3 | "autoload": { 4 | "psr-4": { 5 | "Phpple\\Altable\\": "./src", 6 | "Phpple\\Altable\\Tests\\": "./tests" 7 | } 8 | }, 9 | "license": "MIT", 10 | "require-dev": { 11 | "phpunit/phpunit": "^5.6 || ^7.0 || ^7.1 || ^7.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/ 6 | 7 | 8 | 9 | 10 | ./src 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | 'USE `', 58 | self::PREFIX_TABLE => 'CREATE TABLE `', 59 | self::PREFIX_FIELD => ' `', 60 | self::PREFIX_PK => ' PRIMARY KEY (', 61 | self::PREFIX_INDEX => ' KEY ', 62 | self::PREFIX_UNIQUE_INDEX => ' UNIQUE KEY ', 63 | self::PREFIX_EXTRA => ') ', 64 | ]; 65 | 66 | private static $specialPrefixLens = null; 67 | const SPLIT_FLAG = '`'; 68 | 69 | public function __construct() 70 | { 71 | if (self::$specialPrefixLens === null) { 72 | self::$specialPrefixLens = []; 73 | foreach (self::$specialPrefixes as $key => $prefix) { 74 | self::$specialPrefixLens[$key] = strlen($prefix); 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * 解析某种关键字 81 | * @param string $key 82 | * @param string $line 83 | * @return IEntity|null 84 | */ 85 | public function detectPrefix($key, $line) 86 | { 87 | $prefix = self::$specialPrefixes[$key]; 88 | $len = self::$specialPrefixLens[$key]; 89 | if (strncmp($line, $prefix, $len) !== 0) { 90 | return null; 91 | } 92 | 93 | $method = 'parse' . ucfirst($key); 94 | return $this->$method(substr($line, $len)); 95 | } 96 | 97 | /** 98 | * 解析DB名 99 | * @param string $line 100 | * @return null|string 101 | */ 102 | public function parseDb($line) 103 | { 104 | $pos = strpos($line, self::SPLIT_FLAG); 105 | if ($pos === false) { 106 | return null; 107 | } 108 | return substr($line, 0, $pos); 109 | } 110 | 111 | /** 112 | * 解析TABLE名称 113 | * @param string $line 114 | * @return null|string 115 | */ 116 | public function parseTable($line) 117 | { 118 | $pos = strpos($line, self::SPLIT_FLAG); 119 | if ($pos === false) { 120 | return null; 121 | } 122 | return substr($line, 0, $pos); 123 | } 124 | 125 | /** 126 | * 解析字段名称 127 | * @param string $line 128 | * @return FieldEntity|null 129 | */ 130 | public function parseField($line) 131 | { 132 | if (preg_match(self::FIELD_PARSE_EXP, $line, $ms)) { 133 | $fieldEntity = new FieldEntity(); 134 | $fieldEntity->name = $ms[1]; 135 | $fieldEntity->type = $ms[2]; 136 | // 如果是int现关类型,后面的长度没有意义 137 | // @see https://dev.mysql.com/doc/refman/5.6/en/integer-types.html 138 | if (strpos($fieldEntity->type, 'int') !== false) { 139 | $fieldEntity->length = 0; 140 | } elseif ($fieldEntity->type == 'enum' || $fieldEntity->type == 'set') { 141 | $options = explode(',', $ms[3]); 142 | foreach ($options as $key => $option) { 143 | $options[$key] = trim($option, '\''); 144 | } 145 | $fieldEntity->options = $options; 146 | } else { 147 | // 如果是双精度,可能是DECIMAL(5,2)、DOUBLE(16,2)的样子 148 | // @see https://dev.mysql.com/doc/refman/5.6/en/fixed-point-types.html 149 | // @see https://dev.mysql.com/doc/refman/5.6/en/floating-point-types.html 150 | $fieldEntity->length = isset($ms[3]) ? $ms[3] : 0; 151 | } 152 | $fieldEntity->unsigned = !empty($ms[4]); 153 | $fieldEntity->charset = isset($ms[5]) ? $ms[5] : null; 154 | $fieldEntity->collate = isset($ms[6]) ? $ms[6] : null; 155 | $fieldEntity->notnull = !empty($ms[7]); 156 | $fieldEntity->autoinc = !empty($ms[8]); 157 | $fieldEntity->default = isset($ms[9]) ? trim($ms[9], "'") : null; 158 | $fieldEntity->onupdate = isset($ms[10]) ? $ms[10] : null; 159 | $fieldEntity->comment = isset($ms[11]) ? $ms[11] : null; 160 | return $fieldEntity; 161 | } 162 | return null; 163 | } 164 | 165 | /** 166 | * 解析主键 167 | * @example PRIMARY KEY (`tag_id`,`topic_id`,`type`) 168 | * @param string $line 169 | * @return string[]|null 170 | */ 171 | public function parsePk($line) 172 | { 173 | $pos = strpos($line, ')'); 174 | if ($pos !== false) { 175 | $pks = explode(',', substr($line, 0, $pos)); 176 | foreach ($pks as &$pk) { 177 | $pk = trim($pk, self::SPLIT_FLAG); 178 | } 179 | return $pks; 180 | } 181 | return null; 182 | } 183 | 184 | /** 185 | * 解析索引 186 | * @example 187 | * KEY `nw_ticket_id` (`nw_ticket_id`) 188 | * UNIQUE KEY `admin_search` (`expo_id`,`project_id`,`ticket_status`,`add_time`), 189 | * UNIQUE KEY `ticket_id` (`ticket_id`) USING BTREE 190 | * @param string $line 191 | * @param bool $unique 192 | * @return IndexEntity|null 193 | */ 194 | public function parseIndex($line, $unique = false) 195 | { 196 | $leftPos = strpos($line, '('); 197 | if ($leftPos === false) { 198 | return null; 199 | } 200 | $rightPos = strpos($line, ')', $leftPos + 1); 201 | if ($rightPos === false) { 202 | return null; 203 | } 204 | $indexEntity = new IndexEntity(); 205 | $indexEntity->name = substr($line, 1, $leftPos - 3); 206 | $fields = explode(',', substr($line, $leftPos + 1, $rightPos - $leftPos - 1)); 207 | foreach ($fields as &$field) { 208 | $field = substr($field, 1, -1); 209 | } 210 | $indexEntity->fields = $fields; 211 | 212 | if (preg_match('#USING (BTREE|HASH)#', substr($line, $rightPos + 1), $ms)) { 213 | $indexEntity->type = $ms[1]; 214 | } 215 | $indexEntity->unique = $unique; 216 | return $indexEntity; 217 | } 218 | 219 | /** 220 | * 解析唯一索引 221 | * @param string $line 222 | * @see Parser::parseIndex() 223 | * @return null|IndexEntity 224 | */ 225 | public function parseUniqueIndex($line) 226 | { 227 | return $this->parseIndex($line, true); 228 | } 229 | 230 | /** 231 | * 解析额外的信息,如engine、charset等 232 | * @example ) ENGINE=InnoDB AUTO_INCREMENT=210 DEFAULT CHARSET=utf8; 233 | * @param string $line 234 | * @return ExtraEntity 235 | */ 236 | public function parseExtra($line) 237 | { 238 | if (preg_match(self::EXTRA_PARSE_EXP, $line, $ms)) { 239 | $extraEntity = new ExtraEntity(); 240 | $extraEntity->engine = isset($ms[1]) ? $ms[1] : ''; 241 | $extraEntity->autoIncrement = isset($ms[2]) ? intval($ms[2]) : 0; 242 | $extraEntity->charset = isset($ms[3]) ? $ms[3] : ''; 243 | $extraEntity->collate = isset($ms[4]) ? $ms[4] : ''; 244 | $extraEntity->rowFormat = isset($ms[5]) ? $ms[5] : ''; 245 | $extraEntity->comment = isset($ms[6]) ? $ms[6] : ''; 246 | return $extraEntity; 247 | } 248 | return null; 249 | } 250 | 251 | 252 | /** 253 | * 解析SQL文件 254 | * @param string $file 255 | * @return DbEntity[] 256 | */ 257 | public function parse($file) 258 | { 259 | if (!is_readable($file)) { 260 | throw new \InvalidArgumentException('parser.fileNotReadable'); 261 | } 262 | 263 | $fh = fopen($file, 'r'); 264 | $dbs = []; 265 | $dbEntity = null; 266 | $tableEntity = null; 267 | 268 | $ignoreDb = false; 269 | $ignoreTb = false; 270 | $dbNames = []; 271 | 272 | while (($line = fgets($fh)) !== false) { 273 | $line = rtrim($line); 274 | // 过滤掉注释内容 275 | if (!$line || strncmp($line, '/*!', 3) === 0 || strncmp($line, '--', 2) === 0) { 276 | continue; 277 | } 278 | 279 | //记录库名 280 | $ret = $this->detectPrefix(self::PREFIX_DB, $line); 281 | if ($ret !== null) { 282 | $dbname = $ret; 283 | // 如果在需要被过滤的db名称,则不予处理 284 | // 如果值为null,表示此库全部过滤 285 | if ($dbname 286 | && array_key_exists($dbname, $this->dbFilters) 287 | && $this->dbFilters[$dbname] === null) { 288 | $ignoreDb = true; 289 | continue; 290 | } else { 291 | $ignoreDb = false; 292 | } 293 | 294 | if (isset($dbNames[$ret])) { 295 | $dbEntity = null; 296 | continue; 297 | } 298 | 299 | // 检查是否已经有过该库 300 | $dbEntity = new DbEntity(); 301 | $dbEntity->name = $ret; 302 | $dbs[] = $dbEntity; 303 | $dbNames[$ret] = true; 304 | continue; 305 | } 306 | if ($ignoreDb || !$dbEntity) { 307 | continue; 308 | } 309 | 310 | //记录表名 311 | $ret = $this->detectPrefix(self::PREFIX_TABLE, $line); 312 | if ($ret !== null) { 313 | $tbName = $ret; 314 | // 过滤需要处理的table 315 | if ($dbname 316 | && $tbName 317 | && isset($this->dbFilters[$dbname]) 318 | && in_array($tbName, $this->dbFilters[$dbname])) { 319 | $ignoreTb = true; 320 | continue; 321 | } else { 322 | $ignoreTb = false; 323 | } 324 | 325 | $tableEntity = new TableEntity(); 326 | $tableEntity->name = $tbName = $ret; 327 | continue; 328 | } 329 | if ($ignoreTb) { 330 | continue; 331 | } 332 | 333 | // 找出字段 334 | $ret = $this->detectPrefix(self::PREFIX_FIELD, $line); 335 | if ($ret !== null) { 336 | $tableEntity->addField($ret); 337 | continue; 338 | } 339 | 340 | // 找出主键 341 | $ret = $this->detectPrefix(self::PREFIX_PK, $line); 342 | if ($ret !== null) { 343 | $tableEntity->setPrimaryKeys($ret); 344 | continue; 345 | } 346 | 347 | $ret = $this->detectPrefix(self::PREFIX_INDEX, $line); 348 | if ($ret !== null) { 349 | $tableEntity->addIndex($ret); 350 | continue; 351 | } 352 | 353 | $ret = $this->detectPrefix(self::PREFIX_UNIQUE_INDEX, $line); 354 | if ($ret !== null) { 355 | $tableEntity->addIndex($ret); 356 | continue; 357 | } 358 | 359 | $ret = $this->detectPrefix(self::PREFIX_EXTRA, $line); 360 | if ($ret !== null) { 361 | foreach (['charset', 'autoIncrement', 'engine', 'comment', 'collate'] as $key) { 362 | $tableEntity->$key = $ret->$key; 363 | } 364 | $dbEntity->addTable($tableEntity); 365 | continue; 366 | } 367 | } 368 | fclose($fh); 369 | return $dbs; 370 | } 371 | 372 | /** 373 | * 根据名称找到对象 374 | * @param DbEntity[] $dbs 375 | * @param string $dbName 376 | * @param string $tbName 377 | * @param string $fieldName 378 | * @example 379 | * 找DbEntity: ->listEntity($dbs, $dbName) 380 | * 找TableEntity: ->listEntity($dbs, $dbName, $tbName) 381 | * 找FieldEntity: ->listEntity($dbs, $dbName, $tbName, $fieldName) 382 | * @return IEntity 383 | * @throws \InvalidArgumentException parser.dbNameRequired 384 | */ 385 | public function findEntity($dbs, $dbName, $tbName = '', $fieldName = '') 386 | { 387 | if (!$dbName) { 388 | throw new \InvalidArgumentException('parser.dbNameRequired'); 389 | } 390 | // 查找db 391 | $foundDb = null; 392 | foreach ($dbs as $db) { 393 | if ($db->name == $dbName) { 394 | $foundDb = $db; 395 | break; 396 | } 397 | } 398 | if (!$foundDb || !$tbName) { 399 | return $foundDb; 400 | } 401 | 402 | // 查找table 403 | $foundTb = null; 404 | foreach ($foundDb->tables as $table) { 405 | if ($table->name == $tbName) { 406 | $foundTb = $table; 407 | break; 408 | } 409 | } 410 | if (!$foundTb || !$fieldName) { 411 | return $foundTb; 412 | } 413 | 414 | // 查找field 415 | foreach ($foundTb->fields as $field) { 416 | if ($field->name == $fieldName) { 417 | return $field; 418 | } 419 | } 420 | return null; 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/Table/DbEntity.php: -------------------------------------------------------------------------------- 1 | tables[] = $table; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Table/ExtraEntity.php: -------------------------------------------------------------------------------- 1 | fields[] = $field; 66 | return $this; 67 | } 68 | 69 | /** 70 | * 设置主键 71 | * @param mixed $fields 72 | */ 73 | public function setPrimaryKeys($fields) 74 | { 75 | foreach ($fields as $field) { 76 | foreach ($this->fields as $f) { 77 | if ($f->name == $field) { 78 | $f->pk = true; 79 | } 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * 添加索引 86 | * @param IndexEntity $index 87 | */ 88 | public function addIndex(IndexEntity $index) 89 | { 90 | $this->indexes[] = $index; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/ParserTest.php: -------------------------------------------------------------------------------- 1 | parser = new Parser(); 32 | } 33 | 34 | /** 35 | * @expectedException \InvalidArgumentException 36 | * @expectedExceptionMessage parser.fileNotReadable 37 | */ 38 | public function testFileNotReadable() 39 | { 40 | $file = __DIR__.'/notexisted.sql'; 41 | $this->parser->parse($file); 42 | } 43 | 44 | public function testCompile() 45 | { 46 | $file = __DIR__ . '/dump.sql'; 47 | $dbs = $this->parser->parse($file); 48 | $this->assertEquals(1, count($dbs)); 49 | foreach ($dbs as $db) { 50 | $this->assertInstanceOf(DbEntity::class, $db); 51 | 52 | foreach ($db->tables as $table) { 53 | $this->assertInstanceOf(TableEntity::class, $table); 54 | } 55 | } 56 | // 字段个数 57 | $this->assertEquals(5, count($dbs[0]->tables[0]->fields)); 58 | $this->assertEquals(11, count($dbs[0]->tables[1]->fields)); 59 | $this->assertEquals('phpple', $dbs[0]->name); 60 | $this->assertEquals(['foo', 'u_user'], [$dbs[0]->tables[0]->name, $dbs[0]->tables[1]->name]); 61 | 62 | $table = $dbs[0]->tables[0]; 63 | $this->assertEquals('foo', $table->name); 64 | $this->assertEquals('utf8', $table->charset); 65 | $this->assertEquals('InnoDB', $table->engine); 66 | 67 | $this->parser->dbFilters = ['phpple' => null]; 68 | $dbs = $this->parser->parse($file); 69 | $this->assertEmpty($dbs); 70 | 71 | $this->parser->dbFilters = ['phpple' => ['foo']]; 72 | $dbs = $this->parser->parse($file); 73 | $this->assertEquals(1, count($dbs)); 74 | $this->assertEquals(1, count($dbs[0]->tables)); 75 | $this->assertEquals('u_user', $dbs[0]->tables[0]->name); 76 | $this->assertEquals('id', $dbs[0]->tables[0]->fields[0]->name); 77 | $this->assertTrue($dbs[0]->tables[0]->fields[0]->pk); 78 | $this->assertEquals('del_flag', $dbs[0]->tables[0]->fields[8]->name); 79 | $this->assertFalse($dbs[0]->tables[0]->fields[8]->pk); 80 | $this->assertTrue($dbs[0]->tables[0]->fields[8]->notnull); 81 | $this->assertContains('tinyint', $dbs[0]->tables[0]->fields[8]->type); 82 | } 83 | 84 | public function testFind() 85 | { 86 | $dbName = 'phpple'; 87 | $tbName = 'u_user'; 88 | $fieldName = 'view_num'; 89 | $notexistName = 'xxsdfsdfsd'; 90 | 91 | $file = __DIR__ . '/dump.sql'; 92 | $dbs = $this->parser->parse($file); 93 | 94 | try { 95 | $this->parser->findEntity($dbs, ''); 96 | } catch (\InvalidArgumentException $ex) { 97 | $this->assertEquals('parser.dbNameRequired', $ex->getMessage()); 98 | } 99 | 100 | $db = $this->parser->findEntity($dbs, $notexistName); 101 | $this->assertNull($db); 102 | 103 | $db = $this->parser->findEntity($dbs, $dbName); 104 | $this->assertInstanceOf(DbEntity::class, $db); 105 | $this->assertEquals($dbName, $db->name); 106 | 107 | $tb = $this->parser->findEntity($dbs, $notexistName, $notexistName); 108 | $this->assertNull($tb); 109 | 110 | $tb = $this->parser->findEntity($dbs, $dbName, $notexistName); 111 | $this->assertNull($tb); 112 | 113 | $tb = $this->parser->findEntity($dbs, $dbName, $tbName); 114 | $this->assertInstanceOf(TableEntity::class, $tb); 115 | $this->assertEquals($tbName, $tb->name); 116 | 117 | $field = $this->parser->findEntity($dbs, $dbName, $tbName, $notexistName); 118 | $this->assertNull($field); 119 | 120 | $field = $this->parser->findEntity($dbs, $dbName, $tbName, $fieldName); 121 | $this->assertInstanceOf(FieldEntity::class, $field); 122 | $this->assertEquals($fieldName, $field->name); 123 | } 124 | 125 | 126 | public function testParseDb() 127 | { 128 | $line = 'USE `fine_db`;'; 129 | $dbname = $this->parser->detectPrefix(Parser::PREFIX_DB, $line); 130 | $this->assertEquals('fine_db', $dbname); 131 | 132 | $line = 'USE `fine_db'; 133 | $dbname = $this->parser->detectPrefix(Parser::PREFIX_DB, $line); 134 | $this->assertNull($dbname); 135 | } 136 | 137 | public function testParseTable() 138 | { 139 | $line = 'CREATE TABLE `post` ('; 140 | $tbname = $this->parser->detectPrefix(Parser::PREFIX_TABLE, $line); 141 | $this->assertEquals('post', $tbname); 142 | 143 | $line = 'CREATE TABLE `post" ('; 144 | $tbname = $this->parser->detectPrefix(Parser::PREFIX_TABLE, $line); 145 | $this->assertNull($tbname); 146 | } 147 | 148 | public function testParseField() 149 | { 150 | // 普通字段 151 | $line = ' `uname` varchar(36) NOT NULL COMMENT \'用户名\','; 152 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 153 | $this->assertEquals('uname', $field->name); 154 | $this->assertEquals('varchar', $field->type); 155 | $this->assertEquals(36, $field->length); 156 | $this->assertTrue($field->notnull); 157 | $this->assertFalse($field->unsigned); 158 | $this->assertEquals('用户名', $field->comment); 159 | $this->assertFalse($field->autoinc); 160 | 161 | // notnull,autoincrement 162 | $line = ' `uid` int(10) unsigned NOT NULL AUTO_INCREMENT,'; 163 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 164 | $this->assertTrue($field->notnull); 165 | $this->assertTrue($field->autoinc); 166 | 167 | // 错误解析 168 | $line = ' `uname` varchar(36) NOT COMMENT \'用户名\','; 169 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 170 | $this->assertNull($field); 171 | 172 | // charset 173 | $line = ' `template_value` mediumtext CHARACTER SET utf8mb4 NOT NULL COMMENT \'配置项json\','; 174 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 175 | $this->assertEquals('utf8mb4', $field->charset); 176 | $this->assertTrue($field->notnull); 177 | $this->assertEquals('配置项json', $field->comment); 178 | 179 | // collate 180 | $line = ' `domain` varchar(32) COLLATE utf8_bin NULL COMMENT \'域名\','; 181 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 182 | $this->assertEquals('utf8_bin', $field->collate); 183 | $this->assertEquals('域名', $field->comment); 184 | $this->assertFalse($field->notnull); 185 | 186 | // onupdate 187 | $line = ' `start_time` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),'; 188 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 189 | $this->assertEquals('CURRENT_TIMESTAMP(6)', $field->default); 190 | $this->assertEquals(6, $field->length); 191 | $this->assertEquals('CURRENT_TIMESTAMP(6)', $field->onupdate); 192 | 193 | // enum类型 194 | $line = " `ssl_type` enum('','ANY','X509','SPECIFIED') CHARACTER SET utf8 NOT NULL DEFAULT '',"; 195 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 196 | $this->assertNotNull($field); 197 | $this->assertEquals('enum', $field->type); 198 | $this->assertEquals(['', 'ANY', 'X509', 'SPECIFIED'], $field->options); 199 | $this->assertEquals('utf8', $field->charset); 200 | $this->assertEquals('', $field->default); 201 | 202 | // set类型 203 | $line = " `Column_priv` set('Select','Insert','Update','References') CHARACTER SET utf8 NOT NULL DEFAULT '',"; 204 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 205 | $this->assertEquals('set', $field->type); 206 | $this->assertEquals(['Select','Insert','Update','References'], $field->options); 207 | $this->assertEquals('utf8', $field->charset); 208 | $this->assertEquals('', $field->default); 209 | 210 | // 同时有charset和collate 211 | $line = " `community_desc` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '帖子描述',"; 212 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 213 | $this->assertEquals('community_desc', $field->name); 214 | $this->assertEquals(256, $field->length); 215 | $this->assertEquals('utf8mb4', $field->charset); 216 | $this->assertEquals('utf8mb4_unicode_ci', $field->collate); 217 | $this->assertEquals('帖子描述', $field->comment); 218 | 219 | // 乱码 220 | $line = " `auth_type` tinyint(1) unsigned NOT NULL COMMENT '验è¯<81>类型 1 Basic验è¯<81>',"; 221 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 222 | $this->assertNotEquals(1, $field->length); 223 | $this->assertEquals('验è¯<81>类型 1 Basic验è¯<81>', $field->comment); 224 | 225 | $line = " `campus_name_new` varchar(100) NOT NULL DEFAULT '' COMMENT '教学中心名称'"; 226 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 227 | $this->assertEquals('campus_name_new', $field->name); 228 | 229 | // 默认值有空格 230 | $line = ' `sub_item_date` datetime NOT NULL DEFAULT \'0000-00-00 00:00:00\' COMMENT \'购物时间\','; 231 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 232 | $this->assertNotNull($field); 233 | $this->assertEquals('sub_item_date', $field->name); 234 | $this->assertEquals('0000-00-00 00:00:00', $field->default); 235 | } 236 | 237 | public function testParsePk() 238 | { 239 | $line = ' PRIMARY KEY (`uid`)'; 240 | $pks = $this->parser->detectPrefix(Parser::PREFIX_PK, $line); 241 | $this->assertEquals(['uid'], $pks); 242 | 243 | $line = ' PRIMARY KEY (`city_id`,`uid`)'; 244 | $pks = $this->parser->detectPrefix(Parser::PREFIX_PK, $line); 245 | $this->assertEquals(['city_id', 'uid'], $pks); 246 | 247 | $line = 'PRIMARY KEY (`uid`)'; 248 | $pks = $this->parser->detectPrefix(Parser::PREFIX_PK, $line); 249 | $this->assertNull($pks); 250 | 251 | $line = ' PRIMARY KEY (`uid`'; 252 | $pks = $this->parser->detectPrefix(Parser::PREFIX_PK, $line); 253 | $this->assertNull($pks); 254 | 255 | $line = ' PRIMARY KEY (`third_remark_id`) USING BTREE,'; 256 | $pks = $this->parser->detectPrefix(Parser::PREFIX_PK, $line); 257 | $this->assertEquals(['third_remark_id'], $pks); 258 | } 259 | 260 | public function testParseIndex() 261 | { 262 | $line = ' KEY `idx_email` (`email`)'; 263 | $index = $this->parser->detectPrefix(Parser::PREFIX_INDEX, $line); 264 | $this->assertEquals(['email'], $index->fields); 265 | 266 | $line = ' KEY `idx_city_id_sex` (`city_id`,`sex`) USING BTREE'; 267 | $index = $this->parser->detectPrefix(Parser::PREFIX_INDEX, $line); 268 | $this->assertEquals('idx_city_id_sex', $index->name); 269 | $this->assertEquals('BTREE', $index->type); 270 | 271 | $line = ' KEY `idx_city_id_sex` `city_id`'; 272 | $index = $this->parser->detectPrefix(Parser::PREFIX_INDEX, $line); 273 | $this->assertNull($index); 274 | $line = ' KEY `idx_city_id_sex`(`city_id`'; 275 | $index = $this->parser->detectPrefix(Parser::PREFIX_INDEX, $line); 276 | $this->assertNull($index); 277 | } 278 | 279 | public function testExtra() 280 | { 281 | $line = ') ENGINE=MyIsam AUTO_INCREMENT=1100000 DEFAULT CHARSET=utf9;'; 282 | $extra = $this->parser->detectPrefix(Parser::PREFIX_EXTRA, $line); 283 | $this->assertInstanceOf(ExtraEntity::class, $extra); 284 | $this->assertEquals('utf9', $extra->charset); 285 | $this->assertEquals('MyIsam', $extra->engine); 286 | 287 | $line = ') 3ENGINE=InnoDB 3AUTO_INCREMENT=1100000 DEFAULT CHARSET=utf9;'; 288 | $extra = $this->parser->detectPrefix(Parser::PREFIX_EXTRA, $line); 289 | $this->assertNull($extra); 290 | 291 | $line = ') ENGINE=InnoDB AUTO_INCREMENT=1018 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;'; 292 | $extra = $this->parser->detectPrefix(Parser::PREFIX_EXTRA, $line); 293 | $this->assertNotNull($extra); 294 | $this->assertEquals('utf8', $extra->charset); 295 | $this->assertEquals('utf8_bin', $extra->collate); 296 | 297 | $line = ') ENGINE=InnoDB AUTO_INCREMENT=9688 DEFAULT CHARSET=utf8 COMMENT=\'婚礼请柬-用户创建的请帖管理\';'; 298 | $extra = $this->parser->detectPrefix(Parser::PREFIX_EXTRA, $line); 299 | $this->assertNotNull($extra); 300 | $this->assertEquals(9688, $extra->autoIncrement); 301 | $this->assertEquals('utf8', $extra->charset); 302 | $this->assertEquals('婚礼请柬-用户创建的请帖管理', $extra->comment); 303 | 304 | $line = ') ENGINE=InnoDB AUTO_INCREMENT=252912 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT=\'员工信息历史记录表\';'; 305 | $extra = $this->parser->detectPrefix(Parser::PREFIX_EXTRA, $line); 306 | $this->assertNotNull($extra); 307 | $this->assertEquals(252912, $extra->autoIncrement); 308 | $this->assertEquals('utf8', $extra->charset); 309 | $this->assertEquals('COMPACT', $extra->rowFormat); 310 | } 311 | 312 | /** 313 | * 如果一个字段是默认的null,会出现解析错误 314 | */ 315 | public function testDefaultNull() 316 | { 317 | $line = ' `name_ab` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT \'校区简称\','; 318 | $field = $this->parser->detectPrefix(Parser::PREFIX_FIELD, $line); 319 | $this->assertNotNull($field); 320 | $this->assertEquals('NULL', $field->default); 321 | $this->assertFalse($field->notnull); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /tests/dump.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 5.6.38, for osx10.13 (x86_64) 2 | -- 3 | -- Host: localhost Database: phpple 4 | -- ------------------------------------------------------ 5 | -- Server version 5.6.38 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!40101 SET NAMES utf8 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Current Database: `phpple` 20 | -- 21 | 22 | CREATE DATABASE /*!32312 IF NOT EXISTS*/ `phpple` /*!40100 DEFAULT CHARACTER SET utf8 */; 23 | 24 | USE `phpple`; 25 | 26 | -- 27 | -- Table structure for table `foo` 28 | -- 29 | 30 | DROP TABLE IF EXISTS `foo`; 31 | /*!40101 SET @saved_cs_client = @@character_set_client */; 32 | /*!40101 SET character_set_client = utf8 */; 33 | CREATE TABLE `foo` ( 34 | `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, 35 | `user_id` int(10) unsigned NOT NULL DEFAULT '0', 36 | `uname` varchar(36) NOT NULL COMMENT '用户名', 37 | `password` char(40) NOT NULL, 38 | `email` varchar(60) NOT NULL DEFAULT '', 39 | PRIMARY KEY (`uid`), 40 | UNIQUE KEY `idex_uname` (`uname`), 41 | KEY `idx_uid_username` (`uid`,`uname`) 42 | ) ENGINE=InnoDB AUTO_INCREMENT=1100000 DEFAULT CHARSET=utf8; 43 | /*!40101 SET character_set_client = @saved_cs_client */; 44 | 45 | -- 46 | -- Table structure for table `u_user` 47 | -- 48 | 49 | DROP TABLE IF EXISTS `u_user`; 50 | /*!40101 SET @saved_cs_client = @@character_set_client */; 51 | /*!40101 SET character_set_client = utf8 */; 52 | CREATE TABLE `u_user` ( 53 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 54 | `username` varchar(20) NOT NULL, 55 | `password` char(40) NOT NULL, 56 | `email` char(64) NOT NULL DEFAULT '', 57 | `phone` bigint(20) unsigned NOT NULL DEFAULT '0', 58 | `view_num` int(10) unsigned NOT NULL DEFAULT '0', 59 | `city_id` mediumint(8) unsigned NOT NULL DEFAULT '0', 60 | `status` tinyint(3) unsigned NOT NULL DEFAULT '0', 61 | `del_flag` tinyint(3) unsigned NOT NULL DEFAULT '0', 62 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 63 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 64 | PRIMARY KEY (`id`) 65 | ) ENGINE=InnoDB AUTO_INCREMENT=12104 DEFAULT CHARSET=utf8; 66 | /*!40101 SET character_set_client = @saved_cs_client */; 67 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 68 | 69 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 70 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 71 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 72 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 73 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 74 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 75 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 76 | 77 | -- Dump completed on 2018-08-30 10:09:26 78 | --------------------------------------------------------------------------------