1   
  2   
  3  __version__ = '$Revision: 6398 $'.split()[1]  
  4  __date__ = '$Date: 2007-06-20 10:42:04 -0700 (Wed, 20 Jun 2007) $'.split()[1]  
  5  __author__ = 'Kurt Schwehr' 
  6  __doc__=''' 
  7  Helper functions to create SQL statements.  
  8   
  9  @license: GPL 
 10  @todo: How do I assemble queries like this:: 
 11   
 12      SELECT COUNT(samplename) AS count FROM \\ 
 13          (SELECT DISTINCT(samplename) AS samplename FROM \\ 
 14           ams WHERE corenum=1 AND coretype='p'); 
 15   
 16  @todo: subqueries 
 17  @todo: make a super class so that inserts and selects can verify based on the create str 
 18  @todo: take the super class info from the database? 
 19   
 20  @bug: FIX: write some doc tests! 
 21  @bug: had no protection from SQL injection attacks or quoting mistakes 
 22   
 23  @note: This is not as snazzy as SQLAlchemy or SQLObject, but it works and is simple 
 24   
 25  @author: '''+__author__+''' 
 26  @version: ''' + __version__ +''' 
 27  @copyright: 2006 
 28  @note: postgres is sort of case sensitive, so go all lowercase for fields and tables 
 29  @var __date__: Date of last svn commit 
 30   
 31  @undocumented: __version__ __author__ __doc__ myparser 
 32  ''' 
 33   
 34   
 35  import sys 
 36   
 37  from BitVector import BitVector 
 38   
 39   
 40   
 41   
 42   
 43  BOMBASTIC= 4 
 44  VERBOSE  = 3 
 45  TRACE    = 2 
 46  TERSE    = 1 
 47  ALWAYS   = 0 
 48  NEVER    = 0  
 49   
 50   
 51   
 53      ''' 
 54      Added the verbosity options to a parser 
 55      ''' 
 56      parser.add_option('-v','--verbose',action="count",dest='verbosity',default=0, 
 57                          help='how much information to give.  Specify multiple times to increase verbosity') 
 58      parser.add_option('--verbosity',dest='verbosity',type='int', 
 59                          help='Specify verbosity.  Should be in the range of ' 
 60                          +str(ALWAYS)+'...'+str(BOMBASTIC)+' (None...Bombastic)') 
 61      parser.add_option('--noisy',dest='verbosity', action='store_const', const=2*BOMBASTIC, 
 62                          help='Go for the max verbosity ['+str(2*BOMBASTIC)+']') 
  63   
 64   
 65  import datetime 
 67      ''' 
 68      Convert a UTC sec time to a SQL timestamp 
 69   
 70      >>> sec2timestamp(int(1169703371)) 
 71      '2007-01-25 05:36:11' 
 72   
 73      ''' 
 74   
 75      d = datetime.datetime.utcfromtimestamp(utcsec) 
 76      s = '%d-%02d-%02d %02d:%02d:%02d' % (d.year,d.month,d.day,d.hour,d.minute,d.second) 
 77      return s 
  78   
 79   
 81      ''' 
 82      Construct an sql select query 
 83   
 84      Sometimes it just gets ugly having all that comma and WHERE AND 
 85      logic in there.  This code takes care of that 
 86      ''' 
 88          self.fields = [] 
 89          self.where = [] 
 90          self.limit = None 
 91          self.from_tables = [] 
 92          self.orderby = None 
 93          self.desc = False  
 94          return 
  95   
 97          "Make the returned rows come in some order" 
 98          if str != type(field): print "ERROR: fix throw type exception" 
 99          self.orderby = field 
100          self.desc = desc 
101          return 
 102   
104          "Add a field name to return" 
105          if str != type(fieldname): print "ERROR: fix throw type exception" 
106          self.fields.append(fieldname) 
107          return 
 108   
110          " Add expressions to chain together with ANDs" 
111          if str != type(boolTest): 
112              print "ERROR: fix throw type exception" 
113          self.where.append(boolTest) 
114          return 
 115   
117          "Which tables the query will pull from" 
118          if str != type(tableName): 
119              print "ERROR: fix throw type exception" 
120          self.from_tables.append(tableName) 
121          return 
 122   
124          "Set the maximum number of items to return" 
125          if int != type(numOfItems): 
126              print "ERROR: fix throw type exception" 
127          self.limit = numOfItems 
128          return 
 129   
131          "Return the query as a string" 
132          if len(self.fields) < 1: print "ERROR: Must specify at least one from!\n  FIX: throw some exception?" 
133          s = 'SELECT ' 
134           
135          if dbType == 'postgres': 
136              s+=','.join([f.lower() for f in self.fields]) 
137          else: 
138              s+=','.join(self.fields) 
139          s += self.fields[-1] + ' ' 
140   
141          if len(self.from_tables)<1: print "ERROR: fix throw some exception" 
142          s += 'FROM ' 
143          for i in range (len(self.from_tables)-1): 
144              s += self.from_tables[i]+',' 
145          s += self.from_tables[-1] 
146   
147          if (len(self.where)>0): s += ' WHERE ' 
148          for i in range (len(self.where)-1): 
149              s += self.where[i]+' AND ' 
150          if (len(self.where)>0): s += self.where[-1] 
151   
152          if (None != self.orderby): 
153              s += ' ORDER BY ' + self.orderby 
154              if self.desc: s += ' DESC' 
155   
156          if (None != self.limit): 
157              s += ' LIMIT ' + str(self.limit) 
158   
159          s += ';' 
160          return s 
  161   
163      ''' 
164      Helper for building create SQL commands. 
165   
166      FIX: add type checking - what did I mean by this??? 
167      @todo: FIX - add a remove command to nuke a field 
168      ''' 
169   
170 -    def __init__(self,table,dbType='postgres'): 
 171          '''Kick it off with no fields 
172   
173          table - which table are we going to insert into''' 
174          self.table = table 
175          self.dbType = dbType 
176          self.fields = [] 
177          self.types = [] 
178          self.postgis = [];  
179          return 
 180   
181 -    def add(self,field,typeStr): 
 182          ''' 
183          Unchecked field.  Provide the field and type all in one.  Use 
184          this if nothing matches what you need. 
185   
186          e.g.: 
187          create.add('corenumber','INTEGER') 
188          create.add('username','VARCHAR(40)') 
189          create.add('id','INTEGER PRIMARY KEY') 
190   
191          @param field: name of the field 
192          @param typeStr: the type of field 
193   
194          @todo: Allow setting of primary key in a simple way 
195          ''' 
196          self.fields.append(field) 
197          self.types.append(typeStr) 
198          return 
 199   
201          ''' 
202          Add a primary key based on the field name. 
203          @todo: FIX: complain if trying to add a second primary key 
204          ''' 
205   
206          self.fields.append(keyName) 
207          if   'sqlite'  ==self.dbType: self.types.append('INTEGER PRIMARY KEY') 
208          elif 'postgres'==self.dbType: self.types.append('SERIAL PRIMARY KEY') 
209          else: 
210              print 'Do not know how to construct a primary key for database type of',self.dbType 
211              assert False 
212          return 
 213   
215          ''' 
216          SQL integer field 
217          @param field: name of the field 
218          ''' 
219          self.fields.append(field) 
220          self.types.append("INTEGER") 
 221   
223          ''' 
224          SQL floating point field 
225          @param field: name of the field 
226          ''' 
227   
228          self.fields.append(field) 
229          self.types.append("REAL") 
 230   
232          ''' 
233          SQL VARCHAR field... variable length up to a max size 
234          @param field: name of the field 
235          @param length: max length of the field 
236          ''' 
237          self.fields.append(field) 
238          self.types.append("VARCHAR("+str(length)+")") 
 239   
240   
242          ''' 
243          SQL Boolean field 
244          @param field: name of the field 
245          ''' 
246          self.fields.append(field) 
247          self.types.append("BOOL") 
 248   
250          ''' 
251          SQL Boolean field 
252          @param field: name of the field 
253          @param length: largest possible size 
254          ''' 
255          assert (length>0) 
256          self.fields.append(field) 
257          self.types.append('BIT VARYING('+str(length)+')') 
 258   
259   
261          ''' 
262          @param precision: overall digits including to right of decimal 
263          @param scale: number of digits to the right of decimal 
264          ''' 
265          self.fields.append(field) 
266          self.types.append('DECIMAL('+str(precision)+','+str(scale)+')') 
 267   
269          '''SQL TIMESTAMP field 
270          @param field: name of the field 
271          ''' 
272          self.fields.append(field) 
273          self.types.append("TIMESTAMP") 
274          return 
 275   
276 -    def addPostGIS(self,field,typeName,dimension,SRID='-1'): 
 277          ''' 
278          Add a spatial column to the table using the OpenGIS 
279          AddGeometryColumn function using current schema: 
280   
281          AddGeometryColumn(<table_name>,<column_name>, <srid>, <type>, <dimension>) 
282   
283          @param field: Name of the field in the db table 
284          @type field: str 
285          @param typeName: OpenGIS geometry type (e.g. POINT) 
286          @type typeName: str 
287          @param dimension: x,y would be 2 
288          @type dimension: int 
289          @param SRID: spatial referencing system identifier (FIX: give some more info!) 
290          @type SRID: int 
291          ''' 
292   
293          int(dimension)  
294          self.postgis.append((field,typeName,dimension,SRID)) 
 295   
296   
298          '''Return the SQL string for the table creation 
299          @rtype: str''' 
300          assert (len(self.fields)>0) 
301          assert (len(self.types)>0) 
302          assert (len(self.fields)==len(self.types)) 
303          cstr = 'CREATE TABLE ' 
304          if 'postgres'==self.dbType: 
305              cstr += self.table.lower()+' (' 
306              for i in range(len(self.fields)-1): 
307                  cstr += str(self.fields[i].lower())+' '+str(self.types[i])+', ' 
308              cstr += str(self.fields[-1].lower())+' '+str(self.types[-1]) 
309          else: 
310              cstr += self.table+' ( ' 
311              for i in range(len(self.fields)-1): 
312                  cstr += str(self.fields[i])+' '+str(self.types[i])+', ' 
313              cstr += str(self.fields[-1])+' '+str(self.types[-1]) 
314          cstr += ' ); ' 
315   
316          cmds=[] 
317          for postgisFields in self.postgis: 
318              table    = '\''+self.table.lower()+'\'' 
319              field    = '\''+postgisFields[0].lower()+'\'' 
320              typeName = '\''+postgisFields[1]+'\'' 
321              dim      = str(postgisFields[2]) 
322              SRID     = str(postgisFields[3]) 
323              fieldStr = ','.join((table,field,SRID,typeName,dim)) 
324              addCmd = 'SELECT AddGeometryColumn('+fieldStr+')' 
325              cmds.append(addCmd) 
326   
327          retStr = cstr + ';'.join(cmds) 
328          if len(cmds)>0: retStr += ';' 
329          return retStr 
  330   
332      ''' 
333      Help create an SQL insert statement for injecting data into a database.  Wee! 
334   
335      @todo: FIX: provide some sort of validation, maybe with the CREATE string or class? 
336      @todo: Put in a remove/delete call to pull a value out so that it is not inserted  
337   
338      @todo: FIX:  MUST REWRITE THIS CLASS TO BE TYPE AWARE. 
339      ''' 
340 -    def __init__(self,table,dbType='postgres'): 
 341          '''Create an insert with no values 
342   
343          @param table: which table are we going to insert into 
344          @param dbType: sqlite can not handle True/False keyworks (at version 3.2.8) 
345          ''' 
346          self.table = table 
347          self.dbType = dbType 
348          self.fields = [] 
349          self.values = [] 
350          self.postGIS = [] 
351          return 
 352       
354          '''Print out a safer dump to std out rather than str for debugging''' 
355          print '\n === dump insert for table',self.table,'===' 
356          for i in range(1,len(self.fields)): 
357              print self.fields[i], self.values[i],'    (',type(self.fields[i]), type(self.values[i]),')' 
358          print  
 359   
361          "Return the SQL string for the insert" 
362          if 0==len(self.fields): 
363              print "WARNING: empty insert.  returning empty string" 
364              return ""   
365           
366          s = 'INSERT INTO ' 
367   
368          if 'postgres'==self.dbType: s+= self.table.lower() + ' ' 
369          else: s+= self.table + ' ' 
370   
371          assert(len(self.fields)==len(self.values)) 
372          fields = None 
373          if 'postgres'==self.dbType: 
374              fields = [f.lower() for f in self.fields] 
375          else: fields = self.fields 
376           
377           
378   
379           
380           
381           
382           
383   
384           
385           
386          s2List=[] 
387          for i in range(len(fields)): 
388               
389              if bool == type(self.values[i]): 
390                  if 'sqlite'==self.dbType: 
391                      if self.values[i]: s2List.append('1') 
392                      else: s2List.append('0') 
393                  else: s2List.append(str(self.values[i])) 
394              elif isinstance(self.values[i],BitVector): s2List.append('\''+str(self.values[i])+'\'') 
395              elif str == type(self.values[i]):          s2List.append('\''+str(self.values[i])+'\'') 
396              elif type(self.values[i]) in (int, float): s2List.append(str(self.values[i])) 
397               
398              elif not self.values[i]:   
399                  print 'FIX: what was I trying to accomplish with this?',fields[i],self.values[i] 
400                  s2List.append('NULL') 
401              else: 
402                  s2List.append(str(self.values[i])) 
403   
404          s1List = fields 
405          for entry in self.postGIS: 
406              s1List.append(entry[0].lower()) 
407               
408              s2List.append('GeomFromText(\'' + entry[1] + '\',4326)') 
409               
410          s += '(' + ','.join(s1List) + ') VALUES (' + ','.join(s2List) + ');' 
411          return s 
 412   
413 -    def addPostGIS(self,field,value): 
 414          ''' 
415          Handle postGIS geometry 
416          ''' 
417          self.postGIS.append((field,value)) 
 418   
419   
420 -    def add(self,field,value): 
 421          '''Add a field value pair to the insert 
422   
423          @note: Integers and floats should NOT be converted to strings. 
424          @param field: name of the field 
425          @param value: value to be assigned to that field. 
426          ''' 
427   
428          if type(value)==str: 
429               
430              value = value.replace('"','""')   
431          self.fields.append(field) 
432          self.values.append(value) 
433          return 
  434   
435   
436   
437   
438  import datetime 
439   
441      ''' Take a list and make an insert string.  This works with 
442      dictionaries too.  Here is a quick example: 
443   
444      >>> aList = [('one',1),('2','two'),('threepoint',3.)] 
445      >>> sqlInsertStrFromList('myTable',aList,dbType='sqlite') 
446      "insert into myTable (one,2,threepoint) values (1,'two',3.0);" 
447      >>> sqlInsertStrFromList('myTable',aList) 
448      "insert into mytable (one,2,threepoint) values (1,'two',3.0);" 
449   
450      @param table: Which table to insert into 
451      @type table: str 
452      @param aList: list of tubles pairs to insert - (name, value) 
453      @type aList(list) 
454      @return: complete SQL insert command 
455      @rtype: str 
456      ''' 
457   
458      if 'postgres'==dbType: table = table.lower() 
459      ins = "insert into " + table + " (" 
460      first = [] 
461      if 'postgres'==dbType: first = [f[0].lower() for f in aList] 
462      else: first = [f[0] for f in aList] 
463      ins += ','.join(first) + ") values (" 
464      first=True 
465      for pair in aList: 
466          value = pair[1] 
467          if first: first=False 
468          else:     ins+="," 
469           
470           
471           
472           
473           
474          if type(value)!=int and type(value)!=float: ins+="'" 
475          ins+=str(value) 
476          if type(value)!=int and type(value)!=float: ins+="'" 
477           
478      ins += ");" 
479      return ins 
 480   
481   
482   
483   
484  if __name__=='__main__': 
485      from optparse import OptionParser 
486      myparser = OptionParser(usage="%prog [options]", 
487                              version="%prog "+__version__) 
488      myparser.add_option('--test','--doc-test',dest='doctest',default=False,action='store_true', 
489                          help='run the documentation tests') 
490   
491      addVerbosityOptions(myparser) 
492      (options,args) = myparser.parse_args() 
493   
494      success=True 
495   
496      if options.doctest: 
497          import os; print os.path.basename(sys.argv[0]), 'doctests ...', 
498          sys.argv= [sys.argv[0]] 
499          if options.verbosity>=TERSE: sys.argv.append('-v') 
500          import doctest 
501          numfail,numtests=doctest.testmod() 
502          if numfail==0: print 'ok' 
503          else:  
504              print 'FAILED' 
505              success=False 
506   
507      if not success: 
508          sys.exit('Something Failed') 
509