[Code Cleanup/Cache] Fixed an issue with the Redis cache engine where retrieving an empty array from the cache would be considered a miss rather than a hit.  It's more efficient as a hit, because otherwise the cache manager will keep requesting it during the same request.
[Code Cleanup] Fixed a "undeclared variable 'value'" error in the registry service when a variable isn't a delta integer.
* [Performance/Platform] Several of the original database tables (e.g. created in 4.x and early 5.x) had a redundant unique index for the 'id' column.  This isn't necessary since those tables already have a unique primary key.  This added a slight delay when modifying records, and it wasted a little disk space.  These redundant indexes are cleaned up by the update.
[Devblocks/Platform/Plugins] Added an _insert($table, $fields) method to DevblocksORMHelper.  This works like _update(), but it doesn't require that a blank row is inserted first, which is more efficient for simple immutable rows.  This method automatically escapes field values.
* [Performance/Activity Log] The insertion of activity log entries has been optimized.  Previously each insertion ran two database queries: one to insert a blank row and retrieve the new ID, and the second to update it. The same insert/update pattern is used elsewhere, but it's not necessary here because activity log records are never modified. This process now happens in a single query.
* [Performance/Watchers/VAs] The code that retrieves a list of watchers for a record has been optimized.  Previously, this was inefficiently finding results with a JOIN between links and worker records, which always bypassed the worker cache.  It could also run a second query to retrieve worker data in some situations.  The links are now retrieved as IDs without a JOIN, and the worker cache is always used to return record data.
* [Performance/Mail/Parser] Lookups are now cached when checking if an email address is owned by a worker.  This functionality is used frequently in the email parser where every bit of extra performance helps.
* [Performance/Mail/Parser] Mail routing rules are now cached when processing new email.  This functionality is used frequently in the email parser where every bit of extra performance helps.  The cache is automatically cleared every 20 minutes in order to rebalance the order that rules are checked in based on how often they match.
[Code Cleanup]
* [Performance/Mail/Parser] Multiple message headers are now inserted into the database in a single query.  Previously, headers were inserted individually.  Early profiling shows a 33% reduction in total queries when inserting many tickets at once (which the scheduler does quite often when polling POP3 mailboxes).
* [Performance/Mail/Anti-Spam] Optimized the method used to compute spam probabilities on incoming mail.  Previously, ticket and message data was always retrieved from the database, and scores were always saved directly back to ticket records.  Now arbitrary content can be analyzed and the spam score and interesting words are returned without extra database queries being performed.  In the parser, the anti-spam fields can be set at the same time as everything else, which eliminates several redundant queries for every message being processed.

* [Mail/Parser/Anti-Spam] Previously, the anti-spam algorithm split subjects like 'CamelCapsWordsWithoutSpaces' into multiple words before analyzing them.  Our research shows that these spam subjects haven't been popular for many years, so this code has been removed.  Additionally, this approach had the side-effect of not analyzing words at all when they alternated cases every letter 'LiKe tHiS'.  This content is now converted to lower-case and compared like normal.  The subject of an email message generally only represents a small portion of its overall content, and this change shouldn't have a negative impact on spam classification.
* [Performance/Mail/Routing] The parser's mail routing behavior no longer directly modifies records in the database.  Instead, changes are made to the in-memory 'model' that the parser manages before a ticket is created.  Any custom field values that are set from routing are now handled by the same code that sets custom fields on mail filtering behaviors (combining both actions into a single operation).
* [Performance/Mail/Parser] Previously, the mail parser created a ticket record in several steps (ID, meta, message links, spam score, group and bucket).  For instance, this prevented Virtual Attendants from detecting ticket changes too early in the process before the associated records were also inserted (custom fields, messages, activity log, etc).  While this worked, it had the downside of being quite inefficient, because event listeners and Virtual Attendants still reacted to every record change, but just ignored those events when the fields they cared about weren't present.  After a few design changes in the parser, tickets are now created in a single step, after all their related records have been created.  This is far more efficient, and early profiling shows a decent performance gain.
[Development/Plugins] trigger_error() is now used in DEVELOPMENT_MODE when a SQL query fails.  Previously these were only silently sent to the error log.
[Search] Updated the full-text max results to 500.
* [Cache/Platform] The cache service can now be instructed to store certain keys in local memory for the duration of the current request.  This is useful when the same underlying data is used over a short time period by multiple features without any communication between them (e.g. worklists, Virtual Attendant behaviors).  This can also be used when the cached data changes to frequently to persist for a longer period of time, or when the active cache engine can't manage volatile objects.
* [Cache/Filesystem/Platform] The filesystem cache engine now supports exclusive locking when writing to files. Previously, in busy environments it was possible (albeit rare) for two webserver processes to simultaneously write to the same cache file, which could lead to inconsistent file contents.
* [Cache/Redis/Platform] The Redis cache engine now generates a unique key prefix every time the cache is globally cleared.  This effectively namespaces keys so that old keys will become inactive and be automatically cleaned up (assuming Redis is running in 'maxmemory' mode).  This is proxy- and multi-tenant friendly since the 'flush' commands are never used.  Objects are now given a default time-to-live of 24 hours to help facilitate this.  This approach enables a lot of dynamic content to be cached without concern over exhausting the available memory (e.g. searches, records, worklists).  The unique prefix is stored in Redis, and the automatic namespacing is cluster friendly.  If the prefix is lost for any reason (expired, or Redis restarts) then a new one will be generated automatically and content will be re-cached by Cerb as it is retrieved again.
* [Cache/Platform] Cache engines now declare whether they support volatile content with an isVolatile() method.  This allows Cerb to adapt to the type of caching being used.  For instance, thousands of cache files won't be created in the local filesystem since this can create I/O bottlenecks (particularly when keys are frequently written, since reads are likely efficiently cached in memory by the OS already).  However, hundreds of thousands of cache keys could be written to Redis, Memcached, APC, or XCache without an I/O bottleneck.
* [Performance/Search/Cache] The results of full-text searches are now cached.  With a volatile cache engine (Redis, Memcached) they are cached for 5 minutes, and otherwise they expire at the end of the current request.  This solves the long-standing issue where full-text searches were being performed twice when subtotals were enabled.  In 6.7+, full-text search filter run first, so their results can be cached consistently regardless of other factors.  This means that if more than one worker searches for the same thing, the intermediate results are shared, even if the rest of their worklist filters differ.  Additionally, when a worker changes pages, and adds or removes filters, the expensive search no longer needs to run again.  This is a major performance improvement, since most logged 'slow queries' are the result of full-text searches being repeated (by subtotals, and by workers changing other filters).  For the best performance, you should enable a memory-based cache engine from Setup->Configure->Cache.