mirror of
https://github.com/miniflux/v2.git
synced 2025-09-30 19:22:11 +00:00
feat: multi db support
This commit is contained in:
parent
10b2b36895
commit
fe6c000897
25 changed files with 2612 additions and 1433 deletions
|
@ -168,7 +168,13 @@ func Parse() {
|
|||
printErrorAndExit(fmt.Errorf("unable to generate javascript bundle: %v", err))
|
||||
}
|
||||
|
||||
kind, err := database.DetectKind(config.Opts.DatabaseURL())
|
||||
if err != nil {
|
||||
printErrorAndExit(fmt.Errorf("unable to parse database kind: %v", err))
|
||||
}
|
||||
|
||||
db, err := database.NewConnectionPool(
|
||||
kind,
|
||||
config.Opts.DatabaseURL(),
|
||||
config.Opts.DatabaseMinConns(),
|
||||
config.Opts.DatabaseMaxConns(),
|
||||
|
@ -179,14 +185,14 @@ func Parse() {
|
|||
}
|
||||
defer db.Close()
|
||||
|
||||
store := storage.NewStorage(db)
|
||||
store := storage.NewStorage(kind, db)
|
||||
|
||||
if err := store.Ping(); err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
if flagMigrate {
|
||||
if err := database.Migrate(db); err != nil {
|
||||
if err := database.Migrate(kind, db); err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
return
|
||||
|
@ -228,12 +234,12 @@ func Parse() {
|
|||
|
||||
// Run migrations and start the daemon.
|
||||
if config.Opts.RunMigrations() {
|
||||
if err := database.Migrate(db); err != nil {
|
||||
if err := database.Migrate(kind, db); err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.IsSchemaUpToDate(db); err != nil {
|
||||
if err := database.IsSchemaUpToDate(kind, db); err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -183,8 +183,8 @@ func NewConfigOptions() *configOptions {
|
|||
},
|
||||
},
|
||||
"DATABASE_URL": {
|
||||
ParsedStringValue: "user=postgres password=postgres dbname=miniflux2 sslmode=disable",
|
||||
RawValue: "user=postgres password=postgres dbname=miniflux2 sslmode=disable",
|
||||
ParsedStringValue: "postgres://postgres:postgres/postgres?sslmode=disable",
|
||||
RawValue: "postgres://postgres:postgres/postgres?sslmode=disable",
|
||||
ValueType: stringType,
|
||||
Secret: true,
|
||||
},
|
||||
|
@ -779,7 +779,7 @@ func (c *configOptions) IsAuthProxyUserCreationAllowed() bool {
|
|||
}
|
||||
|
||||
func (c *configOptions) IsDefaultDatabaseURL() bool {
|
||||
return c.options["DATABASE_URL"].RawValue == "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
|
||||
return c.options["DATABASE_URL"].RawValue == "postgres://postgres:postgres/postgres?sslmode=disable"
|
||||
}
|
||||
|
||||
func (c *configOptions) IsOAuth2UserCreationAllowed() bool {
|
||||
|
|
|
@ -348,7 +348,7 @@ func TestDatabaseMinConnsOptionParsing(t *testing.T) {
|
|||
func TestDatabaseURLOptionParsing(t *testing.T) {
|
||||
configParser := NewConfigParser()
|
||||
|
||||
if configParser.options.DatabaseURL() != "user=postgres password=postgres dbname=miniflux2 sslmode=disable" {
|
||||
if configParser.options.DatabaseURL() != "postgres://postgres:postgres/postgres?sslmode=disable" {
|
||||
t.Fatal("Expected DATABASE_URL to have default value")
|
||||
}
|
||||
|
||||
|
|
396
internal/database/cockroach.go
Normal file
396
internal/database/cockroach.go
Normal file
|
@ -0,0 +1,396 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package database // import "miniflux.app/v2/internal/database"
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
var cockroachSchemaVersion = len(cockroachMigrations)
|
||||
|
||||
// Order is important. Add new migrations at the end of the list.
|
||||
var cockroachMigrations = []Migration{
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE schema_version (
|
||||
version STRING NOT NULL,
|
||||
rowid INT8 NOT VISIBLE NOT NULL DEFAULT unique_rowid(),
|
||||
CONSTRAINT schema_version_pkey PRIMARY KEY (rowid ASC)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TYPE entry_status AS ENUM ('unread', 'read', 'removed');
|
||||
CREATE TYPE entry_sorting_direction AS ENUM ('asc', 'desc');
|
||||
CREATE TYPE webapp_display_mode AS ENUM ('fullscreen', 'standalone', 'minimal-ui', 'browser');
|
||||
CREATE TYPE entry_sorting_order AS ENUM ('published_at', 'created_at');
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE users (
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
username STRING NOT NULL,
|
||||
password STRING NULL,
|
||||
is_admin BOOL NULL DEFAULT false,
|
||||
language STRING NULL DEFAULT 'en_US':::STRING,
|
||||
timezone STRING NULL DEFAULT 'UTC':::STRING,
|
||||
theme STRING NULL DEFAULT 'light_serif':::STRING,
|
||||
last_login_at TIMESTAMPTZ NULL,
|
||||
entry_direction entry_sorting_direction NULL DEFAULT 'asc':::entry_sorting_direction,
|
||||
keyboard_shortcuts BOOL NULL DEFAULT true,
|
||||
entries_per_page INT8 NULL DEFAULT 100:::INT8,
|
||||
show_reading_time BOOL NULL DEFAULT true,
|
||||
entry_swipe BOOL NULL DEFAULT true,
|
||||
stylesheet STRING NOT NULL DEFAULT '':::STRING,
|
||||
google_id STRING NOT NULL DEFAULT '':::STRING,
|
||||
openid_connect_id STRING NOT NULL DEFAULT '':::STRING,
|
||||
display_mode webapp_display_mode NULL DEFAULT 'standalone':::webapp_display_mode,
|
||||
entry_order entry_sorting_order NULL DEFAULT 'published_at':::entry_sorting_order,
|
||||
default_reading_speed INT8 NULL DEFAULT 265:::INT8,
|
||||
cjk_reading_speed INT8 NULL DEFAULT 500:::INT8,
|
||||
default_home_page STRING NULL DEFAULT 'unread':::STRING,
|
||||
categories_sorting_order STRING NOT NULL DEFAULT 'unread_count':::STRING,
|
||||
gesture_nav STRING NOT NULL DEFAULT 'tap':::STRING,
|
||||
mark_read_on_view BOOL NULL DEFAULT true,
|
||||
media_playback_rate DECIMAL NULL DEFAULT 1:::DECIMAL,
|
||||
block_filter_entry_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||
keep_filter_entry_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||
mark_read_on_media_player_completion BOOL NULL DEFAULT false,
|
||||
custom_js STRING NOT NULL DEFAULT '':::STRING,
|
||||
external_font_hosts STRING NOT NULL DEFAULT '':::STRING,
|
||||
always_open_external_links BOOL NULL DEFAULT false,
|
||||
open_external_links_in_new_tab BOOL NULL DEFAULT true,
|
||||
CONSTRAINT users_pkey PRIMARY KEY (id ASC),
|
||||
UNIQUE INDEX users_username_key (username ASC),
|
||||
UNIQUE INDEX users_google_id_idx (google_id ASC) WHERE google_id != '':::STRING,
|
||||
UNIQUE INDEX users_openid_connect_id_idx (openid_connect_id ASC) WHERE openid_connect_id != '':::STRING
|
||||
);
|
||||
CREATE TABLE user_sessions (
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
user_id INT8 NOT NULL,
|
||||
token STRING NOT NULL,
|
||||
created_at TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||
user_agent STRING NULL,
|
||||
ip STRING NULL,
|
||||
CONSTRAINT sessions_pkey PRIMARY KEY (id ASC),
|
||||
UNIQUE INDEX sessions_token_key (token ASC),
|
||||
UNIQUE INDEX sessions_user_id_token_key (user_id ASC, token ASC)
|
||||
);
|
||||
CREATE TABLE categories (
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
user_id INT8 NOT NULL,
|
||||
title STRING NOT NULL,
|
||||
hide_globally BOOL NOT NULL DEFAULT false,
|
||||
CONSTRAINT categories_pkey PRIMARY KEY (id ASC),
|
||||
UNIQUE INDEX categories_user_id_title_key (user_id ASC, title ASC)
|
||||
);
|
||||
CREATE TABLE feeds (
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
user_id INT8 NOT NULL,
|
||||
category_id INT8 NOT NULL,
|
||||
title STRING NOT NULL,
|
||||
feed_url STRING NOT NULL,
|
||||
site_url STRING NOT NULL,
|
||||
checked_at TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||
etag_header STRING NULL DEFAULT '':::STRING,
|
||||
last_modified_header STRING NULL DEFAULT '':::STRING,
|
||||
parsing_error_msg STRING NULL DEFAULT '':::STRING,
|
||||
parsing_error_count INT8 NULL DEFAULT 0:::INT8,
|
||||
scraper_rules STRING NULL DEFAULT '':::STRING,
|
||||
rewrite_rules STRING NULL DEFAULT '':::STRING,
|
||||
crawler BOOL NULL DEFAULT false,
|
||||
username STRING NULL DEFAULT '':::STRING,
|
||||
password STRING NULL DEFAULT '':::STRING,
|
||||
user_agent STRING NULL DEFAULT '':::STRING,
|
||||
disabled BOOL NULL DEFAULT false,
|
||||
next_check_at TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||
ignore_http_cache BOOL NULL DEFAULT false,
|
||||
fetch_via_proxy BOOL NULL DEFAULT false,
|
||||
blocklist_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||
keeplist_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||
allow_self_signed_certificates BOOL NOT NULL DEFAULT false,
|
||||
cookie STRING NULL DEFAULT '':::STRING,
|
||||
hide_globally BOOL NOT NULL DEFAULT false,
|
||||
url_rewrite_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||
no_media_player BOOL NULL DEFAULT false,
|
||||
apprise_service_urls STRING NULL DEFAULT '':::STRING,
|
||||
disable_http2 BOOL NULL DEFAULT false,
|
||||
description STRING NULL DEFAULT '':::STRING,
|
||||
ntfy_enabled BOOL NULL DEFAULT false,
|
||||
ntfy_priority INT8 NULL DEFAULT 3:::INT8,
|
||||
webhook_url STRING NULL DEFAULT '':::STRING,
|
||||
pushover_enabled BOOL NULL DEFAULT false,
|
||||
pushover_priority INT8 NULL DEFAULT 0:::INT8,
|
||||
ntfy_topic STRING NULL DEFAULT '':::STRING,
|
||||
proxy_url STRING NULL DEFAULT '':::STRING,
|
||||
block_filter_entry_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||
keep_filter_entry_rules STRING NOT NULL DEFAULT '':::STRING,
|
||||
CONSTRAINT feeds_pkey PRIMARY KEY (id ASC),
|
||||
UNIQUE INDEX feeds_user_id_feed_url_key (user_id ASC, feed_url ASC),
|
||||
INDEX feeds_user_category_idx (user_id ASC, category_id ASC),
|
||||
INDEX feeds_feed_id_hide_globally_idx (id ASC, hide_globally ASC)
|
||||
);
|
||||
CREATE TABLE entries (
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
user_id INT8 NOT NULL,
|
||||
feed_id INT8 NOT NULL,
|
||||
hash STRING NOT NULL,
|
||||
published_at TIMESTAMPTZ NOT NULL,
|
||||
title STRING NOT NULL,
|
||||
url STRING NOT NULL,
|
||||
author STRING NULL,
|
||||
content STRING NULL,
|
||||
status entry_status NULL DEFAULT 'unread':::entry_status,
|
||||
starred BOOL NULL DEFAULT false,
|
||||
comments_url STRING NULL DEFAULT '':::STRING,
|
||||
document_vectors TSVECTOR NULL,
|
||||
changed_at TIMESTAMPTZ NOT NULL,
|
||||
share_code STRING NOT NULL DEFAULT '':::STRING,
|
||||
reading_time INT8 NOT NULL DEFAULT 0:::INT8,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now():::TIMESTAMPTZ,
|
||||
tags STRING[] NULL DEFAULT ARRAY[]:::STRING[],
|
||||
CONSTRAINT entries_pkey PRIMARY KEY (id ASC),
|
||||
UNIQUE INDEX entries_feed_id_hash_key (feed_id ASC, hash ASC),
|
||||
INDEX entries_feed_idx (feed_id ASC),
|
||||
INDEX entries_user_status_idx (user_id ASC, status ASC),
|
||||
INVERTED INDEX document_vectors_idx (document_vectors),
|
||||
UNIQUE INDEX entries_share_code_idx (share_code ASC) WHERE share_code != '':::STRING,
|
||||
INDEX entries_user_feed_idx (user_id ASC, feed_id ASC),
|
||||
INDEX entries_id_user_status_idx (id ASC, user_id ASC, status ASC),
|
||||
INDEX entries_feed_id_status_hash_idx (feed_id ASC, status ASC, hash ASC),
|
||||
INDEX entries_user_id_status_starred_idx (user_id ASC, status ASC, starred ASC),
|
||||
INDEX entries_user_status_feed_idx (user_id ASC, status ASC, feed_id ASC),
|
||||
INDEX entries_user_status_changed_idx (user_id ASC, status ASC, changed_at ASC),
|
||||
INDEX entries_user_status_published_idx (user_id ASC, status ASC, published_at ASC),
|
||||
INDEX entries_user_status_created_idx (user_id ASC, status ASC, created_at ASC),
|
||||
INDEX entries_user_status_changed_published_idx (user_id ASC, status ASC, changed_at ASC, published_at ASC)
|
||||
);
|
||||
CREATE TABLE enclosures (
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
user_id INT8 NOT NULL,
|
||||
entry_id INT8 NOT NULL,
|
||||
url STRING NOT NULL,
|
||||
size INT8 NULL DEFAULT 0:::INT8,
|
||||
mime_type STRING NULL DEFAULT '':::STRING,
|
||||
media_progression INT8 NULL DEFAULT 0:::INT8,
|
||||
CONSTRAINT enclosures_pkey PRIMARY KEY (id ASC),
|
||||
UNIQUE INDEX enclosures_user_entry_url_unique_idx (user_id ASC, entry_id ASC, url ASC),
|
||||
INDEX enclosures_entry_id_idx (entry_id ASC)
|
||||
);
|
||||
CREATE TABLE icons (
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
hash STRING NOT NULL,
|
||||
mime_type STRING NOT NULL,
|
||||
content BYTES NOT NULL,
|
||||
external_id STRING NULL DEFAULT '':::STRING,
|
||||
CONSTRAINT icons_pkey PRIMARY KEY (id ASC),
|
||||
UNIQUE INDEX icons_hash_key (hash ASC),
|
||||
UNIQUE INDEX icons_external_id_idx (external_id ASC) WHERE external_id != '':::STRING
|
||||
);
|
||||
CREATE TABLE feed_icons (
|
||||
feed_id INT8 NOT NULL,
|
||||
icon_id INT8 NOT NULL,
|
||||
CONSTRAINT feed_icons_pkey PRIMARY KEY (feed_id ASC, icon_id ASC)
|
||||
);
|
||||
CREATE TABLE integrations (
|
||||
user_id INT8 NOT NULL,
|
||||
pinboard_enabled BOOL NULL DEFAULT false,
|
||||
pinboard_token STRING NULL DEFAULT '':::STRING,
|
||||
pinboard_tags STRING NULL DEFAULT 'miniflux':::STRING,
|
||||
pinboard_mark_as_unread BOOL NULL DEFAULT false,
|
||||
instapaper_enabled BOOL NULL DEFAULT false,
|
||||
instapaper_username STRING NULL DEFAULT '':::STRING,
|
||||
instapaper_password STRING NULL DEFAULT '':::STRING,
|
||||
fever_enabled BOOL NULL DEFAULT false,
|
||||
fever_username STRING NULL DEFAULT '':::STRING,
|
||||
fever_token STRING NULL DEFAULT '':::STRING,
|
||||
wallabag_enabled BOOL NULL DEFAULT false,
|
||||
wallabag_url STRING NULL DEFAULT '':::STRING,
|
||||
wallabag_client_id STRING NULL DEFAULT '':::STRING,
|
||||
wallabag_client_secret STRING NULL DEFAULT '':::STRING,
|
||||
wallabag_username STRING NULL DEFAULT '':::STRING,
|
||||
wallabag_password STRING NULL DEFAULT '':::STRING,
|
||||
nunux_keeper_enabled BOOL NULL DEFAULT false,
|
||||
nunux_keeper_url STRING NULL DEFAULT '':::STRING,
|
||||
nunux_keeper_api_key STRING NULL DEFAULT '':::STRING,
|
||||
telegram_bot_enabled BOOL NULL DEFAULT false,
|
||||
telegram_bot_token STRING NULL DEFAULT '':::STRING,
|
||||
telegram_bot_chat_id STRING NULL DEFAULT '':::STRING,
|
||||
googlereader_enabled BOOL NULL DEFAULT false,
|
||||
googlereader_username STRING NULL DEFAULT '':::STRING,
|
||||
googlereader_password STRING NULL DEFAULT '':::STRING,
|
||||
espial_enabled BOOL NULL DEFAULT false,
|
||||
espial_url STRING NULL DEFAULT '':::STRING,
|
||||
espial_api_key STRING NULL DEFAULT '':::STRING,
|
||||
espial_tags STRING NULL DEFAULT 'miniflux':::STRING,
|
||||
linkding_enabled BOOL NULL DEFAULT false,
|
||||
linkding_url STRING NULL DEFAULT '':::STRING,
|
||||
linkding_api_key STRING NULL DEFAULT '':::STRING,
|
||||
wallabag_only_url BOOL NULL DEFAULT false,
|
||||
matrix_bot_enabled BOOL NULL DEFAULT false,
|
||||
matrix_bot_user STRING NULL DEFAULT '':::STRING,
|
||||
matrix_bot_password STRING NULL DEFAULT '':::STRING,
|
||||
matrix_bot_url STRING NULL DEFAULT '':::STRING,
|
||||
matrix_bot_chat_id STRING NULL DEFAULT '':::STRING,
|
||||
linkding_tags STRING NULL DEFAULT '':::STRING,
|
||||
linkding_mark_as_unread BOOL NULL DEFAULT false,
|
||||
notion_enabled BOOL NULL DEFAULT false,
|
||||
notion_token STRING NULL DEFAULT '':::STRING,
|
||||
notion_page_id STRING NULL DEFAULT '':::STRING,
|
||||
readwise_enabled BOOL NULL DEFAULT false,
|
||||
readwise_api_key STRING NULL DEFAULT '':::STRING,
|
||||
apprise_enabled BOOL NULL DEFAULT false,
|
||||
apprise_url STRING NULL DEFAULT '':::STRING,
|
||||
apprise_services_url STRING NULL DEFAULT '':::STRING,
|
||||
shiori_enabled BOOL NULL DEFAULT false,
|
||||
shiori_url STRING NULL DEFAULT '':::STRING,
|
||||
shiori_username STRING NULL DEFAULT '':::STRING,
|
||||
shiori_password STRING NULL DEFAULT '':::STRING,
|
||||
shaarli_enabled BOOL NULL DEFAULT false,
|
||||
shaarli_url STRING NULL DEFAULT '':::STRING,
|
||||
shaarli_api_secret STRING NULL DEFAULT '':::STRING,
|
||||
webhook_enabled BOOL NULL DEFAULT false,
|
||||
webhook_url STRING NULL DEFAULT '':::STRING,
|
||||
webhook_secret STRING NULL DEFAULT '':::STRING,
|
||||
telegram_bot_topic_id INT8 NULL,
|
||||
telegram_bot_disable_web_page_preview BOOL NULL DEFAULT false,
|
||||
telegram_bot_disable_notification BOOL NULL DEFAULT false,
|
||||
telegram_bot_disable_buttons BOOL NULL DEFAULT false,
|
||||
rssbridge_enabled BOOL NULL DEFAULT false,
|
||||
rssbridge_url STRING NULL DEFAULT '':::STRING,
|
||||
omnivore_enabled BOOL NULL DEFAULT false,
|
||||
omnivore_api_key STRING NULL DEFAULT '':::STRING,
|
||||
omnivore_url STRING NULL DEFAULT '':::STRING,
|
||||
linkace_enabled BOOL NULL DEFAULT false,
|
||||
linkace_url STRING NULL DEFAULT '':::STRING,
|
||||
linkace_api_key STRING NULL DEFAULT '':::STRING,
|
||||
linkace_tags STRING NULL DEFAULT '':::STRING,
|
||||
linkace_is_private BOOL NULL DEFAULT true,
|
||||
linkace_check_disabled BOOL NULL DEFAULT true,
|
||||
linkwarden_enabled BOOL NULL DEFAULT false,
|
||||
linkwarden_url STRING NULL DEFAULT '':::STRING,
|
||||
linkwarden_api_key STRING NULL DEFAULT '':::STRING,
|
||||
readeck_enabled BOOL NULL DEFAULT false,
|
||||
readeck_only_url BOOL NULL DEFAULT false,
|
||||
readeck_url STRING NULL DEFAULT '':::STRING,
|
||||
readeck_api_key STRING NULL DEFAULT '':::STRING,
|
||||
readeck_labels STRING NULL DEFAULT '':::STRING,
|
||||
raindrop_enabled BOOL NULL DEFAULT false,
|
||||
raindrop_token STRING NULL DEFAULT '':::STRING,
|
||||
raindrop_collection_id STRING NULL DEFAULT '':::STRING,
|
||||
raindrop_tags STRING NULL DEFAULT '':::STRING,
|
||||
betula_url STRING NULL DEFAULT '':::STRING,
|
||||
betula_token STRING NULL DEFAULT '':::STRING,
|
||||
betula_enabled BOOL NULL DEFAULT false,
|
||||
ntfy_enabled BOOL NULL DEFAULT false,
|
||||
ntfy_url STRING NULL DEFAULT '':::STRING,
|
||||
ntfy_topic STRING NULL DEFAULT '':::STRING,
|
||||
ntfy_api_token STRING NULL DEFAULT '':::STRING,
|
||||
ntfy_username STRING NULL DEFAULT '':::STRING,
|
||||
ntfy_password STRING NULL DEFAULT '':::STRING,
|
||||
ntfy_icon_url STRING NULL DEFAULT '':::STRING,
|
||||
cubox_enabled BOOL NULL DEFAULT false,
|
||||
cubox_api_link STRING NULL DEFAULT '':::STRING,
|
||||
discord_enabled BOOL NULL DEFAULT false,
|
||||
discord_webhook_link STRING NULL DEFAULT '':::STRING,
|
||||
ntfy_internal_links BOOL NULL DEFAULT false,
|
||||
slack_enabled BOOL NULL DEFAULT false,
|
||||
slack_webhook_link STRING NULL DEFAULT '':::STRING,
|
||||
pushover_enabled BOOL NULL DEFAULT false,
|
||||
pushover_user STRING NULL DEFAULT '':::STRING,
|
||||
pushover_token STRING NULL DEFAULT '':::STRING,
|
||||
pushover_device STRING NULL DEFAULT '':::STRING,
|
||||
pushover_prefix STRING NULL DEFAULT '':::STRING,
|
||||
rssbridge_token STRING NULL DEFAULT '':::STRING,
|
||||
karakeep_enabled BOOL NULL DEFAULT false,
|
||||
karakeep_api_key STRING NULL DEFAULT '':::STRING,
|
||||
karakeep_url STRING NULL DEFAULT '':::STRING,
|
||||
CONSTRAINT integrations_pkey PRIMARY KEY (user_id ASC)
|
||||
);
|
||||
CREATE TABLE sessions (
|
||||
id STRING NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now():::TIMESTAMPTZ,
|
||||
CONSTRAINT sessions_pkey PRIMARY KEY (id ASC)
|
||||
);
|
||||
CREATE TABLE api_keys (
|
||||
id INT8 NOT NULL DEFAULT unique_rowid(),
|
||||
user_id INT8 NOT NULL,
|
||||
token STRING NOT NULL,
|
||||
description STRING NOT NULL,
|
||||
last_used_at TIMESTAMPTZ NULL,
|
||||
created_at TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||
CONSTRAINT api_keys_pkey PRIMARY KEY (id ASC),
|
||||
UNIQUE INDEX api_keys_token_key (token ASC),
|
||||
UNIQUE INDEX api_keys_user_id_description_key (user_id ASC, description ASC)
|
||||
);
|
||||
CREATE TABLE acme_cache (
|
||||
key VARCHAR(400) NOT NULL,
|
||||
data BYTES NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
CONSTRAINT acme_cache_pkey PRIMARY KEY (key ASC)
|
||||
);
|
||||
CREATE TABLE webauthn_credentials (
|
||||
handle BYTES NOT NULL,
|
||||
cred_id BYTES NOT NULL,
|
||||
user_id INT8 NOT NULL,
|
||||
key BYTES NOT NULL,
|
||||
attestation_type VARCHAR(255) NOT NULL,
|
||||
aaguid BYTES NULL,
|
||||
sign_count INT8 NULL,
|
||||
clone_warning BOOL NULL,
|
||||
name STRING NULL,
|
||||
added_on TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||
last_seen_on TIMESTAMPTZ NULL DEFAULT now():::TIMESTAMPTZ,
|
||||
CONSTRAINT webauthn_credentials_pkey PRIMARY KEY (handle ASC),
|
||||
UNIQUE INDEX webauthn_credentials_cred_id_key (cred_id ASC)
|
||||
);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE user_sessions ADD CONSTRAINT sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ALTER TABLE categories ADD CONSTRAINT categories_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ALTER TABLE feeds ADD CONSTRAINT feeds_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ALTER TABLE feeds ADD CONSTRAINT feeds_category_id_fkey FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE;
|
||||
ALTER TABLE entries ADD CONSTRAINT entries_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ALTER TABLE entries ADD CONSTRAINT entries_feed_id_fkey FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE;
|
||||
ALTER TABLE enclosures ADD CONSTRAINT enclosures_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ALTER TABLE enclosures ADD CONSTRAINT enclosures_entry_id_fkey FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE;
|
||||
ALTER TABLE feed_icons ADD CONSTRAINT feed_icons_feed_id_fkey FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE;
|
||||
ALTER TABLE feed_icons ADD CONSTRAINT feed_icons_icon_id_fkey FOREIGN KEY (icon_id) REFERENCES icons(id) ON DELETE CASCADE;
|
||||
ALTER TABLE api_keys ADD CONSTRAINT api_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ALTER TABLE webauthn_credentials ADD CONSTRAINT webauthn_credentials_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; `
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE user_sessions VALIDATE CONSTRAINT sessions_user_id_fkey;
|
||||
ALTER TABLE categories VALIDATE CONSTRAINT categories_user_id_fkey;
|
||||
ALTER TABLE feeds VALIDATE CONSTRAINT feeds_user_id_fkey;
|
||||
ALTER TABLE feeds VALIDATE CONSTRAINT feeds_category_id_fkey;
|
||||
ALTER TABLE entries VALIDATE CONSTRAINT entries_user_id_fkey;
|
||||
ALTER TABLE entries VALIDATE CONSTRAINT entries_feed_id_fkey;
|
||||
ALTER TABLE enclosures VALIDATE CONSTRAINT enclosures_user_id_fkey;
|
||||
ALTER TABLE enclosures VALIDATE CONSTRAINT enclosures_entry_id_fkey;
|
||||
ALTER TABLE feed_icons VALIDATE CONSTRAINT feed_icons_feed_id_fkey;
|
||||
ALTER TABLE feed_icons VALIDATE CONSTRAINT feed_icons_icon_id_fkey;
|
||||
ALTER TABLE api_keys VALIDATE CONSTRAINT api_keys_user_id_fkey;
|
||||
ALTER TABLE webauthn_credentials VALIDATE CONSTRAINT webauthn_credentials_user_id_fkey;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
}
|
|
@ -7,13 +7,68 @@ import (
|
|||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DBKind int
|
||||
|
||||
const (
|
||||
DBKindPostgres DBKind = iota
|
||||
DBKindCockroach
|
||||
DBKindSqlite
|
||||
)
|
||||
|
||||
var dbKindProto = map[DBKind]string{
|
||||
DBKindPostgres: "postgresql",
|
||||
DBKindCockroach: "cockroachdb",
|
||||
DBKindSqlite: "sqlite",
|
||||
}
|
||||
|
||||
var dbKindDriver = map[DBKind]string{
|
||||
DBKindPostgres: "postgres",
|
||||
DBKindCockroach: "postgres",
|
||||
DBKindSqlite: "sqlite",
|
||||
}
|
||||
|
||||
func DetectKind(conn string) (DBKind, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(conn, "postgres"),
|
||||
strings.HasPrefix(conn, "postgresql"):
|
||||
return DBKindPostgres, nil
|
||||
case strings.HasPrefix(conn, "cockroach"),
|
||||
strings.HasPrefix(conn, "cockroachdb"):
|
||||
return DBKindCockroach, nil
|
||||
case strings.HasPrefix(conn, "file"),
|
||||
strings.HasPrefix(conn, "sqlite"):
|
||||
return DBKindSqlite, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown db kind in conn string: %q", conn)
|
||||
}
|
||||
}
|
||||
|
||||
type Migration func(*sql.Tx) error
|
||||
|
||||
var dbKindMigrations = map[DBKind][]Migration{
|
||||
DBKindPostgres: postgresMigrations,
|
||||
DBKindCockroach: cockroachMigrations,
|
||||
DBKindSqlite: sqliteMigrations,
|
||||
}
|
||||
|
||||
var dbKindSchemaVersion = map[DBKind]int{
|
||||
DBKindPostgres: postgresSchemaVersion,
|
||||
DBKindCockroach: cockroachSchemaVersion,
|
||||
DBKindSqlite: sqliteSchemaVersion,
|
||||
}
|
||||
|
||||
// Migrate executes database migrations.
|
||||
func Migrate(db *sql.DB) error {
|
||||
func Migrate(kind DBKind, db *sql.DB) error {
|
||||
var currentVersion int
|
||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||
|
||||
migrations := dbKindMigrations[kind]
|
||||
schemaVersion := dbKindSchemaVersion[kind]
|
||||
|
||||
slog.Info("Running database migrations",
|
||||
slog.Int("current_version", currentVersion),
|
||||
slog.Int("latest_version", schemaVersion),
|
||||
|
@ -32,14 +87,24 @@ func Migrate(db *sql.DB) error {
|
|||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`TRUNCATE schema_version`); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES ($1)`, newVersion); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
if kind == DBKindSqlite {
|
||||
if _, err := tx.Exec(`DELETE FROM schema_version`); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES (?)`, newVersion); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
} else {
|
||||
if _, err := tx.Exec(`TRUNCATE schema_version`); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES ($1)`, newVersion); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
|
@ -51,7 +116,9 @@ func Migrate(db *sql.DB) error {
|
|||
}
|
||||
|
||||
// IsSchemaUpToDate checks if the database schema is up to date.
|
||||
func IsSchemaUpToDate(db *sql.DB) error {
|
||||
func IsSchemaUpToDate(kind DBKind, db *sql.DB) error {
|
||||
schemaVersion := dbKindSchemaVersion[kind]
|
||||
|
||||
var currentVersion int
|
||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||
if currentVersion < schemaVersion {
|
||||
|
@ -59,3 +126,25 @@ func IsSchemaUpToDate(db *sql.DB) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewConnectionPool(kind DBKind, dsn string, minConnections, maxConnections int, connectionLifetime time.Duration) (*sql.DB, error) {
|
||||
driver := dbKindDriver[kind]
|
||||
|
||||
// replace cockroachdb protocol with postgres
|
||||
// we use cockroachdb protocol to detect cockroachdb but go wants postgres
|
||||
if kind == DBKindCockroach {
|
||||
split := strings.SplitN(dsn, ":", 2)
|
||||
dsn = fmt.Sprintf("postgres:%s", split[1])
|
||||
}
|
||||
|
||||
db, err := sql.Open(driver, dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(maxConnections)
|
||||
db.SetMaxIdleConns(minConnections)
|
||||
db.SetConnMaxLifetime(connectionLifetime)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
378
internal/database/sqlite.go
Normal file
378
internal/database/sqlite.go
Normal file
|
@ -0,0 +1,378 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package database // import "miniflux.app/v2/internal/database"
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var sqliteSchemaVersion = len(sqliteMigrations)
|
||||
|
||||
// Order is important. Add new migrations at the end of the list.
|
||||
var sqliteMigrations = []Migration{
|
||||
func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE schema_version (
|
||||
version INTEGER NOT NULL
|
||||
);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
language TEXT NOT NULL DEFAULT 'en_US',
|
||||
timezone TEXT NOT NULL DEFAULT 'UTC',
|
||||
theme TEXT NOT NULL DEFAULT 'light_serif',
|
||||
last_login_at DATETIME,
|
||||
entry_direction TEXT NOT NULL DEFAULT 'asc' CHECK (entry_direction IN ('asc','desc')),
|
||||
keyboard_shortcuts INTEGER NOT NULL DEFAULT 1,
|
||||
entries_per_page INTEGER NOT NULL DEFAULT 100,
|
||||
show_reading_time INTEGER NOT NULL DEFAULT 1,
|
||||
entry_swipe INTEGER NOT NULL DEFAULT 1,
|
||||
stylesheet TEXT NOT NULL DEFAULT '',
|
||||
google_id TEXT NOT NULL DEFAULT '',
|
||||
openid_connect_id TEXT NOT NULL DEFAULT '',
|
||||
display_mode TEXT NOT NULL DEFAULT 'standalone' CHECK (display_mode IN ('fullscreen','standalone','minimal-ui','browser')),
|
||||
entry_order TEXT NOT NULL DEFAULT 'published_at' CHECK (entry_order IN ('published_at','created_at')),
|
||||
default_reading_speed INTEGER NOT NULL DEFAULT 265,
|
||||
cjk_reading_speed INTEGER NOT NULL DEFAULT 500,
|
||||
default_home_page TEXT NOT NULL DEFAULT 'unread',
|
||||
categories_sorting_order TEXT NOT NULL DEFAULT 'unread_count',
|
||||
gesture_nav TEXT NOT NULL DEFAULT 'tap',
|
||||
mark_read_on_view INTEGER NOT NULL DEFAULT 1,
|
||||
media_playback_rate REAL NOT NULL DEFAULT 1.0,
|
||||
block_filter_entry_rules TEXT NOT NULL DEFAULT '',
|
||||
keep_filter_entry_rules TEXT NOT NULL DEFAULT '',
|
||||
mark_read_on_media_player_completion INTEGER NOT NULL DEFAULT 0,
|
||||
custom_js TEXT NOT NULL DEFAULT '',
|
||||
external_font_hosts TEXT NOT NULL DEFAULT '',
|
||||
always_open_external_links INTEGER NOT NULL DEFAULT 0,
|
||||
open_external_links_in_new_tab INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
CREATE TABLE user_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||
user_agent TEXT,
|
||||
ip TEXT,
|
||||
UNIQUE(user_id, token),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
hide_globally INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(user_id, title),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE feeds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
feed_url TEXT NOT NULL,
|
||||
site_url TEXT NOT NULL,
|
||||
checked_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||
etag_header TEXT NOT NULL DEFAULT '',
|
||||
last_modified_header TEXT NOT NULL DEFAULT '',
|
||||
parsing_error_msg TEXT NOT NULL DEFAULT '',
|
||||
parsing_error_count INTEGER NOT NULL DEFAULT 0,
|
||||
scraper_rules TEXT NOT NULL DEFAULT '',
|
||||
rewrite_rules TEXT NOT NULL DEFAULT '',
|
||||
crawler INTEGER NOT NULL DEFAULT 0,
|
||||
username TEXT NOT NULL DEFAULT '',
|
||||
password TEXT NOT NULL DEFAULT '',
|
||||
user_agent TEXT NOT NULL DEFAULT '',
|
||||
disabled INTEGER NOT NULL DEFAULT 0,
|
||||
next_check_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||
ignore_http_cache INTEGER NOT NULL DEFAULT 0,
|
||||
fetch_via_proxy INTEGER NOT NULL DEFAULT 0,
|
||||
blocklist_rules TEXT NOT NULL DEFAULT '',
|
||||
keeplist_rules TEXT NOT NULL DEFAULT '',
|
||||
allow_self_signed_certificates INTEGER NOT NULL DEFAULT 0,
|
||||
cookie TEXT NOT NULL DEFAULT '',
|
||||
hide_globally INTEGER NOT NULL DEFAULT 0,
|
||||
url_rewrite_rules TEXT NOT NULL DEFAULT '',
|
||||
no_media_player INTEGER NOT NULL DEFAULT 0,
|
||||
apprise_service_urls TEXT NOT NULL DEFAULT '',
|
||||
disable_http2 INTEGER NOT NULL DEFAULT 0,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
ntfy_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
ntfy_priority INTEGER NOT NULL DEFAULT 3,
|
||||
webhook_url TEXT NOT NULL DEFAULT '',
|
||||
pushover_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
pushover_priority INTEGER NOT NULL DEFAULT 0,
|
||||
ntfy_topic TEXT NOT NULL DEFAULT '',
|
||||
proxy_url TEXT NOT NULL DEFAULT '',
|
||||
block_filter_entry_rules TEXT NOT NULL DEFAULT '',
|
||||
keep_filter_entry_rules TEXT NOT NULL DEFAULT '',
|
||||
UNIQUE(user_id, feed_url),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
feed_id INTEGER NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
published_at DATETIME NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
author TEXT,
|
||||
content TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'unread' CHECK (status IN ('unread','read','removed')),
|
||||
starred INTEGER NOT NULL DEFAULT 0,
|
||||
comments_url TEXT NOT NULL DEFAULT '',
|
||||
changed_at DATETIME NOT NULL,
|
||||
share_code TEXT NOT NULL DEFAULT '',
|
||||
reading_time INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||
tags TEXT NOT NULL DEFAULT '', -- TODO: adapt code (comma or JSON)
|
||||
UNIQUE(feed_id, hash),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE enclosures (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
entry_id INTEGER NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
mime_type TEXT NOT NULL DEFAULT '',
|
||||
media_progression INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(user_id, entry_id, url),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE icons (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hash TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
content BLOB NOT NULL,
|
||||
external_id TEXT NOT NULL DEFAULT '',
|
||||
UNIQUE(hash)
|
||||
);
|
||||
CREATE TABLE feed_icons (
|
||||
feed_id INTEGER NOT NULL,
|
||||
icon_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (feed_id, icon_id),
|
||||
FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (icon_id) REFERENCES icons(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE integrations (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
pinboard_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
pinboard_token TEXT NOT NULL DEFAULT '',
|
||||
pinboard_tags TEXT NOT NULL DEFAULT 'miniflux',
|
||||
pinboard_mark_as_unread INTEGER NOT NULL DEFAULT 0,
|
||||
instapaper_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
instapaper_username TEXT NOT NULL DEFAULT '',
|
||||
instapaper_password TEXT NOT NULL DEFAULT '',
|
||||
fever_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
fever_username TEXT NOT NULL DEFAULT '',
|
||||
fever_token TEXT NOT NULL DEFAULT '',
|
||||
wallabag_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
wallabag_url TEXT NOT NULL DEFAULT '',
|
||||
wallabag_client_id TEXT NOT NULL DEFAULT '',
|
||||
wallabag_client_secret TEXT NOT NULL DEFAULT '',
|
||||
wallabag_username TEXT NOT NULL DEFAULT '',
|
||||
wallabag_password TEXT NOT NULL DEFAULT '',
|
||||
nunux_keeper_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
nunux_keeper_url TEXT NOT NULL DEFAULT '',
|
||||
nunux_keeper_api_key TEXT NOT NULL DEFAULT '',
|
||||
telegram_bot_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
telegram_bot_token TEXT NOT NULL DEFAULT '',
|
||||
telegram_bot_chat_id TEXT NOT NULL DEFAULT '',
|
||||
googlereader_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
googlereader_username TEXT NOT NULL DEFAULT '',
|
||||
googlereader_password TEXT NOT NULL DEFAULT '',
|
||||
espial_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
espial_url TEXT NOT NULL DEFAULT '',
|
||||
espial_api_key TEXT NOT NULL DEFAULT '',
|
||||
espial_tags TEXT NOT NULL DEFAULT 'miniflux',
|
||||
linkding_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
linkding_url TEXT NOT NULL DEFAULT '',
|
||||
linkding_api_key TEXT NOT NULL DEFAULT '',
|
||||
wallabag_only_url INTEGER NOT NULL DEFAULT 0,
|
||||
matrix_bot_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
matrix_bot_user TEXT NOT NULL DEFAULT '',
|
||||
matrix_bot_password TEXT NOT NULL DEFAULT '',
|
||||
matrix_bot_url TEXT NOT NULL DEFAULT '',
|
||||
matrix_bot_chat_id TEXT NOT NULL DEFAULT '',
|
||||
linkding_tags TEXT NOT NULL DEFAULT '',
|
||||
linkding_mark_as_unread INTEGER NOT NULL DEFAULT 0,
|
||||
notion_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
notion_token TEXT NOT NULL DEFAULT '',
|
||||
notion_page_id TEXT NOT NULL DEFAULT '',
|
||||
readwise_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
readwise_api_key TEXT NOT NULL DEFAULT '',
|
||||
apprise_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
apprise_url TEXT NOT NULL DEFAULT '',
|
||||
apprise_services_url TEXT NOT NULL DEFAULT '',
|
||||
shiori_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
shiori_url TEXT NOT NULL DEFAULT '',
|
||||
shiori_username TEXT NOT NULL DEFAULT '',
|
||||
shiori_password TEXT NOT NULL DEFAULT '',
|
||||
shaarli_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
shaarli_url TEXT NOT NULL DEFAULT '',
|
||||
shaarli_api_secret TEXT NOT NULL DEFAULT '',
|
||||
webhook_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
webhook_url TEXT NOT NULL DEFAULT '',
|
||||
webhook_secret TEXT NOT NULL DEFAULT '',
|
||||
telegram_bot_topic_id INTEGER,
|
||||
telegram_bot_disable_web_page_preview INTEGER NOT NULL DEFAULT 0,
|
||||
telegram_bot_disable_notification INTEGER NOT NULL DEFAULT 0,
|
||||
telegram_bot_disable_buttons INTEGER NOT NULL DEFAULT 0,
|
||||
rssbridge_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
rssbridge_url TEXT NOT NULL DEFAULT '',
|
||||
omnivore_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
omnivore_api_key TEXT NOT NULL DEFAULT '',
|
||||
omnivore_url TEXT NOT NULL DEFAULT '',
|
||||
linkace_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
linkace_url TEXT NOT NULL DEFAULT '',
|
||||
linkace_api_key TEXT NOT NULL DEFAULT '',
|
||||
linkace_tags TEXT NOT NULL DEFAULT '',
|
||||
linkace_is_private INTEGER NOT NULL DEFAULT 1,
|
||||
linkace_check_disabled INTEGER NOT NULL DEFAULT 1,
|
||||
linkwarden_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
linkwarden_url TEXT NOT NULL DEFAULT '',
|
||||
linkwarden_api_key TEXT NOT NULL DEFAULT '',
|
||||
readeck_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
readeck_only_url INTEGER NOT NULL DEFAULT 0,
|
||||
readeck_url TEXT NOT NULL DEFAULT '',
|
||||
readeck_api_key TEXT NOT NULL DEFAULT '',
|
||||
readeck_labels TEXT NOT NULL DEFAULT '',
|
||||
raindrop_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
raindrop_token TEXT NOT NULL DEFAULT '',
|
||||
raindrop_collection_id TEXT NOT NULL DEFAULT '',
|
||||
raindrop_tags TEXT NOT NULL DEFAULT '',
|
||||
betula_url TEXT NOT NULL DEFAULT '',
|
||||
betula_token TEXT NOT NULL DEFAULT '',
|
||||
betula_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
ntfy_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
ntfy_url TEXT NOT NULL DEFAULT '',
|
||||
ntfy_topic TEXT NOT NULL DEFAULT '',
|
||||
ntfy_api_token TEXT NOT NULL DEFAULT '',
|
||||
ntfy_username TEXT NOT NULL DEFAULT '',
|
||||
ntfy_password TEXT NOT NULL DEFAULT '',
|
||||
ntfy_icon_url TEXT NOT NULL DEFAULT '',
|
||||
cubox_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
cubox_api_link TEXT NOT NULL DEFAULT '',
|
||||
discord_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
discord_webhook_link TEXT NOT NULL DEFAULT '',
|
||||
ntfy_internal_links INTEGER NOT NULL DEFAULT 0,
|
||||
slack_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
slack_webhook_link TEXT NOT NULL DEFAULT '',
|
||||
pushover_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
pushover_user TEXT NOT NULL DEFAULT '',
|
||||
pushover_token TEXT NOT NULL DEFAULT '',
|
||||
pushover_device TEXT NOT NULL DEFAULT '',
|
||||
pushover_prefix TEXT NOT NULL DEFAULT '',
|
||||
rssbridge_token TEXT NOT NULL DEFAULT '',
|
||||
karakeep_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
karakeep_api_key TEXT NOT NULL DEFAULT '',
|
||||
karakeep_url TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT (DATETIME('now'))
|
||||
);
|
||||
CREATE TABLE api_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL,
|
||||
last_used_at DATETIME,
|
||||
created_at DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||
UNIQUE(user_id, description),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE acme_cache (
|
||||
key TEXT PRIMARY KEY,
|
||||
data BLOB NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
);
|
||||
CREATE TABLE webauthn_credentials (
|
||||
handle BLOB PRIMARY KEY,
|
||||
cred_id BLOB NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
key BLOB NOT NULL,
|
||||
attestation_type TEXT NOT NULL,
|
||||
aaguid BLOB,
|
||||
sign_count INTEGER,
|
||||
clone_warning INTEGER,
|
||||
name TEXT,
|
||||
added_on DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||
last_seen_on DATETIME NOT NULL DEFAULT (DATETIME('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE UNIQUE INDEX icons_external_id_uq ON icons(external_id) WHERE external_id != '';
|
||||
CREATE UNIQUE INDEX users_google_id_uq ON users(google_id) WHERE google_id != '';
|
||||
CREATE UNIQUE INDEX users_openid_connect_id_uq ON users(openid_connect_id) WHERE openid_connect_id != '';
|
||||
CREATE UNIQUE INDEX entries_share_code_uq ON entries(share_code) WHERE share_code != '';
|
||||
|
||||
CREATE INDEX feeds_user_category_idx ON feeds(user_id, category_id);
|
||||
CREATE INDEX feeds_id_hide_globally_idx ON feeds(id, hide_globally);
|
||||
CREATE INDEX entries_feed_idx ON entries(feed_id);
|
||||
CREATE INDEX entries_user_status_idx ON entries(user_id, status);
|
||||
CREATE INDEX entries_user_feed_idx ON entries(user_id, feed_id);
|
||||
CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
|
||||
CREATE INDEX entries_user_status_published_idx ON entries(user_id, status, published_at);
|
||||
CREATE INDEX entries_user_status_created_idx ON entries(user_id, status, created_at);
|
||||
CREATE INDEX entries_id_user_status_idx ON entries(id, user_id, status);
|
||||
CREATE INDEX entries_feed_id_status_hash_idx ON entries(feed_id, status, hash);
|
||||
CREATE INDEX entries_user_id_status_starred_idx ON entries(user_id, status, starred);
|
||||
CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
|
||||
CREATE INDEX entries_user_status_changed_published_idx ON entries(user_id, status, changed_at, published_at);
|
||||
CREATE INDEX enclosures_entry_id_idx ON enclosures(entry_id);
|
||||
CREATE INDEX feed_icons_icon_id_idx ON feed_icons(icon_id);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE VIRTUAL TABLE entries_fts USING fts5(
|
||||
title,
|
||||
content,
|
||||
tags,
|
||||
entry_id UNINDEXED,
|
||||
content='',
|
||||
-- as close to PostgreSQL as possible while being multilingual
|
||||
tokenize = "unicode61 remove_diacritics 2 tokenchars '-_'"
|
||||
);
|
||||
|
||||
CREATE TRIGGER entries_ai AFTER INSERT ON entries BEGIN
|
||||
INSERT INTO entries_fts(rowid,title,content,tags,entry_id)
|
||||
VALUES (new.id,new.title,COALESCE(new.content,''),COALESCE(new.tags,''),new.id);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER entries_au AFTER UPDATE ON entries BEGIN
|
||||
INSERT INTO entries_fts(entries_fts,rowid) VALUES('delete', old.id);
|
||||
INSERT INTO entries_fts(rowid,title,content,tags,entry_id)
|
||||
VALUES (new.id,new.title,COALESCE(new.content,''),COALESCE(new.tags,''),new.id);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER entries_ad AFTER DELETE ON entries BEGIN
|
||||
INSERT INTO entries_fts(entries_fts,rowid) VALUES('delete', old.id);
|
||||
END;
|
||||
`)
|
||||
return err
|
||||
},
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/v2/internal/database"
|
||||
"miniflux.app/v2/internal/model"
|
||||
|
||||
"github.com/lib/pq"
|
||||
|
@ -49,7 +50,6 @@ func (s *Storage) GetEnclosures(entryID int64) (model.EnclosureList, error) {
|
|||
&enclosure.MimeType,
|
||||
&enclosure.MediaProgression,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`store: unable to fetch enclosure row: %v`, err)
|
||||
}
|
||||
|
@ -150,15 +150,20 @@ func (s *Storage) createEnclosure(tx *sql.Tx, enclosure *model.Enclosure) error
|
|||
return nil
|
||||
}
|
||||
|
||||
query := `
|
||||
urlPart := "md5(url)"
|
||||
if s.kind == database.DBKindCockroach {
|
||||
urlPart = "url"
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
INSERT INTO enclosures
|
||||
(url, size, mime_type, entry_id, user_id, media_progression)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (user_id, entry_id, md5(url)) DO NOTHING
|
||||
ON CONFLICT (user_id, entry_id, %s) DO NOTHING
|
||||
RETURNING
|
||||
id
|
||||
`
|
||||
`, urlPart)
|
||||
if err := tx.QueryRow(
|
||||
query,
|
||||
enclosureURL,
|
||||
|
@ -226,7 +231,6 @@ func (s *Storage) UpdateEnclosure(enclosure *model.Enclosure) error {
|
|||
enclosure.MediaProgression,
|
||||
enclosure.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(`store: unable to update enclosure #%d : %v`, enclosure.ID, err)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
"miniflux.app/v2/internal/database"
|
||||
"miniflux.app/v2/internal/model"
|
||||
|
||||
"github.com/lib/pq"
|
||||
|
@ -70,17 +71,21 @@ func (s *Storage) NewEntryQueryBuilder(userID int64) *EntryQueryBuilder {
|
|||
// UpdateEntryTitleAndContent updates entry title and content.
|
||||
func (s *Storage) UpdateEntryTitleAndContent(entry *model.Entry) error {
|
||||
truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
|
||||
query := `
|
||||
setweightPart := "setweight(to_tsvector($4), 'A') || setweight(to_tsvector($5), 'B')"
|
||||
if s.kind == database.DBKindCockroach {
|
||||
setweightPart = "to_tsvector(substring($4 || ' ' || $4 || ' ' || coalesce($5, '') for 1000000))"
|
||||
}
|
||||
query := fmt.Sprintf(`
|
||||
UPDATE
|
||||
entries
|
||||
SET
|
||||
title=$1,
|
||||
content=$2,
|
||||
reading_time=$3,
|
||||
document_vectors = setweight(to_tsvector($4), 'A') || setweight(to_tsvector($5), 'B')
|
||||
document_vectors = %s
|
||||
WHERE
|
||||
id=$6 AND user_id=$7
|
||||
`
|
||||
`, setweightPart)
|
||||
|
||||
if _, err := s.db.Exec(
|
||||
query,
|
||||
|
@ -100,7 +105,11 @@ func (s *Storage) UpdateEntryTitleAndContent(entry *model.Entry) error {
|
|||
// createEntry add a new entry.
|
||||
func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||
truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
|
||||
query := `
|
||||
setweightPart := "setweight(to_tsvector($11), 'A') || setweight(to_tsvector($12), 'B')"
|
||||
if s.kind == database.DBKindCockroach {
|
||||
setweightPart = "to_tsvector(substring($11 || ' ' || $11 || ' ' || coalesce($12, '') for 1000000))"
|
||||
}
|
||||
query := fmt.Sprintf(`
|
||||
INSERT INTO entries
|
||||
(
|
||||
title,
|
||||
|
@ -130,12 +139,12 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
|||
$9,
|
||||
$10,
|
||||
now(),
|
||||
setweight(to_tsvector($11), 'A') || setweight(to_tsvector($12), 'B'),
|
||||
%s,
|
||||
$13
|
||||
)
|
||||
RETURNING
|
||||
id, status, created_at, changed_at
|
||||
`
|
||||
`, setweightPart)
|
||||
err := tx.QueryRow(
|
||||
query,
|
||||
entry.Title,
|
||||
|
@ -178,7 +187,11 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
|||
// it default to time.Now() which could change the order of items on the history page.
|
||||
func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
|
||||
truncatedTitle, truncatedContent := truncateTitleAndContentForTSVectorField(entry.Title, entry.Content)
|
||||
query := `
|
||||
setweightPart := "setweight(to_tsvector($7), 'A') || setweight(to_tsvector($8), 'B')"
|
||||
if s.kind == database.DBKindCockroach {
|
||||
setweightPart = "to_tsvector(substring($7 || ' ' || $7 || ' ' || coalesce($8, '') for 1000000))"
|
||||
}
|
||||
query := fmt.Sprintf(`
|
||||
UPDATE
|
||||
entries
|
||||
SET
|
||||
|
@ -188,13 +201,13 @@ func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
|
|||
content=$4,
|
||||
author=$5,
|
||||
reading_time=$6,
|
||||
document_vectors = setweight(to_tsvector($7), 'A') || setweight(to_tsvector($8), 'B'),
|
||||
document_vectors = %s,
|
||||
tags=$12
|
||||
WHERE
|
||||
user_id=$9 AND feed_id=$10 AND hash=$11
|
||||
RETURNING
|
||||
id
|
||||
`
|
||||
`, setweightPart)
|
||||
err := tx.QueryRow(
|
||||
query,
|
||||
entry.Title,
|
||||
|
|
|
@ -7,16 +7,19 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/database"
|
||||
)
|
||||
|
||||
// Storage handles all operations related to the database.
|
||||
type Storage struct {
|
||||
db *sql.DB
|
||||
kind database.DBKind
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewStorage returns a new Storage.
|
||||
func NewStorage(db *sql.DB) *Storage {
|
||||
return &Storage{db}
|
||||
func NewStorage(kind database.DBKind, db *sql.DB) *Storage {
|
||||
return &Storage{kind, db}
|
||||
}
|
||||
|
||||
// DatabaseVersion returns the version of the database which is in use.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue