core (memory/sqlite): added stanza_id:

/!\ database schema change /!\
stanza_id is a new field in history added to prepare the implementation of MAM for messages.
A new "last_stanza_id" can be used in filters to retrieve last message with a know stanza id (useful for history synchronisation).
This commit is contained in:
Goffi 2018-12-01 10:08:17 +01:00
commit 0b8e750956

View file

@ -36,7 +36,7 @@ import hashlib
import sqlite3
import json
CURRENT_DB_VERSION = 5
CURRENT_DB_VERSION = 6
# XXX: DATABASE schemas are used in the following way:
# - 'current' key is for the actual database schema, for a new base
@ -56,7 +56,7 @@ DATABASE_SCHEMAS = {
("FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE",))),
('message_types', (("type TEXT PRIMARY KEY",),
())),
('history', (("uid TEXT PRIMARY KEY", "update_uid TEXT", "profile_id INTEGER", "source TEXT", "dest TEXT", "source_res TEXT", "dest_res TEXT",
('history', (("uid TEXT PRIMARY KEY", "stanza_id TEXT", "update_uid TEXT", "profile_id INTEGER", "source TEXT", "dest TEXT", "source_res TEXT", "dest_res TEXT",
"timestamp DATETIME NOT NULL", "received_timestamp DATETIME", # XXX: timestamp is the time when the message was emitted. If received time stamp is not NULL, the message was delayed and timestamp is the declared value (and received_timestamp the time of reception)
"type TEXT", "extra BLOB"),
("FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE", "FOREIGN KEY(type) REFERENCES message_types(type)",
@ -98,6 +98,8 @@ DATABASE_SCHEMAS = {
)),
)),
},
6: {'cols create': {'history': ('stanza_id TEXT',)},
},
5: {'create': {'files': (("id TEXT NOT NULL", "version TEXT NOT NULL", "parent TEXT NOT NULL",
"type TEXT CHECK(type in ('{file}', '{directory}')) NOT NULL DEFAULT '{file}'".format(
file=C.FILE_TYPE_FILE, directory=C.FILE_TYPE_DIRECTORY),
@ -117,7 +119,7 @@ DATABASE_SCHEMAS = {
},
}
NOT_IN_EXTRA = ('received_timestamp', 'update_uid') # keys which are in message data extra but not stored in sqlite's extra field
NOT_IN_EXTRA = ('stanza_id', 'received_timestamp', 'update_uid') # keys which are in message data extra but not stored in sqlite's extra field
# this is specific to this sqlite storage and for now only used for received_timestamp
# because this value is stored in a separate field
@ -423,8 +425,8 @@ class SqliteStorage(object):
extra = pickle.dumps({k: v for k, v in data['extra'].iteritems() if k not in NOT_IN_EXTRA}, 0)
from_jid = data['from']
to_jid = data['to']
d = self.dbpool.runQuery("INSERT INTO history(uid, update_uid, profile_id, source, dest, source_res, dest_res, timestamp, received_timestamp, type, extra) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
(data['uid'], data['extra'].get('update_uid'), self.profiles[profile], data['from'].userhost(), to_jid.userhost(), from_jid.resource, to_jid.resource, data['timestamp'], data.get('received_timestamp'), data['type'], sqlite3.Binary(extra)))
d = self.dbpool.runQuery("INSERT INTO history(uid, stanza_id, update_uid, profile_id, source, dest, source_res, dest_res, timestamp, received_timestamp, type, extra) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(data['uid'], data['extra'].get('stanza_id'), data['extra'].get('update_uid'), self.profiles[profile], data['from'].userhost(), to_jid.userhost(), from_jid.resource, to_jid.resource, data['timestamp'], data.get('received_timestamp'), data['type'], sqlite3.Binary(extra)))
d.addCallbacks(self._addToHistoryCb, self._addToHistoryEb, callbackArgs=[data], errbackArgs=[data])
d.addErrback(self._logHistoryError, from_jid, to_jid, data)
return d
@ -434,7 +436,7 @@ class SqliteStorage(object):
result = []
current = {'uid': None}
for row in reversed(query_result):
uid, update_uid, source, dest, source_res, dest_res, timestamp, received_timestamp,\
uid, stanza_id, update_uid, source, dest, source_res, dest_res, timestamp, received_timestamp,\
type_, extra, message, message_lang, subject, subject_lang, thread, thread_parent = row
if uid != current['uid']:
# new message
@ -452,6 +454,8 @@ class SqliteStorage(object):
'extra': extra,
'timestamp': timestamp,
}
if stanza_id is not None:
current['extra']['stanza_id'] = stanza_id
if update_uid is not None:
current['extra']['update_uid'] = update_uid
if received_timestamp is not None:
@ -492,7 +496,7 @@ class SqliteStorage(object):
- 0 for no message (returns the empty list)
- None for unlimited
@param between (bool): confound source and dest (ignore the direction)
@param search (unicode): pattern to filter the history results
@param filters (dict[unicode, unicode]): pattern to filter the history results
@param profile (unicode): %(doc_profile)s
@return: list of tuple as in [messageNew]
"""
@ -502,29 +506,54 @@ class SqliteStorage(object):
if limit == 0:
return defer.succeed([])
query_parts = [u"SELECT uid, update_uid, source, dest, source_res, dest_res, timestamp, received_timestamp,\
query_parts = [u"SELECT uid, stanza_id, update_uid, source, dest, source_res, dest_res, timestamp, received_timestamp,\
type, extra, message, message.language, subject, subject.language, thread_id, thread.parent_id\
FROM history LEFT JOIN message ON history.uid = message.history_uid\
LEFT JOIN subject ON history.uid=subject.history_uid\
LEFT JOIN thread ON history.uid=thread.history_uid\
WHERE profile_id=? AND"] # FIXME: not sure if it's the best request, messages and subjects can appear several times here
WHERE profile_id=?"] # FIXME: not sure if it's the best request, messages and subjects can appear several times here
values = [self.profiles[profile]]
def test_jid(type_, _jid):
values.append(_jid.userhost())
if _jid.resource:
values.append(_jid.resource)
return u'(%s=? AND %s_res=?)' % (type_, type_)
return u'%s=?' % (type_, )
def test_jid(type_, jid_):
values.append(jid_.userhost())
if jid_.resource:
values.append(jid_.resource)
return u'({type_}=? AND {type_}_res=?)'.format(type_=type_)
return u'{type_}=?'.format(type_=type_)
if between:
query_parts.append(u"((%s AND %s) OR (%s AND %s))" % (test_jid('source', from_jid),
test_jid('dest', to_jid),
test_jid('source', to_jid),
test_jid('dest', from_jid)))
if not from_jid and not to_jid:
# not jid specified, we want all one2one communications
pass
elif between:
if not from_jid or not to_jid:
# we only have one jid specified, we check all messages
# from or to this jid
jid_ = from_jid or to_jid
query_parts.append(u"AND ({source} OR {dest})".format(
source=test_jid(u'source', jid_),
dest=test_jid(u'dest' , jid_)))
else:
# we have 2 jids specified, we check all communications between
# those 2 jids
query_parts.append(
u"AND (({source_from} AND {dest_to}) "
u"OR ({source_to} AND {dest_from}))".format(
source_from=test_jid('source', from_jid),
dest_to=test_jid('dest', to_jid),
source_to=test_jid('source', to_jid),
dest_from=test_jid('dest', from_jid)))
else:
query_parts.append(u"%s AND %s" % (test_jid('source', from_jid),
test_jid('dest', to_jid)))
# we want one communication in specific direction (from somebody or
# to somebody).
q = []
if from_jid is not None:
q.append(test_jid('source', from_jid))
if to_jid is not None:
q.append(test_jid('dest', to_jid))
query_parts.append(u"AND " + u" AND ".join(q))
# set to True if "ORDER BY" is already added
order = False
if filters:
if 'body' in filters:
@ -542,10 +571,20 @@ class SqliteStorage(object):
types = filters['not_types'].split()
query_parts.append(u"AND type NOT IN ({})".format(u','.join("?"*len(types))))
values.extend(types)
if 'last_stanza_id' in filters:
# this request get the last message with a "stanza_id" that we
# have in history. This is mainly used to retrieve messages sent
# while we were offline, using MAM (XEP-0313).
if (filters[u'last_stanza_id'] is not True
or from_jid is not None or to_jid is not None
or limit != 1):
raise ValueError(u"Unexpected values for last_stanza_id filter")
query_parts.append(u"AND stanza_id IS NOT NULL ORDER BY history.rowid DESC")
order = True
query_parts.append(u"ORDER BY timestamp DESC") # we reverse the order in sqliteHistoryToList
# we use DESC here so LIMIT keep the last messages
if not order:
query_parts.append(u"ORDER BY timestamp DESC") # we reverse the order in sqliteHistoryToList
# we use DESC here so LIMIT keep the last messages
if limit is not None:
query_parts.append(u"LIMIT ?")
values.append(limit)