Distributing One-Time Data "Hot Fixes"

Rolling out changes to *production data* when you don't have direct access to that data can be tricky. This tip makes the process easier.

Distributing One-Time Data "Hot Fixes"

Fixing production client data for a subset of a large user base is a tricky problem.

For example, we support about 75 tax collectors spread across four counties in Pennsylvania.  We prepare their annual tax data files which include billing information for all the taxpayers in their district.  Occasionally, we realize after sending out the data files that we made a mistake in the files for some of the collectors.

So how do you distribute a one-time data fix to multiple users without impacting unaffected users?

The ApplyHotFixes() Routine

At startup, our tax collection software calls the following routine:

Sub ApplyHotFixes()
    On Error GoTo Err_ApplyHotFixes

    If NeedsHotFix8335 Then ApplyHotFix8335
    If NeedsHotFix8044 Then ApplyHotFix8044
    If NeedsHotFix7668 Then ApplyHotFix7668
    If NeedsHotFix6484 Then ApplyHotFix6484

    Exit Sub
    Select Case Err.Number
    Case Else
        LogErr Err, Errors, "tcHotFixes", "ApplyHotFixes", elHide
    End Select
    Resume Exit_ApplyHotFixes
End Sub

The routine goes through a series of checks to see whether the current user's data file requires a particular hot fix.  If it does, the hotfix gets applied.

If there is an error within any of the calls, the error gets logged via the LogErr call (either to a local text file or to FogBugz via the BugzScout feature).  The elHide parameter is an error logging enumerated value that tells the LogErr function not to display the error to the user.  I include this to avoid an unexpected error message annoying the user on every startup.

This entire routine runs in under a tenth of a second almost every time it's called.

Key Objectives: Speed and Safety

In test driven development, the first step is to create a failing test.

We treat the NeedsHotFix#### function the same way here.  It should return True before running the associated ApplyHotFix#### routine and return False after that routine runs.

Once we confirm it works the way we expect, we can put it in place to act as a gatekeeper for the ApplyHotFix#### routine.

Achieving Speed

Rather than running an expensive query to check whether a problem exists in the data, I start with a series of very fast guard clauses.  

The goal is that these checks will disqualify the data environment without having to run the query in the first place.  Here's an example of what that looks like in practice:

Private Function NeedsHotFix8335() As Boolean
    If Duplicate.TaxDistrict <> td_BermudianSpringsSD Then Exit Function
    If App.G.Year <> 2020 Then Exit Function
    If App.G.CountyID <> C.CountyID.Adams Then Exit Function
    If Duplicate.EntityType <> et_School Then Exit Function
    If ECount("*", "Receipts", "(BillID=146128) AND (ReceiptID=1) AND (TCReceiptID=3117)") = 0 Then Exit Function
    NeedsHotFix8335 = True
End Function

I start out with four extremely fast-executing checks that will fail for most circumstances.  I run them in order from fastest to slowest to execute.  The first four have similarly fast execution speeds.  As a tie-breaker, I start with the most restrictive check (i.e., the one most likely to return True in the greatest number of situations).

Only after all four instant checks pass do I finally use Allen Browne's ECount() function to look inside the data to see if the hot fix needs to be applied.

Achieving Safety

Within my ApplyHotFix#### routines, I try to rely as little as possible on the NeedsHotFix#### routine being run first.

For example, I use the most restrictive WHERE clause I possibly can so that if somehow the ApplyHotFix#### routine gets run accidentally it doesn't ruin someone's day.  Here's an example of that:

Private Sub ApplyHotFix8335()
    Dim SQL As New clsSQL
    SQL.From = "Receipts"
    'This hot fix should only be applied to a single record 
    '    in the Bermudian Springs school district file
    SQL.Where = "District='10'"
    SQL.AndWhere "SchoolDist='B'"
    SQL.AndWhere "BillID=146128"
    SQL.AndWhere "ReceiptID=1"
    SQL.AndWhere "TCReceiptID=3117"
    SQL.SetValue "TCReceiptID", 1003117
    Debug.Print SQL.RecordsAffected
End Sub

In my development environment, I would run the above routine a few times to confirm that (A) it only changed the number of records it was supposed to on the first run and (B) it did not change any records on subsequent runs.

Sample Execution

Note: In the screenshot below, I did not switch the routine signatures to make them Public.  By fully qualifying the routine name of a Private Sub or Function you can call it from the Immediate Window!  Try it, it really works (you will have to type the name out...IntelliSense won't autocomplete a private routine).

High Signal to Noise Ratio

I use the NeedsHotFix#### and ApplyHotFix#### naming conventions because they convey a lot of information in a very compact format.

The number that gets appended to each routine name corresponds with a case number in our FogBugz database.  I can highlight the number then press [Ctrl] + [Win] + [F] and my browser will immediately open to the case information associated with that number.  

The FogBugz case provides way more detail and context than I would ever consider putting into a code comment.

(If you're curious how the [Ctrl] + [Win] + [F] trick works, it's very similar in concept to the [Ctrl] + [Win] + [G] AutoHotkey shortcut that I describe in this article.)

Referenced articles

Debugging Private Procedures
After more than 14 years as a VBA developer, I recently discovered that you can debug private procedures without temporarily making them public!
A Mind-Reading Script
What if you could highlight any text, press a hotkey, and have Windows do what you want almost every time? You’d have my top productivity hack.

External references

Microsoft Access tips: Extended DCount()
Code to count the number of records in a table/query in Microsoft Access, with the option to count the number of distinct values. A replacement for DCount().

Image by zoli gy from Pixabay