1# This file is part of Buildbot.  Buildbot is free software: you can
2# redistribute it and/or modify it under the terms of the GNU General Public
3# License as published by the Free Software Foundation, version 2.
4#
5# This program is distributed in the hope that it will be useful, but WITHOUT
6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
8# details.
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc., 51
12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13#
14# Copyright Buildbot Team Members
15
16import sqlalchemy as sa
17
18from twisted.internet import defer
19
20from buildbot.db import NULL
21from buildbot.db import base
22
23
24class BuildDataDict(dict):
25    pass
26
27
28class BuildDataConnectorComponent(base.DBConnectorComponent):
29
30    def _insert_race_hook(self, conn):
31        # called so tests can simulate a race condition during insertion
32        pass
33
34    @defer.inlineCallbacks
35    def setBuildData(self, buildid, name, value, source):
36        def thd(conn):
37            build_data_table = self.db.model.build_data
38
39            update_values = {
40                'value': value,
41                'length': len(value),
42                'source': source,
43            }
44
45            insert_values = {
46                'buildid': buildid,
47                'name': name,
48                'value': value,
49                'length': len(value),
50                'source': source,
51            }
52
53            while True:
54                q = build_data_table.update()
55                q = q.where((build_data_table.c.buildid == buildid) &
56                            (build_data_table.c.name == name))
57                q = q.values(update_values)
58                r = conn.execute(q)
59                if r.rowcount > 0:
60                    return
61                r.close()
62
63                self._insert_race_hook(conn)
64
65                try:
66                    q = build_data_table.insert().values(insert_values)
67                    r = conn.execute(q)
68                    return
69                except (sa.exc.IntegrityError, sa.exc.ProgrammingError):
70                    # there's been a competing insert, retry
71                    pass
72
73        yield self.db.pool.do(thd)
74
75    @defer.inlineCallbacks
76    def getBuildData(self, buildid, name):
77        def thd(conn):
78            build_data_table = self.db.model.build_data
79
80            q = build_data_table.select().where((build_data_table.c.buildid == buildid) &
81                                                (build_data_table.c.name == name))
82            res = conn.execute(q)
83            row = res.fetchone()
84            if not row:
85                return None
86            return self._row2dict(conn, row)
87        res = yield self.db.pool.do(thd)
88        return res
89
90    @defer.inlineCallbacks
91    def getBuildDataNoValue(self, buildid, name):
92        def thd(conn):
93            build_data_table = self.db.model.build_data
94
95            q = sa.select([build_data_table.c.buildid,
96                           build_data_table.c.name,
97                           build_data_table.c.length,
98                           build_data_table.c.source])
99            q = q.where((build_data_table.c.buildid == buildid) &
100                        (build_data_table.c.name == name))
101            res = conn.execute(q)
102            row = res.fetchone()
103            if not row:
104                return None
105            return self._row2dict_novalue(conn, row)
106        res = yield self.db.pool.do(thd)
107        return res
108
109    @defer.inlineCallbacks
110    def getAllBuildDataNoValues(self, buildid):
111        def thd(conn):
112            build_data_table = self.db.model.build_data
113
114            q = sa.select([build_data_table.c.buildid,
115                           build_data_table.c.name,
116                           build_data_table.c.length,
117                           build_data_table.c.source])
118            q = q.where(build_data_table.c.buildid == buildid)
119
120            return [self._row2dict_novalue(conn, row)
121                    for row in conn.execute(q).fetchall()]
122        res = yield self.db.pool.do(thd)
123        return res
124
125    @defer.inlineCallbacks
126    def deleteOldBuildData(self, older_than_timestamp):
127        build_data = self.db.model.build_data
128        builds = self.db.model.builds
129
130        def count_build_datum(conn):
131            res = conn.execute(sa.select([sa.func.count(build_data.c.id)]))
132            count = res.fetchone()[0]
133            res.close()
134            return count
135
136        def thd(conn):
137            count_before = count_build_datum(conn)
138
139            if self.db._engine.dialect.name == 'sqlite':
140                # sqlite does not support delete with a join, so for this case we use a subquery,
141                # which is much slower
142
143                q = sa.select([builds.c.id])
144                q = q.where((builds.c.complete_at >= older_than_timestamp) |
145                            (builds.c.complete_at == NULL))
146
147                q = build_data.delete().where(build_data.c.buildid.notin_(q))
148            else:
149                q = build_data.delete()
150                q = q.where(builds.c.id == build_data.c.buildid)
151                q = q.where((builds.c.complete_at >= older_than_timestamp) |
152                            (builds.c.complete_at == NULL))
153            res = conn.execute(q)
154            res.close()
155
156            count_after = count_build_datum(conn)
157            return count_before - count_after
158
159        res = yield self.db.pool.do(thd)
160        return res
161
162    def _row2dict(self, conn, row):
163        return BuildDataDict(buildid=row.buildid,
164                             name=row.name,
165                             value=row.value,
166                             length=row.length,
167                             source=row.source)
168
169    def _row2dict_novalue(self, conn, row):
170        return BuildDataDict(buildid=row.buildid,
171                             name=row.name,
172                             value=None,
173                             length=row.length,
174                             source=row.source)
175