Completed
Push — master ( 6cab57...287bd7 )
by Sam
02:34
created

src/DB/Column.php (2 issues)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * This file contains only a single class.
4
 *
5
 * @file
6
 * @package Tabulate
7
 */
8
9
namespace WordPress\Tabulate\DB;
10
11
/**
12
 * The column class represents a single column in a single table in the database.
13
 */
14
class Column {
15
16
	/**
17
	 * The table to which this column belongs.
18
	 *
19
	 * @var Table
20
	 */
21
	private $table;
22
23
	/**
24
	 * The name of this column.
25
	 *
26
	 * @var string
27
	 */
28
	private $name;
29
30
	/**
31
	 * The type of this column.
32
	 *
33
	 * @var string
34
	 */
35
	private $type;
36
37
	/**
38
	 * The size, or length, of this column.
39
	 *
40
	 * @var integer
41
	 */
42
	private $size;
43
44
	/**
45
	 * This column's collation.
46
	 *
47
	 * @var string
48
	 */
49
	private $collation;
50
51
	/**
52
	 * The total number of digits in a DECIMAL column.
53
	 *
54
	 * @var integer
55
	 */
56
	private $precision;
57
58
	/**
59
	 * The number of digits after the decimal point in a DECIMAL column.
60
	 *
61
	 * @var integer
62
	 */
63
	private $scale;
64
65
	/**
66
	 * Whether or not this column is the Primary Key.
67
	 *
68
	 * @var boolean
69
	 */
70
	private $is_primary_key = false;
71
72
	/**
73
	 * Whether or not this column is a Unique Key.
74
	 *
75
	 * @var boolean
76
	 */
77
	private $is_unique = false;
78
79
	/**
80
	 * The default value for this column.
81
	 *
82
	 * @var mixed
83
	 */
84
	private $default_value;
85
86
	/**
87
	 * Whether or not this column is auto-incrementing.
88
	 *
89
	 * @var boolean
90
	 */
91
	private $is_auto_increment = false;
92
93
	/**
94
	 * Whether NULL values are allowed for this column.
95
	 *
96
	 * @var boolean
97
	 */
98
	private $nullable;
99
100
	/**
101
	 * Is this an unsigned number?
102
	 *
103
	 * @var boolean
104
	 */
105
	private $unsigned = false;
106
107
	/**
108
	 * ENUM options.
109
	 *
110
	 * @var string[]
111
	 */
112
	private $options;
113
114
	/**
115
	 * The comment attached to this column.
116
	 *
117
	 * @var string
118
	 */
119
	private $comment;
120
121
	/**
122
	 * The table that this column refers to, or false if it is not a foreign key.
123
	 *
124
	 * @var Table|false
125
	 */
126
	private $references = false;
127
128
	/**
129
	 * Create a column of a given table and based on given info.
130
	 *
131
	 * @param \WordPress\Tabulate\DB\Table $table The table that this column belongs to.
132
	 * @param string[]                     $info The output array of a SHOW COLUMNS query.
133
	 */
134
	public function __construct( Table $table, $info = false ) {
135
		$this->table = $table;
136
		$this->parse_info( $info );
137
	}
138
139
	/**
140
	 * Take the output of SHOW COLUMNS and populate this object's data.
141
	 *
142
	 * @param string[] $info The output array of a SHOW COLUMNS query.
143
	 */
144
	protected function parse_info( $info ) {
145
146
		// Name.
147
		$this->name = $info['Field'];
148
149
		// Type.
150
		$this->parse_type( $info['Type'] );
151
152
		// Default.
153
		$this->default_value = $info['Default'];
154
155
		// Primary key.
156
		if ( 'PRI' === strtoupper( $info['Key'] ) ) {
157
			$this->is_primary_key = true;
158
			if ( 'auto_increment' === $info['Extra'] ) {
159
				$this->is_auto_increment = true;
160
			}
161
		}
162
163
		// Unique key.
164
		$this->is_unique = ( 'UNI' === strtoupper( $info['Key'] ) );
165
166
		// Comment.
167
		$this->comment = $info['Comment'];
168
169
		// Collation.
170
		$this->collation = $info['Collation'];
171
172
		// Is this column NULL?
173
		$this->nullable = ( 'YES' === $info['Null'] );
174
175
		// Is this a foreign key?
176
		if ( in_array( $this->get_name(), $this->get_table()->get_foreign_key_names(), true ) ) {
177
			$referenced_tables = $this->get_table()->get_referenced_tables( false );
178
			$this->references = $referenced_tables[ $this->get_name() ];
179
		}
180
181
	}
182
183
	/**
184
	 * Get this column's name.
185
	 *
186
	 * @return string The name of this column.
187
	 */
188
	public function get_name() {
189
		return $this->name;
190
	}
191
192
	/**
193
	 * Get the valid options for this column; only applies to ENUM and SET.
194
	 *
195
	 * @return array The available options.
196
	 */
197
	public function get_options() {
198
		return $this->options;
199
	}
200
201
	/**
202
	 * Get the human-readable title of this column.
203
	 */
204
	public function get_title() {
205
		return \WordPress\Tabulate\Text::titlecase( $this->get_name() );
206
	}
207
208
	/**
209
	 * Get this column's type.
210
	 *
211
	 * @return string The type of this column.
212
	 */
213
	public function get_type() {
214
		return $this->type;
215
	}
216
217
	/**
218
	 * Get the definitive list of xtypes.
219
	 *
220
	 * @return string[] The xtypes.
221
	 */
222
	public static function get_xtypes() {
223
		return array(
224
			'text_short' => array(
225
				'name' => 'text_short',
226
				'title' => 'Text (short)',
227
				'type' => 'VARCHAR',
228
				'sizes' => 1,
229
				'options' => array(),
230
			),
231
			'text_long' => array(
232
				'name' => 'text_long',
233
				'title' => 'Text (long)',
234
				'type' => 'TEXT',
235
				'sizes' => 0,
236
				'options' => array( 'autop', 'html', 'md', 'rst', 'plain' ),
237
			),
238
			'integer' => array(
239
				'name' => 'integer',
240
				'title' => 'Integer',
241
				'type' => 'INT',
242
				'sizes' => 1,
243
				'options' => array(),
244
			),
245
			'boolean' => array(
246
				'name' => 'boolean',
247
				'title' => 'Boolean',
248
				'type' => 'TINYINT',
249
				'sizes' => 0,
250
				'options' => array(),
251
			),
252
			'decimal' => array(
253
				'name' => 'decimal',
254
				'title' => 'Decimal',
255
				'type' => 'DECIMAL',
256
				'sizes' => 2,
257
				'options' => array(),
258
			),
259
			'date' => array(
260
				'name' => 'date',
261
				'title' => 'Date',
262
				'type' => 'DATE',
263
				'sizes' => 0,
264
				'options' => array(),
265
			),
266
			'time' => array(
267
				'name' => 'time',
268
				'title' => 'Time',
269
				'type' => 'TIME',
270
				'sizes' => 0,
271
				'options' => array(),
272
			),
273
			'datetime' => array(
274
				'name' => 'datetime',
275
				'title' => 'Date & Time',
276
				'type' => 'DATETIME',
277
				'sizes' => 0,
278
				'options' => array(),
279
			),
280
			'fk' => array(
281
				'name' => 'fk',
282
				'title' => 'Cross Reference',
283
				'type' => 'INT',
284
				'sizes' => 1,
285
				'options' => array(),
286
			),
287
			'point' => array(
288
				'name' => 'point',
289
				'title' => 'Geographic location',
290
				'type' => 'POINT',
291
				'sizes' => 0,
292
				'options' => array(),
293
			),
294
			'enum' => array(
295
				'name' => 'enum',
296
				'title' => 'Fixed list',
297
				'type' => 'ENUM',
298
				'sizes' => 0,
299
				'options' => array(),
300
			),
301
		);
302
	}
303
304
	/**
305
	 * Get the X-Type of this column.
306
	 *
307
	 * @return string[] An array containing details of the xtype: name, title, type, sizes, and options.
308
	 */
309
	public function get_xtype() {
310
		$xtypes = self::get_xtypes();
311
		if ( $this->is_foreign_key() ) {
312
			return $xtypes['fk'];
313
		}
314
		if ( $this->is_boolean() ) {
315
			return $xtypes['boolean'];
316
		}
317
		// Otherwise fall back on the first xtype with a matching type.
318
		foreach ( $xtypes as $xtype ) {
319
			if ( strtoupper( $this->get_type() ) === $xtype['type'] ) {
320
				return $xtype;
321
			}
322
		}
323
		return false;
324
	}
325
326
	/**
327
	 * Set the X-Type of this column.
328
	 *
329
	 * @param string $type The name of the type.
330
	 */
331
	public function set_xtype( $type ) {
332
		$option_name = TABULATE_SLUG . '_xtypes';
333
		$xtypes = update_option( $option_name );
334
		$table_name = $this->get_table()->get_name();
335
		if ( ! is_array( $xtypes[ $table_name ] ) ) {
336
			$xtypes[ $table_name ] = array();
337
		}
338
		$xtypes[ $table_name ][ $this->get_name() ] = $type;
339
		update_option( $option_name, $xtypes );
340
	}
341
342
	/**
343
	 * Get the column's comment.
344
	 *
345
	 * @return string
346
	 */
347
	public function get_comment() {
348
		return $this->comment;
349
	}
350
351
	/**
352
	 * Get the default value for this column.
353
	 *
354
	 * @return mixed
355
	 */
356
	public function get_default() {
357
		if ( 'CURRENT_TIMESTAMP' === $this->default_value ) {
358
			return date( 'Y-m-d h:i:s' );
359
		}
360
		return $this->default_value;
361
	}
362
363
	/**
364
	 * Get this column's size, or (for ENUM columns) its CSV options string.
365
	 *
366
	 * @return string The size of this column.
367
	 */
368
	public function get_size() {
369
		$size = $this->size;
370
		if ( 'decimal' === $this->get_type() ) {
371
			$size = "$this->precision,$this->scale";
372
		}
373
		if ( 'enum' === $this->get_type() ) {
374
			return "'" . join( "','", $this->get_options() ) . "'";
375
		}
376
		return $size;
377
	}
378
379
	/**
380
	 * Whether or not a non-NULL value needs to be supplied for this column.
381
	 *
382
	 * Not-NULL columns that have default values are *not* considered to be
383
	 * required.
384
	 *
385
	 * @return boolean
386
	 */
387
	public function is_required() {
388
		$has_default = ( $this->get_default() !== null || $this->is_auto_increment() );
389
		return ( ! $this->nullable() && ! $has_default );
390
	}
391
392
	/**
393
	 * Whether or not this column is the Primary Key for its table.
394
	 *
395
	 * @return boolean True if this is the PK, false otherwise.
396
	 */
397
	public function is_primary_key() {
398
		return $this->is_primary_key;
399
	}
400
401
	/**
402
	 * Whether or not this column is a unique key.
403
	 *
404
	 * @return boolean True if this is a Unique Key, false otherwise.
405
	 */
406
	public function is_unique() {
407
		return $this->is_unique;
408
	}
409
410
	/**
411
	 * Whether or not this column is an auto-incrementing integer.
412
	 *
413
	 * @return boolean True if this column has AUTO_INCREMENT set, false otherwise.
414
	 */
415
	public function is_auto_increment() {
416
		return $this->is_auto_increment;
417
	}
418
419
	/**
420
	 * Whether or not this column is allowed to have NULL values.
421
	 *
422
	 * @return boolean
423
	 */
424
	public function nullable() {
425
		return $this->nullable;
426
	}
427
428
	/**
429
	 * Only NOT NULL text fields are allowed to have empty strings.
430
	 *
431
	 * @return boolean
432
	 */
433
	public function allows_empty_string() {
434
		$text_types = array( 'text', 'varchar', 'char' );
435
		$is_text_type = in_array( $this->get_type(), $text_types, true );
436
		return ( ! $this->nullable() ) && $is_text_type;
437
	}
438
439
	/**
440
	 * Is this a boolean field?
441
	 *
442
	 * This method deals with the silliness that is MySQL's boolean datatype. Or, rather, it will do when it's finished.
443
	 * For now, it just reports true when this is a TINYINT(1) column.
444
	 *
445
	 * @return boolean
446
	 */
447
	public function is_boolean() {
448
		return $this->get_type() === 'tinyint' && $this->get_size() === 1;
449
	}
450
451
	/**
452
	 * Whether this column is an unsigned number.
453
	 *
454
	 * @return boolean
455
	 */
456
	public function is_unsigned() {
457
		return $this->unsigned;
458
	}
459
460
	/**
461
	 * Whether or not this column is an integer, float, or decimal column.
462
	 */
463
	public function is_numeric() {
464
		$is_int = substr( $this->get_type(), 0, 3 ) === 'int';
465
		$is_decimal = substr( $this->get_type(), 0, 7 ) === 'decimal';
466
		$is_float = substr( $this->get_type(), 0, 5 ) === 'float';
467
		return $is_int || $is_decimal || $is_float;
468
	}
469
470
	/**
471
	 * Whether or not this column is a foreign key.
472
	 *
473
	 * @return boolean True if $this->_references is not empty, otherwise false.
474
	 */
475
	public function is_foreign_key() {
476
		return ! empty( $this->references );
477
	}
478
479
	/**
480
	 * Get the table object of the referenced table, if this column is a foreign
481
	 * key.
482
	 *
483
	 * @return Table The referenced table.
484
	 */
485
	public function get_referenced_table() {
486
		return $this->table->get_database()->get_table( $this->references );
487
	}
488
489
	/**
490
	 * Get the table that this column belongs to.
491
	 *
492
	 * @return Table The table object.
493
	 */
494
	public function get_table() {
495
		return $this->table;
496
	}
497
498
	/**
499
	 * Take an SQL string and parse out column information.
500
	 *
501
	 * @param string $type_string The SQL.
502
	 */
503
	private function parse_type( $type_string ) {
504
505
		$this->unsigned = ( false !== stripos( $type_string, 'unsigned' ) );
506
507
		$varchar_pattern = '/^((?:var)?char)\((\d+)\)/';
508
		$decimal_pattern = '/^decimal\((\d+),(\d+)\)/';
509
		$float_pattern = '/^float\((\d+),(\d+)\)/';
510
		$integer_pattern = '/^((?:big|medium|small|tiny)?int|year)\(?(\d+)\)?/';
511
		$enum_pattern = '/^(enum|set)\(\'(.*?)\'\)/';
512
513
		$this->type = $type_string;
514
		$this->size = null;
515
		$this->precision = null;
516
		$this->scale = null;
517
		$this->options = null;
518
		if ( preg_match( $varchar_pattern, $type_string, $matches ) ) {
519
			$this->type = $matches[1];
520
			$this->size = (int) $matches[2];
521
		} elseif ( preg_match( $decimal_pattern, $type_string, $matches ) ) {
522
			$this->type = 'decimal';
523
			$this->precision = $matches[1];
524
			$this->scale = $matches[2];
525
		} elseif ( preg_match( $float_pattern, $type_string, $matches ) ) {
526
			$this->type = 'float';
527
			$this->precision = $matches[1];
528
			$this->scale = $matches[2];
529
		} elseif ( preg_match( $integer_pattern, $type_string, $matches ) ) {
530
			$this->type = $matches[1];
531
			$this->size = (int) $matches[2];
532
		} elseif ( preg_match( $enum_pattern, $type_string, $matches ) ) {
533
			$this->type = $matches[1];
534
			$values = explode( "','", $matches[2] );
535
			$this->options = array_combine( $values, $values );
536
		}
537
	}
538
539
	/**
540
	 * Get a human-readable string representation of this column.
541
	 *
542
	 * @return string
543
	 */
544
	public function __toString() {
545
		$pk = ($this->is_primary_key) ? ' PK' : '';
546
		$auto = ($this->is_auto_increment) ? ' AI' : '';
547
		if ( $this->references ) {
548
			$ref = ' References ' . $this->references . '.';
549
		} else {
550
			$ref = '';
551
		}
552
		$size = ($this->size > 0) ? "($this->size)" : '';
553
		return $this->name . ' ' . strtoupper( $this->type ) . $size . $pk . $auto . $ref;
554
	}
555
556
	/**
557
	 * Get the defining SQL for this column.
558
	 *
559
	 * @return string
560
	 */
561
	public function get_current_column_definition() {
562
		return self::get_column_definition(
563
			$this->get_name(),
564
			$this->get_xtype()['name'],
565
			$this->get_size(),
566
			$this->nullable(),
567
			$this->get_default(),
568
			$this->is_auto_increment(),
569
			$this->is_unique(),
570
			$this->is_primary_key(),
0 ignored issues
show
$this->is_primary_key() is of type boolean, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
571
			$this->get_comment(),
0 ignored issues
show
$this->get_comment() is of type string, but the function expects a object<WordPress\Tabulate\DB\Table>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
572
			$this->get_referenced_table()
573
		);
574
	}
575
576
	/**
577
	 * Alter this column.
578
	 *
579
	 * @param string                       $new_name The column name.
580
	 * @param string                       $xtype_name The x-type name. Must exist.
581
	 * @param string                       $size Either a single integer, or a x,y string of two integers.
582
	 * @param boolean                      $nullable Whether to allow NULl values.
583
	 * @param string                       $default The default value.
584
	 * @param boolean                      $auto_increment Auto-increment or not.
585
	 * @param boolean                      $unique Whether a unique constraint should apply.
586
	 * @param string                       $comment The column's comment. Default empty.
587
	 * @param \WordPress\Tabulate\DB\Table $target_table The target table for a foreign key.
588
	 * @param string                       $after The column that this one will be after.
589
	 * @throws Exception If unable to alter the table.
590
	 */
591
	public function alter( $new_name = null, $xtype_name = null, $size = null, $nullable = null, $default = null, $auto_increment = null, $unique = null, $comment = null, $target_table = null, $after = null ) {
592
		// Any that have not been set explicitely should be unchanged.
593
		$new_name = ! is_null( $new_name ) ? (string) $new_name : $this->get_name();
594
		$xtype_name = ! is_null( $xtype_name ) ? (string) $xtype_name : $this->get_xtype()['name'];
595
		$size = ! is_null( $size ) ? $size : $this->get_size();
596
		$nullable = ! is_null( $nullable ) ? (boolean) $nullable : $this->nullable();
597
		$default = ! is_null( $default ) ? (string) $default : $this->get_default();
598
		$auto_increment = ! is_null( $auto_increment ) ? (boolean) $auto_increment : $this->is_auto_increment();
599
		$unique = ! is_null( $unique ) ? (boolean) $unique : $this->is_unique();
600
		$comment = ! is_null( $comment ) ? (string) $comment : $this->get_comment();
601
		$target_table = ! is_null( $target_table ) ? $target_table : $this->get_referenced_table();
602
603
		// Check the current column definition.
604
		$col_def = self::get_column_definition( $new_name, $xtype_name, $size, $nullable, $default, $auto_increment, $unique, $comment, $target_table, $after );
605
		if ( $this->get_current_column_definition() === $col_def ) {
606
			return;
607
		}
608
609
		// Drop the unique key if it exists; it'll be re-created after.
610
		$table = $this->get_table();
611
		$wpdb = $table->get_database()->get_wpdb();
612
		if ( $this->is_unique() ) {
613
			$sql = 'SHOW INDEXES FROM `' . $table->get_name() . '` WHERE Column_name LIKE "' . $this->get_name() . '"';
614
			foreach ( $wpdb->get_results( $sql, ARRAY_A ) as $index ) {
615
				$sql = "DROP INDEX `" . $index['Key_name'] . "` ON `" . $table->get_name() . "`";
616
				$wpdb->query( $sql );
617
			}
618
		}
619
620
		// Drop the FK if it exists; it'll be re-created after.
621
		if ( $this->is_foreign_key() ) {
622
			$fks_sql = 'SELECT CONSTRAINT_NAME AS fk_name FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS '
623
				. ' WHERE TABLE_SCHEMA = SCHEMA() '
624
				. ' AND TABLE_NAME = "' . $table->get_name() . '" '
625
				. ' AND CONSTRAINT_TYPE = "FOREIGN KEY" ';
626
			$fks = $wpdb->get_results( $fks_sql );
627
			foreach ( $fks as $key ) {
628
				$sql = 'ALTER TABLE `' . $table->get_name() . '` DROP FOREIGN KEY `' . $key->fk_name . '`';
629
				$wpdb->query( $sql );
630
			}
631
			$this->references = false;
632
		}
633
634
		// Alter the column.
635
		$sql = "ALTER TABLE `" . $table->get_name() . "` CHANGE COLUMN `" . $this->get_name() . "` $col_def";
636
		$wpdb->hide_errors();
637
		$altered = $wpdb->query( $sql );
638
		if ( false === $altered ) {
639
			$err = "Unable to alter '" . $table->get_name() . "." . $this->get_name() . "' "
640
				. " &mdash; $wpdb->last_error &mdash; <code>$sql</code>";
641
			throw new Exception( $err );
642
		}
643
		$wpdb->show_errors();
644
645
		// Reset the Column and Table objects' data.
646
		$table->reset();
647
		$sql = "SHOW FULL COLUMNS FROM `" . $table->get_name() . "` LIKE '$new_name'";
648
		$column_info = $table->get_database()->get_wpdb()->get_row( $sql, ARRAY_A );
649
		$this->parse_info( $column_info );
650
		if ( $this->is_foreign_key() ) {
651
			$this->get_referenced_table()->reset();
652
		}
653
	}
654
655
	/**
656
	 * Get an SQL column definition.
657
	 *
658
	 * @param string                       $name The column name.
659
	 * @param string                       $xtype_name The x-type name. Must exist.
660
	 * @param string                       $size Either a single integer, or a x,y string of two integers.
661
	 * @param boolean                      $nullable Whether to allow NULl values.
662
	 * @param string                       $default The default value.
663
	 * @param boolean                      $auto_increment Auto-increment or not.
664
	 * @param boolean                      $unique Whether a unique constraint should apply.
665
	 * @param string                       $comment The column's comment. Default empty.
666
	 * @param \WordPress\Tabulate\DB\Table $target_table The target table for a foreign key.
667
	 * @param string                       $after The column that this one will be after.
668
	 * @return string
669
	 */
670
	public static function get_column_definition( $name, $xtype_name = null, $size = null, $nullable = true, $default = null, $auto_increment = null, $unique = null, $comment = null, $target_table = null, $after = null ) {
671
		// Type.
672
		$xtypes = self::get_xtypes();
673
		$xtype = ( isset( $xtypes[ $xtype_name ] ) ) ? $xtypes[ $xtype_name ] : $xtypes['text_short'];
674
		$type_str = $xtype['type'];
675
		// Size or options.
676
		$size_str = '';
677
		if ( is_numeric( $xtype['sizes'] ) && $xtype['sizes'] > 0 ) {
678
			$size_str = '(' . ( $size ? : 50 ) . ')';
679
		}
680
		if ( 'enum' === $xtype_name ) {
681
			// If not already wraped in quotes, explode and quote each option.
682
			if ( 0 === preg_match( '/^["\'].*["\']$/', $size ) ) {
683
				$size = "'" . join( "','", explode( ',', $size ) ) . "'";
684
			}
685
			$size_str = "($size)";
686
		}
687
		if ( 'boolean' === $xtype_name ) {
688
			$size_str = '(1)';
689
		}
690
		// Nullable.
691
		$null_str = (true === $nullable) ? 'NULL' : 'NOT NULL';
692
		// Default.
693
		$default_str = '';
694
		if ( 'text_long' !== $xtype_name ) {
695
			$default_str = ! empty( $default ) ? "DEFAULT '$default'" : ( $nullable ? 'DEFAULT NULL' : '' );
696
		}
697
		$auto_increment_str = '';
698
		if ( $auto_increment && 'integer' === $xtype['name'] ) {
699
			$auto_increment_str = 'AUTO_INCREMENT';
700
		}
701
		$unique_str = $unique ? 'UNIQUE' : '';
702
		$comment_str = ! is_null( $comment ) ? "COMMENT '$comment'" : '';
703
704
		$after_str = ( ! empty( $after ) ) ? "AFTER `$after`" : '';
705
		if ( 'FIRST' === strtoupper( $after ) ) {
706
			$after_str = " FIRST ";
707
		}
708
709
		$ref_str = '';
710
		$sign_str = '';
711
		if ( $target_table instanceof \WordPress\Tabulate\DB\Table ) {
712
			$pk_col = $target_table->get_pk_column();
713
			$ref_str = ', ADD CONSTRAINT `' . $name . '_fk_to_' . $target_table->get_name() . '`'
714
				. ' FOREIGN KEY (`' . $name . '`) '
715
				. ' REFERENCES `' . $target_table->get_name() . '` '
716
				. ' (`' . $pk_col->get_name() . '`)';
717
			$type_str = $pk_col->get_type();
718
			$size_str = '(' . $pk_col->get_size() . ')';
719
			$sign_str = ($pk_col->is_unsigned()) ? 'UNSIGNED' : '';
720
		}
721
722
		// Put it all together.
723
		$col_def = "`$name` $type_str$size_str $sign_str $null_str $default_str $auto_increment_str $unique_str $comment_str $after_str $ref_str";
724
		return preg_replace( '/ +/', ' ', trim( $col_def ) );
725
726
	}
727
}
728