2017-12-14 22:16:26 -05:00
"use strict" ;
const sql = require ( './sql' ) ;
2018-04-02 21:25:20 -04:00
const sqlInit = require ( './sql_init' ) ;
2017-12-14 22:16:26 -05:00
const log = require ( './log' ) ;
2021-06-29 22:15:57 +02:00
const ws = require ( './ws' ) ;
2018-04-01 21:27:46 -04:00
const syncMutexService = require ( './sync_mutex' ) ;
2018-03-28 23:41:22 -04:00
const cls = require ( './cls' ) ;
2021-06-29 22:15:57 +02:00
const entityChangesService = require ( './entity_changes' ) ;
2019-11-10 11:25:41 +01:00
const optionsService = require ( './options' ) ;
2023-01-03 13:52:37 +01:00
const BBranch = require ( '../becca/entities/bbranch' ) ;
2020-12-16 15:01:20 +01:00
const noteRevisionService = require ( './note_revisions' ) ;
2021-06-29 22:15:57 +02:00
const becca = require ( "../becca/becca" ) ;
2021-06-29 23:45:45 +02:00
const utils = require ( "../services/utils" ) ;
2022-12-27 10:22:50 +01:00
const { sanitizeAttributeName } = require ( "./sanitize_attribute_name" ) ;
2022-12-16 16:00:49 +01:00
const noteTypes = require ( "../services/note_types" ) . getNoteTypeNames ( ) ;
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 ;
2021-10-03 10:00:38 +02:00
this . reloadNeeded = false ;
2017-12-14 22:16:26 -05:00
}
2019-02-01 22:48:51 +01:00
2020-06-20 12:31:38 +02:00
findAndFixIssues ( query , fixerCb ) {
const results = sql . getRows ( query ) ;
2019-02-01 22:48:51 +01:00
2019-12-10 22:03:00 +01:00
for ( const res of results ) {
try {
2020-07-01 22:42:59 +02:00
sql . transactional ( ( ) => 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 ) ;
2019-11-10 14:16:12 +01:00
}
2019-01-21 22:51:49 +01:00
2022-05-03 00:30:09 +02:00
/** @returns {boolean} true if cycle was found and we should try again */
2020-03-11 22:43:20 +01:00
const checkTreeCycle = ( noteId , path ) => {
2019-12-10 22:03:00 +01:00
if ( noteId === 'root' ) {
2022-05-03 00:30:09 +02:00
return false ;
2019-11-10 14:16:12 +01:00
}
2019-12-10 22:03:00 +01:00
for ( const parentNoteId of childToParents [ noteId ] ) {
if ( path . includes ( parentNoteId ) ) {
2022-05-03 00:30:09 +02:00
if ( this . autoFix ) {
const branch = becca . getBranchFromChildAndParent ( noteId , parentNoteId ) ;
branch . markAsDeleted ( 'cycle-autofix' ) ;
logFix ( ` Branch ' ${ branch . branchId } ' between child ' ${ noteId } ' and parent ' ${ parentNoteId } ' has been deleted since it was causing a tree cycle. ` ) ;
2019-02-02 12:41:20 +01:00
2022-05-03 00:30:09 +02:00
return true ;
}
else {
2022-12-27 10:22:50 +01:00
logError ( ` Tree cycle detected at parent-child relationship: ' ${ parentNoteId } ' - ' ${ noteId } ', whole path: ' ${ path } ' ` ) ;
2022-05-03 00:30:09 +02:00
this . unrecoveredConsistencyErrors = true ;
}
2019-12-10 22:03:00 +01:00
} else {
const newPath = path . slice ( ) ;
newPath . push ( noteId ) ;
2019-11-10 11:43:33 +01:00
2022-05-03 00:30:09 +02:00
const retryNeeded = checkTreeCycle ( parentNoteId , newPath ) ;
if ( retryNeeded ) {
return true ;
}
2019-12-10 22:03:00 +01:00
}
2019-11-10 11:43:33 +01:00
}
2022-05-03 00:30:09 +02:00
return false ;
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-11-10 14:16:12 +01:00
2019-12-10 22:03:00 +01:00
for ( const noteId of noteIds ) {
2022-05-03 00:30:09 +02:00
const retryNeeded = checkTreeCycle ( noteId , [ ] ) ;
if ( retryNeeded ) {
return true ;
}
}
return false ;
}
checkAndRepairTreeCycles ( ) {
let treeFixed = false ;
while ( this . checkTreeCycles ( ) ) {
// fixing cycle means deleting branches, we might need to create a new branch to recover the note
this . findExistencyIssues ( ) ;
treeFixed = true ;
2019-12-10 22:03:00 +01:00
}
2019-11-23 19:56:52 +01:00
2022-05-03 00:30:09 +02:00
if ( treeFixed ) {
this . reloadNeeded = true ;
2019-12-10 22:03:00 +01:00
}
}
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
2020-07-01 22:42:59 +02:00
LEFT JOIN notes USING ( noteId )
2019-12-10 22:03:00 +01:00
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 ) {
2021-05-02 11:23:58 +02:00
const branch = becca . getBranch ( branchId ) ;
2021-05-02 20:32:50 +02:00
branch . markAsDeleted ( ) ;
2019-11-11 23:26:46 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Branch ' ${ branchId } ' has been deleted since it references missing note ' ${ noteId } ' ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Branch ' ${ branchId } ' references missing note ' ${ noteId } ' ` ) ;
2019-12-10 22:03:00 +01:00
}
} ) ;
2020-06-20 12:31:38 +02:00
this . findAndFixIssues ( `
2022-11-06 15:18:32 +01:00
SELECT branchId , branches . parentNoteId AS parentNoteId
2019-12-10 22:03:00 +01:00
FROM branches
2020-07-01 22:42:59 +02:00
LEFT JOIN notes ON notes . noteId = branches . parentNoteId
2019-12-10 22:03:00 +01:00
WHERE branches . isDeleted = 0
2022-12-27 14:44:28 +01:00
AND branches . noteId != 'root'
2019-12-10 22:03:00 +01:00
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 ) {
2021-05-02 11:23:58 +02:00
const branch = becca . getBranch ( branchId ) ;
2019-12-10 22:03:00 +01:00
branch . parentNoteId = 'root' ;
2020-06-20 12:31:38 +02:00
branch . save ( ) ;
2019-11-23 19:56:52 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Branch ' ${ branchId } ' was set to root parent since it was referencing missing parent note ' ${ parentNoteId } ' ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Branch ' ${ branchId } ' references missing parent note ' ${ parentNoteId } ' ` ) ;
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 attributeId , attributes . noteId
FROM attributes
2020-07-01 22:42:59 +02:00
LEFT JOIN notes USING ( noteId )
2019-12-10 22:03:00 +01:00
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 ) {
2021-05-02 11:23:58 +02:00
const attribute = becca . getAttribute ( attributeId ) ;
2021-05-02 20:32:50 +02:00
attribute . markAsDeleted ( ) ;
2019-12-10 22:03:00 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Attribute ' ${ attributeId } ' has been deleted since it references missing source note ' ${ noteId } ' ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Attribute ' ${ attributeId } ' references missing source note ' ${ noteId } ' ` ) ;
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 attributeId , attributes . value AS noteId
FROM attributes
2020-07-01 22:42:59 +02:00
LEFT JOIN notes ON notes . noteId = attributes . value
2019-12-10 22:03:00 +01:00
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 ) {
2021-05-02 11:23:58 +02:00
const attribute = becca . getAttribute ( attributeId ) ;
2021-05-02 20:32:50 +02:00
attribute . markAsDeleted ( ) ;
2019-12-10 22:03:00 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Relation ' ${ attributeId } ' has been deleted since it references missing note ' ${ noteId } ' ` )
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Relation ' ${ attributeId } ' references missing note ' ${ noteId } ' ` )
2019-12-10 22:03:00 +01:00
}
} ) ;
}
2019-11-10 14:16:12 +01:00
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
2020-07-01 22:42:59 +02:00
JOIN notes USING ( noteId )
2019-12-10 22:03:00 +01:00
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 ) {
2021-05-02 11:23:58 +02:00
const branch = becca . getBranch ( branchId ) ;
2021-05-02 20:32:50 +02:00
branch . markAsDeleted ( ) ;
2019-11-10 14:16:12 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Branch ' ${ branchId } ' has been deleted since associated note ' ${ noteId } ' is deleted. ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Branch ' ${ branchId } ' is not deleted even though associated note ' ${ noteId } ' is deleted. ` )
2019-11-10 14:16:12 +01:00
}
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
2020-07-01 22:42:59 +02:00
JOIN notes AS parentNote ON parentNote . noteId = branches . parentNoteId
2019-12-10 22:03:00 +01:00
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 ) {
2021-05-02 11:23:58 +02:00
const branch = becca . getBranch ( branchId ) ;
2021-05-02 20:32:50 +02:00
branch . markAsDeleted ( ) ;
2018-03-13 19:18:52 -04:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Branch ' ${ branchId } ' has been deleted since associated parent note ' ${ parentNoteId } ' is deleted. ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Branch ' ${ branchId } ' is not deleted even though associated parent note ' ${ parentNoteId } ' is deleted. ` )
2019-11-10 11:43:33 +01:00
}
2019-02-02 11:26:27 +01:00
} ) ;
2018-11-15 13:58:14 +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
2020-07-01 22:42:59 +02:00
LEFT JOIN branches ON notes . noteId = branches . noteId AND branches . isDeleted = 0
2019-12-10 22:03:00 +01:00
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 ) {
2023-01-03 13:52:37 +01:00
const branch = new BBranch ( {
2019-12-10 22:03:00 +01:00
parentNoteId : 'root' ,
noteId : noteId ,
prefix : 'recovered'
} ) . save ( ) ;
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Created missing branch ' ${ branch . branchId } ' for note ' ${ noteId } ' ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` No undeleted branch found for note ' ${ noteId } ' ` ) ;
2019-11-10 14:16:12 +01:00
}
} ) ;
2018-11-15 13:58:14 +01:00
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 ) {
2021-05-02 19:59:16 +02:00
const branchIds = sql . getColumn (
` SELECT branchId
2019-12-10 22:03:00 +01:00
FROM branches
WHERE noteId = ?
and parentNoteId = ?
2021-12-21 13:22:13 +01:00
and isDeleted = 0
2021-12-27 13:37:51 +01:00
ORDER BY utcDateModified ` , [noteId, parentNoteId]);
2019-12-10 22:03:00 +01:00
2021-05-02 19:59:16 +02:00
const branches = branchIds . map ( branchId => becca . getBranch ( branchId ) ) ;
2019-12-10 22:03:00 +01:00
// 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 ) ) {
2021-05-02 20:32:50 +02:00
branch . markAsDeleted ( ) ;
2019-12-10 22:03:00 +01:00
2022-12-27 21:17:40 +01:00
logFix ( ` Removing branch ' ${ branch . branchId } ' since it's a parent-child duplicate of branch ' ${ origBranch . branchId } ' ` ) ;
2019-12-10 22:03:00 +01:00
}
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Duplicate branches for note ' ${ noteId } ' and parent ' ${ parentNoteId } ' ` ) ;
2019-12-10 22:03:00 +01:00
}
} ) ;
}
2018-11-15 13:58:14 +01:00
2020-06-20 12:31:38 +02:00
findLogicIssues ( ) {
2022-01-12 19:32:23 +01:00
const noteTypesStr = noteTypes . map ( nt => ` ' ${ nt } ' ` ) . join ( ", " ) ;
2022-01-31 21:25:18 +01:00
2020-06-20 12:31:38 +02:00
this . findAndFixIssues ( `
2019-12-10 22:03:00 +01:00
SELECT noteId , type
FROM notes
WHERE isDeleted = 0
2022-01-12 19:32:23 +01:00
AND type NOT IN ( $ { noteTypesStr } ) ` ,
2020-06-20 12:31:38 +02:00
( { noteId , type } ) => {
2019-12-10 22:03:00 +01:00
if ( this . autoFix ) {
2021-05-02 11:23:58 +02:00
const note = becca . 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
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Note ' ${ noteId } ' type has been change to file since it had invalid type ' ${ type } ' ` )
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Note ' ${ noteId } ' has invalid type ' ${ type } ' ` ) ;
2019-12-10 22:03:00 +01:00
}
} ) ;
2020-06-20 12:31:38 +02:00
this . findAndFixIssues ( `
2021-09-11 14:34:37 +02:00
SELECT notes . noteId , notes . isProtected , notes . type , notes . mime
2019-12-10 22:03:00 +01:00
FROM notes
2020-07-01 22:42:59 +02:00
LEFT JOIN note _contents USING ( noteId )
2019-12-10 22:03:00 +01:00
WHERE note _contents . noteId IS NULL ` ,
2021-09-11 14:34:37 +02:00
( { noteId , isProtected , type , mime } ) => {
2019-12-10 22:03:00 +01:00
if ( this . autoFix ) {
2021-09-12 11:18:06 +02:00
// it might be possible that the note_content is not available only because of the interrupted
2023-01-15 21:04:17 +01:00
// sync, and it will come later. It's therefore important to guarantee that this artifical
2021-09-12 11:18:06 +02:00
// record won't overwrite the real one coming from the sync.
const fakeDate = "2000-01-01 00:00:00Z" ;
2021-09-11 14:34:37 +02:00
// manually creating row since this can also affect deleted notes
sql . upsert ( "note_contents" , "noteId" , {
noteId : noteId ,
content : getBlankContent ( isProtected , type , mime ) ,
2021-09-12 11:18:06 +02:00
utcDateModified : fakeDate ,
dateModified : fakeDate
2021-09-11 14:34:37 +02:00
} ) ;
2020-04-04 09:46:49 +02:00
2021-09-11 14:34:37 +02:00
const hash = utils . hash ( utils . randomString ( 10 ) ) ;
entityChangesService . addEntityChange ( {
entityName : 'note_contents' ,
entityId : noteId ,
hash : hash ,
isErased : false ,
2021-09-12 11:18:06 +02:00
utcDateChanged : fakeDate ,
2021-09-11 14:34:37 +02:00
isSynced : true
} ) ;
2019-12-10 22:03:00 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Note ' ${ noteId } ' content was set to empty string since there was no corresponding row ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Note ' ${ noteId } ' content row does not exist ` ) ;
2019-12-10 22:03:00 +01:00
}
} ) ;
2022-05-31 14:09:46 +02:00
if ( sqlInit . getDbSize ( ) < 500000 ) {
// querying for "content IS NULL" is expensive since content is not indexed. See e.g. https://github.com/zadam/trilium/issues/2887
this . findAndFixIssues ( `
SELECT notes . noteId , notes . type , notes . mime
FROM notes
JOIN note _contents USING ( noteId )
WHERE isDeleted = 0
AND isProtected = 0
AND content IS NULL ` ,
( { noteId , type , mime } ) => {
if ( this . autoFix ) {
const note = becca . getNote ( noteId ) ;
const blankContent = getBlankContent ( false , type , mime ) ;
note . setContent ( blankContent ) ;
2019-12-10 22:03:00 +01:00
2022-05-31 14:09:46 +02:00
this . reloadNeeded = true ;
2021-10-03 10:00:38 +02:00
2022-12-27 10:22:50 +01:00
logFix ( ` Note ' ${ noteId } ' content was set to ' ${ blankContent } ' since it was null even though it is not deleted ` ) ;
2022-05-31 14:09:46 +02:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Note ' ${ noteId } ' content is null even though it is not deleted ` ) ;
2022-05-31 14:09:46 +02:00
}
} ) ;
}
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 note _revisions . noteRevisionId
FROM note _revisions
2020-07-01 22:42:59 +02:00
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-12-16 15:01:20 +01:00
noteRevisionService . eraseNoteRevisions ( [ noteRevisionId ] ) ;
2019-12-10 22:03:00 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Note revision content ' ${ noteRevisionId } ' was created and set to erased since it did not exist. ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Note revision content ' ${ noteRevisionId } ' does not exist ` ) ;
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
2020-07-01 22:42:59 +02:00
JOIN notes ON notes . noteId = branches . parentNoteId
2019-12-10 22:03:00 +01:00
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 ) {
2021-09-12 11:18:06 +02:00
const branchIds = sql . getColumn ( `
SELECT branchId
FROM branches
WHERE isDeleted = 0
AND parentNoteId = ? ` , [parentNoteId]);
2019-12-10 22:03:00 +01:00
2021-05-02 19:59:16 +02:00
const branches = branchIds . map ( branchId => becca . getBranch ( branchId ) ) ;
2019-12-10 22:03:00 +01:00
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
2022-12-27 10:22:50 +01:00
logFix ( ` Child branch ' ${ branch . branchId } ' has been moved to root since it was a child of a search note ' ${ parentNoteId } ' ` )
2019-12-10 22:03:00 +01:00
}
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Search note ' ${ parentNoteId } ' has children ` ) ;
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 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 ) {
2021-05-02 11:23:58 +02:00
const relation = becca . getAttribute ( attributeId ) ;
2021-05-02 20:32:50 +02:00
relation . markAsDeleted ( ) ;
2019-12-10 22:03:00 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Removed relation ' ${ relation . attributeId } ' of name ' ${ relation . name } ' with empty target. ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Relation ' ${ attributeId } ' has empty target. ` ) ;
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 attributeId ,
type
FROM attributes
WHERE isDeleted = 0
AND type != 'label'
2020-09-08 20:42:50 +02:00
AND type != 'relation' ` ,
2020-06-20 12:31:38 +02:00
( { attributeId , type } ) => {
2019-12-10 22:03:00 +01:00
if ( this . autoFix ) {
2021-05-02 11:23:58 +02:00
const attribute = becca . 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
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Attribute ' ${ attributeId } ' type was changed to label since it had invalid type ' ${ type } ' ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Attribute ' ${ attributeId } ' has invalid type ' ${ type } ' ` ) ;
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 attributeId ,
attributes . noteId
FROM attributes
2021-09-12 11:18:06 +02:00
JOIN notes ON attributes . noteId = notes . noteId
2019-12-10 22:03:00 +01:00
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 ) {
2021-05-02 11:23:58 +02:00
const attribute = becca . getAttribute ( attributeId ) ;
2021-05-02 20:32:50 +02:00
attribute . markAsDeleted ( ) ;
2019-12-10 22:03:00 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Removed attribute ' ${ attributeId } ' because owning note ' ${ noteId } ' is also deleted. ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Attribute ' ${ attributeId } ' is not deleted even though owning 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 attributeId ,
attributes . value AS targetNoteId
FROM attributes
2021-09-12 11:18:06 +02:00
JOIN notes ON attributes . value = notes . noteId
2019-12-10 22:03:00 +01:00
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 ) {
2021-05-02 11:23:58 +02:00
const attribute = becca . getAttribute ( attributeId ) ;
2021-05-02 20:32:50 +02:00
attribute . markAsDeleted ( ) ;
2019-12-10 22:03:00 +01:00
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Removed attribute ' ${ attributeId } ' because target note ' ${ targetNoteId } ' is also deleted. ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Attribute ' ${ attributeId } ' is not deleted even though target note ' ${ targetNoteId } ' is deleted. ` ) ;
2019-12-10 22:03:00 +01:00
}
} ) ;
}
2019-02-01 22:48:51 +01:00
2020-08-02 23:43:39 +02:00
runEntityChangeChecks ( entityName , key ) {
2020-06-20 12:31:38 +02:00
this . findAndFixIssues ( `
2021-09-08 21:54:26 +02:00
SELECT
2021-08-07 21:21:30 +02:00
$ { key } as entityId
2021-09-08 21:54:26 +02:00
FROM
2021-08-07 21:21:30 +02:00
$ { entityName }
LEFT JOIN entity _changes ON entity _changes . entityName = '${entityName}'
AND entity _changes . entityId = $ { key }
WHERE
entity _changes . id IS NULL ` ,
2020-06-20 12:31:38 +02:00
( { entityId } ) => {
2021-09-08 21:54:26 +02:00
const entity = sql . getRow ( ` SELECT * FROM ${ entityName } WHERE ${ key } = ? ` , [ entityId ] ) ;
2021-08-07 21:21:30 +02:00
2021-09-08 21:54:26 +02:00
if ( this . autoFix ) {
entityChangesService . addEntityChange ( {
entityName ,
entityId ,
hash : utils . randomString ( 10 ) , // doesn't matter, will force sync but that's OK
isErased : ! ! entity . isErased ,
utcDateChanged : entity . utcDateModified || entity . utcDateCreated ,
isSynced : entityName !== 'options' || entity . isSynced
} ) ;
2019-02-02 11:26:27 +01:00
2022-12-27 10:22:50 +01:00
logFix ( ` Created missing entity change for entityName ' ${ entityName } ', entityId ' ${ entityId } ' ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Missing entity change for 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
this . findAndFixIssues ( `
2020-02-19 19:51:36 +01:00
SELECT
id , entityId
FROM
2020-08-02 23:43:39 +02:00
entity _changes
2020-02-19 19:51:36 +01:00
LEFT JOIN $ { entityName } ON entityId = $ { key }
2020-12-14 13:15:32 +01:00
WHERE
entity _changes . isErased = 0
AND entity _changes . entityName = '${entityName}'
2020-02-19 19:51:36 +01:00
AND $ { key } IS NULL ` ,
2020-06-20 12:31:38 +02:00
( { id , entityId } ) => {
2020-02-19 19:51:36 +01:00
if ( this . autoFix ) {
2020-08-02 23:27:48 +02:00
sql . execute ( "DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?" , [ entityName , entityId ] ) ;
2020-02-19 19:51:36 +01:00
2022-12-27 10:22:50 +01:00
logFix ( ` Deleted extra entity change id ' ${ id } ', entityName ' ${ entityName } ', entityId ' ${ entityId } ' ` ) ;
2020-02-19 19:51:36 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Unrecognized entity change id ' ${ id } ', entityName ' ${ entityName } ', entityId ' ${ entityId } ' ` ) ;
2020-02-19 19:51:36 +01:00
}
} ) ;
2021-12-21 14:25:26 +01:00
this . findAndFixIssues ( `
SELECT
id , entityId
FROM
entity _changes
JOIN $ { entityName } ON entityId = $ { key }
WHERE
entity _changes . isErased = 1
AND entity _changes . entityName = '${entityName}' ` ,
( { id , entityId } ) => {
if ( this . autoFix ) {
sql . execute ( ` DELETE FROM ${ entityName } WHERE ${ key } = ? ` , [ entityId ] ) ;
this . reloadNeeded = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` Erasing entityName ' ${ entityName } ', entityId ' ${ entityId } ' since entity change id ' ${ id } ' has it as erased. ` ) ;
2021-12-21 14:25:26 +01:00
} else {
2022-12-27 10:22:50 +01:00
logError ( ` Entity change id ' ${ id } ' has entityName ' ${ entityName } ', entityId ' ${ entityId } ' as erased, but it's not. ` ) ;
2021-12-21 14:25:26 +01:00
}
} ) ;
2019-12-10 22:03:00 +01:00
}
2019-02-02 11:26:27 +01:00
2020-08-02 23:43:39 +02:00
findEntityChangeIssues ( ) {
this . runEntityChangeChecks ( "notes" , "noteId" ) ;
this . runEntityChangeChecks ( "note_contents" , "noteId" ) ;
this . runEntityChangeChecks ( "note_revisions" , "noteRevisionId" ) ;
this . runEntityChangeChecks ( "branches" , "branchId" ) ;
this . runEntityChangeChecks ( "attributes" , "attributeId" ) ;
2022-01-10 17:09:20 +01:00
this . runEntityChangeChecks ( "etapi_tokens" , "etapiTokenId" ) ;
2020-08-02 23:43:39 +02:00
this . runEntityChangeChecks ( "options" , "name" ) ;
2019-12-10 22:03:00 +01:00
}
2019-02-02 11:26:27 +01:00
2020-07-04 11:02:05 +02:00
findWronglyNamedAttributes ( ) {
const attrNames = sql . getColumn ( ` SELECT DISTINCT name FROM attributes ` ) ;
for ( const origName of attrNames ) {
2022-12-23 14:18:40 +01:00
const fixedName = sanitizeAttributeName ( origName ) ;
2020-07-04 11:02:05 +02:00
2020-11-17 22:35:20 +01:00
if ( fixedName !== origName ) {
2020-07-04 11:02:05 +02:00
if ( this . autoFix ) {
// there isn't a good way to update this:
// - just SQL query will fix it in DB but not notify frontend (or other caches) that it has been fixed
// - renaming the attribute would break the invariant that single attribute never changes the name
// - deleting the old attribute and creating new will create duplicates across synchronized cluster (specifically in the initial migration)
// But in general we assume there won't be many such problems
sql . execute ( 'UPDATE attributes SET name = ? WHERE name = ?' , [ fixedName , origName ] ) ;
this . fixedIssues = true ;
2021-10-03 10:00:38 +02:00
this . reloadNeeded = true ;
2020-07-04 11:02:05 +02:00
2022-12-27 10:22:50 +01:00
logFix ( ` Renamed incorrectly named attributes ' ${ origName } ' to ' ${ fixedName } ' ` ) ;
2020-07-04 11:02:05 +02:00
}
else {
this . unrecoveredConsistencyErrors = true ;
2022-12-27 10:22:50 +01:00
logFix ( ` There are incorrectly named attributes ' ${ origName } ' ` ) ;
2020-07-04 11:02:05 +02:00
}
}
}
}
2021-02-15 20:44:31 +01:00
findSyncIssues ( ) {
const lastSyncedPush = parseInt ( sql . getValue ( "SELECT value FROM options WHERE name = 'lastSyncedPush'" ) ) ;
const maxEntityChangeId = sql . getValue ( "SELECT MAX(id) FROM entity_changes" ) ;
if ( lastSyncedPush > maxEntityChangeId ) {
if ( this . autoFix ) {
sql . execute ( "UPDATE options SET value = ? WHERE name = 'lastSyncedPush'" , [ maxEntityChangeId ] ) ;
this . fixedIssues = true ;
logFix ( ` Fixed incorrect lastSyncedPush - was ${ lastSyncedPush } , needs to be at maximum ${ maxEntityChangeId } ` ) ;
}
else {
this . unrecoveredConsistencyErrors = true ;
logFix ( ` Incorrect lastSyncedPush - is ${ lastSyncedPush } , needs to be at maximum ${ maxEntityChangeId } ` ) ;
}
}
}
2020-07-01 22:42:59 +02:00
runAllChecksAndFixers ( ) {
2019-12-10 22:03:00 +01:00
this . unrecoveredConsistencyErrors = false ;
this . fixedIssues = false ;
2021-10-03 10:00:38 +02:00
this . reloadNeeded = false ;
2019-02-02 11:26:27 +01:00
2021-12-21 14:25:26 +01:00
this . findEntityChangeIssues ( ) ;
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-07-04 11:02:05 +02:00
this . findWronglyNamedAttributes ( ) ;
2021-02-15 20:44:31 +01:00
this . findSyncIssues ( ) ;
2020-05-03 22:49:20 +02:00
// root branch should always be expanded
2022-12-27 14:44:28 +01:00
sql . execute ( "UPDATE branches SET isExpanded = 1 WHERE noteId = 'root'" ) ;
2020-05-03 22:49:20 +02:00
2020-12-09 22:49:55 +01:00
if ( ! this . unrecoveredConsistencyErrors ) {
2019-12-10 22:03:00 +01:00
// we run this only if basic checks passed since this assumes basic data consistency
2018-01-01 19:41:22 -05:00
2022-05-03 00:30:09 +02:00
this . checkAndRepairTreeCycles ( ) ;
2019-12-10 22:03:00 +01:00
}
2018-01-01 19:41:22 -05:00
2021-10-03 10:00:38 +02:00
if ( this . reloadNeeded ) {
2021-09-12 11:18:06 +02:00
require ( "../becca/becca_loader" ) . reload ( ) ;
2021-05-01 11:38:20 +02:00
}
2019-12-10 22:03:00 +01:00
return ! this . unrecoveredConsistencyErrors ;
}
2018-01-04 21:37:36 -05:00
2020-12-16 15:01:20 +01:00
runDbDiagnostics ( ) {
2020-12-25 13:06:58 +01:00
function getTableRowCount ( tableName ) {
2020-12-16 15:01:20 +01:00
const count = sql . getValue ( ` SELECT COUNT(1) FROM ${ tableName } ` ) ;
2019-11-30 09:15:08 +01:00
2020-12-23 21:22:41 +01:00
return ` ${ tableName } : ${ count } ` ;
2020-12-16 15:01:20 +01:00
}
2019-11-30 09:15:08 +01:00
2022-01-10 17:09:20 +01:00
const tables = [ "notes" , "note_revisions" , "branches" , "attributes" , "etapi_tokens" ] ;
2020-12-23 21:22:41 +01:00
2022-12-21 15:19:05 +01:00
log . info ( ` Table counts: ${ tables . map ( tableName => getTableRowCount ( tableName ) ) . join ( ", " ) } ` ) ;
2019-12-10 22:03:00 +01:00
}
2019-11-30 09:15:08 +01:00
2019-12-10 22:03:00 +01:00
async runChecks ( ) {
let elapsedTimeMs ;
2018-01-04 21:37:36 -05:00
2020-06-20 12:31:38 +02:00
await syncMutexService . doExclusively ( ( ) => {
2022-12-27 14:44:28 +01:00
elapsedTimeMs = this . runChecksInner ( ) ;
2019-12-10 22:03:00 +01:00
} ) ;
2018-01-01 19:47:50 -05:00
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 {
2021-09-30 22:26:56 +02:00
log . info ( ` All consistency checks passed ` +
( this . fixedIssues ? "after some fixes" : "with no errors detected" ) +
` (took ${ elapsedTimeMs } ms) `
) ;
2019-12-10 22:03:00 +01:00
}
2017-12-14 23:30:38 -05:00
}
2022-12-27 14:44:28 +01:00
runChecksInner ( ) {
const startTimeMs = Date . now ( ) ;
this . runDbDiagnostics ( ) ;
this . runAllChecksAndFixers ( ) ;
return Date . now ( ) - startTimeMs ;
}
2017-12-14 22:16:26 -05:00
}
2021-09-11 14:34:37 +02:00
function getBlankContent ( isProtected , type , mime ) {
if ( isProtected ) {
2022-01-31 21:25:18 +01:00
return null ; // this is wrong for protected non-erased notes, but we cannot create a valid value without password
2021-09-11 14:34:37 +02:00
}
if ( mime === 'application/json' ) {
return '{}' ;
}
2022-01-31 21:25:18 +01:00
return '' ; // empty string might be wrong choice for some note types, but it's the best guess
2021-09-11 14:34:37 +02:00
}
2019-02-02 09:26:57 +01:00
function logFix ( message ) {
2022-12-21 15:19:05 +01:00
log . info ( ` Consistency issue fixed: ${ message } ` ) ;
2019-02-02 09:26:57 +01:00
}
function logError ( message ) {
2022-12-21 15:19:05 +01:00
log . info ( ` Consistency error: ${ message } ` ) ;
2019-02-02 09:26:57 +01:00
}
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
}
2022-12-27 14:44:28 +01:00
async function runOnDemandChecks ( autoFix ) {
2019-12-10 22:03:00 +01:00
const consistencyChecks = new ConsistencyChecks ( autoFix ) ;
2022-12-27 14:44:28 +01:00
await consistencyChecks . runChecks ( ) ;
}
function runOnDemandChecksWithoutExclusiveLock ( autoFix ) {
const consistencyChecks = new ConsistencyChecks ( autoFix ) ;
consistencyChecks . runChecksInner ( ) ;
2019-12-10 22:03:00 +01:00
}
2021-11-17 22:57:09 +01:00
function runEntityChangesChecks ( ) {
const consistencyChecks = new ConsistencyChecks ( true ) ;
consistencyChecks . findEntityChangeIssues ( ) ;
}
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)
2021-11-12 21:19:23 +01:00
setTimeout ( cls . wrap ( runPeriodicChecks ) , 4 * 1000 ) ;
2020-06-20 21:42:41 +02:00
} ) ;
2017-12-14 22:16:26 -05:00
2019-12-10 22:03:00 +01:00
module . exports = {
2021-11-17 22:57:09 +01:00
runOnDemandChecks ,
2022-12-27 14:44:28 +01:00
runOnDemandChecksWithoutExclusiveLock ,
2021-11-17 22:57:09 +01:00
runEntityChangesChecks
2020-06-20 12:31:38 +02:00
} ;