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' ) ;
2021-06-29 22:15:57 +02:00
const Branch = require ( '../becca/entities/branch' ) ;
2020-04-04 09:46:49 +02:00
const dateUtils = require ( './date_utils' ) ;
2020-11-17 22:35:20 +01:00
const attributeService = require ( './attributes' ) ;
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" ) ;
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
}
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
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-11-10 14:16:12 +01:00
}
2019-12-10 22:03:00 +01:00
if ( ! childToParents [ noteId ] || childToParents [ noteId ] . length === 0 ) {
logError ( ` No parents found for note ${ noteId } ` ) ;
2019-11-10 14:16:12 +01:00
2019-12-10 22:03:00 +01:00
this . unrecoveredConsistencyErrors = true ;
return ;
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 ) ) {
logError ( ` Tree cycle detected at parent-child relationship: ${ parentNoteId } - ${ noteId } , whole path: ${ path } ` ) ;
2019-02-02 12:41:20 +01:00
2019-12-10 22:03:00 +01:00
this . unrecoveredConsistencyErrors = true ;
} else {
const newPath = path . slice ( ) ;
newPath . push ( noteId ) ;
2019-11-10 11:43:33 +01:00
2019-12-10 22:03:00 +01:00
checkTreeCycle ( parentNoteId , newPath ) ;
}
2019-11-10 11:43:33 +01:00
}
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 ) {
checkTreeCycle ( noteId , [ ] ) ;
}
2019-11-23 19:56:52 +01:00
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
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
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
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
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 ) {
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
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
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
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
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
logFix ( ` Relation ${ attributeId } has been deleted since it references missing note ${ noteId } ` )
} else {
logError ( ` Relation ${ attributeId } references missing note ${ noteId } ` )
}
} ) ;
}
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
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-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
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-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 ) {
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-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 = ?
and isDeleted = 0 ` , [noteId, parentNoteId]);
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
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 } ` ) ;
}
} ) ;
}
2018-11-15 13:58:14 +01:00
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 ) {
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
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
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 ` ,
2020-06-20 12:31:38 +02:00
( { noteId } ) => {
2019-12-10 22:03:00 +01:00
if ( this . autoFix ) {
2021-05-02 11:23:58 +02:00
const note = becca . 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
2021-06-29 23:45:45 +02:00
const utcDateModified = dateUtils . utcNowDateTime ( ) ;
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 ,
2021-06-29 23:45:45 +02:00
utcDateModified : utcDateModified
2020-04-04 09:46:49 +02:00
} ) ;
2021-06-29 23:45:45 +02:00
const hash = utils . hash ( noteId + "|null" ) ;
entityChangesService . addEntityChange ( {
entityName : 'note_contents' ,
entityId : noteId ,
hash : hash ,
isErased : false ,
2021-06-30 20:54:15 +02:00
utcDateChanged : utcDateModified ,
isSynced : true
} ) ;
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-12-16 15:01:20 +01:00
note . setContent ( '' ) ;
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
2020-07-01 22:42:59 +02:00
JOIN note _contents USING ( noteId )
2019-12-10 22:03:00 +01:00
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 ) {
2021-05-02 11:23:58 +02:00
const note = becca . 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 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
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 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-05-02 19:59:16 +02:00
const branchIds = sql . getColumn ( ` SELECT branchId
2019-12-10 22:03:00 +01:00
FROM branches
WHERE isDeleted = 0
AND parentNoteId = ? ` , [parentNoteId]);
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
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 ) {
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
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'
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
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
2020-07-01 22:42:59 +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
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
2020-07-01 22:42:59 +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
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. ` ) ;
}
} ) ;
}
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 ( `
2019-02-02 11:26:27 +01:00
SELECT
$ { key } as entityId
FROM
$ { entityName }
2020-08-02 23:43:39 +02:00
LEFT JOIN entity _changes ON entity _changes . entityName = '${entityName}'
AND entity _changes . entityId = $ { key }
2019-02-02 11:26:27 +01:00
WHERE
2021-06-30 20:54:15 +02:00
entity _changes . id IS NULL ` ,
2020-06-20 12:31:38 +02:00
( { entityId } ) => {
2019-12-10 22:03:00 +01:00
if ( this . autoFix ) {
2021-05-17 22:05:35 +02:00
const entity = becca . getEntity ( entityName , entityId ) ;
2020-12-07 23:04:17 +01:00
2020-12-16 20:58:43 +01:00
entityChangesService . addEntityChange ( {
entityName ,
entityId ,
hash : entity . generateHash ( ) ,
isErased : false ,
2021-06-30 20:54:15 +02:00
utcDateChanged : entity . getUtcDateChanged ( ) ,
isSynced : entityName !== 'options' || entity . isSynced
} ) ;
2019-02-02 11:26:27 +01:00
2020-08-02 23:43:39 +02:00
logFix ( ` Created missing entity change for entityName= ${ entityName } , entityId= ${ entityId } ` ) ;
2019-12-10 22:03:00 +01:00
} else {
2020-08-02 23:43:39 +02: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
2020-08-02 23:43:39 +02:00
logFix ( ` Deleted extra entity change id= ${ id } , entityName= ${ entityName } , entityId= ${ entityId } ` ) ;
2020-02-19 19:51:36 +01:00
} else {
2020-08-02 23:43:39 +02:00
logError ( ` Unrecognized entity change id= ${ id } , entityName= ${ entityName } , entityId= ${ entityId } ` ) ;
2020-02-19 19:51:36 +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" ) ;
this . runEntityChangeChecks ( "api_tokens" , "apiTokenId" ) ;
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 ) {
2020-11-17 22:35:20 +01:00
const fixedName = attributeService . 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 ;
logFix ( ` Renamed incorrectly named attributes " ${ origName } " to ${ fixedName } ` ) ;
}
else {
this . unrecoveredConsistencyErrors = true ;
logFix ( ` There are incorrectly named attributes " ${ origName } " ` ) ;
}
}
}
}
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 ;
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-08-02 23:43:39 +02:00
this . findEntityChangeIssues ( ) ;
2019-02-01 22:48:51 +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
2020-06-20 12:31:38 +02:00
sql . execute ( "UPDATE branches SET isExpanded = 1 WHERE branchId = '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
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
2021-05-01 11:38:20 +02:00
if ( this . fixedIssues ) {
2021-06-29 22:15:57 +02:00
require ( "../becca/becca_loader" ) . load ( ) ;
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
2020-12-23 21:22:41 +01:00
const tables = [ "notes" , "note_revisions" , "branches" , "attributes" , "api_tokens" ] ;
2020-12-25 13:06:58 +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 ( ( ) => {
2020-07-01 22:42:59 +02:00
const startTimeMs = Date . now ( ) ;
2018-01-04 21:37:36 -05:00
2020-06-20 12:31:38 +02:00
this . runDbDiagnostics ( ) ;
2019-11-30 09:15:08 +01:00
2020-07-01 22:42:59 +02:00
this . runAllChecksAndFixers ( ) ;
2018-01-04 21:37:36 -05:00
2020-07-01 22:42:59 +02:00
elapsedTimeMs = Date . now ( ) - startTimeMs ;
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 {
log . info ( ` All consistency checks passed (took ${ elapsedTimeMs } ms) ` ) ;
}
2017-12-14 23:30:38 -05:00
}
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
} ;