-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathsqlparse.php
More file actions
executable file
·370 lines (333 loc) · 13.1 KB
/
sqlparse.php
File metadata and controls
executable file
·370 lines (333 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
<?php
class CreateSqlParser
{
const RAND_INT = 'rand_int';
const RAND_FLOAT = 'rand_float';
const INCR_INT = 'incr_int';
const INCR_DAY = 'incr_day';
const INCR_DAY_GROUPLY = 'incr_day_grouply';
const RAND_TIMESTAMP = 'rand_timestamp';
const RAND_TIMESTAMP_MYSQL = 'rand_timestamp_mysql';
const IGNORE = 'ignore';
const INCR_STR_PREFIX = 'incr_str_prefix';
const RAND_STR = 'rand_str';
const RAND_STR_LIST = 'rand_str_list';
const CONST_STR = 'const_str';
const CONST_STR_LIST = 'const_str_list';
const RAND_PIC_URL = 'rand_pic_url';
const errorParseError = 1001;
public function getApiReturn($error, $msg, $data)
{
$ret['error'] = $error;
$ret['msg'] = $msg;
$ret['data'] = $data;
return $ret;
}
/**
* 解析字段名没有 ` 的SQL,正则以空格为分隔符,常见软件: datagrip
* 正则匹配规则: 若干空格 + 不含空格的字符串(字段名) + 空格 + 不含空格的字符串(类型) + 空格 + 包含换行的任意字符(额外信息) + 逗号或者)[其中右括号结尾是表里没有任何索引的情况],支持跨行匹配
* @param $sql
CREATE TABLE t_supplier_product
(
id INT AUTO_INCREMENT
PRIMARY KEY,
supplier_id INT NOT NULL
COMMENT '供应商id',
product_detail_id INT NOT NULL
COMMENT '单品id',
price DOUBLE NOT NULL
COMMENT '采购价',
KEY (`supplier_id`)
)
COMMENT '供应商货品'
ENGINE = InnoDB
CHARSET = utf8;
* @return array 每个元素包含如下字段
* origin: 原SQL
* key: 字段名
* type: 类型,包含可选的长度 int(11) 、text
* others: 其他,NOT NULL AUTO_INCREMENT COMMENT '我是注释' 这种
*/
private function parseWithoutBackQuote($sql){
$content = explode('(',$sql,2); //先拿到 create table 之后的避免 正则把 第一列 吃了
$sql = $content[1];
//提前删除 INDEX 相关 INDEX cust_id (cust_id) USING BTREE,
$sql = preg_replace("# *INDEX.+#","",$sql);
$pattern = "#( *)([^\s]+) ([^\s]+) ([\s\S]+?)[,)]#im";
preg_match_all($pattern, $sql, $matches);
$ret = [];
for ($cnt = 0; $cnt < count($matches[0]); $cnt++) {
if ( false !== stripos($matches[3][$cnt], 'KEY')
|| false !== stripos($matches[2][$cnt], 'KEY') ) { //排除 PRIMARY KEY (id) 和 KEY (id) ,虽然也可以像上面那样正则替换掉线
continue;
}
$item = [
'origin' => $matches[0][$cnt],
'key' => $matches[2][$cnt],
'type' => $matches[3][$cnt],
'others' => $matches[4][$cnt],
];
$ret []= $item;
}
return $ret;
}
/**
* 解析字段名有 ` 包含的SQL,常见软件:Navicat
* 正则匹配规则: 任意字符若干(空格或KEY) + `列名`+ 空格 + 非空字符串(类型) + 空格 + 额外信息 + 逗号或者),其中右括号结尾是表里没有任何索引的情况,支持跨行匹配
* 如果需要改动,需要注意: COMMENT 换行、 表无索引时最后个字段的情况,目前以 ` 来区分字段而非空格
* @param $sql
CREATE TABLE `im_feed` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) NOT NULL DEFAULT '0',
`user_id` bigint(11) NOT NULL DEFAULT '0' COMMENT '学号或者老师工号',
`content` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '',
`is_deleted` tinyint(4) NOT NULL DEFAULT '0',
`photos` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '',
`create_time` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
* @return array 每个元素包含如下字段
* origin: 原SQL
* key: 字段名
* type: 类型,包含可选的长度 int(11) 、text
* others: 其他,NOT NULL AUTO_INCREMENT COMMENT '我是注释' 这种
*/
private function parseWithBackQuote($sql){
//提前删除 INDEX 相关 INDEX cust_id (cust_id) USING BTREE,
$sql = preg_replace("# *INDEX.+#","",$sql);
$pattern = "#(.*)`(.+)` ([^\s]+) ([\s\S]+?)[,)]#im";
preg_match_all($pattern, $sql, $matches);
// echo json_encode($matches);exit();
$ret = [];
for ($cnt = 0; $cnt < count($matches[0]); $cnt++) {
if (false !== stripos($matches[1][$cnt], 'KEY')) { //索引 排除
continue;
}
$item = [
'origin' => $matches[0][$cnt],
'key' => $matches[2][$cnt],
'type' => $matches[3][$cnt],
'others' => $matches[4][$cnt],
];
$ret []= $item;
}
return $ret;
}
public function execute($input)
{
$sql = $input;
$ret = [];
//解析表名,兼容以下两种,返回的表名带不带 `都可以成功插入
// CREATE TABLE `test` (
// CREATE TABLE t_supplier_product
// (
$sql = str_replace("IF NOT EXISTS","",$sql);
$pattern = "#CREATE TABLE (.+?)[\s]#i";
preg_match($pattern, $sql, $matches);
if (empty($matches)) {
return $this->getApiReturn(self::errorParseError, '不是建表SQL,未包含 CREATE TABLE', []);
}
$ret['table_name'] = $matches[1];
//解析字段,推荐个在线正则网站 https://regexr.com/
$matchList = $this->parseWithBackQuote($sql); //带`的解析失败则用不带`的解析,大部分SQL是带`的
if (empty($matchList)) {
$matchList = $this->parseWithoutBackQuote($sql);
}
$ret['list'] = [];
if (empty($matchList)) {
return $this->getApiReturn(self::errorParseError, '未查找到SQL字段', []);
}
// echo json_encode($matchList); exit();
// 解析后拿到的每个item:
// "origin":" `baoguang_pv` int(11) NOT NULL DEFAULT '0' COMMENT '昨日曝光pv',",
// "key":"baoguang_pv", ======= 字段名
// "type":"bigint(20)", ======= 类型,包含可选的长度 int(11) 、text
// "others":"NOT NULL DEFAULT '0' COMMENT '昨日曝光pv'" ======= 其他,NOT NULL AUTO_INCREMENT COMMENT '我是注释' 这种
foreach ($matchList as $item){
$size = 0;
$type = $item['type'];
$sizeArr = explode('(',$item['type']);
//如果有()说明是有数字的那种
if(!empty($sizeArr) && count($sizeArr) >= 2 ) {
$type = $sizeArr[0];
$size = explode( ")" ,$sizeArr[1] )[0] ;
}
$entry = $this->genDefaultAttribute($item['key'], $type , $size, $item['others']);
$entry['key'] = $item['key'];
$ret['list'] [] = $entry;
}
$ret['group_size'] = 5; //组大小
$ret['count'] = 3; //多少条SQL
return $this->getApiReturn(0, '', $ret);
}
/**
* @param $key
* @param $type string SQL的字段类型 varchar,int
* @param $size string SQL的字段类型后跟随的大小 如 varchar(10) 中的10
* @param $others string varchar(10) 后面的一串其他完整内容,包含 自增、非空、默认值等
* @return array item 必备包含 'key' 字段名 'method' 生成规则 'value' 默认值
*/
private function genDefaultAttribute($key, $type, $size, $others)
{
$type = strtolower(trim($type));
$incrStrPre = ['Boss', 'Player', 'Test', 'PM', 'Programmer', 'Worker', 'Actor', 'SB'];
switch ($type) {
case 'varchar':
$item = [
'desc' => '前缀+自增',
'method' => self::INCR_STR_PREFIX,
'value' => $incrStrPre[rand(0, count($incrStrPre) - 1)],
];
break;
case 'int':
case 'mediumint':
case 'integer':
$item = [
'desc' => '随机整数',
'method' => self::RAND_INT,
'value' => '100,500',
];
break;
case 'bigint':
$item = [
'desc' => '随机整数',
'method' => self::RAND_INT,
'value' => "1000000,99999999",
];
break;
case 'tinyint':
$item = [
'desc' => '随机整数',
'method' => self::RAND_INT,
'value' => "0,{$size}", //tinyint(4) 一般是0-4的枚举值
];
break;
case 'float':
case 'double':
$item = [
'desc' => '随机浮点',
'method' => self::RAND_FLOAT,
'value' => '1,10,5',
];
break;
case 'date': //这个待定
case 'datetime':
case 'timestamp':
$item = [
'method' => self::RAND_TIMESTAMP_MYSQL,
'value' => '20180407,20180408',
];
break;
case 'text':
default:
$item = [
'method' => self::CONST_STR,
'value' => '1',
];
break;
}
//自增ID
$autoInc = stripos($others, "AUTO_INCREMENT");
if ($autoInc !== false) {
$item = [
'method' => self::IGNORE,
'desc' => '自增ID,忽略',
];
}
//注释
$expComment = explode("COMMENT ", $others);
if (!empty($expComment) && count($expComment) >= 2 ) {
$item['comment'] = trim($expComment[1], "',");
}
//通用配置
$commonItem = $this->parseFileForAttribute('conf/common.ini',$key);
if(!empty($commonItem)){
$item = $commonItem;
}
//个性化配置
//针对你、你公司 常用的字段设置默认值,存放不可告人的数据秘密,配置文件在.gitignore里
$localItem = $this->parseFileForAttribute('conf/local.ini',$key);
if(!empty($localItem)){
$item = $localItem;
}
return $item;
}
/**
* 解析ini文件拿到自定义默认值,根据字段名猜测用户想要的是哪个类型,配置文件样例如下,目前只支持 模糊查找 和 精确匹配
[0]
key = avatar
method = RAND_PIC_URL
value = 300,400
way = search 跟key的匹配方式,search为模糊搜索,输入key包含 avatar 就走这个匹配
[1]
key = index_day
method = INCR_DAY
value = 20180301
way = match match为精确匹配,输入key 等于 index_day 就走这个匹配
*
*
* @param $fileName string 配置文件路径
* @param $key string 字段名
* @return array
*/
private function parseFileForAttribute($fileName,$key)
{
if(!file_exists($fileName)){
return [];
}
$item = [];
$matchArray = parse_ini_file($fileName,true);
foreach ($matchArray as $match) {
if(empty($match['way'])){ //默认key规则为相等
$match['way'] = 'match';
}
if($match['way'] == 'search' && false !== stripos($key, $match['key'])){
$item = $match;
$item['method'] = strtolower($item['method']);
}else if ($match['way'] == 'match' && $key == $match['key']) {
$item = $match;
$item['method'] = strtolower($item['method']);
}
}
unset($item['way']);
return $item;
}
}
date_default_timezone_set("Asia/Shanghai");
$defaultSql = "
CREATE TABLE `im_feed` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) NOT NULL DEFAULT '0',
`user_id` bigint(11) NOT NULL DEFAULT '0' COMMENT '学号或者老师工号',
`content` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '',
`is_deleted` tinyint(4) NOT NULL DEFAULT '0',
`photos` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '',
`create_time` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
";
$defaultSql2 = "
CREATE TABLE t_supplier_product
(
id INT AUTO_INCREMENT
PRIMARY KEY,
supplier_id INT NOT NULL
COMMENT '供应商id',
product_detail_id INT NOT NULL
COMMENT '单品id',
price DOUBLE NOT NULL
COMMENT '采购价',
KEY (`supplier_id`)
)
COMMENT '供应商货品'
ENGINE = InnoDB
CHARSET = utf8;
";
$parser = new CreateSqlParser();
if(empty($_POST['sql'])){
$sql = $defaultSql;
}else{
$sql = $_POST['sql'];
}
$ret = $parser->execute($sql);
echo json_encode($ret);exit();