]> git.decadent.org.uk Git - dak.git/blobdiff - daklib/dbconn.py
Merge branch 'master' of ssh://ftp-master.debian.org/srv/ftp.debian.org/git/dak
[dak.git] / daklib / dbconn.py
index fa52a9187bbc080caeaaa43919033b0435021b41..67852949fe35083dededb021e20418292315fa57 100755 (executable)
@@ -53,9 +53,10 @@ from tempfile import mkstemp, mkdtemp
 from inspect import getargspec
 
 import sqlalchemy
-from sqlalchemy import create_engine, Table, MetaData, Column, Integer
+from sqlalchemy import create_engine, Table, MetaData, Column, Integer, desc, \
+    Text, ForeignKey
 from sqlalchemy.orm import sessionmaker, mapper, relation, object_session, \
-    backref, MapperExtension, EXT_CONTINUE
+    backref, MapperExtension, EXT_CONTINUE, object_mapper
 from sqlalchemy import types as sqltypes
 
 # Don't remove this, we re-export the exceptions to scripts which import us
@@ -272,6 +273,65 @@ class ORMObject(object):
                 raise DBUpdateError(self.validation_message % \
                     (property, str(self)))
 
+    @classmethod
+    @session_wrapper
+    def get(cls, primary_key,  session = None):
+        '''
+        This is a support function that allows getting an object by its primary
+        key.
+
+        Architecture.get(3[, session])
+
+        instead of the more verbose
+
+        session.query(Architecture).get(3)
+        '''
+        return session.query(cls).get(primary_key)
+
+    def session(self, replace = False):
+        '''
+        Returns the current session that is associated with the object. May
+        return None is object is in detached state.
+        '''
+
+        return object_session(self)
+
+    def clone(self, session = None):
+        '''
+        Clones the current object in a new session and returns the new clone. A
+        fresh session is created if the optional session parameter is not
+        provided. The function will fail if a session is provided and has
+        unflushed changes.
+
+        RATIONALE: SQLAlchemy's session is not thread safe. This method clones
+        an existing object to allow several threads to work with their own
+        instances of an ORMObject.
+
+        WARNING: Only persistent (committed) objects can be cloned. Changes
+        made to the original object that are not committed yet will get lost.
+        The session of the new object will always be rolled back to avoid
+        ressource leaks.
+        '''
+
+        if self.session() is None:
+            raise RuntimeError( \
+                'Method clone() failed for detached object:\n%s' % self)
+        self.session().flush()
+        mapper = object_mapper(self)
+        primary_key = mapper.primary_key_from_instance(self)
+        object_class = self.__class__
+        if session is None:
+            session = DBConn().session()
+        elif len(session.new) + len(session.dirty) + len(session.deleted) > 0:
+            raise RuntimeError( \
+                'Method clone() failed due to unflushed changes in session.')
+        new_object = session.query(object_class).get(primary_key)
+        session.rollback()
+        if new_object is None:
+            raise RuntimeError( \
+                'Method clone() failed for non-persistent object:\n%s' % self)
+        return new_object
+
 __all__.append('ORMObject')
 
 ################################################################################
@@ -406,34 +466,37 @@ __all__.append('get_archive')
 
 ################################################################################
 
-class BinAssociation(object):
-    def __init__(self, *args, **kwargs):
-        pass
-
-    def __repr__(self):
-        return '<BinAssociation %s (%s, %s)>' % (self.ba_id, self.binary, self.suite)
+class BinContents(ORMObject):
+    def properties(silf):
+        return ['file', 'binary']
 
-__all__.append('BinAssociation')
+__all__.append('BinContents')
 
 ################################################################################
 
-class BinContents(object):
-    def __init__(self, *args, **kwargs):
-        pass
-
-    def __repr__(self):
-        return '<BinContents (%s, %s)>' % (self.binary, self.filename)
-
-__all__.append('BinContents')
+class DBBinary(ORMObject):
+    def __init__(self, package = None, source = None, version = None, \
+        maintainer = None, architecture = None, poolfile = None, \
+        binarytype = 'deb'):
+        self.package = package
+        self.source = source
+        self.version = version
+        self.maintainer = maintainer
+        self.architecture = architecture
+        self.poolfile = poolfile
+        self.binarytype = binarytype
 
-################################################################################
+    def properties(self):
+        return ['package', 'version', 'maintainer', 'source', 'architecture', \
+            'poolfile', 'binarytype', 'fingerprint', 'install_date', \
+            'suites_count', 'binary_id', 'contents_count']
 
-class DBBinary(object):
-    def __init__(self, *args, **kwargs):
-        pass
+    def not_null_constraints(self):
+        return ['package', 'version', 'maintainer', 'source',  'poolfile', \
+            'binarytype']
 
-    def __repr__(self):
-        return '<DBBinary %s (%s, %s)>' % (self.package, self.version, self.architecture)
+    def get_component_name(self):
+        return self.poolfile.location.component.component_name
 
 __all__.append('DBBinary')
 
@@ -449,129 +512,42 @@ def get_suites_binary_in(package, session=None):
     @return: list of Suite objects for the given package
     """
 
-    return session.query(Suite).join(BinAssociation).join(DBBinary).filter_by(package=package).all()
+    return session.query(Suite).filter(Suite.binaries.any(DBBinary.package == package)).all()
 
 __all__.append('get_suites_binary_in')
 
 @session_wrapper
-def get_binary_from_id(binary_id, session=None):
-    """
-    Returns DBBinary object for given C{id}
-
-    @type binary_id: int
-    @param binary_id: Id of the required binary
-
-    @type session: Session
-    @param session: Optional SQLA session object (a temporary one will be
-    generated if not supplied)
-
-    @rtype: DBBinary
-    @return: DBBinary object for the given binary (None if not present)
-    """
-
-    q = session.query(DBBinary).filter_by(binary_id=binary_id)
-
-    try:
-        return q.one()
-    except NoResultFound:
-        return None
-
-__all__.append('get_binary_from_id')
-
-@session_wrapper
-def get_binaries_from_name(package, version=None, architecture=None, session=None):
-    """
-    Returns list of DBBinary objects for given C{package} name
+def get_component_by_package_suite(package, suite_list, arch_list=[], session=None):
+    '''
+    Returns the component name of the newest binary package in suite_list or
+    None if no package is found. The result can be optionally filtered by a list
+    of architecture names.
 
     @type package: str
     @param package: DBBinary package name to search for
 
-    @type version: str or None
-    @param version: Version to search for (or None)
-
-    @type architecture: str, list or None
-    @param architecture: Architectures to limit to (or None if no limit)
-
-    @type session: Session
-    @param session: Optional SQL session object (a temporary one will be
-    generated if not supplied)
+    @type suite_list: list of str
+    @param suite_list: list of suite_name items
 
-    @rtype: list
-    @return: list of DBBinary objects for the given name (may be empty)
-    """
+    @type arch_list: list of str
+    @param arch_list: optional list of arch_string items that defaults to []
 
-    q = session.query(DBBinary).filter_by(package=package)
-
-    if version is not None:
-        q = q.filter_by(version=version)
-
-    if architecture is not None:
-        if not isinstance(architecture, list):
-            architecture = [architecture]
-        q = q.join(Architecture).filter(Architecture.arch_string.in_(architecture))
-
-    ret = q.all()
-
-    return ret
-
-__all__.append('get_binaries_from_name')
-
-@session_wrapper
-def get_binaries_from_source_id(source_id, session=None):
-    """
-    Returns list of DBBinary objects for given C{source_id}
-
-    @type source_id: int
-    @param source_id: source_id to search for
-
-    @type session: Session
-    @param session: Optional SQL session object (a temporary one will be
-    generated if not supplied)
-
-    @rtype: list
-    @return: list of DBBinary objects for the given name (may be empty)
-    """
-
-    return session.query(DBBinary).filter_by(source_id=source_id).all()
-
-__all__.append('get_binaries_from_source_id')
-
-@session_wrapper
-def get_binary_from_name_suite(package, suitename, session=None):
-    ### For dak examine-package
-    ### XXX: Doesn't use object API yet
-
-    sql = """SELECT DISTINCT(b.package), b.version, c.name, su.suite_name
-             FROM binaries b, files fi, location l, component c, bin_associations ba, suite su
-             WHERE b.package='%(package)s'
-               AND b.file = fi.id
-               AND fi.location = l.id
-               AND l.component = c.id
-               AND ba.bin=b.id
-               AND ba.suite = su.id
-               AND su.suite_name %(suitename)s
-          ORDER BY b.version DESC"""
-
-    return session.execute(sql % {'package': package, 'suitename': suitename})
-
-__all__.append('get_binary_from_name_suite')
-
-@session_wrapper
-def get_binary_components(package, suitename, arch, session=None):
-    # Check for packages that have moved from one component to another
-    query = """SELECT c.name FROM binaries b, bin_associations ba, suite s, location l, component c, architecture a, files f
-    WHERE b.package=:package AND s.suite_name=:suitename
-      AND (a.arch_string = :arch OR a.arch_string = 'all')
-      AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id
-      AND f.location = l.id
-      AND l.component = c.id
-      AND b.file = f.id"""
-
-    vals = {'package': package, 'suitename': suitename, 'arch': arch}
+    @rtype: str or NoneType
+    @return: name of component or None
+    '''
 
-    return session.execute(query, vals)
+    q = session.query(DBBinary).filter_by(package = package). \
+        join(DBBinary.suites).filter(Suite.suite_name.in_(suite_list))
+    if len(arch_list) > 0:
+        q = q.join(DBBinary.architecture). \
+            filter(Architecture.arch_string.in_(arch_list))
+    binary = q.order_by(desc(DBBinary.version)).first()
+    if binary is None:
+        return None
+    else:
+        return binary.get_component_name()
 
-__all__.append('get_binary_components')
+__all__.append('get_component_by_package_suite')
 
 ################################################################################
 
@@ -914,9 +890,9 @@ __all__.append('ChangePendingSource')
 
 ################################################################################
 
-class Component(object):
-    def __init__(self, *args, **kwargs):
-        pass
+class Component(ORMObject):
+    def __init__(self, component_name = None):
+        self.component_name = component_name
 
     def __eq__(self, val):
         if isinstance(val, str):
@@ -930,8 +906,12 @@ class Component(object):
         # This signals to use the normal comparison operator
         return NotImplemented
 
-    def __repr__(self):
-        return '<Component %s>' % self.component_name
+    def properties(self):
+        return ['component_name', 'component_id', 'description', 'location', \
+            'meets_dfsg']
+
+    def not_null_constraints(self):
+        return ['component_name']
 
 
 __all__.append('Component')
@@ -1223,12 +1203,12 @@ class PoolFile(ORMObject):
     def fullpath(self):
         return os.path.join(self.location.path, self.filename)
 
-    def is_valid(self, filesize = -1, md5sum = None):\
-        return self.filesize == filesize and self.md5sum == md5sum
+    def is_valid(self, filesize = -1, md5sum = None):
+        return self.filesize == long(filesize) and self.md5sum == md5sum
 
     def properties(self):
         return ['filename', 'file_id', 'filesize', 'md5sum', 'sha1sum', \
-            'sha256sum', 'location', 'source', 'last_used']
+            'sha256sum', 'location', 'source', 'binary', 'last_used']
 
     def not_null_constraints(self):
         return ['filename', 'md5sum', 'location']
@@ -1638,13 +1618,15 @@ __all__.append('get_dbchange')
 ################################################################################
 
 class Location(ORMObject):
-    def __init__(self, path = None):
+    def __init__(self, path = None, component = None):
         self.path = path
+        self.component = component
         # the column 'type' should go away, see comment at mapper
         self.archive_type = 'pool'
 
     def properties(self):
-        return ['path', 'archive_type', 'component', 'files_count']
+        return ['path', 'location_id', 'archive_type', 'component', \
+            'files_count']
 
     def not_null_constraints(self):
         return ['path', 'archive_type']
@@ -2242,7 +2224,7 @@ class DBSource(ORMObject):
     def properties(self):
         return ['source', 'source_id', 'maintainer', 'changedby', \
             'fingerprint', 'poolfile', 'version', 'suites_count', \
-            'install_date']
+            'install_date', 'binaries_count']
 
     def not_null_constraints(self):
         return ['source', 'version', 'install_date', 'maintainer', \
@@ -2543,14 +2525,10 @@ def add_deb_to_db(u, filename, session=None):
 
     # Add and flush object so it has an ID
     session.add(bin)
-    session.flush()
 
-    # Add BinAssociations
-    for suite_name in u.pkg.changes["distribution"].keys():
-        ba = BinAssociation()
-        ba.binary_id = bin.binary_id
-        ba.suite_id = get_suite(suite_name).suite_id
-        session.add(ba)
+    suite_names = u.pkg.changes["distribution"].keys()
+    bin.suites = session.query(Suite). \
+        filter(Suite.suite_name.in_(suite_names)).all()
 
     session.flush()
 
@@ -2624,7 +2602,7 @@ class Suite(ORMObject):
         self.version = version
 
     def properties(self):
-        return ['suite_name', 'version']
+        return ['suite_name', 'version', 'sources_count', 'binaries_count']
 
     def not_null_constraints(self):
         return ['suite_name', 'version']
@@ -2787,7 +2765,7 @@ __all__.append('get_suite_src_formats')
 
 ################################################################################
 
-class Uid(object):
+class Uid(ORMObject):
     def __init__(self, uid = None, name = None):
         self.uid = uid
         self.name = name
@@ -2804,8 +2782,11 @@ class Uid(object):
         # This signals to use the normal comparison operator
         return NotImplemented
 
-    def __repr__(self):
-        return '<Uid %s (%s)>' % (self.uid, self.name)
+    def properties(self):
+        return ['uid', 'name', 'fingerprint']
+
+    def not_null_constraints(self):
+        return ['uid']
 
 __all__.append('Uid')
 
@@ -2921,12 +2902,11 @@ class DBConn(object):
             # The following tables have primary keys but sqlalchemy
             # version 0.5 fails to reflect them correctly with database
             # versions before upgrade #41.
-            #'changes',
-            #'build_queue_files',
+            'changes',
+            'build_queue_files',
         )
 
         tables_no_primary = (
-            'bin_contents',
             'changes_pending_files_map',
             'changes_pending_source_files',
             'changes_pool_files',
@@ -2937,8 +2917,8 @@ class DBConn(object):
             'suite_build_queue_copy',
             'udeb_contents',
             # see the comment above
-            'changes',
-            'build_queue_files',
+            #'changes',
+            #'build_queue_files',
         )
 
         views = (
@@ -2978,6 +2958,14 @@ class DBConn(object):
             table = Table(table_name, self.db_meta, autoload=True)
             setattr(self, 'tbl_%s' % table_name, table)
 
+        # bin_contents needs special attention until update #41 has been
+        # applied
+        self.tbl_bin_contents = Table('bin_contents', self.db_meta, \
+            Column('file', Text, primary_key = True),
+            Column('binary_id', Integer, ForeignKey('binaries.id'), \
+                primary_key = True),
+            autoload=True, useexisting=True)
+
         for view_name in views:
             view = Table(view_name, self.db_meta, autoload=True)
             setattr(self, 'view_%s' % view_name, view)
@@ -2994,13 +2982,6 @@ class DBConn(object):
                properties = dict(archive_id = self.tbl_archive.c.id,
                                  archive_name = self.tbl_archive.c.name))
 
-        mapper(BinAssociation, self.tbl_bin_associations,
-               properties = dict(ba_id = self.tbl_bin_associations.c.id,
-                                 suite_id = self.tbl_bin_associations.c.suite,
-                                 suite = relation(Suite),
-                                 binary_id = self.tbl_bin_associations.c.bin,
-                                 binary = relation(DBBinary)))
-
         mapper(PendingBinContents, self.tbl_pending_bin_contents,
                properties = dict(contents_id =self.tbl_pending_bin_contents.c.id,
                                  filename = self.tbl_pending_bin_contents.c.filename,
@@ -3039,17 +3020,18 @@ class DBConn(object):
                                  maintainer_id = self.tbl_binaries.c.maintainer,
                                  maintainer = relation(Maintainer),
                                  source_id = self.tbl_binaries.c.source,
-                                 source = relation(DBSource),
+                                 source = relation(DBSource, backref='binaries'),
                                  arch_id = self.tbl_binaries.c.architecture,
                                  architecture = relation(Architecture),
                                  poolfile_id = self.tbl_binaries.c.file,
-                                 poolfile = relation(PoolFile),
+                                 poolfile = relation(PoolFile, backref=backref('binary', uselist = False)),
                                  binarytype = self.tbl_binaries.c.type,
                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
                                  fingerprint = relation(Fingerprint),
                                  install_date = self.tbl_binaries.c.install_date,
-                                 binassociations = relation(BinAssociation,
-                                                            primaryjoin=(self.tbl_binaries.c.id==self.tbl_bin_associations.c.bin))))
+                                 suites = relation(Suite, secondary=self.tbl_bin_associations,
+                                     backref=backref('binaries', lazy='dynamic'))),
+                extension = validator)
 
         mapper(BinaryACL, self.tbl_binary_acl,
                properties = dict(binary_acl_id = self.tbl_binary_acl.c.id))
@@ -3061,7 +3043,8 @@ class DBConn(object):
 
         mapper(Component, self.tbl_component,
                properties = dict(component_id = self.tbl_component.c.id,
-                                 component_name = self.tbl_component.c.name))
+                                 component_name = self.tbl_component.c.name),
+               extension = validator)
 
         mapper(DBConfig, self.tbl_config,
                properties = dict(config_id = self.tbl_config.c.id))
@@ -3153,7 +3136,8 @@ class DBConn(object):
         mapper(Location, self.tbl_location,
                properties = dict(location_id = self.tbl_location.c.id,
                                  component_id = self.tbl_location.c.component,
-                                 component = relation(Component),
+                                 component = relation(Component, \
+                                     backref=backref('location', uselist = False)),
                                  archive_id = self.tbl_location.c.archive,
                                  archive = relation(Archive),
                                  # FIXME: the 'type' column is old cruft and
@@ -3211,7 +3195,7 @@ class DBConn(object):
                                  srcfiles = relation(DSCFile,
                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
                                  suites = relation(Suite, secondary=self.tbl_src_associations,
-                                     backref='sources'),
+                                     backref=backref('sources', lazy='dynamic')),
                                  srcuploaders = relation(SrcUploader)),
                extension = validator)
 
@@ -3246,13 +3230,20 @@ class DBConn(object):
 
         mapper(Uid, self.tbl_uid,
                properties = dict(uid_id = self.tbl_uid.c.id,
-                                 fingerprint = relation(Fingerprint)))
+                                 fingerprint = relation(Fingerprint)),
+               extension = validator)
 
         mapper(UploadBlock, self.tbl_upload_blocks,
                properties = dict(upload_block_id = self.tbl_upload_blocks.c.id,
                                  fingerprint = relation(Fingerprint, backref="uploadblocks"),
                                  uid = relation(Uid, backref="uploadblocks")))
 
+        mapper(BinContents, self.tbl_bin_contents,
+            properties = dict(
+                binary = relation(DBBinary,
+                    backref=backref('contents', lazy='dynamic')),
+                file = self.tbl_bin_contents.c.file))
+
     ## Connection functions
     def __createconn(self):
         from config import Config