package sqlstore import ( "context" "database/sql" "fmt" ) // The sequence tables. Each component owns its own, so that a component // added later does not need a migration to a table another one is already // writing. const ( roomSequences = "roomserver_sequences" syncSequences = "sync_sequences" ) // The counters in sync_sequences. const ( seqReceipt = "receipt" seqToDevice = "to_device" seqAccountData = "account_data" seqPresence = "presence" ) // execQuerier is the part of *sql.Tx and *sql.DB that the sequence helpers // need, so that a counter can be read outside a transaction and bumped // inside one. type execQuerier interface { ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } // nextSequence hands out the next value of a counter. // // The UPDATE takes a row lock that both engines hold until the transaction // ends, so two concurrent writers queue rather than collide, and a value // handed out by a transaction that then rolls back is never used — which is // what keeps a stream gapless. A database sequence would be faster and // would not have that property: PostgreSQL's are explicitly not // transactional, so a rolled-back write would leave a hole that every // reader of the stream would have to be taught to skip. // // It must be called inside a transaction. Called on a bare *sql.DB the two // statements are two transactions, and two callers can then be handed the // same value. func nextSequence(ctx context.Context, db execQuerier, q func(string) string, table, name string) (int64, error) { bump := fmt.Sprintf(`UPDATE %s SET value = value + 1 WHERE name = ?`, table) if _, err := db.ExecContext(ctx, q(bump), name); err != nil { return 0, fmt.Errorf("advance the %s counter: %w", name, err) } return currentSequence(ctx, db, q, table, name) } // currentSequence reads a counter without advancing it: the top of a stream // as a sync token has to name it. func currentSequence(ctx context.Context, db execQuerier, q func(string) string, table, name string) (int64, error) { query := fmt.Sprintf(`SELECT value FROM %s WHERE name = ?`, table) var value int64 if err := db.QueryRowContext(ctx, q(query), name).Scan(&value); err != nil { return 0, fmt.Errorf("read the %s counter: %w", name, err) } return value, nil }