Passed
Push — master ( f71b04...7dcb0c )
by P.R.
12:30
created

extractColumnsFromTableDescription()   D

Complexity

Conditions 30
Paths 30

Size

Total Lines 107
Code Lines 77

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 30
eloc 77
nc 30
nop 1
dl 0
loc 107
rs 4.1666
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types=1);
3
4
namespace SetBased\Stratum\MySql\Loader;
5
6
use SetBased\Exception\FallenException;
7
use SetBased\Helper\Cast;
8
use SetBased\Stratum\Backend\StratumStyle;
9
use SetBased\Stratum\Common\Exception\RoutineLoaderException;
10
use SetBased\Stratum\Common\Loader\CommonRoutineLoader;
11
use SetBased\Stratum\Common\Loader\Helper\LoaderContext;
12
use SetBased\Stratum\MySql\Loader\Helper\RoutineParametersHelper;
13
use SetBased\Stratum\MySql\Loader\Helper\SqlModeHelper;
14
use SetBased\Stratum\MySql\MySqlMetadataLayer;
15
16
/**
17
 * Class for loading a single stored routine into a MySQL instance from pseudo SQL file.
18
 */
19
class MySqlRoutineLoader extends CommonRoutineLoader
20
{
21
  //--------------------------------------------------------------------------------------------------------------------
22
  /**
23
   * MySQL's and MariaDB's SQL/PSM syntax.
24
   */
25
  const SQL_PSM_SYNTAX = 1;
26
27
  /**
28
   * Oracle PL/SQL syntax.
29
   */
30
  const PL_SQL_SYNTAX = 2;
31
32
  /**
33
   * The default character set under which the stored routine will be loaded and run.
34
   *
35
   * @var string
36
   */
37
  private string $characterSet;
38
39
  /**
40
   * The default collate under which the stored routine will be loaded and run.
41
   *
42
   * @var string
43
   */
44
  private string $collate;
45
46
  /**
47
   * The metadata layer.
48
   *
49
   * @var MySqlMetadataLayer
50
   */
51
  private MySqlMetadataLayer $dl;
52
53
  /**
54
   * The SQL mode helper object.
55
   *
56
   * @var SqlModeHelper
57
   */
58
  private SqlModeHelper $sqlModeHelper;
59
60
  /**
61
   * The syntax of the stored routine. Either SQL_PSM_SYNTAX or PL_SQL_SYNTAX.
62
   *
63
   * @var int
64
   */
65
  private int $syntax;
66
67
  //--------------------------------------------------------------------------------------------------------------------
68
  /**
69
   * Object constructor.
70
   *
71
   * @param StratumStyle       $io            The output decorator.
72
   * @param MySqlMetadataLayer $dl            The metadata layer.
73
   * @param SqlModeHelper      $sqlModeHelper
74
   * @param string             $characterSet  The default character set under which the stored routine will be loaded
75
   *                                          and run.
76
   * @param string             $collate       The key or index columns (depending on the designation type) of the
77
   *                                          stored routine.
78
   */
79
  public function __construct(StratumStyle       $io,
80
                              MySqlMetadataLayer $dl,
81
                              SqlModeHelper      $sqlModeHelper,
82
                              string             $characterSet,
83
                              string             $collate)
84
  {
85
    parent::__construct($io);
86
87
    $this->dl            = $dl;
88
    $this->sqlModeHelper = $sqlModeHelper;
89
    $this->characterSet  = $characterSet;
90
    $this->collate       = $collate;
91
  }
92
93
  //--------------------------------------------------------------------------------------------------------------------
94
  /**
95
   * Extract column metadata from the rows returned by the SQL statement 'describe table'.
96
   *
97
   * @param array $description The description of the table.
98
   */
99
  private static function extractColumnsFromTableDescription(array $description): array
100
  {
101
    $ret = [];
102
103
    foreach ($description as $column)
104
    {
105
      preg_match('/^(?<data_type>\w+)(?<extra>.*)?$/', $column['Type'], $parts1);
106
107
      $tmp = ['column_name'       => $column['Field'],
108
              'data_type'         => $parts1['data_type'],
109
              'numeric_precision' => null,
110
              'numeric_scale'     => null,
111
              'dtd_identifier'    => $column['Type']];
112
113
      switch ($parts1[1])
114
      {
115
        case 'tinyint':
116
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
117
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 4);
118
          $tmp['numeric_scale']     = 0;
119
          break;
120
121
        case 'smallint':
122
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
123
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 6);
124
          $tmp['numeric_scale']     = 0;
125
          break;
126
127
        case 'mediumint':
128
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
129
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 9);
130
          $tmp['numeric_scale']     = 0;
131
          break;
132
133
        case 'int':
134
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
135
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 11);
136
          $tmp['numeric_scale']     = 0;
137
          break;
138
139
        case 'bigint':
140
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
141
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 20);
142
          $tmp['numeric_scale']     = 0;
143
          break;
144
145
        case 'year':
146
          // Nothing to do.
147
          break;
148
149
        case 'float':
150
          $tmp['numeric_precision'] = 12;
151
          break;
152
153
        case 'double':
154
          $tmp['numeric_precision'] = 22;
155
          break;
156
157
        case 'binary':
158
        case 'char':
159
        case 'varbinary':
160
        case 'varchar':
161
          // Nothing to do (binary) strings.
162
          break;
163
164
        case 'decimal':
165
          preg_match('/^\((?<precision>\d+),(<?scale>\d+)\)$/', $parts1['extra'], $parts2);
166
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 65);
167
          $tmp['numeric_scale']     = Cast::toManInt($parts2['scale'] ?? 0);
168
          break;
169
170
        case 'time':
171
        case 'timestamp':
172
        case 'date':
173
        case 'datetime':
174
          // Nothing to do date and time.
175
          break;
176
177
        case 'enum':
178
        case 'set':
179
          // Nothing to do sets.
180
          break;
181
182
        case 'bit':
183
          preg_match('/^\((?<precision>\d+)\)$/', $parts1['extra'], $parts2);
184
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision']);
185
          break;
186
187
        case 'tinytext':
188
        case 'text':
189
        case 'mediumtext':
190
        case 'longtext':
191
        case 'tinyblob':
192
        case 'blob':
193
        case 'mediumblob':
194
        case 'longblob':
195
          // Nothing to do CLOBs and BLOBs.
196
          break;
197
198
        default:
199
          throw new FallenException('data type', $parts1[1]);
200
      }
201
202
      $ret[] = $tmp;
203
    }
204
205
    return $ret;
206
  }
207
208
  //--------------------------------------------------------------------------------------------------------------------
209
  /**
210
   * Drops a stored routine.
211
   *
212
   * @param LoaderContext $context The loader context.
213
   */
214
  protected function dropStoredRoutine(LoaderContext $context): void
215
  {
216
    if (!empty($context->oldRdbmsMetadata))
217
    {
218
      $this->dl->dropRoutine($context->oldRdbmsMetadata['routine_type'],
219
                             $context->oldRdbmsMetadata['routine_name']);
220
    }
221
  }
222
223
  //--------------------------------------------------------------------------------------------------------------------
224
  /**
225
   * @inheritdoc
226
   */
227
  protected function extractInsertMultipleTableColumns(LoaderContext $context): void
228
  {
229
    if ($context->docBlock->getDesignation()['type']!=='insert_multiple')
230
    {
231
      return;
232
    }
233
234
    $tableName        = $context->docBlock->getDesignation()['table_name'];
235
    $keys             = $context->docBlock->getDesignation()['keys'];
236
    $isTemporaryTable = !$this->dl->checkTableExists($tableName);
237
238
    // Create temporary table if table is a temporary table.
239
    if ($isTemporaryTable)
240
    {
241
      $this->dl->callProcedure($context->storedRoutine->getName());
242
    }
243
244
    $rdbmsColumns = $this->dl->describeTable($tableName);
245
246
    // Drop temporary table if table is temporary.
247
    if ($isTemporaryTable)
248
    {
249
      $this->dl->dropTemporaryTable($tableName);
250
    }
251
252
    // Check number of columns in the table match the number of fields given in the designation type.
253
    $n1 = sizeof($keys);
254
    $n2 = sizeof($rdbmsColumns);
255
    if ($n1!==$n2)
256
    {
257
      throw new RoutineLoaderException("Number of fields %d and number of columns %d don't match.", $n1, $n2);
258
    }
259
260
    $context->newPhpStratumMetadata['insert_multiple_table_columns'] = self::extractColumnsFromTableDescription($rdbmsColumns);
261
  }
262
263
  //--------------------------------------------------------------------------------------------------------------------
264
  /**
265
   * @inheritdoc
266
   */
267
  protected function extractName(LoaderContext $context): void
268
  {
269
    $n = preg_match('/create\s+(?<type>procedure|function)\s+(?<name>[a-zA-Z0-9_]+)/i',
270
                    $context->storedRoutine->getCode(),
271
                    $matches);
272
    if ($n!==1)
273
    {
274
      throw new RoutineLoaderException('Unable to find the stored routine name and type.');
275
    }
276
277
    $context->storedRoutine->setName($matches['name']);
278
    $context->storedRoutine->setType($matches['type']);
279
    $this->extractSyntax($context);
280
  }
281
282
  //--------------------------------------------------------------------------------------------------------------------
283
  /**
284
   * @inheritdoc
285
   */
286
  protected function extractStoredRoutineParameters(LoaderContext $context): void
287
  {
288
    $routineParametersHelper = new RoutineParametersHelper($this->dl);
289
    $context->storedRoutine->setParameters($routineParametersHelper->getParameters($context));
290
  }
291
292
  //--------------------------------------------------------------------------------------------------------------------
293
  /**
294
   * @inheritdoc
295
   */
296
  protected function loadRoutineFile(LoaderContext $context): void
297
  {
298
    if ($this->syntax===self::PL_SQL_SYNTAX)
299
    {
300
      $this->sqlModeHelper->addIfRequiredOracleMode();
301
    }
302
    else
303
    {
304
      $this->sqlModeHelper->removeIfRequiredOracleMode();
305
    }
306
307
    $this->dropStoredRoutine($context);
308
    $this->dl->setCharacterSet($this->characterSet, $this->collate);
309
    $this->dl->executeNone($this->routineSourceCode);
310
  }
311
312
  //--------------------------------------------------------------------------------------------------------------------
313
  /**
314
   * Returns whether the source file must be load or reloaded.
315
   */
316
  protected function mustReload(LoaderContext $context): bool
317
  {
318
    if (parent::mustReload($context))
319
    {
320
      return true;
321
    }
322
323
    if (!$this->sqlModeHelper->compare($context->oldRdbmsMetadata['sql_mode']))
324
    {
325
      return true;
326
    }
327
328
    if ($context->oldRdbmsMetadata['character_set_client']!==$this->characterSet)
329
    {
330
      return true;
331
    }
332
333
    if ($context->oldRdbmsMetadata['collation_connection']!==$this->collate)
334
    {
335
      return true;
336
    }
337
338
    return false;
339
  }
340
341
  //--------------------------------------------------------------------------------------------------------------------
342
  /**
343
   * @inheritdoc
344
   */
345
  protected function updateMetadata(LoaderContext $context): void
346
  {
347
    parent::updateMetadata($context);
348
349
    $context->newPhpStratumMetadata['character_set_client'] = $this->characterSet;
350
    $context->newPhpStratumMetadata['collation_connection'] = $this->collate;
351
    $context->newPhpStratumMetadata['sql_mode']             = $this->sqlModeHelper->getCanonicalSqlMode();
352
  }
353
354
  //--------------------------------------------------------------------------------------------------------------------
355
  /**
356
   * Detects the syntax of the stored procedure. Either SQL/PSM or PL/SQL.
357
   *
358
   * @param LoaderContext $context The loader context.
359
   */
360
  private function extractSyntax(LoaderContext $context): void
361
  {
362
    if ($this->sqlModeHelper->hasOracleMode())
363
    {
364
      $key1 = $this->findFirstMatchingLine($context, '/^\s*(as|is)\s*$/i');
365
      $key2 = $this->findFirstMatchingLine($context, '/^\s*begin\s*$/i');
366
367
      if ($key1!==null && $key2!==null && $key1<$key2)
368
      {
369
        $this->syntax = self::PL_SQL_SYNTAX;
370
      }
371
      else
372
      {
373
        $this->syntax = self::SQL_PSM_SYNTAX;
374
      }
375
    }
376
    else
377
    {
378
      $this->syntax = self::SQL_PSM_SYNTAX;
379
    }
380
  }
381
382
  //--------------------------------------------------------------------------------------------------------------------
383
  /**
384
   * Returns the key of the source line that match a regex pattern.
385
   *
386
   * @param LoaderContext $context The loader context.
387
   * @param string        $pattern The regex pattern.
388
   *
389
   * @return int|null
390
   */
391
  private function findFirstMatchingLine(LoaderContext $context, string $pattern): ?int
392
  {
393
    foreach ($context->storedRoutine->getCodeLines() as $key => $line)
394
    {
395
      if (preg_match($pattern, $line)===1)
396
      {
397
        return $key;
398
      }
399
    }
400
401
    return null;
402
  }
403
404
  //--------------------------------------------------------------------------------------------------------------------
405
}
406
407
//----------------------------------------------------------------------------------------------------------------------
408