From b8a7e7443ddac7a17116f017a0531bccc959ebff Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 12 Mar 2023 01:31:10 +0100 Subject: [PATCH] feat(backend): use jsonb instead of composite type arrays --- backend/db/db.go | 43 +- backend/db/entries.go | 37 -- backend/db/field.go | 56 +- backend/db/member.go | 99 +-- backend/db/names_pronouns.go | 50 +- backend/db/queries/generate.go | 3 - backend/db/queries/queries.member.sql | 31 - backend/db/queries/queries.member.sql.go | 803 ----------------------- backend/db/queries/queries.user.sql | 21 - backend/db/queries/queries.user.sql.go | 320 --------- backend/db/tokens.go | 4 +- backend/db/user.go | 135 ++-- go.mod | 2 +- scripts/migrate/006_jsonb_arrays.sql | 24 + 14 files changed, 161 insertions(+), 1467 deletions(-) delete mode 100644 backend/db/queries/generate.go delete mode 100644 backend/db/queries/queries.member.sql delete mode 100644 backend/db/queries/queries.member.sql.go delete mode 100644 backend/db/queries/queries.user.sql delete mode 100644 backend/db/queries/queries.user.sql.go create mode 100644 scripts/migrate/006_jsonb_arrays.sql diff --git a/backend/db/db.go b/backend/db/db.go index 1f16edd..7a15a62 100644 --- a/backend/db/db.go +++ b/backend/db/db.go @@ -7,12 +7,12 @@ import ( "net/url" "os" - "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "codeberg.org/u1f320/pronouns.cc/backend/log" "emperror.dev/errors" "github.com/Masterminds/squirrel" "github.com/jackc/pgconn" "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/log/zapadapter" "github.com/jackc/pgx/v4/pgxpool" "github.com/mediocregopher/radix/v4" "github.com/minio/minio-go/v7" @@ -23,10 +23,8 @@ var sq = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) const ErrNothingToUpdate = errors.Sentinel("nothing to update") -type querier interface { - Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) - QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row - Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error) +type Execer interface { + Exec(ctx context.Context, sql string, arguments ...interface{}) (commandTag pgconn.CommandTag, err error) } type DB struct { @@ -37,25 +35,25 @@ type DB struct { minio *minio.Client minioBucket string baseURL *url.URL - - q queries.Querier } func New() (*DB, error) { - pool, err := pgxpool.Connect(context.Background(), os.Getenv("DATABASE_URL")) + pgxCfg, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL")) + if err != nil { + return nil, errors.Wrap(err, "parsing config") + } + pgxCfg.ConnConfig.LogLevel = pgx.LogLevelDebug + pgxCfg.ConnConfig.Logger = zapadapter.NewLogger(log.Logger) + + pool, err := pgxpool.ConnectConfig(context.Background(), pgxCfg) + // pool, err := pgxpool.Connect(context.Background(), os.Getenv("DATABASE_URL")) if err != nil { return nil, errors.Wrap(err, "creating postgres client") } - var redis radix.Client - if os.Getenv("REDIS") != "" { - redis, err = (&radix.PoolConfig{}).New(context.Background(), "tcp", os.Getenv("REDIS")) - if err != nil { - return nil, errors.Wrap(err, "creating redis client") - } - } else { - log.Warn("$REDIS was empty! Any functionality using Redis (such as authentication) will not work") - redis = &dummyRedis{} + redis, err := (&radix.PoolConfig{}).New(context.Background(), "tcp", os.Getenv("REDIS")) + if err != nil { + return nil, errors.Wrap(err, "creating redis client") } minioClient, err := minio.New(os.Getenv("MINIO_ENDPOINT"), &minio.Options{ @@ -78,8 +76,6 @@ func New() (*DB, error) { minio: minioClient, minioBucket: os.Getenv("MINIO_BUCKET"), baseURL: baseURL, - - q: queries.NewQuerier(pool), } return db, nil @@ -162,3 +158,12 @@ func (db *DB) GetDelJSON(ctx context.Context, key string, v any) error { } return nil } + +// NotNull is a little helper that returns an *empty slice* when the slice's length is 0. +// This is to prevent nil slices from being marshaled as JSON null +func NotNull[T any](slice []T) []T { + if len(slice) == 0 { + return []T{} + } + return slice +} diff --git a/backend/db/entries.go b/backend/db/entries.go index 7110da7..c7cde77 100644 --- a/backend/db/entries.go +++ b/backend/db/entries.go @@ -3,8 +3,6 @@ package db import ( "fmt" "strings" - - "codeberg.org/u1f320/pronouns.cc/backend/db/queries" ) type WordStatus int @@ -80,38 +78,3 @@ func (p PronounEntry) String() string { return strings.Join(split[:1], "/") } - -func dbEntriesToFieldEntries(entries []queries.FieldEntry) []FieldEntry { - out := make([]FieldEntry, len(entries)) - for i := range entries { - out[i] = FieldEntry{ - *entries[i].Value, WordStatus(*entries[i].Status), - } - } - return out -} - -func entriesToDBEntries(entries []FieldEntry) []queries.FieldEntry { - out := make([]queries.FieldEntry, len(entries)) - for i := range entries { - status := int32(entries[i].Status) - out[i] = queries.FieldEntry{ - Value: &entries[i].Value, - Status: &status, - } - } - return out -} - -func pronounEntriesToDBEntries(entries []PronounEntry) []queries.PronounEntry { - out := make([]queries.PronounEntry, len(entries)) - for i := range entries { - status := int32(entries[i].Status) - out[i] = queries.PronounEntry{ - Value: &entries[i].Pronouns, - DisplayValue: entries[i].DisplayText, - Status: &status, - } - } - return out -} diff --git a/backend/db/field.go b/backend/db/field.go index 2fa2f34..4e5296f 100644 --- a/backend/db/field.go +++ b/backend/db/field.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "emperror.dev/errors" + "github.com/georgysavva/scany/pgxscan" "github.com/jackc/pgx/v4" "github.com/rs/xid" ) @@ -52,18 +52,14 @@ func (f Field) Validate() string { // UserFields returns the fields associated with the given user ID. func (db *DB) UserFields(ctx context.Context, id xid.ID) (fs []Field, err error) { - qfields, err := db.q.GetUserFields(ctx, id.String()) + sql, args, err := sq.Select("*").From("user_fields").Where("user_id = ?", id).OrderBy("id").ToSql() if err != nil { - return nil, errors.Wrap(err, "querying fields") + return fs, errors.Wrap(err, "building sql") } - fs = make([]Field, len(qfields)) - for i := range qfields { - fs[i] = Field{ - ID: int64(*qfields[i].ID), - Name: *qfields[i].Name, - Entries: dbEntriesToFieldEntries(qfields[i].Entries), - } + err = pgxscan.Select(ctx, db, &fs, sql, args...) + if err != nil { + return fs, errors.Wrap(err, "executing query") } return fs, nil @@ -81,34 +77,25 @@ func (db *DB) SetUserFields(ctx context.Context, tx pgx.Tx, userID xid.ID, field return errors.Wrap(err, "deleting existing fields") } - querier := queries.NewQuerier(tx) for _, field := range fields { - querier.InsertUserField(ctx, queries.InsertUserFieldParams{ - UserID: userID.String(), - Name: field.Name, - Entries: entriesToDBEntries(field.Entries), - }) - } - if err != nil { - return errors.Wrap(err, "inserting new fields") + _, err := tx.Exec(ctx, "INSERT INTO user_fields (user_id, name, entries) VALUES ($1, $2, $3)", userID, field.Name, field.Entries) + if err != nil { + return errors.Wrap(err, "inserting new fields") + } } return nil } // MemberFields returns the fields associated with the given member ID. func (db *DB) MemberFields(ctx context.Context, id xid.ID) (fs []Field, err error) { - qfields, err := db.q.GetMemberFields(ctx, id.String()) + sql, args, err := sq.Select("*").From("member_fields").Where("member_id = ?", id).OrderBy("id").ToSql() if err != nil { - return nil, errors.Wrap(err, "querying fields") + return fs, errors.Wrap(err, "building sql") } - fs = make([]Field, len(qfields)) - for i := range qfields { - fs[i] = Field{ - ID: int64(*qfields[i].ID), - Name: *qfields[i].Name, - Entries: dbEntriesToFieldEntries(qfields[i].Entries), - } + err = pgxscan.Select(ctx, db, &fs, sql, args...) + if err != nil { + return fs, errors.Wrap(err, "executing query") } return fs, nil @@ -126,16 +113,11 @@ func (db *DB) SetMemberFields(ctx context.Context, tx pgx.Tx, memberID xid.ID, f return errors.Wrap(err, "deleting existing fields") } - querier := queries.NewQuerier(tx) for _, field := range fields { - querier.InsertMemberField(ctx, queries.InsertMemberFieldParams{ - MemberID: memberID.String(), - Name: field.Name, - Entries: entriesToDBEntries(field.Entries), - }) - } - if err != nil { - return errors.Wrap(err, "inserting new fields") + _, err := tx.Exec(ctx, "INSERT INTO member_fields (member_id, name, entries) VALUES ($1, $2, $3)", memberID, field.Name, field.Entries) + if err != nil { + return errors.Wrap(err, "inserting new fields") + } } return nil } diff --git a/backend/db/member.go b/backend/db/member.go index b700d79..2c54a13 100644 --- a/backend/db/member.go +++ b/backend/db/member.go @@ -3,7 +3,6 @@ package db import ( "context" - "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "emperror.dev/errors" "github.com/georgysavva/scany/pgxscan" "github.com/jackc/pgconn" @@ -33,64 +32,36 @@ const ( ErrMemberNameInUse = errors.Sentinel("member name already in use") ) -func (db *DB) getMember(ctx context.Context, q querier, id xid.ID) (m Member, err error) { - qm, err := queries.NewQuerier(q).GetMemberByID(ctx, id.String()) +func (db *DB) Member(ctx context.Context, id xid.ID) (m Member, err error) { + sql, args, err := sq.Select("*").From("members").Where("id = ?", id).ToSql() if err != nil { - return m, errors.Wrap(err, "getting member from db") + return m, errors.Wrap(err, "building sql") } - userID, err := xid.FromString(qm.UserID) + err = pgxscan.Get(ctx, db, &m, sql, args...) if err != nil { - return m, errors.Wrap(err, "parsing user ID") - } - - m = Member{ - ID: id, - UserID: userID, - Name: qm.Name, - DisplayName: qm.DisplayName, - Bio: qm.Bio, - AvatarURLs: qm.AvatarUrls, - Links: qm.Links, - Names: fieldEntriesFromDB(qm.Names), - Pronouns: pronounsFromDB(qm.Pronouns), + return m, errors.Wrap(err, "executing query") } return m, nil } -func (db *DB) Member(ctx context.Context, id xid.ID) (m Member, err error) { - return db.getMember(ctx, db, id) -} - // UserMember returns a member scoped by user. func (db *DB) UserMember(ctx context.Context, userID xid.ID, memberRef string) (m Member, err error) { - qm, err := db.q.GetMemberByName(ctx, userID.String(), memberRef) + sql, args, err := sq.Select("*").From("members").Where("user_id = ?", userID).Where("(id = ? or name = ?)", memberRef, memberRef).ToSql() if err != nil { - return m, errors.Wrap(err, "getting member from db") + return m, errors.Wrap(err, "building sql") } - memberID, err := xid.FromString(qm.ID) + err = pgxscan.Get(ctx, db, &m, sql, args...) if err != nil { - return m, errors.Wrap(err, "parsing member ID") - } - - m = Member{ - ID: memberID, - UserID: userID, - Name: qm.Name, - DisplayName: qm.DisplayName, - Bio: qm.Bio, - AvatarURLs: qm.AvatarUrls, - Links: qm.Links, - Names: fieldEntriesFromDB(qm.Names), - Pronouns: pronounsFromDB(qm.Pronouns), + return m, errors.Wrap(err, "executing query") } return m, nil } // UserMembers returns all of a user's members, sorted by name. func (db *DB) UserMembers(ctx context.Context, userID xid.ID) (ms []Member, err error) { - sql, args, err := sq.Select("id", "user_id", "name", "display_name", "bio", "avatar_urls"). + sql, args, err := sq.Select("id", "user_id", "name", "display_name", "bio", "avatar_urls", "names", "pronouns"). From("members").Where("user_id = ?", userID). OrderBy("name", "id").ToSql() if err != nil { @@ -109,17 +80,19 @@ func (db *DB) UserMembers(ctx context.Context, userID xid.ID) (ms []Member, err } // CreateMember creates a member. -func (db *DB) CreateMember(ctx context.Context, tx pgx.Tx, userID xid.ID, name string, displayName *string, bio string, links []string) (m Member, err error) { +func (db *DB) CreateMember( + ctx context.Context, tx pgx.Tx, userID xid.ID, + name string, displayName *string, bio string, links []string, +) (m Member, err error) { sql, args, err := sq.Insert("members"). Columns("user_id", "id", "name", "display_name", "bio", "links"). Values(userID, xid.New(), name, displayName, bio, links). - Suffix("RETURNING id").ToSql() + Suffix("RETURNING *").ToSql() if err != nil { return m, errors.Wrap(err, "building sql") } - var id xid.ID - err = tx.QueryRow(ctx, sql, args...).Scan(&id) + err = pgxscan.Get(ctx, tx, &m, sql, args...) if err != nil { pge := &pgconn.PgError{} if errors.As(err, &pge) { @@ -131,12 +104,6 @@ func (db *DB) CreateMember(ctx context.Context, tx pgx.Tx, userID xid.ID, name s return m, errors.Wrap(err, "executing query") } - - m, err = db.getMember(ctx, tx, id) - if err != nil { - return m, errors.Wrap(err, "getting created member") - } - return m, nil } @@ -177,10 +144,20 @@ func (db *DB) UpdateMember( avatarURLs []string, ) (m Member, err error) { if name == nil && displayName == nil && bio == nil && links == nil && avatarURLs == nil { - return db.getMember(ctx, tx, id) + // get member + sql, args, err := sq.Select("*").From("members").Where("id = ?", id).ToSql() + if err != nil { + return m, errors.Wrap(err, "building sql") + } + + err = pgxscan.Get(ctx, tx, &m, sql, args...) + if err != nil { + return m, errors.Wrap(err, "executing query") + } + return m, nil } - builder := sq.Update("members").Where("id = ?", id) + builder := sq.Update("members").Where("id = ?", id).Suffix("RETURNING *") if name != nil { if *name == "" { builder = builder.Set("name", nil) @@ -203,19 +180,11 @@ func (db *DB) UpdateMember( } } if links != nil { - if len(*links) == 0 { - builder = builder.Set("links", nil) - } else { - builder = builder.Set("links", *links) - } + builder = builder.Set("links", *links) } if avatarURLs != nil { - if len(avatarURLs) == 0 { - builder = builder.Set("avatar_urls", nil) - } else { - builder = builder.Set("avatar_urls", avatarURLs) - } + builder = builder.Set("avatar_urls", avatarURLs) } sql, args, err := builder.ToSql() @@ -223,7 +192,7 @@ func (db *DB) UpdateMember( return m, errors.Wrap(err, "building sql") } - _, err = tx.Exec(ctx, sql, args...) + err = pgxscan.Get(ctx, tx, &m, sql, args...) if err != nil { pge := &pgconn.PgError{} if errors.As(err, &pge) { @@ -234,11 +203,5 @@ func (db *DB) UpdateMember( return m, errors.Wrap(err, "executing sql") } - - m, err = db.getMember(ctx, tx, id) - if err != nil { - return m, errors.Wrap(err, "getting member") - } - return m, nil } diff --git a/backend/db/names_pronouns.go b/backend/db/names_pronouns.go index c58b0ad..0525555 100644 --- a/backend/db/names_pronouns.go +++ b/backend/db/names_pronouns.go @@ -3,55 +3,33 @@ package db import ( "context" - "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "emperror.dev/errors" "github.com/jackc/pgx/v4" "github.com/rs/xid" ) func (db *DB) SetUserNamesPronouns(ctx context.Context, tx pgx.Tx, userID xid.ID, names []FieldEntry, pronouns []PronounEntry) (err error) { - _, err = queries.NewQuerier(tx).UpdateUserNamesPronouns(ctx, queries.UpdateUserNamesPronounsParams{ - ID: userID.String(), - Names: entriesToDBEntries(names), - Pronouns: pronounEntriesToDBEntries(pronouns), - }) + sql, args, err := sq.Update("users").Set("names", names).Set("pronouns", pronouns).Where("id = ?", userID).ToSql() if err != nil { - return errors.Wrap(err, "executing update names/pronouns query") + return errors.Wrap(err, "building sql") + } + + _, err = tx.Exec(ctx, sql, args...) + if err != nil { + return errors.Wrap(err, "executing query") } return nil } func (db *DB) SetMemberNamesPronouns(ctx context.Context, tx pgx.Tx, memberID xid.ID, names []FieldEntry, pronouns []PronounEntry) (err error) { - _, err = queries.NewQuerier(tx).UpdateMemberNamesPronouns(ctx, queries.UpdateMemberNamesPronounsParams{ - ID: memberID.String(), - Names: entriesToDBEntries(names), - Pronouns: pronounEntriesToDBEntries(pronouns), - }) + sql, args, err := sq.Update("members").Set("names", names).Set("pronouns", pronouns).Where("id = ?", memberID).ToSql() if err != nil { - return errors.Wrap(err, "executing update names/pronouns query") + return errors.Wrap(err, "building sql") + } + + _, err = tx.Exec(ctx, sql, args...) + if err != nil { + return errors.Wrap(err, "executing query") } return nil } - -func fieldEntriesFromDB(dn []queries.FieldEntry) []FieldEntry { - names := make([]FieldEntry, len(dn)) - for i := range dn { - names[i] = FieldEntry{ - Value: *dn[i].Value, - Status: WordStatus(*dn[i].Status), - } - } - return names -} - -func pronounsFromDB(dn []queries.PronounEntry) []PronounEntry { - pronouns := make([]PronounEntry, len(dn)) - for i := range dn { - pronouns[i] = PronounEntry{ - DisplayText: dn[i].DisplayValue, - Pronouns: *dn[i].Value, - Status: WordStatus(*dn[i].Status), - } - } - return pronouns -} diff --git a/backend/db/queries/generate.go b/backend/db/queries/generate.go deleted file mode 100644 index da76eb1..0000000 --- a/backend/db/queries/generate.go +++ /dev/null @@ -1,3 +0,0 @@ -package queries - -//go:generate pggen gen go --query-glob queries.user.sql --query-glob queries.member.sql --postgres-connection "postgres://pggen:pggen@localhost/pggen" diff --git a/backend/db/queries/queries.member.sql b/backend/db/queries/queries.member.sql deleted file mode 100644 index c88bf91..0000000 --- a/backend/db/queries/queries.member.sql +++ /dev/null @@ -1,31 +0,0 @@ --- name: GetMemberByID :one -SELECT * FROM members -WHERE id = pggen.arg('id'); - --- name: GetMemberByName :one -SELECT * FROM members -WHERE user_id = pggen.arg('user_id') AND ( - id = pggen.arg('member_ref') - OR name = pggen.arg('member_ref') -); - --- name: GetMembers :many -SELECT * FROM members -WHERE user_id = pggen.arg('user_id') -ORDER BY name, id; - --- name: UpdateMemberNamesPronouns :one -UPDATE members SET -names = pggen.arg('names'), -pronouns = pggen.arg('pronouns') -WHERE id = pggen.arg('id') -RETURNING *; - --- name: GetMemberFields :many -SELECT * FROM member_fields WHERE member_id = pggen.arg('member_id') ORDER BY id ASC; - --- name: InsertMemberField :one -INSERT INTO member_fields -(member_id, name, entries) VALUES -(pggen.arg('member_id'), pggen.arg('name'), pggen.arg('entries')) -RETURNING *; diff --git a/backend/db/queries/queries.member.sql.go b/backend/db/queries/queries.member.sql.go deleted file mode 100644 index 08a30a0..0000000 --- a/backend/db/queries/queries.member.sql.go +++ /dev/null @@ -1,803 +0,0 @@ -// Code generated by pggen. DO NOT EDIT. - -package queries - -import ( - "context" - "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" -) - -// Querier is a typesafe Go interface backed by SQL queries. -// -// Methods ending with Batch enqueue a query to run later in a pgx.Batch. After -// calling SendBatch on pgx.Conn, pgxpool.Pool, or pgx.Tx, use the Scan methods -// to parse the results. -type Querier interface { - GetMemberByID(ctx context.Context, id string) (GetMemberByIDRow, error) - // GetMemberByIDBatch enqueues a GetMemberByID query into batch to be executed - // later by the batch. - GetMemberByIDBatch(batch genericBatch, id string) - // GetMemberByIDScan scans the result of an executed GetMemberByIDBatch query. - GetMemberByIDScan(results pgx.BatchResults) (GetMemberByIDRow, error) - - GetMemberByName(ctx context.Context, userID string, memberRef string) (GetMemberByNameRow, error) - // GetMemberByNameBatch enqueues a GetMemberByName query into batch to be executed - // later by the batch. - GetMemberByNameBatch(batch genericBatch, userID string, memberRef string) - // GetMemberByNameScan scans the result of an executed GetMemberByNameBatch query. - GetMemberByNameScan(results pgx.BatchResults) (GetMemberByNameRow, error) - - GetMembers(ctx context.Context, userID string) ([]GetMembersRow, error) - // GetMembersBatch enqueues a GetMembers query into batch to be executed - // later by the batch. - GetMembersBatch(batch genericBatch, userID string) - // GetMembersScan scans the result of an executed GetMembersBatch query. - GetMembersScan(results pgx.BatchResults) ([]GetMembersRow, error) - - UpdateMemberNamesPronouns(ctx context.Context, params UpdateMemberNamesPronounsParams) (UpdateMemberNamesPronounsRow, error) - // UpdateMemberNamesPronounsBatch enqueues a UpdateMemberNamesPronouns query into batch to be executed - // later by the batch. - UpdateMemberNamesPronounsBatch(batch genericBatch, params UpdateMemberNamesPronounsParams) - // UpdateMemberNamesPronounsScan scans the result of an executed UpdateMemberNamesPronounsBatch query. - UpdateMemberNamesPronounsScan(results pgx.BatchResults) (UpdateMemberNamesPronounsRow, error) - - GetMemberFields(ctx context.Context, memberID string) ([]GetMemberFieldsRow, error) - // GetMemberFieldsBatch enqueues a GetMemberFields query into batch to be executed - // later by the batch. - GetMemberFieldsBatch(batch genericBatch, memberID string) - // GetMemberFieldsScan scans the result of an executed GetMemberFieldsBatch query. - GetMemberFieldsScan(results pgx.BatchResults) ([]GetMemberFieldsRow, error) - - InsertMemberField(ctx context.Context, params InsertMemberFieldParams) (InsertMemberFieldRow, error) - // InsertMemberFieldBatch enqueues a InsertMemberField query into batch to be executed - // later by the batch. - InsertMemberFieldBatch(batch genericBatch, params InsertMemberFieldParams) - // InsertMemberFieldScan scans the result of an executed InsertMemberFieldBatch query. - InsertMemberFieldScan(results pgx.BatchResults) (InsertMemberFieldRow, error) - - GetUserByID(ctx context.Context, id string) (GetUserByIDRow, error) - // GetUserByIDBatch enqueues a GetUserByID query into batch to be executed - // later by the batch. - GetUserByIDBatch(batch genericBatch, id string) - // GetUserByIDScan scans the result of an executed GetUserByIDBatch query. - GetUserByIDScan(results pgx.BatchResults) (GetUserByIDRow, error) - - GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error) - // GetUserByUsernameBatch enqueues a GetUserByUsername query into batch to be executed - // later by the batch. - GetUserByUsernameBatch(batch genericBatch, username string) - // GetUserByUsernameScan scans the result of an executed GetUserByUsernameBatch query. - GetUserByUsernameScan(results pgx.BatchResults) (GetUserByUsernameRow, error) - - UpdateUserNamesPronouns(ctx context.Context, params UpdateUserNamesPronounsParams) (UpdateUserNamesPronounsRow, error) - // UpdateUserNamesPronounsBatch enqueues a UpdateUserNamesPronouns query into batch to be executed - // later by the batch. - UpdateUserNamesPronounsBatch(batch genericBatch, params UpdateUserNamesPronounsParams) - // UpdateUserNamesPronounsScan scans the result of an executed UpdateUserNamesPronounsBatch query. - UpdateUserNamesPronounsScan(results pgx.BatchResults) (UpdateUserNamesPronounsRow, error) - - GetUserFields(ctx context.Context, userID string) ([]GetUserFieldsRow, error) - // GetUserFieldsBatch enqueues a GetUserFields query into batch to be executed - // later by the batch. - GetUserFieldsBatch(batch genericBatch, userID string) - // GetUserFieldsScan scans the result of an executed GetUserFieldsBatch query. - GetUserFieldsScan(results pgx.BatchResults) ([]GetUserFieldsRow, error) - - InsertUserField(ctx context.Context, params InsertUserFieldParams) (InsertUserFieldRow, error) - // InsertUserFieldBatch enqueues a InsertUserField query into batch to be executed - // later by the batch. - InsertUserFieldBatch(batch genericBatch, params InsertUserFieldParams) - // InsertUserFieldScan scans the result of an executed InsertUserFieldBatch query. - InsertUserFieldScan(results pgx.BatchResults) (InsertUserFieldRow, error) -} - -type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name -} - -var _ Querier = &DBQuerier{} - -// genericConn is a connection to a Postgres database. This is usually backed by -// *pgx.Conn, pgx.Tx, or *pgxpool.Pool. -type genericConn interface { - // Query executes sql with args. If there is an error the returned Rows will - // be returned in an error state. So it is allowed to ignore the error - // returned from Query and handle it in Rows. - Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) - - // QueryRow is a convenience wrapper over Query. Any error that occurs while - // querying is deferred until calling Scan on the returned Row. That Row will - // error with pgx.ErrNoRows if no rows are returned. - QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row - - // Exec executes sql. sql can be either a prepared statement name or an SQL - // string. arguments should be referenced positionally from the sql string - // as $1, $2, etc. - Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error) -} - -// genericBatch batches queries to send in a single network request to a -// Postgres server. This is usually backed by *pgx.Batch. -type genericBatch interface { - // Queue queues a query to batch b. query can be an SQL query or the name of a - // prepared statement. See Queue on *pgx.Batch. - Queue(query string, arguments ...interface{}) -} - -// NewQuerier creates a DBQuerier that implements Querier. conn is typically -// *pgx.Conn, pgx.Tx, or *pgxpool.Pool. -func NewQuerier(conn genericConn) *DBQuerier { - return NewQuerierConfig(conn, QuerierConfig{}) -} - -type QuerierConfig struct { - // DataTypes contains pgtype.Value to use for encoding and decoding instead - // of pggen-generated pgtype.ValueTranscoder. - // - // If OIDs are available for an input parameter type and all of its - // transitive dependencies, pggen will use the binary encoding format for - // the input parameter. - DataTypes []pgtype.DataType -} - -// NewQuerierConfig creates a DBQuerier that implements Querier with the given -// config. conn is typically *pgx.Conn, pgx.Tx, or *pgxpool.Pool. -func NewQuerierConfig(conn genericConn, cfg QuerierConfig) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver(cfg.DataTypes)} -} - -// WithTx creates a new DBQuerier that uses the transaction to run all queries. -func (q *DBQuerier) WithTx(tx pgx.Tx) (*DBQuerier, error) { - return &DBQuerier{conn: tx}, nil -} - -// preparer is any Postgres connection transport that provides a way to prepare -// a statement, most commonly *pgx.Conn. -type preparer interface { - Prepare(ctx context.Context, name, sql string) (sd *pgconn.StatementDescription, err error) -} - -// PrepareAllQueries executes a PREPARE statement for all pggen generated SQL -// queries in querier files. Typical usage is as the AfterConnect callback -// for pgxpool.Config -// -// pgx will use the prepared statement if available. Calling PrepareAllQueries -// is an optional optimization to avoid a network round-trip the first time pgx -// runs a query if pgx statement caching is enabled. -func PrepareAllQueries(ctx context.Context, p preparer) error { - if _, err := p.Prepare(ctx, getMemberByIDSQL, getMemberByIDSQL); err != nil { - return fmt.Errorf("prepare query 'GetMemberByID': %w", err) - } - if _, err := p.Prepare(ctx, getMemberByNameSQL, getMemberByNameSQL); err != nil { - return fmt.Errorf("prepare query 'GetMemberByName': %w", err) - } - if _, err := p.Prepare(ctx, getMembersSQL, getMembersSQL); err != nil { - return fmt.Errorf("prepare query 'GetMembers': %w", err) - } - if _, err := p.Prepare(ctx, updateMemberNamesPronounsSQL, updateMemberNamesPronounsSQL); err != nil { - return fmt.Errorf("prepare query 'UpdateMemberNamesPronouns': %w", err) - } - if _, err := p.Prepare(ctx, getMemberFieldsSQL, getMemberFieldsSQL); err != nil { - return fmt.Errorf("prepare query 'GetMemberFields': %w", err) - } - if _, err := p.Prepare(ctx, insertMemberFieldSQL, insertMemberFieldSQL); err != nil { - return fmt.Errorf("prepare query 'InsertMemberField': %w", err) - } - if _, err := p.Prepare(ctx, getUserByIDSQL, getUserByIDSQL); err != nil { - return fmt.Errorf("prepare query 'GetUserByID': %w", err) - } - if _, err := p.Prepare(ctx, getUserByUsernameSQL, getUserByUsernameSQL); err != nil { - return fmt.Errorf("prepare query 'GetUserByUsername': %w", err) - } - if _, err := p.Prepare(ctx, updateUserNamesPronounsSQL, updateUserNamesPronounsSQL); err != nil { - return fmt.Errorf("prepare query 'UpdateUserNamesPronouns': %w", err) - } - if _, err := p.Prepare(ctx, getUserFieldsSQL, getUserFieldsSQL); err != nil { - return fmt.Errorf("prepare query 'GetUserFields': %w", err) - } - if _, err := p.Prepare(ctx, insertUserFieldSQL, insertUserFieldSQL); err != nil { - return fmt.Errorf("prepare query 'InsertUserField': %w", err) - } - return nil -} - -// FieldEntry represents the Postgres composite type "field_entry". -type FieldEntry struct { - Value *string `json:"value"` - Status *int32 `json:"status"` -} - -// PronounEntry represents the Postgres composite type "pronoun_entry". -type PronounEntry struct { - Value *string `json:"value"` - DisplayValue *string `json:"display_value"` - Status *int32 `json:"status"` -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver(types []pgtype.DataType) *typeResolver { - ci := pgtype.NewConnInfo() - for _, typ := range types { - if txt, ok := typ.Value.(textPreferrer); ok && typ.OID != unknownOID { - typ.Value = txt.ValueTranscoder - } - ci.RegisterDataType(typ) - } - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true -} - -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} - -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use -} - -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} - -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} - -// newFieldEntry creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'field_entry'. -func (tr *typeResolver) newFieldEntry() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "field_entry", - compositeField{name: "value", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "status", typeName: "int4", defaultVal: &pgtype.Int4{}}, - ) -} - -// newFieldEntryRaw returns all composite fields for the Postgres composite -// type 'field_entry' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newFieldEntryRaw(v FieldEntry) []interface{} { - return []interface{}{ - v.Value, - v.Status, - } -} - -// newPronounEntry creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'pronoun_entry'. -func (tr *typeResolver) newPronounEntry() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "pronoun_entry", - compositeField{name: "value", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "display_value", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "status", typeName: "int4", defaultVal: &pgtype.Int4{}}, - ) -} - -// newPronounEntryRaw returns all composite fields for the Postgres composite -// type 'pronoun_entry' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newPronounEntryRaw(v PronounEntry) []interface{} { - return []interface{}{ - v.Value, - v.DisplayValue, - v.Status, - } -} - -// newFieldEntryArray creates a new pgtype.ValueTranscoder for the Postgres -// '_field_entry' array type. -func (tr *typeResolver) newFieldEntryArray() pgtype.ValueTranscoder { - return tr.newArrayValue("_field_entry", "field_entry", tr.newFieldEntry) -} - -// newFieldEntryArrayInit creates an initialized pgtype.ValueTranscoder for the -// Postgres array type '_field_entry' to encode query parameters. -func (tr *typeResolver) newFieldEntryArrayInit(ps []FieldEntry) pgtype.ValueTranscoder { - dec := tr.newFieldEntryArray() - if err := dec.Set(tr.newFieldEntryArrayRaw(ps)); err != nil { - panic("encode []FieldEntry: " + err.Error()) // should always succeed - } - return textPreferrer{ValueTranscoder: dec, typeName: "_field_entry"} -} - -// newFieldEntryArrayRaw returns all elements for the Postgres array type '_field_entry' -// as a slice of interface{} for use with the pgtype.Value Set method. -func (tr *typeResolver) newFieldEntryArrayRaw(vs []FieldEntry) []interface{} { - elems := make([]interface{}, len(vs)) - for i, v := range vs { - elems[i] = tr.newFieldEntryRaw(v) - } - return elems -} - -// newPronounEntryArray creates a new pgtype.ValueTranscoder for the Postgres -// '_pronoun_entry' array type. -func (tr *typeResolver) newPronounEntryArray() pgtype.ValueTranscoder { - return tr.newArrayValue("_pronoun_entry", "pronoun_entry", tr.newPronounEntry) -} - -// newPronounEntryArrayInit creates an initialized pgtype.ValueTranscoder for the -// Postgres array type '_pronoun_entry' to encode query parameters. -func (tr *typeResolver) newPronounEntryArrayInit(ps []PronounEntry) pgtype.ValueTranscoder { - dec := tr.newPronounEntryArray() - if err := dec.Set(tr.newPronounEntryArrayRaw(ps)); err != nil { - panic("encode []PronounEntry: " + err.Error()) // should always succeed - } - return textPreferrer{ValueTranscoder: dec, typeName: "_pronoun_entry"} -} - -// newPronounEntryArrayRaw returns all elements for the Postgres array type '_pronoun_entry' -// as a slice of interface{} for use with the pgtype.Value Set method. -func (tr *typeResolver) newPronounEntryArrayRaw(vs []PronounEntry) []interface{} { - elems := make([]interface{}, len(vs)) - for i, v := range vs { - elems[i] = tr.newPronounEntryRaw(v) - } - return elems -} - -const getMemberByIDSQL = `SELECT * FROM members -WHERE id = $1;` - -type GetMemberByIDRow struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Name string `json:"name"` - Bio *string `json:"bio"` - AvatarUrls []string `json:"avatar_urls"` - Links []string `json:"links"` - DisplayName *string `json:"display_name"` - Names []FieldEntry `json:"names"` - Pronouns []PronounEntry `json:"pronouns"` -} - -// GetMemberByID implements Querier.GetMemberByID. -func (q *DBQuerier) GetMemberByID(ctx context.Context, id string) (GetMemberByIDRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetMemberByID") - row := q.conn.QueryRow(ctx, getMemberByIDSQL, id) - var item GetMemberByIDRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { - return item, fmt.Errorf("query GetMemberByID: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign GetMemberByID row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign GetMemberByID row: %w", err) - } - return item, nil -} - -// GetMemberByIDBatch implements Querier.GetMemberByIDBatch. -func (q *DBQuerier) GetMemberByIDBatch(batch genericBatch, id string) { - batch.Queue(getMemberByIDSQL, id) -} - -// GetMemberByIDScan implements Querier.GetMemberByIDScan. -func (q *DBQuerier) GetMemberByIDScan(results pgx.BatchResults) (GetMemberByIDRow, error) { - row := results.QueryRow() - var item GetMemberByIDRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { - return item, fmt.Errorf("scan GetMemberByIDBatch row: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign GetMemberByID row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign GetMemberByID row: %w", err) - } - return item, nil -} - -const getMemberByNameSQL = `SELECT * FROM members -WHERE user_id = $1 AND ( - id = $2 - OR name = $2 -);` - -type GetMemberByNameRow struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Name string `json:"name"` - Bio *string `json:"bio"` - AvatarUrls []string `json:"avatar_urls"` - Links []string `json:"links"` - DisplayName *string `json:"display_name"` - Names []FieldEntry `json:"names"` - Pronouns []PronounEntry `json:"pronouns"` -} - -// GetMemberByName implements Querier.GetMemberByName. -func (q *DBQuerier) GetMemberByName(ctx context.Context, userID string, memberRef string) (GetMemberByNameRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetMemberByName") - row := q.conn.QueryRow(ctx, getMemberByNameSQL, userID, memberRef) - var item GetMemberByNameRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { - return item, fmt.Errorf("query GetMemberByName: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign GetMemberByName row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign GetMemberByName row: %w", err) - } - return item, nil -} - -// GetMemberByNameBatch implements Querier.GetMemberByNameBatch. -func (q *DBQuerier) GetMemberByNameBatch(batch genericBatch, userID string, memberRef string) { - batch.Queue(getMemberByNameSQL, userID, memberRef) -} - -// GetMemberByNameScan implements Querier.GetMemberByNameScan. -func (q *DBQuerier) GetMemberByNameScan(results pgx.BatchResults) (GetMemberByNameRow, error) { - row := results.QueryRow() - var item GetMemberByNameRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { - return item, fmt.Errorf("scan GetMemberByNameBatch row: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign GetMemberByName row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign GetMemberByName row: %w", err) - } - return item, nil -} - -const getMembersSQL = `SELECT * FROM members -WHERE user_id = $1 -ORDER BY name, id;` - -type GetMembersRow struct { - ID *string `json:"id"` - UserID *string `json:"user_id"` - Name *string `json:"name"` - Bio *string `json:"bio"` - AvatarUrls []string `json:"avatar_urls"` - Links []string `json:"links"` - DisplayName *string `json:"display_name"` - Names []FieldEntry `json:"names"` - Pronouns []PronounEntry `json:"pronouns"` -} - -// GetMembers implements Querier.GetMembers. -func (q *DBQuerier) GetMembers(ctx context.Context, userID string) ([]GetMembersRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetMembers") - rows, err := q.conn.Query(ctx, getMembersSQL, userID) - if err != nil { - return nil, fmt.Errorf("query GetMembers: %w", err) - } - defer rows.Close() - items := []GetMembersRow{} - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - for rows.Next() { - var item GetMembersRow - if err := rows.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { - return nil, fmt.Errorf("scan GetMembers row: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return nil, fmt.Errorf("assign GetMembers row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return nil, fmt.Errorf("assign GetMembers row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GetMembers rows: %w", err) - } - return items, err -} - -// GetMembersBatch implements Querier.GetMembersBatch. -func (q *DBQuerier) GetMembersBatch(batch genericBatch, userID string) { - batch.Queue(getMembersSQL, userID) -} - -// GetMembersScan implements Querier.GetMembersScan. -func (q *DBQuerier) GetMembersScan(results pgx.BatchResults) ([]GetMembersRow, error) { - rows, err := results.Query() - if err != nil { - return nil, fmt.Errorf("query GetMembersBatch: %w", err) - } - defer rows.Close() - items := []GetMembersRow{} - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - for rows.Next() { - var item GetMembersRow - if err := rows.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { - return nil, fmt.Errorf("scan GetMembersBatch row: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return nil, fmt.Errorf("assign GetMembers row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return nil, fmt.Errorf("assign GetMembers row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GetMembersBatch rows: %w", err) - } - return items, err -} - -const updateMemberNamesPronounsSQL = `UPDATE members SET -names = $1, -pronouns = $2 -WHERE id = $3 -RETURNING *;` - -type UpdateMemberNamesPronounsParams struct { - Names []FieldEntry - Pronouns []PronounEntry - ID string -} - -type UpdateMemberNamesPronounsRow struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Name string `json:"name"` - Bio *string `json:"bio"` - AvatarUrls []string `json:"avatar_urls"` - Links []string `json:"links"` - DisplayName *string `json:"display_name"` - Names []FieldEntry `json:"names"` - Pronouns []PronounEntry `json:"pronouns"` -} - -// UpdateMemberNamesPronouns implements Querier.UpdateMemberNamesPronouns. -func (q *DBQuerier) UpdateMemberNamesPronouns(ctx context.Context, params UpdateMemberNamesPronounsParams) (UpdateMemberNamesPronounsRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "UpdateMemberNamesPronouns") - row := q.conn.QueryRow(ctx, updateMemberNamesPronounsSQL, q.types.newFieldEntryArrayInit(params.Names), q.types.newPronounEntryArrayInit(params.Pronouns), params.ID) - var item UpdateMemberNamesPronounsRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { - return item, fmt.Errorf("query UpdateMemberNamesPronouns: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign UpdateMemberNamesPronouns row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign UpdateMemberNamesPronouns row: %w", err) - } - return item, nil -} - -// UpdateMemberNamesPronounsBatch implements Querier.UpdateMemberNamesPronounsBatch. -func (q *DBQuerier) UpdateMemberNamesPronounsBatch(batch genericBatch, params UpdateMemberNamesPronounsParams) { - batch.Queue(updateMemberNamesPronounsSQL, q.types.newFieldEntryArrayInit(params.Names), q.types.newPronounEntryArrayInit(params.Pronouns), params.ID) -} - -// UpdateMemberNamesPronounsScan implements Querier.UpdateMemberNamesPronounsScan. -func (q *DBQuerier) UpdateMemberNamesPronounsScan(results pgx.BatchResults) (UpdateMemberNamesPronounsRow, error) { - row := results.QueryRow() - var item UpdateMemberNamesPronounsRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Bio, &item.AvatarUrls, &item.Links, &item.DisplayName, namesArray, pronounsArray); err != nil { - return item, fmt.Errorf("scan UpdateMemberNamesPronounsBatch row: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign UpdateMemberNamesPronouns row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign UpdateMemberNamesPronouns row: %w", err) - } - return item, nil -} - -const getMemberFieldsSQL = `SELECT * FROM member_fields WHERE member_id = $1 ORDER BY id ASC;` - -type GetMemberFieldsRow struct { - MemberID *string `json:"member_id"` - ID *int `json:"id"` - Name *string `json:"name"` - Entries []FieldEntry `json:"entries"` -} - -// GetMemberFields implements Querier.GetMemberFields. -func (q *DBQuerier) GetMemberFields(ctx context.Context, memberID string) ([]GetMemberFieldsRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetMemberFields") - rows, err := q.conn.Query(ctx, getMemberFieldsSQL, memberID) - if err != nil { - return nil, fmt.Errorf("query GetMemberFields: %w", err) - } - defer rows.Close() - items := []GetMemberFieldsRow{} - entriesArray := q.types.newFieldEntryArray() - for rows.Next() { - var item GetMemberFieldsRow - if err := rows.Scan(&item.MemberID, &item.ID, &item.Name, entriesArray); err != nil { - return nil, fmt.Errorf("scan GetMemberFields row: %w", err) - } - if err := entriesArray.AssignTo(&item.Entries); err != nil { - return nil, fmt.Errorf("assign GetMemberFields row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GetMemberFields rows: %w", err) - } - return items, err -} - -// GetMemberFieldsBatch implements Querier.GetMemberFieldsBatch. -func (q *DBQuerier) GetMemberFieldsBatch(batch genericBatch, memberID string) { - batch.Queue(getMemberFieldsSQL, memberID) -} - -// GetMemberFieldsScan implements Querier.GetMemberFieldsScan. -func (q *DBQuerier) GetMemberFieldsScan(results pgx.BatchResults) ([]GetMemberFieldsRow, error) { - rows, err := results.Query() - if err != nil { - return nil, fmt.Errorf("query GetMemberFieldsBatch: %w", err) - } - defer rows.Close() - items := []GetMemberFieldsRow{} - entriesArray := q.types.newFieldEntryArray() - for rows.Next() { - var item GetMemberFieldsRow - if err := rows.Scan(&item.MemberID, &item.ID, &item.Name, entriesArray); err != nil { - return nil, fmt.Errorf("scan GetMemberFieldsBatch row: %w", err) - } - if err := entriesArray.AssignTo(&item.Entries); err != nil { - return nil, fmt.Errorf("assign GetMemberFields row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GetMemberFieldsBatch rows: %w", err) - } - return items, err -} - -const insertMemberFieldSQL = `INSERT INTO member_fields -(member_id, name, entries) VALUES -($1, $2, $3) -RETURNING *;` - -type InsertMemberFieldParams struct { - MemberID string - Name string - Entries []FieldEntry -} - -type InsertMemberFieldRow struct { - MemberID string `json:"member_id"` - ID int `json:"id"` - Name string `json:"name"` - Entries []FieldEntry `json:"entries"` -} - -// InsertMemberField implements Querier.InsertMemberField. -func (q *DBQuerier) InsertMemberField(ctx context.Context, params InsertMemberFieldParams) (InsertMemberFieldRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertMemberField") - row := q.conn.QueryRow(ctx, insertMemberFieldSQL, params.MemberID, params.Name, q.types.newFieldEntryArrayInit(params.Entries)) - var item InsertMemberFieldRow - entriesArray := q.types.newFieldEntryArray() - if err := row.Scan(&item.MemberID, &item.ID, &item.Name, entriesArray); err != nil { - return item, fmt.Errorf("query InsertMemberField: %w", err) - } - if err := entriesArray.AssignTo(&item.Entries); err != nil { - return item, fmt.Errorf("assign InsertMemberField row: %w", err) - } - return item, nil -} - -// InsertMemberFieldBatch implements Querier.InsertMemberFieldBatch. -func (q *DBQuerier) InsertMemberFieldBatch(batch genericBatch, params InsertMemberFieldParams) { - batch.Queue(insertMemberFieldSQL, params.MemberID, params.Name, q.types.newFieldEntryArrayInit(params.Entries)) -} - -// InsertMemberFieldScan implements Querier.InsertMemberFieldScan. -func (q *DBQuerier) InsertMemberFieldScan(results pgx.BatchResults) (InsertMemberFieldRow, error) { - row := results.QueryRow() - var item InsertMemberFieldRow - entriesArray := q.types.newFieldEntryArray() - if err := row.Scan(&item.MemberID, &item.ID, &item.Name, entriesArray); err != nil { - return item, fmt.Errorf("scan InsertMemberFieldBatch row: %w", err) - } - if err := entriesArray.AssignTo(&item.Entries); err != nil { - return item, fmt.Errorf("assign InsertMemberField row: %w", err) - } - return item, nil -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs if the results from QueryAllDataTypes aren't passed to -// NewQuerierConfig. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/backend/db/queries/queries.user.sql b/backend/db/queries/queries.user.sql deleted file mode 100644 index add0b9c..0000000 --- a/backend/db/queries/queries.user.sql +++ /dev/null @@ -1,21 +0,0 @@ --- name: GetUserByID :one -SELECT * FROM users WHERE id = pggen.arg('id'); - --- name: GetUserByUsername :one -SELECT * FROM users WHERE username = pggen.arg('username'); - --- name: UpdateUserNamesPronouns :one -UPDATE users SET -names = pggen.arg('names'), -pronouns = pggen.arg('pronouns') -WHERE id = pggen.arg('id') -RETURNING *; - --- name: GetUserFields :many -SELECT * FROM user_fields WHERE user_id = pggen.arg('user_id') ORDER BY id ASC; - --- name: InsertUserField :one -INSERT INTO user_fields -(user_id, name, entries) VALUES -(pggen.arg('user_id'), pggen.arg('name'), pggen.arg('entries')) -RETURNING *; diff --git a/backend/db/queries/queries.user.sql.go b/backend/db/queries/queries.user.sql.go deleted file mode 100644 index 8d983e2..0000000 --- a/backend/db/queries/queries.user.sql.go +++ /dev/null @@ -1,320 +0,0 @@ -// Code generated by pggen. DO NOT EDIT. - -package queries - -import ( - "context" - "fmt" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" -) - -const getUserByIDSQL = `SELECT * FROM users WHERE id = $1;` - -type GetUserByIDRow struct { - ID string `json:"id"` - Username string `json:"username"` - DisplayName *string `json:"display_name"` - Bio *string `json:"bio"` - AvatarUrls []string `json:"avatar_urls"` - Links []string `json:"links"` - Discord *string `json:"discord"` - DiscordUsername *string `json:"discord_username"` - MaxInvites int32 `json:"max_invites"` - Names []FieldEntry `json:"names"` - Pronouns []PronounEntry `json:"pronouns"` - DeletedAt pgtype.Timestamptz `json:"deleted_at"` - SelfDelete *bool `json:"self_delete"` - DeleteReason *string `json:"delete_reason"` -} - -// GetUserByID implements Querier.GetUserByID. -func (q *DBQuerier) GetUserByID(ctx context.Context, id string) (GetUserByIDRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetUserByID") - row := q.conn.QueryRow(ctx, getUserByIDSQL, id) - var item GetUserByIDRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil { - return item, fmt.Errorf("query GetUserByID: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign GetUserByID row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign GetUserByID row: %w", err) - } - return item, nil -} - -// GetUserByIDBatch implements Querier.GetUserByIDBatch. -func (q *DBQuerier) GetUserByIDBatch(batch genericBatch, id string) { - batch.Queue(getUserByIDSQL, id) -} - -// GetUserByIDScan implements Querier.GetUserByIDScan. -func (q *DBQuerier) GetUserByIDScan(results pgx.BatchResults) (GetUserByIDRow, error) { - row := results.QueryRow() - var item GetUserByIDRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil { - return item, fmt.Errorf("scan GetUserByIDBatch row: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign GetUserByID row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign GetUserByID row: %w", err) - } - return item, nil -} - -const getUserByUsernameSQL = `SELECT * FROM users WHERE username = $1;` - -type GetUserByUsernameRow struct { - ID string `json:"id"` - Username string `json:"username"` - DisplayName *string `json:"display_name"` - Bio *string `json:"bio"` - AvatarUrls []string `json:"avatar_urls"` - Links []string `json:"links"` - Discord *string `json:"discord"` - DiscordUsername *string `json:"discord_username"` - MaxInvites int32 `json:"max_invites"` - Names []FieldEntry `json:"names"` - Pronouns []PronounEntry `json:"pronouns"` - DeletedAt pgtype.Timestamptz `json:"deleted_at"` - SelfDelete *bool `json:"self_delete"` - DeleteReason *string `json:"delete_reason"` -} - -// GetUserByUsername implements Querier.GetUserByUsername. -func (q *DBQuerier) GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetUserByUsername") - row := q.conn.QueryRow(ctx, getUserByUsernameSQL, username) - var item GetUserByUsernameRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil { - return item, fmt.Errorf("query GetUserByUsername: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign GetUserByUsername row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign GetUserByUsername row: %w", err) - } - return item, nil -} - -// GetUserByUsernameBatch implements Querier.GetUserByUsernameBatch. -func (q *DBQuerier) GetUserByUsernameBatch(batch genericBatch, username string) { - batch.Queue(getUserByUsernameSQL, username) -} - -// GetUserByUsernameScan implements Querier.GetUserByUsernameScan. -func (q *DBQuerier) GetUserByUsernameScan(results pgx.BatchResults) (GetUserByUsernameRow, error) { - row := results.QueryRow() - var item GetUserByUsernameRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil { - return item, fmt.Errorf("scan GetUserByUsernameBatch row: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign GetUserByUsername row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign GetUserByUsername row: %w", err) - } - return item, nil -} - -const updateUserNamesPronounsSQL = `UPDATE users SET -names = $1, -pronouns = $2 -WHERE id = $3 -RETURNING *;` - -type UpdateUserNamesPronounsParams struct { - Names []FieldEntry - Pronouns []PronounEntry - ID string -} - -type UpdateUserNamesPronounsRow struct { - ID string `json:"id"` - Username string `json:"username"` - DisplayName *string `json:"display_name"` - Bio *string `json:"bio"` - AvatarUrls []string `json:"avatar_urls"` - Links []string `json:"links"` - Discord *string `json:"discord"` - DiscordUsername *string `json:"discord_username"` - MaxInvites int32 `json:"max_invites"` - Names []FieldEntry `json:"names"` - Pronouns []PronounEntry `json:"pronouns"` - DeletedAt pgtype.Timestamptz `json:"deleted_at"` - SelfDelete *bool `json:"self_delete"` - DeleteReason *string `json:"delete_reason"` -} - -// UpdateUserNamesPronouns implements Querier.UpdateUserNamesPronouns. -func (q *DBQuerier) UpdateUserNamesPronouns(ctx context.Context, params UpdateUserNamesPronounsParams) (UpdateUserNamesPronounsRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "UpdateUserNamesPronouns") - row := q.conn.QueryRow(ctx, updateUserNamesPronounsSQL, q.types.newFieldEntryArrayInit(params.Names), q.types.newPronounEntryArrayInit(params.Pronouns), params.ID) - var item UpdateUserNamesPronounsRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil { - return item, fmt.Errorf("query UpdateUserNamesPronouns: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign UpdateUserNamesPronouns row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign UpdateUserNamesPronouns row: %w", err) - } - return item, nil -} - -// UpdateUserNamesPronounsBatch implements Querier.UpdateUserNamesPronounsBatch. -func (q *DBQuerier) UpdateUserNamesPronounsBatch(batch genericBatch, params UpdateUserNamesPronounsParams) { - batch.Queue(updateUserNamesPronounsSQL, q.types.newFieldEntryArrayInit(params.Names), q.types.newPronounEntryArrayInit(params.Pronouns), params.ID) -} - -// UpdateUserNamesPronounsScan implements Querier.UpdateUserNamesPronounsScan. -func (q *DBQuerier) UpdateUserNamesPronounsScan(results pgx.BatchResults) (UpdateUserNamesPronounsRow, error) { - row := results.QueryRow() - var item UpdateUserNamesPronounsRow - namesArray := q.types.newFieldEntryArray() - pronounsArray := q.types.newPronounEntryArray() - if err := row.Scan(&item.ID, &item.Username, &item.DisplayName, &item.Bio, &item.AvatarUrls, &item.Links, &item.Discord, &item.DiscordUsername, &item.MaxInvites, namesArray, pronounsArray, &item.DeletedAt, &item.SelfDelete, &item.DeleteReason); err != nil { - return item, fmt.Errorf("scan UpdateUserNamesPronounsBatch row: %w", err) - } - if err := namesArray.AssignTo(&item.Names); err != nil { - return item, fmt.Errorf("assign UpdateUserNamesPronouns row: %w", err) - } - if err := pronounsArray.AssignTo(&item.Pronouns); err != nil { - return item, fmt.Errorf("assign UpdateUserNamesPronouns row: %w", err) - } - return item, nil -} - -const getUserFieldsSQL = `SELECT * FROM user_fields WHERE user_id = $1 ORDER BY id ASC;` - -type GetUserFieldsRow struct { - UserID *string `json:"user_id"` - ID *int `json:"id"` - Name *string `json:"name"` - Entries []FieldEntry `json:"entries"` -} - -// GetUserFields implements Querier.GetUserFields. -func (q *DBQuerier) GetUserFields(ctx context.Context, userID string) ([]GetUserFieldsRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetUserFields") - rows, err := q.conn.Query(ctx, getUserFieldsSQL, userID) - if err != nil { - return nil, fmt.Errorf("query GetUserFields: %w", err) - } - defer rows.Close() - items := []GetUserFieldsRow{} - entriesArray := q.types.newFieldEntryArray() - for rows.Next() { - var item GetUserFieldsRow - if err := rows.Scan(&item.UserID, &item.ID, &item.Name, entriesArray); err != nil { - return nil, fmt.Errorf("scan GetUserFields row: %w", err) - } - if err := entriesArray.AssignTo(&item.Entries); err != nil { - return nil, fmt.Errorf("assign GetUserFields row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GetUserFields rows: %w", err) - } - return items, err -} - -// GetUserFieldsBatch implements Querier.GetUserFieldsBatch. -func (q *DBQuerier) GetUserFieldsBatch(batch genericBatch, userID string) { - batch.Queue(getUserFieldsSQL, userID) -} - -// GetUserFieldsScan implements Querier.GetUserFieldsScan. -func (q *DBQuerier) GetUserFieldsScan(results pgx.BatchResults) ([]GetUserFieldsRow, error) { - rows, err := results.Query() - if err != nil { - return nil, fmt.Errorf("query GetUserFieldsBatch: %w", err) - } - defer rows.Close() - items := []GetUserFieldsRow{} - entriesArray := q.types.newFieldEntryArray() - for rows.Next() { - var item GetUserFieldsRow - if err := rows.Scan(&item.UserID, &item.ID, &item.Name, entriesArray); err != nil { - return nil, fmt.Errorf("scan GetUserFieldsBatch row: %w", err) - } - if err := entriesArray.AssignTo(&item.Entries); err != nil { - return nil, fmt.Errorf("assign GetUserFields row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GetUserFieldsBatch rows: %w", err) - } - return items, err -} - -const insertUserFieldSQL = `INSERT INTO user_fields -(user_id, name, entries) VALUES -($1, $2, $3) -RETURNING *;` - -type InsertUserFieldParams struct { - UserID string - Name string - Entries []FieldEntry -} - -type InsertUserFieldRow struct { - UserID string `json:"user_id"` - ID int `json:"id"` - Name string `json:"name"` - Entries []FieldEntry `json:"entries"` -} - -// InsertUserField implements Querier.InsertUserField. -func (q *DBQuerier) InsertUserField(ctx context.Context, params InsertUserFieldParams) (InsertUserFieldRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertUserField") - row := q.conn.QueryRow(ctx, insertUserFieldSQL, params.UserID, params.Name, q.types.newFieldEntryArrayInit(params.Entries)) - var item InsertUserFieldRow - entriesArray := q.types.newFieldEntryArray() - if err := row.Scan(&item.UserID, &item.ID, &item.Name, entriesArray); err != nil { - return item, fmt.Errorf("query InsertUserField: %w", err) - } - if err := entriesArray.AssignTo(&item.Entries); err != nil { - return item, fmt.Errorf("assign InsertUserField row: %w", err) - } - return item, nil -} - -// InsertUserFieldBatch implements Querier.InsertUserFieldBatch. -func (q *DBQuerier) InsertUserFieldBatch(batch genericBatch, params InsertUserFieldParams) { - batch.Queue(insertUserFieldSQL, params.UserID, params.Name, q.types.newFieldEntryArrayInit(params.Entries)) -} - -// InsertUserFieldScan implements Querier.InsertUserFieldScan. -func (q *DBQuerier) InsertUserFieldScan(results pgx.BatchResults) (InsertUserFieldRow, error) { - row := results.QueryRow() - var item InsertUserFieldRow - entriesArray := q.types.newFieldEntryArray() - if err := row.Scan(&item.UserID, &item.ID, &item.Name, entriesArray); err != nil { - return item, fmt.Errorf("scan InsertUserFieldBatch row: %w", err) - } - if err := entriesArray.AssignTo(&item.Entries); err != nil { - return item, fmt.Errorf("assign InsertUserField row: %w", err) - } - return item, nil -} diff --git a/backend/db/tokens.go b/backend/db/tokens.go index 3a1ddd5..1c8e73e 100644 --- a/backend/db/tokens.go +++ b/backend/db/tokens.go @@ -97,13 +97,13 @@ func (db *DB) InvalidateToken(ctx context.Context, userID xid.ID, tokenID xid.ID return t, nil } -func (db *DB) InvalidateAllTokens(ctx context.Context, q querier, userID xid.ID) error { +func (db *DB) InvalidateAllTokens(ctx context.Context, tx pgx.Tx, userID xid.ID) error { sql, args, err := sq.Update("tokens").Where("user_id = ?", userID).Set("invalidated", true).ToSql() if err != nil { return errors.Wrap(err, "building sql") } - _, err = q.Exec(ctx, sql, args...) + _, err = tx.Exec(ctx, sql, args...) if err != nil { return errors.Wrap(err, "executing query") } diff --git a/backend/db/user.go b/backend/db/user.go index 1070494..a62af5f 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -5,11 +5,10 @@ import ( "regexp" "time" - "codeberg.org/u1f320/pronouns.cc/backend/db/queries" "emperror.dev/errors" "github.com/bwmarrin/discordgo" + "github.com/georgysavva/scany/pgxscan" "github.com/jackc/pgconn" - "github.com/jackc/pgtype" "github.com/jackc/pgx/v4" "github.com/rs/xid" ) @@ -70,13 +69,12 @@ func (db *DB) CreateUser(ctx context.Context, tx pgx.Tx, username string) (u Use return u, ErrInvalidUsername } - sql, args, err := sq.Insert("users").Columns("id", "username").Values(xid.New(), username).Suffix("RETURNING id").ToSql() + sql, args, err := sq.Insert("users").Columns("id", "username").Values(xid.New(), username).Suffix("RETURNING *").ToSql() if err != nil { return u, errors.Wrap(err, "building sql") } - var id xid.ID - err = tx.QueryRow(ctx, sql, args...).Scan(&id) + err = pgxscan.Get(ctx, tx, &u, sql, args...) if err != nil { pge := &pgconn.PgError{} if errors.As(err, &pge) { @@ -88,31 +86,28 @@ func (db *DB) CreateUser(ctx context.Context, tx pgx.Tx, username string) (u Use return u, errors.Cause(err) } - - return db.getUser(ctx, tx, id) + return u, nil } // DiscordUser fetches a user by Discord user ID. func (db *DB) DiscordUser(ctx context.Context, discordID string) (u User, err error) { - sql, args, err := sq.Select("id").From("users").Where("discord = ?", discordID).ToSql() + sql, args, err := sq.Select("*").From("users").Where("discord = ?", discordID).ToSql() if err != nil { return u, errors.Wrap(err, "building sql") } - var id xid.ID - err = db.QueryRow(ctx, sql, args...).Scan(&id) + err = pgxscan.Get(ctx, db, &u, sql, args...) if err != nil { if errors.Cause(err) == pgx.ErrNoRows { return u, ErrUserNotFound } - return u, errors.Wrap(err, "executing id query") + return u, errors.Wrap(err, "executing query") } - - return db.getUser(ctx, db, id) + return u, nil } -func (u *User) UpdateFromDiscord(ctx context.Context, db querier, du *discordgo.User) error { +func (u *User) UpdateFromDiscord(ctx context.Context, ex Execer, du *discordgo.User) error { sql, args, err := sq.Update("users"). Set("discord", du.ID). Set("discord_username", du.String()). @@ -122,7 +117,7 @@ func (u *User) UpdateFromDiscord(ctx context.Context, db querier, du *discordgo. return errors.Wrap(err, "building sql") } - _, err = db.Exec(ctx, sql, args...) + _, err = ex.Exec(ctx, sql, args...) if err != nil { return errors.Wrap(err, "executing query") } @@ -134,49 +129,14 @@ func (u *User) UpdateFromDiscord(ctx context.Context, db querier, du *discordgo. return nil } -func (db *DB) getUser(ctx context.Context, q querier, id xid.ID) (u User, err error) { - qu, err := queries.NewQuerier(q).GetUserByID(ctx, id.String()) - if err != nil { - if errors.Cause(err) == pgx.ErrNoRows { - return u, ErrUserNotFound - } - - return u, errors.Wrap(err, "getting user from database") - } - - var deletedAt *time.Time - if qu.DeletedAt.Status == pgtype.Present { - deletedAt = &qu.DeletedAt.Time - } - - u = User{ - ID: id, - Username: qu.Username, - DisplayName: qu.DisplayName, - Bio: qu.Bio, - AvatarURLs: qu.AvatarUrls, - Names: fieldEntriesFromDB(qu.Names), - Pronouns: pronounsFromDB(qu.Pronouns), - Links: qu.Links, - Discord: qu.Discord, - DiscordUsername: qu.DiscordUsername, - MaxInvites: int(qu.MaxInvites), - DeletedAt: deletedAt, - SelfDelete: qu.SelfDelete, - DeleteReason: qu.DeleteReason, - } - - return u, nil -} - // User gets a user by ID. func (db *DB) User(ctx context.Context, id xid.ID) (u User, err error) { - return db.getUser(ctx, db, id) -} + sql, args, err := sq.Select("*").From("users").Where("id = ?", id).ToSql() + if err != nil { + return u, errors.Wrap(err, "building sql") + } -// Username gets a user by username. -func (db *DB) Username(ctx context.Context, name string) (u User, err error) { - qu, err := db.q.GetUserByUsername(ctx, name) + err = pgxscan.Get(ctx, db, &u, sql, args...) if err != nil { if errors.Cause(err) == pgx.ErrNoRows { return u, ErrUserNotFound @@ -185,23 +145,23 @@ func (db *DB) Username(ctx context.Context, name string) (u User, err error) { return u, errors.Wrap(err, "getting user from db") } - id, err := xid.FromString(qu.ID) + return u, nil +} + +// Username gets a user by username. +func (db *DB) Username(ctx context.Context, name string) (u User, err error) { + sql, args, err := sq.Select("*").From("users").Where("username = ?", name).ToSql() if err != nil { - return u, errors.Wrap(err, "parsing ID") + return u, errors.Wrap(err, "building sql") } - u = User{ - ID: id, - Username: qu.Username, - DisplayName: qu.DisplayName, - Bio: qu.Bio, - AvatarURLs: qu.AvatarUrls, - Names: fieldEntriesFromDB(qu.Names), - Pronouns: pronounsFromDB(qu.Pronouns), - Links: qu.Links, - Discord: qu.Discord, - DiscordUsername: qu.DiscordUsername, - MaxInvites: int(qu.MaxInvites), + err = pgxscan.Get(ctx, db, &u, sql, args...) + if err != nil { + if errors.Cause(err) == pgx.ErrNoRows { + return u, ErrUserNotFound + } + + return u, errors.Wrap(err, "getting user from db") } return u, nil @@ -251,10 +211,20 @@ func (db *DB) UpdateUser( avatarURLs []string, ) (u User, err error) { if displayName == nil && bio == nil && links == nil && avatarURLs == nil { - return db.getUser(ctx, tx, id) + sql, args, err := sq.Select("*").From("users").Where("id = ?", id).ToSql() + if err != nil { + return u, errors.Wrap(err, "building sql") + } + + err = pgxscan.Get(ctx, db, &u, sql, args...) + if err != nil { + return u, errors.Wrap(err, "getting user from db") + } + + return u, nil } - builder := sq.Update("users").Where("id = ?", id) + builder := sq.Update("users").Where("id = ?", id).Suffix("RETURNING *") if displayName != nil { if *displayName == "" { builder = builder.Set("display_name", nil) @@ -270,19 +240,11 @@ func (db *DB) UpdateUser( } } if links != nil { - if len(*links) == 0 { - builder = builder.Set("links", nil) - } else { - builder = builder.Set("links", *links) - } + builder = builder.Set("links", *links) } if avatarURLs != nil { - if len(avatarURLs) == 0 { - builder = builder.Set("avatar_urls", nil) - } else { - builder = builder.Set("avatar_urls", avatarURLs) - } + builder = builder.Set("avatar_urls", avatarURLs) } sql, args, err := builder.ToSql() @@ -290,19 +252,14 @@ func (db *DB) UpdateUser( return u, errors.Wrap(err, "building sql") } - _, err = tx.Exec(ctx, sql, args...) + err = pgxscan.Get(ctx, tx, &u, sql, args...) if err != nil { return u, errors.Wrap(err, "executing sql") } - - u, err = db.getUser(ctx, tx, id) - if err != nil { - return u, errors.Wrap(err, "getting updated user") - } return u, nil } -func (db *DB) DeleteUser(ctx context.Context, q querier, id xid.ID, selfDelete bool, reason string) error { +func (db *DB) DeleteUser(ctx context.Context, tx pgx.Tx, id xid.ID, selfDelete bool, reason string) error { builder := sq.Update("users").Set("deleted_at", time.Now().UTC()).Set("self_delete", selfDelete).Where("id = ?", id) if !selfDelete { builder = builder.Set("delete_reason", reason) @@ -312,7 +269,7 @@ func (db *DB) DeleteUser(ctx context.Context, q querier, id xid.ID, selfDelete b return errors.Wrap(err, "building sql") } - _, err = q.Exec(ctx, sql, args...) + _, err = tx.Exec(ctx, sql, args...) if err != nil { return errors.Wrap(err, "executing query") } diff --git a/go.mod b/go.mod index 7092a88..4f665b1 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/gobwas/glob v0.2.3 github.com/golang-jwt/jwt/v4 v4.4.1 github.com/jackc/pgconn v1.12.0 - github.com/jackc/pgtype v1.11.0 github.com/jackc/pgx/v4 v4.16.0 github.com/joho/godotenv v1.4.0 github.com/mediocregopher/radix/v4 v4.1.0 @@ -36,6 +35,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.0 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.11.0 // indirect github.com/jackc/puddle v1.2.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.9 // indirect diff --git a/scripts/migrate/006_jsonb_arrays.sql b/scripts/migrate/006_jsonb_arrays.sql new file mode 100644 index 0000000..c9ad3d1 --- /dev/null +++ b/scripts/migrate/006_jsonb_arrays.sql @@ -0,0 +1,24 @@ +-- +migrate Up + +-- 2023-03-11: Change composite type arrays to use jsonb columns +-- Composite types aren't actually supported by pgx and this allows us to drop pggen as a dev dependency. + +-- Delete old columns +alter table users drop column names; +alter table users drop column pronouns; + +alter table members drop column names; +alter table members drop column pronouns; + +alter table user_fields drop column entries; +alter table member_fields drop column entries; + +-- Create new columns +alter table users add column names jsonb not null default '[]'; +alter table users add column pronouns jsonb not null default '[]'; + +alter table members add column names jsonb not null default '[]'; +alter table members add column pronouns jsonb not null default '[]'; + +alter table user_fields add column entries jsonb not null default '[]'; +alter table member_fields add column entries jsonb not null default '[]';