Skip to content

Commit 86ba0e5

Browse files
committed
Extract CHANGE COLUMN logic to a private method
This completes the changes in the previous commit.
1 parent 5cd057d commit 86ba0e5

File tree

1 file changed

+144
-142
lines changed

1 file changed

+144
-142
lines changed

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

Lines changed: 144 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -3063,152 +3063,14 @@ private function execute_alter() {
30633063
$new_field->mysql_data_type
30643064
);
30653065

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 );
31483070
}
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-
);
31653071
}
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`"
31723072
);
31733073

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-
32123074
if ( ',' === $alter_terminator->token ) {
32133075
/*
32143076
* If the terminator was a comma,
@@ -3389,6 +3251,146 @@ private function execute_drop() {
33893251
}
33903252
}
33913253

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+
33923394
/**
33933395
* Translates a SHOW query.
33943396
*

0 commit comments

Comments
 (0)