]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
merge from master with sqla
[dak.git] / daklib / dbconn.py
1 #!/usr/bin/python
2
3 """ DB access class
4
5 @contact: Debian FTPMaster <ftpmaster@debian.org>
6 @copyright: 2000, 2001, 2002, 2003, 2004, 2006  James Troup <james@nocrew.org>
7 @copyright: 2008-2009  Mark Hymers <mhy@debian.org>
8 @copyright: 2009  Joerg Jaspert <joerg@debian.org>
9 @copyright: 2009  Mike O'Connor <stew@debian.org>
10 @license: GNU General Public License version 2 or later
11 """
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ################################################################################
28
29 # < mhy> I need a funny comment
30 # < sgran> two peanuts were walking down a dark street
31 # < sgran> one was a-salted
32 #  * mhy looks up the definition of "funny"
33
34 ################################################################################
35
36 import os
37 import psycopg2
38 import traceback
39
40 from singleton import Singleton
41 from config import Config
42
43 ################################################################################
44
45 class Cache(object):
46     def __init__(self, hashfunc=None):
47         if hashfunc:
48             self.hashfunc = hashfunc
49         else:
50             self.hashfunc = lambda x: x['value']
51
52         self.data = {}
53
54     def SetValue(self, keys, value):
55         self.data[self.hashfunc(keys)] = value
56
57     def GetValue(self, keys):
58         return self.data.get(self.hashfunc(keys))
59
60 ################################################################################
61
62 class DBConn(Singleton):
63     """
64     database module init.
65     """
66     def __init__(self, *args, **kwargs):
67         super(DBConn, self).__init__(*args, **kwargs)
68
69     def _startup(self, *args, **kwargs):
70         self.__createconn()
71         self.__init_caches()
72
73     ## Connection functions
74     def __createconn(self):
75         cnf = Config()
76         connstr = "dbname=%s" % "projectbstew" #cnf["DB::Name"]
77         print( "connstr: %s "% connstr)
78         if cnf["DB::Host"]:
79            connstr += " host=%s" % cnf["DB::Host"]
80         if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
81            connstr += " port=%s" % cnf["DB::Port"]
82
83         self.db_con = psycopg2.connect(connstr)
84
85     def reconnect(self):
86         try:
87             self.db_con.close()
88         except psycopg2.InterfaceError:
89             pass
90
91         self.db_con = None
92         self.__createconn()
93
94     ## Cache functions
95     def __init_caches(self):
96         self.caches = {'suite':         Cache(),
97                        'section':       Cache(),
98                        'priority':      Cache(),
99                        'override_type': Cache(),
100                        'architecture':  Cache(),
101                        'archive':       Cache(),
102                        'component':     Cache(),
103                        'content_path_names':     Cache(),
104                        'content_file_names':     Cache(),
105                        'location':      Cache(lambda x: '%s_%s_%s' % (x['location'], x['component'], x['location'])),
106                        'maintainer':    {}, # TODO
107                        'keyring':       {}, # TODO
108                        'source':        Cache(lambda x: '%s_%s_' % (x['source'], x['version'])),
109                        'files':         Cache(lambda x: '%s_%s_' % (x['filename'], x['location'])),
110                        'maintainer':    {}, # TODO
111                        'fingerprint':   {}, # TODO
112                        'queue':         {}, # TODO
113                        'uid':           {}, # TODO
114                        'suite_version': Cache(lambda x: '%s_%s' % (x['source'], x['suite'])),
115                       }
116
117         self.prepared_statements = {}
118
119     def prepare(self,name,statement):
120         if not self.prepared_statements.has_key(name):
121             c = self.cursor()
122             c.execute(statement)
123             self.prepared_statements[name] = statement
124
125     def clear_caches(self):
126         self.__init_caches()
127
128     ## Functions to pass through to the database connector
129     def cursor(self):
130         return self.db_con.cursor()
131
132     def commit(self):
133         return self.db_con.commit()
134
135     ## Get functions
136     def __get_single_id(self, query, values, cachename=None):
137         # This is a bit of a hack but it's an internal function only
138         if cachename is not None:
139             res = self.caches[cachename].GetValue(values)
140             if res:
141                 return res
142
143         c = self.db_con.cursor()
144         c.execute(query, values)
145
146         if c.rowcount != 1:
147             return None
148
149         res = c.fetchone()[0]
150
151         if cachename is not None:
152             self.caches[cachename].SetValue(values, res)
153
154         return res
155
156     def __get_id(self, retfield, table, qfield, value):
157         query = "SELECT %s FROM %s WHERE %s = %%(value)s" % (retfield, table, qfield)
158         return self.__get_single_id(query, {'value': value}, cachename=table)
159
160     def get_suite_id(self, suite):
161         """
162         Returns database id for given C{suite}.
163         Results are kept in a cache during runtime to minimize database queries.
164
165         @type suite: string
166         @param suite: The name of the suite
167
168         @rtype: int
169         @return: the database id for the given suite
170
171         """
172         suiteid = self.__get_id('id', 'suite', 'suite_name', suite)
173         if suiteid is None:
174             return None
175         else:
176             return int(suiteid)
177
178     def get_section_id(self, section):
179         """
180         Returns database id for given C{section}.
181         Results are kept in a cache during runtime to minimize database queries.
182
183         @type section: string
184         @param section: The name of the section
185
186         @rtype: int
187         @return: the database id for the given section
188
189         """
190         return self.__get_id('id', 'section', 'section', section)
191
192     def get_priority_id(self, priority):
193         """
194         Returns database id for given C{priority}.
195         Results are kept in a cache during runtime to minimize database queries.
196
197         @type priority: string
198         @param priority: The name of the priority
199
200         @rtype: int
201         @return: the database id for the given priority
202
203         """
204         return self.__get_id('id', 'priority', 'priority', priority)
205
206     def get_override_type_id(self, override_type):
207         """
208         Returns database id for given override C{type}.
209         Results are kept in a cache during runtime to minimize database queries.
210
211         @type override_type: string
212         @param override_type: The name of the override type
213
214         @rtype: int
215         @return: the database id for the given override type
216
217         """
218         return self.__get_id('id', 'override_type', 'type', override_type)
219
220     def get_architecture_id(self, architecture):
221         """
222         Returns database id for given C{architecture}.
223         Results are kept in a cache during runtime to minimize database queries.
224
225         @type architecture: string
226         @param architecture: The name of the override type
227
228         @rtype: int
229         @return: the database id for the given architecture
230
231         """
232         return self.__get_id('id', 'architecture', 'arch_string', architecture)
233
234     def get_archive_id(self, archive):
235         """
236         returns database id for given c{archive}.
237         results are kept in a cache during runtime to minimize database queries.
238
239         @type archive: string
240         @param archive: the name of the override type
241
242         @rtype: int
243         @return: the database id for the given archive
244
245         """
246         return self.__get_id('id', 'archive', 'lower(name)', archive)
247
248     def get_component_id(self, component):
249         """
250         Returns database id for given C{component}.
251         Results are kept in a cache during runtime to minimize database queries.
252
253         @type component: string
254         @param component: The name of the override type
255
256         @rtype: int
257         @return: the database id for the given component
258
259         """
260         return self.__get_id('id', 'component', 'lower(name)', component)
261
262     def get_location_id(self, location, component, archive):
263         """
264         Returns database id for the location behind the given combination of
265           - B{location} - the path of the location, eg. I{/srv/ftp.debian.org/ftp/pool/}
266           - B{component} - the id of the component as returned by L{get_component_id}
267           - B{archive} - the id of the archive as returned by L{get_archive_id}
268         Results are kept in a cache during runtime to minimize database queries.
269
270         @type location: string
271         @param location: the path of the location
272
273         @type component: int
274         @param component: the id of the component
275
276         @type archive: int
277         @param archive: the id of the archive
278
279         @rtype: int
280         @return: the database id for the location
281
282         """
283
284         archive_id = self.get_archive_id(archive)
285
286         if not archive_id:
287             return None
288
289         res = None
290
291         if component:
292             component_id = self.get_component_id(component)
293             if component_id:
294                 res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND component=%(component)s AND archive=%(archive)s",
295                         {'location': location,
296                          'archive': int(archive_id),
297                          'component': component_id}, cachename='location')
298         else:
299             res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND archive=%(archive)d",
300                     {'location': location, 'archive': archive_id, 'component': ''}, cachename='location')
301
302         return res
303
304     def get_source_id(self, source, version):
305         """
306         Returns database id for the combination of C{source} and C{version}
307           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
308           - B{version}
309         Results are kept in a cache during runtime to minimize database queries.
310
311         @type source: string
312         @param source: source package name
313
314         @type version: string
315         @param version: the source version
316
317         @rtype: int
318         @return: the database id for the source
319
320         """
321         return self.__get_single_id("SELECT id FROM source s WHERE s.source=%(source)s AND s.version=%(version)s",
322                                  {'source': source, 'version': version}, cachename='source')
323
324     def get_suite_version(self, source, suite):
325         """
326         Returns database id for a combination of C{source} and C{suite}.
327
328           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
329           - B{suite} - a suite name, eg. I{unstable}
330
331         Results are kept in a cache during runtime to minimize database queries.
332
333         @type source: string
334         @param source: source package name
335
336         @type suite: string
337         @param suite: the suite name
338
339         @rtype: string
340         @return: the version for I{source} in I{suite}
341
342         """
343         return self.__get_single_id("""
344         SELECT s.version FROM source s, suite su, src_associations sa
345         WHERE sa.source=s.id
346           AND sa.suite=su.id
347           AND su.suite_name=%(suite)s
348           AND s.source=%(source)""", {'suite': suite, 'source': source}, cachename='suite_version')
349
350
351     def get_files_id (self, filename, size, md5sum, location_id):
352         """
353         Returns -1, -2 or the file_id for filename, if its C{size} and C{md5sum} match an
354         existing copy.
355
356         The database is queried using the C{filename} and C{location_id}. If a file does exist
357         at that location, the existing size and md5sum are checked against the provided
358         parameters. A size or checksum mismatch returns -2. If more than one entry is
359         found within the database, a -1 is returned, no result returns None, otherwise
360         the file id.
361
362         Results are kept in a cache during runtime to minimize database queries.
363
364         @type filename: string
365         @param filename: the filename of the file to check against the DB
366
367         @type size: int
368         @param size: the size of the file to check against the DB
369
370         @type md5sum: string
371         @param md5sum: the md5sum of the file to check against the DB
372
373         @type location_id: int
374         @param location_id: the id of the location as returned by L{get_location_id}
375
376         @rtype: int / None
377         @return: Various return values are possible:
378                    - -2: size/checksum error
379                    - -1: more than one file found in database
380                    - None: no file found in database
381                    - int: file id
382
383         """
384         values = {'filename' : filename,
385                   'location' : location_id}
386
387         res = self.caches['files'].GetValue( values )
388
389         if not res:
390             query = """SELECT id, size, md5sum
391                        FROM files
392                        WHERE filename = %(filename)s AND location = %(location)s"""
393
394             cursor = self.db_con.cursor()
395             cursor.execute( query, values )
396
397             if cursor.rowcount == 0:
398                 res = None
399
400             elif cursor.rowcount != 1:
401                 res = -1
402
403             else:
404                 row = cursor.fetchone()
405
406                 if row[1] != int(size) or row[2] != md5sum:
407                     res =  -2
408
409                 else:
410                     self.caches['files'].SetValue(values, row[0])
411                     res = row[0]
412
413         return res
414
415
416     def get_or_set_contents_file_id(self, filename):
417         """
418         Returns database id for given filename.
419
420         Results are kept in a cache during runtime to minimize database queries.
421         If no matching file is found, a row is inserted.
422
423         @type filename: string
424         @param filename: The filename
425
426         @rtype: int
427         @return: the database id for the given component
428         """
429         try:
430             values={'value': filename}
431             query = "SELECT id FROM content_file_names WHERE file = %(value)s"
432             id = self.__get_single_id(query, values, cachename='content_file_names')
433             if not id:
434                 c = self.db_con.cursor()
435                 c.execute( "INSERT INTO content_file_names VALUES (DEFAULT, %(value)s) RETURNING id",
436                            values )
437
438                 id = c.fetchone()[0]
439                 self.caches['content_file_names'].SetValue(values, id)
440
441             return id
442         except:
443             traceback.print_exc()
444             raise
445
446     def get_or_set_contents_path_id(self, path):
447         """
448         Returns database id for given path.
449
450         Results are kept in a cache during runtime to minimize database queries.
451         If no matching file is found, a row is inserted.
452
453         @type path: string
454         @param path: The filename
455
456         @rtype: int
457         @return: the database id for the given component
458         """
459         try:
460             values={'value': path}
461             query = "SELECT id FROM content_file_paths WHERE path = %(value)s"
462             id = self.__get_single_id(query, values, cachename='content_path_names')
463             if not id:
464                 c = self.db_con.cursor()
465                 c.execute( "INSERT INTO content_file_paths VALUES (DEFAULT, %(value)s) RETURNING id",
466                            values )
467
468                 id = c.fetchone()[0]
469                 self.caches['content_path_names'].SetValue(values, id)
470
471             return id
472         except:
473             traceback.print_exc()
474             raise
475
476     def get_suite_architectures(self, suite):
477         """
478         Returns list of architectures for C{suite}.
479
480         @type suite: string, int
481         @param suite: the suite name or the suite_id
482
483         @rtype: list
484         @return: the list of architectures for I{suite}
485         """
486
487         suite_id = None
488         if type(suite) == str:
489             suite_id = self.get_suite_id(suite)
490         elif type(suite) == int:
491             suite_id = suite
492         else:
493             return None
494
495         c = self.db_con.cursor()
496         c.execute( """SELECT a.arch_string FROM suite_architectures sa
497                       JOIN architecture a ON (a.id = sa.architecture)
498                       WHERE suite='%s'""" % suite_id )
499
500         return map(lambda x: x[0], c.fetchall())
501
502     def insert_content_paths(self, bin_id, fullpaths):
503         """
504         Make sure given path is associated with given binary id
505
506         @type bin_id: int
507         @param bin_id: the id of the binary
508         @type fullpaths: list
509         @param fullpaths: the list of paths of the file being associated with the binary
510
511         @return: True upon success
512         """
513
514         c = self.db_con.cursor()
515
516         c.execute("BEGIN WORK")
517         try:
518
519             for fullpath in fullpaths:
520                 (path, file) = os.path.split(fullpath)
521
522                 if path.startswith( "./" ):
523                     path = path[2:]
524                 # Get the necessary IDs ...
525                 file_id = self.get_or_set_contents_file_id(file)
526                 path_id = self.get_or_set_contents_path_id(path)
527
528                 c.execute("""INSERT INTO deb_contents
529                 
530                                (binary_pkg, filepath, filename)
531                            VALUES ( '%d', '%d', '%d')""" % (bin_id, path_id, file_id) )
532
533             c.execute("COMMIT")
534             return True
535         except:
536             traceback.print_exc()
537             c.execute("ROLLBACK")
538             return False
539
540     def insert_pending_content_paths(self, package, fullpaths):
541         """
542         Make sure given paths are temporarily associated with given
543         package
544
545         @type package: dict
546         @param package: the package to associate with should have been read in from the binary control file
547         @type fullpaths: list
548         @param fullpaths: the list of paths of the file being associated with the binary
549
550         @return: True upon success
551         """
552
553         c = self.db_con.cursor()
554
555         c.execute("BEGIN WORK")
556         try:
557             arch_id = self.get_architecture_id(package['Architecture'])
558
559             # Remove any already existing recorded files for this package
560             c.execute("""DELETE FROM pending_content_associations
561                          WHERE package=%(Package)s
562                          AND version=%(Version)s
563                          AND architecture=%(ArchID)s""", {'Package': package['Package'],
564                                                           'Version': package['Version'],
565                                                           'ArchID':  arch_id})
566
567             for fullpath in fullpaths:
568                 (path, file) = os.path.split(fullpath)
569
570                 if path.startswith( "./" ):
571                     path = path[2:]
572                 # Get the necessary IDs ...
573                 file_id = self.get_or_set_contents_file_id(file)
574                 path_id = self.get_or_set_contents_path_id(path)
575
576                 c.execute("""INSERT INTO pending_content_associations
577                                (package, version, architecture, filepath, filename)
578                             VALUES (%%(Package)s, %%(Version)s, '%d', '%d', '%d')"""
579                     % (arch_id, path_id, file_id), package )
580
581             c.execute("COMMIT")
582             return True
583         except:
584             traceback.print_exc()
585             c.execute("ROLLBACK")
586             return False