store: refactor access hash and session tables

* Move sessions to user_login metadata, as that data rarely changes after login.
* Merge user and channel access hashes. Those IDs don't conflict.
* Split usernames into a new table to allow better `ON CONFLICT` updates
  (when a username moves to another entity, we want the old row to be replaced).
  Usernames also don't need to be scoped to a login.
This commit is contained in:
Tulir Asokan
2024-08-22 16:21:24 +03:00
parent e611c87342
commit b25c09fc53
14 changed files with 129 additions and 195 deletions
+37 -85
View File
@@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"github.com/gotd/td/session"
"github.com/gotd/td/telegram/updates"
"go.mau.fi/util/dbutil"
)
@@ -19,14 +18,6 @@ type ScopedStore struct {
}
const (
// Session Storage Queries
loadSessionQuery = `SELECT session_data FROM telegram_session WHERE user_id=$1`
storeSessionQuery = `
INSERT INTO telegram_session (user_id, session_data)
VALUES ($1, $2)
ON CONFLICT (user_id) DO UPDATE SET session_data=excluded.session_data
`
// State Storage Queries
allChannelsQuery = "SELECT channel_id, pts FROM telegram_channel_state WHERE user_id=$1"
getChannelPtsQuery = "SELECT pts FROM telegram_channel_state WHERE user_id=$1 AND channel_id=$2"
@@ -51,54 +42,23 @@ const (
setSeqQuery = "UPDATE telegram_user_state SET seq=$1 WHERE user_id=$2"
setDateSeqQuery = "UPDATE telegram_user_state SET date=$1, seq=$2 WHERE user_id=$3"
// Channel Access Hasher Queries
getChannelAccessHashQuery = "SELECT access_hash FROM telegram_channel_access_hashes WHERE user_id=$1 AND channel_id=$2"
setChannelAccessHashQuery = `
INSERT INTO telegram_channel_access_hashes (user_id, channel_id, access_hash)
getAccessHashQuery = "SELECT access_hash FROM telegram_access_hash WHERE user_id=$1 AND entity_id=$2"
setAccessHashQuery = `
INSERT INTO telegram_access_hash (user_id, entity_id, access_hash)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, channel_id) DO UPDATE SET access_hash=excluded.access_hash
`
// User Access Hash Queries
getUserAccessHashQuery = "SELECT access_hash FROM telegram_user_metadata WHERE receiver_id=$1 AND user_id=$2"
setUserAccessHashQuery = `
INSERT INTO telegram_user_metadata (receiver_id, user_id, access_hash)
VALUES ($1, $2, $3)
ON CONFLICT (receiver_id, user_id) DO UPDATE SET access_hash=excluded.access_hash
ON CONFLICT (user_id, entity_id) DO UPDATE SET access_hash=excluded.access_hash
`
// User Username Queries
getUserUsernameQuery = "SELECT username FROM telegram_user_metadata WHERE receiver_id=$1 AND user_id=$2"
setUserUsernameQuery = `
INSERT INTO telegram_user_metadata (receiver_id, user_id, username)
VALUES ($1, $2, $3)
ON CONFLICT (receiver_id, user_id) DO UPDATE SET username=excluded.username
`
// User Metadata Queries
getUserMetadataQuery = "SELECT username, access_hash FROM telegram_user_metadata WHERE receiver_id=$1 AND user_id=$2"
setUserMetadataQuery = `
INSERT INTO telegram_user_metadata (receiver_id, user_id, username, access_hash)
VALUES ($1, $2, $3, $4)
ON CONFLICT (receiver_id, user_id) DO UPDATE SET
username=excluded.username,
access_hash=excluded.access_hash
getUsernameQuery = "SELECT username FROM telegram_username WHERE entity_id=$1"
setUsernameQuery = `
INSERT INTO telegram_username (username, entity_id)
VALUES ($1, $2)
ON CONFLICT (username) DO UPDATE SET entity_id=excluded.entity_id
`
clearUsernameQuery = `DELETE FROM telegram_username WHERE entity_id=$1`
)
var _ session.Storage = (*ScopedStore)(nil)
func (s *ScopedStore) LoadSession(ctx context.Context) (sessionData []byte, err error) {
row := s.db.QueryRow(ctx, loadSessionQuery, s.telegramUserID)
err = row.Scan(&sessionData)
return
}
func (s *ScopedStore) StoreSession(ctx context.Context, data []byte) error {
_, err := s.db.Exec(ctx, storeSessionQuery, s.telegramUserID, data)
return err
}
var _ updates.StateStorage = (*ScopedStore)(nil)
func (s *ScopedStore) ForEachChannels(ctx context.Context, userID int64, f func(ctx context.Context, channelID int64, pts int) error) error {
@@ -181,57 +141,49 @@ func (s *ScopedStore) SetDateSeq(ctx context.Context, userID int64, date int, se
var _ updates.ChannelAccessHasher = (*ScopedStore)(nil)
func (s *ScopedStore) GetChannelAccessHash(ctx context.Context, userID int64, channelID int64) (accessHash int64, found bool, err error) {
// Deprecated: only for interface, don't use directly. Use GetAccessHash instead
func (s *ScopedStore) GetChannelAccessHash(ctx context.Context, userID, channelID int64) (accessHash int64, found bool, err error) {
s.assertUserIDMatches(userID)
err = s.db.QueryRow(ctx, getChannelAccessHashQuery, userID, channelID).Scan(&accessHash)
if errors.Is(err, sql.ErrNoRows) {
return 0, false, nil
}
return accessHash, err == nil, err
accessHash, err = s.GetAccessHash(ctx, channelID)
found = accessHash != 0
return
}
func (s *ScopedStore) SetChannelAccessHash(ctx context.Context, userID int64, channelID int64, accessHash int64) (err error) {
// Deprecated: only for interface, don't use directly. Use SetAccessHash instead
func (s *ScopedStore) SetChannelAccessHash(ctx context.Context, userID, channelID, accessHash int64) (err error) {
s.assertUserIDMatches(userID)
_, err = s.db.Exec(ctx, setChannelAccessHashQuery, userID, channelID, accessHash)
return s.SetAccessHash(ctx, channelID, accessHash)
}
var ErrNoAccessHash = errors.New("access hash not found")
func (s *ScopedStore) GetAccessHash(ctx context.Context, userID int64) (accessHash int64, err error) {
err = s.db.QueryRow(ctx, getAccessHashQuery, s.telegramUserID, userID).Scan(&accessHash)
if errors.Is(err, sql.ErrNoRows) {
err = ErrNoAccessHash
}
return
}
func (s *ScopedStore) GetUserAccessHash(ctx context.Context, userID int64) (accessHash int64, found bool, err error) {
err = s.db.QueryRow(ctx, getUserAccessHashQuery, s.telegramUserID, userID).Scan(&accessHash)
if errors.Is(err, sql.ErrNoRows) {
return 0, false, nil
}
return accessHash, err == nil, err
}
func (s *ScopedStore) SetUserAccessHash(ctx context.Context, userID, accessHash int64) (err error) {
_, err = s.db.Exec(ctx, setUserAccessHashQuery, s.telegramUserID, userID, accessHash)
func (s *ScopedStore) SetAccessHash(ctx context.Context, userID, accessHash int64) (err error) {
_, err = s.db.Exec(ctx, setAccessHashQuery, s.telegramUserID, userID, accessHash)
return
}
func (s *ScopedStore) GetUserUsername(ctx context.Context, userID int64) (username string, found bool, err error) {
err = s.db.QueryRow(ctx, getUserUsernameQuery, s.telegramUserID, userID).Scan(&username)
func (s *ScopedStore) GetUsername(ctx context.Context, userID int64) (username string, err error) {
err = s.db.QueryRow(ctx, getUsernameQuery, userID).Scan(&username)
if errors.Is(err, sql.ErrNoRows) {
return "", false, nil
err = nil
}
return username, err == nil, err
}
func (s *ScopedStore) SetUserUsername(ctx context.Context, userID int64, username string) (err error) {
_, err = s.db.Exec(ctx, setUserUsernameQuery, s.telegramUserID, userID, username)
return
}
func (s *ScopedStore) GetUserMetadata(ctx context.Context, userID int64) (username string, accessHash int64, found bool, err error) {
err = s.db.QueryRow(ctx, getUserMetadataQuery, s.telegramUserID, userID).Scan(&username, &accessHash)
if errors.Is(err, sql.ErrNoRows) {
return "", 0, false, nil
func (s *ScopedStore) SetUsername(ctx context.Context, userID int64, username string) (err error) {
if username == "" {
_, err = s.db.Exec(ctx, clearUsernameQuery, userID)
} else {
_, err = s.db.Exec(ctx, setUsernameQuery, username, userID)
}
return username, accessHash, err == nil, err
}
func (s *ScopedStore) SetUserMetadata(ctx context.Context, userID int64, username string, accessHash int64) (err error) {
_, err = s.db.Exec(ctx, setUserMetadataQuery, s.telegramUserID, userID, username, accessHash)
return
}
+15 -24
View File
@@ -1,12 +1,7 @@
-- v0 -> v3: Latest revision
CREATE TABLE telegram_session (
user_id BIGINT PRIMARY KEY,
session_data BYTEA NOT NULL
);
-- v0 -> v1: Latest revision
CREATE TABLE telegram_user_state (
user_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL PRIMARY KEY,
pts BIGINT NOT NULL,
qts BIGINT NOT NULL,
date BIGINT NOT NULL,
@@ -14,39 +9,35 @@ CREATE TABLE telegram_user_state (
);
CREATE TABLE telegram_channel_state (
user_id BIGINT,
channel_id BIGINT,
user_id BIGINT NOT NULL,
channel_id BIGINT NOT NULL,
pts BIGINT NOT NULL,
PRIMARY KEY (user_id, channel_id)
);
CREATE INDEX idx_telegram_channel_state_user_id ON telegram_channel_state (user_id);
CREATE INDEX telegram_channel_state_user_id_idx ON telegram_channel_state (user_id);
CREATE TABLE telegram_channel_access_hashes (
user_id BIGINT,
channel_id BIGINT,
CREATE TABLE telegram_access_hash (
user_id BIGINT NOT NULL,
entity_id BIGINT NOT NULL,
access_hash BIGINT NOT NULL,
PRIMARY KEY (user_id, channel_id)
PRIMARY KEY (user_id, entity_id)
);
CREATE TABLE telegram_user_metadata (
receiver_id BIGINT,
user_id BIGINT,
CREATE TABLE telegram_username (
username TEXT NOT NULL,
entity_id BIGINT NOT NULL,
access_hash BIGINT NOT NULL,
username TEXT,
PRIMARY KEY (receiver_id, user_id)
PRIMARY KEY (username)
);
CREATE INDEX telegram_username_entity_idx ON telegram_username (entity_id);
CREATE TABLE telegram_file (
id TEXT PRIMARY KEY,
mxc TEXT NOT NULL,
mime_type TEXT,
size BIGINT
);
-- TODO this will be unnecessary once the queries switch to reading telegram_user_metadata
CREATE INDEX idx_ghost_username ON ghost ((metadata->>'username'));
@@ -1,3 +0,0 @@
-- v2: Add index for ghost username metadata field
CREATE INDEX idx_ghost_username ON ghost ((metadata->>'username'));
@@ -1,15 +0,0 @@
-- v3: Move the user access hash to a table so it can be per-user
CREATE TABLE telegram_user_metadata (
receiver_id INTEGER,
user_id INTEGER,
access_hash INTEGER NOT NULL,
username TEXT,
PRIMARY KEY (receiver_id, user_id)
);
INSERT INTO telegram_user_metadata (receiver_id, user_id, access_hash, username)
SELECT ul.id, g.id, g.metadata->>'access_hash', g.metadata->>'username'
FROM user_login ul, ghost g;