Skip to content

Commit efc33d7

Browse files
committed
Merge pull request #43 from gratipay/fix-race-condition
Try to handle changes to type definitions
2 parents 8c4f58d + 363123f commit efc33d7

File tree

2 files changed

+49
-1
lines changed

2 files changed

+49
-1
lines changed

postgres/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
from postgres.cursors import SimpleTupleCursor, SimpleNamedTupleCursor
181181
from postgres.cursors import SimpleDictCursor, SimpleCursorBase
182182
from postgres.orm import Model
183+
from psycopg2 import DataError
183184
from psycopg2.extras import register_composite, CompositeCaster
184185
from psycopg2.pool import ThreadedConnectionPool as ConnectionPool
185186

@@ -824,7 +825,22 @@ def make_DelegatingCaster(postgres):
824825
825826
"""
826827
class DelegatingCaster(CompositeCaster):
828+
829+
def parse(self, s, curs, retry=True):
830+
# Override to protect against race conditions:
831+
# https://github.com/gratipay/postgres.py/issues/26
832+
833+
try:
834+
return super(DelegatingCaster, self).parse(s, curs)
835+
except (DataError, ValueError):
836+
if not retry:
837+
raise
838+
# Re-fetch the type info and retry once
839+
self._refetch_type_info(curs)
840+
return self.parse(s, curs, False)
841+
827842
def make(self, values):
843+
# Override to delegate to the model registry.
828844
if self.name not in postgres.model_registry:
829845

830846
# This is probably a bug, not a normal user error. It means
@@ -838,6 +854,12 @@ def make(self, values):
838854
instance = ModelSubclass(record)
839855
return instance
840856

857+
def _refetch_type_info(self, curs):
858+
"""Given a cursor, update the current object with a fresh type definition.
859+
"""
860+
new_self = self._from_db(self.name, curs)
861+
self.__dict__.update(new_self.__dict__)
862+
841863
return DelegatingCaster
842864

843865

tests.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from postgres.cursors import TooFew, TooMany, SimpleDictCursor
99
from postgres.orm import ReadOnly, Model
1010
from psycopg2 import InterfaceError, ProgrammingError
11-
from pytest import raises
11+
from pytest import mark, raises
1212

1313

1414
DATABASE_URL = os.environ['DATABASE_URL']
@@ -334,6 +334,32 @@ def test_unregister_unregisters_multiple(self):
334334
self.db.unregister_model(self.MyModel)
335335
assert self.db.model_registry == {}
336336

337+
def test_add_column_doesnt_break_anything(self):
338+
self.db.run("ALTER TABLE foo ADD COLUMN boo text")
339+
one = self.db.one("SELECT foo.*::foo FROM foo WHERE bar='baz'")
340+
assert one.boo is None
341+
342+
def test_replace_column_different_type(self):
343+
self.db.run("CREATE TABLE grok (bar int)")
344+
self.db.run("INSERT INTO grok VALUES (0)")
345+
class EmptyModel(Model): pass
346+
self.db.register_model(EmptyModel, 'grok')
347+
# Add a new column then drop the original one
348+
self.db.run("ALTER TABLE grok ADD COLUMN biz text NOT NULL DEFAULT 'x'")
349+
self.db.run("ALTER TABLE grok DROP COLUMN bar")
350+
# The number of columns hasn't changed but the names and types have
351+
one = self.db.one("SELECT grok.*::grok FROM grok LIMIT 1")
352+
assert one.biz == 'x'
353+
assert not hasattr(one, 'bar')
354+
355+
@mark.xfail(raises=AttributeError)
356+
def test_replace_column_same_type_different_name(self):
357+
self.db.run("ALTER TABLE foo ADD COLUMN biz text NOT NULL DEFAULT 0")
358+
self.db.run("ALTER TABLE foo DROP COLUMN bar")
359+
one = self.db.one("SELECT foo.*::foo FROM foo LIMIT 1")
360+
assert one.biz == 0
361+
assert not hasattr(one, 'bar')
362+
337363

338364
# cursor_factory
339365
# ==============

0 commit comments

Comments
 (0)