# Copyright 2014 OpenStack Foundation
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import re
import time
import logging
import threading

import alembic
import alembic.config
import six
import sqlalchemy
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, DateTime, Text, UniqueConstraint
from sqlalchemy.schema import ForeignKey
from sqlalchemy.orm import mapper, sessionmaker, relationship, scoped_session, joinedload
from sqlalchemy.orm.session import Session
from sqlalchemy.sql import exists
from sqlalchemy.sql.expression import and_

metadata = MetaData()
project_table = Table(
    'project', metadata,
    Column('key', Integer, primary_key=True),
    Column('name', String(255), index=True, unique=True, nullable=False),
    Column('subscribed', Boolean, index=True, default=False),
    Column('description', Text, nullable=False, default=''),
    Column('updated', DateTime, index=True),
    )
branch_table = Table(
    'branch', metadata,
    Column('key', Integer, primary_key=True),
    Column('project_key', Integer, ForeignKey("project.key"), index=True),
    Column('name', String(255), index=True, nullable=False),
    )
topic_table = Table(
    'topic', metadata,
    Column('key', Integer, primary_key=True),
    Column('name', String(255), index=True, nullable=False),
    Column('sequence', Integer, index=True, unique=True, nullable=False),
    )
project_topic_table = Table(
    'project_topic', metadata,
    Column('key', Integer, primary_key=True),
    Column('project_key', Integer, ForeignKey("project.key"), index=True),
    Column('topic_key', Integer, ForeignKey("topic.key"), index=True),
    Column('sequence', Integer, nullable=False),
    UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const'),
    )
change_table = Table(
    'change', metadata,
    Column('key', Integer, primary_key=True),
    Column('project_key', Integer, ForeignKey("project.key"), index=True),
    Column('id', String(255), index=True, unique=True, nullable=False),
    Column('number', Integer, index=True, unique=True, nullable=False),
    Column('branch', String(255), index=True, nullable=False),
    Column('change_id', String(255), index=True, nullable=False),
    Column('topic', String(255), index=True),
    Column('account_key', Integer, ForeignKey("account.key"), index=True),
    Column('subject', Text, nullable=False),
    Column('created', DateTime, index=True, nullable=False),
    Column('updated', DateTime, index=True, nullable=False),
    Column('status', String(16), index=True, nullable=False),
    Column('hidden', Boolean, index=True, nullable=False),
    Column('reviewed', Boolean, index=True, nullable=False),
    Column('starred', Boolean, index=True, nullable=False),
    Column('held', Boolean, index=True, nullable=False),
    Column('pending_rebase', Boolean, index=True, nullable=False),
    Column('pending_topic', Boolean, index=True, nullable=False),
    Column('pending_starred', Boolean, index=True, nullable=False),
    Column('pending_status', Boolean, index=True, nullable=False),
    Column('pending_status_message', Text),
    Column('last_seen', DateTime, index=True),
    Column('outdated', Boolean, index=True, nullable=False),
    )
change_conflict_table = Table(
    'change_conflict', metadata,
    Column('key', Integer, primary_key=True),
    Column('change1_key', Integer, ForeignKey("change.key"), index=True),
    Column('change2_key', Integer, ForeignKey("change.key"), index=True),
    )
revision_table = Table(
    'revision', metadata,
    Column('key', Integer, primary_key=True),
    Column('change_key', Integer, ForeignKey("change.key"), index=True),
    Column('number', Integer, index=True, nullable=False),
    Column('message', Text, nullable=False),
    Column('commit', String(255), index=True, nullable=False),
    Column('parent', String(255), index=True, nullable=False),
    # TODO: fetch_ref, fetch_auth are unused; remove
    Column('fetch_auth', Boolean, nullable=False),
    Column('fetch_ref', String(255), nullable=False),
    Column('pending_message', Boolean, index=True, nullable=False),
    Column('can_submit', Boolean, nullable=False),
    )
message_table = Table(
    'message', metadata,
    Column('key', Integer, primary_key=True),
    Column('revision_key', Integer, ForeignKey("revision.key"), index=True),
    Column('account_key', Integer, ForeignKey("account.key"), index=True),
    Column('id', String(255), index=True), #, unique=True, nullable=False),
    Column('created', DateTime, index=True, nullable=False),
    Column('message', Text, nullable=False),
    Column('draft', Boolean, index=True, nullable=False),
    Column('pending', Boolean, index=True, nullable=False),
    )
comment_table = Table(
    'comment', metadata,
    Column('key', Integer, primary_key=True),
    Column('file_key', Integer, ForeignKey("file.key"), index=True),
    Column('account_key', Integer, ForeignKey("account.key"), index=True),
    Column('id', String(255), index=True), #, unique=True, nullable=False),
    Column('in_reply_to', String(255)),
    Column('created', DateTime, index=True, nullable=False),
    Column('parent', Boolean, nullable=False),
    Column('line', Integer),
    Column('message', Text, nullable=False),
    Column('draft', Boolean, index=True, nullable=False),
    )
label_table = Table(
    'label', metadata,
    Column('key', Integer, primary_key=True),
    Column('change_key', Integer, ForeignKey("change.key"), index=True),
    Column('category', String(255), nullable=False),
    Column('value', Integer, nullable=False),
    Column('description', String(255), nullable=False),
    )
permitted_label_table = Table(
    'permitted_label', metadata,
    Column('key', Integer, primary_key=True),
    Column('change_key', Integer, ForeignKey("change.key"), index=True),
    Column('category', String(255), nullable=False),
    Column('value', Integer, nullable=False),
    )
approval_table = Table(
    'approval', metadata,
    Column('key', Integer, primary_key=True),
    Column('change_key', Integer, ForeignKey("change.key"), index=True),
    Column('account_key', Integer, ForeignKey("account.key"), index=True),
    Column('category', String(255), nullable=False),
    Column('value', Integer, nullable=False),
    Column('draft', Boolean, index=True, nullable=False),
    )
account_table = Table(
    'account', metadata,
    Column('key', Integer, primary_key=True),
    Column('id', Integer, index=True, unique=True, nullable=False),
    Column('name', String(255), index=True),
    Column('username', String(255), index=True),
    Column('email', String(255), index=True),
    )
pending_cherry_pick_table = Table(
    'pending_cherry_pick', metadata,
    Column('key', Integer, primary_key=True),
    Column('revision_key', Integer, ForeignKey("revision.key"), index=True),
    # Branch is a str here to avoid FK complications if the branch
    # entry is removed.
    Column('branch', String(255), nullable=False),
    Column('message', Text, nullable=False),
    )
sync_query_table = Table(
    'sync_query', metadata,
    Column('key', Integer, primary_key=True),
    Column('name', String(255), index=True, unique=True, nullable=False),
    Column('updated', DateTime, index=True),
    )
file_table = Table(
    'file', metadata,
    Column('key', Integer, primary_key=True),
    Column('revision_key', Integer, ForeignKey("revision.key"), index=True),
    Column('path', Text, nullable=False, index=True),
    Column('old_path', Text, index=True),
    Column('inserted', Integer),
    Column('deleted', Integer),
    Column('status', String(1), nullable=False),
    )


class Account(object):
    def __init__(self, id, name=None, username=None, email=None):
        self.id = id
        self.name = name
        self.username = username
        self.email = email

class Project(object):
    def __init__(self, name, subscribed=False, description=''):
        self.name = name
        self.subscribed = subscribed
        self.description = description

    def delete(self):
        # This unusual delete method is to accomodate the
        # self-referential many-to-many relationship from
        # change_conflict.  With the default cascade configuration,
        # the entry from the association table is deleted regardless
        # of whether the change being deleted is change1 or change2.
        # However, when both changes are deleted at once (only likely
        # to happen when the entire project is deleted), SQLAlchemy
        # cascades through both relationships and attempts to delete
        # the entry twice.  Since we rarely delete projects, we add a
        # special case here to delete a project's changes one at a
        # time to avoid this situation.
        session = Session.object_session(self)
        for c in self.changes:
            session.delete(c)
            session.flush()
            session.expire_all()
        session.delete(self)

    def createChange(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        c = Change(*args, **kw)
        self.changes.append(c)
        session.add(c)
        session.flush()
        return c

    def createBranch(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        b = Branch(*args, **kw)
        self.branches.append(b)
        session.add(b)
        session.flush()
        return b

class Branch(object):
    def __init__(self, project, name):
        self.project_key = project.key
        self.name = name

class ProjectTopic(object):
    def __init__(self, project, topic, sequence):
        self.project_key = project.key
        self.topic_key = topic.key
        self.sequence = sequence

class Topic(object):
    def __init__(self, name, sequence):
        self.name = name
        self.sequence = sequence

    def addProject(self, project):
        session = Session.object_session(self)
        seq = max([x.sequence for x in self.project_topics] + [0])
        pt = ProjectTopic(project, self, seq+1)
        self.project_topics.append(pt)
        self.projects.append(project)
        session.add(pt)
        session.flush()

    def removeProject(self, project):
        session = Session.object_session(self)
        for pt in self.project_topics:
            if pt.project_key == project.key:
                self.project_topics.remove(pt)
                session.delete(pt)
        self.projects.remove(project)
        session.flush()

class Change(object):
    def __init__(self, project, id, owner, number, branch, change_id,
                 subject, created, updated, status, topic=None,
                 hidden=False, reviewed=False, starred=False, held=False,
                 pending_rebase=False, pending_topic=False,
                 pending_starred=False, pending_status=False,
                 pending_status_message=None, outdated=False):
        self.project_key = project.key
        self.account_key = owner.key
        self.id = id
        self.number = number
        self.branch = branch
        self.change_id = change_id
        self.topic = topic
        self.subject = subject
        self.created = created
        self.updated = updated
        self.status = status
        self.hidden = hidden
        self.reviewed = reviewed
        self.starred = starred
        self.held = held
        self.pending_rebase = pending_rebase
        self.pending_topic = pending_topic
        self.pending_starred = pending_starred
        self.pending_status = pending_status
        self.pending_status_message = pending_status_message
        self.outdated = outdated

    def getCategories(self):
        categories = set([label.category for label in self.labels])
        return sorted(categories)

    def getMaxForCategory(self, category):
        if not hasattr(self, '_approval_cache'):
            self._updateApprovalCache()
        return self._approval_cache.get(category, 0)

    def _updateApprovalCache(self):
        cat_min = {}
        cat_max = {}
        cat_value = {}
        for approval in self.approvals:
            if approval.draft:
                continue
            cur_min = cat_min.get(approval.category, 0)
            cur_max = cat_max.get(approval.category, 0)
            cur_min = min(approval.value, cur_min)
            cur_max = max(approval.value, cur_max)
            cat_min[approval.category] = cur_min
            cat_max[approval.category] = cur_max
            cur_value = cat_value.get(approval.category, 0)
            if abs(cur_min) > abs(cur_value):
                cur_value = cur_min
            if abs(cur_max) > abs(cur_value):
                cur_value = cur_max
            cat_value[approval.category] = cur_value
        self._approval_cache = cat_value

    def getMinMaxPermittedForCategory(self, category):
        if not hasattr(self, '_permitted_cache'):
            self._updatePermittedCache()
        return self._permitted_cache.get(category, (0,0))

    def _updatePermittedCache(self):
        cache = {}
        for label in self.labels:
            if label.category not in cache:
                cache[label.category] = [0, 0]
            if label.value > cache[label.category][1]:
                cache[label.category][1] = label.value
            if label.value < cache[label.category][0]:
                cache[label.category][0] = label.value
        self._permitted_cache = cache

    def createRevision(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        r = Revision(*args, **kw)
        self.revisions.append(r)
        session.add(r)
        session.flush()
        return r

    def createLabel(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        l = Label(*args, **kw)
        self.labels.append(l)
        session.add(l)
        session.flush()
        return l

    def createApproval(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        l = Approval(*args, **kw)
        self.approvals.append(l)
        session.add(l)
        session.flush()
        return l

    def createPermittedLabel(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        l = PermittedLabel(*args, **kw)
        self.permitted_labels.append(l)
        session.add(l)
        session.flush()
        return l

    @property
    def owner_name(self):
        owner_name = 'Anonymous Coward'
        if self.owner:
            if self.owner.name:
                owner_name = self.owner.name
            elif self.owner.username:
                owner_name = self.owner.username
            elif self.owner.email:
                owner_name = self.owner.email
        return owner_name

    @property
    def conflicts(self):
        return tuple(set(self.conflicts1 + self.conflicts2))

    def addConflict(self, other):
        session = Session.object_session(self)
        if other in self.conflicts1 or other in self.conflicts2:
            return
        if self in other.conflicts1 or self in other.conflicts2:
            return
        self.conflicts1.append(other)
        session.flush()
        session.expire(other, attribute_names=['conflicts2'])

    def delConflict(self, other):
        session = Session.object_session(self)
        if other in self.conflicts1:
            self.conflicts1.remove(other)
            session.flush()
            session.expire(other, attribute_names=['conflicts2'])
        if self in other.conflicts1:
            other.conflicts1.remove(self)
            session.flush()
            session.expire(self, attribute_names=['conflicts2'])

class Revision(object):
    def __init__(self, change, number, message, commit, parent,
                 fetch_auth, fetch_ref, pending_message=False,
                 can_submit=False):
        self.change_key = change.key
        self.number = number
        self.message = message
        self.commit = commit
        self.parent = parent
        self.fetch_auth = fetch_auth
        self.fetch_ref = fetch_ref
        self.pending_message = pending_message
        self.can_submit = can_submit

    def createMessage(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        m = Message(*args, **kw)
        self.messages.append(m)
        session.add(m)
        session.flush()
        return m

    def createPendingCherryPick(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        c = PendingCherryPick(*args, **kw)
        self.pending_cherry_picks.append(c)
        session.add(c)
        session.flush()
        return c

    def createFile(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        f = File(*args, **kw)
        self.files.append(f)
        session.add(f)
        session.flush()
        if hasattr(self, '_file_cache'):
            self._file_cache[f.path] = f
        return f

    def getFile(self, path):
        if not hasattr(self, '_file_cache'):
            self._file_cache = {}
            for f in self.files:
                self._file_cache[f.path] = f
        return self._file_cache.get(path, None)

    def getPendingMessage(self):
        for m in self.messages:
            if m.pending:
                return m
        return None

    def getDraftMessage(self):
        for m in self.messages:
            if m.draft:
                return m
        return None


class Message(object):
    def __init__(self, revision, id, author, created, message, draft=False, pending=False):
        self.revision_key = revision.key
        self.account_key = author.key
        self.id = id
        self.created = created
        self.message = message
        self.draft = draft
        self.pending = pending

    @property
    def author_name(self):
        author_name = 'Anonymous Coward'
        if self.author:
            if self.author.name:
                author_name = self.author.name
            elif self.author.username:
                author_name = self.author.username
            elif self.author.email:
                author_name = self.author.email
        return author_name

class Comment(object):
    def __init__(self, file, id, author, in_reply_to, created, parent, line, message, draft=False):
        self.file_key = file.key
        self.account_key = author.key
        self.id = id
        self.in_reply_to = in_reply_to
        self.created = created
        self.parent = parent
        self.line = line
        self.message = message
        self.draft = draft

class Label(object):
    def __init__(self, change, category, value, description):
        self.change_key = change.key
        self.category = category
        self.value = value
        self.description = description

class PermittedLabel(object):
    def __init__(self, change, category, value):
        self.change_key = change.key
        self.category = category
        self.value = value

class Approval(object):
    def __init__(self, change, reviewer, category, value, draft=False):
        self.change_key = change.key
        self.account_key = reviewer.key
        self.category = category
        self.value = value
        self.draft = draft

class PendingCherryPick(object):
    def __init__(self, revision, branch, message):
        self.revision_key = revision.key
        self.branch = branch
        self.message = message

class SyncQuery(object):
    def __init__(self, name):
        self.name = name

class File(object):
    STATUS_ADDED = 'A'
    STATUS_DELETED = 'D'
    STATUS_RENAMED = 'R'
    STATUS_COPIED = 'C'
    STATUS_REWRITTEN = 'W'
    STATUS_MODIFIED = 'M'

    def __init__(self, revision, path, status, old_path=None,
                 inserted=None, deleted=None):
        self.revision_key = revision.key
        self.path = path
        self.status = status
        self.old_path = old_path
        self.inserted = inserted
        self.deleted = deleted

    @property
    def display_path(self):
        if not self.old_path:
            return self.path
        pre = []
        post = []
        for start in range(min(len(self.old_path), len(self.path))):
            if self.path[start] == self.old_path[start]:
                pre.append(self.old_path[start])
            else:
                break
        pre = ''.join(pre)
        for end in range(1, min(len(self.old_path), len(self.path))-1):
            if self.path[0-end] == self.old_path[0-end]:
                post.insert(0, self.old_path[0-end])
            else:
                break
        post = ''.join(post)
        mid = '{%s => %s}' % (self.old_path[start:0-end+1], self.path[start:0-end+1])
        if pre and post:
            mid = '{%s => %s}' % (self.old_path[start:0-end+1],
                                  self.path[start:0-end+1])
            return pre + mid + post
        else:
            return '%s => %s' % (self.old_path, self.path)

    def createComment(self, *args, **kw):
        session = Session.object_session(self)
        args = [self] + list(args)
        c = Comment(*args, **kw)
        self.comments.append(c)
        session.add(c)
        session.flush()
        return c


mapper(Account, account_table)
mapper(Project, project_table, properties=dict(
    branches=relationship(Branch, backref='project',
                          order_by=branch_table.c.name,
                          cascade='all, delete-orphan'),
    changes=relationship(Change, backref='project',
                         order_by=change_table.c.number,
                         cascade='all, delete-orphan'),
    topics=relationship(Topic,
                        secondary=project_topic_table,
                        order_by=topic_table.c.name,
                        viewonly=True),
    unreviewed_changes=relationship(Change,
                                    primaryjoin=and_(project_table.c.key==change_table.c.project_key,
                                                     change_table.c.hidden==False,
                                                     change_table.c.status!='MERGED',
                                                     change_table.c.status!='ABANDONED',
                                                     change_table.c.reviewed==False),
                                    order_by=change_table.c.number,
                                ),
    open_changes=relationship(Change,
                              primaryjoin=and_(project_table.c.key==change_table.c.project_key,
                                               change_table.c.status!='MERGED',
                                               change_table.c.status!='ABANDONED'),
                              order_by=change_table.c.number,
                          ),
))
mapper(Branch, branch_table)
mapper(Topic, topic_table, properties=dict(
    projects=relationship(Project,
                          secondary=project_topic_table,
                          order_by=project_table.c.name,
                          viewonly=True),
    project_topics=relationship(ProjectTopic),
))
mapper(ProjectTopic, project_topic_table)
mapper(Change, change_table, properties=dict(
        owner=relationship(Account),
        conflicts1=relationship(Change,
                                secondary=change_conflict_table,
                                primaryjoin=change_table.c.key==change_conflict_table.c.change1_key,
                                secondaryjoin=change_table.c.key==change_conflict_table.c.change2_key,
                                ),
        conflicts2=relationship(Change,
                                secondary=change_conflict_table,
                                primaryjoin=change_table.c.key==change_conflict_table.c.change2_key,
                                secondaryjoin=change_table.c.key==change_conflict_table.c.change1_key,
                                ),
        revisions=relationship(Revision, backref='change',
                               order_by=revision_table.c.number,
                               cascade='all, delete-orphan'),
        messages=relationship(Message,
                              secondary=revision_table,
                              order_by=message_table.c.created,
                              viewonly=True),
        labels=relationship(Label, backref='change',
                            order_by=(label_table.c.category, label_table.c.value),
                            cascade='all, delete-orphan'),
        permitted_labels=relationship(PermittedLabel, backref='change',
                                      order_by=(permitted_label_table.c.category,
                                                permitted_label_table.c.value),
                                      cascade='all, delete-orphan'),
        approvals=relationship(Approval, backref='change',
                               order_by=(approval_table.c.category,
                                         approval_table.c.value),
                               cascade='all, delete-orphan'),
        draft_approvals=relationship(Approval,
                                     primaryjoin=and_(change_table.c.key==approval_table.c.change_key,
                                                      approval_table.c.draft==True),
                                     order_by=(approval_table.c.category,
                                               approval_table.c.value))
        ))
mapper(Revision, revision_table, properties=dict(
        messages=relationship(Message, backref='revision',
                              cascade='all, delete-orphan'),
        files=relationship(File, backref='revision',
                           cascade='all, delete-orphan'),
        pending_cherry_picks=relationship(PendingCherryPick, backref='revision',
                                          cascade='all, delete-orphan'),
        ))
mapper(Message, message_table, properties=dict(
        author=relationship(Account)))
mapper(File, file_table, properties=dict(
       comments=relationship(Comment, backref='file',
                             order_by=(comment_table.c.line,
                                       comment_table.c.created),
                             cascade='all, delete-orphan'),
       draft_comments=relationship(Comment,
                                   primaryjoin=and_(file_table.c.key==comment_table.c.file_key,
                                                    comment_table.c.draft==True),
                                   order_by=(comment_table.c.line,
                                             comment_table.c.created)),
       ))

mapper(Comment, comment_table, properties=dict(
        author=relationship(Account)))
mapper(Label, label_table)
mapper(PermittedLabel, permitted_label_table)
mapper(Approval, approval_table, properties=dict(
        reviewer=relationship(Account)))
mapper(PendingCherryPick, pending_cherry_pick_table)
mapper(SyncQuery, sync_query_table)

def match(expr, item):
    if item is None:
        return False
    return re.match(expr, item) is not None

@sqlalchemy.event.listens_for(sqlalchemy.engine.Engine, "connect")
def add_sqlite_match(dbapi_connection, connection_record):
    dbapi_connection.create_function("matches", 2, match)

class Database(object):
    def __init__(self, app, dburi, search):
        self.log = logging.getLogger('gertty.db')
        self.dburi = dburi
        self.search = search
        self.engine = create_engine(self.dburi)
        #metadata.create_all(self.engine)
        self.migrate(app)
        # If we want the objects returned from query() to be usable
        # outside of the session, we need to expunge them from the session,
        # and since the DatabaseSession always calls commit() on the session
        # when the context manager exits, we need to inform the session to
        # expire objects when it does so.
        self.session_factory = sessionmaker(bind=self.engine,
                                            expire_on_commit=False,
                                            autoflush=False)
        self.session = scoped_session(self.session_factory)
        self.lock = threading.Lock()

    def getSession(self):
        return DatabaseSession(self)

    def migrate(self, app):
        conn = self.engine.connect()
        context = alembic.migration.MigrationContext.configure(conn)
        current_rev = context.get_current_revision()
        self.log.debug('Current migration revision: %s' % current_rev)

        has_table = self.engine.dialect.has_table(conn, "project")

        config = alembic.config.Config()
        config.set_main_option("script_location", "gertty:alembic")
        config.set_main_option("sqlalchemy.url", self.dburi)
        config.gertty_app = app

        if current_rev is None and has_table:
            self.log.debug('Stamping database as initial revision')
            alembic.command.stamp(config, "44402069e137")
        alembic.command.upgrade(config, 'head')

class DatabaseSession(object):
    def __init__(self, database):
        self.database = database
        self.session = database.session
        self.search = database.search

    def __enter__(self):
        self.database.lock.acquire()
        self.start = time.time()
        return self

    def __exit__(self, etype, value, tb):
        if etype:
            self.session().rollback()
        else:
            self.session().commit()
        self.session().close()
        self.session = None
        end = time.time()
        self.database.log.debug("Database lock held %s seconds" % (end-self.start,))
        self.database.lock.release()

    def abort(self):
        self.session().rollback()

    def commit(self):
        self.session().commit()

    def delete(self, obj):
        self.session().delete(obj)

    def vacuum(self):
        self.session().execute("VACUUM")

    def getProjects(self, subscribed=False, unreviewed=False, topicless=False):
        """Retrieve projects.

        :param subscribed: If True limit to only subscribed projects.
        :param unreviewed: If True limit to only projects with unreviewed
            changes.
        :param topicless: If True limit to only projects without topics.
        """
        query = self.session().query(Project)
        if subscribed:
            query = query.filter_by(subscribed=subscribed)
            if unreviewed:
                query = query.filter(exists().where(Project.unreviewed_changes))
        if topicless:
            query = query.filter_by(topics=None)
        return query.order_by(Project.name).all()

    def getTopics(self):
        return self.session().query(Topic).order_by(Topic.sequence).all()

    def getProject(self, key):
        try:
            return self.session().query(Project).filter_by(key=key).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getProjectByName(self, name):
        try:
            return self.session().query(Project).filter_by(name=name).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getTopic(self, key):
        try:
            return self.session().query(Topic).filter_by(key=key).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getTopicByName(self, name):
        try:
            return self.session().query(Topic).filter_by(name=name).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getSyncQueryByName(self, name):
        try:
            return self.session().query(SyncQuery).filter_by(name=name).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return self.createSyncQuery(name)

    def getChange(self, key, lazy=True):
        query = self.session().query(Change).filter_by(key=key)
        if not lazy:
            query = query.options(joinedload(Change.revisions).joinedload(Revision.files).joinedload(File.comments))
        try:
            return query.one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getChangeByID(self, id):
        try:
            return self.session().query(Change).filter_by(id=id).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getChangeIDs(self, ids):
        # Returns a set of IDs that exist in the local database matching
        # the set of supplied IDs. This is used when sync'ing the changesets
        # locally with the remote changes.
        if not ids:
            return set()
        query = self.session().query(Change.id)
        return set(ids).intersection(r[0] for r in query.all())

    def getChangesByChangeID(self, change_id):
        try:
            return self.session().query(Change).filter_by(change_id=change_id)
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getChangeByNumber(self, number):
        try:
            return self.session().query(Change).filter_by(number=number).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getPendingCherryPick(self, key):
        try:
            return self.session().query(PendingCherryPick).filter_by(key=key).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getChanges(self, query, unreviewed=False, sort_by='number'):
        self.database.log.debug("Search query: %s sort: %s" % (query, sort_by))
        q = self.session().query(Change).filter(self.search.parse(query))
        if not isinstance(sort_by, (list, tuple)):
            sort_by = [sort_by]
        if unreviewed:
            q = q.filter(change_table.c.hidden==False, change_table.c.reviewed==False)
        for s in sort_by:
            if s == 'updated':
                q = q.order_by(change_table.c.updated)
            elif s == 'last-seen':
                q = q.order_by(change_table.c.last_seen)
            elif s == 'number':
                q = q.order_by(change_table.c.number)
            elif s == 'project':
                q = q.filter(project_table.c.key == change_table.c.project_key)
                q = q.order_by(project_table.c.name)
        self.database.log.debug("Search SQL: %s" % q)
        try:
            return q.all()
        except sqlalchemy.orm.exc.NoResultFound:
            return []

    def getRevision(self, key):
        try:
            return self.session().query(Revision).filter_by(key=key).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getRevisionByCommit(self, commit):
        try:
            return self.session().query(Revision).filter_by(commit=commit).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getRevisionsByParent(self, parent):
        if isinstance(parent, six.string_types):
            parent = (parent,)
        try:
            return self.session().query(Revision).filter(Revision.parent.in_(parent)).all()
        except sqlalchemy.orm.exc.NoResultFound:
            return []

    def getRevisionByNumber(self, change, number):
        try:
            return self.session().query(Revision).filter_by(change_key=change.key, number=number).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getFile(self, key):
        try:
            return self.session().query(File).filter_by(key=key).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getComment(self, key):
        try:
            return self.session().query(Comment).filter_by(key=key).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getCommentByID(self, id):
        try:
            return self.session().query(Comment).filter_by(id=id).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getMessage(self, key):
        try:
            return self.session().query(Message).filter_by(key=key).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getMessageByID(self, id):
        try:
            return self.session().query(Message).filter_by(id=id).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getHeld(self):
        return self.session().query(Change).filter_by(held=True).all()

    def getOutdated(self):
        return self.session().query(Change).filter_by(outdated=True).all()

    def getPendingMessages(self):
        return self.session().query(Message).filter_by(pending=True).all()

    def getPendingTopics(self):
        return self.session().query(Change).filter_by(pending_topic=True).all()

    def getPendingRebases(self):
        return self.session().query(Change).filter_by(pending_rebase=True).all()

    def getPendingStarred(self):
        return self.session().query(Change).filter_by(pending_starred=True).all()

    def getPendingStatusChanges(self):
        return self.session().query(Change).filter_by(pending_status=True).all()

    def getPendingCherryPicks(self):
        return self.session().query(PendingCherryPick).all()

    def getPendingCommitMessages(self):
        return self.session().query(Revision).filter_by(pending_message=True).all()

    def getAccountByID(self, id, name=None, username=None, email=None):
        try:
            account = self.session().query(Account).filter_by(id=id).one()
        except sqlalchemy.orm.exc.NoResultFound:
            account = self.createAccount(id)
        if name is not None and account.name != name:
            account.name = name
        if username is not None and account.username != username:
            account.username = username
        if email is not None and account.email != email:
            account.email = email
        return account

    def getAccountByUsername(self, username):
        try:
            return self.session().query(Account).filter_by(username=username).one()
        except sqlalchemy.orm.exc.NoResultFound:
            return None

    def getSystemAccount(self):
        return self.getAccountByID(0, 'Gerrit Code Review')

    def createProject(self, *args, **kw):
        o = Project(*args, **kw)
        self.session().add(o)
        self.session().flush()
        return o

    def createAccount(self, *args, **kw):
        a = Account(*args, **kw)
        self.session().add(a)
        self.session().flush()
        return a

    def createSyncQuery(self, *args, **kw):
        o = SyncQuery(*args, **kw)
        self.session().add(o)
        self.session().flush()
        return o

    def createTopic(self, *args, **kw):
        o = Topic(*args, **kw)
        self.session().add(o)
        self.session().flush()
        return o
