mod factoids plugin to use a separate key-value relationship table

this avoids duplication, and allows one to set a bunch of aliases for a factoid, without creating duplicates of the same fact content.
This commit is contained in:
Daniel Folkinshteyn 2010-04-02 00:03:01 -04:00 committed by Valentin Lorentz
parent 471921eab6
commit 436d2bade8

View File

@ -102,23 +102,21 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
cursor = db.cursor() cursor = db.cursor()
cursor.execute("""CREATE TABLE keys ( cursor.execute("""CREATE TABLE keys (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
key TEXT UNIQUE ON CONFLICT IGNORE, key TEXT UNIQUE ON CONFLICT REPLACE
locked BOOLEAN
)""") )""")
cursor.execute("""CREATE TABLE factoids ( cursor.execute("""CREATE TABLE factoids (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
key_id INTEGER,
added_by TEXT, added_by TEXT,
added_at TIMESTAMP, added_at TIMESTAMP,
usage_count INTEGER, fact TEXT UNIQUE ON CONFLICT REPLACE,
fact TEXT locked BOOLEAN
)""")
cursor.execute("""CREATE TABLE relations (
id INTEGER PRIMARY KEY,
key_id INTEGER,
fact_id INTEGER,
usage_count INTEGER
)""") )""")
cursor.execute("""CREATE TRIGGER remove_factoids
BEFORE DELETE ON keys
BEGIN
DELETE FROM factoids WHERE key_id = old.id;
END
""")
db.commit() db.commit()
return db return db
@ -153,31 +151,52 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
doc=method._fake__doc__ % (s, s), doc=method._fake__doc__ % (s, s),
name=callbacks.formatCommand(command)) name=callbacks.formatCommand(command))
return super(Factoids, self).getCommandHelp(command, simpleSyntax) return super(Factoids, self).getCommandHelp(command, simpleSyntax)
def learn(self, irc, msg, args, channel, key, factoid): def _getKeyAndFactId(self, channel, key, factoid):
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE ?", (key,)) cursor.execute("SELECT id FROM keys WHERE key=?", (key,))
results = cursor.fetchall() keyresults = cursor.fetchall()
if len(results) == 0: cursor.execute("SELECT id FROM factoids WHERE fact=?", (factoid,))
cursor.execute("""INSERT INTO keys VALUES (NULL, ?, 0)""", (key,)) factresults = cursor.fetchall()
return (keyresults, factresults,)
def learn(self, irc, msg, args, channel, key, factoid):
# if neither key nor factoid exist, add them.
# if key exists but factoid doesn't, add factoid, link it to existing key
# if factoid exists but key doesn't, add key, link it to existing factoid
# if both key and factoid already exist, and are linked, do nothing, print nice message
db = self.getDb(channel)
cursor = db.cursor()
(keyid, factid) = self._getKeyAndFactId(channel, key, factoid)
if len(keyid) == 0:
cursor.execute("""INSERT INTO keys VALUES (NULL, ?)""", (key,))
db.commit() db.commit()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE ?", (key,)) if len(factid) == 0:
results = cursor.fetchall()
(id, locked) = map(int, results[0])
capability = ircdb.makeChannelCapability(channel, 'factoids')
if not locked:
if ircdb.users.hasUser(msg.prefix): if ircdb.users.hasUser(msg.prefix):
name = ircdb.users.getUser(msg.prefix).name name = ircdb.users.getUser(msg.prefix).name
else: else:
name = msg.nick name = msg.nick
cursor.execute("""INSERT INTO factoids VALUES cursor.execute("""INSERT INTO factoids VALUES
(NULL, ?, ?, ?, ?, ?)""", (NULL, ?, ?, ?, ?)""",
(id, name, int(time.time()), 0, factoid)) (name, int(time.time()), factoid, 0))
db.commit()
(keyid, factid) = self._getKeyAndFactId(channel, key, factoid)
cursor.execute("""SELECT id, key_id, fact_id from relations
WHERE key_id=? AND fact_id=?""",
(keyid[0][0], factid[0][0],))
existingrelation = cursor.fetchall()
if len(existingrelation) == 0:
cursor.execute("""INSERT INTO relations VALUES (NULL, ?, ?, ?)""",
(keyid[0][0],factid[0][0],0,))
db.commit() db.commit()
irc.replySuccess() irc.replySuccess()
else: else:
irc.error('That factoid is locked.') irc.error("This key-factoid relationship already exists.")
learn = wrap(learn, ['factoid']) learn = wrap(learn, ['factoid'])
learn._fake__doc__ = _("""[<channel>] <key> %s <value> learn._fake__doc__ = _("""[<channel>] <key> %s <value>
@ -192,8 +211,8 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
def _lookupFactoid(self, channel, key): def _lookupFactoid(self, channel, key):
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
cursor.execute("""SELECT factoids.fact, factoids.id FROM factoids, keys cursor.execute("""SELECT factoids.fact, factoids.id, relations.id FROM factoids, keys, relations
WHERE keys.key LIKE ? AND factoids.key_id=keys.id WHERE keys.key LIKE ? AND relations.key_id=keys.id AND relations.fact_id=factoids.id
ORDER BY factoids.id ORDER BY factoids.id
LIMIT 20""", (key,)) LIMIT 20""", (key,))
return cursor.fetchall() return cursor.fetchall()
@ -213,12 +232,13 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
if self.registryValue('keepRankInfo', channel): if self.registryValue('keepRankInfo', channel):
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
for (fact,id) in factoids: for (fact,factid,relationid) in factoids:
cursor.execute("""SELECT factoids.usage_count cursor.execute("""SELECT relations.usage_count
FROM factoids FROM relations
WHERE factoids.id=?""", (id,)) WHERE relations.id=?""", (relationid,))
old_count = cursor.fetchall()[0][0] old_count = cursor.fetchall()[0][0]
cursor.execute("UPDATE factoids SET usage_count=? WHERE id=?", (old_count + 1, id,)) cursor.execute("UPDATE relations SET usage_count=? WHERE id=?",
(old_count + 1, relationid,))
db.commit() db.commit()
def _replyFactoids(self, irc, msg, key, channel, factoids, def _replyFactoids(self, irc, msg, key, channel, factoids,
@ -300,10 +320,10 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
numfacts = self.registryValue('rankListLength', channel) numfacts = self.registryValue('rankListLength', channel)
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
cursor.execute("""SELECT keys.key, factoids.usage_count cursor.execute("""SELECT keys.key, relations.usage_count
FROM keys, factoids FROM keys, relations
WHERE factoids.key_id=keys.id WHERE relations.key_id=keys.id
ORDER BY factoids.usage_count DESC ORDER BY relations.usage_count DESC
LIMIT ?""", (numfacts,)) LIMIT ?""", (numfacts,))
factkeys = cursor.fetchall() factkeys = cursor.fetchall()
s = [ "#%d %s (%d)" % (i+1, key[0], key[1]) for i, key in enumerate(factkeys) ] s = [ "#%d %s (%d)" % (i+1, key[0], key[1]) for i, key in enumerate(factkeys) ]
@ -320,7 +340,10 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
""" """
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
cursor.execute("UPDATE keys SET locked=1 WHERE key LIKE ?", (key,)) cursor.execute("UPDATE factoids, keys, relations "
"SET factoids.locked=1 WHERE key LIKE ? AND "
"factoids.id=relations.fact_id AND "
"keys.id=relations.key_id", (key,))
db.commit() db.commit()
irc.replySuccess() irc.replySuccess()
lock = wrap(lock, ['channel', 'text']) lock = wrap(lock, ['channel', 'text'])
@ -335,19 +358,48 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
""" """
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
cursor.execute("UPDATE keys SET locked=0 WHERE key LIKE ?", (key,)) cursor.execute("""UPDATE factoids, keys, relations
SET factoids.locked=1 WHERE key LIKE ? AND
factoids.id=relations.fact_id AND
keys.id=relations.key_id""", (key,))
db.commit() db.commit()
irc.replySuccess() irc.replySuccess()
unlock = wrap(unlock, ['channel', 'text']) unlock = wrap(unlock, ['channel', 'text'])
def _deleteRelation(self, channel, relationlist):
db = self.getDb(channel)
cursor = db.cursor()
for (keyid, factid, relationid) in relationlist:
cursor.execute("""DELETE FROM relations where relations.id=?""",
(relationid,))
db.commit()
cursor.execute("""SELECT id FROM relations
WHERE relations.key_id=?""", (keyid,))
remaining_key_relations = cursor.fetchall()
if len(remaining_key_relations) == 0:
cursor.execute("""DELETE FROM keys where id=?""", (keyid,))
cursor.execute("""SELECT id FROM relations
WHERE relations.fact_id=?""", (factid,))
remaining_fact_relations = cursor.fetchall()
if len(remaining_fact_relations) == 0:
cursor.execute("""DELETE FROM factoids where id=?""", (factid,))
db.commit()
@internationalizeDocstring @internationalizeDocstring
def forget(self, irc, msg, args, channel, words): def forget(self, irc, msg, args, channel, words):
"""[<channel>] <key> [<number>|*] """[<channel>] <key> [<number>|*]
Removes the factoid <key> from the factoids database. If there are Removes a key-fact relationship for key <key> from the factoids
more than one factoid with such a key, a number is necessary to database. If there is more than one such relationship for this key,
determine which one should be removed. A * can be used to remove all a number is necessary to determine which one should be removed.
factoids associated with a key. <channel> is only necessary if A * can be used to remove all relationships for <key>.
If as a result, the key (factoid) remains without any relationships to
a factoid (key), it shall be removed from the database.
<channel> is only necessary if
the message isn't sent in the channel itself. the message isn't sent in the channel itself.
""" """
number = None number = None
@ -362,29 +414,26 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
key = ' '.join(words) key = ' '.join(words)
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
cursor.execute("""SELECT keys.id, factoids.id cursor.execute("""SELECT keys.id, factoids.id, relations.id
FROM keys, factoids FROM keys, factoids, relations
WHERE key LIKE ? AND WHERE key LIKE ? AND
factoids.key_id=keys.id""", (key,)) relations.key_id=keys.id AND
relations.fact_id=factoids.id""", (key,))
results = cursor.fetchall() results = cursor.fetchall()
if len(results) == 0: if len(results) == 0:
irc.error(_('There is no such factoid.')) irc.error(_('There is no such factoid.'))
elif len(results) == 1 or number is True: elif len(results) == 1 or number is True:
(id, _) = results[0] self._deleteRelation(channel, results)
cursor.execute("""DELETE FROM factoids WHERE key_id=?""", (id,))
cursor.execute("""DELETE FROM keys WHERE key LIKE ?""", (key,))
db.commit()
irc.replySuccess() irc.replySuccess()
else: else:
if number is not None: if number is not None:
#results = cursor.fetchall() #results = cursor.fetchall()
try: try:
(foo, id) = results[number-1] arelation = results[number-1]
except IndexError: except IndexError:
irc.error(_('Invalid factoid number.')) irc.error(_('Invalid factoid number.'))
return return
cursor.execute("DELETE FROM factoids WHERE id=?", (id,)) self._deleteRelation(channel, [arelation,])
db.commit()
irc.replySuccess() irc.replySuccess()
else: else:
irc.error(_('%s factoids have that key. ' irc.error(_('%s factoids have that key. '
@ -402,15 +451,18 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
""" """
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
cursor.execute("""SELECT fact, key_id FROM factoids cursor.execute("""SELECT id, key_id, fact_id FROM relations
ORDER BY random() ORDER BY random()
LIMIT 3""") LIMIT 3""")
results = cursor.fetchall() results = cursor.fetchall()
if len(results) != 0: if len(results) != 0:
L = [] L = []
for (factoid, id) in results: for (relationid, keyid, factid) in results:
cursor.execute("""SELECT key FROM keys WHERE id=?""", (id,)) cursor.execute("""SELECT keys.key, factoids.fact
(key,) = cursor.fetchone() FROM keys, factoids
WHERE factoids.id=? AND
keys.id=?""", (factid,keyid,))
(key,factoid) = cursor.fetchall()[0]
L.append('"%s": %s' % (ircutils.bold(key), factoid)) L.append('"%s": %s' % (ircutils.bold(key), factoid))
irc.reply('; '.join(L)) irc.reply('; '.join(L))
else: else:
@ -427,19 +479,21 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
""" """
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
cursor.execute("SELECT id, locked FROM keys WHERE key LIKE ?", (key,)) cursor.execute("SELECT id FROM keys WHERE key LIKE ?", (key,))
results = cursor.fetchall() results = cursor.fetchall()
if len(results) == 0: if len(results) == 0:
irc.error(_('No factoid matches that key.')) irc.error(_('No factoid matches that key.'))
return return
(id, locked) = map(int, results[0]) id = results[0][0]
cursor.execute("""SELECT added_by, added_at, usage_count FROM factoids cursor.execute("""SELECT factoids.added_by, factoids.added_at, factoids.locked, relations.usage_count
WHERE key_id=? FROM factoids, relations
ORDER BY id""", (id,)) WHERE relations.key_id=? AND
relations.fact_id=factoids.id
ORDER BY relations.id""", (id,))
factoids = cursor.fetchall() factoids = cursor.fetchall()
L = [] L = []
counter = 0 counter = 0
for (added_by, added_at, usage_count) in factoids: for (added_by, added_at, locked, usage_count) in factoids:
counter += 1 counter += 1
added_at = time.strftime(conf.supybot.reply.format.time(), added_at = time.strftime(conf.supybot.reply.format.time(),
time.localtime(int(added_at))) time.localtime(int(added_at)))
@ -463,9 +517,10 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
db = self.getDb(channel) db = self.getDb(channel)
cursor = db.cursor() cursor = db.cursor()
cursor.execute("""SELECT factoids.id, factoids.fact cursor.execute("""SELECT factoids.id, factoids.fact
FROM keys, factoids FROM keys, factoids, relations
WHERE keys.key LIKE ? AND WHERE keys.key LIKE ? AND
keys.id=factoids.key_id""", (key,)) keys.id=relations.key_id AND
factoids.id=relations.fact_id""", (key,))
results = cursor.fetchall() results = cursor.fetchall()
if len(results) == 0: if len(results) == 0:
irc.error(format(_('I couldn\'t find any key %q'), key)) irc.error(format(_('I couldn\'t find any key %q'), key))
@ -502,7 +557,8 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
target = 'factoids.fact' target = 'factoids.fact'
if 'factoids' not in tables: if 'factoids' not in tables:
tables.append('factoids') tables.append('factoids')
criteria.append('factoids.key_id=keys.id') tables.append('relations')
criteria.append('factoids.id=relations.fact_id AND keys.id=relations.key_id')
elif option == 'regexp': elif option == 'regexp':
criteria.append('%s(TARGET)' % predicateName) criteria.append('%s(TARGET)' % predicateName)
def p(s, r=arg): def p(s, r=arg):
@ -516,7 +572,10 @@ class Factoids(callbacks.Plugin, plugins.ChannelDBHandler):
sql = """SELECT keys.key FROM %s WHERE %s""" % \ sql = """SELECT keys.key FROM %s WHERE %s""" % \
(', '.join(tables), ' AND '.join(criteria)) (', '.join(tables), ' AND '.join(criteria))
sql = sql + " ORDER BY keys.key" sql = sql + " ORDER BY keys.key"
print sql
sql = sql.replace('TARGET', target) sql = sql.replace('TARGET', target)
print sql
print formats
cursor.execute(sql, formats) cursor.execute(sql, formats)
if cursor.rowcount == 0: if cursor.rowcount == 0:
irc.reply(_('No keys matched that query.')) irc.reply(_('No keys matched that query.'))