Quick Intro

Update scripts implement the hook_update_N() hook and should be placed in your modules .install file. The examples I'm about to show you are from a real project, I've trimmed some of them for the sake of being brief and names have been changed to protect the innocent. I've also left out several update scripts for the sake of not being redundant.

Examples

Now to the good stuff, Examples! and plenty of them.

Simple Update Script Using variable_set() and module_enable()

This example is probably one of the simplest and most common types of update scripts that I'll write. When working on a new feature, I'll download a new contrib module or maybe write a new custom module. When we deploy this feature we will have to enable that module and then do any setup and configuration for that. Typically there is a "misc" module of some sort that I use to enable the module using module_enable() and then setup our configuration using variable_set().

Simple Update Script Using Batch API

This next example takes advantage of the Batch API. In this case we wanted to remove a bunch of url aliases that were created by pathauto for content types that won't need one. This can also be used as an example for a potential helper function, since we are removing a bunch of aliases, a path_delete_multiple() function might be a nice to have, but since that doesn't exist, I copied the code from path_delete() and modified it here to meet my needs.

/**
* Implements hook_update_N().
* Removes aliases for content types that do not need them.
*/function mysite_misc_update_7002(&$sandbox){// Set default patterns to empty so that aliases are not generated for every// node. You will need to set the pathauto pattern for every content type that// you do want to have a generated alias.
variable_set('pathauto_node_pattern','');// Remove existing aliases for node types: marquee, news_feed$types=array('marquee','news_feed',);if(!isset($sandbox['max'])){$count_query= db_select('node','n')->condition('n.type',$types,'IN');$count_query->addExpression('COUNT(n.nid)','count');$sandbox['max']=$count_query->execute()->fetchField();$sandbox['position']=0;}$limit=200;$nids= db_select('node','n')->condition('n.type',$types,'IN')->fields('n',array('nid'))->orderBy('n.nid')->range($sandbox['position'],$limit)->execute()->fetchCol();// Loop through the node id's and build source paths... this is basically what// path_delete_multiple() would look like if it existed$sources=array();foreach($nidsas$nid){$sources[]='node/'.$nid;}unset($nids);$paths= db_select('url_alias')->condition('source',$sources,'IN')->fields('url_alias')->execute();
db_delete('url_alias')->condition('source',$sources,'IN')->execute();foreach($pathsas$path){$path=(array)$path;
module_invoke_all('path_delete',$path);
drupal_clear_path_cache($path['source']);}$sandbox['position']+=$limit;if($sandbox['max']>0&&$sandbox['max']>$sandbox['position']){$sandbox['#finished']=$sandbox['position']/$sandbox['max'];}else{$sandbox['#finished']=1;}}

Simple Update Script to Replace Default Images

This one is fairly unique. We were changing the default image for several content types which are managed in Features. In order to not override the Features on production and to properly test the changes in a staging environment, I decided that I could upload the new image files as unmanaged files, and then write a script to update the managed files to the new files. This made the most sense to me since Features needs to know what the file.fid is and there is no easy way to be sure of that unless the file already exists on production. I also took the time to remove any default images that were no longer going to be used.

/**
* Implements hook_update_N().
* Replaces our default images with the new ones by updating the managed_file,
* instead of having to upload new files and have overridden features.
* In order for this to work we need to rsync our new images up to prod as well.
*/function mysite_misc_update_7005(){// Features reports field_article_banner_image 'default_image' => '57'// We want to use the 4x3 image here$file= file_load(57);// Update everything about our file$file->filename='4x3_default.jpg';$file->uri='public://default_images/4x3_default.jpg';$file->filemime='image/jpeg';$file->filesize=49937;
file_save($file);// Features reports field_blog_image 'default_image' => '84'// We want to use the 4x3 image here as well, in this case, we are updating// the feature to use 57, and we will delete 84 here.$file= file_load(84);if(isset($file->fid)){
file_delete($file,TRUE);}}

Another Update Script Using Batch API

For this example, we were adding a new field to a content type which was managed in Features, and needed to update some of the nodes to a certain value for this new field. In this case it was all content that was created by a few different users. This example also uses the Batch API.

Simple Update Script that Updates a Block

This example updates a blocks configuration to set the pages. It also updates a link in the menu to go to a different page.

/**
* Implements hook_update_N().
* Updates a marquee page settings and the link to the main menu.
*/function mysite_shows_update_7001(){// Set the pages the block should be visible.$pages=<<<MY_MARQUEE_PAGES_7001
mypage
mypage/latest
mypage/most-popular
MY_MARQUEE_PAGES_7001;
db_update('block')->fields(array('pages'=>$pages,))->condition('module','views')->condition('delta','marquee-block_2')->execute();// Update the link to go to latest$link= menu_link_get_preferred('mypage','main-menu');$link['link_path']='mypage/latest';
menu_link_save($link);}

Simple Update Script to Automatically "reset" a Menu Link

This is also a-bit of a special use-case. In this example we had a custom module that defined a path in hook_menu(). We needed to change the path and what would happen is that Drupal does not remove the original link but notices that it has changed from what is in the database and adds a "reset" link from the Menu page. Instead of having to remember to login and click that link after deploying to production, this update script automatically finds that link (and any others that were going to the old path) and resets them, removing the duplicate item in our menu.

Heck yeah, update scripts FTW! Update scripts are great and a really good standard to set. One tip I learned is that if you programmatically create a block in an update script it won't actually show up until you visit the block page, so to get around this call _block_rehash() in your update script.

This is a great article on a topic that is incredibly important in ensuring the quality of deployments. Without scripted update scripts you can't truly know how things are going to work in your live environment and you are also reducing yourself to having one shot to get it right. Scripted deploys are critical in being able to make test runs so that you know with much more certainty how things are going to work on your live server. Trying to update a live server without update scripts is akin to trying to put on a play without any rehearsals; it's not likely to go very well, and it may be pretty embarrassing. That's ok for hobby sites, but most professional websites are worthy of more attention.

Update scripts go in your module's install file. So if your module is located in sites/all/modules/custom/my_module, your update script would be defined in sites/all/modules/custom/my_module/my_module.install

Great article with many helpful examples but do you have a particular method to guess all needed functions, variables, db queries and other operations you must whrite in your update script for each change you make on the site via browser? For example, do you have a method to get update/insert queries launched when you change a configuration on a module cause Devel only shows the Select queries launched to display the current page. And how can one be sure not to miss anything?

There's no simple answer, it requires detective work. It often helps to read the module's hook_schema(). Another method I use sometimes is `mysqldump --skip-extended-insert`, then look for the record I want. So for the block example this might output:

When you fetch (re-fetch, it's a sandbox), you should start at 0 again, not at the point we left, that's not interesting when we re-enter the sandbox, especially if it's a nid/uid/etc. nid's != positions.
When you use $sandbox['position'] for range, you actually skip the number of entries defined when the id's do not increment all the way up (deleted nodes for example).

This is not always the case, as it depends of the action you take on the items returned by the query.
If you want to process every item (ex. update node, update alias) then ***range($sandbox['position'], $limit)*** is the correct code.
If you want to delete every item (ex. delete node) then ***range(0, $limit)*** is the correct code.

Do I understand correctly that $sandbox is simply a placeholder to 'hold stuff' in memory while all the processes are rolling? I'm just curious about this specific use. Sorry if this is a total newb question.