2026-02-18 20:22:44 -05:00
package message
import (
"database/sql"
"fmt"
"path/filepath"
2026-02-22 16:21:27 -05:00
"sync"
2026-02-18 20:22:44 -05:00
"time"
_ "github.com/mattn/go-sqlite3" // SQLite driver
2026-03-10 22:17:40 -04:00
"heckel.io/ntfy/v2/db"
2026-02-18 20:22:44 -05:00
"heckel.io/ntfy/v2/util"
)
// SQLite runtime query constants
const (
sqliteInsertMessageQuery = `
INSERT INTO messages ( mid , sequence_id , time , event , expires , topic , message , title , priority , tags , click , icon , actions , attachment_name , attachment_type , attachment_size , attachment_expires , attachment_url , attachment_deleted , sender , user , content_type , encoding , published )
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
`
sqliteDeleteMessageQuery = ` DELETE FROM messages WHERE mid = ? `
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 , id
`
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 , id
`
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
2026-02-22 16:21:27 -05:00
WHERE topic = ? AND id > COALESCE ( ( SELECT id FROM messages WHERE mid = ? ) , 0 ) AND published = 1
2026-02-18 20:22:44 -05:00
ORDER BY time , id
`
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
2026-02-22 16:21:27 -05:00
WHERE topic = ? AND ( id > COALESCE ( ( SELECT id FROM messages WHERE mid = ? ) , 0 ) OR published = 0 )
2026-02-18 20:22:44 -05:00
ORDER BY time , id
`
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
`
2026-02-22 16:21:27 -05:00
sqliteSelectMessagesExpiredQuery = ` SELECT mid FROM messages WHERE expires <= ? AND published = 1 `
sqliteUpdateMessagePublishedQuery = ` UPDATE messages SET published = 1 WHERE mid = ? `
sqliteSelectMessagesCountQuery = ` SELECT COUNT(*) FROM messages `
sqliteSelectTopicsQuery = ` SELECT topic FROM messages GROUP BY topic `
2026-02-18 20:22:44 -05:00
2026-02-21 21:29:29 -05:00
sqliteUpdateAttachmentDeletedQuery = ` UPDATE messages SET attachment_deleted = 1 WHERE mid = ? `
2026-02-18 20:22:44 -05:00
sqliteSelectAttachmentsExpiredQuery = ` SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires <= ? AND attachment_deleted = 0 `
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 >= ? `
2026-03-23 12:44:40 -04:00
sqliteSelectAttachmentsWithSizesQuery = ` SELECT mid, attachment_size FROM messages WHERE attachment_expires > ? AND attachment_deleted = 0 `
2026-02-18 20:22:44 -05:00
2026-02-19 20:48:01 -05:00
sqliteSelectStatsQuery = ` SELECT value FROM stats WHERE key = 'messages' `
sqliteUpdateStatsQuery = ` UPDATE stats SET value = ? WHERE key = 'messages' `
sqliteUpdateMessageTimeQuery = ` UPDATE messages SET time = ? WHERE mid = ? `
2026-02-18 20:22:44 -05:00
)
2026-03-01 13:19:53 -05:00
var sqliteQueries = queries {
2026-02-18 20:22:44 -05:00
insertMessage : sqliteInsertMessageQuery ,
deleteMessage : sqliteDeleteMessageQuery ,
selectScheduledMessageIDsBySeqID : sqliteSelectScheduledMessageIDsBySeqIDQuery ,
deleteScheduledBySequenceID : sqliteDeleteScheduledBySequenceIDQuery ,
updateMessagesForTopicExpiry : sqliteUpdateMessagesForTopicExpiryQuery ,
selectMessagesByID : sqliteSelectMessagesByIDQuery ,
selectMessagesSinceTime : sqliteSelectMessagesSinceTimeQuery ,
selectMessagesSinceTimeScheduled : sqliteSelectMessagesSinceTimeIncludeScheduledQuery ,
selectMessagesSinceID : sqliteSelectMessagesSinceIDQuery ,
selectMessagesSinceIDScheduled : sqliteSelectMessagesSinceIDIncludeScheduledQuery ,
selectMessagesLatest : sqliteSelectMessagesLatestQuery ,
selectMessagesDue : sqliteSelectMessagesDueQuery ,
selectMessagesExpired : sqliteSelectMessagesExpiredQuery ,
updateMessagePublished : sqliteUpdateMessagePublishedQuery ,
selectMessagesCount : sqliteSelectMessagesCountQuery ,
selectTopics : sqliteSelectTopicsQuery ,
2026-02-21 21:29:29 -05:00
updateAttachmentDeleted : sqliteUpdateAttachmentDeletedQuery ,
2026-02-18 20:22:44 -05:00
selectAttachmentsExpired : sqliteSelectAttachmentsExpiredQuery ,
selectAttachmentsSizeBySender : sqliteSelectAttachmentsSizeBySenderQuery ,
selectAttachmentsSizeByUserID : sqliteSelectAttachmentsSizeByUserIDQuery ,
2026-03-23 12:44:40 -04:00
selectAttachmentsWithSizes : sqliteSelectAttachmentsWithSizesQuery ,
2026-02-18 20:22:44 -05:00
selectStats : sqliteSelectStatsQuery ,
updateStats : sqliteUpdateStatsQuery ,
2026-02-19 20:48:01 -05:00
updateMessageTime : sqliteUpdateMessageTimeQuery ,
2026-02-18 20:22:44 -05:00
}
// NewSQLiteStore creates a SQLite file-backed cache
2026-03-01 13:19:53 -05:00
func NewSQLiteStore ( filename , startupQueries string , cacheDuration time . Duration , batchSize int , batchTimeout time . Duration , nop bool ) ( * Cache , error ) {
2026-02-18 20:22:44 -05:00
parentDir := filepath . Dir ( filename )
if ! util . FileExists ( parentDir ) {
return nil , fmt . Errorf ( "cache database directory %s does not exist or is not accessible" , parentDir )
}
2026-03-12 21:17:30 -04:00
d , err := sql . Open ( "sqlite3" , filename )
2026-02-18 20:22:44 -05:00
if err != nil {
return nil , err
}
2026-03-12 21:17:30 -04:00
if err := setupSQLite ( d , startupQueries , cacheDuration ) ; err != nil {
2026-02-18 20:22:44 -05:00
return nil , err
}
2026-03-12 21:17:30 -04:00
return newCache ( db . New ( & db . Host { DB : d } , nil ) , sqliteQueries , & sync . Mutex { } , batchSize , batchTimeout , nop ) , nil
2026-02-18 20:22:44 -05:00
}
// NewMemStore creates an in-memory cache
2026-03-01 13:19:53 -05:00
func NewMemStore ( ) ( * Cache , error ) {
2026-02-18 20:22:44 -05:00
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
2026-03-01 13:19:53 -05:00
func NewNopStore ( ) ( * Cache , error ) {
2026-02-18 20:22:44 -05:00
return NewSQLiteStore ( createMemoryFilename ( ) , "" , 0 , 0 , 0 , true )
}
// createMemoryFilename creates a unique memory filename to use for the SQLite backend.
2026-02-20 16:15:07 -05:00
// 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."
2026-02-18 20:22:44 -05:00
func createMemoryFilename ( ) string {
return fmt . Sprintf ( "file:%s?mode=memory&cache=shared" , util . RandomString ( 10 ) )
}