mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-11-12 07:30:53 +08:00
One insertMessageValues function now yields column name, PostgreSQL type and value side by side per row. The backends build their statements from the row, so the separate column lists (and the order they had to share with the values) are gone. SQLite keeps a one-entry rename for its user column.
176 lines
8.8 KiB
Go
176 lines
8.8 KiB
Go
package message
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
|
"heckel.io/ntfy/v2/db"
|
|
"heckel.io/ntfy/v2/db/schema"
|
|
"heckel.io/ntfy/v2/util"
|
|
)
|
|
|
|
// SQLite runtime query constants
|
|
const (
|
|
sqliteSelectScheduledMessageIDsBySeqIDQuery = `SELECT mid FROM messages WHERE topic = ? AND sequence_id = ? AND published = 0`
|
|
sqliteDeleteScheduledBySequenceIDQuery = `DELETE FROM messages WHERE topic = ? AND sequence_id = ? AND published = 0`
|
|
sqliteUpdateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
|
|
sqliteSelectMessagesByIDQuery = `
|
|
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
|
FROM messages
|
|
WHERE mid = ?
|
|
`
|
|
sqliteSelectMessagesSinceTimeQuery = `
|
|
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
|
FROM messages
|
|
WHERE topic = ? AND time >= ? AND published = 1
|
|
ORDER BY time DESC, id DESC
|
|
`
|
|
sqliteSelectMessagesSinceTimeIncludeScheduledQuery = `
|
|
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
|
FROM messages
|
|
WHERE topic = ? AND time >= ?
|
|
ORDER BY time DESC, id DESC
|
|
`
|
|
sqliteSelectMessageRowIDQuery = `SELECT id FROM messages WHERE mid = ? LIMIT 1`
|
|
sqliteSelectMessagesSinceIDQuery = `
|
|
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
|
FROM messages
|
|
WHERE topic = ? AND id > ? AND published = 1
|
|
ORDER BY time DESC, id DESC
|
|
`
|
|
sqliteSelectMessagesSinceIDIncludeScheduledQuery = `
|
|
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
|
FROM messages
|
|
WHERE topic = ? AND (id > ? OR published = 0)
|
|
ORDER BY time DESC, id DESC
|
|
`
|
|
sqliteSelectMessagesLatestQuery = `
|
|
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
|
FROM messages
|
|
WHERE topic = ? AND published = 1
|
|
ORDER BY time DESC, id DESC
|
|
LIMIT 1
|
|
`
|
|
sqliteSelectMessagesDueQuery = `
|
|
SELECT mid, sequence_id, time, event, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
|
|
FROM messages
|
|
WHERE time <= ? AND published = 0
|
|
ORDER BY time, id
|
|
`
|
|
sqliteUpdateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?`
|
|
sqliteSelectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
|
|
sqliteSelectTopicsQuery = `SELECT topic FROM messages GROUP BY topic`
|
|
|
|
sqliteDeleteExpiredMessagesQuery = `DELETE FROM messages WHERE mid IN (SELECT mid FROM messages WHERE expires <= ? AND published = 1 LIMIT ?)`
|
|
sqliteMarkExpiredAttachmentsDeletedQuery = `UPDATE messages SET attachment_deleted = 1 WHERE mid IN (SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires <= ? AND attachment_deleted = 0 LIMIT ?)`
|
|
sqliteSelectAttachmentsSizeBySenderQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE user = '' AND sender = ? AND attachment_expires >= ?`
|
|
sqliteSelectAttachmentsSizeByUserIDQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE user = ? AND attachment_expires >= ?`
|
|
sqliteSelectAttachmentsWithSizesQuery = `SELECT mid, attachment_size FROM messages WHERE attachment_expires > ? AND attachment_deleted = 0`
|
|
|
|
sqliteSelectStatsQuery = `SELECT value FROM stats WHERE key = 'messages'`
|
|
sqliteUpdateStatsQuery = `UPDATE stats SET value = ? WHERE key = 'messages'`
|
|
sqliteUpdateMessageTimeQuery = `UPDATE messages SET time = ? WHERE mid = ?`
|
|
)
|
|
|
|
var sqliteQueries = queries{
|
|
insertMessages: sqliteInsertMessages,
|
|
selectScheduledMessageIDsBySeqID: sqliteSelectScheduledMessageIDsBySeqIDQuery,
|
|
deleteScheduledBySequenceID: sqliteDeleteScheduledBySequenceIDQuery,
|
|
updateMessagesForTopicExpiry: sqliteUpdateMessagesForTopicExpiryQuery,
|
|
selectMessagesByID: sqliteSelectMessagesByIDQuery,
|
|
selectMessagesSinceTime: sqliteSelectMessagesSinceTimeQuery,
|
|
selectMessagesSinceTimeScheduled: sqliteSelectMessagesSinceTimeIncludeScheduledQuery,
|
|
selectMessageRowID: sqliteSelectMessageRowIDQuery,
|
|
selectMessagesSinceID: sqliteSelectMessagesSinceIDQuery,
|
|
selectMessagesSinceIDScheduled: sqliteSelectMessagesSinceIDIncludeScheduledQuery,
|
|
selectMessagesLatest: sqliteSelectMessagesLatestQuery,
|
|
selectMessagesDue: sqliteSelectMessagesDueQuery,
|
|
deleteExpiredMessages: sqliteDeleteExpiredMessagesQuery,
|
|
updateMessagePublished: sqliteUpdateMessagePublishedQuery,
|
|
selectMessagesCount: sqliteSelectMessagesCountQuery,
|
|
selectTopics: sqliteSelectTopicsQuery,
|
|
markExpiredAttachmentsDeleted: sqliteMarkExpiredAttachmentsDeletedQuery,
|
|
selectAttachmentsSizeBySender: sqliteSelectAttachmentsSizeBySenderQuery,
|
|
selectAttachmentsSizeByUserID: sqliteSelectAttachmentsSizeByUserIDQuery,
|
|
selectAttachmentsWithSizes: sqliteSelectAttachmentsWithSizesQuery,
|
|
selectStats: sqliteSelectStatsQuery,
|
|
updateStats: sqliteUpdateStatsQuery,
|
|
updateMessageTime: sqliteUpdateMessageTimeQuery,
|
|
}
|
|
|
|
// NewSQLiteStore creates a SQLite file-backed cache
|
|
func NewSQLiteStore(filename, startupQueries string, cacheDuration time.Duration, batchSize int, batchTimeout time.Duration, nop bool) (*Cache, error) {
|
|
parentDir := filepath.Dir(filename)
|
|
if !util.FileExists(parentDir) {
|
|
return nil, fmt.Errorf("cache database directory %s does not exist or is not accessible", parentDir)
|
|
}
|
|
d, err := sql.Open("sqlite3", filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := runSQLiteStartupQueries(d, startupQueries); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := schema.Migrate(d, schema.SQLite, schemaStore, sqliteCurrentSchemaVersion, sqliteCreateTables, sqliteMigrations(cacheDuration)); err != nil {
|
|
return nil, err
|
|
}
|
|
return newCache(db.New(&db.Host{DB: d}, nil), sqliteQueries, &sync.Mutex{}, batchSize, batchTimeout, nop), nil
|
|
}
|
|
|
|
// NewMemStore creates an in-memory cache
|
|
func NewMemStore() (*Cache, error) {
|
|
return NewSQLiteStore(createMemoryFilename(), "", 0, 0, 0, false)
|
|
}
|
|
|
|
// NewNopStore creates an in-memory cache that discards all messages;
|
|
// it is always empty and can be used if caching is entirely disabled
|
|
func NewNopStore() (*Cache, error) {
|
|
return NewSQLiteStore(createMemoryFilename(), "", 0, 0, 0, true)
|
|
}
|
|
|
|
// createMemoryFilename creates a unique memory filename to use for the SQLite backend.
|
|
// From mattn/go-sqlite3: "Each connection to ":memory:" opens a brand new in-memory
|
|
// sql database, so if the stdlib's sql engine happens to open another connection and
|
|
// you've only specified ":memory:", that connection will see a brand new database.
|
|
// A workaround is to use "file::memory:?cache=shared" (or "file:foobar?mode=memory&cache=shared").
|
|
// Every connection to this string will point to the same in-memory database."
|
|
func createMemoryFilename() string {
|
|
return fmt.Sprintf("file:%s?mode=memory&cache=shared", util.RandomString(10))
|
|
}
|
|
|
|
// sqliteInsertColumnNames maps the row's column names to the SQLite table's where they differ
|
|
var sqliteInsertColumnNames = map[string]string{"user_id": "user"}
|
|
|
|
// sqliteInsertMessages writes the rows as multi-row INSERTs of at most insertMessagesMaxRows each
|
|
func sqliteInsertMessages(tx *sql.Tx, rows [][]insertValue) error {
|
|
columns := make([]string, 0, len(rows[0]))
|
|
for _, v := range rows[0] {
|
|
if name, ok := sqliteInsertColumnNames[v.column]; ok {
|
|
columns = append(columns, name)
|
|
} else {
|
|
columns = append(columns, v.column)
|
|
}
|
|
}
|
|
for len(rows) > 0 {
|
|
chunk := rows[:min(len(rows), insertMessagesMaxRows)]
|
|
rows = rows[len(chunk):]
|
|
args := make([]any, 0, len(chunk)*len(columns))
|
|
for _, row := range chunk {
|
|
for _, v := range row {
|
|
args = append(args, v.value)
|
|
}
|
|
}
|
|
query := insertMessagesQuery("messages", columns, len(chunk), func(int) string {
|
|
return "?"
|
|
})
|
|
if _, err := tx.Exec(query, args...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|