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