migrations.go 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package database // import "miniflux.app/v2/internal/database"
  4. import (
  5. "database/sql"
  6. "errors"
  7. "miniflux.app/v2/internal/crypto"
  8. )
  9. var schemaVersion = len(migrations)
  10. // Order is important. Add new migrations at the end of the list.
  11. var migrations = [...]func(tx *sql.Tx) error{
  12. func(tx *sql.Tx) (err error) {
  13. sql := `
  14. CREATE TABLE schema_version (
  15. version text not null
  16. );
  17. CREATE TABLE users (
  18. id SERIAL,
  19. username text not null unique,
  20. password text,
  21. is_admin bool default 'f',
  22. language text default 'en_US',
  23. timezone text default 'UTC',
  24. theme text default 'default',
  25. last_login_at timestamp with time zone,
  26. primary key (id)
  27. );
  28. CREATE TABLE sessions (
  29. id SERIAL,
  30. user_id int not null,
  31. token text not null unique,
  32. created_at timestamp with time zone default now(),
  33. user_agent text,
  34. ip text,
  35. primary key (id),
  36. unique (user_id, token),
  37. foreign key (user_id) references users(id) on delete cascade
  38. );
  39. CREATE TABLE categories (
  40. id SERIAL,
  41. user_id int not null,
  42. title text not null,
  43. primary key (id),
  44. unique (user_id, title),
  45. foreign key (user_id) references users(id) on delete cascade
  46. );
  47. CREATE TABLE feeds (
  48. id BIGSERIAL,
  49. user_id int not null,
  50. category_id int not null,
  51. title text not null,
  52. feed_url text not null,
  53. site_url text not null,
  54. checked_at timestamp with time zone default now(),
  55. etag_header text default '',
  56. last_modified_header text default '',
  57. parsing_error_msg text default '',
  58. parsing_error_count int default 0,
  59. primary key (id),
  60. unique (user_id, feed_url),
  61. foreign key (user_id) references users(id) on delete cascade,
  62. foreign key (category_id) references categories(id) on delete cascade
  63. );
  64. CREATE TYPE entry_status as enum('unread', 'read', 'removed');
  65. CREATE TABLE entries (
  66. id BIGSERIAL,
  67. user_id int not null,
  68. feed_id bigint not null,
  69. hash text not null,
  70. published_at timestamp with time zone not null,
  71. title text not null,
  72. url text not null,
  73. author text,
  74. content text,
  75. status entry_status default 'unread',
  76. primary key (id),
  77. unique (feed_id, hash),
  78. foreign key (user_id) references users(id) on delete cascade,
  79. foreign key (feed_id) references feeds(id) on delete cascade
  80. );
  81. CREATE INDEX entries_feed_idx on entries using btree(feed_id);
  82. CREATE TABLE enclosures (
  83. id BIGSERIAL,
  84. user_id int not null,
  85. entry_id bigint not null,
  86. url text not null,
  87. size int default 0,
  88. mime_type text default '',
  89. primary key (id),
  90. foreign key (user_id) references users(id) on delete cascade,
  91. foreign key (entry_id) references entries(id) on delete cascade
  92. );
  93. CREATE TABLE icons (
  94. id BIGSERIAL,
  95. hash text not null unique,
  96. mime_type text not null,
  97. content bytea not null,
  98. primary key (id)
  99. );
  100. CREATE TABLE feed_icons (
  101. feed_id bigint not null,
  102. icon_id bigint not null,
  103. primary key(feed_id, icon_id),
  104. foreign key (feed_id) references feeds(id) on delete cascade,
  105. foreign key (icon_id) references icons(id) on delete cascade
  106. );
  107. `
  108. _, err = tx.Exec(sql)
  109. return err
  110. },
  111. func(tx *sql.Tx) (err error) {
  112. // This used to create a HSTORE `extra` column in the table `users`,
  113. // which hasn't been used since Miniflux 2.0.27.
  114. return nil
  115. },
  116. func(tx *sql.Tx) (err error) {
  117. sql := `
  118. CREATE TABLE tokens (
  119. id text not null,
  120. value text not null,
  121. created_at timestamp with time zone not null default now(),
  122. primary key(id, value)
  123. );
  124. `
  125. _, err = tx.Exec(sql)
  126. return err
  127. },
  128. func(tx *sql.Tx) (err error) {
  129. sql := `
  130. CREATE TYPE entry_sorting_direction AS enum('asc', 'desc');
  131. ALTER TABLE users ADD COLUMN entry_direction entry_sorting_direction default 'asc';
  132. `
  133. _, err = tx.Exec(sql)
  134. return err
  135. },
  136. func(tx *sql.Tx) (err error) {
  137. sql := `
  138. CREATE TABLE integrations (
  139. user_id int not null,
  140. pinboard_enabled bool default 'f',
  141. pinboard_token text default '',
  142. pinboard_tags text default 'miniflux',
  143. pinboard_mark_as_unread bool default 'f',
  144. instapaper_enabled bool default 'f',
  145. instapaper_username text default '',
  146. instapaper_password text default '',
  147. fever_enabled bool default 'f',
  148. fever_username text default '',
  149. fever_password text default '',
  150. fever_token text default '',
  151. primary key(user_id)
  152. );
  153. `
  154. _, err = tx.Exec(sql)
  155. return err
  156. },
  157. func(tx *sql.Tx) (err error) {
  158. sql := `ALTER TABLE feeds ADD COLUMN scraper_rules text default ''`
  159. _, err = tx.Exec(sql)
  160. return err
  161. },
  162. func(tx *sql.Tx) (err error) {
  163. sql := `ALTER TABLE feeds ADD COLUMN rewrite_rules text default ''`
  164. _, err = tx.Exec(sql)
  165. return err
  166. },
  167. func(tx *sql.Tx) (err error) {
  168. sql := `ALTER TABLE feeds ADD COLUMN crawler boolean default 'f'`
  169. _, err = tx.Exec(sql)
  170. return err
  171. },
  172. func(tx *sql.Tx) (err error) {
  173. sql := `ALTER TABLE sessions rename to user_sessions`
  174. _, err = tx.Exec(sql)
  175. return err
  176. },
  177. func(tx *sql.Tx) (err error) {
  178. sql := `
  179. DROP TABLE tokens;
  180. CREATE TABLE sessions (
  181. id text not null,
  182. data jsonb not null,
  183. created_at timestamp with time zone not null default now(),
  184. primary key(id)
  185. );
  186. `
  187. _, err = tx.Exec(sql)
  188. return err
  189. },
  190. func(tx *sql.Tx) (err error) {
  191. sql := `
  192. ALTER TABLE integrations
  193. ADD COLUMN wallabag_enabled bool default 'f',
  194. ADD COLUMN wallabag_url text default '',
  195. ADD COLUMN wallabag_client_id text default '',
  196. ADD COLUMN wallabag_client_secret text default '',
  197. ADD COLUMN wallabag_username text default '',
  198. ADD COLUMN wallabag_password text default '';
  199. `
  200. _, err = tx.Exec(sql)
  201. return err
  202. },
  203. func(tx *sql.Tx) (err error) {
  204. sql := `ALTER TABLE entries ADD COLUMN starred bool default 'f'`
  205. _, err = tx.Exec(sql)
  206. return err
  207. },
  208. func(tx *sql.Tx) (err error) {
  209. sql := `
  210. CREATE INDEX entries_user_status_idx ON entries(user_id, status);
  211. CREATE INDEX feeds_user_category_idx ON feeds(user_id, category_id);
  212. `
  213. _, err = tx.Exec(sql)
  214. return err
  215. },
  216. func(tx *sql.Tx) (err error) {
  217. sql := `
  218. ALTER TABLE integrations
  219. ADD COLUMN nunux_keeper_enabled bool default 'f',
  220. ADD COLUMN nunux_keeper_url text default '',
  221. ADD COLUMN nunux_keeper_api_key text default '';
  222. `
  223. _, err = tx.Exec(sql)
  224. return err
  225. },
  226. func(tx *sql.Tx) (err error) {
  227. sql := `ALTER TABLE enclosures ALTER COLUMN size SET DATA TYPE bigint`
  228. _, err = tx.Exec(sql)
  229. return err
  230. },
  231. func(tx *sql.Tx) (err error) {
  232. sql := `ALTER TABLE entries ADD COLUMN comments_url text default ''`
  233. _, err = tx.Exec(sql)
  234. return err
  235. },
  236. func(tx *sql.Tx) (err error) {
  237. sql := `
  238. ALTER TABLE integrations
  239. ADD COLUMN pocket_enabled bool default 'f',
  240. ADD COLUMN pocket_access_token text default '',
  241. ADD COLUMN pocket_consumer_key text default '';
  242. `
  243. _, err = tx.Exec(sql)
  244. return err
  245. },
  246. func(tx *sql.Tx) (err error) {
  247. sql := `
  248. ALTER TABLE user_sessions ALTER COLUMN ip SET DATA TYPE inet using ip::inet;
  249. `
  250. _, err = tx.Exec(sql)
  251. return err
  252. },
  253. func(tx *sql.Tx) (err error) {
  254. sql := `
  255. ALTER TABLE feeds
  256. ADD COLUMN username text default '',
  257. ADD COLUMN password text default '';
  258. `
  259. _, err = tx.Exec(sql)
  260. return err
  261. },
  262. func(tx *sql.Tx) (err error) {
  263. sql := `
  264. ALTER TABLE entries ADD COLUMN document_vectors tsvector;
  265. UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
  266. CREATE INDEX document_vectors_idx ON entries USING gin(document_vectors);
  267. `
  268. _, err = tx.Exec(sql)
  269. return err
  270. },
  271. func(tx *sql.Tx) (err error) {
  272. sql := `ALTER TABLE feeds ADD COLUMN user_agent text default ''`
  273. _, err = tx.Exec(sql)
  274. return err
  275. },
  276. func(tx *sql.Tx) (err error) {
  277. sql := `
  278. UPDATE
  279. entries
  280. SET
  281. document_vectors = setweight(to_tsvector(substring(coalesce(title, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce(content, '') for 1000000)), 'B')
  282. `
  283. _, err = tx.Exec(sql)
  284. return err
  285. },
  286. func(tx *sql.Tx) (err error) {
  287. sql := `ALTER TABLE users ADD COLUMN keyboard_shortcuts boolean default 't'`
  288. _, err = tx.Exec(sql)
  289. return err
  290. },
  291. func(tx *sql.Tx) (err error) {
  292. sql := `ALTER TABLE feeds ADD COLUMN disabled boolean default 'f';`
  293. _, err = tx.Exec(sql)
  294. return err
  295. },
  296. func(tx *sql.Tx) (err error) {
  297. sql := `
  298. ALTER TABLE users ALTER COLUMN theme SET DEFAULT 'light_serif';
  299. UPDATE users SET theme='light_serif' WHERE theme='default';
  300. UPDATE users SET theme='light_sans_serif' WHERE theme='sansserif';
  301. UPDATE users SET theme='dark_serif' WHERE theme='black';
  302. `
  303. _, err = tx.Exec(sql)
  304. return err
  305. },
  306. func(tx *sql.Tx) (err error) {
  307. sql := `
  308. ALTER TABLE entries ADD COLUMN changed_at timestamp with time zone;
  309. UPDATE entries SET changed_at = published_at;
  310. ALTER TABLE entries ALTER COLUMN changed_at SET not null;
  311. `
  312. _, err = tx.Exec(sql)
  313. return err
  314. },
  315. func(tx *sql.Tx) (err error) {
  316. sql := `
  317. CREATE TABLE api_keys (
  318. id SERIAL,
  319. user_id int not null references users(id) on delete cascade,
  320. token text not null unique,
  321. description text not null,
  322. last_used_at timestamp with time zone,
  323. created_at timestamp with time zone default now(),
  324. primary key(id),
  325. unique (user_id, description)
  326. );
  327. `
  328. _, err = tx.Exec(sql)
  329. return err
  330. },
  331. func(tx *sql.Tx) (err error) {
  332. sql := `
  333. ALTER TABLE entries ADD COLUMN share_code text not null default '';
  334. CREATE UNIQUE INDEX entries_share_code_idx ON entries USING btree(share_code) WHERE share_code <> '';
  335. `
  336. _, err = tx.Exec(sql)
  337. return err
  338. },
  339. func(tx *sql.Tx) (err error) {
  340. // This migration originally used md5(url), but it was changed to
  341. // sha256 because PostgreSQL 18 disables MD5 in FIPS mode, which made
  342. // fresh installs fail while replaying this migration. Existing
  343. // installs that already ran it are migrated later on.
  344. sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, encode(sha256(url::bytea), 'hex'))`
  345. _, err = tx.Exec(sql)
  346. return err
  347. },
  348. func(tx *sql.Tx) (err error) {
  349. sql := `
  350. ALTER TABLE feeds ADD COLUMN next_check_at timestamp with time zone default now();
  351. CREATE INDEX entries_user_feed_idx ON entries (user_id, feed_id);
  352. `
  353. _, err = tx.Exec(sql)
  354. return err
  355. },
  356. func(tx *sql.Tx) (err error) {
  357. sql := `ALTER TABLE feeds ADD COLUMN ignore_http_cache bool default false`
  358. _, err = tx.Exec(sql)
  359. return err
  360. },
  361. func(tx *sql.Tx) (err error) {
  362. sql := `ALTER TABLE users ADD COLUMN entries_per_page int default 100`
  363. _, err = tx.Exec(sql)
  364. return err
  365. },
  366. func(tx *sql.Tx) (err error) {
  367. sql := `ALTER TABLE users ADD COLUMN show_reading_time boolean default 't'`
  368. _, err = tx.Exec(sql)
  369. return err
  370. },
  371. func(tx *sql.Tx) (err error) {
  372. sql := `CREATE INDEX entries_id_user_status_idx ON entries USING btree (id, user_id, status)`
  373. _, err = tx.Exec(sql)
  374. return err
  375. },
  376. func(tx *sql.Tx) (err error) {
  377. sql := `ALTER TABLE feeds ADD COLUMN fetch_via_proxy bool default false`
  378. _, err = tx.Exec(sql)
  379. return err
  380. },
  381. func(tx *sql.Tx) (err error) {
  382. sql := `CREATE INDEX entries_feed_id_status_hash_idx ON entries USING btree (feed_id, status, hash)`
  383. _, err = tx.Exec(sql)
  384. return err
  385. },
  386. func(tx *sql.Tx) (err error) {
  387. sql := `CREATE INDEX entries_user_id_status_starred_idx ON entries (user_id, status, starred)`
  388. _, err = tx.Exec(sql)
  389. return err
  390. },
  391. func(tx *sql.Tx) (err error) {
  392. sql := `ALTER TABLE users ADD COLUMN entry_swipe boolean default 't'`
  393. _, err = tx.Exec(sql)
  394. return err
  395. },
  396. func(tx *sql.Tx) (err error) {
  397. sql := `ALTER TABLE integrations DROP COLUMN fever_password`
  398. _, err = tx.Exec(sql)
  399. return err
  400. },
  401. func(tx *sql.Tx) (err error) {
  402. sql := `
  403. ALTER TABLE feeds
  404. ADD COLUMN blocklist_rules text not null default '',
  405. ADD COLUMN keeplist_rules text not null default ''
  406. `
  407. _, err = tx.Exec(sql)
  408. return err
  409. },
  410. func(tx *sql.Tx) (err error) {
  411. sql := `ALTER TABLE entries ADD COLUMN reading_time int not null default 0`
  412. _, err = tx.Exec(sql)
  413. return err
  414. },
  415. func(tx *sql.Tx) (err error) {
  416. sql := `
  417. ALTER TABLE entries ADD COLUMN created_at timestamp with time zone not null default now();
  418. UPDATE entries SET created_at = published_at;
  419. `
  420. _, err = tx.Exec(sql)
  421. return err
  422. },
  423. func(tx *sql.Tx) (err error) {
  424. hasExtra := false
  425. if err := tx.QueryRow(`
  426. SELECT true
  427. FROM information_schema.columns
  428. WHERE
  429. table_name='users' AND
  430. column_name='extra';
  431. `).Scan(&hasExtra); err != nil && err != sql.ErrNoRows {
  432. return err
  433. }
  434. _, err = tx.Exec(`
  435. ALTER TABLE users
  436. ADD column stylesheet text not null default '',
  437. ADD column google_id text not null default '',
  438. ADD column openid_connect_id text not null default ''
  439. `)
  440. if err != nil {
  441. return err
  442. }
  443. if !hasExtra {
  444. // No need to migrate things from the `extra` column if it's not present
  445. return nil
  446. }
  447. _, err = tx.Exec(`
  448. DECLARE my_cursor CURSOR FOR
  449. SELECT
  450. id,
  451. COALESCE(extra->'custom_css', '') as custom_css,
  452. COALESCE(extra->'google_id', '') as google_id,
  453. COALESCE(extra->'oidc_id', '') as oidc_id
  454. FROM users
  455. FOR UPDATE
  456. `)
  457. if err != nil {
  458. return err
  459. }
  460. defer tx.Exec("CLOSE my_cursor")
  461. for {
  462. var (
  463. userID int64
  464. customStylesheet string
  465. googleID string
  466. oidcID string
  467. )
  468. if err := tx.QueryRow(`FETCH NEXT FROM my_cursor`).Scan(&userID, &customStylesheet, &googleID, &oidcID); err != nil {
  469. if errors.Is(err, sql.ErrNoRows) {
  470. break
  471. }
  472. return err
  473. }
  474. _, err := tx.Exec(
  475. `UPDATE
  476. users
  477. SET
  478. stylesheet=$2,
  479. google_id=$3,
  480. openid_connect_id=$4
  481. WHERE
  482. id=$1
  483. `,
  484. userID, customStylesheet, googleID, oidcID)
  485. if err != nil {
  486. return err
  487. }
  488. }
  489. return err
  490. },
  491. func(tx *sql.Tx) (err error) {
  492. if _, err = tx.Exec(`ALTER TABLE users DROP COLUMN IF EXISTS extra;`); err != nil {
  493. return err
  494. }
  495. _, err = tx.Exec(`
  496. CREATE UNIQUE INDEX users_google_id_idx ON users(google_id) WHERE google_id <> '';
  497. CREATE UNIQUE INDEX users_openid_connect_id_idx ON users(openid_connect_id) WHERE openid_connect_id <> '';
  498. `)
  499. return err
  500. },
  501. func(tx *sql.Tx) (err error) {
  502. _, err = tx.Exec(`
  503. CREATE INDEX entries_feed_url_idx ON entries(feed_id, url) WHERE length(url) < 2000;
  504. CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
  505. CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
  506. `)
  507. return err
  508. },
  509. func(tx *sql.Tx) (err error) {
  510. _, err = tx.Exec(`
  511. CREATE TABLE acme_cache (
  512. key varchar(400) not null primary key,
  513. data bytea not null,
  514. updated_at timestamptz not null
  515. );
  516. `)
  517. return err
  518. },
  519. func(tx *sql.Tx) (err error) {
  520. _, err = tx.Exec(`
  521. ALTER TABLE feeds ADD COLUMN allow_self_signed_certificates boolean not null default false
  522. `)
  523. return err
  524. },
  525. func(tx *sql.Tx) (err error) {
  526. sql := `
  527. CREATE TYPE webapp_display_mode AS enum('fullscreen', 'standalone', 'minimal-ui', 'browser');
  528. ALTER TABLE users ADD COLUMN display_mode webapp_display_mode default 'standalone';
  529. `
  530. _, err = tx.Exec(sql)
  531. return err
  532. },
  533. func(tx *sql.Tx) (err error) {
  534. sql := `ALTER TABLE feeds ADD COLUMN cookie text default ''`
  535. _, err = tx.Exec(sql)
  536. return err
  537. },
  538. func(tx *sql.Tx) (err error) {
  539. _, err = tx.Exec(`
  540. ALTER TABLE categories ADD COLUMN hide_globally boolean not null default false
  541. `)
  542. return err
  543. },
  544. func(tx *sql.Tx) (err error) {
  545. _, err = tx.Exec(`
  546. ALTER TABLE feeds ADD COLUMN hide_globally boolean not null default false
  547. `)
  548. return err
  549. },
  550. func(tx *sql.Tx) (err error) {
  551. sql := `
  552. ALTER TABLE integrations
  553. ADD COLUMN telegram_bot_enabled bool default 'f',
  554. ADD COLUMN telegram_bot_token text default '',
  555. ADD COLUMN telegram_bot_chat_id text default '';
  556. `
  557. _, err = tx.Exec(sql)
  558. return err
  559. },
  560. func(tx *sql.Tx) (err error) {
  561. sql := `
  562. CREATE TYPE entry_sorting_order AS enum('published_at', 'created_at');
  563. ALTER TABLE users ADD COLUMN entry_order entry_sorting_order default 'published_at';
  564. `
  565. _, err = tx.Exec(sql)
  566. return err
  567. },
  568. func(tx *sql.Tx) (err error) {
  569. sql := `
  570. ALTER TABLE integrations
  571. ADD COLUMN googlereader_enabled bool default 'f',
  572. ADD COLUMN googlereader_username text default '',
  573. ADD COLUMN googlereader_password text default '';
  574. `
  575. _, err = tx.Exec(sql)
  576. return err
  577. },
  578. func(tx *sql.Tx) (err error) {
  579. sql := `
  580. ALTER TABLE integrations
  581. ADD COLUMN espial_enabled bool default 'f',
  582. ADD COLUMN espial_url text default '',
  583. ADD COLUMN espial_api_key text default '',
  584. ADD COLUMN espial_tags text default 'miniflux';
  585. `
  586. _, err = tx.Exec(sql)
  587. return err
  588. },
  589. func(tx *sql.Tx) (err error) {
  590. sql := `
  591. ALTER TABLE integrations
  592. ADD COLUMN linkding_enabled bool default 'f',
  593. ADD COLUMN linkding_url text default '',
  594. ADD COLUMN linkding_api_key text default '';
  595. `
  596. _, err = tx.Exec(sql)
  597. return err
  598. },
  599. func(tx *sql.Tx) (err error) {
  600. _, err = tx.Exec(`
  601. ALTER TABLE feeds ADD COLUMN url_rewrite_rules text not null default ''
  602. `)
  603. return err
  604. },
  605. func(tx *sql.Tx) (err error) {
  606. _, err = tx.Exec(`
  607. ALTER TABLE users
  608. ADD COLUMN default_reading_speed int default 265,
  609. ADD COLUMN cjk_reading_speed int default 500;
  610. `)
  611. return
  612. },
  613. func(tx *sql.Tx) (err error) {
  614. _, err = tx.Exec(`
  615. ALTER TABLE users ADD COLUMN default_home_page text default 'unread';
  616. `)
  617. return
  618. },
  619. func(tx *sql.Tx) (err error) {
  620. _, err = tx.Exec(`
  621. ALTER TABLE integrations ADD COLUMN wallabag_only_url bool default 'f';
  622. `)
  623. return
  624. },
  625. func(tx *sql.Tx) (err error) {
  626. _, err = tx.Exec(`
  627. ALTER TABLE users ADD COLUMN categories_sorting_order text not null default 'unread_count';
  628. `)
  629. return
  630. },
  631. func(tx *sql.Tx) (err error) {
  632. sql := `
  633. ALTER TABLE integrations
  634. ADD COLUMN matrix_bot_enabled bool default 'f',
  635. ADD COLUMN matrix_bot_user text default '',
  636. ADD COLUMN matrix_bot_password text default '',
  637. ADD COLUMN matrix_bot_url text default '',
  638. ADD COLUMN matrix_bot_chat_id text default '';
  639. `
  640. _, err = tx.Exec(sql)
  641. return
  642. },
  643. func(tx *sql.Tx) (err error) {
  644. sql := `ALTER TABLE users ADD COLUMN double_tap boolean default 't'`
  645. _, err = tx.Exec(sql)
  646. return err
  647. },
  648. func(tx *sql.Tx) (err error) {
  649. _, err = tx.Exec(`
  650. ALTER TABLE entries ADD COLUMN tags text[] default '{}';
  651. `)
  652. return
  653. },
  654. func(tx *sql.Tx) (err error) {
  655. sql := `
  656. ALTER TABLE users RENAME double_tap TO gesture_nav;
  657. ALTER TABLE users
  658. ALTER COLUMN gesture_nav SET DATA TYPE text using case when gesture_nav = true then 'tap' when gesture_nav = false then 'none' end,
  659. ALTER COLUMN gesture_nav SET default 'tap';
  660. `
  661. _, err = tx.Exec(sql)
  662. return err
  663. },
  664. func(tx *sql.Tx) (err error) {
  665. sql := `
  666. ALTER TABLE integrations ADD COLUMN linkding_tags text default '';
  667. `
  668. _, err = tx.Exec(sql)
  669. return err
  670. },
  671. func(tx *sql.Tx) (err error) {
  672. sql := `
  673. ALTER TABLE feeds ADD COLUMN no_media_player boolean default 'f';
  674. ALTER TABLE enclosures ADD COLUMN media_progression int default 0;
  675. `
  676. _, err = tx.Exec(sql)
  677. return err
  678. },
  679. func(tx *sql.Tx) (err error) {
  680. sql := `
  681. ALTER TABLE integrations ADD COLUMN linkding_mark_as_unread bool default 'f';
  682. `
  683. _, err = tx.Exec(sql)
  684. return err
  685. },
  686. func(tx *sql.Tx) (err error) {
  687. // Delete duplicated rows
  688. sql := `
  689. DELETE FROM enclosures a USING enclosures b
  690. WHERE a.id < b.id
  691. AND a.user_id = b.user_id
  692. AND a.entry_id = b.entry_id
  693. AND a.url = b.url;
  694. `
  695. _, err = tx.Exec(sql)
  696. if err != nil {
  697. return err
  698. }
  699. // Remove previous index
  700. _, err = tx.Exec(`DROP INDEX enclosures_user_entry_url_idx`)
  701. if err != nil {
  702. return err
  703. }
  704. // Create unique index
  705. //
  706. // This originally used md5(url), but it was changed to sha256 because
  707. // PostgreSQL 18 disables MD5 in FIPS mode, which made fresh installs
  708. // fail while replaying this migration. Existing installs that already
  709. // ran it are migrated later on.
  710. _, err = tx.Exec(`CREATE UNIQUE INDEX enclosures_user_entry_url_unique_idx ON enclosures(user_id, entry_id, encode(sha256(url::bytea), 'hex'))`)
  711. if err != nil {
  712. return err
  713. }
  714. return nil
  715. },
  716. func(tx *sql.Tx) (err error) {
  717. sql := `ALTER TABLE users ADD COLUMN mark_read_on_view boolean default 't'`
  718. _, err = tx.Exec(sql)
  719. return err
  720. },
  721. func(tx *sql.Tx) (err error) {
  722. sql := `
  723. ALTER TABLE integrations
  724. ADD COLUMN notion_enabled bool default 'f',
  725. ADD COLUMN notion_token text default '',
  726. ADD COLUMN notion_page_id text default '';
  727. `
  728. _, err = tx.Exec(sql)
  729. return err
  730. },
  731. func(tx *sql.Tx) (err error) {
  732. sql := `
  733. ALTER TABLE integrations
  734. ADD COLUMN readwise_enabled bool default 'f',
  735. ADD COLUMN readwise_api_key text default '';
  736. `
  737. _, err = tx.Exec(sql)
  738. return err
  739. },
  740. func(tx *sql.Tx) (err error) {
  741. sql := `
  742. ALTER TABLE integrations
  743. ADD COLUMN apprise_enabled bool default 'f',
  744. ADD COLUMN apprise_url text default '',
  745. ADD COLUMN apprise_services_url text default '';
  746. `
  747. _, err = tx.Exec(sql)
  748. return err
  749. },
  750. func(tx *sql.Tx) (err error) {
  751. sql := `
  752. ALTER TABLE integrations
  753. ADD COLUMN shiori_enabled bool default 'f',
  754. ADD COLUMN shiori_url text default '',
  755. ADD COLUMN shiori_username text default '',
  756. ADD COLUMN shiori_password text default '';
  757. `
  758. _, err = tx.Exec(sql)
  759. return err
  760. },
  761. func(tx *sql.Tx) (err error) {
  762. sql := `
  763. ALTER TABLE integrations
  764. ADD COLUMN shaarli_enabled bool default 'f',
  765. ADD COLUMN shaarli_url text default '',
  766. ADD COLUMN shaarli_api_secret text default '';
  767. `
  768. _, err = tx.Exec(sql)
  769. return err
  770. },
  771. func(tx *sql.Tx) (err error) {
  772. _, err = tx.Exec(`
  773. ALTER TABLE feeds ADD COLUMN apprise_service_urls text default '';
  774. `)
  775. return err
  776. },
  777. func(tx *sql.Tx) (err error) {
  778. sql := `
  779. ALTER TABLE integrations
  780. ADD COLUMN webhook_enabled bool default 'f',
  781. ADD COLUMN webhook_url text default '',
  782. ADD COLUMN webhook_secret text default '';
  783. `
  784. _, err = tx.Exec(sql)
  785. return err
  786. },
  787. func(tx *sql.Tx) (err error) {
  788. sql := `
  789. ALTER TABLE integrations
  790. ADD COLUMN telegram_bot_topic_id int,
  791. ADD COLUMN telegram_bot_disable_web_page_preview bool default 'f',
  792. ADD COLUMN telegram_bot_disable_notification bool default 'f';
  793. `
  794. _, err = tx.Exec(sql)
  795. return err
  796. },
  797. func(tx *sql.Tx) (err error) {
  798. sql := `
  799. ALTER TABLE integrations ADD COLUMN telegram_bot_disable_buttons bool default 'f';
  800. `
  801. _, err = tx.Exec(sql)
  802. return err
  803. },
  804. func(tx *sql.Tx) (err error) {
  805. sql := `
  806. -- Speed up has_enclosure
  807. CREATE INDEX enclosures_entry_id_idx ON enclosures(entry_id);
  808. -- Speed up unread page
  809. CREATE INDEX entries_user_status_published_idx ON entries(user_id, status, published_at);
  810. CREATE INDEX entries_user_status_created_idx ON entries(user_id, status, created_at);
  811. CREATE INDEX feeds_feed_id_hide_globally_idx ON feeds(id, hide_globally);
  812. -- Speed up history page
  813. CREATE INDEX entries_user_status_changed_published_idx ON entries(user_id, status, changed_at, published_at);
  814. `
  815. _, err = tx.Exec(sql)
  816. return err
  817. },
  818. func(tx *sql.Tx) (err error) {
  819. sql := `
  820. ALTER TABLE integrations
  821. ADD COLUMN rssbridge_enabled bool default 'f',
  822. ADD COLUMN rssbridge_url text default '';
  823. `
  824. _, err = tx.Exec(sql)
  825. return
  826. },
  827. func(tx *sql.Tx) (err error) {
  828. _, err = tx.Exec(`
  829. CREATE TABLE webauthn_credentials (
  830. handle bytea primary key,
  831. cred_id bytea unique not null,
  832. user_id int references users(id) on delete cascade not null,
  833. public_key bytea not null,
  834. attestation_type varchar(255) not null,
  835. aaguid bytea,
  836. sign_count bigint,
  837. clone_warning bool,
  838. name text,
  839. added_on timestamp with time zone default now(),
  840. last_seen_on timestamp with time zone default now()
  841. );
  842. `)
  843. return
  844. },
  845. func(tx *sql.Tx) (err error) {
  846. sql := `
  847. ALTER TABLE integrations
  848. ADD COLUMN omnivore_enabled bool default 'f',
  849. ADD COLUMN omnivore_api_key text default '',
  850. ADD COLUMN omnivore_url text default '';
  851. `
  852. _, err = tx.Exec(sql)
  853. return
  854. },
  855. func(tx *sql.Tx) (err error) {
  856. sql := `
  857. ALTER TABLE integrations
  858. ADD COLUMN linkace_enabled bool default 'f',
  859. ADD COLUMN linkace_url text default '',
  860. ADD COLUMN linkace_api_key text default '',
  861. ADD COLUMN linkace_tags text default '',
  862. ADD COLUMN linkace_is_private bool default 't',
  863. ADD COLUMN linkace_check_disabled bool default 't';
  864. `
  865. _, err = tx.Exec(sql)
  866. return err
  867. },
  868. func(tx *sql.Tx) (err error) {
  869. sql := `
  870. ALTER TABLE integrations
  871. ADD COLUMN linkwarden_enabled bool default 'f',
  872. ADD COLUMN linkwarden_url text default '',
  873. ADD COLUMN linkwarden_api_key text default '';
  874. `
  875. _, err = tx.Exec(sql)
  876. return err
  877. },
  878. func(tx *sql.Tx) (err error) {
  879. sql := `
  880. ALTER TABLE integrations
  881. ADD COLUMN readeck_enabled bool default 'f',
  882. ADD COLUMN readeck_only_url bool default 'f',
  883. ADD COLUMN readeck_url text default '',
  884. ADD COLUMN readeck_api_key text default '',
  885. ADD COLUMN readeck_labels text default '';
  886. `
  887. _, err = tx.Exec(sql)
  888. return err
  889. },
  890. func(tx *sql.Tx) (err error) {
  891. sql := `ALTER TABLE feeds ADD COLUMN disable_http2 bool default 'f'`
  892. _, err = tx.Exec(sql)
  893. return err
  894. },
  895. func(tx *sql.Tx) (err error) {
  896. sql := `ALTER TABLE users ADD COLUMN media_playback_rate numeric default 1;`
  897. _, err = tx.Exec(sql)
  898. return err
  899. },
  900. func(tx *sql.Tx) (err error) {
  901. // the WHERE part speed-up the request a lot
  902. sql := `UPDATE entries SET tags = array_remove(tags, '') WHERE '' = ANY(tags);`
  903. _, err = tx.Exec(sql)
  904. return err
  905. },
  906. func(tx *sql.Tx) (err error) {
  907. // Entry URLs can exceeds btree maximum size
  908. // Checking entry existence is now using entries_feed_id_status_hash_idx index
  909. _, err = tx.Exec(`DROP INDEX entries_feed_url_idx`)
  910. return err
  911. },
  912. func(tx *sql.Tx) (err error) {
  913. sql := `
  914. ALTER TABLE integrations
  915. ADD COLUMN raindrop_enabled bool default 'f',
  916. ADD COLUMN raindrop_token text default '',
  917. ADD COLUMN raindrop_collection_id text default '',
  918. ADD COLUMN raindrop_tags text default '';
  919. `
  920. _, err = tx.Exec(sql)
  921. return err
  922. },
  923. func(tx *sql.Tx) (err error) {
  924. sql := `ALTER TABLE feeds ADD COLUMN description text default ''`
  925. _, err = tx.Exec(sql)
  926. return err
  927. },
  928. func(tx *sql.Tx) (err error) {
  929. sql := `
  930. ALTER TABLE users
  931. ADD COLUMN block_filter_entry_rules text not null default '',
  932. ADD COLUMN keep_filter_entry_rules text not null default ''
  933. `
  934. _, err = tx.Exec(sql)
  935. return err
  936. },
  937. func(tx *sql.Tx) (err error) {
  938. sql := `
  939. ALTER TABLE integrations
  940. ADD COLUMN betula_url text default '',
  941. ADD COLUMN betula_token text default '',
  942. ADD COLUMN betula_enabled bool default 'f';
  943. `
  944. _, err = tx.Exec(sql)
  945. return err
  946. },
  947. func(tx *sql.Tx) (err error) {
  948. sql := `
  949. ALTER TABLE integrations
  950. ADD COLUMN ntfy_enabled bool default 'f',
  951. ADD COLUMN ntfy_url text default '',
  952. ADD COLUMN ntfy_topic text default '',
  953. ADD COLUMN ntfy_api_token text default '',
  954. ADD COLUMN ntfy_username text default '',
  955. ADD COLUMN ntfy_password text default '',
  956. ADD COLUMN ntfy_icon_url text default '';
  957. ALTER TABLE feeds
  958. ADD COLUMN ntfy_enabled bool default 'f',
  959. ADD COLUMN ntfy_priority int default '3';
  960. `
  961. _, err = tx.Exec(sql)
  962. return err
  963. },
  964. func(tx *sql.Tx) (err error) {
  965. sql := `ALTER TABLE users ADD COLUMN mark_read_on_media_player_completion bool default 'f';`
  966. _, err = tx.Exec(sql)
  967. return err
  968. },
  969. func(tx *sql.Tx) (err error) {
  970. sql := `ALTER TABLE users ADD COLUMN custom_js text not null default '';`
  971. _, err = tx.Exec(sql)
  972. return err
  973. },
  974. func(tx *sql.Tx) (err error) {
  975. sql := `ALTER TABLE users ADD COLUMN external_font_hosts text not null default '';`
  976. _, err = tx.Exec(sql)
  977. return err
  978. },
  979. func(tx *sql.Tx) (err error) {
  980. sql := `
  981. ALTER TABLE integrations
  982. ADD COLUMN cubox_enabled bool default 'f',
  983. ADD COLUMN cubox_api_link text default '';
  984. `
  985. _, err = tx.Exec(sql)
  986. return err
  987. },
  988. func(tx *sql.Tx) (err error) {
  989. sql := `
  990. ALTER TABLE integrations
  991. ADD COLUMN discord_enabled bool default 'f',
  992. ADD COLUMN discord_webhook_link text default '';
  993. `
  994. _, err = tx.Exec(sql)
  995. return err
  996. },
  997. func(tx *sql.Tx) (err error) {
  998. sql := `ALTER TABLE integrations ADD COLUMN ntfy_internal_links bool default 'f';`
  999. _, err = tx.Exec(sql)
  1000. return err
  1001. },
  1002. func(tx *sql.Tx) (err error) {
  1003. sql := `
  1004. ALTER TABLE integrations
  1005. ADD COLUMN slack_enabled bool default 'f',
  1006. ADD COLUMN slack_webhook_link text default '';
  1007. `
  1008. _, err = tx.Exec(sql)
  1009. return err
  1010. },
  1011. func(tx *sql.Tx) (err error) {
  1012. _, err = tx.Exec(`ALTER TABLE feeds ADD COLUMN webhook_url text default '';`)
  1013. return err
  1014. },
  1015. func(tx *sql.Tx) (err error) {
  1016. sql := `
  1017. ALTER TABLE integrations
  1018. ADD COLUMN pushover_enabled bool default 'f',
  1019. ADD COLUMN pushover_user text default '',
  1020. ADD COLUMN pushover_token text default '',
  1021. ADD COLUMN pushover_device text default '',
  1022. ADD COLUMN pushover_prefix text default '';
  1023. ALTER TABLE feeds
  1024. ADD COLUMN pushover_enabled bool default 'f',
  1025. ADD COLUMN pushover_priority int default '0';
  1026. `
  1027. _, err = tx.Exec(sql)
  1028. return err
  1029. },
  1030. func(tx *sql.Tx) (err error) {
  1031. sql := `
  1032. ALTER TABLE feeds ADD COLUMN ntfy_topic text default '';
  1033. `
  1034. _, err = tx.Exec(sql)
  1035. return err
  1036. },
  1037. func(tx *sql.Tx) (err error) {
  1038. sql := `
  1039. ALTER TABLE icons ADD COLUMN external_id text default '';
  1040. CREATE UNIQUE INDEX icons_external_id_idx ON icons USING btree(external_id) WHERE external_id <> '';
  1041. `
  1042. _, err = tx.Exec(sql)
  1043. return err
  1044. },
  1045. func(tx *sql.Tx) (err error) {
  1046. _, err = tx.Exec(`
  1047. DECLARE id_cursor CURSOR FOR
  1048. SELECT
  1049. id
  1050. FROM icons
  1051. WHERE external_id = ''
  1052. FOR UPDATE`)
  1053. if err != nil {
  1054. return err
  1055. }
  1056. defer tx.Exec("CLOSE id_cursor")
  1057. for {
  1058. var id int64
  1059. if err := tx.QueryRow(`FETCH NEXT FROM id_cursor`).Scan(&id); err != nil {
  1060. if errors.Is(err, sql.ErrNoRows) {
  1061. break
  1062. }
  1063. return err
  1064. }
  1065. _, err = tx.Exec(
  1066. `
  1067. UPDATE icons SET external_id = $1 WHERE id = $2
  1068. `,
  1069. crypto.GenerateRandomStringHex(20), id)
  1070. if err != nil {
  1071. return err
  1072. }
  1073. }
  1074. return nil
  1075. },
  1076. func(tx *sql.Tx) (err error) {
  1077. _, err = tx.Exec(`ALTER TABLE feeds ADD COLUMN proxy_url text default ''`)
  1078. return err
  1079. },
  1080. func(tx *sql.Tx) (err error) {
  1081. sql := `
  1082. ALTER TABLE integrations ADD COLUMN rssbridge_token text default '';
  1083. `
  1084. _, err = tx.Exec(sql)
  1085. return err
  1086. },
  1087. func(tx *sql.Tx) (err error) {
  1088. _, err = tx.Exec(`ALTER TABLE users ADD COLUMN always_open_external_links bool default 'f'`)
  1089. return err
  1090. },
  1091. func(tx *sql.Tx) (err error) {
  1092. sql := `
  1093. ALTER TABLE integrations
  1094. ADD COLUMN karakeep_enabled bool default 'f',
  1095. ADD COLUMN karakeep_api_key text default '',
  1096. ADD COLUMN karakeep_url text default '';
  1097. `
  1098. _, err = tx.Exec(sql)
  1099. return err
  1100. },
  1101. func(tx *sql.Tx) (err error) {
  1102. _, err = tx.Exec(`ALTER TABLE users ADD COLUMN open_external_links_in_new_tab bool default 't'`)
  1103. return err
  1104. },
  1105. func(tx *sql.Tx) (err error) {
  1106. sql := `
  1107. ALTER TABLE integrations
  1108. DROP COLUMN pocket_enabled,
  1109. DROP COLUMN pocket_access_token,
  1110. DROP COLUMN pocket_consumer_key;
  1111. `
  1112. _, err = tx.Exec(sql)
  1113. return err
  1114. },
  1115. func(tx *sql.Tx) (err error) {
  1116. sql := `
  1117. ALTER TABLE feeds
  1118. ADD COLUMN block_filter_entry_rules text not null default '',
  1119. ADD COLUMN keep_filter_entry_rules text not null default ''
  1120. `
  1121. _, err = tx.Exec(sql)
  1122. return err
  1123. },
  1124. func(tx *sql.Tx) (err error) {
  1125. sql := `
  1126. CREATE TYPE linktaco_link_visibility AS ENUM (
  1127. 'PUBLIC',
  1128. 'PRIVATE'
  1129. );
  1130. ALTER TABLE integrations
  1131. ADD COLUMN linktaco_enabled bool default 'f',
  1132. ADD COLUMN linktaco_api_token text default '',
  1133. ADD COLUMN linktaco_org_slug text default '',
  1134. ADD COLUMN linktaco_tags text default '',
  1135. ADD COLUMN linktaco_visibility linktaco_link_visibility default 'PUBLIC';
  1136. `
  1137. _, err = tx.Exec(sql)
  1138. return err
  1139. },
  1140. func(tx *sql.Tx) (err error) {
  1141. sql := `
  1142. ALTER TABLE integrations ADD COLUMN wallabag_tags text default '';
  1143. `
  1144. _, err = tx.Exec(sql)
  1145. return err
  1146. },
  1147. // This migration replaces deprecated timezones by their equivalent on Debian Trixie.
  1148. func(tx *sql.Tx) (err error) {
  1149. var deprecatedTimeZoneMap = map[string]string{
  1150. // Africa
  1151. "Africa/Asmera": "Africa/Asmara",
  1152. // America - Argentina
  1153. "America/Argentina/ComodRivadavia": "America/Argentina/Catamarca",
  1154. "America/Buenos_Aires": "America/Argentina/Buenos_Aires",
  1155. "America/Catamarca": "America/Argentina/Catamarca",
  1156. "America/Cordoba": "America/Argentina/Cordoba",
  1157. "America/Jujuy": "America/Argentina/Jujuy",
  1158. "America/Mendoza": "America/Argentina/Mendoza",
  1159. "America/Rosario": "America/Argentina/Cordoba",
  1160. // America - US
  1161. "America/Fort_Wayne": "America/Indiana/Indianapolis",
  1162. "America/Indianapolis": "America/Indiana/Indianapolis",
  1163. "America/Knox_IN": "America/Indiana/Knox",
  1164. "America/Louisville": "America/Kentucky/Louisville",
  1165. // America - Greenland
  1166. "America/Godthab": "America/Nuuk",
  1167. // Antarctica
  1168. "Antarctica/South_Pole": "Pacific/Auckland",
  1169. // Asia
  1170. "Asia/Ashkhabad": "Asia/Ashgabat",
  1171. "Asia/Calcutta": "Asia/Kolkata",
  1172. "Asia/Choibalsan": "Asia/Ulaanbaatar",
  1173. "Asia/Chungking": "Asia/Chongqing",
  1174. "Asia/Dacca": "Asia/Dhaka",
  1175. "Asia/Katmandu": "Asia/Kathmandu",
  1176. "Asia/Macao": "Asia/Macau",
  1177. "Asia/Rangoon": "Asia/Yangon",
  1178. "Asia/Saigon": "Asia/Ho_Chi_Minh",
  1179. "Asia/Thimbu": "Asia/Thimphu",
  1180. "Asia/Ujung_Pandang": "Asia/Makassar",
  1181. "Asia/Ulan_Bator": "Asia/Ulaanbaatar",
  1182. // Atlantic
  1183. "Atlantic/Faeroe": "Atlantic/Faroe",
  1184. // Australia
  1185. "Australia/ACT": "Australia/Sydney",
  1186. "Australia/LHI": "Australia/Lord_Howe",
  1187. "Australia/North": "Australia/Darwin",
  1188. "Australia/NSW": "Australia/Sydney",
  1189. "Australia/Queensland": "Australia/Brisbane",
  1190. "Australia/South": "Australia/Adelaide",
  1191. "Australia/Tasmania": "Australia/Hobart",
  1192. "Australia/Victoria": "Australia/Melbourne",
  1193. "Australia/West": "Australia/Perth",
  1194. // Brazil
  1195. "Brazil/Acre": "America/Rio_Branco",
  1196. "Brazil/DeNoronha": "America/Noronha",
  1197. "Brazil/East": "America/Sao_Paulo",
  1198. "Brazil/West": "America/Manaus",
  1199. // Canada
  1200. "Canada/Atlantic": "America/Halifax",
  1201. "Canada/Central": "America/Winnipeg",
  1202. "Canada/Eastern": "America/Toronto",
  1203. "Canada/Mountain": "America/Edmonton",
  1204. "Canada/Newfoundland": "America/St_Johns",
  1205. "Canada/Pacific": "America/Vancouver",
  1206. "Canada/Saskatchewan": "America/Regina",
  1207. "Canada/Yukon": "America/Whitehorse",
  1208. // Europe
  1209. "CET": "Europe/Paris",
  1210. "EET": "Europe/Sofia",
  1211. "Europe/Kiev": "Europe/Kyiv",
  1212. "Europe/Uzhgorod": "Europe/Kyiv",
  1213. "Europe/Zaporozhye": "Europe/Kyiv",
  1214. "MET": "Europe/Paris",
  1215. "WET": "Europe/Lisbon",
  1216. // Chile
  1217. "Chile/Continental": "America/Santiago",
  1218. "Chile/EasterIsland": "Pacific/Easter",
  1219. // Fixed offset and generic zones
  1220. "CST6CDT": "America/Chicago",
  1221. "EST": "America/New_York",
  1222. "EST5EDT": "America/New_York",
  1223. "HST": "Pacific/Honolulu",
  1224. "MST": "America/Denver",
  1225. "MST7MDT": "America/Denver",
  1226. "PST8PDT": "America/Los_Angeles",
  1227. // Countries/Regions
  1228. "Cuba": "America/Havana",
  1229. "Egypt": "Africa/Cairo",
  1230. "Eire": "Europe/Dublin",
  1231. "GB": "Europe/London",
  1232. "GB-Eire": "Europe/London",
  1233. "Hongkong": "Asia/Hong_Kong",
  1234. "Iceland": "Atlantic/Reykjavik",
  1235. "Iran": "Asia/Tehran",
  1236. "Israel": "Asia/Jerusalem",
  1237. "Jamaica": "America/Jamaica",
  1238. "Japan": "Asia/Tokyo",
  1239. "Libya": "Africa/Tripoli",
  1240. "Poland": "Europe/Warsaw",
  1241. "Portugal": "Europe/Lisbon",
  1242. "PRC": "Asia/Shanghai",
  1243. "ROC": "Asia/Taipei",
  1244. "ROK": "Asia/Seoul",
  1245. "Singapore": "Asia/Singapore",
  1246. "Turkey": "Europe/Istanbul",
  1247. // GMT variations
  1248. "GMT+0": "GMT",
  1249. "GMT-0": "GMT",
  1250. "GMT0": "GMT",
  1251. "Greenwich": "GMT",
  1252. "UCT": "UTC",
  1253. "Universal": "UTC",
  1254. "Zulu": "UTC",
  1255. // Mexico
  1256. "Mexico/BajaNorte": "America/Tijuana",
  1257. "Mexico/BajaSur": "America/Mazatlan",
  1258. "Mexico/General": "America/Mexico_City",
  1259. // US zones
  1260. "Navajo": "America/Denver",
  1261. "US/Alaska": "America/Anchorage",
  1262. "US/Aleutian": "America/Adak",
  1263. "US/Arizona": "America/Phoenix",
  1264. "US/Central": "America/Chicago",
  1265. "US/Eastern": "America/New_York",
  1266. "US/East-Indiana": "America/Indiana/Indianapolis",
  1267. "US/Hawaii": "Pacific/Honolulu",
  1268. "US/Indiana-Starke": "America/Indiana/Knox",
  1269. "US/Michigan": "America/Detroit",
  1270. "US/Mountain": "America/Denver",
  1271. "US/Pacific": "America/Los_Angeles",
  1272. "US/Samoa": "Pacific/Pago_Pago",
  1273. // Pacific
  1274. "Kwajalein": "Pacific/Kwajalein",
  1275. "NZ": "Pacific/Auckland",
  1276. "NZ-CHAT": "Pacific/Chatham",
  1277. "Pacific/Enderbury": "Pacific/Kanton",
  1278. "Pacific/Ponape": "Pacific/Pohnpei",
  1279. "Pacific/Truk": "Pacific/Chuuk",
  1280. // Special cases
  1281. "Factory": "UTC", // Factory is used for unconfigured systems
  1282. "W-SU": "Europe/Moscow",
  1283. }
  1284. // Loop through each user and correct the timezone
  1285. rows, err := tx.Query(`SELECT id, timezone FROM users`)
  1286. if err != nil {
  1287. return err
  1288. }
  1289. userTimezoneMap := make(map[int64]string)
  1290. for rows.Next() {
  1291. var userID int64
  1292. var userTimezone string
  1293. if err := rows.Scan(&userID, &userTimezone); err != nil {
  1294. return err
  1295. }
  1296. userTimezoneMap[userID] = userTimezone
  1297. }
  1298. rows.Close()
  1299. for userID, userTimezone := range userTimezoneMap {
  1300. if newTimezone, found := deprecatedTimeZoneMap[userTimezone]; found {
  1301. if _, err := tx.Exec(`UPDATE users SET timezone = $1 WHERE id = $2`, newTimezone, userID); err != nil {
  1302. return err
  1303. }
  1304. }
  1305. }
  1306. return nil
  1307. },
  1308. func(tx *sql.Tx) (err error) {
  1309. sql := `
  1310. ALTER TABLE integrations ADD COLUMN archiveorg_enabled bool default 'f'
  1311. `
  1312. _, err = tx.Exec(sql)
  1313. return err
  1314. },
  1315. func(tx *sql.Tx) (err error) {
  1316. sql := `DROP EXTENSION IF EXISTS hstore;`
  1317. _, err = tx.Exec(sql)
  1318. return err
  1319. },
  1320. func(tx *sql.Tx) (err error) {
  1321. sql := `
  1322. ALTER TABLE integrations ADD COLUMN karakeep_tags text default '';
  1323. `
  1324. _, err = tx.Exec(sql)
  1325. return err
  1326. },
  1327. func(tx *sql.Tx) (err error) {
  1328. sql := `
  1329. ALTER TABLE integrations ADD COLUMN linkwarden_collection_id int;
  1330. `
  1331. _, err = tx.Exec(sql)
  1332. return err
  1333. },
  1334. func(tx *sql.Tx) (err error) {
  1335. sql := `
  1336. ALTER TABLE integrations ADD COLUMN readeck_push_enabled bool default 'f';
  1337. `
  1338. _, err = tx.Exec(sql)
  1339. return err
  1340. },
  1341. func(tx *sql.Tx) (err error) {
  1342. // There is no need to keep an index on the content of deleted entries.
  1343. _, err = tx.Exec(`DROP INDEX document_vectors_idx;`)
  1344. if err != nil {
  1345. return err
  1346. }
  1347. sql := `
  1348. CREATE INDEX document_vectors_idx
  1349. ON entries
  1350. USING gin(document_vectors)
  1351. WHERE status != 'removed';
  1352. `
  1353. _, err = tx.Exec(sql)
  1354. return err
  1355. },
  1356. func(tx *sql.Tx) (err error) {
  1357. _, err = tx.Exec(`UPDATE user_sessions SET ip = '127.0.0.1'::inet WHERE ip IS NULL`)
  1358. if err != nil {
  1359. return err
  1360. }
  1361. _, err = tx.Exec(`UPDATE user_sessions SET created_at = now() WHERE created_at IS NULL`)
  1362. if err != nil {
  1363. return err
  1364. }
  1365. _, err = tx.Exec(`UPDATE user_sessions SET user_agent = '' WHERE user_agent IS NULL`)
  1366. if err != nil {
  1367. return err
  1368. }
  1369. _, err = tx.Exec(`
  1370. ALTER TABLE user_sessions
  1371. ALTER COLUMN ip SET DEFAULT '127.0.0.1'::inet,
  1372. ALTER COLUMN ip SET NOT NULL,
  1373. ALTER COLUMN created_at SET DEFAULT now(),
  1374. ALTER COLUMN created_at SET NOT NULL,
  1375. ALTER COLUMN user_agent SET DEFAULT '',
  1376. ALTER COLUMN user_agent SET NOT NULL
  1377. `)
  1378. return err
  1379. },
  1380. func(tx *sql.Tx) (err error) {
  1381. _, err = tx.Exec(`ALTER TABLE feeds ADD COLUMN ignore_entry_updates bool default 'f'`)
  1382. return err
  1383. },
  1384. func(tx *sql.Tx) (err error) {
  1385. _, err = tx.Exec(`
  1386. DROP TABLE IF EXISTS sessions;
  1387. DROP TABLE IF EXISTS user_sessions;
  1388. CREATE TABLE web_sessions (
  1389. id text not null,
  1390. secret_hash bytea not null,
  1391. user_id int references users(id) on delete cascade,
  1392. created_at timestamp with time zone not null default now(),
  1393. user_agent text not null default '',
  1394. ip inet,
  1395. state jsonb not null default '{}'::jsonb,
  1396. primary key (id),
  1397. check (jsonb_typeof(state) = 'object')
  1398. );
  1399. CREATE INDEX web_sessions_user_id_idx
  1400. ON web_sessions (user_id)
  1401. WHERE user_id IS NOT NULL;
  1402. CREATE INDEX web_sessions_created_at_idx
  1403. ON web_sessions (created_at);
  1404. `)
  1405. return err
  1406. },
  1407. func(tx *sql.Tx) (err error) {
  1408. _, err = tx.Exec(`
  1409. CREATE TABLE entry_tombstones (
  1410. feed_id bigint not null references feeds(id) on delete cascade,
  1411. hash text not null check (hash <> ''),
  1412. deleted_at timestamp with time zone not null default now(),
  1413. primary key (feed_id, hash)
  1414. );
  1415. CREATE INDEX entry_tombstones_deleted_at_idx
  1416. ON entry_tombstones (deleted_at);
  1417. INSERT INTO entry_tombstones (feed_id, hash, deleted_at)
  1418. SELECT feed_id, hash, changed_at
  1419. FROM entries
  1420. WHERE status = 'removed' AND hash <> ''
  1421. ON CONFLICT (feed_id, hash) DO NOTHING;
  1422. DELETE FROM entries WHERE status = 'removed';
  1423. -- The "removed" status is no longer used, so drop the partial
  1424. -- predicate so the planner can use the index for every search.
  1425. DROP INDEX document_vectors_idx;
  1426. CREATE INDEX document_vectors_idx
  1427. ON entries
  1428. USING gin(document_vectors);
  1429. `)
  1430. return err
  1431. },
  1432. func(tx *sql.Tx) (err error) {
  1433. _, err = tx.Exec(`
  1434. DELETE FROM integrations WHERE user_id NOT IN (SELECT id FROM users);
  1435. ALTER TABLE integrations
  1436. ADD CONSTRAINT integrations_user_id_fkey
  1437. FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
  1438. `)
  1439. return err
  1440. },
  1441. func(tx *sql.Tx) (err error) {
  1442. // backup_eligible is nullable: NULL marks pre-migration rows so the login path can backfill it from the assertion on first use.
  1443. _, err = tx.Exec(`
  1444. UPDATE webauthn_credentials SET name = '' WHERE name IS NULL;
  1445. ALTER TABLE webauthn_credentials
  1446. ALTER COLUMN name SET DEFAULT '',
  1447. ALTER COLUMN name SET NOT NULL,
  1448. ADD COLUMN backup_eligible boolean,
  1449. ADD COLUMN backup_state boolean NOT NULL DEFAULT false;
  1450. `)
  1451. return err
  1452. },
  1453. func(tx *sql.Tx) (err error) {
  1454. // entries_feed_idx is redundant: the unique constraint
  1455. // entries_feed_id_hash_key(feed_id, hash) and the explicit
  1456. // entries_feed_id_status_hash_idx(feed_id, status, hash) both
  1457. // cover feed_id-leading lookups, including FK cascade deletes.
  1458. //
  1459. // entries_user_status_idx is redundant: five three-column indexes
  1460. // share the same (user_id, status) prefix and serve every query
  1461. // that the two-column index could.
  1462. _, err = tx.Exec(`
  1463. DROP INDEX IF EXISTS entries_feed_idx;
  1464. DROP INDEX IF EXISTS entries_user_status_idx;
  1465. `)
  1466. return err
  1467. },
  1468. func(tx *sql.Tx) (err error) {
  1469. // PostgreSQL 18 disables MD5 when running in FIPS mode, which makes
  1470. // the unique index on enclosures relying on md5(url) unusable.
  1471. // Replace it with a SHA-256 based expression index.
  1472. _, err = tx.Exec(`
  1473. DROP INDEX IF EXISTS enclosures_user_entry_url_unique_idx;
  1474. CREATE UNIQUE INDEX enclosures_user_entry_url_unique_idx
  1475. ON enclosures (user_id, entry_id, encode(sha256(url::bytea), 'hex'));
  1476. `)
  1477. return err
  1478. },
  1479. }