2021-01-29 20:45:11 +02:00
import Resource from './models/Resource' ;
import shim from './shim' ;
2023-10-21 16:52:22 +02:00
import Database from './database' ;
2024-01-04 19:18:57 +02:00
import { SqlQuery } from './services/database/types' ;
2023-10-21 16:52:22 +02:00
import addMigrationFile from './services/database/addMigrationFile' ;
2023-12-13 21:24:58 +02:00
import sqlStringToLines from './services/database/sqlStringToLines' ;
2024-01-04 19:18:57 +02:00
import migrations from './services/database/migrations' ;
2021-01-29 20:45:11 +02:00
2018-09-29 14:29:07 +02:00
const { sprintf } = require ( 'sprintf-js' ) ;
2017-07-06 21:48:17 +02:00
const structureSql = `
CREATE TABLE folders (
id TEXT PRIMARY KEY ,
title TEXT NOT NULL DEFAULT "" ,
created_time INT NOT NULL ,
2017-07-16 14:53:59 +02:00
updated_time INT NOT NULL
2017-07-06 21:48:17 +02:00
) ;
CREATE INDEX folders_title ON folders ( title ) ;
CREATE INDEX folders_updated_time ON folders ( updated_time ) ;
CREATE TABLE notes (
id TEXT PRIMARY KEY ,
parent_id TEXT NOT NULL DEFAULT "" ,
title TEXT NOT NULL DEFAULT "" ,
body TEXT NOT NULL DEFAULT "" ,
created_time INT NOT NULL ,
updated_time INT NOT NULL ,
is_conflict INT NOT NULL DEFAULT 0 ,
latitude NUMERIC NOT NULL DEFAULT 0 ,
longitude NUMERIC NOT NULL DEFAULT 0 ,
altitude NUMERIC NOT NULL DEFAULT 0 ,
author TEXT NOT NULL DEFAULT "" ,
source_url TEXT NOT NULL DEFAULT "" ,
is_todo INT NOT NULL DEFAULT 0 ,
todo_due INT NOT NULL DEFAULT 0 ,
todo_completed INT NOT NULL DEFAULT 0 ,
source TEXT NOT NULL DEFAULT "" ,
source_application TEXT NOT NULL DEFAULT "" ,
application_data TEXT NOT NULL DEFAULT "" ,
\ ` order \` INT NOT NULL DEFAULT 0
) ;
CREATE INDEX notes_title ON notes ( title ) ;
CREATE INDEX notes_updated_time ON notes ( updated_time ) ;
CREATE INDEX notes_is_conflict ON notes ( is_conflict ) ;
CREATE INDEX notes_is_todo ON notes ( is_todo ) ;
CREATE INDEX notes_order ON notes ( \ ` order \` );
CREATE TABLE tags (
id TEXT PRIMARY KEY ,
title TEXT NOT NULL DEFAULT "" ,
created_time INT NOT NULL ,
2017-07-16 14:53:59 +02:00
updated_time INT NOT NULL
2017-07-06 21:48:17 +02:00
) ;
CREATE INDEX tags_title ON tags ( title ) ;
CREATE INDEX tags_updated_time ON tags ( updated_time ) ;
CREATE TABLE note_tags (
id TEXT PRIMARY KEY ,
note_id TEXT NOT NULL ,
tag_id TEXT NOT NULL ,
created_time INT NOT NULL ,
2017-07-16 14:53:59 +02:00
updated_time INT NOT NULL
2017-07-06 21:48:17 +02:00
) ;
CREATE INDEX note_tags_note_id ON note_tags ( note_id ) ;
CREATE INDEX note_tags_tag_id ON note_tags ( tag_id ) ;
CREATE INDEX note_tags_updated_time ON note_tags ( updated_time ) ;
CREATE TABLE resources (
id TEXT PRIMARY KEY ,
title TEXT NOT NULL DEFAULT "" ,
mime TEXT NOT NULL ,
filename TEXT NOT NULL DEFAULT "" ,
created_time INT NOT NULL ,
2017-07-16 14:53:59 +02:00
updated_time INT NOT NULL
2017-07-06 21:48:17 +02:00
) ;
CREATE INDEX resources_title ON resources ( title ) ;
CREATE INDEX resources_updated_time ON resources ( updated_time ) ;
CREATE TABLE settings (
\ ` key \` TEXT PRIMARY KEY,
\ ` value \` TEXT,
\ ` type \` INT NOT NULL
) ;
CREATE TABLE table_fields (
id INTEGER PRIMARY KEY ,
table_name TEXT NOT NULL ,
field_name TEXT NOT NULL ,
field_type INT NOT NULL ,
field_default TEXT
) ;
2017-07-16 14:53:59 +02:00
CREATE TABLE sync_items (
id INTEGER PRIMARY KEY ,
sync_target INT NOT NULL ,
sync_time INT NOT NULL DEFAULT 0 ,
item_type INT NOT NULL ,
item_id TEXT NOT NULL
) ;
CREATE INDEX sync_items_sync_time ON sync_items ( sync_time ) ;
CREATE INDEX sync_items_sync_target ON sync_items ( sync_target ) ;
CREATE INDEX sync_items_item_type ON sync_items ( item_type ) ;
CREATE INDEX sync_items_item_id ON sync_items ( item_id ) ;
2017-07-19 21:15:55 +02:00
CREATE TABLE deleted_items (
id INTEGER PRIMARY KEY ,
item_type INT NOT NULL ,
item_id TEXT NOT NULL ,
deleted_time INT NOT NULL
) ;
CREATE TABLE version (
version INT NOT NULL
) ;
2017-07-06 21:48:17 +02:00
INSERT INTO version ( version ) VALUES ( 1 ) ;
` ;
2021-05-10 11:32:31 +02:00
export interface TableField {
2021-01-29 20:45:11 +02:00
name : string ;
type : number ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-29 20:45:11 +02:00
default : any ;
description? : string ;
}
export default class JoplinDatabase extends Database {
public static TYPE_INT = 1 ;
public static TYPE_TEXT = 2 ;
public static TYPE_NUMERIC = 3 ;
private initialized_ = false ;
private tableFields_ : Record < string , TableField [ ] > = null ;
private version_ : number = null ;
private tableFieldNames_ : Record < string , string [ ] > = { } ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-29 20:45:11 +02:00
private tableDescriptions_ : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-05-13 18:57:37 +02:00
public constructor ( driver : any ) {
2017-07-06 21:48:17 +02:00
super ( driver ) ;
}
2021-05-13 18:57:37 +02:00
public initialized() {
2017-07-06 21:48:17 +02:00
return this . initialized_ ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-05-13 18:57:37 +02:00
public async open ( options : any ) {
2017-07-06 21:48:17 +02:00
await super . open ( options ) ;
return this . initialize ( ) ;
}
2021-05-13 18:57:37 +02:00
public tableFieldNames ( tableName : string ) {
2020-04-15 01:31:28 +02:00
if ( this . tableFieldNames_ [ tableName ] ) return this . tableFieldNames_ [ tableName ] . slice ( ) ;
2020-04-14 00:10:59 +02:00
2020-03-14 01:46:14 +02:00
const tf = this . tableFields ( tableName ) ;
const output = [ ] ;
2017-07-06 21:48:17 +02:00
for ( let i = 0 ; i < tf . length ; i ++ ) {
output . push ( tf [ i ] . name ) ;
}
2020-04-14 00:10:59 +02:00
this . tableFieldNames_ [ tableName ] = output ;
2020-04-15 01:31:28 +02:00
2020-05-20 00:17:56 +02:00
return output . slice ( ) ;
2017-07-06 21:48:17 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-05-13 18:57:37 +02:00
public tableFields ( tableName : string , options : any = null ) {
2018-09-28 22:03:28 +02:00
if ( options === null ) options = { } ;
2018-03-09 22:59:12 +02:00
if ( ! this . tableFields_ ) throw new Error ( 'Fields have not been loaded yet' ) ;
2019-09-19 23:51:18 +02:00
if ( ! this . tableFields_ [ tableName ] ) throw new Error ( ` Unknown table: ${ tableName } ` ) ;
2018-09-28 22:03:28 +02:00
const output = this . tableFields_ [ tableName ] . slice ( ) ;
if ( options . includeDescription ) {
for ( let i = 0 ; i < output . length ; i ++ ) {
output [ i ] . description = this . fieldDescription ( tableName , output [ i ] . name ) ;
}
}
return output ;
}
2021-05-13 18:57:37 +02:00
public async clearForTesting() {
2019-05-26 20:39:07 +02:00
const tableNames = [
'notes' ,
'folders' ,
'resources' ,
'tags' ,
'note_tags' ,
// 'master_keys',
'item_changes' ,
'note_resources' ,
// 'settings',
'deleted_items' ,
'sync_items' ,
'notes_normalized' ,
'revisions' ,
'resources_to_download' ,
2019-06-08 00:11:08 +02:00
'key_values' ,
2019-05-26 20:39:07 +02:00
] ;
const queries = [ ] ;
for ( const n of tableNames ) {
2019-09-19 23:51:18 +02:00
queries . push ( ` DELETE FROM ${ n } ` ) ;
queries . push ( ` DELETE FROM sqlite_sequence WHERE name=" ${ n } " ` ) ; // Reset autoincremented IDs
2019-05-26 20:39:07 +02:00
}
2019-06-08 00:11:08 +02:00
queries . push ( 'DELETE FROM settings WHERE key="sync.1.context"' ) ;
queries . push ( 'DELETE FROM settings WHERE key="sync.2.context"' ) ;
queries . push ( 'DELETE FROM settings WHERE key="sync.3.context"' ) ;
queries . push ( 'DELETE FROM settings WHERE key="sync.4.context"' ) ;
queries . push ( 'DELETE FROM settings WHERE key="sync.5.context"' ) ;
queries . push ( 'DELETE FROM settings WHERE key="sync.6.context"' ) ;
2019-05-26 20:39:07 +02:00
queries . push ( 'DELETE FROM settings WHERE key="sync.7.context"' ) ;
2019-07-29 15:43:53 +02:00
2019-06-28 01:51:02 +02:00
queries . push ( 'DELETE FROM settings WHERE key="revisionService.lastProcessedChangeId"' ) ;
queries . push ( 'DELETE FROM settings WHERE key="resourceService.lastProcessedChangeId"' ) ;
queries . push ( 'DELETE FROM settings WHERE key="searchEngine.lastProcessedChangeId"' ) ;
2019-05-26 20:39:07 +02:00
await this . transactionExecBatch ( queries ) ;
}
2021-05-13 18:57:37 +02:00
public createDefaultRow ( tableName : string ) {
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-01-29 20:45:11 +02:00
const row : any = { } ;
const fields = this . tableFields ( tableName ) ;
2018-11-13 02:45:08 +02:00
for ( let i = 0 ; i < fields . length ; i ++ ) {
const f = fields [ i ] ;
row [ f . name ] = Database . formatValue ( f . type , f . default ) ;
}
return row ;
}
2021-05-13 18:57:37 +02:00
public fieldByName ( tableName : string , fieldName : string ) {
2020-06-02 23:27:36 +02:00
const fields = this . tableFields ( tableName ) ;
for ( const field of fields ) {
if ( field . name === fieldName ) return field ;
}
throw new Error ( ` No such field: ${ tableName } : ${ fieldName } ` ) ;
}
2021-05-13 18:57:37 +02:00
public fieldDefaultValue ( tableName : string , fieldName : string ) {
2020-06-02 23:27:36 +02:00
return this . fieldByName ( tableName , fieldName ) . default ;
}
2021-05-13 18:57:37 +02:00
public fieldDescription ( tableName : string , fieldName : string ) {
2018-09-29 14:29:07 +02:00
const sp = sprintf ;
2018-09-28 22:03:28 +02:00
if ( ! this . tableDescriptions_ ) {
this . tableDescriptions_ = {
notes : {
2018-09-29 14:29:07 +02:00
parent_id : sp ( 'ID of the notebook that contains this note. Change this ID to move the note to a different notebook.' ) ,
body : sp ( 'The note body, in Markdown. May also contain HTML.' ) ,
is_conflict : sp ( 'Tells whether the note is a conflict or not.' ) ,
is_todo : sp ( 'Tells whether this note is a todo or not.' ) ,
todo_due : sp ( 'When the todo is due. An alarm will be triggered on that date.' ) ,
todo_completed : sp ( 'Tells whether todo is completed or not. This is a timestamp in milliseconds.' ) ,
source_url : sp ( 'The full URL where the note comes from.' ) ,
2018-09-28 22:03:28 +02:00
} ,
folders : { } ,
resources : { } ,
tags : { } ,
2021-08-30 19:53:24 +02:00
item_changes : {
type : 'The type of change - either 1 (created), 2 (updated) or 3 (deleted)' ,
created_time : 'When the event was generated' ,
item_type : 'The item type (see table above for the list of item types)' ,
item_id : 'The item ID' ,
before_change_item : 'Unused' ,
source : 'Unused' ,
} ,
2018-09-28 22:03:28 +02:00
} ;
const baseItems = [ 'notes' , 'folders' , 'tags' , 'resources' ] ;
for ( let i = 0 ; i < baseItems . length ; i ++ ) {
const n = baseItems [ i ] ;
const singular = n . substr ( 0 , n . length - 1 ) ;
2018-09-29 14:29:07 +02:00
this . tableDescriptions_ [ n ] . title = sp ( 'The %s title.' , singular ) ;
this . tableDescriptions_ [ n ] . created_time = sp ( 'When the %s was created.' , singular ) ;
this . tableDescriptions_ [ n ] . updated_time = sp ( 'When the %s was last updated.' , singular ) ;
this . tableDescriptions_ [ n ] . user_created_time = sp ( 'When the %s was created. It may differ from created_time as it can be manually set by the user.' , singular ) ;
this . tableDescriptions_ [ n ] . user_updated_time = sp ( 'When the %s was last updated. It may differ from updated_time as it can be manually set by the user.' , singular ) ;
2018-09-28 22:03:28 +02:00
}
}
const d = this . tableDescriptions_ [ tableName ] ;
return d && d [ fieldName ] ? d [ fieldName ] : '' ;
2017-07-06 21:48:17 +02:00
}
2023-12-13 21:24:58 +02:00
private async countFields ( tableName : string ) : Promise < number > {
const pragmas = await this . selectAll ( ` PRAGMA table_info(" ${ tableName } ") ` ) ;
if ( ! pragmas ) throw new Error ( ` No such table: ${ tableName } ` ) ;
return pragmas . length ;
}
public async refreshTableFields ( newVersion : number ) {
2018-03-09 22:59:12 +02:00
this . logger ( ) . info ( 'Initializing tables...' ) ;
2021-01-29 20:45:11 +02:00
const queries : SqlQuery [ ] = [ ] ;
2018-03-09 22:59:12 +02:00
queries . push ( this . wrapQuery ( 'DELETE FROM table_fields' ) ) ;
2023-12-13 21:24:58 +02:00
const countFieldsNotesFts = await this . countFields ( 'notes_fts' ) ;
const countFieldsItemsFts = await this . countFields ( 'items_fts' ) ;
if ( countFieldsNotesFts !== countFieldsItemsFts ) {
throw new Error ( ` \` notes_fts \` ( ${ countFieldsNotesFts } fields) must have the same number of fields as \` items_fts \` ( ${ countFieldsItemsFts } fields) for the search engine BM25 algorithm to work ` ) ;
}
2024-07-01 19:56:40 +02:00
const tableRows = await this . selectAll ( 'SELECT name FROM sqlite_master WHERE type=\'table\'' ) ;
2023-12-13 21:24:58 +02:00
for ( let i = 0 ; i < tableRows . length ; i ++ ) {
const tableName = tableRows [ i ] . name ;
if ( tableName === 'android_metadata' ) continue ;
if ( tableName === 'table_fields' ) continue ;
if ( tableName === 'sqlite_sequence' ) continue ;
if ( tableName . indexOf ( 'notes_fts' ) === 0 ) continue ;
if ( tableName . indexOf ( 'items_fts' ) === 0 ) continue ;
if ( tableName === 'notes_spellfix' ) continue ;
if ( tableName === 'search_aux' ) continue ;
const pragmas = await this . selectAll ( ` PRAGMA table_info(" ${ tableName } ") ` ) ;
for ( let i = 0 ; i < pragmas . length ; i ++ ) {
const item = pragmas [ i ] ;
// In SQLite, if the default value is a string it has double quotes around it, so remove them here
let defaultValue = item . dflt_value ;
if ( typeof defaultValue === 'string' && defaultValue . length >= 2 && defaultValue [ 0 ] === '"' && defaultValue [ defaultValue . length - 1 ] === '"' ) {
defaultValue = defaultValue . substr ( 1 , defaultValue . length - 2 ) ;
2019-07-29 15:43:53 +02:00
}
2023-12-13 21:24:58 +02:00
const q = Database . insertQuery ( 'table_fields' , {
table_name : tableName ,
field_name : item.name ,
field_type : Database.enumId ( 'fieldType' , item . type ) ,
field_default : defaultValue ,
} ) ;
queries . push ( q ) ;
}
}
2017-07-06 21:48:17 +02:00
2023-12-13 21:24:58 +02:00
queries . push ( { sql : 'UPDATE version SET table_fields_version = ?' , params : [ newVersion ] } ) ;
await this . transactionExecBatch ( queries ) ;
2017-07-06 21:48:17 +02:00
}
2021-05-13 18:57:37 +02:00
public async upgradeDatabase ( fromVersion : number ) {
2017-07-19 21:15:55 +02:00
// INSTRUCTIONS TO UPGRADE THE DATABASE:
//
2023-10-21 16:52:22 +02:00
// 1. Add the migration to lib/services/database/migrations.
// 2. Import the migration and add it to the `migrations` array above.
2017-07-19 21:15:55 +02:00
2017-10-22 19:12:16 +02:00
// IMPORTANT:
//
2019-07-29 15:43:53 +02:00
// Whenever adding a new database property, some additional logic might be needed
2017-10-22 19:12:16 +02:00
// in the synchronizer to handle this property. For example, when adding a property
// that should have a default value, existing remote items will not have this
// default value and thus might cause problems. In that case, the default value
// must be set in the synchronizer too.
2018-12-27 23:49:19 +02:00
// Note: v16 and v17 don't do anything. They were used to debug an issue.
2023-10-21 16:52:22 +02:00
const existingDatabaseVersions = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 , 32 , 33 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 41 ] ;
for ( let i = 0 ; i < migrations . length ; i ++ ) existingDatabaseVersions . push ( existingDatabaseVersions [ existingDatabaseVersions . length - 1 ] + 1 ) ;
2017-07-19 21:15:55 +02:00
let currentVersionIndex = existingDatabaseVersions . indexOf ( fromVersion ) ;
2017-12-28 21:14:03 +02:00
2017-12-02 17:18:15 +02:00
// currentVersionIndex < 0 if for the case where an old version of Joplin used with a newer
// version of the database, so that migration is not run in this case.
2020-02-03 23:40:48 +02:00
if ( currentVersionIndex < 0 ) {
throw new Error (
'Unknown profile version. Most likely this is an old version of Joplin, while the profile was created by a newer version. Please upgrade Joplin at https://joplinapp.org and try again.\n'
+ ` Joplin version: ${ shim . appVersion ( ) } \ n `
+ ` Profile version: ${ fromVersion } \ n `
2020-03-14 01:46:14 +02:00
+ ` Expected version: ${ existingDatabaseVersions [ existingDatabaseVersions . length - 1 ] } ` ) ;
2020-02-03 23:40:48 +02:00
}
2018-05-06 13:11:59 +02:00
2020-10-06 13:47:33 +02:00
this . logger ( ) . info ( ` Upgrading database from version ${ fromVersion } ` ) ;
2022-07-23 09:31:32 +02:00
if ( currentVersionIndex === existingDatabaseVersions . length - 1 ) return fromVersion ;
2018-12-28 22:40:29 +02:00
let latestVersion = fromVersion ;
2017-12-28 21:14:03 +02:00
2017-07-19 21:15:55 +02:00
while ( currentVersionIndex < existingDatabaseVersions . length - 1 ) {
const targetVersion = existingDatabaseVersions [ currentVersionIndex + 1 ] ;
2019-09-19 23:51:18 +02:00
this . logger ( ) . info ( ` Converting database to version ${ targetVersion } ` ) ;
2017-07-19 21:15:55 +02:00
2023-10-21 16:52:22 +02:00
let queries : ( SqlQuery | string ) [ ] = [ ] ;
2017-07-26 19:49:01 +02:00
2022-07-23 09:31:32 +02:00
if ( targetVersion === 1 ) {
2023-12-13 21:24:58 +02:00
queries = this . wrapQueries ( sqlStringToLines ( structureSql ) ) ;
2017-07-26 19:49:01 +02:00
}
2019-07-29 15:43:53 +02:00
2022-07-23 09:31:32 +02:00
if ( targetVersion === 2 ) {
2017-07-19 21:15:55 +02:00
const newTableSql = `
CREATE TABLE deleted_items (
id INTEGER PRIMARY KEY ,
item_type INT NOT NULL ,
item_id TEXT NOT NULL ,
deleted_time INT NOT NULL ,
sync_target INT NOT NULL
) ;
` ;
2018-03-09 22:59:12 +02:00
queries . push ( { sql : 'DROP TABLE deleted_items' } ) ;
2023-12-13 21:24:58 +02:00
queries . push ( { sql : sqlStringToLines ( newTableSql ) [ 0 ] } ) ;
2019-07-29 15:43:53 +02:00
queries . push ( { sql : 'CREATE INDEX deleted_items_sync_target ON deleted_items (sync_target)' } ) ;
2017-07-19 21:15:55 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 3 ) {
2018-03-09 22:59:12 +02:00
queries = this . alterColumnQueries ( 'settings' , { key : 'TEXT PRIMARY KEY' , value : 'TEXT' } ) ;
2017-07-25 23:55:26 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 4 ) {
2019-07-30 09:35:42 +02:00
queries . push ( 'INSERT INTO settings (`key`, `value`) VALUES (\'sync.3.context\', (SELECT `value` FROM settings WHERE `key` = \'sync.context\'))' ) ;
2024-07-01 19:56:40 +02:00
queries . push ( 'DELETE FROM settings WHERE `key` = \'sync.context\'' ) ;
2017-08-19 22:56:28 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 5 ) {
2018-03-09 22:59:12 +02:00
const tableNames = [ 'notes' , 'folders' , 'tags' , 'note_tags' , 'resources' ] ;
2017-08-20 22:11:32 +02:00
for ( let i = 0 ; i < tableNames . length ; i ++ ) {
const n = tableNames [ i ] ;
2019-09-19 23:51:18 +02:00
queries . push ( ` ALTER TABLE ${ n } ADD COLUMN user_created_time INT NOT NULL DEFAULT 0 ` ) ;
queries . push ( ` ALTER TABLE ${ n } ADD COLUMN user_updated_time INT NOT NULL DEFAULT 0 ` ) ;
queries . push ( ` UPDATE ${ n } SET user_created_time = created_time ` ) ;
queries . push ( ` UPDATE ${ n } SET user_updated_time = updated_time ` ) ;
queries . push ( ` CREATE INDEX ${ n } _user_updated_time ON ${ n } (user_updated_time) ` ) ;
2017-08-20 22:11:32 +02:00
}
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 6 ) {
2018-03-09 22:59:12 +02:00
queries . push ( 'CREATE TABLE alarms (id INTEGER PRIMARY KEY AUTOINCREMENT, note_id TEXT NOT NULL, trigger_time INT NOT NULL)' ) ;
queries . push ( 'CREATE INDEX alarm_note_id ON alarms (note_id)' ) ;
2017-11-28 00:50:46 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 7 ) {
2017-12-02 01:15:49 +02:00
queries . push ( 'ALTER TABLE resources ADD COLUMN file_extension TEXT NOT NULL DEFAULT ""' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 8 ) {
2017-12-05 01:38:09 +02:00
queries . push ( 'ALTER TABLE sync_items ADD COLUMN sync_disabled INT NOT NULL DEFAULT "0"' ) ;
queries . push ( 'ALTER TABLE sync_items ADD COLUMN sync_disabled_reason TEXT NOT NULL DEFAULT ""' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 9 ) {
2017-12-14 20:53:08 +02:00
const newTableSql = `
CREATE TABLE master_keys (
id TEXT PRIMARY KEY ,
created_time INT NOT NULL ,
updated_time INT NOT NULL ,
source_application TEXT NOT NULL ,
encryption_method INT NOT NULL ,
checksum TEXT NOT NULL ,
content TEXT NOT NULL
) ;
` ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( newTableSql ) [ 0 ] ) ;
2018-03-09 22:59:12 +02:00
const tableNames = [ 'notes' , 'folders' , 'tags' , 'note_tags' , 'resources' ] ;
2017-12-14 19:58:10 +02:00
for ( let i = 0 ; i < tableNames . length ; i ++ ) {
const n = tableNames [ i ] ;
2019-09-19 23:51:18 +02:00
queries . push ( ` ALTER TABLE ${ n } ADD COLUMN encryption_cipher_text TEXT NOT NULL DEFAULT "" ` ) ;
queries . push ( ` ALTER TABLE ${ n } ADD COLUMN encryption_applied INT NOT NULL DEFAULT 0 ` ) ;
queries . push ( ` CREATE INDEX ${ n } _encryption_applied ON ${ n } (encryption_applied) ` ) ;
2017-12-14 19:58:10 +02:00
}
2017-12-14 23:12:02 +02:00
2018-03-09 22:59:12 +02:00
queries . push ( 'ALTER TABLE sync_items ADD COLUMN force_sync INT NOT NULL DEFAULT 0' ) ;
queries . push ( 'ALTER TABLE resources ADD COLUMN encryption_blob_encrypted INT NOT NULL DEFAULT 0' ) ;
2017-12-13 20:57:40 +02:00
}
2018-03-16 16:32:47 +02:00
const upgradeVersion10 = ( ) = > {
2018-03-13 01:40:43 +02:00
const itemChangesTable = `
CREATE TABLE item_changes (
2018-03-15 20:08:46 +02:00
id INTEGER PRIMARY KEY AUTOINCREMENT ,
2018-03-13 01:40:43 +02:00
item_type INT NOT NULL ,
item_id TEXT NOT NULL ,
type INT NOT NULL ,
created_time INT NOT NULL
) ;
` ;
const noteResourcesTable = `
CREATE TABLE note_resources (
id INTEGER PRIMARY KEY ,
note_id TEXT NOT NULL ,
2018-03-15 20:08:46 +02:00
resource_id TEXT NOT NULL ,
is_associated INT NOT NULL ,
last_seen_time INT NOT NULL
2018-03-13 01:40:43 +02:00
) ;
` ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( itemChangesTable ) [ 0 ] ) ;
2018-03-13 01:40:43 +02:00
queries . push ( 'CREATE INDEX item_changes_item_id ON item_changes (item_id)' ) ;
queries . push ( 'CREATE INDEX item_changes_created_time ON item_changes (created_time)' ) ;
queries . push ( 'CREATE INDEX item_changes_item_type ON item_changes (item_type)' ) ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( noteResourcesTable ) [ 0 ] ) ;
2018-03-13 01:40:43 +02:00
queries . push ( 'CREATE INDEX note_resources_note_id ON note_resources (note_id)' ) ;
queries . push ( 'CREATE INDEX note_resources_resource_id ON note_resources (resource_id)' ) ;
queries . push ( { sql : 'INSERT INTO item_changes (item_type, item_id, type, created_time) SELECT 1, id, 1, ? FROM notes' , params : [ Date . now ( ) ] } ) ;
2019-07-29 15:43:53 +02:00
} ;
2018-03-13 01:40:43 +02:00
2022-07-23 09:31:32 +02:00
if ( targetVersion === 10 ) {
2018-03-16 16:32:47 +02:00
upgradeVersion10 ( ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 11 ) {
2018-03-16 19:39:44 +02:00
// This trick was needed because Electron Builder incorrectly released a dev branch containing v10 as it was
// still being developed, and the db schema was not final at that time. So this v11 was created to
// make sure any invalid db schema that was accidentally created was deleted and recreated.
2018-03-16 16:32:47 +02:00
queries . push ( 'DROP TABLE item_changes' ) ;
queries . push ( 'DROP TABLE note_resources' ) ;
upgradeVersion10 ( ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 12 ) {
2018-05-06 13:11:59 +02:00
queries . push ( 'ALTER TABLE folders ADD COLUMN parent_id TEXT NOT NULL DEFAULT ""' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 13 ) {
2018-10-10 19:53:09 +02:00
queries . push ( 'ALTER TABLE resources ADD COLUMN fetch_status INT NOT NULL DEFAULT "2"' ) ;
2018-10-07 21:11:33 +02:00
queries . push ( 'ALTER TABLE resources ADD COLUMN fetch_error TEXT NOT NULL DEFAULT ""' ) ;
2018-10-08 20:11:53 +02:00
queries . push ( { sql : 'UPDATE resources SET fetch_status = ?' , params : [ Resource . FETCH_STATUS_DONE ] } ) ;
2018-10-07 21:11:33 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 14 ) {
2018-11-13 02:45:08 +02:00
const resourceLocalStates = `
CREATE TABLE resource_local_states (
id INTEGER PRIMARY KEY ,
resource_id TEXT NOT NULL ,
fetch_status INT NOT NULL DEFAULT "2" ,
fetch_error TEXT NOT NULL DEFAULT ""
) ;
` ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( resourceLocalStates ) [ 0 ] ) ;
2018-11-13 02:45:08 +02:00
queries . push ( 'INSERT INTO resource_local_states SELECT null, id, fetch_status, fetch_error FROM resources' ) ;
queries . push ( 'CREATE INDEX resource_local_states_resource_id ON resource_local_states (resource_id)' ) ;
queries . push ( 'CREATE INDEX resource_local_states_resource_fetch_status ON resource_local_states (fetch_status)' ) ;
2019-07-29 15:43:53 +02:00
queries = queries . concat (
this . alterColumnQueries ( 'resources' , {
id : 'TEXT PRIMARY KEY' ,
title : 'TEXT NOT NULL DEFAULT ""' ,
mime : 'TEXT NOT NULL' ,
filename : 'TEXT NOT NULL DEFAULT ""' ,
created_time : 'INT NOT NULL' ,
updated_time : 'INT NOT NULL' ,
user_created_time : 'INT NOT NULL DEFAULT 0' ,
user_updated_time : 'INT NOT NULL DEFAULT 0' ,
file_extension : 'TEXT NOT NULL DEFAULT ""' ,
encryption_cipher_text : 'TEXT NOT NULL DEFAULT ""' ,
encryption_applied : 'INT NOT NULL DEFAULT 0' ,
encryption_blob_encrypted : 'INT NOT NULL DEFAULT 0' ,
2023-08-22 12:58:53 +02:00
} ) ,
2019-07-29 15:43:53 +02:00
) ;
2018-11-13 02:45:08 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 15 ) {
2018-12-10 20:58:49 +02:00
queries . push ( 'CREATE VIRTUAL TABLE notes_fts USING fts4(content="notes", notindexed="id", id, title, body)' ) ;
queries . push ( 'INSERT INTO notes_fts(docid, id, title, body) SELECT rowid, id, title, body FROM notes WHERE is_conflict = 0 AND encryption_applied = 0' ) ;
2018-12-12 23:40:05 +02:00
// Keep the content tables (notes) and the FTS table (notes_fts) in sync.
// More info at https://www.sqlite.org/fts3.html#_external_content_fts4_tables_
queries . push ( `
CREATE TRIGGER notes_fts_before_update BEFORE UPDATE ON notes BEGIN
DELETE FROM notes_fts WHERE docid = old . rowid ;
END ; ` );
queries . push ( `
CREATE TRIGGER notes_fts_before_delete BEFORE DELETE ON notes BEGIN
DELETE FROM notes_fts WHERE docid = old . rowid ;
END ; ` );
queries . push ( `
CREATE TRIGGER notes_after_update AFTER UPDATE ON notes BEGIN
INSERT INTO notes_fts ( docid , id , title , body ) SELECT rowid , id , title , body FROM notes WHERE is_conflict = 0 AND encryption_applied = 0 AND new . rowid = notes . rowid ;
END ; ` );
queries . push ( `
CREATE TRIGGER notes_after_insert AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts ( docid , id , title , body ) SELECT rowid , id , title , body FROM notes WHERE is_conflict = 0 AND encryption_applied = 0 AND new . rowid = notes . rowid ;
END ; ` );
2018-12-10 20:58:49 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 18 ) {
2018-12-29 21:19:18 +02:00
const notesNormalized = `
CREATE TABLE notes_normalized (
id TEXT NOT NULL ,
title TEXT NOT NULL DEFAULT "" ,
body TEXT NOT NULL DEFAULT ""
) ;
` ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( notesNormalized ) [ 0 ] ) ;
2018-12-29 21:19:18 +02:00
queries . push ( 'CREATE INDEX notes_normalized_id ON notes_normalized (id)' ) ;
queries . push ( 'DROP TRIGGER IF EXISTS notes_fts_before_update' ) ;
queries . push ( 'DROP TRIGGER IF EXISTS notes_fts_before_delete' ) ;
queries . push ( 'DROP TRIGGER IF EXISTS notes_after_update' ) ;
queries . push ( 'DROP TRIGGER IF EXISTS notes_after_insert' ) ;
queries . push ( 'DROP TABLE IF EXISTS notes_fts' ) ;
queries . push ( 'CREATE VIRTUAL TABLE notes_fts USING fts4(content="notes_normalized", notindexed="id", id, title, body)' ) ;
// Keep the content tables (notes) and the FTS table (notes_fts) in sync.
// More info at https://www.sqlite.org/fts3.html#_external_content_fts4_tables_
queries . push ( `
CREATE TRIGGER notes_fts_before_update BEFORE UPDATE ON notes_normalized BEGIN
DELETE FROM notes_fts WHERE docid = old . rowid ;
END ; ` );
queries . push ( `
CREATE TRIGGER notes_fts_before_delete BEFORE DELETE ON notes_normalized BEGIN
DELETE FROM notes_fts WHERE docid = old . rowid ;
END ; ` );
queries . push ( `
CREATE TRIGGER notes_after_update AFTER UPDATE ON notes_normalized BEGIN
INSERT INTO notes_fts ( docid , id , title , body ) SELECT rowid , id , title , body FROM notes_normalized WHERE new . rowid = notes_normalized . rowid ;
END ; ` );
queries . push ( `
CREATE TRIGGER notes_after_insert AFTER INSERT ON notes_normalized BEGIN
INSERT INTO notes_fts ( docid , id , title , body ) SELECT rowid , id , title , body FROM notes_normalized WHERE new . rowid = notes_normalized . rowid ;
END ; ` );
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 19 ) {
2019-05-06 22:35:29 +02:00
const newTableSql = `
CREATE TABLE revisions (
id TEXT PRIMARY KEY ,
parent_id TEXT NOT NULL DEFAULT "" ,
item_type INT NOT NULL ,
item_id TEXT NOT NULL ,
item_updated_time INT NOT NULL ,
title_diff TEXT NOT NULL DEFAULT "" ,
body_diff TEXT NOT NULL DEFAULT "" ,
metadata_diff TEXT NOT NULL DEFAULT "" ,
encryption_cipher_text TEXT NOT NULL DEFAULT "" ,
encryption_applied INT NOT NULL DEFAULT 0 ,
updated_time INT NOT NULL ,
created_time INT NOT NULL
) ;
` ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( newTableSql ) [ 0 ] ) ;
2019-05-06 22:35:29 +02:00
queries . push ( 'CREATE INDEX revisions_parent_id ON revisions (parent_id)' ) ;
queries . push ( 'CREATE INDEX revisions_item_type ON revisions (item_type)' ) ;
queries . push ( 'CREATE INDEX revisions_item_id ON revisions (item_id)' ) ;
queries . push ( 'CREATE INDEX revisions_item_updated_time ON revisions (item_updated_time)' ) ;
queries . push ( 'CREATE INDEX revisions_updated_time ON revisions (updated_time)' ) ;
queries . push ( 'ALTER TABLE item_changes ADD COLUMN source INT NOT NULL DEFAULT 1' ) ;
queries . push ( 'ALTER TABLE item_changes ADD COLUMN before_change_item TEXT NOT NULL DEFAULT ""' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 20 ) {
2019-05-11 18:55:40 +02:00
const newTableSql = `
CREATE TABLE migrations (
id INTEGER PRIMARY KEY ,
number INTEGER NOT NULL ,
updated_time INT NOT NULL ,
created_time INT NOT NULL
) ;
` ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( newTableSql ) [ 0 ] ) ;
2019-05-11 18:55:40 +02:00
queries . push ( 'ALTER TABLE resources ADD COLUMN `size` INT NOT NULL DEFAULT -1' ) ;
2023-10-21 16:52:22 +02:00
queries . push ( addMigrationFile ( 20 ) ) ;
2019-05-11 18:55:40 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 21 ) {
2019-05-12 02:15:52 +02:00
queries . push ( 'ALTER TABLE sync_items ADD COLUMN item_location INT NOT NULL DEFAULT 1' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 22 ) {
2019-05-22 16:56:07 +02:00
const newTableSql = `
CREATE TABLE resources_to_download (
id INTEGER PRIMARY KEY ,
resource_id TEXT NOT NULL ,
updated_time INT NOT NULL ,
created_time INT NOT NULL
) ;
` ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( newTableSql ) [ 0 ] ) ;
2019-05-22 16:56:07 +02:00
queries . push ( 'CREATE INDEX resources_to_download_resource_id ON resources_to_download (resource_id)' ) ;
queries . push ( 'CREATE INDEX resources_to_download_updated_time ON resources_to_download (updated_time)' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 23 ) {
2019-06-07 10:05:15 +02:00
const newTableSql = `
CREATE TABLE key_values (
id INTEGER PRIMARY KEY ,
\ ` key \` TEXT NOT NULL,
\ ` value \` TEXT NOT NULL,
\ ` type \` INT NOT NULL,
updated_time INT NOT NULL
) ;
` ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( newTableSql ) [ 0 ] ) ;
2019-06-07 10:05:15 +02:00
queries . push ( 'CREATE UNIQUE INDEX key_values_key ON key_values (key)' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 24 ) {
2019-07-14 17:00:02 +02:00
queries . push ( 'ALTER TABLE notes ADD COLUMN `markup_language` INT NOT NULL DEFAULT 1' ) ; // 1: Markdown, 2: HTML
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 25 ) {
2019-11-11 08:14:56 +02:00
queries . push ( ` CREATE VIEW tags_with_note_count AS
SELECT tags . id as id , tags . title as title , tags . created_time as created_time , tags . updated_time as updated_time , COUNT ( notes . id ) as note_count
FROM tags
LEFT JOIN note_tags nt on nt . tag_id = tags . id
LEFT JOIN notes on notes . id = nt . note_id
WHERE notes . id IS NOT NULL
GROUP BY tags . id ` );
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 26 ) {
2019-12-17 14:45:57 +02:00
const tableNames = [ 'notes' , 'folders' , 'tags' , 'note_tags' , 'resources' ] ;
for ( let i = 0 ; i < tableNames . length ; i ++ ) {
const n = tableNames [ i ] ;
queries . push ( ` ALTER TABLE ${ n } ADD COLUMN is_shared INT NOT NULL DEFAULT 0 ` ) ;
}
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 27 ) {
2023-10-21 16:52:22 +02:00
queries . push ( addMigrationFile ( 27 ) ) ;
2019-12-29 19:58:40 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 28 ) {
2020-02-19 12:13:33 +02:00
queries . push ( 'CREATE INDEX resources_size ON resources(size)' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 29 ) {
2020-04-19 17:02:10 +02:00
queries . push ( 'ALTER TABLE version ADD COLUMN table_fields_version INT NOT NULL DEFAULT 0' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 30 ) {
2020-05-27 18:21:46 +02:00
// Change the type of the "order" field from INT to NUMERIC
// Making it a float provides a much bigger range when inserting notes.
// For example, with an INT, inserting a note C between note A with order 1000 and
// note B with order 1001 wouldn't be possible without changing the order
// value of note A or B. But with a float, we can set the order of note C to 1000.5
queries = queries . concat (
this . alterColumnQueries ( 'notes' , {
id : 'TEXT PRIMARY KEY' ,
parent_id : 'TEXT NOT NULL DEFAULT ""' ,
title : 'TEXT NOT NULL DEFAULT ""' ,
body : 'TEXT NOT NULL DEFAULT ""' ,
created_time : 'INT NOT NULL' ,
updated_time : 'INT NOT NULL' ,
is_conflict : 'INT NOT NULL DEFAULT 0' ,
latitude : 'NUMERIC NOT NULL DEFAULT 0' ,
longitude : 'NUMERIC NOT NULL DEFAULT 0' ,
altitude : 'NUMERIC NOT NULL DEFAULT 0' ,
author : 'TEXT NOT NULL DEFAULT ""' ,
source_url : 'TEXT NOT NULL DEFAULT ""' ,
is_todo : 'INT NOT NULL DEFAULT 0' ,
todo_due : 'INT NOT NULL DEFAULT 0' ,
todo_completed : 'INT NOT NULL DEFAULT 0' ,
source : 'TEXT NOT NULL DEFAULT ""' ,
source_application : 'TEXT NOT NULL DEFAULT ""' ,
application_data : 'TEXT NOT NULL DEFAULT ""' ,
order : 'NUMERIC NOT NULL DEFAULT 0' , // that's the change!
user_created_time : 'INT NOT NULL DEFAULT 0' ,
user_updated_time : 'INT NOT NULL DEFAULT 0' ,
encryption_cipher_text : 'TEXT NOT NULL DEFAULT ""' ,
encryption_applied : 'INT NOT NULL DEFAULT 0' ,
markup_language : 'INT NOT NULL DEFAULT 1' ,
is_shared : 'INT NOT NULL DEFAULT 0' ,
2023-08-22 12:58:53 +02:00
} ) ,
2020-05-27 18:21:46 +02:00
) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 31 ) {
2020-07-28 20:09:50 +02:00
// This empty version is due to the revert of the hierarchical tag feature
// We need to keep the version for the users who have upgraded using
// the pre-release
queries . push ( 'ALTER TABLE tags ADD COLUMN parent_id TEXT NOT NULL DEFAULT ""' ) ;
// Drop the tag note count view, instead compute note count on the fly
// queries.push('DROP VIEW tags_with_note_count');
2023-10-21 16:52:22 +02:00
// queries.push(addMigrationFile(31));
2020-07-28 20:09:50 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 32 ) {
2020-07-28 20:09:50 +02:00
// This is the same as version 25 - this is to complete the
// revert of the hierarchical tag feature.
queries . push ( ` CREATE VIEW IF NOT EXISTS tags_with_note_count AS
SELECT tags . id as id , tags . title as title , tags . created_time as created_time , tags . updated_time as updated_time , COUNT ( notes . id ) as note_count
FROM tags
LEFT JOIN note_tags nt on nt . tag_id = tags . id
LEFT JOIN notes on notes . id = nt . note_id
WHERE notes . id IS NOT NULL
GROUP BY tags . id ` );
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 33 ) {
2020-08-08 01:13:21 +02:00
queries . push ( 'DROP TRIGGER notes_fts_before_update' ) ;
queries . push ( 'DROP TRIGGER notes_fts_before_delete' ) ;
queries . push ( 'DROP TRIGGER notes_after_update' ) ;
queries . push ( 'DROP TRIGGER notes_after_insert' ) ;
queries . push ( 'DROP INDEX notes_normalized_id' ) ;
queries . push ( 'DROP TABLE notes_normalized' ) ;
queries . push ( 'DROP TABLE notes_fts' ) ;
const notesNormalized = `
CREATE TABLE notes_normalized (
id TEXT NOT NULL ,
title TEXT NOT NULL DEFAULT "" ,
body TEXT NOT NULL DEFAULT "" ,
user_created_time INT NOT NULL DEFAULT 0 ,
user_updated_time INT NOT NULL DEFAULT 0 ,
is_todo INT NOT NULL DEFAULT 0 ,
todo_completed INT NOT NULL DEFAULT 0 ,
parent_id TEXT NOT NULL DEFAULT "" ,
latitude NUMERIC NOT NULL DEFAULT 0 ,
longitude NUMERIC NOT NULL DEFAULT 0 ,
altitude NUMERIC NOT NULL DEFAULT 0 ,
source_url TEXT NOT NULL DEFAULT ""
) ;
` ;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( notesNormalized ) [ 0 ] ) ;
2020-08-08 01:13:21 +02:00
queries . push ( 'CREATE INDEX notes_normalized_id ON notes_normalized (id)' ) ;
queries . push ( 'CREATE INDEX notes_normalized_user_created_time ON notes_normalized (user_created_time)' ) ;
queries . push ( 'CREATE INDEX notes_normalized_user_updated_time ON notes_normalized (user_updated_time)' ) ;
queries . push ( 'CREATE INDEX notes_normalized_is_todo ON notes_normalized (is_todo)' ) ;
queries . push ( 'CREATE INDEX notes_normalized_todo_completed ON notes_normalized (todo_completed)' ) ;
queries . push ( 'CREATE INDEX notes_normalized_parent_id ON notes_normalized (parent_id)' ) ;
queries . push ( 'CREATE INDEX notes_normalized_latitude ON notes_normalized (latitude)' ) ;
queries . push ( 'CREATE INDEX notes_normalized_longitude ON notes_normalized (longitude)' ) ;
queries . push ( 'CREATE INDEX notes_normalized_altitude ON notes_normalized (altitude)' ) ;
queries . push ( 'CREATE INDEX notes_normalized_source_url ON notes_normalized (source_url)' ) ;
const tableFields = 'id, title, body, user_created_time, user_updated_time, is_todo, todo_completed, parent_id, latitude, longitude, altitude, source_url' ;
const newVirtualTableSql = `
CREATE VIRTUAL TABLE notes_fts USING fts4 (
content = "notes_normalized" ,
notindexed = "id" ,
notindexed = "user_created_time" ,
notindexed = "user_updated_time" ,
notindexed = "is_todo" ,
notindexed = "todo_completed" ,
notindexed = "parent_id" ,
notindexed = "latitude" ,
notindexed = "longitude" ,
notindexed = "altitude" ,
notindexed = "source_url" ,
$ { tableFields }
) ; `
;
2023-12-13 21:24:58 +02:00
queries . push ( sqlStringToLines ( newVirtualTableSql ) [ 0 ] ) ;
2020-08-08 01:13:21 +02:00
queries . push ( `
CREATE TRIGGER notes_fts_before_update BEFORE UPDATE ON notes_normalized BEGIN
DELETE FROM notes_fts WHERE docid = old . rowid ;
END ; ` );
queries . push ( `
CREATE TRIGGER notes_fts_before_delete BEFORE DELETE ON notes_normalized BEGIN
DELETE FROM notes_fts WHERE docid = old . rowid ;
END ; ` );
queries . push ( `
CREATE TRIGGER notes_after_update AFTER UPDATE ON notes_normalized BEGIN
INSERT INTO notes_fts ( docid , $ { tableFields } ) SELECT rowid , $ { tableFields } FROM notes_normalized WHERE new . rowid = notes_normalized . rowid ;
END ; ` );
queries . push ( `
CREATE TRIGGER notes_after_insert AFTER INSERT ON notes_normalized BEGIN
INSERT INTO notes_fts ( docid , $ { tableFields } ) SELECT rowid , $ { tableFields } FROM notes_normalized WHERE new . rowid = notes_normalized . rowid ;
END ; ` );
2023-10-21 16:52:22 +02:00
queries . push ( addMigrationFile ( 33 ) ) ;
2020-08-08 01:13:21 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 34 ) {
2020-09-06 14:07:00 +02:00
queries . push ( 'CREATE VIRTUAL TABLE search_aux USING fts4aux(notes_fts)' ) ;
queries . push ( 'CREATE VIRTUAL TABLE notes_spellfix USING spellfix1' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 35 ) {
2021-04-29 16:27:38 +02:00
queries . push ( 'ALTER TABLE notes_normalized ADD COLUMN todo_due INT NOT NULL DEFAULT 0' ) ;
queries . push ( 'CREATE INDEX notes_normalized_todo_due ON notes_normalized (todo_due)' ) ;
2023-10-21 16:52:22 +02:00
queries . push ( addMigrationFile ( 35 ) ) ;
2021-04-29 16:27:38 +02:00
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 36 ) {
2021-05-13 18:57:37 +02:00
queries . push ( 'ALTER TABLE folders ADD COLUMN share_id TEXT NOT NULL DEFAULT ""' ) ;
queries . push ( 'ALTER TABLE notes ADD COLUMN share_id TEXT NOT NULL DEFAULT ""' ) ;
queries . push ( 'ALTER TABLE resources ADD COLUMN share_id TEXT NOT NULL DEFAULT ""' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 38 ) {
2021-05-27 13:44:58 +02:00
queries . push ( 'DROP VIEW tags_with_note_count' ) ;
queries . push ( ` CREATE VIEW tags_with_note_count AS
SELECT tags . id as id , tags . title as title , tags . created_time as created_time , tags . updated_time as updated_time , COUNT ( notes . id ) as note_count ,
SUM ( CASE WHEN notes . todo_completed > 0 THEN 1 ELSE 0 END ) AS todo_completed_count
FROM tags
LEFT JOIN note_tags nt on nt . tag_id = tags . id
LEFT JOIN notes on notes . id = nt . note_id
WHERE notes . id IS NOT NULL
GROUP BY tags . id ` );
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 39 ) {
2021-06-12 09:46:49 +02:00
queries . push ( 'ALTER TABLE `notes` ADD COLUMN conflict_original_id TEXT NOT NULL DEFAULT ""' ) ;
}
2022-07-23 09:31:32 +02:00
if ( targetVersion === 40 ) {
2021-11-03 18:24:40 +02:00
queries . push ( 'ALTER TABLE `folders` ADD COLUMN master_key_id TEXT NOT NULL DEFAULT ""' ) ;
queries . push ( 'ALTER TABLE `notes` ADD COLUMN master_key_id TEXT NOT NULL DEFAULT ""' ) ;
queries . push ( 'ALTER TABLE `resources` ADD COLUMN master_key_id TEXT NOT NULL DEFAULT ""' ) ;
}
2021-10-03 17:00:49 +02:00
2022-07-23 09:31:32 +02:00
if ( targetVersion === 41 ) {
2021-11-15 19:19:51 +02:00
queries . push ( 'ALTER TABLE `folders` ADD COLUMN icon TEXT NOT NULL DEFAULT ""' ) ;
}
2023-10-21 16:52:22 +02:00
if ( targetVersion > 41 ) {
const migration = migrations [ targetVersion - 42 ] ;
if ( ! migration ) throw new Error ( ` No such migration: ${ targetVersion } ` ) ;
const migrationQueries = migration ( ) ;
queries = queries . concat ( migrationQueries ) ;
2023-06-13 19:06:16 +02:00
}
2020-10-06 13:47:33 +02:00
const updateVersionQuery = { sql : 'UPDATE version SET version = ?' , params : [ targetVersion ] } ;
queries . push ( updateVersionQuery ) ;
2017-07-19 21:15:55 +02:00
2018-12-28 22:40:29 +02:00
try {
await this . transactionExecBatch ( queries ) ;
} catch ( error ) {
2020-10-06 13:47:33 +02:00
// In some cases listed below, when the upgrade fail it is acceptable (a fallback will be used)
// and in those cases, even though it fails, we still want to set the version number so that the
// migration is not repeated on next upgrade.
let saveVersionAgain = false ;
2020-08-08 01:13:21 +02:00
if ( targetVersion === 15 || targetVersion === 18 || targetVersion === 33 ) {
2020-09-06 14:07:00 +02:00
this . logger ( ) . warn ( 'Could not upgrade to database v15 or v18 or v33 - FTS feature will not be used' , error ) ;
2020-10-06 13:47:33 +02:00
saveVersionAgain = true ;
2020-09-06 14:07:00 +02:00
} else if ( targetVersion === 34 ) {
2020-12-18 15:36:39 +02:00
// if (!shim.isTestingEnv()) this.logger().warn('Could not upgrade to database v34 - fuzzy search will not be used', error);
2020-10-06 13:47:33 +02:00
saveVersionAgain = true ;
2018-12-28 22:40:29 +02:00
} else {
throw error ;
}
2020-10-06 13:47:33 +02:00
if ( saveVersionAgain ) {
this . logger ( ) . info ( 'Migration failed with fallback and will not be repeated - saving version number' ) ;
await this . transactionExecBatch ( [ updateVersionQuery ] ) ;
}
2018-12-28 22:40:29 +02:00
}
latestVersion = targetVersion ;
2019-07-29 15:43:53 +02:00
2017-07-19 21:15:55 +02:00
currentVersionIndex ++ ;
}
2018-12-28 22:40:29 +02:00
return latestVersion ;
}
2021-05-13 18:57:37 +02:00
public async ftsEnabled() {
2018-12-28 22:40:29 +02:00
try {
await this . selectOne ( 'SELECT count(*) FROM notes_fts' ) ;
} catch ( error ) {
this . logger ( ) . warn ( 'FTS check failed' , error ) ;
return false ;
}
this . logger ( ) . info ( 'FTS check succeeded' ) ;
2017-07-19 21:15:55 +02:00
return true ;
}
2021-05-13 18:57:37 +02:00
public async fuzzySearchEnabled() {
2020-09-06 14:07:00 +02:00
try {
await this . selectOne ( 'SELECT count(*) FROM notes_spellfix' ) ;
} catch ( error ) {
this . logger ( ) . warn ( 'Fuzzy search check failed' , error ) ;
return false ;
}
this . logger ( ) . info ( 'Fuzzy search check succeeded' ) ;
return true ;
}
2021-05-13 18:57:37 +02:00
public version() {
2018-12-28 22:40:29 +02:00
return this . version_ ;
}
2021-05-13 18:57:37 +02:00
public async initialize() {
2018-03-09 22:59:12 +02:00
this . logger ( ) . info ( 'Checking for database schema update...' ) ;
2017-07-06 21:48:17 +02:00
2017-07-26 19:49:01 +02:00
let versionRow = null ;
try {
// Will throw if the database has not been created yet, but this is handled below
2018-03-09 22:59:12 +02:00
versionRow = await this . selectOne ( 'SELECT * FROM version LIMIT 1' ) ;
2017-07-26 19:49:01 +02:00
} catch ( error ) {
2019-07-29 15:43:53 +02:00
if ( error . message && error . message . indexOf ( 'no such table: version' ) >= 0 ) {
2017-07-26 22:00:16 +02:00
// Ignore
} else {
2020-09-23 13:14:17 +02:00
this . logger ( ) . info ( error ) ;
2017-07-26 22:00:16 +02:00
}
2017-07-26 19:49:01 +02:00
}
2017-07-06 21:48:17 +02:00
2017-07-26 19:49:01 +02:00
const version = ! versionRow ? 0 : versionRow.version ;
2020-04-19 17:02:10 +02:00
const tableFieldsVersion = ! versionRow ? 0 : versionRow.table_fields_version ;
2018-12-28 22:40:29 +02:00
this . version_ = version ;
2020-10-06 13:47:33 +02:00
this . logger ( ) . info ( 'Current database version' , versionRow ) ;
2017-07-06 21:48:17 +02:00
2018-12-28 22:40:29 +02:00
const newVersion = await this . upgradeDatabase ( version ) ;
this . version_ = newVersion ;
2020-10-06 13:47:33 +02:00
this . logger ( ) . info ( ` New version: ${ newVersion } . Previously recorded version: ${ tableFieldsVersion } ` ) ;
2020-04-19 17:02:10 +02:00
if ( newVersion !== tableFieldsVersion ) await this . refreshTableFields ( newVersion ) ;
2017-07-06 21:48:17 +02:00
2017-07-26 19:49:01 +02:00
this . tableFields_ = { } ;
2017-07-06 21:48:17 +02:00
2020-03-14 01:46:14 +02:00
const rows = await this . selectAll ( 'SELECT * FROM table_fields' ) ;
2017-07-06 21:48:17 +02:00
2017-07-26 19:49:01 +02:00
for ( let i = 0 ; i < rows . length ; i ++ ) {
2020-03-14 01:46:14 +02:00
const row = rows [ i ] ;
2017-07-26 19:49:01 +02:00
if ( ! this . tableFields_ [ row . table_name ] ) this . tableFields_ [ row . table_name ] = [ ] ;
this . tableFields_ [ row . table_name ] . push ( {
name : row.field_name ,
type : row . field_type ,
default : Database . formatValue ( row . field_type , row . field_default ) ,
} ) ;
2017-07-06 21:48:17 +02:00
}
}
}