2
# Copyright (C) 2006 Fabio Tranchitella <fabio@tranchitella.it>
6
# Copyright (c) 2002-2006 Zope Corporation and Contributors.
9
# This software is subject to the provisions of the Zope Public License,
10
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
11
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
12
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
13
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
14
# FOR A PARTICULAR PURPOSE.
16
# This program is free software; you can redistribute it and/or modify
17
# it under the terms of the GNU General Public License as published by
18
# the Free Software Foundation; either version 2 of the License, or
19
# (at your option) any later version.
21
# This program is distributed in the hope that it will be useful,
22
# but WITHOUT ANY WARRANTY; without even the implied warranty of
23
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
# GNU General Public License for more details.
26
# You should have received a copy of the GNU General Public License
27
# along with this program; if not, write to the Free Software
28
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
30
# If you prefer you can use this package using the ZPL license as
31
# published on the Zope web site, http://www.zope.org/Resources/ZPL.
33
"""PostgreSQL Database Adapter for Zope 3"""
35
from zope.interface import implements
36
from zope.rdb import ZopeDatabaseAdapter, parseDSN, ZopeConnection, ZopeCursor
37
from zope.rdb.interfaces import DatabaseException, IZopeConnection
38
from zope.publisher.interfaces import Retry
40
from datetime import date, time, datetime, timedelta
43
import psycopg2.extensions
47
# OIDs from psycopg/pgtypes.h
52
TIMESTAMPTZ_OID = 1184
59
# date/time parsing functions
60
_dateFmt = re.compile(r"^(\d\d\d\d)-?([01]\d)-?([0-3]\d)$")
63
"""Parses ISO-8601 compliant dates and returns a tuple (year, month,
66
The following formats are accepted:
67
YYYY-MM-DD (extended format)
68
YYYYMMDD (basic format)
72
raise ValueError, 'invalid date string: %s' % s
73
year, month, day = m.groups()
74
return int(year), int(month), int(day)
77
_timeFmt = re.compile(
78
r"^([0-2]\d)(?::?([0-5]\d)(?::?([0-5]\d)(?:[.,](\d+))?)?)?$")
81
"""Parses ISO-8601 compliant times and returns a tuple (hour, minute,
84
The following formats are accepted:
85
HH:MM:SS.ssss or HHMMSS.ssss
86
HH:MM:SS,ssss or HHMMSS,ssss
93
raise ValueError, 'invalid time string: %s' % s
94
hr, mn, sc, msc = m.groups(0)
96
sc = float("%s.%s" % (sc, msc))
99
return int(hr), int(mn), sc
102
_tzFmt = re.compile(r"^([+-])([0-2]\d)(?::?([0-5]\d))?$")
105
"""Parses ISO-8601 timezones and returns the offset east of UTC in
108
The following formats are accepted:
112
Z (equivalent to +0000)
118
raise ValueError, 'invalid time zone: %s' % s
119
d, hoff, moff = m.groups(0)
121
return - int(hoff) * 60 - int(moff)
122
return int(hoff) * 60 + int(moff)
125
_tzPos = re.compile(r"[Z+-]")
128
"""Parses ISO-8601 compliant times that may include timezone information
129
and returns a tuple (hour, minute, second, tzoffset).
131
tzoffset is the offset east of UTC in minutes. It will be None if s does
132
not include time zone information.
134
Formats accepted are those listed in the descriptions of parse_time() and
135
parse_tz(). Time zone should immediatelly follow time without intervening
140
return parse_time(s) + (None,)
142
return parse_time(s[:pos]) + (parse_tz(s[pos:]),)
145
_datetimeFmt = re.compile(r"[T ]")
147
def _split_datetime(s):
148
"""Split date and time parts of ISO-8601 compliant timestamp and
149
return a tuple (date, time).
151
' ' or 'T' used to separate date and time parts.
153
m = _datetimeFmt.search(s)
155
raise ValueError, 'time part of datetime missing: %s' % s
157
return s[:pos], s[pos + 1:]
160
def parse_datetime(s):
161
"""Parses ISO-8601 compliant timestamp and returns a tuple (year, month,
162
day, hour, minute, second).
164
Formats accepted are those listed in the descriptions of parse_date() and
165
parse_time() with ' ' or 'T' used to separate date and time parts.
167
dt, tm = _split_datetime(s)
168
return parse_date(dt) + parse_time(tm)
171
def parse_datetimetz(s):
172
"""Parses ISO-8601 compliant timestamp that may include timezone
173
information and returns a tuple (year, month, day, hour, minute, second,
176
tzoffset is the offset east of UTC in minutes. It will be None if s does
177
not include time zone information.
179
Formats accepted are those listed in the descriptions of parse_date() and
180
parse_timetz() with ' ' or 'T' used to separate date and time parts.
182
dt, tm = _split_datetime(s)
183
return parse_date(dt) + parse_timetz(tm)
186
def parse_interval(s):
187
"""Parses PostgreSQL interval notation and returns a tuple (years, months,
188
days, hours, minutes, seconds).
196
date_comp ::= 1 'day'
204
time ::= number ':' number
205
| number ':' number ':' number
206
| number ':' number ':' number '.' fraction
208
years = months = days = 0
209
hours = minutes = seconds = 0
211
# Tests with 7.4.6 on Ubuntu 5.4 interval output returns 'mon' and 'mons'
212
# and not 'month' or 'months' as expected. I've fixed this and left
213
# the original matches there too in case this is dependant on
214
# OS or PostgreSQL release.
215
for i in range(0, len(elements) - 1, 2):
216
count, unit = elements[i:i+2]
217
if unit == 'day' and count == '1':
221
elif unit == 'month' and count == '1':
223
elif unit == 'mon' and count == '1':
225
elif unit == 'months':
229
elif unit == 'year' and count == '1':
231
elif unit == 'years':
234
raise ValueError, 'unknown time interval %s %s' % (count, unit)
235
if len(elements) % 2 == 1:
236
hours, minutes, seconds = parse_time(elements[-1])
237
return (years, months, days, hours, minutes, seconds)
241
def _conv_date(s, cursor):
243
return date(*parse_date(s))
245
def _conv_time(s, cursor):
247
hr, mn, sc = parse_time(s)
248
sc, micro = divmod(sc, 1.0)
249
micro = round(micro * 1000000)
250
return time(hr, mn, int(sc), int(micro))
252
def _conv_timetz(s, cursor):
254
from zope.datetime import tzinfo
255
hr, mn, sc, tz = parse_timetz(s)
256
sc, micro = divmod(sc, 1.0)
257
micro = round(micro * 1000000)
258
if tz: tz = tzinfo(tz)
259
return time(hr, mn, int(sc), int(micro), tz)
261
def _conv_timestamp(s, cursor):
263
y, m, d, hr, mn, sc = parse_datetime(s)
264
sc, micro = divmod(sc, 1.0)
265
micro = round(micro * 1000000)
266
return datetime(y, m, d, hr, mn, int(sc), int(micro))
268
def _conv_timestamptz(s, cursor):
270
from zope.datetime import tzinfo
271
y, m, d, hr, mn, sc, tz = parse_datetimetz(s)
272
sc, micro = divmod(sc, 1.0)
273
micro = round(micro * 1000000)
274
if tz: tz = tzinfo(tz)
275
return datetime(y, m, d, hr, mn, int(sc), int(micro), tz)
277
def _conv_interval(s, cursor):
279
y, m, d, hr, mn, sc = parse_interval(s)
281
# XXX: Currently there's no way to represent years and months as
285
return timedelta(days=d, hours=hr, minutes=mn, seconds=sc)
287
def _get_string_conv(encoding):
288
def _conv_string(s, cursor):
290
s = s.decode(encoding)
295
DATE = psycopg2.extensions.new_type((DATE_OID,), "ZDATE", _conv_date)
296
TIME = psycopg2.extensions.new_type((TIME_OID,), "ZTIME", _conv_time)
297
TIMETZ = psycopg2.extensions.new_type((TIMETZ_OID,), "ZTIMETZ", _conv_timetz)
298
TIMESTAMP = psycopg2.extensions.new_type((TIMESTAMP_OID,), "ZTIMESTAMP", _conv_timestamp)
299
TIMESTAMPTZ = psycopg2.extensions.new_type((TIMESTAMPTZ_OID,), "ZTIMESTAMPTZ", _conv_timestamptz)
300
INTERVAL = psycopg2.extensions.new_type((INTERVAL_OID,), "ZINTERVAL", _conv_interval)
302
def registerTypes(encoding):
303
"""Register type conversions for psycopg"""
304
psycopg2.extensions.register_type(DATE)
305
psycopg2.extensions.register_type(TIME)
306
psycopg2.extensions.register_type(TIMETZ)
307
psycopg2.extensions.register_type(TIMESTAMP)
308
psycopg2.extensions.register_type(TIMESTAMPTZ)
309
psycopg2.extensions.register_type(INTERVAL)
310
STRING = psycopg2.extensions.new_type((CHAR_OID, TEXT_OID, BPCHAR_OID, VARCHAR_OID), "ZSTRING", _get_string_conv(encoding))
311
psycopg2.extensions.register_type(STRING)
314
dsn2option_mapping = {'host': 'host',
318
'password': 'password'}
320
class Psycopg2Adapter(ZopeDatabaseAdapter):
321
"""A psycopg2 adapter for Zope3.
323
The following type conversions are performed:
325
DATE -> datetime.date
326
TIME -> datetime.time
327
TIMETZ -> datetime.time
328
TIMESTAMP -> datetime.datetime
329
TIMESTAMPTZ -> datetime.datetime
331
XXX: INTERVAL cannot be represented exactly as datetime.timedelta since
332
it might be something like '1 month', which is a variable number of days.
336
if not self.isConnected():
338
self._v_connection = Psycopg2Connection(
339
self._connection_factory(), self
341
except psycopg2.Error, error:
342
raise DatabaseException, str(error)
344
def registerTypes(self):
345
registerTypes(self.getEncoding())
347
def _connection_factory(self):
348
"""Create a psycopg2 DBI connection based on the DSN"""
350
conn_info = parseDSN(self.dsn)
352
for dsnname, optname in dsn2option_mapping.iteritems():
353
if conn_info[dsnname]:
354
conn_list.append('%s=%s' % (optname, conn_info[dsnname]))
355
conn_str = ' '.join(conn_list)
356
connection = psycopg2.connect(conn_str)
357
connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE)
360
def disconnect(self):
361
if self.isConnected():
363
self._v_connection.close()
364
except psycopg2.InterfaceError:
366
self._v_connection = None
369
def _handle_psycopg_exception(error):
370
"""Called from a exception handler for psycopg2.Error.
372
If we have a serialization exception or a deadlock, we should retry the
373
transaction by raising a Retry exception. Otherwise, we reraise.
378
# These messages are from PostgreSQL 8.0. They may change between
379
# PostgreSQL releases - if so, the different messages should be added
380
# rather than the existing ones changed so this logic works with
381
# different versions.
383
'ERROR: could not serialize access due to concurrent update'
385
raise Retry(sys.exc_info())
386
if msg.startswith('ERROR: deadlock detected'):
387
raise Retry(sys.exc_info())
391
class IPsycopg2ZopeConnection(IZopeConnection):
392
"""A marker interface stating that this connection uses PostgreSQL."""
395
class Psycopg2Connection(ZopeConnection):
397
implements(IPsycopg2ZopeConnection)
400
"""See IZopeConnection"""
401
return Psycopg2Cursor(self.conn.cursor(), self)
405
ZopeConnection.commit(self)
406
except psycopg2.Error, error:
407
_handle_psycopg_exception(error)
410
class Psycopg2Cursor(ZopeCursor):
412
def execute(self, operation, parameters=None):
413
"""See IZopeCursor"""
415
return ZopeCursor.execute(self, operation, parameters)
416
except psycopg2.Error, error:
417
_handle_psycopg_exception(error)
419
def executemany(operation, seq_of_parameters=None):
420
"""See IZopeCursor"""
421
raise RuntimeError, 'Oos'
423
return ZopeCursor.execute(self, operation, seq_of_parameters)
424
except psycopg2.Error, error:
425
_handle_psycopg_exception(error)