@@ -3063,152 +3063,14 @@ private function execute_alter() {
3063
3063
$ new_field ->mysql_data_type
3064
3064
);
3065
3065
3066
- // Update the column definition using a callback.
3067
- // This is extracted here so that the CHANGE COLUMN logic below can be moved to a method and reused.
3068
- // It will unlock modifying the existing schema (SET/DROP DEFAULT) and implementing additional
3069
- // expressions like MODIFY COLUMN and DROP COLUMN in the future. Additionally, it should also enable
3070
- // modifying multiple columns at once in the future to avoid unnecessary table copying.
3071
- $ update_column_callback = function ( $ old_name ) use ( $ from_name , $ new_field ) {
3072
- if ( $ from_name === $ old_name ) {
3073
- return $ this ->make_sqlite_field_definition ( $ new_field );
3074
- }
3075
- };
3076
-
3077
- /*
3078
- * In SQLite, there is no direct equivalent to the CHANGE COLUMN
3079
- * statement from MySQL. We need to do a bit of work to emulate it.
3080
- *
3081
- * The idea is to:
3082
- * 1. Get the existing table schema.
3083
- * 2. Adjust the column definition.
3084
- * 3. Copy the data out of the old table.
3085
- * 4. Drop the old table to free up the indexes names.
3086
- * 5. Create a new table from the updated schema.
3087
- * 6. Copy the data from step 3 to the new table.
3088
- * 7. Drop the old table copy.
3089
- * 8. Restore any indexes that were dropped in step 4.
3090
- */
3091
-
3092
- // 1. Get the existing table schema.
3093
- $ old_schema = $ this ->get_sqlite_create_table ( $ this ->table_name );
3094
- $ old_indexes = $ this ->get_keys ( $ this ->table_name , false );
3095
-
3096
- // 2. Adjust the column definition.
3097
-
3098
- // First, tokenize the old schema.
3099
- $ tokens = ( new WP_SQLite_Lexer ( $ old_schema ) )->tokens ;
3100
- $ create_table = new WP_SQLite_Query_Rewriter ( $ tokens );
3101
-
3102
- // Now, replace every reference to the old column name with the new column name.
3103
- $ renames = array ();
3104
- while ( true ) {
3105
- $ token = $ create_table ->consume ();
3106
- if ( ! $ token ) {
3107
- break ;
3108
- }
3109
- if ( WP_SQLite_Token::TYPE_STRING !== $ token ->type ) {
3110
- continue ;
3111
- }
3112
-
3113
- // We found the old column name, let's store it and remove it from the old schema.
3114
- $ old_name_token = $ create_table ->drop_last ();
3115
- $ old_name = $ old_name_token ->value ;
3116
-
3117
- // If the next token is a data type, we're dealing with a column definition.
3118
- $ is_column_definition = $ create_table ->peek ()->matches (
3119
- WP_SQLite_Token::TYPE_KEYWORD ,
3120
- WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE
3121
- );
3122
- if ( $ is_column_definition ) {
3123
- // Skip the old field definition in the old schema and store it separately.
3124
- $ field_depth = $ create_table ->depth ;
3125
- $ old_field_tokens = array ( $ old_name_token );
3126
- do {
3127
- $ field_terminator = $ create_table ->skip ();
3128
- $ old_field_tokens [] = new WP_SQLite_Token ( ' ' , WP_SQLite_Token::TYPE_WHITESPACE );
3129
- $ old_field_tokens [] = $ field_terminator ;
3130
- } while (
3131
- ! $ this ->is_create_table_field_terminator (
3132
- $ field_terminator ,
3133
- $ field_depth ,
3134
- $ create_table ->depth
3135
- )
3136
- );
3137
- array_pop ( $ old_field_tokens ); // terminator
3138
- $ old_field = new WP_SQLite_Query_Rewriter ( $ old_field_tokens );
3139
-
3140
- // Add an updated field definition.
3141
- // 1) string = new column definition,
3142
- // 2) null = no change, use the old definition.
3143
- // (We could add "false" to implement DROP COLUMN in the future.)
3144
- $ definition = $ update_column_callback ( $ old_name , $ old_field );
3145
- if ( null === $ definition ) {
3146
- $ old_field ->consume_all ();
3147
- $ definition = $ old_field ->get_updated_query ();
3066
+ $ this ->execute_change (
3067
+ function ( $ old_name ) use ( $ from_name , $ new_field ) {
3068
+ if ( $ from_name === $ old_name ) {
3069
+ return $ this ->make_sqlite_field_definition ( $ new_field );
3148
3070
}
3149
-
3150
- // Save new column name.
3151
- $ new_name = ( new WP_SQLite_Lexer ( $ definition ) )->tokens [0 ] ?? $ old_name ;
3152
- $ renames [ $ old_name ] = $ new_name ->value ;
3153
-
3154
- // Technically it's not a token, but it's fine to cheat a little bit.
3155
- $ create_table ->add ( new WP_SQLite_Token ( $ definition , WP_SQLite_Token::TYPE_KEYWORD ) );
3156
- // Restore the terminating "," or ")" token.
3157
- $ create_table ->add ( $ field_terminator );
3158
- } else {
3159
- // Otherwise, just add the new name in place of the old name we dropped.
3160
- $ create_table ->add (
3161
- isset ( $ renames [ $ old_name ] )
3162
- ? new WP_SQLite_Token ( '" ' . $ renames [ $ old_name ] . '" ' , WP_SQLite_Token::TYPE_STRING )
3163
- : $ old_name_token
3164
- );
3165
3071
}
3166
- }
3167
-
3168
- // 3. Copy the data out of the old table
3169
- $ cache_table_name = "_tmp__ {$ this ->table_name }_ " . rand ( 10000000 , 99999999 );
3170
- $ this ->execute_sqlite_query (
3171
- "CREATE TABLE ` $ cache_table_name` as SELECT * FROM ` $ this ->table_name ` "
3172
3072
);
3173
3073
3174
- // 4. Drop the old table to free up the indexes names
3175
- $ this ->execute_sqlite_query ( "DROP TABLE ` $ this ->table_name ` " );
3176
-
3177
- // 5. Create a new table from the updated schema
3178
- $ this ->execute_sqlite_query ( $ create_table ->get_updated_query () );
3179
-
3180
- // 6. Copy the data from step 3 to the new table
3181
- $ this ->execute_sqlite_query ( "INSERT INTO {$ this ->table_name } SELECT * FROM $ cache_table_name " );
3182
-
3183
- // 7. Drop the old table copy
3184
- $ this ->execute_sqlite_query ( "DROP TABLE ` $ cache_table_name` " );
3185
-
3186
- // 8. Restore any indexes that were dropped in step 4
3187
- foreach ( $ old_indexes as $ row ) {
3188
- /*
3189
- * Skip indexes prefixed with sqlite_autoindex_
3190
- * (these are automatically created by SQLite).
3191
- */
3192
- if ( str_starts_with ( $ row ['index ' ]['name ' ], 'sqlite_autoindex_ ' ) ) {
3193
- continue ;
3194
- }
3195
-
3196
- $ columns = array ();
3197
- foreach ( $ row ['columns ' ] as $ column ) {
3198
- $ columns [] = '` ' . ( $ renames [ $ column ['name ' ] ] ?? $ column ['name ' ] ) . '` ' ;
3199
- }
3200
-
3201
- $ unique = '1 ' === $ row ['index ' ]['unique ' ] ? 'UNIQUE ' : '' ;
3202
-
3203
- /*
3204
- * Use IF NOT EXISTS to avoid collisions with indexes that were
3205
- * a part of the CREATE TABLE statement
3206
- */
3207
- $ this ->execute_sqlite_query (
3208
- "CREATE $ unique INDEX IF NOT EXISTS ` {$ row ['index ' ]['name ' ]}` ON $ this ->table_name ( " . implode ( ', ' , $ columns ) . ') '
3209
- );
3210
- }
3211
-
3212
3074
if ( ', ' === $ alter_terminator ->token ) {
3213
3075
/*
3214
3076
* If the terminator was a comma,
@@ -3389,6 +3251,146 @@ private function execute_drop() {
3389
3251
}
3390
3252
}
3391
3253
3254
+ /**
3255
+ * Translates a CHANGE query.
3256
+ *
3257
+ * In SQLite, there is no direct equivalent to the CHANGE COLUMN
3258
+ * statement from MySQL. We need to do a bit of work to emulate it.
3259
+ *
3260
+ * The idea is to:
3261
+ * 1. Get the existing table schema.
3262
+ * 2. Adjust the column definition.
3263
+ * 3. Copy the data out of the old table.
3264
+ * 4. Drop the old table to free up the indexes names.
3265
+ * 5. Create a new table from the updated schema.
3266
+ * 6. Copy the data from step 3 to the new table.
3267
+ * 7. Drop the old table copy.
3268
+ * 8. Restore any indexes that were dropped in step 4.
3269
+ *
3270
+ * @param callable(string, WP_SQLite_Query_Rewriter): string|null $update_column_callback
3271
+ */
3272
+ private function execute_change ( $ update_column_callback ) {
3273
+ // 1. Get the existing table schema.
3274
+ $ old_schema = $ this ->get_sqlite_create_table ( $ this ->table_name );
3275
+ $ old_indexes = $ this ->get_keys ( $ this ->table_name , false );
3276
+
3277
+ // 2. Adjust the column definition.
3278
+
3279
+ // First, tokenize the old schema.
3280
+ $ tokens = ( new WP_SQLite_Lexer ( $ old_schema ) )->tokens ;
3281
+ $ create_table = new WP_SQLite_Query_Rewriter ( $ tokens );
3282
+
3283
+ // Now, replace every reference to the old column name with the new column name.
3284
+ $ renames = array ();
3285
+ while ( true ) {
3286
+ $ token = $ create_table ->consume ();
3287
+ if ( ! $ token ) {
3288
+ break ;
3289
+ }
3290
+ if ( WP_SQLite_Token::TYPE_STRING !== $ token ->type ) {
3291
+ continue ;
3292
+ }
3293
+
3294
+ // We found the old column name, let's store it and remove it from the old schema.
3295
+ $ old_name_token = $ create_table ->drop_last ();
3296
+ $ old_name = $ old_name_token ->value ;
3297
+
3298
+ // If the next token is a data type, we're dealing with a column definition.
3299
+ $ is_column_definition = $ create_table ->peek ()->matches (
3300
+ WP_SQLite_Token::TYPE_KEYWORD ,
3301
+ WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE
3302
+ );
3303
+ if ( $ is_column_definition ) {
3304
+ // Skip the old field definition in the old schema and store it separately.
3305
+ $ field_depth = $ create_table ->depth ;
3306
+ $ old_field_tokens = array ( $ old_name_token );
3307
+ do {
3308
+ $ field_terminator = $ create_table ->skip ();
3309
+ $ old_field_tokens [] = new WP_SQLite_Token ( ' ' , WP_SQLite_Token::TYPE_WHITESPACE );
3310
+ $ old_field_tokens [] = $ field_terminator ;
3311
+ } while (
3312
+ ! $ this ->is_create_table_field_terminator (
3313
+ $ field_terminator ,
3314
+ $ field_depth ,
3315
+ $ create_table ->depth
3316
+ )
3317
+ );
3318
+ array_pop ( $ old_field_tokens ); // terminator
3319
+ $ old_field = new WP_SQLite_Query_Rewriter ( $ old_field_tokens );
3320
+
3321
+ // Add an updated field definition.
3322
+ // 1) string = new column definition,
3323
+ // 2) null = no change, use the old definition.
3324
+ // (We could add "false" to implement DROP COLUMN in the future.)
3325
+ $ definition = $ update_column_callback ( $ old_name , $ old_field );
3326
+ if ( null === $ definition ) {
3327
+ $ old_field ->consume_all ();
3328
+ $ definition = $ old_field ->get_updated_query ();
3329
+ }
3330
+
3331
+ // Save new column name.
3332
+ $ new_name = ( new WP_SQLite_Lexer ( $ definition ) )->tokens [0 ] ?? $ old_name ;
3333
+ $ renames [ $ old_name ] = $ new_name ->value ;
3334
+
3335
+ // Technically it's not a token, but it's fine to cheat a little bit.
3336
+ $ create_table ->add ( new WP_SQLite_Token ( $ definition , WP_SQLite_Token::TYPE_KEYWORD ) );
3337
+ // Restore the terminating "," or ")" token.
3338
+ $ create_table ->add ( $ field_terminator );
3339
+ } else {
3340
+ // Otherwise, just add the new name in place of the old name we dropped.
3341
+ $ create_table ->add (
3342
+ isset ( $ renames [ $ old_name ] )
3343
+ ? new WP_SQLite_Token ( '" ' . $ renames [ $ old_name ] . '" ' , WP_SQLite_Token::TYPE_STRING )
3344
+ : $ old_name_token
3345
+ );
3346
+ }
3347
+ }
3348
+
3349
+ // 3. Copy the data out of the old table
3350
+ $ cache_table_name = "_tmp__ {$ this ->table_name }_ " . rand ( 10000000 , 99999999 );
3351
+ $ this ->execute_sqlite_query (
3352
+ "CREATE TABLE ` $ cache_table_name` as SELECT * FROM ` $ this ->table_name ` "
3353
+ );
3354
+
3355
+ // 4. Drop the old table to free up the indexes names
3356
+ $ this ->execute_sqlite_query ( "DROP TABLE ` $ this ->table_name ` " );
3357
+
3358
+ // 5. Create a new table from the updated schema
3359
+ $ this ->execute_sqlite_query ( $ create_table ->get_updated_query () );
3360
+
3361
+ // 6. Copy the data from step 3 to the new table
3362
+ $ this ->execute_sqlite_query ( "INSERT INTO {$ this ->table_name } SELECT * FROM $ cache_table_name " );
3363
+
3364
+ // 7. Drop the old table copy
3365
+ $ this ->execute_sqlite_query ( "DROP TABLE ` $ cache_table_name` " );
3366
+
3367
+ // 8. Restore any indexes that were dropped in step 4
3368
+ foreach ( $ old_indexes as $ row ) {
3369
+ /*
3370
+ * Skip indexes prefixed with sqlite_autoindex_
3371
+ * (these are automatically created by SQLite).
3372
+ */
3373
+ if ( str_starts_with ( $ row ['index ' ]['name ' ], 'sqlite_autoindex_ ' ) ) {
3374
+ continue ;
3375
+ }
3376
+
3377
+ $ columns = array ();
3378
+ foreach ( $ row ['columns ' ] as $ column ) {
3379
+ $ columns [] = '` ' . ( $ renames [ $ column ['name ' ] ] ?? $ column ['name ' ] ) . '` ' ;
3380
+ }
3381
+
3382
+ $ unique = '1 ' === $ row ['index ' ]['unique ' ] ? 'UNIQUE ' : '' ;
3383
+
3384
+ /*
3385
+ * Use IF NOT EXISTS to avoid collisions with indexes that were
3386
+ * a part of the CREATE TABLE statement
3387
+ */
3388
+ $ this ->execute_sqlite_query (
3389
+ "CREATE $ unique INDEX IF NOT EXISTS ` {$ row ['index ' ]['name ' ]}` ON $ this ->table_name ( " . implode ( ', ' , $ columns ) . ') '
3390
+ );
3391
+ }
3392
+ }
3393
+
3392
3394
/**
3393
3395
* Translates a SHOW query.
3394
3396
*
0 commit comments