WTF with CF Structs


I like ColdFusion's structs, they are simple and easy to use/reference, work great for transporting bits of related data, and names are so much easier to reference than numbers. 😉 We use them in all of our applications for returning result messages and the like. Never really had any issues with them, until something came up this week that literally had my partner and I going WTF??

For the application we're working on, we have “products”, which can be events, online trainings, etc. Each product has a core set of properties that are identical, but there are some properties unique to each type. For example, events have locations and dates for when they occur and when people can register, while online trainings are always available and may have an access URL attached to it. Rather than have a bunch of maybe properties on the products, we have a struct called ExtraProductDetails that we push those bits into, all nicely encapsulated in a function as we call those from several places:

<cffunction name="getProductExtraDetails" acces="public" returntype="struct" output="false">
    <cfargument name="ProductID" type="numeric" required="true" />
    <cfargument name="ProductTypeID" type="numeric" required="false" />

    <cfset var qProductExtra = "" />
    <cfset var sExtraProductDetails = StructNew() />

    <!--- if we have a location, throw it into the struct--->
    <cfquery name="qProductExtra" datasource="#variables.dsn#">
        SELECT address, buildingname, city, base_states_stateid, postalcode, base_countries_countryid
        FROM products_locations
        WHERE products_productid = #Arguments.ProductID#
    </cfquery>

    <cfif qProductExtra.RecordCount GT 0 OR (IsDefined("Arguments.ProductTypeID") AND Arguments.ProductTypeID EQ 1)>
        <cfset StructAppend(sExtraProductDetails, variables.oUtilities.queryRowToStruct(qProductExtra)) />
    </cfif>

    <!--- what about dates?  --->
    <cfquery name="qProductExtra" datasource="#variables.dsn#">
        SELECT startdate, enddate, registrationstart, registrationend, earlyregistrationend, lateregistrationstart
        FROM products_productdates
        WHERE products_productid = #Arguments.ProductID#
    </cfquery>

    <cfif qProductExtra.RecordCount GT 0 OR (IsDefined("Arguments.ProductTypeID") AND Arguments.ProductTypeID EQ 1)>
        <cfset StructAppend(sExtraProductDetails, variables.oUtilities.queryRowToStruct(qProductExtra)) />
    </cfif>

    <cfquery name="qProductExtra" datasource="#variables.dsn#">
        SELECT onlinetrainingurl
        FROM products
        WHERE productid = #Arguments.ProductID#
    </cfquery>

    <!--- if we have an online training URL, add it to the struct --->
    <cfif qProductExtra.onlinetrainingurl NEQ "" OR (IsDefined("Arguments.ProductTypeID") AND Arguments.ProductTypeID EQ 2)>
        <cfset sExtraProductDetails.onlinetrainingurl = qProductExtra.onlinetrainingurl />
    </cfif>

    <cfreturn sExtraProductDetails />
</cffunction>

Everything seemed great until we were testing a bug fix somewhere else and made an event that had no location…which then borked up one or our table displays royally.


Pretty dataTable


Not so pretty, no longer sorted, broken dataTable

When we explored why the table broke, we discovered that the product with no location was also missing the empty fields that should have been there. You may be thinking, so what? Your function just didn't include it because it didn't have one. Of course, that was our first thought too…and that is where the weirdness starts. We did the usual debugging type things, i.e. dump the struct for the event that has no location within getProductExtraDetails right before the return line:

That's what we expect – looks good. Dump the same structure in the original function that calls getProductExtraDetails:

<cfset sAdditionalDetails = getProductExtraDetails(ProductID = productid) />
<cfdump var="#sAdditionalDetails#" abort="true" />

WTF?? Somewhere in there, it suddenly drops all of the location fields. At first I wondered if it was some oddity with the struct set, but we got the same result if we dumped getProductExtraDetails(ProductID = productid) directly. Then I wondered if it could be an issue with the queryRowToStruct function we use and it being CFSCRIPT, so I rewrote it to tags, but same results. Tried explicitly resetting the sAdditionalDetails struct to new within the loop before populating, in case of some weird bleed over – still no change.

For an extra twist, this code worked fine in earlier testing when we did tests with stuff with no locations. It only started borking recently. Think think think think…not too long ago I cleaned up code to split out the date and location stuff, as an product could have dates without a location, so we wanted it more flexible. So maybe the issue is with the StructAppend? What if we do regular sets instead (while still having a bit of flexibility)?

<cfif qProductExtra.RecordCount GT 0 OR (IsDefined("Arguments.ProductTypeID") AND Arguments.ProductTypeID EQ 1)>
    <cfloop list="#qProductExtra.ColumnList#" index="thisColumn">
        <cfset sExtraProductDetails['#thisColumn#'] = qProductExtra[thisColumn][1] />
    </cfloop>
</cfif>

Same results. Okay, fine, what if we just get clunky with it and do this?

<cfset sExtraProductDetails.address = qProductExtra.address />
<cfset sExtraProductDetails.buildingname = qProductExtra.buildingname />
<cfset sExtraProductDetails.city = qProductExtra.city />
<cfset sExtraProductDetails.base_states_stateid = qProductExtra.base_states_stateid />
<cfset sExtraProductDetails.postalcode = qProductExtra.postalcode />
<cfset sExtraProductDetails.base_countries_countryid = qProductExtra.base_countries_countryid />

They are still getting dropped! It can't just be an issue of it dropping “blanks” because LateRegistrationStart is coming over with a blank as well. So seriously…WTF?

I have absolutely no explanation of what might be causing this bug (and yes, at this point, I'm calling it a serious bug in CF 9). Googling found me no similar issues reported anywhere and nothing in the documents explains why a struct would selectively drop some elements, but not others. I have no idea if you get the same on CF 10 as all of our boxes are CF 9.

At this point, the only fix we found was a ugly work around of basically creating the structure before populating it as well as switching out the StructAppend

<cffunction name="getProductExtraDetails" acces="public" returntype="struct" output="false">
    <cfargument name="ProductID" type="numeric" required="true" />
    <cfargument name="ProductTypeID" type="numeric" required="false" />

    <cfset var qProductExtra = "" />
    <cfset var sExtraProductDetails = StructNew() />
   
    <!--- Prebuild our struct cause otherwise some of the values sometimes get lost when passed back to the callers on whims --->
    <cfif IsDefined(Arguments.ProductTypeID) AND Arguments.ProductTypeID EQ 1>
        <cfset sExtraProductDetails.address = "" />
        <cfset sExtraProductDetails.buildingname = "" />
        <cfset sExtraProductDetails.city = "" />
        <cfset sExtraProductDetails.base_states_stateid = "" />
        <cfset sExtraProductDetails.postalcode = "" />
        <cfset sExtraProductDetails.base_countries_countryid = "" />
        <cfset sExtraProductDetails.startdate = "" />
        <cfset sExtraProductDetails.enddate = "" />
        <cfset sExtraProductDetails.registrationstart = "" />
        <cfset sExtraProductDetails.registrationend = "" />
        <cfset sExtraProductDetails.earlyregistrationend = "" />
        <cfset sExtraProductDetails.lateregistrationstart = "" />
       
    <cfelseif IsDefined(Arguments.ProductTypeID) AND Arguments.ProductTypeID EQ 2>
        <cfset sExtraProductDetails.onlinetrainingurl = "" />
    </cfif>
   
    <!--- if we have a location, throw it into the struct--->
    <cfquery name="qProductExtra" datasource="#variables.dsn#">
        SELECT address, buildingname, city, base_states_stateid, postalcode, base_countries_countryid
        FROM products_locations
        WHERE products_productid = #Arguments.ProductID#
    </cfquery>

    <cfif qProductExtra.RecordCount GT 0 OR (IsDefined("Arguments.ProductTypeID") AND Arguments.ProductTypeID EQ 1)>
        <cfloop list="#qProductExtra.ColumnList#" index="thisColumn">
            <cfset sExtraProductDetails['#thisColumn#'] = qProductExtra[thisColumn][1] />
        </cfloop>
    </cfif>

    <!--- what about dates?  --->
    <cfquery name="qProductExtra" datasource="#variables.dsn#">
        SELECT startdate, enddate, registrationstart, registrationend, earlyregistrationend, lateregistrationstart
        FROM products_productdates
        WHERE products_productid = #Arguments.ProductID#
    </cfquery>

    <cfif qProductExtra.RecordCount GT 0 OR (IsDefined("Arguments.ProductTypeID") AND Arguments.ProductTypeID EQ 1)>
        <cfloop list="#qProductExtra.ColumnList#" index="thisColumn">
            <cfset sExtraProductDetails['#thisColumn#'] = qProductExtra[thisColumn][1] />
        </cfloop>
    </cfif>

    <cfquery name="qProductExtra" datasource="#variables.dsn#">
        SELECT onlinetrainingurl
        FROM products
        WHERE productid = #Arguments.ProductID#
    </cfquery>

    <!--- if we have an online training URL, add it to the struct --->
    <cfif qProductExtra.onlinetrainingurl NEQ "" OR (IsDefined("Arguments.ProductTypeID") AND Arguments.ProductTypeID EQ 2)>
        <cfloop list="#qProductExtra.ColumnList#" index="thisColumn">
            <cfset sExtraProductDetails['#thisColumn#'] = qProductExtra[thisColumn][1] />
        </cfloop>
    </cfif>

    <cfreturn sExtraProductDetails />
</cffunction>

If anyone else has experienced this or has any clues on it, I'd love to hear from you. Also would love to know if it happens on CF 10 or on other CF servers (i.e. Railo? Blue Dragon?)