migrations.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. // Copyright 2020 Frédéric Guillot. All rights reserved.
  2. // Use of this source code is governed by the Apache 2.0
  3. // license that can be found in the LICENSE file.
  4. package database // import "miniflux.app/database"
  5. import (
  6. "database/sql"
  7. )
  8. var schemaVersion = len(migrations)
  9. // Order is important. Add new migrations at the end of the list.
  10. var migrations = []func(tx *sql.Tx) error{
  11. func(tx *sql.Tx) (err error) {
  12. sql := `
  13. CREATE TABLE schema_version (
  14. version text not null
  15. );
  16. CREATE TABLE users (
  17. id serial not null,
  18. username text not null unique,
  19. password text,
  20. is_admin bool default 'f',
  21. language text default 'en_US',
  22. timezone text default 'UTC',
  23. theme text default 'default',
  24. last_login_at timestamp with time zone,
  25. primary key (id)
  26. );
  27. CREATE TABLE sessions (
  28. id serial not null,
  29. user_id int not null,
  30. token text not null unique,
  31. created_at timestamp with time zone default now(),
  32. user_agent text,
  33. ip text,
  34. primary key (id),
  35. unique (user_id, token),
  36. foreign key (user_id) references users(id) on delete cascade
  37. );
  38. CREATE TABLE categories (
  39. id serial not null,
  40. user_id int not null,
  41. title text not null,
  42. primary key (id),
  43. unique (user_id, title),
  44. foreign key (user_id) references users(id) on delete cascade
  45. );
  46. CREATE TABLE feeds (
  47. id bigserial not null,
  48. user_id int not null,
  49. category_id int not null,
  50. title text not null,
  51. feed_url text not null,
  52. site_url text not null,
  53. checked_at timestamp with time zone default now(),
  54. etag_header text default '',
  55. last_modified_header text default '',
  56. parsing_error_msg text default '',
  57. parsing_error_count int default 0,
  58. primary key (id),
  59. unique (user_id, feed_url),
  60. foreign key (user_id) references users(id) on delete cascade,
  61. foreign key (category_id) references categories(id) on delete cascade
  62. );
  63. CREATE TYPE entry_status as enum('unread', 'read', 'removed');
  64. CREATE TABLE entries (
  65. id bigserial not null,
  66. user_id int not null,
  67. feed_id bigint not null,
  68. hash text not null,
  69. published_at timestamp with time zone not null,
  70. title text not null,
  71. url text not null,
  72. author text,
  73. content text,
  74. status entry_status default 'unread',
  75. primary key (id),
  76. unique (feed_id, hash),
  77. foreign key (user_id) references users(id) on delete cascade,
  78. foreign key (feed_id) references feeds(id) on delete cascade
  79. );
  80. CREATE INDEX entries_feed_idx on entries using btree(feed_id);
  81. CREATE TABLE enclosures (
  82. id bigserial not null,
  83. user_id int not null,
  84. entry_id bigint not null,
  85. url text not null,
  86. size int default 0,
  87. mime_type text default '',
  88. primary key (id),
  89. foreign key (user_id) references users(id) on delete cascade,
  90. foreign key (entry_id) references entries(id) on delete cascade
  91. );
  92. CREATE TABLE icons (
  93. id bigserial not null,
  94. hash text not null unique,
  95. mime_type text not null,
  96. content bytea not null,
  97. primary key (id)
  98. );
  99. CREATE TABLE feed_icons (
  100. feed_id bigint not null,
  101. icon_id bigint not null,
  102. primary key(feed_id, icon_id),
  103. foreign key (feed_id) references feeds(id) on delete cascade,
  104. foreign key (icon_id) references icons(id) on delete cascade
  105. );
  106. `
  107. _, err = tx.Exec(sql)
  108. return err
  109. },
  110. func(tx *sql.Tx) (err error) {
  111. sql := `
  112. CREATE EXTENSION IF NOT EXISTS hstore;
  113. ALTER TABLE users ADD COLUMN extra hstore;
  114. CREATE INDEX users_extra_idx ON users using gin(extra);
  115. `
  116. _, err = tx.Exec(sql)
  117. return err
  118. },
  119. func(tx *sql.Tx) (err error) {
  120. sql := `
  121. CREATE TABLE tokens (
  122. id text not null,
  123. value text not null,
  124. created_at timestamp with time zone not null default now(),
  125. primary key(id, value)
  126. );
  127. `
  128. _, err = tx.Exec(sql)
  129. return err
  130. },
  131. func(tx *sql.Tx) (err error) {
  132. sql := `
  133. CREATE TYPE entry_sorting_direction AS enum('asc', 'desc');
  134. ALTER TABLE users ADD COLUMN entry_direction entry_sorting_direction default 'asc';
  135. `
  136. _, err = tx.Exec(sql)
  137. return err
  138. },
  139. func(tx *sql.Tx) (err error) {
  140. sql := `
  141. CREATE TABLE integrations (
  142. user_id int not null,
  143. pinboard_enabled bool default 'f',
  144. pinboard_token text default '',
  145. pinboard_tags text default 'miniflux',
  146. pinboard_mark_as_unread bool default 'f',
  147. instapaper_enabled bool default 'f',
  148. instapaper_username text default '',
  149. instapaper_password text default '',
  150. fever_enabled bool default 'f',
  151. fever_username text default '',
  152. fever_password text default '',
  153. fever_token text default '',
  154. primary key(user_id)
  155. );
  156. `
  157. _, err = tx.Exec(sql)
  158. return err
  159. },
  160. func(tx *sql.Tx) (err error) {
  161. sql := `ALTER TABLE feeds ADD COLUMN scraper_rules text default ''`
  162. _, err = tx.Exec(sql)
  163. return err
  164. },
  165. func(tx *sql.Tx) (err error) {
  166. sql := `ALTER TABLE feeds ADD COLUMN rewrite_rules text default ''`
  167. _, err = tx.Exec(sql)
  168. return err
  169. },
  170. func(tx *sql.Tx) (err error) {
  171. sql := `ALTER TABLE feeds ADD COLUMN crawler boolean default 'f'`
  172. _, err = tx.Exec(sql)
  173. return err
  174. },
  175. func(tx *sql.Tx) (err error) {
  176. sql := `ALTER TABLE sessions rename to user_sessions`
  177. _, err = tx.Exec(sql)
  178. return err
  179. },
  180. func(tx *sql.Tx) (err error) {
  181. sql := `
  182. DROP TABLE tokens;
  183. CREATE TABLE sessions (
  184. id text not null,
  185. data jsonb not null,
  186. created_at timestamp with time zone not null default now(),
  187. primary key(id)
  188. );
  189. `
  190. _, err = tx.Exec(sql)
  191. return err
  192. },
  193. func(tx *sql.Tx) (err error) {
  194. sql := `
  195. ALTER TABLE integrations ADD COLUMN wallabag_enabled bool default 'f';
  196. ALTER TABLE integrations ADD COLUMN wallabag_url text default '';
  197. ALTER TABLE integrations ADD COLUMN wallabag_client_id text default '';
  198. ALTER TABLE integrations ADD COLUMN wallabag_client_secret text default '';
  199. ALTER TABLE integrations ADD COLUMN wallabag_username text default '';
  200. ALTER TABLE integrations ADD COLUMN wallabag_password text default '';
  201. `
  202. _, err = tx.Exec(sql)
  203. return err
  204. },
  205. func(tx *sql.Tx) (err error) {
  206. sql := `ALTER TABLE entries ADD COLUMN starred bool default 'f'`
  207. _, err = tx.Exec(sql)
  208. return err
  209. },
  210. func(tx *sql.Tx) (err error) {
  211. sql := `
  212. CREATE INDEX entries_user_status_idx ON entries(user_id, status);
  213. CREATE INDEX feeds_user_category_idx ON feeds(user_id, category_id);
  214. `
  215. _, err = tx.Exec(sql)
  216. return err
  217. },
  218. func(tx *sql.Tx) (err error) {
  219. sql := `
  220. ALTER TABLE integrations ADD COLUMN nunux_keeper_enabled bool default 'f';
  221. ALTER TABLE integrations ADD COLUMN nunux_keeper_url text default '';
  222. ALTER TABLE integrations ADD COLUMN nunux_keeper_api_key text default '';
  223. `
  224. _, err = tx.Exec(sql)
  225. return err
  226. },
  227. func(tx *sql.Tx) (err error) {
  228. sql := `ALTER TABLE enclosures ALTER COLUMN size SET DATA TYPE bigint`
  229. _, err = tx.Exec(sql)
  230. return err
  231. },
  232. func(tx *sql.Tx) (err error) {
  233. sql := `ALTER TABLE entries ADD COLUMN comments_url text default ''`
  234. _, err = tx.Exec(sql)
  235. return err
  236. },
  237. func(tx *sql.Tx) (err error) {
  238. sql := `
  239. ALTER TABLE integrations ADD COLUMN pocket_enabled bool default 'f';
  240. ALTER TABLE integrations ADD COLUMN pocket_access_token text default '';
  241. ALTER TABLE integrations 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 ADD COLUMN username text default '';
  256. ALTER TABLE feeds ADD COLUMN password text default '';
  257. `
  258. _, err = tx.Exec(sql)
  259. return err
  260. },
  261. func(tx *sql.Tx) (err error) {
  262. sql := `
  263. ALTER TABLE entries ADD COLUMN document_vectors tsvector;
  264. UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
  265. CREATE INDEX document_vectors_idx ON entries USING gin(document_vectors);
  266. `
  267. _, err = tx.Exec(sql)
  268. return err
  269. },
  270. func(tx *sql.Tx) (err error) {
  271. sql := `ALTER TABLE feeds ADD COLUMN user_agent text default ''`
  272. _, err = tx.Exec(sql)
  273. return err
  274. },
  275. func(tx *sql.Tx) (err error) {
  276. sql := `
  277. UPDATE
  278. entries
  279. SET
  280. document_vectors = setweight(to_tsvector(substring(coalesce(title, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce(content, '') for 1000000)), 'B')
  281. `
  282. _, err = tx.Exec(sql)
  283. return err
  284. },
  285. func(tx *sql.Tx) (err error) {
  286. sql := `ALTER TABLE users ADD COLUMN keyboard_shortcuts boolean default 't'`
  287. _, err = tx.Exec(sql)
  288. return err
  289. },
  290. func(tx *sql.Tx) (err error) {
  291. sql := `ALTER TABLE feeds ADD COLUMN disabled boolean default 'f';`
  292. _, err = tx.Exec(sql)
  293. return err
  294. },
  295. func(tx *sql.Tx) (err error) {
  296. sql := `
  297. ALTER TABLE users ALTER COLUMN theme SET DEFAULT 'light_serif';
  298. UPDATE users SET theme='light_serif' WHERE theme='default';
  299. UPDATE users SET theme='light_sans_serif' WHERE theme='sansserif';
  300. UPDATE users SET theme='dark_serif' WHERE theme='black';
  301. `
  302. _, err = tx.Exec(sql)
  303. return err
  304. },
  305. func(tx *sql.Tx) (err error) {
  306. sql := `
  307. ALTER TABLE entries ADD COLUMN changed_at timestamp with time zone;
  308. UPDATE entries SET changed_at = published_at;
  309. ALTER TABLE entries ALTER COLUMN changed_at SET not null;
  310. `
  311. _, err = tx.Exec(sql)
  312. return err
  313. },
  314. func(tx *sql.Tx) (err error) {
  315. sql := `
  316. CREATE TABLE api_keys (
  317. id serial not null,
  318. user_id int not null references users(id) on delete cascade,
  319. token text not null unique,
  320. description text not null,
  321. last_used_at timestamp with time zone,
  322. created_at timestamp with time zone default now(),
  323. primary key(id),
  324. unique (user_id, description)
  325. );
  326. `
  327. _, err = tx.Exec(sql)
  328. return err
  329. },
  330. func(tx *sql.Tx) (err error) {
  331. sql := `
  332. ALTER TABLE entries ADD COLUMN share_code text not null default '';
  333. CREATE UNIQUE INDEX entries_share_code_idx ON entries USING btree(share_code) WHERE share_code <> '';
  334. `
  335. _, err = tx.Exec(sql)
  336. return err
  337. },
  338. func(tx *sql.Tx) (err error) {
  339. sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, md5(url))`
  340. _, err = tx.Exec(sql)
  341. return err
  342. },
  343. func(tx *sql.Tx) (err error) {
  344. sql := `
  345. ALTER TABLE feeds ADD COLUMN next_check_at timestamp with time zone default now();
  346. CREATE INDEX entries_user_feed_idx ON entries (user_id, feed_id);
  347. `
  348. _, err = tx.Exec(sql)
  349. return err
  350. },
  351. func(tx *sql.Tx) (err error) {
  352. sql := `ALTER TABLE feeds ADD COLUMN ignore_http_cache bool default false`
  353. _, err = tx.Exec(sql)
  354. return err
  355. },
  356. func(tx *sql.Tx) (err error) {
  357. sql := `ALTER TABLE users ADD COLUMN entries_per_page int default 100`
  358. _, err = tx.Exec(sql)
  359. return err
  360. },
  361. func(tx *sql.Tx) (err error) {
  362. sql := `ALTER TABLE users ADD COLUMN show_reading_time boolean default 't'`
  363. _, err = tx.Exec(sql)
  364. return err
  365. },
  366. func(tx *sql.Tx) (err error) {
  367. sql := `CREATE INDEX entries_id_user_status_idx ON entries USING btree (id, user_id, status)`
  368. _, err = tx.Exec(sql)
  369. return err
  370. },
  371. func(tx *sql.Tx) (err error) {
  372. sql := `ALTER TABLE feeds ADD COLUMN fetch_via_proxy bool default false`
  373. _, err = tx.Exec(sql)
  374. return err
  375. },
  376. func(tx *sql.Tx) (err error) {
  377. sql := `CREATE INDEX entries_feed_id_status_hash_idx ON entries USING btree (feed_id, status, hash)`
  378. _, err = tx.Exec(sql)
  379. return err
  380. },
  381. func(tx *sql.Tx) (err error) {
  382. sql := `CREATE INDEX entries_user_id_status_starred_idx ON entries (user_id, status, starred)`
  383. _, err = tx.Exec(sql)
  384. return err
  385. },
  386. func(tx *sql.Tx) (err error) {
  387. sql := `ALTER TABLE users ADD COLUMN entry_swipe boolean default 't'`
  388. _, err = tx.Exec(sql)
  389. return err
  390. },
  391. func(tx *sql.Tx) (err error) {
  392. sql := `ALTER TABLE integrations DROP COLUMN fever_password`
  393. _, err = tx.Exec(sql)
  394. return err
  395. },
  396. func(tx *sql.Tx) (err error) {
  397. sql := `
  398. ALTER TABLE feeds
  399. ADD COLUMN blocklist_rules text not null default '',
  400. ADD COLUMN keeplist_rules text not null default ''
  401. `
  402. _, err = tx.Exec(sql)
  403. return err
  404. },
  405. func(tx *sql.Tx) (err error) {
  406. sql := `ALTER TABLE entries ADD COLUMN reading_time int not null default 0`
  407. _, err = tx.Exec(sql)
  408. return err
  409. },
  410. func(tx *sql.Tx) (err error) {
  411. sql := `
  412. ALTER TABLE entries ADD COLUMN created_at timestamp with time zone not null default now();
  413. UPDATE entries SET created_at = published_at;
  414. `
  415. _, err = tx.Exec(sql)
  416. return err
  417. },
  418. func(tx *sql.Tx) (err error) {
  419. _, err = tx.Exec(`
  420. ALTER TABLE users
  421. ADD column stylesheet text not null default '',
  422. ADD column google_id text not null default '',
  423. ADD column openid_connect_id text not null default ''
  424. `)
  425. if err != nil {
  426. return err
  427. }
  428. _, err = tx.Exec(`
  429. DECLARE my_cursor CURSOR FOR
  430. SELECT
  431. id,
  432. COALESCE(extra->'custom_css', '') as custom_css,
  433. COALESCE(extra->'google_id', '') as google_id,
  434. COALESCE(extra->'oidc_id', '') as oidc_id
  435. FROM users
  436. FOR UPDATE
  437. `)
  438. if err != nil {
  439. return err
  440. }
  441. defer tx.Exec("CLOSE my_cursor")
  442. for {
  443. var (
  444. userID int64
  445. customStylesheet string
  446. googleID string
  447. oidcID string
  448. )
  449. if err := tx.QueryRow(`FETCH NEXT FROM my_cursor`).Scan(&userID, &customStylesheet, &googleID, &oidcID); err != nil {
  450. if err == sql.ErrNoRows {
  451. break
  452. }
  453. return err
  454. }
  455. _, err := tx.Exec(
  456. `UPDATE
  457. users
  458. SET
  459. stylesheet=$2,
  460. google_id=$3,
  461. openid_connect_id=$4
  462. WHERE
  463. id=$1
  464. `,
  465. userID, customStylesheet, googleID, oidcID)
  466. if err != nil {
  467. return err
  468. }
  469. }
  470. return err
  471. },
  472. func(tx *sql.Tx) (err error) {
  473. _, err = tx.Exec(`
  474. ALTER TABLE users DROP COLUMN extra;
  475. CREATE UNIQUE INDEX users_google_id_idx ON users(google_id) WHERE google_id <> '';
  476. CREATE UNIQUE INDEX users_openid_connect_id_idx ON users(openid_connect_id) WHERE openid_connect_id <> '';
  477. `)
  478. return err
  479. },
  480. func(tx *sql.Tx) (err error) {
  481. _, err = tx.Exec(`
  482. CREATE INDEX entries_feed_url_idx ON entries(feed_id, url);
  483. CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
  484. CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
  485. `)
  486. return err
  487. },
  488. func(tx *sql.Tx) (err error) {
  489. _, err = tx.Exec(`
  490. CREATE TABLE acme_cache (
  491. key varchar(400) not null primary key,
  492. data bytea not null,
  493. updated_at timestamptz not null
  494. );
  495. `)
  496. return err
  497. },
  498. func(tx *sql.Tx) (err error) {
  499. _, err = tx.Exec(`
  500. ALTER TABLE feeds ADD COLUMN allow_self_signed_certificates boolean not null default false
  501. `)
  502. return err
  503. },
  504. func(tx *sql.Tx) (err error) {
  505. sql := `
  506. CREATE TYPE webapp_display_mode AS enum('fullscreen', 'standalone', 'minimal-ui', 'browser');
  507. ALTER TABLE users ADD COLUMN display_mode webapp_display_mode default 'standalone';
  508. `
  509. _, err = tx.Exec(sql)
  510. return err
  511. },
  512. func(tx *sql.Tx) (err error) {
  513. sql := `ALTER TABLE feeds ADD COLUMN cookie text default ''`
  514. _, err = tx.Exec(sql)
  515. return err
  516. },
  517. func(tx *sql.Tx) (err error) {
  518. _, err = tx.Exec(`
  519. ALTER TABLE categories ADD COLUMN hide_globally boolean not null default false
  520. `)
  521. return err
  522. },
  523. func(tx *sql.Tx) (err error) {
  524. _, err = tx.Exec(`
  525. ALTER TABLE feeds ADD COLUMN hide_globally boolean not null default false
  526. `)
  527. return err
  528. },
  529. func(tx *sql.Tx) (err error) {
  530. sql := `
  531. ALTER TABLE integrations ADD COLUMN telegram_bot_enabled bool default 'f';
  532. ALTER TABLE integrations ADD COLUMN telegram_bot_token text default '';
  533. ALTER TABLE integrations ADD COLUMN telegram_bot_chat_id text default '';
  534. `
  535. _, err = tx.Exec(sql)
  536. return err
  537. },
  538. func(tx *sql.Tx) (err error) {
  539. sql := `
  540. CREATE TYPE entry_sorting_order AS enum('published_at', 'created_at');
  541. ALTER TABLE users ADD COLUMN entry_order entry_sorting_order default 'published_at';
  542. `
  543. _, err = tx.Exec(sql)
  544. return err
  545. },
  546. func(tx *sql.Tx) (err error) {
  547. sql := `
  548. ALTER TABLE integrations ADD COLUMN googlereader_enabled bool default 'f';
  549. ALTER TABLE integrations ADD COLUMN googlereader_username text default '';
  550. ALTER TABLE integrations ADD COLUMN googlereader_password text default '';
  551. `
  552. _, err = tx.Exec(sql)
  553. return err
  554. },
  555. func(tx *sql.Tx) (err error) {
  556. sql := `
  557. ALTER TABLE integrations ADD COLUMN espial_enabled bool default 'f';
  558. ALTER TABLE integrations ADD COLUMN espial_url text default '';
  559. ALTER TABLE integrations ADD COLUMN espial_api_key text default '';
  560. ALTER TABLE integrations ADD COLUMN espial_tags text default 'miniflux';
  561. `
  562. _, err = tx.Exec(sql)
  563. return err
  564. },
  565. func(tx *sql.Tx) (err error) {
  566. sql := `
  567. ALTER TABLE integrations ADD COLUMN linkding_enabled bool default 'f';
  568. ALTER TABLE integrations ADD COLUMN linkding_url text default '';
  569. ALTER TABLE integrations ADD COLUMN linkding_api_key text default '';
  570. `
  571. _, err = tx.Exec(sql)
  572. return err
  573. },
  574. func(tx *sql.Tx) (err error) {
  575. _, err = tx.Exec(`
  576. ALTER TABLE feeds ADD COLUMN url_rewrite_rules text not null default ''
  577. `)
  578. return err
  579. },
  580. func(tx *sql.Tx) (err error) {
  581. _, err = tx.Exec(`
  582. ALTER TABLE users ADD COLUMN default_reading_speed int default 265;
  583. ALTER TABLE users ADD COLUMN cjk_reading_speed int default 500;
  584. `)
  585. return
  586. },
  587. func(tx *sql.Tx) (err error) {
  588. _, err = tx.Exec(`
  589. ALTER TABLE users ADD COLUMN default_home_page text default 'unread';
  590. `)
  591. return
  592. },
  593. func(tx *sql.Tx) (err error) {
  594. _, err = tx.Exec(`
  595. ALTER TABLE integrations ADD COLUMN wallabag_only_url bool default 'f';
  596. `)
  597. return
  598. },
  599. func(tx *sql.Tx) (err error) {
  600. _, err = tx.Exec(`
  601. ALTER TABLE users ADD COLUMN categories_sort_order text not null default 'unread_count';
  602. `)
  603. return
  604. },
  605. }