Notes/src/services/consistency_checks.js

712 lines
28 KiB
JavaScript
Raw Normal View History

2017-12-14 22:16:26 -05:00
"use strict";
const sql = require('./sql');
const sqlInit = require('./sql_init');
2017-12-14 22:16:26 -05:00
const log = require('./log');
2019-08-26 20:21:43 +02:00
const ws = require('./ws.js');
const syncMutexService = require('./sync_mutex');
const repository = require('./repository');
const cls = require('./cls');
const syncTableService = require('./sync_table');
const optionsService = require('./options');
const Branch = require('../entities/branch');
2020-04-04 09:46:49 +02:00
const dateUtils = require('./date_utils');
2017-12-14 22:16:26 -05:00
2019-12-10 22:03:00 +01:00
class ConsistencyChecks {
constructor(autoFix) {
this.autoFix = autoFix;
this.unrecoveredConsistencyErrors = false;
this.fixedIssues = false;
2017-12-14 22:16:26 -05:00
}
2020-06-20 12:31:38 +02:00
findAndFixIssues(query, fixerCb) {
const results = sql.getRows(query);
2019-12-10 22:03:00 +01:00
for (const res of results) {
try {
2020-06-20 12:31:38 +02:00
fixerCb(res);
2018-01-01 19:41:22 -05:00
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
this.fixedIssues = true;
} else {
this.unrecoveredConsistencyErrors = true;
}
} catch (e) {
logError(`Fixer failed with ${e.message} ${e.stack}`);
this.unrecoveredConsistencyErrors = true;
2018-01-01 19:41:22 -05:00
}
}
2019-12-10 22:03:00 +01:00
return results;
2018-01-01 19:41:22 -05:00
}
2018-10-21 21:37:34 +02:00
2020-06-20 12:31:38 +02:00
checkTreeCycles() {
2019-12-10 22:03:00 +01:00
const childToParents = {};
2020-06-20 12:31:38 +02:00
const rows = sql.getRows("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0");
2018-01-01 19:41:22 -05:00
2019-12-10 22:03:00 +01:00
for (const row of rows) {
const childNoteId = row.noteId;
const parentNoteId = row.parentNoteId;
childToParents[childNoteId] = childToParents[childNoteId] || [];
childToParents[childNoteId].push(parentNoteId);
}
2020-03-11 22:43:20 +01:00
const checkTreeCycle = (noteId, path) => {
2019-12-10 22:03:00 +01:00
if (noteId === 'root') {
return;
}
2019-12-10 22:03:00 +01:00
if (!childToParents[noteId] || childToParents[noteId].length === 0) {
logError(`No parents found for note ${noteId}`);
2019-12-10 22:03:00 +01:00
this.unrecoveredConsistencyErrors = true;
return;
}
2019-12-10 22:03:00 +01:00
for (const parentNoteId of childToParents[noteId]) {
if (path.includes(parentNoteId)) {
logError(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`);
2019-12-10 22:03:00 +01:00
this.unrecoveredConsistencyErrors = true;
} else {
const newPath = path.slice();
newPath.push(noteId);
2019-12-10 22:03:00 +01:00
checkTreeCycle(parentNoteId, newPath);
}
}
2020-03-11 22:43:20 +01:00
};
2019-02-02 10:38:33 +01:00
2019-12-10 22:03:00 +01:00
const noteIds = Object.keys(childToParents);
2019-12-10 22:03:00 +01:00
for (const noteId of noteIds) {
checkTreeCycle(noteId, []);
}
2019-12-10 22:03:00 +01:00
if (childToParents['root'].length !== 1 || childToParents['root'][0] !== 'none') {
logError('Incorrect root parent: ' + JSON.stringify(childToParents['root']));
this.unrecoveredConsistencyErrors = true;
}
}
2018-11-19 23:11:36 +01:00
2020-06-20 12:31:38 +02:00
findBrokenReferenceIssues() {
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT branchId, branches.noteId
FROM branches
LEFT JOIN notes USING (noteId)
WHERE branches.isDeleted = 0
AND notes.noteId IS NULL`,
2020-06-20 12:31:38 +02:00
({branchId, noteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const branch = repository.getBranch(branchId);
2019-12-10 22:03:00 +01:00
branch.isDeleted = true;
2020-06-20 12:31:38 +02:00
branch.save();
2019-12-10 22:03:00 +01:00
logFix(`Branch ${branchId} has been deleted since it references missing note ${noteId}`);
} else {
logError(`Branch ${branchId} references missing note ${noteId}`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT branchId, branches.noteId AS parentNoteId
FROM branches
LEFT JOIN notes ON notes.noteId = branches.parentNoteId
WHERE branches.isDeleted = 0
AND branches.branchId != 'root'
AND notes.noteId IS NULL`,
2020-06-20 12:31:38 +02:00
({branchId, parentNoteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const branch = repository.getBranch(branchId);
2019-12-10 22:03:00 +01:00
branch.parentNoteId = 'root';
2020-06-20 12:31:38 +02:00
branch.save();
2019-12-10 22:03:00 +01:00
logFix(`Branch ${branchId} was set to root parent since it was referencing missing parent note ${parentNoteId}`);
} else {
logError(`Branch ${branchId} references missing parent note ${parentNoteId}`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT attributeId, attributes.noteId
FROM attributes
LEFT JOIN notes USING (noteId)
WHERE attributes.isDeleted = 0
AND notes.noteId IS NULL`,
2020-06-20 12:31:38 +02:00
({attributeId, noteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const attribute = repository.getAttribute(attributeId);
2019-12-10 22:03:00 +01:00
attribute.isDeleted = true;
2020-06-20 12:31:38 +02:00
attribute.save();
2019-12-10 22:03:00 +01:00
logFix(`Attribute ${attributeId} has been deleted since it references missing source note ${noteId}`);
} else {
logError(`Attribute ${attributeId} references missing source note ${noteId}`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT attributeId, attributes.value AS noteId
FROM attributes
LEFT JOIN notes ON notes.noteId = attributes.value
WHERE attributes.isDeleted = 0
AND attributes.type = 'relation'
AND notes.noteId IS NULL`,
2020-06-20 12:31:38 +02:00
({attributeId, noteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const attribute = repository.getAttribute(attributeId);
2019-12-10 22:03:00 +01:00
attribute.isDeleted = true;
2020-06-20 12:31:38 +02:00
attribute.save();
2019-12-10 22:03:00 +01:00
logFix(`Relation ${attributeId} has been deleted since it references missing note ${noteId}`)
} else {
logError(`Relation ${attributeId} references missing note ${noteId}`)
}
});
}
2020-06-20 12:31:38 +02:00
findExistencyIssues() {
2019-12-10 22:03:00 +01:00
// principle for fixing inconsistencies is that if the note itself is deleted (isDeleted=true) then all related entities should be also deleted (branches, attributes)
// but if note is not deleted, then at least one branch should exist.
// the order here is important - first we might need to delete inconsistent branches and after that
// another check might create missing branch
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT branchId,
noteId
FROM branches
JOIN notes USING (noteId)
WHERE notes.isDeleted = 1
AND branches.isDeleted = 0`,
2020-06-20 12:31:38 +02:00
({branchId, noteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const branch = repository.getBranch(branchId);
2019-12-10 22:03:00 +01:00
branch.isDeleted = true;
2020-06-20 12:31:38 +02:00
branch.save();
2019-12-10 22:03:00 +01:00
logFix(`Branch ${branchId} has been deleted since associated note ${noteId} is deleted.`);
} else {
logError(`Branch ${branchId} is not deleted even though associated note ${noteId} is deleted.`)
}
2019-12-10 22:03:00 +01:00
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT branchId,
parentNoteId
FROM branches
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
WHERE parentNote.isDeleted = 1
AND branches.isDeleted = 0
2020-06-20 12:31:38 +02:00
`, ({branchId, parentNoteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const branch = repository.getBranch(branchId);
2019-12-10 22:03:00 +01:00
branch.isDeleted = true;
2020-06-20 12:31:38 +02:00
branch.save();
2019-12-10 22:03:00 +01:00
logFix(`Branch ${branchId} has been deleted since associated parent note ${parentNoteId} is deleted.`);
} else {
logError(`Branch ${branchId} is not deleted even though associated parent note ${parentNoteId} is deleted.`)
}
2019-02-02 11:26:27 +01:00
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT DISTINCT notes.noteId
FROM notes
LEFT JOIN branches ON notes.noteId = branches.noteId AND branches.isDeleted = 0
WHERE notes.isDeleted = 0
AND branches.branchId IS NULL
2020-06-20 12:31:38 +02:00
`, ({noteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const branch = new Branch({
2019-12-10 22:03:00 +01:00
parentNoteId: 'root',
noteId: noteId,
prefix: 'recovered'
}).save();
logFix(`Created missing branch ${branch.branchId} for note ${noteId}`);
} else {
logError(`No undeleted branch found for note ${noteId}`);
}
});
2019-12-10 22:03:00 +01:00
// there should be a unique relationship between note and its parent
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT noteId,
parentNoteId
FROM branches
WHERE branches.isDeleted = 0
GROUP BY branches.parentNoteId,
branches.noteId
HAVING COUNT(1) > 1`,
2020-06-20 12:31:38 +02:00
({noteId, parentNoteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const branches = repository.getEntities(
2019-12-10 22:03:00 +01:00
`SELECT *
FROM branches
WHERE noteId = ?
and parentNoteId = ?
and isDeleted = 0`, [noteId, parentNoteId]);
// it's not necessarily "original" branch, it's just the only one which will survive
const origBranch = branches[0];
// delete all but the first branch
for (const branch of branches.slice(1)) {
branch.isDeleted = true;
2020-06-20 12:31:38 +02:00
branch.save();
2019-12-10 22:03:00 +01:00
logFix(`Removing branch ${branch.branchId} since it's parent-child duplicate of branch ${origBranch.branchId}`);
}
} else {
logError(`Duplicate branches for note ${noteId} and parent ${parentNoteId}`);
}
});
}
2020-06-20 12:31:38 +02:00
findLogicIssues() {
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT noteId, type
FROM notes
WHERE isDeleted = 0
AND type NOT IN ('text', 'code', 'render', 'file', 'image', 'search', 'relation-map', 'book')`,
2020-06-20 12:31:38 +02:00
({noteId, type}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const note = repository.getNote(noteId);
2019-12-10 22:03:00 +01:00
note.type = 'file'; // file is a safe option to recover notes if type is not known
2020-06-20 12:31:38 +02:00
note.save();
2019-12-10 22:03:00 +01:00
logFix(`Note ${noteId} type has been change to file since it had invalid type=${type}`)
} else {
logError(`Note ${noteId} has invalid type=${type}`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT notes.noteId
FROM notes
LEFT JOIN note_contents USING (noteId)
WHERE note_contents.noteId IS NULL`,
2020-06-20 12:31:38 +02:00
({noteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const note = repository.getNote(noteId);
2020-04-04 09:46:49 +02:00
if (note.isProtected) {
// this is wrong for non-erased notes but we cannot set a valid value for protected notes
2020-06-20 12:31:38 +02:00
sql.upsert("note_contents", "noteId", {
2020-04-04 09:46:49 +02:00
noteId: noteId,
content: null,
hash: "consistency_checks",
utcDateModified: dateUtils.utcNowDateTime()
});
2020-06-20 12:31:38 +02:00
syncTableService.addNoteContentSync(noteId);
2020-04-04 09:46:49 +02:00
}
else {
// empty string might be wrong choice for some note types but it's a best guess
2020-06-20 12:31:38 +02:00
note.setContent(note.isErased ? null : '');
2020-04-04 09:46:49 +02:00
}
2019-12-10 22:03:00 +01:00
logFix(`Note ${noteId} content was set to empty string since there was no corresponding row`);
} else {
logError(`Note ${noteId} content row does not exist`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT noteId
FROM notes
JOIN note_contents USING (noteId)
WHERE isDeleted = 0
2020-04-04 09:46:49 +02:00
AND isProtected = 0
2019-12-10 22:03:00 +01:00
AND content IS NULL`,
2020-06-20 12:31:38 +02:00
({noteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const note = repository.getNote(noteId);
2020-04-04 09:46:49 +02:00
// empty string might be wrong choice for some note types but it's a best guess
2020-06-20 12:31:38 +02:00
note.setContent('');
2019-12-10 22:03:00 +01:00
logFix(`Note ${noteId} content was set to empty string since it was null even though it is not deleted`);
} else {
logError(`Note ${noteId} content is null even though it is not deleted`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT noteId
FROM notes
JOIN note_contents USING (noteId)
WHERE isErased = 1
AND content IS NOT NULL`,
2020-06-20 12:31:38 +02:00
({noteId}) => {
// we always fix this issue because there does not seem to be a good way to prevent it.
// Scenario in which this can happen:
// 1. user on instance A deletes the note (sync for notes is created, but not for note_contents) and is later erased
// 2. instance B gets synced from instance A, note is updated because of sync row for notes,
// but note_contents is not because erasion does not create sync rows
// 3. therefore note.isErased = true, but note_contents.content remains not updated and not erased.
//
// Considered solutions:
// - don't sync erased notes - this might prevent syncing also of the isDeleted flag and note would continue
// to exist on the other instance
// - create sync rows for erased event - this would be a problem for undeletion since erasion might happen
// on one instance after undelete and thus would win even though there's no user action behind it
//
// So instead we just fix such cases afterwards here.
2020-06-20 12:31:38 +02:00
sql.execute(`UPDATE note_contents SET content = NULL WHERE noteId = ?`, [noteId]);
logFix(`Note ${noteId} content has been set to null since the note is erased`);
});
2019-12-10 22:03:00 +01:00
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT noteId, noteRevisionId
FROM notes
JOIN note_revisions USING (noteId)
WHERE notes.isErased = 1
AND note_revisions.isErased = 0`,
2020-06-20 12:31:38 +02:00
({noteId, noteRevisionId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const noteRevision = repository.getNoteRevision(noteRevisionId);
2019-12-10 22:03:00 +01:00
noteRevision.isErased = true;
2020-06-20 12:31:38 +02:00
noteRevision.setContent(null);
noteRevision.save();
2019-12-10 22:03:00 +01:00
logFix(`Note revision ${noteRevisionId} has been erased since its note ${noteId} is also erased.`);
} else {
logError(`Note revision ${noteRevisionId} is not erased even though note ${noteId} is erased.`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT note_revisions.noteRevisionId
FROM note_revisions
LEFT JOIN note_revision_contents USING (noteRevisionId)
2020-04-04 09:46:49 +02:00
WHERE note_revision_contents.noteRevisionId IS NULL
AND note_revisions.isProtected = 0`,
2020-06-20 12:31:38 +02:00
({noteRevisionId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const noteRevision = repository.getNoteRevision(noteRevisionId);
noteRevision.setContent(null);
2019-12-10 22:03:00 +01:00
noteRevision.isErased = true;
2020-06-20 12:31:38 +02:00
noteRevision.save();
2019-12-10 22:03:00 +01:00
logFix(`Note revision content ${noteRevisionId} was created and set to erased since it did not exist.`);
} else {
logError(`Note revision content ${noteRevisionId} does not exist`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT noteRevisionId
FROM note_revisions
JOIN note_revision_contents USING (noteRevisionId)
WHERE isErased = 0
AND content IS NULL`,
2020-06-20 12:31:38 +02:00
({noteRevisionId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const noteRevision = repository.getNoteRevision(noteRevisionId);
2019-12-10 22:03:00 +01:00
noteRevision.isErased = true;
2020-06-20 12:31:38 +02:00
noteRevision.save();
2019-12-10 22:03:00 +01:00
logFix(`Note revision ${noteRevisionId} content was set to empty string since it was null even though it is not erased`);
} else {
logError(`Note revision ${noteRevisionId} content is null even though it is not erased`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT noteRevisionId
FROM note_revisions
JOIN note_revision_contents USING (noteRevisionId)
WHERE isErased = 1
AND content IS NOT NULL`,
2020-06-20 12:31:38 +02:00
({noteRevisionId}) => {
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
sql.execute(`UPDATE note_revision_contents SET content = NULL WHERE noteRevisionId = ?`, [noteRevisionId]);
logFix(`Note revision ${noteRevisionId} content was set to null since the note revision is erased`);
}
else {
logError(`Note revision ${noteRevisionId} content is not null even though the note revision is erased`);
}
});
2019-12-10 22:03:00 +01:00
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT noteId
FROM notes
WHERE isErased = 1
AND isDeleted = 0`,
2020-06-20 12:31:38 +02:00
({noteId}) => {
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const note = repository.getNote(noteId);
note.isDeleted = true;
2020-06-20 12:31:38 +02:00
note.save();
logFix(`Note ${noteId} was set to deleted since it is erased`);
}
else {
logError(`Note ${noteId} is not deleted even though it is erased`);
}
});
2019-12-10 22:03:00 +01:00
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT parentNoteId
FROM branches
JOIN notes ON notes.noteId = branches.parentNoteId
WHERE notes.isDeleted = 0
AND notes.type == 'search'
AND branches.isDeleted = 0`,
2020-06-20 12:31:38 +02:00
({parentNoteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const branches = repository.getEntities(`SELECT *
2019-12-10 22:03:00 +01:00
FROM branches
WHERE isDeleted = 0
AND parentNoteId = ?`, [parentNoteId]);
for (const branch of branches) {
branch.parentNoteId = 'root';
2020-06-20 12:31:38 +02:00
branch.save();
2019-12-10 22:03:00 +01:00
logFix(`Child branch ${branch.branchId} has been moved to root since it was a child of a search note ${parentNoteId}`)
}
} else {
logError(`Search note ${parentNoteId} has children`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT attributeId
FROM attributes
WHERE isDeleted = 0
AND type = 'relation'
AND value = ''`,
2020-06-20 12:31:38 +02:00
({attributeId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const relation = repository.getAttribute(attributeId);
2019-12-10 22:03:00 +01:00
relation.isDeleted = true;
2020-06-20 12:31:38 +02:00
relation.save();
2019-12-10 22:03:00 +01:00
logFix(`Removed relation ${relation.attributeId} of name "${relation.name} with empty target.`);
} else {
logError(`Relation ${attributeId} has empty target.`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT attributeId,
type
FROM attributes
WHERE isDeleted = 0
AND type != 'label'
AND type != 'label-definition'
AND type != 'relation'
AND type != 'relation-definition'`,
2020-06-20 12:31:38 +02:00
({attributeId, type}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const attribute = repository.getAttribute(attributeId);
2019-12-10 22:03:00 +01:00
attribute.type = 'label';
2020-06-20 12:31:38 +02:00
attribute.save();
2019-12-10 22:03:00 +01:00
logFix(`Attribute ${attributeId} type was changed to label since it had invalid type '${type}'`);
} else {
logError(`Attribute ${attributeId} has invalid type '${type}'`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT attributeId,
attributes.noteId
FROM attributes
JOIN notes ON attributes.noteId = notes.noteId
WHERE attributes.isDeleted = 0
AND notes.isDeleted = 1`,
2020-06-20 12:31:38 +02:00
({attributeId, noteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const attribute = repository.getAttribute(attributeId);
2019-12-10 22:03:00 +01:00
attribute.isDeleted = true;
2020-06-20 12:31:38 +02:00
attribute.save();
2019-12-10 22:03:00 +01:00
logFix(`Removed attribute ${attributeId} because owning note ${noteId} is also deleted.`);
} else {
logError(`Attribute ${attributeId} is not deleted even though owning note ${noteId} is deleted.`);
}
});
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
2019-12-10 22:03:00 +01:00
SELECT attributeId,
attributes.value AS targetNoteId
FROM attributes
JOIN notes ON attributes.value = notes.noteId
WHERE attributes.type = 'relation'
AND attributes.isDeleted = 0
AND notes.isDeleted = 1`,
2020-06-20 12:31:38 +02:00
({attributeId, targetNoteId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
const attribute = repository.getAttribute(attributeId);
2019-12-10 22:03:00 +01:00
attribute.isDeleted = true;
2020-06-20 12:31:38 +02:00
attribute.save();
2019-12-10 22:03:00 +01:00
logFix(`Removed attribute ${attributeId} because target note ${targetNoteId} is also deleted.`);
} else {
logError(`Attribute ${attributeId} is not deleted even though target note ${targetNoteId} is deleted.`);
}
});
}
2020-06-20 12:31:38 +02:00
runSyncRowChecks(entityName, key) {
this.findAndFixIssues(`
2019-02-02 11:26:27 +01:00
SELECT
${key} as entityId
FROM
${entityName}
LEFT JOIN sync ON sync.entityName = '${entityName}' AND entityId = ${key}
WHERE
sync.id IS NULL AND ` + (entityName === 'options' ? 'options.isSynced = 1' : '1'),
2020-06-20 12:31:38 +02:00
({entityId}) => {
2019-12-10 22:03:00 +01:00
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
syncTableService.addEntitySync(entityName, entityId);
2019-02-02 11:26:27 +01:00
2019-12-10 22:03:00 +01:00
logFix(`Created missing sync record for entityName=${entityName}, entityId=${entityId}`);
} else {
logError(`Missing sync record for entityName=${entityName}, entityId=${entityId}`);
}
});
2019-02-02 11:26:27 +01:00
2020-06-20 12:31:38 +02:00
this.findAndFixIssues(`
SELECT
id, entityId
FROM
sync
LEFT JOIN ${entityName} ON entityId = ${key}
WHERE
sync.entityName = '${entityName}'
AND ${key} IS NULL`,
2020-06-20 12:31:38 +02:00
({id, entityId}) => {
if (this.autoFix) {
2020-06-20 12:31:38 +02:00
sql.execute("DELETE FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
} else {
logError(`Unrecognized sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
}
});
2019-12-10 22:03:00 +01:00
}
2019-02-02 11:26:27 +01:00
2020-06-20 12:31:38 +02:00
findSyncRowsIssues() {
this.runSyncRowChecks("notes", "noteId");
this.runSyncRowChecks("note_contents", "noteId");
this.runSyncRowChecks("note_revisions", "noteRevisionId");
this.runSyncRowChecks("branches", "branchId");
this.runSyncRowChecks("recent_notes", "noteId");
this.runSyncRowChecks("attributes", "attributeId");
this.runSyncRowChecks("api_tokens", "apiTokenId");
this.runSyncRowChecks("options", "name");
2019-12-10 22:03:00 +01:00
}
2019-02-02 11:26:27 +01:00
2020-06-20 12:31:38 +02:00
runAllChecks() {
2019-12-10 22:03:00 +01:00
this.unrecoveredConsistencyErrors = false;
this.fixedIssues = false;
2019-02-02 11:26:27 +01:00
2020-06-20 12:31:38 +02:00
this.findBrokenReferenceIssues();
2019-02-02 11:26:27 +01:00
2020-06-20 12:31:38 +02:00
this.findExistencyIssues();
2019-02-02 11:26:27 +01:00
2020-06-20 12:31:38 +02:00
this.findLogicIssues();
2019-02-02 11:26:27 +01:00
2020-06-20 12:31:38 +02:00
this.findSyncRowsIssues();
// root branch should always be expanded
2020-06-20 12:31:38 +02:00
sql.execute("UPDATE branches SET isExpanded = 1 WHERE branchId = 'root'");
2019-12-10 22:03:00 +01:00
if (this.unrecoveredConsistencyErrors) {
// we run this only if basic checks passed since this assumes basic data consistency
2018-01-01 19:41:22 -05:00
2020-06-20 12:31:38 +02:00
this.checkTreeCycles();
2019-12-10 22:03:00 +01:00
}
2018-01-01 19:41:22 -05:00
2019-12-10 22:03:00 +01:00
return !this.unrecoveredConsistencyErrors;
}
2020-06-20 12:31:38 +02:00
showEntityStat(name, query) {
const map = sql.getMap(query);
2019-12-10 22:03:00 +01:00
map[0] = map[0] || 0;
map[1] = map[1] || 0;
2019-12-10 22:03:00 +01:00
log.info(`${name} deleted: ${map[1]}, not deleted ${map[0]}`);
}
2020-06-20 12:31:38 +02:00
runDbDiagnostics() {
this.showEntityStat("Notes", `SELECT isDeleted, count(1)
2019-12-10 22:03:00 +01:00
FROM notes
GROUP BY isDeleted`);
2020-06-20 12:31:38 +02:00
this.showEntityStat("Note revisions", `SELECT isErased, count(1)
2019-12-10 22:03:00 +01:00
FROM note_revisions
GROUP BY isErased`);
2020-06-20 12:31:38 +02:00
this.showEntityStat("Branches", `SELECT isDeleted, count(1)
2019-12-10 22:03:00 +01:00
FROM branches
GROUP BY isDeleted`);
2020-06-20 12:31:38 +02:00
this.showEntityStat("Attributes", `SELECT isDeleted, count(1)
2019-12-10 22:03:00 +01:00
FROM attributes
GROUP BY isDeleted`);
2020-06-20 12:31:38 +02:00
this.showEntityStat("API tokens", `SELECT isDeleted, count(1)
2019-12-10 22:03:00 +01:00
FROM api_tokens
GROUP BY isDeleted`);
}
2019-12-10 22:03:00 +01:00
async runChecks() {
let elapsedTimeMs;
2020-06-20 12:31:38 +02:00
await syncMutexService.doExclusively(() => {
2019-12-10 22:03:00 +01:00
const startTime = new Date();
2020-06-20 12:31:38 +02:00
this.runDbDiagnostics();
2020-06-20 12:31:38 +02:00
this.runAllChecks();
2019-12-10 22:03:00 +01:00
elapsedTimeMs = Date.now() - startTime.getTime();
});
2019-12-10 22:03:00 +01:00
if (this.unrecoveredConsistencyErrors) {
log.info(`Consistency checks failed (took ${elapsedTimeMs}ms)`);
2018-01-01 19:41:22 -05:00
2019-12-10 22:03:00 +01:00
ws.sendMessageToAllClients({type: 'consistency-checks-failed'});
} else {
log.info(`All consistency checks passed (took ${elapsedTimeMs}ms)`);
}
}
2017-12-14 22:16:26 -05:00
}
2019-02-02 09:26:57 +01:00
function logFix(message) {
log.info("Consistency issue fixed: " + message);
}
function logError(message) {
log.info("Consistency error: " + message);
}
2020-06-20 12:31:38 +02:00
function runPeriodicChecks() {
const autoFix = optionsService.getOptionBool('autoFixConsistencyIssues');
2019-12-10 22:03:00 +01:00
const consistencyChecks = new ConsistencyChecks(autoFix);
2020-06-20 12:31:38 +02:00
consistencyChecks.runChecks();
2019-12-10 22:03:00 +01:00
}
2020-06-20 12:31:38 +02:00
function runOnDemandChecks(autoFix) {
2019-12-10 22:03:00 +01:00
const consistencyChecks = new ConsistencyChecks(autoFix);
2020-06-20 12:31:38 +02:00
consistencyChecks.runChecks();
2019-12-10 22:03:00 +01:00
}
2020-06-20 21:42:41 +02:00
sqlInit.dbReady.then(() => {
setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000);
2017-12-14 22:16:26 -05:00
2020-06-20 21:42:41 +02:00
// kickoff checks soon after startup (to not block the initial load)
setTimeout(cls.wrap(runPeriodicChecks), 20 * 1000);
});
2017-12-14 22:16:26 -05:00
2019-12-10 22:03:00 +01:00
module.exports = {
runOnDemandChecks
2020-06-20 12:31:38 +02:00
};