Mercurial > ~dholland > hg > swallowtail > index.cgi
annotate shelltools/query-pr/query.py @ 50:bb0ece71355e
Untabify.
author | David A. Holland |
---|---|
date | Sat, 02 Apr 2022 18:10:52 -0400 |
parents | 4b7f0ee35994 |
children | ef6d572c4e1e |
rev | line source |
---|---|
7
c013fb703183
Empty placeholder scripts so the build will run.
David A. Holland
parents:
diff
changeset
|
1 #!@PYTHON@ |
33 | 2 |
48 | 3 import sys |
33 | 4 import argparse |
5 import psycopg2 | |
6 | |
7 program_description = """ | |
8 Search for and retrieve problem reports. | |
9 """ | |
10 program_version = "@VERSION@" | |
11 | |
12 ############################################################ | |
13 # settings | |
14 | |
15 outfile = sys.stdout | |
16 outmaterial = "headers" | |
17 outformat = "text" | |
18 | |
19 ############################################################ | |
47 | 20 # database field access |
21 | |
22 # | |
23 # Fields of PRs that we might search are spread across a number of | |
24 # tables and require varying joins to get them. And, because of | |
25 # classication schemes, the set of fields isn't static and we can't | |
26 # just assemble a massive view with one column for each field. | |
27 # | |
28 # The QueryBuilder class knows how to arrange for all known fields to | |
29 # be present. | |
30 # | |
31 | |
32 class QueryBuilder: | |
50 | 33 # these fields are in the PRs table |
34 prtable_fields = [ | |
35 "id", "synopsis", "confidential", "state", "locked", | |
36 "timeout_date", "timeout_state", | |
37 "arrival_schemaversion", "arrival_date", "modified_date", | |
38 "closed_date", | |
39 "release", "environment" | |
40 ] | |
47 | 41 |
50 | 42 # these fields are aliases for others |
43 alias_fields = { | |
44 "number" : "id", | |
45 "date" : "arrival_date", | |
46 } | |
47 | 47 |
50 | 48 def __init__(self): |
49 self.present = {} | |
50 self.joined = {} | |
51 self.fromitems = [] | |
52 self.whereitems = [] | |
53 self.order = None | |
47 | 54 |
50 | 55 def setorder(self, order): |
56 self.order = order | |
47 | 57 |
50 | 58 # add to present{} and return the value for convenience (internal) |
59 def makepresent(self, field, name): | |
60 self.present[field] = name | |
61 return name | |
47 | 62 |
50 | 63 # add a join item (once only) (internal) |
64 def addjoin(self, table, as_ = None): | |
65 if as_ is not None: | |
66 key = table + "-" + as_ | |
67 val = table + " AS " + as_ | |
68 else: | |
69 key = table | |
70 val = table | |
71 if key not in self.joined: | |
72 self.joined[key] = True | |
73 self.fromitems.append(val) | |
47 | 74 |
50 | 75 # returns a sql expression for the field |
76 def getfield(self, field): | |
77 # already-fetched fields | |
78 if field in self.present: | |
79 return self.present[field] | |
47 | 80 |
50 | 81 # aliases for other fields |
82 if field in alias_fields: | |
83 return self.getfield(alias_fields[field]) | |
47 | 84 |
50 | 85 # simple fields directly in the PRs table |
86 if field in prtable_fields: | |
87 self.addjoin("PRs") | |
88 return self.makepresent(field, "PRs." + field) | |
47 | 89 |
50 | 90 # now it gets more interesting... |
91 if field == "closed": | |
92 self.addjoin("PRs") | |
93 self.addjoin("states") | |
94 self.addwhere("PRs.state = states.name") | |
95 return self.makepresent(field, "states.closed") | |
47 | 96 |
50 | 97 # XXX let's pick one set of names and use them everywhere |
98 # (e.g. change "posttime" in the schema to "message_date" | |
99 # or something) | |
100 if field == "comment_date" or field == "posttime": | |
101 self.addjoin("PRs") | |
102 self.addjoin("messages") | |
103 self.addwhere("PRs.id = messages.pr") | |
104 return self.makepresent(field, "messages.posttime") | |
47 | 105 |
50 | 106 if field == "comment" or field == "message" or field == "post": |
107 self.addjoin("PRs") | |
108 self.addjoin("messages") | |
109 self.addwhere("PRs.id = messages.pr") | |
110 return self.makepresent(field, "messages.body") | |
47 | 111 |
50 | 112 if field == "attachment": |
113 self.addjoin("PRs") | |
114 self.addjoin("messages") | |
115 self.addjoin("attachments") | |
116 self.addwhere("PRs.id = messages.pr") | |
117 self.addwhere("messages.id = attachments.msgid") | |
118 return self.makepresent(field, "attachments.body") | |
47 | 119 |
50 | 120 if field == "patch": |
121 self.addjoin("PRs") | |
122 self.addjoin("messages") | |
123 self.addjoin("attachments", "patches") | |
124 self.addwhere("PRs.id = messages.pr") | |
125 self.addwhere("messages.id = patches.msgid") | |
126 self.addwhere("patches.mimetype = " + | |
127 "'application/x-patch'") | |
128 return self.makepresent(field, "patches.body") | |
47 | 129 |
50 | 130 if field == "mimetype": |
131 subquery = "((SELECT mtmessages1.pr as pr, " + \ | |
132 "mtmessages1.mimetype as mimetype " + \ | |
133 "FROM messages as mtmessages1) " + \ | |
134 "UNION " + \ | |
135 "(SELECT mtmessages2.pr as pr, " + \ | |
136 "mtattach2.mimetype as mimetype " + \ | |
137 "FROM messages as mtmessages2, " + \ | |
138 " attachments as mtattach2 " + \ | |
139 "WHERE mtmessages2.id = mtattach2.msgid))" | |
140 self.addjoin("PRs") | |
141 self.addjoin(subquery, "mimetypes") | |
142 self.addwhere("PRs.id = mimetypes.pr") | |
143 return self.makepresent(field, "mimetypes.mimetype") | |
47 | 144 |
50 | 145 # XXX: need view userstrings |
146 # select (id, username as name) from users | |
147 # union select (id, realname as name) from users | |
148 # (allow searching emails? ugh) | |
149 if field == "originator" or field == "submitter": | |
150 self.addjoin("PRs") | |
151 self.addjoin("userstrings", "originators") | |
152 self.addwhere("PRs.originator = originators.id") | |
153 return self.makepresent(field, "originators.name") | |
47 | 154 |
50 | 155 if field == "reporter" or field == "respondent": |
156 self.addjoin("PRs") | |
157 self.addjoin("subscriptions") | |
158 self.addjoin("userstrings", "reporters") | |
159 self.addwhere("subscriptions.userid = reporters.id") | |
160 self.addwhere("subscriptions.reporter") | |
161 return self.makepresent(field, "reporters.name") | |
47 | 162 |
50 | 163 if field == "responsible": |
164 self.addjoin("PRs") | |
165 self.addjoin("subscriptions") | |
166 self.addjoin("userstrings", "responsibles") | |
167 self.addwhere("subscriptions.userid = responsibles.id") | |
168 self.addwhere("subscriptions.responsible") | |
169 return self.makepresent(field, "responsibles.name") | |
47 | 170 |
50 | 171 if field in hierclasses: |
172 col = field + "_data" | |
173 self.addjoin("PRs") | |
174 self.addjoin("hierclass_data", col) | |
175 self.addwhere("PRs.id = %s.pr" % col) | |
176 self.addwhere("%s.scheme = '%s'" % (col, field)) | |
177 return self.makepresent(field, "%s.value" % col) | |
47 | 178 |
50 | 179 if field in flatclasses: |
180 col = field + "_data" | |
181 self.addjoin("PRs") | |
182 self.addjoin("flatclass_data", col) | |
183 self.addwhere("PRs.id = %s.pr" % col) | |
184 self.addwhere("%s.scheme = '%s'" % (col, field)) | |
185 return self.makepresent(field, "%s.value" % col) | |
47 | 186 |
50 | 187 if field in textclasses: |
188 col = field + "_data" | |
189 self.addjoin("PRs") | |
190 self.addjoin("textclass_data", col) | |
191 self.addwhere("PRs.id = %s.pr" % col) | |
192 self.addwhere("%s.scheme = '%s'" % (col, field)) | |
193 return self.makepresent(field, "%s.value" % col) | |
47 | 194 |
50 | 195 if field in tagclasses: |
196 col = field + "_data" | |
197 self.addjoin("PRs") | |
198 self.addjoin("tagclass_data", col) | |
199 self.addwhere("PRs.id = %s.pr" % col) | |
200 self.addwhere("%s.scheme = '%s'" % (col, field)) | |
201 return self.makepresent(field, "%s.value" % col) | |
47 | 202 |
50 | 203 sys.stderr.write("Unknown field %s" % field) |
204 exit(1) | |
205 # end getfield | |
47 | 206 |
50 | 207 # emit sql |
208 def build(self, sels): | |
209 s = ", ".join(sels) | |
210 f = ", ".join(self.fromitems) | |
211 w = " and ".join(self.whereitems) | |
212 q = "SELECT %s\nFROM %s\nWHERE %s\n" % (s, f, w) | |
213 if self.order is not None: | |
214 q = q + "ORDER BY " + self.order + "\n" | |
215 return q | |
216 # endif | |
47 | 217 |
218 # end class QueryBuilder | |
219 | |
220 # XXX we need to add dynamically: | |
221 # hierclass_names.name to hierclasses[] | |
222 # flatclass_names.name to flatclasses[] | |
223 # textclass_names.name to textclasses[] | |
224 # tagclass_names.name to tagclasses[] | |
225 | |
226 ############################################################ | |
33 | 227 # database |
228 | |
229 dblink = None | |
230 | |
231 def opendb(): | |
50 | 232 global dblink |
33 | 233 |
50 | 234 host = "localhost" |
235 user = "swallowtail" | |
236 database = "swallowtail" | |
237 dblink = psycopg2.connect("host=%s user=%s dbname=%s" % | |
238 (host, user, database)) | |
33 | 239 # end opendb |
240 | |
241 def closedb(): | |
50 | 242 global dblink |
33 | 243 |
50 | 244 dblink.close() |
245 dblink = None | |
33 | 246 # end closedb |
247 | |
248 def querydb(qtext, args): | |
50 | 249 print "Executing this query:" |
250 print qtext | |
251 print "Args are:" | |
252 print args | |
33 | 253 |
50 | 254 cursor = dblink.cursor() |
255 cursor.execute(qtext, args) | |
256 result = cursor.fetchall() | |
257 cursor.close() | |
258 return result | |
33 | 259 # end querydb |
260 | |
261 ############################################################ | |
262 # query class for searches | |
47 | 263 # XXX: obsolete, remove |
33 | 264 |
265 class Query: | |
50 | 266 def __init__(self): |
267 self.selections = [] | |
268 self.tables = [] | |
269 self.constraints = [] | |
270 self.args = [] | |
271 prtables = ["PRs"] | |
272 prconstraints = [] | |
33 | 273 |
50 | 274 def select(self, s): |
275 self.selections.append(s) | |
33 | 276 |
50 | 277 def addtable(self, t): |
278 assert(t not in self.tables) | |
279 self.tables.append(t) | |
33 | 280 |
50 | 281 def constrain(self, expr): |
282 self.constraints.append(t) | |
33 | 283 |
50 | 284 def internval(self, val): |
285 num = len(self.args) | |
286 self.args[num] = val | |
287 return "$%d" % num | |
33 | 288 |
50 | 289 def textify(self): |
290 s = "SELECT %s\n" % ",".join(self.selections) | |
291 f = "FROM %s\n" % ",".join(self.tables) | |
292 w = "WHERE %s\n" % " AND ".join(self.constraints) | |
293 return s + f + w | |
33 | 294 # end class Query |
295 | |
296 def regexp_constraint(q, field, value): | |
50 | 297 cleanval = q.internval(value) |
298 if not isregexp(value): | |
299 return "%s = %s" % (field, cleanval) | |
300 else: | |
301 # XXX what's the right operator again? | |
302 return "%s ~= %s" % (field, cleanval) | |
33 | 303 # end regexp_constraint |
304 | |
305 def intrange_constraint(q, field, value): | |
50 | 306 (lower, upper) = args.number |
307 if lower is not None: | |
308 assert(typeof(lower) == int) | |
309 prq.constrain("%s >= %d" % (field, lower)) | |
310 if upper is not None: | |
311 assert(typeof(upper) == int) | |
312 prq.constrain("%s <= %d" % (field, upper)) | |
33 | 313 # end intrange_constraint |
314 | |
315 def daterange_constraint(q, field, value): | |
50 | 316 # XXX |
317 assert(0) | |
33 | 318 # end daterange_constraint |
319 | |
320 ############################################################ | |
321 | |
47 | 322 # this is old code that needs to be merged or deleted into the new stuff |
323 def oldstuff(): | |
324 | |
50 | 325 # If we're doing something other than a search, do it now |
326 if args.attach is not None: | |
327 get_attachment(args.attach) | |
328 exit(0) | |
329 if args.message is not None: | |
330 get_message(args.message) | |
331 exit(0) | |
33 | 332 |
50 | 333 if args.prs is not None and len(args.prs) > 0: |
334 show_prs(args.prs) | |
335 exit(0) | |
33 | 336 |
50 | 337 # |
338 # Collect up the search constraints | |
339 # | |
340 | |
341 # 1. Constraints on the PRs table | |
342 checkprtable = False | |
343 prq = Query() | |
344 prq.select("PRs.id as id") | |
345 prq.addtable("PRs") | |
346 if not args.closed: | |
347 checkprtable = True | |
348 prq.addtable("states") | |
349 prq.constrain("PRs.state = states.name") | |
350 prq.constrain("states.closed = FALSE") | |
351 if args.public: | |
352 checkprtable = True | |
353 prq.constrain("NOT PRs.confidential") | |
354 if args.number is not None: | |
355 checkprtable = True | |
356 intrange_constraint(prq, "PRs.id", args.number) | |
357 if args.synopsis is not None: | |
358 checkprtable = True | |
359 regexp_constraint(prq, "PRs.synopsis", args.synopsis) | |
360 if args.confidential is not None: | |
361 checkprtable = True | |
362 assert(typeof(args.confidential) == bool) | |
363 if args.confidential: | |
364 prq.constrain("PRs.confidential") | |
365 else: | |
366 prq.constrain("not PRs.confidential") | |
367 if args.state is not None: | |
368 checkprtable = True | |
369 regexp_constraint(prq, "PRs.state", args.state) | |
370 if args.locked is not None: | |
371 checkprtable = True | |
372 assert(typeof(args.locked) == bool) | |
373 if args.locked: | |
374 prq.constrain("PRs.locked") | |
375 else: | |
376 prq.constrain("not PRs.locked") | |
377 if args.arrival_schemaversion is not None: | |
378 checkprtable = True | |
379 intrange_constraint(prq, "PRs.arrival_schemaversion", | |
380 args.arrival_schemaversion) | |
381 if args.arrival_date is not None: | |
382 checkprtable = True | |
383 daterange_constraint(prq, "PRs.arrival_date", | |
384 args.arrival_date) | |
385 if args.closed_date is not None: | |
386 checkprtable = True | |
387 daterange_constraint(prq, "PRs.closed_date", | |
388 args.closed_date) | |
389 if args.last_modified is not None: | |
390 checkprtable = True | |
391 daterange_constraint(prq, "PRs.last_modified", | |
392 args.last_modified) | |
393 if args.release is not None: | |
394 checkprtable = True | |
395 regexp_constraint(prq, "PRs.release", args.release) | |
396 if args.environment is not None: | |
397 checkprtable = True | |
398 regexp_constraint(prq, "PRs.environment", args.environment) | |
33 | 399 |
50 | 400 if args.originator_name is not None or \ |
401 args.originator_email is not None: | |
402 prq.addtable("usermail as originator") | |
403 prq.constrain("PRs.originator = originator.id") | |
404 if args.originator_name is not None: | |
405 checkprtable = True | |
406 regexp_constraint(prq, "originator.realname", | |
407 args.originator_name) | |
408 if args.originator_email is not None: | |
409 checkprtable = True | |
410 regexp_constraint(prq, "originator.email", | |
411 args.originator_name) | |
412 if args.originator_id is not None: | |
413 checkprtable = True | |
414 intrange_constraint(prq, "PRs.originator", args.originator_id) | |
33 | 415 |
50 | 416 queries = [] |
417 if checkprtable: | |
418 queries.append(prq) | |
33 | 419 |
50 | 420 if args.responsible is not None: |
421 sq = Query() | |
422 sq.select("subscriptions.pr as id") | |
423 sq.addtable("subscriptions") | |
424 sq.addtable("users") | |
425 sq.constrain("subscriptions.userid = users.id") | |
426 regexp_constraint(sq, "users.realname", args.responsible) | |
427 sq.constrain("subscriptions.responsible") | |
428 queries.append(sq) | |
429 if args.respondent is not None: | |
430 sq = Query() | |
431 sq.select("subscriptions.pr as id") | |
432 sq.addtable("subscriptions") | |
433 sq.addtable("users as subscribed") | |
434 sq.constrain("subscriptions.userid = users.id") | |
435 regexp_constraint(sq, "users.realname", args.respondent) | |
436 sq.constrain("subscriptions.reporter") | |
437 queries.append(sq) | |
438 if args.subscribed is not None: | |
439 sq = Query() | |
440 sq.select("subscriptions.pr as id") | |
441 sq.addtable("subscriptions") | |
442 sq.addtable("users as subscribed") | |
443 sq.constrain("subscriptions.userid = users.id") | |
444 regexp_constraint(sq, "users.realname", args.subscribed) | |
445 queries.append(sq) | |
33 | 446 |
50 | 447 if args.messages is not None: |
448 mq = Query() | |
449 mq.select("messages.pr as id") | |
450 mq.addtable("messages") | |
451 regexp_constraint(sq, "messages.text", args.messages) | |
452 queries.append(mq) | |
33 | 453 |
50 | 454 if args.adminlog is not None: |
455 aq = Query() | |
456 aq.select("adminlog.pr as id") | |
457 aq.addtable("adminlog") | |
458 regexp_constraint(sq, "adminlog.change", args.adminlog) | |
459 regexp_constraint(sq, "adminlog.comment", args.adminlog) | |
460 assert(len(aq.constraints) == 2) | |
461 x = "%s OR %s" % (aq.constraints[0], aq.constraints[1]) | |
462 aq.constraints = [x] | |
463 queries.append(aq) | |
33 | 464 |
50 | 465 if args.anytext is not None: |
466 choke("--anytext isn't supported yet") | |
33 | 467 |
50 | 468 for scheme in classification_schemes: |
469 if args[scheme] is not None: | |
470 schemetype = classification_schemetypes[scheme] | |
471 tbl = "%sclass_data" % schemetype | |
472 cq = Query() | |
473 cq.select("scheme.pr as id") | |
474 cq.addtable("%s as scheme" % schemetype) | |
475 cq.constrain("scheme.scheme = '%s'" % scheme) | |
476 regexp_constraint(cq, "scheme.value", args[scheme]) | |
477 queries.append(cq) | |
478 # end loop | |
33 | 479 |
50 | 480 querytexts = [q.textify() for q in queries] |
481 return "INTERSECT\n".join(querytexts) | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
482 |
47 | 483 ############################################################ |
484 # printing | |
485 | |
486 class PrintText: | |
50 | 487 def __init__(self, output): |
488 self.lines = (output == "RAW" or output == "LIST") | |
489 def printheader(self, row): | |
490 # nothing | |
48 | 491 pass |
50 | 492 def printrow(self, row): |
493 # XXX | |
494 print row | |
495 def printfooter(self, row): | |
496 # nothing | |
48 | 497 pass |
47 | 498 # end class PrintText |
499 | |
500 class PrintCsv: | |
50 | 501 def __init__(self, output): |
502 # nothing | |
48 | 503 pass |
50 | 504 def printheader(self, row): |
505 # XXX | |
48 | 506 pass |
50 | 507 def printrow(self, row): |
508 # XXX | |
48 | 509 pass |
50 | 510 def printfooter(self, row): |
511 # nothing | |
48 | 512 pass |
47 | 513 # end class PrintCsv |
514 | |
515 class PrintXml: | |
50 | 516 def __init__(self, output): |
517 # nothing | |
48 | 518 pass |
50 | 519 def printheader(self, row): |
520 # XXX | |
48 | 521 pass |
50 | 522 def printrow(self, row): |
523 # XXX | |
48 | 524 pass |
50 | 525 def printfooter(self, row): |
526 # XXX | |
48 | 527 pass |
47 | 528 # end class PrintXml |
529 | |
530 class PrintJson: | |
50 | 531 def __init__(self, output): |
532 # nothing | |
48 | 533 pass |
50 | 534 def printheader(self, row): |
535 # XXX | |
48 | 536 pass |
50 | 537 def printrow(self, row): |
538 # XXX | |
48 | 539 pass |
50 | 540 def printfooter(self, row): |
541 # XXX | |
48 | 542 pass |
47 | 543 # end class PrintJson |
544 | |
545 class PrintRdf: | |
50 | 546 def __init__(self, output): |
547 # nothing | |
48 | 548 pass |
50 | 549 def printheader(self, row): |
550 # XXX | |
48 | 551 pass |
50 | 552 def printrow(self, row): |
553 # XXX | |
48 | 554 pass |
50 | 555 def printfooter(self, row): |
556 # XXX | |
48 | 557 pass |
47 | 558 # end class PrintRdf |
559 | |
560 class PrintRdflike: | |
50 | 561 def __init__(self, output): |
562 # nothing | |
48 | 563 pass |
50 | 564 def printheader(self, row): |
565 # XXX | |
48 | 566 pass |
50 | 567 def printrow(self, row): |
568 # XXX | |
48 | 569 pass |
50 | 570 def printfooter(self, row): |
571 # XXX | |
48 | 572 pass |
47 | 573 # end class PrintRdflike |
574 | |
575 def print_prs(ids): | |
50 | 576 if sel.outformat == "TEXT": |
577 mkprinter = PrintText | |
578 elif sel.outformat == "CSV": | |
579 mkprinter = PrintCsv | |
580 elif sel.outformat == "XML": | |
581 mkprinter = PrintXml | |
582 elif sel.outformat == "JSON": | |
583 mkprinter = PrintJson | |
584 elif sel.outformat == "RDF": | |
585 mkprinter = PrintRdf | |
586 elif sel.outformat == "RDFLIKE": | |
587 mkprinter = PrintRdflike | |
588 else: | |
589 assert(False) | |
47 | 590 |
50 | 591 # reset the printer |
592 printer = mkprinter(sel.output) | |
47 | 593 |
50 | 594 if sel.output == "RAW": |
595 printer.printheader(ids[0]) | |
596 for id in ids: | |
597 printer(id) | |
598 printer.printfooter(ids[0]) | |
599 return | |
600 elif sel.output == "LIST": | |
601 # XXX is there a clean way to do this passing the | |
602 # whole list of ids at once? | |
603 query = "SELECT id, synopsis\n" + \ | |
604 "FROM PRs\n" + \ | |
605 "WHERE id = $1" | |
606 elif sel.output == "HEADERS": | |
607 query = None # XXX | |
608 elif sel.output == "META": | |
609 query = None # XXX | |
610 elif sel.output == "FULL": | |
611 query = None # XXX | |
612 else: | |
613 assert(False) | |
47 | 614 |
50 | 615 first = True |
616 for id in ids: | |
617 results = querydb(query, [id]) | |
618 if first: | |
619 printer.printheader(results[0]) | |
620 first = False | |
621 for r in results: | |
622 printer.printrow(r) | |
623 printer.printfooter(results[0]) | |
47 | 624 # end print_prs |
625 | |
626 # XXX if in public mode we need to check if the PR is public | |
627 def print_message(pr, msgnum): | |
50 | 628 query = "SELECT users.username AS username,\n" + \ |
629 " users.realname AS realname,\n" + \ | |
630 " messages.id AS id, parent_id,\n" + \ | |
631 " posttime, mimetype, body\n" + \ | |
632 "FROM messages, users\n" + \ | |
633 "WHERE messages.who = users.id\n" + \ | |
634 " AND messages.pr = $1\n" + \ | |
635 " AND messages.number_in_pr = $2\n" | |
636 # Note that while pr is safe, msgnum came from the commandline | |
637 # and may not be. | |
638 results = querydb(query, [pr, msgnum]) | |
639 [result] = results | |
640 (username, realname, id, parent_id, posttime, mimetype, body) = result | |
641 # XXX honor mimetype | |
642 # XXX honor output format (e.g. html) | |
643 sys.stdout.write("From swallowtail@%s %s\n" % (organization,posttime)) | |
644 sys.stdout.write("From: %s (%s)\n" % (username, realname)) | |
645 sys.stdout.write("References: %s\n" % parent_id) | |
646 sys.stdout.write("Date: %s\n" % posttime) | |
647 sys.stdout.write("Content-Type: %s\n" % mimetype) | |
648 sys.stdout.write("\n") | |
649 sys.stdout.write(body) | |
47 | 650 # end print_message |
651 | |
652 # XXX if in public mode we need to check if the PR is public | |
653 def print_attachment(pr, attachnum): | |
50 | 654 query = "SELECT a.mimetype as mimetype, a.body as body\n" + \ |
655 "FROM messages, attachments as a\n" + \ | |
656 "WHERE messages.pr = $1\n" + \ | |
657 " AND messages.id = a.msgid\n" + \ | |
658 " AND a.number_in_pr = $2\n" | |
659 # Note that while pr is safe, attachnum came from the | |
660 # commandline and may not be. | |
661 results = querydb(query, [pr, msgnum]) | |
662 [result] = results | |
663 (mimetype, body) = result | |
664 # XXX honor mimetype | |
665 # XXX need an http output mode so we can send the mimetype! | |
666 sys.stdout.write(body) | |
47 | 667 # end print_attachment |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
668 |
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
669 ############################################################ |
47 | 670 # AST for query |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
671 |
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
672 class Invocation: |
50 | 673 class Query: |
674 Q_TERM = 1 # XXX unused so far | |
675 Q_SQL = 2 | |
676 Q_AND = 3 | |
677 Q_OR = 4 | |
678 Q_TSTRING = 5 | |
679 Q_QSTRING = 6 | |
47 | 680 |
50 | 681 def __init__(self, type): |
682 self.type = type | |
683 def doterm(term): | |
684 self = Query(Q_TERM) | |
685 self.term = term | |
686 return self | |
687 def dosql(s): | |
688 self = Query(Q_SQL) | |
689 self.sql = s | |
690 return self | |
691 def doand(qs): | |
692 self = Query(Q_AND) | |
693 self.args = qs | |
694 return self | |
695 def door(qs): | |
696 self = Query(Q_OR) | |
697 self.args = qs | |
698 return self | |
699 # query term string | |
700 def dotstring(q): | |
701 self = Query(Q_TSTRING) | |
702 self.string = q | |
703 return self | |
704 # whole query string | |
705 def doqstring(q): | |
706 self = Query(Q_QSTRING) | |
707 self.string = q | |
708 return self | |
709 # end class Query | |
47 | 710 |
50 | 711 class Order: |
712 def __init__(self, field, rev = False): | |
713 self.field = field | |
714 self.rev = rev | |
715 def dooldest(ign): | |
716 return Order("number") | |
717 def donewest(ign): | |
718 return Order("number", True) | |
719 def dostaleness(ign): | |
720 return Order("modified_date", True) | |
721 def dofield(field): | |
722 return Order(field) | |
723 def dorevfield(field): | |
724 return Order(field, True) | |
725 # end class Order | |
47 | 726 |
50 | 727 class Search: |
728 def __init__(self, qs, openonly, publiconly, os): | |
729 self.queries = qs | |
730 self.openonly = openonly | |
731 self.publiconly = publiconly | |
732 self.orders = os | |
733 # end class Search | |
47 | 734 |
50 | 735 class Selection: |
736 S_PR = 1 | |
737 S_MESSAGE = 2 | |
738 S_ATTACHMENT = 3 | |
47 | 739 |
50 | 740 def __init__(self, type): |
741 self.type = type | |
742 def dopr(output, outformat): | |
743 self = Selection(S_PR) | |
744 self.output = output | |
745 self.outformat = outformat | |
746 return self | |
747 def domessage(arg): | |
748 self = Selection(S_MESSAGE) | |
749 self.message = arg | |
750 return self | |
751 def doattachment(arg): | |
752 self = Selection(S_ATTACHMENT) | |
753 self.attachment = arg | |
754 return self | |
755 # end class Selection | |
47 | 756 |
50 | 757 class Op: |
758 # operation codes | |
759 OP_FIELDS = 1 | |
760 OP_SHOW = 2 | |
761 OP_RANGE = 3 | |
762 OP_SEARCH = 4 | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
763 |
50 | 764 def __init__(self, type): |
765 self.type = type | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
766 |
50 | 767 def dofields(): |
768 return Op(OP_FIELDS) | |
769 def doshow(field): | |
770 self = Op(OP_SHOW) | |
771 self.field = field | |
772 return self | |
773 def dorange(field): | |
774 self = Op(OP_RANGE) | |
775 self.field = field | |
776 return self | |
777 def dosearch(s, sels): | |
778 self = Op(OP_SEARCH) | |
779 self.search = s | |
780 self.sels = sels | |
781 return self | |
782 # end class Op | |
47 | 783 |
50 | 784 def __init__(self, ops): |
785 self.ops = ops | |
47 | 786 # end class Invocation |
787 | |
788 ############################################################ | |
789 # run (eval the SQL and print the results) | |
790 | |
791 def run_sel(sel, ids): | |
50 | 792 if sel.type == S_PR: |
793 if ids == []: | |
794 sys.stderr.write("No PRs matched.\n") | |
795 exit(1) | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
796 |
50 | 797 print_prs(ids) |
798 elif sel.type == S_MESSAGE: | |
799 if len(ids) != 1: | |
800 sys.stderr.write("Cannot retrieve messages " + | |
801 "from multiple PRs.") | |
802 exit(1) | |
803 print_message(ids[0], sel.message) | |
804 elif sel.type == S_ATTACHMENT: | |
805 if len(ids) != 1: | |
806 sys.stderr.write("Cannot retrieve attachments " + | |
807 "from multiple PRs.") | |
808 exit(1) | |
809 print_message(ids[0], sel.attachment) | |
810 else: | |
811 assert(False) | |
47 | 812 |
813 def run_op(op): | |
50 | 814 if op.type == OP_FIELDS: |
815 list_fields() | |
816 elif op.type == OP_SHOW: | |
817 describe_field(op.field) | |
818 elif op.type == OP_RANGE: | |
819 print_field_range(op.field) | |
820 elif op.type == OP_SEARCH: | |
821 sql = op.search | |
822 args = op.args # XXX not there! | |
823 ids = querydb(op.search, args) | |
824 for s in op.sels: | |
825 run_sel(s, ids) | |
826 else: | |
827 assert(False) | |
47 | 828 |
829 def run(ast): | |
50 | 830 for op in ast.ops: |
831 run_op(op) | |
47 | 832 |
833 ############################################################ | |
834 # compile (convert the AST so the searches are pure SQL) | |
835 | |
836 # | |
837 # XXX this doesn't work, we need to keep the interned strings | |
838 # on return from compile_query. | |
839 # | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
840 |
48 | 841 def matches(s, rx): |
842 # XXX | |
843 return True | |
844 | |
47 | 845 def compile_query(q): |
50 | 846 if q.type == Q_QSTRING: |
847 # XXX should use a split that honors quotes | |
848 terms = q.string.split() | |
849 terms = [dotstring(t) for t in terms] | |
850 return compile_query(doand(terms)) | |
851 if q.type == Q_TSTRING: | |
852 qb = QueryBuilder() | |
853 s = q.string | |
854 if matches(s, "^[0-9]+$"): | |
855 f = qb.getfield("number") | |
856 # Note: s is user-supplied but clean to insert directly | |
857 qb.addwhere("%s = %s" % (f, s)) | |
858 elif matches(s, "^[0-9]+-[0-9]+$"): | |
859 f = qb.getfield("number") | |
860 ss = s.split("-") | |
861 # Note: ss[] is user-supplied but clean | |
862 qb.addwhere("%s >= %s" % (f, ss[0])) | |
863 qb.addwhere("%s <= %s" % (f, ss[1])) | |
864 elif matches(s, "^[0-9]+-$"): | |
865 f = qb.getfield("number") | |
866 ss = s.split("-") | |
867 # Note: ss[] is user-supplied but clean | |
868 qb.addwhere("%s >= %s" % (f, ss[0])) | |
869 elif matches(s, "^-[0-9]+$"): | |
870 f = qb.getfield("number") | |
871 ss = s.split("-") | |
872 # Note: ss[] is user-supplied but clean | |
873 qb.addwhere("%s <= %s" % (f, ss[1])) | |
874 elif matches(s, "^[^:]+:[^:]+$"): | |
875 # XXX honor quoted terms | |
876 # XXX = or LIKE? | |
877 ss = s.split(":") | |
878 # ss[0] is not clean but if it's crap it won't match | |
879 f = qb.getfield(ss[0]) | |
880 # ss[1] is not clean, so intern it for safety | |
881 s = qb.intern(ss[1]) | |
882 qb.addwhere("%s = %s" % (f, s)) | |
883 elif matches(s, "^-[^:]+:[^:]+$"): | |
884 # XXX honor quoted terms | |
885 # XXX <> or NOT LIKE? | |
886 ss = s.split(":") | |
887 # ss[0] is not clean but if it's crap it won't match | |
888 f = qb.getfield(ss[0]) | |
889 # ss[1] is not clean, so intern it for safety | |
890 s = qb.intern(ss[1]) | |
891 qb.addwhere("%s <> %s" % (f, s)) | |
892 elif matches(s, "^-"): | |
893 # XXX <> or NOT LIKE? | |
894 f = qb.getfield("alltext") | |
895 # s is not clean, so intern it for safety | |
896 s = qb.intern(s) | |
897 qb.addwhere("%s <> %s" % (f, s)) | |
898 else: | |
899 # XXX = or LIKE? | |
900 f = qb.getfield("alltext") | |
901 # s is not clean, so intern it for safety | |
902 s = qb.intern(s) | |
903 qb.addwhere("%s = %s" % (f, s)) | |
47 | 904 |
50 | 905 # XXX also does not handle: |
906 # | |
907 # field: with no string (supposed to use a default | |
908 # search string) | |
909 # | |
910 # generated search fields that parse dates: | |
911 # {arrived,closed,modified,etc.}-{before,after}:date | |
912 # | |
913 # stale:time | |
47 | 914 |
50 | 915 return qb.build("PRs.id") |
916 # end Q_TSTRING case | |
917 if q.type == Q_OR: | |
918 subqueries = ["(" + compile_query(sq) + ")" for sq in q.args] | |
919 return " UNION ".join(subqueries) | |
920 if q.type == Q_AND: | |
921 subqueries = ["(" + compile_query(sq) + ")" for sq in q.args] | |
922 return " INTERSECT ".join(subqueries) | |
923 if q.type == Q_SQL: | |
924 return q.sql | |
925 assert(False) | |
47 | 926 # end compile_query |
927 | |
928 def compile_order(qb, o): | |
50 | 929 str = qb.getfield(o.field) |
930 if o.rev: | |
931 str = str + " DESCENDING" | |
932 return str | |
47 | 933 |
934 def compile_search(s): | |
50 | 935 qb2 = QueryBuilder() |
47 | 936 |
50 | 937 # multiple query strings are treated as OR |
938 query = door(s.queries) | |
939 query = compile_query(q) | |
47 | 940 |
50 | 941 if s.openonly: |
942 qb2.addwhere("not %s" % qb.getfield("closed")) | |
943 if s.publiconly: | |
944 qb2.addwhere("not %s" % qb.getfield("confidential")) | |
47 | 945 |
50 | 946 orders = [compile_order(qb2, o) for o in s.orders] |
947 order = ", ".join(orders) | |
948 if order != "": | |
949 qb2.setorder(order) | |
47 | 950 |
50 | 951 if qb2.nonempty(): |
952 qb2.addjoin(query, "search") | |
953 qb2.addjoin("PRs") | |
954 qb2.addwhere("search = PRs.id") | |
955 query = qb2.build(["search"]) | |
47 | 956 |
50 | 957 return query |
47 | 958 # end compile_search |
959 | |
960 def compile_op(op): | |
50 | 961 if op.type == OP_SEARCH: |
962 op.search = compile_search(op.search) | |
963 return op | |
47 | 964 |
965 def compile(ast): | |
50 | 966 ast.ops = [compile_op(op) for op in ast.ops] |
47 | 967 |
968 ############################################################ | |
969 # arg handling | |
970 | |
971 # | |
972 # I swear, all getopt interfaces suck. | |
973 # | |
974 # Provide an argparse action for constructing something out of the | |
975 # argument value and appending that somewhere, since it can't do this | |
976 # on its own. | |
977 # | |
978 # The way you use this: | |
979 # p.add_argument("--foo", action = CtorAppend, dest = 'mylist', | |
980 # const = ctor) | |
981 # where ctor is a function taking the option arg(s) and producing | |
982 # a value to append to mylist. | |
983 # | |
984 # This itself is mangy even for what it is -- it seems like we should | |
985 # be able to pass action=CtorAppend(ctor), but since it has to be a | |
986 # class that doesn't work... unless you make a new class for every | |
987 # ctor you want to use, which seems completely insane. | |
988 # | |
989 class CtorAppend(argparse.Action): | |
50 | 990 def __call__(self, parser, namespace, values, option_string=None): |
991 items = getattr(namespace, self.dest) | |
992 item = self.const(values) | |
993 items.append(item) | |
994 setattr(namespace, self.dest, items) | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
995 |
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
996 def getargs(): |
50 | 997 p = argparse.ArgumentParser(program_description) |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
998 |
50 | 999 # note: -h/--help is built in by default |
1000 p.add_argument("-v", "--version", | |
1001 action='version', version=program_version, | |
1002 help="Print program version and exit") | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1003 |
50 | 1004 p.add_argument("--show", nargs=1, |
1005 action=CtorAppend, dest='ops', | |
1006 const=Invocation.Op.doshow, | |
1007 help="Show description of field") | |
1008 p.add_argument("--range", nargs=1, | |
1009 action=CtorAppend, dest='ops', | |
1010 const=Invocation.Op.dorange, | |
1011 help="Show range of extant values for field") | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1012 |
50 | 1013 p.add_argument("--search", nargs=1, |
1014 action=CtorAppend, dest='queries', | |
1015 const=Invocation.Query.doqstring, | |
1016 help="Force string to be read as a search string") | |
1017 p.add_argument("-s", "--sql", nargs=1, | |
1018 action=CtorAppend, dest='queries', | |
1019 const=Invocation.Query.dosql, | |
1020 help="Supply explicit sql query as search") | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1021 |
50 | 1022 p.add_argument("--open", |
1023 action='store_const', dest='openonly', const="True", | |
1024 help="Exclude closed PRs (default)") | |
1025 p.add_argument("--closed", | |
1026 action='store_const', dest='openonly', const="False", | |
1027 help="Include closed PRs in search") | |
1028 p.add_argument("--public", | |
1029 action='store_const', dest='publiconly', const="True", | |
1030 help="Exclude confidential PRs") | |
1031 p.add_argument("--privileged", | |
1032 action='store_const', dest='publiconly', const="False", | |
1033 help="Allow confidential PRs (default)") | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1034 |
50 | 1035 p.add_argument("--oldest", |
1036 action=CtorAppend, dest='orders', | |
1037 const=Invocation.Order.dooldest, | |
1038 help="Sort output with oldest PRs first") | |
1039 p.add_argument("--newest", | |
1040 action=CtorAppend, dest='orders', | |
1041 const=Invocation.Order.donewest, | |
1042 help="Sort output with newest PRs first") | |
1043 p.add_argument("--staleness", | |
1044 action=CtorAppend, dest='orders', | |
1045 const=Invocation.Order.dostaleness, | |
1046 help="Sort output by time since last modification") | |
1047 p.add_argument("--orderby", nargs=1, | |
1048 action=CtorAppend, dest='orders', | |
1049 const=Invocation.Order.dofield, | |
1050 help="Sort output by specific field") | |
1051 p.add_argument("--revorderby", nargs=1, | |
1052 action=CtorAppend, dest='orders', | |
1053 const=Invocation.Order.dorevfield, | |
1054 help="Sort output by specific field, reversed") | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1055 |
50 | 1056 p.add_argument("-m", "--message", nargs=1, |
1057 action=CtorAppend, dest='selections', | |
1058 const=Invocation.Selection.domessage, | |
1059 help="Print selected message (single PR only)") | |
1060 p.add_argument("-a", "--attachment", nargs=1, | |
1061 action=CtorAppend, dest='selections', | |
1062 const=Invocation.Selection.doattachment, | |
1063 help="Print selected attachment (single PR only)") | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1064 |
50 | 1065 p.add_argument("-r", "--raw", |
1066 action = 'store_const', const="RAW", | |
1067 dest = 'output', | |
1068 help="Print exactly what the database returns") | |
1069 p.add_argument("-l", "--list", | |
1070 action = 'store_const', const="LIST", | |
1071 dest = 'output', | |
1072 help="Print in list form (default)") | |
1073 p.add_argument("--headers", | |
1074 action = 'store_const', const="HEADERS", | |
1075 dest = 'output', | |
1076 help="Print header information only") | |
1077 p.add_argument("--meta", | |
1078 action = 'store_const', const="META", | |
1079 dest = 'output', | |
1080 help="Print all metadata") | |
1081 p.add_argument("--metadata", | |
1082 action = 'store_const', const="META", | |
1083 dest = 'output') | |
1084 p.add_argument("-f", "--full", | |
1085 action = 'store_const', const="FULL", | |
1086 dest = 'output', | |
1087 help="Print everything") | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1088 |
50 | 1089 p.add_argument("--text", |
1090 action = 'store_const', const="TEXT", | |
1091 dest = 'outformat', | |
1092 help="Print in text format (default)") | |
1093 p.add_argument("--csv", | |
1094 action = 'store_const', const="CSV", | |
1095 dest = 'outformat', | |
1096 help="Print a CSV file") | |
1097 p.add_argument("--xml", | |
1098 action = 'store_const', const="XML", | |
1099 dest = 'outformat', | |
1100 help="Print in XML") | |
1101 p.add_argument("--json", | |
1102 action = 'store_const', const="JSON", | |
1103 dest = 'outformat', | |
1104 help="Print in JSON") | |
1105 p.add_argument("--rdf", | |
1106 action = 'store_const', const="RDF", | |
1107 dest = 'outbformat', | |
1108 help="Print in RDF") | |
1109 p.add_argument("--rdflike", | |
1110 action = 'store_const', const="RDFLIKE", | |
1111 dest = 'outformat', | |
1112 help="Print RDF-like text") | |
47 | 1113 |
50 | 1114 p.add_argument("TERM", nargs='*', |
1115 action=CtorAppend, dest='queries', | |
1116 const=Invocation.Query.doqstring, | |
1117 help="Search term") | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1118 |
50 | 1119 args = p.parse_args() |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1120 |
50 | 1121 ops = args.ops |
1122 if ops is None: | |
1123 ops = [] | |
1124 queries = args.queries | |
1125 if queries is not None: | |
1126 openonly = args.openonly | |
1127 if openonly is None: | |
1128 openonly = True | |
1129 publiconly = args.publiconly | |
1130 if publiconly is None: | |
1131 publiconly = False | |
1132 orders = args.orders | |
1133 if orders is None: | |
1134 orders = [Invocation.Order.dooldest(None)] | |
1135 output = args.output | |
1136 if output is None: | |
1137 output = "LIST" | |
1138 outformat = args.outformat | |
1139 if outformat is None: | |
1140 outformat = "TEXT" | |
1141 selections = args.selections | |
1142 if selections is None: | |
1143 sel = Invocation.Selection.dopr(output, outformat) | |
1144 selections = [sel] | |
1145 search = Search(queries, openonly, publiconly, orders) | |
1146 op = dosearch(search, selections) | |
1147 ops.append(op) | |
1148 # endif | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1149 |
50 | 1150 return Invocation(ops) |
33 | 1151 # end getargs |
1152 | |
1153 ############################################################ | |
1154 # main | |
1155 | |
47 | 1156 todo = getargs() |
1157 opendb() | |
1158 fetch_classifications() | |
1159 todo = compile(todo) | |
1160 run(todo) | |
1161 closedb() | |
1162 exit(0) | |
46
73e6dac29391
new stuff (checkpoint when moved between machines)
David A. Holland
parents:
33
diff
changeset
|
1163 |