diff --git a/resources/notes_sqlite.sql b/resources/notes_sqlite.sql index ac0143e841..6b2c4e94e8 100644 --- a/resources/notes_sqlite.sql +++ b/resources/notes_sqlite.sql @@ -3,6 +3,8 @@ CREATE TABLE notes_db.notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, colname TEXT NOT NULL COLLATE NOCASE, doc TEXT NOT NULL DEFAULT '', searchable_text TEXT NOT NULL DEFAULT '', + ctime REAL DEFAULT (unixepoch('subsec')), + mtime REAL DEFAULT (unixepoch('subsec')), UNIQUE(item, colname) ); @@ -41,6 +43,7 @@ BEGIN INSERT INTO notes_fts(rowid, searchable_text) VALUES (NEW.id, NEW.searchable_text); INSERT INTO notes_fts_stemmed(notes_fts_stemmed, rowid, searchable_text) VALUES('delete', OLD.id, OLD.searchable_text); INSERT INTO notes_fts_stemmed(rowid, searchable_text) VALUES (NEW.id, NEW.searchable_text); + UPDATE notes SET mtime=unixepoch('subsec') WHERE id = OLD.id; END; CREATE TRIGGER notes_db.notes_db_resources_delete_trg BEFORE DELETE ON notes_db.resources diff --git a/src/calibre/db/notes/connect.py b/src/calibre/db/notes/connect.py index fea0629bc7..2448262420 100644 --- a/src/calibre/db/notes/connect.py +++ b/src/calibre/db/notes/connect.py @@ -205,7 +205,10 @@ class Notes: srcdir = self.path_to_retired_dir_for_item(field_name, item_id, item_value) remove_with_retry(srcdir, is_dir=True) - def set_note(self, conn, field_name, item_id, item_value, marked_up_text='', used_resource_hashes=(), searchable_text=copy_marked_up_text): + def set_note( + self, conn, field_name, item_id, item_value, marked_up_text='', used_resource_hashes=(), + searchable_text=copy_marked_up_text, ctime=None, mtime=None + ): if searchable_text is copy_marked_up_text: searchable_text = marked_up_text searchable_text = item_value + '\n' + searchable_text @@ -228,9 +231,19 @@ class Notes: resources_to_add = new_resources - old_resources if note_id is None: self.remove_retired_entry(field_name, item_id, item_value) - note_id = conn.get(''' - INSERT INTO notes_db.notes (item,colname,doc,searchable_text) VALUES (?,?,?,?) RETURNING notes.id; - ''', (item_id, field_name, marked_up_text, searchable_text), all=False) + if ctime is not None or mtime is not None: + now = time.time() + if ctime is None: + ctime = now + if mtime is None: + mtime = now + note_id = conn.get(''' + INSERT INTO notes_db.notes (item,colname,doc,searchable_text,ctime,mtime) VALUES (?,?,?,?,?,?) RETURNING notes.id; + ''', (item_id, field_name, marked_up_text, searchable_text, ctime, mtime), all=False) + else: + note_id = conn.get(''' + INSERT INTO notes_db.notes (item,colname,doc,searchable_text) VALUES (?,?,?,?) RETURNING notes.id; + ''', (item_id, field_name, marked_up_text, searchable_text), all=False) else: conn.execute('UPDATE notes_db.notes SET doc=?,searchable_text=? WHERE id=?', (marked_up_text, searchable_text, note_id)) if resources_to_potentially_remove: @@ -246,11 +259,12 @@ class Notes: return conn.get('SELECT doc FROM notes_db.notes WHERE item=? AND colname=?', (item_id, field_name), all=False) def get_note_data(self, conn, field_name, item_id): - for (note_id, doc, searchable_text) in conn.execute( - 'SELECT id,doc,searchable_text FROM notes_db.notes WHERE item=? AND colname=?', (item_id, field_name) + for (note_id, doc, searchable_text, ctime, mtime) in conn.execute( + 'SELECT id,doc,searchable_text,ctime,mtime FROM notes_db.notes WHERE item=? AND colname=?', (item_id, field_name) ): return { 'id': note_id, 'doc': doc, 'searchable_text': searchable_text, + 'ctime': ctime, 'mtime': mtime, 'resource_hashes': frozenset(self.resources_used_by(conn, note_id)), } @@ -427,6 +441,7 @@ class Notes: try: with open(make_long_path_useable(os.path.join(self.backup_dir, field, str(item_id)))) as f: raw = f.read() + st = os.stat(f.fileno()) except OSError as e: errors.append(_('Failed to read from document for {path} with error: {error}').format(path=item_val, error=e)) continue @@ -441,7 +456,7 @@ class Notes: errors.append(_('Some resources for {} were missing').format(item_val)) resources &= known_resources try: - self.set_note(conn, field, item_id, item_val, doc, resources, searchable_text) + self.set_note(conn, field, item_id, item_val, doc, resources, searchable_text, ctime=st.st_ctime, mtime=st.st_mtime) except Exception as e: errors.append(_('Failed to set note for {path} with error: {error}').format(path=item_val, error=e)) else: diff --git a/src/calibre/db/tests/notes.py b/src/calibre/db/tests/notes.py index 899af62ba0..51593e153a 100644 --- a/src/calibre/db/tests/notes.py +++ b/src/calibre/db/tests/notes.py @@ -2,7 +2,7 @@ # License: GPLv3 Copyright: 2023, Kovid Goyal -import os +import os, time from calibre.db.tests.base import BaseTest @@ -75,7 +75,14 @@ def test_cache_api(self: 'NotesTest'): h2 = cache.add_notes_resource(b'resource2', 'r1.jpg') cache.set_notes_for('authors', author_id, doc, resource_hashes=(h1, h2)) nd = cache.notes_data_for('authors', author_id) - self.ae(nd, {'id': 1, 'searchable_text': authors[0] + '\n' + doc, 'doc': doc, 'resource_hashes': frozenset({h1, h2})}) + self.ae(nd, {'id': 1, 'ctime': nd['ctime'], 'mtime': nd['ctime'], 'searchable_text': authors[0] + '\n' + doc, + 'doc': doc, 'resource_hashes': frozenset({h1, h2})}) + time.sleep(0.01) + cache.set_notes_for('authors', author_id, doc, resource_hashes=(h1, h2)) + n2d = cache.notes_data_for('authors', author_id) + self.ae(nd['ctime'], n2d['ctime']) + self.assertGreater(n2d['mtime'], nd['mtime']) + # test renaming to a new author preserves notes cache.rename_items('authors', {author_id: 'renamed author'}) raid = cache.get_item_id('authors', 'renamed author')