1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Helix\DB; |
4
|
|
|
|
5
|
|
|
use ArrayAccess; |
6
|
|
|
use Helix\DB; |
7
|
|
|
use LogicException; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* Schema control and metadata. |
11
|
|
|
* |
12
|
|
|
* The column definition constants are two bytes each, used in bitwise composition. |
13
|
|
|
* - The high-byte (`<I_CONST>`) is used for the specific index type. |
14
|
|
|
* - Descending index priority. For example, `I_PRIMARY` is `0x8000` (highest bit) |
15
|
|
|
* - The low-byte (`<T_CONST>`) is used for the specific storage type. |
16
|
|
|
* - Inverse size complexity. For example, `BOOL` is `0x0080`, and `T_BLOB` is `0x0002` |
17
|
|
|
* - The final bit `0x0001` flags nullability. |
18
|
|
|
* - Bit `0x0010` is reserved for future use (probably `DateTime`). |
19
|
|
|
* - The literal values may change in the future, do not hard code them. |
20
|
|
|
* - The values may expand to use a total of 4 or 8 bytes to accommodate more stuff. |
21
|
|
|
* |
22
|
|
|
* @method static static factory(DB $db) |
23
|
|
|
*/ |
24
|
|
|
class Schema implements ArrayAccess { |
25
|
|
|
|
26
|
|
|
use FactoryTrait; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* `<TABLE_CONST>`: Multi-column primary key. |
30
|
|
|
*/ |
31
|
|
|
const TABLE_PRIMARY = 0; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* `<TABLE_CONST>`: Associative foreign keys. |
35
|
|
|
*/ |
36
|
|
|
const TABLE_FOREIGN = 1; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* `<TABLE_CONST>`: Groups of columns are unique together. |
40
|
|
|
*/ |
41
|
|
|
const TABLE_UNIQUE = 2; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Higher byte mask (column index type). |
45
|
|
|
*/ |
46
|
|
|
protected const I_MASK = 0xFF00; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* `<I_CONST>`: Column is the primary key. |
50
|
|
|
*/ |
51
|
|
|
const I_PRIMARY = 0x8000; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* `<I_CONST>`: Column is the primary key and auto-increments. |
55
|
|
|
*/ |
56
|
|
|
const I_AUTOINCREMENT = 0x4000 | self::I_PRIMARY; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* `<I_CONST>`: Column is unique. |
60
|
|
|
*/ |
61
|
|
|
const I_UNIQUE = 0x2000; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Lower-byte mask (column storage type). |
65
|
|
|
*/ |
66
|
|
|
protected const T_MASK = 0x00FF; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* `<T_CONST>`: Column is the primary key and auto-increments (8-byte signed integer). |
70
|
|
|
*/ |
71
|
|
|
const T_AUTOINCREMENT = self::I_AUTOINCREMENT | self::T_INT_STRICT; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Flags whether a type is `NOT NULL` |
75
|
|
|
*/ |
76
|
|
|
protected const T_STRICT = 0x0001; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* `<T_CONST>`: Boolean analog (numeric). |
80
|
|
|
*/ |
81
|
|
|
const T_BOOL = 0x0080; |
82
|
|
|
const T_BOOL_STRICT = self::T_BOOL | self::T_STRICT; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* `<T_CONST>`: 8-byte signed integer. |
86
|
|
|
*/ |
87
|
|
|
const T_INT = 0x0040; |
88
|
|
|
const T_INT_STRICT = self::T_INT | self::T_STRICT; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* `<T_CONST>`: 8-byte IEEE floating point number. |
92
|
|
|
*/ |
93
|
|
|
const T_FLOAT = 0x0020; |
94
|
|
|
const T_FLOAT_STRICT = self::T_FLOAT | self::T_STRICT; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* `<T_CONST>`: UTF-8 up to 255 bytes. |
98
|
|
|
*/ |
99
|
|
|
const T_STRING = 0x0008; |
100
|
|
|
const T_STRING_STRICT = self::T_STRING | self::T_STRICT; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* `<T_CONST>`: UTF-8 up to 64KiB. |
104
|
|
|
*/ |
105
|
|
|
const T_TEXT = 0x0004; |
106
|
|
|
const T_TEXT_STRICT = self::T_TEXT | self::T_STRICT; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* `<T_CONST>`: Arbitrary binary data up to 4GiB. |
110
|
|
|
*/ |
111
|
|
|
const T_BLOB = 0x0002; |
112
|
|
|
const T_BLOB_STRICT = self::T_BLOB | self::T_STRICT; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Maps native/annotated types to storage types. |
116
|
|
|
*/ |
117
|
|
|
protected const PHP_TYPES = [ |
118
|
|
|
'bool' => self::T_BOOL, |
119
|
|
|
'double' => self::T_FLOAT, |
120
|
|
|
'float' => self::T_FLOAT, |
121
|
|
|
'int' => self::T_INT, |
122
|
|
|
'string' => self::T_STRING, |
123
|
|
|
'String' => self::T_TEXT, |
124
|
|
|
'STRING' => self::T_BLOB |
125
|
|
|
]; |
126
|
|
|
/** |
127
|
|
|
* Driver-specific schema phrases. |
128
|
|
|
*/ |
129
|
|
|
protected const COLUMN_DEFINITIONS = [ |
130
|
|
|
'mysql' => [ |
131
|
|
|
self::I_AUTOINCREMENT => 'PRIMARY KEY AUTO_INCREMENT', |
132
|
|
|
self::I_PRIMARY => 'PRIMARY KEY', |
133
|
|
|
self::I_UNIQUE => 'UNIQUE', |
134
|
|
|
self::T_BLOB => 'LONGBLOB NULL DEFAULT NULL', |
135
|
|
|
self::T_BOOL => 'BOOLEAN NULL DEFAULT NULL', |
136
|
|
|
self::T_FLOAT => 'DOUBLE PRECISION NULL DEFAULT NULL', |
137
|
|
|
self::T_INT => 'BIGINT NULL DEFAULT NULL', |
138
|
|
|
self::T_STRING => 'VARCHAR(255) NULL DEFAULT NULL', |
139
|
|
|
self::T_TEXT => 'TEXT NULL DEFAULT NULL', |
140
|
|
|
self::T_BLOB_STRICT => 'LONGBLOB NOT NULL', |
141
|
|
|
self::T_BOOL_STRICT => 'BOOLEAN NOT NULL', |
142
|
|
|
self::T_FLOAT_STRICT => 'DOUBLE PRECISION NOT NULL', |
143
|
|
|
self::T_INT_STRICT => 'BIGINT NOT NULL', |
144
|
|
|
self::T_STRING_STRICT => 'VARCHAR(255) NOT NULL', |
145
|
|
|
self::T_TEXT_STRICT => 'TEXT NOT NULL', |
146
|
|
|
], |
147
|
|
|
'sqlite' => [ |
148
|
|
|
self::I_AUTOINCREMENT => 'PRIMARY KEY AUTOINCREMENT', |
149
|
|
|
self::I_PRIMARY => 'PRIMARY KEY', |
150
|
|
|
self::I_UNIQUE => 'UNIQUE', |
151
|
|
|
self::T_BLOB => 'BLOB DEFAULT NULL', |
152
|
|
|
self::T_BOOL => 'BOOLEAN DEFAULT NULL', |
153
|
|
|
self::T_FLOAT => 'REAL DEFAULT NULL', |
154
|
|
|
self::T_INT => 'INTEGER DEFAULT NULL', |
155
|
|
|
self::T_STRING => 'TEXT DEFAULT NULL', |
156
|
|
|
self::T_TEXT => 'TEXT DEFAULT NULL', |
157
|
|
|
self::T_BLOB_STRICT => 'BLOB NOT NULL', |
158
|
|
|
self::T_BOOL_STRICT => 'BOOLEAN NOT NULL', |
159
|
|
|
self::T_FLOAT_STRICT => 'REAL NOT NULL', |
160
|
|
|
self::T_INT_STRICT => 'INTEGER NOT NULL', |
161
|
|
|
self::T_STRING_STRICT => 'TEXT NOT NULL', |
162
|
|
|
self::T_TEXT_STRICT => 'TEXT NOT NULL', |
163
|
|
|
] |
164
|
|
|
]; |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* @var int[] |
168
|
|
|
*/ |
169
|
|
|
protected $colDefs; |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* @var DB |
173
|
|
|
*/ |
174
|
|
|
protected $db; |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* @var Table[] |
178
|
|
|
*/ |
179
|
|
|
protected $tables = []; |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* @param DB $db |
183
|
|
|
*/ |
184
|
|
|
public function __construct (DB $db) { |
185
|
|
|
$this->db = $db; |
186
|
|
|
$this->colDefs ??= self::COLUMN_DEFINITIONS[$db->getDriver()]; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* `ALTER TABLE $table ADD COLUMN $column ...` |
191
|
|
|
* |
192
|
|
|
* @param string $table |
193
|
|
|
* @param string $column |
194
|
|
|
* @param int $type |
195
|
|
|
* @return $this |
196
|
|
|
*/ |
197
|
|
|
public function addColumn (string $table, string $column, int $type = self::T_STRING) { |
198
|
|
|
$type = $this->colDefs[$type & self::T_MASK]; |
199
|
|
|
$this->db->exec("ALTER TABLE {$table} ADD COLUMN {$column} {$type}"); |
200
|
|
|
unset($this->tables[$table]); |
201
|
|
|
return $this; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Creates the underlying storage for an {@link EAV} |
206
|
|
|
* |
207
|
|
|
* @param Record $record From {@link DB::getRecord()} |
208
|
|
|
* @param string $property The EAV property name in the record. |
209
|
|
|
* @return $this |
210
|
|
|
*/ |
211
|
|
|
public function createEavTable (Record $record, string $property) { |
212
|
|
|
$eav = $record->getEav($property); |
213
|
|
|
return $this->createTable($eav, [ |
214
|
|
|
'entity' => self::T_INT_STRICT, |
215
|
|
|
'attribute' => self::T_STRING_STRICT, |
216
|
|
|
'value' => self::PHP_TYPES[$eav->getValueType()], |
217
|
|
|
], [ |
218
|
|
|
self::TABLE_PRIMARY => ['entity', 'attribute'], |
219
|
|
|
self::TABLE_FOREIGN => ['entity' => $record['id']] |
220
|
|
|
]); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Creates the underlying storage for a {@link Junction} |
225
|
|
|
* |
226
|
|
|
* @param Junction $junction From {@link DB::getJunction()} |
227
|
|
|
* @return $this |
228
|
|
|
*/ |
229
|
|
|
public function createJunctionTable (Junction $junction) { |
230
|
|
|
$records = $junction->getRecords(); |
231
|
|
|
return $this->createTable($junction, array_map(fn() => self::T_INT_STRICT, $records), [ |
232
|
|
|
self::TABLE_PRIMARY => array_keys($records), |
233
|
|
|
self::TABLE_FOREIGN => array_map(fn(Record $record) => $record['id'], $records) |
234
|
|
|
]); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Creates the underlying storage for a {@link Record} |
239
|
|
|
* |
240
|
|
|
* @param Record $record From {@link DB::getRecord()} |
241
|
|
|
* @param array[] $constraints See {@link Schema::createTable()} |
242
|
|
|
* @return $this |
243
|
|
|
*/ |
244
|
|
|
public function createRecordTable (Record $record, array $constraints = []) { |
245
|
|
|
$columns = $record->getTypes(); |
246
|
|
|
array_walk($columns, function(string &$type, string $property) use ($record) { |
247
|
|
|
$type = self::PHP_TYPES[$type] | ($record->isNullable($property) ? 0 : self::T_STRICT); |
248
|
|
|
}); |
249
|
|
|
$columns['id'] = self::T_AUTOINCREMENT; // force to autoincrement |
250
|
|
|
unset($constraints[self::TABLE_PRIMARY]); // `id` is the sole pk |
251
|
|
|
return $this->createTable($record, $columns, $constraints); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* `CREATE TABLE $table ...` |
256
|
|
|
* |
257
|
|
|
* At least one column must be given. |
258
|
|
|
* |
259
|
|
|
* `$constraints` is a multidimensional array of table-level constraints. |
260
|
|
|
* - `C_PRIMARY => [col, col, col]` |
261
|
|
|
* - A list of columns composing the primary key. |
262
|
|
|
* - `C_UNIQUE => [ [col, col, col] , ... ]` |
263
|
|
|
* - One or more column groups, each group composing a unique key. |
264
|
|
|
* - `C_FOREIGN => [ local column => <Column> ]` |
265
|
|
|
* - Associative local columns that are each foreign keys to a {@link Column} |
266
|
|
|
* |
267
|
|
|
* @param string $table |
268
|
|
|
* @param int[] $columns `[ name => <I_CONST> | <T_CONST> ]` |
269
|
|
|
* @param array[] $constraints `[ <TABLE_CONST> => table constraint spec ]` |
270
|
|
|
* @return $this |
271
|
|
|
*/ |
272
|
|
|
public function createTable (string $table, array $columns, array $constraints = []) { |
273
|
|
|
$defs = $this->toColumnDefinitions($columns); |
274
|
|
|
|
275
|
|
|
/** @var string[] $pk */ |
276
|
|
|
if ($pk = $constraints[self::TABLE_PRIMARY] ?? []) { |
277
|
|
|
$defs[] = $this->toPrimaryKeyConstraint($table, $pk); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** @var string[] $unique */ |
281
|
|
|
foreach ($constraints[self::TABLE_UNIQUE] ?? [] as $unique) { |
282
|
|
|
$defs[] = $this->toUniqueKeyConstraint($table, $unique); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** @var string $local */ |
286
|
|
|
/** @var Column $foreign */ |
287
|
|
|
foreach ($constraints[self::TABLE_FOREIGN] ?? [] as $local => $foreign) { |
288
|
|
|
$defs[] = $this->toForeignKeyConstraint($table, $local, $foreign); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
$sql = sprintf( |
292
|
|
|
"CREATE TABLE %s (%s)", |
293
|
|
|
$table, |
294
|
|
|
implode(', ', $defs) |
295
|
|
|
); |
296
|
|
|
|
297
|
|
|
$this->db->exec($sql); |
298
|
|
|
return $this; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* `ALTER TABLE $table DROP COLUMN $column` |
303
|
|
|
* |
304
|
|
|
* @param string $table |
305
|
|
|
* @param string $column |
306
|
|
|
* @return $this |
307
|
|
|
*/ |
308
|
|
|
public function dropColumn (string $table, string $column) { |
309
|
|
|
$this->db->exec("ALTER TABLE {$table} DROP COLUMN {$column}"); |
310
|
|
|
unset($this->tables[$table]); |
311
|
|
|
return $this; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* `DROP TABLE IF EXISTS $table` |
316
|
|
|
* |
317
|
|
|
* @param string $table |
318
|
|
|
*/ |
319
|
|
|
public function dropTable (string $table): void { |
320
|
|
|
$this->db->exec("DROP TABLE IF EXISTS {$table}"); |
321
|
|
|
unset($this->tables[$table]); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* @param string $name |
326
|
|
|
* @return null|Table |
327
|
|
|
*/ |
328
|
|
|
public function getTable (string $name) { |
329
|
|
|
if (!isset($this->tables[$name])) { |
330
|
|
|
if ($this->db->isSQLite()) { |
331
|
|
|
$info = $this->db->query("PRAGMA table_info({$name})")->fetchAll(); |
332
|
|
|
$cols = array_column($info, 'name'); |
333
|
|
|
} |
334
|
|
|
else { |
335
|
|
|
$cols = $this->db->query( |
336
|
|
|
"SELECT column_name FROM information_schema.tables WHERE table_name = \"{$name}\"" |
337
|
|
|
)->fetchAll(DB::FETCH_COLUMN); |
338
|
|
|
} |
339
|
|
|
if (!$cols) { |
|
|
|
|
340
|
|
|
return null; |
341
|
|
|
} |
342
|
|
|
$this->tables[$name] = Table::factory($this->db, $name, $cols); |
343
|
|
|
} |
344
|
|
|
return $this->tables[$name]; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* Whether a table exists. |
349
|
|
|
* |
350
|
|
|
* @param string $table |
351
|
|
|
* @return bool |
352
|
|
|
*/ |
353
|
|
|
final public function offsetExists ($table): bool { |
354
|
|
|
return (bool)$this->offsetGet($table); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* Returns a table by name. |
359
|
|
|
* |
360
|
|
|
* @param string $table |
361
|
|
|
* @return null|Table |
362
|
|
|
*/ |
363
|
|
|
public function offsetGet ($table) { |
364
|
|
|
return $this->getTable($table); |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* @param $offset |
369
|
|
|
* @param $value |
370
|
|
|
* @throws LogicException |
371
|
|
|
*/ |
372
|
|
|
final public function offsetSet ($offset, $value) { |
373
|
|
|
throw new LogicException('The schema cannot be altered this way.'); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* @param $offset |
378
|
|
|
* @throws LogicException |
379
|
|
|
*/ |
380
|
|
|
final public function offsetUnset ($offset) { |
381
|
|
|
throw new LogicException('The schema cannot be altered this way.'); |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* `ALTER TABLE $table RENAME COLUMN $oldName TO $newName` |
386
|
|
|
* |
387
|
|
|
* @param string $table |
388
|
|
|
* @param string $oldName |
389
|
|
|
* @param string $newName |
390
|
|
|
* @return $this |
391
|
|
|
*/ |
392
|
|
|
public function renameColumn (string $table, string $oldName, string $newName) { |
393
|
|
|
$this->db->exec("ALTER TABLE {$table} RENAME COLUMN {$oldName} TO {$newName}"); |
394
|
|
|
unset($this->tables[$table]); |
395
|
|
|
return $this; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* `ALTER TABLE $oldName RENAME TO $newName` |
400
|
|
|
* |
401
|
|
|
* @param string $oldName |
402
|
|
|
* @param string $newName |
403
|
|
|
* @return $this |
404
|
|
|
*/ |
405
|
|
|
public function renameTable (string $oldName, string $newName) { |
406
|
|
|
$this->db->exec("ALTER TABLE {$oldName} RENAME TO {$newName}"); |
407
|
|
|
unset($this->tables[$oldName]); |
408
|
|
|
return $this; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* Sorts according to index priority, storage size/complexity, and name. |
413
|
|
|
* |
414
|
|
|
* @param int[] $types |
415
|
|
|
* @return int[] |
416
|
|
|
*/ |
417
|
|
|
protected function sortColumns (array $types): array { |
418
|
|
|
uksort($types, function(string $a, string $b) use ($types) { |
419
|
|
|
// descending index priority, increasing size, name |
420
|
|
|
return $types[$b] <=> $types[$a] ?: $a <=> $b; |
421
|
|
|
}); |
422
|
|
|
return $types; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* @param int[] $columns `[ name => <I_CONST> | <T_CONST> ]` |
427
|
|
|
* @return string[] |
428
|
|
|
*/ |
429
|
|
|
protected function toColumnDefinitions (array $columns): array { |
430
|
|
|
assert(count($columns) > 0); |
431
|
|
|
$columns = $this->sortColumns($columns); |
432
|
|
|
$defs = []; |
433
|
|
|
|
434
|
|
|
/** |
435
|
|
|
* @var string $name |
436
|
|
|
* @var int $type |
437
|
|
|
*/ |
438
|
|
|
foreach ($columns as $name => $type) { |
439
|
|
|
$defs[$name] = sprintf("%s %s", $name, $this->colDefs[$type & self::T_MASK]); |
440
|
|
|
if ($indexSql = $type & self::I_MASK) { |
441
|
|
|
$defs[$name] .= " {$this->colDefs[$indexSql]}"; |
442
|
|
|
} |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
return $defs; |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
/** |
449
|
|
|
* @param string $table |
450
|
|
|
* @param string $local |
451
|
|
|
* @param Column $foreign |
452
|
|
|
* @return string |
453
|
|
|
*/ |
454
|
|
|
protected function toForeignKeyConstraint (string $table, string $local, Column $foreign): string { |
455
|
|
|
return sprintf( |
456
|
|
|
'CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s) ON DELETE CASCADE', |
457
|
|
|
$this->toForeignKeyConstraint_name($table, $local), |
458
|
|
|
$local, |
459
|
|
|
$foreign->getQualifier(), |
460
|
|
|
$foreign->getName() |
461
|
|
|
); |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
/** |
465
|
|
|
* `FK_TABLE__COLUMN__COLUMN__COLUMN` |
466
|
|
|
* |
467
|
|
|
* @param string $table |
468
|
|
|
* @param string $column |
469
|
|
|
* @return string |
470
|
|
|
*/ |
471
|
|
|
protected function toForeignKeyConstraint_name (string $table, string $column): string { |
472
|
|
|
return 'FK_' . $table . '__' . $column; |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
/** |
476
|
|
|
* @param string $table |
477
|
|
|
* @param string[] $columns |
478
|
|
|
* @return string |
479
|
|
|
*/ |
480
|
|
|
protected function toPrimaryKeyConstraint (string $table, array $columns): string { |
481
|
|
|
return sprintf( |
482
|
|
|
'CONSTRAINT %s PRIMARY KEY (%s)', |
483
|
|
|
$this->toPrimaryKeyConstraint_name($table, $columns), |
484
|
|
|
implode(',', $columns) |
485
|
|
|
); |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
/** |
489
|
|
|
* `PK_TABLE__COLUMN__COLUMN__COLUMN` |
490
|
|
|
* |
491
|
|
|
* @param string $table |
492
|
|
|
* @param string[] $columns |
493
|
|
|
* @return string |
494
|
|
|
*/ |
495
|
|
|
protected function toPrimaryKeyConstraint_name (string $table, array $columns): string { |
496
|
|
|
sort($columns, SORT_STRING); |
497
|
|
|
return 'PK_' . $table . '__' . implode('__', $columns); |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
/** |
501
|
|
|
* @param string $table |
502
|
|
|
* @param string[] $columns |
503
|
|
|
* @return string |
504
|
|
|
*/ |
505
|
|
|
protected function toUniqueKeyConstraint (string $table, array $columns): string { |
506
|
|
|
return sprintf( |
507
|
|
|
'CONSTRAINT %s UNIQUE (%s)', |
508
|
|
|
$this->toUniqueKeyConstraint_name($table, $columns), |
509
|
|
|
implode(',', $columns) |
510
|
|
|
); |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* `UQ_TABLE__COLUMN__COLUMN__COLUMN` |
515
|
|
|
* |
516
|
|
|
* @param string $table |
517
|
|
|
* @param string[] $columns |
518
|
|
|
* @return string |
519
|
|
|
*/ |
520
|
|
|
protected function toUniqueKeyConstraint_name (string $table, array $columns): string { |
521
|
|
|
sort($columns, SORT_STRING); |
522
|
|
|
return 'UQ_' . $table . '__' . implode('__', $columns); |
523
|
|
|
} |
524
|
|
|
} |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.