changeset 50:bb0ece71355e

Untabify.
author David A. Holland
date Sat, 02 Apr 2022 18:10:52 -0400
parents 4b7f0ee35994
children ef6d572c4e1e
files shelltools/query-pr/query.py
diffstat 1 files changed, 883 insertions(+), 883 deletions(-) [+]
line wrap: on
line diff
--- a/shelltools/query-pr/query.py	Sat Apr 02 18:10:30 2022 -0400
+++ b/shelltools/query-pr/query.py	Sat Apr 02 18:10:52 2022 -0400
@@ -30,190 +30,190 @@
 #
 
 class QueryBuilder:
-	# these fields are in the PRs table
-	prtable_fields = [
-		"id", "synopsis", "confidential", "state", "locked",
-		"timeout_date", "timeout_state",
-		"arrival_schemaversion", "arrival_date", "modified_date",
-		"closed_date",
-		"release", "environment"
-	]
+        # these fields are in the PRs table
+        prtable_fields = [
+                "id", "synopsis", "confidential", "state", "locked",
+                "timeout_date", "timeout_state",
+                "arrival_schemaversion", "arrival_date", "modified_date",
+                "closed_date",
+                "release", "environment"
+        ]
 
-	# these fields are aliases for others
-	alias_fields = {
-		"number" : "id",
-		"date" : "arrival_date",
-	}
+        # these fields are aliases for others
+        alias_fields = {
+                "number" : "id",
+                "date" : "arrival_date",
+        }
 
-	def __init__(self):
-		self.present = {}
-		self.joined = {}
-		self.fromitems = []
-		self.whereitems = []
-		self.order = None
+        def __init__(self):
+                self.present = {}
+                self.joined = {}
+                self.fromitems = []
+                self.whereitems = []
+                self.order = None
 
-	def setorder(self, order):
-		self.order = order
+        def setorder(self, order):
+                self.order = order
 
-	# add to present{} and return the value for convenience (internal)
-	def makepresent(self, field, name):
-		self.present[field] = name
-		return name
+        # add to present{} and return the value for convenience (internal)
+        def makepresent(self, field, name):
+                self.present[field] = name
+                return name
 
-	# add a join item (once only) (internal)
-	def addjoin(self, table, as_ = None):
-		if as_ is not None:
-			key = table + "-" + as_
-			val = table + " AS " + as_
-		else:
-			key = table
-			val = table
-		if key not in self.joined:
-			self.joined[key] = True
-			self.fromitems.append(val)
+        # add a join item (once only) (internal)
+        def addjoin(self, table, as_ = None):
+                if as_ is not None:
+                        key = table + "-" + as_
+                        val = table + " AS " + as_
+                else:
+                        key = table
+                        val = table
+                if key not in self.joined:
+                        self.joined[key] = True
+                        self.fromitems.append(val)
 
-	# returns a sql expression for the field
-	def getfield(self, field):
-		# already-fetched fields
-		if field in self.present:
-			return self.present[field]
+        # returns a sql expression for the field
+        def getfield(self, field):
+                # already-fetched fields
+                if field in self.present:
+                        return self.present[field]
 
-		# aliases for other fields
-		if field in alias_fields:
-			return self.getfield(alias_fields[field])
+                # aliases for other fields
+                if field in alias_fields:
+                        return self.getfield(alias_fields[field])
 
-		# simple fields directly in the PRs table
-		if field in prtable_fields:
-			self.addjoin("PRs")
-			return self.makepresent(field, "PRs." + field)
+                # simple fields directly in the PRs table
+                if field in prtable_fields:
+                        self.addjoin("PRs")
+                        return self.makepresent(field, "PRs." + field)
 
-		# now it gets more interesting...
-		if field == "closed":
-			self.addjoin("PRs")
-			self.addjoin("states")
-			self.addwhere("PRs.state = states.name")
-			return self.makepresent(field, "states.closed")
+                # now it gets more interesting...
+                if field == "closed":
+                        self.addjoin("PRs")
+                        self.addjoin("states")
+                        self.addwhere("PRs.state = states.name")
+                        return self.makepresent(field, "states.closed")
 
-		# XXX let's pick one set of names and use them everywhere
-		# (e.g. change "posttime" in the schema to "message_date"
-		# or something)
-		if field == "comment_date" or field == "posttime":
-			self.addjoin("PRs")
-			self.addjoin("messages")
-			self.addwhere("PRs.id = messages.pr")
-			return self.makepresent(field, "messages.posttime")
+                # XXX let's pick one set of names and use them everywhere
+                # (e.g. change "posttime" in the schema to "message_date"
+                # or something)
+                if field == "comment_date" or field == "posttime":
+                        self.addjoin("PRs")
+                        self.addjoin("messages")
+                        self.addwhere("PRs.id = messages.pr")
+                        return self.makepresent(field, "messages.posttime")
 
-		if field == "comment" or field == "message" or field == "post":
-			self.addjoin("PRs")
-			self.addjoin("messages")
-			self.addwhere("PRs.id = messages.pr")
-			return self.makepresent(field, "messages.body")
+                if field == "comment" or field == "message" or field == "post":
+                        self.addjoin("PRs")
+                        self.addjoin("messages")
+                        self.addwhere("PRs.id = messages.pr")
+                        return self.makepresent(field, "messages.body")
 
-		if field == "attachment":
-			self.addjoin("PRs")
-			self.addjoin("messages")
-			self.addjoin("attachments")
-			self.addwhere("PRs.id = messages.pr")
-			self.addwhere("messages.id = attachments.msgid")
-			return self.makepresent(field, "attachments.body")
+                if field == "attachment":
+                        self.addjoin("PRs")
+                        self.addjoin("messages")
+                        self.addjoin("attachments")
+                        self.addwhere("PRs.id = messages.pr")
+                        self.addwhere("messages.id = attachments.msgid")
+                        return self.makepresent(field, "attachments.body")
 
-		if field == "patch":
-			self.addjoin("PRs")
-			self.addjoin("messages")
-			self.addjoin("attachments", "patches")
-			self.addwhere("PRs.id = messages.pr")
-			self.addwhere("messages.id = patches.msgid")
-			self.addwhere("patches.mimetype = " +
-					"'application/x-patch'")
-			return self.makepresent(field, "patches.body")
+                if field == "patch":
+                        self.addjoin("PRs")
+                        self.addjoin("messages")
+                        self.addjoin("attachments", "patches")
+                        self.addwhere("PRs.id = messages.pr")
+                        self.addwhere("messages.id = patches.msgid")
+                        self.addwhere("patches.mimetype = " +
+                                        "'application/x-patch'")
+                        return self.makepresent(field, "patches.body")
 
-		if field == "mimetype":
-			subquery = "((SELECT mtmessages1.pr as pr, " + \
-				"mtmessages1.mimetype as mimetype " + \
-				"FROM messages as mtmessages1) " + \
-				"UNION " + \
-				"(SELECT mtmessages2.pr as pr, " + \
-				"mtattach2.mimetype as mimetype " + \
-				"FROM messages as mtmessages2, " + \
-				"     attachments as mtattach2 " + \
-				"WHERE mtmessages2.id = mtattach2.msgid))"
-			self.addjoin("PRs")
-			self.addjoin(subquery, "mimetypes")
-			self.addwhere("PRs.id = mimetypes.pr")
-			return self.makepresent(field, "mimetypes.mimetype")
+                if field == "mimetype":
+                        subquery = "((SELECT mtmessages1.pr as pr, " + \
+                                "mtmessages1.mimetype as mimetype " + \
+                                "FROM messages as mtmessages1) " + \
+                                "UNION " + \
+                                "(SELECT mtmessages2.pr as pr, " + \
+                                "mtattach2.mimetype as mimetype " + \
+                                "FROM messages as mtmessages2, " + \
+                                "     attachments as mtattach2 " + \
+                                "WHERE mtmessages2.id = mtattach2.msgid))"
+                        self.addjoin("PRs")
+                        self.addjoin(subquery, "mimetypes")
+                        self.addwhere("PRs.id = mimetypes.pr")
+                        return self.makepresent(field, "mimetypes.mimetype")
 
-		# XXX: need view userstrings
-		# select (id, username as name) from users
-		# union select (id, realname as name) from users
-		# (allow searching emails? ugh)
-		if field == "originator" or field == "submitter":
-			self.addjoin("PRs")
-			self.addjoin("userstrings", "originators")
-			self.addwhere("PRs.originator = originators.id")
-			return self.makepresent(field, "originators.name")
+                # XXX: need view userstrings
+                # select (id, username as name) from users
+                # union select (id, realname as name) from users
+                # (allow searching emails? ugh)
+                if field == "originator" or field == "submitter":
+                        self.addjoin("PRs")
+                        self.addjoin("userstrings", "originators")
+                        self.addwhere("PRs.originator = originators.id")
+                        return self.makepresent(field, "originators.name")
 
-		if field == "reporter" or field == "respondent":
-			self.addjoin("PRs")
-			self.addjoin("subscriptions")
-			self.addjoin("userstrings", "reporters")
-			self.addwhere("subscriptions.userid = reporters.id")
-			self.addwhere("subscriptions.reporter")
-			return self.makepresent(field, "reporters.name")
+                if field == "reporter" or field == "respondent":
+                        self.addjoin("PRs")
+                        self.addjoin("subscriptions")
+                        self.addjoin("userstrings", "reporters")
+                        self.addwhere("subscriptions.userid = reporters.id")
+                        self.addwhere("subscriptions.reporter")
+                        return self.makepresent(field, "reporters.name")
 
-		if field == "responsible":
-			self.addjoin("PRs")
-			self.addjoin("subscriptions")
-			self.addjoin("userstrings", "responsibles")
-			self.addwhere("subscriptions.userid = responsibles.id")
-			self.addwhere("subscriptions.responsible")
-			return self.makepresent(field, "responsibles.name")
+                if field == "responsible":
+                        self.addjoin("PRs")
+                        self.addjoin("subscriptions")
+                        self.addjoin("userstrings", "responsibles")
+                        self.addwhere("subscriptions.userid = responsibles.id")
+                        self.addwhere("subscriptions.responsible")
+                        return self.makepresent(field, "responsibles.name")
 
-		if field in hierclasses:
-			col = field + "_data"
-			self.addjoin("PRs")
-			self.addjoin("hierclass_data", col)
-			self.addwhere("PRs.id = %s.pr" % col)
-			self.addwhere("%s.scheme = '%s'" % (col, field))
-			return self.makepresent(field, "%s.value" % col)
+                if field in hierclasses:
+                        col = field + "_data"
+                        self.addjoin("PRs")
+                        self.addjoin("hierclass_data", col)
+                        self.addwhere("PRs.id = %s.pr" % col)
+                        self.addwhere("%s.scheme = '%s'" % (col, field))
+                        return self.makepresent(field, "%s.value" % col)
 
-		if field in flatclasses:
-			col = field + "_data"
-			self.addjoin("PRs")
-			self.addjoin("flatclass_data", col)
-			self.addwhere("PRs.id = %s.pr" % col)
-			self.addwhere("%s.scheme = '%s'" % (col, field))
-			return self.makepresent(field, "%s.value" % col)
+                if field in flatclasses:
+                        col = field + "_data"
+                        self.addjoin("PRs")
+                        self.addjoin("flatclass_data", col)
+                        self.addwhere("PRs.id = %s.pr" % col)
+                        self.addwhere("%s.scheme = '%s'" % (col, field))
+                        return self.makepresent(field, "%s.value" % col)
 
-		if field in textclasses:
-			col = field + "_data"
-			self.addjoin("PRs")
-			self.addjoin("textclass_data", col)
-			self.addwhere("PRs.id = %s.pr" % col)
-			self.addwhere("%s.scheme = '%s'" % (col, field))
-			return self.makepresent(field, "%s.value" % col)
+                if field in textclasses:
+                        col = field + "_data"
+                        self.addjoin("PRs")
+                        self.addjoin("textclass_data", col)
+                        self.addwhere("PRs.id = %s.pr" % col)
+                        self.addwhere("%s.scheme = '%s'" % (col, field))
+                        return self.makepresent(field, "%s.value" % col)
 
-		if field in tagclasses:
-			col = field + "_data"
-			self.addjoin("PRs")
-			self.addjoin("tagclass_data", col)
-			self.addwhere("PRs.id = %s.pr" % col)
-			self.addwhere("%s.scheme = '%s'" % (col, field))
-			return self.makepresent(field, "%s.value" % col)
+                if field in tagclasses:
+                        col = field + "_data"
+                        self.addjoin("PRs")
+                        self.addjoin("tagclass_data", col)
+                        self.addwhere("PRs.id = %s.pr" % col)
+                        self.addwhere("%s.scheme = '%s'" % (col, field))
+                        return self.makepresent(field, "%s.value" % col)
 
-		sys.stderr.write("Unknown field %s" % field)
-		exit(1)
-	# end getfield
+                sys.stderr.write("Unknown field %s" % field)
+                exit(1)
+        # end getfield
 
-	# emit sql
-	def build(self, sels):
-		s = ", ".join(sels)
-		f = ", ".join(self.fromitems)
-		w = " and ".join(self.whereitems)
-		q = "SELECT %s\nFROM %s\nWHERE %s\n" % (s, f, w)
-		if self.order is not None:
-			q = q + "ORDER BY " + self.order + "\n"
-		return q
-	# endif
+        # emit sql
+        def build(self, sels):
+                s = ", ".join(sels)
+                f = ", ".join(self.fromitems)
+                w = " and ".join(self.whereitems)
+                q = "SELECT %s\nFROM %s\nWHERE %s\n" % (s, f, w)
+                if self.order is not None:
+                        q = q + "ORDER BY " + self.order + "\n"
+                return q
+        # endif
 
 # end class QueryBuilder
 
@@ -229,33 +229,33 @@
 dblink = None
 
 def opendb():
-	global dblink
+        global dblink
 
-	host = "localhost"
-	user = "swallowtail"
-	database = "swallowtail"
-	dblink = psycopg2.connect("host=%s user=%s dbname=%s" %
-					(host, user, database))
+        host = "localhost"
+        user = "swallowtail"
+        database = "swallowtail"
+        dblink = psycopg2.connect("host=%s user=%s dbname=%s" %
+                                        (host, user, database))
 # end opendb
 
 def closedb():
-	global dblink
+        global dblink
 
-	dblink.close()
-	dblink = None
+        dblink.close()
+        dblink = None
 # end closedb
 
 def querydb(qtext, args):
-	print "Executing this query:"
-	print qtext
-	print "Args are:"
-	print args
+        print "Executing this query:"
+        print qtext
+        print "Args are:"
+        print args
 
-	cursor = dblink.cursor()
-	cursor.execute(qtext, args)
-	result = cursor.fetchall()
-	cursor.close()
-	return result
+        cursor = dblink.cursor()
+        cursor.execute(qtext, args)
+        result = cursor.fetchall()
+        cursor.close()
+        return result
 # end querydb
 
 ############################################################
@@ -263,58 +263,58 @@
 # XXX: obsolete, remove
 
 class Query:
-	def __init__(self):
-		self.selections = []
-		self.tables = []
-		self.constraints = []
-		self.args = []
-	prtables = ["PRs"]
-	prconstraints = []
+        def __init__(self):
+                self.selections = []
+                self.tables = []
+                self.constraints = []
+                self.args = []
+        prtables = ["PRs"]
+        prconstraints = []
 
-	def select(self, s):
-		self.selections.append(s)
+        def select(self, s):
+                self.selections.append(s)
 
-	def addtable(self, t):
-		assert(t not in self.tables)
-		self.tables.append(t)
+        def addtable(self, t):
+                assert(t not in self.tables)
+                self.tables.append(t)
 
-	def constrain(self, expr):
-		self.constraints.append(t)
+        def constrain(self, expr):
+                self.constraints.append(t)
 
-	def internval(self, val):
-		num = len(self.args)
-		self.args[num] = val
-		return "$%d" % num
+        def internval(self, val):
+                num = len(self.args)
+                self.args[num] = val
+                return "$%d" % num
 
-	def textify(self):
-		s = "SELECT %s\n" % ",".join(self.selections)
-		f = "FROM %s\n" % ",".join(self.tables)
-		w = "WHERE %s\n" % " AND ".join(self.constraints)
-		return s + f + w
+        def textify(self):
+                s = "SELECT %s\n" % ",".join(self.selections)
+                f = "FROM %s\n" % ",".join(self.tables)
+                w = "WHERE %s\n" % " AND ".join(self.constraints)
+                return s + f + w
 # end class Query
 
 def regexp_constraint(q, field, value):
-	cleanval = q.internval(value)
-	if not isregexp(value):
-		return "%s = %s" % (field, cleanval)
-	else:
-		# XXX what's the right operator again?
-		return "%s ~= %s" % (field, cleanval)
+        cleanval = q.internval(value)
+        if not isregexp(value):
+                return "%s = %s" % (field, cleanval)
+        else:
+                # XXX what's the right operator again?
+                return "%s ~= %s" % (field, cleanval)
 # end regexp_constraint
 
 def intrange_constraint(q, field, value):
-	(lower, upper) = args.number
-	if lower is not None:
-		assert(typeof(lower) == int)
-		prq.constrain("%s >= %d" % (field, lower))
-	if upper is not None:
-		assert(typeof(upper) == int)
-		prq.constrain("%s <= %d" % (field, upper))
+        (lower, upper) = args.number
+        if lower is not None:
+                assert(typeof(lower) == int)
+                prq.constrain("%s >= %d" % (field, lower))
+        if upper is not None:
+                assert(typeof(upper) == int)
+                prq.constrain("%s <= %d" % (field, upper))
 # end intrange_constraint
 
 def daterange_constraint(q, field, value):
-	# XXX
-	assert(0)
+        # XXX
+        assert(0)
 # end daterange_constraint
 
 ############################################################
@@ -322,513 +322,513 @@
 # this is old code that needs to be merged or deleted into the new stuff
 def oldstuff():
 
-	# If we're doing something other than a search, do it now
-	if args.attach is not None:
-		get_attachment(args.attach)
-		exit(0)
-	if args.message is not None:
-		get_message(args.message)
-		exit(0)
+        # If we're doing something other than a search, do it now
+        if args.attach is not None:
+                get_attachment(args.attach)
+                exit(0)
+        if args.message is not None:
+                get_message(args.message)
+                exit(0)
 
-	if args.prs is not None and len(args.prs) > 0:
-		show_prs(args.prs)
-		exit(0)
+        if args.prs is not None and len(args.prs) > 0:
+                show_prs(args.prs)
+                exit(0)
 
-	#
-	# Collect up the search constraints
-	#
-	
-	# 1. Constraints on the PRs table
-	checkprtable = False
-	prq = Query()
-	prq.select("PRs.id as id")
-	prq.addtable("PRs")
-	if not args.closed:
-		checkprtable = True
-		prq.addtable("states")
-		prq.constrain("PRs.state = states.name")
-		prq.constrain("states.closed = FALSE")
-	if args.public:
-		checkprtable = True
-		prq.constrain("NOT PRs.confidential")
-	if args.number is not None:
-		checkprtable = True
-		intrange_constraint(prq, "PRs.id", args.number)
-	if args.synopsis is not None:
-		checkprtable = True
-		regexp_constraint(prq, "PRs.synopsis", args.synopsis)
-	if args.confidential is not None:
-		checkprtable = True
-		assert(typeof(args.confidential) == bool)
-		if args.confidential:
-			prq.constrain("PRs.confidential")
-		else:
-			prq.constrain("not PRs.confidential")
-	if args.state is not None:
-		checkprtable = True
-		regexp_constraint(prq, "PRs.state", args.state)
-	if args.locked is not None:
-		checkprtable = True
-		assert(typeof(args.locked) == bool)
-		if args.locked:
-			prq.constrain("PRs.locked")
-		else:
-			prq.constrain("not PRs.locked")
-	if args.arrival_schemaversion is not None:
-		checkprtable = True
-		intrange_constraint(prq, "PRs.arrival_schemaversion",
-					args.arrival_schemaversion)
-	if args.arrival_date is not None:
-		checkprtable = True
-		daterange_constraint(prq, "PRs.arrival_date",
-					args.arrival_date)
-	if args.closed_date is not None:
-		checkprtable = True
-		daterange_constraint(prq, "PRs.closed_date",
-					args.closed_date)
-	if args.last_modified is not None:
-		checkprtable = True
-		daterange_constraint(prq, "PRs.last_modified",
-					args.last_modified)
-	if args.release is not None:
-		checkprtable = True
-		regexp_constraint(prq, "PRs.release", args.release)
-	if args.environment is not None:
-		checkprtable = True
-		regexp_constraint(prq, "PRs.environment", args.environment)
+        #
+        # Collect up the search constraints
+        #
+        
+        # 1. Constraints on the PRs table
+        checkprtable = False
+        prq = Query()
+        prq.select("PRs.id as id")
+        prq.addtable("PRs")
+        if not args.closed:
+                checkprtable = True
+                prq.addtable("states")
+                prq.constrain("PRs.state = states.name")
+                prq.constrain("states.closed = FALSE")
+        if args.public:
+                checkprtable = True
+                prq.constrain("NOT PRs.confidential")
+        if args.number is not None:
+                checkprtable = True
+                intrange_constraint(prq, "PRs.id", args.number)
+        if args.synopsis is not None:
+                checkprtable = True
+                regexp_constraint(prq, "PRs.synopsis", args.synopsis)
+        if args.confidential is not None:
+                checkprtable = True
+                assert(typeof(args.confidential) == bool)
+                if args.confidential:
+                        prq.constrain("PRs.confidential")
+                else:
+                        prq.constrain("not PRs.confidential")
+        if args.state is not None:
+                checkprtable = True
+                regexp_constraint(prq, "PRs.state", args.state)
+        if args.locked is not None:
+                checkprtable = True
+                assert(typeof(args.locked) == bool)
+                if args.locked:
+                        prq.constrain("PRs.locked")
+                else:
+                        prq.constrain("not PRs.locked")
+        if args.arrival_schemaversion is not None:
+                checkprtable = True
+                intrange_constraint(prq, "PRs.arrival_schemaversion",
+                                        args.arrival_schemaversion)
+        if args.arrival_date is not None:
+                checkprtable = True
+                daterange_constraint(prq, "PRs.arrival_date",
+                                        args.arrival_date)
+        if args.closed_date is not None:
+                checkprtable = True
+                daterange_constraint(prq, "PRs.closed_date",
+                                        args.closed_date)
+        if args.last_modified is not None:
+                checkprtable = True
+                daterange_constraint(prq, "PRs.last_modified",
+                                        args.last_modified)
+        if args.release is not None:
+                checkprtable = True
+                regexp_constraint(prq, "PRs.release", args.release)
+        if args.environment is not None:
+                checkprtable = True
+                regexp_constraint(prq, "PRs.environment", args.environment)
 
-	if args.originator_name is not None or \
-			args.originator_email is not None:
-		prq.addtable("usermail as originator")
-		prq.constrain("PRs.originator = originator.id")
-	if args.originator_name is not None:
-		checkprtable = True
-		regexp_constraint(prq, "originator.realname",
-					args.originator_name)
-	if args.originator_email is not None:
-		checkprtable = True
-		regexp_constraint(prq, "originator.email",
-					args.originator_name)
-	if args.originator_id is not None:
-		checkprtable = True
-		intrange_constraint(prq, "PRs.originator", args.originator_id)
+        if args.originator_name is not None or \
+                        args.originator_email is not None:
+                prq.addtable("usermail as originator")
+                prq.constrain("PRs.originator = originator.id")
+        if args.originator_name is not None:
+                checkprtable = True
+                regexp_constraint(prq, "originator.realname",
+                                        args.originator_name)
+        if args.originator_email is not None:
+                checkprtable = True
+                regexp_constraint(prq, "originator.email",
+                                        args.originator_name)
+        if args.originator_id is not None:
+                checkprtable = True
+                intrange_constraint(prq, "PRs.originator", args.originator_id)
 
-	queries = []
-	if checkprtable:
-		queries.append(prq)
+        queries = []
+        if checkprtable:
+                queries.append(prq)
 
-	if args.responsible is not None:
-		sq = Query()
-		sq.select("subscriptions.pr as id")
-		sq.addtable("subscriptions")
-		sq.addtable("users")
-		sq.constrain("subscriptions.userid = users.id")
-		regexp_constraint(sq, "users.realname", args.responsible)
-		sq.constrain("subscriptions.responsible")
-		queries.append(sq)
-	if args.respondent is not None:
-		sq = Query()
-		sq.select("subscriptions.pr as id")
-		sq.addtable("subscriptions")
-		sq.addtable("users as subscribed")
-		sq.constrain("subscriptions.userid = users.id")
-		regexp_constraint(sq, "users.realname", args.respondent)
-		sq.constrain("subscriptions.reporter")
-		queries.append(sq)
-	if args.subscribed is not None:
-		sq = Query()
-		sq.select("subscriptions.pr as id")
-		sq.addtable("subscriptions")
-		sq.addtable("users as subscribed")
-		sq.constrain("subscriptions.userid = users.id")
-		regexp_constraint(sq, "users.realname", args.subscribed)
-		queries.append(sq)
+        if args.responsible is not None:
+                sq = Query()
+                sq.select("subscriptions.pr as id")
+                sq.addtable("subscriptions")
+                sq.addtable("users")
+                sq.constrain("subscriptions.userid = users.id")
+                regexp_constraint(sq, "users.realname", args.responsible)
+                sq.constrain("subscriptions.responsible")
+                queries.append(sq)
+        if args.respondent is not None:
+                sq = Query()
+                sq.select("subscriptions.pr as id")
+                sq.addtable("subscriptions")
+                sq.addtable("users as subscribed")
+                sq.constrain("subscriptions.userid = users.id")
+                regexp_constraint(sq, "users.realname", args.respondent)
+                sq.constrain("subscriptions.reporter")
+                queries.append(sq)
+        if args.subscribed is not None:
+                sq = Query()
+                sq.select("subscriptions.pr as id")
+                sq.addtable("subscriptions")
+                sq.addtable("users as subscribed")
+                sq.constrain("subscriptions.userid = users.id")
+                regexp_constraint(sq, "users.realname", args.subscribed)
+                queries.append(sq)
 
-	if args.messages is not None:
-		mq = Query()
-		mq.select("messages.pr as id")
-		mq.addtable("messages")
-		regexp_constraint(sq, "messages.text", args.messages)
-		queries.append(mq)
+        if args.messages is not None:
+                mq = Query()
+                mq.select("messages.pr as id")
+                mq.addtable("messages")
+                regexp_constraint(sq, "messages.text", args.messages)
+                queries.append(mq)
 
-	if args.adminlog is not None:
-		aq = Query()
-		aq.select("adminlog.pr as id")
-		aq.addtable("adminlog")
-		regexp_constraint(sq, "adminlog.change", args.adminlog)
-		regexp_constraint(sq, "adminlog.comment", args.adminlog)
-		assert(len(aq.constraints) == 2)
-		x = "%s OR %s" % (aq.constraints[0], aq.constraints[1])
-		aq.constraints = [x]
-		queries.append(aq)
+        if args.adminlog is not None:
+                aq = Query()
+                aq.select("adminlog.pr as id")
+                aq.addtable("adminlog")
+                regexp_constraint(sq, "adminlog.change", args.adminlog)
+                regexp_constraint(sq, "adminlog.comment", args.adminlog)
+                assert(len(aq.constraints) == 2)
+                x = "%s OR %s" % (aq.constraints[0], aq.constraints[1])
+                aq.constraints = [x]
+                queries.append(aq)
 
-	if args.anytext is not None:
-		choke("--anytext isn't supported yet")
+        if args.anytext is not None:
+                choke("--anytext isn't supported yet")
 
-	for scheme in classification_schemes:	
-		if args[scheme] is not None:
-			schemetype = classification_schemetypes[scheme]
-			tbl = "%sclass_data" % schemetype
-			cq = Query()
-			cq.select("scheme.pr as id")
-			cq.addtable("%s as scheme" % schemetype)
-			cq.constrain("scheme.scheme = '%s'" % scheme)
-			regexp_constraint(cq, "scheme.value", args[scheme])
-			queries.append(cq)
-	# end loop
+        for scheme in classification_schemes:   
+                if args[scheme] is not None:
+                        schemetype = classification_schemetypes[scheme]
+                        tbl = "%sclass_data" % schemetype
+                        cq = Query()
+                        cq.select("scheme.pr as id")
+                        cq.addtable("%s as scheme" % schemetype)
+                        cq.constrain("scheme.scheme = '%s'" % scheme)
+                        regexp_constraint(cq, "scheme.value", args[scheme])
+                        queries.append(cq)
+        # end loop
 
-	querytexts = [q.textify() for q in queries]
-	return "INTERSECT\n".join(querytexts)
+        querytexts = [q.textify() for q in queries]
+        return "INTERSECT\n".join(querytexts)
 
 ############################################################
 # printing
 
 class PrintText:
-	def __init__(self, output):
-		self.lines = (output == "RAW" or output == "LIST")
-	def printheader(self, row):
-		# nothing
+        def __init__(self, output):
+                self.lines = (output == "RAW" or output == "LIST")
+        def printheader(self, row):
+                # nothing
                 pass
-	def printrow(self, row):
-		# XXX
-		print row
-	def printfooter(self, row):
-		# nothing
+        def printrow(self, row):
+                # XXX
+                print row
+        def printfooter(self, row):
+                # nothing
                 pass
 # end class PrintText
 
 class PrintCsv:
-	def __init__(self, output):
-		# nothing
+        def __init__(self, output):
+                # nothing
                 pass
-	def printheader(self, row):
-		# XXX
+        def printheader(self, row):
+                # XXX
                 pass
-	def printrow(self, row):
-		# XXX
+        def printrow(self, row):
+                # XXX
                 pass
-	def printfooter(self, row):
-		# nothing
+        def printfooter(self, row):
+                # nothing
                 pass
 # end class PrintCsv
 
 class PrintXml:
-	def __init__(self, output):
-		# nothing
+        def __init__(self, output):
+                # nothing
                 pass
-	def printheader(self, row):
-		# XXX
+        def printheader(self, row):
+                # XXX
                 pass
-	def printrow(self, row):
-		# XXX
+        def printrow(self, row):
+                # XXX
                 pass
-	def printfooter(self, row):
-		# XXX
+        def printfooter(self, row):
+                # XXX
                 pass
 # end class PrintXml
 
 class PrintJson:
-	def __init__(self, output):
-		# nothing
+        def __init__(self, output):
+                # nothing
                 pass
-	def printheader(self, row):
-		# XXX
+        def printheader(self, row):
+                # XXX
                 pass
-	def printrow(self, row):
-		# XXX
+        def printrow(self, row):
+                # XXX
                 pass
-	def printfooter(self, row):
-		# XXX
+        def printfooter(self, row):
+                # XXX
                 pass
 # end class PrintJson
 
 class PrintRdf:
-	def __init__(self, output):
-		# nothing
+        def __init__(self, output):
+                # nothing
                 pass
-	def printheader(self, row):
-		# XXX
+        def printheader(self, row):
+                # XXX
                 pass
-	def printrow(self, row):
-		# XXX
+        def printrow(self, row):
+                # XXX
                 pass
-	def printfooter(self, row):
-		# XXX
+        def printfooter(self, row):
+                # XXX
                 pass
 # end class PrintRdf
 
 class PrintRdflike:
-	def __init__(self, output):
-		# nothing
+        def __init__(self, output):
+                # nothing
                 pass
-	def printheader(self, row):
-		# XXX
+        def printheader(self, row):
+                # XXX
                 pass
-	def printrow(self, row):
-		# XXX
+        def printrow(self, row):
+                # XXX
                 pass
-	def printfooter(self, row):
-		# XXX
+        def printfooter(self, row):
+                # XXX
                 pass
 # end class PrintRdflike
 
 def print_prs(ids):
-	if sel.outformat == "TEXT":
-		mkprinter = PrintText
-	elif sel.outformat == "CSV":
-		mkprinter = PrintCsv
-	elif sel.outformat == "XML":
-		mkprinter = PrintXml
-	elif sel.outformat == "JSON":
-		mkprinter = PrintJson
-	elif sel.outformat == "RDF":
-		mkprinter = PrintRdf
-	elif sel.outformat == "RDFLIKE":
-		mkprinter = PrintRdflike
-	else:
-		assert(False)
+        if sel.outformat == "TEXT":
+                mkprinter = PrintText
+        elif sel.outformat == "CSV":
+                mkprinter = PrintCsv
+        elif sel.outformat == "XML":
+                mkprinter = PrintXml
+        elif sel.outformat == "JSON":
+                mkprinter = PrintJson
+        elif sel.outformat == "RDF":
+                mkprinter = PrintRdf
+        elif sel.outformat == "RDFLIKE":
+                mkprinter = PrintRdflike
+        else:
+                assert(False)
 
-	# reset the printer
-	printer = mkprinter(sel.output)
+        # reset the printer
+        printer = mkprinter(sel.output)
 
-	if sel.output == "RAW":
-		printer.printheader(ids[0])
-		for id in ids:
-			printer(id)
-		printer.printfooter(ids[0])
-		return
-	elif sel.output == "LIST":
-		# XXX is there a clean way to do this passing the
-		# whole list of ids at once?
-		query = "SELECT id, synopsis\n" + \
-			"FROM PRs\n" + \
-			"WHERE id = $1"
-	elif sel.output == "HEADERS":
-		query = None # XXX
-	elif sel.output == "META":
-		query = None # XXX
-	elif sel.output == "FULL":
-		query = None # XXX
-	else:
-		assert(False)
+        if sel.output == "RAW":
+                printer.printheader(ids[0])
+                for id in ids:
+                        printer(id)
+                printer.printfooter(ids[0])
+                return
+        elif sel.output == "LIST":
+                # XXX is there a clean way to do this passing the
+                # whole list of ids at once?
+                query = "SELECT id, synopsis\n" + \
+                        "FROM PRs\n" + \
+                        "WHERE id = $1"
+        elif sel.output == "HEADERS":
+                query = None # XXX
+        elif sel.output == "META":
+                query = None # XXX
+        elif sel.output == "FULL":
+                query = None # XXX
+        else:
+                assert(False)
 
-	first = True
-	for id in ids:
-		results = querydb(query, [id])
-		if first:
-			printer.printheader(results[0])
-			first = False
-		for r in results:
-			printer.printrow(r)
-	printer.printfooter(results[0])
+        first = True
+        for id in ids:
+                results = querydb(query, [id])
+                if first:
+                        printer.printheader(results[0])
+                        first = False
+                for r in results:
+                        printer.printrow(r)
+        printer.printfooter(results[0])
 # end print_prs
 
 # XXX if in public mode we need to check if the PR is public
 def print_message(pr, msgnum):
-	query = "SELECT users.username AS username,\n" + \
-		"       users.realname AS realname,\n" + \
-		"       messages.id AS id, parent_id,\n" + \
-		"       posttime, mimetype, body\n" + \
-		"FROM messages, users\n" + \
-		"WHERE messages.who = users.id\n" + \
-		"  AND messages.pr = $1\n" + \
-		"  AND messages.number_in_pr = $2\n"
-	# Note that while pr is safe, msgnum came from the commandline
-	# and may not be.
-	results = querydb(query, [pr, msgnum])
-	[result] = results
-	(username, realname, id, parent_id, posttime, mimetype, body) = result
-	# XXX honor mimetype
-	# XXX honor output format (e.g. html)
-	sys.stdout.write("From swallowtail@%s  %s\n" % (organization,posttime))
-	sys.stdout.write("From: %s (%s)\n" % (username, realname))
-	sys.stdout.write("References: %s\n" % parent_id)
-	sys.stdout.write("Date: %s\n" % posttime)
-	sys.stdout.write("Content-Type: %s\n" % mimetype)
-	sys.stdout.write("\n")
-	sys.stdout.write(body)
+        query = "SELECT users.username AS username,\n" + \
+                "       users.realname AS realname,\n" + \
+                "       messages.id AS id, parent_id,\n" + \
+                "       posttime, mimetype, body\n" + \
+                "FROM messages, users\n" + \
+                "WHERE messages.who = users.id\n" + \
+                "  AND messages.pr = $1\n" + \
+                "  AND messages.number_in_pr = $2\n"
+        # Note that while pr is safe, msgnum came from the commandline
+        # and may not be.
+        results = querydb(query, [pr, msgnum])
+        [result] = results
+        (username, realname, id, parent_id, posttime, mimetype, body) = result
+        # XXX honor mimetype
+        # XXX honor output format (e.g. html)
+        sys.stdout.write("From swallowtail@%s  %s\n" % (organization,posttime))
+        sys.stdout.write("From: %s (%s)\n" % (username, realname))
+        sys.stdout.write("References: %s\n" % parent_id)
+        sys.stdout.write("Date: %s\n" % posttime)
+        sys.stdout.write("Content-Type: %s\n" % mimetype)
+        sys.stdout.write("\n")
+        sys.stdout.write(body)
 # end print_message
 
 # XXX if in public mode we need to check if the PR is public
 def print_attachment(pr, attachnum):
-	query = "SELECT a.mimetype as mimetype, a.body as body\n" + \
-		"FROM messages, attachments as a\n" + \
-		"WHERE messages.pr = $1\n" + \
-		"  AND messages.id = a.msgid\n" + \
-		"  AND a.number_in_pr = $2\n"
-	# Note that while pr is safe, attachnum came from the
-	# commandline and may not be.
-	results = querydb(query, [pr, msgnum])
-	[result] = results
-	(mimetype, body) = result
-	# XXX honor mimetype
-	# XXX need an http output mode so we can send the mimetype!
-	sys.stdout.write(body)
+        query = "SELECT a.mimetype as mimetype, a.body as body\n" + \
+                "FROM messages, attachments as a\n" + \
+                "WHERE messages.pr = $1\n" + \
+                "  AND messages.id = a.msgid\n" + \
+                "  AND a.number_in_pr = $2\n"
+        # Note that while pr is safe, attachnum came from the
+        # commandline and may not be.
+        results = querydb(query, [pr, msgnum])
+        [result] = results
+        (mimetype, body) = result
+        # XXX honor mimetype
+        # XXX need an http output mode so we can send the mimetype!
+        sys.stdout.write(body)
 # end print_attachment
 
 ############################################################
 # AST for query
 
 class Invocation:
-	class Query:
-		Q_TERM = 1  # XXX unused so far
-		Q_SQL = 2
-		Q_AND = 3
-		Q_OR = 4
-		Q_TSTRING = 5
-		Q_QSTRING = 6
+        class Query:
+                Q_TERM = 1  # XXX unused so far
+                Q_SQL = 2
+                Q_AND = 3
+                Q_OR = 4
+                Q_TSTRING = 5
+                Q_QSTRING = 6
 
-		def __init__(self, type):
-			self.type = type
-		def doterm(term):
-			self = Query(Q_TERM)
-			self.term = term
-			return self
-		def dosql(s):
-			self = Query(Q_SQL)
-			self.sql = s
-			return self
-		def doand(qs):
-			self = Query(Q_AND)
-			self.args = qs
-			return self
-		def door(qs):
-			self = Query(Q_OR)
-			self.args = qs
-			return self
-		# query term string
-		def dotstring(q):
-			self = Query(Q_TSTRING)
-			self.string = q
-			return self
-		# whole query string
-		def doqstring(q):
-			self = Query(Q_QSTRING)
-			self.string = q
-			return self
-	# end class Query
+                def __init__(self, type):
+                        self.type = type
+                def doterm(term):
+                        self = Query(Q_TERM)
+                        self.term = term
+                        return self
+                def dosql(s):
+                        self = Query(Q_SQL)
+                        self.sql = s
+                        return self
+                def doand(qs):
+                        self = Query(Q_AND)
+                        self.args = qs
+                        return self
+                def door(qs):
+                        self = Query(Q_OR)
+                        self.args = qs
+                        return self
+                # query term string
+                def dotstring(q):
+                        self = Query(Q_TSTRING)
+                        self.string = q
+                        return self
+                # whole query string
+                def doqstring(q):
+                        self = Query(Q_QSTRING)
+                        self.string = q
+                        return self
+        # end class Query
 
-	class Order:
-		def __init__(self, field, rev = False):
-			self.field = field
-			self.rev = rev
-		def dooldest(ign):
-			return Order("number")
-		def donewest(ign):
-			return Order("number", True)
-		def dostaleness(ign):
-			return Order("modified_date", True)
-		def dofield(field):
-			return Order(field)
-		def dorevfield(field):
-			return Order(field, True)
-	# end class Order
+        class Order:
+                def __init__(self, field, rev = False):
+                        self.field = field
+                        self.rev = rev
+                def dooldest(ign):
+                        return Order("number")
+                def donewest(ign):
+                        return Order("number", True)
+                def dostaleness(ign):
+                        return Order("modified_date", True)
+                def dofield(field):
+                        return Order(field)
+                def dorevfield(field):
+                        return Order(field, True)
+        # end class Order
 
-	class Search:
-		def __init__(self, qs, openonly, publiconly, os):
-			self.queries = qs
-			self.openonly = openonly
-			self.publiconly = publiconly
-			self.orders = os
-	# end class Search
+        class Search:
+                def __init__(self, qs, openonly, publiconly, os):
+                        self.queries = qs
+                        self.openonly = openonly
+                        self.publiconly = publiconly
+                        self.orders = os
+        # end class Search
 
-	class Selection:
-		S_PR = 1
-		S_MESSAGE = 2
-		S_ATTACHMENT = 3
+        class Selection:
+                S_PR = 1
+                S_MESSAGE = 2
+                S_ATTACHMENT = 3
 
-		def __init__(self, type):
-			self.type = type
-		def dopr(output, outformat):
-			self = Selection(S_PR)
-			self.output = output
-			self.outformat = outformat
-			return self
-		def domessage(arg):
-			self = Selection(S_MESSAGE)
-			self.message = arg
-			return self
-		def doattachment(arg):
-			self = Selection(S_ATTACHMENT)
-			self.attachment = arg
-			return self
-	# end class Selection
+                def __init__(self, type):
+                        self.type = type
+                def dopr(output, outformat):
+                        self = Selection(S_PR)
+                        self.output = output
+                        self.outformat = outformat
+                        return self
+                def domessage(arg):
+                        self = Selection(S_MESSAGE)
+                        self.message = arg
+                        return self
+                def doattachment(arg):
+                        self = Selection(S_ATTACHMENT)
+                        self.attachment = arg
+                        return self
+        # end class Selection
 
-	class Op:
-		# operation codes
-		OP_FIELDS = 1
-		OP_SHOW = 2
-		OP_RANGE = 3
-		OP_SEARCH = 4
+        class Op:
+                # operation codes
+                OP_FIELDS = 1
+                OP_SHOW = 2
+                OP_RANGE = 3
+                OP_SEARCH = 4
 
-		def __init__(self, type):
-			self.type = type
+                def __init__(self, type):
+                        self.type = type
 
-		def dofields():
-			return Op(OP_FIELDS)
-		def doshow(field):
-			self = Op(OP_SHOW)
-			self.field = field
-			return self
-		def dorange(field):
-			self = Op(OP_RANGE)
-			self.field = field
-			return self
-		def dosearch(s, sels):
-			self = Op(OP_SEARCH)
-			self.search = s
-			self.sels = sels
-			return self
-	# end class Op
+                def dofields():
+                        return Op(OP_FIELDS)
+                def doshow(field):
+                        self = Op(OP_SHOW)
+                        self.field = field
+                        return self
+                def dorange(field):
+                        self = Op(OP_RANGE)
+                        self.field = field
+                        return self
+                def dosearch(s, sels):
+                        self = Op(OP_SEARCH)
+                        self.search = s
+                        self.sels = sels
+                        return self
+        # end class Op
 
-	def __init__(self, ops):
-		self.ops = ops
+        def __init__(self, ops):
+                self.ops = ops
 # end class Invocation
 
 ############################################################
 # run (eval the SQL and print the results)
 
 def run_sel(sel, ids):
-	if sel.type == S_PR:
-		if ids == []:
-			sys.stderr.write("No PRs matched.\n")
-			exit(1)
+        if sel.type == S_PR:
+                if ids == []:
+                        sys.stderr.write("No PRs matched.\n")
+                        exit(1)
 
-		print_prs(ids)
-	elif sel.type == S_MESSAGE:
-		if len(ids) != 1:
-			sys.stderr.write("Cannot retrieve messages " +
-				"from multiple PRs.")
-			exit(1)
-		print_message(ids[0], sel.message)
-	elif sel.type == S_ATTACHMENT:
-		if len(ids) != 1:
-			sys.stderr.write("Cannot retrieve attachments " +
-				"from multiple PRs.")
-			exit(1)
-		print_message(ids[0], sel.attachment)
-	else:
-		assert(False)
+                print_prs(ids)
+        elif sel.type == S_MESSAGE:
+                if len(ids) != 1:
+                        sys.stderr.write("Cannot retrieve messages " +
+                                "from multiple PRs.")
+                        exit(1)
+                print_message(ids[0], sel.message)
+        elif sel.type == S_ATTACHMENT:
+                if len(ids) != 1:
+                        sys.stderr.write("Cannot retrieve attachments " +
+                                "from multiple PRs.")
+                        exit(1)
+                print_message(ids[0], sel.attachment)
+        else:
+                assert(False)
 
 def run_op(op):
-	if op.type == OP_FIELDS:
-		list_fields()
-	elif op.type == OP_SHOW:
-		describe_field(op.field)
-	elif op.type == OP_RANGE:
-		print_field_range(op.field)
-	elif op.type == OP_SEARCH:
-		sql = op.search
-		args = op.args # XXX not there!
-		ids = querydb(op.search, args)
-		for s in op.sels:
-			run_sel(s, ids)
-	else:
-		assert(False)
+        if op.type == OP_FIELDS:
+                list_fields()
+        elif op.type == OP_SHOW:
+                describe_field(op.field)
+        elif op.type == OP_RANGE:
+                print_field_range(op.field)
+        elif op.type == OP_SEARCH:
+                sql = op.search
+                args = op.args # XXX not there!
+                ids = querydb(op.search, args)
+                for s in op.sels:
+                        run_sel(s, ids)
+        else:
+                assert(False)
 
 def run(ast):
-	for op in ast.ops:
-		run_op(op)
+        for op in ast.ops:
+                run_op(op)
 
 ############################################################
 # compile (convert the AST so the searches are pure SQL)
@@ -843,127 +843,127 @@
         return True
 
 def compile_query(q):
-	if q.type == Q_QSTRING:
-		# XXX should use a split that honors quotes
-		terms = q.string.split()
-		terms = [dotstring(t) for t in terms]
-		return compile_query(doand(terms))
-	if q.type == Q_TSTRING:
-		qb = QueryBuilder()
-		s = q.string
-		if matches(s, "^[0-9]+$"):
-			f = qb.getfield("number")
-			# Note: s is user-supplied but clean to insert directly
-			qb.addwhere("%s = %s" % (f, s))
-		elif matches(s, "^[0-9]+-[0-9]+$"):
-			f = qb.getfield("number")
-			ss = s.split("-")
-			# Note: ss[] is user-supplied but clean
-			qb.addwhere("%s >= %s" % (f, ss[0]))
-			qb.addwhere("%s <= %s" % (f, ss[1]))
-		elif matches(s, "^[0-9]+-$"):
-			f = qb.getfield("number")
-			ss = s.split("-")
-			# Note: ss[] is user-supplied but clean
-			qb.addwhere("%s >= %s" % (f, ss[0]))
-		elif matches(s, "^-[0-9]+$"):
-			f = qb.getfield("number")
-			ss = s.split("-")
-			# Note: ss[] is user-supplied but clean
-			qb.addwhere("%s <= %s" % (f, ss[1]))
-		elif matches(s, "^[^:]+:[^:]+$"):
-			# XXX honor quoted terms
-			# XXX = or LIKE?
-			ss = s.split(":")
-			# ss[0] is not clean but if it's crap it won't match
-			f = qb.getfield(ss[0])
-			# ss[1] is not clean, so intern it for safety
-			s = qb.intern(ss[1])
-			qb.addwhere("%s = %s" % (f, s))
-		elif matches(s, "^-[^:]+:[^:]+$"):
-			# XXX honor quoted terms
-			# XXX <> or NOT LIKE?
-			ss = s.split(":")
-			# ss[0] is not clean but if it's crap it won't match
-			f = qb.getfield(ss[0])
-			# ss[1] is not clean, so intern it for safety
-			s = qb.intern(ss[1])
-			qb.addwhere("%s <> %s" % (f, s))
-		elif matches(s, "^-"):
-			# XXX <> or NOT LIKE?
-			f = qb.getfield("alltext")
-			# s is not clean, so intern it for safety
-			s = qb.intern(s)
-			qb.addwhere("%s <> %s" % (f, s))
-		else:
-			# XXX = or LIKE?
-			f = qb.getfield("alltext")
-			# s is not clean, so intern it for safety
-			s = qb.intern(s)
-			qb.addwhere("%s = %s" % (f, s))
+        if q.type == Q_QSTRING:
+                # XXX should use a split that honors quotes
+                terms = q.string.split()
+                terms = [dotstring(t) for t in terms]
+                return compile_query(doand(terms))
+        if q.type == Q_TSTRING:
+                qb = QueryBuilder()
+                s = q.string
+                if matches(s, "^[0-9]+$"):
+                        f = qb.getfield("number")
+                        # Note: s is user-supplied but clean to insert directly
+                        qb.addwhere("%s = %s" % (f, s))
+                elif matches(s, "^[0-9]+-[0-9]+$"):
+                        f = qb.getfield("number")
+                        ss = s.split("-")
+                        # Note: ss[] is user-supplied but clean
+                        qb.addwhere("%s >= %s" % (f, ss[0]))
+                        qb.addwhere("%s <= %s" % (f, ss[1]))
+                elif matches(s, "^[0-9]+-$"):
+                        f = qb.getfield("number")
+                        ss = s.split("-")
+                        # Note: ss[] is user-supplied but clean
+                        qb.addwhere("%s >= %s" % (f, ss[0]))
+                elif matches(s, "^-[0-9]+$"):
+                        f = qb.getfield("number")
+                        ss = s.split("-")
+                        # Note: ss[] is user-supplied but clean
+                        qb.addwhere("%s <= %s" % (f, ss[1]))
+                elif matches(s, "^[^:]+:[^:]+$"):
+                        # XXX honor quoted terms
+                        # XXX = or LIKE?
+                        ss = s.split(":")
+                        # ss[0] is not clean but if it's crap it won't match
+                        f = qb.getfield(ss[0])
+                        # ss[1] is not clean, so intern it for safety
+                        s = qb.intern(ss[1])
+                        qb.addwhere("%s = %s" % (f, s))
+                elif matches(s, "^-[^:]+:[^:]+$"):
+                        # XXX honor quoted terms
+                        # XXX <> or NOT LIKE?
+                        ss = s.split(":")
+                        # ss[0] is not clean but if it's crap it won't match
+                        f = qb.getfield(ss[0])
+                        # ss[1] is not clean, so intern it for safety
+                        s = qb.intern(ss[1])
+                        qb.addwhere("%s <> %s" % (f, s))
+                elif matches(s, "^-"):
+                        # XXX <> or NOT LIKE?
+                        f = qb.getfield("alltext")
+                        # s is not clean, so intern it for safety
+                        s = qb.intern(s)
+                        qb.addwhere("%s <> %s" % (f, s))
+                else:
+                        # XXX = or LIKE?
+                        f = qb.getfield("alltext")
+                        # s is not clean, so intern it for safety
+                        s = qb.intern(s)
+                        qb.addwhere("%s = %s" % (f, s))
 
-		# XXX also does not handle:
-		#
-		# field: with no string (supposed to use a default
-		# search string)
-		#
-		# generated search fields that parse dates:
-		# {arrived,closed,modified,etc.}-{before,after}:date
-		#
-		# stale:time
+                # XXX also does not handle:
+                #
+                # field: with no string (supposed to use a default
+                # search string)
+                #
+                # generated search fields that parse dates:
+                # {arrived,closed,modified,etc.}-{before,after}:date
+                #
+                # stale:time
 
-		return qb.build("PRs.id")
-	# end Q_TSTRING case
-	if q.type == Q_OR:
-		subqueries = ["(" + compile_query(sq) + ")" for sq in q.args]
-		return " UNION ".join(subqueries)
-	if q.type == Q_AND:
-		subqueries = ["(" + compile_query(sq) + ")" for sq in q.args]
-		return " INTERSECT ".join(subqueries)
-	if q.type == Q_SQL:
-		return q.sql
-	assert(False)
+                return qb.build("PRs.id")
+        # end Q_TSTRING case
+        if q.type == Q_OR:
+                subqueries = ["(" + compile_query(sq) + ")" for sq in q.args]
+                return " UNION ".join(subqueries)
+        if q.type == Q_AND:
+                subqueries = ["(" + compile_query(sq) + ")" for sq in q.args]
+                return " INTERSECT ".join(subqueries)
+        if q.type == Q_SQL:
+                return q.sql
+        assert(False)
 # end compile_query
 
 def compile_order(qb, o):
-	str = qb.getfield(o.field)
-	if o.rev:
-		str = str + " DESCENDING"
-	return str
+        str = qb.getfield(o.field)
+        if o.rev:
+                str = str + " DESCENDING"
+        return str
 
 def compile_search(s):
-	qb2 = QueryBuilder()
+        qb2 = QueryBuilder()
 
-	# multiple query strings are treated as OR
-	query = door(s.queries)
-	query = compile_query(q)
+        # multiple query strings are treated as OR
+        query = door(s.queries)
+        query = compile_query(q)
 
-	if s.openonly:
-		qb2.addwhere("not %s" % qb.getfield("closed"))
-	if s.publiconly:
-		qb2.addwhere("not %s" % qb.getfield("confidential"))
+        if s.openonly:
+                qb2.addwhere("not %s" % qb.getfield("closed"))
+        if s.publiconly:
+                qb2.addwhere("not %s" % qb.getfield("confidential"))
 
-	orders = [compile_order(qb2, o) for o in s.orders]
-	order = ", ".join(orders)
-	if order != "":
-		qb2.setorder(order)
+        orders = [compile_order(qb2, o) for o in s.orders]
+        order = ", ".join(orders)
+        if order != "":
+                qb2.setorder(order)
 
-	if qb2.nonempty():
-		qb2.addjoin(query, "search")
-		qb2.addjoin("PRs")
-		qb2.addwhere("search = PRs.id")
-		query = qb2.build(["search"])
+        if qb2.nonempty():
+                qb2.addjoin(query, "search")
+                qb2.addjoin("PRs")
+                qb2.addwhere("search = PRs.id")
+                query = qb2.build(["search"])
 
-	return query
+        return query
 # end compile_search
 
 def compile_op(op):
-	if op.type == OP_SEARCH:
-		op.search = compile_search(op.search)
-	return op
+        if op.type == OP_SEARCH:
+                op.search = compile_search(op.search)
+        return op
 
 def compile(ast):
-	ast.ops = [compile_op(op) for op in ast.ops]
+        ast.ops = [compile_op(op) for op in ast.ops]
 
 ############################################################
 # arg handling
@@ -987,167 +987,167 @@
 # ctor you want to use, which seems completely insane.
 #
 class CtorAppend(argparse.Action):
-	def __call__(self, parser, namespace, values, option_string=None):
-		items = getattr(namespace, self.dest)
-		item = self.const(values)
-		items.append(item)
-		setattr(namespace, self.dest, items)
+        def __call__(self, parser, namespace, values, option_string=None):
+                items = getattr(namespace, self.dest)
+                item = self.const(values)
+                items.append(item)
+                setattr(namespace, self.dest, items)
 
 def getargs():
-	p = argparse.ArgumentParser(program_description)
+        p = argparse.ArgumentParser(program_description)
 
-	# note: -h/--help is built in by default
-	p.add_argument("-v", "--version",
-			action='version', version=program_version,
-			help="Print program version and exit")
+        # note: -h/--help is built in by default
+        p.add_argument("-v", "--version",
+                        action='version', version=program_version,
+                        help="Print program version and exit")
 
-	p.add_argument("--show", nargs=1,
-			action=CtorAppend, dest='ops',
-			const=Invocation.Op.doshow,
-			help="Show description of field")
-	p.add_argument("--range", nargs=1,
-			action=CtorAppend, dest='ops',
-			const=Invocation.Op.dorange,
-			help="Show range of extant values for field")
+        p.add_argument("--show", nargs=1,
+                        action=CtorAppend, dest='ops',
+                        const=Invocation.Op.doshow,
+                        help="Show description of field")
+        p.add_argument("--range", nargs=1,
+                        action=CtorAppend, dest='ops',
+                        const=Invocation.Op.dorange,
+                        help="Show range of extant values for field")
 
-	p.add_argument("--search", nargs=1,
-			action=CtorAppend, dest='queries',
-			const=Invocation.Query.doqstring,
-			help="Force string to be read as a search string")
-	p.add_argument("-s", "--sql", nargs=1,
-			action=CtorAppend, dest='queries',
-			const=Invocation.Query.dosql,
-			help="Supply explicit sql query as search")
+        p.add_argument("--search", nargs=1,
+                        action=CtorAppend, dest='queries',
+                        const=Invocation.Query.doqstring,
+                        help="Force string to be read as a search string")
+        p.add_argument("-s", "--sql", nargs=1,
+                        action=CtorAppend, dest='queries',
+                        const=Invocation.Query.dosql,
+                        help="Supply explicit sql query as search")
 
-	p.add_argument("--open",
-			action='store_const', dest='openonly', const="True",
-			help="Exclude closed PRs (default)")
-	p.add_argument("--closed",
-			action='store_const', dest='openonly', const="False",
-			help="Include closed PRs in search")
-	p.add_argument("--public",
-			action='store_const', dest='publiconly', const="True",
-			help="Exclude confidential PRs")
-	p.add_argument("--privileged",
-			action='store_const', dest='publiconly', const="False",
-			help="Allow confidential PRs (default)")
+        p.add_argument("--open",
+                        action='store_const', dest='openonly', const="True",
+                        help="Exclude closed PRs (default)")
+        p.add_argument("--closed",
+                        action='store_const', dest='openonly', const="False",
+                        help="Include closed PRs in search")
+        p.add_argument("--public",
+                        action='store_const', dest='publiconly', const="True",
+                        help="Exclude confidential PRs")
+        p.add_argument("--privileged",
+                        action='store_const', dest='publiconly', const="False",
+                        help="Allow confidential PRs (default)")
 
-	p.add_argument("--oldest",
-			action=CtorAppend, dest='orders',
-			const=Invocation.Order.dooldest,
-			help="Sort output with oldest PRs first")
-	p.add_argument("--newest",
-			action=CtorAppend, dest='orders',
-			const=Invocation.Order.donewest,
-			help="Sort output with newest PRs first")
-	p.add_argument("--staleness",
-			action=CtorAppend, dest='orders',
-			const=Invocation.Order.dostaleness,
-			help="Sort output by time since last modification")
-	p.add_argument("--orderby", nargs=1,
-			action=CtorAppend, dest='orders',
-			const=Invocation.Order.dofield,
-			help="Sort output by specific field")
-	p.add_argument("--revorderby", nargs=1,
-			action=CtorAppend, dest='orders',
-			const=Invocation.Order.dorevfield,
-			help="Sort output by specific field, reversed")
+        p.add_argument("--oldest",
+                        action=CtorAppend, dest='orders',
+                        const=Invocation.Order.dooldest,
+                        help="Sort output with oldest PRs first")
+        p.add_argument("--newest",
+                        action=CtorAppend, dest='orders',
+                        const=Invocation.Order.donewest,
+                        help="Sort output with newest PRs first")
+        p.add_argument("--staleness",
+                        action=CtorAppend, dest='orders',
+                        const=Invocation.Order.dostaleness,
+                        help="Sort output by time since last modification")
+        p.add_argument("--orderby", nargs=1,
+                        action=CtorAppend, dest='orders',
+                        const=Invocation.Order.dofield,
+                        help="Sort output by specific field")
+        p.add_argument("--revorderby", nargs=1,
+                        action=CtorAppend, dest='orders',
+                        const=Invocation.Order.dorevfield,
+                        help="Sort output by specific field, reversed")
 
-	p.add_argument("-m", "--message", nargs=1,
-			action=CtorAppend, dest='selections',
-			const=Invocation.Selection.domessage,
-			help="Print selected message (single PR only)")
-	p.add_argument("-a", "--attachment", nargs=1,
-			action=CtorAppend, dest='selections',
-			const=Invocation.Selection.doattachment,
-			help="Print selected attachment (single PR only)")
+        p.add_argument("-m", "--message", nargs=1,
+                        action=CtorAppend, dest='selections',
+                        const=Invocation.Selection.domessage,
+                        help="Print selected message (single PR only)")
+        p.add_argument("-a", "--attachment", nargs=1,
+                        action=CtorAppend, dest='selections',
+                        const=Invocation.Selection.doattachment,
+                        help="Print selected attachment (single PR only)")
 
-	p.add_argument("-r", "--raw",
-			action = 'store_const', const="RAW",
-			dest = 'output',
-			help="Print exactly what the database returns")
-	p.add_argument("-l", "--list",
-			action = 'store_const', const="LIST",
-			dest = 'output',
-			help="Print in list form (default)")
-	p.add_argument("--headers",
-			action = 'store_const', const="HEADERS",
-			dest = 'output',
-			help="Print header information only")
-	p.add_argument("--meta",
-			action = 'store_const', const="META",
-			dest = 'output',
-			help="Print all metadata")
-	p.add_argument("--metadata",
-			action = 'store_const', const="META",
-			dest = 'output')
-	p.add_argument("-f", "--full",
-			action = 'store_const', const="FULL",
-			dest = 'output',
-			help="Print everything")
+        p.add_argument("-r", "--raw",
+                        action = 'store_const', const="RAW",
+                        dest = 'output',
+                        help="Print exactly what the database returns")
+        p.add_argument("-l", "--list",
+                        action = 'store_const', const="LIST",
+                        dest = 'output',
+                        help="Print in list form (default)")
+        p.add_argument("--headers",
+                        action = 'store_const', const="HEADERS",
+                        dest = 'output',
+                        help="Print header information only")
+        p.add_argument("--meta",
+                        action = 'store_const', const="META",
+                        dest = 'output',
+                        help="Print all metadata")
+        p.add_argument("--metadata",
+                        action = 'store_const', const="META",
+                        dest = 'output')
+        p.add_argument("-f", "--full",
+                        action = 'store_const', const="FULL",
+                        dest = 'output',
+                        help="Print everything")
 
-	p.add_argument("--text",
-			action = 'store_const', const="TEXT",
-			dest = 'outformat',
-			help="Print in text format (default)")
-	p.add_argument("--csv",
-			action = 'store_const', const="CSV",
-			dest = 'outformat',
-			help="Print a CSV file")
-	p.add_argument("--xml",
-			action = 'store_const', const="XML",
-			dest = 'outformat',
-			help="Print in XML")
-	p.add_argument("--json",
-			action = 'store_const', const="JSON",
-			dest = 'outformat',
-			help="Print in JSON")
-	p.add_argument("--rdf",
-			action = 'store_const', const="RDF",
-			dest = 'outbformat',
-			help="Print in RDF")
-	p.add_argument("--rdflike",
-			action = 'store_const', const="RDFLIKE",
-			dest = 'outformat',
-			help="Print RDF-like text")
+        p.add_argument("--text",
+                        action = 'store_const', const="TEXT",
+                        dest = 'outformat',
+                        help="Print in text format (default)")
+        p.add_argument("--csv",
+                        action = 'store_const', const="CSV",
+                        dest = 'outformat',
+                        help="Print a CSV file")
+        p.add_argument("--xml",
+                        action = 'store_const', const="XML",
+                        dest = 'outformat',
+                        help="Print in XML")
+        p.add_argument("--json",
+                        action = 'store_const', const="JSON",
+                        dest = 'outformat',
+                        help="Print in JSON")
+        p.add_argument("--rdf",
+                        action = 'store_const', const="RDF",
+                        dest = 'outbformat',
+                        help="Print in RDF")
+        p.add_argument("--rdflike",
+                        action = 'store_const', const="RDFLIKE",
+                        dest = 'outformat',
+                        help="Print RDF-like text")
 
-	p.add_argument("TERM", nargs='*',
-			action=CtorAppend, dest='queries',
-			const=Invocation.Query.doqstring,
-			help="Search term")
+        p.add_argument("TERM", nargs='*',
+                        action=CtorAppend, dest='queries',
+                        const=Invocation.Query.doqstring,
+                        help="Search term")
 
-	args = p.parse_args()
+        args = p.parse_args()
 
-	ops = args.ops
-	if ops is None:
-		ops = []
-	queries = args.queries
-	if queries is not None:
-		openonly = args.openonly
-		if openonly is None:
-			openonly = True
-		publiconly = args.publiconly
-		if publiconly is None:
-			publiconly = False
-		orders = args.orders
-		if orders is None:
-			orders = [Invocation.Order.dooldest(None)]
-		output = args.output
-		if output is None:
-			output = "LIST"
-		outformat = args.outformat
-		if outformat is None:
-			outformat = "TEXT"
-		selections = args.selections
-		if selections is None:
-			sel = Invocation.Selection.dopr(output, outformat)
-			selections = [sel]
-		search = Search(queries, openonly, publiconly, orders)
-		op = dosearch(search, selections)
-		ops.append(op)
-	# endif
+        ops = args.ops
+        if ops is None:
+                ops = []
+        queries = args.queries
+        if queries is not None:
+                openonly = args.openonly
+                if openonly is None:
+                        openonly = True
+                publiconly = args.publiconly
+                if publiconly is None:
+                        publiconly = False
+                orders = args.orders
+                if orders is None:
+                        orders = [Invocation.Order.dooldest(None)]
+                output = args.output
+                if output is None:
+                        output = "LIST"
+                outformat = args.outformat
+                if outformat is None:
+                        outformat = "TEXT"
+                selections = args.selections
+                if selections is None:
+                        sel = Invocation.Selection.dopr(output, outformat)
+                        selections = [sel]
+                search = Search(queries, openonly, publiconly, orders)
+                op = dosearch(search, selections)
+                ops.append(op)
+        # endif
 
-	return Invocation(ops)
+        return Invocation(ops)
 # end getargs
 
 ############################################################