|
1
|
|
|
<?php |
|
2
|
|
|
declare(strict_types=1); |
|
3
|
|
|
|
|
4
|
|
|
namespace SetBased\Stratum\MySql\Helper; |
|
5
|
|
|
|
|
6
|
|
|
use SetBased\Exception\FallenException; |
|
7
|
|
|
use SetBased\Helper\Cast; |
|
8
|
|
|
use SetBased\Helper\InvalidCastException; |
|
9
|
|
|
use SetBased\Stratum\Backend\StratumStyle; |
|
10
|
|
|
use SetBased\Stratum\Common\DocBlock\DocBlockReflection; |
|
11
|
|
|
use SetBased\Stratum\Common\Exception\RoutineLoaderException; |
|
12
|
|
|
use SetBased\Stratum\Common\Helper\Util; |
|
13
|
|
|
use SetBased\Stratum\Middle\Exception\ResultException; |
|
14
|
|
|
use SetBased\Stratum\MySql\Exception\MySqlQueryErrorException; |
|
15
|
|
|
use SetBased\Stratum\MySql\MySqlMetaDataLayer; |
|
16
|
|
|
use Symfony\Component\Console\Formatter\OutputFormatter; |
|
17
|
|
|
|
|
18
|
|
|
/** |
|
19
|
|
|
* Class for loading a single stored routine into a MySQL instance from pseudo SQL file. |
|
20
|
|
|
*/ |
|
21
|
|
|
class RoutineLoaderHelper |
|
22
|
|
|
{ |
|
23
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
24
|
|
|
/** |
|
25
|
|
|
* MySQL's and MariaDB's SQL/PSM syntax. |
|
26
|
|
|
*/ |
|
27
|
|
|
const SQL_PSM_SYNTAX = 1; |
|
28
|
|
|
|
|
29
|
|
|
/** |
|
30
|
|
|
* Oracle PL/SQL syntax. |
|
31
|
|
|
*/ |
|
32
|
|
|
const PL_SQL_SYNTAX = 2; |
|
33
|
|
|
|
|
34
|
|
|
/** |
|
35
|
|
|
* The revision of the metadata of the stored routines. |
|
36
|
|
|
*/ |
|
37
|
|
|
const METADATA_REVISION = '4'; |
|
38
|
|
|
|
|
39
|
|
|
/** |
|
40
|
|
|
* The metadata of the table columns of the table for bulk insert. |
|
41
|
|
|
* |
|
42
|
|
|
* @var array[] |
|
43
|
|
|
*/ |
|
44
|
|
|
private array $bulkInsertColumns; |
|
45
|
|
|
|
|
46
|
|
|
/** |
|
47
|
|
|
* The keys in the nested array for bulk inserting data. |
|
48
|
|
|
* |
|
49
|
|
|
* @var string[] |
|
50
|
|
|
*/ |
|
51
|
|
|
private array $bulkInsertKeys; |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* The name of table for bulk insert. |
|
55
|
|
|
* |
|
56
|
|
|
* @var string |
|
57
|
|
|
*/ |
|
58
|
|
|
private string $bulkInsertTableName; |
|
59
|
|
|
|
|
60
|
|
|
/** |
|
61
|
|
|
* The default character set under which the stored routine will be loaded and run. |
|
62
|
|
|
* |
|
63
|
|
|
* @var string |
|
64
|
|
|
*/ |
|
65
|
|
|
private string $characterSet; |
|
66
|
|
|
|
|
67
|
|
|
/** |
|
68
|
|
|
* The default collate under which the stored routine will be loaded and run. |
|
69
|
|
|
* |
|
70
|
|
|
* @var string |
|
71
|
|
|
*/ |
|
72
|
|
|
private string $collate; |
|
73
|
|
|
|
|
74
|
|
|
/** |
|
75
|
|
|
* The designation type of the stored routine. |
|
76
|
|
|
* |
|
77
|
|
|
* @var string |
|
78
|
|
|
*/ |
|
79
|
|
|
private string $designationType; |
|
80
|
|
|
|
|
81
|
|
|
/** |
|
82
|
|
|
* The meta data layer. |
|
83
|
|
|
* |
|
84
|
|
|
* @var MySqlMetaDataLayer |
|
85
|
|
|
*/ |
|
86
|
|
|
private MySqlMetaDataLayer $dl; |
|
87
|
|
|
|
|
88
|
|
|
/** |
|
89
|
|
|
* The DocBlock reflection object. |
|
90
|
|
|
* |
|
91
|
|
|
* @var DocBlockReflection|null |
|
92
|
|
|
*/ |
|
93
|
|
|
private ?DocBlockReflection $docBlockReflection; |
|
94
|
|
|
|
|
95
|
|
|
/** |
|
96
|
|
|
* The last modification time of the source file. |
|
97
|
|
|
* |
|
98
|
|
|
* @var int |
|
99
|
|
|
*/ |
|
100
|
|
|
private int $filemtime; |
|
101
|
|
|
|
|
102
|
|
|
/** |
|
103
|
|
|
* The key or index columns (depending on the designation type) of the stored routine. |
|
104
|
|
|
* |
|
105
|
|
|
* @var string[] |
|
106
|
|
|
*/ |
|
107
|
|
|
private array $indexColumns; |
|
108
|
|
|
|
|
109
|
|
|
/** |
|
110
|
|
|
* The Output decorator. |
|
111
|
|
|
* |
|
112
|
|
|
* @var StratumStyle |
|
113
|
|
|
*/ |
|
114
|
|
|
private StratumStyle $io; |
|
115
|
|
|
|
|
116
|
|
|
/** |
|
117
|
|
|
* The metadata of the stored routine. Note: this data is stored in the metadata file and is generated by PhpStratum. |
|
118
|
|
|
* |
|
119
|
|
|
* @var array |
|
120
|
|
|
*/ |
|
121
|
|
|
private array $phpStratumMetadata; |
|
122
|
|
|
|
|
123
|
|
|
/** |
|
124
|
|
|
* The old metadata of the stored routine. Note: this data comes from the metadata file. |
|
125
|
|
|
* |
|
126
|
|
|
* @var array |
|
127
|
|
|
*/ |
|
128
|
|
|
private array $phpStratumOldMetadata; |
|
129
|
|
|
|
|
130
|
|
|
/** |
|
131
|
|
|
* A map from all possible placeholders to their actual values. |
|
132
|
|
|
* |
|
133
|
|
|
* @var array |
|
134
|
|
|
*/ |
|
135
|
|
|
private array $placeholderPool; |
|
136
|
|
|
|
|
137
|
|
|
/** |
|
138
|
|
|
* A map from placeholders that are actually used in the stored routine to their values. |
|
139
|
|
|
* |
|
140
|
|
|
* @var array |
|
141
|
|
|
*/ |
|
142
|
|
|
private array $placeholders = []; |
|
143
|
|
|
|
|
144
|
|
|
/** |
|
145
|
|
|
* The old metadata of the stored routine. Note: this data comes from information_schema.ROUTINES. |
|
146
|
|
|
* |
|
147
|
|
|
* @var array |
|
148
|
|
|
*/ |
|
149
|
|
|
private array $rdbmsOldRoutineMetadata; |
|
150
|
|
|
|
|
151
|
|
|
/** |
|
152
|
|
|
* The return type of the stored routine (only if designation type singleton0, singleton1, or function). |
|
153
|
|
|
* |
|
154
|
|
|
* @var string|null |
|
155
|
|
|
*/ |
|
156
|
|
|
private ?string $returnType = null; |
|
157
|
|
|
|
|
158
|
|
|
/** |
|
159
|
|
|
* The name of the stored routine. |
|
160
|
|
|
* |
|
161
|
|
|
* @var string |
|
162
|
|
|
*/ |
|
163
|
|
|
private string $routineName; |
|
164
|
|
|
|
|
165
|
|
|
/** |
|
166
|
|
|
* The routine parameters. |
|
167
|
|
|
* |
|
168
|
|
|
* @var RoutineParametersHelper|null |
|
169
|
|
|
*/ |
|
170
|
|
|
private ?RoutineParametersHelper $routineParameters = null; |
|
171
|
|
|
|
|
172
|
|
|
/** |
|
173
|
|
|
* The source code as a single string of the stored routine. |
|
174
|
|
|
* |
|
175
|
|
|
* @var string |
|
176
|
|
|
*/ |
|
177
|
|
|
private string $routineSourceCode; |
|
178
|
|
|
|
|
179
|
|
|
/** |
|
180
|
|
|
* The source code as an array of lines string of the stored routine. |
|
181
|
|
|
* |
|
182
|
|
|
* @var array |
|
183
|
|
|
*/ |
|
184
|
|
|
private array $routineSourceCodeLines; |
|
185
|
|
|
|
|
186
|
|
|
/** |
|
187
|
|
|
* The source filename holding the stored routine. |
|
188
|
|
|
* |
|
189
|
|
|
* @var string |
|
190
|
|
|
*/ |
|
191
|
|
|
private string $sourceFilename; |
|
192
|
|
|
|
|
193
|
|
|
/** |
|
194
|
|
|
* The SQL mode helper object. |
|
195
|
|
|
* |
|
196
|
|
|
* @var SqlModeHelper |
|
197
|
|
|
*/ |
|
198
|
|
|
private SqlModeHelper $sqlModeHelper; |
|
199
|
|
|
|
|
200
|
|
|
/** |
|
201
|
|
|
* The syntax of the stored routine. Either SQL_PSM_SYNTAX or PL_SQL_SYNTAX. |
|
202
|
|
|
* |
|
203
|
|
|
* @var int |
|
204
|
|
|
*/ |
|
205
|
|
|
private int $syntax; |
|
206
|
|
|
|
|
207
|
|
|
/** |
|
208
|
|
|
* A map from all possible table and column names to their actual column type. |
|
209
|
|
|
* |
|
210
|
|
|
* @var array |
|
211
|
|
|
*/ |
|
212
|
|
|
private array $typeHintPool; |
|
213
|
|
|
|
|
214
|
|
|
/** |
|
215
|
|
|
* A map from the table and column names that are actually use as type hint in the stored routine to their actual |
|
216
|
|
|
* column type. |
|
217
|
|
|
* |
|
218
|
|
|
* @var array |
|
219
|
|
|
*/ |
|
220
|
|
|
private array $typeHints = []; |
|
221
|
|
|
|
|
222
|
1 |
|
//-------------------------------------------------------------------------------------------------------------------- |
|
223
|
|
|
|
|
224
|
|
|
/** |
|
225
|
|
|
* Object constructor. |
|
226
|
|
|
* |
|
227
|
|
|
* @param MySqlMetaDataLayer $dl The meta data layer. |
|
228
|
|
|
* @param StratumStyle $io The output for log messages. |
|
229
|
|
|
* @param SqlModeHelper $sqlModeHelper |
|
230
|
|
|
* @param string $routineFilename The filename of the source of the stored routine. |
|
231
|
|
|
* @param array $phpStratumMetadata The metadata of the stored routine from PhpStratum. |
|
232
|
1 |
|
* @param array $placeholderPool A map from placeholders to their actual values. |
|
233
|
1 |
|
* @param array $typeHintPool A map from type hints to their actual types. |
|
234
|
1 |
|
* @param array $rdbmsOldRoutineMetadata The old metadata of the stored routine from MySQL. |
|
235
|
1 |
|
* @param string $characterSet The default character set under which the stored routine will |
|
236
|
1 |
|
* be loaded and run. |
|
237
|
1 |
|
* @param string $collate The key or index columns (depending on the designation type) of |
|
238
|
1 |
|
* the stored routine. |
|
239
|
1 |
|
*/ |
|
240
|
1 |
|
public function __construct(MySqlMetaDataLayer $dl, |
|
241
|
|
|
StratumStyle $io, |
|
242
|
|
|
SqlModeHelper $sqlModeHelper, |
|
243
|
|
|
string $routineFilename, |
|
244
|
|
|
array $phpStratumMetadata, |
|
245
|
|
|
array $placeholderPool, |
|
246
|
|
|
array $typeHintPool, |
|
247
|
|
|
array $rdbmsOldRoutineMetadata, |
|
248
|
|
|
string $characterSet, |
|
249
|
|
|
string $collate) |
|
250
|
|
|
{ |
|
251
|
|
|
$this->dl = $dl; |
|
252
|
|
|
$this->io = $io; |
|
253
|
1 |
|
$this->sqlModeHelper = $sqlModeHelper; |
|
254
|
|
|
$this->sourceFilename = $routineFilename; |
|
255
|
1 |
|
$this->phpStratumMetadata = $phpStratumMetadata; |
|
256
|
|
|
$this->placeholderPool = $placeholderPool; |
|
257
|
1 |
|
$this->typeHintPool = $typeHintPool; |
|
258
|
|
|
$this->rdbmsOldRoutineMetadata = $rdbmsOldRoutineMetadata; |
|
259
|
1 |
|
$this->characterSet = $characterSet; |
|
260
|
|
|
$this->collate = $collate; |
|
261
|
1 |
|
} |
|
262
|
1 |
|
|
|
263
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
264
|
|
|
/** |
|
265
|
1 |
|
* Extract column metadata from the rows returned by the SQL statement 'describe table'. |
|
266
|
|
|
* |
|
267
|
1 |
|
* @param array $description The description of the table. |
|
268
|
|
|
* |
|
269
|
1 |
|
* @return array |
|
270
|
1 |
|
* |
|
271
|
1 |
|
* @throws InvalidCastException |
|
272
|
1 |
|
*/ |
|
273
|
1 |
|
private static function extractColumnsFromTableDescription(array $description): array |
|
274
|
|
|
{ |
|
275
|
1 |
|
$ret = []; |
|
276
|
1 |
|
|
|
277
|
1 |
|
foreach ($description as $column) |
|
278
|
1 |
|
{ |
|
279
|
1 |
|
preg_match('/^(?<data_type>\w+)(?<extra>.*)?$/', $column['Type'], $parts1); |
|
280
|
|
|
|
|
281
|
1 |
|
$tmp = ['column_name' => $column['Field'], |
|
282
|
1 |
|
'data_type' => $parts1['data_type'], |
|
283
|
1 |
|
'numeric_precision' => null, |
|
284
|
1 |
|
'numeric_scale' => null, |
|
285
|
1 |
|
'dtd_identifier' => $column['Type']]; |
|
286
|
|
|
|
|
287
|
1 |
|
switch ($parts1[1]) |
|
288
|
1 |
|
{ |
|
289
|
1 |
|
case 'tinyint': |
|
290
|
1 |
|
preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2); |
|
291
|
1 |
|
$tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 4); |
|
292
|
|
|
$tmp['numeric_scale'] = 0; |
|
293
|
1 |
|
break; |
|
294
|
1 |
|
|
|
295
|
1 |
|
case 'smallint': |
|
296
|
1 |
|
preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2); |
|
297
|
1 |
|
$tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 6); |
|
298
|
|
|
$tmp['numeric_scale'] = 0; |
|
299
|
1 |
|
break; |
|
300
|
|
|
|
|
301
|
1 |
|
case 'mediumint': |
|
302
|
|
|
preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2); |
|
303
|
1 |
|
$tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 9); |
|
304
|
1 |
|
$tmp['numeric_scale'] = 0; |
|
305
|
1 |
|
break; |
|
306
|
|
|
|
|
307
|
1 |
|
case 'int': |
|
308
|
1 |
|
preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2); |
|
309
|
1 |
|
$tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 11); |
|
310
|
|
|
$tmp['numeric_scale'] = 0; |
|
311
|
1 |
|
break; |
|
312
|
1 |
|
|
|
313
|
1 |
|
case 'bigint': |
|
314
|
1 |
|
preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2); |
|
315
|
|
|
$tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 20); |
|
316
|
1 |
|
$tmp['numeric_scale'] = 0; |
|
317
|
|
|
break; |
|
318
|
1 |
|
|
|
319
|
1 |
|
case 'year': |
|
320
|
1 |
|
// Nothing to do. |
|
321
|
1 |
|
break; |
|
322
|
1 |
|
|
|
323
|
|
|
case 'float': |
|
324
|
1 |
|
$tmp['numeric_precision'] = 12; |
|
325
|
1 |
|
break; |
|
326
|
1 |
|
|
|
327
|
1 |
|
case 'double': |
|
328
|
|
|
$tmp['numeric_precision'] = 22; |
|
329
|
1 |
|
break; |
|
330
|
|
|
|
|
331
|
1 |
|
case 'binary': |
|
332
|
1 |
|
case 'char': |
|
333
|
|
|
case 'varbinary': |
|
334
|
1 |
|
case 'varchar': |
|
335
|
|
|
// Nothing to do (binary) strings. |
|
336
|
1 |
|
break; |
|
337
|
1 |
|
|
|
338
|
1 |
|
case 'decimal': |
|
339
|
1 |
|
preg_match('/^\((?<precision>\d+),(<?scale>\d+)\)$/', $parts1['extra'], $parts2); |
|
340
|
|
|
$tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 65); |
|
341
|
|
|
$tmp['numeric_scale'] = Cast::toManInt($parts2['scale'] ?? 0); |
|
342
|
|
|
break; |
|
343
|
|
|
|
|
344
|
|
|
case 'time': |
|
345
|
|
|
case 'timestamp': |
|
346
|
|
|
case 'date': |
|
347
|
|
|
case 'datetime': |
|
348
|
|
|
// Nothing to do date and time. |
|
349
|
|
|
break; |
|
350
|
|
|
|
|
351
|
|
|
case 'enum': |
|
352
|
|
|
case 'set': |
|
353
|
|
|
// Nothing to do sets. |
|
354
|
|
|
break; |
|
355
|
|
|
|
|
356
|
1 |
|
case 'bit': |
|
357
|
|
|
preg_match('/^\((?<precision>\d+)\)$/', $parts1['extra'], $parts2); |
|
358
|
|
|
$tmp['numeric_precision'] = Cast::toManInt($parts2['precision']); |
|
359
|
1 |
|
break; |
|
360
|
|
|
|
|
361
|
|
|
case 'tinytext': |
|
362
|
|
|
case 'text': |
|
363
|
|
|
case 'mediumtext': |
|
364
|
|
|
case 'longtext': |
|
365
|
|
|
case 'tinyblob': |
|
366
|
|
|
case 'blob': |
|
367
|
|
|
case 'mediumblob': |
|
368
|
|
|
case 'longblob': |
|
369
|
|
|
// Nothing to do CLOBs and BLOBs. |
|
370
|
|
|
break; |
|
371
|
|
|
|
|
372
|
|
|
default: |
|
373
|
1 |
|
throw new FallenException('data type', $parts1[1]); |
|
374
|
|
|
} |
|
375
|
1 |
|
|
|
376
|
1 |
|
$ret[] = $tmp; |
|
377
|
1 |
|
} |
|
378
|
|
|
|
|
379
|
1 |
|
return $ret; |
|
380
|
1 |
|
} |
|
381
|
|
|
|
|
382
|
1 |
|
//-------------------------------------------------------------------------------------------------------------------- |
|
383
|
|
|
/** |
|
384
|
1 |
|
* Loads the stored routine into the instance of MySQL and returns the metadata of the stored routine. |
|
385
|
1 |
|
* |
|
386
|
1 |
|
* @return array |
|
387
|
1 |
|
* |
|
388
|
1 |
|
* @throws RoutineLoaderException |
|
389
|
1 |
|
* @throws MySqlQueryErrorException |
|
390
|
1 |
|
* @throws ResultException |
|
391
|
|
|
* @throws InvalidCastException |
|
392
|
1 |
|
*/ |
|
393
|
|
|
public function loadStoredRoutine(): array |
|
394
|
1 |
|
{ |
|
395
|
1 |
|
$this->routineName = pathinfo($this->sourceFilename, PATHINFO_FILENAME); |
|
|
|
|
|
|
396
|
1 |
|
$this->phpStratumOldMetadata = $this->phpStratumMetadata; |
|
397
|
|
|
$this->filemtime = filemtime($this->sourceFilename); |
|
398
|
|
|
|
|
399
|
1 |
|
$load = $this->mustLoadStoredRoutine(); |
|
400
|
|
|
if ($load) |
|
401
|
|
|
{ |
|
402
|
|
|
$this->io->text(sprintf('Loading routine <dbo>%s</dbo>', OutputFormatter::escape($this->routineName))); |
|
403
|
|
|
|
|
404
|
|
|
$this->readSourceCode(); |
|
405
|
|
|
$this->updateSourceTypeHints(); |
|
406
|
|
|
$this->extractPlaceholders(); |
|
407
|
|
|
$this->extractDesignationType(); |
|
408
|
1 |
|
$this->extractReturnType(); |
|
409
|
|
|
$this->extractRoutineTypeAndName(); |
|
410
|
1 |
|
$this->extractSyntax(); |
|
411
|
|
|
$this->validateReturnType(); |
|
412
|
|
|
$this->loadRoutineFile(); |
|
413
|
|
|
$this->extractBulkInsertTableColumnsInfo(); |
|
414
|
|
|
$this->extractParameters(); |
|
415
|
|
|
$this->updateMetadata(); |
|
416
|
|
|
} |
|
417
|
|
|
|
|
418
|
|
|
return $this->phpStratumMetadata; |
|
419
|
|
|
} |
|
420
|
|
|
|
|
421
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
422
|
|
|
/** |
|
423
|
|
|
* Drops the stored routine if it exists. |
|
424
|
|
|
* |
|
425
|
1 |
|
* @throws MySqlQueryErrorException |
|
426
|
|
|
*/ |
|
427
|
|
|
private function dropRoutineIfExists(): void |
|
428
|
1 |
|
{ |
|
429
|
|
|
if (!empty($this->rdbmsOldRoutineMetadata)) |
|
430
|
|
|
{ |
|
431
|
1 |
|
$this->dl->dropRoutine($this->rdbmsOldRoutineMetadata['routine_type'], $this->routineName); |
|
432
|
|
|
} |
|
433
|
|
|
} |
|
434
|
1 |
|
|
|
435
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
436
|
1 |
|
/** |
|
437
|
|
|
* Extracts the column names and column types of the current table for bulk insert. |
|
438
|
|
|
* |
|
439
|
|
|
* @throws InvalidCastException |
|
440
|
1 |
|
* @throws MySqlQueryErrorException |
|
441
|
|
|
* @throws ResultException |
|
442
|
|
|
* @throws RoutineLoaderException |
|
443
|
1 |
|
*/ |
|
444
|
|
|
private function extractBulkInsertTableColumnsInfo(): void |
|
445
|
1 |
|
{ |
|
446
|
|
|
// Return immediately if designation type is not appropriate for this method. |
|
447
|
|
|
if ($this->designationType!=='bulk_insert') |
|
448
|
|
|
{ |
|
449
|
1 |
|
return; |
|
450
|
1 |
|
} |
|
451
|
1 |
|
|
|
452
|
|
|
// Check if table is a temporary table or a non-temporary table. |
|
453
|
|
|
$isTemporaryTable = !$this->dl->checkTableExists($this->bulkInsertTableName); |
|
454
|
|
|
|
|
455
|
|
|
// Create temporary table if table is a temporary table. |
|
456
|
1 |
|
if ($isTemporaryTable) |
|
457
|
|
|
{ |
|
458
|
|
|
$this->dl->callProcedure($this->routineName); |
|
459
|
|
|
} |
|
460
|
|
|
|
|
461
|
|
|
// Get information about the columns of the table. |
|
462
|
|
|
$description = $this->dl->describeTable($this->bulkInsertTableName); |
|
463
|
|
|
|
|
464
|
|
|
// Drop temporary table if table is temporary. |
|
465
|
1 |
|
if ($isTemporaryTable) |
|
466
|
|
|
{ |
|
467
|
1 |
|
$this->dl->dropTemporaryTable($this->bulkInsertTableName); |
|
468
|
1 |
|
} |
|
469
|
|
|
|
|
470
|
|
|
// Check number of columns in the table match the number of fields given in the designation type. |
|
471
|
|
|
$n1 = sizeof($this->bulkInsertKeys); |
|
472
|
1 |
|
$n2 = sizeof($description); |
|
473
|
|
|
if ($n1!=$n2) |
|
474
|
|
|
{ |
|
475
|
|
|
throw new RoutineLoaderException("Number of fields %d and number of columns %d don't match.", $n1, $n2); |
|
476
|
|
|
} |
|
477
|
1 |
|
|
|
478
|
1 |
|
$this->bulkInsertColumns = self::extractColumnsFromTableDescription($description); |
|
479
|
1 |
|
} |
|
480
|
|
|
|
|
481
|
1 |
|
//-------------------------------------------------------------------------------------------------------------------- |
|
482
|
1 |
|
/** |
|
483
|
1 |
|
* Extracts the designation type of the stored routine. |
|
484
|
1 |
|
* |
|
485
|
|
|
* @throws RoutineLoaderException |
|
486
|
|
|
*/ |
|
487
|
|
|
private function extractDesignationType(): void |
|
488
|
1 |
|
{ |
|
489
|
1 |
|
$tags = $this->docBlockReflection->getTags('type'); |
|
490
|
1 |
|
if (count($tags)===0) |
|
491
|
|
|
{ |
|
492
|
1 |
|
throw new RoutineLoaderException('Tag @type not found in DocBlock.'); |
|
493
|
1 |
|
} |
|
494
|
1 |
|
elseif (count($tags)>1) |
|
495
|
1 |
|
{ |
|
496
|
|
|
throw new RoutineLoaderException('Multiple @type tags found in DocBlock.'); |
|
497
|
|
|
} |
|
498
|
|
|
|
|
499
|
1 |
|
$tag = $tags[0]; |
|
500
|
1 |
|
$this->designationType = $tag['arguments']['type']; |
|
501
|
|
|
switch ($this->designationType) |
|
502
|
|
|
{ |
|
503
|
1 |
|
case 'bulk_insert': |
|
504
|
|
|
$tag['arguments']['table'] = $tag['arguments']['ex1']; |
|
505
|
|
|
$tag['arguments']['columns'] = $tag['arguments']['ex2']; |
|
506
|
|
|
if ($tag['arguments']['table']==='' || $tag['arguments']['columns']==='' || $tag['description'][0]!='') |
|
507
|
|
|
{ |
|
508
|
|
|
throw new RoutineLoaderException('Invalid @type tag. Expected: @type bulk_insert <table_name> <columns>'); |
|
509
|
|
|
} |
|
510
|
|
|
$this->bulkInsertTableName = $tag['arguments']['table']; |
|
511
|
|
|
$this->bulkInsertKeys = explode(',', $tag['arguments']['columns']); |
|
512
|
|
|
break; |
|
513
|
|
|
|
|
514
|
1 |
|
case 'rows_with_key': |
|
515
|
|
|
case 'rows_with_index': |
|
516
|
1 |
|
$tag['arguments']['columns'] = $tag['arguments']['ex1']; |
|
517
|
1 |
|
if ($tag['arguments']['columns']==='' || $tag['arguments']['ex2']!=='') |
|
518
|
1 |
|
{ |
|
519
|
|
|
throw new RoutineLoaderException('Invalid @type tag. Expected: @type %s <columns>', $this->designationType); |
|
520
|
|
|
} |
|
521
|
|
|
$this->indexColumns = explode(',', $tag['arguments']['columns']); |
|
522
|
|
|
break; |
|
523
|
|
|
|
|
524
|
|
|
default: |
|
525
|
|
|
if ($tag['arguments']['ex1']!=='' || $tag['arguments']['ex2']!=='') |
|
526
|
|
|
{ |
|
527
|
|
|
throw new RoutineLoaderException('Error: Expected: @type %s', $this->designationType); |
|
528
|
1 |
|
} |
|
529
|
|
|
} |
|
530
|
1 |
|
} |
|
531
|
1 |
|
|
|
532
|
1 |
|
//-------------------------------------------------------------------------------------------------------------------- |
|
533
|
1 |
|
/** |
|
534
|
|
|
* Extracts DocBlock parts to be used by the wrapper generator. |
|
535
|
1 |
|
*/ |
|
536
|
|
|
private function extractDocBlockPartsWrapper(): array |
|
537
|
|
|
{ |
|
538
|
|
|
return ['short_description' => $this->docBlockReflection->getShortDescription(), |
|
539
|
|
|
'long_description' => $this->docBlockReflection->getLongDescription(), |
|
540
|
|
|
'parameters' => $this->routineParameters->extractDocBlockPartsWrapper()]; |
|
541
|
|
|
} |
|
542
|
|
|
|
|
543
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
544
|
1 |
|
/** |
|
545
|
|
|
* Extracts routine parameters. |
|
546
|
1 |
|
* |
|
547
|
|
|
* @throws MySqlQueryErrorException |
|
548
|
1 |
|
* @throws RoutineLoaderException |
|
549
|
1 |
|
*/ |
|
550
|
|
|
private function extractParameters(): void |
|
551
|
1 |
|
{ |
|
552
|
|
|
$this->routineParameters = new RoutineParametersHelper($this->dl, |
|
553
|
1 |
|
$this->io, |
|
554
|
|
|
$this->docBlockReflection, |
|
555
|
1 |
|
$this->routineName); |
|
556
|
|
|
|
|
557
|
|
|
$this->routineParameters->extractRoutineParameters(); |
|
558
|
|
|
} |
|
559
|
|
|
|
|
560
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
561
|
|
|
/** |
|
562
|
|
|
* Extracts the placeholders from the stored routine source. |
|
563
|
|
|
* |
|
564
|
1 |
|
* @throws RoutineLoaderException |
|
565
|
|
|
*/ |
|
566
|
|
|
private function extractPlaceholders(): void |
|
567
|
|
|
{ |
|
568
|
|
|
$unknown = []; |
|
569
|
|
|
|
|
570
|
|
|
preg_match_all('(@[A-Za-z0-9_.]+(%(type|sort))?@)', $this->routineSourceCode, $matches); |
|
571
|
|
|
if (!empty($matches[0])) |
|
572
|
|
|
{ |
|
573
|
1 |
|
foreach ($matches[0] as $placeholder) |
|
574
|
|
|
{ |
|
575
|
1 |
|
if (isset($this->placeholderPool[strtoupper($placeholder)])) |
|
576
|
|
|
{ |
|
577
|
1 |
|
$this->placeholders[$placeholder] = $this->placeholderPool[strtoupper($placeholder)]; |
|
578
|
|
|
} |
|
579
|
1 |
|
else |
|
580
|
1 |
|
{ |
|
581
|
1 |
|
$unknown[] = $placeholder; |
|
582
|
1 |
|
} |
|
583
|
|
|
} |
|
584
|
|
|
} |
|
585
|
|
|
|
|
586
|
1 |
|
$this->logUnknownPlaceholders($unknown); |
|
587
|
1 |
|
} |
|
588
|
|
|
|
|
589
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
590
|
|
|
/** |
|
591
|
1 |
|
* Extracts the return type of the stored routine. |
|
592
|
1 |
|
* |
|
593
|
|
|
* @throws RoutineLoaderException |
|
594
|
|
|
*/ |
|
595
|
1 |
|
private function extractReturnType(): void |
|
596
|
|
|
{ |
|
597
|
|
|
$tags = $this->docBlockReflection->getTags('return'); |
|
598
|
|
|
|
|
599
|
|
|
switch ($this->designationType) |
|
600
|
|
|
{ |
|
601
|
|
|
case 'function': |
|
602
|
|
|
case 'singleton0': |
|
603
|
|
|
case 'singleton1': |
|
604
|
|
|
if (count($tags)===0) |
|
605
|
|
|
{ |
|
606
|
|
|
throw new RoutineLoaderException('Tag @return not found in DocBlock.'); |
|
607
|
|
|
} |
|
608
|
1 |
|
$tag = $tags[0]; |
|
609
|
|
|
if ($tag['arguments']['type']==='') |
|
610
|
1 |
|
{ |
|
611
|
1 |
|
throw new RoutineLoaderException('Invalid return tag. Expected: @return <type>.'); |
|
612
|
|
|
} |
|
613
|
1 |
|
$this->returnType = $tag['arguments']['type']; |
|
614
|
|
|
break; |
|
615
|
1 |
|
|
|
616
|
|
|
default: |
|
617
|
|
|
if (count($tags)!==0) |
|
618
|
|
|
{ |
|
619
|
|
|
throw new RoutineLoaderException('Redundant @type tag found in DocBlock.'); |
|
620
|
|
|
} |
|
621
|
|
|
} |
|
622
|
|
|
} |
|
623
|
|
|
|
|
624
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
625
|
|
|
/** |
|
626
|
|
|
* Extracts the name of the stored routine and the stored routine type (i.e. procedure or function) source. |
|
627
|
|
|
* |
|
628
|
1 |
|
* @throws RoutineLoaderException |
|
629
|
|
|
*/ |
|
630
|
1 |
|
private function extractRoutineTypeAndName(): void |
|
631
|
|
|
{ |
|
632
|
1 |
|
$n = preg_match('/create\\s+(procedure|function)\\s+([a-zA-Z0-9_]+)/i', $this->routineSourceCode, $matches); |
|
633
|
1 |
|
if ($n==1) |
|
634
|
|
|
{ |
|
635
|
1 |
|
if ($this->routineName!=$matches[2]) |
|
636
|
|
|
{ |
|
637
|
|
|
throw new RoutineLoaderException("Stored routine name '%s' does not correspond with filename.", $matches[2]); |
|
638
|
|
|
} |
|
639
|
|
|
} |
|
640
|
|
|
else |
|
641
|
1 |
|
{ |
|
642
|
|
|
throw new RoutineLoaderException('Unable to find the stored routine name and type.'); |
|
643
|
|
|
} |
|
644
|
|
|
} |
|
645
|
|
|
|
|
646
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
647
|
|
|
/** |
|
648
|
|
|
* Detects the syntax of the stored procedure. Either SQL/PSM or PL/SQL. |
|
649
|
|
|
*/ |
|
650
|
|
|
private function extractSyntax(): void |
|
651
|
|
|
{ |
|
652
|
|
|
if ($this->sqlModeHelper->hasOracleMode()) |
|
653
|
|
|
{ |
|
654
|
|
|
$key1 = $this->findFirstMatchingLine('/^\s*(as|is)\s*$/i'); |
|
655
|
|
|
$key2 = $this->findFirstMatchingLine('/^\s*begin\s*$/i'); |
|
656
|
|
|
|
|
657
|
|
|
if ($key1!==null && $key2!==null && $key1<$key2) |
|
658
|
1 |
|
{ |
|
659
|
|
|
$this->syntax = self::PL_SQL_SYNTAX; |
|
660
|
1 |
|
} |
|
661
|
|
|
else |
|
662
|
1 |
|
{ |
|
663
|
|
|
$this->syntax = self::SQL_PSM_SYNTAX; |
|
664
|
1 |
|
} |
|
665
|
|
|
} |
|
666
|
|
|
else |
|
667
|
|
|
{ |
|
668
|
1 |
|
$this->syntax = self::SQL_PSM_SYNTAX; |
|
669
|
|
|
} |
|
670
|
|
|
} |
|
671
|
|
|
|
|
672
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
673
|
|
|
/** |
|
674
|
|
|
* Returns the key of the source line that match a regex pattern. |
|
675
|
|
|
* |
|
676
|
|
|
* @param string $pattern The regex pattern. |
|
677
|
1 |
|
* |
|
678
|
|
|
* @return int|null |
|
679
|
1 |
|
*/ |
|
680
|
|
|
private function findFirstMatchingLine(string $pattern): ?int |
|
681
|
|
|
{ |
|
682
|
|
|
foreach ($this->routineSourceCodeLines as $key => $line) |
|
683
|
|
|
{ |
|
684
|
|
|
if (preg_match($pattern, $line)===1) |
|
685
|
1 |
|
{ |
|
686
|
|
|
return $key; |
|
687
|
|
|
} |
|
688
|
1 |
|
} |
|
689
|
1 |
|
|
|
690
|
1 |
|
return null; |
|
691
|
1 |
|
} |
|
692
|
|
|
|
|
693
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
694
|
|
|
/** |
|
695
|
|
|
* Loads the stored routine into the database. |
|
696
|
|
|
* |
|
697
|
|
|
* @throws MySqlQueryErrorException |
|
698
|
|
|
*/ |
|
699
|
|
|
private function loadRoutineFile(): void |
|
700
|
|
|
{ |
|
701
|
|
|
if ($this->syntax===self::PL_SQL_SYNTAX) |
|
702
|
1 |
|
{ |
|
703
|
|
|
$this->sqlModeHelper->addIfRequiredOracleMode(); |
|
704
|
|
|
} |
|
705
|
1 |
|
else |
|
706
|
|
|
{ |
|
707
|
|
|
$this->sqlModeHelper->removeIfRequiredOracleMode(); |
|
708
|
|
|
} |
|
709
|
|
|
|
|
710
|
|
|
$routineSource = $this->substitutePlaceHolders(); |
|
711
|
|
|
$this->dropRoutineIfExists(); |
|
712
|
|
|
$this->dl->setCharacterSet($this->characterSet, $this->collate); |
|
713
|
|
|
$this->dl->loadRoutine($routineSource); |
|
714
|
|
|
} |
|
715
|
|
|
|
|
716
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
717
|
|
|
/** |
|
718
|
|
|
* Logs the unknown placeholder (if any). |
|
719
|
|
|
* |
|
720
|
|
|
* @param array $unknown The unknown placeholders. |
|
721
|
|
|
* |
|
722
|
|
|
* @throws RoutineLoaderException |
|
723
|
|
|
*/ |
|
724
|
|
|
private function logUnknownPlaceholders(array $unknown): void |
|
725
|
|
|
{ |
|
726
|
|
|
// Return immediately if there are no unknown placeholders. |
|
727
|
|
|
if (empty($unknown)) |
|
728
|
|
|
{ |
|
729
|
1 |
|
return; |
|
730
|
|
|
} |
|
731
|
|
|
|
|
732
|
1 |
|
sort($unknown); |
|
733
|
|
|
$this->io->text('Unknown placeholder(s):'); |
|
734
|
|
|
$this->io->listing($unknown); |
|
735
|
|
|
|
|
736
|
|
|
$replace = []; |
|
737
|
|
|
foreach ($unknown as $placeholder) |
|
738
|
|
|
{ |
|
739
|
|
|
$replace[$placeholder] = '<error>'.$placeholder.'</error>'; |
|
740
|
|
|
} |
|
741
|
|
|
$code = strtr(OutputFormatter::escape($this->routineSourceCode), $replace); |
|
742
|
|
|
|
|
743
|
|
|
$this->io->text(explode(PHP_EOL, $code)); |
|
744
|
|
|
|
|
745
|
|
|
throw new RoutineLoaderException('Unknown placeholder(s) found.'); |
|
746
|
|
|
} |
|
747
|
|
|
|
|
748
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
749
|
|
|
/** |
|
750
|
|
|
* Returns whether the source file must be load or reloaded. |
|
751
|
|
|
* |
|
752
|
|
|
* @return bool |
|
753
|
|
|
*/ |
|
754
|
|
|
private function mustLoadStoredRoutine(): bool |
|
755
|
|
|
{ |
|
756
|
|
|
// If this is the first time we see the source file it must be loaded. |
|
757
|
|
|
if (empty($this->phpStratumOldMetadata)) |
|
758
|
|
|
{ |
|
759
|
|
|
return true; |
|
760
|
|
|
} |
|
761
|
|
|
|
|
762
|
|
|
// If the source file has changed the source file must be loaded. |
|
763
|
|
|
if ($this->phpStratumOldMetadata['timestamp']!==$this->filemtime) |
|
764
|
|
|
{ |
|
765
|
|
|
return true; |
|
766
|
|
|
} |
|
767
|
|
|
|
|
768
|
1 |
|
// If the value of a placeholder has changed the source file must be loaded. |
|
769
|
|
|
foreach ($this->phpStratumOldMetadata['placeholders'] as $placeHolder => $oldValue) |
|
770
|
1 |
|
{ |
|
771
|
1 |
|
if (!isset($this->placeholderPool[strtoupper($placeHolder)]) || $this->placeholderPool[strtoupper($placeHolder)]!==$oldValue) |
|
772
|
|
|
{ |
|
773
|
1 |
|
return true; |
|
774
|
|
|
} |
|
775
|
|
|
} |
|
776
|
|
|
|
|
777
|
|
|
// If the value of a type has changed the source file must be loaded. |
|
778
|
1 |
|
foreach ($this->phpStratumOldMetadata['type_hints'] as $typeHint => $oldValue) |
|
779
|
1 |
|
{ |
|
780
|
1 |
|
if (!isset($this->typeHintPool[$typeHint]) || $this->typeHintPool[$typeHint]!==$oldValue) |
|
781
|
|
|
{ |
|
782
|
1 |
|
return true; |
|
783
|
1 |
|
} |
|
784
|
|
|
} |
|
785
|
|
|
|
|
786
|
|
|
// If stored routine not exists in database the source file must be loaded. |
|
787
|
|
|
if (empty($this->rdbmsOldRoutineMetadata)) |
|
788
|
|
|
{ |
|
789
|
|
|
return true; |
|
790
|
1 |
|
} |
|
791
|
1 |
|
|
|
792
|
1 |
|
// If current sql-mode is different the source file must reload. |
|
793
|
1 |
|
if (!$this->sqlModeHelper->compare($this->rdbmsOldRoutineMetadata['sql_mode'])) |
|
794
|
|
|
{ |
|
795
|
1 |
|
return true; |
|
796
|
|
|
} |
|
797
|
|
|
|
|
798
|
|
|
// If current character set is different the source file must reload. |
|
799
|
|
|
if ($this->rdbmsOldRoutineMetadata['character_set_client']!==$this->characterSet) |
|
800
|
|
|
{ |
|
801
|
|
|
return true; |
|
802
|
|
|
} |
|
803
|
|
|
|
|
804
|
1 |
|
// If current collation is different the source file must reload. |
|
805
|
|
|
if ($this->rdbmsOldRoutineMetadata['collation_connection']!==$this->collate) |
|
806
|
1 |
|
{ |
|
807
|
|
|
return true; |
|
808
|
1 |
|
} |
|
809
|
1 |
|
|
|
810
|
1 |
|
return false; |
|
811
|
|
|
} |
|
812
|
1 |
|
|
|
813
|
1 |
|
//-------------------------------------------------------------------------------------------------------------------- |
|
814
|
1 |
|
/** |
|
815
|
|
|
* Reads the source code of the stored routine. |
|
816
|
1 |
|
* |
|
817
|
1 |
|
* @throws RoutineLoaderException |
|
818
|
|
|
*/ |
|
819
|
1 |
|
private function readSourceCode(): void |
|
820
|
|
|
{ |
|
821
|
1 |
|
$this->routineSourceCode = file_get_contents($this->sourceFilename); |
|
822
|
1 |
|
$this->routineSourceCodeLines = explode(PHP_EOL, $this->routineSourceCode); |
|
823
|
1 |
|
|
|
824
|
1 |
|
if ($this->routineSourceCodeLines===false) |
|
825
|
|
|
{ |
|
826
|
1 |
|
throw new RoutineLoaderException('Source file is empty.'); |
|
827
|
|
|
} |
|
828
|
|
|
|
|
829
|
|
|
$start = $this->findFirstMatchingLine('/^\s*\/\*\*\s*$/'); |
|
830
|
|
|
$end = $this->findFirstMatchingLine('/^\s*\*\/\s*$/'); |
|
831
|
|
|
if ($start!==null && $end!==null && $start<$end) |
|
832
|
|
|
{ |
|
833
|
1 |
|
$lines = array_slice($this->routineSourceCodeLines, $start, $end - $start + 1); |
|
834
|
|
|
$docBlock = implode(PHP_EOL, $lines); |
|
835
|
1 |
|
} |
|
836
|
1 |
|
else |
|
837
|
1 |
|
{ |
|
838
|
1 |
|
$docBlock = ''; |
|
839
|
1 |
|
} |
|
840
|
1 |
|
|
|
841
|
1 |
|
DocBlockReflection::setTagParameters('param', ['name']); |
|
842
|
|
|
DocBlockReflection::setTagParameters('type', ['type', 'ex1', 'ex2']); |
|
843
|
1 |
|
DocBlockReflection::setTagParameters('return', ['type']); |
|
844
|
|
|
DocBlockReflection::setTagParameters('paramAddendum', ['name', 'type', 'delimiter', 'enclosure', 'escape']); |
|
845
|
1 |
|
|
|
846
|
|
|
$this->docBlockReflection = new DocBlockReflection($docBlock); |
|
847
|
|
|
} |
|
848
|
1 |
|
|
|
849
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
850
|
1 |
|
/** |
|
851
|
1 |
|
* Returns the source of the routine with all placeholders substituted with their values. |
|
852
|
1 |
|
* |
|
853
|
|
|
* @return string |
|
854
|
|
|
*/ |
|
855
|
|
|
private function substitutePlaceHolders(): string |
|
856
|
|
|
{ |
|
857
|
|
|
$realpath = realpath($this->sourceFilename); |
|
858
|
|
|
|
|
859
|
|
|
$this->placeholders['__FILE__'] = "'".$this->dl->realEscapeString($realpath)."'"; |
|
860
|
|
|
$this->placeholders['__ROUTINE__'] = "'".$this->routineName."'"; |
|
861
|
|
|
$this->placeholders['__DIR__'] = "'".$this->dl->realEscapeString(dirname($realpath))."'"; |
|
862
|
1 |
|
|
|
863
|
|
|
$lines = explode(PHP_EOL, $this->routineSourceCode); |
|
864
|
|
|
$routineSource = []; |
|
865
|
1 |
|
foreach ($lines as $i => $line) |
|
866
|
|
|
{ |
|
867
|
1 |
|
$this->placeholders['__LINE__'] = $i + 1; |
|
868
|
1 |
|
$routineSource[$i] = strtr($line, $this->placeholders); |
|
869
|
|
|
} |
|
870
|
1 |
|
$routineSource = implode(PHP_EOL, $routineSource); |
|
871
|
|
|
|
|
872
|
|
|
unset($this->placeholders['__FILE__']); |
|
873
|
|
|
unset($this->placeholders['__ROUTINE__']); |
|
874
|
|
|
unset($this->placeholders['__DIR__']); |
|
875
|
|
|
unset($this->placeholders['__LINE__']); |
|
876
|
1 |
|
|
|
877
|
|
|
return $routineSource; |
|
878
|
|
|
} |
|
879
|
1 |
|
|
|
880
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
881
|
|
|
/** |
|
882
|
1 |
|
* Updates the metadata for the stored routine. |
|
883
|
1 |
|
*/ |
|
884
|
1 |
|
private function updateMetadata(): void |
|
885
|
|
|
{ |
|
886
|
|
|
$this->phpStratumMetadata['routine_name'] = $this->routineName; |
|
887
|
|
|
$this->phpStratumMetadata['designation'] = $this->designationType; |
|
888
|
|
|
$this->phpStratumMetadata['return'] = $this->returnType; |
|
889
|
|
|
$this->phpStratumMetadata['parameters'] = $this->routineParameters->getParameters(); |
|
890
|
|
|
$this->phpStratumMetadata['timestamp'] = $this->filemtime; |
|
891
|
|
|
$this->phpStratumMetadata['placeholders'] = $this->placeholders; |
|
892
|
|
|
$this->phpStratumMetadata['type_hints'] = $this->typeHints; |
|
893
|
|
|
$this->phpStratumMetadata['phpdoc'] = $this->extractDocBlockPartsWrapper(); |
|
894
|
|
|
|
|
895
|
|
|
if (in_array($this->designationType, ['rows_with_index', 'rows_with_key'])) |
|
896
|
|
|
{ |
|
897
|
|
|
$this->phpStratumMetadata['index_columns'] = $this->indexColumns; |
|
898
|
|
|
} |
|
899
|
|
|
|
|
900
|
|
|
if ($this->designationType==='bulk_insert') |
|
901
|
|
|
{ |
|
902
|
|
|
$this->phpStratumMetadata['bulk_insert_table_name'] = $this->bulkInsertTableName; |
|
903
|
|
|
$this->phpStratumMetadata['bulk_insert_columns'] = $this->bulkInsertColumns; |
|
904
|
|
|
$this->phpStratumMetadata['bulk_insert_keys'] = $this->bulkInsertKeys; |
|
905
|
|
|
} |
|
906
|
|
|
} |
|
907
|
|
|
|
|
908
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
909
|
|
|
/** |
|
910
|
|
|
* Updates the source of the stored routine based on the values of type hints. |
|
911
|
|
|
* |
|
912
|
|
|
* @throws RoutineLoaderException |
|
913
|
|
|
*/ |
|
914
|
|
|
private function updateSourceTypeHints(): void |
|
915
|
|
|
{ |
|
916
|
|
|
$types = ['int', |
|
917
|
|
|
'smallint', |
|
918
|
|
|
'tinyint', |
|
919
|
|
|
'mediumint', |
|
920
|
|
|
'bigint', |
|
921
|
|
|
'decimal', |
|
922
|
|
|
'float', |
|
923
|
|
|
'double', |
|
924
|
|
|
'bit', |
|
925
|
|
|
'date', |
|
926
|
|
|
'datetime', |
|
927
|
|
|
'timestamp', |
|
928
|
|
|
'time', |
|
929
|
|
|
'year', |
|
930
|
|
|
'char', |
|
931
|
|
|
'varchar', |
|
932
|
|
|
'binary', |
|
933
|
|
|
'varbinary', |
|
934
|
|
|
'enum', |
|
935
|
|
|
'set', |
|
936
|
|
|
'inet4', |
|
937
|
|
|
'inet6', |
|
938
|
|
|
'tinyblob', |
|
939
|
|
|
'blob', |
|
940
|
|
|
'mediumblob', |
|
941
|
|
|
'longblob', |
|
942
|
|
|
'tinytext', |
|
943
|
|
|
'text', |
|
944
|
|
|
'mediumtext', |
|
945
|
|
|
'longtext']; |
|
946
|
|
|
$parts = ['whitespace' => '(?<whitespace>\s+)', |
|
947
|
|
|
'type_list' => str_replace('type-list', |
|
948
|
|
|
implode('|', $types), |
|
949
|
|
|
'(?<datatype>(type-list).*)'), |
|
950
|
|
|
'nullable' => '(?<nullable>not\s+null)?', |
|
951
|
|
|
'hint' => '(?<hint>\s+--\s+type:\s+(\w+\.)?\w+\.\w+\s*)']; |
|
952
|
|
|
$pattern = '/'.implode('', $parts).'$/i'; |
|
953
|
|
|
|
|
954
|
|
|
foreach ($this->routineSourceCodeLines as $index => $line) |
|
955
|
|
|
{ |
|
956
|
|
|
if (preg_match('/'.$parts['hint'].'/i', $line)) |
|
957
|
|
|
{ |
|
958
|
|
|
$n = preg_match($pattern, $line, $matches); |
|
959
|
|
|
if ($n===0) |
|
960
|
|
|
{ |
|
961
|
|
|
throw new RoutineLoaderException("Found a type hint at line %d, but unable to find data type.", $index + 1); |
|
962
|
|
|
} |
|
963
|
|
|
|
|
964
|
|
|
$hint = trim(preg_replace('/\s+--\s+type:\s+/i', '', $matches['hint'])); |
|
965
|
|
|
if (!isset($this->typeHintPool[$hint])) |
|
966
|
|
|
{ |
|
967
|
|
|
throw new RoutineLoaderException("Unknown type hint '%s' found at line %d.", $hint, $index + 1); |
|
968
|
|
|
} |
|
969
|
|
|
|
|
970
|
|
|
if (preg_match('/(?<punctuation>\s*[;,]\s*)$/', $matches['datatype'], $other)) |
|
971
|
|
|
{ |
|
972
|
|
|
$punctuation = $other['punctuation']; |
|
973
|
|
|
} |
|
974
|
|
|
else |
|
975
|
|
|
{ |
|
976
|
|
|
$punctuation = ''; |
|
977
|
|
|
} |
|
978
|
|
|
|
|
979
|
|
|
$actualType = $this->typeHintPool[$hint]; |
|
980
|
|
|
$newLine = sprintf('%s%s%s%s%s%s', |
|
981
|
|
|
mb_substr($line, 0, -mb_strlen($matches[0])), |
|
982
|
|
|
$matches['whitespace'], |
|
983
|
|
|
$actualType, // <== the real replacement |
|
984
|
|
|
$matches['nullable'], |
|
985
|
|
|
$punctuation, |
|
986
|
|
|
$matches['hint']); |
|
987
|
|
|
$this->typeHints[$hint] = $actualType; |
|
988
|
|
|
|
|
989
|
|
|
if (str_replace(' ', '', $line)!==str_replace(' ', '', $newLine)) |
|
990
|
|
|
{ |
|
991
|
|
|
$this->routineSourceCodeLines[$index] = $newLine; |
|
992
|
|
|
} |
|
993
|
|
|
} |
|
994
|
|
|
} |
|
995
|
|
|
|
|
996
|
|
|
$blocks = []; |
|
997
|
|
|
$start = null; |
|
998
|
|
|
$length = 0; |
|
999
|
|
|
foreach ($this->routineSourceCodeLines as $index => $line) |
|
1000
|
|
|
{ |
|
1001
|
|
|
$n = preg_match('/--\s+type:\s+.*$/', $line, $matches); |
|
1002
|
|
|
if ($n!==0) |
|
1003
|
|
|
{ |
|
1004
|
|
|
if ($start===null) |
|
1005
|
|
|
{ |
|
1006
|
|
|
$start = $index; |
|
1007
|
|
|
} |
|
1008
|
|
|
$length = max($length, mb_strlen($line) - mb_strlen($matches[0]) + 2); |
|
1009
|
|
|
} |
|
1010
|
|
|
else |
|
1011
|
|
|
{ |
|
1012
|
|
|
if ($start!==null) |
|
1013
|
|
|
{ |
|
1014
|
|
|
$blocks[] = ['first' => $start, 'last' => $index, 'length' => $length]; |
|
1015
|
|
|
$start = null; |
|
1016
|
|
|
$length = 0; |
|
1017
|
|
|
} |
|
1018
|
|
|
} |
|
1019
|
|
|
} |
|
1020
|
|
|
|
|
1021
|
|
|
foreach ($blocks as $block) |
|
1022
|
|
|
{ |
|
1023
|
|
|
for ($index = $block['first']; $index<$block['last']; $index++) |
|
1024
|
|
|
{ |
|
1025
|
|
|
preg_match('/\s+type:\s+.*$/', $this->routineSourceCodeLines[$index], $matches); |
|
1026
|
|
|
$leftPart = mb_substr($this->routineSourceCodeLines[$index], 0, -mb_strlen($matches[0])); |
|
1027
|
|
|
$leftPart = $leftPart.str_repeat(' ', $block['length'] - mb_strlen($leftPart) + 1); |
|
1028
|
|
|
|
|
1029
|
|
|
$this->routineSourceCodeLines[$index] = $leftPart.ltrim($matches[0]); |
|
1030
|
|
|
} |
|
1031
|
|
|
} |
|
1032
|
|
|
|
|
1033
|
|
|
$routineSourceCode = implode(PHP_EOL, $this->routineSourceCodeLines); |
|
1034
|
|
|
if ($this->routineSourceCode!==$routineSourceCode) |
|
1035
|
|
|
{ |
|
1036
|
|
|
$this->routineSourceCode = $routineSourceCode; |
|
1037
|
|
|
Util::writeTwoPhases($this->sourceFilename, $this->routineSourceCode, $this->io); |
|
1038
|
|
|
} |
|
1039
|
|
|
} |
|
1040
|
|
|
|
|
1041
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
1042
|
|
|
/** |
|
1043
|
|
|
* Validates the specified return type of the stored routine. |
|
1044
|
|
|
* |
|
1045
|
|
|
* @throws RoutineLoaderException |
|
1046
|
|
|
*/ |
|
1047
|
|
|
private function validateReturnType(): void |
|
1048
|
|
|
{ |
|
1049
|
|
|
// Return immediately if designation type is not appropriate for this method. |
|
1050
|
|
|
if (!in_array($this->designationType, ['function', 'singleton0', 'singleton1'])) |
|
1051
|
|
|
{ |
|
1052
|
|
|
return; |
|
1053
|
|
|
} |
|
1054
|
|
|
|
|
1055
|
|
|
$types = explode('|', $this->returnType); |
|
|
|
|
|
|
1056
|
|
|
$diff = array_diff($types, ['string', 'int', 'float', 'double', 'bool', 'null']); |
|
1057
|
|
|
|
|
1058
|
|
|
if (!($this->returnType=='mixed' || $this->returnType=='bool' || empty($diff))) |
|
1059
|
|
|
{ |
|
1060
|
|
|
throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or a combination of 'int', 'float', 'string', and 'null'."); |
|
1061
|
|
|
} |
|
1062
|
|
|
|
|
1063
|
|
|
// The following tests are applicable for singleton0 routines only. |
|
1064
|
|
|
if ($this->designationType!=='singleton0') |
|
1065
|
|
|
{ |
|
1066
|
|
|
return; |
|
1067
|
|
|
} |
|
1068
|
|
|
|
|
1069
|
|
|
// Return mixed is OK. |
|
1070
|
|
|
if (in_array($this->returnType, ['bool', 'mixed'])) |
|
1071
|
|
|
{ |
|
1072
|
|
|
return; |
|
1073
|
|
|
} |
|
1074
|
|
|
|
|
1075
|
|
|
// In all other cases return type must contain null. |
|
1076
|
|
|
$parts = explode('|', $this->returnType); |
|
1077
|
|
|
$key = in_array('null', $parts); |
|
1078
|
|
|
if ($key===false) |
|
1079
|
|
|
{ |
|
1080
|
|
|
throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or contain 'null' (with a combination of 'int', 'float', and 'string')."); |
|
1081
|
|
|
} |
|
1082
|
|
|
} |
|
1083
|
|
|
|
|
1084
|
|
|
//-------------------------------------------------------------------------------------------------------------------- |
|
1085
|
|
|
} |
|
1086
|
|
|
|
|
1087
|
|
|
//---------------------------------------------------------------------------------------------------------------------- |
|
1088
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.