At the end of November, I finished the third episode with mild suspense: I suggested that the problem of optimistic “locking” could perhaps be solved in PostgreSQL with something other than extra qualifications, row version numbers or timestamps.
Let’s start this episode with action!
moviesdb=> INSERT INTO film VALUES (47478, 'Seven Samurai', 1956); INSERT 0 1 moviesdb=> SELECT xmin, xmax, ctid, title, release_year FROM film; xmin | xmax | ctid | title | release_year --------+------+-------+----------------+-------------- 853969 | 0 | (0,1) | Seven Samurai | 1956 (1 row) moviesdb=> UPDATE film SET title = 'Seven Samurai' WHERE id = 47478; UPDATE 1 moviesdb=> SELECT xmin, xmax, ctid, title, release_year FROM film; xmin | xmax | ctid | title | release_year --------+------+-------+---------------+-------------- 853970 | 0 | (0,2) | Seven Samurai | 1956 (1 row) moviesdb=> UPDATE film SET title = 'Sichinin Samurai' WHERE id = 47478; UPDATE 1 moviesdb=> SELECT xmin, xmax, ctid, title, release_year FROM film; xmin | xmax | ctid | title | release_year --------+------+-------+------------------+-------------- 853971 | 0 | (0,3) | Sichinin Samurai | 1956 (1 row) moviesdb=> UPDATE film SET release_year = 1954 WHERE id = 47478; UPDATE 1 moviesdb=> SELECT xmin, xmax, ctid, title, release_year FROM film; xmin | xmax | ctid | title | release_year --------+------+-------+------------------+-------------- 853972 | 0 | (0,4) | Sichinin Samurai | 1954 (1 row) moviesdb=> VACUUM FULL film; VACUUM moviesdb=> SELECT xmin, xmax, ctid, title, release_year FROM film; xmin | xmax | ctid | title | release_year --------+------+-------+------------------+-------------- 853972 | 0 | (0,1) | Sichinin Samurai | 1954 (1 row) moviesdb=> BEGIN; DELETE FROM film WHERE id = 47478; ROLLBACK; BEGIN DELETE 1 ROLLBACK moviesdb=> SELECT xmin, xmax, ctid, title, release_year FROM film; xmin | xmax | ctid | title | release_year --------+--------+-------+------------------+-------------- 853972 | 853974 | (0,1) | Sichinin Samurai | 1954 (1 row) moviesdb=> BEGIN; UPDATE film SET release_year = 1956 WHERE id = 47478; BEGIN UPDATE 1 moviesdb=> SELECT xmin, xmax, ctid, title, release_year FROM film; xmin | xmax | ctid | title | release_year --------+------+-------+------------------+-------------- 853975 | 0 | (0,2) | Sichinin Samurai | 1956 (1 row) moviesdb=> ROLLBACK; ROLLBACK moviesdb=> SELECT xmin, xmax, ctid, title, release_year FROM film; xmin | xmax | ctid | title | release_year --------+--------+-------+------------------+-------------- 853972 | 853975 | (0,1) | Sichinin Samurai | 1954 (1 row)
What element in the queries above could be used as a surrogate “row version identifier?” If you examine the changes carefully, you’ll notice that the xmin system column provides that capability. The ctid, a row locator, on the other hand, does not survive the VACUUM operation, and xmax is only used when a row is deleted (or updated, causing it to move).
So my suggestion, in terms of web user interfaces, is that fetching a row for a possible update should include the xmin value, e.g., in the get() method of the Film class, use the following:
def get(self, db): try: row = db.fetchone( "SELECT xmin, title, release_year FROM film WHERE id = %s", (self.id,))
The xmin value can then be sent as a hidden field to the web client, and used in the update() method to implement optimistic concurrency control.
Filed under: PostgreSQL, Python, User interfaces
