2026-02-16 12:13:10 -05:00
package webpush
import (
"database/sql"
2026-02-17 12:04:51 -05:00
"fmt"
2026-02-16 12:13:10 -05:00
_ "github.com/mattn/go-sqlite3" // SQLite driver
)
const (
sqliteCreateWebPushSubscriptionsTableQuery = `
CREATE TABLE IF NOT EXISTS subscription (
id TEXT PRIMARY KEY ,
endpoint TEXT NOT NULL ,
key_auth TEXT NOT NULL ,
key_p256dh TEXT NOT NULL ,
user_id TEXT NOT NULL ,
subscriber_ip TEXT NOT NULL ,
updated_at INT NOT NULL ,
warned_at INT NOT NULL DEFAULT 0
) ;
CREATE UNIQUE INDEX IF NOT EXISTS idx_endpoint ON subscription ( endpoint ) ;
CREATE INDEX IF NOT EXISTS idx_subscriber_ip ON subscription ( subscriber_ip ) ;
CREATE TABLE IF NOT EXISTS subscription_topic (
subscription_id TEXT NOT NULL ,
topic TEXT NOT NULL ,
PRIMARY KEY ( subscription_id , topic ) ,
FOREIGN KEY ( subscription_id ) REFERENCES subscription ( id ) ON DELETE CASCADE
) ;
CREATE INDEX IF NOT EXISTS idx_topic ON subscription_topic ( topic ) ;
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY ,
version INT NOT NULL
2026-02-23 11:17:57 -05:00
) ;
2026-02-16 12:13:10 -05:00
`
sqliteBuiltinStartupQueries = `
PRAGMA foreign_keys = ON ;
`
2026-02-21 21:29:29 -05:00
sqliteSelectWebPushSubscriptionIDByEndpointQuery = ` SELECT id FROM subscription WHERE endpoint = ? `
sqliteSelectWebPushSubscriptionCountBySubscriberIPQuery = ` SELECT COUNT(*) FROM subscription WHERE subscriber_ip = ? `
sqliteSelectWebPushSubscriptionsForTopicQuery = `
2026-02-16 12:13:10 -05:00
SELECT id , endpoint , key_auth , key_p256dh , user_id
FROM subscription_topic st
JOIN subscription s ON s . id = st . subscription_id
WHERE st . topic = ?
ORDER BY endpoint
`
sqliteSelectWebPushSubscriptionsExpiringSoonQuery = `
SELECT id , endpoint , key_auth , key_p256dh , user_id
FROM subscription
WHERE warned_at = 0 AND updated_at <= ?
`
sqliteInsertWebPushSubscriptionQuery = `
INSERT INTO subscription ( id , endpoint , key_auth , key_p256dh , user_id , subscriber_ip , updated_at , warned_at )
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? )
ON CONFLICT ( endpoint )
DO UPDATE SET key_auth = excluded . key_auth , key_p256dh = excluded . key_p256dh , user_id = excluded . user_id , subscriber_ip = excluded . subscriber_ip , updated_at = excluded . updated_at , warned_at = excluded . warned_at
`
sqliteUpdateWebPushSubscriptionWarningSentQuery = ` UPDATE subscription SET warned_at = ? WHERE id = ? `
2026-02-16 19:12:14 -05:00
sqliteUpdateWebPushSubscriptionUpdatedAtQuery = ` UPDATE subscription SET updated_at = ? WHERE endpoint = ? `
2026-02-16 12:13:10 -05:00
sqliteDeleteWebPushSubscriptionByEndpointQuery = ` DELETE FROM subscription WHERE endpoint = ? `
sqliteDeleteWebPushSubscriptionByUserIDQuery = ` DELETE FROM subscription WHERE user_id = ? `
sqliteDeleteWebPushSubscriptionByAgeQuery = ` DELETE FROM subscription WHERE updated_at <= ? ` // Full table scan!
2026-02-21 21:29:29 -05:00
sqliteInsertWebPushSubscriptionTopicQuery = ` INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?) `
sqliteDeleteWebPushSubscriptionTopicAllQuery = ` DELETE FROM subscription_topic WHERE subscription_id = ? `
sqliteDeleteWebPushSubscriptionTopicWithoutSubscriptionQuery = ` DELETE FROM subscription_topic WHERE subscription_id NOT IN (SELECT id FROM subscription) `
2026-02-16 12:13:10 -05:00
)
// SQLite schema management queries
const (
sqliteCurrentWebPushSchemaVersion = 1
2026-02-21 21:29:29 -05:00
sqliteInsertWebPushSchemaVersionQuery = ` INSERT INTO schemaVersion VALUES (1, ?) `
2026-02-16 12:13:10 -05:00
sqliteSelectWebPushSchemaVersionQuery = ` SELECT version FROM schemaVersion WHERE id = 1 `
)
// NewSQLiteStore creates a new SQLite-backed web push store.
2026-02-16 19:53:34 -05:00
func NewSQLiteStore ( filename , startupQueries string ) ( Store , error ) {
2026-02-16 12:13:10 -05:00
db , err := sql . Open ( "sqlite3" , filename )
if err != nil {
return nil , err
}
2026-02-17 12:04:51 -05:00
if err := setupSQLite ( db ) ; err != nil {
2026-02-16 12:13:10 -05:00
return nil , err
}
2026-02-17 12:04:51 -05:00
if err := runSQLiteStartupQueries ( db , startupQueries ) ; err != nil {
2026-02-16 12:13:10 -05:00
return nil , err
}
2026-02-16 19:53:34 -05:00
return & commonStore {
2026-02-16 12:13:10 -05:00
db : db ,
2026-02-16 19:53:34 -05:00
queries : storeQueries {
2026-02-21 21:29:29 -05:00
selectSubscriptionIDByEndpoint : sqliteSelectWebPushSubscriptionIDByEndpointQuery ,
selectSubscriptionCountBySubscriberIP : sqliteSelectWebPushSubscriptionCountBySubscriberIPQuery ,
2026-02-16 19:53:34 -05:00
selectSubscriptionsForTopic : sqliteSelectWebPushSubscriptionsForTopicQuery ,
selectSubscriptionsExpiringSoon : sqliteSelectWebPushSubscriptionsExpiringSoonQuery ,
2026-02-22 14:25:15 -05:00
upsertSubscription : sqliteInsertWebPushSubscriptionQuery ,
2026-02-16 19:53:34 -05:00
updateSubscriptionWarningSent : sqliteUpdateWebPushSubscriptionWarningSentQuery ,
updateSubscriptionUpdatedAt : sqliteUpdateWebPushSubscriptionUpdatedAtQuery ,
deleteSubscriptionByEndpoint : sqliteDeleteWebPushSubscriptionByEndpointQuery ,
deleteSubscriptionByUserID : sqliteDeleteWebPushSubscriptionByUserIDQuery ,
deleteSubscriptionByAge : sqliteDeleteWebPushSubscriptionByAgeQuery ,
insertSubscriptionTopic : sqliteInsertWebPushSubscriptionTopicQuery ,
deleteSubscriptionTopicAll : sqliteDeleteWebPushSubscriptionTopicAllQuery ,
2026-02-21 21:29:29 -05:00
deleteSubscriptionTopicWithoutSubscription : sqliteDeleteWebPushSubscriptionTopicWithoutSubscriptionQuery ,
2026-02-16 19:53:34 -05:00
} ,
2026-02-16 12:13:10 -05:00
} , nil
}
2026-02-17 12:04:51 -05:00
func setupSQLite ( db * sql . DB ) error {
var schemaVersion int
err := db . QueryRow ( sqliteSelectWebPushSchemaVersionQuery ) . Scan ( & schemaVersion )
2026-02-16 12:13:10 -05:00
if err != nil {
2026-02-17 12:04:51 -05:00
return setupNewSQLite ( db )
2026-02-16 12:13:10 -05:00
}
2026-02-17 12:04:51 -05:00
if schemaVersion > sqliteCurrentWebPushSchemaVersion {
return fmt . Errorf ( "unexpected schema version: version %d is higher than current version %d" , schemaVersion , sqliteCurrentWebPushSchemaVersion )
}
return nil
2026-02-16 12:13:10 -05:00
}
2026-02-17 12:04:51 -05:00
func setupNewSQLite ( db * sql . DB ) error {
2026-02-23 11:17:57 -05:00
tx , err := db . Begin ( )
if err != nil {
2026-02-16 12:13:10 -05:00
return err
}
2026-02-23 11:17:57 -05:00
defer tx . Rollback ( )
if _ , err := tx . Exec ( sqliteCreateWebPushSubscriptionsTableQuery ) ; err != nil {
2026-02-16 12:13:10 -05:00
return err
}
2026-02-23 11:17:57 -05:00
if _ , err := tx . Exec ( sqliteInsertWebPushSchemaVersionQuery , sqliteCurrentWebPushSchemaVersion ) ; err != nil {
return err
}
return tx . Commit ( )
2026-02-16 12:13:10 -05:00
}
2026-02-17 12:04:51 -05:00
func runSQLiteStartupQueries ( db * sql . DB , startupQueries string ) error {
2026-02-16 12:13:10 -05:00
if _ , err := db . Exec ( startupQueries ) ; err != nil {
return err
}
if _ , err := db . Exec ( sqliteBuiltinStartupQueries ) ; err != nil {
return err
}
return nil
}