I dug around for a bit in the admin site and discovered that most of the ACF fields remained intact, but 3 Text fields and one Yes/No field was no longer showing up from one particular Field Group we had set up.
So I took a look at the database, and here is what I learned:
ACF Tables
ACF is written in a way that allows it to work without creating additional tables in your WordPress instance. This presumably gives it the advantage of sneaking easily through WordPress upgrades (although the topic at hand makes me wonder about this...), but it makes for some wonky use of the standard WordPress tables.
ACF stores all its data in the wp_postmeta table, except for Field Groups, which are in the wp_posts table with post_type='acf'. Fields are associated with their Field Groups via the post_id of the main Field entry in wp_postmeta
ACF Records in wp_postmeta
Each ACF Field has one main record in the wp_postmeta table, plus two records for each use of that Field in a post or page or whereever you set the ACF Field to be used. Let's take a look at the records associated with one of the ACF Fields that still existed in the WordPress instance I was dealing with, the Description field:
In wp_postmeta there is one record with the following data:
meta_id: 41
post_id: 15
meta_key: field_51e859f459a64
meta_value: a:12:{s:3:"key";s:19:"field_51e859f459a64";s:5:"label";s:11:"Description";s:4:"name";s:11:"description";s:4:"type";s:8:"textarea";s:12:"instructions";s:37:"Enter the description of this product";s:8:"required";s:1:"1";s:13:"default_value";s:0:"";s:11:"placeholder";s:0:"";s:9:"maxlength";s:0:"";s:10:"formatting";s:2:"br";s:17:"conditional_logic";a:3:{s:6:"status";s:1:"0";s:5:"rules";a:1:{i:0;a:3:{s:5:"field";s:19:"field_51e86279a9bee";s:8:"operator";s:2:"==";s:5:"value";s:1:"1";}}s:8:"allorany";s:3:"all";}s:8:"order_no";i:1;}
The meta_id is just the unique identifier for this record in the wp_postmeta table.
The post_id is the id of the Field Group post in the wp_posts table to which this Field is associated.
The meta_key is a unique identifier generated by ACF for this field.
Finally, that scary looking meta_value is just a serialized PHP array that constitutes the settings for the ACF Field. (If you look closely, you can see that this particular field has the label 'Description' and is a 'textarea' field. This field happens to be conditional on another field, which manifests down in the "rules" sub-array. You can see the field id which this field is dependent on, and that it's dependent on the other field having a value of 1, and so on. The point of this post isn't to go too far into the bowels of ACF meta data, though. Just enough to fix the problem that I had. So let's move on.)
There are also a bunch of records that look like this in the database (I'll leave out meta_id here since we already know what that means):
post_id: 14
meta_key: description
meta_value: This is a description
and this:
post_id: 14
meta_key: _description
meta_value: field_51e859f459a6
There is one pair like this for each use of the field on the web site. Here, the post_id is not the id of the Field Group any more, but rather the id of the actual post in which this ACF Field is being used (this could be a page id or taxonomy_term id too, depending on the ACF Field's settings). The meta_key in the first case is just the 'name' you see in the meta_value of the main ACF record above (a la s:4:"name";s:11:"description";). The meta_value in the first case is the value of this instance of the field.
Now why that second record? Well, there needs to be a way to associate this instance of a description field with the main description field record, and the second record here serves that purpose. I'm guessing that the '_' is just a convention used by the ACF author to make it easy to determine which record is contains the actual value and which one contains the association info.
Armed with that info, we're ready to tackle the problem I encountered.
The Problem
When I looked at the wp_postmeta records associated with one of my missing fields (let's use the 'brochure_url' field as an example), I noticed that while there were indeed still lots of 'brochure_url' and '_brochure_url' records, there was no longer a 'field_xxxxxxxxxxxxx' record for this field. Uh oh!
I have no idea why that record went missing. Perhaps the upgrade script tried to do some database cleaning that doesn't mix well with ACF, or perhaps my client (or even myself?) accidentally deleted the field in the admin interface. Either way, for each of my missing fields, the main ACF Field record was gone but all the data pair records were still there.
The Fix
The fix turned out to be fairly easy, with one little trick requiring some work directly in the database. All I did was go back to the WordPress admin and recreate the missing fields, being careful to use exactly the same name as before. (I could easily determine the name since the data pair records were still in the database.)
After recreating the fields, they all have unique identifiers that no longer matched the ones that were used by these fields before. For example, my '_brochure_url' records all had the meta_value 'field_51e859f459a64', but my new main record for brochure_url has the meta_key 'field_51e860fa1ed53'.
Now for the trick: I went into my database (was stuck with phpMyAdmin), found the main field_51e860fa1ed53 record in wp_postmeta, and changed two bits of data to 'field_51e859f459a64': the meta_key, and the corresponding portion in the serialized meta_value for this record.
Et voila! The site was back to normal.
So just to be clear, here's the 'before' picture:
The association record:
meta_key: _description
meta_value: field_51e859f459a64
The main field record:
post_id: 15
meta_key: field_51e860fa1ed53
meta_value: a:14:{s:3:"key";s:19:"field_51e860fa1ed53";s:5:"label";s:12:"Brochure URL";s:4:"name";s:12:"brochure_url";s:4:"type";s:4:"text";s:12:"instructions";s:35:"A URL to a brochure pdf or web site";s:8:"required";s:1:"0";s:13:"default_value";s:0:"";s:11:"placeholder";s:0:"";s:7:"prepend";s:0:"";s:6:"append";s:0:"";s:10:"formatting";s:4:"none";s:9:"maxlength";s:0:"";s:17:"conditional_logic";a:3:{s:6:"status";s:1:"0";s:5:"rules";a:1:{i:0;a:3:{s:5:"field";s:4:"null";s:8:"operator";s:2:"==";s:5:"value";s:0:"";}}s:8:"allorany";s:3:"all";}s:8:"order_no";i:5;}
The association record:
post_id: 14
meta_key: _description
meta_value: field_51e859f459a64
The main field record:
post_id: 15
meta_key: field_51e859f459a64
meta_value: a:14:{s:3:"key";s:19:"field_51e859f459a64";s:5:"label";s:12:"Brochure URL";s:4:"name";s:12:"brochure_url";s:4:"type";s:4:"text";s:12:"instructions";s:35:"A URL to a brochure pdf or web site";s:8:"required";s:1:"0";s:13:"default_value";s:0:"";s:11:"placeholder";s:0:"";s:7:"prepend";s:0:"";s:6:"append";s:0:"";s:10:"formatting";s:4:"none";s:9:"maxlength";s:0:"";s:17:"conditional_logic";a:3:{s:6:"status";s:1:"0";s:5:"rules";a:1:{i:0;a:3:{s:5:"field";s:4:"null";s:8:"operator";s:2:"==";s:5:"value";s:0:"";}}s:8:"allorany";s:3:"all";}s:8:"order_no";i:5;}
Note the two places that I had to change in the main ACF Field record so that the field's unique id corresponded to the old unique id.