1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 import cPickle
40 import textwrap
41 import os
42 import sys
43
44 from twisted.persisted import styles
45
46 from buildbot.db import util
47 from buildbot.db.schema import base
48 from buildbot.util import json
49
50
51
52
53 TABLES = [
54
55 textwrap.dedent("""
56 CREATE TABLE version (
57 version INTEGER NOT NULL -- contains one row, currently set to 1
58 );
59 """),
60
61
62
63
64
65 textwrap.dedent("""
66 CREATE TABLE last_access (
67 `who` VARCHAR(256) NOT NULL, -- like 'buildbot-0.8.0'
68 `writing` INTEGER NOT NULL, -- 1 if you are writing, 0 if you are reading
69 -- PRIMARY KEY (who, writing),
70 `last_access` TIMESTAMP -- seconds since epoch
71 );
72 """),
73
74 textwrap.dedent("""
75 CREATE TABLE changes_nextid (next_changeid INTEGER);
76 """),
77
78 textwrap.dedent("""
79 -- Changes are immutable: once added, never changed
80 CREATE TABLE changes (
81 `changeid` INTEGER PRIMARY KEY NOT NULL, -- also serves as 'change number'
82 `author` VARCHAR(1024) NOT NULL,
83 `comments` VARCHAR(1024) NOT NULL, -- too short?
84 `is_dir` SMALLINT NOT NULL, -- old, for CVS
85 `branch` VARCHAR(1024) NULL,
86 `revision` VARCHAR(256), -- CVS uses NULL. too short for darcs?
87 `revlink` VARCHAR(256) NULL,
88 `when_timestamp` INTEGER NOT NULL, -- copied from incoming Change
89 `category` VARCHAR(256) NULL
90 );
91 """),
92
93 textwrap.dedent("""
94 CREATE TABLE change_links (
95 `changeid` INTEGER NOT NULL,
96 `link` VARCHAR(1024) NOT NULL
97 );
98 """),
99
100 textwrap.dedent("""
101 CREATE TABLE change_files (
102 `changeid` INTEGER NOT NULL,
103 `filename` VARCHAR(1024) NOT NULL
104 );
105 """),
106
107 textwrap.dedent("""
108 CREATE TABLE change_properties (
109 `changeid` INTEGER NOT NULL,
110 `property_name` VARCHAR(256) NOT NULL,
111 `property_value` VARCHAR(1024) NOT NULL -- too short?
112 );
113 """),
114
115
116 textwrap.dedent("""
117 CREATE TABLE schedulers (
118 `schedulerid` INTEGER PRIMARY KEY, -- joins to other tables
119 `name` VARCHAR(127) UNIQUE NOT NULL,
120 `state` VARCHAR(1024) NOT NULL -- JSON-encoded state dictionary
121 );
122 """),
123
124 textwrap.dedent("""
125 CREATE TABLE scheduler_changes (
126 `schedulerid` INTEGER,
127 `changeid` INTEGER,
128 `important` SMALLINT
129 );
130 """),
131
132 textwrap.dedent("""
133 CREATE TABLE scheduler_upstream_buildsets (
134 `buildsetid` INTEGER,
135 `schedulerid` INTEGER,
136 `active` SMALLINT
137 );
138 """),
139
140
141 textwrap.dedent("""
142 -- SourceStamps are immutable: once added, never changed
143 CREATE TABLE sourcestamps (
144 `id` INTEGER PRIMARY KEY,
145 `branch` VARCHAR(256) default NULL,
146 `revision` VARCHAR(256) default NULL,
147 `patchid` INTEGER default NULL
148 );
149 """),
150 textwrap.dedent("""
151 CREATE TABLE patches (
152 `id` INTEGER PRIMARY KEY,
153 `patchlevel` INTEGER NOT NULL,
154 `patch_base64` TEXT NOT NULL, -- encoded bytestring
155 `subdir` TEXT -- usually NULL
156 );
157 """),
158 textwrap.dedent("""
159 CREATE TABLE sourcestamp_changes (
160 `sourcestampid` INTEGER NOT NULL,
161 `changeid` INTEGER NOT NULL
162 );
163 """),
164
165
166 textwrap.dedent("""
167 -- BuildSets are mutable. Python code may not cache them. Every
168 -- BuildRequest must have exactly one associated BuildSet.
169 CREATE TABLE buildsets (
170 `id` INTEGER PRIMARY KEY NOT NULL,
171 `external_idstring` VARCHAR(256),
172 `reason` VARCHAR(256),
173 `sourcestampid` INTEGER NOT NULL,
174 `submitted_at` INTEGER NOT NULL,
175 `complete` SMALLINT NOT NULL default 0,
176 `complete_at` INTEGER,
177 `results` SMALLINT -- 0=SUCCESS,2=FAILURE, from status/builder.py
178 -- results is NULL until complete==1
179 );
180 """),
181 textwrap.dedent("""
182 CREATE TABLE buildset_properties (
183 `buildsetid` INTEGER NOT NULL,
184 `property_name` VARCHAR(256) NOT NULL,
185 `property_value` VARCHAR(1024) NOT NULL -- too short?
186 );
187 """),
188
189 textwrap.dedent("""
190 -- the buildrequests table represents the queue of builds that need to be
191 -- done. In an idle buildbot, all requests will have complete=1.
192 -- BuildRequests are mutable. Python code may not cache them.
193 CREATE TABLE buildrequests (
194 `id` INTEGER PRIMARY KEY NOT NULL,
195
196 -- every BuildRequest has a BuildSet
197 -- the sourcestampid and reason live in the BuildSet
198 `buildsetid` INTEGER NOT NULL,
199
200 `buildername` VARCHAR(256) NOT NULL,
201
202 `priority` INTEGER NOT NULL default 0,
203
204 -- claimed_at is the time at which a master most recently asserted that
205 -- it is responsible for running the build: this will be updated
206 -- periodically to maintain the claim
207 `claimed_at` INTEGER default 0,
208
209 -- claimed_by indicates which buildmaster has claimed this request. The
210 -- 'name' contains hostname/basedir, and will be the same for subsequent
211 -- runs of any given buildmaster. The 'incarnation' contains bootime/pid,
212 -- and will be different for subsequent runs. This allows each buildmaster
213 -- to distinguish their current claims, their old claims, and the claims
214 -- of other buildmasters, to treat them each appropriately.
215 `claimed_by_name` VARCHAR(256) default NULL,
216 `claimed_by_incarnation` VARCHAR(256) default NULL,
217
218 `complete` INTEGER default 0, -- complete=0 means 'pending'
219
220 -- results is only valid when complete==1
221 `results` SMALLINT, -- 0=SUCCESS,1=WARNINGS,etc, from status/builder.py
222
223 `submitted_at` INTEGER NOT NULL,
224
225 `complete_at` INTEGER
226 );
227 """),
228
229 textwrap.dedent("""
230 -- this records which builds have been started for each request
231 CREATE TABLE builds (
232 `id` INTEGER PRIMARY KEY NOT NULL,
233 `number` INTEGER NOT NULL, -- BuilderStatus.getBuild(number)
234 -- 'number' is scoped to both the local buildmaster and the buildername
235 `brid` INTEGER NOT NULL, -- matches buildrequests.id
236 `start_time` INTEGER NOT NULL,
237 `finish_time` INTEGER
238 );
239 """),
240 ]
241
248
250
251 c = self.conn.cursor()
252 c.execute("CREATE TABLE test_unicode (`name` VARCHAR(100))")
253 q = util.sql_insert(self.dbapi, 'test_unicode', ["name"])
254 try:
255 val = u"Frosty the \N{SNOWMAN}"
256 c.execute(q, [val])
257 c.execute("SELECT * FROM test_unicode")
258 row = c.fetchall()[0]
259 if row[0] != val:
260 raise UnicodeError("Your database doesn't support unicode data; for MySQL, set the default collation to utf8_general_ci.")
261 finally:
262 pass
263 c.execute("DROP TABLE test_unicode")
264
266
267 c = self.conn.cursor()
268 for t in TABLES:
269 try:
270 c.execute(t)
271 except:
272 print >>sys.stderr, "error executing SQL query: %s" % t
273 raise
274
276
277 def remove_none(x):
278 if x is None: return u""
279 elif isinstance(x, str):
280 return x.decode("utf8")
281 else:
282 return x
283 try:
284 values = tuple(remove_none(x) for x in
285 (change.number, change.who,
286 change.comments, change.isdir,
287 change.branch, change.revision, change.revlink,
288 change.when, change.category))
289 except UnicodeDecodeError, e:
290 raise UnicodeError("Trying to import change data as UTF-8 failed. Please look at contrib/fix_changes_pickle_encoding.py: %s" % str(e))
291
292 q = util.sql_insert(self.dbapi, 'changes',
293 """changeid author comments is_dir branch revision
294 revlink when_timestamp category""".split())
295 cursor.execute(q, values)
296
297 for link in change.links:
298 cursor.execute(util.sql_insert(self.dbapi, 'change_links', ('changeid', 'link')),
299 (change.number, link))
300 for filename in change.files:
301 cursor.execute(util.sql_insert(self.dbapi, 'change_files', ('changeid', 'filename')),
302 (change.number, filename))
303 for propname,propvalue in change.properties.properties.items():
304 encoded_value = json.dumps(propvalue)
305 cursor.execute(util.sql_insert(self.dbapi, 'change_properties',
306 ('changeid', 'property_name', 'property_value')),
307 (change.number, propname, encoded_value))
308
310
311 changes_pickle = os.path.join(self.basedir, "changes.pck")
312 if os.path.exists(changes_pickle):
313 if not self.quiet: print "migrating changes.pck to database"
314
315
316
317 source = cPickle.load(open(changes_pickle,"rb"))
318 styles.doUpgrade()
319
320 if not self.quiet: print " (%d Change objects)" % len(source.changes)
321
322
323
324 have_unnumbered = False
325 for c in source.changes:
326 if c.revision and c.number is None:
327 have_unnumbered = True
328 break
329 if have_unnumbered:
330 n = 1
331 for c in source.changes:
332 if c.revision:
333 c.number = n
334 n = n + 1
335
336
337 cursor = self.conn.cursor()
338 for c in source.changes:
339 if not c.revision:
340 continue
341 self._addChangeToDatabase(c, cursor)
342
343
344 max_changeid = max([ c.number for c in source.changes if c.revision ] + [ 0 ])
345 cursor.execute("""INSERT into changes_nextid VALUES (%d)""" % (max_changeid+1))
346
347 if not self.quiet:
348 print "moving changes.pck to changes.pck.old; delete it or keep it as a backup"
349 os.rename(changes_pickle, changes_pickle+".old")
350 else:
351 c = self.conn.cursor()
352 c.execute("""INSERT into changes_nextid VALUES (1)""")
353
355 c = self.conn.cursor()
356 c.execute("""INSERT INTO version VALUES (1)""")
357