]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
The other way round
[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" % cnf["DB::Name"]
77         if cnf["DB::Host"]:
78            connstr += " host=%s" % cnf["DB::Host"]
79         if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
80            connstr += " port=%s" % cnf["DB::Port"]
81
82         self.db_con = psycopg2.connect(connstr)
83
84     def reconnect(self):
85         try:
86             self.db_con.close()
87         except psycopg2.InterfaceError:
88             pass
89
90         self.db_con = None
91         self.__createconn()
92
93     ## Cache functions
94     def __init_caches(self):
95         self.caches = {'suite':         Cache(),
96                        'section':       Cache(),
97                        'priority':      Cache(),
98                        'override_type': Cache(),
99                        'architecture':  Cache(),
100                        'archive':       Cache(),
101                        'component':     Cache(),
102                        'content_path_names':     Cache(),
103                        'content_file_names':     Cache(),
104                        'location':      Cache(lambda x: '%s_%s_%s' % (x['location'], x['component'], x['location'])),
105                        'maintainer':    {}, # TODO
106                        'keyring':       {}, # TODO
107                        'source':        Cache(lambda x: '%s_%s_' % (x['source'], x['version'])),
108                        'files':         Cache(lambda x: '%s_%s_' % (x['filename'], x['location'])),
109                        'maintainer':    {}, # TODO
110                        'fingerprint':   {}, # TODO
111                        'queue':         {}, # TODO
112                        'uid':           {}, # TODO
113                        'suite_version': Cache(lambda x: '%s_%s' % (x['source'], x['suite'])),
114                       }
115
116         self.prepared_statements = {}
117
118     def prepare(self,name,statement):
119         if not self.prepared_statements.has_key(name):
120             c = self.cursor()
121             c.execute(statement)
122             self.prepared_statements[name] = statement
123
124     def clear_caches(self):
125         self.__init_caches()
126
127     ## Functions to pass through to the database connector
128     def cursor(self):
129         return self.db_con.cursor()
130
131     def commit(self):
132         return self.db_con.commit()
133
134     ## Get functions
135     def __get_single_id(self, query, values, cachename=None):
136         # This is a bit of a hack but it's an internal function only
137         if cachename is not None:
138             res = self.caches[cachename].GetValue(values)
139             if res:
140                 return res
141
142         c = self.db_con.cursor()
143         c.execute(query, values)
144
145         if c.rowcount != 1:
146             return None
147
148         res = c.fetchone()[0]
149
150         if cachename is not None:
151             self.caches[cachename].SetValue(values, res)
152
153         return res
154
155     def __get_id(self, retfield, table, qfield, value):
156         query = "SELECT %s FROM %s WHERE %s = %%(value)s" % (retfield, table, qfield)
157         return self.__get_single_id(query, {'value': value}, cachename=table)
158
159     def get_suite_id(self, suite):
160         """
161         Returns database id for given C{suite}.
162         Results are kept in a cache during runtime to minimize database queries.
163
164         @type suite: string
165         @param suite: The name of the suite
166
167         @rtype: int
168         @return: the database id for the given suite
169
170         """
171         suiteid = self.__get_id('id', 'suite', 'suite_name', suite)
172         if suiteid is None:
173             return None
174         else:
175             return int(suiteid)
176
177     def get_section_id(self, section):
178         """
179         Returns database id for given C{section}.
180         Results are kept in a cache during runtime to minimize database queries.
181
182         @type section: string
183         @param section: The name of the section
184
185         @rtype: int
186         @return: the database id for the given section
187
188         """
189         return self.__get_id('id', 'section', 'section', section)
190
191     def get_priority_id(self, priority):
192         """
193         Returns database id for given C{priority}.
194         Results are kept in a cache during runtime to minimize database queries.
195
196         @type priority: string
197         @param priority: The name of the priority
198
199         @rtype: int
200         @return: the database id for the given priority
201
202         """
203         return self.__get_id('id', 'priority', 'priority', priority)
204
205     def get_override_type_id(self, override_type):
206         """
207         Returns database id for given override C{type}.
208         Results are kept in a cache during runtime to minimize database queries.
209
210         @type override_type: string
211         @param override_type: The name of the override type
212
213         @rtype: int
214         @return: the database id for the given override type
215
216         """
217         return self.__get_id('id', 'override_type', 'type', override_type)
218
219     def get_architecture_id(self, architecture):
220         """
221         Returns database id for given C{architecture}.
222         Results are kept in a cache during runtime to minimize database queries.
223
224         @type architecture: string
225         @param architecture: The name of the override type
226
227         @rtype: int
228         @return: the database id for the given architecture
229
230         """
231         return self.__get_id('id', 'architecture', 'arch_string', architecture)
232
233     def get_archive_id(self, archive):
234         """
235         returns database id for given c{archive}.
236         results are kept in a cache during runtime to minimize database queries.
237
238         @type archive: string
239         @param archive: the name of the override type
240
241         @rtype: int
242         @return: the database id for the given archive
243
244         """
245         return self.__get_id('id', 'archive', 'lower(name)', archive)
246
247     def get_component_id(self, component):
248         """
249         Returns database id for given C{component}.
250         Results are kept in a cache during runtime to minimize database queries.
251
252         @type component: string
253         @param component: The name of the override type
254
255         @rtype: int
256         @return: the database id for the given component
257
258         """
259         return self.__get_id('id', 'component', 'lower(name)', component)
260
261     def get_location_id(self, location, component, archive):
262         """
263         Returns database id for the location behind the given combination of
264           - B{location} - the path of the location, eg. I{/srv/ftp.debian.org/ftp/pool/}
265           - B{component} - the id of the component as returned by L{get_component_id}
266           - B{archive} - the id of the archive as returned by L{get_archive_id}
267         Results are kept in a cache during runtime to minimize database queries.
268
269         @type location: string
270         @param location: the path of the location
271
272         @type component: int
273         @param component: the id of the component
274
275         @type archive: int
276         @param archive: the id of the archive
277
278         @rtype: int
279         @return: the database id for the location
280
281         """
282
283         archive_id = self.get_archive_id(archive)
284
285         if not archive_id:
286             return None
287
288         res = None
289
290         if component:
291             component_id = self.get_component_id(component)
292             if component_id:
293                 res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND component=%(component)s AND archive=%(archive)s",
294                         {'location': location,
295                          'archive': int(archive_id),
296                          'component': component_id}, cachename='location')
297         else:
298             res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND archive=%(archive)d",
299                     {'location': location, 'archive': archive_id, 'component': ''}, cachename='location')
300
301         return res
302
303     def get_source_id(self, source, version):
304         """
305         Returns database id for the combination of C{source} and C{version}
306           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
307           - B{version}
308         Results are kept in a cache during runtime to minimize database queries.
309
310         @type source: string
311         @param source: source package name
312
313         @type version: string
314         @param version: the source version
315
316         @rtype: int
317         @return: the database id for the source
318
319         """
320         return self.__get_single_id("SELECT id FROM source s WHERE s.source=%(source)s AND s.version=%(version)s",
321                                  {'source': source, 'version': version}, cachename='source')
322
323     def get_suite_version(self, source, suite):
324         """
325         Returns database id for a combination of C{source} and C{suite}.
326
327           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
328           - B{suite} - a suite name, eg. I{unstable}
329
330         Results are kept in a cache during runtime to minimize database queries.
331
332         @type source: string
333         @param source: source package name
334
335         @type suite: string
336         @param suite: the suite name
337
338         @rtype: string
339         @return: the version for I{source} in I{suite}
340
341         """
342         return self.__get_single_id("""
343         SELECT s.version FROM source s, suite su, src_associations sa
344         WHERE sa.source=s.id
345           AND sa.suite=su.id
346           AND su.suite_name=%(suite)s
347           AND s.source=%(source)""", {'suite': suite, 'source': source}, cachename='suite_version')
348
349
350     def get_files_id (self, filename, size, md5sum, location_id):
351         """
352         Returns -1, -2 or the file_id for filename, if its C{size} and C{md5sum} match an
353         existing copy.
354
355         The database is queried using the C{filename} and C{location_id}. If a file does exist
356         at that location, the existing size and md5sum are checked against the provided
357         parameters. A size or checksum mismatch returns -2. If more than one entry is
358         found within the database, a -1 is returned, no result returns None, otherwise
359         the file id.
360
361         Results are kept in a cache during runtime to minimize database queries.
362
363         @type filename: string
364         @param filename: the filename of the file to check against the DB
365
366         @type size: int
367         @param size: the size of the file to check against the DB
368
369         @type md5sum: string
370         @param md5sum: the md5sum of the file to check against the DB
371
372         @type location_id: int
373         @param location_id: the id of the location as returned by L{get_location_id}
374
375         @rtype: int / None
376         @return: Various return values are possible:
377                    - -2: size/checksum error
378                    - -1: more than one file found in database
379                    - None: no file found in database
380                    - int: file id
381
382         """
383         values = {'filename' : filename,
384                   'location' : location_id}
385
386         res = self.caches['files'].GetValue( values )
387
388         if not res:
389             query = """SELECT id, size, md5sum
390                        FROM files
391                        WHERE filename = %(filename)s AND location = %(location)s"""
392
393             cursor = self.db_con.cursor()
394             cursor.execute( query, values )
395
396             if cursor.rowcount == 0:
397                 res = None
398
399             elif cursor.rowcount != 1:
400                 res = -1
401
402             else:
403                 row = cursor.fetchone()
404
405                 if row[1] != int(size) or row[2] != md5sum:
406                     res =  -2
407
408                 else:
409                     self.caches['files'].SetValue(values, row[0])
410                     res = row[0]
411
412         return res
413
414
415     def get_or_set_contents_file_id(self, filename):
416         """
417         Returns database id for given filename.
418
419         Results are kept in a cache during runtime to minimize database queries.
420         If no matching file is found, a row is inserted.
421
422         @type filename: string
423         @param filename: The filename
424
425         @rtype: int
426         @return: the database id for the given component
427         """
428         try:
429             values={'value': filename}
430             query = "SELECT id FROM content_file_names WHERE file = %(value)s"
431             id = self.__get_single_id(query, values, cachename='content_file_names')
432             if not id:
433                 c = self.db_con.cursor()
434                 c.execute( "INSERT INTO content_file_names VALUES (DEFAULT, %(value)s) RETURNING id",
435                            values )
436
437                 id = c.fetchone()[0]
438                 self.caches['content_file_names'].SetValue(values, id)
439
440             return id
441         except:
442             traceback.print_exc()
443             raise
444
445     def get_or_set_contents_path_id(self, path):
446         """
447         Returns database id for given path.
448
449         Results are kept in a cache during runtime to minimize database queries.
450         If no matching file is found, a row is inserted.
451
452         @type path: string
453         @param path: The filename
454
455         @rtype: int
456         @return: the database id for the given component
457         """
458         try:
459             values={'value': path}
460             query = "SELECT id FROM content_file_paths WHERE path = %(value)s"
461             id = self.__get_single_id(query, values, cachename='content_path_names')
462             if not id:
463                 c = self.db_con.cursor()
464                 c.execute( "INSERT INTO content_file_paths VALUES (DEFAULT, %(value)s) RETURNING id",
465                            values )
466
467                 id = c.fetchone()[0]
468                 self.caches['content_path_names'].SetValue(values, id)
469
470             return id
471         except:
472             traceback.print_exc()
473             raise
474
475     def get_suite_architectures(self, suite):
476         """
477         Returns list of architectures for C{suite}.
478
479         @type suite: string, int
480         @param suite: the suite name or the suite_id
481
482         @rtype: list
483         @return: the list of architectures for I{suite}
484         """
485
486         suite_id = None
487         if type(suite) == str:
488             suite_id = self.get_suite_id(suite)
489         elif type(suite) == int:
490             suite_id = suite
491         else:
492             return None
493
494         c = self.db_con.cursor()
495         c.execute( """SELECT a.arch_string FROM suite_architectures sa
496                       JOIN architecture a ON (a.id = sa.architecture)
497                       WHERE suite='%s'""" % suite_id )
498
499         return map(lambda x: x[0], c.fetchall())
500
501     def insert_content_paths(self, bin_id, fullpaths):
502         """
503         Make sure given path is associated with given binary id
504
505         @type bin_id: int
506         @param bin_id: the id of the binary
507         @type fullpaths: list
508         @param fullpaths: the list of paths of the file being associated with the binary
509
510         @return: True upon success
511         """
512
513         c = self.db_con.cursor()
514
515         c.execute("BEGIN WORK")
516         try:
517
518             for fullpath in fullpaths:
519                 (path, file) = os.path.split(fullpath)
520
521                 if path.startswith( "./" ):
522                     path = path[2:]
523                 # Get the necessary IDs ...
524                 file_id = self.get_or_set_contents_file_id(file)
525                 path_id = self.get_or_set_contents_path_id(path)
526
527                 c.execute("""INSERT INTO content_associations
528                                (binary_pkg, filepath, filename)
529                            VALUES ( '%d', '%d', '%d')""" % (bin_id, path_id, file_id) )
530
531             c.execute("COMMIT")
532             return True
533         except:
534             traceback.print_exc()
535             c.execute("ROLLBACK")
536             return False
537
538     def insert_pending_content_paths(self, package, fullpaths):
539         """
540         Make sure given paths are temporarily associated with given
541         package
542
543         @type package: dict
544         @param package: the package to associate with should have been read in from the binary control file
545         @type fullpaths: list
546         @param fullpaths: the list of paths of the file being associated with the binary
547
548         @return: True upon success
549         """
550
551         c = self.db_con.cursor()
552
553         c.execute("BEGIN WORK")
554         try:
555             arch_id = self.get_architecture_id(package['Architecture'])
556
557             # Remove any already existing recorded files for this package
558             c.execute("""DELETE FROM pending_content_associations
559                          WHERE package=%(Package)s
560                          AND version=%(Version)s
561                          AND architecture=%(ArchID)s""", {'Package': package['Package'],
562                                                           'Version': package['Version'],
563                                                           'ArchID':  arch_id})
564
565             for fullpath in fullpaths:
566                 (path, file) = os.path.split(fullpath)
567
568                 if path.startswith( "./" ):
569                     path = path[2:]
570                 # Get the necessary IDs ...
571                 file_id = self.get_or_set_contents_file_id(file)
572                 path_id = self.get_or_set_contents_path_id(path)
573
574                 c.execute("""INSERT INTO pending_content_associations
575                                (package, version, architecture, filepath, filename)
576                             VALUES (%%(Package)s, %%(Version)s, '%d', '%d', '%d')"""
577                     % (arch_id, path_id, file_id), package )
578
579             c.execute("COMMIT")
580             return True
581         except:
582             traceback.print_exc()
583             c.execute("ROLLBACK")
584             return False