1 |
|
# |
2 |
|
# This file is part of GNU Enterprise. |
3 |
|
# |
4 |
|
# GNU Enterprise is free software; you can redistribute it |
5 |
|
# and/or modify it under the terms of the GNU General Public |
6 |
|
# License as published by the Free Software Foundation; either |
7 |
|
# version 2, or (at your option) any later version. |
8 |
|
# |
9 |
|
# GNU Enterprise is distributed in the hope that it will be |
10 |
|
# useful, but WITHOUT ANY WARRANTY; without even the implied |
11 |
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR |
12 |
|
# PURPOSE. See the GNU General Public License for more details. |
13 |
|
# |
14 |
|
# You should have received a copy of the GNU General Public |
15 |
|
# License along with program; see the file COPYING. If not, |
16 |
|
# write to the Free Software Foundation, Inc., 59 Temple Place |
17 |
|
# - Suite 330, Boston, MA 02111-1307, USA. |
18 |
|
# |
19 |
|
# Copyright 2000-2003 Free Software Foundation |
20 |
|
# |
21 |
|
# FILE: |
22 |
|
# odbc/DBdriver.py |
23 |
|
# |
24 |
|
# DESCRIPTION: |
25 |
|
# Driver to provide access to data via the public domain win32all ODBC Driver |
26 |
|
# |
27 |
|
# NOTES: |
28 |
|
# Only works under Win32... requires the win32all extensions. |
29 |
|
# (http://aspn.activestate.com/ASPN/Downloads/ActivePython/Extensions/Win32all) |
30 |
|
# |
31 |
|
# Supported attributes (via connections.conf or <database> tag) |
32 |
|
# |
33 |
|
# service= This is the ODBC DSN= string to use. |
34 |
|
# |
35 |
|
# |
36 |
|
|
37 |
|
|
38 |
|
import sys, string, types |
39 |
|
from gnue.common.datasources import GDataObjects, GConditions, GConnections |
40 |
|
from gnue.common.apps import GDebug |
41 |
|
|
42 |
|
|
43 |
|
try: |
44 |
|
import dbi, odbc |
45 |
|
except ImportError, message: |
46 |
|
tmsg = _("Driver not installed: win32all ODBC driver\n\n[%s") % message |
47 |
|
raise GConnections.AdapterNotInstalled, tmsg |
48 |
|
|
49 |
|
|
50 |
|
|
51 |
|
|
52 |
|
class ODBC_RecordSet(GDataObjects.RecordSet): |
53 |
|
def _postChanges(self): |
54 |
|
if not self.isPending(): return |
55 |
|
if self._deleteFlag: |
56 |
|
statement = self._buildDeleteStatement() |
57 |
|
elif self._insertFlag: |
58 |
|
statement = self._buildInsertStatement() |
59 |
|
elif self._updateFlag: |
60 |
|
statement = self._buildUpdateStatement() |
61 |
|
|
62 |
|
GDebug.printMesg(5, "_postChanges: statement=%s" % statement) |
63 |
|
|
64 |
|
try: |
65 |
|
self._parent._update_cursor.execute(statement) |
66 |
|
|
67 |
|
# Set _initialData to be the just-now posted values |
68 |
|
if not self._deleteFlag: |
69 |
|
self._initialData = {} |
70 |
|
for key in self._fields.keys(): |
71 |
|
self._initialData[key] = self._fields[key] |
72 |
|
|
73 |
|
except self._parent._dataObject._DatabaseError, err: |
74 |
|
raise GDataObjects.ConnectionError, err |
75 |
|
|
76 |
|
self._updateFlag = 0 |
77 |
|
self._insertFlag = 0 |
78 |
|
self._deleteFlag = 0 |
79 |
|
|
80 |
|
return 1 |
81 |
|
|
82 |
|
|
83 |
|
# If a vendor can do any of these more efficiently (i.e., use a known |
84 |
|
# PRIMARY KEY or ROWID, then override these methods. Otherwise, leave |
85 |
|
# as default. Note that these functions are specific to DB-SIG based |
86 |
|
# drivers (i.e., these functions are not in the base RecordSet class) |
87 |
|
|
88 |
|
def _buildDeleteStatement(self): |
89 |
|
if self._initialData.has_key(self._parent._primaryIdField): |
90 |
|
where = [self._parent._primaryIdFormat % \ |
91 |
|
self._initialData[self._parent._primaryIdField] ] |
92 |
|
else: |
93 |
|
where = [] |
94 |
|
for field in self._initialData.keys(): |
95 |
|
if self._parent.isFieldBound(field): |
96 |
|
if self._initialData[field] == None: |
97 |
|
where.append ("%s IS NULL" % field) |
98 |
|
else: |
99 |
|
where.append ("%s='%s'" % (field, self._initialData[field])) |
100 |
|
|
101 |
|
statement = "DELETE FROM %s WHERE %s" % \ |
102 |
|
(self._parent._dataObject.table, string.join(where,' AND ') ) |
103 |
|
return statement |
104 |
|
|
105 |
|
def _buildInsertStatement(self): |
106 |
|
vals = [] |
107 |
|
fields = [] |
108 |
|
|
109 |
|
# TODO: This should actually only insert modified fields. |
110 |
|
# TODO: Unfortunately, self._modifiedFlags is not being |
111 |
|
# TODO: set for new records (!@#$) |
112 |
|
#for field in self._modifiedFlags.keys(): |
113 |
|
|
114 |
|
for field in self._fields.keys(): |
115 |
|
if self._parent.isFieldBound(field): |
116 |
|
fields.append (field) |
117 |
|
if self._fields[field] == None or self._fields[field] == '': |
118 |
|
vals.append ("NULL") # % (self._fields[field])) |
119 |
|
else: |
120 |
|
try: |
121 |
|
if self._parent._fieldTypes[field] == 'number': |
122 |
|
vals.append ("%s" % (self._fields[field])) |
123 |
|
else: |
124 |
|
vals.append ("'%s'" % (self._fields[field])) |
125 |
|
except ValueError: |
126 |
|
vals.append ("%s" % (self._fields[field])) |
127 |
|
|
128 |
|
return "INSERT INTO %s (%s) VALUES (%s)" % \ |
129 |
|
(self._parent._dataObject.table, string.join(fields,','), \ |
130 |
|
string.join(vals,',') ) |
131 |
|
|
132 |
|
|
133 |
|
def _buildUpdateStatement(self): |
134 |
|
updates = [] |
135 |
|
for field in self._modifiedFlags.keys(): |
136 |
|
try: |
137 |
|
if self._parent._fieldTypes[field] == 'number': |
138 |
|
updates.append ("%s=%s" % (field, self._fields[field])) |
139 |
|
else: |
140 |
|
updates.append ("%s='%s'" % (field, self._fields[field])) |
141 |
|
except KeyError: |
142 |
|
updates.append ("%s='%s'" % (field, self._fields[field])) |
143 |
|
|
144 |
|
if self._initialData.has_key(self._parent._primaryIdField): |
145 |
|
where = [self._parent._primaryIdFormat % \ |
146 |
|
self._initialData[self._parent._primaryIdField] ] |
147 |
|
else: |
148 |
|
where = [] |
149 |
|
for field in self._initialData.keys(): |
150 |
|
if self._initialData[field] == None: |
151 |
|
where.append ("%s IS NULL" % field) |
152 |
|
else: |
153 |
|
try: |
154 |
|
if self._parent._fieldTypes[field] == 'number': |
155 |
|
where.append ("%s=%s" % (field, self._initialData[field])) |
156 |
|
else: |
157 |
|
where.append ("%s='%s'" % (field, self._initialData[field])) |
158 |
|
except KeyError: |
159 |
|
where.append ("%s='%s'" % (field, self._initialData[field])) |
160 |
|
|
161 |
|
return "UPDATE %s SET %s WHERE %s" % \ |
162 |
|
(self._parent._dataObject.table, string.join(updates,','), \ |
163 |
|
string.join(where,' AND ') ) |
164 |
|
|
165 |
|
|
166 |
|
class ODBC_ResultSet(GDataObjects.ResultSet): |
167 |
|
def __init__(self, dataObject, cursor=None, \ |
168 |
|
defaultValues={}, masterRecordSet=None): |
169 |
|
GDataObjects.ResultSet.__init__( |
170 |
|
self,dataObject,cursor,defaultValues,masterRecordSet) |
171 |
|
self._recordSetClass = ODBC_RecordSet |
172 |
|
self._fieldNames = None |
173 |
|
self._fieldTypes = {} |
174 |
|
|
175 |
|
# self._recordCount = cursor.rowcount > 0 and cursor.rowcount or 0 |
176 |
|
self._recordCount = 0 |
177 |
|
|
178 |
|
# If a DB driver supports a unique identifier for rows, |
179 |
|
# list it here. _primaryIdField is the field name (lower case) |
180 |
|
# that would appear in the recordset (note that this can be |
181 |
|
# a system generated format). If a primary id is supported, |
182 |
|
# _primaryIdFormat is the WHERE clause to be used. It will have |
183 |
|
# the string % (fieldvalue) format applied to it. |
184 |
|
self._primaryIdField = None |
185 |
|
self._primaryIdFormat = "__gnue__ = '%s'" |
186 |
|
|
187 |
|
GDebug.printMesg(5, 'ResultSet created') |
188 |
|
|
189 |
|
def _loadNextRecord(self): |
190 |
|
if self._cursor: |
191 |
|
rs = None |
192 |
|
|
193 |
|
try: |
194 |
|
rs = self._cursor.fetchone() |
195 |
|
except self._dataObject._DatabaseError, err: |
196 |
|
pass |
197 |
|
# TODO: It seems that popy does what the other drivers don't |
198 |
|
# TODO: and raises this error ALOT need to find out why |
199 |
|
# raise GDataObjects.ConnectionError, err |
200 |
|
|
201 |
|
if rs: |
202 |
|
if not self._fieldNames: |
203 |
|
self._fieldNames = [] |
204 |
|
for t in (self._cursor.description): |
205 |
|
self._fieldNames.append (string.lower(t[0])) |
206 |
|
self._fieldTypes[string.lower(t[0])] = (string.lower(t[1])) |
207 |
|
i = 0 |
208 |
|
dict = {} |
209 |
|
for f in (rs): |
210 |
|
dict[self._fieldNames[i]] = f |
211 |
|
i = i + 1 |
212 |
|
self._cachedRecords.append (self._recordSetClass(parent=self, \ |
213 |
|
initialData=dict)) |
214 |
|
return 1 |
215 |
|
else: |
216 |
|
return 0 |
217 |
|
else: |
218 |
|
return 0 |
219 |
|
|
220 |
|
|
221 |
|
class ODBC_DataObject(GDataObjects.DataObject): |
222 |
|
|
223 |
|
conditionElements = { |
224 |
|
'add': (2, 999, '(%s)', '+' ), |
225 |
|
'sub': (2, 999, '(%s)', '-' ), |
226 |
|
'mul': (2, 999, '(%s)', '*' ), |
227 |
|
'div': (2, 999, '(%s)', '/' ), |
228 |
|
'and': (1, 999, '(%s)', ' AND ' ), |
229 |
|
'or': (2, 999, '(%s)', ' OR ' ), |
230 |
|
'not': (1, 1, '(NOT %s)', None ), |
231 |
|
'negate': (1, 1, '-%s', None ), |
232 |
|
'eq': (2, 2, '(%s = %s)', None ), |
233 |
|
'ne': (2, 2, '(%s != %s)', None ), |
234 |
|
'gt': (2, 2, '(%s > %s)', None ), |
235 |
|
'ge': (2, 2, '(%s >= %s)', None ), |
236 |
|
'lt': (2, 2, '(%s < %s)', None ), |
237 |
|
'le': (2, 2, '(%s <= %s)', None ), |
238 |
|
'like': (2, 2, '%s LIKE %s', None ), |
239 |
|
'notlike': (2, 2, '%s NOT LIKE %s', None ), |
240 |
|
'between': (3, 3, '%s BETWEEN %s AND %s', None ) } |
241 |
|
|
242 |
|
def __init__(self, strictQueryCount=1): |
243 |
|
GDataObjects.DataObject.__init__(self) |
244 |
|
|
245 |
|
GDebug.printMesg (1,"DB-SIG database driver backend initializing") |
246 |
|
|
247 |
|
self._resultSetClass = ODBC_ResultSet |
248 |
|
self._DatabaseError = None |
249 |
|
self._strictQueryCount = strictQueryCount |
250 |
|
|
251 |
|
|
252 |
|
# This should be over-ridden only if driver needs more than user/pass |
253 |
|
def getLoginFields(self): |
254 |
|
return [['_username', 'User Name',0],['_password', 'Password',1]] |
255 |
|
|
256 |
|
|
257 |
|
def connect(self, connectData={}): |
258 |
|
|
259 |
|
GDebug.printMesg(1,"ODBC database driver initializing") |
260 |
|
self._DatabaseError = odbc.error |
261 |
|
|
262 |
|
try: |
263 |
|
service = connectData['service'] |
264 |
|
except KeyError: |
265 |
|
service = "" |
266 |
|
|
267 |
|
try: |
268 |
|
self._dataConnection = odbc.odbc( "%s/%s/%s" % ( |
269 |
|
service, |
270 |
|
connectData['_username'], |
271 |
|
connectData['_password'])) |
272 |
|
|
273 |
|
except dbi.opError, value: |
274 |
|
raise GDataObjects.LoginError, value |
275 |
|
|
276 |
|
except self._DatabaseError, value: |
277 |
|
raise GDataObjects.LoginError, value |
278 |
|
|
279 |
|
self._postConnect() |
280 |
|
|
281 |
|
|
282 |
|
# |
283 |
|
# Schema (metadata) functions |
284 |
|
# |
285 |
|
|
286 |
|
# TODO: See postgresql for an example of what these functions do. |
287 |
|
|
288 |
|
# Return a list of the types of Schema objects this driver provides |
289 |
|
def getSchemaTypes(self): |
290 |
|
return None # [('table',_('Tables'),1)] |
291 |
|
|
292 |
|
# Return a list of Schema objects |
293 |
|
def getSchemaList(self, type=None): |
294 |
|
return [] |
295 |
|
|
296 |
|
# Find a schema object with specified name |
297 |
|
def getSchemaByName(self, name, type=None): |
298 |
|
return None |
299 |
|
|
300 |
|
def _postConnect(self): |
301 |
|
self.triggerExtensions = TriggerExtensions(self._dataConnection) |
302 |
|
|
303 |
|
def _createResultSet(self, conditions={}, readOnly=0, masterRecordSet=None,sql=""): |
304 |
|
try: |
305 |
|
cursor = self._dataConnection.cursor() |
306 |
|
cursor.execute(self._buildQuery(conditions)) |
307 |
|
|
308 |
|
except dbi.progError, err: |
309 |
|
raise GDataObjects.ConnectionError, err |
310 |
|
|
311 |
|
except self._DatabaseError, err: |
312 |
|
raise GDataObjects.ConnectionError, err |
313 |
|
rs = self._resultSetClass(self, cursor=cursor, masterRecordSet=masterRecordSet) |
314 |
|
|
315 |
|
# pull a record count for the upcomming query |
316 |
|
if self._strictQueryCount: |
317 |
|
rs._recordCount = self._getQueryCount(conditions) |
318 |
|
|
319 |
|
if readOnly: |
320 |
|
rs._readonly = readOnly |
321 |
|
return rs |
322 |
|
|
323 |
|
|
324 |
|
def _getQueryCount(self,conditions={}): |
325 |
|
cursor = self._dataConnection.cursor() |
326 |
|
|
327 |
|
cursor.execute(self._buildQueryCount(conditions)) |
328 |
|
rs = cursor.fetchone() |
329 |
|
return int(rs[0]) |
330 |
|
|
331 |
|
|
332 |
|
def _buildQueryCount(self, conditions={}): |
333 |
|
q = "SELECT count(*) FROM %s%s" % (self.table, self._conditionToSQL(conditions)) |
334 |
|
|
335 |
|
GDebug.printMesg(5,q) |
336 |
|
|
337 |
|
return q |
338 |
|
|
339 |
|
def commit(self): |
340 |
|
GDebug.printMesg (5,"DB-SIG database driver: commit()") |
341 |
|
|
342 |
|
try: |
343 |
|
self._dataConnection.commit() |
344 |
|
except self._DatabaseError, value: |
345 |
|
raise GDataObjects.ConnectionError, value |
346 |
|
|
347 |
|
self._beginTransaction() |
348 |
|
|
349 |
|
def rollback(self): |
350 |
|
GDebug.printMesg (5,"DB-SIG database driver: rollback()") |
351 |
|
|
352 |
|
try: |
353 |
|
self._dataConnection.rollback() |
354 |
|
except: |
355 |
|
pass # I'm SURE this isn't right (jcater) |
356 |
|
# But not all db's support transactions |
357 |
|
|
358 |
|
self._beginTransaction() |
359 |
|
|
360 |
|
|
361 |
|
def _buildQuery(self, conditions={},forDetail=None,additionalSQL=""): |
362 |
|
return None |
363 |
|
|
364 |
|
|
365 |
|
# Used to convert a condition tree to an sql where clause |
366 |
|
def _conditionToSQL (self, condition): |
367 |
|
if condition == {} or condition == None: |
368 |
|
return "" |
369 |
|
elif type(condition) == types.DictType: |
370 |
|
cond = GConditions.buildConditionFromDict(condition) |
371 |
|
else: |
372 |
|
cond = condition |
373 |
|
|
374 |
|
if not len(cond._children): |
375 |
|
return "" |
376 |
|
elif len(cond._children) > 1: |
377 |
|
chillun = cond._children |
378 |
|
cond._children = [] |
379 |
|
_and = GConditions.GCand(cond) |
380 |
|
_and._children = chillun |
381 |
|
|
382 |
|
|
383 |
|
where = " WHERE (%s)" % (self.__conditionToSQL (cond._children[0])) |
384 |
|
GDebug.printMesg(5, where) |
385 |
|
return where |
386 |
|
|
387 |
|
# Used internally by _conditionToSQL |
388 |
|
def __conditionToSQL (self, element): |
389 |
|
if type(element) != types.InstanceType: |
390 |
|
return "%s" % element |
391 |
|
else: |
392 |
|
otype = string.lower(element._type[2:]) |
393 |
|
if otype == 'cfield': |
394 |
|
return "%s" % element.name |
395 |
|
elif otype == 'cconst': |
396 |
|
if element.value == None: |
397 |
|
return "NULL" |
398 |
|
elif element.type == 'number': |
399 |
|
return "%s" % element.value |
400 |
|
else: |
401 |
|
return "'%s'" % element.value |
402 |
|
elif otype == 'param': |
403 |
|
v = element.getValue() |
404 |
|
return (v == None and "NULL") or ("'%s'" % element.getValue()) |
405 |
|
elif self.conditionElements.has_key(otype): |
406 |
|
for i in range(0, len(element._children)): |
407 |
|
element._children[i] = self.__conditionToSQL(element._children[i]) |
408 |
|
if len(element._children) < self.conditionElements[otype][0]: |
409 |
|
tmsg = _('Condition element "%s" expects at least %s arguments; found %s') % \ |
410 |
|
(otype, self.conditionElements[otype][0], len(element._children)) |
411 |
|
raise GConditions.ConditionError, tmsg |
412 |
|
if len(element._children) > self.conditionElements[otype][1]: |
413 |
|
tmsg = _('Condition element "%s" expects at most %s arguments; found %s') % \ |
414 |
|
(otype, self.conditionElements[otype][0], len(element._children)) |
415 |
|
raise GConditions.ConditionError, tmsg |
416 |
|
if self.conditionElements[otype][3] == None: |
417 |
|
return self.conditionElements[otype][2] % tuple(element._children) |
418 |
|
else: |
419 |
|
return self.conditionElements[otype][2] % \ |
420 |
|
(string.join(element._children, self.conditionElements[otype][3])) |
421 |
|
else: |
422 |
|
tmsg = _('Condition clause "%s" is not supported by this db driver.') % otype |
423 |
|
raise GConditions.ConditionNotSupported, tmsg |
424 |
|
|
425 |
|
# Code necessary to force the connection into transaction mode... |
426 |
|
# this is usually not necessary (MySQL is one of few DBs that must force) |
427 |
|
def _beginTransaction(self): |
428 |
|
pass |
429 |
|
|
430 |
|
|
431 |
|
class ODBC_DataObject_Object(ODBC_DataObject): |
432 |
|
def _buildQuery(self, conditions={}): |
433 |
|
GDebug.printMesg(7,'Implicit Fields: %s' % self._fieldReferences) |
434 |
|
if len(self._fieldReferences): |
435 |
|
q = "SELECT %s FROM %s%s" % \ |
436 |
|
(string.join(self._fieldReferences.keys(),","), self.table, |
437 |
|
self._conditionToSQL(conditions)) |
438 |
|
else: |
439 |
|
q = "SELECT * FROM %s%s" % (self.table, self._conditionToSQL(conditions)) |
440 |
|
|
441 |
|
if hasattr(self,'order_by'): |
442 |
|
q = "%s ORDER BY %s " % (q, self.order_by) |
443 |
|
|
444 |
|
GDebug.printMesg(5,q) |
445 |
|
|
446 |
|
return q |
447 |
|
|
448 |
|
|
449 |
|
|
450 |
|
# |
451 |
|
# Extensions to Trigger Namespaces |
452 |
|
# |
453 |
|
class TriggerExtensions: |
454 |
|
|
455 |
|
def __init__(self, connection): |
456 |
|
self.__connection = connection |
457 |
|
|
458 |
|
|
459 |
|
|
460 |
|
|
461 |
|
###################################### |
462 |
|
# |
463 |
|
# The following hashes describe |
464 |
|
# this driver's characteristings. |
465 |
|
# |
466 |
|
###################################### |
467 |
|
|
468 |
|
# |
469 |
|
# All datasouce "types" and corresponding DataObject class |
470 |
|
# |
471 |
|
supportedDataObjects = { |
472 |
|
'object': ODBC_DataObject_Object, |
473 |
|
# 'sql': ODBC_DataObject_SQL |
474 |
|
} |
475 |
|
|
476 |
|
|