MSSQLColumnMeta   F
last analyzed

Complexity

Total Complexity 103

Size/Duplication

Total Lines 438
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 229
c 2
b 1
f 0
dl 0
loc 438
ccs 0
cts 215
cp 0
rs 2
wmc 103

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 57 4
D getMySQLType() 0 119 37
A getMaxValue() 0 3 1
A getMinValue() 0 3 1
A getScale() 0 3 1
D getLength() 0 31 21
D getOracleType() 0 73 33
A getName() 0 3 1
A getDistinctValueCount() 0 3 1
A isNullable() 0 3 1
A getOriginalType() 0 3 1
A getPrecision() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like MSSQLColumnMeta often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MSSQLColumnMeta, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * Database Access Layer.
6
 * @author Doug Wright
7
 */
8
9
namespace DVDoug\DB;
10
11
use Exception;
12
use function in_array;
13
use function sprintf;
14
use function strtoupper;
15
16
/**
17
 * Metadata about a database column.
18
 * @author Doug Wright
19
 */
20
class MSSQLColumnMeta implements ColumnMetaInterface
21
{
22
    use DDLGeneration;
23
24
    /**
25
     * Database connection.
26
     */
27
    protected DatabaseInterface $connection;
28
29
    /**
30
     * Database name.
31
     */
32
    protected string $database;
33
34
    /**
35
     * Table name.
36
     */
37
    protected string $table;
38
39
    /**
40
     * Column name.
41
     */
42
    protected string $name;
43
44
    /**
45
     * Column type.
46
     */
47
    protected string $type;
48
49
    /**
50
     * Column length.
51
     */
52
    protected int $length;
53
54
    /**
55
     * Column precision.
56
     */
57
    protected ?int $precision;
58
59
    /**
60
     * Column scale.
61
     */
62
    protected ?int $scale;
63
64
    /**
65
     * Column nullable?
66
     */
67
    protected bool $isNullable;
68
69
    /**
70
     * Column max value.
71
     */
72
    protected ?string $maxValue;
73
74
    /**
75
     * Column min value.
76
     */
77
    protected ?string $minValue;
78
79
    /**
80
     * Number of distinct values.
81
     */
82
    protected int $distinctValues;
83
84
    /**
85
     * Constructor.
86
     * @param DatabaseInterface $aConnection connection to database
87
     * @param string            $aDatabase   database/schema name
88
     * @param string            $aTable      table name
89
     * @param string            $aColumnName column name
90
     */
91
    public function __construct(DatabaseInterface $aConnection, $aDatabase, $aTable, $aColumnName)
92
    {
93
        $this->connection = $aConnection;
94
        $this->database = $aDatabase;
95
        $this->table = $aTable;
96
        $this->name = $aColumnName;
97
98
        /*
99
         * Basic metadata from the schema
100
         */
101
        $statement = $this->connection->prepare('SELECT TABLE_SCHEMA,
102
                                                      TABLE_NAME,
103
                                                      COLUMN_NAME,
104
                                                      DATA_TYPE,
105
                                                      CHARACTER_MAXIMUM_LENGTH,
106
                                                      NUMERIC_PRECISION,
107
                                                      COALESCE(DATETIME_PRECISION, NUMERIC_SCALE) AS SCALE,
108
                                                      IS_NULLABLE
109
                                               FROM INFORMATION_SCHEMA.COLUMNS
110
                                               WHERE TABLE_SCHEMA = :database
111
                                                     AND TABLE_NAME = :table_name
112
                                                     AND COLUMN_NAME = :column_name');
113
        $statement->bindParamToValue(':database', $this->database);
114
        $statement->bindParamToValue(':table_name', $this->table);
115
        $statement->bindParamToValue(':column_name', $this->name);
116
        $statement->execute();
0 ignored issues
show
Bug introduced by
The call to DVDoug\DB\StatementInterface::execute() has too few arguments starting with boundInputParams. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

116
        $statement->/** @scrutinizer ignore-call */ 
117
                    execute();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
117
118
        $meta = $statement->fetchAssoc(false);
119
120
        $this->type = strtoupper($meta['DATA_TYPE']);
121
        $this->length = $meta['CHARACTER_MAXIMUM_LENGTH'] ?: $meta['NUMERIC_PRECISION'];
122
        $this->precision = $meta['NUMERIC_PRECISION'];
123
        $this->scale = $meta['SCALE'];
124
        $this->isNullable = ($meta['IS_NULLABLE'] == 'YES');
125
126
        /*
127
         * Metadata from the data stored
128
         */
129
        if (!in_array($this->type, ['TEXT', 'NTEXT', 'IMAGE'])) {
130
            $query = sprintf('SELECT COUNT(*) AS COUNT FROM (SELECT %s FROM %s.%s GROUP BY %s) distinctvalues',
131
                $this->connection->quoteIdentifier($this->name),
132
                $this->connection->quoteIdentifier($this->database),
133
                $this->connection->quoteIdentifier($this->table),
134
                $this->connection->quoteIdentifier($this->name));
135
            $this->distinctValues = $this->connection->query($query)->fetchAssoc(false)['COUNT'];
136
        }
137
138
        if (!in_array($this->type, ['BIT', 'TEXT', 'NTEXT', 'IMAGE', 'UNIQUEIDENTIFIER'])) {
139
            $query = sprintf('SELECT MIN(%s) AS ROWMIN, MAX(%s) AS ROWMAX FROM %s.%s WHERE %s IS NOT NULL',
140
                $this->connection->quoteIdentifier($this->name),
141
                $this->connection->quoteIdentifier($this->name),
142
                $this->connection->quoteIdentifier($this->database),
143
                $this->connection->quoteIdentifier($this->table),
144
                $this->connection->quoteIdentifier($this->name));
145
            $data = $this->connection->query($query)->fetchAssoc(false);
146
            $this->maxValue = $data['ROWMAX'];
147
            $this->minValue = $data['ROWMIN'];
148
        }
149
    }
150
151
    /**
152
     * Get column name.
153
     */
154
    public function getName(): string
155
    {
156
        return $this->name;
157
    }
158
159
    /**
160
     * Get column type as used by originating database.
161
     */
162
    public function getOriginalType(): string
163
    {
164
        return $this->type;
165
    }
166
167
    /**
168
     * Get column type as suitable for MySQL.
169
     *
170
     * @throws Exception
171
     */
172
    public function getMySQLType(): string
173
    {
174
        switch ($this->type) {
175
            case 'BIT':
176
            case 'TINYINT':
177
                return 'TINYINT UNSIGNED';
178
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
179
180
            case 'SMALLINT':
181
                return 'SMALLINT';
182
                break;
183
184
            case 'INT':
185
                return 'INT';
186
                break;
187
188
            case 'BIGINT':
189
                return 'BIGINT';
190
                break;
191
192
            case 'DECIMAL':
193
            case 'NUMERIC':
194
            case 'MONEY':
195
            case 'SMALLMONEY':
196
                return 'DECIMAL';
197
                break;
198
199
            case 'FLOAT':
200
                return 'FLOAT';
201
                break;
202
203
            case 'REAL':
204
                return 'DOUBLE';
205
                break;
206
207
            case 'DATE':
208
                return 'DATE';
209
                break;
210
211
            case 'DATETIME':
212
            case 'DATETIME2':
213
            case 'SMALLDATETIME':
214
                /*
215
                 * Work out whether date or datetime
216
                */
217
                $query = sprintf("SELECT COUNT(*) AS COUNT FROM %s.%s WHERE %s IS NOT NULL AND CONVERT(VARCHAR(8), %s, 108) != '00:00:00'",
218
                    $this->connection->quoteIdentifier($this->database),
219
                    $this->connection->quoteIdentifier($this->table),
220
                    $this->connection->quoteIdentifier($this->name),
221
                    $this->connection->quoteIdentifier($this->name));
222
                $rows = $this->connection->query($query)->fetchAssoc(false);
223
224
                if ($rows['COUNT'] > 0) {
225
                    return 'DATETIME';
226
                } else {
227
                    return 'DATE';
228
                }
229
                break;
230
231
            case 'DATETIMEOFFSET':
232
                if ($this->minValue >= '1970-01-01 00:00:01' && $this->maxValue <= '2038-01-19 03:14:07') {
233
                    return 'TIMESTAMP';
234
                } else {
235
                    return 'DATETIME';
236
                }
237
                break;
238
239
            case 'TIME':
240
                return 'TIME';
241
                break;
242
243
            case 'CHAR':
244
            case 'NCHAR':
245
                return 'CHAR';
246
                break;
247
248
            case 'VARCHAR':
249
            case 'NVARCHAR':
250
                if ($this->getLength() == -1) {
251
                    return 'LONGTEXT';
252
                } else {
253
                    return 'VARCHAR';
254
                }
255
                break;
256
257
            case 'TEXT':
258
            case 'NTEXT':
259
                return 'LONGTEXT';
260
                break;
261
262
            case 'BINARY':
263
                return 'BINARY';
264
                break;
265
266
            case 'VARBINARY':
267
                if ($this->getLength() == -1) {
268
                    return 'LONGBLOB';
269
                } else {
270
                    return 'VARBINARY';
271
                }
272
                break;
273
274
            case 'IMAGE':
275
                return 'LONGBLOB';
276
                break;
277
278
            case 'ROWVERSION':
279
            case 'TIMESTAMP': //XXX rowversion, not a time
280
            case 'HIERARCHYID':
281
            case 'XML':
282
                return 'VARCHAR';
283
                break;
284
285
            case 'UNIQUEIDENTIFIER':
286
                return 'CHAR';
287
                break;
288
289
            default:
290
                throw new Exception("Unknown conversion for column type {$this->type}");
291
        }
292
    }
293
294
    /**
295
     * Get column type as suitable for Oracle.
296
     *
297
     * @throws Exception
298
     */
299
    public function getOracleType(): string
300
    {
301
        switch ($this->type) {
302
            case 'BIT':
303
            case 'TINYINT':
304
            case 'SMALLINT':
305
            case 'INT':
306
            case 'BIGINT':
307
            case 'DECIMAL':
308
            case 'NUMERIC':
309
            case 'MONEY':
310
            case 'SMALLMONEY':
311
                return 'NUMBER';
312
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
313
314
            case 'FLOAT':
315
                return 'BINARY_FLOAT';
316
                break;
317
318
            case 'REAL':
319
                return 'BINARY_DOUBLE';
320
                break;
321
322
            case 'DATE':
323
                return 'DATE';
324
                break;
325
326
            case 'DATETIME':
327
            case 'DATETIME2':
328
            case 'SMALLDATETIME':
329
            case 'DATETIMEOFFSET':
330
                if ($this->precision) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->precision of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
331
                    return 'TIMESTAMP';
332
                } else {
333
                    return 'DATE';
334
                }
335
                break;
336
337
            case 'TIME':
338
                return 'TIME';
339
                break;
340
341
            case 'CHAR':
342
            case 'NCHAR':
343
                return 'NCHAR';
344
                break;
345
346
            case 'VARCHAR':
347
            case 'NVARCHAR':
348
            case 'TEXT':
349
            case 'NTEXT':
350
                return 'NVARCHAR';
351
                break;
352
353
            case 'BINARY':
354
            case 'VARBINARY':
355
            case 'IMAGE':
356
                return 'BLOB';
357
                break;
358
359
            case 'ROWVERSION':
360
            case 'TIMESTAMP': //XXX rowversion, not a time
361
            case 'HIERARCHYID':
362
            case 'XML':
363
                return 'NVARCHAR';
364
                break;
365
366
            case 'UNIQUEIDENTIFIER':
367
                return 'CHAR';
368
                break;
369
370
            default:
371
                throw new Exception("Unknown conversion for column type {$this->type}");
372
        }
373
    }
374
375
    /**
376
     * Get length of column.
377
     */
378
    public function getLength(): int
379
    {
380
        switch ($this->getOriginalType()) {
381
            case 'TINYINT':
382
            case 'SMALLINT':
383
            case 'INT':
384
            case 'BIGINT':
385
            case 'DECIMAL':
386
            case 'NUMERIC':
387
            case 'MONEY':
388
            case 'SMALLMONEY':
389
            case 'BIT':
390
            case 'FLOAT':
391
            case 'REAL':
392
            case 'CHAR':
393
            case 'NCHAR':
394
            case 'VARCHAR':
395
            case 'NVARCHAR':
396
            case 'ROWVERSION':
397
            case 'TIMESTAMP':
398
            case 'HIERARCHYID':
399
            case 'XML':
400
                return $this->length;
401
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
402
403
            case 'UNIQUEIDENTIFIER':
404
                return 36;
405
                break;
406
407
            default:
408
                return 0;
409
        }
410
    }
411
412
    /**
413
     * Get column precision (number of digits).
414
     */
415
    public function getPrecision(): ?int
416
    {
417
        return $this->precision;
418
    }
419
420
    /**
421
     * Get column scale (number of digits after decimal place).
422
     */
423
    public function getScale(): ?int
424
    {
425
        return $this->scale;
426
    }
427
428
    /**
429
     * Get whether column is nullable.
430
     */
431
    public function isNullable(): bool
432
    {
433
        return $this->isNullable;
434
    }
435
436
    /**
437
     * Get max value.
438
     */
439
    public function getMaxValue(): ?string
440
    {
441
        return $this->maxValue;
442
    }
443
444
    /**
445
     * Get min value.
446
     */
447
    public function getMinValue(): ?string
448
    {
449
        return $this->minValue;
450
    }
451
452
    /**
453
     * The number of distinct values in this column.
454
     */
455
    public function getDistinctValueCount(): int
456
    {
457
        return $this->distinctValues;
458
    }
459
}
460