Author Archives: Greg Gaughan

A Different View (part 2)

In the previous post, we saw how we could use a single, raw query in Django to combat ORMs’ tendency to generate query explosions within loops. We used raw() with joins and some column renaming to ensure all the data we needed came back in one go. We had to modify the property names in the template slightly (e.g. book.jacket.image became book.jacket_image) and the result was a RawQuerySet which had a fair number of limitations but, we avoided the typical order-of-magnitude increase in query count.

Super models

supermodel - by Jamie Beck and Kevin Burg

There is a way we can use a raw query, to get all the benefits described in the previous post, and return a real QuerySet. We need a Django model and so need an underlying table. A much underused feature of SQL databases is the view. Views are virtual tables derived from queries. Most are currently implemented as read-only, but they provide many benefits – again another blog post would be needed to explain them all – but they include:

  • adding a level of abstraction, effectively an API (which is also available to other applications)
  • protecting the application from underlying table and column changes
  • giving fine-grained control of permissions, especially row-level
  • providing a way to efficiently add calculated columns such as line_total = quantity * price
  • avoiding de-normalising things which should remain derived from other data
  • avoiding the not-insubstantial cost of repeatedly sending the same complicated queries over the network
  • enabling the database to pre-compile and cache the privilege-checks and query plans

So we could define a database view (with a few extra foreign-key columns for use later):

CREATE VIEW vbook AS
SELECT
  book.id,
  book.id AS book_id,
  book.title,
  jacket.id AS jacket_id,
  jacket.image AS jacket_image,
  author.id AS author_id
  author.name AS author_name,
  shelf."position"
FROM
  book
  NATURAL JOIN shelf
  NATURAL JOIN author
  NATURAL JOIN jacket

and then map a Django model to it, using Meta.db_table='vbook', and set the Meta.managed = False to tell Django not to maintain the table definition.

(note again the use of natural join for simplicity (and short table names) and that they don’t actually work as-is with Django’s table/column naming)

We’ve done this in the past for some of our projects, and shoe-horning the view-creation script into Django/buildout was always the fiddly bit.

Recently, we used django-postgres which makes the model-view mapping a bit simpler. You subclass pg.View instead of models.Model and set the sql property to your query, e.g.

class vBook(pg.View):    title = models.CharField(max_length=100)
    jacket_image = models.ImageField(null=True)
    author_name = models.CharField(max_length=100)
    shelf_position = models.IntegerField()
 
    sql = BOOK_DETAILS

manage.py sync_pgviews then creates the views for the models in the specified application. It’s quite a lightweight tool, and simply builds and issues the SQL CREATE VIEW statements needed to wrap each view-based model’s sql query.

(There is a Kickstarter project under way which should also add view handling to Django)

book_listingWe now have an almost fully-fledged Django model. We can query the objects against an effectively de-normalised table, while still retaining an essentially normalised schema. We can chain refinement methods to the resulting QuerySet to further filter and order the objects, including select_related() if necessary (although it would always be preferable, given time, to put the extra join into the view). We can have custom object managers, Meta options, introspection and all the other features of a Django model, even some read-only admin which would be good for reporting.

Note that we removed the

WHERE shelf.position <= 10

from the view definition. We can define the view without such filters and leave it to the Django application to apply them, e.g.

vbooks = vBook.objects.filter(shelf__position__lte = 10)

This gives a more flexible model, more akin to the original Book model, although you could keep the WHERE clauses in if you wanted to logically partition a table into multiple views/models.

Agile

Another great reason for using such view-based models is to postpone decisions about optimising bits of the system before we know anything about it. As an example, let’s say that a book needs to have an average rating that should be calculated from the entries in a ratings table. This could accurately be calculated using SQL’s AVG function, but that might be expensive and so we might need to pre-calculate it and possibly store it outside of the database. That would require us to design, build, test, document, and maintain:

  • an external data source capable of handling the unknown load and volume
  • a way to link a book to the external data source, possibly using new drivers
  • a way of updating the external data source for a book, duplicating the rating table entries
  • a way to synchronise the two data storage systems, possibly involving cron jobs and queues
  • a way to build and sustain the data source and links in whatever production environment we use, e.g. AWS

All of this would add complexity, up-front R&D and ongoing maintenance. The results would not be as fresh or reliable as the database results, and not necessarily any faster. Using AVG in a view leaves us scope to replace how it is calculated at a later date, once we know more about the performance.

Often, caching the book results using Django’s cache is enough and we can stick with the basic approach. Premature optimisation could well be a huge waste of effort, and the view-based models let us defer those decisions and get started very quickly.

Abstract

Picasso - Skull and Pitcher

Defining views early in a project could also be a way to postpone building complex parts of the system. Let’s say a book has a complicated ‘cost’ attribute and we’re not sure yet how it will be calculated or where all the data will come from but we need to start to display it during the initial iterations. We could add the column to the book view on day one and worry about where it comes from later. e.g.

CREATE VIEW vbook AS
SELECT
  book.id,
  book.id AS book_id,
  book.title,
  6.283185 AS cost, --todo: use a cost function here instead  ...

And then vbook.cost can be used in the knowledge that the reference won’t change. Also, if the cost calculation is defined within the view and it needs to change post-production, the view can be recreated while the application is running with no migration or down-time.

More modelling

We can further enhance the view-based model and add relationships to give us back the all-too-convenient dot-notation – still useful if we’re careful to use it outside of large loops. We should make sure any relationships don’t give Django the impression that it needs to try to maintain integrity – it doesn’t need to since the underlying table only has virtual rows. We can do this using on_delete=models.DO_NOTHING, e.g.

class vBook(pg.View):
    title = models.CharField(max_length=100)
    jacket_image = models.ImageField(null=True)
    author_name = models.CharField(max_length=100)
    shelf_position = models.IntegerField()
 
    author = models.ForeignKey(Author,                               on_delete=models.DO_NOTHING)             #needs author_id to be returned by the sql 
    sql = BOOK_DETAILS

These are, of course, complete Django relationships and so we can access them from any direction, e.g.

my_author.vbook_set.all()

would return an author’s books as vBook objects using a single extra query. You could even go further and use select_related('vbook') when getting the author. Django treats these models just like any other.

We can’t save or create using such view-based models – the database will reject that for anything but the simplest views, so we still use the underlying table-based models to do that. But we can link the models together, with a one-to-one relationship, to make things easier, e.g.

class vBook(pg.View):
    book = models.OneToOneField(Book,                                on_delete=models.DO_NOTHING)           #needs book_id to be returned by the sql    title = models.CharField(max_length=100)
    jacket_image = models.ImageField(null=True)
    author_name = models.CharField(max_length=100)
    shelf_position = models.IntegerField()
 
    author = models.ForeignKey(Author,
                               on_delete=models.DO_NOTHING)
             #needs author_id to be returned by the sql
 
    sql = BOOK_DETAILS

Now, given a vbook instance, we can update the underlying book, for example:

vbook.book.title='A Tale of Two Cities'
vbook.book.title.save()

And also, given a book instance, we can ask for all the pre-joined book information:

book.vbook.author_name

There are still some questions about how best to use these models, e.g. how to share methods with underlying table-based models and how best to name fields from other models.

Deferred Optimisation

Once we’ve defined view-based models to bring together the core concepts of the system, we can build the application to use them for reading and the base-table models for writing. We then have a variety of options to greatly optimise the system at a later date.

Here are some ways we can optimise database views. The important points are:

  • just by using views to join tables, we’ve already made great savings in the number of database queries needed
  • the ideas below can be applied later, once we know if and where further optimisation is needed
  • none of these would require any changes to the application code using the data: they’re true implementation changes, and database views give us such a clear separation of interface and implementation that the application could stay running!

Pull from other tables

For example, at a later point, we could modify the view to join to a table containing pre-calculated ratings and return that instead of the AVG sub-select.

Pull from external systems

We could use PostgreSQL’s foreign data wrappers to join to external data sources. It might be useful in itself to map a Django model to such a foreign table without an intermediate view, if all the data is foreign.

Use database functions

Database functions can be written in a number of languages including PL/pgSQL, Python, Ruby and Javascript. They are pre-compiled and are very efficient not least because they can access the data without it needing to be returned first and are in situ so can filter out unwanted results.

Load-balance across read-only replicas

PostgreSQL has the concept of standby/slave servers that can be kept synchronised with a master server. These can be made available for read-only queries to spread the load. Since we can be sure that view-based models are only used for reading, we could use Django’s database router to route all queries against those models to the slave servers.

Materialise

Views are virtual, except when they’re not. PostgreSQL 9.3 added materialised views.

CREATE MATERIALIZED VIEW vbook AS
SELECT ...

These are still declared as derived tables but the data in them is physically stored for much faster access: the results are effectively cached in the database. These are useful if the application can handle some staleness, and the only change necessary is in the way the view is declared – the application needn’t know or care. Such views can be refreshed periodically (although before 9.4 the refresh takes out a table lock). Indexes can be added to materialised views, potentially giving massive performance increases, especially for calculated columns.

Although I think materialisation is a last resort, it’s a very powerful one. I’ve added a MaterializedView class to isotoma/django-postgres which should help create them, though it could do with some more testing and options to control refreshing.

This really gives the best of both worlds: a single query can provide Django objects with the speed expected from de-normalised storage, but derived by the database from a normalised schema.

Summary

Django’s ORM makes accessing related objects simple and convenient. However, when using an ORM, accessing related objects in loops often leads to an explosion of supporting queries, which can go unnoticed during development but which can lead to poor performance. Django’s ORM has some methods that try to alleviate the problem but they have limitations. We can use raw SQL to efficiently join related information in the database to avoid these query explosions, as well as giving us more powerful ways to group and summarise data. I think these SQL queries should be given more prominence in our projects.

We can go further and push the raw SQL into the database by declaring virtual tables (database views). We can then map Django models onto these virtual tables to also give an extra layer of abstraction on top of the base models. This lets us defer implementation decisions and provides lots of ways to optimise the system at a later stage, once we know more about its performance, without affecting the application code.

A Different View (materialised)

About us: Isotoma is a bespoke software development company based in York and London specialising in web apps, mobile apps and product design. If you’d like to know more you can review our work or get in touch.

A Different View

A Different View

One of the consequences of the mismatches between databases and ORMs is that iterating over objects can generate explosions of database queries. Often, these explosions go unnoticed because the individual queries are fast and the number of concurrent users is small, especially during development. The explosions are sometimes hard to detect, either because developers don’t look for them or the tools that can expose the explosions don’t report everything (e.g. the Django debug toolbar doesn’t report the queries generated by ajax calls).

Exploding

debug_toolbarDjango does a good job of lazily evaluating what it can, but the simple act of using the convenient dot-notation inside a loop can turn a single database query into a cascade of supporting queries.

For example, given this book model:

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author)
    jacket = models.ForeignKey(Jacket)
    shelf = models.ForeignKey(Shelf)

and this books query:

books = Book.objects.filter(shelf__position__lte = 10)

which returns 10 rows. Then this template:

{% for book in books %}
  {{ book.jacket.image }} {{ book.title }} 
  by {{ book.author.name }}
{% endfor %}

book_listing_simplewould issue a single query to get all 10 rows, but would then generate 2 additional queries per iteration to get the jacket and the author for each book. So a total of 21 queries. You really need to use the Django debug toolbar to realise this – install it now and keep your eye on it until the very end of the project: it only takes a late-breaking, well-intentioned dot to cause an explosion.

A good way to detect when explosions happen is to check the number of queries in your unit tests.

self.assertNumQueries(self.BOOKS_QUERY_COUNT, self.client.get, reverse('books'))

Add these at the start of your project. It means you have to keep increasing the counts while developing, but it does make it obvious when something has exploded.

Remember, every one of those queries involves the following steps:

  • create a connection to the database (tcp/ip – handshake – username, password) (set CONN_MAX_AGE in Django 1.6 to help reduce this overhead)
  • send the SQL query over the network
  • parse the SQL query
  • check the table and column read/write permissions
  • optimise the query plan based on the latest statistics
  • start a transaction
  • perform the query (checking for other users’ locks)
  • return the resulting metadata and rows over the network
  • commit the transaction
  • close the connection

Joining

A far better approach is to get the database to do the work. It can join together the jacket and author information much more efficiently (orders of magnitude more efficiently – but that’s another topic).

Django added select_related() to try to address this problem. Given a field name, it passes the required table to the database to be joined with the original queryset, e.g.

books = Book.objects.filter(shelf__position__lte = 10
                           ).select_related('jacket',
                                            'author')

It is limited though, since it only works with some relationships in some directions, and it pulls in all the columns which is usually very wasteful, especially if large text fields are present. Adding defer() or only() to work around this can help, but isn’t pretty, e.g.

books = Book.objects.filter(shelf__position__lte = 10
                           ).select_related('jacket', 'author'
                           ).defer('author__biography',
                                   'author__summary',
                                   'author__resume')

prefetch_related() was added later to help with more complex relationships but still issues multiple queries and does the ‘joining’ in Python. Besides that, it also has a number of caveats and special interactions with other methods. Trying to combine these with annotations for aggregate counts and such would lead to some fragile, hard-to-construct and, I think, hard-to-read syntax that can fail even for slightly complicated queries.

So instead, we could issue the following query:

books = Book.objects.raw("""
  SELECT
    book.id, book.title,
    jacket.image AS jacket_image,
    author.name AS author_name
  FROM book
    NATURAL JOIN shelf NATURAL JOIN author NATURAL JOIN jacket
  WHERE shelf.position <= 10
""")

(note: I’ll use natural join throughout for simplicity, and short table names for that matter, but they don’t actually work as-is with Django’s table/column naming)

And then, a slightly modified template:

{% for book in books %}
  {{ book.jacket_image }} {{ book.title }} 
  by {{ book.author_name }}
{% endfor %}

would only issue 1 query, regardless of the number of books.

Adding further related information to the original template will dramatically increase the number of queries, whereas joining another table to the raw SQL version will add none. Similarly, returning 20 books instead of 10 would just about double the number of queries for the original version (so 41 for our example), whereas the raw one would still only issue a single query.

As well as avoiding query explosions, there are other benefits to using raw SQL. A full explanation needs another blog post, but some of the major ones are:

  • it’s the only way to do anything but the simplest of aggregation queries.
  • it only returns what is required (assuming you avoid SELECT *). Pulling every column, including large blobs, over the network when they’re not referenced wastes CPU, temporary sort/cache space and network bandwidth.
  • you can use easy-to-read comparisons (e.g. position <= 10 instead of shelf__position__lte = 10).
  • if you need/want, you can use database-specific features, such as function calls, range queries, NULL ordering, etc.
  • checking the query plan is easier – there’s no need to set a breakpoint and then print the queryset.query.
  • it returns consistent results, whereas multiple queries, even in the same transaction, can see different data depending on what’s been committed by concurrent users (unless we used the serializable isolation level, which we don’t).

Formatting

I’ve tried a number of ways to embed SQL queries into Django modules:

...raw("""
SELECT ...
FROM ...
WHERE ...
""")

is jarring.

...raw("""SELECT ...
          FROM ...
          WHERE ...
       """)

is better, but adds too much whitespace and makes it harder to edit.

...raw("""SELECT ..."""
       """FROM ..."""
       """WHERE ...""")

avoids the extra whitespace but doesn’t ensure enough whitespace unless you’re careful to add a space at the end of each line.

None are great and none of them provide an easy way to copy and paste the query, which is necessary to tune it and check its plan in a database client.

Elevating

For a better approach, I suggest creating an sql directory (alongside models.py – it is that important) and create python modules in there to hold the named query declarations, e.g. sql/books.py:

BOOK_DETAILS = """
SELECT
  book.id,
  book.title,
  jacket.image AS jacket_image,
  author.name AS author_name
FROM
  book
  NATURAL JOIN shelf
  NATURAL JOIN author
  NATURAL JOIN jacket
WHERE
  shelf.position <= 10
"""

then it can be called like this:

from sql.books import BOOK_DETAILS
 
books = Book.objects.raw(BOOK_DETAILS)

Clicking the BOOK_DETAILS reference in any decent IDE will take you to the query. Now that the query has a bit more respect it can more easily be copied and pasted into other tools, and formatted as you’d like, and re-used without repetition.

One more note about formatting. Although you could put Python comments before the declaration, I suggest keeping the query comments inside the query because they may be useful on the server, e.g. when viewing the database logs. So instead of:

#Get top 10 book summaries
#(including author and jacket details)
BOOK_DETAILS = """
SELECT
...
"""

use:

BOOK_DETAILS = """
/*
Get top 10 book summaries
(including author and jacket details)
*/
SELECT
...
"""

Downsides

There are some downsides with raw(), however.

  • You need some SQL knowledge of course, and an understanding of how Django links tables together, but I think you should have that anyway.
  • If your SQL does introduce some non-standard syntax (which shouldn’t be the case for straightforward joins) it could make your application less portable.  Although I’d argue that some features are worth that sacrifice, and would hope the likelihood of switching to a different database would be low if you’re already using a good one.
  • Even though raw() ensures only 1 query is used, that query must still be sent over the network and parsed every time it’s run.
  • The real problem is that raw() returns a RawQuerySet, not a QuerySet, so no further refinement methods can be applied (e.g. filter, order_by) – though arguably they’re better off being added to the SQL.

We can do better, as I’ll explain in part 2

About us: Isotoma is a bespoke software development company based in York and London specialising in web apps, mobile apps and product design. If you’d like to know more you can review our work or get in touch.

The Third Manifesto Implementers’ Workshop

Earlier this month I went to the Third Manifesto Implementers’ Workshop at Northumbria University in Newcastle. A group of us discussed recent developments in implementing the relational data model.

The relational data model was proposed by E. F. Codd in 1969 in response to the complex, hierarchical, data storage solutions of the time which required programs to be written and compiled for each database query. It was a powerful abstraction, but unfortunately SQL and its implementations missed out on important features, and broke it in fundamental ways. In response to this problem, and the industry’s approach towards object-databases, Chris Date and Hugh Darwen wrote “The Third Manifesto” (TTM) to put forward their ideas on how future database systems should work. I urge you to read their books (even if you’re not interested in the subject) – the language is amazing: precise, concise, comprehensive and easy to read – other technical authors don’t come close.

The relational model treats data as sets of logical propositions and allows them to be queried, manipulated and constrained declaratively. It abstracts away from physical storage and access issues which is why it will still be used a hundred years from now (and why NoSQL discussions like these http://wiki.apache.org/couchdb/EntityRelationship http://www.cmlenz.net/archives/2007/10/couchdb-joins are retrograde). If you’re writing loops to query your data, or having to navigate prescribed connection paths, then your abstractions are feeble and limited.

At the workshop, I talked about my project, Dee, which implements the relational ideas from TTM in Python. You can see my slides here (or here if you have an older browser).

Erwin Smout gave a couple of talks about implementing transition constraints and dispensing with data definition language.

David Livingstone walked us through the RAQUEL architecture – a layered approach along the lines of the OSI network model.

Hugh Darwen discussed the features and implementation of IBM’s Business System 12, one of the first ever relational database systems which had some surprisingly dynamic features, including key inferencing, so that view definitions could keep tabs on their underlying constraints.

Chris Date took us through his latest thoughts on how to update relational views in a generic way. The aim is for database users to be able to treat views and base tables in the same way, for both reading and writing. Lots to think about here, and my to-do list for Dee has grown another section.

Adrian Hudnott discussed a couple of research projects around optimising multiple relational assignments and tracking the source of updates so that transition constraints could be more effective.

Renaud de Landtsheer gave an insight into the work he’s been doing implementing first-order-logic constraints within Oracle databases.

I sat next to Toon Koppelaars (whose name went down well with the Geordies) and then I realised I had his book (Applied Mathematics for Database Professionals) waiting to be read on my desk at work, thanks to the eclectic Isotoma library (and Wes).

It was a packed couple of days with plenty of food for thought. Thank you to David Livingstone and Safwat Mansi for organising and hosting such an enjoyable and interesting event.

Tamper-protection for Bank Transactions

If you need to send electronic transactions to Swedish banks, you’ll be required to add anti-tampering seals to the files. The banks recommend you use a third-party system to create the HMAC SHA256-128 seals, but that could involve a fair amount of expensive server software and maintenance contracts (some linked to the number of people who work in your company).

Instead, you can do it yourself in Python like this:

import hmac
import hashlib
import string

NORMALISE = string.maketrans(
    '\xC9\xC4\xD6\xC5\xDC\xE9\xE4\xF6\xE5\xFC' + ''.join(
    [chr(x) for x in range(0,32)]) + ''.join(
    [chr(x) for x in range(127,256)
     if x not in (201,196,214,197,220,233,
                  228,246,229,252)]),
    '\x40\x5B\x5C\x5D\x5E\x60\x7B\x7C\x7D\x7E' + ''.join(
    [chr(195) for x in range(0,32)]) + ''.join(
    [chr(195) for x in range(127,256)
     if x not in (201,196,214,197,220,233,
                  228,246,229,252)]))

def hex_to_bytes(hexs):
    """Convert string of hex into bytes"""
    return ''.join(['%s' % chr(int(hexs[i:i+2], 16))
                    for i in range(0, len(hexs), 2)])

def get_signature(contents, key):
    """Calculate the HMAC SHA256-128 signature

       contents - an iso-8859-1 (latin-1) encoded string
       key - a string of hex characters

       Returns a 32 char string of hex characters (128 bits)
    """
    key = hex_to_bytes(key)

    #Normalise the contents
    contents = contents.translate(NORMALISE, '\r\n')

    dig = hmac.new(key, msg=contents,
                   digestmod=hashlib.sha256).digest()
    return ''.join(['%02X' % ord(x) for x in dig[:16]])

And then to calculate the signature for a file:

>>> print get_signature(open('bankfile.dat').read(),
...                     '1234567890abcdef1234567890abcdef')
25122AE4179BD51DC87AD6EA08D16D45

Black Box BlackBerry

Debugging software is best done using the scientific method: gather evidence about the effects of the bug, conjure up hypotheses to explain the behaviour, experiment to test the hypotheses and modify the code to change the behaviour. Rinse and repeat. If you can’t consistently reproduce the bug though, it can get tricky.

Recently, while developing a site targeted at mobile devices, we came across an intermittent problem when using a BlackBerry device. Testing mobile sites with desktop browsers and emulators can only take you so far. Eventually you reach the point where real devices begin to exhibit their own peccadillos and so we use DeviceAnywhere to access a whole host of remote-controlled physical devices.

Using the BlackBerry Curve, occasionally, our login page wouldn’t proceed to the home page after successful authentication. But we could never reproduce the this in our development environments, only on live; sometimes.
One major difference between the two environments was that the live one had dozens of servers behind a load-balancer which used a URL parameter for session affinity (we couldn’t assume all mobile devices would support cookies), whereas the development environment was a single server. We also had a staging environment which closely reproduced the live environment, although there were only a couple of servers behind its load-balancer. Initial tests on the staging environment indicated that the problem didn’t appear there either.

To rule out the mobile network provider, we installed the excellent Opera Mini browser on the BlackBerry and it worked every time. This also ruled out any issues with pages being cached by Akamai, the content delivery network. So we were now looking for a problem with our code interacting with the BlackBerry browser, but only behind our live load-balancer; sometimes.

After painstakingly tracing through the live Apache logs we closed in on the unexpected cause: a bug in the BlackBerry browser. When a server tells a browser to redirect it sends the full URL, including in our case the all-important session parameter. This URL was being tampered with before the browser navigated to it. The parameter name was being converted to lower-case (if it wasn’t preceded by a slash). This meant that the load-balancer didn’t use it for server affinity so the home page server probably didn’t have a logged-in session, and so it would bounce back to the login page.

The reason this problem had been so hard to reproduce was that in development there was only one server so affinity wasn’t an issue and the server software didn’t care about the case of the session parameter. Also the site URL was different and so the session parameter always had a preceding slash which didn’t trigger the BlackBerry URL tampering, so it never appeared as lower-case in the development logs. And on the staging environment, because there were only two servers, the device would hit the same server, notwithstanding any affinity failure caused by the lower-casing, half of the time by chance alone. The live environment was more likely to fail, but even it gave a sizeable probability of hitting the same server successively by chance alone.

We built a test server and, using some black box reverse-engineering (because the BlackBerry browser is closed-source), we reckon the logic inside the browser’s redirect code goes something like this: “lower-case all the characters in the location URL up to the first slash” presumably with the intention of making the DNS name lower-case. But it should be: “… up to the first slash or ?” to preserve the case of any query parameters.

Googling for this issue returns a number of other sites having redirect and login issues with BlackBerrys. I wonder how many are caused by this subtle, case-sensitive bug?

We’ve since searched our logs and found the bug across this wide range of BlackBerry devices/versions:BlackBerry8100/4.2.0

  • BlackBerry8100/4.5.0.52
  • BlackBerry8110/4.3.0
  • BlackBerry8120/4.5.0.52
  • BlackBerry8310/4.2.2
  • BlackBerry8700/4.2.1
  • BlackBerry8800/4.2.1
  • BlackBerry8820/4.2.2
  • BlackBerry8830/4.2.2
  • BlackBerry8900/4.6.1.101
  • BlackBerry8900/4.6.1.109
  • BlackBerry9000/4.6.0.125
  • BlackBerry9000/4.6.0.221

We’ve logged it with BlackBerry. I’ll post an update if we receive any response.

Good Spam

Following on from this bright idea to use a spamming tool to create a blog from an Open University course feed, the OU asked us to build a plugin for WordPress MU that will automatically create a bunch of blogs from Open University feeds. We also built a WordPress widget, to implement another bright idea, that can then deliver the blog entries for a course in periodic chunks.

So you can consume the course in your feed-reader at a pace that suits. This should be released soon, meanwhile, the testing is very educational.

Every day this week I’ve been mostly learning about James Clerk Maxwell.