mirror of
https://github.com/miniflux/v2.git
synced 2025-09-15 18:57:04 +00:00
Merge fd9072d187
into 87e65f800e
This commit is contained in:
commit
f43708732f
12 changed files with 1666 additions and 121 deletions
2
.envrc
Normal file
2
.envrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
use flake . --show-trace
|
||||
dotenv_if_exists .env
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@
|
|||
*.deb
|
||||
*.rpm
|
||||
miniflux-*
|
||||
.direnv/
|
||||
|
|
44
.helix/languages.toml
Normal file
44
.helix/languages.toml
Normal file
|
@ -0,0 +1,44 @@
|
|||
[language-server]
|
||||
nil = { command = "nil" }
|
||||
taplo = { command = "taplo", args = ["lsp", "stdio"] }
|
||||
yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
|
||||
marksman = { command = "marksman", args = ["server"] }
|
||||
vscode-json-language-server = { command = "vscode-json-language-server", args = [
|
||||
"--stdio",
|
||||
], config = { json = { validate = { enable = true } } } }
|
||||
gopls = { command = "gopls", config = { staticcheck = true } }
|
||||
|
||||
[[language]]
|
||||
name = "nix"
|
||||
auto-format = true
|
||||
formatter = { command = "nixpkgs-fmt" }
|
||||
language-servers = ["nil"]
|
||||
|
||||
[[language]]
|
||||
name = "toml"
|
||||
auto-format = true
|
||||
language-servers = ["taplo"]
|
||||
|
||||
[[language]]
|
||||
name = "yaml"
|
||||
auto-format = true
|
||||
formatter = { command = "prettier", args = ["--parser", "yaml"] }
|
||||
language-servers = ["yaml-language-server"]
|
||||
|
||||
[[language]]
|
||||
name = "json"
|
||||
auto-format = true
|
||||
formatter = { command = "prettier", args = ["--parser", "json"] }
|
||||
language-servers = ["vscode-json-language-server"]
|
||||
|
||||
[[language]]
|
||||
name = "markdown"
|
||||
auto-format = true
|
||||
formatter = { command = "prettier", args = ["--parser", "markdown"] }
|
||||
language-servers = ["marksman"]
|
||||
|
||||
[[language]]
|
||||
name = "go"
|
||||
auto-format = true
|
||||
language-servers = ["gopls"]
|
||||
formatter = { command = "gofumpt" }
|
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1757545623,
|
||||
"narHash": "sha256-mCxPABZ6jRjUQx3bPP4vjA68ETbPLNz9V2pk9tO7pRQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8cd5ce828d5d1d16feff37340171a98fc3bf6526",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
43
flake.nix
Normal file
43
flake.nix
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
git
|
||||
|
||||
gnumake
|
||||
foreman
|
||||
|
||||
nil
|
||||
nixfmt-rfc-style
|
||||
|
||||
nodePackages.prettier
|
||||
nodePackages.yaml-language-server
|
||||
nodePackages.vscode-langservers-extracted
|
||||
markdownlint-cli
|
||||
nodePackages.markdown-link-check
|
||||
marksman
|
||||
taplo
|
||||
|
||||
go
|
||||
gopls
|
||||
go-tools
|
||||
gofumpt
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@ import (
|
|||
"miniflux.app/v2/internal/crypto"
|
||||
)
|
||||
|
||||
var schemaVersion = len(migrations)
|
||||
var cockroachSchemaVersion = len(cockroachMigrations)
|
||||
|
||||
// Order is important. Add new migrations at the end of the list.
|
||||
var migrations = [...]func(tx *sql.Tx) error{
|
||||
var cockroachMigrations = []Migration{
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TABLE schema_version (
|
||||
|
@ -123,13 +123,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE EXTENSION IF NOT EXISTS hstore;
|
||||
ALTER TABLE users ADD COLUMN extra hstore;
|
||||
CREATE INDEX users_extra_idx ON users using gin(extra);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
return nil
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
|
@ -263,11 +257,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE user_sessions ALTER COLUMN ip SET DATA TYPE inet using ip::inet;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
return nil
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
|
@ -281,7 +271,13 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN document_vectors tsvector;
|
||||
UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || title || ' ' || coalesce(content, '') for 1000000));
|
||||
CREATE INDEX document_vectors_idx ON entries USING gin(document_vectors);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
|
@ -292,16 +288,6 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
UPDATE
|
||||
entries
|
||||
SET
|
||||
document_vectors = setweight(to_tsvector(substring(coalesce(title, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce(content, '') for 1000000)), 'B')
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN keyboard_shortcuts boolean default 't'`
|
||||
_, err = tx.Exec(sql)
|
||||
|
@ -325,6 +311,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN changed_at timestamp with time zone;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
UPDATE entries SET changed_at = published_at;
|
||||
ALTER TABLE entries ALTER COLUMN changed_at SET not null;
|
||||
`
|
||||
|
@ -350,19 +342,31 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN share_code text not null default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE UNIQUE INDEX entries_share_code_idx ON entries USING btree(share_code) WHERE share_code <> '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, md5(url))`
|
||||
sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, url);`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE feeds ADD COLUMN next_check_at timestamp with time zone default now();
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE INDEX entries_user_feed_idx ON entries (user_id, feed_id);
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
|
@ -430,6 +434,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE entries ADD COLUMN created_at timestamp with time zone not null default now();
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
UPDATE entries SET created_at = published_at;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
|
@ -442,62 +452,9 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
ADD column google_id text not null default '',
|
||||
ADD column openid_connect_id text not null default ''
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
DECLARE my_cursor CURSOR FOR
|
||||
SELECT
|
||||
id,
|
||||
COALESCE(extra->'custom_css', '') as custom_css,
|
||||
COALESCE(extra->'google_id', '') as google_id,
|
||||
COALESCE(extra->'oidc_id', '') as oidc_id
|
||||
FROM users
|
||||
FOR UPDATE
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Exec("CLOSE my_cursor")
|
||||
|
||||
for {
|
||||
var (
|
||||
userID int64
|
||||
customStylesheet string
|
||||
googleID string
|
||||
oidcID string
|
||||
)
|
||||
|
||||
if err := tx.QueryRow(`FETCH NEXT FROM my_cursor`).Scan(&userID, &customStylesheet, &googleID, &oidcID); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := tx.Exec(
|
||||
`UPDATE
|
||||
users
|
||||
SET
|
||||
stylesheet=$2,
|
||||
google_id=$3,
|
||||
openid_connect_id=$4
|
||||
WHERE
|
||||
id=$1
|
||||
`,
|
||||
userID, customStylesheet, googleID, oidcID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
if _, err = tx.Exec(`ALTER TABLE users DROP COLUMN extra;`); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`
|
||||
CREATE UNIQUE INDEX users_google_id_idx ON users(google_id) WHERE google_id <> '';
|
||||
CREATE UNIQUE INDEX users_openid_connect_id_idx ON users(openid_connect_id) WHERE openid_connect_id <> '';
|
||||
|
@ -506,7 +463,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
_, err = tx.Exec(`
|
||||
CREATE INDEX entries_feed_url_idx ON entries(feed_id, url) WHERE length(url) < 2000;
|
||||
CREATE INDEX entries_feed_url_idx ON entries(feed_id, url);
|
||||
CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
|
||||
CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
|
||||
`)
|
||||
|
@ -531,6 +488,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TYPE webapp_display_mode AS enum('fullscreen', 'standalone', 'minimal-ui', 'browser');
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE users ADD COLUMN display_mode webapp_display_mode default 'standalone';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
|
@ -566,6 +529,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE TYPE entry_sorting_order AS enum('published_at', 'created_at');
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE users ADD COLUMN entry_order entry_sorting_order default 'published_at';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
|
@ -659,10 +628,21 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE users RENAME double_tap TO gesture_nav;
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN gesture_nav SET DATA TYPE text using case when gesture_nav = true then 'tap' when gesture_nav = false then 'none' end,
|
||||
ALTER COLUMN gesture_nav SET default 'tap';
|
||||
ALTER TABLE users ADD COLUMN gesture_nav text DEFAULT 'tap' NOT NULL;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
UPDATE users SET gesture_nav = CASE WHEN double_tap THEN 'tap' ELSE 'none' END;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE users DROP COLUMN double_tap;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
|
@ -710,7 +690,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
}
|
||||
|
||||
// Create unique index
|
||||
_, err = tx.Exec(`CREATE UNIQUE INDEX enclosures_user_entry_url_unique_idx ON enclosures(user_id, entry_id, md5(url))`)
|
||||
_, err = tx.Exec(`CREATE UNIQUE INDEX enclosures_user_entry_url_unique_idx ON enclosures(user_id, entry_id, url);`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1045,6 +1025,12 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE icons ADD COLUMN external_id text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
CREATE UNIQUE INDEX icons_external_id_idx ON icons USING btree(external_id) WHERE external_id <> '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
|
@ -1079,7 +1065,6 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
UPDATE icons SET external_id = $1 WHERE id = $2
|
||||
`,
|
||||
crypto.GenerateRandomStringHex(20), id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1160,7 +1145,7 @@ var migrations = [...]func(tx *sql.Tx) error{
|
|||
},
|
||||
// This migration replaces deprecated timezones by their equivalent on Debian Trixie.
|
||||
func(tx *sql.Tx) (err error) {
|
||||
var deprecatedTimeZoneMap = map[string]string{
|
||||
deprecatedTimeZoneMap := map[string]string{
|
||||
// Africa
|
||||
"Africa/Asmera": "Africa/Asmara",
|
||||
|
|
@ -7,13 +7,60 @@ import (
|
|||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DBKind int
|
||||
|
||||
const (
|
||||
DBKindPostgres DBKind = iota
|
||||
DBKindCockroach
|
||||
)
|
||||
|
||||
var dbKindProto = map[DBKind]string{
|
||||
DBKindPostgres: "postgresql",
|
||||
DBKindCockroach: "cockroachdb",
|
||||
}
|
||||
|
||||
var dbKindDriver = map[DBKind]string{
|
||||
DBKindPostgres: "postgres",
|
||||
DBKindCockroach: "postgres",
|
||||
}
|
||||
|
||||
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
|
||||
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,
|
||||
}
|
||||
|
||||
var dbKindSchemaVersion = map[DBKind]int{
|
||||
DBKindPostgres: postgresSchemaVersion,
|
||||
DBKindCockroach: cockroachSchemaVersion,
|
||||
}
|
||||
|
||||
// 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),
|
||||
|
@ -51,7 +98,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 +108,18 @@ 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]
|
||||
|
||||
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
|
@ -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 {
|
||||
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