SQLAlchemy - Storing Application Version Strings

One example of when you might need to store application version numbers in your database is when you’re tracking which users have which versions of a mobile app installed. In that case, you may want to preserve the ability to easily sort the version numbers, so you can answer questions like “Which users have a version greater than or equal to 9.3.2?”

In that case, you have two options. You can store the version number in three different fields, such as major, minor and revision number. Or, you can pad the values such that regular string sorting would work. Why? Because otherwise, the sorting will be incorrect. For example, the string 10.0.0 is less than the string 9.0.0 because the first character 9 is greater than 1. This would be the behavior you would get with a simple SELECT * FROM users ORDER BY version DESC;

For example, the version 9.3.2 might be stored as 0009.0003.0002. Here is a drop-in implementation in SQLAlchemy which allows you to only deal with the “nice” native format in Python, while storing the padded version in the database.

from sqlalchemy import types


def to_version_string(value):
    if not value:
        return None
    parts = value.split(VERSION_DELIM)
    if len(parts) > VERSION_NUMBER_PIECES:
        raise ValueError('Version string has too many parts')
    while len(parts) < VERSION_NUMBER_PIECES:
        parts.append('0')
    for part in parts:
        try:
            assert str(int(part)) == part
        except (ValueError, AssertionError):
            raise ValueError('Version part is not an integer')
    return VERSION_DELIM.join([part.zfill(4) for part in parts])


def from_version_string(value):
    if not value:
        return None
    parts = value.split(VERSION_DELIM)
    return VERSION_DELIM.join([str(int(part)) for part in parts])


class VersionString(types.TypeDecorator):
    """ stores a string like '0.9.4' as '0000.0009.0004' so it can be sorted """

    impl = types.String

    def process_bind_param(self, value, dialect):
        return to_version_string(value)

    def process_result_value(self, value, dialect):
        return from_version_string(value)

And some quick tests:

from unittest import TestCase
from models import to_version_string, from_version_string


class VersionStringTests(TestCase):

    def test_to_version_string(self):
        self.assertEquals(to_version_string('9.3.2'), '0009.0003.0002')
        self.assertEquals(to_version_string('9.3'), '0009.0003.0000')
        self.assertEquals(to_version_string('9'), '0009.0000.0000')
        self.assertEquals(to_version_string(''), None)
        self.assertEquals(to_version_string(None), None)

    def test_to_version_string_bad(self):
        for value in [
            '9.3.2.1',
            '9.3.a',
            '9.03.2',
            'foobar',
        ]:
            with self.assertRaises(ValueError):
                to_version_string(value)

    def test_from_version_string(self):
        self.assertEquals(from_version_string('0009.0003.0002'), '9.3.2')
        self.assertEquals(from_version_string('0000.0000.0000'), '0.0.0')
        self.assertEquals(from_version_string(None), None)

Updated: