Skip to content

Commit e7b8ce7

Browse files
authored
Add support for ON UPDATE CURRENT_TIMESTAMP columns (#150)
The `ON UPDATE CURRENT_TIMESTAMP` is a special MySQL-specific construct that can be used in column definitions. There is no such functionality in SQLite. This pull request implements the `ON UPDATE CURRENT_TIMESTAMP` functionality for both `CREATE` and `ALTER` table commands using **triggers**. With queries such as: ```sql CREATE TABLE _tmp_table ( id int(11) NOT NULL, created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP ); ALTER TABLE _tmp_table ADD COLUMN updated_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP; ``` The following triggers will be added: ```sql -- created_at CREATE TRIGGER "___tmp_table_created_at_on_update__" AFTER UPDATE ON "_tmp_table" FOR EACH ROW BEGIN UPDATE "_tmp_table" SET "created_at" = CURRENT_TIMESTAMP WHERE id = NEW.id; END; -- updated_at CREATE TRIGGER "___tmp_table_updated_at_on_update__" AFTER UPDATE ON "_tmp_table" FOR EACH ROW BEGIN UPDATE "_tmp_table" SET "updated_at" = CURRENT_TIMESTAMP WHERE id = NEW.id; END; ``` When the `ON UPDATE` is dropped using a query such as the following: ```sql ALTER TABLE _tmp_table CHANGE created_at created_at timestamp NULL, CHANGE COLUMN updated_at updated_at timestamp NULL ``` The triggers are dropped as well. To ensure this always works, the trigger is always dropped when a column is being altered. Then, if `ON UPDATE` is present, it will be recreated; otherwise, it will remain dropped. Resolves #148.
1 parent 4c63355 commit e7b8ce7

2 files changed

Lines changed: 241 additions & 6 deletions

File tree

tests/WP_SQLite_Translator_Tests.php

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,155 @@ public function testAlterTableAddNotNullVarcharColumn() {
10271027
);
10281028
}
10291029

1030+
public function testColumnWithOnUpdate() {
1031+
// CREATE TABLE with ON UPDATE
1032+
$this->assertQuery(
1033+
'CREATE TABLE _tmp_table (
1034+
id int(11) NOT NULL,
1035+
created_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP
1036+
);'
1037+
);
1038+
$results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
1039+
$this->assertEquals(
1040+
array(
1041+
(object) array(
1042+
'Field' => 'id',
1043+
'Type' => 'int(11)',
1044+
'Null' => 'NO',
1045+
'Key' => '',
1046+
'Default' => '0',
1047+
'Extra' => '',
1048+
),
1049+
(object) array(
1050+
'Field' => 'created_at',
1051+
'Type' => 'timestamp',
1052+
'Null' => 'YES',
1053+
'Key' => '',
1054+
'Default' => null,
1055+
'Extra' => '',
1056+
),
1057+
),
1058+
$results
1059+
);
1060+
1061+
// ADD COLUMN with ON UPDATE
1062+
$this->assertQuery(
1063+
'ALTER TABLE _tmp_table ADD COLUMN updated_at timestamp NULL ON UPDATE CURRENT_TIMESTAMP'
1064+
);
1065+
$results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
1066+
$this->assertEquals(
1067+
array(
1068+
(object) array(
1069+
'Field' => 'id',
1070+
'Type' => 'int(11)',
1071+
'Null' => 'NO',
1072+
'Key' => '',
1073+
'Default' => '0',
1074+
'Extra' => '',
1075+
),
1076+
(object) array(
1077+
'Field' => 'created_at',
1078+
'Type' => 'timestamp',
1079+
'Null' => 'YES',
1080+
'Key' => '',
1081+
'Default' => null,
1082+
'Extra' => '',
1083+
),
1084+
(object) array(
1085+
'Field' => 'updated_at',
1086+
'Type' => 'timestamp',
1087+
'Null' => 'YES',
1088+
'Key' => '',
1089+
'Default' => null,
1090+
'Extra' => '',
1091+
),
1092+
),
1093+
$results
1094+
);
1095+
1096+
// assert ON UPDATE triggers
1097+
$results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" );
1098+
$this->assertEquals(
1099+
array(
1100+
(object) array(
1101+
'type' => 'trigger',
1102+
'name' => '___tmp_table_created_at_on_update__',
1103+
'tbl_name' => '_tmp_table',
1104+
'rootpage' => '0',
1105+
'sql' => "CREATE TRIGGER \"___tmp_table_created_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"created_at\" = CURRENT_TIMESTAMP WHERE id = NEW.id;\n\t\t\tEND",
1106+
),
1107+
(object) array(
1108+
'type' => 'trigger',
1109+
'name' => '___tmp_table_updated_at_on_update__',
1110+
'tbl_name' => '_tmp_table',
1111+
'rootpage' => '0',
1112+
'sql' => "CREATE TRIGGER \"___tmp_table_updated_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"updated_at\" = CURRENT_TIMESTAMP WHERE id = NEW.id;\n\t\t\tEND",
1113+
),
1114+
),
1115+
$results
1116+
);
1117+
1118+
// on INSERT, no timestamps are expected
1119+
$this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (1)' );
1120+
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 1' );
1121+
$this->assertNull( $result[0]->created_at );
1122+
$this->assertNull( $result[0]->updated_at );
1123+
1124+
// on UPDATE, we expect timestamps in form YYYY-MM-DD HH:MM:SS
1125+
$this->assertQuery( 'UPDATE _tmp_table SET id = 2 WHERE id = 1' );
1126+
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 2' );
1127+
$this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->created_at );
1128+
$this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated_at );
1129+
1130+
// drop ON UPDATE
1131+
$this->assertQuery(
1132+
'ALTER TABLE _tmp_table
1133+
CHANGE created_at created_at timestamp NULL,
1134+
CHANGE COLUMN updated_at updated_at timestamp NULL'
1135+
);
1136+
$results = $this->assertQuery( 'DESCRIBE _tmp_table;' );
1137+
$this->assertEquals(
1138+
array(
1139+
(object) array(
1140+
'Field' => 'id',
1141+
'Type' => 'int(11)',
1142+
'Null' => 'NO',
1143+
'Key' => '',
1144+
'Default' => '0',
1145+
'Extra' => '',
1146+
),
1147+
(object) array(
1148+
'Field' => 'created_at',
1149+
'Type' => 'timestamp',
1150+
'Null' => 'YES',
1151+
'Key' => '',
1152+
'Default' => null,
1153+
'Extra' => '',
1154+
),
1155+
(object) array(
1156+
'Field' => 'updated_at',
1157+
'Type' => 'timestamp',
1158+
'Null' => 'YES',
1159+
'Key' => '',
1160+
'Default' => null,
1161+
'Extra' => '',
1162+
),
1163+
),
1164+
$results
1165+
);
1166+
1167+
// assert ON UPDATE triggers are removed
1168+
$results = $this->assertQuery( "SELECT * FROM sqlite_master WHERE type = 'trigger'" );
1169+
$this->assertEquals( array(), $results );
1170+
1171+
// now, no timestamps are expected
1172+
$this->assertQuery( 'INSERT INTO _tmp_table (id) VALUES (10)' );
1173+
$this->assertQuery( 'UPDATE _tmp_table SET id = 11 WHERE id = 10' );
1174+
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE id = 11' );
1175+
$this->assertNull( $result[0]->created_at );
1176+
$this->assertNull( $result[0]->updated_at );
1177+
}
1178+
10301179
public function testAlterTableWithColumnFirstAndAfter() {
10311180
$this->assertQuery(
10321181
"CREATE TABLE _tmp_table (

wp-includes/sqlite/class-wp-sqlite-translator.php

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,7 @@ private function execute_create_table() {
864864
$table = $this->parse_create_table();
865865

866866
$definitions = array();
867+
$on_updates = array();
867868
foreach ( $table->fields as $field ) {
868869
/*
869870
* Do not include the inline PRIMARY KEY definition
@@ -877,6 +878,10 @@ private function execute_create_table() {
877878
}
878879

879880
$definitions[] = $this->make_sqlite_field_definition( $field );
881+
if ( $field->on_update ) {
882+
$on_updates[ $field->name ] = $field->on_update;
883+
}
884+
880885
$this->update_data_type_cache(
881886
$table->name,
882887
$field->name,
@@ -917,6 +922,10 @@ private function execute_create_table() {
917922
$constraint->value
918923
);
919924
}
925+
926+
foreach ( $on_updates as $column => $on_update ) {
927+
$this->add_column_on_update_current_timestamp( $table->name, $column );
928+
}
920929
}
921930

922931
/**
@@ -1054,6 +1063,7 @@ private function parse_mysql_create_table_field() {
10541063
$result->default = false;
10551064
$result->auto_increment = false;
10561065
$result->primary_key = false;
1066+
$result->on_update = false;
10571067

10581068
$field_name_token = $this->rewriter->skip(); // Field name.
10591069
$this->rewriter->add( new WP_SQLite_Token( "\n", WP_SQLite_Token::TYPE_WHITESPACE ) );
@@ -1108,6 +1118,22 @@ private function parse_mysql_create_table_field() {
11081118
continue;
11091119
}
11101120

1121+
if (
1122+
$token->matches(
1123+
WP_SQLite_Token::TYPE_KEYWORD,
1124+
WP_SQLite_Token::FLAG_KEYWORD_RESERVED,
1125+
array( 'ON UPDATE' )
1126+
) && $this->rewriter->peek()->matches(
1127+
WP_SQLite_Token::TYPE_KEYWORD,
1128+
WP_SQLite_Token::FLAG_KEYWORD_RESERVED,
1129+
array( 'CURRENT_TIMESTAMP' )
1130+
)
1131+
) {
1132+
$this->rewriter->skip();
1133+
$result->on_update = true;
1134+
continue;
1135+
}
1136+
11111137
if ( $this->is_create_table_field_terminator( $token, $definition_depth ) ) {
11121138
$this->rewriter->add( $token );
11131139
break;
@@ -2927,6 +2953,7 @@ private function execute_alter() {
29272953
$op_subject = strtoupper( $op_raw_subject );
29282954
$mysql_index_type = $this->normalize_mysql_index_type( $op_subject );
29292955
$is_index_op = (bool) $mysql_index_type;
2956+
$on_update = false;
29302957

29312958
if ( 'ADD' === $op_type && ! $is_index_op ) {
29322959
if ( 'COLUMN' === $op_subject ) {
@@ -2947,18 +2974,44 @@ private function execute_alter() {
29472974
)
29482975
);
29492976

2950-
// Drop "FIRST" and "AFTER <another-column>", as these are not supported in SQLite.
2951-
$column_position = $this->rewriter->peek(
2977+
$comma = $this->rewriter->peek(
2978+
array(
2979+
'type' => WP_SQLite_Token::TYPE_OPERATOR,
2980+
'value' => ',',
2981+
)
2982+
);
2983+
2984+
// Handle "ON UPDATE CURRENT_TIMESTAMP".
2985+
$on_update_token = $this->rewriter->peek(
29522986
array(
29532987
'type' => WP_SQLite_Token::TYPE_KEYWORD,
2954-
'value' => array( 'FIRST', 'AFTER' ),
2988+
'value' => array( 'ON UPDATE' ),
29552989
)
29562990
);
29572991

2958-
$comma = $this->rewriter->peek(
2992+
if ( $on_update_token && ( ! $comma || $on_update_token->position < $comma->position ) ) {
2993+
$this->rewriter->consume(
2994+
array(
2995+
'type' => WP_SQLite_Token::TYPE_KEYWORD,
2996+
'value' => array( 'ON UPDATE' ),
2997+
)
2998+
);
2999+
if ( $this->rewriter->peek()->matches(
3000+
WP_SQLite_Token::TYPE_KEYWORD,
3001+
WP_SQLite_Token::FLAG_KEYWORD_RESERVED,
3002+
array( 'CURRENT_TIMESTAMP' )
3003+
) ) {
3004+
$this->rewriter->drop_last();
3005+
$this->rewriter->skip();
3006+
$on_update = $column_name;
3007+
}
3008+
}
3009+
3010+
// Drop "FIRST" and "AFTER <another-column>", as these are not supported in SQLite.
3011+
$column_position = $this->rewriter->peek(
29593012
array(
2960-
'type' => WP_SQLite_Token::TYPE_OPERATOR,
2961-
'value' => ',',
3013+
'type' => WP_SQLite_Token::TYPE_KEYWORD,
3014+
'value' => array( 'FIRST', 'AFTER' ),
29623015
)
29633016
);
29643017

@@ -3205,9 +3258,17 @@ private function execute_alter() {
32053258
)
32063259
);
32073260
$this->rewriter->drop_last();
3261+
3262+
$on_update_trigger_name = $this->get_column_on_update_current_timestamp_trigger_name( $this->table_name, $op_subject );
3263+
$this->execute_sqlite_query( "DROP TRIGGER IF EXISTS \"$on_update_trigger_name\"" );
3264+
32083265
$this->execute_sqlite_query(
32093266
$this->rewriter->get_updated_query()
32103267
);
3268+
3269+
if ( $on_update ) {
3270+
$this->add_column_on_update_current_timestamp( $this->table_name, $on_update );
3271+
}
32113272
} while ( $comma );
32123273

32133274
$this->results = 1;
@@ -4258,4 +4319,29 @@ private function generate_index_name( $table, $original_index_name ) {
42584319
// to allow easier splitting on __ later.
42594320
return preg_replace( '/_{2,}/', '_', $table ) . '__' . $original_index_name;
42604321
}
4322+
4323+
/**
4324+
* @param string $table
4325+
* @param string $column
4326+
*/
4327+
private function add_column_on_update_current_timestamp( $table, $column ) {
4328+
$trigger_name = $this->get_column_on_update_current_timestamp_trigger_name( $table, $column );
4329+
$this->execute_sqlite_query(
4330+
"CREATE TRIGGER \"$trigger_name\"
4331+
AFTER UPDATE ON \"$table\"
4332+
FOR EACH ROW
4333+
BEGIN
4334+
UPDATE \"$table\" SET \"$column\" = CURRENT_TIMESTAMP WHERE id = NEW.id;
4335+
END"
4336+
);
4337+
}
4338+
4339+
/**
4340+
* @param string $table
4341+
* @param string $column
4342+
* @return string
4343+
*/
4344+
private function get_column_on_update_current_timestamp_trigger_name( $table, $column ) {
4345+
return "__{$table}_{$column}_on_update__";
4346+
}
42614347
}

0 commit comments

Comments
 (0)